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..d5c4270ffa --- /dev/null +++ b/docs/security/evm-uint64-overflow-attack-analysis.md @@ -0,0 +1,324 @@ +# 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 ← 余额检查完全失效! +} +``` + +### 第三层:实际转账静默失败 + +**文件**:`plugin/dapp/evm/executor/vm/state/statedb.go:472-482` + +```go +func (mdb *MemoryStateDB) Transfer(sender, recipient string, amount uint64) bool { + value := int64(amount) // value = -4200000000000 + if value < 0 { + return false // ← 转账未执行,攻击者零成本! + } + // ... + ret, err = mdb.CoinsAccount.Transfer(sender, recipient, int64(amount)) +} +``` + +### 第四层:EVM 无视 Transfer 返回值 + +**文件**:`plugin/dapp/evm/executor/vm/runtime/evm.go:233,257-262` + +```go +func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, + gas uint64, value uint64) (...) { + + // preCheck 通过(因为 CanTransfer 被绕过) + // Transfer 静默失败,但返回值被丢弃! + evm.Transfer(evm.StateDB, caller.Address(), to.Address(), value) + + // 合约以原始 uint64 value 继续执行: + var bigValue = new(big.Int).SetUint64(value) // = 18446739873709551616 + if evm.CheckIsEthTx() && value != 0 { + 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 事件吻合 +``` + +## 根因总结 + +| 层 | 文件 | 行号 | 问题 | +|----|------|------|------| +| **RPC** | `rpc/rpc.go` | 69,80 | `int64` Amount 通过 protobuf varint 可传入 > 2^63 值,`uint64()` 恢复大值 | +| **StateDB** | `state/statedb.go` | 453 | `CanTransfer`: `int64(amount)` 溢出为负数,余额校验恒成立 | +| **StateDB** | `state/statedb.go` | 479-481 | `Transfer`: `int64(amount)` 溢出为负数后 `value < 0` return false,转账未执行 | +| **EVM** | `vm/runtime/evm.go` | 233 | `evm.Transfer()` 返回值被丢弃,继续执行合约 | +| **EVM** | `vm/runtime/evm.go` | 257-262 | 合约以原始 `uint64` value 执行,mq.value 未受 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` 的位置都可能产生溢出。 + +--- + +从最底层到最外层,分层防御。 + +### 修复原则 + +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 +} +``` + +### 第三层:`rpc.go` — RPC 入口处校验 Amount 合法性 + +**文件**:`plugin/dapp/evm/rpc/rpc.go` + +在 RPC 入口处预先校验 `int64` Amount 为正数,防止 protobuf varint 编码绕过限制。 + +**`CreateCallTx`**: +```go +if amountInt64 <= 0 { + return nil, types.ErrAmount +} +``` + +**`CreateDeployTx`**: +```go +if amountInt64 < 0 { + return nil, types.ErrAmount +} +``` + +**`CreateTransferOnlyTx`**: +```go +if in.Amount <= 0 { + return nil, types.ErrAmount +} +``` + +### 举一反三:同类问题修复 + +审计过程中还发现并修复了同类溢出问题: + +| 文件 | 问题 | 修复 | +|------|------|------| +| `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` | + +### 防御层次总览 + +| 层 | 文件 | 防护机制 | +|----|------|----------| +| ③ RPC 入口 | `rpc/rpc.go` | 拒绝 `int64 <= 0` 的 Amount(阻止 protobuf varint 绕过) | +| ① 状态DB边界 | `state/statedb.go` | 拒绝 `uint64 > MaxInt64` 的金额(CanTransfer + Transfer + SubBalance + AddBalance) | +| ② EVM 执行 | `vm/runtime/evm.go` | 检查 Transfer 返回值,失败时回滚(Call + Create) | +| ② 执行器入口 | `exec.go` | transfer-only 路径检查 Transfer 返回值 | +| ① 预编译合约 | `token.go` | 拒绝 uint256 → int64 溢出的 transfer 金额 | +| - | `chain33/rpc/ethrpc/types/tx.go:AssembleChain33Tx` | `bigAmount.Int64()` + `uint64(amount)` 同类型溢出,已同步修复(`IsInt64` 检查 + nil 保护) | diff --git a/plugin/dapp/evm/executor/exec.go b/plugin/dapp/evm/executor/exec.go index 3f9f08d5f8..438e729675 100644 --- a/plugin/dapp/evm/executor/exec.go +++ b/plugin/dapp/evm/executor/exec.go @@ -108,7 +108,10 @@ func (evm *EVMExecutor) innerExec(msg *common.Message, txHash []byte, sigType in } env.StateDB.Snapshot() - env.Transfer(env.StateDB, caller, receiver, msg.Value()) + 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 + } 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..940220119a 100644 --- a/plugin/dapp/evm/executor/vm/runtime/evm.go +++ b/plugin/dapp/evm/executor/vm/runtime/evm.go @@ -230,7 +230,11 @@ 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) + 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 + } log.Info("evm call", "caller address", caller.Address().String(), "contract address", to.Address().String(), "value", value) // 从ForkV20EVMState开始,状态数据存储发生变更,需要做数据迁移 cfg := evm.StateDB.GetConfig() @@ -481,7 +485,11 @@ func (evm *EVM) Create(caller ContractRef, contractAddr common.Address, code []b return nil, -1, gas, err } - evm.Transfer(evm.StateDB, caller.Address(), contractAddr, value) + // 向合约地址转账 + 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 + } // 创建新的合约对象,包含双方地址以及合约代码,可用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..81bb7ac14e 100644 --- a/plugin/dapp/evm/executor/vm/runtime/token.go +++ b/plugin/dapp/evm/executor/vm/runtime/token.go @@ -121,8 +121,20 @@ 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 溢出 + 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(), amount.Int64()) + 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()) diff --git a/plugin/dapp/evm/executor/vm/state/statedb.go b/plugin/dapp/evm/executor/vm/state/statedb.go index d2907c5765..d9579db05d 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 ... @@ -439,6 +444,15 @@ func (mdb *MemoryStateDB) GetChangedData(version int) (kvSet []*types.KeyValue, // CanTransfer 借助coins执行器进行转账相关操作 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 + } + var senderAcc *types.Account conf := types.ConfSub(mdb.api.GetConfig(), evmtypes.ExecutorName) ethMapFromExecutor := conf.GStr("ethMapFromExecutor") @@ -450,7 +464,7 @@ func (mdb *MemoryStateDB) CanTransfer(sender string, amount uint64) bool { } log15.Info("CanTransfer", "balance", senderAcc.Balance, "sender", sender, "evmPlatformAddr", mdb.evmPlatformAddr) - return senderAcc.Balance >= int64(amount) + return senderAcc.Balance >= value } // TransferType 定义转账类型 @@ -476,6 +490,11 @@ func (mdb *MemoryStateDB) Transfer(sender, recipient string, amount uint64) bool err error ) + // 防止 uint64 → int64 溢出导致转账静默失败 + if amount > math.MaxInt64 { + log15.Error("transfer amount overflow", "sender", sender, "recipient", recipient, "amount", amount) + return false + } value := int64(amount) if value < 0 { return false @@ -488,9 +507,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..1f3b5ff4c1 100644 --- a/plugin/dapp/evm/executor/vm/state/statedb_test.go +++ b/plugin/dapp/evm/executor/vm/state/statedb_test.go @@ -1,13 +1,138 @@ 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" ) +// TestCanTransferOverflowProtection 验证对 uint64→int64 溢出的防护 +// 当 amount > math.MaxInt64 时,CanTransfer 必须在越过余额检查前返回 false +func TestCanTransferOverflowProtection(t *testing.T) { + // 不设置 CoinsAccount/api —— 溢出防护应在访问账户前拦截 + mdb := &MemoryStateDB{} + + // 攻击者使用的实际值:接近 MaxUint64 + if mdb.CanTransfer("sender", 18446739873709551616) { + t.Fatal("CanTransfer should reject overflow uint64 amount (attack value)") + } + + // 边界:MaxInt64 + 1,刚好溢出 + if mdb.CanTransfer("sender", uint64(math.MaxInt64)+1) { + t.Fatal("CanTransfer should reject amount > MaxInt64") + } + + // 零值 + if mdb.CanTransfer("sender", 0) { + t.Fatal("CanTransfer should reject zero amount") + } +} + +// TestTransferOverflowProtection 验证对 uint64→int64 溢出的防护 +// 当 amount > math.MaxInt64 时,Transfer 必须返回 false 且不执行实际转账 +func TestTransferOverflowProtection(t *testing.T) { + mdb := &MemoryStateDB{} + + // 攻击者使用的实际值 + if mdb.Transfer("sender", "recipient", 18446739873709551616) { + t.Fatal("Transfer should reject overflow uint64 amount (attack value)") + } + + // 边界:MaxInt64 + 1 + if mdb.Transfer("sender", "recipient", uint64(math.MaxInt64)+1) { + t.Fatal("Transfer should reject amount > MaxInt64") + } + + // 零值转账应成功(无需实际转账) + if !mdb.Transfer("sender", "recipient", 0) { + t.Fatal("Transfer should accept zero amount (no-op)") + } +} + +// TestCanTransferMaxInt64 验证 MaxInt64 本身是合法的(边界情况) +func TestCanTransferMaxInt64(t *testing.T) { + mdb := &MemoryStateDB{} + // MaxInt64 不应该触发溢出防护 + // 如果没有 panic(访问 nil api),说明溢出检查正确通过了 + defer func() { + if r := recover(); r == nil { + t.Log("MaxInt64 passed overflow guard as expected (nil api panic is expected)") + } + }() + mdb.CanTransfer("sender", math.MaxInt64) +} + +// TestTransferMaxInt64 验证 MaxInt64 边界情况 +func TestTransferMaxInt64(t *testing.T) { + mdb := &MemoryStateDB{} + // MaxInt64 不应该触发溢出防护 + // 如果没有 panic(访问 nil api),说明溢出检查正确通过了 + defer func() { + if r := recover(); r == nil { + t.Log("MaxInt64 passed overflow guard as expected (nil api panic is expected)") + } + }() + mdb.Transfer("sender", "recipient", math.MaxInt64) +} + +// TestAttackSimulationFullFlow 模拟完整攻击链路: +// 攻击者通过 RPC 传入 amount=18446739873709551616 (接近 MaxUint64), +// 经过 int64→uint64 转换恢复后,进入 CanTransfer 和 Transfer。 +// 修复后,这两个入口都必须拒绝此值。 +// 本测试使用真实的 CoinsAccount + StateDB 搭建完整上下文。 +func TestAttackSimulationFullFlow(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(带 in-memory StateDB) + coinsAccount, err := account.NewAccountDB(cfg, "coins", cfg.GetCoinSymbol(), stateDB) + if err != nil { + t.Fatalf("failed to create coins account: %v", err) + } + + // 构建 MemoryStateDB(模拟完整区块执行上下文) + execAddr := address.ExecAddress(cfg.ExecName("evm")) + mdb := NewMemoryStateDB(stateDB, localDB, coinsAccount, 1, api) + mdb.evmPlatformAddr = execAddr + + sender := "14KEKbY3kNFLfQEGJbNweV4whre7NpqzuB" + + // 攻击者的实际交易值:amount = 18446739873709551616 ≈ 2^64 + attackAmount := uint64(18446739873709551616) + + t.Run("CanTransfer rejects attack value", func(t *testing.T) { + if mdb.CanTransfer(sender, attackAmount) { + t.Fatal("BUG: CanTransfer accepted overflow amount — balance check bypassed!") + } + t.Log("✓ Attack overflow value correctly rejected by CanTransfer") + }) + + t.Run("Transfer rejects attack value", func(t *testing.T) { + if mdb.Transfer(sender, execAddr, attackAmount) { + t.Fatal("BUG: Transfer accepted overflow amount — would mint free WBTY!") + } + t.Log("✓ Attack overflow value correctly rejected by Transfer") + }) + + // MaxInt64 应正常通过(不会被溢出防护误杀) + t.Run("MaxInt64 is allowed", func(t *testing.T) { + if !mdb.CanTransfer(sender, math.MaxInt64) { + t.Log("MaxInt64 balance check result depends on actual balance, guard clause passed") + } + }) +} + 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/rpc/rpc.go b/plugin/dapp/evm/rpc/rpc.go index af54cf94ef..256c2e10e8 100644 --- a/plugin/dapp/evm/rpc/rpc.go +++ b/plugin/dapp/evm/rpc/rpc.go @@ -21,6 +21,9 @@ import ( // CreateEvmCallTx 创建未签名的部署合约交易 func (c *channelClient) CreateDeployTx(ctx context.Context, in evmtypes.EvmContractCreateReq) (*types.UnsignTx, error) { amountInt64 := in.Amount + if amountInt64 < 0 { + return nil, types.ErrAmount + } exec := in.ParaName + evmtypes.ExecutorName toAddr := address.ExecAddress(exec) @@ -67,6 +70,9 @@ func (c *channelClient) CreateDeployTx(ctx context.Context, in evmtypes.EvmContr func (c *channelClient) CreateCallTx(ctx context.Context, in evmtypes.EvmContractCallReq) (*types.UnsignTx, error) { amountInt64 := in.Amount + if amountInt64 <= 0 { + return nil, types.ErrAmount + } feeInt64 := in.Fee exec := in.ParaName + evmtypes.ExecutorName toAddr := address.ExecAddress(exec) @@ -104,6 +110,9 @@ func (c *channelClient) CreateCallTx(ctx context.Context, in evmtypes.EvmContrac } func (c *channelClient) CreateTransferOnlyTx(ctx context.Context, in evmtypes.EvmTransferOnlyReq) (*types.UnsignTx, error) { + if in.Amount <= 0 { + return nil, types.ErrAmount + } exec := in.ParaName + evmtypes.ExecutorName toAddr := address.ExecAddress(exec)