关于 ethereum-wingman
Ethereum Wingman 是面向构建 Ethereum 应用的 AI Agent 开发指南,涵盖智能合约、DeFi、安全实践和 SpeedRun Ethereum,并提供 Scaffold-ETH 2 实现与测试规范。
主要功能
- 01智能合约开发指南
- 02DeFi 协议指南
- 03安全与代币操作规范
- 04Scaffold-ETH 2 前端约定
- 05构建与浏览器测试流程
适用场景
- 01使用 Scaffold-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 Agent 的全面 Ethereum 开发指南。涵盖智能合约开发、DeFi 协议、安全最佳实践和 SpeedRun Ethereum 课程。
AI AGENT 指令 — 请先阅读
🚫 关键:外部合约与 Scaffold 钩子
这些规则是强制要求。违反规则会在生产环境中导致实际问题。
-
所有合约都必须添加到 externalContracts.ts — 任何你要与之交互的合约(代币、协议等),都必须将其地址和 ABI 添加到
packages/nextjs/contracts/externalContracts.ts。请先阅读该文件——其中的模式一目了然。 -
只能使用 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:每个链上按钮都必须显示加载状态并禁用
任何会触发区块链交易的按钮都必须:
- 点击后立即禁用
- 显示加载动画/旋转图标(“正在授权……”“正在质押……”等)
- 保持禁用状态,直到状态更新确认操作已完成
- 完成后显示成功/错误反馈
// ✅ 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(或分叉网络)后,打开应用并进行完整的测试:
- 打开应用:使用浏览器工具打开应用,截取页面快照并确认页面已加载
- 检查页面标题:标题是否正确,而不是“Scaffold-ETH 2”?
- 连接钱包:连接流程是否正常?
- 错误网络测试:在错误的网络上连接,确认出现“Switch to Base”
- 切换网络:点击切换按钮,确认切换成功
- 授权流程:如果应用需要代币授权:
- 额度不足时,确认会显示“Approve”按钮
- 点击“Approve”:按钮是否会禁用?是否会显示“Approving...”?
- 等待交易完成:按钮是否会重新出现?界面是否会更新并显示操作按钮?
- 主要操作:点击主要操作(质押、存款、铸造等):
- 按钮是否会禁用并显示加载动画?
- 交易是否成功?
- 确认后界面是否会更新?
- 余额或状态是否正确变化?
- 错误处理:在钱包中拒绝交易,确认界面能否正常恢复
- 地址显示:所有地址是否都以 ENS 或 blockies 的形式显示,而不是原始十六进制?
- 分享 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 步:测试前端
前端运行后,打开浏览器并测试应用:
- 访问
http://localhost:3000 - 截取页面快照以获取页面元素(临时钱包地址位于页眉中)
- 点击水龙头,向临时钱包充值 ETH
- 必要时从巨鲸账户转入代币(使用页面中的临时钱包地址)
- 逐一点击应用中的功能,确认其正常运行
使用 cursor-browser-extension MCP 工具进行浏览器自动化。
详细工作流请参阅 tools/testing/frontend-testing.md。
发布 Scaffold-ETH 2 项目时:
- 更新 README.md:将默认的 SE2 说明替换为项目描述
- 更新页脚链接:在
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)
将 SE2 部署到 Vercel(Monorepo 设置):
SE2 是一个 monorepo,Vercel 需要特殊配置:
- 将 Root Directory 设置为 Vercel 项目设置中的
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。不要在根目录使用带有 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.htmltrailingSlash: 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 等)需要确保以下三项正确:
- 自定义 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 笔主网交易)
如果这是一个新应用(子域名尚不存在):
交易 #1 — 创建子域名:
- 在钱包浏览器中打开
https://app.ens.domains/yourname.eth(你的钱包个人资料) - 前往“Subnames”选项卡 → “New subname”
- 输入标签(例如
token)→ Next → Skip profile → Open Wallet → Confirm - 如果 Gas 卡住:将 MetaMask 切换到 Ethereum 网络 → Activity 选项卡 → “Speed up”
交易 #2 — 设置 IPFS 内容哈希:
- 前往
https://app.ens.domains/<name>.yourname.eth - 前往“Records”选项卡 → “Edit Records” → “Other”选项卡
- 在 Content Hash 字段中粘贴:
ipfs://<CID> - 保存 → 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、Chainlinkwhales.json- 用于测试资金注入的大额 Token 持有者
最关键的概念
在 ETHEREUM 上,没有任何操作会自动执行。
智能合约无法自行执行。没有 cron 任务、没有调度器,也没有后台进程。对于每个“需要执行”的函数:
- 让任何人都可以调用(而不仅仅是管理员)
- 给调用者一个理由(利润、奖励或自身利益)
- 确保激励足够,能够覆盖 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:简单 NFT | ERC-721 | 铸造、元数据、tokenURI |
| 1:质押 | 协调 | 截止日期、托管、门槛 |
| 2:代币供应商 | ERC-20 | 授权模式、买入/卖出 |
| 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(代币化金库)
- 生息金库的标准接口
- 通过份额记账进行存款/取款
- 防范通胀攻击
安全审查清单
部署前,请确认:
- 所有管理员函数都已设置访问控制
- 已实施重入保护(CEI + nonReentrant)
- 代币小数位数处理正确
- 可抵御预言机操纵
- 已处理整数溢出(0.8+ 或 SafeMath)
- 已检查返回值(SafeERC20)
- 已进行输入验证
- 状态变更时已触发事件
- 已为维护函数设计激励机制
- 不使用无限授权(使用精确数量,绝不使用 type(uint256).max)
回复指南
帮助开发者时:
- 遵循 fork 工作流——始终使用
yarn fork,绝不使用yarn chain - 直接回答——优先回答他们的问题
- 提供代码——给出可运行的示例
- 提醒注意事项——主动指出相关陷阱
- 参考挑战——推荐通过 SpeedRun Ethereum 练习
- 询问激励机制——对于任何“自动”执行的函数,都要询问谁会调用它以及原因