mcpField
W
web3-testing

web3-testing

github.com/sickn33テスト・品質48.8k219 回インストール今日更新
リポジトリ

web3-testing について

Hardhat と Foundry による Web3 スマートコントラクトのテストを扱うガイダンスです。単体・統合テスト、ファジング、ガス確認、メインネットのフォーク、カバレッジ、検証手順を説明し、参照される補助ファイルは含まれません。

主な機能

  1. 01Hardhat と Foundry のテストガイダンス
  2. 02単体・統合テストのパターン
  3. 03ファジングと境界値テスト
  4. 04ガスとカバレッジの確認
  5. 05メインネットフォークテスト
  6. 06コントラクト検証の手順

活用シーン

  1. 01スマートコントラクトの単体テスト作成
  2. 02ガス使用量とテストカバレッジの確認
  3. 03メインネットのフォーク状態でのテスト
  4. 04コントラクト検証手順の準備

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/sickn33/agentic-awesome-skills --skill web3-testing

インストール後、Claude Code で/web3-testingを入力して読み込まれたか確認します。

詳細

補足資料の利用可否

このカタログの内容には以下のインラインガイダンスが含まれていますが、次の補足パスは含まれていません。これらは外部の前提資料であり、同梱ファイルではありません。

  • <support-root>/references/hardhat-setup.md
  • <support-root>/references/foundry-setup.md
  • <support-root>/references/test-patterns.md
  • <support-root>/references/mainnet-forking.md
  • <support-root>/references/contract-verification.md
  • <support-root>/assets/hardhat-config.js
  • <support-root>/assets/test-suite.js
  • <support-root>/assets/foundry.toml
  • <support-root>/scripts/test-contract.sh

これらのパスに依存する手順を実行する前に、元の補足資料または既存のインストール環境を求めてください。このカタログには、検証済みのダウンロード元が記載されていません。各パスをそのインストール環境からの相対パスとして解決し、内容を確認したうえで、該当するライセンスと実行上の前提条件を検証してください。必要なファイルが存在することを確認するまでは、コマンドを実行したり、参照先の手順に従ったと主張したりしないでください。不足しているスクリプトを捏造したり、同じ名前の無関係なファイルを同等のものとして扱ったりしないでください。ここで完全に説明されている独立した手順は、ユーザーから許可された範囲内で実行できます。依存する手順は利用できないものとして報告してください。

Web3 スマートコントラクトのテスト

Hardhat、Foundry、高度なテストパターンを使用した、スマートコントラクトの包括的なテスト戦略を習得します。

このスキルを使用しない場合

  • タスクが Web3 スマートコントラクトのテストに関係しない場合
  • この範囲外の別のドメインやツールが必要な場合

手順

  • 目標、制約、必要な入力を明確にします。
  • 関連するベストプラクティスを適用し、結果を検証します。
  • 実行可能な手順と検証方法を提示します。

このスキルを使用する場合

  • スマートコントラクトのユニットテストを作成する
  • 統合テストスイートをセットアップする
  • ガス最適化テストを実施する
  • エッジケースを見つけるためにファジングを行う
  • 現実的なテストのためにメインネットをフォークする
  • テストカバレッジのレポート作成を自動化する
  • Etherscan でコントラクトを検証する

Hardhat テストのセットアップ

CommonJS のスニペットは、旧式の Hardhat 2/ethers 5 ツールチェーンを例示しています。プロジェクトのロックファイルに基づき、互換性が確認されたバージョンを固定してください。移行せずに Hardhat 3/ethers 6 と組み合わせないでください。コントラクトのフィクスチャとインターフェースはプロジェクト固有の前提条件であり、デプロイ可能なコードとして同梱されていません。ローカル開発チェーンと使い捨てのキーのみを使用してください。フォークした状態はローカルのものです。テストの近道として、資金のあるネットワークにブロードキャストしたり、デプロイしたりしないでください。リモートでのソース検証は、別途行う公開操作です。

// hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");
require("@nomiclabs/hardhat-etherscan");
require("hardhat-gas-reporter");
require("solidity-coverage");

module.exports = {
  solidity: {
    version: "0.8.19",
    settings: {
      optimizer: {
        enabled: true,
        runs: 200,
      },
    },
  },
  networks: {
    hardhat: {
      forking: {
        url: process.env.MAINNET_RPC_URL,
        blockNumber: 15000000,
      },
    },
  },
  gasReporter: {
    enabled: true,
    currency: "USD",
    coinmarketcap: process.env.COINMARKETCAP_API_KEY,
  },
  etherscan: {
    apiKey: process.env.ETHERSCAN_API_KEY,
  },
};

ユニットテストのパターン

