mcpField
E
ethereum-wingman

ethereum-wingman

github.com/austintgriffith开发45401 次安装38 周前更新
代码仓库

关于 ethereum-wingman

Ethereum Wingman 是面向构建 Ethereum 应用的 AI Agent 开发指南,涵盖智能合约、DeFi、安全实践和 SpeedRun Ethereum,并提供 Scaffold-ETH 2 实现与测试规范。

主要功能

  1. 01智能合约开发指南
  2. 02DeFi 协议指南
  3. 03安全与代币操作规范
  4. 04Scaffold-ETH 2 前端约定
  5. 05构建与浏览器测试流程

适用场景

  1. 01使用 Scaffold-ETH 2 构建 Ethereum 应用
  2. 02实现代币授权和链上操作
  3. 03检查前端交易流程
  4. 04测试已部署的 Ethereum 应用

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/austintgriffith/ethereum-wingman --skill ethereum-wingman

安装后在 Claude Code 中输入/ethereum-wingman确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

AGENTS.md
metadata.json
README.md
references/
automation-and-incentives.md
critical-gotchas.md
historical-hacks.md
scripts/
check-gotchas.sh
init-project.sh
setup-cursor.sh

详细介绍

Ethereum Wingman

面向 AI Agent 的全面 Ethereum 开发指南。涵盖智能合约开发、DeFi 协议、安全最佳实践和 SpeedRun Ethereum 课程。


AI AGENT 指令 — 请先阅读

🚫 关键:外部合约与 Scaffold 钩子

这些规则是强制要求。违反规则会在生产环境中导致实际问题。

  1. 所有合约都必须添加到 externalContracts.ts — 任何你要与之交互的合约(代币、协议等),都必须将其地址和 ABI 添加到 packages/nextjs/contracts/externalContracts.ts。请先阅读该文件——其中的模式一目了然。

  2. 只能使用 Scaffold 钩子——绝不可直接使用 WAGMI — 始终使用 useScaffoldReadContract 和 useScaffoldWriteContract,绝不可使用 useWriteContract 或 useReadContract 等原生 wagmi 钩子。

原因: Scaffold 钩子使用 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:每个链上按钮都必须显示加载状态并禁用

任何会触发区块链交易的按钮都必须:

  1. 点击后立即禁用
  2. 显示加载动画/旋转图标(“正在授权……”“正在质押……”等)
  3. 保持禁用状态,直到状态更新确认操作已完成
  4. 完成后显示成功/错误反馈
// ✅ 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:三按钮流程——网络 → 授权 → 执行操作

当用户需要先授权代币,再执行某项操作(质押、存款、兑换)时,共有三种状态。每次只显示一个按钮:

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>
)}

重要: 始终通过钩子读取授权额度,这样 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 解析、blockie 头像、复制到剪贴板、地址截断和区块浏览器链接。直接显示十六进制地址不可接受。

规则 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”即可解析为地址)、blockie 头像预览、验证和粘贴处理。普通输入框不具备这些功能。

配套使用:显示地址用 <Address/>,输入地址用 <AddressInput/>。务必如此。

规则 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 内置了一个 hook——useNativeCurrencyPrice(),也可以查看左下角页脚中的价格显示组件。它从主网 Uniswap V2 WETH/DAI 交易池读取价格。
  • **自定义代币:**使用 DexScreener API(https://api.dexscreener.com/latest/dex/tokens/TOKEN_ADDRESS)、链上 Uniswap 报价器,或可用的 Chainlink 预言机。

这适用于显示和输入:

  • 显示余额?在旁边显示 USD 金额。
  • 用户输入要发送/质押/兑换的金额?在输入框下方显示实时 USD 预览。
  • 交易确认?显示用户即将执行操作的 USD 金额。
规则 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 开始——统计信息、表单、数据等——不要重复屏幕顶部已经显示的内容。

规则 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 次以上请求,说明存在 bug:

  • Hooks 循环重新渲染
  • 重复调用 Hook
  • 缺少依赖数组
  • 在不需要的 Hook 上设置 watch: true
规则 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)
  • 已将 favicon 更新为非 SE2 默认图标
  • 已将 README 更新为非 SE2 默认内容
  • 页脚中的 “Fork me” 链接指向你的实际代码仓库(而非 SE2)
  • 浏览器标签页标题正确
  • RPC 覆盖项已设置为 Alchemy
  • pollingInterval 为 3000
  • 所有合约地址均与已部署的地址一致
  • 生产代码中没有硬编码的测试网/localhost 值
  • 所有地址显示均使用 <Address/>
  • 每个链上按钮都有自己的加载状态和禁用状态
  • 授权流程遵循网络检查 → 授权 → 操作的模式

🧪 构建验证流程(必需)

代码编译通过并不代表构建完成。只有像真实用户一样进行测试,构建才算完成。

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

