ethereum-wingman

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Ethereum Wingman

Ethereum 开发助手

Comprehensive Ethereum development guide for AI agents. Covers smart contract development, DeFi protocols, security best practices, and the SpeedRun Ethereum curriculum.

面向AI代理的全面以太坊开发指南,涵盖智能合约开发、DeFi协议、安全最佳实践以及SpeedRun Ethereum学习课程。

AI AGENT INSTRUCTIONS - READ THIS FIRST

AI代理指令 - 请先阅读

🚫 CRITICAL: External Contracts & Scaffold Hooks

🚫 关键注意事项:外部合约与Scaffold钩子

These rules are MANDATORY. Violations cause real bugs in production.
  1. ALL CONTRACTS IN externalContracts.ts — Any contract you want to interact with (tokens, protocols, etc.) MUST be added to
    packages/nextjs/contracts/externalContracts.ts
    with its address and ABI. Read the file first — the pattern is self-evident.
  2. SCAFFOLD HOOKS ONLY — NEVER RAW WAGMI — Always use
    useScaffoldReadContract
    and
    useScaffoldWriteContract
    , NEVER raw wagmi hooks like
    useWriteContract
    or
    useReadContract
    .
Why this matters: Scaffold hooks use
useTransactor
which waits for transaction confirmation (not just wallet signing). Raw wagmi's
writeContractAsync
resolves the moment the user signs in MetaMask — BEFORE the tx is mined. This causes buttons to re-enable while transactions are still pending.
typescript
// ❌ 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
以下规则为强制性要求,违反会导致生产环境出现真实漏洞。
  1. 所有合约必须放入externalContracts.ts — 任何你需要交互的合约(代币、协议等)都必须添加到
    packages/nextjs/contracts/externalContracts.ts
    中,同时填写地址和ABI。请先阅读该文件,其模式一目了然。
  2. 仅使用Scaffold钩子 — 禁止使用原生wagmi — 始终使用
    useScaffoldReadContract
    useScaffoldWriteContract
    ,绝对不要使用原生wagmi钩子如
    useWriteContract
    useReadContract
原因: Scaffold钩子使用
useTransactor
,它会等待交易确认(而不仅仅是钱包签名)。原生wagmi的
writeContractAsync
在用户在MetaMask中签名后就会返回,此时交易尚未上链。这会导致按钮在交易仍处于待处理状态时重新启用。
typescript
// ❌ 错误:原生wagmi - 签名后立即返回,不等待确认
const { writeContractAsync } = useWriteContract();
await writeContractAsync({...}); // MetaMask签名后立即返回!

// ✅ 正确:Scaffold钩子 - 等待交易上链确认
const { writeContractAsync } = useScaffoldWriteContract("MyContract");
await writeContractAsync({...}); // 等待真实的链上确认

🚨 BEFORE ANY TOKEN/APPROVAL/SECURITY CODE CHANGE

🚨 在进行任何代币/授权/安全代码修改前

STOP. Re-read the "Critical Gotchas" section below before writing or modifying ANY code that touches:
  • Token approvals (
    approve
    ,
    allowance
    ,
    transferFrom
    )
  • Token transfers (
    transfer
    ,
    safeTransfer
    ,
    safeTransferFrom
    )
  • Access control or permissions
  • Price calculations or oracle usage
  • Vault deposits/withdrawals
This is not optional. The gotchas section exists because these are the exact mistakes that lose real money. Every time you think "I'll just quickly fix this" is exactly when you need to re-read it.

请暂停。在编写或修改任何涉及以下内容的代码前,重新阅读下方的“关键陷阱”部分:
  • 代币授权(
    approve
    allowance
    transferFrom
  • 代币转账(
    transfer
    safeTransfer
    safeTransferFrom
  • 访问控制或权限
  • 价格计算或预言机使用
  • 金库存款/取款
这并非可选步骤。 陷阱部分的存在是因为这些正是会导致真实资金损失的错误。每当你觉得“我快速修复一下就行”的时候,恰恰是你需要重新阅读该部分的时候。

🚨 FRONTEND UX RULES (MANDATORY)

🚨 前端UX规则(强制性要求)

These are HARD RULES, not suggestions. A build is NOT done until all of these are satisfied. These rules have been learned the hard way. Do not skip them.
这些是硬性规则,而非建议。只有满足所有这些规则,构建工作才算完成。 这些规则是从惨痛教训中总结而来,请不要跳过。

Rule 1: Every Onchain Button — Loader + Disable

规则1:所有链上按钮 — 加载状态 + 禁用状态

ANY button that triggers a blockchain transaction MUST:
  1. Disable immediately on click
  2. Show a loader/spinner ("Approving...", "Staking...", etc.)
  3. Stay disabled until the state updates confirm the action completed
  4. Show success/error feedback when done
typescript
// ✅ 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>
❌ NEVER use a single shared
isLoading
for multiple buttons.
Each button gets its own loading state. A shared state causes the WRONG loading text to appear when UI conditionally switches between buttons.
任何触发区块链交易的按钮必须:
  1. 点击后立即禁用
  2. 显示加载器/加载动画(如“授权中...”、“质押中...”等)
  3. 保持禁用状态直到状态更新确认操作完成
  4. 操作完成后显示成功/错误反馈
typescript
// ✅ 正确:为每个操作设置独立的加载状态
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("授权失败");
    } finally {
      setIsApproving(false);
    }
  }}
>
  {isApproving ? "授权中..." : "授权"}
</button>
❌ 绝对不要为多个按钮使用单一共享的
isLoading
状态。
每个按钮都应有自己的加载状态。共享状态会导致UI在按钮间切换时显示错误的加载文本。

Rule 2: Three-Button Flow — Network → Approve → Action

规则2:三步按钮流程 — 网络检查 → 授权 → 操作

When a user needs to approve tokens then perform an action (stake, deposit, swap), there are THREE states. Show exactly ONE button at a time:
1. Wrong network?       → "Switch to Base" button
2. Not enough approved? → "Approve" button
3. Enough approved?     → "Stake" / "Deposit" / action button
typescript
// 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>
)}
Critical: Always read allowance via a hook so UI updates automatically. Never rely on local state alone. If the user clicks Approve while on the wrong network, EVERYTHING BREAKS — that's why wrong network check comes FIRST.
当用户需要先授权代币再执行操作(质押、存款、兑换)时,存在三种状态。每次仅显示一个按钮:
1. 网络错误?       → “切换至Base网络”按钮
2. 授权额度不足? → “授权”按钮
3. 授权额度充足?     → “质押” / “存款” / 操作按钮
typescript
// 始终通过钩子读取授权额度(交易确认后自动更新)
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 ? "切换中..." : "切换至Base网络"}
  </button>
) : needsApproval ? (
  <button onClick={handleApprove} disabled={isApproving}>
    {isApproving ? "授权中..." : "授权 $TOKEN"}
  </button>
) : (
  <button onClick={handleStake} disabled={isStaking}>
    {isStaking ? "质押中..." : "质押"}
  </button>
)}
关键: 始终通过钩子读取授权额度,这样UI会自动更新。永远不要仅依赖本地状态。如果用户在错误网络上点击授权,所有功能都会崩溃 — 这就是为什么网络检查要放在第一步。

Rule 3: Address Display — Always
<Address/>

规则3:地址显示 — 始终使用
<Address/>
组件

EVERY time you display an Ethereum address, use scaffold-eth's
<Address/>
component.
typescript
// ✅ CORRECT
import { Address } from "~~/components/scaffold-eth";
<Address address={userAddress} />

// ❌ WRONG — never render raw hex
<span>{userAddress}</span>
<p>0x1234...5678</p>
<Address/>
handles ENS resolution, blockie avatars, copy-to-clipboard, truncation, and block explorer links. Raw hex is unacceptable.
每次显示以太坊地址时,都要使用scaffold-eth的
<Address/>
组件。
typescript
// ✅ 正确
import { Address } from "~~/components/scaffold-eth";
<Address address={userAddress} />

// ❌ 错误 — 永远不要直接渲染十六进制地址
<span>{userAddress}</span>
<p>0x1234...5678</p>
<Address/>
组件支持ENS解析、区块头像、复制到剪贴板、地址截断以及区块浏览器链接。直接显示十六进制地址是不可接受的。

Rule 3b: Address Input — Always
<AddressInput/>

规则3b:地址输入 — 始终使用
<AddressInput/>
组件

EVERY time the user needs to enter an Ethereum address, use scaffold-eth's
<AddressInput/>
component.
typescript
// ✅ 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/>
provides ENS resolution (type "vitalik.eth" → resolves to address), blockie avatar preview, validation, and paste handling. A raw input gives none of this.
The pair:
<Address/>
for DISPLAY,
<AddressInput/>
for INPUT. Always.
每次需要用户输入以太坊地址时,都要使用scaffold-eth的
<AddressInput/>
组件。
typescript
// ✅ 正确
import { AddressInput } from "~~/components/scaffold-eth";
<AddressInput value={recipient} onChange={setRecipient} placeholder="接收方地址" />

// ❌ 错误 — 永远不要为地址使用原生文本输入框
<input type="text" value={recipient} onChange={e => setRecipient(e.target.value)} />
<AddressInput/>
组件支持ENS解析(输入“vitalik.eth”会解析为对应地址)、区块头像预览、验证以及粘贴处理。原生输入框不具备这些功能。
搭配使用: 显示地址用
<Address/>
,输入地址用
<AddressInput/>
,必须严格遵守。

Rule 3c: USD Values — Show Dollar Amounts Everywhere

规则3c:USD价值 — 始终显示美元金额

EVERY token or ETH amount displayed should include its USD value. EVERY token or ETH input should show a live USD preview.
typescript
// ✅ 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
Where to get prices:
  • ETH price: SE2 has a built-in hook —
    useNativeCurrencyPrice()
    or check the price display component in the bottom-left footer. It reads from mainnet Uniswap V2 WETH/DAI pool.
  • Custom tokens: Use DexScreener API (
    https://api.dexscreener.com/latest/dex/tokens/TOKEN_ADDRESS
    ), on-chain Uniswap quoter, or Chainlink oracle if available.
This applies to both display AND input:
  • Displaying a balance? Show USD next to it.
  • User entering an amount to send/stake/swap? Show live USD preview below the input.
  • Transaction confirmation? Show USD value of what they're about to do.
所有显示的代币或ETH金额都应包含对应的USD价值。 所有代币或ETH输入框都应显示实时USD预览。
typescript
// ✅ 正确 — 显示时附带USD价值
<span>1,000 TOKEN (~$4.20)</span>
<span>0.5 ETH (~$1,250.00)</span>

// ✅ 正确 — 输入时显示实时USD预览
<input value={amount} onChange={...} />
<span className="text-sm text-gray-500">
  ≈ ${(parseFloat(amount || "0") * tokenPrice).toFixed(2)} USD
</span>

// ❌ 错误 — 仅显示金额,无USD参考
<span>1,000 TOKEN</span>  // 用户无法知晓其价值
获取价格的方式:
  • 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: No Duplicate Titles — Header IS the Title

规则3d:禁止重复标题 — 头部已包含标题

DO NOT put the app name as an
<h1>
at the top of the page body.
The header already displays the app name. Repeating it wastes space and looks amateur.
typescript
// ❌ 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>
The SE2 header component already handles the app title. Your page content should start with the actual UI — stats, forms, data — not repeat what's already visible at the top of the screen.
不要在页面主体顶部将应用名称作为
<h1>
标签。
头部组件已经显示了应用名称,重复显示会浪费空间且显得不专业。
typescript
// ❌ 错误 — AI代理常犯此错误
<Header />  {/* 已显示“🦞 $TOKEN Hub” */}
<main>
  <h1>🦞 $TOKEN Hub</h1>  {/* 重复!请删除。 */}
  <p>在Base网络上购买、发送和追踪TOKEN</p>
  ...
</main>

// ✅ 正确 — 直接进入内容
<Header />  {/* 显示应用名称 */}
<main>
  <div className="grid grid-cols-2 gap-4">
    {/* 统计信息、余额、操作 — 无冗余标题 */}
  </div>
</main>
SE2头部组件已处理应用标题。 页面内容应直接从实际UI开始 — 统计信息、表单、数据 — 不要重复显示顶部已有的内容。

Rule 4: RPC Configuration — ALWAYS Alchemy

规则4:RPC配置 — 始终使用Alchemy

NEVER use public RPCs (
mainnet.base.org
, etc.) — they rate-limit and cause random failures.
In
scaffold.config.ts
, ALWAYS set:
typescript
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
Monitor RPC usage: Sensible = 1 request every 3 seconds. If you see 15+ requests/second, you have a bug:
  • Hooks re-rendering in loops
  • Duplicate hook calls
  • Missing dependency arrays
  • watch: true
    on hooks that don't need it
永远不要使用公共RPC(如
mainnet.base.org
等) — 它们会限流并导致随机失败。
scaffold.config.ts
中,必须设置:
typescript
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秒,而非默认的30000毫秒
监控RPC使用情况: 合理频率为每3秒1次请求。如果看到每秒15次以上请求,说明存在bug:
  • 钩子循环重渲染
  • 重复的钩子调用
  • 缺失依赖数组
  • 在不需要的钩子上设置了
    watch: true

Rule 5: Pre-Publish Checklist

规则5:发布前检查清单

BEFORE deploying frontend to Vercel/production:
Open Graph / Twitter Cards (REQUIRED):
typescript
// 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"],
  },
};
⚠️ The OG image URL MUST be:
  • Absolute URL starting with
    https://
  • The LIVE production domain (NOT
    localhost
    , NOT relative path)
  • NOT an environment variable that could be unset or localhost
  • Actually reachable (test by visiting the URL in a browser)
Full checklist — EVERY item must pass:
  • OG image URL is absolute, live production domain
  • OG title and description set (not default SE2 text)
  • Twitter card type set (
    summary_large_image
    )
  • Favicon updated from SE2 default
  • README updated from SE2 default
  • Footer "Fork me" link → your actual repo (not SE2)
  • Browser tab title is correct
  • RPC overrides set to Alchemy
  • pollingInterval
    is 3000
  • All contract addresses match what's deployed
  • No hardcoded testnet/localhost values in production code
  • Every address display uses
    <Address/>
  • Every onchain button has its own loader + disabled state
  • Approve flow has network check → approve → action pattern

在将前端部署到Vercel/生产环境前:
Open Graph / Twitter卡片(必填):
typescript
// 在app/layout.tsx中
export const metadata: Metadata = {
  title: "你的应用名称",
  description: "应用描述",
  openGraph: {
    title: "你的应用名称",
    description: "应用描述",
    images: [{ url: "https://YOUR-LIVE-DOMAIN.com/og-image.png" }],
  },
  twitter: {
    card: "summary_large_image",
    title: "你的应用名称",
    description: "应用描述",
    images: ["https://YOUR-LIVE-DOMAIN.com/og-image.png"],
  },
};
⚠️ OG图片URL必须满足:
  • https://
    开头的绝对URL
  • 指向生产环境的真实域名(不是
    localhost
    ,不是相对路径)
  • 不是可能未设置或指向localhost的环境变量
  • 实际可访问(通过浏览器访问该URL进行测试)
