DeFiVulnLabs靶场全系列详解(四十二)转账函数固定2300个gas导致合约可用性遭到破坏

admin 2025年5月15日10:23:55评论1 views字数 4487阅读14分57秒阅读模式
01
前言
此内容仅作为展示Solidity常见错误的概念证明。它严格用于教育目的,不应被解释为鼓励或认可任何形式的非法活动或实际的黑客攻击企图。所提供的信息仅供参考和学习,基于此内容采取的任何行动均由个人全权负责。使用这些信息应遵守适用的法律、法规和道德标准。
DeFiVulnLabs一共有47个漏洞实验,包括各种经典的合约漏洞和一些少见的可能造成安全问题的不安全代码,本系列将逐一解析每个漏洞,包括官方的解释和自己的理解。
02
不正确的转账函数导致的合约可用性遭到破坏
漏洞解析:

    transfer和send会固定最多能使用2300个gas,如果调用者也是合约代码,我们称为合约代码A(企业A),存在漏洞的合约称为合约代码B(企业B)

  如果B合约向A合约转账(如果此时A合约方法里有receive 或 fallback,接收到接受过来的转账后,就会触发这两个方法),那么此时大概率都会失败,因为receive 或 fallback接受基本都需要超过2300GAS才能执行,因此会产生dos类的问题,企业A的合约将永远无法收到这笔转账。

真实漏洞案例:
https://github.com/code-423n4/2022-12-escher-findings/issues/99
代码地址:
https://github.com/SunWeb3Sec/DeFiVulnLabs/blob/main/src/test/payable-transfer.sol
代码解析:
以下是一个普通的bank合约,合约提供了withdraw函数和deposit函数,这个合约非常简单且乍一看没有任何问题,而实际上如果部署了也并"没有任何问题"。
contract SimpleBank {    mapping(address => uintprivate balances;    function deposit() public payable {        balances[msg.sender] += msg.value;    }        function getBalance() public view returns (uint) {        return balances[msg.sender];    }        function withdraw(uint amountpublic {        require(balances[msg.sender] >= amount);        balances[msg.sender] -= amount;        // the issue is here        payable(msg.sender).transfer(amount);    }}
用户(钱包EOA)调用withdraw的时候就会调用transfer进行提现,可以看到是功能都是可以正常使用的。
DeFiVulnLabs靶场全系列详解(四十二)转账函数固定2300个gas导致合约可用性遭到破坏
问题代码在这里,调用了transfer发送ETH
 payable(msg.sender).transfer(amount)
因此我们需要"额外的",自己给它加上一些代码,让他增加gas消耗,这里就回到了之前所说的(如果此时合约方法里有receive 或 fallback,接收到接受过来的转账后,就会触发这两个方法)
也就是
  1. 如果接收方是普通 EOA 钱包

    • transfer
       成功,因为 EOA 没有 receive/fallback 逻辑,Gas 消耗极低。
  2. 如果接收方是 Receiver Contract

    • transfer
       触发 receive 函数,但 Gas 不足(需 3000,上限 2300)。
    • 结果
      转账失败,ETH 未发送,withdraw 函数回滚。
03
测试复现
首先我们使用forge复现
首先是simpleBank和FixedSimple进行实例化
contract ContractTest is Test {    SimpleBank SimpleBankContract;    FixedSimpleBank FixedSimpleBankContract;        function setUp() public {        SimpleBankContract = new SimpleBank();        FixedSimpleBankContract = new FixedSimpleBank();    }        function testTransferFail() public {        SimpleBankContract.deposit{value1 ether}();        assertEq(SimpleBankContract.getBalance(), 1 ether);        vm.expectRevert();        SimpleBankContract.withdraw(1 ether);    }        function testCall() public {        FixedSimpleBankContract.deposit{value1 ether}();        assertEq(FixedSimpleBankContract.getBalance(), 1 ether);        FixedSimpleBankContract.withdraw(1 ether);    }        receive() external payable {        //just a example for out of gas        SimpleBankContract.deposit{value1 ether}();    }}
这里的的操作合约是ContractTest ,而且ContractTest 合约里有receive方法。
DeFiVulnLabs靶场全系列详解(四十二)转账函数固定2300个gas导致合约可用性遭到破坏
符合我们上面所说的条件,使用call方法转账成功了,使用transfer转账失败了

04

Remix合约测试复现
先部署之前的simpleBank合约
DeFiVulnLabs靶场全系列详解(四十二)转账函数固定2300个gas导致合约可用性遭到破坏
编写了段测试代码,这段合约代码会将ETH转入合约中,然后调用提现(接受到ETH),就触发fallback
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;// 定义合约 A 的接口interface ISimpleBank {    function deposit() external payable;    function withdraw(uint amount) external;    function getBalance() external view returns (uint);}contract ContractB {    // 合约 A 的实例    ISimpleBank public immutable contractA;    // 初始化时绑定合约 A 的地址    constructor(address _contractA) {        contractA = ISimpleBank(_contractA);    }    // 存款到合约 A    function depositETH() external payable {        // 将 ETH 存入合约 A(msg.value 是发送的 ETH 金额)        contractA.deposit{value: msg.value}();    }    // 从合约 A 中提取 ETH    function withdrawETH(uint amount) external {        contractA.withdraw(amount);    }    // 接收 ETH 的 fallback 函数(当合约 A 的 transfer 发送 ETH 到此合约时触发)    receive() external payable {        contractA.deposit{value: msg.value}();    }    // 获取合约 B 的 ETH 余额    function getBalance() external view returns (uint) {        return address(this).balance;    }    // 获取合约 B 在合约 A 中的存款余额    function getDepositInContractA() external view returns (uint) {        return contractA.getBalance();    }}
DeFiVulnLabs靶场全系列详解(四十二)转账函数固定2300个gas导致合约可用性遭到破坏
产生报错 Out of gas。
Note: The called function should be payable if you send value and the value you send should be less than your current balance. If the transaction failed for not having enough gas, try increasing the gas limit gently.
符合预期,因为是合约调用合约,然后触发fallback函数
05
如何修复该问题
将transfer替换成call方法,然后再进行重入的校验和检查

(bool success, ) = payable(msg.sender).call{value: amount}("");

contract FixedSimpleBank {    mapping(address => uintprivate balances;   function deposit() public payable {        balances[msg.sender] += msg.value;    }   function getBalance() public view returns (uint) {        return balances[msg.sender];    }   function withdraw(uint amountpublic {        require(balances[msg.sender] >= amount);        balances[msg.sender] -= amount;        (bool success, ) = payable(msg.sender).call{value: amount}("");        require(success, " Transfer of ETH Failed");    }}
那什么时候才用transfer和send呢?
由于这两个函数转账操作上限的gas是2300,因此这可以非常直接的有效的防范重入攻击,但是可能现在已逐渐无法满足现代智能合约的需求。
查询相关的资料,send方法目前已经基本很少用了。
然后transfer如果是普通钱包转账就用它,如果涉及到对合约的交易,就始终使用call发送ETH,配合重入攻击防护代码校验。
06
感谢关注
个人语雀账号:https://www.yuque.com/iceqaq

原文始发于微信公众号(Ice ThirdSpace):DeFiVulnLabs靶场全系列详解(四十二)转账函数固定2300个gas导致合约可用性遭到破坏

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年5月15日10:23:55
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   DeFiVulnLabs靶场全系列详解(四十二)转账函数固定2300个gas导致合约可用性遭到破坏https://cn-sec.com/archives/4066438.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息