From 783fd8a6d78ea40e1307e82b324ca6d34d9bb97d Mon Sep 17 00:00:00 2001 From: 0xinit <28729137+0xinit@users.noreply.github.com> Date: Wed, 4 Mar 2026 10:22:14 -0800 Subject: [PATCH 01/14] feat: add polymarket skill --- skills/polymarket/SKILL.md | 605 +++++++++++++++++++++++++++++++++++++ 1 file changed, 605 insertions(+) create mode 100644 skills/polymarket/SKILL.md diff --git a/skills/polymarket/SKILL.md b/skills/polymarket/SKILL.md new file mode 100644 index 0000000..29dc14b --- /dev/null +++ b/skills/polymarket/SKILL.md @@ -0,0 +1,605 @@ +--- +name: polymarket +description: "Polymarket CLOB prediction market integration — authentication (L1/L2 HMAC signing), order placement (limit, market, GTC, GTD, FOK), orderbook reading, market data (Gamma API), WebSocket subscriptions, CTF conditional token operations (split, merge, redeem), gasless trading via relayer, and builder program attribution." +license: Apache-2.0 +metadata: + author: 0xinit + version: "1.0" + chain: polygon + category: Trading +tags: + - polymarket + - prediction-markets + - clob + - conditional-tokens + - orderbook + - polygon + - trading +--- + +# Polymarket + +Polymarket is a prediction market protocol running on Polygon. It operates a Central Limit Order Book (CLOB) where users trade binary outcome tokens priced between $0 and $1. Each market has Yes and No tokens backed by USDC through the Conditional Token Framework (CTF). The CLOB uses off-chain matching with on-chain settlement via Polygon. Authentication is two-layered: L1 (EIP-712 wallet signing) to derive API credentials, L2 (HMAC-SHA256) to authenticate trading requests. + +Base URLs: +- **CLOB API**: `https://clob.polymarket.com` +- **Gamma API** (market data): `https://gamma-api.polymarket.com` +- **Data API** (trades/positions): `https://data-api.polymarket.com` +- **WebSocket (Market)**: `wss://ws-subscriptions-clob.polymarket.com/ws/market` +- **WebSocket (User)**: `wss://ws-subscriptions-clob.polymarket.com/ws/user` +- **Relayer** (gasless): `https://relayer-v2.polymarket.com/` + +## What You Probably Got Wrong + +> LLMs have stale training data. These are the most common mistakes. + +- **"Polymarket uses standard API keys"** -- Polymarket has two-level auth. You first sign an EIP-712 message with your private key (L1) to create HMAC credentials, then use those credentials (L2) to sign every trading request. You cannot skip L1 or use the credentials without HMAC signing. +- **"I can trade with just a private key"** -- You also need a **funder address** (your Polygon proxy wallet) and must select the correct **signature type** (0=EOA, 1=POLY_PROXY, 2=GNOSIS_SAFE). Most new integrations use type `2`. Without the correct funder + sig type combo, orders silently fail. +- **"Prices are in dollars"** -- Prices are probabilities between 0 and 1. A Yes token at 0.65 means the market implies 65% probability. Buying at 0.65 pays $1.00 if the event occurs, netting $0.35 profit per share. +- **"I can use any price increment"** -- Each market has a tick size (0.1, 0.01, 0.001, or 0.0001). Orders with prices that do not conform to the tick size are rejected with `INVALID_ORDER_MIN_TICK_SIZE`. Always query the tick size before placing orders. +- **"FOK amount is share count"** -- For FOK/FAK BUY orders, `amount` is the **dollar amount to spend**, not shares. For SELL orders, `amount` is shares. Getting this wrong causes unexpected fill sizes. +- **"WebSocket stays connected automatically"** -- You must send `PING` every 10 seconds. Without heartbeats, the connection drops silently after ~10 seconds. +- **"Neg risk markets work like standard markets"** -- Multi-outcome events use a different exchange contract (`0xC5d563A36AE78145C45a50134d48A1215220f80a`) and require `negRisk: true` in order options. Using the standard exchange contract for neg risk markets causes transaction reverts. +- **"I can use ethers v6"** -- The `@polymarket/clob-client` SDK requires ethers v5 (`Wallet` from `ethers`). Ethers v6 changed the Signer interface and is not compatible. + +## API Configuration + +| API | Base URL | Auth | Purpose | +|-----|----------|------|---------| +| CLOB | `https://clob.polymarket.com` | L2 for trades, none for reads | Orderbook, prices, order submission | +| Gamma | `https://gamma-api.polymarket.com` | None | Events, markets, search | +| Data | `https://data-api.polymarket.com` | None | Trades, positions, user data | +| WS Market | `wss://ws-subscriptions-clob.polymarket.com/ws/market` | None | Real-time orderbook | +| WS User | `wss://ws-subscriptions-clob.polymarket.com/ws/user` | API creds in message | Trade/order updates | +| Relayer | `https://relayer-v2.polymarket.com/` | Builder headers | Gasless transactions | + +## Contract Addresses (Polygon) + +| Contract | Address | Last verified March 2026 | +|----------|---------|--------------------------| +| USDC (USDC.e) | `0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174` | Bridged USDC collateral | +| CTF (Conditional Tokens) | `0x4D97DCd97eC945f40cF65F87097ACe5EA0476045` | Token storage and operations | +| CTF Exchange | `0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E` | Standard market trading | +| Neg Risk CTF Exchange | `0xC5d563A36AE78145C45a50134d48A1215220f80a` | Multi-outcome market trading | +| Neg Risk Adapter | `0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296` | Neg risk conversions | + +## Authentication + +Polymarket uses two-level auth: **L1** (EIP-712 signing) to create credentials, **L2** (HMAC-SHA256) to authenticate requests. + +### L1: Derive API Credentials + +L1 proves wallet ownership via EIP-712 signature. Used once to create or derive API credentials. + +```typescript +import { ClobClient } from "@polymarket/clob-client"; +import { Wallet } from "ethers"; + +const HOST = "https://clob.polymarket.com"; +const CHAIN_ID = 137; +const signer = new Wallet(process.env.PRIVATE_KEY!); + +const tempClient = new ClobClient(HOST, CHAIN_ID, signer); +const apiCreds = await tempClient.createOrDeriveApiKey(); +// { apiKey: "uuid", secret: "base64...", passphrase: "string" } +``` + +The EIP-712 domain used under the hood: + +```typescript +const domain = { + name: "ClobAuthDomain", + version: "1", + chainId: 137, +}; + +const types = { + ClobAuth: [ + { name: "address", type: "address" }, + { name: "timestamp", type: "string" }, + { name: "nonce", type: "uint256" }, + { name: "message", type: "string" }, + ], +}; +``` + +### L2: Initialize Trading Client + +L2 uses HMAC-SHA256 signatures from the derived credentials. Required for all trade endpoints. + +```typescript +import { ClobClient, Side, OrderType } from "@polymarket/clob-client"; +import { Wallet } from "ethers"; + +const signer = new Wallet(process.env.PRIVATE_KEY!); + +const client = new ClobClient( + "https://clob.polymarket.com", + 137, + signer, + apiCreds, + 2, // signatureType: 0=EOA, 1=POLY_PROXY, 2=GNOSIS_SAFE + process.env.POLYMARKET_FUNDER_ADDRESS! // proxy wallet from polymarket.com/settings +); +``` + +### Signature Types + +| Type | Value | When to Use | +|------|-------|-------------| +| EOA | `0` | Standard wallet. Funder = wallet address. Needs POL for gas. | +| POLY_PROXY | `1` | Magic Link proxy. User exported PK from Polymarket.com. | +| GNOSIS_SAFE | `2` | Most common for new integrations. Gnosis Safe multisig proxy. | + +### L2 Headers (sent automatically by SDK) + +| Header | Description | +|--------|-------------| +| `POLY_ADDRESS` | Polygon signer address | +| `POLY_SIGNATURE` | HMAC-SHA256 signature of request | +| `POLY_TIMESTAMP` | Current UNIX timestamp | +| `POLY_API_KEY` | API key from credential creation | +| `POLY_PASSPHRASE` | Passphrase from credential creation | + +## Order Placement + +All orders are limit orders. Market orders are limit orders with a marketable price that execute immediately. + +### Order Types + +| Type | Behavior | Use Case | +|------|----------|----------| +| **GTC** | Good-Til-Cancelled. Rests on book until filled or cancelled. | Default limit orders | +| **GTD** | Good-Til-Date. Active until expiration (UTC seconds). Min = `now + 60 + N`. | Auto-expire before events | +| **FOK** | Fill-Or-Kill. Fill entirely immediately or cancel. | All-or-nothing market orders | +| **FAK** | Fill-And-Kill. Fill what is available, cancel rest. | Partial-fill market orders | + +### Limit Order (GTC) + +```typescript +const response = await client.createAndPostOrder( + { + tokenID: "TOKEN_ID", + price: 0.50, + size: 10, + side: Side.BUY, + }, + { + tickSize: "0.01", + negRisk: false, + }, + OrderType.GTC +); +console.log(response.orderID, response.status); +``` + +### Two-Step Pattern (Sign Then Submit) + +```typescript +const signedOrder = await client.createOrder( + { tokenID: "TOKEN_ID", price: 0.50, size: 10, side: Side.BUY }, + { tickSize: "0.01", negRisk: false } +); +const response = await client.postOrder(signedOrder, OrderType.GTC); +``` + +### Market Order (FOK) + +```typescript +// BUY: amount = dollar amount to spend. SELL: amount = shares to sell. +const response = await client.createAndPostMarketOrder( + { tokenID: "TOKEN_ID", side: Side.BUY, amount: 100, price: 0.55 }, + { tickSize: "0.01", negRisk: false }, + OrderType.FOK +); +``` + +### GTD Order (Expiring) + +```typescript +// Expire in 1 hour. Security threshold: add 60 seconds minimum. +const expiration = Math.floor(Date.now() / 1000) + 60 + 3600; + +const response = await client.createAndPostOrder( + { tokenID: "TOKEN_ID", price: 0.50, size: 10, side: Side.BUY, expiration }, + { tickSize: "0.01", negRisk: false }, + OrderType.GTD +); +``` + +### Post-Only Orders + +Guarantee maker status. Rejected if the order would cross the spread. + +```typescript +const response = await client.postOrder(signedOrder, OrderType.GTC, true); +``` + +Post-only works with GTC and GTD only. Rejected if combined with FOK or FAK. + +### Batch Orders (up to 15) + +```typescript +import { PostOrdersArgs } from "@polymarket/clob-client"; + +const orders: PostOrdersArgs[] = [ + { + order: await client.createOrder( + { tokenID: "TOKEN_ID", price: 0.48, side: Side.BUY, size: 500 }, + { tickSize: "0.01", negRisk: false } + ), + orderType: OrderType.GTC, + }, + { + order: await client.createOrder( + { tokenID: "TOKEN_ID", price: 0.52, side: Side.SELL, size: 500 }, + { tickSize: "0.01", negRisk: false } + ), + orderType: OrderType.GTC, + }, +]; +const response = await client.postOrders(orders); +``` + +### Cancel Orders + +```typescript +await client.cancelOrder("0xORDER_ID"); +await client.cancelOrders(["0xID_1", "0xID_2"]); +await client.cancelAll(); +await client.cancelMarketOrders({ market: "0xCONDITION_ID" }); +await client.cancelMarketOrders({ + market: "0xCONDITION_ID", + asset_id: "TOKEN_ID", +}); +``` + +### Heartbeat (Dead Man's Switch) + +If heartbeat not received within 10 seconds (5s buffer), all open orders are cancelled. + +```typescript +let heartbeatId = ""; +setInterval(async () => { + const resp = await client.postHeartbeat(heartbeatId); + heartbeatId = resp.heartbeat_id; +}, 5000); +``` + +## Orderbook and Market Data + +### Read Orderbook (No Auth) + +```typescript +const readClient = new ClobClient("https://clob.polymarket.com", 137); +const book = await readClient.getOrderBook("TOKEN_ID"); +console.log("Best bid:", book.bids[0], "Best ask:", book.asks[0]); + +const mid = await readClient.getMidpoint("TOKEN_ID"); +const spread = await readClient.getSpread("TOKEN_ID"); +const lastPrice = await readClient.getLastTradePrice("TOKEN_ID"); +``` + +### Price History + +```typescript +import { PriceHistoryInterval } from "@polymarket/clob-client"; + +const history = await readClient.getPricesHistory({ + market: "TOKEN_ID", + interval: PriceHistoryInterval.ONE_DAY, + fidelity: 60, +}); +// Each entry: { t: timestamp, p: price } +``` + +### Gamma API (Events and Markets) + +```bash +# Active events sorted by volume +curl "https://gamma-api.polymarket.com/events?active=true&closed=false&sort=volume_24hr&ascending=false&limit=100" + +# Event by slug +curl "https://gamma-api.polymarket.com/events?slug=fed-decision-in-october" + +# Events by tag +curl "https://gamma-api.polymarket.com/events?tag_id=100381&limit=10&active=true&closed=false" + +# Discover tags +curl "https://gamma-api.polymarket.com/tags/ranked" +``` + +### Batch Orderbook Queries + +All orderbook queries have batch variants (up to 500 tokens): + +```typescript +const prices = await readClient.getPrices([ + { token_id: "TOKEN_A", side: Side.BUY }, + { token_id: "TOKEN_B", side: Side.BUY }, +]); +``` + +| Single | Batch | REST | +|--------|-------|------| +| `getOrderBook()` | `getOrderBooks()` | `POST /books` | +| `getPrice()` | `getPrices()` | `POST /prices` | +| `getMidpoint()` | `getMidpoints()` | `POST /midpoints` | +| `getSpread()` | `getSpreads()` | `POST /spreads` | + +## WebSocket Subscriptions + +### Market Channel (Public) + +```typescript +const ws = new WebSocket("wss://ws-subscriptions-clob.polymarket.com/ws/market"); + +ws.onopen = () => { + ws.send(JSON.stringify({ + type: "market", + assets_ids: ["TOKEN_ID"], + custom_feature_enabled: true, + })); + setInterval(() => ws.send("PING"), 10_000); +}; + +ws.onmessage = (event) => { + if (event.data === "PONG") return; + const msg = JSON.parse(event.data); + switch (msg.event_type) { + case "book": + console.log("Snapshot:", msg.bids.length, "bids", msg.asks.length, "asks"); + break; + case "price_change": + for (const pc of msg.price_changes) { + console.log(`${pc.side} ${pc.size}@${pc.price}`); + } + break; + case "last_trade_price": + console.log(`Trade: ${msg.side} ${msg.size}@${msg.price}`); + break; + case "tick_size_change": + console.log(`Tick: ${msg.old_tick_size} -> ${msg.new_tick_size}`); + break; + } +}; +``` + +Set `custom_feature_enabled: true` to enable `best_bid_ask`, `new_market`, and `market_resolved` events. + +### Market Channel Event Types + +| Event | Trigger | Key Fields | +|-------|---------|------------| +| `book` | On subscribe + trade affects book | `bids[]`, `asks[]`, `hash`, `timestamp` | +| `price_change` | Order placed or cancelled | `price_changes[]` with `price`, `size`, `side` | +| `last_trade_price` | Trade executed | `price`, `side`, `size`, `fee_rate_bps` | +| `tick_size_change` | Price hits >0.96 or <0.04 | `old_tick_size`, `new_tick_size` | +| `best_bid_ask` | Top-of-book changes | `best_bid`, `best_ask`, `spread` | +| `market_resolved` | Market resolved | `winning_asset_id`, `winning_outcome` | + +`tick_size_change` is critical for bots -- if the tick size changes and you use the old one, orders are rejected. + +### User Channel (Authenticated) + +Subscribes by condition IDs (market IDs), not asset IDs. + +```typescript +const ws = new WebSocket("wss://ws-subscriptions-clob.polymarket.com/ws/user"); + +ws.onopen = () => { + ws.send(JSON.stringify({ + auth: { + apiKey: process.env.POLY_API_KEY!, + secret: process.env.POLY_API_SECRET!, + passphrase: process.env.POLY_PASSPHRASE!, + }, + markets: ["0xCONDITION_ID"], + type: "USER", + })); + setInterval(() => ws.send("PING"), 10_000); +}; +``` + +### Dynamic Subscribe/Unsubscribe + +```typescript +// Add more assets without reconnecting +ws.send(JSON.stringify({ assets_ids: ["NEW_TOKEN_ID"], operation: "subscribe" })); + +// Remove assets +ws.send(JSON.stringify({ assets_ids: ["OLD_TOKEN_ID"], operation: "unsubscribe" })); +``` + +## CTF Operations (Split, Merge, Redeem) + +The Conditional Token Framework creates ERC1155 tokens for market outcomes. Every binary market has Yes and No tokens, each backed by $1.00 USDC. + +### Split: USDC into Outcome Tokens + +``` +$100 USDC -> 100 Yes tokens + 100 No tokens +``` + +| Parameter | Type | Value | +|-----------|------|-------| +| `collateralToken` | address | `0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174` (USDC) | +| `parentCollectionId` | bytes32 | `0x0000...0000` (32 zero bytes) | +| `conditionId` | bytes32 | Market condition ID | +| `partition` | uint[] | `[1, 2]` for binary (Yes=1, No=2) | +| `amount` | uint256 | USDC amount to split | + +Prerequisites: USDC balance on Polygon, USDC approval for CTF contract. + +### Merge: Outcome Tokens Back to USDC + +``` +100 Yes tokens + 100 No tokens -> $100 USDC +``` + +Same parameters as split. Burns one unit of each position per unit of collateral returned. Requires equal amounts of both tokens. + +### Redeem: Winning Tokens After Resolution + +``` +Market resolves YES: + 100 Yes tokens -> $100 USDC + 100 No tokens -> $0 +``` + +Redemption burns your entire token balance for the condition -- no amount parameter. Winning tokens are always redeemable with no deadline. + +### Standard vs Neg Risk Markets + +| Feature | Standard | Neg Risk | +|---------|----------|----------| +| Exchange | CTF Exchange | Neg Risk CTF Exchange | +| Multi-outcome | Independent | Linked via conversion | +| `negRisk` flag | `false` | `true` | +| Order option | `negRisk: false` | `negRisk: true` | + +### Approval Matrix + +| Operation | Contract to Approve | Token | +|-----------|-------------------|-------| +| Buy (standard) | CTF Exchange | USDC | +| Sell (standard) | CTF Exchange | Conditional tokens | +| Buy (neg risk) | Neg Risk CTF Exchange | USDC | +| Sell (neg risk) | Neg Risk CTF Exchange | Conditional tokens | +| Split | CTF | USDC | +| Neg risk conversion | Neg Risk Adapter | Conditional tokens | + +## Gasless Trading (Builder Program) + +The Relayer Client enables gasless transactions. Polymarket pays gas fees; users only need USDC. Requires Builder Program membership. + +```bash +npm install @polymarket/builder-relayer-client @polymarket/builder-signing-sdk +``` + +```typescript +import { createWalletClient, http, type Hex } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { polygon } from "viem/chains"; +import { RelayClient } from "@polymarket/builder-relayer-client"; +import { BuilderConfig } from "@polymarket/builder-signing-sdk"; + +const account = privateKeyToAccount(process.env.PRIVATE_KEY as Hex); +const wallet = createWalletClient({ + account, + chain: polygon, + transport: http(process.env.RPC_URL), +}); + +const builderConfig = new BuilderConfig({ + localBuilderCreds: { + key: process.env.POLY_BUILDER_API_KEY!, + secret: process.env.POLY_BUILDER_SECRET!, + passphrase: process.env.POLY_BUILDER_PASSPHRASE!, + }, +}); + +const relayClient = new RelayClient( + "https://relayer-v2.polymarket.com/", + 137, + wallet, + builderConfig +); +``` + +### Gasless Token Approval Example + +```typescript +import { encodeFunctionData, maxUint256 } from "viem"; + +const USDC = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"; +const CTF = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045"; + +const approveTx = { + to: USDC, + data: encodeFunctionData({ + abi: [{ + name: "approve", type: "function", + inputs: [{ name: "spender", type: "address" }, { name: "amount", type: "uint256" }], + outputs: [{ type: "bool" }], + }], + functionName: "approve", + args: [CTF, maxUint256], + }), + value: "0", +}; + +const response = await relayClient.execute([approveTx], "Approve USDC for CTF"); +await response.wait(); +``` + +## Builder Program + +Builders receive order attribution and relayer access. Setup: + +1. Go to `polymarket.com/settings?tab=builder` +2. Create builder profile and generate API keys +3. Add builder config to your CLOB client + +```typescript +import { BuilderConfig, type BuilderApiKeyCreds } from "@polymarket/builder-signing-sdk"; + +const builderCreds: BuilderApiKeyCreds = { + key: process.env.POLY_BUILDER_API_KEY!, + secret: process.env.POLY_BUILDER_SECRET!, + passphrase: process.env.POLY_BUILDER_PASSPHRASE!, +}; + +const builderConfig = new BuilderConfig({ localBuilderCreds: builderCreds }); + +const client = new ClobClient( + "https://clob.polymarket.com", + 137, + signer, + apiCreds, + 2, + funderAddress, + undefined, + false, + builderConfig +); +// Orders automatically include builder attribution headers +``` + +### Builder Headers + +| Header | Description | +|--------|-------------| +| `POLY_BUILDER_API_KEY` | Builder API key | +| `POLY_BUILDER_TIMESTAMP` | Unix timestamp | +| `POLY_BUILDER_PASSPHRASE` | Builder passphrase | +| `POLY_BUILDER_SIGNATURE` | HMAC-SHA256 of request | + +### Remote Signing + +Keep builder credentials on a separate server for security: + +```typescript +const builderConfig = new BuilderConfig({ + remoteBuilderConfig: { url: "https://your-server.com/sign" }, +}); +``` + +Your server receives `{ method, path, body }` and returns the 4 `POLY_BUILDER_*` headers. + +## Related Skills + +- [gmx](../gmx/SKILL.md) -- GMX perpetual futures on Arbitrum/Avalanche +- [vertex](../vertex/SKILL.md) -- Vertex edge DEX with cross-chain orderbook +- [hyperliquid](../hyperliquid/SKILL.md) -- Hyperliquid perpetual futures on its own L1 + +## References + +- [Polymarket CLOB Docs](https://docs.polymarket.com/) +- [CLOB Client SDK (TypeScript)](https://github.com/Polymarket/clob-client) +- [CLOB Client SDK (Python)](https://github.com/Polymarket/py-clob-client) +- [Builder Relayer Client](https://github.com/Polymarket/builder-relayer-client) +- [Builder Signing SDK](https://github.com/Polymarket/builder-signing-sdk) +- [Gamma API](https://gamma-api.polymarket.com) +- [CTF Contracts (Gnosis)](https://github.com/gnosis/conditional-tokens-contracts) +- [Polymarket App](https://polymarket.com) From b9c8164ae93026ec6a1b5a22550d7f391297a72e Mon Sep 17 00:00:00 2001 From: 0xinit <28729137+0xinit@users.noreply.github.com> Date: Wed, 4 Mar 2026 10:47:38 -0800 Subject: [PATCH 02/14] feat: add polymarket examples --- .../examples/ctf-split-merge/README.md | 177 ++++++++++++++ .../polymarket/examples/market-data/README.md | 175 ++++++++++++++ .../polymarket/examples/place-order/README.md | 102 ++++++++ .../examples/websocket-feed/README.md | 223 ++++++++++++++++++ 4 files changed, 677 insertions(+) create mode 100644 skills/polymarket/examples/ctf-split-merge/README.md create mode 100644 skills/polymarket/examples/market-data/README.md create mode 100644 skills/polymarket/examples/place-order/README.md create mode 100644 skills/polymarket/examples/websocket-feed/README.md diff --git a/skills/polymarket/examples/ctf-split-merge/README.md b/skills/polymarket/examples/ctf-split-merge/README.md new file mode 100644 index 0000000..9659369 --- /dev/null +++ b/skills/polymarket/examples/ctf-split-merge/README.md @@ -0,0 +1,177 @@ +# CTF Split and Merge on Polymarket + +Demonstrates splitting USDC into conditional outcome tokens, merging tokens back to USDC, and redeeming winning tokens after market resolution using the Conditional Token Framework. + +## Prerequisites + +```bash +npm install viem @polymarket/clob-client +``` + +Environment variables: + +``` +PRIVATE_KEY=0x... # Polygon wallet private key +RPC_URL=https://... # Polygon RPC endpoint +``` + +## Full Working Code + +```typescript +import { + createPublicClient, + createWalletClient, + http, + parseAbi, + type Hex, + type Address, +} from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { polygon } from "viem/chains"; + +const USDC: Address = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"; +const CTF: Address = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045"; + +// USDC.e on Polygon has 6 decimals +const USDC_DECIMALS = 6; + +const ctfAbi = parseAbi([ + "function splitPosition(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] calldata partition, uint256 amount) external", + "function mergePositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] calldata partition, uint256 amount) external", + "function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] calldata indexSets) external", + "function balanceOf(address account, uint256 id) external view returns (uint256)", +]); + +const erc20Abi = parseAbi([ + "function approve(address spender, uint256 amount) external returns (bool)", + "function allowance(address owner, address spender) external view returns (uint256)", + "function balanceOf(address account) external view returns (uint256)", +]); + +// Zero bytes32 for parentCollectionId (top-level positions) +const PARENT_COLLECTION_ID = "0x0000000000000000000000000000000000000000000000000000000000000000" as Hex; + +// Binary market partition: Yes=1, No=2 +const BINARY_PARTITION = [1n, 2n]; + +async function main() { + const account = privateKeyToAccount(process.env.PRIVATE_KEY as Hex); + const transport = http(process.env.RPC_URL); + + const publicClient = createPublicClient({ chain: polygon, transport }); + const walletClient = createWalletClient({ account, chain: polygon, transport }); + + // Replace with a real condition ID from the Gamma API + const CONDITION_ID = "0xYOUR_CONDITION_ID" as Hex; + + // Replace with real token IDs from the market's tokens array + const YES_TOKEN_ID = 12345n; // BigInt of the Yes token ID + const NO_TOKEN_ID = 67890n; // BigInt of the No token ID + + const usdcAmount = 100n * 10n ** BigInt(USDC_DECIMALS); // 100 USDC + + // --- Check and Approve USDC for CTF --- + const allowance = await publicClient.readContract({ + address: USDC, + abi: erc20Abi, + functionName: "allowance", + args: [account.address, CTF], + }); + + if (allowance < usdcAmount) { + console.log("Approving USDC for CTF contract..."); + const approveTx = await walletClient.writeContract({ + address: USDC, + abi: erc20Abi, + functionName: "approve", + args: [CTF, usdcAmount], + }); + await publicClient.waitForTransactionReceipt({ hash: approveTx }); + console.log("Approved:", approveTx); + } + + // --- Split: USDC -> Yes + No Tokens --- + console.log("\nSplitting 100 USDC into outcome tokens..."); + const splitTx = await walletClient.writeContract({ + address: CTF, + abi: ctfAbi, + functionName: "splitPosition", + args: [USDC, PARENT_COLLECTION_ID, CONDITION_ID, BINARY_PARTITION, usdcAmount], + }); + const splitReceipt = await publicClient.waitForTransactionReceipt({ hash: splitTx }); + console.log("Split tx:", splitTx, "status:", splitReceipt.status); + + // Check token balances + const yesBalance = await publicClient.readContract({ + address: CTF, + abi: ctfAbi, + functionName: "balanceOf", + args: [account.address, YES_TOKEN_ID], + }); + const noBalance = await publicClient.readContract({ + address: CTF, + abi: ctfAbi, + functionName: "balanceOf", + args: [account.address, NO_TOKEN_ID], + }); + console.log(`Yes tokens: ${yesBalance}, No tokens: ${noBalance}`); + + // --- Merge: Yes + No Tokens -> USDC --- + // Merge half back to USDC + const mergeAmount = 50n * 10n ** BigInt(USDC_DECIMALS); + console.log("\nMerging 50 token pairs back to USDC..."); + const mergeTx = await walletClient.writeContract({ + address: CTF, + abi: ctfAbi, + functionName: "mergePositions", + args: [USDC, PARENT_COLLECTION_ID, CONDITION_ID, BINARY_PARTITION, mergeAmount], + }); + const mergeReceipt = await publicClient.waitForTransactionReceipt({ hash: mergeTx }); + console.log("Merge tx:", mergeTx, "status:", mergeReceipt.status); + + // --- Redeem: After Market Resolution --- + // Only call this after the market has resolved + console.log("\nRedeeming positions (only works after resolution)..."); + try { + const redeemTx = await walletClient.writeContract({ + address: CTF, + abi: ctfAbi, + functionName: "redeemPositions", + // indexSets [1, 2] redeems both outcomes; only winner pays out + args: [USDC, PARENT_COLLECTION_ID, CONDITION_ID, [1n, 2n]], + }); + const redeemReceipt = await publicClient.waitForTransactionReceipt({ hash: redeemTx }); + console.log("Redeem tx:", redeemTx, "status:", redeemReceipt.status); + } catch (err) { + console.log("Redeem failed (market likely not resolved yet):", (err as Error).message); + } +} + +main().catch(console.error); +``` + +## Expected Output + +``` +Approving USDC for CTF contract... +Approved: 0xabc123... + +Splitting 100 USDC into outcome tokens... +Split tx: 0xdef456... status: success +Yes tokens: 100000000, No tokens: 100000000 + +Merging 50 token pairs back to USDC... +Merge tx: 0xghi789... status: success + +Redeeming positions (only works after resolution)... +Redeem tx: 0xjkl012... status: success +``` + +## Notes + +- **Split** requires USDC approval for the CTF contract, not the Exchange contract. +- **Merge** requires equal amounts of both Yes and No tokens. No approval needed since you hold the tokens. +- **Redeem** burns your entire token balance for the condition. There is no amount parameter. Pass `indexSets: [1, 2]` to redeem both outcomes; only the winning outcome pays. +- USDC.e on Polygon has 6 decimals. All amounts are in base units (1 USDC = 1,000,000). +- For neg risk (multi-outcome) markets, use the Neg Risk Adapter (`0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296`) for conversions between outcomes. +- Token IDs are large uint256 values. Get them from the Gamma API `tokens` array on the market object rather than computing them manually. diff --git a/skills/polymarket/examples/market-data/README.md b/skills/polymarket/examples/market-data/README.md new file mode 100644 index 0000000..e21cc8c --- /dev/null +++ b/skills/polymarket/examples/market-data/README.md @@ -0,0 +1,175 @@ +# Polymarket Market Data + +Demonstrates fetching events, markets, orderbook data, and price history from the Gamma API and CLOB API. No authentication required. + +## Prerequisites + +```bash +npm install @polymarket/clob-client +``` + +## Full Working Code + +```typescript +import { ClobClient, Side, PriceHistoryInterval } from "@polymarket/clob-client"; + +const CLOB_HOST = "https://clob.polymarket.com"; +const GAMMA_HOST = "https://gamma-api.polymarket.com"; +const CHAIN_ID = 137; + +interface GammaEvent { + id: string; + title: string; + slug: string; + volume: number; + markets: Array<{ + id: string; + question: string; + tokens: Array<{ token_id: string; outcome: string }>; + minimum_tick_size: string; + enableOrderBook: boolean; + }>; +} + +async function fetchTopEvents(): Promise { + const url = `${GAMMA_HOST}/events?active=true&closed=false&sort=volume_24hr&ascending=false&limit=10`; + const res = await fetch(url); + return res.json() as Promise; +} + +async function fetchEventBySlug(slug: string): Promise { + const url = `${GAMMA_HOST}/events?slug=${encodeURIComponent(slug)}`; + const res = await fetch(url); + return res.json() as Promise; +} + +async function main() { + // --- Top Events by 24h Volume --- + const topEvents = await fetchTopEvents(); + console.log("=== Top Events by 24h Volume ==="); + for (const event of topEvents.slice(0, 5)) { + console.log(` ${event.title} (vol: $${event.volume.toLocaleString()})`); + } + + // --- Fetch Specific Event --- + const events = await fetchEventBySlug("fed-decision-in-october"); + if (events.length > 0) { + const event = events[0]; + console.log(`\n=== Event: ${event.title} ===`); + for (const market of event.markets) { + console.log(` Market: ${market.question}`); + console.log(` Tick size: ${market.minimum_tick_size}`); + for (const token of market.tokens) { + console.log(` ${token.outcome}: ${token.token_id.slice(0, 16)}...`); + } + } + } + + // --- Orderbook Data (no auth needed) --- + const readClient = new ClobClient(CLOB_HOST, CHAIN_ID); + + // Use a token ID from a real market + const TOKEN_ID = topEvents[0]?.markets[0]?.tokens[0]?.token_id; + if (!TOKEN_ID) { + console.log("No active markets found"); + return; + } + + console.log(`\n=== Orderbook for ${TOKEN_ID.slice(0, 16)}... ===`); + + const book = await readClient.getOrderBook(TOKEN_ID); + console.log("Top 5 bids:"); + for (const bid of book.bids.slice(0, 5)) { + console.log(` ${bid.price} x ${bid.size}`); + } + console.log("Top 5 asks:"); + for (const ask of book.asks.slice(0, 5)) { + console.log(` ${ask.price} x ${ask.size}`); + } + + const mid = await readClient.getMidpoint(TOKEN_ID); + const spread = await readClient.getSpread(TOKEN_ID); + const lastPrice = await readClient.getLastTradePrice(TOKEN_ID); + console.log(`Midpoint: ${mid.mid}`); + console.log(`Spread: ${spread.spread}`); + console.log(`Last trade: ${lastPrice.price} (${lastPrice.side})`); + + // --- Price History --- + const history = await readClient.getPricesHistory({ + market: TOKEN_ID, + interval: PriceHistoryInterval.ONE_WEEK, + fidelity: 60, + }); + console.log(`\n=== Price History (last week, hourly) ===`); + for (const point of history.slice(-5)) { + const date = new Date(point.t * 1000).toISOString(); + console.log(` ${date}: ${point.p}`); + } + + // --- Batch Prices --- + const tokens = topEvents[0]?.markets[0]?.tokens; + if (tokens && tokens.length >= 2) { + const batchPrices = await readClient.getPrices([ + { token_id: tokens[0].token_id, side: Side.BUY }, + { token_id: tokens[1].token_id, side: Side.BUY }, + ]); + console.log("\n=== Batch Prices ==="); + console.log(` ${tokens[0].outcome}: ${batchPrices[0]}`); + console.log(` ${tokens[1].outcome}: ${batchPrices[1]}`); + } + + // --- Estimate Fill Price --- + const estimatedPrice = await readClient.calculateMarketPrice( + TOKEN_ID, Side.BUY, 500, OrderType.FOK + ); + console.log(`\nEstimated fill for 500 shares: ${estimatedPrice}`); +} + +main().catch(console.error); +``` + +## Expected Output + +``` +=== Top Events by 24h Volume === + Will the Fed cut rates in March? (vol: $12,345,678) + Presidential Election 2028 (vol: $9,876,543) + ... + +=== Event: Fed Decision in October === + Market: Will the Fed cut rates? + Tick size: 0.01 + Yes: 7182736451829... + No: 9283746152839... + +=== Orderbook for 7182736451829... === +Top 5 bids: + 0.62 x 1500 + 0.61 x 3200 + ... +Top 5 asks: + 0.64 x 800 + 0.65 x 2100 + ... +Midpoint: 0.63 +Spread: 0.02 +Last trade: 0.63 (BUY) + +=== Price History (last week, hourly) === + 2026-02-28T12:00:00.000Z: 0.58 + ... + +=== Batch Prices === + Yes: 0.64 + No: 0.36 + +Estimated fill for 500 shares: 0.641 +``` + +## Notes + +- Gamma API and CLOB read endpoints require no authentication. +- Always include `active=true&closed=false` when fetching events unless you need historical data. +- Events contain their markets, reducing API calls. Prefer fetching events over individual markets. +- Pagination: use `limit` and `offset` parameters. Response includes `has_more` to signal more pages. +- If bid-ask spread exceeds $0.10, the Polymarket UI shows last traded price instead of midpoint. diff --git a/skills/polymarket/examples/place-order/README.md b/skills/polymarket/examples/place-order/README.md new file mode 100644 index 0000000..071b83e --- /dev/null +++ b/skills/polymarket/examples/place-order/README.md @@ -0,0 +1,102 @@ +# Place Order on Polymarket + +Demonstrates authentication, client setup, and placing limit, market, and GTD orders on the Polymarket CLOB. + +## Prerequisites + +```bash +npm install @polymarket/clob-client ethers@^5.7.0 +``` + +Environment variables: + +``` +PRIVATE_KEY=0x... # Polygon wallet private key +FUNDER_ADDRESS=0x... # Proxy wallet from polymarket.com/settings +``` + +## Full Working Code + +```typescript +import { ClobClient, Side, OrderType } from "@polymarket/clob-client"; +import { Wallet } from "ethers"; + +const HOST = "https://clob.polymarket.com"; +const CHAIN_ID = 137; + +async function main() { + const signer = new Wallet(process.env.PRIVATE_KEY!); + + // Step 1: Derive API credentials (L1 auth) + const tempClient = new ClobClient(HOST, CHAIN_ID, signer); + const apiCreds = await tempClient.createOrDeriveApiKey(); + console.log("API Key:", apiCreds.apiKey); + + // Step 2: Initialize trading client (L2 auth) + const client = new ClobClient( + HOST, + CHAIN_ID, + signer, + apiCreds, + 2, // GNOSIS_SAFE + process.env.FUNDER_ADDRESS! + ); + + // Replace with a real token ID from the Gamma API + const TOKEN_ID = "YOUR_TOKEN_ID"; + + // Fetch tick size and neg risk flag for this market + const tickSize = await client.getTickSize(TOKEN_ID); + const negRisk = await client.getNegRisk(TOKEN_ID); + console.log("Tick size:", tickSize, "Neg risk:", negRisk); + + // --- GTC Limit Order --- + const gtcResponse = await client.createAndPostOrder( + { tokenID: TOKEN_ID, price: 0.45, size: 20, side: Side.BUY }, + { tickSize, negRisk }, + OrderType.GTC + ); + console.log("GTC Order:", gtcResponse.orderID, gtcResponse.status); + + // --- FOK Market Order (buy $50 worth) --- + const fokResponse = await client.createAndPostMarketOrder( + { tokenID: TOKEN_ID, side: Side.BUY, amount: 50, price: 0.55 }, + { tickSize, negRisk }, + OrderType.FOK + ); + console.log("FOK Order:", fokResponse.orderID, fokResponse.status); + + // --- GTD Order (expires in 2 hours) --- + const expiration = Math.floor(Date.now() / 1000) + 60 + 7200; + const gtdResponse = await client.createAndPostOrder( + { tokenID: TOKEN_ID, price: 0.40, size: 50, side: Side.BUY, expiration }, + { tickSize, negRisk }, + OrderType.GTD + ); + console.log("GTD Order:", gtdResponse.orderID, "expires:", new Date(expiration * 1000).toISOString()); + + // --- Cancel All --- + await client.cancelAll(); + console.log("All orders cancelled"); +} + +main().catch(console.error); +``` + +## Expected Output + +``` +API Key: a1b2c3d4-e5f6-7890-abcd-ef1234567890 +Tick size: 0.01 Neg risk: false +GTC Order: 0xabc123... live +FOK Order: 0xdef456... matched +GTD Order: 0xghi789... expires: 2026-03-04T16:00:00.000Z +All orders cancelled +``` + +## Notes + +- `createOrDeriveApiKey()` is idempotent. It creates credentials on first call and derives existing ones on subsequent calls. +- FOK BUY `amount` is dollars to spend, not share count. The response includes the actual shares filled. +- GTD expiration must be at least `now + 60` seconds. The 60-second buffer is a protocol-enforced security threshold. +- Always query `tickSize` and `negRisk` per market before placing orders. Using stale values causes `INVALID_ORDER_MIN_TICK_SIZE` rejections. diff --git a/skills/polymarket/examples/websocket-feed/README.md b/skills/polymarket/examples/websocket-feed/README.md new file mode 100644 index 0000000..273d4ba --- /dev/null +++ b/skills/polymarket/examples/websocket-feed/README.md @@ -0,0 +1,223 @@ +# Polymarket WebSocket Feed + +Demonstrates connecting to the Polymarket market and user WebSocket channels for real-time orderbook updates, trade prints, and order lifecycle events. + +## Prerequisites + +```bash +npm install ws @polymarket/clob-client +``` + +Environment variables (only needed for user channel): + +``` +POLY_API_KEY=... +POLY_API_SECRET=... +POLY_PASSPHRASE=... +``` + +## Full Working Code + +```typescript +import WebSocket from "ws"; + +const MARKET_WS = "wss://ws-subscriptions-clob.polymarket.com/ws/market"; +const USER_WS = "wss://ws-subscriptions-clob.polymarket.com/ws/user"; + +// Replace with real token IDs from the Gamma API +const TOKEN_IDS = ["YOUR_TOKEN_ID_1", "YOUR_TOKEN_ID_2"]; +const CONDITION_ID = "0xYOUR_CONDITION_ID"; + +interface BookEvent { + event_type: "book"; + asset_id: string; + market: string; + bids: Array<{ price: string; size: string }>; + asks: Array<{ price: string; size: string }>; + timestamp: string; +} + +interface PriceChangeEvent { + event_type: "price_change"; + market: string; + price_changes: Array<{ + asset_id: string; + price: string; + size: string; + side: string; + best_bid: string; + best_ask: string; + }>; +} + +interface LastTradeEvent { + event_type: "last_trade_price"; + asset_id: string; + price: string; + side: string; + size: string; +} + +interface TickSizeChangeEvent { + event_type: "tick_size_change"; + asset_id: string; + old_tick_size: string; + new_tick_size: string; +} + +type MarketEvent = BookEvent | PriceChangeEvent | LastTradeEvent | TickSizeChangeEvent; + +function connectMarketChannel(): void { + const ws = new WebSocket(MARKET_WS); + let pingInterval: NodeJS.Timeout; + + ws.on("open", () => { + console.log("[market] connected"); + + ws.send(JSON.stringify({ + type: "market", + assets_ids: TOKEN_IDS, + custom_feature_enabled: true, + })); + + // Heartbeat every 10s to keep connection alive + pingInterval = setInterval(() => ws.send("PING"), 10_000); + }); + + ws.on("message", (data: WebSocket.Data) => { + const raw = data.toString(); + if (raw === "PONG") return; + + const msg: MarketEvent = JSON.parse(raw); + switch (msg.event_type) { + case "book": { + const bestBid = msg.bids[0]; + const bestAsk = msg.asks[0]; + console.log( + `[book] ${msg.asset_id.slice(0, 12)}... ` + + `bid: ${bestBid?.price ?? "---"} x ${bestBid?.size ?? "0"} | ` + + `ask: ${bestAsk?.price ?? "---"} x ${bestAsk?.size ?? "0"}` + ); + break; + } + case "price_change": { + for (const pc of msg.price_changes) { + // size "0" means the price level was removed + const action = pc.size === "0" ? "REMOVED" : pc.side; + console.log( + `[price] ${action} ${pc.size}@${pc.price} ` + + `(BBO: ${pc.best_bid}/${pc.best_ask})` + ); + } + break; + } + case "last_trade_price": { + console.log(`[trade] ${msg.side} ${msg.size}@${msg.price}`); + break; + } + case "tick_size_change": { + // Critical for bots: update tick size immediately or orders get rejected + console.log(`[tick] ${msg.old_tick_size} -> ${msg.new_tick_size}`); + break; + } + } + }); + + ws.on("close", () => { + clearInterval(pingInterval); + console.log("[market] disconnected, reconnecting in 5s..."); + setTimeout(connectMarketChannel, 5000); + }); + + ws.on("error", (err) => { + console.error("[market] error:", err.message); + }); +} + +function connectUserChannel(): void { + if (!process.env.POLY_API_KEY) { + console.log("[user] skipping — no API credentials configured"); + return; + } + + const ws = new WebSocket(USER_WS); + let pingInterval: NodeJS.Timeout; + + ws.on("open", () => { + console.log("[user] connected"); + + ws.send(JSON.stringify({ + auth: { + apiKey: process.env.POLY_API_KEY!, + secret: process.env.POLY_API_SECRET!, + passphrase: process.env.POLY_PASSPHRASE!, + }, + markets: [CONDITION_ID], + type: "USER", + })); + + pingInterval = setInterval(() => ws.send("PING"), 10_000); + }); + + ws.on("message", (data: WebSocket.Data) => { + const raw = data.toString(); + if (raw === "PONG") return; + + const msg = JSON.parse(raw); + if (msg.event_type === "trade") { + console.log( + `[user:trade] ${msg.side} ${msg.size}@${msg.price} ` + + `status: ${msg.status}` + ); + } else if (msg.event_type === "order") { + console.log( + `[user:order] ${msg.type} ${msg.side} ` + + `${msg.original_size}@${msg.price} matched: ${msg.size_matched}` + ); + } + }); + + ws.on("close", () => { + clearInterval(pingInterval); + console.log("[user] disconnected, reconnecting in 5s..."); + setTimeout(connectUserChannel, 5000); + }); + + ws.on("error", (err) => { + console.error("[user] error:", err.message); + }); +} + +// Dynamic subscribe example: add/remove assets after connection +function addAsset(ws: WebSocket, tokenId: string): void { + ws.send(JSON.stringify({ assets_ids: [tokenId], operation: "subscribe" })); +} + +function removeAsset(ws: WebSocket, tokenId: string): void { + ws.send(JSON.stringify({ assets_ids: [tokenId], operation: "unsubscribe" })); +} + +connectMarketChannel(); +connectUserChannel(); +``` + +## Expected Output + +``` +[market] connected +[book] 718273645182... bid: 0.62 x 1500 | ask: 0.64 x 800 +[price] BUY 200@0.63 (BBO: 0.63/0.64) +[trade] BUY 50@0.63 +[price] REMOVED 0@0.61 (BBO: 0.62/0.64) +[user] connected +[user:trade] BUY 10@0.63 status: MATCHED +[user:order] PLACEMENT SELL 100@0.65 matched: 0 +``` + +## Notes + +- Market channel subscribes by **token IDs** (asset IDs). User channel subscribes by **condition IDs** (market IDs). Mixing these up produces no data and no errors. +- `custom_feature_enabled: true` enables `best_bid_ask`, `new_market`, and `market_resolved` events on the market channel. +- A `price_change` with `size: "0"` means the price level was removed from the book. +- User channel trade statuses follow the lifecycle: `MATCHED -> MINED -> CONFIRMED` (or `RETRYING -> FAILED`). +- The sports WebSocket (`wss://sports-api.polymarket.com/ws`) requires no subscription message. Connect and receive all active sports data. Its heartbeat is server-initiated: respond to `ping` with `pong` within 10 seconds. From 3db094bc66ed44ba114b5eb161fb7178ba11b0dc Mon Sep 17 00:00:00 2001 From: 0xinit <28729137+0xinit@users.noreply.github.com> Date: Wed, 4 Mar 2026 11:13:51 -0800 Subject: [PATCH 03/14] feat: add polymarket docs and resources --- skills/polymarket/docs/troubleshooting.md | 115 +++++++++++++++++++ skills/polymarket/resources/api-endpoints.md | 100 ++++++++++++++++ skills/polymarket/resources/error-codes.md | 86 ++++++++++++++ skills/polymarket/resources/order-types.md | 94 +++++++++++++++ 4 files changed, 395 insertions(+) create mode 100644 skills/polymarket/docs/troubleshooting.md create mode 100644 skills/polymarket/resources/api-endpoints.md create mode 100644 skills/polymarket/resources/error-codes.md create mode 100644 skills/polymarket/resources/order-types.md diff --git a/skills/polymarket/docs/troubleshooting.md b/skills/polymarket/docs/troubleshooting.md new file mode 100644 index 0000000..6c50806 --- /dev/null +++ b/skills/polymarket/docs/troubleshooting.md @@ -0,0 +1,115 @@ +# Polymarket Troubleshooting + +Common issues and solutions for Polymarket CLOB integration. Last verified March 2026. + +## Authentication Failures + +### "L1 auth headers missing or invalid" + +The EIP-712 signature is malformed or the timestamp is stale. + +- Ensure `POLY_ADDRESS`, `POLY_SIGNATURE`, `POLY_TIMESTAMP`, and `POLY_NONCE` headers are all present. +- Timestamps must be within 60 seconds of server time. Sync your system clock. +- The SDK handles this automatically via `createOrDeriveApiKey()`. If calling the REST API directly, double-check the EIP-712 domain uses `ClobAuthDomain` with chain ID `137`. + +### "L2 HMAC signature mismatch" + +The HMAC-SHA256 signature does not match the request body. + +- Verify `apiKey`, `secret`, and `passphrase` match what was returned by `createOrDeriveApiKey()`. +- The secret is base64-encoded. Decode it before using as HMAC key. +- Request body must be serialized identically to what was signed. Whitespace or key ordering differences break HMAC. +- If credentials were lost, call `createOrDeriveApiKey()` again to re-derive them. + +### "Invalid signature type" or orders silently fail + +Wrong `signatureType` or `funderAddress` combination. + +- New integrations should use signature type `2` (GNOSIS_SAFE). +- The funder address is your **proxy wallet**, found at `polymarket.com/settings`. It is not your EOA address. +- Type `1` (POLY_PROXY) is only for users who exported their private key from the Polymarket Magic Link wallet. + +## Order Rejections + +### `INVALID_ORDER_MIN_TICK_SIZE` + +Price does not conform to the market tick size. + +- Query the tick size: `client.getTickSize(tokenID)` or check `minimum_tick_size` on the market object. +- Tick sizes are `0.1`, `0.01`, `0.001`, or `0.0001`. Round your price accordingly. +- Tick sizes can change when prices approach 0 or 1. Subscribe to `tick_size_change` WebSocket events. + +### `INVALID_ORDER_NOT_ENOUGH_BALANCE` + +Insufficient USDC or token balance, or missing approval. + +- Check that the funder address has enough USDC (for BUY) or conditional tokens (for SELL). +- Verify the funder has approved the correct exchange contract (CTF Exchange for standard, Neg Risk CTF Exchange for neg risk markets). +- Max order size = `balance - sum(openOrderSize - filledAmount)`. Open orders reserve balance. + +### `FOK_ORDER_NOT_FILLED_ERROR` + +Fill-Or-Kill order could not be fully filled. + +- The orderbook did not have enough liquidity at your limit price. +- Use FAK instead if partial fills are acceptable. +- Use `client.calculateMarketPrice()` to estimate fill price before submitting. + +### `INVALID_POST_ONLY_ORDER` + +Post-only order would cross the spread. + +- Your limit price is marketable (buy price >= best ask, or sell price <= best bid). +- Adjust the price to rest behind the spread, or remove the post-only flag. + +## WebSocket Issues + +### Connection drops after ~10 seconds + +You are not sending heartbeats. + +- Send `PING` as a text message every 10 seconds. The server responds with `PONG`. +- Implement a heartbeat interval immediately after `onopen`. + +### No messages received after subscribing + +- Verify the `assets_ids` are valid and the markets are active. +- For the market channel, use **token IDs** (asset IDs). For the user channel, use **condition IDs** (market IDs). Mixing these up produces no errors but no data. +- Check that your subscription message format matches the expected schema. + +### Auth failed on user channel + +- API credentials may have expired or been revoked. +- Re-derive credentials with `createOrDeriveApiKey()` and update the subscription message. + +## CTF Operation Errors + +### Split transaction reverts + +- Confirm USDC approval for the CTF contract (`0x4D97DCd97eC945f40cF65F87097ACe5EA0476045`), not the Exchange contract. +- Verify the condition ID belongs to an active, unresolved market. +- Ensure the USDC amount does not exceed your balance. + +### Merge fails with "insufficient balance" + +- You need **equal amounts** of both Yes and No tokens for the condition. +- Check ERC1155 balances for both token IDs before calling merge. + +### Redeem returns zero + +- The market has not resolved yet, or you hold the losing outcome tokens. +- Only winning tokens pay out. Losing tokens are burned for $0. +- Verify resolution status via the Gamma API before attempting redemption. + +## Gasless Transaction Failures + +### "Builder credentials invalid" + +- Builder API keys are separate from trading API keys. Generate them at `polymarket.com/settings?tab=builder`. +- If using remote signing, ensure your signing server returns all 4 `POLY_BUILDER_*` headers. + +### Relayer returns `STATE_FAILED` + +- The underlying transaction reverted. Check the encoded calldata for correctness. +- For token approvals, verify the spender address and amount are correct. +- Deploy the Safe wallet (`client.deploy()`) before executing other transactions. diff --git a/skills/polymarket/resources/api-endpoints.md b/skills/polymarket/resources/api-endpoints.md new file mode 100644 index 0000000..c30ffec --- /dev/null +++ b/skills/polymarket/resources/api-endpoints.md @@ -0,0 +1,100 @@ +# Polymarket API Endpoints + +Quick reference for all Polymarket API endpoints. Last verified March 2026. + +## CLOB API (`https://clob.polymarket.com`) + +### Public (No Auth) + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/book?token_id={id}` | Orderbook for a token | +| POST | `/books` | Batch orderbooks (up to 500) | +| GET | `/price?token_id={id}&side={BUY\|SELL}` | Best price for a token | +| POST | `/prices` | Batch prices | +| GET | `/midpoint?token_id={id}` | Midpoint price | +| POST | `/midpoints` | Batch midpoints | +| GET | `/spread?token_id={id}` | Bid-ask spread | +| POST | `/spreads` | Batch spreads | +| GET | `/last-trade-price?token_id={id}` | Last trade price | +| GET | `/prices-history` | Price history (params: `market`, `interval`, `fidelity`, `startTs`, `endTs`) | +| GET | `/tick-size?token_id={id}` | Market tick size | +| GET | `/neg-risk?token_id={id}` | Neg risk flag | + +### Authenticated (L2 HMAC) + +| Method | Path | Description | +|--------|------|-------------| +| POST | `/order` | Place a single order | +| POST | `/orders` | Place batch orders (up to 15) | +| DELETE | `/order/{id}` | Cancel a single order | +| DELETE | `/orders` | Cancel multiple orders | +| DELETE | `/cancel-all` | Cancel all open orders | +| DELETE | `/cancel-market-orders` | Cancel by market or token | +| POST | `/heartbeat` | Heartbeat (dead man's switch) | + +### Auth Management (L1 EIP-712) + +| Method | Path | Description | +|--------|------|-------------| +| POST | `/auth/api-key` | Create new API credentials | +| GET | `/auth/derive-api-key` | Derive existing credentials | + +## Gamma API (`https://gamma-api.polymarket.com`) + +All endpoints are public. No authentication required. + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/events` | List events (params: `active`, `closed`, `slug`, `tag_id`, `series_id`, `sort`, `ascending`, `limit`, `offset`) | +| GET | `/markets` | List markets (params: `slug`) | +| GET | `/tags/ranked` | Ranked tags | +| GET | `/sports` | Sports metadata | + +### Sort Options for Events + +| Value | Description | +|-------|-------------| +| `volume_24hr` | 24-hour trading volume | +| `volume` | Total trading volume | +| `liquidity` | Current liquidity | +| `start_date` | Event start date | +| `end_date` | Event end date | +| `competitive` | Competitiveness score | +| `closed_time` | Time market closed | + +## Data API (`https://data-api.polymarket.com`) + +Public endpoints for trades, positions, and user data. + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/trades` | Trade history | +| GET | `/positions` | User positions | + +## WebSocket Endpoints + +| Endpoint | Auth | Subscribe By | +|----------|------|-------------| +| `wss://ws-subscriptions-clob.polymarket.com/ws/market` | None | Token IDs (asset IDs) | +| `wss://ws-subscriptions-clob.polymarket.com/ws/user` | API creds in message | Condition IDs (market IDs) | +| `wss://sports-api.polymarket.com/ws` | None | No subscription needed | + +## Relayer (`https://relayer-v2.polymarket.com/`) + +Requires Builder Program credentials. Used for gasless transactions. + +| Method | Path | Description | +|--------|------|-------------| +| POST | `/execute` | Execute gasless transactions | +| POST | `/deploy` | Deploy Safe/Proxy wallet | + +## Bridge (`https://bridge.polymarket.com`) + +| Method | Path | Description | +|--------|------|-------------| +| POST | `/deposit` | Create deposit addresses | +| POST | `/withdraw` | Create withdrawal addresses | +| POST | `/quote` | Preview withdrawal fees | +| GET | `/status/{address}` | Track deposit/withdrawal status | +| GET | `/supported-assets` | List supported tokens and chains | diff --git a/skills/polymarket/resources/error-codes.md b/skills/polymarket/resources/error-codes.md new file mode 100644 index 0000000..b9b7e6f --- /dev/null +++ b/skills/polymarket/resources/error-codes.md @@ -0,0 +1,86 @@ +# Polymarket Error Codes Reference + +Quick reference for CLOB API error codes, WebSocket errors, and relayer failures. Last verified March 2026. + +## Order Rejection Errors + +| Error Code | Cause | Fix | +|------------|-------|-----| +| `INVALID_ORDER_MIN_TICK_SIZE` | Price does not conform to market tick size | Query `getTickSize(tokenID)` and round price to valid increment | +| `INVALID_ORDER_MIN_SIZE` | Order size below minimum threshold | Increase size above the market minimum | +| `INVALID_ORDER_DUPLICATED` | Identical order already exists on book | Change price, size, or cancel existing order | +| `INVALID_ORDER_NOT_ENOUGH_BALANCE` | Insufficient USDC/token balance or missing approval | Check balance and contract approval for the funder address | +| `INVALID_ORDER_EXPIRATION` | GTD expiration is in the past | Set expiration to `now + 60 + N` (at least 60 seconds in the future) | +| `INVALID_POST_ONLY_ORDER_TYPE` | Post-only combined with FOK or FAK | Post-only only works with GTC and GTD | +| `INVALID_POST_ONLY_ORDER` | Post-only order would cross the spread | Adjust price to rest behind the spread | +| `FOK_ORDER_NOT_FILLED_ERROR` | FOK could not be fully filled | Reduce size, increase price limit, or use FAK for partial fills | +| `EXECUTION_ERROR` | System error during trade execution | Retry after a short delay | +| `ORDER_DELAYED` | Order delayed due to market conditions (sports) | Wait for the delay period; order will process | +| `MARKET_NOT_READY` | Market is not yet accepting orders | Wait for market activation or check market status via Gamma API | + +## Authentication Errors + +| Error | Cause | Fix | +|-------|-------|-----| +| `401 Unauthorized` | Missing or invalid L2 HMAC headers | Re-derive credentials with `createOrDeriveApiKey()` | +| `403 Forbidden` | Wrong signature type or funder mismatch | Verify signature type (0/1/2) and funder address | +| Invalid L1 signature | EIP-712 signature malformed or timestamp expired | Sync system clock; timestamps must be within 60 seconds | +| HMAC mismatch | Request body changed after signing | Ensure body serialization matches exactly what was signed | + +## Heartbeat Errors + +| Error | Cause | Fix | +|-------|-------|-----| +| `400` with heartbeat ID | Heartbeat ID expired | Update to the ID returned in the error response and retry | +| All orders cancelled | No heartbeat received within 10 seconds | Send heartbeat every 5 seconds with current heartbeat ID | + +## WebSocket Errors + +| Symptom | Cause | Fix | +|---------|-------|-----| +| Connection closes immediately | No subscription message sent | Send subscription JSON right after `onopen` | +| Drops after ~10 seconds | Missing PING heartbeats | Send `PING` every 10 seconds | +| No messages received | Invalid token/condition IDs | Verify IDs are correct and markets are active | +| Auth failure (user channel) | Expired or revoked API credentials | Re-derive credentials | +| No data (market channel) | Used condition IDs instead of token IDs | Market channel requires token IDs (asset IDs) | +| No data (user channel) | Used token IDs instead of condition IDs | User channel requires condition IDs (market IDs) | + +## CTF Operation Errors + +| Error | Cause | Fix | +|-------|-------|-----| +| Split reverts | Missing USDC approval for CTF contract | Approve CTF contract (`0x4D97...6045`), not the Exchange | +| Split reverts | Insufficient USDC balance | Check USDC.e balance on Polygon | +| Merge reverts | Unequal token balances | Need equal amounts of both Yes and No tokens | +| Redeem returns zero | Market not resolved | Check resolution status via Gamma API first | +| Redeem returns zero | Holding losing outcome tokens | Only winning tokens pay out | +| Wrong exchange | Used standard exchange for neg risk market | Check `negRisk` flag; use Neg Risk CTF Exchange if true | + +## Relayer Errors + +| State | Cause | Fix | +|-------|-------|-----| +| `STATE_FAILED` | Underlying transaction reverted | Check encoded calldata; verify contract addresses and amounts | +| `STATE_INVALID` | Transaction rejected as invalid | Verify Safe wallet is deployed; check nonce | +| Builder auth failed | Invalid builder credentials | Generate new keys at `polymarket.com/settings?tab=builder` | + +## Bridge Errors + +| Error | Cause | Fix | +|-------|-------|-----| +| Deposit not detected | Amount below minimum | Check minimum deposit for the source chain | +| Deposit not detected | Unsupported token | Call `/supported-assets` before depositing | +| `FAILED` status | Bridging error | For Ethereum: `recovery.polymarket.com`. For Polygon: `matic-recovery.polymarket.com` | +| High slippage on withdrawal | Amount too large for Uniswap pool | Break into smaller amounts (< $50,000) | + +## HTTP Status Codes + +| Code | Meaning | +|------|---------| +| `200` | Success | +| `400` | Bad request (invalid parameters or order rejection) | +| `401` | Unauthorized (missing or invalid auth headers) | +| `403` | Forbidden (wrong signature type or permissions) | +| `404` | Resource not found | +| `429` | Rate limited | +| `500` | Server error | diff --git a/skills/polymarket/resources/order-types.md b/skills/polymarket/resources/order-types.md new file mode 100644 index 0000000..c0e5059 --- /dev/null +++ b/skills/polymarket/resources/order-types.md @@ -0,0 +1,94 @@ +# Polymarket Order Types Reference + +Quick reference for order types, tick sizes, statuses, and trading rules. Last verified March 2026. + +## Order Types + +| Type | Behavior | Amount Semantics | Post-Only | +|------|----------|------------------|-----------| +| **GTC** | Good-Til-Cancelled. Rests on book until filled or cancelled. | `size` = share count | Yes | +| **GTD** | Good-Til-Date. Expiration = UTC seconds. Min: `now + 60 + N`. | `size` = share count | Yes | +| **FOK** | Fill-Or-Kill. Fill entirely or cancel. | BUY: `amount` = dollars. SELL: `amount` = shares. | No | +| **FAK** | Fill-And-Kill. Fill available, cancel rest. | BUY: `amount` = dollars. SELL: `amount` = shares. | No | + +## Tick Sizes + +| Tick Size | Precision | Valid Prices | +|-----------|-----------|-------------| +| `0.1` | 1 decimal | 0.1, 0.2, 0.3, ..., 0.9 | +| `0.01` | 2 decimals | 0.01, 0.02, ..., 0.99 | +| `0.001` | 3 decimals | 0.001, 0.002, ..., 0.999 | +| `0.0001` | 4 decimals | 0.0001, 0.0002, ..., 0.9999 | + +Tick sizes change dynamically when prices approach extremes (>0.96 or <0.04). Monitor `tick_size_change` WebSocket events. + +## Signature Types + +| Type | Value | Funder | Gas | +|------|-------|--------|-----| +| EOA | `0` | Wallet address | Needs POL | +| POLY_PROXY | `1` | Magic Link proxy | Needs POL | +| GNOSIS_SAFE | `2` | Safe proxy wallet | Needs POL (or use Relayer) | + +## Insert Statuses (Order Placement Response) + +| Status | Description | +|--------|-------------| +| `live` | Resting on the book | +| `matched` | Matched immediately | +| `delayed` | Marketable but subject to matching delay | +| `unmatched` | Marketable but failed to delay — placement still successful | + +## Trade Statuses (Settlement Lifecycle) + +``` +MATCHED -> MINED -> CONFIRMED + | ^ + v | +RETRYING ---+ + | + v + FAILED +``` + +| Status | Terminal | Description | +|--------|----------|-------------| +| `MATCHED` | No | Sent to executor for onchain submission | +| `MINED` | No | Mined on chain, no finality yet | +| `CONFIRMED` | Yes | Finalized — trade successful | +| `RETRYING` | No | Failed, being resubmitted | +| `FAILED` | Yes | Permanently failed | + +## Relayer Transaction States + +| State | Terminal | Description | +|-------|----------|-------------| +| `STATE_NEW` | No | Received by relayer | +| `STATE_EXECUTED` | No | Submitted onchain | +| `STATE_MINED` | No | Included in a block | +| `STATE_CONFIRMED` | Yes | Finalized successfully | +| `STATE_FAILED` | Yes | Failed permanently | +| `STATE_INVALID` | Yes | Rejected as invalid | + +## Batch Limits + +| Operation | Max per Request | +|-----------|----------------| +| Place orders | 15 | +| Cancel orders | Unlimited (cancel all) | +| Batch orderbook queries | 500 tokens | + +## Sports Market Rules + +- Outstanding limit orders are auto-cancelled when game begins. +- Marketable orders have a 3-second placement delay. +- Game start times can shift. Monitor accordingly. + +## Balance Constraints + +``` +maxOrderSize = balance - sum(openOrderSize - filledAmount) +``` + +- **BUY**: Requires USDC allowance >= spending amount on the exchange contract. +- **SELL**: Requires conditional token allowance >= selling amount on the exchange contract. From f7f1be0b6c602447d3a11f17849b5ee98ddb780b Mon Sep 17 00:00:00 2001 From: 0xinit <28729137+0xinit@users.noreply.github.com> Date: Wed, 4 Mar 2026 11:31:06 -0800 Subject: [PATCH 04/14] feat: add polymarket starter template --- skills/polymarket/templates/polymarket-bot.ts | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 skills/polymarket/templates/polymarket-bot.ts diff --git a/skills/polymarket/templates/polymarket-bot.ts b/skills/polymarket/templates/polymarket-bot.ts new file mode 100644 index 0000000..4077024 --- /dev/null +++ b/skills/polymarket/templates/polymarket-bot.ts @@ -0,0 +1,128 @@ +/** + * Polymarket Trading Bot — Starter Template + * + * Simple market-making bot that places bid/ask orders around the midpoint + * and maintains them with heartbeat protection. + * + * Usage: + * PRIVATE_KEY=0x... FUNDER_ADDRESS=0x... TOKEN_ID=... npx tsx polymarket-bot.ts + * + * Last verified: March 2026 + */ + +import { ClobClient, Side, OrderType } from "@polymarket/clob-client"; +import { Wallet } from "ethers"; + +const HOST = "https://clob.polymarket.com"; +const CHAIN_ID = 137; + +// Spread width on each side of the midpoint +const HALF_SPREAD = 0.02; +// Number of shares per order +const ORDER_SIZE = 100; +// How often to refresh quotes (ms) +const REFRESH_INTERVAL_MS = 30_000; + +async function createClient(): Promise { + const signer = new Wallet(process.env.PRIVATE_KEY!); + const tempClient = new ClobClient(HOST, CHAIN_ID, signer); + const apiCreds = await tempClient.createOrDeriveApiKey(); + + return new ClobClient( + HOST, + CHAIN_ID, + signer, + apiCreds, + 2, + process.env.FUNDER_ADDRESS! + ); +} + +function roundToTick(price: number, tickSize: string): number { + const tick = parseFloat(tickSize); + const rounded = Math.round(price / tick) * tick; + const decimals = tickSize.split(".")[1]?.length ?? 0; + return parseFloat(rounded.toFixed(decimals)); +} + +async function run(): Promise { + const tokenId = process.env.TOKEN_ID!; + if (!tokenId) { + console.error("TOKEN_ID env var required"); + process.exit(1); + } + + const client = await createClient(); + console.log("Client initialized"); + + const tickSize = await client.getTickSize(tokenId); + const negRisk = await client.getNegRisk(tokenId); + console.log(`Market: tick=${tickSize} negRisk=${negRisk}`); + + // Heartbeat: cancels all orders if not received within 10 seconds + let heartbeatId = ""; + const heartbeatInterval = setInterval(async () => { + try { + const resp = await client.postHeartbeat(heartbeatId); + heartbeatId = resp.heartbeat_id; + } catch (err) { + console.error("Heartbeat failed:", (err as Error).message); + } + }, 5_000); + + const quoteLoop = async (): Promise => { + try { + await client.cancelAll(); + + const midResult = await client.getMidpoint(tokenId); + const mid = parseFloat(midResult.mid); + if (isNaN(mid) || mid <= 0 || mid >= 1) { + console.log("Invalid midpoint, skipping cycle:", midResult.mid); + return; + } + + const bidPrice = roundToTick(mid - HALF_SPREAD, tickSize); + const askPrice = roundToTick(mid + HALF_SPREAD, tickSize); + + if (bidPrice <= 0 || askPrice >= 1) { + console.log("Prices out of range, skipping cycle"); + return; + } + + const [bidResp, askResp] = await Promise.all([ + client.createAndPostOrder( + { tokenID: tokenId, price: bidPrice, size: ORDER_SIZE, side: Side.BUY }, + { tickSize, negRisk }, + OrderType.GTC + ), + client.createAndPostOrder( + { tokenID: tokenId, price: askPrice, size: ORDER_SIZE, side: Side.SELL }, + { tickSize, negRisk }, + OrderType.GTC + ), + ]); + + console.log( + `[${new Date().toISOString()}] mid=${mid} ` + + `bid=${bidPrice} (${bidResp.status}) ` + + `ask=${askPrice} (${askResp.status})` + ); + } catch (err) { + console.error("Quote cycle error:", (err as Error).message); + } + }; + + await quoteLoop(); + const refreshInterval = setInterval(quoteLoop, REFRESH_INTERVAL_MS); + + process.on("SIGINT", async () => { + console.log("\nShutting down..."); + clearInterval(heartbeatInterval); + clearInterval(refreshInterval); + await client.cancelAll(); + console.log("All orders cancelled. Exiting."); + process.exit(0); + }); +} + +run().catch(console.error); From 012fb698bae3523de409d2dafc687b2ebcfa9ffa Mon Sep 17 00:00:00 2001 From: 0xinit <28729137+0xinit@users.noreply.github.com> Date: Wed, 4 Mar 2026 12:08:43 -0800 Subject: [PATCH 05/14] feat: add eip-reference skill --- skills/eip-reference/SKILL.md | 764 +++++++++++++++++----------------- 1 file changed, 392 insertions(+), 372 deletions(-) diff --git a/skills/eip-reference/SKILL.md b/skills/eip-reference/SKILL.md index 1f33315..8579fcd 100644 --- a/skills/eip-reference/SKILL.md +++ b/skills/eip-reference/SKILL.md @@ -1,35 +1,118 @@ --- name: eip-reference -description: Quick reference for essential EIPs and ERCs. Covers token standards, signature schemes, account abstraction, and chain-level EIPs with implementation patterns. Use when you need the correct interface, gotcha, or specification detail for any major Ethereum standard. +description: "Ethereum Improvement Proposals and ERC standards reference — ERC-20, ERC-721, ERC-1155, ERC-4626, ERC-2981, EIP-712, EIP-1559, EIP-2612 (Permit), EIP-4337 (Account Abstraction), EIP-4844 (Proto-Danksharding), EIP-7702 (EOA Delegation), ERC-8004 (Agent Identity). Quick lookup, interface signatures, and implementation patterns." license: Apache-2.0 metadata: - author: cryptoskills + author: 0xinit version: "1.0" - chain: multichain + chain: ethereum category: Infrastructure tags: - eip - erc - - standards - - tokens - - signatures + - ethereum-standards + - erc-20 + - erc-721 + - erc-1155 + - eip-712 + - eip-4337 + - eip-7702 --- # EIP / ERC Reference -Canonical reference for the Ethereum standards that matter most to smart contract and dApp developers. Provides correct interfaces, key behavioral rules, and the gotchas that trip up both humans and LLMs. +Canonical reference for Ethereum Improvement Proposals and ERC standards. Covers correct interfaces, behavioral rules, implementation patterns, and the gotchas that trip up humans and LLMs alike. Use this when you need the right function signature, the correct domain separator construction, or the nuance that separates a working implementation from a buggy one. ## What You Probably Got Wrong -> These misconceptions appear in LLM-generated code constantly. Fix your mental model before writing code. +> These misconceptions appear in LLM-generated code constantly. Fix your mental model before writing a single line. -- **EIP != ERC** — An EIP (Ethereum Improvement Proposal) is the proposal process. An ERC (Ethereum Request for Comments) is the subset of EIPs that define application-layer standards (tokens, signatures, wallets). ERC-20 started as EIP-20 and became ERC-20 upon acceptance. Chain-level changes like EIP-1559 stay as EIPs — they are never ERCs. -- **ERC-20 `approve` has a race condition** — If Alice approves Bob for 100, then changes to 50, Bob can front-run: spend the 100, then spend the new 50, totaling 150. Mitigation: approve to 0 first, or use `increaseAllowance`/`decreaseAllowance` (OpenZeppelin), or use ERC-2612 `permit`. USDT requires resetting to 0 before setting a new nonzero allowance — it will revert otherwise. -- **ERC-721 `transferFrom` skips receiver checks** — `transferFrom` does NOT call `onERC721Received` on the recipient. Tokens sent to contracts that cannot handle them are permanently locked. Use `safeTransferFrom` unless you have a specific reason not to (gas optimization in trusted contexts). -- **EIP-712 domain separator MUST include `chainId`** — Omitting `chainId` from the `EIP712Domain` allows signature replay across chains. A signature valid on mainnet becomes valid on every fork and L2 that shares the contract address. Always include `chainId` and `verifyingContract`. -- **ERC-4337 bundler != relayer** — A bundler packages `UserOperation` objects into a transaction and submits to the `EntryPoint`. A relayer (meta-transaction pattern) wraps a signed message into `msg.data` and calls a trusted forwarder. Different trust models, different gas accounting, different entry points. Do not conflate them. -- **EIP-1559 `baseFee` is protocol-controlled, not user-set** — Users set `maxFeePerGas` and `maxPriorityFeePerGas`. The protocol sets `baseFee` per block based on gas utilization of the previous block. The base fee is burned, not paid to validators. The priority fee goes to the validator. Effective gas price = `min(baseFee + maxPriorityFeePerGas, maxFeePerGas)`. -- **ERC-4626 share/asset math is rounding-sensitive** — `convertToShares` and `convertToAssets` must round in favor of the vault (down on deposit/mint, up on withdraw/redeem) to prevent share inflation attacks. First-depositor attacks exploit vaults that skip this. +- **EIP != ERC** — An EIP (Ethereum Improvement Proposal) covers the entire proposal process. An ERC (Ethereum Request for Comments) is the subset of EIPs defining application-layer standards (tokens, signatures, wallets). ERC-20 started as EIP-20 and became ERC-20 upon acceptance. Chain-level changes like EIP-1559 stay as EIPs — they are never ERCs. +- **ERC-20 `approve` has a race condition** — If Alice approves Bob for 100, then changes to 50, Bob can front-run: spend the original 100, then spend the new 50, totaling 150. Mitigation: approve to 0 first, use `increaseAllowance`/`decreaseAllowance`, or use ERC-2612 `permit`. USDT requires resetting to 0 before setting a new nonzero allowance — it reverts otherwise. +- **ERC-721 `transferFrom` skips receiver checks** — `transferFrom` does NOT call `onERC721Received` on the recipient. Tokens sent to contracts that cannot handle them are permanently locked. Use `safeTransferFrom` unless you have a specific reason not to. +- **EIP-712 domain separator MUST include `chainId`** — Omitting `chainId` from the `EIP712Domain` allows signature replay across chains. A signature valid on mainnet becomes valid on every fork and L2 sharing the contract address. Always include `chainId` and `verifyingContract`. +- **ERC-4337 bundler != relayer** — A bundler packages `UserOperation` objects and submits to the `EntryPoint`. A relayer wraps a signed message and calls a trusted forwarder. Different trust models, different gas accounting, different entry points. +- **EIP-1559 `baseFee` is protocol-controlled** — Users set `maxFeePerGas` and `maxPriorityFeePerGas`. The protocol sets `baseFee` per block. The base fee is burned, the priority fee goes to the validator. Confusing these causes incorrect gas estimation. +- **ERC-4626 share/asset math is rounding-sensitive** — `convertToShares` and `convertToAssets` must round in favor of the vault to prevent share inflation attacks. First-depositor attacks exploit vaults that skip this. +- **EIP-2612 permit signatures can be front-run** — The approval still takes effect, but the original `permit` call reverts. Always check allowance before calling permit. +- **ERC-1155 has no per-token approval** — Only `setApprovalForAll` exists (operator model). There is no `approve(tokenId)` like ERC-721. +- **`decimals()` is OPTIONAL** — Part of `IERC20Metadata`, not `IERC20`. USDC uses 6, WBTC uses 8. Never assume 18. + +## How to Look Up Any EIP + +When a user asks about ANY EIP or ERC — even ones not covered in this skill — fetch the full spec on demand. + +### Step 1: Determine if it's an EIP or ERC + +- **ERC** (ERC-20, ERC-721, ERC-1155, ERC-4626, etc.) — application-level standards. Repo: `ethereum/ERCs` +- **EIP** (EIP-1559, EIP-4844, EIP-7702, etc.) — core/networking/interface changes. Repo: `ethereum/EIPs` +- Rule of thumb: token standards and contract interfaces are ERCs. Protocol-level changes are EIPs. +- If unsure, try ERC first (more common in user queries), fall back to EIP. + +### Step 2: Fetch the raw spec + +**For ERCs** (application-level — tokens, wallets, contract standards): + +``` +WebFetch: https://raw.githubusercontent.com/ethereum/ERCs/master/ERCS/erc-{number}.md +``` + +**For EIPs** (core/networking — gas, consensus, transaction types): + +``` +WebFetch: https://raw.githubusercontent.com/ethereum/EIPs/master/EIPS/eip-{number}.md +``` + +Examples: +- ERC-20 → `https://raw.githubusercontent.com/ethereum/ERCs/master/ERCS/erc-20.md` +- ERC-4337 → `https://raw.githubusercontent.com/ethereum/ERCs/master/ERCS/erc-4337.md` +- EIP-1559 → `https://raw.githubusercontent.com/ethereum/EIPs/master/EIPS/eip-1559.md` +- EIP-7702 → `https://raw.githubusercontent.com/ethereum/EIPs/master/EIPS/eip-7702.md` + +### Step 3: Parse and summarize + +The fetched markdown has YAML frontmatter (`eip`, `title`, `status`, `type`, `category`, `author`, `created`, `requires`) followed by sections: Simple Summary, Abstract, Motivation, Specification, Rationale, Backwards Compatibility, Security Considerations, Copyright. + +Extract and present: title, status, what it does, key interfaces/types, and security considerations. + +### Alternative methods + +```bash +# GitHub CLI (requires auth) +gh api repos/ethereum/ERCs/contents/ERCS/erc-{number}.md --jq '.content' | base64 -d +gh api repos/ethereum/EIPs/contents/EIPS/eip-{number}.md --jq '.content' | base64 -d +``` + +### Sources + +| Source | URL | Best for | +|--------|-----|----------| +| EIPs repo | https://github.com/ethereum/EIPs | Core/networking specs, raw markdown | +| ERCs repo | https://github.com/ethereum/ERCs | Token/application standards, raw markdown | +| EIPs website | https://eips.ethereum.org/all | Browsing all EIPs with status filters | + +- Raw EIP specs: `https://raw.githubusercontent.com/ethereum/EIPs/master/EIPS/eip-{number}.md` +- Raw ERC specs: `https://raw.githubusercontent.com/ethereum/ERCs/master/ERCS/erc-{number}.md` +- Browse all: https://eips.ethereum.org/all + +## EIP vs ERC + +| Type | Scope | Examples | +|------|-------|---------| +| **EIP** | Protocol-level changes (consensus, networking, EVM) | EIP-1559, EIP-4844, EIP-7702 | +| **ERC** | Application-level standards (tokens, wallets, signing) | ERC-20, ERC-721, ERC-4337 | + +ERCs are a subset of EIPs. "ERC-20" and "EIP-20" refer to the same proposal. The ERC designation applies once the proposal reaches application-layer Final status. + +### Status Lifecycle + +``` +Draft -> Review -> Last Call -> Final + -> Stagnant (no activity 6+ months) + -> Withdrawn +``` + +Only **Final** status EIPs should be relied on in production. Draft/Review standards may change without notice. ## Token Standards @@ -52,16 +135,30 @@ interface IERC20 { ``` **Key rules:** -- `transfer` and `transferFrom` MUST return `true` on success. Some tokens (USDT) do not return a value — use OpenZeppelin `SafeERC20` to handle both. -- `decimals()` is OPTIONAL per the spec (part of `IERC20Metadata`). Never assume 18 — USDC and USDT use 6, WBTC uses 8. -- Zero-address transfers SHOULD emit `Transfer` events. Minting is `Transfer(address(0), to, amount)`. Burning is `Transfer(from, address(0), amount)`. +- `transfer` and `transferFrom` MUST return `true` on success. Some tokens (USDT) do not return a value — use OpenZeppelin `SafeERC20`. +- `decimals()` is OPTIONAL (part of `IERC20Metadata`). USDC/USDT use 6, WBTC uses 8. +- Minting: `Transfer(address(0), to, amount)`. Burning: `Transfer(from, address(0), amount)`. +- Fee-on-transfer tokens deduct on every transfer — always measure `balanceOf` before/after. + +```typescript +import { erc20Abi, formatUnits } from 'viem'; + +const balance = await publicClient.readContract({ + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC + abi: erc20Abi, + functionName: 'balanceOf', + args: ['0xYourAddress...'], +}); +// balance is bigint in base units — format with correct decimals +const formatted = formatUnits(balance, 6); // USDC has 6 decimals +``` ### ERC-721 — Non-Fungible Token Each token has a unique `tokenId`. Ownership is 1:1. ```solidity -interface IERC721 { +interface IERC721 is IERC165 { function balanceOf(address owner) external view returns (uint256); function ownerOf(uint256 tokenId) external view returns (address); function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external; @@ -79,16 +176,17 @@ interface IERC721 { ``` **Key rules:** -- `safeTransferFrom` calls `IERC721Receiver.onERC721Received` on the recipient if it is a contract. Reverts if the receiver does not implement it or returns the wrong selector. -- `transferFrom` does NOT perform receiver checks. Tokens can be permanently lost if sent to a contract that cannot handle them. -- `approve` clears on transfer — approved address is reset when the token moves. +- `safeTransferFrom` calls `IERC721Receiver.onERC721Received` on contract recipients. Reverts if not implemented or wrong selector returned. +- `transferFrom` skips receiver checks — tokens can be permanently lost. +- `approve` clears on transfer — the approved address resets when the token moves. +- `ownerOf` MUST revert for nonexistent tokens (never return `address(0)`). ### ERC-1155 — Multi-Token Single contract managing multiple token types (fungible and non-fungible) identified by `id`. ```solidity -interface IERC1155 { +interface IERC1155 is IERC165 { function balanceOf(address account, uint256 id) external view returns (uint256); function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids) external view returns (uint256[] memory); @@ -107,13 +205,13 @@ interface IERC1155 { ``` **Key rules:** -- No `transferFrom` — ALL transfers are safe transfers that call `onERC1155Received` or `onERC1155BatchReceived`. +- ALL transfers are safe — `onERC1155Received` / `onERC1155BatchReceived` is always called. - No per-token approval — only `setApprovalForAll` (operator model). -- Batch operations reduce gas for multi-token transfers. +- Batch operations save gas on multi-token transfers. ### ERC-4626 — Tokenized Vault -Standard interface for yield-bearing vaults. The vault is itself an ERC-20 representing shares. +Standard for yield-bearing vaults. The vault itself is an ERC-20 representing shares. ```solidity interface IERC4626 is IERC20 { @@ -121,18 +219,18 @@ interface IERC4626 is IERC20 { function totalAssets() external view returns (uint256); function convertToShares(uint256 assets) external view returns (uint256); function convertToAssets(uint256 shares) external view returns (uint256); - function maxDeposit(address receiver) external view returns (uint256); - function previewDeposit(uint256 assets) external view returns (uint256); function deposit(uint256 assets, address receiver) external returns (uint256 shares); - function maxMint(address receiver) external view returns (uint256); - function previewMint(uint256 shares) external view returns (uint256); function mint(uint256 shares, address receiver) external returns (uint256 assets); - function maxWithdraw(address owner) external view returns (uint256); - function previewWithdraw(uint256 assets) external view returns (uint256); function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares); + function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets); + function maxDeposit(address receiver) external view returns (uint256); + function maxMint(address receiver) external view returns (uint256); + function maxWithdraw(address owner) external view returns (uint256); function maxRedeem(address owner) external view returns (uint256); + function previewDeposit(uint256 assets) external view returns (uint256); + function previewMint(uint256 shares) external view returns (uint256); + function previewWithdraw(uint256 assets) external view returns (uint256); function previewRedeem(uint256 shares) external view returns (uint256); - function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets); event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares); event Withdraw(address indexed sender, address indexed receiver, address indexed owner, uint256 assets, uint256 shares); @@ -141,45 +239,28 @@ interface IERC4626 is IERC20 { **Key rules:** - `deposit`/`mint` are asset-denominated vs share-denominated entry. `withdraw`/`redeem` are asset-denominated vs share-denominated exit. -- `preview*` functions MUST return the exact value that would be used in the corresponding action (not an estimate). -- Rounding: `convertToShares` rounds DOWN, `convertToAssets` rounds DOWN. This protects the vault from share manipulation. `previewMint` and `previewWithdraw` round UP (caller pays more). -- First-depositor attack: attacker deposits 1 wei, donates tokens to inflate share price, subsequent depositors get 0 shares. Mitigate with virtual shares/assets offset or minimum initial deposit. +- `preview*` functions MUST return exact values (not estimates). +- Rounding: favor the vault. `convertToShares` rounds DOWN, `previewMint`/`previewWithdraw` round UP. +- First-depositor attack: attacker deposits 1 wei, donates tokens to inflate share price. Mitigate with virtual shares/assets offset or minimum deposit. -## Signature Standards - -### EIP-191 — Personal Sign - -Prefixed message signing to prevent raw transaction signing. The `personal_sign` RPC method. - -``` -0x19 <1 byte version> -``` - -**Version `0x45` (E)** — `personal_sign`: -``` -"\x19Ethereum Signed Message:\n" + len(message) + message -``` +### ERC-2981 — NFT Royalties ```solidity -// Recovering a personal_sign signature -bytes32 messageHash = keccak256(abi.encodePacked( - "\x19Ethereum Signed Message:\n32", - dataHash -)); -address signer = ECDSA.recover(messageHash, signature); +interface IERC2981 is IERC165 { + function royaltyInfo(uint256 tokenId, uint256 salePrice) + external view returns (address receiver, uint256 royaltyAmount); +} ``` -**Key rules:** -- The prefix prevents users from being tricked into signing valid Ethereum transactions. -- `len(message)` is the decimal string length of the message in bytes, NOT the hex length. -- For fixed-length data (bytes32), the length is always "32". +Returns royalty recipient and amount for a given sale price. Enforcement is voluntary — marketplaces query this but the standard cannot force payment. -### EIP-712 — Typed Structured Data +## Signature & Auth Standards -Structured, human-readable signing. Users see what they sign in their wallet. +### EIP-712 — Typed Structured Data Signing + +Structured, human-readable signing. Users see the data they sign in their wallet. ```solidity -// Domain separator — MUST include chainId and verifyingContract bytes32 DOMAIN_SEPARATOR = keccak256(abi.encode( keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), keccak256(bytes("MyProtocol")), @@ -188,12 +269,10 @@ bytes32 DOMAIN_SEPARATOR = keccak256(abi.encode( address(this) )); -// Type hash for the struct being signed bytes32 constant PERMIT_TYPEHASH = keccak256( "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" ); -// Final hash to sign bytes32 digest = keccak256(abi.encodePacked( "\x19\x01", DOMAIN_SEPARATOR, @@ -201,52 +280,43 @@ bytes32 digest = keccak256(abi.encodePacked( )); ``` -**Key rules:** -- Domain separator MUST be recomputed if `block.chainid` changes (fork protection). Cache it but verify against current chain ID. -- Nested structs: type string must include referenced types in alphabetical order after the primary type. -- Arrays in typed data: `keccak256(abi.encodePacked(array))` for fixed-size element arrays, element-wise encoding for struct arrays. -- `bytes` and `string` fields are hashed with `keccak256` before encoding. - -### ERC-1271 — Contract Signature Verification - -Allows smart contracts (multisigs, smart accounts) to validate signatures. - -```solidity -interface IERC1271 { - // MUST return 0x1626ba7e if signature is valid - function isValidSignature(bytes32 hash, bytes memory signature) - external view returns (bytes4 magicValue); -} - -bytes4 constant ERC1271_MAGIC_VALUE = 0x1626ba7e; -``` - -**Verification pattern (supporting both EOA and contract signers):** - -```solidity -function _isValidSignature(address signer, bytes32 hash, bytes memory signature) internal view returns (bool) { - if (signer.code.length > 0) { - // Contract signer — delegate to ERC-1271 - try IERC1271(signer).isValidSignature(hash, signature) returns (bytes4 magicValue) { - return magicValue == 0x1626ba7e; - } catch { - return false; - } - } else { - // EOA signer — ecrecover - return ECDSA.recover(hash, signature) == signer; - } -} +```typescript +const signature = await walletClient.signTypedData({ + domain: { + name: 'MyProtocol', + version: '1', + chainId: 1, + verifyingContract: '0xContractAddress...', + }, + types: { + Permit: [ + { name: 'owner', type: 'address' }, + { name: 'spender', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + ], + }, + primaryType: 'Permit', + message: { + owner: '0xOwner...', + spender: '0xSpender...', + value: 1000000n, + nonce: 0n, + deadline: BigInt(Math.floor(Date.now() / 1000) + 3600), + }, +}); ``` **Key rules:** -- Always check `signer.code.length` to determine EOA vs contract before choosing verification path. -- The `hash` parameter is the EIP-712 digest, NOT the raw message. -- Wrap in try/catch — malicious contracts can revert, consume gas, or return unexpected values. +- Domain separator MUST be recomputed if `block.chainid` changes (fork protection). +- Nested structs: referenced types appended alphabetically after the primary type. +- `bytes` and `string` fields are `keccak256`-ed before encoding. +- Do NOT list `EIP712Domain` in the `types` object — viem derives it from the `domain` field. ### ERC-2612 — Permit (Gasless Approval) -EIP-712 signed approvals for ERC-20 tokens. Users approve via signature instead of an on-chain transaction. +EIP-712 signed approvals for ERC-20 tokens. Eliminates the separate `approve` transaction. ```solidity interface IERC20Permit { @@ -263,234 +333,170 @@ interface IERC20Permit { - `deadline` is a Unix timestamp. Always check `block.timestamp <= deadline`. - Nonces are sequential per-owner. Cannot skip or reorder. - Not all ERC-20 tokens support permit. DAI uses a non-standard permit with `allowed` (bool) instead of `value` (uint256). -- Permit signatures can be front-run — the approval still takes effect, but the `permit` call reverts if already used. Handle this gracefully (check allowance before calling permit). - -## Account Abstraction +- Permit signatures can be front-run — check allowance before calling permit. -### ERC-4337 — Account Abstraction via Entry Point +### EIP-4361 — Sign-In With Ethereum (SIWE) -Decouples transaction validation from EOAs. Smart contract wallets validate their own transactions. +Standard message format for using an Ethereum address to authenticate with off-chain services. -**Core flow:** -``` -User creates UserOperation - → Bundler collects UserOperations into a bundle - → Bundler calls EntryPoint.handleOps(userOps, beneficiary) - → EntryPoint calls account.validateUserOp(userOp, userOpHash, missingAccountFunds) - → If paymaster: EntryPoint calls paymaster.validatePaymasterUserOp(...) - → EntryPoint executes the operation via account ``` +example.com wants you to sign in with your Ethereum account: +0xAddress... -**UserOperation struct (v0.7):** +I accept the Terms of Service: https://example.com/tos -```solidity -struct PackedUserOperation { - address sender; - uint256 nonce; - bytes initCode; // factory address + calldata (for first-time account deployment) - bytes callData; // the actual operation to execute - bytes32 accountGasLimits; // packed: verificationGasLimit (16 bytes) + callGasLimit (16 bytes) - uint256 preVerificationGas; - bytes32 gasFees; // packed: maxPriorityFeePerGas (16 bytes) + maxFeePerGas (16 bytes) - bytes paymasterAndData; // paymaster address + verification gas + postOp gas + custom data - bytes signature; -} +URI: https://example.com +Version: 1 +Chain ID: 1 +Nonce: abc123 +Issued At: 2026-03-04T12:00:00.000Z ``` -**EntryPoint address (v0.7):** `0x0000000071727De22E5E9d8BAf0edAc6f37da032` +Used by dApps for wallet-based authentication. The message is human-readable, domain-bound (prevents phishing), and includes a server-issued nonce for replay protection. -**Key rules:** -- `validateUserOp` MUST return `SIG_VALIDATION_FAILED` (1) on invalid signature, NOT revert. Reverting wastes bundler gas. -- `nonce` uses a key-space scheme: upper 192 bits = key, lower 64 bits = sequence. Allows parallel nonce channels. -- `initCode` is only used for first UserOp (account deployment). Empty on subsequent operations. -- Bundlers simulate `validateUserOp` off-chain before inclusion. Banned opcodes during validation: `BALANCE`, `GASPRICE`, `TIMESTAMP`, `BLOCKHASH`, `CREATE`, etc. -- Paymasters can sponsor gas (gasless UX) or accept ERC-20 payment. +### ERC-1271 — Contract Signature Verification -### ERC-7579 — Modular Smart Accounts +Allows smart contracts (multisigs, smart accounts) to validate signatures. -Standard interface for modular account components. Accounts install/uninstall modules for validators, executors, hooks, and fallback handlers. +```solidity +interface IERC1271 { + function isValidSignature(bytes32 hash, bytes memory signature) + external view returns (bytes4 magicValue); +} +// MUST return 0x1626ba7e if valid +``` -**Module types:** -| Type ID | Role | Called by | -|---------|------|-----------| -| 1 | Validator | Account (during validateUserOp) | -| 2 | Executor | External trigger (automation) | -| 3 | Fallback handler | Account (delegatecall on unknown function) | -| 4 | Hook | Account (before/after execution) | +**Dual verification pattern (EOA + contract):** ```solidity -interface IERC7579Account { - function execute(bytes32 mode, bytes calldata executionCalldata) external; - function installModule(uint256 moduleTypeId, address module, bytes calldata initData) external; - function uninstallModule(uint256 moduleTypeId, address module, bytes calldata deInitData) external; - function isModuleInstalled(uint256 moduleTypeId, address module, bytes calldata additionalContext) - external view returns (bool); - function supportsExecutionMode(bytes32 mode) external view returns (bool); - function supportsModule(uint256 moduleTypeId) external view returns (bool); +function _isValidSignature(address signer, bytes32 hash, bytes memory signature) internal view returns (bool) { + if (signer.code.length > 0) { + try IERC1271(signer).isValidSignature(hash, signature) returns (bytes4 magicValue) { + return magicValue == 0x1626ba7e; + } catch { + return false; + } + } else { + return ECDSA.recover(hash, signature) == signer; + } } ``` -**Key rules:** -- Execution modes encode call type (single, batch, delegatecall) and exec type (default, try) in a `bytes32`. -- Modules MUST be stateless with respect to the account — store per-account state via mappings keyed by `msg.sender`. -- `installModule` and `uninstallModule` should be access-controlled (only the account itself or authorized validators). - -## Chain & Gas +## Gas & Transaction Standards ### EIP-1559 — Fee Market -Replaced the first-price gas auction with a base fee + priority fee model. +Base fee + priority fee model. The base fee is burned. -**Transaction fields:** -- `maxFeePerGas` — Maximum total fee per gas unit the sender will pay. -- `maxPriorityFeePerGas` — Tip to the validator (above base fee). -- `baseFeePerGas` — Protocol-determined, burned. Not set by users. +| Field | Set by | Description | +|-------|--------|-------------| +| `baseFeePerGas` | Protocol | Adjusts per block based on utilization. Burned. | +| `maxPriorityFeePerGas` | User | Tip to the validator. | +| `maxFeePerGas` | User | Maximum total fee per gas unit. | -**Base fee adjustment:** -- Target: 50% gas utilization per block (15M gas of 30M limit). -- Block >50% full → base fee increases (up to 12.5% per block). -- Block <50% full → base fee decreases (up to 12.5% per block). -- Base fee is entirely burned (EIP-1559 burn mechanism). - -**Effective gas price:** ``` effectiveGasPrice = min(baseFeePerGas + maxPriorityFeePerGas, maxFeePerGas) ``` -**Refund:** `(maxFeePerGas - effectiveGasPrice) * gasUsed` is refunded to sender. +Base fee increases up to 12.5% per block when utilization exceeds 50% target (15M gas of 30M limit). ### EIP-4844 — Blob Transactions (Proto-Danksharding) -Type 3 transactions carrying binary large objects (blobs) for L2 data availability. - -**Key properties:** -- Blobs are ~128 KB each, max 6 per transaction (post-Pectra: higher targets). -- Blob data is NOT accessible from the EVM — only the blob's versioned hash (commitment). -- Blobs are pruned from consensus nodes after ~18 days. -- Separate fee market: `blobBaseFee` adjusts independently from execution `baseFee`. - -**Transaction fields (added to EIP-1559):** -- `maxFeePerBlobGas` — Maximum fee per blob gas unit. -- `blobVersionedHashes` — List of versioned hashes (one per blob). +Type 3 transactions carrying blobs for L2 data availability. -**Precompile:** `BLOBHASH` opcode (0x49) returns versioned hash at given index. `Point evaluation precompile` at `0x0A` verifies KZG proofs. - -**Who uses this:** L2 rollups (Arbitrum, Optimism, Base, Scroll) post their data as blobs instead of calldata, reducing costs by ~10-100x. +- Blobs are ~128 KB each, target 6 per block (post-Pectra), max 9. +- Blob data is NOT accessible from the EVM — only the versioned hash. +- Blobs are pruned after ~18 days. +- Separate fee market with independently adjusting `blobBaseFee`. +- Used by Arbitrum, Optimism, Base, and Scroll for data posting. ### EIP-2930 — Access Lists -Type 1 transactions that declare which addresses and storage keys will be accessed. +Pre-declare which addresses and storage keys will be accessed. -```json -{ - "accessList": [ - { - "address": "0xContractAddress", - "storageKeys": [ - "0x0000000000000000000000000000000000000000000000000000000000000001" - ] - } - ] -} +```typescript +const accessList = await publicClient.createAccessList({ + account: '0xSender...', + to: '0xContract...', + data: encodedCalldata, +}); ``` -**Key rules:** -- Pre-warming accessed slots costs 2400 gas per slot (vs 2600 for cold access). Net savings only when you access each declared slot. -- Useful for cross-contract calls where you know which storage slots will be read. -- `eth_createAccessList` RPC method generates an optimal access list for a given transaction. - -## Proxy & Upgrade Patterns - -### EIP-1967 — Proxy Storage Slots +Pre-warming costs 2,400 gas per slot (vs 2,600 cold access). Useful for cross-contract calls accessing known storage. -Standardized storage slots for proxy contracts to avoid collisions with implementation storage. +## Account Abstraction -```solidity -// Implementation slot: bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1) -bytes32 constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; +### ERC-4337 — Account Abstraction via Entry Point -// Admin slot: bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1) -bytes32 constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; +Smart contract wallets with `UserOperation` objects processed by bundlers. -// Beacon slot: bytes32(uint256(keccak256("eip1967.proxy.beacon")) - 1) -bytes32 constant BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50; +**Core flow:** +``` +User creates UserOperation + -> Bundler collects and simulates + -> Bundler calls EntryPoint.handleOps(userOps, beneficiary) + -> EntryPoint calls account.validateUserOp(...) + -> If paymaster: validates sponsorship + -> EntryPoint executes the operation ``` -**Key rules:** -- The `-1` offset prevents the slot from being a known hash preimage, avoiding potential collisions with Solidity mappings. -- Read implementation address: `sload(IMPLEMENTATION_SLOT)`. -- Block explorers and tools rely on these standard slots for proxy detection. - -### EIP-1822 — UUPS (Universal Upgradeable Proxy Standard) - -Upgrade logic lives in the implementation, not the proxy. The proxy is minimal. +**PackedUserOperation (v0.7):** ```solidity -// Implementation contains upgrade logic -function upgradeTo(address newImplementation) external onlyOwner { - require(newImplementation.code.length > 0, "Not a contract"); - // ERC-1822: implementation stores its own address for verification - require( - IERC1822(newImplementation).proxiableUUID() == IMPLEMENTATION_SLOT, - "UUID mismatch" - ); - StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value = newImplementation; -} - -function proxiableUUID() external pure returns (bytes32) { - return IMPLEMENTATION_SLOT; +struct PackedUserOperation { + address sender; + uint256 nonce; // 192-bit key + 64-bit sequence for parallel channels + bytes initCode; // factory address + calldata (first-time only) + bytes callData; + bytes32 accountGasLimits; // verificationGasLimit (16 bytes) + callGasLimit (16 bytes) + uint256 preVerificationGas; + bytes32 gasFees; // maxPriorityFeePerGas (16 bytes) + maxFeePerGas (16 bytes) + bytes paymasterAndData; // paymaster address + gas limits + custom data + bytes signature; } ``` -**UUPS vs Transparent Proxy:** -| | UUPS (EIP-1822) | Transparent (EIP-1967) | -|---|---|---| -| Upgrade logic location | Implementation | Proxy | -| Gas per call | Lower (no admin check) | Higher (checks if caller is admin) | -| Risk | Bricked if implementation lacks `upgradeTo` | Cannot brick upgrade path | -| Deploy cost | Lower (minimal proxy) | Higher (proxy has upgrade logic) | +**EntryPoint v0.7:** `0x0000000071727De22E5E9d8BAf0edAc6f37da032` **Key rules:** -- UUPS implementations MUST include upgrade logic. If you deploy an implementation without `upgradeTo`, the proxy is permanently locked. -- Always use `_disableInitializers()` in implementation constructors to prevent initialization of the implementation itself. -- OpenZeppelin's `UUPSUpgradeable` provides the standard implementation. +- `validateUserOp` MUST return `SIG_VALIDATION_FAILED` (1) on bad signatures, NOT revert. +- Banned opcodes during validation: `GASPRICE`, `TIMESTAMP`, `BLOCKHASH`, `CREATE`, etc. +- Paymasters pre-deposit ETH to EntryPoint and can sponsor gas or accept ERC-20 payment. -### EIP-7201 — Namespaced Storage Layout +### EIP-7702 — EOA Delegation -Deterministic storage locations for upgradeable contracts, preventing slot collisions across inheritance. +EOAs temporarily or persistently delegate execution to smart contract code. Type `0x04` transactions include an `authorizationList`. -```solidity -// Formula: keccak256(abi.encode(uint256(keccak256("myprotocol.storage.MyStruct")) - 1)) & ~bytes32(uint256(0xff)) -// The -1 and masking prevent hash preimage attacks and align to 256-byte boundaries +``` +authorization_tuple = (chain_id, address, nonce, y_parity, r, s) +``` -/// @custom:storage-location erc7201:myprotocol.storage.Counter -struct CounterStorage { - uint256 count; - mapping(address => uint256) perUser; -} +When processed, the EOA's code is set to `0xef0100 || address`. Calls execute the delegated contract's code in the EOA's context (like delegatecall). -function _getCounterStorage() private pure returns (CounterStorage storage $) { - // keccak256(abi.encode(uint256(keccak256("myprotocol.storage.Counter")) - 1)) & ~bytes32(uint256(0xff)) - bytes32 COUNTER_STORAGE_LOCATION = 0x...; - assembly { - $.slot := COUNTER_STORAGE_LOCATION - } -} -``` +```typescript +import { walletClient } from './config'; -**Key rules:** -- Each struct gets its own deterministic namespace. No inheritance slot conflicts. -- The `@custom:storage-location` NatSpec annotation lets tools (OpenZeppelin Upgrades) verify layout. -- Replaces the fragile "append-only" storage pattern used by older upgradeable contracts. -- OpenZeppelin v5+ uses EIP-7201 by default for all upgradeable contracts. +const authorization = await walletClient.signAuthorization({ + contractAddress: '0xBatchExecutor...', +}); -## Agent Identity & Reputation (ERC-8004) +const hash = await walletClient.writeContract({ + address: walletClient.account.address, + abi: batchExecutorAbi, + functionName: 'executeBatch', + args: [[ + { target: '0xTokenA...', value: 0n, data: approveCalldata }, + { target: '0xRouter...', value: 0n, data: swapCalldata }, + ]], + authorizationList: [authorization], +}); +``` -ERC-8004 defines onchain identity for AI agents — three registries for agent discovery, reputation, and validation. +**EIP-7702 + ERC-4337**: Complementary. Bundlers accept `eip7702Auth` on UserOperations, letting EOAs participate in AA without migrating addresses. -### Three Registries +### ERC-8004 — Agent Identity Registry -**Identity Registry**: Agents register their name, supported skills, service endpoints, and metadata. Think DNS for AI agents. +Onchain identity for AI agents — three registries for discovery, reputation, and validation. ```solidity interface IAgentIdentityRegistry { @@ -503,126 +509,146 @@ interface IAgentIdentityRegistry { function registerAgent(AgentIdentity calldata identity) external returns (uint256 agentId); function getAgent(uint256 agentId) external view returns (AgentIdentity memory); - function updateAgent(uint256 agentId, AgentIdentity calldata identity) external; function resolveByName(string calldata name) external view returns (uint256 agentId); event AgentRegistered(uint256 indexed agentId, address indexed owner, string name); } ``` -**Reputation Registry**: Immutable feedback after agent interactions. Cannot be modified or deleted — only appended. Score aggregation is left to consumers. +Reputation registry provides immutable feedback (append-only, no edits). Validation registry enables third-party verification of agent capabilities. Integrates with x402 for payment authentication. + +## Proxy & Upgrade Patterns + +### EIP-1967 — Proxy Storage Slots ```solidity -interface IAgentReputationRegistry { - function submitFeedback(uint256 agentId, uint8 score, bytes calldata details) external; - function getFeedbackCount(uint256 agentId) external view returns (uint256); +// Implementation: bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1) +bytes32 constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; - event FeedbackSubmitted(uint256 indexed agentId, address indexed reviewer, uint8 score); -} +// Admin: bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1) +bytes32 constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + +// Beacon: bytes32(uint256(keccak256("eip1967.proxy.beacon")) - 1) +bytes32 constant BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50; ``` -**Validation Registry**: Independent third-party verification of agent capabilities. Validators stake reputation on their assessments. +### EIP-1822 — UUPS Proxy -```solidity -interface IAgentValidationRegistry { - function validateAgent(uint256 agentId, bytes calldata proof) external; - function isValidated(uint256 agentId, address validator) external view returns (bool); +Upgrade logic in the implementation, not the proxy. Smaller proxy, cheaper deploys. **Risk**: if you deploy an implementation without `upgradeTo`, the proxy is permanently bricked. + +### EIP-7201 — Namespaced Storage - event AgentValidated(uint256 indexed agentId, address indexed validator); +Deterministic storage locations for upgradeable contracts. Prevents slot collisions across inheritance. OpenZeppelin v5+ uses this by default. + +```solidity +/// @custom:storage-location erc7201:myprotocol.storage.Counter +struct CounterStorage { + uint256 count; + mapping(address => uint256) perUser; } ``` -**Integration with x402**: Agents registered via ERC-8004 can use their identity for x402 payment authentication — the agent's onchain identity serves as both discovery mechanism and payment credential. See the `x402` skill for payment flow details. +## Interface Detection -## EOA Delegation (EIP-7702) +### ERC-165 — Standard Interface Detection -EIP-7702 (Pectra, May 2025) lets EOAs temporarily or persistently delegate their execution to smart contract code. This bridges the gap between EOAs and smart contract accounts. +```solidity +function supportsInterface(bytes4 interfaceId) external view returns (bool); +``` -### Transaction Format +**Common interface IDs:** -Type `0x04` transactions include an `authorizationList` — an array of signed authorization tuples: +| Interface | ID | +|-----------|-----| +| IERC165 | `0x01ffc9a7` | +| IERC721 | `0x80ac58cd` | +| IERC721Metadata | `0x5b5e139f` | +| IERC1155 | `0xd9b67a26` | +| IERC2981 | `0x2a55205a` | -``` -authorization_tuple = (chain_id, address, nonce, y_parity, r, s) -``` +ERC-20 predates ERC-165 — do not rely on `supportsInterface` to detect ERC-20 tokens. -When processed, the EOA's code is set to a delegation designator: `0xef0100 || address`. Any calls to the EOA now execute the delegated contract's code, with the EOA as `msg.sender` and `address(this)`. +### EIP-6963 — Multi-Wallet Discovery -### Key Mechanics +Replaces the `window.ethereum` single-provider model. Wallets announce themselves via DOM events, eliminating the provider collision problem. -| Property | Behavior | -|----------|----------| -| Delegation scope | Per-transaction (reverts after) or persistent (until revoked) | -| Code execution | Delegated contract runs in EOA's context (like delegatecall) | -| Storage | Uses EOA's storage slots | -| msg.sender | Callers see the EOA address | -| Revocation | Set delegation to `address(0)` | -| Nonce | Authorization nonce is separate from tx nonce | +```typescript +window.addEventListener('eip6963:announceProvider', (event) => { + const { info, provider } = event.detail; + // info.name, info.icon, info.rdns, info.uuid +}); +window.dispatchEvent(new Event('eip6963:requestProvider')); +``` -### viem Integration +## Chain & Network -```typescript -import { walletClient } from './config'; -import { parseEther } from 'viem'; -import { signAuthorization } from 'viem/experimental'; +### EIP-155 — Replay Protection -// Sign authorization to delegate to a batch executor -const authorization = await walletClient.signAuthorization({ - contractAddress: '0xBatchExecutor...', // contract with batch logic -}); +Chain ID in transaction signatures prevents cross-chain replay. Common IDs: -// Execute batch via delegated code -const hash = await walletClient.writeContract({ - address: walletClient.account.address, // call yourself (delegated) - abi: batchExecutorAbi, - functionName: 'executeBatch', - args: [[ - { target: '0xTokenA...', value: 0n, data: approveCalldata }, - { target: '0xRouter...', value: 0n, data: swapCalldata }, - ]], - authorizationList: [authorization], -}); -``` +| Chain | ID | Chain | ID | +|-------|----|-------|----| +| Ethereum Mainnet | 1 | Polygon | 137 | +| Sepolia | 11155111 | Arbitrum One | 42161 | +| Base | 8453 | Optimism | 10 | -### Interaction with ERC-4337 +### EIP-1193 — Provider API -EIP-7702 and ERC-4337 are complementary: -- **EIP-7702 alone**: EOA gets smart account features (batching, sponsorship) but no persistent account abstraction infrastructure -- **ERC-4337 alone**: Full AA but requires deploying a new smart account contract -- **Combined**: Bundlers accept `eip7702Auth` on UserOperations — EOAs can participate in the ERC-4337 ecosystem without migrating to a new address +Standard JavaScript API for Ethereum providers (`window.ethereum`). -See the `account-abstraction` skill for full implementation patterns. +```typescript +interface EIP1193Provider { + request(args: { method: string; params?: unknown[] }): Promise; + on(event: string, listener: (...args: unknown[]) => void): void; + removeListener(event: string, listener: (...args: unknown[]) => void): void; +} +``` ## Quick Lookup Table -| Number | Name | Type | Status | Summary | -|--------|------|------|--------|---------| -| ERC-20 | Token Standard | ERC | Final | Fungible token interface (transfer, approve, allowance) | -| ERC-165 | Interface Detection | ERC | Final | `supportsInterface(bytes4)` — standard introspection | -| ERC-173 | Contract Ownership | ERC | Final | `owner()` + `transferOwnership()` standard | -| EIP-191 | Signed Data Standard | EIP | Final | Prefixed signing to prevent transaction-signing tricks | -| ERC-721 | Non-Fungible Token | ERC | Final | Unique token with `ownerOf`, `safeTransferFrom` | -| ERC-1155 | Multi-Token | ERC | Final | Multiple token types in one contract | -| ERC-1271 | Contract Signatures | ERC | Final | `isValidSignature` for smart contract wallets | -| EIP-712 | Typed Data Signing | EIP | Final | Structured, human-readable signature requests | -| EIP-1014 | CREATE2 | EIP | Final | Deterministic contract addresses from salt + initcode | -| EIP-1559 | Fee Market | EIP | Final | Base fee + priority fee, base fee burned | -| EIP-1822 | UUPS Proxy | EIP | Final | Upgrade logic in implementation, not proxy | -| EIP-1967 | Proxy Storage Slots | EIP | Final | Standard slots for impl/admin/beacon addresses | -| EIP-2098 | Compact Signatures | EIP | Final | 64-byte signatures (r + yParityAndS) | -| ERC-2612 | Permit | ERC | Final | Gasless ERC-20 approvals via EIP-712 signature | -| EIP-2930 | Access Lists | EIP | Final | Declare accessed addresses/slots for gas savings | -| ERC-3009 | Transfer With Authorization | ERC | Final | Gasless token transfers via EIP-712 signatures | -| ERC-4337 | Account Abstraction | ERC | Draft | Smart accounts via EntryPoint + Bundler | -| ERC-4626 | Tokenized Vault | ERC | Final | Standardized yield vault (deposit/withdraw/redeem) | -| EIP-4844 | Blob Transactions | EIP | Final | L2 data availability via blobs (~128 KB, pruned) | -| ERC-6900 | Modular Accounts v1 | ERC | Draft | Plugin architecture for smart accounts | -| EIP-7201 | Namespaced Storage | EIP | Final | Deterministic storage slots for upgradeable contracts | -| ERC-7579 | Modular Accounts v2 | ERC | Draft | Minimal modular smart account interface | -| EIP-7594 | PeerDAS | EIP | Final | Peer Data Availability Sampling for blob scaling | -| EIP-7702 | Set EOA Account Code | EIP | Final | EOA delegation to smart contract code | -| EIP-7951 | secp256r1 Precompile | EIP | Final | Native P-256/passkey signature verification | -| ERC-8004 | Agent Identity Registry | ERC | Draft | Onchain identity, reputation, and validation for AI agents | +| Number | Name | Type | Status | +|--------|------|------|--------| +| ERC-20 | Token Standard | ERC | Final | +| ERC-165 | Interface Detection | ERC | Final | +| EIP-155 | Replay Protection (Chain ID) | EIP | Final | +| EIP-191 | Signed Data Standard | EIP | Final | +| ERC-721 | Non-Fungible Token | ERC | Final | +| ERC-1155 | Multi-Token | ERC | Final | +| ERC-1271 | Contract Signature Verification | ERC | Final | +| EIP-712 | Typed Structured Data Signing | EIP | Final | +| EIP-1014 | CREATE2 Deterministic Addresses | EIP | Final | +| EIP-1193 | JavaScript Provider API | EIP | Final | +| EIP-1559 | Fee Market (Base + Priority Fee) | EIP | Final | +| EIP-1822 | UUPS Proxy | EIP | Final | +| EIP-1967 | Proxy Storage Slots | EIP | Final | +| EIP-2098 | Compact 64-byte Signatures | EIP | Final | +| ERC-2612 | ERC-20 Permit (Gasless Approval) | ERC | Final | +| EIP-2718 | Typed Transaction Envelope | EIP | Final | +| EIP-2930 | Access Lists (Type 1 Tx) | EIP | Final | +| ERC-2981 | NFT Royalty Standard | ERC | Final | +| ERC-3009 | Transfer With Authorization | ERC | Final | +| EIP-3156 | Flash Loan Standard | EIP | Final | +| ERC-4337 | Account Abstraction (EntryPoint) | ERC | Final | +| EIP-4361 | Sign-In With Ethereum | EIP | Final | +| ERC-4626 | Tokenized Vault | ERC | Final | +| EIP-4844 | Blob Transactions (Proto-Danksharding) | EIP | Final | +| EIP-6093 | Custom Errors for Tokens | EIP | Final | +| EIP-6963 | Multi-Wallet Discovery | EIP | Final | +| EIP-7201 | Namespaced Storage Layout | EIP | Final | +| ERC-7579 | Modular Smart Accounts | ERC | Draft | +| EIP-7702 | EOA Delegation (Set Account Code) | EIP | Final | +| EIP-7951 | secp256r1 Precompile (Passkeys) | EIP | Final | +| ERC-8004 | Agent Identity Registry | ERC | Draft | + +Last verified: March 2026 + +## Related Skills + +- **eth-concepts** — EVM internals, gas mechanics, storage layout, transaction types +- **account-abstraction** — Full ERC-4337/EIP-7702/ERC-7579 implementation patterns +- **solidity-security** — Security patterns, CEI, reentrancy guards, access control +- **evm-nfts** — NFT minting, metadata, marketplace integration patterns +- **x402** — Agent payment protocol (integrates with ERC-8004) ## References @@ -631,21 +657,15 @@ See the `account-abstraction` skill for full implementation patterns. - [ERC-721](https://eips.ethereum.org/EIPS/eip-721) — Non-Fungible Token - [ERC-1155](https://eips.ethereum.org/EIPS/eip-1155) — Multi-Token - [ERC-4626](https://eips.ethereum.org/EIPS/eip-4626) — Tokenized Vault -- [EIP-191](https://eips.ethereum.org/EIPS/eip-191) — Signed Data Standard +- [ERC-2981](https://eips.ethereum.org/EIPS/eip-2981) — NFT Royalty Standard - [EIP-712](https://eips.ethereum.org/EIPS/eip-712) — Typed Structured Data -- [ERC-1271](https://eips.ethereum.org/EIPS/eip-1271) — Contract Signature Verification - [ERC-2612](https://eips.ethereum.org/EIPS/eip-2612) — Permit Extension - [ERC-4337](https://eips.ethereum.org/EIPS/eip-4337) — Account Abstraction -- [ERC-7579](https://eips.ethereum.org/EIPS/eip-7579) — Modular Smart Accounts - [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) — Fee Market Change - [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844) — Shard Blob Transactions -- [EIP-1967](https://eips.ethereum.org/EIPS/eip-1967) — Proxy Storage Slots -- [EIP-1822](https://eips.ethereum.org/EIPS/eip-1822) — UUPS -- [EIP-7201](https://eips.ethereum.org/EIPS/eip-7201) — Namespaced Storage - [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702) — Set EOA Account Code -- [ERC-8004: Agent Identity Registry](https://eips.ethereum.org/EIPS/eip-8004) — Onchain agent identity standard -- [ERC-3009: Transfer With Authorization](https://eips.ethereum.org/EIPS/eip-3009) — Gasless token transfers -- [EIP-7702: Set EOA Account Code](https://eips.ethereum.org/EIPS/eip-7702) — EOA delegation -- [EIP-7594: PeerDAS](https://eips.ethereum.org/EIPS/eip-7594) — Data availability sampling -- [EIP-7951: secp256r1 Precompile](https://eips.ethereum.org/EIPS/eip-7951) — Passkey support -- [OpenZeppelin Contracts](https://docs.openzeppelin.com/contracts) — Reference implementations +- [ERC-8004](https://eips.ethereum.org/EIPS/eip-8004) — Agent Identity Registry +- [EIP-6963](https://eips.ethereum.org/EIPS/eip-6963) — Multi-Wallet Discovery +- [EIP-7951](https://eips.ethereum.org/EIPS/eip-7951) — secp256r1 Precompile +- [OpenZeppelin Contracts v5](https://docs.openzeppelin.com/contracts/5.x/) — Reference implementations +- [Viem Documentation](https://viem.sh/) — TypeScript Ethereum library From 60abc7588d088d5d0146effeb3c2d281790c9c3f Mon Sep 17 00:00:00 2001 From: 0xinit <28729137+0xinit@users.noreply.github.com> Date: Wed, 4 Mar 2026 12:34:19 -0800 Subject: [PATCH 06/14] feat: add eip-reference examples --- .../examples/eip712-signing/README.md | 223 ++++++++++++++++++ .../examples/erc20-token/README.md | 161 +++++++++++++ .../examples/erc4626-vault/README.md | 197 ++++++++++++++++ .../examples/erc721-nft/README.md | 177 ++++++++++++++ 4 files changed, 758 insertions(+) create mode 100644 skills/eip-reference/examples/eip712-signing/README.md create mode 100644 skills/eip-reference/examples/erc20-token/README.md create mode 100644 skills/eip-reference/examples/erc4626-vault/README.md create mode 100644 skills/eip-reference/examples/erc721-nft/README.md diff --git a/skills/eip-reference/examples/eip712-signing/README.md b/skills/eip-reference/examples/eip712-signing/README.md new file mode 100644 index 0000000..5f25163 --- /dev/null +++ b/skills/eip-reference/examples/eip712-signing/README.md @@ -0,0 +1,223 @@ +# EIP-712 Typed Data Signing — Full Example + +End-to-end EIP-712 signing: Solidity verifier contract, viem signing on the frontend, and ERC-1271 support for smart contract wallets. + +## Solidity — Verifier Contract + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; + +/// @title OrderVerifier +/// @notice Verifies EIP-712 signed orders. Supports both EOA and ERC-1271 contract signers. +contract OrderVerifier is EIP712 { + bytes32 private constant ORDER_TYPEHASH = keccak256( + "Order(address maker,address token,uint256 amount,uint256 nonce,uint256 deadline)" + ); + + mapping(address maker => uint256 nonce) public nonces; + mapping(bytes32 orderHash => bool filled) public filledOrders; + + event OrderFilled(address indexed maker, address indexed token, uint256 amount, uint256 nonce); + + error OrderExpired(uint256 deadline, uint256 currentTime); + error InvalidNonce(uint256 expected, uint256 provided); + error InvalidSignature(); + error OrderAlreadyFilled(); + + struct Order { + address maker; + address token; + uint256 amount; + uint256 nonce; + uint256 deadline; + } + + constructor() EIP712("OrderVerifier", "1") {} + + /// @notice Fill a signed order after verifying the EIP-712 signature. + function fillOrder(Order calldata order, bytes calldata signature) external { + if (block.timestamp > order.deadline) { + revert OrderExpired(order.deadline, block.timestamp); + } + if (order.nonce != nonces[order.maker]) { + revert InvalidNonce(nonces[order.maker], order.nonce); + } + + bytes32 structHash = keccak256(abi.encode( + ORDER_TYPEHASH, + order.maker, + order.token, + order.amount, + order.nonce, + order.deadline + )); + bytes32 digest = _hashTypedDataV4(structHash); + + if (filledOrders[digest]) revert OrderAlreadyFilled(); + + if (!_isValidSignature(order.maker, digest, signature)) { + revert InvalidSignature(); + } + + filledOrders[digest] = true; + nonces[order.maker]++; + + emit OrderFilled(order.maker, order.token, order.amount, order.nonce); + } + + /// @dev Supports both EOA (ecrecover) and smart contract (ERC-1271) signers. + function _isValidSignature( + address signer, bytes32 digest, bytes memory signature + ) internal view returns (bool) { + if (signer.code.length > 0) { + try IERC1271(signer).isValidSignature(digest, signature) returns (bytes4 magicValue) { + return magicValue == IERC1271.isValidSignature.selector; + } catch { + return false; + } + } + return ECDSA.recover(digest, signature) == signer; + } +} +``` + +## TypeScript — Sign Order with viem + +```typescript +import { createWalletClient, createPublicClient, http, custom } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { sepolia } from 'viem/chains'; + +const VERIFIER = '0xDeployedVerifierAddress...' as const; + +const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); +const walletClient = createWalletClient({ + account, + chain: sepolia, + transport: http(process.env.RPC_URL), +}); +const publicClient = createPublicClient({ + chain: sepolia, + transport: http(process.env.RPC_URL), +}); + +// Fetch current nonce +const nonce = await publicClient.readContract({ + address: VERIFIER, + abi: orderVerifierAbi, + functionName: 'nonces', + args: [account.address], +}); + +const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600); + +// Sign the order +const signature = await walletClient.signTypedData({ + domain: { + name: 'OrderVerifier', + version: '1', + chainId: sepolia.id, + verifyingContract: VERIFIER, + }, + types: { + Order: [ + { name: 'maker', type: 'address' }, + { name: 'token', type: 'address' }, + { name: 'amount', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + ], + }, + primaryType: 'Order', + message: { + maker: account.address, + token: '0xTokenAddress...', + amount: 1000000000000000000n, // 1 token (18 decimals) + nonce, + deadline, + }, +}); + +console.log('Signature:', signature); +``` + +## TypeScript — Submit Signed Order + +```typescript +const order = { + maker: account.address, + token: '0xTokenAddress...' as `0x${string}`, + amount: 1000000000000000000n, + nonce, + deadline, +}; + +const fillHash = await walletClient.writeContract({ + address: VERIFIER, + abi: orderVerifierAbi, + functionName: 'fillOrder', + args: [order, signature], +}); + +const receipt = await publicClient.waitForTransactionReceipt({ hash: fillHash }); +if (receipt.status !== 'success') throw new Error('Fill failed'); +console.log('Order filled in tx:', fillHash); +``` + +## Browser Wallet — signTypedData with Injected Provider + +```typescript +import { createWalletClient, custom } from 'viem'; +import { mainnet } from 'viem/chains'; + +const walletClient = createWalletClient({ + chain: mainnet, + transport: custom(window.ethereum!), +}); + +const [address] = await walletClient.requestAddresses(); + +const signature = await walletClient.signTypedData({ + account: address, + domain: { + name: 'OrderVerifier', + version: '1', + chainId: 1, + verifyingContract: '0xMainnetVerifier...', + }, + types: { + Order: [ + { name: 'maker', type: 'address' }, + { name: 'token', type: 'address' }, + { name: 'amount', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + ], + }, + primaryType: 'Order', + message: { + maker: address, + token: '0xTokenAddress...', + amount: 1000000000000000000n, + nonce: 0n, + deadline: BigInt(Math.floor(Date.now() / 1000) + 3600), + }, +}); +``` + +## Key Points + +- OpenZeppelin's `EIP712` base contract handles domain separator caching and fork protection. +- `_hashTypedDataV4` computes the full `\x19\x01 || domainSeparator || structHash` digest. +- Never include `EIP712Domain` in the `types` object — viem derives it from `domain`. +- Always include `chainId` and `verifyingContract` in the domain to prevent replay. +- For smart contract wallets (Safe, ERC-4337 accounts), use ERC-1271 verification as fallback. +- Set `deadline` at least 30 minutes in the future to survive mempool delays. +- Nonces must be fetched immediately before signing to avoid mismatch. + +Last verified: March 2026 diff --git a/skills/eip-reference/examples/erc20-token/README.md b/skills/eip-reference/examples/erc20-token/README.md new file mode 100644 index 0000000..253e6a1 --- /dev/null +++ b/skills/eip-reference/examples/erc20-token/README.md @@ -0,0 +1,161 @@ +# ERC-20 Token — Deploy and Interact + +Minimal ERC-20 token with OpenZeppelin v5, Permit extension, and viem interaction. + +## Solidity — Token Contract + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +/// @title ExampleToken +/// @notice ERC-20 with permit (ERC-2612) and owner-gated minting. +contract ExampleToken is ERC20, ERC20Permit, Ownable { + uint256 public constant MAX_SUPPLY = 1_000_000_000e18; // 1B tokens + + error ExceedsMaxSupply(uint256 requested, uint256 available); + + constructor(address initialOwner) + ERC20("ExampleToken", "EXT") + ERC20Permit("ExampleToken") + Ownable(initialOwner) + { + _mint(initialOwner, 100_000_000e18); // 100M initial + } + + /// @notice Mint new tokens. Only callable by owner. + /// @param to Recipient address. + /// @param amount Amount in base units (18 decimals). + function mint(address to, uint256 amount) external onlyOwner { + if (totalSupply() + amount > MAX_SUPPLY) { + revert ExceedsMaxSupply(amount, MAX_SUPPLY - totalSupply()); + } + _mint(to, amount); + } +} +``` + +## TypeScript — Deploy with viem + +```typescript +import { createWalletClient, createPublicClient, http, parseEther } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { sepolia } from 'viem/chains'; + +const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); + +const walletClient = createWalletClient({ + account, + chain: sepolia, + transport: http(process.env.RPC_URL), +}); + +const publicClient = createPublicClient({ + chain: sepolia, + transport: http(process.env.RPC_URL), +}); + +const hash = await walletClient.deployContract({ + abi: ExampleTokenAbi, + bytecode: ExampleTokenBytecode, + args: [account.address], +}); + +const receipt = await publicClient.waitForTransactionReceipt({ hash }); +if (receipt.status !== 'success') throw new Error('Deploy failed'); +console.log('Token deployed at:', receipt.contractAddress); +``` + +## TypeScript — Transfer and Approve + +```typescript +import { erc20Abi, formatUnits, parseUnits } from 'viem'; + +const TOKEN = '0xDeployedTokenAddress...' as const; + +// Check balance +const balance = await publicClient.readContract({ + address: TOKEN, + abi: erc20Abi, + functionName: 'balanceOf', + args: [account.address], +}); +console.log('Balance:', formatUnits(balance, 18)); + +// Transfer tokens +const transferHash = await walletClient.writeContract({ + address: TOKEN, + abi: erc20Abi, + functionName: 'transfer', + args: ['0xRecipient...', parseUnits('1000', 18)], +}); + +// Approve spender (use forceApprove pattern for USDT compatibility) +const approveHash = await walletClient.writeContract({ + address: TOKEN, + abi: erc20Abi, + functionName: 'approve', + args: ['0xSpender...', parseUnits('5000', 18)], +}); +``` + +## TypeScript — Gasless Approval via Permit + +```typescript +const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600); // 1 hour + +const nonce = await publicClient.readContract({ + address: TOKEN, + abi: erc20PermitAbi, + functionName: 'nonces', + args: [account.address], +}); + +const signature = await walletClient.signTypedData({ + domain: { + name: 'ExampleToken', + version: '1', + chainId: sepolia.id, + verifyingContract: TOKEN, + }, + types: { + Permit: [ + { name: 'owner', type: 'address' }, + { name: 'spender', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + ], + }, + primaryType: 'Permit', + message: { + owner: account.address, + spender: '0xSpender...', + value: parseUnits('1000', 18), + nonce, + deadline, + }, +}); + +// Submit permit on-chain (can be sent by anyone — gasless for the token holder) +const { v, r, s } = parseSignature(signature); +const permitHash = await walletClient.writeContract({ + address: TOKEN, + abi: erc20PermitAbi, + functionName: 'permit', + args: [account.address, '0xSpender...', parseUnits('1000', 18), deadline, v, r, s], +}); +``` + +## Key Points + +- OpenZeppelin v5 `ERC20Permit` inherits `EIP712` — domain separator is auto-managed with fork protection. +- `parseUnits`/`formatUnits` from viem handle decimal conversion. Never use JavaScript `Number` for token amounts. +- Always check `receipt.status` after transactions — reverted txs still return a receipt. +- For USDT compatibility, approve to 0 before setting a new nonzero allowance. + +Last verified: March 2026 diff --git a/skills/eip-reference/examples/erc4626-vault/README.md b/skills/eip-reference/examples/erc4626-vault/README.md new file mode 100644 index 0000000..67bb8a1 --- /dev/null +++ b/skills/eip-reference/examples/erc4626-vault/README.md @@ -0,0 +1,197 @@ +# ERC-4626 Tokenized Vault — Build and Interact + +Yield-bearing vault with OpenZeppelin v5, first-depositor protection, and viem interaction. + +## Solidity — Vault Contract + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {ERC4626} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + +/// @title SimpleVault +/// @notice ERC-4626 vault accepting a single ERC-20 asset. +/// @dev OpenZeppelin v5 ERC4626 includes virtual shares/assets offset +/// to mitigate first-depositor inflation attacks by default. +contract SimpleVault is ERC4626 { + using Math for uint256; + + error DepositTooSmall(uint256 deposited, uint256 minimum); + + uint256 public constant MIN_DEPOSIT = 1000; // Minimum first deposit in asset units + + constructor(IERC20 asset_) + ERC4626(asset_) + ERC20("Simple Vault Shares", "svSHARE") + {} + + /// @notice Deposit assets, receive vault shares. + /// @dev Overrides to enforce minimum deposit on first deposit. + function deposit(uint256 assets, address receiver) public override returns (uint256) { + if (totalSupply() == 0 && assets < MIN_DEPOSIT) { + revert DepositTooSmall(assets, MIN_DEPOSIT); + } + return super.deposit(assets, receiver); + } + + /// @notice View the underlying asset balance held by the vault. + function totalAssets() public view override returns (uint256) { + return IERC20(asset()).balanceOf(address(this)); + } +} +``` + +## Solidity — Vault with Custom Yield Source + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {ERC4626} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +/// @title YieldVault +/// @notice ERC-4626 vault that deploys assets to an external yield source. +contract YieldVault is ERC4626 { + using SafeERC20 for IERC20; + + address public immutable yieldSource; + uint256 private _totalDeposited; + + constructor(IERC20 asset_, address yieldSource_) + ERC4626(asset_) + ERC20("Yield Vault Shares", "yvSHARE") + { + yieldSource = yieldSource_; + } + + function totalAssets() public view override returns (uint256) { + // Assets in vault + assets deployed to yield source + return IERC20(asset()).balanceOf(address(this)) + _deployedAssets(); + } + + function _deployedAssets() internal view returns (uint256) { + // Query the external yield source for deposited balance + return IYieldSource(yieldSource).balanceOf(address(this)); + } + + function _afterDeposit(uint256 assets) internal { + // Deploy received assets to yield source + IERC20(asset()).forceApprove(yieldSource, assets); + IYieldSource(yieldSource).deposit(assets); + } +} + +interface IYieldSource { + function deposit(uint256 amount) external; + function withdraw(uint256 amount) external; + function balanceOf(address account) external view returns (uint256); +} +``` + +## TypeScript — Deposit and Withdraw with viem + +```typescript +import { createPublicClient, createWalletClient, http, parseUnits, formatUnits } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { sepolia } from 'viem/chains'; +import { erc20Abi, erc4626Abi } from 'viem'; + +const VAULT = '0xDeployedVaultAddress...' as const; +const ASSET = '0xUnderlyingAssetAddress...' as const; + +const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); +const publicClient = createPublicClient({ chain: sepolia, transport: http(process.env.RPC_URL) }); +const walletClient = createWalletClient({ account, chain: sepolia, transport: http(process.env.RPC_URL) }); + +// Step 1: Approve vault to spend assets +const depositAmount = parseUnits('1000', 18); + +await walletClient.writeContract({ + address: ASSET, + abi: erc20Abi, + functionName: 'approve', + args: [VAULT, depositAmount], +}); + +// Step 2: Deposit assets, receive shares +const depositHash = await walletClient.writeContract({ + address: VAULT, + abi: erc4626Abi, + functionName: 'deposit', + args: [depositAmount, account.address], +}); + +const depositReceipt = await publicClient.waitForTransactionReceipt({ hash: depositHash }); +if (depositReceipt.status !== 'success') throw new Error('Deposit failed'); +``` + +## TypeScript — Preview and Redeem + +```typescript +// Preview: how many assets will I get for my shares? +const myShares = await publicClient.readContract({ + address: VAULT, + abi: erc4626Abi, + functionName: 'balanceOf', + args: [account.address], +}); + +const assetsOut = await publicClient.readContract({ + address: VAULT, + abi: erc4626Abi, + functionName: 'previewRedeem', + args: [myShares], +}); +console.log('Shares:', formatUnits(myShares, 18)); +console.log('Assets on redeem:', formatUnits(assetsOut, 18)); + +// Redeem all shares for assets +const redeemHash = await walletClient.writeContract({ + address: VAULT, + abi: erc4626Abi, + functionName: 'redeem', + args: [myShares, account.address, account.address], +}); + +const redeemReceipt = await publicClient.waitForTransactionReceipt({ hash: redeemHash }); +if (redeemReceipt.status !== 'success') throw new Error('Redeem failed'); +``` + +## TypeScript — Check Exchange Rate + +```typescript +const oneShare = parseUnits('1', 18); +const assetsPerShare = await publicClient.readContract({ + address: VAULT, + abi: erc4626Abi, + functionName: 'convertToAssets', + args: [oneShare], +}); +console.log('1 share =', formatUnits(assetsPerShare, 18), 'assets'); + +const oneAsset = parseUnits('1', 18); +const sharesPerAsset = await publicClient.readContract({ + address: VAULT, + abi: erc4626Abi, + functionName: 'convertToShares', + args: [oneAsset], +}); +console.log('1 asset =', formatUnits(sharesPerAsset, 18), 'shares'); +``` + +## Key Points + +- **Rounding**: `convertToShares` rounds DOWN (depositor gets fewer shares). `previewMint` and `previewWithdraw` round UP (caller pays more). This protects the vault. +- **First-depositor attack**: OpenZeppelin v5 includes a virtual offset (`_decimalsOffset()`) by default. This makes the attack economically infeasible. +- **deposit vs mint**: `deposit(assets, receiver)` specifies assets in, returns shares received. `mint(shares, receiver)` specifies shares wanted, returns assets spent. Same for `withdraw` vs `redeem`. +- **preview vs convert**: `preview*` returns exact amounts for the corresponding operation. `convert*` returns a theoretical rate without accounting for fees or limits. +- The vault itself is an ERC-20 token — shares can be transferred, approved, and used in other DeFi protocols. + +Last verified: March 2026 diff --git a/skills/eip-reference/examples/erc721-nft/README.md b/skills/eip-reference/examples/erc721-nft/README.md new file mode 100644 index 0000000..fa06e68 --- /dev/null +++ b/skills/eip-reference/examples/erc721-nft/README.md @@ -0,0 +1,177 @@ +# ERC-721 NFT — Mint, Transfer, and Query + +ERC-721 NFT contract with OpenZeppelin v5, on-chain metadata, royalties, and viem interaction. + +## Solidity — NFT Contract + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import {ERC2981} from "@openzeppelin/contracts/token/common/ERC2981.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; + +/// @title ExampleNFT +/// @notice ERC-721 with sequential minting, royalties (ERC-2981), and configurable base URI. +contract ExampleNFT is ERC721, ERC2981, Ownable { + using Strings for uint256; + + uint256 private _nextTokenId; + uint256 public constant MAX_SUPPLY = 10_000; + uint256 public constant MINT_PRICE = 0.01 ether; + string private _baseTokenURI; + + error MaxSupplyReached(); + error InsufficientPayment(uint256 sent, uint256 required); + error WithdrawFailed(); + + constructor(address initialOwner, string memory baseURI, address royaltyReceiver) + ERC721("ExampleNFT", "ENFT") + Ownable(initialOwner) + { + _baseTokenURI = baseURI; + // 5% royalty (500 basis points) + _setDefaultRoyalty(royaltyReceiver, 500); + } + + /// @notice Public mint. One token per call. + function mint() external payable returns (uint256) { + if (_nextTokenId >= MAX_SUPPLY) revert MaxSupplyReached(); + if (msg.value < MINT_PRICE) revert InsufficientPayment(msg.value, MINT_PRICE); + + uint256 tokenId = _nextTokenId++; + _safeMint(msg.sender, tokenId); + return tokenId; + } + + /// @notice Owner-only batch mint for airdrops. + function mintBatch(address to, uint256 count) external onlyOwner { + for (uint256 i; i < count; ++i) { + if (_nextTokenId >= MAX_SUPPLY) revert MaxSupplyReached(); + uint256 tokenId = _nextTokenId++; + _safeMint(to, tokenId); + } + } + + function withdraw() external onlyOwner { + (bool ok,) = msg.sender.call{value: address(this).balance}(""); + if (!ok) revert WithdrawFailed(); + } + + function _baseURI() internal view override returns (string memory) { + return _baseTokenURI; + } + + function setBaseURI(string calldata newBaseURI) external onlyOwner { + _baseTokenURI = newBaseURI; + } + + // ERC-165: declare support for ERC-721 + ERC-2981 + function supportsInterface(bytes4 interfaceId) + public view override(ERC721, ERC2981) returns (bool) + { + return super.supportsInterface(interfaceId); + } +} +``` + +## TypeScript — Mint and Query with viem + +```typescript +import { createPublicClient, createWalletClient, http, parseEther } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { sepolia } from 'viem/chains'; + +const NFT_ADDRESS = '0xDeployedNFTAddress...' as const; + +const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); +const publicClient = createPublicClient({ chain: sepolia, transport: http(process.env.RPC_URL) }); +const walletClient = createWalletClient({ account, chain: sepolia, transport: http(process.env.RPC_URL) }); + +// Mint an NFT +const mintHash = await walletClient.writeContract({ + address: NFT_ADDRESS, + abi: exampleNftAbi, + functionName: 'mint', + value: parseEther('0.01'), +}); + +const receipt = await publicClient.waitForTransactionReceipt({ hash: mintHash }); +if (receipt.status !== 'success') throw new Error('Mint failed'); + +// Parse Transfer event to get tokenId +const transferLog = receipt.logs.find( + (log) => log.topics[0] === '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef' +); +const tokenId = BigInt(transferLog!.topics[3]!); +console.log('Minted tokenId:', tokenId); +``` + +## TypeScript — Safe Transfer and Approval + +```typescript +// Check ownership +const owner = await publicClient.readContract({ + address: NFT_ADDRESS, + abi: exampleNftAbi, + functionName: 'ownerOf', + args: [tokenId], +}); + +// Approve a specific address for one token +await walletClient.writeContract({ + address: NFT_ADDRESS, + abi: exampleNftAbi, + functionName: 'approve', + args: ['0xApproved...', tokenId], +}); + +// Safe transfer (calls onERC721Received on contract recipients) +await walletClient.writeContract({ + address: NFT_ADDRESS, + abi: exampleNftAbi, + functionName: 'safeTransferFrom', + args: [account.address, '0xRecipient...', tokenId], +}); +``` + +## TypeScript — Query Royalty Info + +```typescript +const [receiver, royaltyAmount] = await publicClient.readContract({ + address: NFT_ADDRESS, + abi: exampleNftAbi, + functionName: 'royaltyInfo', + args: [tokenId, parseEther('1')], // For a 1 ETH sale +}); +// receiver = royalty recipient address +// royaltyAmount = 0.05 ETH (5% of 1 ETH) +``` + +## Receiving Contract — IERC721Receiver + +If your contract needs to receive ERC-721 tokens via `safeTransferFrom`: + +```solidity +import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; + +contract NFTVault is IERC721Receiver { + function onERC721Received( + address, address, uint256, bytes calldata + ) external pure returns (bytes4) { + return IERC721Receiver.onERC721Received.selector; + } +} +``` + +## Key Points + +- Use `_safeMint` (not `_mint`) to trigger receiver checks on contract recipients. +- `supportsInterface` must be overridden when inheriting multiple ERC-165 contracts. +- `ownerOf` reverts for nonexistent tokens — wrap in try/catch when querying. +- Approval clears on transfer. `setApprovalForAll` (operator) persists. +- ERC-2981 royalties are advisory — enforcement depends on marketplace implementation. + +Last verified: March 2026 From 4ee69098472a788bc00e5d0875fbd723928f0f76 Mon Sep 17 00:00:00 2001 From: 0xinit <28729137+0xinit@users.noreply.github.com> Date: Wed, 4 Mar 2026 13:02:55 -0800 Subject: [PATCH 07/14] feat: add eip-reference docs and resources --- skills/eip-reference/docs/troubleshooting.md | 131 +++++------- skills/eip-reference/resources/chain-ids.md | 92 +++++++++ .../resources/interface-signatures.md | 186 ++++++++++++++++++ .../eip-reference/resources/quick-lookup.md | 90 +++++++++ 4 files changed, 414 insertions(+), 85 deletions(-) create mode 100644 skills/eip-reference/resources/chain-ids.md create mode 100644 skills/eip-reference/resources/interface-signatures.md create mode 100644 skills/eip-reference/resources/quick-lookup.md diff --git a/skills/eip-reference/docs/troubleshooting.md b/skills/eip-reference/docs/troubleshooting.md index 80a70dd..8bf9b92 100644 --- a/skills/eip-reference/docs/troubleshooting.md +++ b/skills/eip-reference/docs/troubleshooting.md @@ -6,7 +6,7 @@ Common issues when implementing Ethereum standards, with root causes and fixes. **Symptom:** Spender drains more than intended when allowance is updated from N to M. -**Root cause:** Spender front-runs the `approve(M)` call, spending the existing allowance N, then spends the new allowance M, totaling N+M. +**Root cause:** Spender front-runs the `approve(M)` call, spending existing allowance N, then spends the new allowance M (total: N+M). **Fix:** ```solidity @@ -14,30 +14,29 @@ Common issues when implementing Ethereum standards, with root causes and fixes. token.approve(spender, 0); token.approve(spender, newAmount); -// Option 2: Use increaseAllowance/decreaseAllowance (OpenZeppelin) -token.increaseAllowance(spender, additionalAmount); +// Option 2: Use SafeERC20 (OpenZeppelin v5) +SafeERC20.forceApprove(token, spender, newAmount); -// Option 3: Use ERC-2612 permit (gasless, single-use nonce) +// Option 3: Use ERC-2612 permit (gasless, nonce-protected) token.permit(owner, spender, value, deadline, v, r, s); ``` ## ERC-20: USDT Nonzero-to-Nonzero Approve Reverts -**Symptom:** `approve` call reverts with no error message on USDT. +**Symptom:** `approve` reverts silently on USDT. -**Root cause:** USDT's implementation requires allowance to be 0 before setting a new nonzero value. +**Root cause:** USDT requires allowance to be 0 before setting a new nonzero value. -**Fix:** Always set allowance to 0 before approving a new amount. OpenZeppelin `SafeERC20.forceApprove` handles this automatically. +**Fix:** Use `SafeERC20.forceApprove` which handles the zero-first pattern automatically. ## ERC-721: safeTransferFrom Reverts on Contract Recipient -**Symptom:** `safeTransferFrom` reverts when sending to a contract address. +**Symptom:** `safeTransferFrom` reverts when sending to a contract. -**Root cause:** The receiving contract does not implement `IERC721Receiver` or returns the wrong selector from `onERC721Received`. +**Root cause:** Receiving contract missing `IERC721Receiver` or returns wrong selector. **Fix:** ```solidity -// Receiving contract must implement: function onERC721Received( address operator, address from, uint256 tokenId, bytes calldata data ) external returns (bytes4) { @@ -45,17 +44,16 @@ function onERC721Received( } ``` -If the receiving contract intentionally should not accept NFTs, this is working as designed. If you must send to a contract that cannot be modified, use `transferFrom` instead — but the token may be permanently locked. +If the receiving contract cannot be modified, use `transferFrom` instead — but the token may be permanently locked. ## EIP-712: Domain Separator Mismatch Across Chains -**Symptom:** Signature verification fails after deploying the same contract on a different chain, or after a chain fork. +**Symptom:** Signature verification fails after deploying on a different chain or after a fork. -**Root cause:** The domain separator was computed at deploy time with the original `chainId`. On another chain, `block.chainid` differs and the digest changes. +**Root cause:** Domain separator was computed at deploy time with the original `chainId`. **Fix:** ```solidity -// Recompute domain separator if chainId changed (fork protection) function _domainSeparator() internal view returns (bytes32) { if (block.chainid == _cachedChainId) { return _cachedDomainSeparator; @@ -64,125 +62,88 @@ function _domainSeparator() internal view returns (bytes32) { } ``` -OpenZeppelin's `EIP712` base contract does this automatically. If you are hand-rolling EIP-712, always check `block.chainid` before using a cached separator. +OpenZeppelin's `EIP712` base contract handles this automatically. -## EIP-712: Signature Valid on Mainnet but Fails on Testnet +## EIP-712: Frontend chainId Mismatch -**Symptom:** `signTypedData` works on mainnet but verification fails on Sepolia (or vice versa). +**Symptom:** `signTypedData` works on mainnet but verification fails on testnet. -**Root cause:** The `chainId` in the domain passed to `signTypedData` on the frontend does not match the chain the contract is deployed on. - -**Fix:** Ensure the frontend reads `chainId` from the connected wallet, not from a hardcoded value: +**Root cause:** Hardcoded `chainId` in the frontend domain object. +**Fix:** ```typescript -import { getChainId } from "viem/actions"; - -const chainId = await getChainId(publicClient); -// Use this chainId in the domain object +const chainId = await publicClient.getChainId(); +// Use this dynamic chainId in the domain, not a hardcoded value ``` -## ERC-4337: UserOp Simulation Failure - -**Symptom:** Bundler rejects UserOperation with `AA** error` codes. - -**Common error codes:** -| Code | Meaning | Fix | -|------|---------|-----| -| `AA10` | Sender already constructed | Remove `initCode` — account already deployed | -| `AA13` | initCode failed or returned wrong address | Check factory `createAccount` uses CREATE2 with correct salt | -| `AA21` | Insufficient stake/deposit | Fund account via `entryPoint.depositTo` | -| `AA23` | Reverted during validation | `validateUserOp` is reverting instead of returning `SIG_VALIDATION_FAILED` | -| `AA25` | Invalid nonce | Query current nonce with `entryPoint.getNonce(sender, key)` | -| `AA31` | Paymaster deposit too low | Top up paymaster deposit on EntryPoint | -| `AA33` | Paymaster validation reverted | Check `validatePaymasterUserOp` logic | -| `AA34` | Paymaster validation out of gas | Increase `paymasterVerificationGasLimit` | -| `AA40` | Over verificationGasLimit | Increase `verificationGasLimit` | -| `AA41` | Over paymasterVerificationGasLimit | Increase paymaster gas limit in `paymasterAndData` | -| `AA51` | Prefund below actualGasCost | Increase `preVerificationGas` or pre-fund account | - -## ERC-2612: Permit Signature Expired - -**Symptom:** `permit` call reverts with `"ERC2612: expired deadline"`. - -**Root cause:** The `deadline` timestamp in the signed permit is in the past by the time the transaction is mined. - -**Fix:** -- Set `deadline` far enough in the future to survive mempool delays. 30 minutes is common, 1 hour is safe. -- For immediate use: `deadline = block.timestamp + 1800` (30 minutes). -- Never set `deadline = block.timestamp` on the frontend — by the time the tx mines, it has already expired. - ## ERC-2612: Permit Nonce Mismatch -**Symptom:** `permit` call reverts with `"ERC2612: invalid signature"` even though the signature looks correct. +**Symptom:** `permit` reverts with "invalid signature" despite correct signing. -**Root cause:** The nonce used when signing does not match the current on-chain nonce. This happens when: -1. A previous permit was submitted but not yet mined (nonce is stale) -2. The nonce was fetched from a stale RPC response -3. Another transaction incremented the nonce between signing and submission +**Root cause:** Nonce fetched before another transaction incremented it. **Fix:** ```typescript -// Always fetch nonce immediately before signing const nonce = await publicClient.readContract({ address: tokenAddress, abi: erc20PermitAbi, - functionName: "nonces", + functionName: 'nonces', args: [ownerAddress], }); -// Use this nonce in the signTypedData call +// Sign immediately after fetching — do not cache ``` ## ERC-2612: Permit Front-Running -**Symptom:** `permit` call reverts even though the signature is valid. +**Symptom:** `permit` reverts even though the signature is valid. -**Root cause:** Someone else submitted the permit signature first (front-ran). The nonce was incremented, so the original `permit` call sees a used nonce. +**Root cause:** Someone submitted the permit signature first, incrementing the nonce. -**Fix:** Check allowance before calling `permit`. If allowance is already set, skip the permit call: +**Fix:** ```solidity if (token.allowance(owner, spender) < amount) { token.permit(owner, spender, amount, deadline, v, r, s); } ``` -## ERC-165: Interface ID Calculation Errors +## ERC-4337: UserOp Simulation Failure -**Symptom:** `supportsInterface` returns `false` for an interface the contract implements. +**Common error codes:** + +| Code | Meaning | Fix | +|------|---------|-----| +| `AA10` | Sender already constructed | Remove `initCode` | +| `AA21` | Insufficient deposit | Fund via `entryPoint.depositTo` | +| `AA23` | Validation reverted | Return `SIG_VALIDATION_FAILED`, do not revert | +| `AA25` | Invalid nonce | Query `entryPoint.getNonce(sender, key)` | +| `AA31` | Paymaster deposit too low | Top up paymaster on EntryPoint | +| `AA33` | Paymaster validation reverted | Check `validatePaymasterUserOp` logic | -**Root cause:** The interface ID was calculated incorrectly. The ID is the XOR of all function selectors in the interface — NOT including inherited functions. +## ERC-4626: First-Depositor Share Inflation -**Fix:** -```solidity -// Correct: XOR of selectors in IERC721 only (not IERC165 selectors) -bytes4 constant IERC721_ID = 0x80ac58cd; +**Symptom:** Second depositor receives 0 shares despite depositing significant assets. -// Verify in Foundry -bytes4 id = type(IERC721).interfaceId; -assertEq(id, 0x80ac58cd); -``` +**Root cause:** Attacker deposits 1 wei, donates tokens directly to vault, inflating share price beyond the second depositor's amount. -Common mistake: including `supportsInterface` itself in the XOR. `supportsInterface` belongs to ERC-165 (`0x01ffc9a7`), not to the derived interface. +**Fix:** Use OpenZeppelin's `ERC4626` which includes virtual shares/assets offset by default in v5. -## EIP-1967: Proxy Not Detected by Block Explorer +## EIP-1967: Proxy Not Detected by Explorer -**Symptom:** Etherscan shows the proxy's own ABI (fallback, receive) instead of the implementation's ABI. +**Symptom:** Etherscan shows proxy ABI instead of implementation ABI. -**Root cause:** The implementation address is not stored in the standard EIP-1967 slot (`0x360894a...`), or the proxy does not emit the standard `Upgraded(address)` event. +**Root cause:** Implementation not stored in standard EIP-1967 slot or missing `Upgraded` event. -**Fix:** Ensure the proxy writes to the standard slot and emits the event: +**Fix:** Ensure standard slot and event emission: ```solidity event Upgraded(address indexed implementation); - bytes32 constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; ``` -After deployment, verify on Etherscan by clicking "Is this a proxy?" → "Verify" → "Save". The explorer reads the EIP-1967 slot to detect the implementation. - ## References - [EIP-20](https://eips.ethereum.org/EIPS/eip-20) — Token Standard - [EIP-712](https://eips.ethereum.org/EIPS/eip-712) — Typed Structured Data - [ERC-2612](https://eips.ethereum.org/EIPS/eip-2612) — Permit Extension - [ERC-4337](https://eips.ethereum.org/EIPS/eip-4337) — Account Abstraction -- [EIP-165](https://eips.ethereum.org/EIPS/eip-165) — Interface Detection +- [ERC-4626](https://eips.ethereum.org/EIPS/eip-4626) — Tokenized Vault - [EIP-1967](https://eips.ethereum.org/EIPS/eip-1967) — Proxy Storage Slots diff --git a/skills/eip-reference/resources/chain-ids.md b/skills/eip-reference/resources/chain-ids.md new file mode 100644 index 0000000..1b4e7a7 --- /dev/null +++ b/skills/eip-reference/resources/chain-ids.md @@ -0,0 +1,92 @@ +# EIP-155 Chain IDs Reference + +Common chain IDs per EIP-155 for transaction replay protection and multi-chain development. + +Last verified: March 2026 + +## Mainnet Chains + +| Chain | Chain ID | Currency | Type | +|-------|----------|----------|------| +| Ethereum Mainnet | 1 | ETH | L1 | +| Polygon PoS | 137 | POL | L1 | +| BNB Smart Chain | 56 | BNB | L1 | +| Avalanche C-Chain | 43114 | AVAX | L1 | +| Fantom Opera | 250 | FTM | L1 | +| Gnosis Chain | 100 | xDAI | L1 | +| Monad | 143 | MON | L1 | + +## L2 / Rollup Chains + +| Chain | Chain ID | Currency | Type | +|-------|----------|----------|------| +| Arbitrum One | 42161 | ETH | L2 (Optimistic) | +| Arbitrum Nova | 42170 | ETH | L2 (AnyTrust) | +| Optimism | 10 | ETH | L2 (Optimistic) | +| Base | 8453 | ETH | L2 (Optimistic) | +| Scroll | 534352 | ETH | L2 (zkRollup) | +| zkSync Era | 324 | ETH | L2 (zkRollup) | +| Polygon zkEVM | 1101 | ETH | L2 (zkRollup) | +| Linea | 59144 | ETH | L2 (zkRollup) | +| Blast | 81457 | ETH | L2 (Optimistic) | +| MegaETH | 6342 | ETH | L2 | + +## Testnets + +| Chain | Chain ID | Currency | Mainnet | +|-------|----------|----------|---------| +| Sepolia | 11155111 | ETH | Ethereum | +| Holesky | 17000 | ETH | Ethereum | +| Arbitrum Sepolia | 421614 | ETH | Arbitrum | +| Optimism Sepolia | 11155420 | ETH | Optimism | +| Base Sepolia | 84532 | ETH | Base | +| Polygon Amoy | 80002 | POL | Polygon | + +## Usage in viem + +```typescript +import { mainnet, arbitrum, optimism, base, sepolia } from 'viem/chains'; + +// Chain IDs are available as properties +console.log(mainnet.id); // 1 +console.log(arbitrum.id); // 42161 +console.log(base.id); // 8453 +console.log(sepolia.id); // 11155111 +``` + +## Usage in EIP-712 Domain + +```solidity +// Always use block.chainid for dynamic fork protection +bytes32 DOMAIN_SEPARATOR = keccak256(abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes("MyProtocol")), + keccak256(bytes("1")), + block.chainid, // Dynamic — correct on any chain/fork + address(this) +)); +``` + +## How to Add a Custom Chain in viem + +```typescript +import { defineChain } from 'viem'; + +const myChain = defineChain({ + id: 99999, + name: 'My Chain', + nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, + rpcUrls: { + default: { http: ['https://rpc.mychain.io'] }, + }, + blockExplorers: { + default: { name: 'Explorer', url: 'https://explorer.mychain.io' }, + }, +}); +``` + +## References + +- [EIP-155](https://eips.ethereum.org/EIPS/eip-155) — Simple Replay Attack Protection +- [chainlist.org](https://chainlist.org) — Community-maintained chain registry +- [Viem Chains](https://viem.sh/docs/chains/introduction) — Built-in chain definitions diff --git a/skills/eip-reference/resources/interface-signatures.md b/skills/eip-reference/resources/interface-signatures.md new file mode 100644 index 0000000..b0f7f36 --- /dev/null +++ b/skills/eip-reference/resources/interface-signatures.md @@ -0,0 +1,186 @@ +# Interface Signatures — Copy-Paste Reference + +Solidity interfaces for the most-used EIPs. Copy directly into your contracts. + +Last verified: March 2026 + +## IERC20 + +```solidity +interface IERC20 { + function totalSupply() external view returns (uint256); + function balanceOf(address account) external view returns (uint256); + function transfer(address to, uint256 amount) external returns (bool); + function allowance(address owner, address spender) external view returns (uint256); + function approve(address spender, uint256 amount) external returns (bool); + function transferFrom(address from, address to, uint256 amount) external returns (bool); + + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); +} +``` + +## IERC20Metadata + +```solidity +interface IERC20Metadata is IERC20 { + function name() external view returns (string memory); + function symbol() external view returns (string memory); + function decimals() external view returns (uint8); +} +``` + +## IERC20Permit (ERC-2612) + +```solidity +interface IERC20Permit { + function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external; + function nonces(address owner) external view returns (uint256); + function DOMAIN_SEPARATOR() external view returns (bytes32); +} +``` + +## IERC721 + +```solidity +interface IERC721 is IERC165 { + function balanceOf(address owner) external view returns (uint256); + function ownerOf(uint256 tokenId) external view returns (address); + function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external; + function safeTransferFrom(address from, address to, uint256 tokenId) external; + function transferFrom(address from, address to, uint256 tokenId) external; + function approve(address to, uint256 tokenId) external; + function setApprovalForAll(address operator, bool approved) external; + function getApproved(uint256 tokenId) external view returns (address); + function isApprovedForAll(address owner, address operator) external view returns (bool); + + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); + event ApprovalForAll(address indexed owner, address indexed operator, bool approved); +} +``` + +## IERC721Receiver + +```solidity +interface IERC721Receiver { + function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) external returns (bytes4); +} +``` + +## IERC1155 + +```solidity +interface IERC1155 is IERC165 { + function balanceOf(address account, uint256 id) external view returns (uint256); + function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids) external view returns (uint256[] memory); + function setApprovalForAll(address operator, bool approved) external; + function isApprovedForAll(address account, address operator) external view returns (bool); + function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes calldata data) external; + function safeBatchTransferFrom(address from, address to, uint256[] calldata ids, uint256[] calldata amounts, bytes calldata data) external; + + event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value); + event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values); +} +``` + +## IERC4626 + +```solidity +interface IERC4626 is IERC20 { + function asset() external view returns (address); + function totalAssets() external view returns (uint256); + function convertToShares(uint256 assets) external view returns (uint256); + function convertToAssets(uint256 shares) external view returns (uint256); + function deposit(uint256 assets, address receiver) external returns (uint256 shares); + function mint(uint256 shares, address receiver) external returns (uint256 assets); + function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares); + function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets); + function maxDeposit(address receiver) external view returns (uint256); + function maxMint(address receiver) external view returns (uint256); + function maxWithdraw(address owner) external view returns (uint256); + function maxRedeem(address owner) external view returns (uint256); + function previewDeposit(uint256 assets) external view returns (uint256); + function previewMint(uint256 shares) external view returns (uint256); + function previewWithdraw(uint256 assets) external view returns (uint256); + function previewRedeem(uint256 shares) external view returns (uint256); + + event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares); + event Withdraw(address indexed sender, address indexed receiver, address indexed owner, uint256 assets, uint256 shares); +} +``` + +## IERC2981 (Royalties) + +```solidity +interface IERC2981 is IERC165 { + function royaltyInfo(uint256 tokenId, uint256 salePrice) external view returns (address receiver, uint256 royaltyAmount); +} +``` + +## IERC165 + +```solidity +interface IERC165 { + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} +``` + +## IERC1271 (Contract Signatures) + +```solidity +interface IERC1271 { + function isValidSignature(bytes32 hash, bytes memory signature) external view returns (bytes4 magicValue); + // Must return 0x1626ba7e if valid +} +``` + +## IAccount (ERC-4337) + +```solidity +interface IAccount { + function validateUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash, + uint256 missingAccountFunds + ) external returns (uint256 validationData); +} +``` + +## IPaymaster (ERC-4337) + +```solidity +interface IPaymaster { + function validatePaymasterUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash, + uint256 maxCost + ) external returns (bytes memory context, uint256 validationData); + + function postOp( + PostOpMode mode, + bytes calldata context, + uint256 actualGasCost, + uint256 actualUserOpFeePerGas + ) external; +} +``` + +## IERC3156FlashLender + +```solidity +interface IERC3156FlashLender { + function maxFlashLoan(address token) external view returns (uint256); + function flashFee(address token, uint256 amount) external view returns (uint256); + function flashLoan(IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data) external returns (bool); +} +``` + +## IERC3156FlashBorrower + +```solidity +interface IERC3156FlashBorrower { + function onFlashLoan(address initiator, address token, uint256 amount, uint256 fee, bytes calldata data) external returns (bytes32); + // Must return keccak256("ERC3156FlashBorrower.onFlashLoan") +} +``` diff --git a/skills/eip-reference/resources/quick-lookup.md b/skills/eip-reference/resources/quick-lookup.md new file mode 100644 index 0000000..916f130 --- /dev/null +++ b/skills/eip-reference/resources/quick-lookup.md @@ -0,0 +1,90 @@ +# EIP / ERC Quick Lookup Table + +Complete reference table for the most important Ethereum standards. Sorted by number. + +Last verified: March 2026 + +| Number | Name | Type | Category | Status | +|--------|------|------|----------|--------| +| ERC-20 | Fungible Token Standard | ERC | Token | Final | +| ERC-137 | Ethereum Name Service (ENS) | ERC | Naming | Final | +| EIP-155 | Replay Protection (Chain ID) | EIP | Protocol | Final | +| ERC-165 | Standard Interface Detection | ERC | Detection | Final | +| ERC-173 | Contract Ownership Standard | ERC | Access | Final | +| EIP-191 | Signed Data Standard | EIP | Signature | Final | +| ERC-721 | Non-Fungible Token Standard | ERC | Token | Final | +| ERC-777 | Token Standard (advanced) | ERC | Token | Final | +| ERC-1155 | Multi-Token Standard | ERC | Token | Final | +| ERC-1271 | Contract Signature Validation | ERC | Signature | Final | +| EIP-712 | Typed Structured Data Signing | EIP | Signature | Final | +| EIP-1014 | CREATE2 Deterministic Addresses | EIP | Protocol | Final | +| EIP-1153 | Transient Storage (TSTORE/TLOAD) | EIP | EVM | Final | +| EIP-1193 | JavaScript Provider API | EIP | Interface | Final | +| EIP-1559 | Fee Market (Base + Priority Fee) | EIP | Protocol | Final | +| EIP-1820 | Pseudo-Introspection Registry | EIP | Detection | Final | +| EIP-1822 | UUPS Proxy Standard | EIP | Proxy | Final | +| EIP-1967 | Standard Proxy Storage Slots | EIP | Proxy | Final | +| EIP-2098 | Compact 64-byte Signatures | EIP | Signature | Final | +| ERC-2309 | Consecutive Transfer Extension | ERC | Token | Final | +| ERC-2612 | ERC-20 Permit (Gasless Approval) | ERC | Token | Final | +| EIP-2718 | Typed Transaction Envelope | EIP | Protocol | Final | +| EIP-2771 | Meta-Transactions (Trusted Forwarder) | EIP | Protocol | Final | +| EIP-2929 | Gas Cost Increases for State Access | EIP | Protocol | Final | +| EIP-2930 | Access Lists (Type 1 Tx) | EIP | Protocol | Final | +| EIP-2935 | Historical Block Hashes in State | EIP | Protocol | Final | +| EIP-2537 | BLS12-381 Precompile | EIP | Protocol | Final | +| ERC-2981 | NFT Royalty Standard | ERC | Token | Final | +| ERC-3009 | Transfer With Authorization | ERC | Token | Final | +| EIP-3156 | Flash Loan Standard | EIP | DeFi | Final | +| ERC-3525 | Semi-Fungible Token | ERC | Token | Final | +| EIP-4361 | Sign-In With Ethereum (SIWE) | EIP | Auth | Final | +| ERC-4337 | Account Abstraction (EntryPoint) | ERC | Account | Final | +| ERC-4494 | ERC-721 Permit | ERC | Token | Draft | +| ERC-4626 | Tokenized Vault Standard | ERC | DeFi | Final | +| EIP-4844 | Blob Transactions (Proto-Danksharding) | EIP | Protocol | Final | +| ERC-5192 | Soulbound Token (Non-Transferable) | ERC | Token | Final | +| ERC-5267 | EIP-712 Domain Retrieval | ERC | Signature | Final | +| ERC-5805 | Voting with Delegation (Governor) | ERC | Governance | Final | +| EIP-6093 | Custom Errors for Tokens | EIP | Token | Final | +| EIP-6780 | SELFDESTRUCT Restriction | EIP | Protocol | Final | +| ERC-6900 | Modular Smart Accounts v1 | ERC | Account | Draft | +| EIP-6963 | Multi-Wallet Discovery | EIP | Interface | Final | +| EIP-7201 | Namespaced Storage Layout | EIP | Proxy | Final | +| ERC-7579 | Modular Smart Accounts v2 | ERC | Account | Draft | +| EIP-7594 | PeerDAS (Data Availability Sampling) | EIP | Protocol | Final | +| EIP-7685 | Execution Layer Requests | EIP | Protocol | Final | +| EIP-7702 | Set EOA Account Code (Delegation) | EIP | Account | Final | +| EIP-7710 | Smart Account Delegation | EIP | Account | Draft | +| EIP-7825 | Transaction Gas Cap | EIP | Protocol | Final | +| EIP-7935 | Gas Limit 60M | EIP | Protocol | Final | +| EIP-7951 | secp256r1 Precompile (Passkeys) | EIP | Protocol | Final | +| ERC-8004 | Agent Identity Registry | ERC | AI | Draft | + +## Category Key + +| Category | Description | +|----------|-------------| +| Token | Token standards (fungible, non-fungible, multi-token) | +| Signature | Signing, verification, and permit standards | +| Protocol | Core Ethereum protocol changes | +| Proxy | Upgradeable contract patterns | +| Account | Smart account and account abstraction | +| DeFi | DeFi primitives (vaults, flash loans) | +| Detection | Interface and capability detection | +| Interface | JavaScript/provider APIs | +| Auth | Authentication standards | +| Governance | Voting and governance | +| AI | AI agent standards | +| EVM | EVM-level changes | +| Access | Ownership and access control | +| Naming | Name resolution | + +## Status Key + +| Status | Meaning | +|--------|---------| +| Final | Accepted and immutable. Safe for production. | +| Draft | Active development. May change. | +| Review | Under formal review. Likely to finalize. | +| Stagnant | No activity for 6+ months. | +| Withdrawn | Explicitly abandoned. | From dacec33945f2229d0061b0b846e65b7418a75b6f Mon Sep 17 00:00:00 2001 From: 0xinit <28729137+0xinit@users.noreply.github.com> Date: Wed, 4 Mar 2026 13:18:27 -0800 Subject: [PATCH 08/14] feat: add eip-reference starter template --- .../eip-reference/templates/erc20-token.sol | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 skills/eip-reference/templates/erc20-token.sol diff --git a/skills/eip-reference/templates/erc20-token.sol b/skills/eip-reference/templates/erc20-token.sol new file mode 100644 index 0000000..396230c --- /dev/null +++ b/skills/eip-reference/templates/erc20-token.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; +import {ERC20Burnable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +/// @title MyToken +/// @author Your Name +/// @notice ERC-20 token with Permit (ERC-2612), burn, and capped supply. +/// @dev Built on OpenZeppelin v5. Permit enables gasless approvals via EIP-712 signatures. +/// The EIP712 domain separator is auto-managed with fork protection. +contract MyToken is ERC20, ERC20Permit, ERC20Burnable, Ownable { + /// @notice Maximum token supply in base units (18 decimals). + uint256 public constant MAX_SUPPLY = 1_000_000_000e18; // 1 billion + + /// @notice Thrown when a mint would exceed MAX_SUPPLY. + /// @param requested Amount requested to mint. + /// @param available Remaining mintable supply. + error ExceedsMaxSupply(uint256 requested, uint256 available); + + /// @param initialOwner Address receiving ownership and initial mint. + constructor(address initialOwner) + ERC20("MyToken", "MTK") + ERC20Permit("MyToken") + Ownable(initialOwner) + { + // Mint 10% of max supply to the initial owner + _mint(initialOwner, 100_000_000e18); + } + + /// @notice Mint new tokens. Restricted to contract owner. + /// @param to Recipient of the minted tokens. + /// @param amount Amount to mint in base units (18 decimals). + function mint(address to, uint256 amount) external onlyOwner { + if (totalSupply() + amount > MAX_SUPPLY) { + revert ExceedsMaxSupply(amount, MAX_SUPPLY - totalSupply()); + } + _mint(to, amount); + } +} + +// Deployment (Foundry): +// forge create src/MyToken.sol:MyToken \ +// --constructor-args 0xYourAddress \ +// --rpc-url $RPC_URL \ +// --private-key $PRIVATE_KEY \ +// --verify --etherscan-api-key $ETHERSCAN_KEY + +// Deployment (Hardhat): +// const token = await ethers.deployContract("MyToken", [owner.address]); +// await token.waitForDeployment(); + +// Installation: +// forge install OpenZeppelin/openzeppelin-contracts +// # or +// npm install @openzeppelin/contracts@^5.0.0 + +// Last verified: March 2026 +// OpenZeppelin Contracts v5.x From d6557d501bff71b1e62ce0daabf6628da5c1deb2 Mon Sep 17 00:00:00 2001 From: 0xinit <28729137+0xinit@users.noreply.github.com> Date: Wed, 4 Mar 2026 14:05:11 -0800 Subject: [PATCH 09/14] feat: add solana-simd skill --- skills/solana-simd/SKILL.md | 675 ++++++++++++++++++++++++++++++++++++ 1 file changed, 675 insertions(+) create mode 100644 skills/solana-simd/SKILL.md diff --git a/skills/solana-simd/SKILL.md b/skills/solana-simd/SKILL.md new file mode 100644 index 0000000..73ffe4d --- /dev/null +++ b/skills/solana-simd/SKILL.md @@ -0,0 +1,675 @@ +--- +name: solana-simd +description: "Solana Improvement Documents reference — accounts model, PDAs, CPIs, Token Extensions (Token-2022), priority fees (SIMD-0096), address lookup tables, versioned transactions, rent mechanics, and key SIMD proposals (0033 fee markets, 0047 syscall, 0096 priority fees, 0172 staking)." +license: Apache-2.0 +metadata: + author: 0xinit + version: "1.0" + chain: solana + category: Infrastructure +tags: + - solana + - simd + - improvement-proposals + - pda + - cpi + - token-extensions + - priority-fees + - anchor +--- + +# Solana Improvement Documents (SIMD) Reference + +Solana Improvement Documents (SIMDs) are the formal mechanism for proposing changes to the Solana protocol. They cover everything from fee market restructuring to new syscalls, staking changes, and token standards. This skill covers the SIMDs that matter most for developers, along with the core Solana primitives (accounts, PDAs, CPIs, rent) that every program depends on. + +## What You Probably Got Wrong + +> AI models trained on Ethereum or outdated Solana docs produce broken code. Fix these assumptions first. + +- **Solana programs are stateless — state lives in accounts, not contracts** — There is no contract storage. Programs are executable code only. All mutable state is stored in separate data accounts that programs read and write via instruction inputs. If you write Solana code like Solidity (expecting `self.balances[user]`), it will not compile. + +- **PDA bumps MUST be canonical** — `Pubkey::find_program_address` returns the canonical bump (highest valid bump 255..0). Always store and reuse this bump. Never call `create_program_address` with an arbitrary bump — it may produce a valid pubkey on the ed25519 curve, meaning it has a private key and is not a PDA. Anchor's `seeds` constraint handles this automatically. + +- **`invoke_signed` is NOT the same as `invoke`** — `invoke` passes through existing signers from the transaction. `invoke_signed` lets a PDA sign by providing the seeds + bump. Use `invoke` when a human wallet signs. Use `invoke_signed` when your program's PDA needs to authorize a CPI call. + +- **Rent exemption is not optional in practice** — Accounts below the rent-exempt minimum are garbage collected. Since SIMD-0084 (epoch-based rent collection removal), all new accounts must be rent-exempt at creation. The minimum is ~0.00089088 SOL per byte. Always calculate: `Rent::get()?.minimum_balance(data_len)`. + +- **Token-2022 is NOT a drop-in replacement for SPL Token** — Programs that hardcode the SPL Token program ID (`TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA`) will reject Token-2022 tokens (`TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb`). You must check both program IDs or use the `spl_token_2022::check_spl_token_program_account` helper. + +- **Priority fees are per compute unit, not per transaction** — `ComputeBudgetProgram.setComputeUnitPrice({ microLamports })` sets the price PER compute unit. Total priority fee = `microLamports * compute_units_consumed / 1_000_000`. Overpaying happens when you set high `microLamports` without reducing the compute unit limit. + +- **Account data size is fixed at creation** — You cannot resize an account after creation (unless you use `realloc`). Plan your data layout carefully. `realloc` has constraints: max 10KB increase per instruction, and the account must be owned by the calling program. + +- **CPI depth limit is 4, not unlimited** — Program A calls B calls C calls D — that is depth 4, the maximum. Design your program architecture accordingly. Each CPI level also reduces available compute units. + +## How to Look Up Any SIMD + +When a user asks about ANY SIMD — even ones not covered in this skill — fetch the full spec on demand. + +### Step 1: Find the filename + +SIMD filenames include a slug (e.g., `0096-reward-collected-priority-fee-in-entirety.md`), so you can't construct the URL from just the number. First, list the proposals directory and find the matching file: + +``` +WebFetch: https://api.github.com/repos/solana-foundation/solana-improvement-documents/contents/proposals +Prompt: "Find the filename that starts with {NNNN} (zero-padded to 4 digits)" +``` + +Examples: SIMD-96 → look for `0096-*`, SIMD-33 → look for `0033-*` + +### Step 2: Fetch the raw spec + +Once you have the full filename: + +``` +WebFetch: https://raw.githubusercontent.com/solana-foundation/solana-improvement-documents/main/proposals/{full-filename} +``` + +Examples: +- SIMD-0096 → `https://raw.githubusercontent.com/solana-foundation/solana-improvement-documents/main/proposals/0096-reward-collected-priority-fee-in-entirety.md` +- SIMD-0033 → `https://raw.githubusercontent.com/solana-foundation/solana-improvement-documents/main/proposals/0033-timely-vote-credits.md` + +### Step 3: Parse and summarize + +The fetched markdown has YAML frontmatter (`simd`, `title`, `authors`, `category`, `type`, `status`, `created`) followed by sections: Summary, Motivation, New Terminology, Detailed Design, Impact, Security Considerations. + +Extract and present: title, status, what it changes, implementation details, and impact on validators/developers. + +### Alternative methods + +```bash +# GitHub CLI — list all proposals and grep for the number +gh api repos/solana-foundation/solana-improvement-documents/contents/proposals --jq '.[].name' | grep "^{NNNN}" + +# Then fetch the matched file +gh api repos/solana-foundation/solana-improvement-documents/contents/proposals/{matched-filename} --jq '.content' | base64 -d +``` + +### Sources + +| Source | URL | Best for | +|--------|-----|----------| +| GitHub repo | https://github.com/solana-foundation/solana-improvement-documents | Raw specs, PR discussions, git blame | +| Solana Forum | https://forum.solana.com/c/simd/5 | Community discussion, context, rationale | + +- Raw specs: `https://raw.githubusercontent.com/solana-foundation/solana-improvement-documents/main/proposals/{filename}` +- All proposals: https://github.com/solana-foundation/solana-improvement-documents/tree/main/proposals +- Forum discussions: https://forum.solana.com/c/simd/5 + +## SIMD Status Lifecycle + +``` +Draft -> Review -> Accepted -> Implemented -> Activated + -> Rejected + -> Withdrawn +``` + +- **Draft**: Initial proposal, open for feedback +- **Review**: Under formal review by core contributors +- **Accepted**: Approved for implementation +- **Implemented**: Code merged, awaiting feature gate activation +- **Activated**: Live on mainnet-beta +- **Rejected/Withdrawn**: Not proceeding + +## Core Solana Concepts + +### Accounts Model + +Everything on Solana is an account. Programs, token mints, user wallets, PDAs — all accounts with different configurations. + +| Field | Type | Description | +|-------|------|-------------| +| `lamports` | `u64` | Balance in lamports (1 SOL = 1e9 lamports) | +| `data` | `Vec` | Arbitrary byte array, max 10 MB | +| `owner` | `Pubkey` | Program that owns this account (can write to `data`) | +| `executable` | `bool` | Whether this account is a program | +| `rent_epoch` | `u64` | Deprecated since SIMD-0084 | + +Key rules: +- Only the **owner program** can modify an account's `data` and debit `lamports` +- Any program can **credit** lamports to any account +- Account data size is set at creation and requires `realloc` to change +- The System Program owns all wallet accounts (no data, just lamports) + +### Programs + +Programs are stateless executable accounts. They process instructions that reference other accounts. + +```rust +use anchor_lang::prelude::*; + +declare_id!("YourProgramID11111111111111111111111111111"); + +#[program] +pub mod my_program { + use super::*; + + pub fn initialize(ctx: Context, data: u64) -> Result<()> { + let account = &mut ctx.accounts.my_account; + account.data = data; + account.authority = ctx.accounts.authority.key(); + Ok(()) + } +} + +#[derive(Accounts)] +pub struct Initialize<'info> { + #[account( + init, + payer = authority, + space = 8 + MyAccount::INIT_SPACE + )] + pub my_account: Account<'info, MyAccount>, + #[account(mut)] + pub authority: Signer<'info>, + pub system_program: Program<'info, System>, +} + +#[account] +#[derive(InitSpace)] +pub struct MyAccount { + pub data: u64, + pub authority: Pubkey, +} +``` + +### PDAs (Program Derived Addresses) + +PDAs are deterministic addresses derived from seeds and a program ID. They are NOT on the ed25519 curve, so no private key exists — only the deriving program can sign for them. + +#### Derivation + +```rust +// On-chain: find canonical PDA +let (pda, bump) = Pubkey::find_program_address( + &[b"vault", user.key().as_ref()], + ctx.program_id, +); + +// Verify PDA matches expected account +require_keys_eq!(pda, ctx.accounts.vault.key()); +``` + +```typescript +// Client-side: derive the same PDA +import { PublicKey } from "@solana/web3.js"; + +const [pda, bump] = PublicKey.findProgramAddressSync( + [Buffer.from("vault"), userPublicKey.toBuffer()], + programId +); +``` + +#### Anchor PDA Constraints + +```rust +#[derive(Accounts)] +pub struct WithdrawFromVault<'info> { + #[account( + mut, + seeds = [b"vault", authority.key().as_ref()], + bump, + // Anchor derives the PDA, verifies it matches, stores the bump + )] + pub vault: Account<'info, Vault>, + pub authority: Signer<'info>, +} +``` + +#### Canonical Bump Rule + +```rust +// CORRECT: Use find_program_address (tries bumps 255 down to 0) +let (pda, bump) = Pubkey::find_program_address(&seeds, &program_id); + +// WRONG: Never use create_program_address with a guess +// This may produce a valid ed25519 pubkey (has private key = NOT a PDA) +let pda = Pubkey::create_program_address(&[&seeds, &[arbitrary_bump]], &program_id); +``` + +### CPIs (Cross-Program Invocations) + +#### `invoke` — Pass-Through Signing + +```rust +use anchor_lang::prelude::*; +use anchor_spl::token::{self, Transfer, Token, TokenAccount}; + +pub fn transfer_tokens(ctx: Context, amount: u64) -> Result<()> { + let cpi_accounts = Transfer { + from: ctx.accounts.source.to_account_info(), + to: ctx.accounts.destination.to_account_info(), + authority: ctx.accounts.authority.to_account_info(), + }; + let cpi_program = ctx.accounts.token_program.to_account_info(); + let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts); + token::transfer(cpi_ctx, amount)?; + Ok(()) +} +``` + +#### `invoke_signed` — PDA Signing + +```rust +pub fn transfer_from_vault(ctx: Context, amount: u64) -> Result<()> { + let seeds = &[ + b"vault", + ctx.accounts.authority.key.as_ref(), + &[ctx.accounts.vault.bump], + ]; + let signer_seeds = &[&seeds[..]]; + + let cpi_accounts = Transfer { + from: ctx.accounts.vault_token.to_account_info(), + to: ctx.accounts.destination.to_account_info(), + authority: ctx.accounts.vault.to_account_info(), + }; + let cpi_ctx = CpiContext::new_with_signer( + ctx.accounts.token_program.to_account_info(), + cpi_accounts, + signer_seeds, + ); + token::transfer(cpi_ctx, amount)?; + Ok(()) +} +``` + +### Rent + +Since SIMD-0084, all accounts must be rent-exempt at creation. The minimum balance is proportional to the account's data size. + +```rust +// Calculate rent-exempt minimum +let rent = Rent::get()?; +let lamports = rent.minimum_balance(data_len); +``` + +| Data Size | Rent-Exempt Minimum | +|-----------|-------------------| +| 0 bytes (wallet) | 0.00089088 SOL | +| 165 bytes (token account) | 0.00203928 SOL | +| 82 bytes (mint) | 0.00144768 SOL | +| 200 bytes | 0.00227616 SOL | +| 1,000 bytes | 0.00795168 SOL | +| 10,000 bytes | 0.07228128 SOL | + +Reclaiming rent: close the account and transfer lamports back. + +```rust +// Anchor: close an account and reclaim rent +#[derive(Accounts)] +pub struct CloseVault<'info> { + #[account( + mut, + close = authority, + seeds = [b"vault", authority.key().as_ref()], + bump, + )] + pub vault: Account<'info, Vault>, + #[account(mut)] + pub authority: Signer<'info>, +} +``` + +## Key SIMDs Deep-Dive + +### SIMD-0033 — Timely Vote Credits + +**Status**: Activated + +Validators earn more vote credits for voting quickly. A vote landing within 2 slots of the voted-on slot earns more credits than one landing 32 slots later. This incentivizes low-latency validator infrastructure and faster consensus convergence. + +**Developer impact**: None directly. Validators with better infrastructure earn more staking rewards, affecting APY calculations for staking protocols. + +### SIMD-0046 — Versioned Transactions + +**Status**: Activated + +Introduced `VersionedTransaction` and Address Lookup Tables (ALTs). Legacy transactions are limited to 35 accounts (1232-byte size limit). V0 transactions reference ALTs to compress account lists, supporting 256+ accounts per transaction. + +```typescript +import { + Connection, + VersionedTransaction, + TransactionMessage, + AddressLookupTableProgram, + PublicKey, +} from "@solana/web3.js"; + +// Fetch an existing lookup table +const lookupTableAddress = new PublicKey("..."); +const lookupTableAccount = await connection + .getAddressLookupTable(lookupTableAddress) + .then((res) => res.value); + +if (!lookupTableAccount) throw new Error("Lookup table not found"); + +// Build a V0 transaction using the lookup table +const messageV0 = new TransactionMessage({ + payerKey: payer.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions, +}).compileToV0Message([lookupTableAccount]); + +const tx = new VersionedTransaction(messageV0); +tx.sign([payer]); +const sig = await connection.sendTransaction(tx); +``` + +### SIMD-0047 — Syscall Probing + +**Status**: Accepted + +Allows programs to query whether a syscall is available before calling it. Enables forward-compatible programs that can use new features when available and fall back gracefully when they are not. Important for programs deployed across multiple clusters at different feature activation stages. + +### SIMD-0096 — Reward Full Priority Fee to Validator + +**Status**: Activated + +100% of priority fees go to the block-producing validator (previously 50% was burned). This aligns validator incentives and reduces off-protocol fee arrangements. Developers still set priority fees the same way, but the economic distribution changed. + +```typescript +import { ComputeBudgetProgram } from "@solana/web3.js"; + +// Set compute unit limit (reduces wasted compute budget) +const setLimitIx = ComputeBudgetProgram.setComputeUnitLimit({ + units: 200_000, +}); + +// Set priority fee in micro-lamports per compute unit +const setPriceIx = ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: 50_000, // 50,000 micro-lamports per CU +}); + +// Total priority fee = 200,000 CU * 50,000 microLamports / 1,000,000 = 10,000 lamports +// = 0.00001 SOL +``` + +### SIMD-0172 — Staking Rewards Distribution + +**Status**: Accepted + +Changes how staking rewards are distributed. Instead of bulk reward distribution at epoch boundaries (which causes slot-level congestion and delayed reward visibility), rewards are distributed incrementally within epochs. This smooths validator economics and reduces epoch-boundary performance impact. + +## Token Extensions (Token-2022) + +Token-2022 (`TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb`) extends SPL Token with built-in features that previously required custom programs. + +### Key Extensions + +| Extension | Description | Use Case | +|-----------|-------------|----------| +| Transfer Fees | Percentage fee on every transfer | Protocol revenue, tax tokens | +| Confidential Transfers | Encrypted balances and amounts | Privacy-preserving payments | +| Interest-Bearing | Accumulating display balance | Yield-bearing stablecoins | +| Permanent Delegate | Authority that can transfer/burn any holder's tokens | Regulated assets, compliance | +| Non-Transferable | Soulbound tokens | Credentials, reputation | +| Transfer Hook | Custom program invoked on every transfer | Royalties, compliance checks | +| Metadata Pointer | On-chain metadata directly in mint | Eliminates Metaplex dependency | +| Group/Member | Hierarchical token relationships | Collections, token sets | + +### Creating a Token-2022 Mint with Transfer Fees + +```typescript +import { + Connection, + Keypair, + SystemProgram, + Transaction, +} from "@solana/web3.js"; +import { + TOKEN_2022_PROGRAM_ID, + createInitializeMintInstruction, + createInitializeTransferFeeConfigInstruction, + getMintLen, + ExtensionType, +} from "@solana/spl-token"; + +async function createMintWithTransferFee( + connection: Connection, + payer: Keypair, + mintAuthority: Keypair, + feeBasisPoints: number, // e.g., 100 = 1% + maxFee: bigint // max fee in token base units +): Promise { + const mintKeypair = Keypair.generate(); + + const extensions = [ExtensionType.TransferFeeConfig]; + const mintLen = getMintLen(extensions); + const lamports = await connection.getMinimumBalanceForRentExemption(mintLen); + + const tx = new Transaction().add( + SystemProgram.createAccount({ + fromPubkey: payer.publicKey, + newAccountPubkey: mintKeypair.publicKey, + space: mintLen, + lamports, + programId: TOKEN_2022_PROGRAM_ID, + }), + createInitializeTransferFeeConfigInstruction( + mintKeypair.publicKey, + mintAuthority.publicKey, // transferFeeConfigAuthority + mintAuthority.publicKey, // withdrawWithheldAuthority + feeBasisPoints, + maxFee, + TOKEN_2022_PROGRAM_ID + ), + createInitializeMintInstruction( + mintKeypair.publicKey, + 9, // decimals + mintAuthority.publicKey, + null, // freezeAuthority + TOKEN_2022_PROGRAM_ID + ) + ); + + await connection.sendTransaction(tx, [payer, mintKeypair]); + return mintKeypair; +} +``` + +### Checking Token Program Compatibility + +```rust +use anchor_lang::prelude::*; + +// Accept both SPL Token and Token-2022 +pub fn check_token_program(token_program: &AccountInfo) -> Result<()> { + let key = token_program.key(); + require!( + key == spl_token::id() || key == spl_token_2022::id(), + ErrorCode::InvalidTokenProgram + ); + Ok(()) +} +``` + +## Versioned Transactions and Address Lookup Tables + +### Creating an Address Lookup Table + +```typescript +import { + Connection, + Keypair, + AddressLookupTableProgram, + PublicKey, +} from "@solana/web3.js"; + +async function createLookupTable( + connection: Connection, + payer: Keypair, + addresses: PublicKey[] +): Promise { + const slot = await connection.getSlot(); + + // Step 1: Create the lookup table + const [createIx, lookupTableAddress] = + AddressLookupTableProgram.createLookupTable({ + authority: payer.publicKey, + payer: payer.publicKey, + recentSlot: slot, + }); + + // Step 2: Extend with addresses (max 30 per instruction) + const extendIx = AddressLookupTableProgram.extendLookupTable({ + payer: payer.publicKey, + authority: payer.publicKey, + lookupTable: lookupTableAddress, + addresses, + }); + + // Send both in one transaction + const tx = new Transaction().add(createIx, extendIx); + await connection.sendTransaction(tx, [payer]); + + return lookupTableAddress; +} +``` + +### When to Use ALTs + +- Transactions referencing more than ~20 unique accounts +- DeFi aggregators routing through multiple pools +- Batch operations touching many token accounts +- Any transaction hitting the 1232-byte size limit + +## Priority Fees in Practice + +### Estimating Priority Fees + +```typescript +import { Connection } from "@solana/web3.js"; + +async function estimatePriorityFee( + connection: Connection, + accountKeys: string[] +): Promise { + const fees = await connection.getRecentPrioritizationFees({ + lockedWritableAccounts: accountKeys.map( + (key) => new PublicKey(key) + ), + }); + + if (fees.length === 0) return 0; + + // Sort by fee, take the median for a balanced estimate + const sorted = fees + .map((f) => f.prioritizationFee) + .sort((a, b) => a - b); + const median = sorted[Math.floor(sorted.length / 2)]; + + return median; +} +``` + +### Setting Priority Fees on a Transaction + +```typescript +import { + Connection, + Transaction, + ComputeBudgetProgram, +} from "@solana/web3.js"; + +function addPriorityFee( + tx: Transaction, + computeUnits: number, + microLamportsPerCU: number +): Transaction { + // Always set BOTH compute unit limit and price + tx.add( + ComputeBudgetProgram.setComputeUnitLimit({ units: computeUnits }), + ComputeBudgetProgram.setComputeUnitPrice({ microLamports: microLamportsPerCU }) + ); + return tx; +} + +// Cost calculation: +// priority_fee_lamports = computeUnits * microLamportsPerCU / 1_000_000 +// Example: 200,000 CU * 50,000 microLamports = 10,000 lamports = 0.00001 SOL +``` + +### Priority Fee Tiers (March 2026 Estimates) + +| Tier | microLamports/CU | Use Case | +|------|------------------|----------| +| Low | 1,000 - 10,000 | Non-urgent transfers | +| Medium | 10,000 - 100,000 | Normal DeFi operations | +| High | 100,000 - 1,000,000 | Time-sensitive swaps | +| Urgent | 1,000,000+ | NFT mints, liquidations | + +These vary significantly by network congestion. Always use `getRecentPrioritizationFees` for real-time estimates. + +## Key Constants + +| Parameter | Value | +|-----------|-------| +| Slot time | ~400ms | +| Epoch length | ~2 days (~432,000 slots) | +| Max transaction size | 1,232 bytes | +| Max accounts per tx (legacy) | ~35 | +| Max accounts per tx (v0 + ALT) | 256+ | +| Max CPI depth | 4 | +| Compute unit limit (default) | 200,000 per instruction | +| Compute unit limit (max) | 1,400,000 per transaction | +| Min rent per byte | ~0.00089 SOL | +| Max account data | 10 MB | +| Max `realloc` increase per ix | 10,240 bytes (10 KB) | +| Lamports per SOL | 1,000,000,000 | + +## Network Endpoints + +| Network | RPC | WebSocket | +|---------|-----|-----------| +| Mainnet | `https://api.mainnet-beta.solana.com` | `wss://api.mainnet-beta.solana.com` | +| Devnet | `https://api.devnet.solana.com` | `wss://api.devnet.solana.com` | +| Testnet | `https://api.testnet.solana.com` | `wss://api.testnet.solana.com` | + +Public endpoints are rate-limited. Use Helius, QuickNode, Triton, or Alchemy for production. + +## Quick SIMD Lookup + +| SIMD | Title | Status | +|------|-------|--------| +| 0002 | Fee-payer signed first | Activated | +| 0033 | Timely Vote Credits | Activated | +| 0046 | Versioned Transactions | Activated | +| 0047 | Syscall Probing | Accepted | +| 0048 | Native Program Upgrades | Activated | +| 0072 | Priority Fee Market | Activated | +| 0083 | Token Extensions (Token-2022) | Activated | +| 0084 | Remove Rent Collection | Activated | +| 0096 | Reward Full Priority Fee to Validator | Activated | +| 0105 | QUIC Protocol for TPU | Activated | +| 0118 | Partitioned Epoch Rewards | Activated | +| 0133 | Increase Account Data Limit | Review | +| 0148 | Token Metadata in Token-2022 | Activated | +| 0159 | Reduce Rent Cost | Draft | +| 0163 | Multiple Delegations per Account | Review | +| 0172 | Staking Rewards Distribution | Accepted | +| 0175 | Confidential Transfers v2 | Review | +| 0185 | Vote Account Size Reduction | Draft | +| 0186 | Precompile for Secp256r1 | Activated | +| 0193 | ZK Token Proof Program | Review | + +Full list: https://github.com/solana-foundation/solana-improvement-documents/tree/main/proposals + +## Related Skills + +- **solana-agent-kit** — Solana Agent Kit for building AI agents that interact with Solana +- **jupiter** — Jupiter aggregator for token swaps and DCA +- **drift** — Drift Protocol for perpetuals and margin trading + +## References + +- SIMD Repository: https://github.com/solana-foundation/solana-improvement-documents +- Solana Docs: https://docs.solana.com +- Anchor Framework: https://www.anchor-lang.com +- SPL Token Docs: https://spl.solana.com/token +- Token-2022 Docs: https://spl.solana.com/token-2022 +- Solana Cookbook: https://solanacookbook.com +- Compute Budget: https://docs.solana.com/developing/programming-model/runtime#compute-budget +- Address Lookup Tables: https://docs.solana.com/developing/lookup-tables + +Last verified: 2026-03-01 From 2f460915079027bf87bea80dbf445fd10d83c5e4 Mon Sep 17 00:00:00 2001 From: 0xinit <28729137+0xinit@users.noreply.github.com> Date: Wed, 4 Mar 2026 14:28:44 -0800 Subject: [PATCH 10/14] feat: add solana-simd examples --- .../solana-simd/examples/cpi-invoke/README.md | 273 +++++++++++++++ .../examples/pda-derivation/README.md | 237 +++++++++++++ .../examples/priority-fees/README.md | 239 +++++++++++++ .../examples/token-extensions/README.md | 320 ++++++++++++++++++ 4 files changed, 1069 insertions(+) create mode 100644 skills/solana-simd/examples/cpi-invoke/README.md create mode 100644 skills/solana-simd/examples/pda-derivation/README.md create mode 100644 skills/solana-simd/examples/priority-fees/README.md create mode 100644 skills/solana-simd/examples/token-extensions/README.md diff --git a/skills/solana-simd/examples/cpi-invoke/README.md b/skills/solana-simd/examples/cpi-invoke/README.md new file mode 100644 index 0000000..1e2d69e --- /dev/null +++ b/skills/solana-simd/examples/cpi-invoke/README.md @@ -0,0 +1,273 @@ +# Cross-Program Invocation (CPI) + +Demonstrates `invoke` (pass-through signing) and `invoke_signed` (PDA signing) patterns with Anchor. Covers SPL Token transfers, system program calls, and multi-level CPI chains. + +## Anchor — Token Transfer via CPI (User Signs) + +When a user's wallet authorizes the transfer, use a standard CPI. The signer privilege passes through from the transaction. + +```rust +use anchor_lang::prelude::*; +use anchor_spl::token::{self, Token, TokenAccount, Transfer}; + +declare_id!("CpiDemo1111111111111111111111111111111111111"); + +#[program] +pub mod cpi_demo { + use super::*; + + pub fn transfer_user_tokens( + ctx: Context, + amount: u64, + ) -> Result<()> { + let cpi_accounts = Transfer { + from: ctx.accounts.user_token.to_account_info(), + to: ctx.accounts.destination_token.to_account_info(), + authority: ctx.accounts.user.to_account_info(), + }; + let cpi_ctx = CpiContext::new( + ctx.accounts.token_program.to_account_info(), + cpi_accounts, + ); + token::transfer(cpi_ctx, amount)?; + Ok(()) + } +} + +#[derive(Accounts)] +pub struct TransferUserTokens<'info> { + #[account( + mut, + token::authority = user, + )] + pub user_token: Account<'info, TokenAccount>, + #[account(mut)] + pub destination_token: Account<'info, TokenAccount>, + pub user: Signer<'info>, + pub token_program: Program<'info, Token>, +} +``` + +## Anchor — PDA-Signed Token Transfer (invoke_signed) + +When a PDA-controlled vault needs to transfer tokens, the program signs on behalf of the PDA using `invoke_signed` (Anchor wraps this with `CpiContext::new_with_signer`). + +```rust +use anchor_lang::prelude::*; +use anchor_spl::token::{self, Token, TokenAccount, Mint, Transfer}; + +declare_id!("VaultCPI111111111111111111111111111111111111"); + +#[program] +pub mod vault_cpi { + use super::*; + + pub fn initialize_vault(ctx: Context) -> Result<()> { + let vault = &mut ctx.accounts.vault; + vault.authority = ctx.accounts.authority.key(); + vault.bump = ctx.bumps.vault; + vault.token_bump = ctx.bumps.vault_token; + Ok(()) + } + + pub fn withdraw_from_vault( + ctx: Context, + amount: u64, + ) -> Result<()> { + // PDA signs the token transfer + let authority_key = ctx.accounts.authority.key(); + let seeds = &[ + b"vault".as_ref(), + authority_key.as_ref(), + &[ctx.accounts.vault.bump], + ]; + let signer_seeds = &[&seeds[..]]; + + let cpi_accounts = Transfer { + from: ctx.accounts.vault_token.to_account_info(), + to: ctx.accounts.user_token.to_account_info(), + authority: ctx.accounts.vault.to_account_info(), + }; + let cpi_ctx = CpiContext::new_with_signer( + ctx.accounts.token_program.to_account_info(), + cpi_accounts, + signer_seeds, + ); + token::transfer(cpi_ctx, amount)?; + Ok(()) + } +} + +#[derive(Accounts)] +pub struct InitializeVault<'info> { + #[account( + init, + payer = authority, + space = 8 + VaultState::INIT_SPACE, + seeds = [b"vault", authority.key().as_ref()], + bump, + )] + pub vault: Account<'info, VaultState>, + #[account( + init, + payer = authority, + token::mint = mint, + token::authority = vault, + seeds = [b"vault_token", authority.key().as_ref()], + bump, + )] + pub vault_token: Account<'info, TokenAccount>, + pub mint: Account<'info, Mint>, + #[account(mut)] + pub authority: Signer<'info>, + pub token_program: Program<'info, Token>, + pub system_program: Program<'info, System>, + pub rent: Sysvar<'info, Rent>, +} + +#[derive(Accounts)] +pub struct WithdrawFromVault<'info> { + #[account( + mut, + seeds = [b"vault", authority.key().as_ref()], + bump = vault.bump, + has_one = authority, + )] + pub vault: Account<'info, VaultState>, + #[account( + mut, + seeds = [b"vault_token", authority.key().as_ref()], + bump = vault.token_bump, + token::authority = vault, + )] + pub vault_token: Account<'info, TokenAccount>, + #[account(mut)] + pub user_token: Account<'info, TokenAccount>, + pub authority: Signer<'info>, + pub token_program: Program<'info, Token>, +} + +#[account] +#[derive(InitSpace)] +pub struct VaultState { + pub authority: Pubkey, + pub bump: u8, + pub token_bump: u8, +} +``` + +## TypeScript Client — CPI Interaction + +```typescript +import { + Connection, + PublicKey, + Keypair, + SystemProgram, +} from "@solana/web3.js"; +import { TOKEN_PROGRAM_ID, getAssociatedTokenAddress } from "@solana/spl-token"; +import { Program, AnchorProvider, Wallet, BN } from "@coral-xyz/anchor"; +import type { VaultCpi } from "../target/types/vault_cpi"; +import idl from "../target/idl/vault_cpi.json"; + +const PROGRAM_ID = new PublicKey("VaultCPI111111111111111111111111111111111111"); + +async function withdrawFromVault( + program: Program, + authority: Keypair, + mint: PublicKey, + amount: number +) { + const [vaultPDA] = PublicKey.findProgramAddressSync( + [Buffer.from("vault"), authority.publicKey.toBuffer()], + PROGRAM_ID + ); + const [vaultTokenPDA] = PublicKey.findProgramAddressSync( + [Buffer.from("vault_token"), authority.publicKey.toBuffer()], + PROGRAM_ID + ); + const userToken = await getAssociatedTokenAddress( + mint, + authority.publicKey + ); + + const tx = await program.methods + .withdrawFromVault(new BN(amount)) + .accounts({ + vault: vaultPDA, + vaultToken: vaultTokenPDA, + userToken, + authority: authority.publicKey, + tokenProgram: TOKEN_PROGRAM_ID, + }) + .signers([authority]) + .rpc(); + + console.log("Withdraw tx:", tx); +} +``` + +## Raw `invoke` and `invoke_signed` (Without Anchor) + +For native Solana programs (no Anchor), use the raw instruction interface. + +```rust +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + program::invoke_signed, + pubkey::Pubkey, +}; + +pub fn process_pda_transfer( + program_id: &Pubkey, + accounts: &[AccountInfo], + amount: u64, +) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + let source = next_account_info(accounts_iter)?; + let destination = next_account_info(accounts_iter)?; + let pda = next_account_info(accounts_iter)?; + let token_program = next_account_info(accounts_iter)?; + + // Derive PDA and verify + let (expected_pda, bump) = Pubkey::find_program_address( + &[b"authority"], + program_id, + ); + assert_eq!(*pda.key, expected_pda); + + let seeds = &[b"authority".as_ref(), &[bump]]; + let signer_seeds = &[&seeds[..]]; + + invoke_signed( + &spl_token::instruction::transfer( + token_program.key, + source.key, + destination.key, + pda.key, + &[], + amount, + )?, + &[source.clone(), destination.clone(), pda.clone()], + signer_seeds, + )?; + + Ok(()) +} +``` + +## CPI Depth and Compute Budget + +| Level | Description | Available CU | +|-------|-------------|-------------| +| 0 | Top-level instruction | Full budget | +| 1 | First CPI | Reduced | +| 2 | Second CPI | Further reduced | +| 3 | Third CPI | Minimal | +| 4 | Max depth | Almost none | +| 5+ | **Fails** | N/A | + +Each CPI level consumes compute units for the invocation overhead. Design programs to minimize CPI depth. If you need program A to call program B to call program C, consider whether A can call B and C directly in separate instructions within the same transaction. + +Last verified: 2026-03-01 diff --git a/skills/solana-simd/examples/pda-derivation/README.md b/skills/solana-simd/examples/pda-derivation/README.md new file mode 100644 index 0000000..ad697a7 --- /dev/null +++ b/skills/solana-simd/examples/pda-derivation/README.md @@ -0,0 +1,237 @@ +# PDA Derivation + +Derive Program Derived Addresses (PDAs) on-chain with Anchor and off-chain with `@solana/web3.js`. Covers canonical bumps, multi-seed PDAs, and PDA-to-PDA derivation. + +## Anchor Program — Vault with PDA + +```rust +use anchor_lang::prelude::*; + +declare_id!("Vau1tPDA111111111111111111111111111111111111"); + +#[program] +pub mod pda_vault { + use super::*; + + pub fn create_vault(ctx: Context) -> Result<()> { + let vault = &mut ctx.accounts.vault; + vault.authority = ctx.accounts.authority.key(); + vault.bump = ctx.bumps.vault; + vault.balance = 0; + Ok(()) + } + + pub fn deposit(ctx: Context, amount: u64) -> Result<()> { + let vault = &mut ctx.accounts.vault; + + // Transfer SOL from depositor to vault PDA + let ix = anchor_lang::solana_program::system_instruction::transfer( + &ctx.accounts.authority.key(), + &vault.key(), + amount, + ); + anchor_lang::solana_program::program::invoke( + &ix, + &[ + ctx.accounts.authority.to_account_info(), + vault.to_account_info(), + ctx.accounts.system_program.to_account_info(), + ], + )?; + + vault.balance = vault.balance.checked_add(amount) + .ok_or(ErrorCode::Overflow)?; + Ok(()) + } + + pub fn withdraw(ctx: Context, amount: u64) -> Result<()> { + let vault = &mut ctx.accounts.vault; + + require!(vault.balance >= amount, ErrorCode::InsufficientBalance); + + // PDA signs to transfer SOL back + **vault.to_account_info().try_borrow_mut_lamports()? -= amount; + **ctx.accounts.authority.to_account_info().try_borrow_mut_lamports()? += amount; + + vault.balance = vault.balance.checked_sub(amount) + .ok_or(ErrorCode::Overflow)?; + Ok(()) + } +} + +#[derive(Accounts)] +pub struct CreateVault<'info> { + #[account( + init, + payer = authority, + space = 8 + Vault::INIT_SPACE, + seeds = [b"vault", authority.key().as_ref()], + bump, + )] + pub vault: Account<'info, Vault>, + #[account(mut)] + pub authority: Signer<'info>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct Deposit<'info> { + #[account( + mut, + seeds = [b"vault", authority.key().as_ref()], + bump = vault.bump, + )] + pub vault: Account<'info, Vault>, + #[account(mut)] + pub authority: Signer<'info>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct Withdraw<'info> { + #[account( + mut, + seeds = [b"vault", authority.key().as_ref()], + bump = vault.bump, + has_one = authority, + )] + pub vault: Account<'info, Vault>, + #[account(mut)] + pub authority: Signer<'info>, +} + +#[account] +#[derive(InitSpace)] +pub struct Vault { + pub authority: Pubkey, + pub bump: u8, + pub balance: u64, +} + +#[error_code] +pub enum ErrorCode { + #[msg("Insufficient balance in vault")] + InsufficientBalance, + #[msg("Arithmetic overflow")] + Overflow, +} +``` + +## TypeScript Client — Derive and Interact + +```typescript +import { + Connection, + PublicKey, + Keypair, + SystemProgram, +} from "@solana/web3.js"; +import { Program, AnchorProvider, Wallet, BN } from "@coral-xyz/anchor"; +import type { PdaVault } from "../target/types/pda_vault"; +import idl from "../target/idl/pda_vault.json"; + +const PROGRAM_ID = new PublicKey("Vau1tPDA111111111111111111111111111111111111"); + +function deriveVaultPDA(authority: PublicKey): [PublicKey, number] { + return PublicKey.findProgramAddressSync( + [Buffer.from("vault"), authority.toBuffer()], + PROGRAM_ID + ); +} + +async function main() { + const connection = new Connection("https://api.devnet.solana.com", "confirmed"); + const wallet = new Wallet(Keypair.generate()); + const provider = new AnchorProvider(connection, wallet, { + commitment: "confirmed", + }); + const program = new Program(idl as PdaVault, PROGRAM_ID, provider); + + const [vaultPDA, bump] = deriveVaultPDA(wallet.publicKey); + console.log("Vault PDA:", vaultPDA.toBase58()); + console.log("Canonical bump:", bump); + + // Create vault + const txCreate = await program.methods + .createVault() + .accounts({ + vault: vaultPDA, + authority: wallet.publicKey, + systemProgram: SystemProgram.programId, + }) + .rpc(); + console.log("Created vault:", txCreate); + + // Deposit 0.1 SOL + const depositAmount = new BN(100_000_000); // 0.1 SOL in lamports + const txDeposit = await program.methods + .deposit(depositAmount) + .accounts({ + vault: vaultPDA, + authority: wallet.publicKey, + systemProgram: SystemProgram.programId, + }) + .rpc(); + console.log("Deposited:", txDeposit); + + // Read vault state + const vaultAccount = await program.account.vault.fetch(vaultPDA); + console.log("Vault balance:", vaultAccount.balance.toString(), "lamports"); + console.log("Vault authority:", vaultAccount.authority.toBase58()); + console.log("Stored bump:", vaultAccount.bump); +} + +main().catch(console.error); +``` + +## Multi-Seed PDA Example + +PDAs can use multiple seeds for namespacing. A common pattern is user + protocol + identifier. + +```rust +#[derive(Accounts)] +#[instruction(pool_id: u64)] +pub struct CreatePosition<'info> { + #[account( + init, + payer = user, + space = 8 + Position::INIT_SPACE, + seeds = [ + b"position", + pool.key().as_ref(), + user.key().as_ref(), + &pool_id.to_le_bytes(), + ], + bump, + )] + pub position: Account<'info, Position>, + pub pool: Account<'info, Pool>, + #[account(mut)] + pub user: Signer<'info>, + pub system_program: Program<'info, System>, +} +``` + +```typescript +// Client-side derivation must match seed order and encoding exactly +const [positionPDA] = PublicKey.findProgramAddressSync( + [ + Buffer.from("position"), + poolPublicKey.toBuffer(), + userPublicKey.toBuffer(), + new BN(poolId).toArrayLike(Buffer, "le", 8), + ], + programId +); +``` + +## Common Mistakes + +| Mistake | Consequence | Fix | +|---------|-------------|-----| +| Wrong seed order | Different PDA derived | Match program seed order exactly | +| `toBase58()` instead of `toBuffer()` for pubkey seed | Wrong bytes | Always use `.toBuffer()` for pubkey seeds | +| Not storing bump in account | Recompute every instruction (wastes CU) | Store bump in account data, use `bump = account.bump` | +| Using `create_program_address` with arbitrary bump | May produce valid keypair address | Always use `find_program_address` for canonical bump | + +Last verified: 2026-03-01 diff --git a/skills/solana-simd/examples/priority-fees/README.md b/skills/solana-simd/examples/priority-fees/README.md new file mode 100644 index 0000000..b0ae3f7 --- /dev/null +++ b/skills/solana-simd/examples/priority-fees/README.md @@ -0,0 +1,239 @@ +# Priority Fees + +Estimate, set, and optimize priority fees for Solana transactions. Covers compute budget instructions, dynamic fee estimation, and versioned transactions with priority fees. + +## Basic Priority Fee Setup + +Every priority-fee transaction needs two Compute Budget instructions: one to set the compute unit limit and one to set the price per compute unit. + +```typescript +import { + Connection, + PublicKey, + Keypair, + Transaction, + SystemProgram, + ComputeBudgetProgram, + sendAndConfirmTransaction, +} from "@solana/web3.js"; + +async function transferWithPriorityFee( + connection: Connection, + sender: Keypair, + recipient: PublicKey, + lamports: number, + microLamportsPerCU: number +): Promise { + const tx = new Transaction().add( + // Set compute unit limit (SOL transfer uses ~450 CU, pad to 1000) + ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000 }), + // Set price per compute unit + ComputeBudgetProgram.setComputeUnitPrice({ microLamports: microLamportsPerCU }), + // Actual instruction + SystemProgram.transfer({ + fromPubkey: sender.publicKey, + toPubkey: recipient, + lamports, + }) + ); + + const sig = await sendAndConfirmTransaction(connection, tx, [sender]); + // Total priority fee = 1,000 CU * microLamportsPerCU / 1,000,000 lamports + return sig; +} +``` + +## Dynamic Fee Estimation + +Use `getRecentPrioritizationFees` to estimate competitive fees based on recent block data. Filter by the writable accounts your transaction touches for account-specific fee markets. + +```typescript +import { Connection, PublicKey } from "@solana/web3.js"; + +interface FeeEstimate { + low: number; + medium: number; + high: number; + veryHigh: number; +} + +async function estimatePriorityFees( + connection: Connection, + writableAccounts: PublicKey[] +): Promise { + const recentFees = await connection.getRecentPrioritizationFees({ + lockedWritableAccounts: writableAccounts, + }); + + if (recentFees.length === 0) { + return { low: 0, medium: 0, high: 0, veryHigh: 0 }; + } + + // Filter out zero-fee slots (no priority fee transactions) + const nonZeroFees = recentFees + .map((f) => f.prioritizationFee) + .filter((f) => f > 0) + .sort((a, b) => a - b); + + if (nonZeroFees.length === 0) { + return { low: 1_000, medium: 10_000, high: 100_000, veryHigh: 1_000_000 }; + } + + const percentile = (p: number) => + nonZeroFees[Math.floor(nonZeroFees.length * p)] ?? nonZeroFees[0]; + + return { + low: percentile(0.25), + medium: percentile(0.5), + high: percentile(0.75), + veryHigh: percentile(0.95), + }; +} + +// Usage +async function main() { + const connection = new Connection("https://api.mainnet-beta.solana.com"); + + // Fee market for a specific AMM pool + const ammPoolAccount = new PublicKey("..."); + const fees = await estimatePriorityFees(connection, [ammPoolAccount]); + + console.log("Fee estimates (microLamports/CU):"); + console.log(" Low (p25):", fees.low); + console.log(" Medium (p50):", fees.medium); + console.log(" High (p75):", fees.high); + console.log(" Very High (p95):", fees.veryHigh); +} +``` + +## Priority Fees with Versioned Transactions + +When using Address Lookup Tables, priority fee instructions work the same way but go into the versioned transaction message. + +```typescript +import { + Connection, + PublicKey, + Keypair, + TransactionMessage, + VersionedTransaction, + ComputeBudgetProgram, + TransactionInstruction, +} from "@solana/web3.js"; + +async function sendVersionedTxWithPriorityFee( + connection: Connection, + payer: Keypair, + instructions: TransactionInstruction[], + lookupTableAddress: PublicKey, + computeUnits: number, + microLamports: number +): Promise { + const lookupTableAccount = await connection + .getAddressLookupTable(lookupTableAddress) + .then((res) => res.value); + + if (!lookupTableAccount) { + throw new Error("Lookup table not found"); + } + + const { blockhash } = await connection.getLatestBlockhash(); + + // Prepend compute budget instructions + const allInstructions = [ + ComputeBudgetProgram.setComputeUnitLimit({ units: computeUnits }), + ComputeBudgetProgram.setComputeUnitPrice({ microLamports }), + ...instructions, + ]; + + const messageV0 = new TransactionMessage({ + payerKey: payer.publicKey, + recentBlockhash: blockhash, + instructions: allInstructions, + }).compileToV0Message([lookupTableAccount]); + + const tx = new VersionedTransaction(messageV0); + tx.sign([payer]); + + const sig = await connection.sendTransaction(tx, { + skipPreflight: false, + }); + + await connection.confirmTransaction(sig, "confirmed"); + return sig; +} +``` + +## Simulate to Get Actual Compute Usage + +Avoid overpaying by simulating first to learn actual compute consumption, then setting the limit with a small buffer. + +```typescript +import { + Connection, + Transaction, + Keypair, + ComputeBudgetProgram, +} from "@solana/web3.js"; + +async function simulateAndSend( + connection: Connection, + payer: Keypair, + instructions: TransactionInstruction[], + microLamportsPerCU: number +): Promise { + // Step 1: Simulate without compute budget to learn actual usage + const simTx = new Transaction().add(...instructions); + simTx.feePayer = payer.publicKey; + simTx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash; + + const simulation = await connection.simulateTransaction(simTx); + if (simulation.value.err) { + throw new Error(`Simulation failed: ${JSON.stringify(simulation.value.err)}`); + } + + const unitsConsumed = simulation.value.unitsConsumed ?? 200_000; + // 20% buffer over actual usage + const computeLimit = Math.ceil(unitsConsumed * 1.2); + + // Step 2: Send with tight compute budget + const finalTx = new Transaction().add( + ComputeBudgetProgram.setComputeUnitLimit({ units: computeLimit }), + ComputeBudgetProgram.setComputeUnitPrice({ microLamports: microLamportsPerCU }), + ...instructions + ); + + const sig = await sendAndConfirmTransaction(connection, finalTx, [payer]); + console.log(`Used ~${unitsConsumed} CU, set limit to ${computeLimit}`); + return sig; +} +``` + +## Cost Calculation Reference + +``` +priority_fee (lamports) = compute_units * microLamports_per_CU / 1,000,000 +base_fee = 5,000 lamports (fixed per signature) +total_fee = base_fee + priority_fee +``` + +| Compute Units | microLamports/CU | Priority Fee | Total Fee (1 sig) | +|---------------|-----------------|--------------|-------------------| +| 200,000 | 1,000 | 200 lamports | 5,200 lamports | +| 200,000 | 10,000 | 2,000 lamports | 7,000 lamports | +| 200,000 | 100,000 | 20,000 lamports | 25,000 lamports | +| 200,000 | 1,000,000 | 200,000 lamports | 205,000 lamports | +| 1,400,000 | 100,000 | 140,000 lamports | 145,000 lamports | + +1 SOL = 1,000,000,000 lamports. A 200,000 CU transaction at 100,000 microLamports/CU costs 0.000025 SOL in total fees. + +## Common Mistakes + +| Mistake | Impact | Fix | +|---------|--------|-----| +| Setting price but not limit | Default 200K CU used, overpay | Always set both instructions | +| Very high limit with high price | Massive overpay | Simulate first, use actual + 20% buffer | +| Not filtering by writable accounts | Irrelevant fee data | Pass `lockedWritableAccounts` to fee API | +| Compute budget instructions not first | May fail | Place CU instructions before all others | + +Last verified: 2026-03-01 diff --git a/skills/solana-simd/examples/token-extensions/README.md b/skills/solana-simd/examples/token-extensions/README.md new file mode 100644 index 0000000..5f9812a --- /dev/null +++ b/skills/solana-simd/examples/token-extensions/README.md @@ -0,0 +1,320 @@ +# Token Extensions (Token-2022) + +Create and interact with Token-2022 mints using transfer fees, non-transferable (soulbound) tokens, and metadata pointer extensions. All examples use `@solana/spl-token` with the Token-2022 program. + +## Create Mint with Transfer Fee + +Every transfer of this token automatically withholds a percentage as a fee that the withdraw authority can collect. + +```typescript +import { + Connection, + Keypair, + SystemProgram, + Transaction, + sendAndConfirmTransaction, +} from "@solana/web3.js"; +import { + TOKEN_2022_PROGRAM_ID, + createInitializeMintInstruction, + createInitializeTransferFeeConfigInstruction, + getMintLen, + ExtensionType, + createMintToInstruction, + createAssociatedTokenAccountInstruction, + getAssociatedTokenAddressSync, + ASSOCIATED_TOKEN_PROGRAM_ID, +} from "@solana/spl-token"; + +async function createTransferFeeMint( + connection: Connection, + payer: Keypair, + decimals: number, + feeBasisPoints: number, + maxFee: bigint +): Promise { + const mintKeypair = Keypair.generate(); + const extensions = [ExtensionType.TransferFeeConfig]; + const mintLen = getMintLen(extensions); + const lamports = await connection.getMinimumBalanceForRentExemption(mintLen); + + const tx = new Transaction().add( + SystemProgram.createAccount({ + fromPubkey: payer.publicKey, + newAccountPubkey: mintKeypair.publicKey, + space: mintLen, + lamports, + programId: TOKEN_2022_PROGRAM_ID, + }), + createInitializeTransferFeeConfigInstruction( + mintKeypair.publicKey, + payer.publicKey, // transferFeeConfigAuthority + payer.publicKey, // withdrawWithheldAuthority + feeBasisPoints, // fee in basis points (100 = 1%) + maxFee, // max fee cap in base units + TOKEN_2022_PROGRAM_ID + ), + createInitializeMintInstruction( + mintKeypair.publicKey, + decimals, + payer.publicKey, // mintAuthority + payer.publicKey, // freezeAuthority + TOKEN_2022_PROGRAM_ID + ) + ); + + await sendAndConfirmTransaction(connection, tx, [payer, mintKeypair]); + console.log("Mint with transfer fee:", mintKeypair.publicKey.toBase58()); + return mintKeypair; +} + +// Usage: 1% fee, max 1000 tokens (with 9 decimals) +// createTransferFeeMint(connection, payer, 9, 100, 1_000_000_000_000n); +``` + +## Create Non-Transferable (Soulbound) Token + +Non-transferable tokens cannot be moved after minting. Useful for credentials, certifications, and reputation. + +```typescript +import { + Connection, + Keypair, + SystemProgram, + Transaction, + sendAndConfirmTransaction, +} from "@solana/web3.js"; +import { + TOKEN_2022_PROGRAM_ID, + createInitializeMintInstruction, + createInitializeNonTransferableMintInstruction, + getMintLen, + ExtensionType, + createMintToInstruction, + createAssociatedTokenAccountInstruction, + getAssociatedTokenAddressSync, + ASSOCIATED_TOKEN_PROGRAM_ID, +} from "@solana/spl-token"; + +async function createSoulboundToken( + connection: Connection, + payer: Keypair, + recipient: Keypair +): Promise { + const mintKeypair = Keypair.generate(); + const extensions = [ExtensionType.NonTransferable]; + const mintLen = getMintLen(extensions); + const lamports = await connection.getMinimumBalanceForRentExemption(mintLen); + + // Step 1: Create mint with non-transferable extension + const createMintTx = new Transaction().add( + SystemProgram.createAccount({ + fromPubkey: payer.publicKey, + newAccountPubkey: mintKeypair.publicKey, + space: mintLen, + lamports, + programId: TOKEN_2022_PROGRAM_ID, + }), + createInitializeNonTransferableMintInstruction( + mintKeypair.publicKey, + TOKEN_2022_PROGRAM_ID + ), + createInitializeMintInstruction( + mintKeypair.publicKey, + 0, // 0 decimals for NFT-like tokens + payer.publicKey, + null, // no freeze authority + TOKEN_2022_PROGRAM_ID + ) + ); + + await sendAndConfirmTransaction(connection, createMintTx, [payer, mintKeypair]); + + // Step 2: Create ATA for recipient and mint + const recipientATA = getAssociatedTokenAddressSync( + mintKeypair.publicKey, + recipient.publicKey, + false, + TOKEN_2022_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ); + + const mintTx = new Transaction().add( + createAssociatedTokenAccountInstruction( + payer.publicKey, + recipientATA, + recipient.publicKey, + mintKeypair.publicKey, + TOKEN_2022_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ), + createMintToInstruction( + mintKeypair.publicKey, + recipientATA, + payer.publicKey, + 1, // mint exactly 1 + [], + TOKEN_2022_PROGRAM_ID + ) + ); + + await sendAndConfirmTransaction(connection, mintTx, [payer]); + console.log("Soulbound token minted to:", recipient.publicKey.toBase58()); + // Any attempt to transfer this token will fail +} +``` + +## Create Mint with Metadata Pointer + +Metadata pointer stores token metadata directly in the mint account, eliminating the need for Metaplex. + +```typescript +import { + Connection, + Keypair, + SystemProgram, + Transaction, + sendAndConfirmTransaction, +} from "@solana/web3.js"; +import { + TOKEN_2022_PROGRAM_ID, + createInitializeMintInstruction, + createInitializeMetadataPointerInstruction, + getMintLen, + ExtensionType, + TYPE_SIZE, + LENGTH_SIZE, +} from "@solana/spl-token"; +import { + createInitializeInstruction, + pack, + TokenMetadata, +} from "@solana/spl-token-metadata"; + +async function createMintWithMetadata( + connection: Connection, + payer: Keypair, + name: string, + symbol: string, + uri: string +): Promise { + const mintKeypair = Keypair.generate(); + + const metadata: TokenMetadata = { + mint: mintKeypair.publicKey, + name, + symbol, + uri, + additionalMetadata: [], + }; + + const mintLen = getMintLen([ExtensionType.MetadataPointer]); + const metadataLen = TYPE_SIZE + LENGTH_SIZE + pack(metadata).length; + const totalLen = mintLen + metadataLen; + const lamports = await connection.getMinimumBalanceForRentExemption(totalLen); + + const tx = new Transaction().add( + SystemProgram.createAccount({ + fromPubkey: payer.publicKey, + newAccountPubkey: mintKeypair.publicKey, + space: mintLen, // initial space excludes metadata + lamports, + programId: TOKEN_2022_PROGRAM_ID, + }), + createInitializeMetadataPointerInstruction( + mintKeypair.publicKey, + payer.publicKey, // metadata update authority + mintKeypair.publicKey, // metadata address (self for embedded) + TOKEN_2022_PROGRAM_ID + ), + createInitializeMintInstruction( + mintKeypair.publicKey, + 9, + payer.publicKey, + null, + TOKEN_2022_PROGRAM_ID + ), + createInitializeInstruction({ + programId: TOKEN_2022_PROGRAM_ID, + mint: mintKeypair.publicKey, + metadata: mintKeypair.publicKey, + name: metadata.name, + symbol: metadata.symbol, + uri: metadata.uri, + mintAuthority: payer.publicKey, + updateAuthority: payer.publicKey, + }) + ); + + await sendAndConfirmTransaction(connection, tx, [payer, mintKeypair]); + console.log("Mint with metadata:", mintKeypair.publicKey.toBase58()); + return mintKeypair; +} +``` + +## Anchor — Accept Both SPL Token and Token-2022 + +Programs that need to work with both token standards must validate the token program dynamically. + +```rust +use anchor_lang::prelude::*; +use anchor_spl::token_interface::{ + self, Mint, TokenAccount, TokenInterface, TransferChecked, +}; + +declare_id!("DualTkn1111111111111111111111111111111111111"); + +#[program] +pub mod dual_token { + use super::*; + + /// Transfer tokens using whichever token program owns the mint + pub fn transfer_any_token( + ctx: Context, + amount: u64, + ) -> Result<()> { + let cpi_accounts = TransferChecked { + from: ctx.accounts.source.to_account_info(), + mint: ctx.accounts.mint.to_account_info(), + to: ctx.accounts.destination.to_account_info(), + authority: ctx.accounts.authority.to_account_info(), + }; + let cpi_ctx = CpiContext::new( + ctx.accounts.token_program.to_account_info(), + cpi_accounts, + ); + token_interface::transfer_checked(cpi_ctx, amount, ctx.accounts.mint.decimals)?; + Ok(()) + } +} + +#[derive(Accounts)] +pub struct TransferAnyToken<'info> { + #[account(mut)] + pub source: InterfaceAccount<'info, TokenAccount>, + pub mint: InterfaceAccount<'info, Mint>, + #[account(mut)] + pub destination: InterfaceAccount<'info, TokenAccount>, + pub authority: Signer<'info>, + pub token_program: Interface<'info, TokenInterface>, +} +``` + +Key Anchor types for Token-2022 compatibility: +- `InterfaceAccount<'info, TokenAccount>` instead of `Account<'info, TokenAccount>` +- `InterfaceAccount<'info, Mint>` instead of `Account<'info, Mint>` +- `Interface<'info, TokenInterface>` instead of `Program<'info, Token>` +- `transfer_checked` instead of `transfer` (required by Token-2022) + +## Extension Compatibility Matrix + +| Extension | Composable with | Incompatible with | +|-----------|----------------|-------------------| +| Transfer Fee | Metadata Pointer, Interest-Bearing | Confidential Transfers | +| Non-Transferable | Metadata Pointer | Transfer Fee, Permanent Delegate | +| Metadata Pointer | Transfer Fee, Non-Transferable, Interest-Bearing | None | +| Permanent Delegate | Transfer Fee, Metadata Pointer | Non-Transferable | +| Interest-Bearing | Metadata Pointer, Transfer Fee | Confidential Transfers | +| Confidential Transfers | Metadata Pointer | Transfer Fee, Interest-Bearing | + +Last verified: 2026-03-01 From ed1b800eecec84121e1124f1a75c738544da307f Mon Sep 17 00:00:00 2001 From: 0xinit <28729137+0xinit@users.noreply.github.com> Date: Wed, 4 Mar 2026 14:52:33 -0800 Subject: [PATCH 11/14] feat: add solana-simd docs and resources --- skills/solana-simd/docs/troubleshooting.md | 175 ++++++++++++++++++ skills/solana-simd/resources/account-sizes.md | 109 +++++++++++ skills/solana-simd/resources/error-codes.md | 114 ++++++++++++ skills/solana-simd/resources/simd-lookup.md | 65 +++++++ 4 files changed, 463 insertions(+) create mode 100644 skills/solana-simd/docs/troubleshooting.md create mode 100644 skills/solana-simd/resources/account-sizes.md create mode 100644 skills/solana-simd/resources/error-codes.md create mode 100644 skills/solana-simd/resources/simd-lookup.md diff --git a/skills/solana-simd/docs/troubleshooting.md b/skills/solana-simd/docs/troubleshooting.md new file mode 100644 index 0000000..b7b0e27 --- /dev/null +++ b/skills/solana-simd/docs/troubleshooting.md @@ -0,0 +1,175 @@ +# Solana SIMD Troubleshooting + +## Transaction Too Large + +``` +Transaction too large: 1644 > 1232 +``` + +**Cause**: Legacy transactions are limited to 1,232 bytes. Too many accounts or instruction data pushes past this limit. + +**Fix**: Use versioned transactions with Address Lookup Tables (ALTs) to compress account references. + +```typescript +import { TransactionMessage, VersionedTransaction } from "@solana/web3.js"; + +const lookupTableAccount = await connection + .getAddressLookupTable(lookupTableAddress) + .then((res) => res.value); + +const messageV0 = new TransactionMessage({ + payerKey: payer.publicKey, + recentBlockhash: blockhash, + instructions, +}).compileToV0Message([lookupTableAccount]); + +const tx = new VersionedTransaction(messageV0); +``` + +If you don't have a lookup table, create one first. See `examples/priority-fees/` for the full pattern. ALTs take one epoch (~2 days) to become active after creation on mainnet. + +## PDA Derivation Mismatch + +``` +Error: Account not found / seeds constraint was violated +``` + +**Cause**: Client-side PDA derivation does not match on-chain. Common reasons: +1. Seed order is wrong +2. Seed encoding differs (string vs pubkey bytes) +3. Using a non-canonical bump + +**Fix**: Ensure seeds match exactly between client and program. + +```typescript +// Client must match program seeds EXACTLY in order and encoding +const [pda, bump] = PublicKey.findProgramAddressSync( + [ + Buffer.from("vault"), // string seed + userPublicKey.toBuffer(), // pubkey as 32 bytes + ], + programId +); +``` + +```rust +// On-chain Anchor constraint +#[account( + seeds = [b"vault", authority.key().as_ref()], + bump, +)] +pub vault: Account<'info, Vault>, +``` + +Checklist: (1) Same seed prefix string, (2) Same pubkey serialization, (3) Same program ID, (4) Using `findProgramAddressSync` not `createProgramAddressSync`. + +## CPI Depth Exceeded + +``` +Cross-program invocation with unauthorized signer or writable account +Program returned error: exceeded CPI call depth +``` + +**Cause**: CPI call chain exceeds 4 levels. Program A -> B -> C -> D -> E fails at E. + +**Fix**: Flatten your architecture. Common strategies: +- Combine logic into fewer programs +- Use instruction-level composition instead of CPI chains (multiple instructions in one transaction) +- Pass pre-computed results as instruction data instead of calling intermediary programs + +## Priority Fee Too Low / Transaction Not Landing + +``` +Transaction simulation failed: Blockhash not found +``` + +**Cause**: Transaction expired before being included in a block. Usually because priority fee was too low during congestion. + +**Fix**: Estimate fees using recent data, set both compute limit and price. + +```typescript +const fees = await connection.getRecentPrioritizationFees({ + lockedWritableAccounts: [hotAccount], +}); + +const sorted = fees.map((f) => f.prioritizationFee).sort((a, b) => a - b); +const p75 = sorted[Math.floor(sorted.length * 0.75)]; + +tx.add( + ComputeBudgetProgram.setComputeUnitLimit({ units: 200_000 }), + ComputeBudgetProgram.setComputeUnitPrice({ microLamports: p75 }) +); +``` + +Also set `skipPreflight: false` and use `confirmed` commitment to catch simulation errors early. + +## Token-2022 Incompatibility + +``` +Error: incorrect program id for instruction +``` + +**Cause**: Program hardcodes SPL Token program ID but receives a Token-2022 token account. + +**Fix**: Accept both program IDs. + +```rust +let valid = token_program.key() == spl_token::id() + || token_program.key() == spl_token_2022::id(); +require!(valid, MyError::InvalidTokenProgram); +``` + +On the client side, check which program owns the mint before building instructions: + +```typescript +import { TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID } from "@solana/spl-token"; + +const mintInfo = await connection.getAccountInfo(mintAddress); +const tokenProgramId = mintInfo.owner.equals(TOKEN_2022_PROGRAM_ID) + ? TOKEN_2022_PROGRAM_ID + : TOKEN_PROGRAM_ID; +``` + +## Account Realloc Failures + +``` +memory allocation failed, out of memory +``` + +**Cause**: Attempting to increase account data by more than 10,240 bytes in a single instruction, or insufficient lamports for the new rent-exempt minimum. + +**Fix**: Realloc in chunks of 10 KB max, and transfer additional lamports before or during realloc. + +```rust +account_info.realloc(new_len, false)?; + +let rent = Rent::get()?; +let new_min_balance = rent.minimum_balance(new_len); +let current_lamports = account_info.lamports(); +if current_lamports < new_min_balance { + // Transfer the difference from payer + let diff = new_min_balance - current_lamports; + invoke( + &system_instruction::transfer(payer.key, account_info.key, diff), + &[payer.clone(), account_info.clone()], + )?; +} +``` + +## Anchor IDL Not Found + +``` +Error: IDL not found for program +``` + +**Cause**: The IDL was not published on-chain, or the program was deployed without `anchor deploy`. + +**Fix**: Publish the IDL separately. + +```bash +anchor idl init --filepath target/idl/my_program.json --provider.cluster devnet +# Or update an existing IDL +anchor idl upgrade --filepath target/idl/my_program.json --provider.cluster devnet +``` + +Last verified: 2026-03-01 diff --git a/skills/solana-simd/resources/account-sizes.md b/skills/solana-simd/resources/account-sizes.md new file mode 100644 index 0000000..47d81e2 --- /dev/null +++ b/skills/solana-simd/resources/account-sizes.md @@ -0,0 +1,109 @@ +# Solana Account Sizes and Rent Costs + +## Common Account Sizes + +Rent-exempt minimum calculated at ~0.00089088 SOL per byte base + 128 bytes overhead. Formula: `rent.minimum_balance(data_len)` where the runtime adds a fixed overhead. + +| Account Type | Data Size (bytes) | Rent-Exempt Minimum (SOL) | +|-------------|-------------------|--------------------------| +| System account (wallet) | 0 | 0.00089088 | +| SPL Token Mint | 82 | 0.00144768 | +| SPL Token Account | 165 | 0.00203928 | +| Associated Token Account | 165 | 0.00203928 | +| Token-2022 Mint (base) | 82 | 0.00144768 | +| Token-2022 Mint + Transfer Fee | 178 | 0.00213408 | +| Token-2022 Mint + Metadata Pointer | 114 | 0.00167616 | +| Token-2022 Mint + Non-Transferable | 83 | 0.00145480 | +| Anchor account (8-byte discriminator) | 8 + data | varies | +| Address Lookup Table | 56 + (32 * num_addresses) | varies | + +## Rent Calculation + +```typescript +import { Connection } from "@solana/web3.js"; + +async function calculateRent( + connection: Connection, + dataSize: number +): Promise { + return connection.getMinimumBalanceForRentExemption(dataSize); +} + +// Common calculations +// 0 bytes: 890,880 lamports (0.00089088 SOL) +// 100 bytes: 1,447,680 lamports (0.00144768 SOL) +// 500 bytes: 4,310,400 lamports (0.00431040 SOL) +// 1 KB: 7,948,800 lamports (0.00794880 SOL) +// 10 KB: 72,192,000 lamports (0.07219200 SOL) +// 1 MB: 7,143,360,000 lamports (7.14 SOL) +// 10 MB: 71,433,600,000 lamports (71.43 SOL) — max account size +``` + +```rust +// On-chain rent calculation +use solana_program::rent::Rent; +use solana_program::sysvar::Sysvar; + +let rent = Rent::get()?; +let min_balance = rent.minimum_balance(data_len); +``` + +## Anchor Space Calculation + +Anchor accounts use an 8-byte discriminator prefix. Calculate space with `INIT_SPACE` derive macro or manually. + +| Rust Type | Size (bytes) | +|-----------|-------------| +| `bool` | 1 | +| `u8` / `i8` | 1 | +| `u16` / `i16` | 2 | +| `u32` / `i32` | 4 | +| `u64` / `i64` | 8 | +| `u128` / `i128` | 16 | +| `f32` | 4 | +| `f64` | 8 | +| `Pubkey` | 32 | +| `String` (borsh) | 4 + len | +| `Vec` | 4 + (len * sizeof(T)) | +| `Option` | 1 + sizeof(T) | +| `[T; N]` (array) | N * sizeof(T) | +| Enum (C-style) | 1 | +| Enum (with data) | 1 + max(variant sizes) | + +```rust +// Example: calculate space for an account +#[account] +#[derive(InitSpace)] +pub struct GameState { + pub authority: Pubkey, // 32 + pub score: u64, // 8 + pub level: u8, // 1 + pub is_active: bool, // 1 + #[max_len(32)] + pub name: String, // 4 + 32 = 36 +} +// Total: 8 (discriminator) + 32 + 8 + 1 + 1 + 36 = 86 bytes +``` + +## Account Size Limits + +| Limit | Value | +|-------|-------| +| Maximum account data | 10,485,760 bytes (10 MB) | +| Maximum `realloc` increase per instruction | 10,240 bytes (10 KB) | +| Maximum accounts per transaction (legacy) | ~35 | +| Maximum accounts per transaction (v0 + ALT) | 256+ | + +## Address Lookup Table Sizes + +| Addresses in Table | Data Size | Rent (SOL) | +|-------------------|-----------|------------| +| 1 | 88 | 0.00149568 | +| 10 | 376 | 0.00355488 | +| 50 | 1,656 | 0.01268928 | +| 100 | 3,256 | 0.02412288 | +| 256 | 8,248 | 0.05979648 | + +Formula: `56 + (32 * num_addresses)` bytes + +Last verified: 2026-03-01 diff --git a/skills/solana-simd/resources/error-codes.md b/skills/solana-simd/resources/error-codes.md new file mode 100644 index 0000000..c7d9572 --- /dev/null +++ b/skills/solana-simd/resources/error-codes.md @@ -0,0 +1,114 @@ +# Solana Program Error Codes + +## System Program Errors + +| Code | Name | Cause | Fix | +|------|------|-------|-----| +| 0 | `AccountAlreadyInUse` | Account already has data or lamports | Use a different keypair or PDA | +| 1 | `ResultWithNegativeLamports` | Would produce negative balance | Check balance before debit | +| 2 | `InvalidProgramId` | Wrong program ID | Verify program deploys to expected address | +| 3 | `InvalidAccountDataLength` | Data size mismatch | Match expected account size | +| 4 | `MaxSeedLengthExceeded` | PDA seed > 32 bytes | Shorten seeds (max 32 bytes each) | +| 5 | `AddressWithSeedMismatch` | PDA does not match expected | Verify seeds and program ID | +| 6 | `NonceNoRecentBlockhashes` | Nonce account has no blockhash | Advance nonce first | +| 7 | `NonceBlockhashNotExpired` | Nonce blockhash still valid | Wait for blockhash to expire | +| 8 | `NonceUnexpectedBlockhashValue` | Stale nonce value | Fetch current nonce value | + +## SPL Token Errors + +| Code | Name | Cause | Fix | +|------|------|-------|-----| +| 0 | `NotRentExempt` | Account below rent-exempt minimum | Fund account sufficiently | +| 1 | `InsufficientFunds` | Not enough tokens | Check balance before transfer | +| 2 | `InvalidMint` | Mint account invalid | Verify mint address | +| 3 | `MintMismatch` | Token account mint != expected mint | Use correct token account for the mint | +| 4 | `OwnerMismatch` | Token account owner mismatch | Verify token account authority | +| 5 | `FixedSupply` | Mint has no authority | Cannot mint more (authority is None) | +| 6 | `AlreadyInUse` | Account already initialized | Use uninitialized account | +| 7 | `InvalidNumberOfProvidedSigners` | Wrong signer count for multisig | Provide correct number of signers | +| 8 | `InvalidNumberOfRequiredSigners` | Multisig threshold invalid | Threshold must be <= total signers | +| 9 | `UninitializedState` | Account not initialized | Initialize account first | +| 10 | `NativeNotSupported` | Operation not supported for native SOL | Use System Program for SOL operations | +| 11 | `NonNativeHasBalance` | Closing account with balance | Transfer all tokens out first | +| 12 | `InvalidInstruction` | Malformed instruction data | Check instruction format | +| 13 | `InvalidState` | Account in wrong state | Verify account state before operation | +| 14 | `Overflow` | Arithmetic overflow | Use checked math | +| 15 | `AuthorityTypeNotSupported` | Unsupported authority operation | Check token program capabilities | +| 16 | `MintCannotFreeze` | Mint has no freeze authority | Set freeze authority at mint init | +| 17 | `AccountFrozen` | Token account is frozen | Thaw account before operation | +| 18 | `MintDecimalsMismatch` | Decimals don't match | Use correct decimals for the mint | + +## Anchor Framework Errors + +| Code | Name | Cause | Fix | +|------|------|-------|-----| +| 100 | `InstructionMissing` | No instruction data | Include instruction discriminator | +| 101 | `InstructionFallbackNotFound` | Unknown instruction | Check method name in IDL | +| 102 | `InstructionDidNotDeserialize` | Bad instruction data | Match IDL argument types | +| 1000 | `InstructionDidNotSerialize` | Serialization failed | Check data types | +| 2000 | `IdlInstructionStub` | IDL instruction not implemented | Implement the function | +| 2001 | `IdlInstructionInvalidProgram` | Wrong program for IDL op | Use correct program ID | +| 2006 | `ConstraintMut` | Account not mutable | Add `#[account(mut)]` | +| 2003 | `ConstraintHasOne` | `has_one` check failed | Account field doesn't match expected | +| 2004 | `ConstraintSigner` | Missing signer | Account must be a transaction signer | +| 2006 | `ConstraintMut` | Not marked mutable | Add `mut` to account constraint | +| 2011 | `ConstraintOwner` | Wrong program owner | Account owned by unexpected program | +| 2012 | `ConstraintRentExempt` | Below rent-exempt minimum | Add sufficient lamports | +| 2014 | `ConstraintSeeds` | PDA seeds mismatch | Verify seeds match constraint | +| 2016 | `ConstraintSpace` | Insufficient space | Increase `space` in `init` | +| 2019 | `ConstraintTokenMint` | Token account mint mismatch | Use correct mint | +| 2020 | `ConstraintTokenOwner` | Token account owner mismatch | Use correct owner | +| 3000 | `AccountDiscriminatorAlreadySet` | Account already initialized | Use `init_if_needed` or check first | +| 3001 | `AccountDiscriminatorNotFound` | Missing 8-byte discriminator | Account may not be initialized | +| 3002 | `AccountDiscriminatorMismatch` | Wrong account type | Verify account type matches | +| 3003 | `AccountDidNotDeserialize` | Deserialization failed | Check account data format | +| 3004 | `AccountDidNotSerialize` | Serialization failed | Check data types | +| 3005 | `AccountNotEnoughKeys` | Missing accounts | Pass all required accounts | +| 3007 | `AccountNotProgramOwned` | Account not owned by program | Verify account ownership | +| 3012 | `AccountNotInitialized` | Expected initialized account | Initialize first | +| 3014 | `AccountOwnedByWrongProgram` | Wrong owner program | Check `owner` constraint | + +## Transaction-Level Errors + +| Error | Cause | Fix | +|-------|-------|-----| +| `BlockhashNotFound` | Blockhash expired (~90 seconds) | Refetch blockhash, resubmit | +| `InsufficientFundsForFee` | Cannot pay transaction fee | Fund payer with SOL | +| `InvalidAccountIndex` | Account index out of bounds | Check account list length | +| `ProgramFailedToComplete` | Compute budget exceeded | Increase CU limit or optimize | +| `TransactionTooLarge` | Exceeds 1,232 bytes | Use versioned tx + ALT | +| `DuplicateInstruction` | Same instruction twice | Remove duplicate | +| `AccountInUse` | Write lock conflict | Retry or serialize access | +| `AccountLoadedTwice` | Same account in tx twice | Reference account once | + +## Decoding Custom Program Errors + +```typescript +import { SendTransactionError } from "@solana/web3.js"; + +function decodeError(err: unknown): string { + if (err instanceof SendTransactionError) { + const logs = err.logs ?? []; + // Look for "Program log: AnchorError" or "Program log: Error" + const errorLog = logs.find( + (log) => log.includes("AnchorError") || log.includes("Error Code:") + ); + if (errorLog) return errorLog; + + // Look for custom program error number + const customErr = logs.find((log) => + log.includes("custom program error:") + ); + if (customErr) { + const match = customErr.match(/custom program error: (0x[0-9a-fA-F]+)/); + if (match) { + const code = parseInt(match[1], 16); + return `Custom error code: ${code} (${match[1]})`; + } + } + } + return String(err); +} +``` + +Last verified: 2026-03-01 diff --git a/skills/solana-simd/resources/simd-lookup.md b/skills/solana-simd/resources/simd-lookup.md new file mode 100644 index 0000000..a11d3e1 --- /dev/null +++ b/skills/solana-simd/resources/simd-lookup.md @@ -0,0 +1,65 @@ +# SIMD Lookup Table + +Quick reference for key Solana Improvement Documents. Full list at https://github.com/solana-foundation/solana-improvement-documents/tree/main/proposals + +## Core Protocol + +| SIMD | Title | Status | Developer Impact | +|------|-------|--------|-----------------| +| 0002 | Fee-payer signs first | Activated | Transaction signing order | +| 0033 | Timely Vote Credits | Activated | Staking APY calculations | +| 0046 | Versioned Transactions | Activated | ALTs, 256+ accounts per tx | +| 0047 | Syscall Probing | Accepted | Forward-compatible programs | +| 0048 | Native Program Upgrades | Activated | BPF loader changes | +| 0052 | Durable Transaction Nonces | Activated | Offline signing, scheduled tx | +| 0072 | Priority Fee Market | Activated | Per-CU priority fees | +| 0084 | Remove Rent Collection | Activated | All accounts must be rent-exempt | +| 0096 | Reward Full Priority Fee to Validator | Activated | 100% priority fee to leader | +| 0105 | QUIC Protocol for TPU | Activated | Connection-based tx submission | +| 0118 | Partitioned Epoch Rewards | Activated | Smoother reward distribution | + +## Token Standards + +| SIMD | Title | Status | Developer Impact | +|------|-------|--------|-----------------| +| 0083 | Token Extensions (Token-2022) | Activated | Transfer fees, confidential, metadata | +| 0148 | Token Metadata in Token-2022 | Activated | On-chain metadata without Metaplex | + +## Staking & Consensus + +| SIMD | Title | Status | Developer Impact | +|------|-------|--------|-----------------| +| 0122 | Stake-weighted Quality of Service | Activated | Tx delivery proportional to stake | +| 0163 | Multiple Delegations per Account | Review | Staking protocol architecture | +| 0172 | Staking Rewards Distribution | Accepted | Incremental reward distribution | +| 0185 | Vote Account Size Reduction | Draft | Validator cost reduction | + +## In Progress + +| SIMD | Title | Status | Developer Impact | +|------|-------|--------|-----------------| +| 0133 | Increase Account Data Limit | Review | Larger on-chain data | +| 0159 | Reduce Rent Cost | Draft | Lower account creation costs | +| 0175 | Confidential Transfers v2 | Review | Enhanced privacy features | +| 0186 | Precompile for Secp256r1 | Activated | WebAuthn / passkey support | +| 0193 | ZK Token Proof Program | Review | On-chain ZK verification | + +## How to Read SIMD Numbers + +- Proposals are numbered sequentially (0001, 0002, ..., 0193+) +- Number does NOT indicate priority or importance +- Status progression: Draft -> Review -> Accepted -> Implemented -> Activated +- "Activated" means live on mainnet-beta with feature gate enabled +- "Accepted" means approved but not yet deployed + +## Checking Feature Gate Status + +```bash +# Check if a feature is activated on mainnet +solana feature status --url mainnet-beta + +# List all features and their activation status +solana feature status --url mainnet-beta +``` + +Last verified: 2026-03-01 From b61a3a3b3b3e9d98e1419fecb1570593b5047b33 Mon Sep 17 00:00:00 2001 From: 0xinit <28729137+0xinit@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:09:48 -0800 Subject: [PATCH 12/14] feat: add solana-simd starter template --- .../solana-simd/templates/anchor-program.rs | 206 ++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 skills/solana-simd/templates/anchor-program.rs diff --git a/skills/solana-simd/templates/anchor-program.rs b/skills/solana-simd/templates/anchor-program.rs new file mode 100644 index 0000000..1538182 --- /dev/null +++ b/skills/solana-simd/templates/anchor-program.rs @@ -0,0 +1,206 @@ +// Anchor Program Starter Template +// +// Usage: +// 1. anchor init my_program && cd my_program +// 2. Replace programs/my_program/src/lib.rs with this file +// 3. Update declare_id! with your program ID +// 4. anchor build && anchor test +// +// Features demonstrated: +// - PDA creation with canonical bump storage +// - CPI to SPL Token program (PDA-signed transfer) +// - Account validation constraints +// - Custom error codes + +use anchor_lang::prelude::*; +use anchor_spl::token::{self, Token, TokenAccount, Mint, Transfer}; + +declare_id!("11111111111111111111111111111111"); + +#[program] +pub mod my_program { + use super::*; + + pub fn initialize(ctx: Context, vault_name: String) -> Result<()> { + require!(vault_name.len() <= 32, MyError::NameTooLong); + + let vault = &mut ctx.accounts.vault; + vault.authority = ctx.accounts.authority.key(); + vault.bump = ctx.bumps.vault; + vault.name = vault_name; + vault.total_deposits = 0; + Ok(()) + } + + pub fn deposit(ctx: Context, amount: u64) -> Result<()> { + require!(amount > 0, MyError::ZeroAmount); + + let cpi_accounts = Transfer { + from: ctx.accounts.user_token.to_account_info(), + to: ctx.accounts.vault_token.to_account_info(), + authority: ctx.accounts.authority.to_account_info(), + }; + let cpi_ctx = CpiContext::new( + ctx.accounts.token_program.to_account_info(), + cpi_accounts, + ); + token::transfer(cpi_ctx, amount)?; + + let vault = &mut ctx.accounts.vault; + vault.total_deposits = vault.total_deposits + .checked_add(amount) + .ok_or(MyError::Overflow)?; + + emit!(DepositEvent { + vault: vault.key(), + depositor: ctx.accounts.authority.key(), + amount, + total: vault.total_deposits, + }); + + Ok(()) + } + + pub fn withdraw(ctx: Context, amount: u64) -> Result<()> { + require!(amount > 0, MyError::ZeroAmount); + + let vault = &mut ctx.accounts.vault; + require!(vault.total_deposits >= amount, MyError::InsufficientBalance); + + // PDA signs the token transfer + let authority_key = ctx.accounts.authority.key(); + let seeds = &[ + b"vault".as_ref(), + authority_key.as_ref(), + &[vault.bump], + ]; + let signer_seeds = &[&seeds[..]]; + + let cpi_accounts = Transfer { + from: ctx.accounts.vault_token.to_account_info(), + to: ctx.accounts.user_token.to_account_info(), + authority: vault.to_account_info(), + }; + let cpi_ctx = CpiContext::new_with_signer( + ctx.accounts.token_program.to_account_info(), + cpi_accounts, + signer_seeds, + ); + token::transfer(cpi_ctx, amount)?; + + vault.total_deposits = vault.total_deposits + .checked_sub(amount) + .ok_or(MyError::Overflow)?; + + emit!(WithdrawEvent { + vault: vault.key(), + withdrawer: ctx.accounts.authority.key(), + amount, + remaining: vault.total_deposits, + }); + + Ok(()) + } +} + +#[derive(Accounts)] +#[instruction(vault_name: String)] +pub struct Initialize<'info> { + #[account( + init, + payer = authority, + space = 8 + Vault::INIT_SPACE, + seeds = [b"vault", authority.key().as_ref()], + bump, + )] + pub vault: Account<'info, Vault>, + #[account(mut)] + pub authority: Signer<'info>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct Deposit<'info> { + #[account( + mut, + seeds = [b"vault", authority.key().as_ref()], + bump = vault.bump, + has_one = authority, + )] + pub vault: Account<'info, Vault>, + #[account( + mut, + token::authority = authority, + )] + pub user_token: Account<'info, TokenAccount>, + #[account( + mut, + token::mint = user_token.mint, + token::authority = vault, + )] + pub vault_token: Account<'info, TokenAccount>, + pub authority: Signer<'info>, + pub token_program: Program<'info, Token>, +} + +#[derive(Accounts)] +pub struct Withdraw<'info> { + #[account( + mut, + seeds = [b"vault", authority.key().as_ref()], + bump = vault.bump, + has_one = authority, + )] + pub vault: Account<'info, Vault>, + #[account( + mut, + token::mint = user_token.mint, + token::authority = vault, + )] + pub vault_token: Account<'info, TokenAccount>, + #[account( + mut, + token::authority = authority, + )] + pub user_token: Account<'info, TokenAccount>, + pub authority: Signer<'info>, + pub token_program: Program<'info, Token>, +} + +#[account] +#[derive(InitSpace)] +pub struct Vault { + pub authority: Pubkey, + pub bump: u8, + #[max_len(32)] + pub name: String, + pub total_deposits: u64, +} + +#[event] +pub struct DepositEvent { + pub vault: Pubkey, + pub depositor: Pubkey, + pub amount: u64, + pub total: u64, +} + +#[event] +pub struct WithdrawEvent { + pub vault: Pubkey, + pub withdrawer: Pubkey, + pub amount: u64, + pub remaining: u64, +} + +#[error_code] +pub enum MyError { + #[msg("Amount must be greater than zero")] + ZeroAmount, + #[msg("Vault name exceeds 32 characters")] + NameTooLong, + #[msg("Insufficient balance in vault")] + InsufficientBalance, + #[msg("Arithmetic overflow")] + Overflow, +} From ed2020ba3ac11762f9a49c4e1be2e4f81cc28a5c Mon Sep 17 00:00:00 2001 From: 0xinit <28729137+0xinit@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:14:22 -0800 Subject: [PATCH 13/14] feat: redesign website with crimson theme and Unbounded font --- website/app/globals.css | 252 +++++++++++++++++++++++++---- website/app/layout.tsx | 26 ++- website/app/page.tsx | 146 +++++++++++++---- website/app/skills/[slug]/page.tsx | 210 +++++++++++++++--------- website/components/search.tsx | 34 +++- website/components/skill-card.tsx | 85 ++++++---- website/components/skill-grid.tsx | 157 ++++++++++-------- 7 files changed, 660 insertions(+), 250 deletions(-) diff --git a/website/app/globals.css b/website/app/globals.css index 3e7f314..990ee60 100644 --- a/website/app/globals.css +++ b/website/app/globals.css @@ -1,61 +1,259 @@ @import "tailwindcss"; @plugin "@tailwindcss/typography"; +@keyframes fade-up { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes pulse-glow { + 0%, 100% { opacity: 0.4; } + 50% { opacity: 1; } +} + :root { - --background: #09090b; - --foreground: #fafafa; - --card: #18181b; - --card-hover: #27272a; - --border: #27272a; - --muted: #a1a1aa; - --accent: #6366f1; - --accent-hover: #818cf8; + --bg: #06060a; + --surface: #0c0c12; + --surface-hover: #12121a; + --border: #1a1a24; + --border-hover: #2a2a36; + --text-primary: #e2e2e8; + --text-secondary: #6e6e7a; + --text-muted: #44444e; + --accent: #e8384f; + --accent-dim: #d42f45; + --accent-glow: rgba(232, 56, 79, 0.08); + --accent-border: rgba(232, 56, 79, 0.25); } body { - background: var(--background); - color: var(--foreground); - font-family: var(--font-sans), ui-sans-serif, system-ui, sans-serif; + background: var(--bg); + color: var(--text-primary); + font-family: var(--font-body), ui-sans-serif, system-ui, sans-serif; +} + +/* Noise texture overlay */ +body::before { + content: ""; + position: fixed; + inset: 0; + z-index: -1; + opacity: 0.025; + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='1'/%3E%3C/svg%3E"); + background-size: 256px; + pointer-events: none; +} + +/* Grid pattern background */ +.grid-bg { + position: relative; +} +.grid-bg::after { + content: ""; + position: fixed; + inset: 0; + z-index: -1; + pointer-events: none; + background-image: + linear-gradient(var(--border) 1px, transparent 1px), + linear-gradient(90deg, var(--border) 1px, transparent 1px); + background-size: 64px 64px; + opacity: 0.4; +} + +/* Staggered card animations */ +.card-animate { + animation: fade-up 0.5s ease both; +} + +/* Accent glow line */ +.glow-line { + position: relative; +} +.glow-line::after { + content: ""; + position: absolute; + bottom: -2px; + left: 0; + right: 0; + height: 1px; + background: linear-gradient(90deg, var(--accent), transparent 80%); + opacity: 0.3; +} + +/* Command palette search styling */ +.search-input { + background: var(--surface); + border: 1px solid var(--border); + transition: all 0.2s ease; +} + +.search-input:focus { + border-color: var(--accent-border); + box-shadow: 0 0 0 3px var(--accent-glow), inset 0 1px 2px rgba(0, 0, 0, 0.3); + outline: none; +} + +/* Card left accent border */ +.skill-card { + position: relative; + background: var(--surface); + border: 1px solid var(--border); + transition: all 0.25s ease; +} + +.skill-card::before { + content: ""; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 2px; + background: var(--accent); + opacity: 0; + transition: opacity 0.25s ease; +} + +.skill-card:hover { + background: var(--surface-hover); + border-color: var(--border-hover); + transform: translateY(-1px); +} + +.skill-card:hover::before { + opacity: 1; +} + +/* Filter pills */ +.filter-pill { + border: 1px solid var(--border); + color: var(--text-secondary); + transition: all 0.15s ease; + font-family: var(--font-mono), ui-monospace, monospace; + letter-spacing: 0.02em; +} + +.filter-pill:hover { + border-color: var(--border-hover); + color: var(--text-primary); +} + +.filter-pill-active { + border-color: var(--accent-border) !important; + background: var(--accent-glow) !important; + color: var(--accent) !important; +} + +/* Stat counter */ +.stat-value { + font-family: var(--font-mono), ui-monospace, monospace; + font-variant-numeric: tabular-nums; + letter-spacing: -0.02em; } /* Syntax highlighting overrides */ [data-rehype-pretty-code-figure] pre { - @apply overflow-x-auto rounded-lg border border-zinc-800 bg-zinc-950 px-4 py-3 text-sm leading-relaxed; + overflow-x: auto; + border-radius: 8px; + border: 1px solid var(--border); + background: #08080e !important; + padding: 16px 20px; + font-size: 13px; + line-height: 1.7; } [data-rehype-pretty-code-figure] code { - @apply grid; + display: grid; } [data-rehype-pretty-code-figure] [data-line] { - @apply px-1; + padding: 0 4px; } /* Inline code */ :not(pre) > code { - @apply rounded bg-zinc-800 px-1.5 py-0.5 text-sm text-zinc-200; + border-radius: 4px; + background: var(--surface); + border: 1px solid var(--border); + padding: 2px 6px; + font-size: 0.85em; + color: var(--accent-dim); + font-family: var(--font-mono), ui-monospace, monospace; } -/* Prose overrides for dark mode */ +/* Prose overrides */ .prose { - --tw-prose-body: #d4d4d8; - --tw-prose-headings: #fafafa; - --tw-prose-links: #818cf8; - --tw-prose-bold: #fafafa; - --tw-prose-code: #e4e4e7; - --tw-prose-pre-bg: #09090b; - --tw-prose-td-borders: #3f3f46; - --tw-prose-th-borders: #52525b; + --tw-prose-body: #b4b4be; + --tw-prose-headings: #e2e2e8; + --tw-prose-links: var(--accent-dim); + --tw-prose-bold: #e2e2e8; + --tw-prose-code: var(--accent-dim); + --tw-prose-pre-bg: #08080e; + --tw-prose-td-borders: var(--border); + --tw-prose-th-borders: var(--border-hover); +} + +.prose a { + text-decoration-color: rgba(212, 47, 69, 0.3); + text-underline-offset: 3px; + transition: text-decoration-color 0.15s ease; +} + +.prose a:hover { + text-decoration-color: var(--accent-dim); } .prose table { - @apply text-sm; + font-size: 0.85rem; + font-family: var(--font-body), ui-sans-serif, system-ui, sans-serif; } .prose th { - @apply text-left font-semibold text-zinc-200; + text-align: left; + font-weight: 600; + color: var(--text-primary); + font-family: var(--font-mono), ui-monospace, monospace; + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.05em; } .prose td { - @apply text-zinc-400; + color: var(--text-secondary); +} + +.prose h2 { + padding-bottom: 8px; + border-bottom: 1px solid var(--border); +} + +.prose h2 a, +.prose h3 a { + text-decoration: none; + color: inherit; +} + +/* Scrollbar */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: var(--border-hover); + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--text-muted); } diff --git a/website/app/layout.tsx b/website/app/layout.tsx index 3867fc9..2c1fea8 100644 --- a/website/app/layout.tsx +++ b/website/app/layout.tsx @@ -1,10 +1,23 @@ import type { Metadata } from "next"; -import { Inter } from "next/font/google"; +import { Unbounded, JetBrains_Mono, DM_Sans } from "next/font/google"; import "./globals.css"; -const inter = Inter({ +const unbounded = Unbounded({ subsets: ["latin"], - variable: "--font-sans", + variable: "--font-display", + display: "swap", +}); + +const dmSans = DM_Sans({ + subsets: ["latin"], + variable: "--font-body", + display: "swap", +}); + +const jetbrainsMono = JetBrains_Mono({ + subsets: ["latin"], + variable: "--font-mono", + display: "swap", }); export const metadata: Metadata = { @@ -13,7 +26,7 @@ export const metadata: Metadata = { template: "%s | CryptoSkills", }, description: - "Open-source agent skills directory covering 93 protocols across Ethereum, Solana, L2s, DeFi, NFTs, and more. Production-ready code for AI coding agents.", + "Open-source agent skills directory covering 96 protocols across Ethereum, Solana, L2s, DeFi, NFTs, and more. Production-ready code for AI coding agents.", metadataBase: new URL("https://cryptoskills.sh"), openGraph: { type: "website", @@ -31,7 +44,10 @@ export default function RootLayout({ children: React.ReactNode; }) { return ( - + {children} ); diff --git a/website/app/page.tsx b/website/app/page.tsx index 88ccad2..128b7d4 100644 --- a/website/app/page.tsx +++ b/website/app/page.tsx @@ -6,41 +6,121 @@ export default function Home() { const categories = getAllCategories(); const chains = getAllChains(); + const categoryCount = new Set(skills.map((s) => s.category)).size; + const chainCount = new Set(skills.map((s) => s.chain)).size; + return ( -
-
-

- CryptoSkills -

-

- Agent skills for all of crypto.{" "} - - {skills.length} protocols — production-ready code for AI coding - agents. +

+
+ {/* Header */} +
+
+
+

+ Crypto + Skills +

+

+ Open-source agent skills for all of crypto. Production-ready + protocol knowledge for AI coding agents. +

+
+ +
+ + {/* Stats bar */} +
+
+
+ {skills.length} +
+
+ Skills +
+
+
+
+ {categoryCount} +
+
+ Categories +
+
+
+
+ {chainCount} +
+
+ Chains +
+
+
+
+ + {/* Grid */} +
+ +
+
+ + {/* Footer */} +
+ + ); } diff --git a/website/app/skills/[slug]/page.tsx b/website/app/skills/[slug]/page.tsx index 85d59d3..0b29462 100644 --- a/website/app/skills/[slug]/page.tsx +++ b/website/app/skills/[slug]/page.tsx @@ -38,6 +38,25 @@ export async function generateMetadata({ }; } +const STAT_ITEMS = [ + { + key: "hasExamples" as const, + label: "Examples", + color: "text-emerald-400", + }, + { key: "hasDocs" as const, label: "Docs", color: "text-blue-400" }, + { + key: "hasResources" as const, + label: "Resources", + color: "text-amber-400", + }, + { + key: "hasTemplates" as const, + label: "Templates", + color: "text-violet-400", + }, +]; + export default async function SkillPage({ params, }: { @@ -64,85 +83,124 @@ export default async function SkillPage({ }; return ( -
-