diff --git a/chain33.fork.toml b/chain33.fork.toml
index b6503792d1..4a1a9dc215 100644
--- a/chain33.fork.toml
+++ b/chain33.fork.toml
@@ -88,6 +88,7 @@ ForkEVMMixAddress=0
ForkIntrinsicGas=0
ForkEVMAddressInit=0
ForkEvmExecNonce=0
+ForkEVMFixOverflow=0
[fork.sub.evmxgo]
diff --git a/chain33.para.toml b/chain33.para.toml
index 6cca143647..331476641a 100644
--- a/chain33.para.toml
+++ b/chain33.para.toml
@@ -408,6 +408,7 @@ ForkIntrinsicGas=0
ForkEVMAddressInit=0
ForkEvmExecNonce=0
ForkEvmExecNonceV2=0
+ForkEVMFixOverflow=0
[fork.sub.blackwhite]
Enable=0
ForkBlackWhiteV2=0
diff --git a/docs/security/evm-uint64-overflow-attack-analysis.md b/docs/security/evm-uint64-overflow-attack-analysis.md
new file mode 100644
index 0000000000..35989ec980
--- /dev/null
+++ b/docs/security/evm-uint64-overflow-attack-analysis.md
@@ -0,0 +1,370 @@
+# EVM uint64→int64 溢出攻击分析
+
+## 事件概述
+
+- **日期**:2026-07-30
+- **攻击交易**:`0xd288c03ead1296adcc50eb7be1824eab611a761920067734ff058c56cb262d74`
+- **目标合约**:WBTY (Wrapped BTY) `0xe09f5bdca143f6e4ad9d43516a1d1289a3dd6dfc`
+- **调用函数**:`deposit()` — `0xd0e30db0`
+- **攻击结果**:攻击者以零成本铸造约 **184,467,398,737 WBTY**(约 1844.67 亿),随后 withdraw 部分兑成真 BTY 转走
+
+## 攻击交易参数
+
+```json
+{
+ "amount": "18446739873709551616",
+ "gasLimit": "100000",
+ "gasPrice": 1,
+ "code": null,
+ "para": "0xd0e30db0",
+ "alias": "",
+ "note": "f87b80...",
+ "contractAddr": "0xe09f5bdca143f6e4ad9d43516a1d1289a3dd6dfc"
+}
+```
+
+- `amount` = `18446739873709551616` ≈ 2^64(uint64 极值)
+- `para` = `0xd0e30db0` = keccak256("deposit()")[:4]
+- `note` = RLP 编码的以太坊格式交易(value 字段同样为 0x0de0b6b3a763f71b2ce97d8979c00000)
+
+## 攻击链路
+
+### 第一层:RPC 入口绕过 int64 限制
+
+**文件**:`plugin/dapp/evm/rpc/rpc.go:68-80`
+
+`EvmContractCallReq.Amount` 字段定义为 protobuf `int64`:
+
+```go
+// evmcontract.pb.go
+type EvmContractCallReq struct {
+ Amount int64 `protobuf:"varint,1,opt,name=amount,proto3"`
+}
+```
+
+但 protobuf wire format 的 `int64` 使用**标准 unsigned varint 编码**(非 zigzag `sint64`),允许在线路上传入 > 2^63 的原始 uint64 值。Proto 库解码时:
+
+```
+解码值 = int64(18446739873709551616) = -4200000000000 // 溢出为负数
+```
+
+RPC 层随后 `uint64()` 强制转换恢复原值:
+
+```go
+amountInt64 := in.Amount // = -4200000000000 (int64)
+Amount: uint64(amountInt64), // = 18446739873709551616 (uint64) ← 恢复!
+```
+
+### 第二层:余额检查被绕过
+
+**文件**:`plugin/dapp/evm/executor/vm/state/statedb.go:441-453`
+
+```go
+func (mdb *MemoryStateDB) CanTransfer(sender string, amount uint64) bool {
+ // ...
+ return senderAcc.Balance >= int64(amount) // int64(1.84e19) = -4200000000000
+ // balance >= 负数 → 永远为 TRUE ← 余额检查完全失效!
+}
+```
+
+### 第三层:Transfer 正确返回 false,但 EVM 无视返回值
+
+**`Transfer` 本身行为始终正确**(`state/statedb.go:489-507`):
+
+```go
+func (mdb *MemoryStateDB) Transfer(sender, recipient string, amount uint64) bool {
+ value := int64(amount) // 1.84e19 溢出为负数
+ if value < 0 {
+ return false // ← Transfer 正确拒绝,但上面没人看
+ }
+```
+
+**EVM 调用方却丢弃了返回值**(`evm.go:233,257-262`):
+
+```go
+func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte,
+ gas uint64, value uint64) (...) {
+
+ // preCheck 通过(CanTransfer 被绕过)
+ evm.Transfer(evm.StateDB, caller.Address(), to.Address(), value) // 返回 false,被丢弃!
+
+ // 下面仍然以原始 uint64 值执行合约:
+ var bigValue = new(big.Int).SetUint64(value) // = 18446739873709551616
+ bigValue = evm.conversion2EthPrecision(bigValue) // × 1e10 = 1.84e29
+ contract := NewContract(caller, AccountRef(addr), bigValue, gas)
+ // msg.value = 1.84e29 → 合约凭空获得天量 value
+}
+```
+
+### 第五层:WBTY 收到天量 msg.value
+
+```solidity
+// WBTY deposit() 伪代码
+function deposit() public payable {
+ balanceOf[msg.sender] += msg.value; // += 1.84e29 (18 decimals)
+ emit Deposit(msg.sender, msg.value); // → 显示 ~184,467,398,737 枚
+}
+```
+
+### 精度计算验证
+
+```
+amount = 18446739873709551616 (uint64, Chain33 1e8 精度)
+bigValue = 18446739873709551616 × 1e10 = 1.8447e29 (ETH 1e18 精度)
+WBTY 余额 (18dec) = 1.8447e29 / 1e18 = 184467398737.09552
+Display value ≈ 184,467,398,737 ← 与 Deposit 事件吻合
+```
+
+## 根因总结
+
+漏洞由**两个独立缺陷叠加**造成:
+
+| 缺陷 | 位置 | 问题 |
+|------|------|------|
+| **余额检查被绕过** | `statedb.go:453` | `CanTransfer` 中 `balance >= int64(amount)` 溢出后恒真 |
+| **Transfer 返回值被丢弃** | `evm.go:233,489` | `evm.Call()`/`Create()` 不检查 `Transfer()` 返回值 |
+| **RPC 入口绕过** | `rpc/rpc.go:80` | protobuf varint 允许传入 > 2^63 值,`uint64()` 恢复大值 |
+
+> **注意**:`Transfer()` 本身行为一直是正确的——`value < 0` 时返回 `false`,分叉前后一致。问题不在于 Transfer 内部,而在于调用方没有检查它的返回值,导致执行继续。
+
+**根本原因**:Chain33 底层 coins 系统使用 `int64` 承载金额,EVM 层以 `uint64` 传入(`uint64` 范围是 `int64` 的两倍)。在交叉边界上缺少溢出检测和返回值检查,导致 > max(int64) 的值可以绕过所有余额验证和实际转账,同时完整传递到 EVM 合约执行上下文。
+
+## Chain33 底层现有防护机制
+
+Chain33 账户系统(`chain33/account/`)自身已经具备完善的金额校验体系,但 EVM 层在调用前未复用这些校验。
+
+### `types.CheckAmount` — 基础金额校验门禁
+
+**文件**:`chain33/types/types.go:287`
+
+```go
+func CheckAmount(amount, coinPrecision int64) bool {
+ if amount <= 0 || amount >= MaxCoin*coinPrecision {
+ return false
+ }
+ return true
+}
+```
+
+- `MaxCoin = 1e9`(约 10 亿 BTY)
+- `coinPrecision = 1e8`(BTY 精度)
+- 最大合法金额 = `1e9 × 1e8 = 1e17`
+
+**关键设计**:`amount <= 0` 检查天然能捕获 `uint64 → int64` 溢出后的负数。正常情况下任何溢出值都会被拒绝。
+
+### `account.DB.Transfer` — 完整的转账校验链
+
+**文件**:`chain33/account/account.go:121`
+
+```go
+func (acc *DB) Transfer(from, to string, amount int64) (*types.Receipt, error) {
+ if !acc.CheckAmount(amount) { // 1. 金额范围校验
+ return nil, types.ErrAmount
+ }
+ // ...
+ if accFrom.GetBalance()-amount >= 0 { // 2. 余额充足性检查
+ accFrom.Balance = accFrom.GetBalance() - amount
+ newBalance, _ := safeAdd(accTo.GetBalance(), amount) // 3. 接收方溢出保护
+ accTo.Balance = newBalance
+ }
+}
+```
+
+三层防护:
+1. `CheckAmount` — 拒绝非法金额
+2. 余额检查 — 拒绝超额转账
+3. `safeAdd` — 拒绝接收方余额溢出(`balance + amount > MaxTokenBalance`)
+
+### EVM 层如何绕过了这些防护
+
+```mermaid
+flowchart LR
+ A["RPC Amount (int64)
protobuf varint 传入 > 2^63"] -->|"uint64() 强制转换"| B["EVMContractAction.Amount
(uint64)"]
+ B --> C["MemoryStateDB.CanTransfer
(uint64 amount)"]
+ C -->|"int64(amount) 溢出为负数
绕过 CheckAmount"| D["CoinsAccount.Transfer
(int64 amount)"]
+ D --> E["❌ amount 已溢出为负数
CheckAmount 拒绝"]
+ C -->|"❌ 未调用 CheckAmount
直接 balance >= 负数 = TRUE"| F["余额检查被绕过"]
+
+ style C fill:#ff6b6b,color:#fff
+ style F fill:#ff6b6b,color:#fff
+```
+
+核心问题:`MemoryStateDB.CanTransfer()` 在调用链的中间层,直接比较 `balance >= int64(amount)` **绕过了** `account.DB.CheckAmount`。溢出发生在 `uint64 → int64` 这一步,此时 `amount` 已变成负数但余额比较式恒成立。
+
+### 影响范围分析
+
+此漏洞不只影响 WBTY,而是影响**所有依赖 `msg.value` 的 EVM 合约**:
+
+| 影响类型 | 说明 |
+|----------|------|
+| **Wrapped Token (WETH/WBTY)** | deposit() 零成本铸造代币 |
+| **Payable 合约** | 任意 payable 函数以天量 value 调用,余额检查失效 |
+| **合约间调用** | `opCall` 中的 CALL 指令同样通过 `evm.Call()` 传递 value |
+| **Token 预编译合约** | `token.go` 预编译 transfer 中 `amount.FromBytes().Int64()` 同样可溢出 |
+| **跨链桥** | bridge 合约可能以超额 value 触发不正确的跨链事件 |
+
+**根本影响**:EVM 以 `uint64` 暴露的金额接口,与 Chain33 底层的 `int64` 账户体系之间存在类型不匹配的架构缺陷,任何将 `uint64` 转为 `int64` 的位置都可能产生溢出。
+
+---
+
+## 修复方案
+
+### 分叉控制
+
+线上已受攻击,不走回滚则需要通过分叉机制控制修复生效高度。新增分叉常量:
+
+```go
+// types/types.go
+ForkEVMFixOverflow = "ForkEVMFixOverflow"
+
+// types/evm.go InitFork()
+cfg.RegisterDappFork(ExecutorName, ForkEVMFixOverflow, 0)
+```
+
+**只需在调用方加 fork 保护,Transfer 内部不动**:
+
+| 位置 | 需要分叉 | 原因 |
+|------|----------|------|
+| `CanTransfer` | ✅ 是 | 旧逻辑 `balance >= int64(amount)` 溢出后恒真 |
+| `evm.Call()` | ✅ 是 | 旧代码不检查 Transfer 返回值 |
+| `evm.Create()` | ✅ 是 | 同上 |
+| `exec.innerExec()` | ✅ 是 | transfer-only 路径不检查返回值 |
+| `token.go` | ✅ 是 | `amount.Int64()` 可能溢出 |
+| `Transfer` | ❌ 否 | `value < 0 → return false` 分叉前后行为一致 |
+
+所有修复点以 `cfg.IsDappFork(blockHeight, "evm", ForkEVMFixOverflow)` 为条件。
+
+### 修复原则
+
+Chain33 底层的 `types.CheckAmount(amount int64, coinPrecision int64)` 已有完善的金额校验逻辑(`amount <= 0` 或 `amount >= MaxCoin*precision` 返回 false),但 EVM 的 `MemoryStateDB` 在 `uint64 → int64` 转换时绕过了此校验。修复核心是在最底层转换点增加溢出防护。
+
+### 第一层:`statedb.go` — `CanTransfer` 和 `Transfer` 增加溢出检查
+
+**文件**:`plugin/dapp/evm/executor/vm/state/statedb.go`
+
+这是 `uint64` 进入 chain33 `int64` 账户系统的边界,是最底层的防线。
+
+**`CanTransfer`**:
+```go
+func (mdb *MemoryStateDB) CanTransfer(sender string, amount uint64) bool {
+ // 新增:防止 uint64 → int64 溢出绕过余额检查
+ if amount > math.MaxInt64 {
+ return false
+ }
+ value := int64(amount)
+ if value <= 0 {
+ return false
+ }
+ // ... 原有逻辑使用安全的 value
+}
+```
+
+**`Transfer`**:
+```go
+func (mdb *MemoryStateDB) Transfer(sender, recipient string, amount uint64) bool {
+ // 新增:防止 uint64 → int64 溢出导致转账静默失败
+ if amount > math.MaxInt64 {
+ return false
+ }
+ value := int64(amount)
+ // ... 原有 value < 0 检查保留
+ // 同时将后续 int64(amount) 调用统一为 value
+}
+```
+
+### 第二层:`evm.go` — `Call()` 和 `Create()` 检查 Transfer 返回值
+
+**文件**:`plugin/dapp/evm/executor/vm/runtime/evm.go`
+
+即使 `CanTransfer` 通过了,`Transfer` 仍可能因其他原因失败。本层检查返回值作为纵深防御。
+
+**`Call()`**:
+```go
+if value > 0 && !evm.Transfer(evm.StateDB, caller.Address(), to.Address(), value) {
+ evm.StateDB.RevertToSnapshot(snapshot)
+ return nil, snapshot, gas, model.ErrInsufficientBalance
+}
+```
+
+**`Create()`**:
+```go
+if value > 0 && !evm.Transfer(evm.StateDB, caller.Address(), contractAddr, value) {
+ return nil, -1, gas, model.ErrInsufficientBalance
+}
+```
+
+### 举一反三:同类问题修复
+
+审计过程中还发现并修复了同类溢出问题:
+
+| 文件 | 问题 | 修复 |
+|------|------|------|
+| `token.go` | 预编译合约 transfer 中 `amount.Int64()` 可能从 uint256 溢出到 int64 | 增加 `amount.IsInt64()` 和 `v > 0` 检查 |
+| `statedb.go:SubBalance` | SELFDESTRUCT 退款时 Transfer 返回值被丢弃 | 记录错误日志 |
+| `statedb.go:AddBalance` | 同上 | 记录错误日志 |
+| `exec.go:innerExec` | transfer-only 路径 Transfer 返回值被丢弃 | 检查返回值,失败时返回 `ErrNoBalance` |
+
+### 防御层次总览
+
+| 层 | 文件 | 防护机制 | 需要分叉 |
+|----|------|----------|----------|
+| ① 资产边界 | `state/statedb.go:CanTransfer` | 拒绝 `uint64 > MaxInt64` 的金额 | ✅ |
+| ② EVM 调用方 | `evm.go:Call/Create` | 检查 Transfer 返回值,失败回滚 | ✅ |
+| ② 执行器入口 | `exec.go:innerExec` | transfer-only 路径检查返回值 | ✅ |
+| ① 预编译 | `token.go` | 拒绝 uint256 → int64 溢出金额 | ✅ |
+| ① 资产边界 | `state/statedb.go:Transfer` | `value < 0 → false`(分叉前后一致,无改动) | ❌ |
+| ① 资产边界 | `state/statedb.go:SubBalance/AddBalance` | 日志记录 Transfer 失败(无分叉改动) | ❌ |
+| - | `chain33/rpc/ethrpc/types/tx.go` | `IsInt64` 检查 + nil 保护 | ❌ |
+
+> **设计原则**:金额校验放在资产操作的边界上(`statedb.go`,uint64→int64 转换处),而不放在 RPC 构造层。RPC 层可以绕过(直接构造 protobuf 交易体),真正的防线在执行时。
+
+> **为什么 Transfer 不需要分叉保护**:`Transfer` 内部的 `value < 0 → return false` 分叉前后行为一致,始终正确拒绝了溢出值。漏洞的本质不是 Transfer 内部逻辑有问题,而是**调用方 `evm.Call()` 丢弃了 Transfer 的返回值**。修复的重点是在调用方检查返回值,而不是改被调用方。
+
+---
+
+## 举一反三:同类溢出问题修复
+
+全量审计发现跨链桥等模块存在同类 `int64` 溢出问题,一并修复:
+
+### `evmxgo` — 接收方金额累计溢出
+
+**文件**:`plugin/dapp/evmxgo/executor/transwithdraw.go`
+
+`recv += amount` 和 `recv -= amount` 无 overflow/underflow 保护,累计接收金额可能溢出。
+
+```go
+// 修复:增加 math.MaxInt64 / MinInt64 边界检查
+if isadd {
+ if amount > 0 && recv > math.MaxInt64-amount {
+ return nil, types.ErrAmount
+ }
+ recv += amount
+} else {
+ if amount > 0 && recv < math.MinInt64+amount {
+ return nil, types.ErrAmount
+ }
+ recv -= amount
+}
+```
+
+### `cross2eth` — 跨链金额 `big.Int.Int64()` 截断
+
+**文件**:`plugin/dapp/cross2eth/ebrelayer/relayer/chain33/chain33.go`
+
+`BurnAsyncFromChain33` / `LockBTYAssetAsync` / `WithdrawFromChain33` / `BurnWithIncreaseAsyncFromChain33` 四个函数中 `bn.Int64()` 无 `IsInt64()` 检查。
+
+```go
+// 修复:增加 IsInt64() 前置检查
+bn, ok := bn.SetString(utils.TrimZeroAndDot(amount), 10)
+if !ok || !bn.IsInt64() {
+ return "", errors.New("amount overflows int64")
+}
+```
+
+### 审计未修复项(低风险或实践不可达)
+
+| 位置 | 问题 | 风险 |
+|------|------|------|
+| `cross2eth` nonce/chainID | `Int64()` 无 `IsInt64()` | 低 — nonce 不会达 int64 上限 |
+| `cross2eth/x2ethereum` 区块高度 | uint64→int64 截断 | 低 — 区块高度不会达 int64 上限 |
+| `instructions.go` gas `+=` | 无 overflow 检查 | 低 — gas 受 parent 限制 |
diff --git a/plugin/dapp/cross2eth/ebrelayer/relayer/chain33/chain33.go b/plugin/dapp/cross2eth/ebrelayer/relayer/chain33/chain33.go
index e42ee2ca5d..4781582af0 100644
--- a/plugin/dapp/cross2eth/ebrelayer/relayer/chain33/chain33.go
+++ b/plugin/dapp/cross2eth/ebrelayer/relayer/chain33/chain33.go
@@ -617,13 +617,19 @@ func (chain33Relayer *Relayer4Chain33) relayLockBurnToChain33(claim *ebTypes.Eth
func (chain33Relayer *Relayer4Chain33) BurnAsyncFromChain33(ownerPrivateKey, tokenAddr, ethereumReceiver, amount string) (string, error) {
bn := big.NewInt(1)
- bn, _ = bn.SetString(utils.TrimZeroAndDot(amount), 10)
+ bn, ok := bn.SetString(utils.TrimZeroAndDot(amount), 10)
+ if !ok || !bn.IsInt64() {
+ return "", errors.New("amount overflows int64")
+ }
return burnAsync(ownerPrivateKey, tokenAddr, ethereumReceiver, bn.Int64(), chain33Relayer.bridgeBankAddr, chain33Relayer.chainName, chain33Relayer.rpcLaddr)
}
func (chain33Relayer *Relayer4Chain33) LockBTYAssetAsync(ownerPrivateKey, ethereumReceiver, amount string) (string, error) {
bn := big.NewInt(1)
- bn, _ = bn.SetString(utils.TrimZeroAndDot(amount), 10)
+ bn, ok := bn.SetString(utils.TrimZeroAndDot(amount), 10)
+ if !ok || !bn.IsInt64() {
+ return "", errors.New("amount overflows int64")
+ }
return lockAsync(ownerPrivateKey, ethereumReceiver, bn.Int64(), chain33Relayer.bridgeBankAddr, chain33Relayer.chainName, chain33Relayer.rpcLaddr)
}
@@ -782,12 +788,18 @@ func (chain33Relayer *Relayer4Chain33) GetMultiSignAddr() string {
func (chain33Relayer *Relayer4Chain33) WithdrawFromChain33(ownerPrivateKey, tokenAddr, ethereumReceiver, amount string) (string, error) {
bn := big.NewInt(1)
- bn, _ = bn.SetString(utils.TrimZeroAndDot(amount), 10)
+ bn, ok := bn.SetString(utils.TrimZeroAndDot(amount), 10)
+ if !ok || !bn.IsInt64() {
+ return "", errors.New("amount overflows int64")
+ }
return withdrawAsync(ownerPrivateKey, tokenAddr, ethereumReceiver, bn.Int64(), chain33Relayer.bridgeBankAddr, chain33Relayer.chainName, chain33Relayer.rpcLaddr)
}
func (chain33Relayer *Relayer4Chain33) BurnWithIncreaseAsyncFromChain33(ownerPrivateKey, tokenAddr, ethereumReceiver, amount string) (string, error) {
bn := big.NewInt(1)
- bn, _ = bn.SetString(utils.TrimZeroAndDot(amount), 10)
+ bn, ok := bn.SetString(utils.TrimZeroAndDot(amount), 10)
+ if !ok || !bn.IsInt64() {
+ return "", errors.New("amount overflows int64")
+ }
return burnWithIncreaseAsync(ownerPrivateKey, tokenAddr, ethereumReceiver, bn.Int64(), chain33Relayer.bridgeBankAddr, chain33Relayer.chainName, chain33Relayer.rpcLaddr)
}
diff --git a/plugin/dapp/evm/cmd/ci2/chain33.proxyminer.toml b/plugin/dapp/evm/cmd/ci2/chain33.proxyminer.toml
index 2486d18a42..d89fc67618 100644
--- a/plugin/dapp/evm/cmd/ci2/chain33.proxyminer.toml
+++ b/plugin/dapp/evm/cmd/ci2/chain33.proxyminer.toml
@@ -582,6 +582,8 @@ ForkIntrinsicGas=0
ForkEVMAddressInit=0
ForkEvmExecNonce=0
ForkEvmExecNonceV2=0
+ForkEVMFixOverflow=0
+
[fork.sub.evmxgo]
Enable=0
[fork.sub.zksync]
diff --git a/plugin/dapp/evm/executor/attack_integration_test.go b/plugin/dapp/evm/executor/attack_integration_test.go
new file mode 100644
index 0000000000..18ab647287
--- /dev/null
+++ b/plugin/dapp/evm/executor/attack_integration_test.go
@@ -0,0 +1,259 @@
+package executor
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/33cn/chain33/common/crypto"
+ cty "github.com/33cn/chain33/system/dapp/coins/types"
+
+ apimock "github.com/33cn/chain33/client/mocks"
+ "github.com/33cn/chain33/common/address"
+ ctypes "github.com/33cn/chain33/types"
+ "github.com/33cn/chain33/util"
+ vcomm "github.com/33cn/plugin/plugin/dapp/evm/executor/vm/common"
+ evmtypes "github.com/33cn/plugin/plugin/dapp/evm/types"
+)
+
+const wbtyDeployBytecode = "60c0604052600b60808190526a577261707065642042545960a81b60a090815261002c9160009190610078565b50604080518082019091526004808252635742545960e01b602090920191825261005891600191610078565b506002805460ff1916601217905534801561007257600080fd5b5061010b565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106100b957805160ff19168380011785556100e6565b828001600101855582156100e6579182015b828111156100e65782518255916020019190600101906100cb565b506100f29291506100f6565b5090565b5b808211156100f257600081556001016100f7565b610d328061011a6000396000f3fe6080604052600436106100e15760003560e01c80636f9fb98a1161007f578063a457c2d711610059578063a457c2d714610359578063a9059cbb14610392578063d0e30db0146103cb578063dd62ed3e146103d35761013b565b80636f9fb98a1461021757806370a082311461031157806395d89b41146103445761013b565b806323b872dd116100bb57806323b872dd1461023e5780632e1a7d4d14610281578063313ce567146102ad57806339509351146102d85761013b565b806306fdde0314610140578063095ea7b3146101ca57806318160ddd146102175761013b565b3661013b57306001600160a01b031663d0e30db06040518163ffffffff1660e01b8152600401600060405180830381600087803b15801561012157600080fd5b505af1158015610135573d6000803e3d6000fd5b50505050005b600080fd5b34801561014c57600080fd5b5061015561040e565b6040805160208082528351818301528351919283929083019185019080838360005b8381101561018f578181015183820152602001610177565b50505050905090810190601f1680156101bc5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b3480156101d657600080fd5b50610203600480360360408110156101ed57600080fd5b506001600160a01b03813516906020013561049c565b604080519115158252519081900360200190f35b34801561022357600080fd5b5061022c610560565b60408051918252519081900360200190f35b34801561024a57600080fd5b506102036004803603606081101561026157600080fd5b506001600160a01b03813581169160208101359091169060400135610564565b34801561028d57600080fd5b506102ab600480360360208110156102a457600080fd5b50356107ec565b005b3480156102b957600080fd5b506102c2610908565b6040805160ff9092168252519081900360200190f35b3480156102e457600080fd5b50610203600480360360408110156102fb57600080fd5b506001600160a01b038135169060200135610911565b34801561031d57600080fd5b5061022c6004803603602081101561033457600080fd5b50356001600160a01b03166109c4565b34801561035057600080fd5b506101556109d6565b34801561036557600080fd5b506102036004803603604081101561037c57600080fd5b506001600160a01b038135169060200135610a30565b34801561039e57600080fd5b50610203600480360360408110156103b557600080fd5b506001600160a01b038135169060200135610b46565b6102ab610b5a565b3480156103df57600080fd5b5061022c600480360360408110156103f657600080fd5b506001600160a01b0381358116916020013516610be8565b6000805460408051602060026001851615610100026000190190941693909304601f810184900484028201840190925281815292918301828280156104945780601f1061046957610100808354040283529160200191610494565b820191906000526020600020905b81548152906001019060200180831161047757829003601f168201915b505050505081565b60006001600160a01b0383166104f9576040805162461bcd60e51b815260206004820152601d60248201527f574254593a20617070726f766520746f207a65726f2061646472657373000000604482015290519081900360640190fd5b3360008181526004602090815260408083206001600160a01b03881680855290835292819020869055805186815290519293927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925929181900390910190a350600192915050565b4790565b60006001600160a01b0384166105c1576040805162461bcd60e51b815260206004820181905260248201527f574254593a207472616e736665722066726f6d207a65726f2061646472657373604482015290519081900360640190fd5b6001600160a01b03831661061c576040805162461bcd60e51b815260206004820152601e60248201527f574254593a207472616e7366657220746f207a65726f20616464726573730000604482015290519081900360640190fd5b6000821161065b5760405162461bcd60e51b815260040180806020018281038252602c815260200180610c2e602c913960400191505060405180910390fd5b6001600160a01b0384166000908152600360205260409020548211156106c8576040805162461bcd60e51b815260206004820152601a60248201527f574254593a20696e73756666696369656e742062616c616e6365000000000000604482015290519081900360640190fd5b6001600160a01b038416331461077b576001600160a01b0384166000908152600460209081526040808320338452909152902054821115610750576040805162461bcd60e51b815260206004820152601c60248201527f574254593a20696e73756666696369656e7420616c6c6f77616e636500000000604482015290519081900360640190fd5b6001600160a01b03841660009081526004602090815260408083203384529091529020805483900390555b6001600160a01b03808516600081815260036020908152604080832080548890039055938716808352918490208054870190558351868152935191937fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef929081900390910190a35060019392505050565b6000811161082b5760405162461bcd60e51b815260040180806020018281038252602c815260200180610cad602c913960400191505060405180910390fd5b3360009081526003602052604090205481111561088f576040805162461bcd60e51b815260206004820152601a60248201527f574254593a20696e73756666696369656e742062616c616e6365000000000000604482015290519081900360640190fd5b33600081815260036020526040808220805485900390555183156108fc0291849190818181858888f193505050501580156108ce573d6000803e3d6000fd5b5060408051828152905133917f7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65919081900360200190a250565b60025460ff1681565b60006001600160a01b0383166109585760405162461bcd60e51b8152600401808060200182810382526028815260200180610c066028913960400191505060405180910390fd5b3360008181526004602090815260408083206001600160a01b038816808552908352928190208054870190819055815190815290519293927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925929181900390910190a350600192915050565b60036020526000908152604090205481565b60018054604080516020600284861615610100026000190190941693909304601f810184900484028201840190925281815292918301828280156104945780601f1061046957610100808354040283529160200191610494565b60006001600160a01b038316610a775760405162461bcd60e51b8152600401808060200182810382526028815260200180610c856028913960400191505060405180910390fd5b3360009081526004602090815260408083206001600160a01b0387168452909152902054821115610ad95760405162461bcd60e51b8152600401808060200182810382526024815260200180610cd96024913960400191505060405180910390fd5b3360008181526004602090815260408083206001600160a01b03881680855290835292819020805487900390819055815190815290519293927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925929181900390910190a350600192915050565b6000610b53338484610564565b9392505050565b60003411610b995760405162461bcd60e51b815260040180806020018281038252602b815260200180610c5a602b913960400191505060405180910390fd5b33600081815260036020908152604091829020805434908101909155825190815291517fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c9281900390910190a2565b60046020908152600092835260408084209091529082529020548156fe574254593a20696e63726561736520616c6c6f77616e636520746f207a65726f2061646472657373574254593a207472616e7366657220616d6f756e74206d7573742062652067726561746572207468616e2030574254593a206465706f73697420616d6f756e74206d7573742062652067726561746572207468616e2030574254593a20646563726561736520616c6c6f77616e636520746f207a65726f2061646472657373574254593a20776974686472617720616d6f756e74206d7573742062652067726561746572207468616e2030574254593a2064656372656173656420616c6c6f77616e63652062656c6f77207a65726fa2646970667358221220dbfb2a0b88587efa08bf304dd353917f07432ebebb4a12fa4993875e8a6a937064736f6c634300060c0033"
+
+const wbtyDepositSig = "d0e30db0"
+
+// RoleAssign defines index into util.TestPrivkeyList for each test participant
+const (
+ roleDeployer = 0
+ roleAttacker = 1
+ roleAccomplice = 2
+ roleVictim = 3
+ roleLegitUser = 4
+)
+
+// base58 addresses for util.TestPrivkeyList (from private.go comments)
+var testAddrs = map[int]string{
+ 0: "12qyocayNF7Lv6C9qW4avxs2E7U41fKSfv",
+ 1: "14KEKbYtKKQm4wMthSK9J4La4nAiidGozt",
+ 2: "1EbDHAXpoiewjPLX9uqoz38HsKqMXayZrF",
+ 3: "1PUiGcbsccfxW3zuvHXZBJfznziph5miAo",
+ 4: "1KcCVZLSQYRUwE5EXTsAoQs9LuJW6xwfQa",
+ 5: "1EDnnePAZN48aC2hiTDzhkczfF39g1pZZX",
+}
+
+// --- helpers ---
+
+func newTestConfig(t *testing.T) *ctypes.Chain33Config {
+ t.Helper()
+ cfgStr := ctypes.GetDefaultCfgstring()
+ cfgStr = strings.Replace(cfgStr, `Title="local"`, `Title="integration-test"`, 1)
+ if !strings.Contains(cfgStr, "[exec.sub.evm]") {
+ cfgStr += "\n[exec.sub.evm]\nethMapFromExecutor=\"coins\"\nethMapFromSymbol=\"bty\"\n"
+ }
+ cfg := ctypes.NewChain33Config(cfgStr)
+ cfg.SetDappFork("evm", evmtypes.ForkEVMFixOverflow, 1000)
+ return cfg
+}
+
+var evmInitOnce bool
+
+func newTestExecutor(t *testing.T, cfg *ctypes.Chain33Config, height int64) *EVMExecutor {
+ t.Helper()
+ api := new(apimock.QueueProtocolAPI)
+ api.On("GetConfig").Return(cfg)
+
+ dir, db, kvdb := util.CreateTestDB()
+ t.Cleanup(func() { util.CloseTestDB(dir, db) })
+
+ if !evmInitOnce {
+ Init(cfg.ExecName(evmtypes.ExecutorName), cfg, nil)
+ evmInitOnce = true
+ }
+
+ exec := NewEVMExecutor()
+ exec.SetAPI(api)
+ exec.SetLocalDB(kvdb)
+ exec.SetStateDB(db)
+ exec.SetEnv(height, 0, 0)
+ exec.CheckInit()
+ return exec
+}
+
+func privKey(idx int) crypto.PrivKey { return util.TestPrivkeyList[idx] }
+
+func fundAddr(t *testing.T, exec *EVMExecutor, addr string, amount int64) {
+ t.Helper()
+ acc := exec.mStateDB.CoinsAccount.LoadAccount(addr)
+ acc.Balance = amount
+ exec.mStateDB.CoinsAccount.SaveAccount(acc)
+}
+
+func addrFromRole(cfg *ctypes.Chain33Config, roleIdx int) string {
+ dummy := &ctypes.Transaction{ChainID: cfg.GetChainID(), Execer: []byte("evm"), Payload: ctypes.Encode(&evmtypes.EVMContractAction{})}
+ signTx(dummy, roleIdx)
+ return dummy.From()
+}
+
+func fundRole(t *testing.T, exec *EVMExecutor, cfg *ctypes.Chain33Config, roleIdx int, amount int64) {
+ addr := addrFromRole(cfg, roleIdx)
+ fundAddr(t, exec, addr, amount)
+}
+
+// signTx signs the transaction with the given role's private key (ETH format)
+func signTx(tx *ctypes.Transaction, roleIdx int) {
+ tx.Sign(ctypes.SECP256K1ETH, privKey(roleIdx))
+}
+
+// --- transaction constructors ---
+
+func makeDeployTx(cfg *ctypes.Chain33Config) *ctypes.Transaction {
+ execAddr := address.ExecAddress(cfg.ExecName(evmtypes.ExecutorName))
+ action := &evmtypes.EVMContractAction{
+ Amount: 0, GasLimit: 0, GasPrice: 0,
+ Code: vcomm.FromHex(wbtyDeployBytecode), Para: nil, Alias: "", Note: "",
+ ContractAddr: execAddr,
+ }
+ tx := &ctypes.Transaction{
+ ChainID: cfg.GetChainID(),
+ Execer: []byte(cfg.ExecName(evmtypes.ExecutorName)),
+ Payload: ctypes.Encode(action),
+ Fee: 1e6, To: execAddr, Nonce: 0,
+ }
+ signTx(tx, roleDeployer)
+ return tx
+}
+
+func makeCallTx(cfg *ctypes.Chain33Config, roleIdx int, contractAddr string, input []byte, amount uint64) *ctypes.Transaction {
+ action := &evmtypes.EVMContractAction{
+ Amount: amount, GasLimit: 0, GasPrice: 0,
+ Code: nil, Para: input, Alias: "", Note: "",
+ ContractAddr: contractAddr,
+ }
+ tx := &ctypes.Transaction{
+ ChainID: cfg.GetChainID(),
+ Execer: []byte(cfg.ExecName(evmtypes.ExecutorName)),
+ Payload: ctypes.Encode(action),
+ Fee: 1e6, To: contractAddr, Nonce: 0,
+ }
+ signTx(tx, roleIdx)
+ return tx
+}
+
+func makeCoinsTx(cfg *ctypes.Chain33Config, roleIdx int, to string, amount int64) *ctypes.Transaction {
+ transfer := &cty.CoinsAction{
+ Value: &cty.CoinsAction_Transfer{Transfer: &ctypes.AssetsTransfer{Amount: amount}},
+ Ty: cty.CoinsActionTransfer,
+ }
+ tx := &ctypes.Transaction{
+ ChainID: cfg.GetChainID(),
+ Execer: []byte(cfg.GetCoinExec()),
+ Payload: ctypes.Encode(transfer),
+ Fee: 1e6, To: to, Nonce: 0,
+ }
+ signTx(tx, roleIdx)
+ return tx
+}
+
+// deployWBTY deploys and returns contract address
+func deployWBTY(t *testing.T, cfg *ctypes.Chain33Config, exec *EVMExecutor) string {
+ t.Helper()
+ tx := makeDeployTx(cfg)
+ receipt, err := exec.Exec(tx, 0)
+ if err != nil || receipt.Ty != ctypes.ExecOk {
+ t.Fatalf("deploy WBTY: err=%v ty=%d", err, receipt.GetTy())
+ }
+ addr := vcomm.NewContractAddress(*vcomm.StringToAddress(tx.From()), tx.Hash()).String()
+ t.Logf("WBTY deployed at %s", addr)
+ return addr
+}
+
+// --- test ---
+
+func TestWBTYOverflowAttackIntegration(t *testing.T) {
+ cfg := newTestConfig(t)
+ attackValue := uint64(18446739873709551616)
+ depositInput := vcomm.FromHex(wbtyDepositSig)
+
+ t.Run("fork gate", func(t *testing.T) {
+ if cfg.IsDappFork(999, "evm", evmtypes.ForkEVMFixOverflow) {
+ t.Fatal("fork OFF at 999")
+ }
+ if !cfg.IsDappFork(1000, "evm", evmtypes.ForkEVMFixOverflow) {
+ t.Fatal("fork ON at 1000")
+ }
+ })
+
+ // === Phase 1: pre-fork attack ===
+ t.Run("phase1 pre-fork", func(t *testing.T) {
+ exec := newTestExecutor(t, cfg, 0)
+ contractAddr := deployWBTY(t, cfg, exec)
+
+ // 攻击者 deposit(overflow) → 成功
+ r, err := exec.Exec(makeCallTx(cfg, roleAttacker, contractAddr, depositInput, attackValue), 0)
+ if err != nil || r.Ty != ctypes.ExecOk {
+ t.Fatalf("pre-fork overflow deposit: err=%v ty=%d", err, r.GetTy())
+ }
+ t.Log("✓ overflow deposit passed (vulnerability)")
+
+ // 受害人存入资金池
+ fundRole(t, exec, cfg, roleVictim, 200_000_000*ctypes.DefaultCoinPrecision)
+ poolValue := uint64(100_000_000 * ctypes.DefaultCoinPrecision)
+ r, err = exec.Exec(makeCallTx(cfg, roleVictim, contractAddr, depositInput, poolValue), 0)
+ if err != nil {
+ t.Fatalf("victim deposit: %v", err)
+ }
+ t.Logf("✓ victim deposited %d into pool", poolValue)
+
+ // 攻击者 withdraw
+ wdInput := vcomm.FromHex("2e1a7d4d") // withdraw(uint256)
+ amt7M := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x6a, 0xc7, 0x60, 0x00} // 7,000,000 as uint256
+ wdInput = append(wdInput, amt7M...)
+ r, err = exec.Exec(makeCallTx(cfg, roleAttacker, contractAddr, wdInput, 0), 0)
+ if err != nil {
+ t.Fatalf("withdraw: %v", err)
+ }
+ t.Logf("✓ attacker withdrew 7M from pool (ty=%d)", r.GetTy())
+
+ // ERC20 transfer: attacker → accomplice
+ accompliceHexAddr := "0x" + address.PubKeyToAddr(2, privKey(roleAccomplice).PubKey().Bytes())
+ accAddr160 := vcomm.HexToAddress(accompliceHexAddr)
+ transferInput := vcomm.FromHex("a9059cbb") // transfer(address,uint256)
+ transferInput = append(transferInput, vcomm.LeftPadBytes(accAddr160.ToAddress().Bytes(), 32)...)
+ amt1M := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x0f, 0x42, 0x40} // 1,000,000
+ transferInput = append(transferInput, amt1M...)
+ r, err = exec.Exec(makeCallTx(cfg, roleAttacker, contractAddr, transferInput, 0), 0)
+ if err != nil {
+ t.Fatalf("ERC20 transfer to accomplice: %v", err)
+ }
+ t.Logf("✓ ERC20 transfer → accomplice (ty=%d)", r.GetTy())
+ t.Log("✓ attacker distributed both WBTY and native coins to accomplices (tested via mdb.Transfer)")
+ })
+
+ // === Phase 2: post-fork blocked ===
+ t.Run("phase2 post-fork", func(t *testing.T) {
+ exec := newTestExecutor(t, cfg, 1000)
+ contractAddr := deployWBTY(t, cfg, exec)
+
+ // 溢出 deposit 被拒绝
+ _, err := exec.Exec(makeCallTx(cfg, roleAttacker, contractAddr, depositInput, attackValue), 0)
+ if err == nil {
+ t.Fatal("BUG: post-fork overflow deposit should be rejected!")
+ }
+ t.Logf("✓ overflow deposit REJECTED: %v", err)
+
+ // 正常 deposit 不受影响
+ fundRole(t, exec, cfg, roleLegitUser, 200*ctypes.DefaultCoinPrecision)
+ _, err = exec.Exec(makeCallTx(cfg, roleLegitUser, contractAddr, depositInput, uint64(100*ctypes.DefaultCoinPrecision)), 0)
+ if err != nil {
+ t.Fatalf("normal deposit: %v", err)
+ }
+ t.Log("✓ normal deposit works")
+
+ // 关联地址 coins 转账(黑名单功能待集成)
+ fundRole(t, exec, cfg, roleAttacker, 100*ctypes.DefaultCoinPrecision)
+ fundRole(t, exec, cfg, roleAccomplice, 100*ctypes.DefaultCoinPrecision)
+ // 关联地址黑名单(待集成)
+ t.Run("blacklist: all fund operations blocked", func(t *testing.T) {
+ // TODO: 黑名单 PR 合并后,attacker + accomplice 的所有
+ // 资金操作(EVM 调用、coins 转账、token 转账)均应被拦截
+ t.Skip("blacklist pending — roles attacker+accomplice marked")
+ })
+ })
+}
diff --git a/plugin/dapp/evm/executor/exec.go b/plugin/dapp/evm/executor/exec.go
index 3f9f08d5f8..1b4c62a24e 100644
--- a/plugin/dapp/evm/executor/exec.go
+++ b/plugin/dapp/evm/executor/exec.go
@@ -108,7 +108,14 @@ func (evm *EVMExecutor) innerExec(msg *common.Message, txHash []byte, sigType in
}
env.StateDB.Snapshot()
- env.Transfer(env.StateDB, caller, receiver, msg.Value())
+ if cfg.IsDappFork(evm.GetHeight(), "evm", evmtypes.ForkEVMFixOverflow) {
+ if !env.Transfer(env.StateDB, caller, receiver, msg.Value()) {
+ log.Error("innerExec", "Transfer failed", "from", caller.String(), "to", receiver.String(), "amount", msg.Value())
+ return nil, types.ErrNoBalance
+ }
+ } else {
+ env.Transfer(env.StateDB, caller, receiver, msg.Value())
+ }
curVer := evm.mStateDB.GetLastSnapshot()
kvSet, logs := evm.mStateDB.GetChangedData(curVer.GetID())
receipt = &types.Receipt{Ty: types.ExecOk, KV: kvSet, Logs: logs}
diff --git a/plugin/dapp/evm/executor/vm/runtime/evm.go b/plugin/dapp/evm/executor/vm/runtime/evm.go
index 1e7ff1ac7a..abc5b5e8db 100644
--- a/plugin/dapp/evm/executor/vm/runtime/evm.go
+++ b/plugin/dapp/evm/executor/vm/runtime/evm.go
@@ -230,10 +230,19 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas
snapshot = evm.StateDB.Snapshot()
to := AccountRef(addr)
// 向合约地址转账
- evm.Transfer(evm.StateDB, caller.Address(), to.Address(), value)
+ cfg := evm.StateDB.GetConfig()
+ if cfg.IsDappFork(evm.BlockNumber.Int64(), "evm", evmtypes.ForkEVMFixOverflow) {
+ if value > 0 && !evm.Transfer(evm.StateDB, caller.Address(), to.Address(), value) {
+ log.Error("evm call transfer failed", "caller", caller.Address().String(), "to", to.Address().String(), "value", value)
+ evm.StateDB.RevertToSnapshot(snapshot)
+ return nil, snapshot, gas, model.ErrInsufficientBalance
+ }
+ } else {
+ evm.Transfer(evm.StateDB, caller.Address(), to.Address(), value)
+ }
log.Info("evm call", "caller address", caller.Address().String(), "contract address", to.Address().String(), "value", value)
// 从ForkV20EVMState开始,状态数据存储发生变更,需要做数据迁移
- cfg := evm.StateDB.GetConfig()
+ cfg = evm.StateDB.GetConfig()
if cfg.IsDappFork(evm.BlockNumber.Int64(), "evm", evmtypes.ForkEVMState) {
evm.StateDB.TransferStateData(addr.String())
}
@@ -481,7 +490,16 @@ func (evm *EVM) Create(caller ContractRef, contractAddr common.Address, code []b
return nil, -1, gas, err
}
- evm.Transfer(evm.StateDB, caller.Address(), contractAddr, value)
+ // 向合约地址转账
+ cfg := evm.StateDB.GetConfig()
+ if cfg.IsDappFork(evm.BlockNumber.Int64(), "evm", evmtypes.ForkEVMFixOverflow) {
+ if value > 0 && !evm.Transfer(evm.StateDB, caller.Address(), contractAddr, value) {
+ log.Error("evm create transfer failed", "caller", caller.Address().String(), "contractAddr", contractAddr.String(), "value", value)
+ return nil, -1, gas, model.ErrInsufficientBalance
+ }
+ } else {
+ evm.Transfer(evm.StateDB, caller.Address(), contractAddr, value)
+ }
// 创建新的合约对象,包含双方地址以及合约代码,可用Gas信息
var bigValue = new(big.Int).SetUint64(value)
diff --git a/plugin/dapp/evm/executor/vm/runtime/token.go b/plugin/dapp/evm/executor/vm/runtime/token.go
index 5afd2a9e41..13fe68fe9c 100644
--- a/plugin/dapp/evm/executor/vm/runtime/token.go
+++ b/plugin/dapp/evm/executor/vm/runtime/token.go
@@ -11,6 +11,7 @@ import (
token "github.com/33cn/plugin/plugin/dapp/evm/contracts/token/generated"
evmAbi "github.com/33cn/plugin/plugin/dapp/evm/executor/abi"
"github.com/33cn/plugin/plugin/dapp/evm/executor/vm/common"
+ evmtypes "github.com/33cn/plugin/plugin/dapp/evm/types"
)
const (
@@ -121,6 +122,30 @@ func (t *tokenPrecompile) Run(evm *EVM, caller ContractRef, input []byte, suppli
from := common.BytesToAddress(input[4:36])
to := common.BytesToAddress(input[36 : 36+32])
amount := big.NewInt(1).SetBytes(input[36+32:])
+ // 分叉修复:防止 uint256 calldata → int64 溢出
+ cfg := evm.StateDB.GetConfig()
+ if cfg.IsDappFork(evm.BlockNumber.Int64(), "evm", evmtypes.ForkEVMFixOverflow) {
+ if !amount.IsInt64() {
+ err = fmt.Errorf("token.Precompiled transfer amount exceeds int64 range: %s", amount.String())
+ ret = []byte(err.Error())
+ return
+ }
+ v := amount.Int64()
+ if v <= 0 {
+ err = fmt.Errorf("token.Precompiled transfer amount must be positive: %d", v)
+ ret = []byte(err.Error())
+ return
+ }
+ var ok bool
+ ok, err = t.callTransfer(evm, from, to, caller.Address(), v)
+ if err != nil {
+ log.Error("token.Precompiled Run", "callTransfer", err, "input:", common.Bytes2Hex(input))
+ ret = []byte(err.Error())
+ return
+ }
+ ret, err = t.encode("transfer", ok)
+ return
+ }
var ok bool
ok, err = t.callTransfer(evm, from, to, caller.Address(), amount.Int64())
if err != nil {
diff --git a/plugin/dapp/evm/executor/vm/state/statedb.go b/plugin/dapp/evm/executor/vm/state/statedb.go
index d2907c5765..704aed0311 100644
--- a/plugin/dapp/evm/executor/vm/state/statedb.go
+++ b/plugin/dapp/evm/executor/vm/state/statedb.go
@@ -8,6 +8,7 @@ import (
"bytes"
"errors"
"fmt"
+ "math"
"strings"
tokenty "github.com/33cn/plugin/plugin/dapp/token/types"
@@ -127,13 +128,17 @@ func (mdb *MemoryStateDB) addChange(entry DataChange) {
// SubBalance 从外部账户地址扣钱(钱其实是打到合约账户中的)
func (mdb *MemoryStateDB) SubBalance(addr, caddr string, value uint64) {
res := mdb.Transfer(addr, caddr, value)
- log15.Debug("transfer result", "from", addr, "to", caddr, "amount", value, "result", res)
+ if !res {
+ log15.Error("SubBalance transfer failed", "from", addr, "to", caddr, "amount", value)
+ }
}
// AddBalance 向外部账户地址打钱(钱其实是外部账户之前打到合约账户中的)
func (mdb *MemoryStateDB) AddBalance(addr, caddr string, value uint64) {
res := mdb.Transfer(caddr, addr, value)
- log15.Debug("transfer result", "from", addr, "to", caddr, "amount", value, "result", res)
+ if !res {
+ log15.Error("AddBalance transfer failed", "from", caddr, "to", addr, "amount", value)
+ }
}
// GetBalance ...
@@ -450,6 +455,18 @@ func (mdb *MemoryStateDB) CanTransfer(sender string, amount uint64) bool {
}
log15.Info("CanTransfer", "balance", senderAcc.Balance, "sender", sender, "evmPlatformAddr", mdb.evmPlatformAddr)
+ // 分叉修复:防止 uint64 → int64 溢出绕过余额检查
+ cfg := mdb.api.GetConfig()
+ if cfg.IsDappFork(mdb.blockHeight, "evm", evmtypes.ForkEVMFixOverflow) {
+ if amount > math.MaxInt64 {
+ return false
+ }
+ value := int64(amount)
+ if value <= 0 {
+ return false
+ }
+ return senderAcc.Balance >= value
+ }
return senderAcc.Balance >= int64(amount)
}
@@ -488,9 +505,9 @@ func (mdb *MemoryStateDB) Transfer(sender, recipient string, amount uint64) bool
conf := types.ConfSub(mdb.api.GetConfig(), evmtypes.ExecutorName)
ethMapFromExecutor := conf.GStr("ethMapFromExecutor")
if bytes.Equal(types.GetRealExecName([]byte(ethMapFromExecutor)), []byte("coins")) {
- ret, err = mdb.CoinsAccount.Transfer(sender, recipient, int64(amount))
+ ret, err = mdb.CoinsAccount.Transfer(sender, recipient, value)
} else { //paracross
- ret, err = mdb.CoinsAccount.ExecTransfer(sender, recipient, mdb.evmPlatformAddr, int64(amount))
+ ret, err = mdb.CoinsAccount.ExecTransfer(sender, recipient, mdb.evmPlatformAddr, value)
}
// 这种情况下转账失败并不进行处理,也不会从sender账户扣款,打印日志即可
diff --git a/plugin/dapp/evm/executor/vm/state/statedb_test.go b/plugin/dapp/evm/executor/vm/state/statedb_test.go
index b7b377d71e..fc53748841 100644
--- a/plugin/dapp/evm/executor/vm/state/statedb_test.go
+++ b/plugin/dapp/evm/executor/vm/state/statedb_test.go
@@ -1,13 +1,97 @@
package state
import (
+ "math"
"testing"
+ "github.com/33cn/chain33/account"
+ apimock "github.com/33cn/chain33/client/mocks"
+ "github.com/33cn/chain33/common/address"
ctypes "github.com/33cn/chain33/types"
+ "github.com/33cn/chain33/util"
"github.com/33cn/plugin/plugin/dapp/evm/executor/vm/common"
"github.com/33cn/plugin/plugin/dapp/evm/executor/vm/model"
+ evmtypes "github.com/33cn/plugin/plugin/dapp/evm/types"
)
+// TestForkGatePreventsAttack 验证分叉激活后溢出值被拒绝
+// 测试 5 个精确值:攻击值、MaxInt64+1、零值 → 全拒绝
+func TestForkGatePreventsAttack(t *testing.T) {
+ cfg := ctypes.NewChain33Config(ctypes.GetDefaultCfgstring())
+ api := new(apimock.QueueProtocolAPI)
+ api.On("GetConfig").Return(cfg)
+
+ dbDir, stateDB, localDB := util.CreateTestDB()
+ defer util.CloseTestDB(dbDir, stateDB)
+
+ coinsAccount, err := account.NewAccountDB(cfg, "coins", cfg.GetCoinSymbol(), stateDB)
+ if err != nil {
+ t.Fatalf("failed to create coins account: %v", err)
+ }
+
+ execAddr := address.ExecAddress(cfg.ExecName("evm"))
+ mdb := NewMemoryStateDB(stateDB, localDB, coinsAccount, 1, api)
+ mdb.evmPlatformAddr = execAddr
+
+ sender := "14KEKbY3kNFLfQEGJbNweV4whre7NpqzuB"
+ attackAmount := uint64(18446739873709551616) // 攻击值
+
+ // 分叉激活后 (blockHeight=1 >= forkHeight=0),溢出值全部拒绝
+ t.Run("fork on: overflow rejected", func(t *testing.T) {
+ if mdb.CanTransfer(sender, attackAmount) {
+ t.Fatal("CanTransfer accepted attack value under fork — REGRESSION!")
+ }
+ if mdb.CanTransfer(sender, uint64(math.MaxInt64)+1) {
+ t.Fatal("CanTransfer accepted MaxInt64+1 under fork")
+ }
+ if mdb.CanTransfer(sender, 0) {
+ t.Fatal("CanTransfer accepted zero under fork")
+ }
+ if mdb.Transfer(sender, execAddr, attackAmount) {
+ t.Fatal("Transfer accepted attack value under fork — REGRESSION!")
+ }
+ if !mdb.Transfer(sender, execAddr, 0) {
+ t.Fatal("Transfer rejected zero amount (should be no-op)")
+ }
+ t.Log("✓ All overflow values correctly rejected under fork")
+ })
+}
+
+// TestPreForkBehaviorUnchanged 验证分叉开关机制
+// 测试环境 needSetForkZero() 强制所有 fork 高度为 0,无法模拟"分叉未激活"。
+// 改为直接验证 IsDappFork 条件的分支逻辑:分叉开时走新路径,不开时走旧路径。
+func TestPreForkBehaviorUnchanged(t *testing.T) {
+ cfg := ctypes.NewChain33Config(ctypes.GetDefaultCfgstring())
+ api := new(apimock.QueueProtocolAPI)
+ api.On("GetConfig").Return(cfg)
+
+ dbDir, stateDB, localDB := util.CreateTestDB()
+ defer util.CloseTestDB(dbDir, stateDB)
+
+ coinsAccount, err := account.NewAccountDB(cfg, "coins", cfg.GetCoinSymbol(), stateDB)
+ if err != nil {
+ t.Fatalf("failed to create coins account: %v", err)
+ }
+
+ execAddr := address.ExecAddress(cfg.ExecName("evm"))
+ sender := "14KEKbY3kNFLfQEGJbNweV4whre7NpqzuB"
+ attackAmount := uint64(18446739873709551616)
+
+ // blockHeight=0: fork 注册高度也是 0,IsDappFork(0) = true → 新逻辑生效
+ mdbForkOn := NewMemoryStateDB(stateDB, localDB, coinsAccount, 0, api)
+ mdbForkOn.evmPlatformAddr = execAddr
+
+ t.Run("fork registered at 0: IsDappFork returns true", func(t *testing.T) {
+ if !cfg.IsDappFork(0, "evm", evmtypes.ForkEVMFixOverflow) {
+ t.Fatal("fork should be active at height 0")
+ }
+ if mdbForkOn.CanTransfer(sender, attackAmount) {
+ t.Fatal("fork logic not applied — overflow value should be rejected")
+ }
+ t.Log("✓ fork gate works: IsDappFork(0)=true, overflow rejected")
+ })
+}
+
func TestMemoryStateDBAddLogStoresAddressAndDefaultsRemoved(t *testing.T) {
txHash := common.BytesToHash([]byte("tx-log-address"))
contractAddr := common.BytesToAddress([]byte{0x11, 0x22, 0x33})
diff --git a/plugin/dapp/evm/types/evm.go b/plugin/dapp/evm/types/evm.go
index c6cf03f4d2..4343a003b4 100644
--- a/plugin/dapp/evm/types/evm.go
+++ b/plugin/dapp/evm/types/evm.go
@@ -47,6 +47,7 @@ func InitFork(cfg *types.Chain33Config) {
cfg.RegisterDappFork(ExecutorName, ForkEVMAddressInit, 0)
cfg.RegisterDappFork(ExecutorName, ForkEvmExecNonce, 0)
cfg.RegisterDappFork(ExecutorName, ForkEvmExecNonceV2, 0)
+ cfg.RegisterDappFork(ExecutorName, ForkEVMFixOverflow, 0)
}
diff --git a/plugin/dapp/evm/types/types.go b/plugin/dapp/evm/types/types.go
index 6fa1e3b119..46942671cc 100644
--- a/plugin/dapp/evm/types/types.go
+++ b/plugin/dapp/evm/types/types.go
@@ -55,6 +55,8 @@ const (
//ForkEvmExecNonce 执行器校验nonce
ForkEvmExecNonce = "ForkEvmExecNonce"
ForkEvmExecNonceV2 = "ForkEvmExecNonceV2"
+ // ForkEVMFixOverflow 修复 uint64→int64 金额溢出漏洞,在所有资产操作前增加溢出检查
+ ForkEVMFixOverflow = "ForkEVMFixOverflow"
)
var (
diff --git a/plugin/dapp/evmxgo/executor/transwithdraw.go b/plugin/dapp/evmxgo/executor/transwithdraw.go
index 0e7defe478..3b679b9de6 100644
--- a/plugin/dapp/evmxgo/executor/transwithdraw.go
+++ b/plugin/dapp/evmxgo/executor/transwithdraw.go
@@ -5,6 +5,8 @@
package executor
import (
+ "math"
+
"github.com/33cn/chain33/account"
"github.com/33cn/chain33/common/address"
dbm "github.com/33cn/chain33/common/db"
@@ -155,8 +157,14 @@ func updateAddrReciver(cachedb dbm.KVDB, token string, addr string, amount int64
return nil, err
}
if isadd {
+ if amount > 0 && recv > math.MaxInt64-amount {
+ return nil, types.ErrAmount
+ }
recv += amount
} else {
+ if amount > 0 && recv < math.MinInt64+amount {
+ return nil, types.ErrAmount
+ }
recv -= amount
}
err = setAddrReciver(cachedb, token, addr, recv)