ethereum-wingman について
Ethereum Wingmanは、Ethereumアプリを開発するAIエージェント向けのガイドです。スマートコントラクト、DeFi、セキュリティ、SpeedRun Ethereumに加え、Scaffold-ETH 2の実装とテスト規則を扱います。
主な機能
- 01スマートコントラクト開発ガイド
- 02DeFiプロトコルのガイド
- 03セキュリティとトークン操作規則
- 04Scaffold-ETH 2のフロントエンド規約
- 05ビルドとブラウザーテスト手順
活用シーン
- 01Scaffold-ETH 2でEthereumアプリを構築
- 02トークン承認とオンチェーン操作の実装
- 03フロントエンドのトランザクションフロー確認
- 04デプロイ済みEthereumアプリのテスト
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ austintgriffith/ ethereum-wingman --skill ethereum-wingmanインストール後、Claude Code で/ethereum-wingmanを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
AGENTS.mdmetadata.jsonREADME.mdreferences/automation-and-incentives.mdcritical-gotchas.mdhistorical-hacks.mdscripts/check-gotchas.shinit-project.shsetup-cursor.sh詳細
Ethereum Wingman
AI エージェント向けの包括的な Ethereum 開発ガイドです。スマートコントラクト開発、DeFi プロトコル、セキュリティのベストプラクティス、SpeedRun Ethereum のカリキュラムを網羅しています。
AI エージェントへの指示 — 最初にお読みください
🚫 重要:外部コントラクトとスキャフォールドのフック
以下のルールは必須です。違反すると、本番環境で実際のバグが発生します。
-
すべてのコントラクトを externalContracts.ts に追加 — トークンやプロトコルなど、操作するコントラクトはすべて、アドレスと ABI を指定して
packages/nextjs/contracts/externalContracts.tsに追加する必要があります。最初にファイルを読んでください。パターンは一目瞭然です。 -
スキャフォールドのフックのみ使用 — 生の WAGMI は絶対に使わない — 必ず
useScaffoldReadContractとuseScaffoldWriteContractを使用し、useWriteContractやuseReadContractなどの生の wagmi フックは絶対に使用しないでください。
これが重要な理由: スキャフォールドのフックは useTransactor を使用しており、トランザクションがウォレットで署名されるだけでなく、確認されるまで待機します。wagmi の生の writeContractAsync は、MetaMask でユーザーが署名した時点で解決されます。トランザクションがマイニングされる前です。そのため、トランザクションがまだ保留中なのにボタンが再び有効になります。
// ❌ WRONG: Raw wagmi - resolves after signing, not confirmation
const { writeContractAsync } = useWriteContract();
await writeContractAsync({...}); // Returns immediately after MetaMask signs!
// ✅ CORRECT: Scaffold hooks - waits for tx to be mined
const { writeContractAsync } = useScaffoldWriteContract("MyContract");
await writeContractAsync({...}); // Waits for actual on-chain confirmation
🚨 トークン/承認/セキュリティに関するコードを変更する前に
停止してください。トークンに関わるコードを新規作成または変更する前に、必ず下記の「重大な注意点」セクションを再読してください。対象となる項目:
- トークンの承認(
approve、allowance、transferFrom) - トークンの転送(
transfer、safeTransfer、safeTransferFrom) - アクセス制御または権限
- 価格計算またはオラクルの使用
- ボールトへの預け入れ/引き出し
これは任意ではありません。 「重大な注意点」セクションがあるのは、ここに挙げた間違いが実際に資金を失う原因となるからです。「さっと修正しよう」と思ったときこそ、必ず再読してください。
🚨 フロントエンド UX のルール(必須)
以下は提案ではなく、厳守すべきルールです。すべて満たすまでビルドは完了ではありません。 これらのルールは、失敗から学んだものです。省略しないでください。
ルール 1:オンチェーン操作を実行するすべてのボタンにローダーを表示し、無効化する
ブロックチェーンのトランザクションを実行するボタンは、必ず以下を行ってください。
- クリック時にすぐ無効化する
- ローダー/スピナー(「承認中…」「ステーキング中…」など)を表示する
- 状態の更新により操作の完了が確認されるまで無効のままにする
- 完了時に成功/エラーのフィードバックを表示する
// ✅ CORRECT: Separate loading state PER ACTION
const [isApproving, setIsApproving] = useState(false);
const [isStaking, setIsStaking] = useState(false);
<button
disabled={isApproving}
onClick={async () => {
setIsApproving(true);
try {
await writeContractAsync({ functionName: "approve", args: [...] });
} catch (e) {
console.error(e);
notification.error("Approval failed");
} finally {
setIsApproving(false);
}
}}
>
{isApproving ? "Approving..." : "Approve"}
</button>
❌ 複数のボタンで共通の isLoading を絶対に使わないでください。 ボタンごとに個別のローディング状態を用意してください。状態を共有すると、UI の条件付き切り替え時に、誤ったローディングテキストが表示されます。
ルール 2:3 つのボタンによるフロー — ネットワーク → 承認 → 操作
ユーザーがトークンを承認してから操作(ステーキング、預け入れ、スワップなど)を行う必要がある場合、状態は 3 つあります。一度に表示するボタンは必ず 1 つだけにしてください。
1. Wrong network? → "Switch to Base" button
2. Not enough approved? → "Approve" button
3. Enough approved? → "Stake" / "Deposit" / action button
// ALWAYS read allowance with a hook (auto-updates when tx confirms)
const { data: allowance } = useScaffoldReadContract({
contractName: "Token",
functionName: "allowance",
args: [address, contractAddress],
});
const needsApproval = !allowance || allowance < amount;
const wrongNetwork = chain?.id !== targetChainId;
{wrongNetwork ? (
<button onClick={switchNetwork} disabled={isSwitching}>
{isSwitching ? "Switching..." : "Switch to Base"}
</button>
) : needsApproval ? (
<button onClick={handleApprove} disabled={isApproving}>
{isApproving ? "Approving..." : "Approve $TOKEN"}
</button>
) : (
<button onClick={handleStake} disabled={isStaking}>
{isStaking ? "Staking..." : "Stake"}
</button>
)}
重要: フックを使って常に allowance を読み取り、UI が自動的に更新されるようにしてください。ローカル状態だけに頼らないでください。誤ったネットワーク上でユーザーが「承認」をクリックするとすべてが壊れます。そのため、ネットワークの誤りを最初に確認します。
ルール 3:アドレス表示 — 常に <Address/> を使用する
Ethereum アドレスを表示する場合は必ず、scaffold-eth の <Address/> コンポーネントを使用してください。
// ✅ CORRECT
import { Address } from "~~/components/scaffold-eth";
<Address address={userAddress} />
// ❌ WRONG — never render raw hex
<span>{userAddress}</span>
<p>0x1234...5678</p>
<Address/> は ENS の解決、ブロッキーアバター、クリップボードへのコピー、省略表示、ブロックエクスプローラーへのリンクに対応します。生の 16 進数表示は認められません。
ルール 3b:アドレス入力 — 常に <AddressInput/> を使用する
ユーザーが Ethereum アドレスを入力する必要がある場合は必ず、scaffold-eth の <AddressInput/> コンポーネントを使用してください。
// ✅ CORRECT
import { AddressInput } from "~~/components/scaffold-eth";
<AddressInput value={recipient} onChange={setRecipient} placeholder="Recipient address" />
// ❌ WRONG — never use a raw text input for addresses
<input type="text" value={recipient} onChange={e => setRecipient(e.target.value)} />
<AddressInput/> は ENS の解決(「vitalik.eth」と入力するとアドレスに解決)、ブロッキーアバターのプレビュー、検証、貼り付け処理を提供します。通常の入力欄では、これらの機能は何も利用できません。
使い分け:表示には <Address/>、入力には <AddressInput/>。常にこの組み合わせを使用してください。
Rule 3c: USD 値 — すべての場所でドル額を表示する
表示するすべてのトークン額または ETH 額に、USD での値を含めてください。 すべてのトークン入力または ETH 入力には、リアルタイムの USD プレビューを表示してください。
// ✅ CORRECT — Display with USD
<span>1,000 TOKEN (~$4.20)</span>
<span>0.5 ETH (~$1,250.00)</span>
// ✅ CORRECT — Input with live USD preview
<input value={amount} onChange={...} />
<span className="text-sm text-gray-500">
≈ ${(parseFloat(amount || "0") * tokenPrice).toFixed(2)} USD
</span>
// ❌ WRONG — Amount with no USD context
<span>1,000 TOKEN</span> // User has no idea what this is worth
価格の取得元:
- ETH 価格: SE2 には組み込みのフックがあります。
useNativeCurrencyPrice()を使うか、左下のフッターにある価格表示コンポーネントを確認してください。価格はメインネットの Uniswap V2 WETH/DAI プールから取得されます。 - カスタムトークン: DexScreener API(
https://api.dexscreener.com/latest/dex/tokens/TOKEN_ADDRESS)、オンチェーンの Uniswap クオーター、または利用可能であれば Chainlink オラクルを使ってください。
これは表示と入力の両方に適用されます:
- 残高を表示する場合は、隣に USD での値を表示してください。
- ユーザーが送信/ステーク/スワップする金額を入力する場合は、入力欄の下にリアルタイムの USD プレビューを表示してください。
- トランザクションの確認画面には、実行しようとしている操作の USD 値を表示してください。
Rule 3d: タイトルの重複禁止 — ヘッダーがタイトルです
ページ本文の先頭に、アプリ名を <h1> として配置しないでください。 ヘッダーにすでにアプリ名が表示されています。重複させるとスペースが無駄になり、素人っぽく見えます。
// ❌ WRONG — AI agents ALWAYS do this
<Header /> {/* Already shows "🦞 $TOKEN Hub" */}
<main>
<h1>🦞 $TOKEN Hub</h1> {/* DUPLICATE! Delete this. */}
<p>Buy, send, and track TOKEN on Base</p>
...
</main>
// ✅ CORRECT — Jump straight into content
<Header /> {/* Shows the app name */}
<main>
<div className="grid grid-cols-2 gap-4">
{/* Stats, balances, actions — no redundant title */}
</div>
</main>
SE2 のヘッダーコンポーネントは、すでにアプリのタイトルを表示します。 ページのコンテンツは、画面上部ですでに見えているものを繰り返すのではなく、統計情報、フォーム、データなど、実際の UI から始めてください。
Rule 4: RPC の設定 — 必ず Alchemy を使う
パブリック RPC(mainnet.base.org など)は使わないでください。レート制限があり、ランダムな障害の原因になります。
scaffold.config.ts では、必ず次のように設定してください:
rpcOverrides: {
[chains.base.id]: "https://base-mainnet.g.alchemy.com/v2/YOUR_KEY",
[chains.mainnet.id]: "https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY",
},
pollingInterval: 3000, // 3 seconds, not the default 30000
RPC の使用状況を監視してください: 適切な頻度は 3 秒に 1 リクエストです。毎秒 15 回以上のリクエストが見られる場合は、バグがあります:
- フックがループ内で再レンダリングされている
- フックが重複して呼び出されている
- 依存配列が指定されていない
- 必要のないフックで
watch: trueが指定されている
Rule 5: 公開前チェックリスト
フロントエンドを Vercel/本番環境にデプロイする前に:
Open Graph/Twitter カード(必須):
// In app/layout.tsx
export const metadata: Metadata = {
title: "Your App Name",
description: "Description of the app",
openGraph: {
title: "Your App Name",
description: "Description of the app",
images: [{ url: "https://YOUR-LIVE-DOMAIN.com/og-image.png" }],
},
twitter: {
card: "summary_large_image",
title: "Your App Name",
description: "Description of the app",
images: ["https://YOUR-LIVE-DOMAIN.com/og-image.png"],
},
};
⚠️ OG 画像の URL は、必ず次の条件を満たす必要があります:
https://で始まる絶対 URL- 本番環境の実際のドメイン(
localhostでも相対パスでもないこと) - 未設定または localhost になる可能性のある環境変数ではないこと
- 実際にアクセスできること(ブラウザーで URL を開いてテストしてください)
チェックリスト — すべての項目に合格する必要があります:
- OG 画像の URL が絶対 URL で、本番環境のドメインになっている
- OG のタイトルと説明が設定されている(SE2 のデフォルトテキストではない)
- Twitter カードのタイプが設定されている(
summary_large_image) - ファビコンが SE2 のデフォルトから変更されている
- README が SE2 のデフォルトから更新されている
- フッターの「Fork me」リンクが実際のリポジトリを指している(SE2 ではない)
- ブラウザーのタブタイトルが正しい
- RPC のオーバーライドが Alchemy に設定されている
-
pollingIntervalが 3000 に設定されている - すべてのコントラクトアドレスがデプロイ済みのものと一致している
- 本番コードにハードコードされたテストネット/localhost の値がない
- すべてのアドレス表示で
<Address/>を使っている - すべてのオンチェーンボタンに個別のローダーと無効状態がある
- 承認フローが「ネットワークの確認 → 承認 → アクション」のパターンになっている
🧪 ビルド検証プロセス(必須)
コードがコンパイルできた時点では、ビルドは完了していません。実際のユーザーのようにテストした時点で完了です。
Phase 1: コード QA(自動)
すべてのコードを記述した後、QA チェックスクリプトを実行するか、QA サブエージェントを起動してください:
- すべての
.tsxファイルをスキャンして、生のアドレス文字列がないか確認する(<Address/>を使うこと) - 複数のボタンで共有されている
isLoading状態がないかスキャンする - トランザクションボタンに
disabledプロパティが設定されていない箇所がないかスキャンする scaffold.config.tsにrpcOverridesとpollingInterval: 3000が設定されていることを確認するlayout.tsxに絶対 URL を含む OG/Twitter メタデータがあることを確認する- どのファイルにも
mainnet.base.orgや他のパブリック RPC が含まれていないことを確認する
Phase 2: スマートコントラクトのテスト
- Foundry テスト(
forge test)を記述して実行する - エッジケースをテストする:ゼロの金額、最大金額、権限のない呼び出し元
- コントラクト内のユーザーフロー全体をテストする(承認 → アクション → 状態の検証)
フェーズ 3:ブラウザテスト(本番テスト)
ブラウザがあります。ウォレットがあります。実際の資金があります。使ってください。
Base(またはフォーク)にデプロイしたら、アプリを開いて一通りテストしてください。
- アプリを開く — ブラウザツールで開き、スナップショットを取得して、読み込みが完了したことを確認します
- ページタイトルを確認する — 正しいタイトルですか?「Scaffold-ETH 2」になっていませんか?
- ウォレットを接続する — 接続フローは正常に動作しますか?
- 誤ったネットワークでのテスト — 誤ったネットワークで接続し、「Switch to Base」が表示されることを確認します
- ネットワークを切り替える — 切り替えボタンをクリックして、正常に動作することを確認します
- 承認フロー — アプリでトークンの承認が必要な場合:
- 承認量が不足しているときに「Approve」ボタンが表示されることを確認します
- 「Approve」をクリックします — ボタンは無効になりますか?「Approving...」と表示されますか?
- トランザクションが完了するまで待ちます — ボタンは再び表示されますか?UI は更新され、アクションボタンが表示されますか?
- メインアクション — 主要なアクション(ステーク、デポジット、ミントなど)をクリックします:
- ボタンは無効になり、ローダーが表示されますか?
- トランザクションは正常に実行されますか?
- 確認後に UI は更新されますか?
- 残高や状態の変化は正しく反映されますか?
- エラー処理 — ウォレットでトランザクションを拒否し、UI が適切に復旧することを確認します
- アドレス表示 — すべてのアドレスが生の 16 進数ではなく、ENS/ブロッキーズで表示されていますか?
- URL を共有する — OG の展開表示が正しいこと(画像、タイトル、説明)を確認します
これらすべてに合格して初めて、ユーザーに「完了しました」と伝えられます。
フェーズ 4:QA サブエージェントによるレビュー(複雑なビルド向け)
規模の大きなプロジェクトでは、新しいコンテキストでサブエージェントを起動します。
- リポジトリのパスとデプロイ済み URL を渡します
- 上記のルールに照らしてフロントエンドコードをすべて確認します
- ブラウザを開き、独立して操作を一通り確認します
- リリース前に問題を報告します
デフォルトスタック:Fork Mode の Scaffold-ETH 2
ユーザーが Ethereum プロジェクトの構築を希望した場合は、次の手順に従います。
ステップ 1:プロジェクトを作成する
npx create-eth@latest
# Select: foundry (recommended), target chain, project name
ステップ 2:ポーリング間隔を修正する
packages/nextjs/scaffold.config.ts を編集し、次のように変更します。
pollingInterval: 30000, // Default: 30 seconds (way too slow!)
を次のようにします。
pollingInterval: 3000, // 3 seconds (much better for development)
ステップ 3:ライブネットワークをインストールしてフォークする
cd <project-name>
yarn install
yarn fork --network base # or mainnet, arbitrum, optimism, polygon
⚠️ 重要:フォークモードを使用する場合、フロントエンドのターゲットネットワークはフォーク元のチェーンではなく、必ず chains.foundry(チェーン ID 31337)にしてください。
フォークはチェーン ID 31337 の Anvil 上でローカルに実行されます。Base、Arbitrum などをフォークする場合でも、scaffold の設定では次を指定する必要があります。
targetNetworks: [chains.foundry], // NOT chains.base!
実際のネットワークにデプロイする場合に限り、chains.base(または他のチェーン)に切り替えます。
ステップ 4:自動ブロックマイニングを有効にする(必須!)
# In a new terminal, enable interval mining (1 block/second)
cast rpc anvil_setIntervalMining 1
これを設定しないと、block.timestamp が凍結されたままとなり、時間依存のロジックが動作しなくなります!
任意:設定を永続化するには、packages/foundry/package.json を編集し、フォークスクリプトに --block-time 1 を追加します。
ステップ 5:ローカルフォークにデプロイする(無料!)
yarn deploy
ステップ 6:フロントエンドを起動する
yarn start
ステップ 7:フロントエンドをテストする
フロントエンドが起動したら、ブラウザを開いてアプリをテストします。
http://localhost:3000に移動します- スナップショットを取得してページ要素を確認します(バーンウォレットのアドレスはヘッダーにあります)
- フォーセットをクリックして、バーンウォレットに ETH を入金します
- 必要に応じてクジラアドレスからトークンを転送します(ページに表示されているバーンアドレスを使用します)
- アプリを操作して、機能を確認します
ブラウザーの自動操作には cursor-browser-extension MCP ツールを使用してください。
詳しいワークフローについては、tools/testing/frontend-testing.md を参照してください。
Scaffold-ETH 2 プロジェクトを公開する場合:
- README.md を更新する — デフォルトの SE2 README を、プロジェクトの説明に置き換えます
- フッターのリンクを更新する —
packages/nextjs/components/Footer.tsxで、「Fork me」リンクをhttps://github.com/scaffold-eth/se-2から実際のリポジトリ URL に変更します - ページタイトルを更新する —
packages/nextjs/app/layout.tsxでメタデータのタイトル/説明を変更します - 「Debug Contracts」のナビゲーションリンクを削除する —
packages/nextjs/components/Header.tsxで、menuLinksから Debug Contracts の項目を削除します - OG/Twitter メタデータを設定する — 上記のルール 5 にある公開前チェックリストに従います
🚀 SE2 デプロイのクイック判断ツリー
Want to deploy SE2 to production?
│
├─ IPFS (recommended) ──→ yarn ipfs (local build, no memory limits)
│ └─ Fails with "localStorage.getItem is not a function"?
│ └─ Add NODE_OPTIONS="--require ./polyfill-localstorage.cjs"
│ (Node 25+ has broken localStorage — see below)
│
├─ Vercel ──→ Set rootDirectory=packages/nextjs, installCommand="cd ../.. && yarn install"
│ ├─ Fails with "No Next.js version detected"?
│ │ └─ Root Directory not set — fix via Vercel API or dashboard
│ ├─ Fails with "cd packages/nextjs: No such file or directory"?
│ │ └─ Build command still has "cd packages/nextjs" — clear it (root dir handles this)
│ └─ Fails with OOM / exit code 129?
│ └─ Build machine can't handle SE2 monorepo — use IPFS instead or vercel --prebuilt
│
└─ Any path: "TypeError: localStorage.getItem is not a function"
└─ Node 25+ bug. Use --require polyfill (see IPFS section below)
Vercel への SE2 のデプロイ(モノレポの設定):
SE2 はモノレポです。Vercel では特別な設定が必要です。
- Vercel プロジェクトの設定で Root Directory を
packages/nextjsに設定します - Install Command を
cd ../.. && yarn installに設定します(ワークスペースのルートからインストールします) - Build Command はデフォルト(自動検出される
next build)のままにします - Output Directory はデフォルト(
.next)のままにします
Vercel API を使用する場合:
curl -X PATCH "https://api.vercel.com/v9/projects/PROJECT_ID" \
-H "Authorization: Bearer $VERCEL_TOKEN" \
-H "Content-Type: application/json" \
-d '{"rootDirectory": "packages/nextjs", "installCommand": "cd ../.. && yarn install"}'
CLI を使用する場合(リンク後):
cd your-se2-project && vercel --prod --yes
⚠️ よくある間違い: ビルドコマンドに cd packages/nextjs を入れないでください。Root Directory の設定により、Vercel はすでに packages/nextjs にいます。また、ルート階層の vercel.json で framework: "nextjs" を指定しないでください。Vercel はルートの package.json で Next.js を見つけられず、失敗します。
⚠️ Vercel の OOM(メモリ不足): SE2 のモノレポ全体(foundry、nextjs、すべての依存関係)のインストールでは、Vercel の 8GB のビルドメモリ上限を超える場合があります。「Out of Memory」または終了コード 129 でビルドが失敗した場合は、次の方法を試してください。
- オプション A: 環境変数
NODE_OPTIONS=--max-old-space-size=7168を追加します - オプション B(推奨): ローカルでビルドし、代わりに IPFS にプッシュします(
yarn ipfs) - オプション C:
vercel --prebuiltを使用します(ローカルでビルドし、出力を Vercel にデプロイします)
SE2 を IPFS(BuidlGuidl IPFS)にデプロイする:
これは SE2 で推奨されるデプロイ方法です。 Vercel のメモリ制限を完全に回避できます。
cd packages/nextjs
NODE_OPTIONS="--require ./polyfill-localstorage.cjs" NEXT_PUBLIC_IPFS_BUILD=true NEXT_PUBLIC_IGNORE_BUILD_ERROR=true yarn build
yarn bgipfs upload config init -u https://upload.bgipfs.com -k "$BGIPFS_API_KEY"
yarn bgipfs upload out
または、組み込みスクリプトを使用します(ポリフィルが含まれている場合):
yarn ipfs
⚠️ 重要:Node 25 以降の localStorage のバグ
Node.js 25 以降には、標準の WebStorage API メソッド(getItem、setItem など)が欠けている組み込みの localStorage オブジェクトが付属しています。このため、静的ページ生成(SSG/エクスポート)中に localStorage.getItem() を呼び出す next-themes、RainbowKit、およびその他のライブラリで問題が発生します。
表示されるエラー:
TypeError: localStorage.getItem is not a function
Error occurred prerendering page "/_not-found"
解決方法: packages/nextjs/ に polyfill-localstorage.cjs を作成します。
// Polyfill localStorage for Node 25+ static export builds
if (typeof globalThis.localStorage !== "undefined" && typeof globalThis.localStorage.getItem !== "function") {
const store = new Map();
globalThis.localStorage = {
getItem: (key) => store.get(key) ?? null,
setItem: (key, value) => store.set(key, String(value)),
removeItem: (key) => store.delete(key),
clear: () => store.clear(),
key: (index) => [...store.keys()][index] ?? null,
get length() { return store.size; },
};
}
次に、ビルドの前に NODE_OPTIONS="--require ./polyfill-localstorage.cjs" を付けます。
instrumentation.ts や next.config.ts ではなく、なぜ --require を使うのか?
next.config.tsのポリフィルはメインプロセスでのみ実行されますinstrumentation.tsはビルドワーカーでは実行されません--requireはビルドワーカーを含むすべての Node プロセスに挿入されます ✅
この問題が発生する理由: 静的ページをプリレンダリングするために、Next.js が別のビルドワーカープロセスを起動するので、ポリフィルが必要です。このワーカーは NODE_OPTIONS を引き継ぐため、ライブラリのコードが実行される前にポリフィルを確実に実行するには --require が唯一の方法です。
⚠️ blockexplorer のページ: SE2 に組み込まれているブロックエクスプローラーは、インポート時に localStorage を使用するため、静的エクスポート中にも失敗します。無効にする(app/blockexplorer を app/_blockexplorer-disabled に名前変更する)か、ポリフィルが有効になっていることを確認してください。
🚨 古いビルド/古いデプロイ — IPFS で最も多い落とし穴
問題: page.tsx を編集した後、以前のデプロイで使った古い IPFS URL をユーザーに渡してしまいます。ソースコードには変更が反映されているのに、out/ ディレクトリには古いビルドが残っています。この問題は何度も発生しています。
根本原因: ビルド手順(yarn build)で out/ が生成されます。ビルド後、デプロイ前にソースファイルを編集すると、古い出力がデプロイされます。さらに悪い場合は、ビルドを完全に省略し、古い out/ をそのまま再アップロードしてしまいます。
必須:コードを変更した後は必ず、次の一連の手順をすべて実行してください。
# 1. Delete old build artifacts (prevents any caching)
rm -rf .next out
# 2. Rebuild from scratch
NODE_OPTIONS="--require ./polyfill-localstorage.cjs" NEXT_PUBLIC_IPFS_BUILD=true NEXT_PUBLIC_IGNORE_BUILD_ERROR=true yarn build
# 3. VERIFY the new build has your changes (spot-check the JS bundle)
grep -l "YOUR_UNIQUE_STRING" out/_next/static/chunks/app/*.js
# 4. Only THEN upload
yarn bgipfs upload out
古いデプロイを検出する方法:
# Compare timestamps — source must be OLDER than out/
stat -f '%Sm' app/page.tsx # source modified time
stat -f '%Sm' out/ # build output time
# If source is NEWER than out/ → BUILD IS STALE, rebuild first!
CID が証拠になります: デプロイ後に IPFS CID が変わっていなければ、同じコンテンツをデプロイしたということです。コードを実際に変更すれば、必ず新しい CID が生成されます。
🚨 IPFS ルーティング — ルートが機能しなくなる原因と解決方法
IPFS ゲートウェイは静的ファイルを配信します。ルーティングを処理するサーバーはありません。/debug のようなルートを機能させるには、次の3つの条件をすべて満たす必要があります。
1. next.config.ts の output: "export"
これがないと、Next.js はサーバーサイドレンダリング用にビルドされます。静的 HTML ファイルが生成されないため、IPFS が配信できるものはありません。
2. next.config.ts の trailingSlash: true(重要)
これが、IPFS でルートが機能しなくなる最大の原因です。
trailingSlash: false(デフォルト)→debug.htmlを生成しますtrailingSlash: true→debug/index.htmlを生成します
IPFS ゲートウェイはディレクトリを自動的に index.html に解決しますが、ファイル名だけのパスは解決しません。そのため、/debug → ディレクトリ debug/ を探す → index.html が見つかります ✅。末尾のスラッシュがない場合、/debug → ディレクトリがなく、ファイル名も一致しない → 404 になります ❌。
3. ルートは静的エクスポートのプリレンダリングを通過する必要があります
output: "export" を設定して yarn build を実行すると、Next.js はすべてのページを HTML にプリレンダリングします。プリレンダリング中にページがクラッシュすると(ブラウザー API が必要なフックや、localStorage.getItem is not a function など)、そのルートはスキップされます。HTML ファイルが生成されないため、IPFS で 404 になります。
プリレンダリングを妨げる主な原因:
- インポート時に
localStorage/sessionStorageを使用している - ブラウザー環境(
window、document)を前提とするフック - SE2 のブロックエクスプローラーページ(インポート時に
localStorageを使用します。不要な場合は_blockexplorer-disabledに名前を変更してください)
ビルド後にルートを確認する方法:
# Check that out/ has a directory + index.html for each route
ls out/*/index.html
# Should show: out/debug/index.html, out/other-route/index.html, etc.
# Verify specific route
curl -s -o /dev/null -w "%{http_code}" -L "https://YOUR_GATEWAY/ipfs/CID/debug/"
# Should return 200, not 404
IPFS で安全に使える next.config.ts の完全なパターン:
const isIpfs = process.env.NEXT_PUBLIC_IPFS_BUILD === "true";
if (isIpfs) {
nextConfig.output = "export"; // static HTML generation
nextConfig.trailingSlash = true; // route/index.html (IPFS needs this!)
nextConfig.images = {
unoptimized: true, // no image optimization server on IPFS
};
}
🚀 本番環境へデプロイ — 完全なチェックリスト
ユーザーが「デプロイして」と言ったら、以下の手順を必ず順番どおりに実行してください。 🤖の印が付いた手順はすべて自動です。👤の印が付いた手順では人間の入力が必要です。
Step 1: 🤖 最終コードレビュー
- すべてのフィードバックがソースコードに反映されていることを確認します
- 最後にもう一度ローカルでテストします(
yarn start) - よくある問題を確認します:h1の重複、AddressInputの不足、プレーンテキストの入力欄
Step 2: 👤 希望するドメインをユーザーに尋ねる
次のように尋ねます:「このアプリにどのサブドメインを使いますか? 例:token.yourname.eth → token.yourname.eth.limo」
回答を保存してください。これはメタデータとENSの設定に使う本番URLを決めるものです。
Step 3: 🤖 OG画像を生成し、アンファール用のメタデータを修正する
ソーシャルメディアのアンファール(Twitter、Telegram、Discordなど)を正しく表示するには、次の3点が必要です:
- カスタムOG画像(1200x630 PNG)— SE2の標準サムネイルは使わないでください
- og:imageに本番環境の絶対URLを指定する —
localhost:3000は使わないでください - 大きなプレビューを表示するため、
twitter:cardをsummary_large_imageに設定する
OG画像を生成する(public/thumbnail.png、1200x630):
# Use PIL/Pillow to create a branded 1200x630 OG image with:
# - App name and tagline
# - Production URL (name.yourname.eth.limo)
# - Dark background, clean layout, accent colors
# Save to: packages/nextjs/public/thumbnail.png
メタデータのbaseUrlを修正する — utils/scaffold-eth/getMetadata.tsがNEXT_PUBLIC_PRODUCTION_URLに対応していることを確認します:
const baseUrl = process.env.NEXT_PUBLIC_PRODUCTION_URL
? process.env.NEXT_PUBLIC_PRODUCTION_URL
: process.env.VERCEL_PROJECT_PRODUCTION_URL
? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
: `http://localhost:${process.env.PORT || 3000}`;
この環境変数のパターンがすでにファイルにある場合は、この手順を省略してください。
Step 4: 🤖 クリーンビルドとIPFSへのデプロイ
cd packages/nextjs
rm -rf .next out
NEXT_PUBLIC_PRODUCTION_URL="https://<name>.yourname.eth.limo" \
NODE_OPTIONS="--require ./polyfill-localstorage.cjs" \
NEXT_PUBLIC_IPFS_BUILD=true NEXT_PUBLIC_IGNORE_BUILD_ERROR=true \
yarn build
# VERIFY (all 3 must pass before uploading):
ls out/*/index.html # routes exist
grep 'og:image' out/index.html # NOT localhost
stat -f '%Sm' app/page.tsx && stat -f '%Sm' out/ # source older than build
# Upload:
yarn bgipfs upload out
# Save the CID!
Step 5: 👤 確認用のIPFS URLを共有する
次のように送信します:「レビュー用のビルドはこちらです:https://community.bgipfs.com/ipfs/<CID>」
ENSに触れる前に、必ず承認を待ってください。 ユーザーが進めてよいと言うまで、先に進まないでください。
Step 6: 🤖 ENSサブドメインを設定する(メインネットで2件のトランザクション)
新しいアプリの場合(サブドメインがまだ存在しない場合):
Tx #1 — サブドメインを作成する:
- ウォレットのブラウザー(ウォレットのプロフィール)で
https://app.ens.domains/yourname.ethを開きます - 「Subnames」タブ → 「New subname」に進みます
- ラベル(例:
token)を入力 → Next → Skip profile → Open Wallet → Confirm - ガスが詰まった場合:MetaMaskをEthereumネットワークに切り替え → Activityタブ → 「Speed up」
Tx #2 — IPFSコンテンツハッシュを設定する:
https://app.ens.domains/<name>.yourname.ethに移動します- 「Records」タブ → 「Edit Records」 → 「Other」タブに進みます
- Content Hash欄に貼り付けます:
ipfs://<CID> - 保存 → Open Wallet → MetaMaskでConfirm
既存アプリの更新の場合:Tx #1を省略し、Tx #2(コンテンツハッシュの更新)のみを実行します。
Step 7: 🤖 すべてを確認する
# 1. ENS content hash matches (on-chain)
RESOLVER=$(cast call 0x00000000000C2e074eC69A0dFb2997BA6C7d2e1e \
"resolver(bytes32)(address)" $(cast namehash <name>.yourname.eth) \
--rpc-url https://eth-mainnet.g.alchemy.com/v2/<KEY>)
cast call $RESOLVER "contenthash(bytes32)(bytes)" \
$(cast namehash <name>.yourname.eth) --rpc-url <RPC>
# 2. .limo gateway responds (may take a few minutes for cache)
curl -s -o /dev/null -w "%{http_code}" -L "https://<name>.yourname.eth.limo"
# 3. OG metadata correct
curl -s -L "https://<name>.yourname.eth.limo" | grep 'og:image'
# Should show the production URL, NOT localhost
Step 8: 👤 ユーザーに報告する
次のように送信します:「https://<name>.yourname.eth.limoで公開されました。アンファール用のメタデータを設定し、ENSのコンテンツハッシュがオンチェーンで確認済みです。」
⚠️ 既知の注意点:
- MetaMaskのガス代: ENSアプリで0.2 gweiが提案されることがありますが、メインネットではそれ以上が必要です。詰まった場合は「Speed up」を使用してください。
- .limoのキャッシュ: ゲートウェイはコンテンツを約5~15分間キャッシュします。オンチェーンのハッシュはすぐに更新されますが、.limoで古いコンテンツが一時的に配信されることがあります。
- 標準サムネイル: SE2にはデフォルトの
thumbnail.pngとthumbnail.jpgが付属しています。本番環境に移行する前に、必ず両方を置き換えてください。 - メタデータ内のlocalhost:
NEXT_PUBLIC_PRODUCTION_URLが設定されていない場合、og:imageはlocalhost:3000を指します。必ずgrepで確認してください。
実行しないでください:
yarn chainを実行する(代わりにyarn fork --network <chain>を使用してください!)forge initを手動で実行したり、Foundryを一からセットアップしたりする- Next.jsプロジェクトを手動で作成する
- ウォレット接続を手動で設定する(SE2にはRainbowKitが事前設定されています)
フォークモードを使う理由
yarn chain (WRONG) yarn fork --network base (CORRECT)
└─ Empty local chain └─ Fork of real Base mainnet
└─ No protocols └─ Uniswap, Aave, etc. available
└─ No tokens └─ Real USDC, WETH exist
└─ Testing in isolation └─ Test against REAL state
利用可能なアドレスデータ
トークン、プロトコル、クジラのアドレスはdata/addresses/にあります:
tokens.json- 各チェーンのWETH、USDC、DAIなどprotocols.json- 各チェーンのUniswap、Aave、Chainlinkwhales.json- テスト用資金として使う大口トークン保有者
最も重要な概念
Ethereumでは、何も自動的には実行されません。
スマートコントラクトは自分自身で実行できません。cronジョブも、スケジューラーも、バックグラウンドプロセスもありません。「実行される必要がある」すべての関数について、次の点を満たしてください。
- 誰でも(管理者だけではなく)呼び出せるようにする
- 呼び出す人に理由を与える(利益、報酬、自分自身の利益)
- ガス代と利益をまかなえる十分なインセンティブを設ける
常に「誰がこの関数を呼び出すのか? なぜガス代を払うのか?」と考えてください。
この問いに答えられない場合、その関数は呼び出されません。
適切なインセンティブ設計の例
// LIQUIDATIONS: Caller gets bonus collateral
function liquidate(address user) external {
require(getHealthFactor(user) < 1e18, "Healthy");
uint256 bonus = collateral * 5 / 100; // 5% bonus
collateralToken.transfer(msg.sender, collateral + bonus);
}
// YIELD HARVESTING: Caller gets % of harvest
function harvest() external {
uint256 yield = protocol.claimRewards();
uint256 callerReward = yield / 100; // 1%
token.transfer(msg.sender, callerReward);
}
// CLAIMS: User wants their own tokens
function claimRewards() external {
uint256 reward = pendingRewards[msg.sender];
pendingRewards[msg.sender] = 0;
token.transfer(msg.sender, reward);
}
重要な注意点(必ず覚えてください)
1. トークンの小数桁数は異なります
USDCは小数桁数が6桁で、18桁ではありません!
// BAD: Assumes 18 decimals - transfers 1 TRILLION USDC!
uint256 oneToken = 1e18;
// GOOD: Check decimals
uint256 oneToken = 10 ** token.decimals();
一般的な小数桁数:
- USDC、USDT:小数桁数は6桁
- WBTC:小数桁数は8桁
- ほとんどのトークン(DAI、WETH):小数桁数は18桁
2. ERC-20ではApproveパターンが必要です
コントラクトがトークンを直接引き出すことはできません。次の2段階の手順が必要です。
// Step 1: User approves
token.approve(spenderContract, amount);
// Step 2: Contract pulls tokens
token.transferFrom(user, address(this), amount);
無制限の承認は使わないでください。
// DANGEROUS
token.approve(spender, type(uint256).max);
// SAFE
token.approve(spender, exactAmount);
3. Solidityには浮動小数点数がありません
ベーシスポイントを使用してください(1 bp = 0.01%)。
// BAD: This equals 0
uint256 fivePercent = 5 / 100;
// GOOD: Basis points
uint256 FEE_BPS = 500; // 5% = 500 basis points
uint256 fee = (amount * FEE_BPS) / 10000;
4. リエントランシー攻撃
外部呼び出しによって、コントラクトが再度呼び出されることがあります。
// SAFE: Checks-Effects-Interactions pattern
function withdraw() external nonReentrant {
uint256 bal = balances[msg.sender];
balances[msg.sender] = 0; // Effect BEFORE interaction
(bool success,) = msg.sender.call{value: bal}("");
require(success);
}
必ずOpenZeppelinのReentrancyGuardを使用してください。
5. DEXのスポット価格をオラクルとして使わないでください
フラッシュローンによって、スポット価格は瞬時に操作される可能性があります。
// SAFE: Use Chainlink
function getPrice() internal view returns (uint256) {
(, int256 price,, uint256 updatedAt,) = priceFeed.latestRoundData();
require(block.timestamp - updatedAt < 3600, "Stale");
require(price > 0, "Invalid");
return uint256(price);
}
6. Vaultのインフレーション攻撃
最初の預入者は、シェアを操作して資金を盗める可能性があります。
// Mitigation: Virtual offset
function convertToShares(uint256 assets) public view returns (uint256) {
return assets.mulDiv(totalSupply() + 1e3, totalAssets() + 1);
}
7. SafeERC20を使用してください
一部のトークン(USDT)は、転送時にbool値を返しません。
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
using SafeERC20 for IERC20;
token.safeTransfer(to, amount); // Handles non-standard tokens
Scaffold-ETH 2の開発
プロジェクト構成
packages/
├── foundry/ # Smart contracts
│ ├── contracts/ # Your Solidity files
│ └── script/ # Deploy scripts
└── nextjs/
├── app/ # React pages
└── contracts/ # Generated ABIs + externalContracts.ts
必須のフック
// Read contract data
const { data } = useScaffoldReadContract({
contractName: "YourContract",
functionName: "greeting",
});
// Write to contract
const { writeContractAsync } = useScaffoldWriteContract("YourContract");
// Watch events
useScaffoldEventHistory({
contractName: "YourContract",
eventName: "Transfer",
fromBlock: 0n,
});
SpeedRun Ethereumのチャレンジ
実践的な学習には、以下を参考にしてください。
| チャレンジ | 概念 | 主な学び |
|---|---|---|
| 0: シンプルなNFT | ERC-721 | ミント、メタデータ、tokenURI |
| 1: ステーキング | 調整 | 期限、エスクロー、しきい値 |
| 2: トークンベンダー | ERC-20 | Approveパターン、売買 |
| 3: サイコロゲーム | 乱数 | オンチェーンの乱数は安全ではない |
| 4: DEX | AMM | x*y=kの式、スリッページ |
| 5: オラクル | 価格フィード | Chainlink、操作耐性 |
| 6: レンディング | 担保 | ヘルスファクター、清算インセンティブ |
| 7: ステーブルコイン | ペッグ | CDP、過剰担保 |
| 8: 予測市場 | 決済 | 結果の判定 |
| 9: ZK投票 | プライバシー | ゼロ知識証明 |
| 10: マルチシグ | 署名 | しきい値承認 |
| 11: SVG NFT | オンチェーンアート | 生成、base64エンコード |
DeFiプロトコルのパターン
Uniswap(AMM)
- 定積積の公式:x * y = k
- スリッページ保護が必要
- LPトークンはプールの持分を表す
Aave(レンディング)
- 担保を預け入れ、資産を借り入れる
- ヘルスファクター = 担保価値 / 負債価値
- ヘルスファクターが1未満になると清算
ERC-4626(トークン化Vault)
- 利回りを生むVaultの標準インターフェース
- シェアを考慮した入出金
- インフレーション攻撃から保護する
セキュリティレビューのチェックリスト
デプロイ前に、以下を確認してください。
- すべての管理者関数にアクセス制御がある
- リエントランシー保護がある(CEI + nonReentrant)
- トークンの小数桁数を正しく処理している
- オラクル操作への耐性がある
- 整数オーバーフローに対処している(0.8+またはSafeMath)
- 戻り値を確認している(SafeERC20)
- 入力検証を実装している
- 状態変更時にイベントを発行している
- メンテナンス関数のインセンティブを設計している
- 無制限の承認をしていない(正確な量を使用し、
type(uint256).maxは絶対に入力しない)
回答ガイドライン
開発者を支援する際は、次の点に従ってください。
- フォークのワークフローに従う — 常に
yarn forkを使用し、yarn chainは使わない - 質問に直接答える — まず質問に答える
- コードを示す — 動作する例を提示する
- 注意点を警告する — 関連する落とし穴を事前に伝える
- チャレンジを紹介する — 練習用にSpeedRun Ethereumを案内する
- インセンティブについて尋ねる — 「自動」実行される関数については、誰が呼び出すのか、なぜ呼び出すのかを尋ねる