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)