diff --git a/README.md b/README.md index d9bd922..e5988c2 100644 --- a/README.md +++ b/README.md @@ -1,66 +1,373 @@ # MegaETH Token List -The official token registry for the MegaETH ecosystem. This repository maintains a curated list of ERC-20 tokens deployed on MegaETH and their corresponding Ethereum mainnet addresses for bridging. +The official token registry for the MegaETH ecosystem. This repository maintains a curated list of tokens deployed on MegaETH and their corresponding addresses on other chains for bridging and cross-chain tracking. + +The generated tokenlist follows the [Uniswap Token List](https://github.com/Uniswap/token-lists) standard with MegaETH-specific extensions for tracking bridge mechanics and token origins. ## Supported Chains -| Chain | Chain ID | Type | -| -------- | -------- | ---- | -| Ethereum | 1 | L1 | -| MegaETH | 4326 | L2 | +| Chain | Chain ID | Type | Description | +| -------- | -------- | ------ | --------------------------------------- | +| Ethereum | 1 | L1 | Ethereum mainnet | +| MegaETH | 4326 | L2 | MegaETH mainnet | +| Solana | - | Source | Non-EVM source chain for bridged assets | + +> **Note:** Only EVM chains (Ethereum, MegaETH) appear in the generated tokenlist. Non-EVM chains like Solana are tracked as source chains — their addresses appear in the `extensions` field. + +--- + +## Quick Start + +### Adding a Token + +1. Create a folder: `data/YOUR_TOKEN/` +2. Add `data.json` with token info +3. Add `logo.svg` or `logo.png` (256×256 recommended) +4. Submit a PR + +--- + +## Token Data Schema + +### Root Fields -## Adding a Token +| Field | Type | Required | Description | +| ------------- | ------ | -------- | ---------------------------------- | +| `name` | string | ✓ | Full token name | +| `symbol` | string | ✓ | Token ticker symbol | +| `decimals` | number | ✓ | Token decimal places | +| `description` | string | | Token description (max 1000 chars) | +| `website` | string | | Project website URL | -1. Create a folder under `data/` with your token symbol (e.g., `data/WETH/`) -2. Add a `data.json` file with token information -3. Add a `logo.svg` or `logo.png` file (256x256 recommended) +### Per-Chain Fields -### data.json Schema +Each chain entry in `tokens` supports: + +| Field | Type | Description | +| ----------- | ------- | ---------------------------------------------------------------- | +| `address` | string | Token contract address (required) | +| `isOrigin` | boolean | `true` if token was originally created on this chain | +| `mechanism` | string | How tokens move: `"native"` `"lock"` `"mint"` `"burn"` | +| `bridge` | string | Bridge contract address (lockbox if lock, endpoint if mint/burn) | +| `isOFT` | boolean | `true` if token is a LayerZero OFT | + +### Mechanism Types + +| Mechanism | Description | `bridge` field contains | +| --------- | ----------------------------------------- | ------------------------ | +| `native` | Token originated here, no bridge involved | (not needed) | +| `lock` | Tokens are locked here when bridging out | Lockbox contract address | +| `mint` | Tokens are minted here from another chain | Mint/bridge endpoint | +| `burn` | Tokens are burned here when bridging out | Burn/bridge endpoint | + +--- + +## Examples + +### 1. Native Token (Single Chain) + +A token that only exists on MegaETH: ```json { - "name": "Token Name", - "symbol": "TKN", + "name": "MEGA Token", + "symbol": "MEGA", + "decimals": 18, + "tokens": { + "megaeth": { + "address": "0x28B7E77f82B25B95953825F1E3eA0E36c1c29861", + "isOrigin": true, + "mechanism": "native" + } + } +} +``` + +**Output:** + +```json +{ + "chainId": 4326, + "symbol": "MEGA", + "extensions": { + "isOrigin": true, + "mechanism": "native", + "isOFT": "unknown" + } +} +``` + +--- + +### 2. Canonical Bridge (ETH) + +Native ETH bridged via the official MegaETH bridge: + +```json +{ + "name": "Ether", + "symbol": "ETH", + "decimals": 18, + "tokens": { + "ethereum": { + "address": "0x0000000000000000000000000000000000000000", + "isOrigin": true, + "mechanism": "lock", + "bridge": "0x0CA3A2FBC3D770b578223FBB6b062fa875a2eE75" + }, + "megaeth": { + "address": "0x0000000000000000000000000000000000000000", + "isOrigin": false, + "mechanism": "mint", + "bridge": "0x4200000000000000000000000000000000000010" + } + } +} +``` + +**Output (MegaETH):** + +```json +{ + "chainId": 4326, + "symbol": "ETH", + "extensions": { + "isOrigin": false, + "mechanism": "mint", + "isOFT": "unknown", + "originChain": "ethereum", + "originBridgeAddress": "0x0CA3A2FBC3D770b578223FBB6b062fa875a2eE75", + "originMechanism": "lock", + "bridgeAddress": "0x4200000000000000000000000000000000000010", + "bridgeType": "canonical" + } +} +``` + +--- + +### 3. OFT with Lockbox (CUSD) + +CUSD is an OFT that uses a lockbox on Ethereum. Tokens are locked on Ethereum, minted on MegaETH: + +```json +{ + "name": "Cap USD", + "symbol": "CUSD", "decimals": 18, "tokens": { "ethereum": { - "address": "0x..." + "address": "0xcCcc62962d17b8914c62D74FfB843d73B2a3cccC", + "isOrigin": true, + "mechanism": "lock", + "bridge": "0xA62571EbdFfAbC3051a2e5B9e1f57b23D830c8Fd", + "isOFT": true }, "megaeth": { - "address": "0x...", - "bridge": "0x..." + "address": "0xcCcc62962d17b8914c62D74FfB843d73B2a3cccC", + "isOrigin": false, + "mechanism": "mint", + "bridge": "0xOFTEndpoint...", + "isOFT": true } } } ``` -### Native vs Bridged Tokens +**Output (Ethereum - Origin):** -- **Native token**: No `bridge` field - token is native to that chain -- **Bridged token**: Has `bridge` field with the bridge contract address +```json +{ + "chainId": 1, + "symbol": "CUSD", + "extensions": { + "isOrigin": true, + "mechanism": "lock", + "isOFT": true, + "bridgeAddress": "0xA62571EbdFfAbC3051a2e5B9e1f57b23D830c8Fd", + "bridgeType": "others" + } +} +``` -Example for a bridged token on MegaETH: +**Output (MegaETH - Minted):** ```json { - "megaeth": { - "address": "0xTokenAddress...", - "bridge": "0xBridgeAddress..." + "chainId": 4326, + "symbol": "CUSD", + "extensions": { + "isOrigin": false, + "mechanism": "mint", + "isOFT": true, + "originChain": "ethereum", + "originBridgeAddress": "0xA62571EbdFfAbC3051a2e5B9e1f57b23D830c8Fd", + "originMechanism": "lock", + "bridgeAddress": "0xOFTEndpoint...", + "bridgeType": "others" } } ``` -### Optional Fields +> **For data trackers:** `originBridgeAddress` + `originMechanism: "lock"` tells you where backing tokens are locked. Don't double-count — tokens in the lockbox back the minted supply. -- `description` - Token description (max 1000 characters) -- `website` - Project website URL -- `bridge` - Bridge contract address (per-chain, indicates token is bridged) +--- -### Requirements +### 4. Pure OFT (Burn & Mint) -- Token must be deployed on at least one supported chain -- Logo must be SVG or PNG format, minimum 200x200px -- Addresses must be checksummed (EIP-55) +An OFT that burns on source and mints on destination (no lockbox): + +```json +{ + "name": "USDe", + "symbol": "USDe", + "decimals": 18, + "tokens": { + "ethereum": { + "address": "0x4c9edd5852cd905f086c759e8383e09bff1e68b3", + "isOrigin": true, + "mechanism": "burn", + "bridge": "0x5d3a1Ff2b6BAb83b63cd9AD0787074081a52ef34", + "isOFT": true + }, + "megaeth": { + "address": "0x5d3a1Ff2b6BAb83b63cd9AD0787074081a52ef34", + "isOrigin": false, + "mechanism": "mint", + "bridge": "0x5d3a1Ff2b6BAb83b63cd9AD0787074081a52ef34", + "isOFT": true + } + } +} +``` + +**Output (Ethereum):** + +```json +{ + "chainId": 1, + "symbol": "USDe", + "extensions": { + "isOrigin": true, + "mechanism": "burn", + "isOFT": true, + "bridgeAddress": "0x5d3a1Ff2b6BAb83b63cd9AD0787074081a52ef34", + "bridgeType": "others" + } +} +``` + +> **For data trackers:** `mechanism: "burn"` means total supply is conserved across chains. When tokens burn on Ethereum, they mint on MegaETH. + +--- + +### 5. Bridged from Non-EVM Chain (Solana) + +A token bridged from Solana via Wormhole: + +```json +{ + "name": "Wrapped SOL", + "symbol": "WSOL", + "decimals": 9, + "tokens": { + "solana": { + "address": "So11111111111111111111111111111111111111112" + }, + "megaeth": { + "address": "0x9a96E366F6b2ED5850A38B58D355a80aFD998411", + "isOrigin": false, + "mechanism": "mint", + "bridge": "0xWormholeBridge..." + } + } +} +``` + +**Output:** + +```json +{ + "chainId": 4326, + "symbol": "WSOL", + "extensions": { + "isOrigin": false, + "mechanism": "mint", + "isOFT": "unknown", + "originChain": "solana", + "bridgeAddress": "0xWormholeBridge...", + "bridgeType": "others", + "sourceChain": "solana", + "sourceAddress": "So11111111111111111111111111111111111111112" + } +} +``` + +--- + +## Extensions Reference + +The `extensions` object in the generated tokenlist: + +### Origin & Mechanism + +| Field | Type | Description | +| ----------------- | ---------------------- | ------------------------------------- | +| `isOrigin` | `boolean \| "unknown"` | Is this the canonical origin chain? | +| `originChain` | `string` | Which chain has the origin supply | +| `mechanism` | `string \| "unknown"` | `"native"` `"lock"` `"mint"` `"burn"` | +| `originMechanism` | `string` | Mechanism on origin chain | + +### Bridge Info + +| Field | Type | Description | +| --------------------- | -------- | ------------------------------------------------- | +| `bridgeAddress` | `string` | Bridge contract on this chain | +| `bridgeType` | `string` | `"canonical"` (official) or `"others"` | +| `originBridgeAddress` | `string` | Bridge contract on origin chain (lockbox if lock) | + +### Token Flags + +| Field | Type | Description | +| ------- | ---------------------- | ---------------------------------- | +| `isOFT` | `boolean \| "unknown"` | LayerZero Omnichain Fungible Token | + +### Non-EVM Source + +| Field | Type | Description | +| --------------- | -------- | ------------------------------------ | +| `sourceChain` | `string` | Source chain name (e.g., `"solana"`) | +| `sourceAddress` | `string` | Token address on source chain | + +--- + +## For Data Trackers + +This tokenlist helps track token movement across chains without double-counting: + +| Check | What it tells you | +| --------------------- | ---------------------------------------------------------------- | +| `isOrigin: true` | This chain holds the canonical supply | +| `mechanism: "lock"` | Tokens here are locked, backing supply elsewhere | +| `mechanism: "mint"` | Tokens here are minted, backed by locked/burned tokens elsewhere | +| `mechanism: "burn"` | Tokens burned here are minted elsewhere (supply conserved) | +| `originBridgeAddress` | Where to watch for locked/backing tokens | + +### Example: CUSD Supply Tracking + +``` +Ethereum (isOrigin: true, mechanism: lock) +├── Circulating: tokens held by users +└── Locked: tokens in bridgeAddress (backs MegaETH supply) + +MegaETH (isOrigin: false, mechanism: mint) +└── Minted: backed 1:1 by Ethereum locked tokens + +Total Supply = Ethereum Circulating + Ethereum Locked + = Ethereum Circulating + MegaETH Minted +``` + +--- ## Development @@ -81,49 +388,21 @@ pnpm install pnpm generate ``` -This creates `megaeth.tokenlist.json` in the project root. +### Run Tests -## Output Format +```bash +pnpm test +``` -The generated token list follows the [Uniswap Token List](https://github.com/Uniswap/token-lists) schema with extensions: +--- -```json -{ - "name": "MegaETH Token List", - "timestamp": "2025-01-05T00:00:00.000Z", - "version": { - "major": 1, - "minor": 0, - "patch": 0 - }, - "tokens": [ - { - "chainId": 1, - "address": "0x...", - "name": "Token Name", - "symbol": "TKN", - "decimals": 18, - "logoURI": "https://...", - "extensions": { - "isNative": true - } - }, - { - "chainId": 4326, - "address": "0x...", - "name": "Token Name", - "symbol": "TKN", - "decimals": 18, - "logoURI": "https://...", - "extensions": { - "isNative": false, - "bridgeAddress": "0x...", - "bridgeType": "canonical" - } - } - ] -} -``` +## Requirements + +- Token must be deployed on at least one supported chain +- Logo must be SVG or PNG, minimum 200×200px +- EVM addresses must be checksummed ([EIP-55](https://eips.ethereum.org/EIPS/eip-55)) + +--- ## License diff --git a/src/__tests__/generate.test.ts b/src/__tests__/generate.test.ts index 7bc60b8..be07497 100644 --- a/src/__tests__/generate.test.ts +++ b/src/__tests__/generate.test.ts @@ -58,12 +58,12 @@ describe('Token List Generation', () => { } }); - test('native tokens have isNative: true', () => { - const nativeTokens = tokenList.tokens.filter((t) => t.extensions.isNative); + test('native tokens have isOrigin: true', () => { + const nativeTokens = tokenList.tokens.filter((t) => t.extensions.isOrigin); expect(nativeTokens.length).toBeGreaterThan(0); for (const token of nativeTokens) { - expect(token.extensions.isNative).toBe(true); + expect(token.extensions.isOrigin).toBe(true); expect(token.extensions.bridgeAddress).toBeUndefined(); expect(token.extensions.bridgeType).toBeUndefined(); } @@ -91,6 +91,6 @@ describe('Token List Generation', () => { const wethTokens = tokenList.tokens.filter((t) => t.symbol === 'WETH'); expect(wethTokens.length).toBe(1); expect(wethTokens[0].chainId).toBe(4326); - expect(wethTokens[0].extensions.isNative).toBe(true); + expect(wethTokens[0].extensions.isOrigin).toBe(true); }); }); diff --git a/src/chains.ts b/src/chains.ts index 36e2758..ec09b7e 100644 --- a/src/chains.ts +++ b/src/chains.ts @@ -1,28 +1,41 @@ -// Chain configuration for MegaETH + Ethereum +// Chain configuration for MegaETH ecosystem export const CHAINS = { ethereum: { id: 1, name: 'Ethereum', layer: 1, + evmCompatible: true, }, megaeth: { id: 4326, name: 'MegaETH', layer: 2, + evmCompatible: true, + }, + solana: { + id: null, // Non-EVM chain, uses string identifier + name: 'Solana', + layer: 1, + evmCompatible: false, }, } as const export type Chain = keyof typeof CHAINS +export type EvmChain = 'ethereum' | 'megaeth' +export type SourceChain = 'solana' // Non-EVM chains used only for source tracking export type L1Chain = 'ethereum' export type L2Chain = 'megaeth' -// Chain ID lookup -export const CHAIN_IDS: Record = { +// Chain ID lookup (EVM chains only - used for tokenlist generation) +export const CHAIN_IDS: Record = { ethereum: 1, megaeth: 4326, } +// Source chains (non-EVM) - used for tracking bridged asset origins +export const SOURCE_CHAINS: readonly SourceChain[] = ['solana'] as const + // L2 to L1 mapping (for bridge relationships) export const L2_TO_L1: Record = { megaeth: 'ethereum', diff --git a/src/generate.ts b/src/generate.ts index 57f3b35..2bf5e22 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -1,7 +1,18 @@ import * as fs from 'fs' import * as path from 'path' -import { CHAIN_IDS, type Chain } from './chains' -import type { TokenData, TokenList, TokenListToken } from './types' +import { + CHAIN_IDS, + SOURCE_CHAINS, + type EvmChain, + type SourceChain, +} from './chains' +import type { + Mechanism, + Token, + TokenData, + TokenList, + TokenListToken, +} from './types' const DATA_DIR = path.join(__dirname, '..', 'data') const OUTPUT_FILE = path.join(__dirname, '..', 'megaeth.tokenlist.json') @@ -29,6 +40,69 @@ function readTokenData(symbol: string): TokenData { return JSON.parse(content) as TokenData } +// Find the origin chain info (chain name, bridge address, mechanism) +function findOriginInfo(tokenData: TokenData): { + chain: string + bridge?: string + mechanism?: Mechanism +} | null { + // First check EVM chains for explicit isOrigin + for (const [chain, chainToken] of Object.entries(tokenData.tokens)) { + if (chainToken?.isOrigin === true) { + return { + chain, + bridge: chainToken.bridge, + mechanism: chainToken.mechanism, + } + } + } + // Check non-EVM source chains + for (const [chain, chainToken] of Object.entries(tokenData.tokens)) { + if (SOURCE_CHAINS.includes(chain as SourceChain) && chainToken?.address) { + return { + chain, + bridge: undefined, + mechanism: undefined, + } + } + } + return null +} + +// Find source chain info (non-EVM chains like Solana) for a token +function findSourceChain( + tokenData: TokenData +): { chain: string; address: string } | null { + for (const [chain, chainToken] of Object.entries(tokenData.tokens)) { + if (SOURCE_CHAINS.includes(chain as SourceChain) && chainToken?.address) { + return { + chain, + address: chainToken.address, + } + } + } + return null +} + +// Infer mechanism if not explicitly set +function inferMechanism( + chainToken: Token, + isOrigin: boolean +): Mechanism | 'unknown' { + // Explicit mechanism takes precedence + if (chainToken.mechanism) { + return chainToken.mechanism + } + // Infer from other fields + if (isOrigin) { + return chainToken.bridge ? 'lock' : 'native' + } + // Non-origin chain + if (chainToken.isOFT) return 'burn' + if (chainToken.bridge) return 'mint' + return 'unknown' +} + export function generate(): TokenList { // Read all token directories const tokenDirs = fs @@ -45,33 +119,59 @@ export function generate(): TokenList { const tokenDir = path.join(DATA_DIR, symbol) const tokenData = readTokenData(symbol) const logoExt = getLogoExtension(tokenDir) + const sourceChain = findSourceChain(tokenData) + const originInfo = findOriginInfo(tokenData) - // Create token entries for each chain + // Create token entries for each EVM chain for (const [chain, chainToken] of Object.entries(tokenData.tokens)) { if (!chainToken?.address) continue - const chainId = CHAIN_IDS[chain as Chain] + const chainId = CHAIN_IDS[chain as EvmChain] if (!chainId) continue + const isOrigin = chainToken.isOrigin === true + const mechanism = inferMechanism(chainToken, isOrigin) + + // Build extensions object + const extensions: TokenListToken['extensions'] = { + isOrigin: chainToken.isOrigin ?? 'unknown', + mechanism, + isOFT: chainToken.isOFT ?? 'unknown', + } + + // Add origin chain info for non-origin tokens + if (!isOrigin && originInfo) { + extensions.originChain = originInfo.chain + // Include origin bridge info so trackers know where backing is + if (originInfo.bridge) { + extensions.originBridgeAddress = originInfo.bridge + } + if (originInfo.mechanism) { + extensions.originMechanism = originInfo.mechanism + } + } + + // Add bridge address for this chain + if (chainToken.bridge) { + extensions.bridgeAddress = chainToken.bridge + extensions.bridgeType = CANONICAL_BRIDGES.has(chainToken.bridge) + ? 'canonical' + : 'others' + } + + // Add source chain info if bridged from non-EVM chain + if (sourceChain) { + extensions.sourceChain = sourceChain.chain + extensions.sourceAddress = sourceChain.address + } + const token: TokenListToken = { chainId, address: chainToken.address, name: tokenData.name, symbol: tokenData.symbol, decimals: tokenData.decimals, - extensions: chainToken.bridge - ? { - isNative: chainToken.isNative ?? 'unknown', - isOFT: chainToken.isOFT ?? 'unknown', - bridgeAddress: chainToken.bridge, - bridgeType: CANONICAL_BRIDGES.has(chainToken.bridge) - ? 'canonical' - : 'others', - } - : { - isNative: chainToken.isNative ?? 'unknown', - isOFT: chainToken.isOFT ?? 'unknown', - }, + extensions, } if (logoExt) { diff --git a/src/types.ts b/src/types.ts index 2e69fed..32c88d5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,11 +1,18 @@ import type { Chain } from './chains' +// Bridge/token mechanism types +export type Mechanism = 'native' | 'lock' | 'mint' | 'burn' + // Token address on a specific chain export interface Token { address: string - bridge?: string // Bridge address if token is bridged (not native) - isNative?: boolean // True if token is native to the chain - isOFT?: boolean // True if token is OFT (LayerZero Omnichain Fungible Token) + // Origin and mechanism + isOrigin?: boolean // True if token was originally created on this chain + mechanism?: Mechanism // How tokens enter/exit this chain + // Bridge address (interpretation depends on mechanism) + bridge?: string // Lock: lockbox address | Mint/Burn: bridge endpoint + // Token type flags + isOFT?: boolean // True if token is a LayerZero OFT } // Token data.json schema @@ -18,12 +25,24 @@ export interface TokenData { tokens: Partial> } -// Token extensions for native/bridged status +// Token extensions in generated output export interface TokenExtensions { - isNative: boolean | 'unknown' - isOFT: boolean | 'unknown' - bridgeAddress?: string - bridgeType?: 'canonical' | 'others' + // Origin tracking + isOrigin: boolean | 'unknown' // Is this the canonical origin chain? + originChain?: string // Which chain has the origin supply + // Mechanism + mechanism: Mechanism | 'unknown' // How tokens move on this chain + // Bridge info (this chain) + bridgeAddress?: string // Bridge contract (lockbox if lock, endpoint if mint/burn) + bridgeType?: 'canonical' | 'others' // Official MegaETH bridge vs third-party + // Origin bridge info (for non-origin chains) + originBridgeAddress?: string // Bridge address on origin chain + originMechanism?: Mechanism // Mechanism on origin chain (usually 'lock' or 'burn') + // Token type flags + isOFT: boolean | 'unknown' // LayerZero OFT token + // Source chain for non-EVM bridged tokens + sourceChain?: string // e.g., "solana" + sourceAddress?: string // Address on source chain } // Uniswap TokenList standard types