const { expect } = require("chai");
const { ethers, network } = require("hardhat");
const {
  loadFixture,
  time,
} = require("@nomicfoundation/hardhat-network-helpers");

describe("Token Contract", function () {
  // Fixture for test setup
  async function deployTokenFixture() {
    const [owner, addr1, addr2] = await ethers.getSigners();

    const Token = await ethers.getContractFactory("Token");
    const token = await Token.deploy();

    return { token, owner, addr1, addr2 };
  }

  describe("Deployment", function () {
    it("Should set the right owner", async function () {
      const { token, owner } = await loadFixture(deployTokenFixture);
      expect(await token.owner()).to.equal(owner.address);
    });

    it("Should assign total supply to owner", async function () {
      const { token, owner } = await loadFixture(deployTokenFixture);
      const ownerBalance = await token.balanceOf(owner.address);
      expect(await token.totalSupply()).to.equal(ownerBalance);
    });
  });

  describe("Transactions", function () {
    it("Should transfer tokens between accounts", async function () {
      const { token, owner, addr1 } = await loadFixture(deployTokenFixture);

      await expect(token.transfer(addr1.address, 50)).to.changeTokenBalances(
        token,
        [owner, addr1],
        [-50, 50],
      );
    });

    it("Should fail if sender doesn't have enough tokens", async function () {
      const { token, owner, addr1 } = await loadFixture(deployTokenFixture);
      const initialBalance = await token.balanceOf(addr1.address);

      await expect(
        token.connect(addr1).transfer(owner.address, 1),
      ).to.be.revertedWith("Insufficient balance");
    });

    it("Should emit Transfer event", async function () {
      const { token, owner, addr1 } = await loadFixture(deployTokenFixture);

      await expect(token.transfer(addr1.address, 50))
        .to.emit(token, "Transfer")
        .withArgs(owner.address, addr1.address, 50);
    });
  });

  describe("Time-based tests", function () {
    it("Should handle time-locked operations", async function () {
      const { token } = await loadFixture(deployTokenFixture);

      // Increase time by 1 day
      await time.increase(86400);

      // Test time-dependent functionality
    });
  });

  describe("Gas optimization", function () {
    it("Should use gas efficiently", async function () {
      const { token, addr1 } = await loadFixture(deployTokenFixture);

      const tx = await token.transfer(addr1.address, 100);
      const receipt = await tx.wait();

      expect(receipt.gasUsed).to.be.lessThan(50000);
    });
  });
});

Foundry テスト(Forge)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "forge-std/Test.sol";
import "../src/Token.sol";

interface IERC20MetadataForTest {
    function symbol() external view returns (string memory);
}

contract TokenTest is Test {
    Token token;
    address owner = address(1);
    address user1 = address(2);
    address user2 = address(3);

    function setUp() public {
        vm.prank(owner);
        token = new Token();
    }

    function testInitialSupply() public {
        assertEq(token.totalSupply(), 1000000 * 10**18);
    }

    function testTransfer() public {
        vm.prank(owner);
        token.transfer(user1, 100);

        assertEq(token.balanceOf(user1), 100);
        assertEq(token.balanceOf(owner), token.totalSupply() - 100);
    }

    function testTransferInsufficientBalance() public {
        vm.expectRevert(); // Prefer the exact expected error from the tested contract.
        vm.prank(user1);
        token.transfer(user2, 100); // Should fail
    }

    function testCannotTransferToZeroAddress() public {
        vm.prank(owner);
        vm.expectRevert("Invalid recipient");
        token.transfer(address(0), 100);
    }

    // Fuzzing test
    function testFuzzTransfer(uint256 amount) public {
        vm.assume(amount > 0 && amount <= token.totalSupply());

        vm.prank(owner);
        token.transfer(user1, amount);

        assertEq(token.balanceOf(user1), amount);
    }

    // Test with cheatcodes
    function testDealAndPrank() public {
        // Give ETH to address
        vm.deal(user1, 10 ether);

        // Impersonate address
        vm.prank(user1);

        // Test functionality
        assertEq(user1.balance, 10 ether);
    }

    // Mainnet fork test
    function testForkMainnet() public {
        vm.createSelectFork(vm.envString("MAINNET_RPC_URL"), 15000000);

        // Interact with mainnet contracts
        address dai = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
        assertEq(IERC20MetadataForTest(dai).symbol(), "DAI");
    }
}

高度なテストパターン

スナップショットとリバート
describe("Complex State Changes", function () {
  let snapshotId;

  beforeEach(async function () {
    snapshotId = await network.provider.send("evm_snapshot");
  });

  afterEach(async function () {
    await network.provider.send("evm_revert", [snapshotId]);
  });

  it("Test 1", async function () {
    // Make state changes
  });

  it("Test 2", async function () {
    // State reverted, clean slate
  });
});
メインネットのフォーク
describe("Mainnet Fork Tests", function () {
  let uniswapRouter, dai, usdc;

  before(async function () {
    await network.provider.request({
      method: "hardhat_reset",
      params: [
        {
          forking: {
            jsonRpcUrl: process.env.MAINNET_RPC_URL,
            blockNumber: 15000000,
          },
        },
      ],
    });

    // Connect to existing mainnet contracts
    uniswapRouter = await ethers.getContractAt(
      "IUniswapV2Router",
      "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D",
    );

    dai = await ethers.getContractAt(
      "IERC20",
      "0x6B175474E89094C44Da98b954EedeAC495271d0F",
    );
  });

  it("Should swap on Uniswap", async function () {
    // Test with real Uniswap contracts
  });
});
アカウントのなりすまし
it("Should impersonate whale account", async function () {
  const whaleAddress = "0x...";

  await network.provider.request({
    method: "hardhat_impersonateAccount",
    params: [whaleAddress],
  });

  const whale = await ethers.getSigner(whaleAddress);

  // Use whale's tokens
  await dai
    .connect(whale)
    .transfer(addr1.address, ethers.utils.parseEther("1000"));
});

ガス最適化テスト

const { expect } = require("chai");

describe("Gas Optimization", function () {
  it("Compare gas usage between implementations", async function () {
    const Implementation1 =
      await ethers.getContractFactory("OptimizedContract");
    const Implementation2 = await ethers.getContractFactory(
      "UnoptimizedContract",
    );

    const contract1 = await Implementation1.deploy();
    const contract2 = await Implementation2.deploy();

    const tx1 = await contract1.doSomething();
    const receipt1 = await tx1.wait();

    const tx2 = await contract2.doSomething();
    const receipt2 = await tx2.wait();

    console.log("Optimized gas:", receipt1.gasUsed.toString());
    console.log("Unoptimized gas:", receipt2.gasUsed.toString());

    expect(receipt1.gasUsed).to.be.lessThan(receipt2.gasUsed);
  });
});

カバレッジレポート

# Generate coverage report
npx hardhat coverage

# Output shows:
# File                | % Stmts | % Branch | % Funcs | % Lines |
# -------------------|---------|----------|---------|---------|
# contracts/Token.sol |   100   |   90     |   100   |   95    |

コントラクトの検証

// Verify on Etherscan
await hre.run("verify:verify", {
  address: contractAddress,
  constructorArguments: [arg1, arg2],
});
# Or via CLI
npx hardhat verify --network mainnet CONTRACT_ADDRESS "Constructor arg1" "arg2"

CI/CD 統合

リポジトリでサポートされている Node のバージョンと、レビュー済みでコミットに固定された Actions を使用してください。コミット済みのロックファイルからインストールし、コンパイルして、ローカルチェーン上でテストを実行し、カバレッジを収集します。RPC の認証情報はプラットフォームのシークレットストアで管理してください。信頼できないフォークのジョブにシークレットを公開しないでください。外部へのカバレッジアップロードは任意ですが、開示するソースコードやカバレッジデータについて、アップロードの許可が必要です。カバレッジ率が高いだけでは、セキュリティ監査を実施したことにはなりません。

リソース

  • references/hardhat-setup.md: Hardhat 設定ガイド
  • references/foundry-setup.md: Foundry テストフレームワーク
  • references/test-patterns.md: テストのベストプラクティス
  • references/mainnet-forking.md: フォークテストの戦略
  • references/contract-verification.md: Etherscan での検証
  • assets/hardhat-config.js: Hardhat の完全な設定
  • assets/test-suite.js: 包括的なテスト例
  • assets/foundry.toml: Foundry の設定
  • scripts/test-contract.sh: 自動テストスクリプト

ベストプラクティス

  1. テストカバレッジ:カバレッジ90%超を目指します
  2. エッジケース:境界条件をテストします
  3. ガス上限:関数がブロックのガス上限に達しないことを確認します
  4. リエントランシー:リエントランシーの脆弱性がないかテストします
  5. アクセス制御:権限のないアクセスを試みてテストします
  6. イベント:イベントが発行されることを確認します
  7. フィクスチャ:コードの重複を避けるため、フィクスチャを使用します
  8. メインネットフォーク:実際のコントラクトを使ってテストします
  9. ファジング:プロパティベースのテストを使用します
  10. CI/CD:コミットごとにテストを自動化します

制限事項

  • タスクが上記で説明した範囲に明確に該当する場合にのみ、このスキルを使用してください。
  • 出力を、環境固有の検証、テスト、専門家によるレビューの代わりとして扱わないでください。
  • 必要な入力、権限、安全上の境界、成功基準が不足している場合は、作業を中止して確認を求めてください。

コメント