From 37c677b121ffc4367c9d8fe22cef79ec3e8e30b0 Mon Sep 17 00:00:00 2001 From: king Date: Mon, 31 Aug 2026 10:51:07 +0800 Subject: [PATCH 1/2] fix(collateralize,issuance): return repaid collateral to borrow record 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. --- chain33.fork.toml | 2 + .../collateralize/executor/collateralizedb.go | 10 +- .../collateralize/executor/vuln_fix_test.go | 298 ++++++++++++++++++ .../dapp/collateralize/types/collateralize.go | 1 + plugin/dapp/collateralize/types/types.go | 1 + plugin/dapp/issuance/executor/issuancedb.go | 11 +- .../dapp/issuance/executor/vuln_fix_test.go | 296 +++++++++++++++++ plugin/dapp/issuance/types/issuance.go | 1 + plugin/dapp/issuance/types/types.go | 1 + 9 files changed, 617 insertions(+), 4 deletions(-) create mode 100644 plugin/dapp/collateralize/executor/vuln_fix_test.go create mode 100644 plugin/dapp/issuance/executor/vuln_fix_test.go diff --git a/chain33.fork.toml b/chain33.fork.toml index e26dcf12aa..4cdc8fafcd 100644 --- a/chain33.fork.toml +++ b/chain33.fork.toml @@ -143,11 +143,13 @@ ForkBadRepeatSecret=2715575 Enable=0 ForkIssuanceTableUpdate=0 ForkIssuancePrecision=0 +ForkIssuanceRepayOwner=0 [fork.sub.collateralize] Enable=0 ForkCollateralizeTableUpdate=0 ForkCollateralizePrecision=0 +ForkCollateralizeRepayOwner=0 [fork.sub.jsvm] Enable=0 diff --git a/plugin/dapp/collateralize/executor/collateralizedb.go b/plugin/dapp/collateralize/executor/collateralizedb.go index 2ad8d6cf48..1f8545d1c1 100644 --- a/plugin/dapp/collateralize/executor/collateralizedb.go +++ b/plugin/dapp/collateralize/executor/collateralizedb.go @@ -739,9 +739,15 @@ func (action *Action) CollateralizeRepay(repay *pty.CollateralizeRepay) (*types. kv = append(kv, receipt.KV...) // 抵押物归还 - receipt, err = action.coinsAccount.ExecTransferFrozen(coll.CreateAddr, action.fromaddr, action.execaddr, borrowRecord.CollateralValue) + // ForkCollateralizeRepayOwner 分叉后抵押物退还给借款记录所有者, + // 避免第三方代还时窃取借款人抵押物(分叉前保持旧行为,退还调用者) + toAddr := action.fromaddr + if cfg.IsDappFork(action.Collateralize.GetHeight(), pty.CollateralizeX, pty.ForkCollateralizeRepayOwner) { + toAddr = borrowRecord.AccountAddr + } + receipt, err = action.coinsAccount.ExecTransferFrozen(coll.CreateAddr, toAddr, action.execaddr, borrowRecord.CollateralValue) if err != nil { - clog.Error("CollateralizeRepay.ExecTransferFrozen", "addr", coll.CreateAddr, "execaddr", action.execaddr, "amount", borrowRecord.CollateralValue) + clog.Error("CollateralizeRepay.ExecTransferFrozen", "addr", coll.CreateAddr, "toAddr", toAddr, "execaddr", action.execaddr, "amount", borrowRecord.CollateralValue) return nil, err } logs = append(logs, receipt.Logs...) diff --git a/plugin/dapp/collateralize/executor/vuln_fix_test.go b/plugin/dapp/collateralize/executor/vuln_fix_test.go new file mode 100644 index 0000000000..c888e1973e --- /dev/null +++ b/plugin/dapp/collateralize/executor/vuln_fix_test.go @@ -0,0 +1,298 @@ +package executor + +// Regression tests for the CollateralizeRepay collateral-theft vulnerability. +// Before ForkCollateralizeRepayOwner, a third party repaying someone's borrow +// record received the borrower's collateral. After the fork the collateral is +// returned to the borrow record owner (record.AccountAddr) instead of the caller. + +import ( + "sync" + "testing" + "time" + + "github.com/33cn/chain33/account" + apimock "github.com/33cn/chain33/client/mocks" + "github.com/33cn/chain33/common" + dbm "github.com/33cn/chain33/common/db" + "github.com/33cn/chain33/system/dapp" + "github.com/33cn/chain33/types" + "github.com/33cn/chain33/util" + pkt "github.com/33cn/plugin/plugin/dapp/collateralize/types" + tokenE "github.com/33cn/plugin/plugin/dapp/token/executor" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +// repayOwnerInitOnce guards driver registration: initEnv in collateralize_test.go +// may already have registered the driver, and registering twice panics. +var repayOwnerInitOnce sync.Once + +// newRepayOwnerEnv builds a test env with ForkCollateralizeRepayOwner registered +// at the given height, so both pre-fork and post-fork behavior can be tested. +func newRepayOwnerEnv(repayForkHeight int64) *execEnv { + repayOwnerInitOnce.Do(func() { + // ignore duplicated driver registration when run together with other tests + defer func() { recover() }() + cfg0 := types.NewChain33Config(types.GetDefaultCfgstring()) + cfg0.SetTitleOnlyForTest("chain33") + cfg0.RegisterDappFork(pkt.CollateralizeX, pkt.ForkCollateralizeTableUpdate, 0) + cfg0.RegisterDappFork(pkt.CollateralizeX, pkt.ForkCollateralizeRepayOwner, 0) + Init(pkt.CollateralizeX, cfg0, nil) + }) + + cfg := types.NewChain33Config(types.GetDefaultCfgstring()) + cfg.SetTitleOnlyForTest("chain33") + cfg.RegisterDappFork(pkt.CollateralizeX, pkt.ForkCollateralizeTableUpdate, 0) + cfg.RegisterDappFork(pkt.CollateralizeX, pkt.ForkCollateralizeRepayOwner, repayForkHeight) + _, ldb, kvdb := util.CreateTestDB() + + accountA := types.Account{Balance: total, Frozen: 0, Addr: string(Nodes[0])} + accountAToken := types.Account{Balance: totalToken, Frozen: 0, Addr: string(Nodes[0])} + accountB := types.Account{Balance: total, Frozen: 0, Addr: string(Nodes[1])} + accountBToken := types.Account{Balance: types.DefaultCoinPrecision / 10, Frozen: 0, Addr: string(Nodes[1])} + accountC := types.Account{Balance: total, Frozen: 0, Addr: string(Nodes[2])} + + api := new(apimock.QueueProtocolAPI) + api.On("GetConfig", mock.Anything).Return(cfg, nil) + + execAddr := dapp.ExecAddress(pkt.CollateralizeX) + stateDB, _ := dbm.NewGoMemDB("1", "2", 100) + + accA := account.NewCoinsAccount(cfg) + accA.SetDB(stateDB) + accA.SaveExecAccount(execAddr, &accountA) + manageKeySet("issuance-manage", accountA.Addr, stateDB) + addrKeySet(accountA.Addr, stateDB) + tokenAccA, _ := account.NewAccountDB(cfg, tokenE.GetName(), pkt.CCNYTokenName, stateDB) + tokenAccA.SaveExecAccount(execAddr, &accountAToken) + + accB := account.NewCoinsAccount(cfg) + accB.SetDB(stateDB) + accB.SaveExecAccount(execAddr, &accountB) + manageKeySet("issuance-price-feed", accountB.Addr, stateDB) + tokenAccB, _ := account.NewAccountDB(cfg, tokenE.GetName(), pkt.CCNYTokenName, stateDB) + tokenAccB.SaveExecAccount(execAddr, &accountBToken) + + accC := account.NewCoinsAccount(cfg) + accC.SetDB(stateDB) + accC.SaveExecAccount(execAddr, &accountC) + manageKeySet("issuance-guarantor", accountC.Addr, stateDB) + + return &execEnv{ + blockTime: time.Now().Unix(), + blockHeight: cfg.GetDappFork(pkt.CollateralizeX, "Enable"), + difficulty: 1539918074, + kvdb: kvdb, + api: api, + db: stateDB, + execAddr: execAddr, + cfg: cfg, + ldb: ldb, + } +} + +func repayOwnerNewExec(env *execEnv) *Collateralize { + exec := newCollateralize().(*Collateralize) + exec.SetAPI(env.api) + exec.SetStateDB(env.db) + exec.SetLocalDB(env.kvdb) + exec.SetEnv(env.blockHeight, env.blockTime, env.difficulty) + return exec +} + +func repayOwnerExecTx(t *testing.T, env *execEnv, exec *Collateralize, tx *types.Transaction, index int) (*types.Receipt, error) { + receipt, err := exec.Exec(tx, index) + if err != nil { + return receipt, err + } + for _, kv := range receipt.KV { + env.db.Set(kv.Key, kv.Value) + } + receiptData := &types.ReceiptData{Ty: receipt.Ty, Logs: receipt.Logs} + set, err := exec.ExecLocal(tx, receiptData, index) + assert.Nil(t, err) + util.SaveKVList(env.ldb, set.KV) + return receipt, nil +} + +func repayOwnerGiveToken(env *execEnv, addr string, amount int64) { + tokenAcc, _ := account.NewAccountDB(env.cfg, tokenE.GetName(), pkt.CCNYTokenName, env.db) + tokenAcc.SaveExecAccount(env.execAddr, &types.Account{Balance: amount, Addr: addr}) +} + +func repayOwnerTokenBalance(env *execEnv, addr string) int64 { + tokenAcc, _ := account.NewAccountDB(env.cfg, tokenE.GetName(), pkt.CCNYTokenName, env.db) + return tokenAcc.LoadExecAccount(addr, env.execAddr).GetBalance() +} + +func repayOwnerCoinBalance(env *execEnv, addr string) int64 { + acc := account.NewCoinsAccount(env.cfg) + acc.SetDB(env.db) + return acc.LoadExecAccount(addr, env.execAddr).GetBalance() +} + +// repayOwnerPreparePool: manage(A) + create(A, 1000 CCNY) + feed(B, price=1) +func repayOwnerPreparePool(t *testing.T, env *execEnv, exec *Collateralize) string { + p := &pkt.CollateralizeManageTx{} + p.Period = 3600 * 24 * 365 + p.LiquidationRatio = 0.25 + p.DebtCeiling = 100 + p.StabilityFeeRatio = 0.0001 + p.TotalBalance = 10000 + tx, err := pkt.CreateRawCollateralizeManageTx(env.cfg, p) + assert.Nil(t, err) + tx.Execer = []byte(pkt.CollateralizeX) + tx, err = signTx(tx, PrivKeyA) + assert.Nil(t, err) + _, err = repayOwnerExecTx(t, env, exec, tx, 1) + assert.Nil(t, err) + + p1 := &pkt.CollateralizeCreateTx{TotalBalance: 1000} + tx, err = pkt.CreateRawCollateralizeCreateTx(env.cfg, p1) + assert.Nil(t, err) + tx.Execer = []byte(pkt.CollateralizeX) + tx, err = signTx(tx, PrivKeyA) + assert.Nil(t, err) + _, err = repayOwnerExecTx(t, env, exec, tx, 1) + assert.Nil(t, err) + collID := common.ToHex(tx.Hash()) + + p2 := &pkt.CollateralizeFeedTx{} + p2.Price = append(p2.Price, 1) + p2.Volume = append(p2.Volume, 100) + tx, err = pkt.CreateRawCollateralizeFeedTx(env.cfg, p2) + assert.Nil(t, err) + tx.Execer = []byte(pkt.CollateralizeX) + tx, err = signTx(tx, PrivKeyB) + assert.Nil(t, err) + exec.SetEnv(env.blockHeight+1, env.blockTime+1, env.difficulty) + _, err = repayOwnerExecTx(t, env, exec, tx, 1) + assert.Nil(t, err) + return collID +} + +// repayOwnerBorrow: borrower B borrows value CCNY +func repayOwnerBorrow(t *testing.T, env *execEnv, exec *Collateralize, collID string, value float64) string { + p := &pkt.CollateralizeBorrowTx{CollateralizeID: collID, Value: value} + tx, err := pkt.CreateRawCollateralizeBorrowTx(env.cfg, p) + assert.Nil(t, err) + tx.Execer = []byte(pkt.CollateralizeX) + tx, err = signTx(tx, PrivKeyB) + assert.Nil(t, err) + exec.SetEnv(env.blockHeight+1, env.blockTime+1, env.difficulty) + _, err = repayOwnerExecTx(t, env, exec, tx, 1) + assert.Nil(t, err) + return common.ToHex(tx.Hash()) +} + +// After ForkCollateralizeRepayOwner: a third party may still repay on behalf of +// the borrower, but the collateral must go back to the borrow record owner. +func TestRepayOwnerForkRepayByNonBorrower(t *testing.T) { + env := newRepayOwnerEnv(0) + exec := repayOwnerNewExec(env) + + // repayer C funds the repayment, C is unrelated to the borrow record + repayFund := 1000 * types.DefaultCoinPrecision + repayOwnerGiveToken(env, string(Nodes[2]), repayFund) + + collID := repayOwnerPreparePool(t, env, exec) + // borrower B borrows 100 CCNY with 400 BTY collateral (price=1, ratio=0.25) + borrowID := repayOwnerBorrow(t, env, exec, collID, 100) + + record, err := queryCollateralizeRecordByID(env.db, collID, borrowID) + assert.Nil(t, err) + assert.Equal(t, string(Nodes[1]), record.AccountAddr) + + coinBBefore := repayOwnerCoinBalance(env, string(Nodes[1])) + coinCBefore := repayOwnerCoinBalance(env, string(Nodes[2])) + tokenCBefore := repayOwnerTokenBalance(env, string(Nodes[2])) + + // C repays B's borrow record + p := &pkt.CollateralizeRepayTx{CollateralizeID: collID, RecordID: borrowID} + tx, err := pkt.CreateRawCollateralizeRepayTx(env.cfg, p) + assert.Nil(t, err) + tx.Execer = []byte(pkt.CollateralizeX) + tx, err = signTx(tx, PrivKeyC) + assert.Nil(t, err) + exec.SetEnv(env.blockHeight+2, env.blockTime+2, env.difficulty) + _, err = repayOwnerExecTx(t, env, exec, tx, 1) + assert.Nil(t, err) + + coinBAfter := repayOwnerCoinBalance(env, string(Nodes[1])) + coinCAfter := repayOwnerCoinBalance(env, string(Nodes[2])) + tokenCAfter := repayOwnerTokenBalance(env, string(Nodes[2])) + + // collateral returns to the borrower B, not to the repayer C + assert.Equal(t, record.CollateralValue, coinBAfter-coinBBefore, "collateral must return to the borrower") + assert.Equal(t, coinCBefore, coinCAfter, "repayer must not receive the collateral") + // the repayer only loses the repayment funds (debt + stability fee) + tokenCost := tokenCBefore - tokenCAfter + assert.True(t, tokenCost >= record.DebtValue, "repayer cost must cover the debt") + assert.True(t, tokenCost < record.CollateralValue, "repayer must not profit from the collateral") +} + +// After ForkCollateralizeRepayOwner: the borrower repaying his own record keeps +// working as before and gets the collateral back. +func TestRepayOwnerForkRepayByBorrower(t *testing.T) { + env := newRepayOwnerEnv(0) + exec := repayOwnerNewExec(env) + + collID := repayOwnerPreparePool(t, env, exec) + borrowID := repayOwnerBorrow(t, env, exec, collID, 100) + + record, err := queryCollateralizeRecordByID(env.db, collID, borrowID) + assert.Nil(t, err) + + coinBBefore := repayOwnerCoinBalance(env, string(Nodes[1])) + tokenBBefore := repayOwnerTokenBalance(env, string(Nodes[1])) + + p := &pkt.CollateralizeRepayTx{CollateralizeID: collID, RecordID: borrowID} + tx, err := pkt.CreateRawCollateralizeRepayTx(env.cfg, p) + assert.Nil(t, err) + tx.Execer = []byte(pkt.CollateralizeX) + tx, err = signTx(tx, PrivKeyB) + assert.Nil(t, err) + exec.SetEnv(env.blockHeight+2, env.blockTime+2, env.difficulty) + _, err = repayOwnerExecTx(t, env, exec, tx, 1) + assert.Nil(t, err) + + assert.Equal(t, record.CollateralValue, repayOwnerCoinBalance(env, string(Nodes[1]))-coinBBefore, + "borrower must get the collateral back") + assert.True(t, tokenBBefore-repayOwnerTokenBalance(env, string(Nodes[1])) >= record.DebtValue, + "borrower cost must cover the debt") +} + +// Before ForkCollateralizeRepayOwner the old behavior is preserved: the repayer +// receives the collateral (documenting the vulnerable pre-fork semantics). +func TestRepayOwnerPreForkRepayByNonBorrower(t *testing.T) { + env := newRepayOwnerEnv(100000000) + exec := repayOwnerNewExec(env) + + repayFund := 1000 * types.DefaultCoinPrecision + repayOwnerGiveToken(env, string(Nodes[2]), repayFund) + + collID := repayOwnerPreparePool(t, env, exec) + borrowID := repayOwnerBorrow(t, env, exec, collID, 100) + + record, err := queryCollateralizeRecordByID(env.db, collID, borrowID) + assert.Nil(t, err) + + coinBBefore := repayOwnerCoinBalance(env, string(Nodes[1])) + coinCBefore := repayOwnerCoinBalance(env, string(Nodes[2])) + + p := &pkt.CollateralizeRepayTx{CollateralizeID: collID, RecordID: borrowID} + tx, err := pkt.CreateRawCollateralizeRepayTx(env.cfg, p) + assert.Nil(t, err) + tx.Execer = []byte(pkt.CollateralizeX) + tx, err = signTx(tx, PrivKeyC) + assert.Nil(t, err) + exec.SetEnv(env.blockHeight+2, env.blockTime+2, env.difficulty) + _, err = repayOwnerExecTx(t, env, exec, tx, 1) + assert.Nil(t, err) + + // pre-fork behavior unchanged: collateral goes to the caller (repayer C) + assert.Equal(t, record.CollateralValue, repayOwnerCoinBalance(env, string(Nodes[2]))-coinCBefore, + "pre-fork: collateral goes to the repayer") + assert.Equal(t, coinBBefore, repayOwnerCoinBalance(env, string(Nodes[1])), + "pre-fork: borrower gets nothing back") +} diff --git a/plugin/dapp/collateralize/types/collateralize.go b/plugin/dapp/collateralize/types/collateralize.go index 0813942ba7..08f3671150 100644 --- a/plugin/dapp/collateralize/types/collateralize.go +++ b/plugin/dapp/collateralize/types/collateralize.go @@ -31,6 +31,7 @@ func InitFork(cfg *types.Chain33Config) { cfg.RegisterDappFork(CollateralizeX, "Enable", 0) cfg.RegisterDappFork(CollateralizeX, ForkCollateralizeTableUpdate, 0) cfg.RegisterDappFork(CollateralizeX, ForkCollateralizePrecision, 0) + cfg.RegisterDappFork(CollateralizeX, ForkCollateralizeRepayOwner, 0) } //InitExecutor ... diff --git a/plugin/dapp/collateralize/types/types.go b/plugin/dapp/collateralize/types/types.go index c1c2c71608..eb2ba24489 100644 --- a/plugin/dapp/collateralize/types/types.go +++ b/plugin/dapp/collateralize/types/types.go @@ -57,4 +57,5 @@ const ( var ( ForkCollateralizeTableUpdate = "ForkCollateralizeTableUpdate" ForkCollateralizePrecision = "ForkCollateralizePrecision" + ForkCollateralizeRepayOwner = "ForkCollateralizeRepayOwner" ) diff --git a/plugin/dapp/issuance/executor/issuancedb.go b/plugin/dapp/issuance/executor/issuancedb.go index 22432f00ba..97b56fa2be 100644 --- a/plugin/dapp/issuance/executor/issuancedb.go +++ b/plugin/dapp/issuance/executor/issuancedb.go @@ -688,9 +688,16 @@ func (action *Action) IssuanceRepay(repay *pty.IssuanceRepay) (*types.Receipt, e kv = append(kv, receipt.KV...) // 抵押物归还 - receipt, err = action.coinsAccount.ExecTransferFrozen(issu.IssuerAddr, action.fromaddr, action.execaddr, debtRecord.CollateralValue) + // ForkIssuanceRepayOwner 分叉后抵押物退还给借款记录所有者, + // 避免第三方代还时窃取借款人抵押物(分叉前保持旧行为,退还调用者) + toAddr := action.fromaddr + cfg := action.Issuance.GetAPI().GetConfig() + if cfg.IsDappFork(action.Issuance.GetHeight(), pty.IssuanceX, pty.ForkIssuanceRepayOwner) { + toAddr = debtRecord.AccountAddr + } + receipt, err = action.coinsAccount.ExecTransferFrozen(issu.IssuerAddr, toAddr, action.execaddr, debtRecord.CollateralValue) if err != nil { - clog.Error("IssuanceRepay.ExecTransferFrozen", "addr", issu.IssuerAddr, "execaddr", action.execaddr, "amount", debtRecord.CollateralValue) + clog.Error("IssuanceRepay.ExecTransferFrozen", "addr", issu.IssuerAddr, "toAddr", toAddr, "execaddr", action.execaddr, "amount", debtRecord.CollateralValue) return nil, err } logs = append(logs, receipt.Logs...) diff --git a/plugin/dapp/issuance/executor/vuln_fix_test.go b/plugin/dapp/issuance/executor/vuln_fix_test.go new file mode 100644 index 0000000000..8fb5fe26f9 --- /dev/null +++ b/plugin/dapp/issuance/executor/vuln_fix_test.go @@ -0,0 +1,296 @@ +package executor + +// Regression tests for the IssuanceRepay collateral-theft vulnerability. +// Before ForkIssuanceRepayOwner, a third party repaying someone's debt record +// received the debtor's collateral. After the fork the collateral is returned +// to the debt record owner (record.AccountAddr) instead of the caller. + +import ( + "sync" + "testing" + "time" + + "github.com/33cn/chain33/account" + apimock "github.com/33cn/chain33/client/mocks" + "github.com/33cn/chain33/common" + dbm "github.com/33cn/chain33/common/db" + "github.com/33cn/chain33/system/dapp" + "github.com/33cn/chain33/types" + "github.com/33cn/chain33/util" + pkt "github.com/33cn/plugin/plugin/dapp/issuance/types" + tokenE "github.com/33cn/plugin/plugin/dapp/token/executor" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +// repayOwnerIssInitOnce guards driver registration: initEnv in issuance_test.go +// may already have registered the driver, and registering twice panics. +var repayOwnerIssInitOnce sync.Once + +// newRepayOwnerIssEnv builds a test env with ForkIssuanceRepayOwner registered +// at the given height, so both pre-fork and post-fork behavior can be tested. +func newRepayOwnerIssEnv(repayForkHeight int64) *execEnv { + repayOwnerIssInitOnce.Do(func() { + // ignore duplicated driver registration when run together with other tests + defer func() { recover() }() + cfg0 := types.NewChain33Config(types.GetDefaultCfgstring()) + cfg0.SetTitleOnlyForTest("chain33") + cfg0.RegisterDappFork(pkt.IssuanceX, pkt.ForkIssuanceTableUpdate, 0) + cfg0.RegisterDappFork(pkt.IssuanceX, pkt.ForkIssuanceRepayOwner, 0) + Init(pkt.IssuanceX, cfg0, nil) + }) + + cfg := types.NewChain33Config(types.GetDefaultCfgstring()) + cfg.SetTitleOnlyForTest("chain33") + cfg.RegisterDappFork(pkt.IssuanceX, pkt.ForkIssuanceTableUpdate, 0) + cfg.RegisterDappFork(pkt.IssuanceX, pkt.ForkIssuanceRepayOwner, repayForkHeight) + _, ldb, kvdb := util.CreateTestDB() + + accountA := types.Account{Balance: total, Frozen: 0, Addr: string(Nodes[0])} + accountAToken := types.Account{Balance: totalToken, Frozen: 0, Addr: string(Nodes[0])} + accountB := types.Account{Balance: total, Frozen: 0, Addr: string(Nodes[1])} + accountC := types.Account{Balance: total, Frozen: 0, Addr: string(Nodes[2])} + + api := new(apimock.QueueProtocolAPI) + api.On("GetConfig", mock.Anything).Return(cfg, nil) + + execAddr := dapp.ExecAddress(pkt.IssuanceX) + stateDB, _ := dbm.NewGoMemDB("1", "2", 100) + + accA := account.NewCoinsAccount(cfg) + accA.SetDB(stateDB) + accA.SaveExecAccount(execAddr, &accountA) + manageKeySet("issuance-manage", accountA.Addr, stateDB) + manageKeySet("issuance-fund", accountA.Addr, stateDB) + tokenAccA, _ := account.NewAccountDB(cfg, tokenE.GetName(), pkt.CCNYTokenName, stateDB) + tokenAccA.SaveExecAccount(execAddr, &accountAToken) + + accB := account.NewCoinsAccount(cfg) + accB.SetDB(stateDB) + accB.SaveExecAccount(execAddr, &accountB) + manageKeySet("issuance-price-feed", accountB.Addr, stateDB) + + accC := account.NewCoinsAccount(cfg) + accC.SetDB(stateDB) + accC.SaveExecAccount(execAddr, &accountC) + manageKeySet("issuance-guarantor", accountC.Addr, stateDB) + + return &execEnv{ + blockTime: time.Now().Unix(), + blockHeight: cfg.GetDappFork(pkt.IssuanceX, "Enable"), + difficulty: 1539918074, + kvdb: kvdb, + api: api, + db: stateDB, + execAddr: execAddr, + cfg: cfg, + ldb: ldb, + } +} + +func repayOwnerIssNewExec(env *execEnv) *Issuance { + exec := newIssuance().(*Issuance) + exec.SetAPI(env.api) + exec.SetStateDB(env.db) + exec.SetLocalDB(env.kvdb) + exec.SetEnv(env.blockHeight, env.blockTime, env.difficulty) + return exec +} + +func repayOwnerIssExecTx(t *testing.T, env *execEnv, exec *Issuance, tx *types.Transaction, index int) (*types.Receipt, error) { + receipt, err := exec.Exec(tx, index) + if err != nil { + return receipt, err + } + for _, kv := range receipt.KV { + env.db.Set(kv.Key, kv.Value) + } + receiptData := &types.ReceiptData{Ty: receipt.Ty, Logs: receipt.Logs} + set, err := exec.ExecLocal(tx, receiptData, index) + assert.Nil(t, err) + util.SaveKVList(env.ldb, set.KV) + return receipt, nil +} + +func repayOwnerIssGiveToken(env *execEnv, addr string, amount int64) { + tokenAcc, _ := account.NewAccountDB(env.cfg, tokenE.GetName(), pkt.CCNYTokenName, env.db) + tokenAcc.SaveExecAccount(env.execAddr, &types.Account{Balance: amount, Addr: addr}) +} + +func repayOwnerIssTokenBalance(env *execEnv, addr string) int64 { + tokenAcc, _ := account.NewAccountDB(env.cfg, tokenE.GetName(), pkt.CCNYTokenName, env.db) + return tokenAcc.LoadExecAccount(addr, env.execAddr).GetBalance() +} + +func repayOwnerIssCoinBalance(env *execEnv, addr string) int64 { + acc := account.NewCoinsAccount(env.cfg) + acc.SetDB(env.db) + return acc.LoadExecAccount(addr, env.execAddr).GetBalance() +} + +// repayOwnerIssPrepare: create(A, 1000 CCNY) + feed(B, price=1) + manage(A, super=B) +func repayOwnerIssPrepare(t *testing.T, env *execEnv, exec *Issuance) string { + p1 := &pkt.IssuanceCreateTx{ + TotalBalance: 1000, + DebtCeiling: 200, + LiquidationRatio: 0.25, + Period: 3600 * 24 * 365, + } + tx, err := pkt.CreateRawIssuanceCreateTx(env.cfg, p1) + assert.Nil(t, err) + tx.Execer = []byte(pkt.IssuanceX) + tx, err = signTx(tx, PrivKeyA) + assert.Nil(t, err) + _, err = repayOwnerIssExecTx(t, env, exec, tx, 1) + assert.Nil(t, err) + issuanceID := common.ToHex(tx.Hash()) + + p2 := &pkt.IssuanceFeedTx{} + p2.Price = append(p2.Price, 1) + p2.Volume = append(p2.Volume, 100) + tx, err = pkt.CreateRawIssuanceFeedTx(env.cfg, p2) + assert.Nil(t, err) + tx.Execer = []byte(pkt.IssuanceX) + tx, err = signTx(tx, PrivKeyB) + assert.Nil(t, err) + exec.SetEnv(env.blockHeight+1, env.blockTime+1, env.difficulty) + _, err = repayOwnerIssExecTx(t, env, exec, tx, 1) + assert.Nil(t, err) + + p3 := &pkt.IssuanceManageTx{} + p3.Addr = append(p3.Addr, string(Nodes[1])) + tx, err = pkt.CreateRawIssuanceManageTx(env.cfg, p3) + assert.Nil(t, err) + tx.Execer = []byte(pkt.IssuanceX) + tx, err = signTx(tx, PrivKeyA) + assert.Nil(t, err) + exec.SetEnv(env.blockHeight+1, env.blockTime+1, env.difficulty) + _, err = repayOwnerIssExecTx(t, env, exec, tx, 1) + assert.Nil(t, err) + + return issuanceID +} + +// repayOwnerIssDebt: debtor B (super address) borrows value CCNY +func repayOwnerIssDebt(t *testing.T, env *execEnv, exec *Issuance, issuanceID string, value float64) string { + p := &pkt.IssuanceDebtTx{IssuanceID: issuanceID, Value: value} + tx, err := pkt.CreateRawIssuanceDebtTx(env.cfg, p) + assert.Nil(t, err) + tx.Execer = []byte(pkt.IssuanceX) + tx, err = signTx(tx, PrivKeyB) + assert.Nil(t, err) + exec.SetEnv(env.blockHeight+1, env.blockTime+1, env.difficulty) + _, err = repayOwnerIssExecTx(t, env, exec, tx, 1) + assert.Nil(t, err) + return common.ToHex(tx.Hash()) +} + +// After ForkIssuanceRepayOwner: a third party may still repay on behalf of the +// debtor, but the collateral must go back to the debt record owner. +func TestRepayOwnerForkRepayByNonDebtor(t *testing.T) { + env := newRepayOwnerIssEnv(0) + exec := repayOwnerIssNewExec(env) + + // repayer C funds the repayment, C is unrelated to the debt record + repayFund := 1000 * types.DefaultCoinPrecision + repayOwnerIssGiveToken(env, string(Nodes[2]), repayFund) + + issuanceID := repayOwnerIssPrepare(t, env, exec) + // debtor B borrows 100 CCNY with 400 BTY collateral (price=1, ratio=0.25) + debtID := repayOwnerIssDebt(t, env, exec, issuanceID, 100) + + record, err := queryIssuanceRecordByID(env.db, issuanceID, debtID) + assert.Nil(t, err) + assert.Equal(t, string(Nodes[1]), record.AccountAddr) + + coinBBefore := repayOwnerIssCoinBalance(env, string(Nodes[1])) + coinCBefore := repayOwnerIssCoinBalance(env, string(Nodes[2])) + tokenCBefore := repayOwnerIssTokenBalance(env, string(Nodes[2])) + + // C repays B's debt record + p := &pkt.IssuanceRepayTx{IssuanceID: issuanceID, DebtID: debtID} + tx, err := pkt.CreateRawIssuanceRepayTx(env.cfg, p) + assert.Nil(t, err) + tx.Execer = []byte(pkt.IssuanceX) + tx, err = signTx(tx, PrivKeyC) + assert.Nil(t, err) + exec.SetEnv(env.blockHeight+2, env.blockTime+2, env.difficulty) + _, err = repayOwnerIssExecTx(t, env, exec, tx, 1) + assert.Nil(t, err) + + coinBAfter := repayOwnerIssCoinBalance(env, string(Nodes[1])) + coinCAfter := repayOwnerIssCoinBalance(env, string(Nodes[2])) + tokenCAfter := repayOwnerIssTokenBalance(env, string(Nodes[2])) + + // collateral returns to the debtor B, not to the repayer C + assert.Equal(t, record.CollateralValue, coinBAfter-coinBBefore, "collateral must return to the debtor") + assert.Equal(t, coinCBefore, coinCAfter, "repayer must not receive the collateral") + // the repayer only loses the repayment funds (the exact debt value) + assert.Equal(t, record.DebtValue, tokenCBefore-tokenCAfter, "repayer cost must equal the debt value") +} + +// After ForkIssuanceRepayOwner: the debtor repaying his own record keeps working +// as before and gets the collateral back. +func TestRepayOwnerForkRepayByDebtor(t *testing.T) { + env := newRepayOwnerIssEnv(0) + exec := repayOwnerIssNewExec(env) + + issuanceID := repayOwnerIssPrepare(t, env, exec) + debtID := repayOwnerIssDebt(t, env, exec, issuanceID, 100) + + record, err := queryIssuanceRecordByID(env.db, issuanceID, debtID) + assert.Nil(t, err) + + coinBBefore := repayOwnerIssCoinBalance(env, string(Nodes[1])) + tokenBBefore := repayOwnerIssTokenBalance(env, string(Nodes[1])) + + p := &pkt.IssuanceRepayTx{IssuanceID: issuanceID, DebtID: debtID} + tx, err := pkt.CreateRawIssuanceRepayTx(env.cfg, p) + assert.Nil(t, err) + tx.Execer = []byte(pkt.IssuanceX) + tx, err = signTx(tx, PrivKeyB) + assert.Nil(t, err) + exec.SetEnv(env.blockHeight+2, env.blockTime+2, env.difficulty) + _, err = repayOwnerIssExecTx(t, env, exec, tx, 1) + assert.Nil(t, err) + + assert.Equal(t, record.CollateralValue, repayOwnerIssCoinBalance(env, string(Nodes[1]))-coinBBefore, + "debtor must get the collateral back") + assert.Equal(t, record.DebtValue, tokenBBefore-repayOwnerIssTokenBalance(env, string(Nodes[1])), + "debtor cost must equal the debt value") +} + +// Before ForkIssuanceRepayOwner the old behavior is preserved: the repayer +// receives the collateral (documenting the vulnerable pre-fork semantics). +func TestRepayOwnerPreForkRepayByNonDebtor(t *testing.T) { + env := newRepayOwnerIssEnv(100000000) + exec := repayOwnerIssNewExec(env) + + repayFund := 1000 * types.DefaultCoinPrecision + repayOwnerIssGiveToken(env, string(Nodes[2]), repayFund) + + issuanceID := repayOwnerIssPrepare(t, env, exec) + debtID := repayOwnerIssDebt(t, env, exec, issuanceID, 100) + + record, err := queryIssuanceRecordByID(env.db, issuanceID, debtID) + assert.Nil(t, err) + + coinBBefore := repayOwnerIssCoinBalance(env, string(Nodes[1])) + coinCBefore := repayOwnerIssCoinBalance(env, string(Nodes[2])) + + p := &pkt.IssuanceRepayTx{IssuanceID: issuanceID, DebtID: debtID} + tx, err := pkt.CreateRawIssuanceRepayTx(env.cfg, p) + assert.Nil(t, err) + tx.Execer = []byte(pkt.IssuanceX) + tx, err = signTx(tx, PrivKeyC) + assert.Nil(t, err) + exec.SetEnv(env.blockHeight+2, env.blockTime+2, env.difficulty) + _, err = repayOwnerIssExecTx(t, env, exec, tx, 1) + assert.Nil(t, err) + + // pre-fork behavior unchanged: collateral goes to the caller (repayer C) + assert.Equal(t, record.CollateralValue, repayOwnerIssCoinBalance(env, string(Nodes[2]))-coinCBefore, + "pre-fork: collateral goes to the repayer") + assert.Equal(t, coinBBefore, repayOwnerIssCoinBalance(env, string(Nodes[1])), + "pre-fork: debtor gets nothing back") +} diff --git a/plugin/dapp/issuance/types/issuance.go b/plugin/dapp/issuance/types/issuance.go index 213f95b1da..130273fca0 100644 --- a/plugin/dapp/issuance/types/issuance.go +++ b/plugin/dapp/issuance/types/issuance.go @@ -31,6 +31,7 @@ func InitFork(cfg *types.Chain33Config) { cfg.RegisterDappFork(IssuanceX, "Enable", 0) cfg.RegisterDappFork(IssuanceX, ForkIssuanceTableUpdate, 0) cfg.RegisterDappFork(IssuanceX, ForkIssuancePrecision, 0) + cfg.RegisterDappFork(IssuanceX, ForkIssuanceRepayOwner, 0) } //InitExecutor ... diff --git a/plugin/dapp/issuance/types/types.go b/plugin/dapp/issuance/types/types.go index 553473fc2a..018b994b25 100644 --- a/plugin/dapp/issuance/types/types.go +++ b/plugin/dapp/issuance/types/types.go @@ -56,4 +56,5 @@ const ( var ( ForkIssuanceTableUpdate = "ForkIssuanceTableUpdate" ForkIssuancePrecision = "ForkIssuancePrecision" + ForkIssuanceRepayOwner = "ForkIssuanceRepayOwner" ) From ed54c9dd6460fcdfff92aa9d80f5563972f9309c Mon Sep 17 00:00:00 2001 From: king Date: Mon, 31 Aug 2026 13:37:48 +0800 Subject: [PATCH 2/2] ci: add ForkCollateralizeRepayOwner/ForkIssuanceRepayOwner to para and 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. --- chain33.para.toml | 2 ++ plugin/dapp/evm/cmd/ci2/chain33.proxyminer.toml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/chain33.para.toml b/chain33.para.toml index e877dbd938..4b01d0c905 100644 --- a/chain33.para.toml +++ b/chain33.para.toml @@ -498,11 +498,13 @@ Enable=0 Enable=0 ForkIssuanceTableUpdate=0 ForkIssuancePrecision=0 +ForkIssuanceRepayOwner=0 [fork.sub.collateralize] Enable=0 ForkCollateralizeTableUpdate=0 ForkCollateralizePrecision=0 +ForkCollateralizeRepayOwner=0 #对已有的平行链如果不是从0开始同步数据,需要设置这个kvmvccmavl的对应平行链高度的fork,如果从0开始同步,statehash会跟以前mavl的不同 [fork.sub.store-kvmvccmavl] diff --git a/plugin/dapp/evm/cmd/ci2/chain33.proxyminer.toml b/plugin/dapp/evm/cmd/ci2/chain33.proxyminer.toml index 7588e667dd..483d974284 100644 --- a/plugin/dapp/evm/cmd/ci2/chain33.proxyminer.toml +++ b/plugin/dapp/evm/cmd/ci2/chain33.proxyminer.toml @@ -640,11 +640,13 @@ ForkBadRepeatSecret=2715575 Enable=0 ForkIssuanceTableUpdate=0 ForkIssuancePrecision=0 +ForkIssuanceRepayOwner=0 [fork.sub.collateralize] Enable=0 ForkCollateralizeTableUpdate=0 ForkCollateralizePrecision=0 +ForkCollateralizeRepayOwner=0 [fork.sub.jsvm] Enable=0