编写完所有代码后,运行 QA 检查脚本或启动 QA 子 Agent:

  • 扫描所有 .tsx 文件中的原始地址字符串(应使用 <Address/>)
  • 检查是否有多个按钮共用 isLoading 状态
  • 检查交易按钮是否缺少 disabled 属性
  • 确认 scaffold.config.ts 中包含 rpcOverrides 和 pollingInterval: 3000
  • 确认 layout.tsx 中包含带绝对 URL 的 OG/Twitter 元数据
  • 确认所有文件中都没有 mainnet.base.org 或其他公共 RPC
阶段 2:智能合约测试
  • 编写并运行 Foundry 测试(forge test)
  • 测试边界情况:零金额、最大金额、未授权调用者
  • 在合约中测试完整的用户流程(授权 → 操作 → 验证状态)
第 3 阶段:浏览器测试(真正的测试)

你有浏览器。你有钱包。你有真金白银。用起来。

部署到 Base(或分叉网络)后,打开应用并进行完整的测试:

  1. 打开应用:使用浏览器工具打开应用,截取页面快照并确认页面已加载
  2. 检查页面标题:标题是否正确,而不是“Scaffold-ETH 2”?
  3. 连接钱包:连接流程是否正常?
  4. 错误网络测试:在错误的网络上连接,确认出现“Switch to Base”
  5. 切换网络:点击切换按钮,确认切换成功
  6. 授权流程:如果应用需要代币授权:
    • 额度不足时,确认会显示“Approve”按钮
    • 点击“Approve”:按钮是否会禁用?是否会显示“Approving...”?
    • 等待交易完成:按钮是否会重新出现?界面是否会更新并显示操作按钮?
  7. 主要操作:点击主要操作(质押、存款、铸造等):
    • 按钮是否会禁用并显示加载动画?
    • 交易是否成功?
    • 确认后界面是否会更新?
    • 余额或状态是否正确变化?
  8. 错误处理:在钱包中拒绝交易,确认界面能否正常恢复
  9. 地址显示:所有地址是否都以 ENS 或 blockies 的形式显示,而不是原始十六进制?
  10. 分享 URL:检查 OG 链接预览是否正确(图片、标题、描述)

只有全部通过这些测试后,你才能告诉用户“已经完成”。

第 4 阶段:QA 子 Agent 审核(适用于复杂构建)

对于较大的项目,使用全新的上下文启动一个子 Agent:

  • 提供仓库路径和已部署的 URL
  • 让它根据上述规则检查所有前端代码
  • 让它打开浏览器并独立点击测试
  • 发布前让它报告问题

默认技术栈:使用 Fork 模式的 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

⚠️ 重要:使用 Fork 模式时,前端目标网络必须是 chains.foundry(链 ID 31337),不能是你要分叉的链!

分叉网络通过 Anvil 在本地运行,链 ID 为 31337。即使你分叉的是 Base、Arbitrum 等网络,脚手架配置也必须使用:

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 步:测试前端

前端运行后,打开浏览器并测试应用:

  1. 访问 http://localhost:3000
  2. 截取页面快照以获取页面元素(临时钱包地址位于页眉中)
  3. 点击水龙头,向临时钱包充值 ETH
  4. 必要时从巨鲸账户转入代币(使用页面中的临时钱包地址)
  5. 逐一点击应用中的功能,确认其正常运行

使用 cursor-browser-extension MCP 工具进行浏览器自动化。 详细工作流请参阅 tools/testing/frontend-testing.md。

发布 Scaffold-ETH 2 项目时:
  1. 更新 README.md:将默认的 SE2 说明替换为项目描述
  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 部署快速决策树
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 部署到 Vercel(Monorepo 设置):

SE2 是一个 monorepo,Vercel 需要特殊配置:

  1. 将 Root Directory 设置为 Vercel 项目设置中的 packages/nextjs
  2. 将 Install Command 设置为 cd ../.. && yarn install(从工作区根目录安装)
  3. Build Command 保持默认值(自动检测为 next build)
  4. 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。不要在根目录使用带有 framework: "nextjs" 的 vercel.json——Vercel 无法在根目录的 package.json 中找到 Next.js,因而会失败。

**⚠️ Vercel 内存不足(OOM):**SE2 的完整 monorepo 安装(foundry + nextjs + 所有依赖项)可能会超过 Vercel 的 8 GB 构建内存。如果构建因“内存不足”/退出代码 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

或者使用内置脚本(如果其中包含 polyfill):

yarn ipfs

⚠️ 重要:Node 25+ 的 localStorage 错误

Node.js 25+ 自带的 localStorage 对象缺少标准 WebStorage API 方法(getItem、setItem 等)。这会导致 next-themes、RainbowKit 以及任何在静态页面生成(SSG/导出)期间调用 localStorage.getItem() 的库出错。

你会看到的错误:

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"

