diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 9d6d8bff..de90fd2e 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -1,6 +1,8 @@ name: CI -on: [push] +on: + pull_request: + branches: main jobs: build: @@ -8,14 +10,22 @@ jobs: steps: - uses: actions/checkout@v4 + with: + fetch-depth: 2 + - name: Use Node.js uses: actions/setup-node@v4 with: node-version: "20.x" + - uses: pnpm/action-setup@v3 with: version: 8 run_install: true - # - run: npm ci - # - run: npm run build --if-present - - run: npm test + + - name: Get changes + run: git diff --name-only -r HEAD^1 HEAD + + - name: Test + run: | + npx tsx ./src/verify.ts $(git diff --name-only -r HEAD^1 HEAD) diff --git a/data/APU/data.json b/data/APU/data.json index 90a6a657..5b28d44d 100644 --- a/data/APU/data.json +++ b/data/APU/data.json @@ -8,4 +8,4 @@ "1": "0x594DaaD7D77592a2b97b725A7AD59D7E188b5bFa", "8453": "0x7A2C5e7788E55Ec0a7ba4aEeC5B3da322718Fb5e" } -} \ No newline at end of file +} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 00000000..7dce95e5 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,36 @@ +import * as viemChains from "viem/chains"; + +export interface SuperchainToken { + chainId: number; + address: string; + name: string; + symbol: string; + decimals: number; + logoURI: string; + extensions: { + standardBridgeAddresses: { + [chainId: string]: string; + }; + opTokenId: string; + }; +} + +export interface TokenData { + name: string; + symbol: string; + decimals: number; + logoURI: string; + opTokenId: string; + addresses: { + [chainId: string]: string; + }; +} + +export const getViemChain = (id: number | string) => { + const chainId = typeof id === "string" ? parseInt(id) : id; + const chain = Object.values(viemChains).find((x) => x.id === chainId); + if (!chain) { + throw new Error(`Chain ${id} not found`); + } + return chain; +}; diff --git a/src/verify.ts b/src/verify.ts new file mode 100644 index 00000000..86f69190 --- /dev/null +++ b/src/verify.ts @@ -0,0 +1,119 @@ +import { readFileSync } from "fs"; +import { join } from "path"; +import { Address, createPublicClient, http } from "viem"; +import { isAddressEqual, isAddress } from "viem/utils"; + +import { OptimismMintableERC20Abi } from "./abis/OptimismMintableERC20"; +import { StandardBridgeAbi } from "./abis/StandardBridge"; +import { getViemChain, TokenData } from "./utils"; + +async function main() { + const [, , ...files] = process.argv; + + for (const path of files) { + if (!path.startsWith("data") || !path.endsWith(".json")) { + continue; + } + + let data: TokenData | null = null; + try { + data = JSON.parse(readFileSync(join(__dirname, "..", path)).toString()); + } catch { + throw new Error(`Invalid JSON at ${path}`); + } + + console.log("Verifying", data!.name); + + let mintable = false; + let base = false; + + for (const [chainId, address] of Object.entries(data!.addresses)) { + const client = createPublicClient({ + chain: getViemChain(chainId), + transport: http(), + }); + + if (!isAddress(address)) { + throw new Error(`Invalid address for chainId ${chainId}`); + } + + const [BRIDGE, REMOTE_TOKEN] = await Promise.all([ + client + .readContract({ + abi: OptimismMintableERC20Abi, + functionName: "BRIDGE", + address: address as Address, + }) + .catch(() => null), + client + .readContract({ + abi: OptimismMintableERC20Abi, + functionName: "REMOTE_TOKEN", + address: address as Address, + }) + .catch(() => null), + ]); + console.log("BRIDGE", BRIDGE); + console.log("REMOTE_TOKEN", REMOTE_TOKEN); + + // mintable + if (BRIDGE && REMOTE_TOKEN) { + mintable = true; + console.log(chainId, "is mintable"); + + const baseChainId = Object.entries(data!.addresses).find( + ([_, address]) => isAddressEqual(address as Address, REMOTE_TOKEN) + ); + if (!baseChainId) { + throw new Error( + `No corresponding token address found for ${ + data!.symbol + } on ${chainId}` + ); + } + + const baseClient = createPublicClient({ + chain: getViemChain(baseChainId[0]), + transport: http(), + }); + + const BASE_BRIDGE = await client + .readContract({ + abi: StandardBridgeAbi, + functionName: "OTHER_BRIDGE", + address: BRIDGE, + }) + .catch(() => null); + if (!BASE_BRIDGE) { + throw new Error("BASE_BRIDGE not found"); + } + console.log("BASE_BRIDGE", BASE_BRIDGE); + + const REMOTE_BRIDGE = await baseClient + .readContract({ + abi: StandardBridgeAbi, + functionName: "OTHER_BRIDGE", + address: BASE_BRIDGE, + }) + .catch(() => null); + console.log("REMOTE_BRIDGE", REMOTE_BRIDGE); + + if (!REMOTE_BRIDGE) { + throw new Error("REMOTE_BRIDGE not found"); + } + if (!isAddressEqual(REMOTE_BRIDGE, BRIDGE)) { + throw new Error("Bridge addresses do not match"); + } + } else { + base = true; + } + } + + if (mintable !== true || base !== true) { + throw new Error("Tokens do not point at each other"); + } else { + console.log("✅"); + } + } +} +main();