完整检查清单 — 所有项必须通过:
  • OG图片URL为绝对路径,指向生产环境真实域名
  • OG标题和描述已设置(不是默认SE2文本)
  • Twitter卡片类型已设置为
    summary_large_image
  • 网站图标已从SE2默认图标更新
  • README已从SE2默认内容更新
  • 页脚的“Fork me”链接指向你的真实仓库(不是SE2)
  • 浏览器标签标题正确
  • RPC覆盖已设置为Alchemy
  • pollingInterval
    设置为3000
  • 所有合约地址与部署的地址一致
  • 生产代码中没有硬编码的测试网/localhost值
  • 所有地址显示使用
    <Address/>
    组件
  • 所有链上按钮都有独立的加载器和禁用状态
  • 授权流程遵循网络检查 → 授权 → 操作的模式

🧪 BUILD VERIFICATION PROCESS (MANDATORY)

🧪 构建验证流程(强制性要求)

A build is NOT done when the code compiles. A build is done when you've tested it like a real user.
代码编译完成不代表构建完成。只有像真实用户一样测试通过后,构建才算完成。

Phase 1: Code QA (Automated)

阶段1:代码QA(自动化)

After writing all code, run the QA check script or spawn a QA sub-agent:
  • Scan all
    .tsx
    files for raw address strings (should use
    <Address/>
    )
  • Scan for shared
    isLoading
    state across multiple buttons
  • Scan for missing
    disabled
    props on transaction buttons
  • Verify
    scaffold.config.ts
    has
    rpcOverrides
    and
    pollingInterval: 3000
  • Verify
    layout.tsx
    has OG/Twitter meta with absolute URLs
  • Verify no
    mainnet.base.org
    or other public RPCs in any file
编写完所有代码后,运行QA检查脚本或启动QA子代理:
  • 扫描所有
    .tsx
    文件,查找直接显示的地址字符串(应使用
    <Address/>
  • 扫描多个按钮是否使用共享的
    isLoading
    状态
  • 扫描交易按钮是否缺失
    disabled
    属性
  • 验证
    scaffold.config.ts
    是否设置了
    rpcOverrides
    pollingInterval: 3000
  • 验证
    layout.tsx
    是否包含带绝对URL的OG/Twitter元数据
  • 验证所有文件中没有
    mainnet.base.org
    或其他公共RPC

Phase 2: Smart Contract Testing

阶段2:智能合约测试

  • Write and run Foundry tests (
    forge test
    )
  • Test edge cases: zero amounts, max amounts, unauthorized callers
  • Test the full user flow in the contract (approve → action → verify state)
  • 编写并运行Foundry测试(
    forge test
  • 测试边缘情况:零金额、最大金额、未授权调用者
  • 测试合约中的完整用户流程(授权 → 操作 → 验证状态)

Phase 3: Browser Testing (THE REAL TEST)

阶段3:浏览器测试(真实测试)

You have a browser. You have a wallet. You have real money. USE THEM.
After deploying to Base (or fork), open the app and do a FULL walkthrough:
  1. Open the app in the browser tool — take a snapshot, verify it loaded
  2. Check the page title — is it correct, not "Scaffold-ETH 2"?
  3. Connect wallet — does the connect flow work?
  4. Wrong network test — connect on wrong network, verify "Switch to Base" appears
  5. Switch network — click the switch button, verify it works
  6. Approve flow — if the app needs token approval:
    • Verify "Approve" button shows when allowance is insufficient
    • Click Approve — does the button disable? Does it show "Approving..."?
    • Wait for tx — does the button come back? Does the UI update to show the action button?
  7. Main action — click the primary action (stake, deposit, mint, etc.):
    • Does the button disable and show a loader?
    • Does the transaction go through?
    • Does the UI update after confirmation?
    • Does the balance/state change reflect correctly?
  8. Error handling — reject a transaction in wallet, verify the UI recovers gracefully
  9. Address displays — are all addresses showing ENS/blockies, not raw hex?
  10. Share the URL — check that the OG unfurl looks correct (image, title, description)
Only after ALL of this passes can you tell the user "it's done."
你有浏览器、钱包和真实资金,请使用它们。
部署到Base网络(或分叉网络)后,打开应用并完成完整流程:
  1. 打开应用 — 截图,验证加载完成
  2. 检查页面标题 — 是否正确,不是“Scaffold-ETH 2”?
  3. 连接钱包 — 连接流程是否正常?
  4. 错误网络测试 — 连接到错误网络,验证是否显示“切换至Base网络”按钮
  5. 切换网络 — 点击切换按钮,验证是否成功
  6. 授权流程 — 如果应用需要代币授权:
    • 验证授权额度不足时是否显示“授权”按钮
    • 点击授权 — 按钮是否禁用?是否显示“授权中...”?
    • 等待交易完成 — 按钮是否恢复?UI是否更新为显示操作按钮?
  7. 主要操作 — 点击主要操作按钮(质押、存款、铸造等):
    • 按钮是否禁用并显示加载器?
    • 交易是否成功上链?
    • 交易确认后UI是否更新?
    • 余额/状态变化是否正确显示?
  8. 错误处理 — 在钱包中拒绝交易,验证UI是否能优雅恢复
  9. 地址显示 — 所有地址是否显示ENS/区块头像,而非原始十六进制?
  10. 分享URL — 检查OG预览是否正确(图片、标题、描述)
只有所有这些步骤都通过后,才能告诉用户“已完成”。

Phase 4: QA Sub-Agent Review (For Complex Builds)

阶段4:QA子代理审核(针对复杂构建)

For bigger projects, spawn a sub-agent with a fresh context:
  • Give it the repo path and deployed URL
  • It reads all frontend code against the rules above
  • It opens the browser and clicks through independently
  • It reports issues back before shipping

对于大型项目,启动一个拥有全新上下文的子代理:
  • 提供仓库路径和部署URL
  • 它会根据上述规则检查所有前端代码
  • 它会独立打开浏览器并完成流程测试
  • 在发布前报告问题

Default Stack: Scaffold-ETH 2 with Fork Mode

默认技术栈:带分叉模式的Scaffold-ETH 2

When a user wants to BUILD any Ethereum project, follow these steps:
Step 1: Create Project
bash
npx create-eth@latest
当用户想要构建任何以太坊项目时,请遵循以下步骤:
步骤1:创建项目
bash
npx create-eth@latest

Select: foundry (recommended), target chain, project name

选择:foundry(推荐)、目标链、项目名称


**Step 2: Fix Polling Interval**

Edit `packages/nextjs/scaffold.config.ts` and change:
```typescript
pollingInterval: 30000,  // Default: 30 seconds (way too slow!)
to:
typescript
pollingInterval: 3000,   // 3 seconds (much better for development)
Step 3: Install & Fork a Live Network
bash
cd <project-name>
yarn install
yarn fork --network base  # or mainnet, arbitrum, optimism, polygon
⚠️ IMPORTANT: When using fork mode, the frontend target network MUST be
chains.foundry
(chain ID 31337), NOT the chain you're forking!
The fork runs locally on Anvil with chain ID 31337. Even if you're forking Base, Arbitrum, etc., the scaffold config must use:
typescript
targetNetworks: [chains.foundry],  // NOT chains.base!
Only switch to
chains.base
(or other chain) when deploying to the REAL network.
Step 4: Enable Auto Block Mining (REQUIRED!)
bash
undefined

**步骤2:修复轮询间隔**

编辑`packages/nextjs/scaffold.config.ts`,将:
```typescript
pollingInterval: 30000,  // 默认:30秒(太慢!)
修改为:
typescript
pollingInterval: 3000,   // 3秒(开发时体验更好)
步骤3:安装依赖并分叉实时网络
bash
cd <project-name>
yarn install
yarn fork --network base  # 或mainnet、arbitrum、optimism、polygon
⚠️ 重要提示:使用分叉模式时,前端目标网络必须设置为
chains.foundry
(链ID 31337),而不是你要分叉的链!
分叉网络在本地Anvil上运行,链ID为31337。即使你分叉的是Base、Arbitrum等网络,scaffold配置也必须设置为:
typescript
targetNetworks: [chains.foundry],  // 不是chains.base!
只有部署到真实网络时,才切换为
chains.base
(或其他链)。
步骤4:启用自动区块挖矿(必填!)
bash
undefined

In a new terminal, enable interval mining (1 block/second)

在新终端中,启用间隔挖矿(每秒1个区块)

cast rpc anvil_setIntervalMining 1

Without this, `block.timestamp` stays FROZEN and time-dependent logic breaks!

**Optional: Make it permanent** by editing `packages/foundry/package.json` to add `--block-time 1` to the fork script.

**Step 5: Deploy to Local Fork (FREE!)**

```bash
yarn deploy
Step 6: Start Frontend
bash
yarn start
Step 7: Test the Frontend
After the frontend is running, open a browser and test the app:
  1. Navigate to
    http://localhost:3000
  2. Take a snapshot to get page elements (burner wallet address is in header)
  3. Click the faucet to fund the burner wallet with ETH
  4. Transfer tokens from whales if needed (use burner address from page)
  5. Click through the app to verify functionality
Use the
cursor-browser-extension
MCP tools for browser automation. See
tools/testing/frontend-testing.md
for detailed workflows.
cast rpc anvil_setIntervalMining 1

如果不设置,`block.timestamp`会冻结,依赖时间的逻辑会崩溃!

**可选:永久设置** — 编辑`packages/foundry/package.json`,在fork脚本中添加`--block-time 1`。

**步骤5:部署到本地分叉网络(免费!)**

```bash
yarn deploy
步骤6:启动前端
bash
yarn start
步骤7:测试前端
前端启动后,打开浏览器并测试应用:
  1. 导航至
    http://localhost:3000
  2. 截图 获取页面元素(测试钱包地址在头部)
  3. 点击水龙头 为测试钱包充值ETH
  4. 从巨鲸地址转账代币(如果需要,使用页面中的测试钱包地址)
  5. 浏览应用 验证功能
使用
cursor-browser-extension
MCP工具进行浏览器自动化。 详细工作流请参考
tools/testing/frontend-testing.md

When Publishing a Scaffold-ETH 2 Project:

发布Scaffold-ETH 2项目时:

  1. Update README.md — Replace the default SE2 readme with your project's description
  2. Update the footer link — In
    packages/nextjs/components/Footer.tsx
    , change the "Fork me" link from
    https://github.com/scaffold-eth/se-2
    to your actual repo URL
  3. Update page title — In
    packages/nextjs/app/layout.tsx
    , change the metadata title/description
  4. Remove "Debug Contracts" nav link — In
    packages/nextjs/components/Header.tsx
    , remove the Debug Contracts entry from
    menuLinks
  5. Set OG/Twitter meta — Follow the Pre-Publish Checklist in Rule 5 above
  1. 更新README.md — 用项目描述替换默认SE2 README
  2. 更新页脚链接 — 在
    packages/nextjs/components/Footer.tsx
    中,将“Fork me”链接从
    https://github.com/scaffold-eth/se-2
    修改为你的真实仓库URL
  3. 更新页面标题 — 在
    packages/nextjs/app/layout.tsx
    中,修改元数据的标题/描述
  4. 移除“Debug Contracts”导航链接 — 在
    packages/nextjs/components/Header.tsx
    中,从
    menuLinks
    中移除Debug Contracts条目
  5. 设置OG/Twitter元数据 — 遵循规则5中的发布前检查清单

🚀 SE2 Deployment Quick Decision Tree

🚀 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)
想要将SE2部署到生产环境?
├─ IPFS(推荐) ──→ yarn ipfs(本地构建,无内存限制)
│   └─ 出现“localStorage.getItem is not a function”错误?
│       └─ 添加 NODE_OPTIONS="--require ./polyfill-localstorage.cjs"
│          (Node 25+的localStorage存在问题 — 见下文)
├─ Vercel ──→ 设置rootDirectory=packages/nextjs,installCommand="cd ../.. && yarn install"
│   ├─ 出现“No Next.js version detected”错误?
│   │   └─ 未设置根目录 — 通过Vercel API或控制台修复
│   ├─ 出现“cd packages/nextjs: No such file or directory”错误?
│   │   └─ 构建命令仍包含“cd packages/nextjs” — 清空该命令(根目录已处理路径)
│   └─ 出现OOM / 退出码129错误?
│       └─ 构建机器无法处理SE2 monorepo — 改用IPFS或vercel --prebuilt
└─ 任何路径出现“TypeError: localStorage.getItem is not a function”错误
    └─ Node 25+的bug。使用--require polyfill(见下文IPFS部分)

Deploying SE2 to Vercel (Monorepo Setup):

将SE2部署到Vercel(Monorepo设置):

SE2 is a monorepo — Vercel needs special configuration:
  1. Set Root Directory to
    packages/nextjs
    in Vercel project settings
  2. Set Install Command to
    cd ../.. && yarn install
    (installs from workspace root)
  3. Leave Build Command as default (
    next build
    — auto-detected)
  4. Leave Output Directory as default (
    .next
    )
Via Vercel API:
bash
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"}'
Via CLI (after linking):
bash
cd your-se2-project && vercel --prod --yes
⚠️ Common mistake: Don't put
cd packages/nextjs
in the build command — Vercel is already in
packages/nextjs
because of the root directory setting. Don't use a root-level
vercel.json
with
framework: "nextjs"
— Vercel can't find Next.js in the root package.json and fails.
⚠️ Vercel OOM (Out of Memory): SE2's full monorepo install (foundry + nextjs + all deps) can exceed Vercel's 8GB build memory. If build fails with "Out of Memory" / exit code 129:
  • Option A: Add env var
    NODE_OPTIONS=--max-old-space-size=7168
  • Option B (recommended): Build locally and push to IPFS instead (
    yarn ipfs
    )
  • Option C: Use
    vercel --prebuilt
    (build locally, deploy output to Vercel)
SE2是monorepo,Vercel需要特殊配置:
  1. 设置根目录 — 在Vercel项目设置中设置为
    packages/nextjs
  2. 设置安装命令 — 为
    cd ../.. && yarn install
    (从工作区根目录安装)
  3. 保留默认构建命令 — (
    next build
    — 会自动检测)
  4. 保留默认输出目录 — (
    .next
通过Vercel API设置:
bash
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设置(已关联项目后):
bash
cd your-se2-project && vercel --prod --yes
⚠️ 常见错误: 不要在构建命令中添加
cd packages/nextjs
— 因为根目录已设置为
packages/nextjs
,Vercel会自动进入该目录。不要在根目录使用
vercel.json
设置
framework: "nextjs"
— Vercel无法在根package.json中找到Next.js,会导致失败。
⚠️ Vercel内存不足(OOM): SE2完整monorepo安装(foundry + nextjs + 所有依赖)可能会超过Vercel的8GB构建内存。如果构建因“内存不足”/退出码129失败:
  • 选项A: 添加环境变量
    NODE_OPTIONS=--max-old-space-size=7168
  • 选项B(推荐): 本地构建后推送到IPFS(
    yarn ipfs
  • 选项C: 使用
    vercel --prebuilt
    (本地构建,将输出部署到Vercel)

Deploying SE2 to IPFS (BuidlGuidl IPFS):

将SE2部署到IPFS(BuidlGuidl IPFS):

This is the RECOMMENDED deploy path for SE2. Avoids Vercel's memory limits entirely.
bash
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
Or use the built-in script (if it includes the polyfill):
bash
yarn ipfs
⚠️ CRITICAL: Node 25+ localStorage Bug
Node.js 25+ ships a built-in
localStorage
object that's MISSING standard WebStorage API methods (
getItem
,
setItem
, etc.). This breaks
next-themes
, RainbowKit, and any library that calls
localStorage.getItem()
during static page generation (SSG/export).
Error you'll see:
TypeError: localStorage.getItem is not a function
Error occurred prerendering page "/_not-found"
The fix: Create
polyfill-localstorage.cjs
in
packages/nextjs/
:
javascript
// 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; },
  };
}
Then prefix the build with:
NODE_OPTIONS="--require ./polyfill-localstorage.cjs"
Why
--require
and not
instrumentation.ts
or
next.config.ts
?
  • next.config.ts
    polyfill runs in the main process only
  • instrumentation.ts
    doesn't run in the build worker
  • --require
    injects into EVERY Node process, including build workers ✅
Why this happens: The polyfill is needed because Next.js spawns a separate build worker process for prerendering static pages. That worker inherits
NODE_OPTIONS
, so
--require
is the only way to guarantee the polyfill runs before any library code.
⚠️ blockexplorer pages: SE2's built-in block explorer uses
localStorage
at import time and will also fail during static export. Either disable it (rename
app/blockexplorer
to
app/_blockexplorer-disabled
) or ensure the polyfill is active.
这是SE2的推荐部署方式,可完全避免Vercel的内存限制。
bash
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
或使用内置脚本(如果已包含polyfill):
bash
yarn ipfs
⚠️ 关键注意事项:Node 25+ localStorage Bug
Node.js 25+内置了
localStorage
对象,但缺少标准WebStorage API方法(
getItem
setItem
等)。这会导致
next-themes
、RainbowKit以及任何在静态页面生成(SSG/export)期间调用
localStorage.getItem()
的库崩溃。
你会看到的错误:
TypeError: localStorage.getItem is not a function
Error occurred prerendering page "/_not-found"
修复方案:
packages/nextjs/
中创建
polyfill-localstorage.cjs
javascript
// 为Node 25+静态导出构建polyfill localStorage
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"
为什么使用
--require
而不是
instrumentation.ts
next.config.ts
  • next.config.ts
    中的polyfill仅在主进程运行
  • instrumentation.ts
    不会在构建工作进程中运行
  • --require
    会注入到所有Node进程,包括构建工作进程 ✅
原因: Next.js会为预渲染静态页面生成单独的构建工作进程,该工作进程会继承
NODE_OPTIONS
,因此
--require
是确保polyfill在任何库代码运行前加载的唯一方式。

🚨 STALE BUILD / STALE DEPLOY — THE #1 IPFS FOOTGUN

🚨 陈旧构建/陈旧部署 — IPFS最容易犯的错误

Problem: You edit
page.tsx
, then give the user the OLD IPFS URL from a previous deploy. The code changes are in the source but the
out/
directory still contains the old build. This has happened MULTIPLE TIMES.
Root cause: The build step (
yarn build
) produces
out/
. If you edit source files AFTER building but BEFORE deploying, the deploy uploads stale output. Or worse — you skip rebuilding entirely and just re-upload the old
out/
.
MANDATORY: After ANY code change, ALWAYS do the full cycle:
bash
undefined
问题: 你修改了
page.tsx
,然后给用户提供了之前部署的旧IPFS URL。代码更改在源码中,但
out/
目录仍包含旧构建内容。这种情况已经发生过多次。
根本原因: 构建步骤(
yarn build
)生成
out/
目录。如果在构建后、部署前修改了源码,部署的将是陈旧的输出内容。更糟的是 — 你可能跳过了重新构建步骤,直接重新上传了旧的
out/
目录。
强制性要求: 任何代码更改后,必须执行完整流程:
bash
undefined

1. Delete old build artifacts (prevents any caching)

1. 删除旧构建产物(防止缓存)

rm -rf .next out
rm -rf .next out

2. Rebuild from scratch

2. 从头重新构建

NODE_OPTIONS="--require ./polyfill-localstorage.cjs" NEXT_PUBLIC_IPFS_BUILD=true NEXT_PUBLIC_IGNORE_BUILD_ERROR=true yarn build
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)

3. 验证新构建包含你的更改(抽查JS bundle)

grep -l "YOUR_UNIQUE_STRING" out/_next/static/chunks/app/*.js
grep -l "你的唯一标识字符串" out/_next/static/chunks/app/*.js

4. Only THEN upload

4. 只有验证通过后再上传

yarn bgipfs upload out

**How to detect a stale deploy:**
```bash
yarn bgipfs upload out

**如何检测陈旧部署:**
```bash

Compare timestamps — source must be OLDER than out/

比较时间戳 — 源码必须早于out/目录

stat -f '%Sm' app/page.tsx # source modified time stat -f '%Sm' out/ # build output time
stat -f '%Sm' app/page.tsx # 源码修改时间 stat -f '%Sm' out/ # 构建输出时间

If source is NEWER than out/ → BUILD IS STALE, rebuild first!

如果源码时间晚于out/目录 → 构建已陈旧,必须重新构建!


**The CID is your proof:** If the IPFS CID didn't change after a deploy, you deployed the same content. A real code change ALWAYS produces a new CID.

**CID是你的证明:** 如果部署后IPFS CID没有变化,说明你部署的是相同内容。真实的代码更改一定会生成新的CID。

🚨 IPFS ROUTING — WHY ROUTES BREAK AND HOW TO FIX

🚨 IPFS路由 — 路由失效的原因及修复方法

IPFS gateways serve static files. There's no server to handle routing. Three things MUST be true for routes like
/debug
to work:
1.
output: "export"
in next.config.ts
Without this, Next.js builds for server rendering — no static HTML files are generated, so IPFS has nothing to serve.
2.
trailingSlash: true
in next.config.ts (CRITICAL)
This is the #1 reason routes break on IPFS:
  • trailingSlash: false
    (default) → generates
    debug.html
  • trailingSlash: true
    → generates
    debug/index.html
IPFS gateways resolve directories to
index.html
automatically, but they do NOT resolve bare filenames. So
/debug
→ looks for directory
debug/
→ finds
index.html
✅. Without trailing slash,
/debug
→ no directory, no file match → 404 ❌.
3. Routes must survive static export prerendering During
yarn build
with
output: "export"
, Next.js prerenders every page to HTML. If a page crashes during prerender (e.g., hooks that need browser APIs,
localStorage.getItem is not a function
), that route gets SKIPPED — no HTML file is generated, and it 404s on IPFS.
Common prerender killers:
  • localStorage
    /
    sessionStorage
    usage at import time
  • Hooks that assume browser environment (
    window
    ,
    document
    )
  • SE2's block explorer pages (use
    localStorage
    at import time — rename to
    _blockexplorer-disabled
    if not needed)
How to verify routes after build:
bash
undefined
IPFS网关提供静态文件服务,没有服务器处理路由。要让
/debug
等路由正常工作,必须满足三个条件:
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
构建后如何验证路由:
bash
undefined

Check that out/ has a directory + index.html for each route

检查out/目录中每个路由是否有对应的目录和index.html

ls out/*/index.html
ls out/*/index.html

Should show: out/debug/index.html, out/other-route/index.html, etc.

应显示:out/debug/index.html, out/other-route/index.html等

Verify specific route

验证特定路由

curl -s -o /dev/null -w "%{http_code}" -L "https://YOUR_GATEWAY/ipfs/CID/debug/"
curl -s -o /dev/null -w "%{http_code}" -L "https://你的网关地址/ipfs/CID/debug/"

Should return 200, not 404

应返回200,而非404


**The complete IPFS-safe next.config.ts pattern:**
```typescript
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
  };
}

**完整的IPFS兼容next.config.ts模式:**
```typescript
const isIpfs = process.env.NEXT_PUBLIC_IPFS_BUILD === "true";
if (isIpfs) {
  nextConfig.output = "export";       // 静态HTML生成
  nextConfig.trailingSlash = true;    // 生成route/index.html(IPFS需要此设置!)
  nextConfig.images = {
    unoptimized: true,                // IPFS上没有图片优化服务器
  };
}

🚀 GO TO PRODUCTION — Full Checklist

🚀 上线生产环境 — 完整检查清单

When the user says "ship it", follow this EXACT sequence. Steps marked 🤖 are fully automatic. Steps marked 👤 need human input.

Step 1: 🤖 Final code review
  • Verify all feedback is incorporated in source code
  • Test locally (
    yarn start
    ) one last time
  • Check for common issues: duplicate h1, missing AddressInput, raw text inputs
Step 2: 👤 Ask the user what domain they want Ask: "What subdomain do you want for this? e.g.
token.yourname.eth
token.yourname.eth.limo
"
Save the answer — it determines the production URL for metadata + ENS setup.
Step 3: 🤖 Generate OG image + fix metadata for unfurls
Social unfurls (Twitter, Telegram, Discord, etc.) need THREE things correct:
  1. Custom OG image (1200x630 PNG) — NOT the stock SE2 thumbnail
  2. Absolute production URL in og:image — NOT
    localhost:3000
  3. twitter:card
    set to
    summary_large_image
    for large preview
Generate the OG image (
public/thumbnail.png
, 1200x630):
python
undefined
当用户说“发布”时,请严格遵循以下步骤。 标记🤖的步骤完全自动化,标记👤的步骤需要人工输入。

步骤1:🤖 最终代码审查
  • 验证所有反馈已整合到源码中
  • 最后一次在本地测试(
    yarn start
  • 检查常见问题:重复的h1、缺失的AddressInput、原生文本输入框
步骤2:👤 询问用户想要的域名 询问:“你想要什么子域名?例如
token.yourname.eth
token.yourname.eth.limo
保存答案 — 它会决定元数据和ENS设置的生产URL。
步骤3:🤖 生成OG图片 + 修复元数据以支持预览
社交平台预览(Twitter、Telegram、Discord等)需要三个正确设置的内容:
  1. 自定义OG图片(1200x630 PNG) — 不要使用SE2默认缩略图
  2. OG图片使用绝对生产URL — 不要使用
    localhost:3000
  3. twitter:card
    设置为
    summary_large_image
    — 以显示大预览图
生成OG图片
public/thumbnail.png
,1200x630):
python
undefined

Use PIL/Pillow to create a branded 1200x630 OG image with:

使用PIL/Pillow创建品牌化的1200x630 OG图片,包含:

- App name and tagline

- 应用名称和标语

- Production URL (name.yourname.eth.limo)

- 生产URL(name.yourname.eth.limo)

- Dark background, clean layout, accent colors

- 深色背景、简洁布局、强调色

Save to: packages/nextjs/public/thumbnail.png

保存到:packages/nextjs/public/thumbnail.png


**Fix metadata baseUrl** — ensure `utils/scaffold-eth/getMetadata.ts` supports `NEXT_PUBLIC_PRODUCTION_URL`:
```typescript
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}`;
If this env var pattern is already in the file, skip this step.
Step 4: 🤖 Clean build + IPFS deploy
bash
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

**修复元数据baseUrl** — 确保`utils/scaffold-eth/getMetadata.ts`支持`NEXT_PUBLIC_PRODUCTION_URL`:
```typescript
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}`;
如果文件中已包含此环境变量模式,跳过此步骤。
步骤4:🤖 清理构建 + IPFS部署
bash
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
ls out/*/index.html # 路由存在 grep 'og:image' out/index.html # 不是localhost stat -f '%Sm' app/page.tsx && stat -f '%Sm' out/ # 源码早于构建

Upload:

上传:

yarn bgipfs upload out
yarn bgipfs upload out

Save the CID!

保存CID!


**Step 5: 👤 Share IPFS URL for verification**
Send: *"Here's the build for review: `https://community.bgipfs.com/ipfs/<CID>`"*
**Wait for approval before touching ENS.** Don't proceed until the user says go.

**Step 6: 🤖 Set up ENS subdomain (2 mainnet transactions)**

If this is a **new app** (subdomain doesn't exist yet):

**Tx #1 — Create subdomain:**
1. Open `https://app.ens.domains/yourname.eth` in the wallet browser (your wallet profile)
2. Go to "Subnames" tab → "New subname"
3. Enter the label (e.g. `token`) → Next → Skip profile → Open Wallet → Confirm
4. If gas is stuck: switch MetaMask to Ethereum network → Activity tab → "Speed up"

**Tx #2 — Set IPFS content hash:**
1. Navigate to `https://app.ens.domains/<name>.yourname.eth`
2. Go to "Records" tab → "Edit Records" → "Other" tab
3. Paste in Content Hash field: `ipfs://<CID>`
4. Save → Open Wallet → Confirm in MetaMask

If this is an **update** to an existing app: skip Tx #1, only do Tx #2 (update the content hash).

**Step 7: 🤖 Verify everything**
```bash

**步骤5:👤 分享IPFS URL供用户验证**
发送:*“这是供审查的构建版本:`https://community.bgipfs.com/ipfs/<CID>`”*
**在用户批准前不要触碰ENS设置。** 必须等用户确认后再继续。

**步骤6:🤖 设置ENS子域名(2笔主网交易)**

如果是**新应用**(子域名不存在):

**交易1 — 创建子域名:**
1. 在钱包浏览器中打开`https://app.ens.domains/yourname.eth`(你的钱包配置文件)
2. 进入“子域名”标签页 → “新子域名”
3. 输入标签(如`token`) → 下一步 → 跳过资料 → 打开钱包 → 确认
4. 如果gas卡住:将MetaMask切换到以太坊网络 → 活动标签页 → “加速”

**交易2 — 设置IPFS内容哈希:**
1. 导航到`https://app.ens.domains/<name>.yourname.eth`
2. 进入“记录”标签页 → “编辑记录” → “其他”标签页
3. 在内容哈希字段粘贴:`ipfs://<CID>`
4. 保存 → 打开钱包 → 在MetaMask中确认

如果是**现有应用更新**:跳过交易1,仅执行交易2(更新内容哈希)。

**步骤7:🤖 验证所有内容**
```bash

1. ENS content hash matches (on-chain)

1. 验证ENS内容哈希与链上一致

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>
RESOLVER=$(cast call 0x00000000000C2e074eC69A0dFb2997BA6C7d2e1e
"resolver(bytes32)(address)" $(cast namehash <name>.yourname.eth)
--rpc-url https://eth-mainnet.g.alchemy.com/v2/<你的密钥>) 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)

2. 验证.limo网关可访问(缓存可能需要5-15分钟)

curl -s -o /dev/null -w "%{http_code}" -L "https://<name>.yourname.eth.limo"
curl -s -o /dev/null -w "%{http_code}" -L "https://<name>.yourname.eth.limo"

3. OG metadata correct

3. 验证OG元数据正确

curl -s -L "https://<name>.yourname.eth.limo" | grep 'og:image'
curl -s -L "https://<name>.yourname.eth.limo" | grep 'og:image'

Should show the production URL, NOT localhost

应显示生产URL,而非localhost


**Step 8: 👤 Report to the user**
Send: *"Live at `https://<name>.yourname.eth.limo` — unfurl metadata set, ENS content hash confirmed on-chain."*

---

**⚠️ Known gotchas:**
- **MetaMask gas:** ENS app sometimes suggests 0.2 gwei — mainnet needs more. Use "Speed up" if stuck.
- **.limo caching:** Gateway caches content for ~5-15 min. On-chain hash updates immediately but .limo may serve stale content briefly.
- **Stock thumbnail:** SE2 ships a default `thumbnail.png` and `thumbnail.jpg`. ALWAYS replace both before production.
- **localhost in metadata:** If `NEXT_PUBLIC_PRODUCTION_URL` isn't set, og:image will point to `localhost:3000`. Always verify with `grep`.

**步骤8:👤 向用户报告结果**
发送:*“已上线:`https://<name>.yourname.eth.limo` — 预览元数据已设置,ENS内容哈希已在链上确认。”*

---

**⚠️ 已知陷阱:**
- **MetaMask gas问题:** ENS应用有时会建议0.2 gwei — 主网需要更高gas。如果卡住,使用“加速”功能。
- **.limo缓存:** 网关会缓存内容约5-15分钟。链上哈希会立即更新,但.limo可能会短暂提供陈旧内容。
- **默认缩略图:** SE2附带默认的`thumbnail.png`和`thumbnail.jpg`。上线前必须全部替换。
- **元数据中的localhost:** 如果未设置`NEXT_PUBLIC_PRODUCTION_URL`,og:image会指向`localhost:3000`。始终使用`grep`验证。

DO NOT:

禁止操作:

  • Run
    yarn chain
    (use
    yarn fork --network <chain>
    instead!)
  • Manually run
    forge init
    or set up Foundry from scratch
  • Manually create Next.js projects
  • Set up wallet connection manually (SE2 has RainbowKit pre-configured)
  • 运行
    yarn chain
    (改用
    yarn fork --network <chain>
    !)
  • 手动运行
    forge init
    或从头设置Foundry
  • 手动创建Next.js项目
  • 手动设置钱包连接(SE2已预配置RainbowKit)

Why Fork Mode?

为什么使用分叉模式?

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
yarn chain(错误)              yarn fork --network base(正确)
└─ 空本地链            └─ 真实Base主网的分叉
└─ 无协议可用                 └─ 可使用Uniswap、Aave等协议
└─ 无代币可用                 └─ 真实USDC、WETH存在
└─ 孤立环境测试         └─ 针对真实状态测试

Address Data Available

可用地址数据

Token, protocol, and whale addresses are in
data/addresses/
:
  • tokens.json
    - WETH, USDC, DAI, etc. per chain
  • protocols.json
    - Uniswap, Aave, Chainlink per chain
  • whales.json
    - Large token holders for test funding

代币、协议和巨鲸地址位于
data/addresses/
  • tokens.json
    - 各链的WETH、USDC、DAI等
  • protocols.json
    - 各链的Uniswap、Aave、Chainlink等
  • whales.json
    - 用于测试充值的大额代币持有者

THE MOST CRITICAL CONCEPT

最关键的概念

NOTHING IS AUTOMATIC ON ETHEREUM.
Smart contracts cannot execute themselves. There is no cron job, no scheduler, no background process. For EVERY function that "needs to happen":
  1. Make it callable by ANYONE (not just admin)
  2. Give callers a REASON (profit, reward, their own interest)
  3. Make the incentive SUFFICIENT to cover gas + profit
Always ask: "Who calls this function? Why would they pay gas?"
If you can't answer this, your function won't get called.
以太坊上没有任何自动执行的操作。
智能合约无法自行执行,没有定时任务、调度器或后台进程。对于每个“需要执行”的函数:
  1. 任何人都能调用(不仅限于管理员)
  2. 给调用者一个理由(利润、奖励、自身利益)
  3. 确保激励足够覆盖gas成本并留有利润
始终要问:“谁会调用这个函数?他们为什么要支付gas?”
如果无法回答这个问题,你的函数永远不会被调用。

Examples of Proper Incentive Design

合理激励设计示例

solidity
// 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);
}

solidity
// 清算:调用者获得额外抵押品
function liquidate(address user) external {
    require(getHealthFactor(user) < 1e18, "健康状态良好");
    uint256 bonus = collateral * 5 / 100; // 5%奖励
    collateralToken.transfer(msg.sender, collateral + bonus);
}

// 收益收割:调用者获得部分收益
function harvest() external {
    uint256 yield = protocol.claimRewards();
    uint256 callerReward = yield / 100; // 1%
    token.transfer(msg.sender, callerReward);
}

// 领取奖励:用户想要自己的代币
function claimRewards() external {
    uint256 reward = pendingRewards[msg.sender];
    pendingRewards[msg.sender] = 0;
    token.transfer(msg.sender, reward);
}

Critical Gotchas (Memorize These)

关键陷阱(必须牢记)

1. Token Decimals Vary

1. 代币小数位数不同

USDC = 6 decimals, not 18!
solidity
// BAD: Assumes 18 decimals - transfers 1 TRILLION USDC!
uint256 oneToken = 1e18;

// GOOD: Check decimals
uint256 oneToken = 10 ** token.decimals();
Common decimals:
  • USDC, USDT: 6 decimals
  • WBTC: 8 decimals
  • Most tokens (DAI, WETH): 18 decimals
USDC的小数位数是6,不是18!
solidity
// 错误:假设18位小数 — 会转账1万亿USDC!
uint256 oneToken = 1e18;

// 正确:检查小数位数
uint256 oneToken = 10 ** token.decimals();
常见小数位数:
  • USDC、USDT:6位小数
  • WBTC:8位小数
  • 大多数代币(DAI、WETH):18位小数

2. ERC-20 Approve Pattern Required

2. 必须使用ERC-20授权模式

Contracts cannot pull tokens directly. Two-step process:
solidity
// Step 1: User approves
token.approve(spenderContract, amount);

// Step 2: Contract pulls tokens
token.transferFrom(user, address(this), amount);
Never use infinite approvals:
solidity
// DANGEROUS
token.approve(spender, type(uint256).max);

// SAFE
token.approve(spender, exactAmount);
合约无法直接提取代币,必须分两步:
solidity
// 步骤1:用户授权
token.approve(spenderContract, amount);

// 步骤2:合约提取代币
token.transferFrom(user, address(this), amount);
永远不要使用无限授权:
solidity
// 危险
token.approve(spender, type(uint256).max);

// 安全
token.approve(spender, exactAmount);

3. No Floating Point in Solidity

3. Solidity中没有浮点数

Use basis points (1 bp = 0.01%):
solidity
// 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;
使用基点(1 bp = 0.01%):
solidity
// 错误:结果为0
uint256 fivePercent = 5 / 100;

// 正确:使用基点
uint256 FEE_BPS = 500; // 5% = 500基点
uint256 fee = (amount * FEE_BPS) / 10000;

4. Reentrancy Attacks

4. 重入攻击

External calls can call back into your contract:
solidity
// 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);
}
Always use OpenZeppelin's ReentrancyGuard.
外部调用可以回调到你的合约:
solidity
// 安全:检查-效果-交互模式
function withdraw() external nonReentrant {
    uint256 bal = balances[msg.sender];
    balances[msg.sender] = 0; // 先更新状态,再执行交互
    (bool success,) = msg.sender.call{value: bal}("");
    require(success);
}
始终使用OpenZeppelin的ReentrancyGuard。

5. Never Use DEX Spot Prices as Oracles

5. 永远不要使用DEX现货价格作为预言机

Flash loans can manipulate spot prices instantly:
solidity
// 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);
}
闪电贷可以瞬间操纵现货价格:
solidity
// 安全:使用Chainlink
function getPrice() internal view returns (uint256) {
    (, int256 price,, uint256 updatedAt,) = priceFeed.latestRoundData();
    require(block.timestamp - updatedAt < 3600, "数据陈旧");
    require(price > 0, "无效价格");
    return uint256(price);
}

6. Vault Inflation Attack

6. 金库通胀攻击

First depositor can steal funds via share manipulation:
solidity
// Mitigation: Virtual offset
function convertToShares(uint256 assets) public view returns (uint256) {
    return assets.mulDiv(totalSupply() + 1e3, totalAssets() + 1);
}
第一个存款者可以通过份额操纵窃取资金:
solidity
// 缓解措施:虚拟偏移
function convertToShares(uint256 assets) public view returns (uint256) {
    return assets.mulDiv(totalSupply() + 1e3, totalAssets() + 1);
}

7. Use SafeERC20

7. 使用SafeERC20

Some tokens (USDT) don't return bool on transfer:
solidity
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
using SafeERC20 for IERC20;

token.safeTransfer(to, amount); // Handles non-standard tokens

有些代币(如USDT)在转账时不返回bool值:
solidity
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
using SafeERC20 for IERC20;

token.safeTransfer(to, amount); // 处理非标准代币

Scaffold-ETH 2 Development

Scaffold-ETH 2开发

Project Structure

项目结构

packages/
├── foundry/              # Smart contracts
│   ├── contracts/        # Your Solidity files
│   └── script/           # Deploy scripts
└── nextjs/
    ├── app/              # React pages
    └── contracts/        # Generated ABIs + externalContracts.ts
packages/
├── foundry/              # 智能合约
│   ├── contracts/        # 你的Solidity文件
│   └── script/           # 部署脚本
└── nextjs/
    ├── app/              # React页面
    └── contracts/        # 生成的ABI + externalContracts.ts

Essential Hooks

核心钩子

typescript
// 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,
});

typescript
// 读取合约数据
const { data } = useScaffoldReadContract({
  contractName: "YourContract",
  functionName: "greeting",
});

// 写入合约
const { writeContractAsync } = useScaffoldWriteContract("YourContract");

// 监听事件
useScaffoldEventHistory({
  contractName: "YourContract",
  eventName: "Transfer",
  fromBlock: 0n,
});

SpeedRun Ethereum Challenges

SpeedRun Ethereum挑战

Reference these for hands-on learning:
ChallengeConceptKey Lesson
0: Simple NFTERC-721Minting, metadata, tokenURI
1: StakingCoordinationDeadlines, escrow, thresholds
2: Token VendorERC-20Approve pattern, buy/sell
3: Dice GameRandomnessOn-chain randomness is insecure
4: DEXAMMx*y=k formula, slippage
5: OraclesPrice FeedsChainlink, manipulation resistance
6: LendingCollateralHealth factor, liquidation incentives
7: StablecoinsPeggingCDP, over-collateralization
8: Prediction MarketsResolutionOutcome determination
9: ZK VotingPrivacyZero-knowledge proofs
10: MultisigSignaturesThreshold approval
11: SVG NFTOn-chain ArtGenerative, base64 encoding

参考这些挑战进行实践学习:
挑战概念核心课程
0: 简单NFTERC-721铸造、元数据、tokenURI
1: 质押协作截止日期、托管、阈值
2: 代币供应商ERC-20授权模式、买卖
3: 骰子游戏随机性链上随机性不安全
4: DEXAMMx*y=k公式、滑点
5: 预言机价格馈送Chainlink、抗操纵
6: 借贷抵押品健康因子、清算激励
7: 稳定币锚定CDP、超额抵押
8: 预测市场结果判定结果确定机制
9: ZK投票隐私零知识证明
10: 多签签名阈值批准
11: SVG NFT链上艺术生成式、base64编码

DeFi Protocol Patterns

DeFi协议模式

Uniswap (AMM)

Uniswap(AMM)

  • Constant product formula: x * y = k
  • Slippage protection required
  • LP tokens represent pool share
  • 恒定乘积公式:x * y = k
  • 需要滑点保护
  • LP代币代表池份额

Aave (Lending)

Aave(借贷)

  • Supply collateral, borrow assets
  • Health factor = collateral value / debt value
  • Liquidation when health factor < 1
  • 提供抵押品,借入资产
  • 健康因子 = 抵押品价值 / 债务价值
  • 健康因子<1时触发清算

ERC-4626 (Tokenized Vaults)

ERC-4626(代币化金库)

  • Standard interface for yield-bearing vaults
  • deposit/withdraw with share accounting
  • Protect against inflation attacks

  • 生息金库的标准接口
  • 存款/取款使用份额记账
  • 抵御通胀攻击

Security Review Checklist

安全审查清单

Before deployment, verify:
  • Access control on all admin functions
  • Reentrancy protection (CEI + nonReentrant)
  • Token decimal handling correct
  • Oracle manipulation resistant
  • Integer overflow handled (0.8+ or SafeMath)
  • Return values checked (SafeERC20)
  • Input validation present
  • Events emitted for state changes
  • Incentives designed for maintenance functions
  • NO infinite approvals (use exact amounts, NEVER type(uint256).max)

部署前验证:
  • 所有管理员函数的访问控制
  • 重入保护(CEI模式 + nonReentrant)
  • 代币小数位数处理正确
  • 预言机抗操纵
  • 整数溢出处理(0.8+版本或SafeMath)
  • 返回值检查(SafeERC20)
  • 输入验证存在
  • 状态变化时触发事件
  • 维护函数的激励设计合理
  • 无无限授权(使用精确金额,绝对不要用type(uint256).max)

Response Guidelines

响应指南

When helping developers:
  1. Follow the fork workflow - Always use
    yarn fork
    , never
    yarn chain
  2. Answer directly - Address their question first
  3. Show code - Provide working examples
  4. Warn about gotchas - Proactively mention relevant pitfalls
  5. Reference challenges - Point to SpeedRun Ethereum for practice
  6. Ask about incentives - For any "automatic" function, ask who calls it and why
帮助开发者时:
  1. 遵循分叉工作流 - 始终使用
    yarn fork
    ,不要使用
    yarn chain
  2. 直接回答问题 - 先解决用户的问题
  3. 提供代码示例 - 给出可运行的示例
  4. 警告陷阱 - 主动提及相关风险
  5. 参考挑战项目 - 引导用户通过SpeedRun Ethereum练习
  6. 询问激励机制 - 对于任何“自动执行”的函数,询问谁会调用它以及为什么愿意支付gas