为什么使用 --require,而不是 instrumentation.ts 或 next.config.ts?

  • next.config.ts 的 polyfill 只会在主进程中运行
  • instrumentation.ts 不会在构建工作进程中运行
  • --require 会注入到每个 Node 进程中,包括构建工作进程 ✅

**发生原因:**需要 polyfill,是因为 Next.js 会为静态页面预渲染启动单独的构建工作进程。该工作进程会继承 NODE_OPTIONS,因此只有 --require 能确保 polyfill 在任何库代码运行之前执行。

**⚠️ blockexplorer 页面:**SE2 内置的区块浏览器会在导入时使用 localStorage,因此静态导出期间也会出错。可以将其禁用(将 app/blockexplorer 重命名为 app/_blockexplorer-disabled),或确保 polyfill 已启用。

🚨 构建过期 / 部署过期——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 这类路由正常工作,以下三点必须成立:

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 的 Hook,或出现 localStorage.getItem is not a function),该路由就会被跳过——不会生成 HTML 文件,因此在 IPFS 上访问时会返回 404。

常见的预渲染故障原因:

  • 在导入时使用 localStorage / sessionStorage
  • 假定浏览器环境存在的 Hook(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 等)需要确保以下三项正确:

  1. 自定义 OG 图片(1200x630 PNG)——不能使用 SE2 默认缩略图
  2. og:image 中的绝对生产环境 URL——不能是 localhost:3000
  3. 将 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 笔主网交易)

如果这是一个新应用(子域名尚不存在):

交易 #1 — 创建子域名:

  1. 在钱包浏览器中打开 https://app.ens.domains/yourname.eth(你的钱包个人资料)
  2. 前往“Subnames”选项卡 → “New subname”
  3. 输入标签(例如 token)→ Next → Skip profile → Open Wallet → Confirm
  4. 如果 Gas 卡住:将 MetaMask 切换到 Ethereum 网络 → Activity 选项卡 → “Speed up”

交易 #2 — 设置 IPFS 内容哈希:

  1. 前往 https://app.ens.domains/<name>.yourname.eth
  2. 前往“Records”选项卡 → “Edit Records” → “Other”选项卡
  3. 在 Content Hash 字段中粘贴:ipfs://<CID>
  4. 保存 → Open Wallet → 在 MetaMask 中确认

如果这是对现有应用的更新:跳过交易 #1,只执行交易 #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 Gas:**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)
为什么使用 Fork 模式?
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
可用的地址数据

Token、协议和巨鲸地址位于 data/addresses/:

  • tokens.json - 每条链上的 WETH、USDC、DAI 等
  • protocols.json - 每条链上的 Uniswap、Aave、Chainlink
  • whales.json - 用于测试资金注入的大额 Token 持有者

最关键的概念

在 ETHEREUM 上,没有任何操作会自动执行。

智能合约无法自行执行。没有 cron 任务、没有调度器,也没有后台进程。对于每个“需要执行”的函数:

  1. 让任何人都可以调用(而不仅仅是管理员)
  2. 给调用者一个理由(利润、奖励或自身利益)
  3. 确保激励足够,能够覆盖 Gas 费用并带来利润

始终要问:“谁会调用这个函数?他们为什么愿意支付 Gas 费?”

如果你无法回答这个问题,你的函数就不会被调用。

合理激励设计示例
// 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 授权模式

合约无法直接提取代币。需要分两步操作:

// 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 个基点 = 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. 金库通胀攻击

首位存款人可以通过操纵份额窃取资金:

// 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
必备 Hooks
// 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:简单 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 协议模式

Uniswap(AMM)
  • 恒定乘积公式:x * y = k
  • 必须提供滑点保护
  • LP 代币代表资金池份额
Aave(借贷)
  • 提供抵押品,借入资产
  • 健康因子 = 抵押品价值 / 债务价值
  • 健康因子 < 1 时触发清算
ERC-4626(代币化金库)
  • 生息金库的标准接口
  • 通过份额记账进行存款/取款
  • 防范通胀攻击

安全审查清单

部署前,请确认:

  • 所有管理员函数都已设置访问控制
  • 已实施重入保护(CEI + nonReentrant)
  • 代币小数位数处理正确
  • 可抵御预言机操纵
  • 已处理整数溢出(0.8+ 或 SafeMath)
  • 已检查返回值(SafeERC20)
  • 已进行输入验证
  • 状态变更时已触发事件
  • 已为维护函数设计激励机制
  • 不使用无限授权(使用精确数量,绝不使用 type(uint256).max)

回复指南

帮助开发者时:

  1. 遵循 fork 工作流——始终使用 yarn fork,绝不使用 yarn chain
  2. 直接回答——优先回答他们的问题
  3. 提供代码——给出可运行的示例
  4. 提醒注意事项——主动指出相关陷阱
  5. 参考挑战——推荐通过 SpeedRun Ethereum 练习
  6. 询问激励机制——对于任何“自动”执行的函数,都要询问谁会调用它以及原因

评论

支持 Markdown