ethereum-wingman by austintgriffith/ethereum-wingman
npx skills add https://github.com/austintgriffith/ethereum-wingman --skill ethereum-wingman面向 AI 代理的全面以太坊开发指南。涵盖智能合约开发、DeFi 协议、安全最佳实践以及 SpeedRun Ethereum 课程。
这些规则是强制性的。违反规则会导致生产环境中的真实错误。
externalContracts.ts 中的所有合约 — 任何你想要交互的合约(代币、协议等)都必须添加到 packages/nextjs/contracts/externalContracts.ts 中,并包含其地址和 ABI。请先阅读该文件 — 模式一目了然。
仅使用 SCAFFOLD HOOKS — 切勿使用原始 WAGMI — 始终使用 useScaffoldReadContract 和 useScaffoldWriteContract,切勿使用原始的 wagmi hooks,如 useWriteContract 或 useReadContract。
Scaffold hooks 使用 ,它会(不仅仅是钱包签名)。原始的 wagmi 的 在用户在 MetaMask 中签名的瞬间就解析了 — 在交易被挖出之前。这会导致按钮在交易仍处于待处理状态时重新启用。
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
useTransactorwriteContractAsync// ❌ 错误:原始 wagmi - 在签名后解析,而非确认后
const { writeContractAsync } = useWriteContract();
await writeContractAsync({...}); // 在 MetaMask 签名后立即返回!
// ✅ 正确:Scaffold hooks - 等待交易被挖出
const { writeContractAsync } = useScaffoldWriteContract("MyContract");
await writeContractAsync({...}); // 等待实际的链上确认
暂停。在编写或修改任何涉及以下内容的代码之前,请重新阅读下面的“关键陷阱”部分:
approve, allowance, transferFrom)transfer, safeTransfer, safeTransferFrom)这不是可选的。 存在陷阱部分是因为这些正是导致真实资金损失的失误。每次你认为“我很快就能搞定这个”的时候,恰恰是你需要重新阅读它的时候。
这些是硬性规定,而非建议。只有所有这些都满足后,构建才算完成。 这些规则是经过惨痛教训得来的。切勿跳过。
任何触发区块链交易的按钮必须:
// ✅ 正确:每个操作独立的加载状态
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 根据条件在不同按钮之间切换时显示错误的加载文本。
当用户需要批准代币然后执行操作(质押、存款、兑换)时,存在三种状态。一次只显示一个按钮:
1. 网络错误? → "切换到 Base" 按钮
2. 授权不足? → "批准" 按钮
3. 授权足够? → "质押" / "存款" / 操作按钮
// 始终使用 hook 读取授权额度(交易确认时自动更新)
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>
)}
关键: 始终通过 hook 读取授权额度,以便 UI 自动更新。切勿仅依赖本地状态。如果用户在错误的网络上点击批准,一切都会出错 — 这就是为什么网络错误检查要放在第一位。
<Address/>每次显示以太坊地址时,使用 scaffold-eth 的 <Address/> 组件。
// ✅ 正确
import { Address } from "~~/components/scaffold-eth";
<Address address={userAddress} />
// ❌ 错误 — 切勿渲染原始十六进制字符串
<span>{userAddress}</span>
<p>0x1234...5678</p>
<Address/> 处理 ENS 解析、区块头像、复制到剪贴板、截断和区块浏览器链接。原始十六进制字符串是不可接受的。
<AddressInput/>每次用户需要输入以太坊地址时,使用 scaffold-eth 的 <AddressInput/> 组件。
// ✅ 正确
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/> 用于输入。始终如此。
显示的每个代币或 ETH 金额都应包含其美元价值。 输入的每个代币或 ETH 金额都应显示实时美元预览。
// ✅ 正确 — 显示时附带美元价值
<span>1,000 TOKEN (~$4.20)</span>
<span>0.5 ETH (~$1,250.00)</span>
// ✅ 正确 — 输入时附带实时美元预览
<input value={amount} onChange={...} />
<span className="text-sm text-gray-500">
≈ ${(parseFloat(amount || "0") * tokenPrice).toFixed(2)} USD
</span>
// ❌ 错误 — 金额没有美元上下文
<span>1,000 TOKEN</span> // 用户不知道这值多少钱
如何获取价格:
useNativeCurrencyPrice() 或查看左下角页脚中的价格显示组件。它从主网 Uniswap V2 WETH/DAI 池读取。https://api.dexscreener.com/latest/dex/tokens/TOKEN_ADDRESS)、链上 Uniswap 报价器,或可用的 Chainlink 预言机。这同时适用于显示和输入:
不要在页面主体顶部放置应用名称作为 <h1>。 页头已经显示了应用名称。重复它会浪费空间,看起来不专业。
// ❌ 错误 — 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 开始 — 统计数据、表单、数据 — 而不是重复屏幕上已经可见的内容。
切勿使用公共 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 秒,不是默认的 30000
监控 RPC 使用情况: 合理情况 = 每 3 秒 1 个请求。如果你看到每秒 15+ 个请求,说明有错误:
watch: true在将前端部署到 Vercel/生产环境之前:
开放图谱 / Twitter 卡片(必需):
// 在 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:// 开头的绝对 URLlocalhost,不是相对路径)完整检查清单 — 每一项都必须通过:
summary_large_image)pollingInterval 为 3000<Address/>代码编译完成并不意味着构建完成。当你像真实用户一样测试过后,构建才算完成。
编写完所有代码后,运行质量保证检查脚本或生成一个质量保证子代理:
.tsx 文件中的原始地址字符串(应使用 <Address/>)isLoading 状态disabled 属性scaffold.config.ts 具有 rpcOverrides 和 pollingInterval: 3000layout.tsx 具有包含绝对 URL 的 OG/Twitter 元数据mainnet.base.org 或其他公共 RPCforge test)你有一个浏览器。你有一个钱包。你有真钱。使用它们。
部署到 Base(或分叉)后,打开应用并进行完整演练:
只有在所有这些都通过后,你才能告诉用户“完成了”。
对于较大的项目,生成一个具有全新上下文的子代理:
当用户想要构建任何以太坊项目时,请遵循以下步骤:
步骤 1:创建项目
npx create-eth@latest
# 选择:foundry(推荐)、目标链、项目名称
步骤 2:修复轮询间隔
编辑 packages/nextjs/scaffold.config.ts 并更改:
pollingInterval: 30000, // 默认:30 秒(太慢了!)
为:
pollingInterval: 3000, // 3 秒(对开发来说好得多)
步骤 3:安装并分叉实时网络
cd <project-name>
yarn install
yarn fork --network base # 或 mainnet, arbitrum, optimism, polygon
⚠️ 重要:使用分叉模式时,前端目标网络必须是 chains.foundry(链 ID 31337),而不是你正在分叉的链!
分叉在本地 Anvil 上运行,链 ID 为 31337。即使你正在分叉 Base、Arbitrum 等,scaffold 配置也必须使用:
targetNetworks: [chains.foundry], // 不是 chains.base!
只有在部署到真实网络时,才切换到 chains.base(或其他链)。
步骤 4:启用自动区块挖矿(必需!)
# 在新的终端中,启用间隔挖矿(1 区块/秒)
cast rpc anvil_setIntervalMining 1
没有这个,block.timestamp 会保持冻结,依赖时间的逻辑会中断!
可选:使其永久化,通过编辑 packages/foundry/package.json 在分叉脚本中添加 --block-time 1。
步骤 5:部署到本地分叉(免费!)
yarn deploy
步骤 6:启动前端
yarn start
步骤 7:测试前端
前端运行后,打开浏览器并测试应用:
http://localhost:3000使用 cursor-browser-extension MCP 工具进行浏览器自动化。有关详细工作流程,请参阅 tools/testing/frontend-testing.md。
packages/nextjs/components/Footer.tsx 中,将 "Fork me" 链接从 https://github.com/scaffold-eth/se-2 更改为你的实际仓库 URLpackages/nextjs/app/layout.tsx 中,更改元数据标题/描述packages/nextjs/components/Header.tsx 中,从 menuLinks 中移除 Debug Contracts 条目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 是一个 monorepo — Vercel 需要特殊配置:
packages/nextjscd ../.. && yarn install(从工作区根目录安装)next build — 自动检测).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 — 由于根目录设置,Vercel 已经在 packages/nextjs 中了。不要在根级别使用带有 framework: "nextjs" 的 vercel.json — Vercel 无法在根目录的 package.json 中找到 Next.js 并会失败。
⚠️ Vercel 内存不足: SE2 的完整 monorepo 安装(foundry + nextjs + 所有依赖项)可能超过 Vercel 的 8GB 构建内存。如果构建因“内存不足”/退出代码 129 而失败:
NODE_OPTIONS=--max-old-space-size=7168yarn ipfs)vercel --prebuilt(在本地构建,将输出部署到 Vercel)这是 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 等)。这会在静态页面生成(SSG/导出)期间破坏 next-themes、RainbowKit 以及任何调用 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 处于活动状态。
问题: 你编辑了 page.tsx,然后给了用户之前部署的旧 IPFS URL。代码更改在源代码中,但 out/ 目录仍然包含旧的构建。这种情况已经发生了多次。
根本原因: 构建步骤 (yarn build) 生成 out/。如果你在构建之后但在部署之前编辑源文件,部署会上传陈旧的输出。或者更糟 — 你完全跳过重新构建,只是重新上传旧的 out/。
强制:在任何代码更改之后,始终执行完整周期:
# 1. 删除旧的构建产物(防止任何缓存)
rm -rf .next out
# 2. 从头开始重新构建
NODE_OPTIONS="--require ./polyfill-localstorage.cjs" NEXT_PUBLIC_IPFS_BUILD=true NEXT_PUBLIC_IGNORE_BUILD_ERROR=true yarn build
# 3. 验证新构建包含你的更改(抽查 JS 包)
grep -l "YOUR_UNIQUE_STRING" out/_next/static/chunks/app/*.js
# 4. 只有在那之后才上传
yarn bgipfs upload out
如何检测陈旧部署:
# 比较时间戳 — 源文件必须比 out/ 更旧
stat -f '%Sm' app/page.tsx # 源文件修改时间
stat -f '%Sm' out/ # 构建输出时间
# 如果源文件比 out/ 更新 → 构建是陈旧的,先重新构建!
CID 是你的证明: 如果部署后 IPFS CID 没有改变,说明你部署了相同的内容。真正的代码更改总是会产生一个新的 CID。
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.htmlIPFS 网关会自动将目录解析为 index.html,但它们不会解析裸文件名。所以 /debug → 寻找目录 debug/ → 找到 index.html ✅。没有尾部斜杠,/debug → 没有目录,没有文件匹配 → 404 ❌。
3. 路由必须在静态导出预渲染中存活 在使用 output: "export" 运行 yarn build 期间,Next.js 将每个页面预渲染为 HTML。如果一个页面在预渲染期间崩溃(例如,需要浏览器 API 的 hooks、localStorage.getItem is not a function),该路由会被跳过 — 不会生成 HTML 文件,并且在 IPFS 上会返回 404。
常见的预渲染杀手:
localStorage / sessionStoragewindow, document)localStorage — 如果不需要,重命名为 _blockexplorer-disabled)构建后如何验证路由:
# 检查 out/ 是否为每个路由都有一个目录 + index.html
ls out/*/index.html
# 应该显示:out/debug/index.html, out/other-route/index.html, 等等。
# 验证特定路由
curl -s -o /dev/null -w "%{http_code}" -L "https://YOUR_GATEWAY/ipfs/CID/debug/"
# 应该返回 200,而不是 404
完整的 IPFS 安全 next.config.ts 模式:
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 上没有图像优化服务器
};
}
当用户说“发布它”时,请遵循这个确切的顺序。标记为 🤖 的步骤是完全自动化的。标记为 👤 的步骤需要人工输入。
步骤 1: 🤖 最终代码审查
yarn start)步骤 2: 👤 询问用户想要什么域名 询问:"你想要什么子域名?例如 token.yourname.eth → token.yourname.eth.limo" 保存答案 — 它决定了元数据和 ENS 设置的生产 URL。
步骤 3: 🤖 生成 OG 图片并修复元数据以正确展开
社交展开(Twitter、Telegram、Discord 等)需要三件事正确:
localhost:3000twitter:card 设置为 summary_large_image 以获取大预览生成 OG 图片 (public/thumbnail.png, 1200x630):
# 使用 PIL/Pillow 创建一个带有品牌标识的 1200x630 OG 图片,包含:
# - 应用名称和标语
# - 生产 URL (name.yourname.eth.limo)
# - 深色背景、简洁布局、强调色
# 保存到: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}`;
如果此环境变量模式已在文件中,请跳过此步骤。
步骤 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
# 验证(上传前必须全部通过):
ls out/*/index.html # 路由存在
grep 'og:image' out/index.html # 不是 localhost
stat -f '%Sm' app/page.tsx && stat -f '%Sm' out/ # 源文件比构建更旧
# 上传:
yarn bgipfs upload out
# 保存 CID!
步骤 5: 👤 分享 IPFS URL 供验证 发送:"这是供审查的构建:https://community.bgipfs.com/ipfs/<CID>" 在触及 ENS 之前等待批准。 在用户说继续之前不要继续。
步骤 6: 🤖 设置 ENS 子域名(2 笔主网交易)
如果这是一个新应用(子域名尚不存在):
交易 #1 — 创建子域名:
https://app.ens.domains/yourname.ethtoken)→ 下一步 → 跳过配置文件 → 打开钱包 → 确认交易 #2 — 设置 IPFS 内容哈希:
https://app.ens.domains/<name>.yourname.ethipfs://<CID>如果这是对现有应用的更新:跳过交易 #1,只执行交易 #2(更新内容哈希)。
步骤 7: 🤖 验证一切
# 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>
# 2. .limo 网关响应(缓存可能需要几分钟)
curl -s -o /dev/null -w "%{http_code}" -L "https://<name>.yourname.eth.limo"
# 3. OG 元数据正确
curl -s -L "https://<name>.yourname.eth.limo" | grep 'og:image'
# 应该显示生产 URL,而不是 localhost
步骤 8: 👤 向用户报告 发送:"已上线:https://<name>.yourname.eth.limo — 展开元数据已设置,ENS 内容哈希已在链上确认。"
⚠️ 已知陷阱:
thumbnail.png 和 thumbnail.jpg。在生产前始终Comprehensive Ethereum development guide for AI agents. Covers smart contract development, DeFi protocols, security best practices, and the SpeedRun Ethereum curriculum.
These rules are MANDATORY. Violations cause real bugs in production.
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.
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.
// ❌ 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
STOP. Re-read the "Critical Gotchas" section below before writing or modifying ANY code that touches:
approve, allowance, transferFrom)transfer, safeTransfer, safeTransferFrom)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.
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.
ANY button that triggers a blockchain transaction MUST:
// ✅ 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 sharedisLoading 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.
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
// 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.
<Address/>EVERY time you display an Ethereum address , use scaffold-eth's <Address/> component.
// ✅ 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.
<AddressInput/>EVERY time the user needs to enter an Ethereum address , use scaffold-eth's <AddressInput/> component.
// ✅ 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.
EVERY token or ETH amount displayed should include its USD value. EVERY token or ETH input should show a live USD preview.
// ✅ 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:
useNativeCurrencyPrice() or check the price display component in the bottom-left footer. It reads from mainnet Uniswap V2 WETH/DAI pool.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:
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.
// ❌ 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.
NEVER use public RPCs (mainnet.base.org, etc.) — they rate-limit and cause random failures.
In scaffold.config.ts, ALWAYS set:
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:
watch: true on hooks that don't need itBEFORE deploying frontend to Vercel/production:
Open Graph / Twitter Cards (REQUIRED):
// 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:
https://localhost, NOT relative path)Full checklist — EVERY item must pass:
summary_large_image)pollingInterval is 3000<Address/>A build is NOT done when the code compiles. A build is done when you've tested it like a real user.
After writing all code, run the QA check script or spawn a QA sub-agent:
.tsx files for raw address strings (should use <Address/>)isLoading state across multiple buttonsdisabled props on transaction buttonsscaffold.config.ts has rpcOverrides and pollingInterval: 3000layout.tsx has OG/Twitter meta with absolute URLsmainnet.base.org or other public RPCs in any fileforge test)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:
Only after ALL of this passes can you tell the user "it's done."
For bigger projects, spawn a sub-agent with a fresh context:
When a user wants to BUILD any Ethereum project, follow these steps:
Step 1: Create Project
npx create-eth@latest
# Select: foundry (recommended), target chain, project name
Step 2: Fix Polling Interval
Edit packages/nextjs/scaffold.config.ts and change:
pollingInterval: 30000, // Default: 30 seconds (way too slow!)
to:
pollingInterval: 3000, // 3 seconds (much better for development)
Step 3: Install & Fork a Live Network
cd <project-name>
yarn install
yarn fork --network base # or mainnet, arbitrum, optimism, polygon
⚠️ IMPORTANT: When using fork mode, the frontend target network MUST bechains.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:
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!)
# In a new terminal, enable interval mining (1 block/second)
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!)
yarn deploy
Step 6: Start Frontend
yarn start
Step 7: Test the Frontend
After the frontend is running, open a browser and test the app:
http://localhost:3000Use the cursor-browser-extension MCP tools for browser automation. See tools/testing/frontend-testing.md for detailed workflows.
packages/nextjs/components/Footer.tsx, change the "Fork me" link from https://github.com/scaffold-eth/se-2 to your actual repo URLpackages/nextjs/app/layout.tsx, change the metadata title/descriptionpackages/nextjs/components/Header.tsx, remove the Debug Contracts entry from menuLinksWant 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 is a monorepo — Vercel needs special configuration:
packages/nextjs in Vercel project settingscd ../.. && yarn install (installs from workspace root)next build — auto-detected).next)Via 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"}'
Via CLI (after linking):
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:
NODE_OPTIONS=--max-old-space-size=7168yarn ipfs)vercel --prebuilt (build locally, deploy output to Vercel)This is the RECOMMENDED deploy path for SE2. Avoids Vercel's memory limits entirely.
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):
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/:
// 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 onlyinstrumentation.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.
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:
# 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
How to detect a stale deploy:
# 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!
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.
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.htmltrailingSlash: true → generates debug/index.htmlIPFS 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 timewindow, document)localStorage at import time — rename to _blockexplorer-disabled if not needed)How to verify routes after build:
# 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
The complete IPFS-safe next.config.ts pattern:
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
};
}
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
yarn start) one last timeStep 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:
localhost:3000twitter:card set to summary_large_image for large previewGenerate the OG image (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
Fix metadata baseUrl — ensure utils/scaffold-eth/getMetadata.ts supports 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}`;
If this env var pattern is already in the file, skip this step.
Step 4: 🤖 Clean build + IPFS deploy
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: 👤 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:
https://app.ens.domains/yourname.eth in the wallet browser (your wallet profile)token) → Next → Skip profile → Open Wallet → ConfirmTx #2 — Set IPFS content hash:
https://app.ens.domains/<name>.yourname.ethipfs://<CID>If this is an update to an existing app: skip Tx #1, only do Tx #2 (update the content hash).
Step 7: 🤖 Verify everything
# 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: 👤 Report to the user Send: "Live athttps://<name>.yourname.eth.limo — unfurl metadata set, ENS content hash confirmed on-chain."
⚠️ Known gotchas:
thumbnail.png and thumbnail.jpg. ALWAYS replace both before production.NEXT_PUBLIC_PRODUCTION_URL isn't set, og:image will point to localhost:3000. Always verify with grep.yarn chain (use yarn fork --network <chain> instead!)forge init or set up Foundry from scratchyarn 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, protocol, and whale addresses are in data/addresses/:
tokens.json - WETH, USDC, DAI, etc. per chainprotocols.json - Uniswap, Aave, Chainlink per chainwhales.json - Large token holders for test fundingNOTHING 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":
Always ask: "Who calls this function? Why would they pay gas?"
If you can't answer this, your function won't get called.
// 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);
}
USDC = 6 decimals, not 18!
// BAD: Assumes 18 decimals - transfers 1 TRILLION USDC!
uint256 oneToken = 1e18;
// GOOD: Check decimals
uint256 oneToken = 10 ** token.decimals();
Common decimals:
Contracts cannot pull tokens directly. Two-step process:
// Step 1: User approves
token.approve(spenderContract, amount);
// Step 2: Contract pulls tokens
token.transferFrom(user, address(this), amount);
Never use infinite approvals:
// DANGEROUS
token.approve(spender, type(uint256).max);
// SAFE
token.approve(spender, exactAmount);
Use basis points (1 bp = 0.01%):
// BAD: This equals 0
uint256 fivePercent = 5 / 100;
// GOOD: Basis points
uint256 FEE_BPS = 500; // 5% = 500 basis points
uint256 fee = (amount * FEE_BPS) / 10000;
External calls can call back into your contract:
// 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.
Flash loans can manipulate spot prices instantly:
// 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);
}
First depositor can steal funds via share manipulation:
// Mitigation: Virtual offset
function convertToShares(uint256 assets) public view returns (uint256) {
return assets.mulDiv(totalSupply() + 1e3, totalAssets() + 1);
}
Some tokens (USDT) don't return bool on transfer:
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
using SafeERC20 for IERC20;
token.safeTransfer(to, amount); // Handles non-standard tokens
packages/
├── foundry/ # Smart contracts
│ ├── contracts/ # Your Solidity files
│ └── script/ # Deploy scripts
└── nextjs/
├── app/ # React pages
└── contracts/ # Generated ABIs + externalContracts.ts
// Read contract data
const { data } = useScaffoldReadContract({
contractName: "YourContract",
functionName: "greeting",
});
// Write to contract
const { writeContractAsync } = useScaffoldWriteContract("YourContract");
// Watch events
useScaffoldEventHistory({
contractName: "YourContract",
eventName: "Transfer",
fromBlock: 0n,
});
Reference these for hands-on learning:
| Challenge | Concept | Key Lesson |
|---|---|---|
| 0: Simple NFT | ERC-721 | Minting, metadata, tokenURI |
| 1: Staking | Coordination | Deadlines, escrow, thresholds |
| 2: Token Vendor | ERC-20 | Approve pattern, buy/sell |
| 3: Dice Game | Randomness | On-chain randomness is insecure |
| 4: DEX | AMM | x*y=k formula, slippage |
| 5: Oracles | Price Feeds | Chainlink, manipulation resistance |
| 6: Lending | Collateral | Health factor, liquidation incentives |
| 7: Stablecoins | Pegging | CDP, over-collateralization |
| 8: Prediction Markets | Resolution | Outcome determination |
Before deployment, verify:
When helping developers:
yarn fork, never yarn chainWeekly Installs
235
Repository
GitHub Stars
40
First Seen
Jan 22, 2026
Security Audits
Gen Agent Trust HubWarnSocketPassSnykWarn
Installed on
opencode149
codex147
gemini-cli135
claude-code134
github-copilot118
cursor106
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
106,200 周安装
| 9: ZK Voting | Privacy | Zero-knowledge proofs |
| 10: Multisig | Signatures | Threshold approval |
| 11: SVG NFT | On-chain Art | Generative, base64 encoding |