Skip to content

fix(collateralize,issuance): return repaid collateral to borrow record owner - #1308

Open
33cn wants to merge 2 commits into
masterfrom
fix-repay-collateral-owner
Open

fix(collateralize,issuance): return repaid collateral to borrow record owner#1308
33cn wants to merge 2 commits into
masterfrom
fix-repay-collateral-owner

Conversation

@33cn

@33cn 33cn commented Aug 31, 2026

Copy link
Copy Markdown
Owner

Vulnerability

Third-party repayment steals the borrower's over-collateralized BTY.

  • plugin/dapp/collateralize/executor/collateralizedb.go:663-769 (CollateralizeRepay): at line 742 the collateral is returned via ExecTransferFrozen(coll.CreateAddr, action.fromaddr, ...) — to the transaction sender, not to borrowRecord.AccountAddr. There is no borrower identity check anywhere in the function.
  • plugin/dapp/issuance/executor/issuancedb.go:630-719 (IssuanceRepay): same flaw at line 691 — collateral goes to action.fromaddr instead of debtRecord.AccountAddr.

Root cause

Any account can repay an arbitrary borrow/debt record (this itself is a legitimate repay-on-behalf feature), but the collateral refund target was hardcoded to the caller. An attacker therefore pays only the debt principal (plus the small stability fee in collateralize) and receives the borrower's full collateral — about 4x the repayment cost at a 0.25 liquidation ratio.

Fix

Change the collateral refund target, not the repayment permission:

  • After the fork, the collateral is returned to the borrow/debt record owner (record.AccountAddr); the caller only loses the repayment funds.
  • Third-party repayment remains allowed. Neighboring functions show the design is not borrower-exclusive: CollateralizeAppend lets any address add collateral to someone else's record, and neither repay path ever checked fromaddr == record.AccountAddr. So repay-on-behalf is kept as a feature and only the theft vector (wrong refund target) is closed.

Fork gating

New dapp forks registered at height 0, following the ForkEVMFixOverflow pattern:

  • ForkCollateralizeRepayOwner in plugin/dapp/collateralize/types (RegisterDappFork in InitFork, gated via cfg.IsDappFork in CollateralizeRepay)
  • ForkIssuanceRepayOwner in plugin/dapp/issuance/types (same pattern in IssuanceRepay)

Both are also added to chain33.fork.toml alongside the existing sibling forks. Before the fork the old behavior (refund to caller) is preserved so running chains keep consensus.

Tests

New regression tests (no changes to existing tests):

  • plugin/dapp/collateralize/executor/vuln_fix_test.go
    • TestRepayOwnerForkRepayByNonBorrower: post-fork, a third-party repayer's collateral stays untouched; collateral goes back to the borrower's exec account; repayer only loses debt+fee, which is less than the collateral value.
    • TestRepayOwnerForkRepayByBorrower: borrower self-repay still works and returns the collateral.
    • TestRepayOwnerPreForkRepayByNonBorrower: pre-fork behavior unchanged (collateral to caller).
  • plugin/dapp/issuance/executor/vuln_fix_test.go: same three scenarios (TestRepayOwnerForkRepayByNonDebtor, TestRepayOwnerForkRepayByDebtor, TestRepayOwnerPreForkRepayByNonDebtor).

go test -ldflags=-checklinkname=0 ./plugin/dapp/collateralize/... ./plugin/dapp/issuance/... passes, including all pre-existing tests.

king added 2 commits August 31, 2026 10:51
…d owner

CollateralizeRepay and IssuanceRepay transferred the collateral back to
the transaction sender (action.fromaddr) instead of the borrow/debt
record owner. Any third party could repay someone else's debt at the
cost of principal plus stability fee and steal the borrower's
over-collateralized BTY.

Gate the fix behind the new dapp forks ForkCollateralizeRepayOwner and
ForkIssuanceRepayOwner: after the fork the collateral is returned to
record.AccountAddr; before the fork the old behavior is preserved to
keep consensus of running chains. Third-party repayment itself remains
allowed (repay-on-behalf is a legitimate feature), only the collateral
refund target is fixed.
…d proxyminer configs

chain33 validates that every registered dapp fork has a config entry on
non-local titles; missing entries crash the node at startup, which broke
ci_cross2eth/ci_parachain_rollup/ci_paracross/ci_rgbx.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant