Skip to content

feat: add EVM account blacklist and bump chain33 to aa71469 - #1304

Merged
33cn merged 12 commits into
masterfrom
feature/account-blacklist
Sep 4, 2026
Merged

feat: add EVM account blacklist and bump chain33 to aa71469#1304
33cn merged 12 commits into
masterfrom
feature/account-blacklist

Conversation

@libangzhu

Copy link
Copy Markdown
Collaborator

Wire ForkAccountBlacklist into EVM CheckTx/exec/runtime/statedb so blocked addresses are rejected consistently with chain33 consensus checks, and depend on github.com/33cn/chain33@aa71469c0974 from master.

Wire ForkAccountBlacklist into EVM CheckTx/exec/runtime/statedb so blocked
addresses are rejected consistently with chain33 consensus checks, and
depend on github.com/33cn/chain33@aa71469c0974 from master.
Resolve Create() conflict by keeping account blacklist check and
ForkEVMFixOverflow transfer protection together.
Keep dependency files unchanged per review feedback; blacklist feature
does not require bumping the chain33 module version in this PR.
@bysomeone bysomeone changed the title feat: add EVM account blacklist and bump chain33 to aa71469 WIP feat: add EVM account blacklist and bump chain33 to aa71469 Jul 30, 2026
@bysomeone

Copy link
Copy Markdown
Collaborator

需要单独提pr处理 chain33 ethereum包升级兼容问题 @vipwzw @libangzhu

@bysomeone
bysomeone force-pushed the feature/account-blacklist branch from 0550d84 to 80d3dfa Compare July 31, 2026 03:19
bysomeone and others added 3 commits August 27, 2026 11:58
…st is merged

The attack integration test had a TODO placeholder skipping blacklist
verification ('blacklist pending — roles attacker+accomplice marked')
waiting for the account blacklist PR to land. Now that ForkAccountBlacklist
is in master:

- Set ForkAccountBlacklist fork height to 1000 in test config (aligned
  with ForkEVMFixOverflow).
- Replace t.Skip with real assertions: blacklisted attacker's EVM call
  is rejected via checkEvmBlockedAccount, blacklisted accomplice's coins
  tx is rejected via chain33 CheckTxBlockedAccount (mempool layer), and
  a legit user deposit still works.

Co-Authored-By: Claude <noreply@anthropic.com>
Merging master into feature/account-blacklist left duplicated
ForkParaFee=-1 and ForkAccountBlacklist=-1 entries in chain33.fork.toml,
chain33.para.toml and chain33.proxyminer.toml (both master and the
blacklist branch had added them independently, and git didn't detect the
duplication). Duplicate TOML keys can cause config parse/behaviour issues
that surface as nodes failing to start mining, leading to CI docker-compose
failures (block_wait stuck at height 0, dht timeout). Keep the master
versions, drop the blacklist branch's copies.

Co-Authored-By: Claude <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.57143% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 29.12%. Comparing base (b34b6b6) to head (eaf1546).
⚠️ Report is 23 commits behind head on master.

Files with missing lines Patch % Lines
plugin/dapp/evm/executor/vm/state/statedb.go 80.95% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1304      +/-   ##
==========================================
+ Coverage   28.91%   29.12%   +0.20%     
==========================================
  Files         382      382              
  Lines       73450    73475      +25     
==========================================
+ Hits        21239    21400     +161     
+ Misses      50233    50087     -146     
- Partials     1978     1988      +10     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Improve codecov patch coverage for the account blacklist logic added in
this PR:

- statedb_test.go: TestBlacklistBlocksFundOps covers isBlockedAccount
  gating in CanTransfer / Transfer / TransferToToken (all three return
  failure when sender or recipient is blacklisted, and TransferToToken
  surfaces ErrBlockedAccount).
- account_blacklist_test.go: TestCreateBlockedAccount covers the Create
  path's checkBlockedAccount, symmetric with the existing Call test.

Co-Authored-By: Claude <noreply@anthropic.com>
@bysomeone
bysomeone self-requested a review August 27, 2026 08:26
@bysomeone bysomeone changed the title WIP feat: add EVM account blacklist and bump chain33 to aa71469 feat: add EVM account blacklist and bump chain33 to aa71469 Aug 27, 2026
bysomeone
bysomeone previously approved these changes Aug 27, 2026
@bysomeone
bysomeone requested a review from vipwzw August 27, 2026 09:16
@vipwzw

vipwzw commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

还需要提供为什么需要专门在evm中过滤的必要性说明

@33cn

33cn commented Aug 31, 2026

Copy link
Copy Markdown
Owner

结论:不需要修改 statedb.go。同理,evm.goCheckTxexec.goinnerExecruntime/evm.goCall/Create 这些 EVM 层黑名单检查也都属于冗余防御——chain33 框架层已经完整拦截。

1. chain33 层不是只在 checkblock,而是三处、全部走同一个核心

三处拦截最终都调用 types.checkTxBlockedAccountCoretypes/account_blacklist.go):

位置 调用 fork 门控
mempool 入口 system/mempool/check.go:76eventprocess.go:216/339 CheckTxBlockedAccountImmediate 无(随升级立即生效)
出块/验块(checkblock) system/consensus/base.go:606/623 AddTxsToBlock CheckTxBlockedAccount
执行器 checkTx executor/execenv.go:173/204 checkTx/checkTxGroup CheckTxBlockedAccount

核心判定覆盖四个维度(checkTxBlockedAccountCore):

  1. tx.From() 发送方
  2. tx.GetTo() 接收方
  3. tx.GetRealToAddr() 真实接收方
  4. EVM payload 目标:EVMContractAction4Chain33.ContractAddr + 20 字节 ParacheckEVMTxBlockedTarget

EVM 特有的目标地址(合约地址 / 纯转账 20 字节地址)已经在 chain33 层被解析并拦截

2. 框架 checkTx 先于 EVM 驱动执行

executor/execenv.gocheckTx(内含 CheckTxBlockedAccount)在 execTx / execCheckTx 中先于驱动的 CheckTxExec 执行。真实节点里,命中黑名单的交易在 mempool / checkblock / checkTx 三层中的第一层就被拦下,根本不会进入 EVM 驱动

3. 为什么 PR 的集成测试“看起来需要” EVM 层检查

PR 里 phase2 的断言直接调用 exec.Exec(...)(驱动层),绕过了框架 checkTxexec.ExecexecTxcheckTx 通过之后才调用的驱动方法;单独调它等于把框架这道闸门拆掉,自然会“看似”需要 EVM 自己拦。这是真实节点里不存在的路径。

4. 实测证据

plugin/dapp/evm/executor/blacklist_verify_test.goTestBlacklistChain33VsEVM 在同一环境下对照验证:

  • types.CheckTxBlockedAccount(即框架 checkTx 实际调用的函数)能拦截:
    • attacker / accomplice 的 EVM 调用(from 维度)
    • attacker 的 coins 转账(from 维度)
    • 转给黑名单的 coins 转账(to 维度)
    • 调用黑名单合约(to/ContractAddr 维度)
    • fork 门控正常(默认配置放行)
  • 绕过框架 checkTx 直接跑 EVM 驱动 exec.Exec 时,同一笔黑名单交易成功执行——说明 EVM 驱动自身没有独立黑名单判断,真正的闸门在框架层。
--- PASS: TestBlacklistChain33VsEVM (0.03s)
  ✓ chain33 层拦截 attacker 的 EVM 调用 (from 维度命中)
  ✓ chain33 层拦截 accomplice 的 EVM 调用 (from 维度命中)
  ✓ chain33 层拦截 attacker 的 coins 转账 (from 维度命中)
  ✓ chain33 层拦截转给 attacker 的 coins 转账 (to 维度命中)
  ✓ chain33 层拦截调用黑名单合约 (to/ContractAddr 维度命中)
  ✓ fork 门控正常
  ✓ EVM 驱动 Exec(绕过 checkTx)未拦截 → EVM 插件自身无独立判断

5. 唯一“缺口”也不支持改 statedb.go

唯一交易级检查看不到的场景是“非黑名单用户触发合约内部逻辑向黑名单地址转账”(交易字段干净)。但:

  • 该场景不影响黑名单冻结目标——攻击者自己的资金想转出,必须由自己的 from 发起,已被 from 维度封死;
  • 对 7.20 攻击实际的 ERC20 分发路径(合约 storage 的 balanceOf),statedb.goTransfer / CanTransfer / TransferToToken 根本不经过,改了也拦不住;它只对内部原生币 / token 转账起很窄的作用。

因此 statedb.go 的改动既冗余又不完整,不需要改

@libangzhu

Copy link
Copy Markdown
Collaborator Author

结论:不需要修改 statedb.go。同理,evm.goCheckTxexec.goinnerExecruntime/evm.goCall/Create 这些 EVM 层黑名单检查也都属于冗余防御——chain33 框架层已经完整拦截。

1. chain33 层不是只在 checkblock,而是三处、全部走同一个核心

三处拦截最终都调用 types.checkTxBlockedAccountCoretypes/account_blacklist.go):

层 位置 调用 fork 门控
mempool 入口 system/mempool/check.go:76eventprocess.go:216/339 CheckTxBlockedAccountImmediate 无(随升级立即生效)
出块/验块(checkblock) system/consensus/base.go:606/623 AddTxsToBlock CheckTxBlockedAccount
执行器 checkTx executor/execenv.go:173/204 checkTx/checkTxGroup CheckTxBlockedAccount
核心判定覆盖四个维度(checkTxBlockedAccountCore):

  1. tx.From() 发送方
  2. tx.GetTo() 接收方
  3. tx.GetRealToAddr() 真实接收方
  4. EVM payload 目标:EVMContractAction4Chain33.ContractAddr + 20 字节 ParacheckEVMTxBlockedTarget

EVM 特有的目标地址(合约地址 / 纯转账 20 字节地址)已经在 chain33 层被解析并拦截

2. 框架 checkTx 先于 EVM 驱动执行

executor/execenv.gocheckTx(内含 CheckTxBlockedAccount)在 execTx / execCheckTx 中先于驱动的 CheckTxExec 执行。真实节点里,命中黑名单的交易在 mempool / checkblock / checkTx 三层中的第一层就被拦下,根本不会进入 EVM 驱动

3. 为什么 PR 的集成测试“看起来需要” EVM 层检查

PR 里 phase2 的断言直接调用 exec.Exec(...)(驱动层),绕过了框架 checkTxexec.ExecexecTxcheckTx 通过之后才调用的驱动方法;单独调它等于把框架这道闸门拆掉,自然会“看似”需要 EVM 自己拦。这是真实节点里不存在的路径。

4. 实测证据

plugin/dapp/evm/executor/blacklist_verify_test.goTestBlacklistChain33VsEVM 在同一环境下对照验证:

  • types.CheckTxBlockedAccount(即框架 checkTx 实际调用的函数)能拦截:

    • attacker / accomplice 的 EVM 调用(from 维度)
    • attacker 的 coins 转账(from 维度)
    • 转给黑名单的 coins 转账(to 维度)
    • 调用黑名单合约(to/ContractAddr 维度)
    • fork 门控正常(默认配置放行)
  • 绕过框架 checkTx 直接跑 EVM 驱动 exec.Exec 时,同一笔黑名单交易成功执行——说明 EVM 驱动自身没有独立黑名单判断,真正的闸门在框架层。

--- PASS: TestBlacklistChain33VsEVM (0.03s)
  ✓ chain33 层拦截 attacker 的 EVM 调用 (from 维度命中)
  ✓ chain33 层拦截 accomplice 的 EVM 调用 (from 维度命中)
  ✓ chain33 层拦截 attacker 的 coins 转账 (from 维度命中)
  ✓ chain33 层拦截转给 attacker 的 coins 转账 (to 维度命中)
  ✓ chain33 层拦截调用黑名单合约 (to/ContractAddr 维度命中)
  ✓ fork 门控正常
  ✓ EVM 驱动 Exec(绕过 checkTx)未拦截 → EVM 插件自身无独立判断

5. 唯一“缺口”也不支持改 statedb.go

唯一交易级检查看不到的场景是“非黑名单用户触发合约内部逻辑向黑名单地址转账”(交易字段干净)。但:

  • 该场景不影响黑名单冻结目标——攻击者自己的资金想转出,必须由自己的 from 发起,已被 from 维度封死;
  • 对 7.20 攻击实际的 ERC20 分发路径(合约 storage 的 balanceOf),statedb.goTransfer / CanTransfer / TransferToToken 根本不经过,改了也拦不住;它只对内部原生币 / token 转账起很窄的作用。

因此 statedb.go 的改动既冗余又不完整,不需要改

@33cn @vipwzw

同意这条分析里关于交易级拦截的结论,并结合主网已全量跑本 PR 分支的现状,补充处理原则。

  1. 交易级拦截在框架,EVM 对交易字段是重复的。

chain33 在 mempool / 出块验块 / executor.checkTx 三层走同一套 CheckTxBlockedAccount,覆盖 from / to / realTo / EVM ContractAddr+Para。真实节点上,命中名单的交易在这三层的第一层就会被拦下,进不了 EVM 驱动。

因此本 PR 在 evm.go 的 CheckTx、exec.go 的 innerExec 再拦一遍交易字段,对真实节点是冗余的:代码会执行,但命中分支对真实交易开不了火。集成测试 phase2 直接调 exec.Exec(...),绕过了框架 checkTx,所以会「看起来需要」EVM 层。这是真实节点里不存在的路径。

  1. 但内部 CALL / 转账检查已经是主网执行语义,不能当冗余直接删。

框架只看交易信封上的地址,不看合约跑起来以后内部又 CALL 了谁、又转给了谁。

举例:普通用户 U(不在名单)调用付款合约 C(不在名单),合约内部再把原生币转给黑名单地址 A,或 CALL A。

信封:from=U、to/ContractAddr=C,框架放行,交易进入 EVM。
然后才走到 runtime/evm.go 的 Call/Create、statedb.go 的 CanTransfer/Transfer/TransferToToken。这里的目标是 A,不在交易字段里。
有这些检查:内部调用/转账失败 → RevertToSnapshot(ExecPack),A 收不到钱。
删掉这些检查再回放同一笔旧交易:转账成功,A 余额变化,receipt / 状态根不同。
当前主网节点已全量跑本 PR 分支。这类「信封干净、内部碰到黑名单」的交易,如果升级窗口里出现过一笔,用去掉检查的二进制回放旧块会对不上,有回放分叉风险。全网一起换新二进制只保证之后的新块一致,保证不了用新代码回放旧块。

  1. 要撤,必须新开 fork,或者先证明历史不依赖这些检查。

开新 fork:旧高度仍走现在的 Call/Create/statedb 检查,新高度才跳过。老代码必须留在二进制里供回放,不是物理删除。
直接删、不开 fork:必须先证明「启用本 PR 且 ForkAccountBlacklist 生效至今」,没有任何一笔执行结果依赖这几处检查(查日志 Call blocked account / Create blocked account / Transfer blocked account 等,或用新旧二进制回放同一段区块比 state hash)。证不了就不能删。
在没有历史审计或新 fork 之前,Call/Create/statedb 的黑名单检查应保留。CheckTx/innerExec 的交易级重复检查仍可单独评估是否收掉,不影响已激活的 ForkAccountBlacklist,也不改历史回放。

@33cn

33cn commented Sep 2, 2026

Copy link
Copy Markdown
Owner

重新审查:以「拦资产打出、不拦打入」为判定标准

先更正上一条评论(#issuecomment-5472790458)中的两处说法:

  1. "runtime/evm.goCall/Create 属于冗余防御" —— 不成立。实测证明这些检查对内部交易真实生效,chain33 静态检查确实看不到。
  2. "ERC20 transfer(黑名单, x) 是盲区" —— 论证方向错了。那是资产打入黑名单,等同冻结,本就不该拦。

@libangzhu 指出的正确标准(主要拦截从黑名单把资产打出)重新逐条核对如下。


A. chain33 已完整拦截(EVM 层再拦一遍无意义)

任何"打出"都必须有一笔交易,而黑名单自己签名的交易 100% 被 from 维度拦死(三层:mempool 无 fork 门控 / 出块验块 / executor.checkTx)。

# 案例 命中维度
A1 黑名单发起 EVM 合约调用 from
A2 黑名单发起 coins / token 转账 from
A3 黑名单提现 evm 子账户余额 fromcoins.Exec_Withdraw 受益人恒为 tx.From
A4 外部直接调用黑名单合约 to / ContractAddr
A5 EVM 纯转账 Para 为 20 字节原始地址 Para
A6 交易组任一笔命中 整组拒绝
A7 黑名单为矿工地址 挖矿交易由矿工签名 → from

B. chain33 拦不住、本 PR 能拦(有真实价值,建议保留

B1. 黑名单合约被内部 CALL 唤醒后转出自身余额 ⭐

干净用户 U → 干净合约 B → (CALL) → 黑名单合约 A → A 转出自己的余额

chain33 只看到 from=U, to=B,放行;A 的地址只存在于 B 的运行时栈上,不在交易字节里。
evm.Call 的 target 检查拦住。

B2. token 预编译 transfer(from=黑名单, to, amount) ⭐⭐ 价值最高

plugin/dapp/evm/executor/vm/runtime/token.go:121 —— from 取自 calldata input[4:36]与 caller 无任何绑定,第三方可任意指定:

from := common.BytesToAddress(input[4:36])
...
ok, err = evm.StateDB.TransferToToken(from.String(), to.String(), tokenName, amount)

chain33 侧看到的 Para 长度为 68+ 字节,IsBlockedAccountRaw 要求精确 20 字节 → 直接返回 false,完全看不见
TransferToToken 的 from 检查是这条链路唯一防线,不可替代。
(前提是调用方合约的创建者在 manage 名单内,现实门槛较高,但门就这一道。)

B3. 黑名单合约 SELFDESTRUCT 把余额打给受益人

opSuicide(instructions.go:908)→ AddBalance(beneficiary, contract),而 AddBalance 内部就是 mdb.Transfer(caddr, addr)(statedb.go:137-140)。
Transfer 的 sender 检查拦住 —— 属意外覆盖,但实测有效。

B4. 黑名单合约内部转出原生币(CALL 携带 value)

env.Transfermdb.Transfer(sender=黑名单合约, ...),与 B1 部分重叠,Transfer 是最后一道闸。

实测验证(在 PR head 25c159b4e 上运行):

--- PASS: TestGap_CallBlockedIsIntercepted          evm.Call 到黑名单地址被拦
--- PASS: TestGap_SelfdestructBeneficiaryIntercepted SELFDESTRUCT 受益人被拦(AddBalance→Transfer)
--- PASS: TestCheckBlockedAccount / TestCallBlockedAccount / TestCreateBlockedAccount  (PR 自带)

C. 谁都拦不住(建议在 PR 中文档化)

ERC20 合约 storage 记账的 transferFrom(黑名单, x, n) —— 黑名单事先 approve 干净地址,余额是 keccak storage 写入,EVM 层无任何钩子可挂。
唯一解:把该代币合约地址一并列入黑名单(冻结整个合约),或由代币合约自身加检查。

附带更正:CALLCODE / DELEGATECALL / STATICCALL 虽然 PR 未打补丁,但不构成打出路径 —— 前两者是借黑名单合约的代码在调用者自己的上下文执行,动的是调用者的存储和余额;STATICCALL 只读。按「拦打出」标准不需要补。(我此前按"完全冻结"标准把它们列为漏网,是标准用错了。)


D. 代码取舍

必须保留(3 处)

位置 覆盖 理由
runtime/evm.go Calltarget 检查 B1 唯一能阻止黑名单合约被内部唤醒
statedb.go TransferToTokenfrom 检查 B2 唯一能拦 token 预编译第三方代打,不可替代
statedb.go Transfersender 检查 B3 / B4 最后一道闸,覆盖 SELFDESTRUCT

实际无意义(建议删除,或至少注明是纯防御冗余)

位置 为什么没意义
evm.go CheckTx 的 from 检查 ① chain33 三层已在驱动之前拦死;② 该函数开头 if IsPara() { return nil },平行链上这段永远不执行
exec.go innerExec 两处 from 检查 msg.From() == tx.From(),chain33 三层已覆盖
statedb.go CanTransfer 的 sender 检查 Transfer 完全重复;且返回 false 会被上层翻译成 ErrNoBalance(exec.go:105-108),错误语义误导排查
runtime/evm.go Call/Createcaller 检查 顶层 caller = tx.From(已拦);内部 caller 是合约,而该合约要能执行必先过 Call 的 target 检查 → 分支不可达
Transfer/TransferToTokenrecipient 检查 拦的是"打入"。符合 chain33「禁止收发任何交易」的原始设计,但按本 PR 优先级属次要;保留无害,删除不影响冻结目标

关于回放兼容

完全同意 @libangzhu 的处理原则:上述"无意义"的代码若已随本分支在主网执行过,不能直接物理删除。删除需满足其一:

  • 新开 fork,旧高度仍走现逻辑,新高度跳过;或
  • 先证明「启用本 PR 且 ForkAccountBlacklist 生效至今」无任何一笔交易的执行结果依赖这些分支(检索 blocked account 日志,或新旧二进制回放同段区块比对 state hash)。

在此之前,D 表的"无意义"仅作为代码评审结论记录,不建议在本 PR 内直接删除。


一处可选的补强

isTransferOnly 路径的 receiver = BytesToAddress(msg.Para())Address.SetBytes(common/address.go:66-71)对超长输入取后 20 字节,而 chain33 的 IsBlockedAccountRaw 要求精确 20 字节。两者不对称 —— 用 32 字节 padding 的 Para 可绕过 chain33 的 Para 维度。
但这条只能打入、不能打出,按本 PR 标准不紧急,仅作记录。


小结

结论
chain33 是否已完全拦截"打出"? 主路径是的(A1–A7,黑名单自签交易全封死)
EVM 层是否有必要单独处理? 有必要,但只需 3 处(B1/B2/B3 对应的 Call target、TransferToToken from、Transfer sender)
statedb.go 是否需要改? 需要,但只有 Transfer 的 sender 与 TransferToToken 的 from 两处有实质意义;CanTransfer 建议撤(受回放约束,见上)
是否存在无解盲区? 有,ERC20 transferFrom,建议文档化

king and others added 2 commits September 3, 2026 04:10
…-blind paths

Answers the review questions on #1304: why filter inside the EVM when chain33
already intercepts at mempool / consensus / executor.checkTx.

The framework only parses the transaction envelope (from / to / ContractAddr /
20-byte Para). Three asset-outflow paths exist only at EVM runtime and are
invisible to it:

- B1  clean user -> clean contract -> CALL -> blocked contract drains itself
      (runtime.Call target check is the only gate)
- B2  token precompile transfer(from,to,amount): `from` comes from calldata,
      unbound to caller; chain33's Para check requires exactly 20 bytes and
      cannot see a 100-byte ABI payload (statedb.TransferToToken from check)
- B3  SELFDESTRUCT pays via AddBalance -> statedb.Transfer(sender=contract).
      opSuicide takes the payer from contract.CodeAddr, so DELEGATECALL into
      blocked code drains the *blocked* address while runtime.DelegateCall has
      no check at all — statedb.Transfer's sender check is the sole gate.

blacklist_gap_test.go proves each path with real coins balances and a
fork-off control run showing the funds do leave when the check is inactive.
B2 goes through the real tokenPrecompile.Run.

The remaining checks (CheckTx from, innerExec from/contractAddr, CanTransfer,
Call/Create caller, recipient dimensions) are defence in depth that never
fires on a real node. They are annotated as such, not removed: the branch is
already live on mainnet, and dropping them would require a new fork or a
history audit to stay replay-safe.

The integration test now asserts that chain33's CheckTxBlockedAccount rejects
the attacker tx first, and treats the direct exec.Exec call as the
defence-in-depth path it is rather than the primary gate.

docs/security/evm-account-blacklist.md records the full analysis, the
one unfixable blind spot (storage-accounted ERC20 transferFrom), and the
replay constraint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@33cn

33cn commented Sep 3, 2026

Copy link
Copy Markdown
Owner

已按 #issuecomment-5503872286 的取舍结论和 @libangzhu 的回放约束更新,推送 aac3ac37f(已合并当前 master)。

改动

源码只加注释,零语义变更。 三处实质防线(Call target / TransferToToken from / Transfer sender)与其余纵深防御分支各自标明角色和原因,并注明"已随本分支在主网执行,撤除需新开 fork"。

docs/security/evm-account-blacklist.md 回答 @vipwzw 的必要性问题:chain33 三层信封检查覆盖什么、EVM 运行时才可见的四条打出路径(B1–B4)、无解盲区(storage 记账的 ERC20 transferFrom)、逐处代码取舍表、回放约束。

vm/runtime/blacklist_gap_test.go 把上一条评论里本地跑过但未提交的 TestGap_* 补进 PR,并加强为:真实 coins 账本、断言余额、每个用例带 fork 关闭的对照跑证明资金确实会流出(防止空转通过)。B2 走真实 tokenPrecompile.Run,100 字节 calldata 指定黑名单 from

集成测试 phase2 先断言 CheckTxBlockedAccount 拒绝(真实闸门),再把直接 exec.Exec 作为第二层验证,不再把它当主路径。

一处需要更正上轮结论的发现

上轮把 Transfer sender 对 SELFDESTRUCT 的覆盖称为"意外覆盖",并认定 CallCode/DelegateCall 不构成打出路径、不需要补。实际情况:

opSuicide(instructions.go)的付款方是 contract.CodeAddr,金额取 contract.Address() 的余额。DELEGATECALL 下两者不同:CodeAddr = 被借代码的合约 A,Address() = 代理 P。

干净用户 U → 干净代理 P(充值 X)→ DELEGATECALL → 黑名单合约 A 的代码执行 SELFDESTRUCT
→ A 向受益人付出 X

runtime.DelegateCall 没有任何黑名单检查,Call 的 target 检查也走不到。statedb.Transfer 的 sender 检查是这条路径唯一的闸TestGapB3b_DelegatecallSelfdestructDrainsCodeAddr 的对照跑证明 fork 关闭时 A 的余额确实被转走、P 分毫未动。

结论不变(statedb.go 的 sender 检查必须保留),但依据从"意外覆盖"变成"唯一防线"。是否要给 DelegateCall 加独立检查可以另议——按回放约束,现在加也需要新 fork。

验证

plugin/dapp/evm/... 全包通过,含 -race -count=3;gofmt / goimports / golangci-lint(CI 的 linter 集合)在改动文件上干净。

opSuicide pays from contract.CodeAddr, not contract.Address(). Under
DELEGATECALL/CALLCODE that is the *callee*, so a clean proxy funded with X
that borrows a blacklisted contract's SELFDESTRUCT makes the blacklisted
address pay X to the beneficiary. Only statedb.Transfer's sender check
caught it before.

Add the same fork-gated checkBlockedAccount(addr) gate to DelegateCall and
CallCode that Call already applies to its target. StaticCall is left alone:
opSuicide returns ErrWriteProtection in read-only context.

No new fork: the branch has not shipped yet, so there is no history to
replay. TestGapB3b now verifies both gates independently (runtime gate via
the full CALL→DELEGATECALL path; statedb gate by running the delegate
contract directly) plus the fork-off control run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@33cn

33cn commented Sep 3, 2026

Copy link
Copy Markdown
Owner

5f7be913f:给 DelegateCall / CallCode 加上与 Call target 同款的 fork 门控 addr 检查,拒绝借用黑名单地址的代码。分支尚未上线,无历史需回放,不开新 fork。

  • StaticCall 不加:只读上下文下 opSuicide 返回 ErrWriteProtection,不构成打出。
  • statedb.Transfer 的 sender 检查保留为兜底。TestGapB3b 现在分别验证两道闸各自独立有效(runtime 闸走完整 CALL→DELEGATECALL 路径;statedb 闸直接构造 delegate 合约对象跑黑名单代码),并保留 fork 关闭的对照跑。
  • 新增 TestDelegateCallBlockedCode 覆盖两个入口的返回值;文档实质防线表更新为 4 处。

@33cn

33cn commented Sep 3, 2026

Copy link
Copy Markdown
Owner

回滚 DELEGATECALL/CALLCODE 的 addr 检查

#issuecomment-5523029815 里"是否要给 DelegateCall 加独立检查"的结论是:不加。此前短暂加过(5f7be913f),验证后确认与 statedb.Transfer 的 sender 检查重复,已回滚(8bf130250),理由如下。

验证方法

  1. 静态opSuicide 是唯一以 contract.CodeAddr 为付款方的指令(instructions.go:908 → AddBalance → statedb.Transfer);DELEGATECALL/CALLCODE 执行黑名单代码时,其余资产操作(CALL value、CREATE endowment)的付款方都是调用者代理,只有 SELFDESTRUCT 会以黑名单地址为付款方。
  2. 实测:临时移除两处检查后跑 TestGapB3b,所有资产断言(A 余额不变、受益人分文未得、fork 关闭对照)全部通过;唯一失败的断言是"代理未被标记自毁"——即两处检查只改变失败方式,不改变资产安全结果。

回滚理由

  • 无增量保护:B3b 的打出路径必然经过 statedb.Transfer(A → 受益人),sender 检查在任何金额(含 0)下都拒绝,两处 runtime 检查拦不到任何它拦不住的钱。
  • 过度拦截:它们会拒绝所有借用黑名单代码的无害调用(不涉及资产的代码复用),与"冻结资产而非冻结代码"的判定标准不符。
  • 因为还没上线,回滚不需要新 fork,也不影响回放。

现状

B3b 的唯一防线是 statedb.Transfer 的 sender 检查;StaticCall 不检查(只读,opSuicide 返回 ErrWriteProtection)。TestGapB3b 的 statedb 兜底子用例与 fork 关闭对照保留,继续证明这条路径是活的且被拦死。文档已同步为单闸门描述。

10*ctypes.DefaultCoinPrecision is an untyped constant assignable to the
int64 parameter directly; the explicit conversion fails the CI linter
(check_fmt job: unconvert at attack_integration_test.go:281).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@33cn
33cn merged commit 020b080 into master Sep 4, 2026
23 checks passed
@33cn
33cn deleted the feature/account-blacklist branch September 4, 2026 02:00
@33cn

33cn commented Sep 4, 2026

Copy link
Copy Markdown
Owner

🎉 This PR is included in version 1.72.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@33cn 33cn added the released label Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants