From fd1c7d1af9f512e85b60b10a9e15cbd6ed2f4eac Mon Sep 17 00:00:00 2001 From: king Date: Mon, 31 Aug 2026 11:41:04 +0800 Subject: [PATCH 1/4] fix(token): check global symbol uniqueness in finishCreate to prevent duplicate mint finishCreate only looked up the per-owner (symbol, owner) record, so the same symbol precreated by N different owners could be finished N times and each finish triggered GenesisInit, over-issuing N*Total of the same token. Add a global symbol uniqueness check in finishCreate (the mavl-token- record already written by the first finish), gated by the new dapp fork ForkTokenFinishCheck (registered at height 0). Pre-fork behavior is unchanged. preCreate is intentionally left per-owner: the global key only exists after a finish (already guarded by checkTokenExist in preCreate), and multiple owners precreating the same symbol before any finish remains a valid first-finisher- wins flow. --- chain33.fork.toml | 2 + chain33.para.toml | 1 + .../dapp/evm/cmd/ci2/chain33.proxyminer.toml | 2 + .../ticket/executor/testdata/chain33.cfg.toml | 1 + .../token/executor/token_finishcheck_test.go | 175 ++++++++++++++++++ plugin/dapp/token/executor/tokendb.go | 7 + plugin/dapp/token/types/const.go | 2 + plugin/dapp/token/types/types.go | 1 + 8 files changed, 191 insertions(+) create mode 100644 plugin/dapp/token/executor/token_finishcheck_test.go diff --git a/chain33.fork.toml b/chain33.fork.toml index e26dcf12aa..237f66165a 100644 --- a/chain33.fork.toml +++ b/chain33.fork.toml @@ -216,6 +216,8 @@ ForkTokenSymbolWithNumber=1298600 ForkTokenCheck=1600000 # 增加Erc20合约对token 合约的支持 ForkTokenEvm=0 +# finishCreate时全局检查token symbol唯一性,防止同一symbol被不同owner重复finish超发 +ForkTokenFinishCheck=0 [fork.sub.trade] Enable=100899 diff --git a/chain33.para.toml b/chain33.para.toml index e877dbd938..12367ec6a5 100644 --- a/chain33.para.toml +++ b/chain33.para.toml @@ -366,6 +366,7 @@ ForkTokenPrice=0 ForkTokenSymbolWithNumber=0 ForkTokenCheck= 0 ForkTokenEvm=0 +ForkTokenFinishCheck=0 [fork.sub.trade] Enable=0 diff --git a/plugin/dapp/evm/cmd/ci2/chain33.proxyminer.toml b/plugin/dapp/evm/cmd/ci2/chain33.proxyminer.toml index 7588e667dd..c7e7172587 100644 --- a/plugin/dapp/evm/cmd/ci2/chain33.proxyminer.toml +++ b/plugin/dapp/evm/cmd/ci2/chain33.proxyminer.toml @@ -714,6 +714,8 @@ ForkTokenSymbolWithNumber=1298600 ForkTokenCheck=1600000 # 增加Erc20合约对token 合约的支持 ForkTokenEvm=0 +# finishCreate时全局检查token symbol唯一性,防止同一symbol被不同owner重复finish超发 +ForkTokenFinishCheck=0 [fork.sub.trade] Enable=100899 diff --git a/plugin/dapp/ticket/executor/testdata/chain33.cfg.toml b/plugin/dapp/ticket/executor/testdata/chain33.cfg.toml index 3ea1726f6d..2e7ba2d785 100644 --- a/plugin/dapp/ticket/executor/testdata/chain33.cfg.toml +++ b/plugin/dapp/ticket/executor/testdata/chain33.cfg.toml @@ -236,6 +236,7 @@ ForkBadTokenSymbol= 0 ForkTokenPrice=0 ForkTokenSymbolWithNumber=0 ForkTokenCheck= 0 +ForkTokenFinishCheck= 0 [fork.sub.manage] Enable=0 diff --git a/plugin/dapp/token/executor/token_finishcheck_test.go b/plugin/dapp/token/executor/token_finishcheck_test.go new file mode 100644 index 0000000000..d96b136ae7 --- /dev/null +++ b/plugin/dapp/token/executor/token_finishcheck_test.go @@ -0,0 +1,175 @@ +// Copyright Fuzamei Corp. 2018 All Rights Reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package executor + +import ( + "strings" + "sync/atomic" + "testing" + + "github.com/33cn/chain33/account" + apimock "github.com/33cn/chain33/client/mocks" + dbm "github.com/33cn/chain33/common/db" + "github.com/33cn/chain33/types" + "github.com/33cn/chain33/util" + pty "github.com/33cn/plugin/plugin/dapp/token/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +// regression tests for fork ForkTokenFinishCheck: +// finishCreate must check global token symbol uniqueness, otherwise the same +// symbol precreated by N different owners can be finished N times and each +// finish triggers GenesisInit, over-issuing N*Total of the same token. + +var dupFinishNonce int64 = 2000000 + +func dupFinishCfg(forkHeight int64) *types.Chain33Config { + cfg := types.NewChain33Config(strings.Replace(types.GetDefaultCfgstring(), "Title=\"local\"", "Title=\"chain33\"", 1)) + cfg.SetDappFork(pty.TokenX, pty.ForkTokenCheckX, 1600000) + cfg.SetDappFork(pty.TokenX, pty.ForkTokenFinishCheckX, forkHeight) + return cfg +} + +// newDupFinishExec builds a token executor with fresh state/local dbs at height 1600000. +// It does not call Init(), to avoid conflicting with the driver registration in token_test.go. +func newDupFinishExec(cfg *types.Chain33Config) (*token, dbm.KVDB, *dbm.GoMemDB) { + ety := types.LoadExecutorType(driverName) + ety.InitFuncList(types.ListMethod(&token{})) + + stateDB, _ := dbm.NewGoMemDB("dupfinish", "dupfinish", 100) + _, _, kvdb := util.CreateTestDB() + + // token-blacklist manage config, needed since ForkTokenBlackListX is active + item := &types.ConfigItem{ + Key: "mavl-manage-token-blacklist", + Value: &types.ConfigItem_Arr{Arr: &types.ArrayConfig{Value: []string{"bty"}}}, + } + stateDB.Set([]byte(item.Key), types.Encode(item)) + + // token-finisher manage config: Nodes[0] is a valid finisher + item2 := &types.ConfigItem{ + Key: "mavl-manage-token-finisher", + Value: &types.ConfigItem_Arr{Arr: &types.ArrayConfig{Value: []string{string(Nodes[0])}}}, + } + stateDB.Set([]byte(item2.Key), types.Encode(item2)) + + exec := newToken() + api := new(apimock.QueueProtocolAPI) + api.On("GetConfig", mock.Anything).Return(cfg, nil) + exec.SetAPI(api) + exec.SetStateDB(stateDB) + exec.SetLocalDB(kvdb) + exec.SetEnv(1600000, 1539918074, 1539918074) + return exec.(*token), kvdb, stateDB +} + +func dupFinishApplyReceipt(stateDB *dbm.GoMemDB, receipt *types.Receipt) { + for _, kv := range receipt.KV { + stateDB.Set(kv.Key, kv.Value) + } +} + +// dupFinishPreCreate executes one preCreate with price=0, owner and signer priv match. +func dupFinishPreCreate(t *testing.T, exec *token, stateDB *dbm.GoMemDB, + symbol, owner, priv string, total int64) error { + p := &pty.TokenPreCreate{ + Name: symbol, Symbol: symbol, Introduction: symbol, + Total: total, Price: 0, Owner: owner, + } + tx, err := types.CallCreateTransaction(pty.TokenX, "TokenPreCreate", p) + assert.Nil(t, err) + tx.Nonce = atomic.AddInt64(&dupFinishNonce, 1) + tx, err = signTx(tx, priv) + assert.Nil(t, err) + receipt, err := exec.Exec(tx, 1) + if err == nil { + dupFinishApplyReceipt(stateDB, receipt) + } + return err +} + +// dupFinishCreate finishes the token creation, signed by the finisher (Nodes[0], PrivKeyA). +func dupFinishCreate(t *testing.T, exec *token, stateDB *dbm.GoMemDB, symbol, owner string) error { + p := &pty.TokenFinishCreate{Symbol: symbol, Owner: owner} + tx, err := types.CallCreateTransaction(pty.TokenX, "TokenFinishCreate", p) + assert.Nil(t, err) + tx.Nonce = atomic.AddInt64(&dupFinishNonce, 1) + tx, err = signTx(tx, PrivKeyA) + assert.Nil(t, err) + receipt, err := exec.Exec(tx, 1) + if err == nil { + dupFinishApplyReceipt(stateDB, receipt) + } + return err +} + +// TestTokenDupFinishCreateRejected verifies with ForkTokenFinishCheck active: +// two owners precreate the same symbol, the first finish succeeds, the second +// finish is rejected and no over-issuance happens. The normal +// precreate/finish flow is not affected. +func TestTokenDupFinishCreateRejected(t *testing.T) { + cfg := dupFinishCfg(0) + exec, _, stateDB := newDupFinishExec(cfg) + symbol := "DUPCHECK" + total := int64(10000 * 1e8) + ownerA := string(Nodes[0]) + ownerB := string(Nodes[1]) + + // both owners can still precreate the same symbol (per-owner records) + err := dupFinishPreCreate(t, exec, stateDB, symbol, ownerA, PrivKeyA, total) + assert.Nil(t, err, "owner A precreate should succeed") + err = dupFinishPreCreate(t, exec, stateDB, symbol, ownerB, PrivKeyB, total) + assert.Nil(t, err, "owner B precreate same symbol should succeed") + + // first finish succeeds and issues total to owner A + err = dupFinishCreate(t, exec, stateDB, symbol, ownerA) + assert.Nil(t, err, "first finishCreate should succeed") + + // second finish of the same symbol by another owner must be rejected + err = dupFinishCreate(t, exec, stateDB, symbol, ownerB) + assert.Equal(t, pty.ErrTokenExist, err, "duplicate finishCreate must be rejected") + + // same-owner repeated finish is still rejected by the status check + err = dupFinishCreate(t, exec, stateDB, symbol, ownerA) + assert.Equal(t, pty.ErrTokenNotPrecreated, err, "repeated finish of created token rejected") + + // only owner A got the genesis amount, total issued equals declared Total + accDB, err := account.NewAccountDB(cfg, "token", symbol, stateDB) + assert.Nil(t, err) + assert.Equal(t, total, accDB.LoadAccount(ownerA).Balance) + assert.Equal(t, int64(0), accDB.LoadAccount(ownerB).Balance) + + tk, err := loadTokenDB(stateDB, symbol) + assert.Nil(t, err) + assert.Equal(t, total, tk.token.Total) +} + +// TestTokenDupFinishCreatePreFork verifies the behavior before the fork height +// stays unchanged: duplicate finishCreate of the same symbol still succeeds. +func TestTokenDupFinishCreatePreFork(t *testing.T) { + cfg := dupFinishCfg(2000000) // fork active above the test height 1600000 + exec, _, stateDB := newDupFinishExec(cfg) + symbol := "PREFORK" + total := int64(10000 * 1e8) + ownerA := string(Nodes[0]) + ownerB := string(Nodes[1]) + + err := dupFinishPreCreate(t, exec, stateDB, symbol, ownerA, PrivKeyA, total) + assert.Nil(t, err) + err = dupFinishPreCreate(t, exec, stateDB, symbol, ownerB, PrivKeyB, total) + assert.Nil(t, err) + + err = dupFinishCreate(t, exec, stateDB, symbol, ownerA) + assert.Nil(t, err) + // pre-fork behavior: no global symbol uniqueness check in finishCreate + err = dupFinishCreate(t, exec, stateDB, symbol, ownerB) + assert.Nil(t, err, "pre-fork duplicate finishCreate keeps the old behavior") + + accDB, err := account.NewAccountDB(cfg, "token", symbol, stateDB) + assert.Nil(t, err) + assert.Equal(t, total, accDB.LoadAccount(ownerA).Balance) + assert.Equal(t, total, accDB.LoadAccount(ownerB).Balance) +} diff --git a/plugin/dapp/token/executor/tokendb.go b/plugin/dapp/token/executor/tokendb.go index 586c77a570..7da41e04a2 100644 --- a/plugin/dapp/token/executor/tokendb.go +++ b/plugin/dapp/token/executor/tokendb.go @@ -251,6 +251,13 @@ func (action *tokenAction) finishCreate(tokenFinish *pty.TokenFinishCreate) (*ty return nil, pty.ErrTokenNotPrecreated } + // 全局检查token symbol唯一性,防止同一symbol被不同owner重复finish导致超发 + if cfg.IsDappFork(action.height, pty.TokenX, pty.ForkTokenFinishCheckX) { + if checkTokenExist(tokenFinish.GetSymbol(), action.db) { + return nil, pty.ErrTokenExist + } + } + approverValid := false conf := types.ConfSub(cfg, driverName) for _, approver := range conf.GStrList("tokenApprs") { diff --git a/plugin/dapp/token/types/const.go b/plugin/dapp/token/types/const.go index ec4ad28277..3525c5c497 100644 --- a/plugin/dapp/token/types/const.go +++ b/plugin/dapp/token/types/const.go @@ -51,6 +51,8 @@ var ( ForkTokenCheckX = "ForkTokenCheck" //ForkTokenEvm token asset can be transfer by evm ForkTokenEvm = "ForkTokenEvm" + // ForkTokenFinishCheckX fork token finish create check global symbol uniqueness + ForkTokenFinishCheckX = "ForkTokenFinishCheck" ) const ( diff --git a/plugin/dapp/token/types/types.go b/plugin/dapp/token/types/types.go index fe06448c99..84c572c823 100644 --- a/plugin/dapp/token/types/types.go +++ b/plugin/dapp/token/types/types.go @@ -30,6 +30,7 @@ func InitFork(cfg *types.Chain33Config) { cfg.RegisterDappFork(TokenX, ForkTokenSymbolWithNumberX, 0) cfg.RegisterDappFork(TokenX, ForkTokenCheckX, 0) cfg.RegisterDappFork(TokenX, ForkTokenEvm, 0) + cfg.RegisterDappFork(TokenX, ForkTokenFinishCheckX, 0) } //InitExecutor ... From 2c09daeecae47e83d34c6807ff3fbd0f26462ac2 Mon Sep 17 00:00:00 2001 From: king Date: Mon, 31 Aug 2026 06:12:11 +0000 Subject: [PATCH 2/4] test(token): add testnode e2e for finishCreate global symbol uniqueness check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit finishCreate 的全局 symbol 唯一性修复需要经过真实链路径验证,因为直接调用 exec.Exec 与 testnode 的 mempool -> 共识 -> executor 完整链路行为不同。 新增 testnode 端到端用例: - fork 生效后,同一 symbol 第二次 finishCreate 被拒绝,仅第一个 owner 持有 Total; - fork 高度之前,旧行为保留(重复 finish 成功,实际发行 2*Total,超发对照)。 Co-Authored-By: Claude Fable 5 --- plugin/dapp/token/executor/vuln_e2e_test.go | 172 ++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 plugin/dapp/token/executor/vuln_e2e_test.go diff --git a/plugin/dapp/token/executor/vuln_e2e_test.go b/plugin/dapp/token/executor/vuln_e2e_test.go new file mode 100644 index 0000000000..ea973f06a6 --- /dev/null +++ b/plugin/dapp/token/executor/vuln_e2e_test.go @@ -0,0 +1,172 @@ +// Copyright Fuzamei Corp. 2018 All Rights Reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package executor_test + +// 链上端到端回归测试(testnode):验证 finishCreate 的全局 symbol 唯一性修复。 +// +// 漏洞:tokendb.go finishCreate 只按 (symbol, owner) 查 per-owner 记录并检查状态, +// 缺少像 preCreate 那样对全局 symbol 唯一性(checkTokenExist)的检查。同一 symbol +// 被两个不同 owner 先后 preCreate 后,两个 owner 都能被 finishCreate,每次 finish 都 +// 执行 GenesisInit(owner, Total),导致同一 symbol 实际发行 N * Total。 +// +// 与 token_finishcheck_test.go(函数调用级单测)的区别:单测直接 exec.Exec(tx, 1) +// 并手工 apply receipt 的 KV,绕过了真实链的 mempool -> 共识 -> executor 完整链路。 +// 本测试用 util/testnode 拉起完整链节点,走真实交易上链路径验证: +// - fork ForkTokenFinishCheck 生效后,同一 symbol 的第二次 finishCreate 被拒绝(不超发); +// - fork 高度之前,旧行为保留(第二次 finishCreate 成功,超发,用于对照复现 bug)。 + +import ( + "strings" + "testing" + + "github.com/33cn/chain33/account" + "github.com/33cn/chain33/common/crypto" + "github.com/33cn/chain33/executor" + rpctypes "github.com/33cn/chain33/rpc/types" + "github.com/33cn/chain33/types" + "github.com/33cn/chain33/util" + "github.com/33cn/chain33/util/testnode" + pty "github.com/33cn/plugin/plugin/dapp/token/types" + "github.com/stretchr/testify/require" + + _ "github.com/33cn/chain33/system" + _ "github.com/33cn/plugin/plugin" +) + +// e2eTokenCfg 构造 token fork 配置。Title 由 "local" 替换为 "chain33",否则 +// needSetForkZero 会把所有 fork 高度强制置 0,无法测试 fork 之前的行为。 +func e2eTokenCfg(forkHeight int64) *types.Chain33Config { + cfg := types.NewChain33Config(strings.Replace(types.GetDefaultCfgstring(), "Title=\"local\"", "Title=\"chain33\"", 1)) + cfg.SetDappFork(pty.TokenX, pty.ForkTokenFinishCheckX, forkHeight) + return cfg +} + +// e2eSendTokenRaw 通过 token 的 jrpc 构造未签名交易,再签名上链,等待并返回交易明细。 +func e2eSendTokenRaw(t *testing.T, mocker *testnode.Chain33Mock, method string, + param interface{}, priv crypto.PrivKey) *rpctypes.TransactionDetail { + t.Helper() + var txhex string + require.Nil(t, mocker.GetJSONC().Call(method, param, &txhex)) + hash, err := mocker.SendAndSign(priv, txhex) + require.Nil(t, err) + detail, err := mocker.WaitTx(hash) + require.Nil(t, err) + return detail +} + +// e2eSendManage 通过 manage 交易配置 token 的 manage 类配置(如 token-blacklist)。 +func e2eSendManage(t *testing.T, mocker *testnode.Chain33Mock, cfg *types.Chain33Config, + key, op, value string) { + t.Helper() + tx := util.CreateManageTx(cfg, mocker.GetHotKey(), key, op, value) + require.NotNil(t, tx) + reply, err := mocker.GetAPI().SendTx(tx) + require.Nil(t, err) + detail, err := mocker.WaitTx(reply.GetMsg()) + require.Nil(t, err) + require.Equal(t, int32(types.ExecOk), detail.Receipt.Ty, "manage %s %s failed", key, op) +} + +// e2eTokenBalance 查询 token 账户命名空间 mavl-token-- 下 addr 的余额。 +func e2eTokenBalance(t *testing.T, mocker *testnode.Chain33Mock, cfg *types.Chain33Config, + stateHash []byte, symbol, addr string) int64 { + t.Helper() + statedb := executor.NewStateDB(mocker.GetClient(), stateHash, nil, nil) + accDB, err := account.NewAccountDB(cfg, "token", symbol, statedb) + require.Nil(t, err) + return accDB.LoadAccount(addr).Balance +} + +// e2ePreCreate 两个 owner 分别 preCreate 同一 symbol(价格 0),返回最后一个交易的区块 StateHash。 +func e2ePreCreate(t *testing.T, mocker *testnode.Chain33Mock, symbol string, + total int64, ownerA, ownerB string) { + t.Helper() + preA := &pty.TokenPreCreate{Name: symbol, Symbol: symbol, Introduction: symbol, + Total: total, Price: 0, Owner: ownerA} + detail := e2eSendTokenRaw(t, mocker, "token.CreateRawTokenPreCreateTx", preA, mocker.GetGenesisKey()) + require.Equal(t, int32(types.ExecOk), detail.Receipt.Ty, "ownerA preCreate should succeed") + + preB := &pty.TokenPreCreate{Name: symbol, Symbol: symbol, Introduction: symbol, + Total: total, Price: 0, Owner: ownerB} + detail = e2eSendTokenRaw(t, mocker, "token.CreateRawTokenPreCreateTx", preB, mocker.GetGenesisKey()) + require.Equal(t, int32(types.ExecOk), detail.Receipt.Ty, "ownerB preCreate same symbol should succeed") +} + +// TestVulnE2ETokenDupFinishCreateRejected 验证 ForkTokenFinishCheck 生效后: +// 两个 owner precreate 同一 symbol,第一次 finishCreate 成功,第二次被拒绝, +// 只有第一个 owner 持有 Total,不超发。 +func TestVulnE2ETokenDupFinishCreateRejected(t *testing.T) { + cfg := e2eTokenCfg(0) // fork 高度 0,立即生效 + mocker := testnode.NewWithConfig(cfg, nil) + defer mocker.Close() + mocker.Listen() + + // 给热键(审批人/超级管理员)拨款,用于支付 manage 交易与 finishCreate 的手续费 + require.Nil(t, mocker.SendHot()) + + symbol := "DUPREJECT" + total := int64(10000 * types.DefaultCoinPrecision) + ownerA := mocker.GetGenesisAddress() + ownerB := mocker.GetHotAddress() + + // 初始化 token-blacklist manage 配置(ForkTokenBlackListX 高度 0 生效,preCreate 必须能读到) + e2eSendManage(t, mocker, cfg, "token-blacklist", "add", "BTY") + e2ePreCreate(t, mocker, symbol, total, ownerA, ownerB) + + // 第一次 finishCreate 成功,写入全局 token 记录 + finA := &pty.TokenFinishCreate{Symbol: symbol, Owner: ownerA} + detail := e2eSendTokenRaw(t, mocker, "token.CreateRawTokenFinishTx", finA, mocker.GetHotKey()) + require.Equal(t, int32(types.ExecOk), detail.Receipt.Ty, "first finishCreate should succeed") + + // 第二次 finishCreate 同一 symbol,被全局唯一性检查拒绝 + finB := &pty.TokenFinishCreate{Symbol: symbol, Owner: ownerB} + detail = e2eSendTokenRaw(t, mocker, "token.CreateRawTokenFinishTx", finB, mocker.GetHotKey()) + require.NotEqual(t, int32(types.ExecOk), detail.Receipt.Ty, "duplicate finishCreate must be rejected") + stateHash := mocker.GetBlock(detail.Height).StateHash + + // 只有 ownerA 持有 Total,ownerB 为 0,无超发 + balA := e2eTokenBalance(t, mocker, cfg, stateHash, symbol, ownerA) + balB := e2eTokenBalance(t, mocker, cfg, stateHash, symbol, ownerB) + t.Logf("symbol=%s declared Total=%d, ownerA=%d, ownerB=%d", symbol, total, balA, balB) + require.Equal(t, total, balA) + require.Equal(t, int64(0), balB) +} + +// TestVulnE2ETokenDupFinishCreatePreFork 验证 fork 高度之前的旧行为: +// 同一 symbol 被重复 finishCreate 仍然成功,实际发行 2 * Total(超发对照,复现 bug)。 +func TestVulnE2ETokenDupFinishCreatePreFork(t *testing.T) { + cfg := e2eTokenCfg(2000000) // fork 生效高度高于测试链高度,旧行为保留 + mocker := testnode.NewWithConfig(cfg, nil) + defer mocker.Close() + mocker.Listen() + + require.Nil(t, mocker.SendHot()) + + symbol := "DUPPREFORK" + total := int64(10000 * types.DefaultCoinPrecision) + ownerA := mocker.GetGenesisAddress() + ownerB := mocker.GetHotAddress() + + e2eSendManage(t, mocker, cfg, "token-blacklist", "add", "BTY") + e2ePreCreate(t, mocker, symbol, total, ownerA, ownerB) + + finA := &pty.TokenFinishCreate{Symbol: symbol, Owner: ownerA} + detail := e2eSendTokenRaw(t, mocker, "token.CreateRawTokenFinishTx", finA, mocker.GetHotKey()) + require.Equal(t, int32(types.ExecOk), detail.Receipt.Ty, "first finishCreate should succeed") + + // fork 之前:无全局 symbol 唯一性检查,第二次 finishCreate 仍然成功 + finB := &pty.TokenFinishCreate{Symbol: symbol, Owner: ownerB} + detail = e2eSendTokenRaw(t, mocker, "token.CreateRawTokenFinishTx", finB, mocker.GetHotKey()) + require.Equal(t, int32(types.ExecOk), detail.Receipt.Ty, "pre-fork duplicate finishCreate keeps old behavior") + stateHash := mocker.GetBlock(detail.Height).StateHash + + // 两个 owner 各持有 Total,实际发行 2 * Total(超发) + balA := e2eTokenBalance(t, mocker, cfg, stateHash, symbol, ownerA) + balB := e2eTokenBalance(t, mocker, cfg, stateHash, symbol, ownerB) + t.Logf("symbol=%s declared Total=%d, ownerA=%d, ownerB=%d, actual issued=%d (2x Total)", + symbol, total, balA, balB, balA+balB) + require.Equal(t, total, balA) + require.Equal(t, total, balB) +} From 3b585cae80b27eb8edd225c020597599eef6be52 Mon Sep 17 00:00:00 2001 From: king Date: Fri, 4 Sep 2026 03:57:36 +0000 Subject: [PATCH 3/4] test(token): move finishCreate testnode e2e out of executor package The e2e test imported _ "github.com/33cn/plugin/plugin", which registers every dapp driver, while token_test.go in the same test binary also calls Init() to register the token driver. Running both together panicked with "Register called twice for driver token" and failed unit-test, coverage and GOARCH=386 in ci_base. Move the test into its own token/testnode package (same layout as ticket/testnode) so the two registration paths never share a binary. Co-Authored-By: Claude Fable 5 --- .../vuln_e2e_test.go => testnode/token_finishcheck_test.go} | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) rename plugin/dapp/token/{executor/vuln_e2e_test.go => testnode/token_finishcheck_test.go} (96%) diff --git a/plugin/dapp/token/executor/vuln_e2e_test.go b/plugin/dapp/token/testnode/token_finishcheck_test.go similarity index 96% rename from plugin/dapp/token/executor/vuln_e2e_test.go rename to plugin/dapp/token/testnode/token_finishcheck_test.go index ea973f06a6..6e788f8230 100644 --- a/plugin/dapp/token/executor/vuln_e2e_test.go +++ b/plugin/dapp/token/testnode/token_finishcheck_test.go @@ -2,10 +2,14 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package executor_test +package testnode // 链上端到端回归测试(testnode):验证 finishCreate 的全局 symbol 唯一性修复。 // +// 单独放在 token/testnode 包:本测试通过 _ "github.com/33cn/plugin/plugin" 注册全部 dapp +// driver,而 executor 包内的 token_test.go 也会调用 Init() 注册 token driver,二者在同一 +// 测试二进制中会触发 "Register called twice for driver token" panic。 +// // 漏洞:tokendb.go finishCreate 只按 (symbol, owner) 查 per-owner 记录并检查状态, // 缺少像 preCreate 那样对全局 symbol 唯一性(checkTokenExist)的检查。同一 symbol // 被两个不同 owner 先后 preCreate 后,两个 owner 都能被 finishCreate,每次 finish 都 From b7e410ba976140c2bbbe07dcba13be960e756b7c Mon Sep 17 00:00:00 2001 From: king Date: Fri, 4 Sep 2026 04:20:50 +0000 Subject: [PATCH 4/4] style(token): drop redundant int64 conversion flagged by unconvert Co-Authored-By: Claude Fable 5 --- plugin/dapp/token/testnode/token_finishcheck_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/dapp/token/testnode/token_finishcheck_test.go b/plugin/dapp/token/testnode/token_finishcheck_test.go index 6e788f8230..02b4036e14 100644 --- a/plugin/dapp/token/testnode/token_finishcheck_test.go +++ b/plugin/dapp/token/testnode/token_finishcheck_test.go @@ -111,7 +111,7 @@ func TestVulnE2ETokenDupFinishCreateRejected(t *testing.T) { require.Nil(t, mocker.SendHot()) symbol := "DUPREJECT" - total := int64(10000 * types.DefaultCoinPrecision) + total := 10000 * types.DefaultCoinPrecision ownerA := mocker.GetGenesisAddress() ownerB := mocker.GetHotAddress() @@ -149,7 +149,7 @@ func TestVulnE2ETokenDupFinishCreatePreFork(t *testing.T) { require.Nil(t, mocker.SendHot()) symbol := "DUPPREFORK" - total := int64(10000 * types.DefaultCoinPrecision) + total := 10000 * types.DefaultCoinPrecision ownerA := mocker.GetGenesisAddress() ownerB := mocker.GetHotAddress()