diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb32885..d6fd06c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,4 +78,4 @@ jobs: run: uv sync - name: Run validation script - run: uv run python scripts/validate_tokens.py + run: uv run python scripts/validate_tokens.py --validate-cross-chain diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 90cd221..c0348e5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -138,6 +138,52 @@ The `data.json` file contains the following fields: - `extensions`: Additional metadata about the token - `coinGeckoId`: The CoinGecko API ID for the token (if listed on CoinGecko) - `bridgeInfo`: Bridge information for bridged tokens + - `crossChainAddresses`: Token addresses on other chains (see below) + +### Cross-Chain Addresses + +If your token exists on other chains, you can include those addresses to help with cross-chain token identification and bridging: + +```json +{ + "extensions": { + "crossChainAddresses": { + "1": { + "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "56": { + "address": "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d", + "decimals": 18 + }, + "8453": { + "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" + } + } + } +} +``` + +**Chain ID Reference:** + +| Supported Chain ID | Network | +| ------------------ | ----------------- | +| `1` | Ethereum Mainnet | +| `10` | Optimism | +| `56` | BNB Chain | +| `137` | Polygon | +| `999` | HyperEVM | +| `8453` | Base | +| `9745` | Plasma | +| `42161` | Arbitrum One | +| `43114` | Avalanche C-Chain | + +**Fields per chain entry:** + +- `address` (required): The token contract address on that chain +- `symbol` (optional): Expected symbol on this chain if it differs from the Monad token's symbol +- `decimals` (optional): Expected decimals on this chain if they differ from the Monad token's decimals + +**Overrides:** Some tokens have different metadata across chains. Use `symbol` or `decimals` overrides when the cross-chain token's on-chain values differ from the Monad token. ## Important Notes diff --git a/mainnet/AUSD/data.json b/mainnet/AUSD/data.json index ded24a9..07a15e4 100644 --- a/mainnet/AUSD/data.json +++ b/mainnet/AUSD/data.json @@ -9,6 +9,30 @@ "bridgeInfo": { "protocol": "LayerZero OFT", "bridgeAddress": "0x9CaB7Ede13dc56652E44D2404E969C212f22689b" + }, + "crossChainAddresses": { + "1": { + "address": "0x00000000eFE302BEAA2b3e6e1b18d08D69a9012a" + }, + "56": { + "address": "0x00000000eFE302BEAA2b3e6e1b18d08D69a9012a" + }, + "137": { + "address": "0x00000000eFE302BEAA2b3e6e1b18d08D69a9012a" + }, + "8453": { + "address": "0x00000000eFE302BEAA2b3e6e1b18d08D69a9012a" + }, + "9745": { + "address": "0x00000000eFE302BEAA2b3e6e1b18d08D69a9012a", + "symbol": "AUSD0" + }, + "42161": { + "address": "0x00000000eFE302BEAA2b3e6e1b18d08D69a9012a" + }, + "43114": { + "address": "0x00000000eFE302BEAA2b3e6e1b18d08D69a9012a" + } } } } diff --git a/mainnet/BTC.b/data.json b/mainnet/BTC.b/data.json index 93639b3..73857cd 100644 --- a/mainnet/BTC.b/data.json +++ b/mainnet/BTC.b/data.json @@ -5,9 +5,18 @@ "symbol": "BTC.b", "decimals": 8, "extensions": { + "coinGeckoId": "bitcoin-avalanche-bridged-btc-b", "bridgeInfo": { "protocol": "Chainlink CCIP", "bridgeAddress": "0x33566fE5976AAa420F3d5C64996641Fc3858CaDB" + }, + "crossChainAddresses": { + "1": { + "address": "0xB0F70C0bD6FD87dbEb7C10dC692a2a6106817072" + }, + "43114": { + "address": "0x152b9d0FdC40C096757F570A51E494bd4b943E50" + } } } } diff --git a/mainnet/Cake/data.json b/mainnet/Cake/data.json index fd7e9bc..fad67a5 100644 --- a/mainnet/Cake/data.json +++ b/mainnet/Cake/data.json @@ -9,6 +9,20 @@ "bridgeInfo": { "protocol": "LayerZero OFT", "bridgeAddress": "0xF59D81cd43f620E722E07f9Cb3f6E41B031017a3" + }, + "crossChainAddresses": { + "1": { + "address": "0x152649eA73beAb28c5b49B26eb48f7EAD6d4c898" + }, + "56": { + "address": "0x0E09FaBB73Bd3Ade0a17ECC321fD13a19e81cE82" + }, + "8453": { + "address": "0x3055913c90Fcc1A6CE9a358911721eEb942013A1" + }, + "42161": { + "address": "0x1b896893dfc86bb67Cf57767298b9073D2c1bA2c" + } } } } diff --git a/mainnet/EUL/data.json b/mainnet/EUL/data.json index 350483c..3f46499 100644 --- a/mainnet/EUL/data.json +++ b/mainnet/EUL/data.json @@ -9,6 +9,23 @@ "bridgeInfo": { "protocol": "LayerZero OFT", "bridgeAddress": "0x831257BFa5478111d2327e08c4068ec37Ac14B81" + }, + "crossChainAddresses": { + "1": { + "address": "0xd9Fcd98c322942075A5C3860693e9f4f03AAE07b" + }, + "56": { + "address": "0x2117E8b79e8E176A670c9fCf945d4348556bfFad" + }, + "8453": { + "address": "0xa153Ad732F831a79b5575Fa02e793EC4E99181b0" + }, + "42161": { + "address": "0x462cD9E0247b2e63831c3189aE738E5E9a5a4b64" + }, + "43114": { + "address": "0x9ceeD3A7f753608372eeAb300486cc7c2F38AC68" + } } } } diff --git a/mainnet/FOLKS/data.json b/mainnet/FOLKS/data.json index bcd6aba..698c538 100644 --- a/mainnet/FOLKS/data.json +++ b/mainnet/FOLKS/data.json @@ -5,6 +5,30 @@ "symbol": "FOLKS", "decimals": 6, "extensions": { - "coinGeckoId": "folks" + "coinGeckoId": "folks", + "bridgeInfo": { + "protocol": "Wormhole NTT", + "bridgeAddress": "0x93FE94Ad887a1B04DBFf1f736bfcD1698D4cfF66" + }, + "crossChainAddresses": { + "1": { + "address": "0xFF7F8F301F7A706E3CfD3D2275f5dc0b9EE8009B" + }, + "56": { + "address": "0xFF7F8F301F7A706E3CfD3D2275f5dc0b9EE8009B" + }, + "137": { + "address": "0xFF7F8F301F7A706E3CfD3D2275f5dc0b9EE8009B" + }, + "8453": { + "address": "0xFF7F8F301F7A706E3CfD3D2275f5dc0b9EE8009B" + }, + "42161": { + "address": "0xFF7F8F301F7A706E3CfD3D2275f5dc0b9EE8009B" + }, + "43114": { + "address": "0xFF7F8F301F7A706E3CfD3D2275f5dc0b9EE8009B" + } + } } } diff --git a/mainnet/LBTC/data.json b/mainnet/LBTC/data.json index 26d5450..bfa2280 100644 --- a/mainnet/LBTC/data.json +++ b/mainnet/LBTC/data.json @@ -9,6 +9,20 @@ "bridgeInfo": { "protocol": "Chainlink CCIP", "bridgeAddress": "0x33566fE5976AAa420F3d5C64996641Fc3858CaDB" + }, + "crossChainAddresses": { + "1": { + "address": "0x8236a87084f8B84306f72007F36F2618A5634494" + }, + "56": { + "address": "0xecAc9C5F704e954931349Da37F60E39f515c11c1" + }, + "8453": { + "address": "0xecAc9C5F704e954931349Da37F60E39f515c11c1" + }, + "43114": { + "address": "0xecAc9C5F704e954931349Da37F60E39f515c11c1" + } } } } diff --git a/mainnet/NXPC/data.json b/mainnet/NXPC/data.json index 54715cd..eea369e 100644 --- a/mainnet/NXPC/data.json +++ b/mainnet/NXPC/data.json @@ -9,6 +9,11 @@ "bridgeInfo": { "protocol": "Chainlink CCIP", "bridgeAddress": "0x33566fE5976AAa420F3d5C64996641Fc3858CaDB" + }, + "crossChainAddresses": { + "43114": { + "address": "0x5E0E90E268BC247Cc850c789A0DB0d5c7621fb59" + } } } } diff --git a/mainnet/SolvBTC/data.json b/mainnet/SolvBTC/data.json index ea2bd07..6058fff 100644 --- a/mainnet/SolvBTC/data.json +++ b/mainnet/SolvBTC/data.json @@ -9,6 +9,26 @@ "bridgeInfo": { "protocol": "Chainlink CCIP", "bridgeAddress": "0x33566fE5976AAa420F3d5C64996641Fc3858CaDB" + }, + "crossChainAddresses": { + "1": { + "address": "0x7A56E1C57C7475CCf742a1832B028F0456652F97" + }, + "56": { + "address": "0x4aae823a6a0b376De6A78e74eCC5b079d38cBCf7" + }, + "137": { + "address": "0xaE4EFbc7736f963982aACb17EFA37fCBAb924cB3" + }, + "8453": { + "address": "0x3B86Ad95859b6AB773f55f8d94B4b9d443EE931f" + }, + "42161": { + "address": "0x3647c54c4c2C65bC7a2D63c0Da2809B399DBBDC0" + }, + "43114": { + "address": "0xbc78D84Ba0c46dFe32cf2895a19939c86b81a777" + } } } } diff --git a/mainnet/USD1/data.json b/mainnet/USD1/data.json index b2b85ef..989425a 100644 --- a/mainnet/USD1/data.json +++ b/mainnet/USD1/data.json @@ -9,6 +9,16 @@ "bridgeInfo": { "protocol": "Chainlink CCIP", "bridgeAddress": "0x33566fE5976AAa420F3d5C64996641Fc3858CaDB" + }, + "crossChainAddresses": { + "1": { + "address": "0x8d0D000Ee44948FC98c9B98A4FA4921476f08B0d", + "decimals": 18 + }, + "56": { + "address": "0x8d0D000Ee44948FC98c9B98A4FA4921476f08B0d", + "decimals": 18 + } } } } diff --git a/mainnet/USDC/data.json b/mainnet/USDC/data.json index 35f4880..6534b1a 100644 --- a/mainnet/USDC/data.json +++ b/mainnet/USDC/data.json @@ -9,6 +9,33 @@ "bridgeInfo": { "protocol": "Circle CCTP", "bridgeAddress": "0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d" + }, + "crossChainAddresses": { + "1": { + "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "10": { + "address": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85" + }, + "56": { + "address": "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d", + "decimals": 18 + }, + "137": { + "address": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359" + }, + "999": { + "address": "0xb88339CB7199b77E23DB6E890353E22632Ba630f" + }, + "8453": { + "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" + }, + "42161": { + "address": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831" + }, + "43114": { + "address": "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E" + } } } } diff --git a/mainnet/USDT0/data.json b/mainnet/USDT0/data.json index b5b0ec8..c3cde82 100644 --- a/mainnet/USDT0/data.json +++ b/mainnet/USDT0/data.json @@ -9,6 +9,28 @@ "bridgeInfo": { "protocol": "LayerZero OFT", "bridgeAddress": "0x9151434b16b9763660705744891fA906F660EcC5" + }, + "crossChainAddresses": { + "1": { + "address": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + "symbol": "USDT" + }, + "10": { + "address": "0x01bFF41798a0BcF287b996046Ca68b395DbC1071", + "symbol": "USD₮0" + }, + "999": { + "address": "0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb", + "symbol": "USD₮0" + }, + "9745": { + "address": "0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb", + "symbol": "USDT0" + }, + "42161": { + "address": "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", + "symbol": "USD₮0" + } } } } diff --git a/mainnet/WBTC/data.json b/mainnet/WBTC/data.json index 17605da..6e13b8e 100644 --- a/mainnet/WBTC/data.json +++ b/mainnet/WBTC/data.json @@ -9,6 +9,26 @@ "bridgeInfo": { "protocol": "LayerZero OFT", "bridgeAddress": "0x0555E30da8f98308EdB960aa94C0Db47230d2B9c" + }, + "crossChainAddresses": { + "1": { + "address": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" + }, + "10": { + "address": "0x0555E30da8f98308EdB960aa94C0Db47230d2B9c" + }, + "56": { + "address": "0x0555E30da8f98308EdB960aa94C0Db47230d2B9c" + }, + "8453": { + "address": "0x0555E30da8f98308EdB960aa94C0Db47230d2B9c" + }, + "42161": { + "address": "0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f" + }, + "43114": { + "address": "0x0555E30da8f98308EdB960aa94C0Db47230d2B9c" + } } } } diff --git a/mainnet/WETH/data.json b/mainnet/WETH/data.json index c2e0f8e..313338d 100644 --- a/mainnet/WETH/data.json +++ b/mainnet/WETH/data.json @@ -9,6 +9,11 @@ "bridgeInfo": { "protocol": "Wormhole NTT", "bridgeAddress": "0x92957b3D0CaB3eA7110fEd1ccc4eF564981a59Fc" + }, + "crossChainAddresses": { + "1": { + "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + } } } } diff --git a/mainnet/WMON/data.json b/mainnet/WMON/data.json index cdb6e68..c43a328 100644 --- a/mainnet/WMON/data.json +++ b/mainnet/WMON/data.json @@ -5,6 +5,15 @@ "symbol": "WMON", "decimals": 18, "extensions": { - "coinGeckoId": "wrapped-monad" + "coinGeckoId": "wrapped-monad", + "bridgeInfo": { + "protocol": "Wormhole NTT", + "bridgeAddress": "0xFEA937F7124E19124671f1685671d3f04a9Af4E4" + }, + "crossChainAddresses": { + "1": { + "address": "0x6917037F8944201b2648198a89906Edf863B9517" + } + } } } diff --git a/mainnet/XAUt0/data.json b/mainnet/XAUt0/data.json index 8907ccd..0644b74 100644 --- a/mainnet/XAUt0/data.json +++ b/mainnet/XAUt0/data.json @@ -9,6 +9,26 @@ "bridgeInfo": { "protocol": "LayerZero OFT", "bridgeAddress": "0x21cAef8A43163Eea865baeE23b9C2E327696A3bf" + }, + "crossChainAddresses": { + "1": { + "address": "0x68749665FF8D2d112Fa859AA293F07A622782F38", + "symbol": "XAUt" + }, + "137": { + "address": "0xF1815bd50389c46847f0Bda824eC8da914045D14" + }, + "999": { + "address": "0xf4D9235269a96aaDaFc9aDAe454a0618eBE37949", + "symbol": "XAUt0" + }, + "9745": { + "address": "0x1B64B9025EEbb9A6239575dF9Ea4b9Ac46D4d193", + "symbol": "XAUt0" + }, + "43114": { + "address": "0x2775d5105276781B4b85bA6eA6a6653bEeD1dd32" + } } } } diff --git a/mainnet/aprMON/data.json b/mainnet/aprMON/data.json index ad69bc1..b1bad41 100644 --- a/mainnet/aprMON/data.json +++ b/mainnet/aprMON/data.json @@ -3,5 +3,16 @@ "address": "0x0c65A0BC65a5D819235B71F554D210D3F80E0852", "name": "aPriori Monad LST", "symbol": "aprMON", - "decimals": 18 + "decimals": 18, + "extensions": { + "bridgeInfo": { + "protocol": "Chainlink CCIP", + "bridgeAddress": "0x33566fE5976AAa420F3d5C64996641Fc3858CaDB" + }, + "crossChainAddresses": { + "1": { + "address": "0x93783ccd94763e11B9a57394e63Ddd9CeedaD925" + } + } + } } diff --git a/mainnet/earnAUSD/data.json b/mainnet/earnAUSD/data.json index 409390f..4840a47 100644 --- a/mainnet/earnAUSD/data.json +++ b/mainnet/earnAUSD/data.json @@ -5,6 +5,15 @@ "symbol": "earnAUSD", "decimals": 6, "extensions": { - "coinGeckoId": "earnausd" + "coinGeckoId": "earnausd", + "bridgeInfo": { + "protocol": "LayerZero OFT", + "bridgeAddress": "0x103222f020e98Bba0AD9809A011FDF8e6F067496" + }, + "crossChainAddresses": { + "1": { + "address": "0xa6916b65c5e3fEdf46c0a2F59bff776e872C8992" + } + } } } diff --git a/mainnet/ezETH/data.json b/mainnet/ezETH/data.json index 4fdd07c..4eb708e 100644 --- a/mainnet/ezETH/data.json +++ b/mainnet/ezETH/data.json @@ -9,6 +9,20 @@ "bridgeInfo": { "protocol": "Hyperlane Warp Route", "bridgeAddress": "0x7A911b0bD4F067FB8DAFF734A78E7A72865100d8" + }, + "crossChainAddresses": { + "1": { + "address": "0xbf5495Efe5DB9ce00f80364C8B423567e58d2110" + }, + "56": { + "address": "0x2416092f143378750bb29b79eD961ab195CcEea5" + }, + "8453": { + "address": "0x2416092f143378750bb29b79eD961ab195CcEea5" + }, + "42161": { + "address": "0x2416092f143378750bb29b79eD961ab195CcEea5" + } } } } diff --git a/mainnet/mHYPER/data.json b/mainnet/mHYPER/data.json index 32058f9..eb05178 100644 --- a/mainnet/mHYPER/data.json +++ b/mainnet/mHYPER/data.json @@ -9,6 +9,11 @@ "bridgeInfo": { "protocol": "LayerZero OFT", "bridgeAddress": "0x08BC5Ef2E2Afe697614bf3A9eaA71dcBb577f2Df" + }, + "crossChainAddresses": { + "1": { + "address": "0x9b5528528656DBC094765E2abB79F293c21191B9" + } } } } diff --git a/mainnet/pufETH/data.json b/mainnet/pufETH/data.json index df241eb..9d210b6 100644 --- a/mainnet/pufETH/data.json +++ b/mainnet/pufETH/data.json @@ -9,6 +9,17 @@ "bridgeInfo": { "protocol": "LayerZero OFT", "bridgeAddress": "0x37D6382B6889cCeF8d6871A8b60E667115eDDBcF" + }, + "crossChainAddresses": { + "1": { + "address": "0xD9A442856C234a39a81a089C06451EBAa4306a72" + }, + "999": { + "address": "0x87d00066cf131ff54B72B134a217D5401E5392b6" + }, + "8453": { + "address": "0x30D91DF53cCCf07e3a5BF6862Db8CFBe1fCB21d3" + } } } } diff --git a/mainnet/syzUSD/data.json b/mainnet/syzUSD/data.json index ee53adc..af72878 100644 --- a/mainnet/syzUSD/data.json +++ b/mainnet/syzUSD/data.json @@ -9,6 +9,14 @@ "bridgeInfo": { "protocol": "LayerZero OFT", "bridgeAddress": "0x484be0540aD49f351eaa04eeB35dF0f937D4E73f" + }, + "crossChainAddresses": { + "1": { + "address": "0x6DFF69eb720986E98Bb3E8b26cb9E02Ec1a35D12" + }, + "9745": { + "address": "0xC8A8DF9B210243c55D31c73090F06787aD0A1Bf6" + } } } } diff --git a/mainnet/thBILL/data.json b/mainnet/thBILL/data.json index af276c2..3b87fd0 100644 --- a/mainnet/thBILL/data.json +++ b/mainnet/thBILL/data.json @@ -9,6 +9,17 @@ "bridgeInfo": { "protocol": "LayerZero OFT", "bridgeAddress": "0xfDD22Ce6D1F66bc0Ec89b20BF16CcB6670F55A5a" + }, + "crossChainAddresses": { + "1": { + "address": "0x5FA487BCa6158c64046B2813623e20755091DA0b" + }, + "999": { + "address": "0xfDD22Ce6D1F66bc0Ec89b20BF16CcB6670F55A5a" + }, + "8453": { + "address": "0xfDD22Ce6D1F66bc0Ec89b20BF16CcB6670F55A5a" + } } } } diff --git a/mainnet/weETH/data.json b/mainnet/weETH/data.json index 10bb2aa..5a8c56b 100644 --- a/mainnet/weETH/data.json +++ b/mainnet/weETH/data.json @@ -9,6 +9,32 @@ "bridgeInfo": { "protocol": "LayerZero OFT", "bridgeAddress": "0xA3D68b74bF0528fdD07263c60d6488749044914b" + }, + "crossChainAddresses": { + "1": { + "address": "0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee" + }, + "10": { + "address": "0x5A7fACB970D094B6C7FF1df0eA68D99E6e73CBFF" + }, + "56": { + "address": "0x04C0599Ae5A44757c0af6F9eC3b93da8976c150A" + }, + "999": { + "address": "0xA3D68b74bF0528fdD07263c60d6488749044914b" + }, + "8453": { + "address": "0x04C0599Ae5A44757c0af6F9eC3b93da8976c150A" + }, + "9745": { + "address": "0xA3D68b74bF0528fdD07263c60d6488749044914b" + }, + "42161": { + "address": "0x35751007a407ca6FEFfE80b3cB397736D2cf4dbe" + }, + "43114": { + "address": "0xA3D68b74bF0528fdD07263c60d6488749044914b" + } } } } diff --git a/mainnet/wsrUSD/data.json b/mainnet/wsrUSD/data.json index 9e2217b..b831d50 100644 --- a/mainnet/wsrUSD/data.json +++ b/mainnet/wsrUSD/data.json @@ -9,6 +9,29 @@ "bridgeInfo": { "protocol": "LayerZero OFT", "bridgeAddress": "0x4809010926aec940b550D34a46A52739f996D75D" + }, + "crossChainAddresses": { + "1": { + "address": "0xd3fD63209FA2D55B07A0f6db36C2f43900be3094" + }, + "56": { + "address": "0x4809010926aec940b550D34a46A52739f996D75D" + }, + "999": { + "address": "0x04716DB62C085D9e08050fcF6F7D775A03d07720" + }, + "8453": { + "address": "0x62344be8CA1c339B46274a4017dd87AF436900B1" + }, + "9745": { + "address": "0x4809010926aec940b550D34a46A52739f996D75D" + }, + "42161": { + "address": "0x4809010926aec940b550D34a46A52739f996D75D" + }, + "43114": { + "address": "0x4809010926aec940b550D34a46A52739f996D75D" + } } } } diff --git a/mainnet/wstETH/data.json b/mainnet/wstETH/data.json index 07f5546..c036c83 100644 --- a/mainnet/wstETH/data.json +++ b/mainnet/wstETH/data.json @@ -9,6 +9,14 @@ "bridgeInfo": { "protocol": "Chainlink CCIP", "bridgeAddress": "0x33566fE5976AAa420F3d5C64996641Fc3858CaDB" + }, + "crossChainAddresses": { + "1": { + "address": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0" + }, + "9745": { + "address": "0x481e638105407Be40c2f2E2e006DE272d05930d0" + } } } } diff --git a/mainnet/xSolvBTC/data.json b/mainnet/xSolvBTC/data.json index d8050d8..5f1ad4a 100644 --- a/mainnet/xSolvBTC/data.json +++ b/mainnet/xSolvBTC/data.json @@ -9,6 +9,29 @@ "bridgeInfo": { "protocol": "Chainlink CCIP", "bridgeAddress": "0x33566fE5976AAa420F3d5C64996641Fc3858CaDB" + }, + "crossChainAddresses": { + "1": { + "address": "0xd9D920AA40f578ab794426F5C90F6C731D159DEf" + }, + "56": { + "address": "0x1346b618dC92810EC74163e4c27004c921D446a5" + }, + "137": { + "address": "0xc99F5c922DAE05B6e2ff83463ce705eF7C91F077" + }, + "999": { + "address": "0xc99F5c922DAE05B6e2ff83463ce705eF7C91F077" + }, + "8453": { + "address": "0xC26C9099BD3789107888c35bb41178079B282561" + }, + "42161": { + "address": "0x346c574C56e1A4aAa8dc88Cda8F7EB12b39947aB" + }, + "43114": { + "address": "0xCC0966D8418d412c599A6421b760a847eB169A8c" + } } } } diff --git a/mainnet/yzPP/data.json b/mainnet/yzPP/data.json index ba9d7a6..80efbe7 100644 --- a/mainnet/yzPP/data.json +++ b/mainnet/yzPP/data.json @@ -9,6 +9,11 @@ "bridgeInfo": { "protocol": "LayerZero OFT", "bridgeAddress": "0xb37476cB1F6111cC682b107B747b8652f90B0984" + }, + "crossChainAddresses": { + "9745": { + "address": "0xEbFC8C2Fe73C431Ef2A371AeA9132110aaB50DCa" + } } } } diff --git a/mainnet/yzUSD/data.json b/mainnet/yzUSD/data.json index b4becff..63cadab 100644 --- a/mainnet/yzUSD/data.json +++ b/mainnet/yzUSD/data.json @@ -9,6 +9,14 @@ "bridgeInfo": { "protocol": "LayerZero OFT", "bridgeAddress": "0x9dcB0D17eDDE04D27F387c89fECb78654C373858" + }, + "crossChainAddresses": { + "1": { + "address": "0x387167e5C088468906Bcd67C06746409a8E44abA" + }, + "9745": { + "address": "0x6695c0f8706C5ACe3Bdf8995073179cCA47926dc" + } } } } diff --git a/scripts/utils/web3.py b/scripts/utils/web3.py index 0f30f58..964d2ad 100644 --- a/scripts/utils/web3.py +++ b/scripts/utils/web3.py @@ -14,6 +14,31 @@ CHAIN_ID = 143 DEFAULT_RPC_URL = "https://rpc.monad.xyz" RPC_URL = os.environ.get("MONAD_RPC_URL", DEFAULT_RPC_URL) + +# Chain RPC configuration with environment variable overrides +CHAIN_RPC_URLS = { + "1": os.environ.get("ETH_RPC_URL", "https://ethereum-rpc.publicnode.com"), + "10": os.environ.get("OPTIMISM_RPC_URL", "https://mainnet.optimism.io"), + "56": os.environ.get("BSC_RPC_URL", "https://bsc-dataseed.binance.org"), + "137": os.environ.get("POLYGON_RPC_URL", "https://polygon-rpc.com"), + "999": os.environ.get("HYPEREVM_RPC_URL", "https://rpc.hyperliquid.xyz/evm"), + "8453": os.environ.get("BASE_RPC_URL", "https://mainnet.base.org"), + "9745": os.environ.get("PLASMA_RPC_URL", "https://rpc.plasma.to"), + "42161": os.environ.get("ARBITRUM_RPC_URL", "https://arb1.arbitrum.io/rpc"), + "43114": os.environ.get("AVALANCHE_RPC_URL", "https://api.avax.network/ext/bc/C/rpc"), +} + +CHAIN_NAMES = { + "1": "Ethereum", + "10": "Optimism", + "56": "BNB Chain", + "137": "Polygon", + "999": "HyperEVM", + "8453": "Base", + "9745": "Plasma", + "42161": "Arbitrum One", + "43114": "Avalanche", +} ERC20_ABI = [ { "constant": True, @@ -104,6 +129,29 @@ def get_web3_connection(rpc_url: Optional[str] = None) -> Web3: return web3 +def get_web3_connection_for_chain(chain_id: str) -> Optional[Web3]: + """Get a Web3 connection for a specific chain. + + Args: + chain_id: The chain ID as a string (e.g., "1" for Ethereum). + + Returns: + Web3: Connected Web3 instance, or None if chain is not supported + or connection fails. + """ + if chain_id not in CHAIN_RPC_URLS: + return None + + rpc_url = CHAIN_RPC_URLS[chain_id] + try: + web3 = Web3(Web3.HTTPProvider(rpc_url)) + if not web3.is_connected(): + return None + return web3 + except Exception: + return None + + def validate_address(address: str) -> str: """Validate and normalize an Ethereum address. diff --git a/scripts/validate_tokens.py b/scripts/validate_tokens.py index 27358e4..c383544 100644 --- a/scripts/validate_tokens.py +++ b/scripts/validate_tokens.py @@ -10,17 +10,21 @@ import re import sys import xml.etree.ElementTree as ET +from concurrent.futures import ThreadPoolExecutor, as_completed from pathlib import Path from typing import Any, Optional import json5 from PIL import Image from utils.web3 import ( + CHAIN_NAMES, + CHAIN_RPC_URLS, DEFAULT_RPC_URL, fetch_token_decimals_with_retry, fetch_token_name_with_retry, fetch_token_symbol_with_retry, get_web3_connection, + get_web3_connection_for_chain, ) from web3 import Web3 @@ -29,6 +33,19 @@ ALLOWED_EXTENSIONS = { "coinGeckoId": str, "bridgeInfo": dict, + "crossChainAddresses": dict, +} +# Known chain IDs for cross-chain address validation +KNOWN_CHAIN_IDS = { + "1", # Ethereum Mainnet + "10", # Optimism + "56", # BNB Chain + "137", # Polygon + "999", # HyperEVM + "8453", # Base + "9745", # Plasma + "42161", # Arbitrum One + "43114", # Avalanche C-Chain } VALID_BRIDGE_PROTOCOLS = { "Chainlink CCIP", @@ -133,6 +150,175 @@ def validate_bridge_info(bridge_info: dict[str, Any]) -> list[str]: return errors +def validate_cross_chain_addresses(cross_chain: dict[str, Any]) -> list[str]: + """Validate the crossChainAddresses extension. + + Args: + cross_chain: The crossChainAddresses dictionary to validate. + + Returns: + list[str]: List of error messages. Empty list if validation passes. + """ + errors = [] + allowed_fields = {"address", "symbol", "decimals"} + + for chain_id, chain_data in cross_chain.items(): + # Validate chain ID format + if chain_id not in KNOWN_CHAIN_IDS: + errors.append(f"Invalid chain ID '{chain_id}' in crossChainAddresses. ") + continue + + if not isinstance(chain_data, dict): + errors.append( + f"Invalid type for crossChainAddresses[{chain_id}]: " + f"expected dict, got {type(chain_data).__name__}" + ) + continue + + # Check for required address field + if "address" not in chain_data: + errors.append(f"Missing required field 'address' in crossChainAddresses[{chain_id}]") + else: + address = chain_data["address"] + # For EVM chains, validate address format + if not is_valid_address(address): + errors.append(f"Invalid address in crossChainAddresses[{chain_id}]: {address}") + + # Validate optional symbol field type + if "symbol" in chain_data: + symbol = chain_data["symbol"] + if not isinstance(symbol, str) or not symbol.strip(): + errors.append( + f"Invalid symbol in crossChainAddresses[{chain_id}]: must be a non-empty string" + ) + + # Validate optional decimals field type + if "decimals" in chain_data: + decimals = chain_data["decimals"] + if not isinstance(decimals, int) or not (MIN_DECIMALS <= decimals <= MAX_DECIMALS): + errors.append( + f"Invalid decimals in crossChainAddresses[{chain_id}]: " + f"must be an integer between {MIN_DECIMALS} and {MAX_DECIMALS}" + ) + + # Check for unknown fields + actual_fields = set(chain_data.keys()) + unknown_fields = actual_fields - allowed_fields + if unknown_fields: + errors.append( + f"Unknown fields in crossChainAddresses[{chain_id}]: {', '.join(unknown_fields)}" + ) + + return errors + + +def validate_single_cross_chain_address( + chain_id: str, + address: str, + expected_symbol: str, + expected_decimals: int, +) -> tuple[list[str], list[str]]: + """Validate a single cross-chain address against expected metadata. + + Args: + chain_id: The chain ID as a string. + address: The token address on the remote chain. + expected_symbol: Expected token symbol. + expected_decimals: Expected decimals. + + Returns: + tuple[list[str], list[str]]: (errors, warnings) + """ + errors = [] + warnings = [] + chain_name = CHAIN_NAMES.get(chain_id, f"Chain {chain_id}") + + web3 = get_web3_connection_for_chain(chain_id) + if web3 is None: + warnings.append(f"Could not connect to {chain_name} RPC") + return errors, warnings + + # Fetch and validate symbol + try: + actual_symbol = fetch_token_symbol_with_retry(web3, address) + if actual_symbol != expected_symbol: + errors.append( + f"Cross-chain symbol mismatch on {chain_name}: " + f"expected '{expected_symbol}', got '{actual_symbol}'" + ) + except Exception as e: + warnings.append(f"Failed to fetch symbol from {chain_name}: {e}") + return errors, warnings + + # Fetch and validate decimals + try: + actual_decimals = fetch_token_decimals_with_retry(web3, address) + if actual_decimals != expected_decimals: + errors.append( + f"Cross-chain decimals mismatch on {chain_name}: " + f"expected {expected_decimals}, got {actual_decimals}" + ) + except Exception as e: + warnings.append(f"Failed to fetch decimals from {chain_name}: {e}") + + return errors, warnings + + +def validate_cross_chain_metadata( + data: dict[str, Any], + max_workers: int = 4, +) -> tuple[list[str], list[str]]: + """Validate cross-chain addresses have matching metadata. + + Args: + data: The token data dictionary. + max_workers: Maximum number of parallel workers for RPC calls. + + Returns: + tuple[list[str], list[str]]: (errors, warnings) + """ + errors = [] + warnings = [] + + extensions = data.get("extensions", {}) + cross_chain = extensions.get("crossChainAddresses", {}) + + if not cross_chain: + return errors, warnings + + monad_symbol = data.get("symbol") + monad_decimals = data.get("decimals") + + with ThreadPoolExecutor(max_workers=max_workers) as executor: + futures = {} + for chain_id, chain_data in cross_chain.items(): + if chain_id not in CHAIN_RPC_URLS: + continue + address = chain_data.get("address") + if not address: + continue + + # Use override symbol/decimals if specified, otherwise use Monad token's values + expected_symbol = chain_data.get("symbol", monad_symbol) + expected_decimals = chain_data.get("decimals", monad_decimals) + + future = executor.submit( + validate_single_cross_chain_address, + chain_id, + address, + expected_symbol, + expected_decimals, + ) + futures[future] = chain_id + + for future in as_completed(futures): + chain_errors, chain_warnings = future.result() + errors.extend(chain_errors) + warnings.extend(chain_warnings) + + return errors, warnings + + def get_svg_dimensions(svg_path: Path) -> tuple[Optional[int], Optional[int]]: """Extract width and height from an SVG file. @@ -212,22 +398,62 @@ def validate_logo_dimensions(token_dir_path: Path) -> list[str]: return errors +def validate_extensions(extensions: Any) -> list[str]: + """Validate the extensions field of token data. + + Args: + extensions: The extensions value to validate. + + Returns: + list[str]: List of error messages. Empty list if validation passes. + """ + errors = [] + + if not isinstance(extensions, dict): + return ["Invalid extensions: must be a dictionary"] + + allowed_tags = ", ".join(ALLOWED_EXTENSIONS.keys()) + for tag, value in extensions.items(): + if tag in ALLOWED_EXTENSIONS: + expected_type = ALLOWED_EXTENSIONS[tag] + if not isinstance(value, expected_type): + type_name = expected_type.__name__ + errors.append( + f"Invalid type for extension '{tag}': expected {type_name}, " + f"got {type(value).__name__}" + ) + elif tag == "bridgeInfo": + bridge_errors = validate_bridge_info(value) + errors.extend(bridge_errors) + elif tag == "crossChainAddresses": + cross_chain_errors = validate_cross_chain_addresses(value) + errors.extend(cross_chain_errors) + else: + errors.append(f"Invalid extension tag: {tag}. Allowed tags are: {allowed_tags}") + + return errors + + def validate_token_data( data: dict[str, Any], token_dir_path: Path, web3: Web3, -) -> list[str]: + validate_cross_chain: bool = False, +) -> tuple[list[str], list[str]]: """Validate token data against required schema and on-chain metadata. Args: data: The token data dictionary to validate. token_dir_path: Path to the token directory. web3: Web3 instance for on-chain validation. + validate_cross_chain: If True, validate cross-chain addresses against + on-chain metadata on other chains. Returns: - list[str]: List of error messages. Empty list if validation passes. + tuple[list[str], list[str]]: (errors, warnings) """ errors = [] + warnings = [] # Check for required fields missing_fields = [field for field in REQUIRED_FIELDS if field not in data] @@ -272,31 +498,22 @@ def validate_token_data( # Validate extensions (optional) if "extensions" in data: - extensions = data.get("extensions") - if not isinstance(extensions, dict): - errors.append("Invalid extensions: must be a dictionary") - else: - allowed_tags = ", ".join(ALLOWED_EXTENSIONS.keys()) - for tag, value in extensions.items(): - if tag in ALLOWED_EXTENSIONS: - expected_type = ALLOWED_EXTENSIONS[tag] - if not isinstance(value, expected_type): - type_name = expected_type.__name__ - errors.append( - f"Invalid type for extension '{tag}': expected {type_name}, " - f"got {type(value).__name__}" - ) - elif tag == "bridgeInfo": - bridge_errors = validate_bridge_info(value) - errors.extend(bridge_errors) - else: - errors.append(f"Invalid extension tag: {tag}. Allowed tags are: {allowed_tags}") + extension_errors = validate_extensions(data.get("extensions")) + errors.extend(extension_errors) # Validate on-chain data onchain_errors = validate_onchain_metadata(data, web3) errors.extend(onchain_errors) - return errors + # Cross-chain metadata validation (optional) + if validate_cross_chain and "extensions" in data: + extensions = data.get("extensions", {}) + if "crossChainAddresses" in extensions: + cc_errors, cc_warnings = validate_cross_chain_metadata(data) + errors.extend(cc_errors) + warnings.extend(cc_warnings) + + return errors, warnings def validate_onchain_metadata(data: dict[str, Any], web3: Web3) -> list[str]: @@ -354,31 +571,34 @@ def validate_onchain_metadata(data: dict[str, Any], web3: Web3) -> list[str]: def validate_token_directory( dir_path: Path, web3: Web3, -) -> tuple[bool, list[str]]: + validate_cross_chain: bool = False, +) -> tuple[bool, list[str], list[str]]: """Validate a token directory and its data.json file. Args: dir_path: Path to the token directory. web3: Web3 instance for on-chain validation. + validate_cross_chain: If True, validate cross-chain addresses against + on-chain metadata on other chains. Returns: - tuple[bool, list[str]]: (is_valid, error_messages) + tuple[bool, list[str], list[str]]: (is_valid, errors, warnings) """ data_file = dir_path / "data.json" if not data_file.exists(): - return False, [f"data.json not found in {dir_path.name}/ directory"] + return False, [f"data.json not found in {dir_path.name}/ directory"], [] try: with data_file.open(mode="r", encoding="utf-8") as f: data = json5.load(f) except ValueError as e: - return False, [f"Invalid JSON5 in data.json: {e}"] + return False, [f"Invalid JSON5 in data.json: {e}"], [] except OSError as e: - return False, [f"Cannot read data.json: {e}"] + return False, [f"Cannot read data.json: {e}"], [] - errors = validate_token_data(data, dir_path, web3) - return len(errors) == 0, errors + errors, warnings = validate_token_data(data, dir_path, web3, validate_cross_chain) + return len(errors) == 0, errors, warnings def main() -> int: @@ -395,6 +615,11 @@ def main() -> int: type=str, help=f"Custom RPC URL (defaults to MONAD_RPC_URL env var or {DEFAULT_RPC_URL})", ) + parser.add_argument( + "--validate-cross-chain", + action="store_true", + help="Enable cross-chain address validation (slower, requires external RPC access)", + ) args = parser.parse_args() @@ -413,19 +638,31 @@ def main() -> int: print("Cannot proceed without RPC connection") return 1 - print(f"Validating {len(token_dirs)} token(s)...\n") + print(f"Validating {len(token_dirs)} token(s)...") + if args.validate_cross_chain: + print("Cross-chain validation enabled\n") + else: + print() all_valid = True for dir_path in token_dirs: token_name = dir_path.name - is_valid, errors = validate_token_directory(dir_path, web3) + is_valid, errors, warnings = validate_token_directory( + dir_path, web3, args.validate_cross_chain + ) - if is_valid: + if is_valid and not warnings: print(f"{token_name} is valid") + elif is_valid and warnings: + print(f"{token_name} is valid (with warnings):") + for warning in warnings: + print(f" [WARN] {warning}") else: print(f"{token_name} is invalid:") for error in errors: print(f" - {error}") + for warning in warnings: + print(f" [WARN] {warning}") all_valid = False if all_valid: