一、漏洞原理说明
漏洞概述
该漏洞主要是由于在伊斯坦布尔硬分叉中实施EIP 1884后,SLOAD操作的gas成本增加,导致使用Solidity的transfer()
或send()
方法向智能合约接收者转移ETH时出现问题。当接收者是智能合约时,这些方法可能无法成功转移ETH。
漏洞产生的具体原因
1. 硬分叉导致SLOAD操作gas成本增加
在伊斯坦布尔硬分叉中实施EIP 1884后,SLOAD操作的gas成本从200增加到了800。这一变化影响了一些现有的智能合约,特别是那些依赖于transfer()
或send()
方法进行ETH转移的合约。
2. transfer()
和send()
方法的局限性
transfer()
和send()
方法在设计上只提供了2300 gas用于执行接收者合约的代码。当接收者是智能合约时,如果满足以下条件之一,转移操作将会失败:
- 未实现可支付的回退函数
:如果智能合约没有实现可支付的回退函数(payable fallback function),则无法接收ETH。 - 回退函数消耗超过2300 gas
:如果智能合约实现了可支付的回退函数,但该函数的执行需要超过2300 gas,那么 transfer()
或send()
方法将无法提供足够的gas,导致转移失败。 - 通过代理调用导致gas消耗超过2300
:即使回退函数本身消耗的gas少于2300,但如果通过代理调用该函数,可能会增加额外的gas消耗,使得总gas消耗超过2300,从而导致转移失败。
代码示例分析
以下是测试文件中的代码片段,展示了漏洞的具体表现:
contractSimpleBank{mapping(address=>uint)private balances;functiondeposit()publicpayable{ balances[msg.sender]+= msg.value;}functiongetBalance()publicviewreturns(uint){return balances[msg.sender];}functionwithdraw(uint amount)public{require(balances[msg.sender]>= amount); balances[msg.sender]-= amount;// 问题出在这里payable(msg.sender).transfer(amount);}}
在
SimpleBank
合约的withdraw
函数中,使用了payable(msg.sender).transfer(amount)
来转移ETH。如果msg.sender
是一个智能合约,并且满足上述提到的任何一个条件,那么这个转移操作将会失败。修复建议
为了避免这个漏洞,建议使用
call
方法,并检查其返回的布尔值,同时结合重入防护机制。以下是修复后的代码示例:contractFixedSimpleBank{mapping(address=>uint)private balances;functiondeposit()publicpayable{ balances[msg.sender]+= msg.value;}functiongetBalance()publicviewreturns(uint){return balances[msg.sender];}functionwithdraw(uint amount)public{require(balances[msg.sender]>= amount); balances[msg.sender]-= amount;(bool success,)=payable(msg.sender).call{value: amount}("");require(success," Transfer of ETH Failed");}}
在
FixedSimpleBank
合约的withdraw
函数中,使用了payable(msg.sender).call{value: amount}("")
来转移ETH,并检查了返回的布尔值success
。如果转移失败,会抛出错误信息。这样可以确保ETH转移的可靠性。二、测试环境构造过程
1. 安装Foundry框架
用户提供了foundry框架可执行文件下载链接https://github.com/foundry-rs/foundry/releases/download/v1.1.0/foundry_v1.1.0_linux_amd64.tar.gz ,同时上传了foundry_v1.1.0_linux_amd64.zip文件。将该压缩包解压后,可得到
forge
等可执行文件,将其配置到系统环境变量中,即可完成Foundry框架的安装。2. 准备测试文件
用户上传了DeFiVulnLabs.zip文件,解压该文件后,得到测试文件
DeFiVulnLabs_unzip/DeFiVulnLabs/src/test/payable-transfer.sol
。该测试文件包含了漏洞合约SimpleBank
和修复后的合约FixedSimpleBank
,以及用于测试的合约ContractTest
。3. 构造测试环境
使用Foundry合约测试工具组里的
forge
工具来构造测试环境。forge
工具可以对Solidity合约进行编译、测试等操作。在包含测试文件的目录下,使用forge
工具可以自动识别并编译测试文件,为后续的漏洞复现测试做好准备。三、复现漏洞情况
测试思路
在
ContractTest
合约中,定义了两个测试函数:testTransferFail()
和testCall()
。testTransferFail()
用于测试使用transfer()
方法进行ETH转移时是否会失败,testCall()
用于测试使用call()
方法进行ETH转移时是否会成功。测试结果分析
1.
testTransferFail()
该测试用例首先调用
SimpleBank
合约的deposit()
函数存入1 ether,然后调用getBalance()
函数验证余额是否正确。接着,使用vm.expectRevert()
期望接下来的withdraw()
函数调用会触发回退。最后调用withdraw()
函数尝试取出1 ether,由于transfer()
方法的局限性,当接收者合约的回退函数消耗超过2300 gas时,转移操作会失败,测试用例通过,符合预期。2.
testCall()
该测试用例首先调用
FixedSimpleBank
合约的deposit()
函数存入1 ether,然后调用getBalance()
函数验证余额是否正确。接着调用withdraw()
函数尝试取出1 ether,由于使用了call()
方法并检查了返回的布尔值,即使接收者合约的回退函数消耗超过2300 gas,也能正常处理,测试用例通过,说明修复后的合约能够正常工作。四、完整运行测试结果
编译信息
Compiling 7 files with Solc 0.8.29Solc 0.8.29 finished in 286.94msCompiler run successful!
这表明使用Solc 0.8.29编译器成功编译了7个文件,编译过程耗时286.94ms。
测试用例执行情况
|
|
|
|
---|---|---|---|
|
|
|
|
|
|
|
|
测试套件结果
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 584.74µs (415.46µs CPU time)Ran 1 test suite in 4.83ms (584.74µs CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)
这表明整个测试套件执行成功,2个测试用例全部通过,没有失败和跳过的测试用例,测试套件执行耗时584.74µs(CPU时间415.46µs),整个测试过程耗时4.83ms。
原文始发于微信公众号(Ice ThirdSpace):DeFiVulnLabs验证——利用Coze进行自动化复现和测试及坑点
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论