From 0c0dd91c5df798f015610359e8300f4585788c8e Mon Sep 17 00:00:00 2001 From: blueogin Date: Fri, 10 Jul 2026 13:58:48 -0400 Subject: [PATCH 1/7] Implement OFT configuration scripts and update LayerZero settings --- packages/bridge-contracts/layerzero.config.ts | 14 +- packages/bridge-contracts/package.json | 1 + .../scripts/oft/OFT_CONFIGURING_GUIDE.md | 162 ++++++++++++++ .../scripts/oft/adapter-ownership.ts | 104 +++++++++ .../{test => scripts}/oft/bridge-oft-token.ts | 18 +- .../scripts/oft/configure-oft.ts | 139 ++++++++++++ .../oft/grant-minter-role.ts | 10 +- .../{test => scripts}/oft/oft.config.json | 0 .../scripts/oft/run-configure.js | 34 +++ .../oft/set-bridge-limits.ts | 18 +- .../{test => scripts}/oft/set-oft-operator.ts | 0 .../test/oft/OFT_CONFIGURING_GUIDE.md | 153 ------------- ...nsfer-oft-adapter-ownership-from-avatar.ts | 164 -------------- .../oft/transfer-oft-adapter-ownership.ts | 201 ------------------ 14 files changed, 469 insertions(+), 549 deletions(-) create mode 100644 packages/bridge-contracts/scripts/oft/OFT_CONFIGURING_GUIDE.md create mode 100644 packages/bridge-contracts/scripts/oft/adapter-ownership.ts rename packages/bridge-contracts/{test => scripts}/oft/bridge-oft-token.ts (96%) create mode 100644 packages/bridge-contracts/scripts/oft/configure-oft.ts rename packages/bridge-contracts/{test => scripts}/oft/grant-minter-role.ts (97%) rename packages/bridge-contracts/{test => scripts}/oft/oft.config.json (100%) create mode 100644 packages/bridge-contracts/scripts/oft/run-configure.js rename packages/bridge-contracts/{test => scripts}/oft/set-bridge-limits.ts (95%) rename packages/bridge-contracts/{test => scripts}/oft/set-oft-operator.ts (100%) delete mode 100644 packages/bridge-contracts/test/oft/OFT_CONFIGURING_GUIDE.md delete mode 100644 packages/bridge-contracts/test/oft/transfer-oft-adapter-ownership-from-avatar.ts delete mode 100644 packages/bridge-contracts/test/oft/transfer-oft-adapter-ownership.ts diff --git a/packages/bridge-contracts/layerzero.config.ts b/packages/bridge-contracts/layerzero.config.ts index 8f86282..c1c6003 100644 --- a/packages/bridge-contracts/layerzero.config.ts +++ b/packages/bridge-contracts/layerzero.config.ts @@ -1,11 +1,14 @@ /** * LayerZero OApp Configuration - * + * * This config file is used by LayerZero Hardhat tools to wire connections * between chains and configure messaging libraries, DVNs, and executors. - * + * + * Pair networks (defaults are development): + * OFT_XDC_NETWORK=production-xdc OFT_CELO_NETWORK=production-celo + * * Usage: - * npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts + * npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts --ci * npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts */ @@ -16,9 +19,8 @@ import { ExecutorOptionType } from "@layerzerolabs/lz-v2-utilities"; import { TwoWayConfig, generateConnectionsConfig } from "@layerzerolabs/metadata-tools"; import { getOftDeploymentAddresses } from "./deploy/utils/getOftDeploymentAddresses"; -// Network names - adjust these based on your deployment -const XDC_NETWORK = "development-xdc"; -const CELO_NETWORK = "development-celo"; +const XDC_NETWORK = process.env.OFT_XDC_NETWORK || "development-xdc"; +const CELO_NETWORK = process.env.OFT_CELO_NETWORK || "development-celo"; // Get contract addresses from hardhat-deploy artifacts (`deployments/`) const { GoodDollarOFTAdapter: xdcOftAdapterAddress } = getOftDeploymentAddresses(XDC_NETWORK); diff --git a/packages/bridge-contracts/package.json b/packages/bridge-contracts/package.json index 4b0d479..739081b 100644 --- a/packages/bridge-contracts/package.json +++ b/packages/bridge-contracts/package.json @@ -8,6 +8,7 @@ "build": "hardhat compile", "test": "hardhat test", "test:oft": "hardhat test ./test/GoodDollarOFT.test.ts", + "oft:configure": "node scripts/oft/run-configure.js", "updateabi": "find ./artifacts/contracts/ -iregex '.*/[a-z]*.json' -exec cp \"{}\" ../bridge-app/src/abi \\;", "start:dev": "hardhat node & yarn build && yarn updateabi && hardhat run scripts/deployDevEnv.ts --network localhost", "mpb:deploy": "hardhat deploy --export-all ./release/mpb.json" diff --git a/packages/bridge-contracts/scripts/oft/OFT_CONFIGURING_GUIDE.md b/packages/bridge-contracts/scripts/oft/OFT_CONFIGURING_GUIDE.md new file mode 100644 index 0000000..3b02960 --- /dev/null +++ b/packages/bridge-contracts/scripts/oft/OFT_CONFIGURING_GUIDE.md @@ -0,0 +1,162 @@ +# OFT (Omnichain Fungible Token) Configuration Guide + +Configure the GoodDollar OFT bridge between XDC and CELO (LayerZero). Post-deploy steps live under `scripts/oft/` and are orchestrated by `yarn oft:configure`. + +## Overview + +1. Deploy OFT proxies/implementations (`deploy/deployOFT.ts`) +2. Configure each network with `yarn oft:configure` (grant minter → limits → LayerZero wire → adapter ownership) +3. Later upgrades use `deploy/upgradeOFT.ts` (`OFT-Upgrade` tag); **proxy addresses stay unchanged** + +## Config values explained + +### CREATE2 salts (`deploy/deployOFT.ts`) + +| Salt | How it is derived | Purpose | +|------|-------------------|---------| +| Proxy salt (MinterBurner) | `keccak256("Development-GoodDollarOFTMinterBurnerV1")` or `Production-...V1` | Deterministic **proxy** address per env | +| Proxy salt (OFT Adapter) | `keccak256("Development-GoodDollarOFTAdapterV1")` or `Production-...V1` | Deterministic **proxy** address per env | +| Implementation salt | `keccak256(compiled bytecode)` of each contract | Deterministic **implementation** address; changes when bytecode changes | + +Proxy salts are fixed strings so redeploys of the same env target the same proxy addresses. Implementation salts track bytecode so a new implementation gets a new CREATE2 address without moving the proxy. + +### LayerZero endpoints (`deploy/deployOFT.ts`) + +Passed as constructor args to `GoodDollarOFTAdapter` implementation: + +| Hardhat network | Endpoint | +|-----------------|----------| +| `development-celo` / `production-celo` | `0x1a44076050125825900e736c501f859c50fE728c` | +| `development-xdc` / `production-xdc` | `0xcb566e3B6934Fa77258d68ea18E931fa75e1aaAa` | + +Override with `LAYERZERO_ENDPOINT` if needed. Wiring peers/DVNs uses `layerzero.config.ts` (not these constructor args alone). + +`layerzero.config.ts` loads adapter addresses from hardhat-deploy artifacts. Pair networks via env (set automatically by `oft:configure`): + +- `OFT_XDC_NETWORK` — default `development-xdc` +- `OFT_CELO_NETWORK` — default `development-celo` + +### Bridge limits (`scripts/oft/oft.config.json`) + +Per-network `limits` object (human-readable G$ amounts; the script converts to 18-decimal wei when the string is short / decimal): + +| Field | Meaning | +|-------|---------| +| `dailyLimit` | Max total G$ bridged in a rolling 24h window (global) | +| `txLimit` | Max G$ per single bridge transaction | +| `accountDailyLimit` | Max G$ one account can bridge in a rolling 24h window | +| `minAmount` | Minimum G$ amount per bridge transaction | +| `onlyWhitelisted` | If `true`, only Identity-whitelisted senders may bridge | + +Skip behavior is controlled by CLI flags on `yarn oft:configure`, not by this JSON file. + +Example: + +```json +{ + "production-xdc": { + "limits": { + "dailyLimit": "1000000", + "txLimit": "100000", + "accountDailyLimit": "50000", + "minAmount": "10", + "onlyWhitelisted": false + } + } +} +``` + +## Step 1: Deploy OFT contracts + +```bash +npx hardhat deploy --tags OFT --network development-xdc +npx hardhat deploy --tags OFT --network development-celo + +npx hardhat deploy --tags OFT --network production-xdc +npx hardhat deploy --tags OFT --network production-celo +``` + +This deploys upgradeable proxies for `GoodDollarOFTMinterBurner` and `GoodDollarOFTAdapter` and writes addresses under `deployments//` (git-ignored). + +`GoodDollarOFTMinterBurner.initialize(nameService, adapter)` sets the OFT adapter as operator; no separate set-operator step is required for the happy path. + +## Step 2: Configure (`yarn oft:configure`) + +Run on **each** network after both sides are deployed (wire needs both adapters in artifacts): + +```bash +yarn oft:configure --network production-xdc +yarn oft:configure --network production-celo +``` + +Development: + +```bash +yarn oft:configure --network development-xdc +yarn oft:configure --network development-celo +``` + +### Order of steps + +1. Grant `MINTER_ROLE` to `GoodDollarOFTMinterBurner` (via Controller/Avatar) +2. Set bridge limits from `oft.config.json` +3. Wire LayerZero (`lz:oapp:wire --ci`) using `layerzero.config.ts` +4. Transfer `GoodDollarOFTAdapter` ownership to DAO Avatar (`adapter-ownership.ts`) + +Ownership transfer is last so limits and wiring still run as the deployer owner. + +### CLI flags + +```bash +yarn oft:configure --network production-xdc --skip-wire +yarn oft:configure --network production-celo --skip-minter --skip-limits +yarn oft:configure --network production-xdc --skip-adapter-ownership +``` + +| Flag | Effect | +|------|--------| +| `--skip-minter` | Skip granting MINTER_ROLE | +| `--skip-limits` | Skip `setBridgeLimits` | +| `--skip-wire` | Skip `lz:oapp:wire` | +| `--skip-adapter-ownership` | Skip ownership transfer to Avatar | + +### Individual helpers (optional) + +Helpers used by `oft:configure` can still be run alone: + +```bash +npx hardhat run scripts/oft/grant-minter-role.ts --network production-xdc +npx hardhat run scripts/oft/set-bridge-limits.ts --network production-xdc +npx hardhat lz:oapp:wire --oapp-config ./layerzero.config.ts --network production-xdc --ci +npx hardhat run scripts/oft/adapter-ownership.ts --network production-xdc +``` + +Other utilities: + +- `scripts/oft/adapter-ownership.ts` — transfer adapter ownership to Avatar (also step 4 of `oft:configure`) +- `scripts/oft/bridge-oft-token.ts` — manual bridge smoke test +- `scripts/oft/set-oft-operator.ts` — set operator if needed outside initialize +- `scripts/oft/transfer-oft-adapter-ownership-from-avatar.ts` — transfer ownership away from Avatar via Controller + +### LayerZero wire notes + +- Wiring may fail with permission errors (`0xc4c52593`) if the OApp owner lacks endpoint delegate permissions +- Prefer running wire **before** transferring ownership to Avatar +- `oft:configure` sets `OFT_XDC_NETWORK` / `OFT_CELO_NETWORK` from whether the Hardhat network name contains `production` + +## Upgrades + +Proxy addresses remain fixed. Deploy new implementations and point proxies at them: + +```bash +npx hardhat deploy --tags OFT-Upgrade --network production-xdc +npx hardhat deploy --tags OFT-Upgrade --network production-celo +``` + +## Verification checklist + +1. Addresses present in `deployments//GoodDollarOFTAdapter.json` and `GoodDollarOFTMinterBurner.json` +2. `GoodDollarOFTMinterBurner` has minter role on GoodDollar +3. LayerZero peers configured between XDC and CELO +4. Bridge limits match `oft.config.json` +5. OFT adapter `owner()` is DAO Avatar (after configure without `--skip-adapter-ownership`) diff --git a/packages/bridge-contracts/scripts/oft/adapter-ownership.ts b/packages/bridge-contracts/scripts/oft/adapter-ownership.ts new file mode 100644 index 0000000..12ade89 --- /dev/null +++ b/packages/bridge-contracts/scripts/oft/adapter-ownership.ts @@ -0,0 +1,104 @@ +/*** + * Transfer GoodDollarOFTAdapter ownership to DAO Avatar. + * + * Used by configure-oft.ts (step 4). Standalone: + * npx hardhat run scripts/oft/adapter-ownership.ts --network development-celo + * + * Must be run by the current OFT adapter owner. + */ + +import { network, ethers } from "hardhat"; +import Contracts from "@gooddollar/goodprotocol/releases/deployment.json"; +import { getOftDeploymentAddresses } from "../../deploy/utils/getOftDeploymentAddresses"; + +export const main = async () => { + const networkName = network.name; + const [signer] = await ethers.getSigners(); + + console.log("=== Transfer GoodDollarOFTAdapter Ownership to Avatar ==="); + console.log("Network:", networkName); + console.log("Signer:", signer.address); + console.log( + "Signer balance:", + ethers.utils.formatEther(await ethers.provider.getBalance(signer.address)), + "ETH/CELO" + ); + + const { GoodDollarOFTAdapter: oftAdapterAddress } = getOftDeploymentAddresses(networkName); + const goodProtocolContracts = Contracts[networkName as keyof typeof Contracts] as any; + + if (!goodProtocolContracts) { + throw new Error(`No GoodProtocol contracts found for network: ${networkName}`); + } + + const avatarAddress = goodProtocolContracts.Avatar; + if (!avatarAddress) { + throw new Error(`Avatar not found in GoodProtocol deployment.json for ${networkName}`); + } + + console.log("\nContract addresses:"); + console.log("GoodDollarOFTAdapter:", oftAdapterAddress); + console.log("Avatar:", avatarAddress); + + const oftAdapter = await ethers.getContractAt("GoodDollarOFTAdapter", oftAdapterAddress); + const currentOwner = await oftAdapter.owner(); + + console.log("\nCurrent owner:", currentOwner); + console.log("Target owner (Avatar):", avatarAddress); + + if (currentOwner.toLowerCase() === avatarAddress.toLowerCase()) { + console.log("\n✅ GoodDollarOFTAdapter is already owned by Avatar. No action needed."); + return; + } + + if (currentOwner.toLowerCase() !== signer.address.toLowerCase()) { + console.log("\n❌ Error: Current owner is not the signer."); + console.log(`Current owner: ${currentOwner}`); + console.log(`Signer: ${signer.address}`); + console.log(`Manually call: oftAdapter.transferOwnership("${avatarAddress}")`); + throw new Error("Signer is not the current owner"); + } + + console.log("\n✅ Signer is the current owner. Proceeding with ownership transfer..."); + + try { + console.log("\nTransferring ownership to Avatar..."); + const tx = await oftAdapter.transferOwnership(avatarAddress); + console.log("Transaction hash:", tx.hash); + console.log("Waiting for confirmation..."); + + const receipt = await tx.wait(); + console.log("✅ Transaction confirmed!"); + console.log("Block number:", receipt.blockNumber); + console.log("Gas used:", receipt.gasUsed.toString()); + + const newOwner = await oftAdapter.owner(); + console.log("\nNew owner:", newOwner); + + if (newOwner.toLowerCase() === avatarAddress.toLowerCase()) { + console.log("\n✅ Successfully transferred ownership to Avatar!"); + } else { + console.log("\n⚠️ Warning: Ownership was not transferred correctly."); + console.log("Expected:", avatarAddress); + console.log("Got:", newOwner); + } + } catch (error: any) { + console.error("\n❌ Error transferring ownership:"); + if (error.message) { + console.error("Error message:", error.message); + } + if (error.reason) { + console.error("Reason:", error.reason); + } + throw error; + } +}; + +if (require.main === module) { + main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); +} diff --git a/packages/bridge-contracts/test/oft/bridge-oft-token.ts b/packages/bridge-contracts/scripts/oft/bridge-oft-token.ts similarity index 96% rename from packages/bridge-contracts/test/oft/bridge-oft-token.ts rename to packages/bridge-contracts/scripts/oft/bridge-oft-token.ts index 45ad1ee..c61f149 100644 --- a/packages/bridge-contracts/test/oft/bridge-oft-token.ts +++ b/packages/bridge-contracts/scripts/oft/bridge-oft-token.ts @@ -2,15 +2,11 @@ * Script to bridge 1 G$ token between XDC and CELO using LayerZero OFT adapter * * Usage: - * # Bridge from XDC to CELO: - * npx hardhat run test/oft/bridge-oft-token.ts --network production-xdc - * # or - * npx hardhat run test/oft/bridge-oft-token.ts --network development-xdc - * - * # Bridge from CELO to XDC: - * npx hardhat run test/oft/bridge-oft-token.ts --network production-celo - * # or - * npx hardhat run test/oft/bridge-oft-token.ts --network development-celo + * npx hardhat run scripts/oft/bridge-oft-token.ts --network production-xdc + * npx hardhat run scripts/oft/bridge-oft-token.ts --network development-xdc + * + * npx hardhat run scripts/oft/bridge-oft-token.ts --network production-celo + * npx hardhat run scripts/oft/bridge-oft-token.ts --network development-celo * * Note: Make sure you have: * - GoodDollarOFTAdapter deployed on both XDC and CELO @@ -374,7 +370,7 @@ const main = async () => { } else if (error.message?.includes('send library') || error.message?.includes('SendLib') || error.message?.includes('receive library') || error.message?.includes('ReceiveLib')) { console.error("\n🔍 DIAGNOSIS: LayerZero library configuration issue"); console.error("Check library configuration using:"); - console.error(` yarn hardhat run test/oft/check-layerzero-config.ts --network ${networkName}`); + console.error(` npx hardhat lz:oapp:config:get --oapp-config ./layerzero.config.ts --network ${networkName}`); } // Check for peer errors @@ -382,7 +378,7 @@ const main = async () => { console.error("\n🔍 DIAGNOSIS: Peer not configured"); console.error("The peer connection between chains is not set."); console.error("\nSOLUTION:"); - console.error(`Run: yarn hardhat run test/oft/set-layerzero-peers.ts --network ${networkName}`); + console.error(`Run: npx hardhat lz:oapp:wire --oapp-config ./layerzero.config.ts --network ${networkName} --ci`); } } diff --git a/packages/bridge-contracts/scripts/oft/configure-oft.ts b/packages/bridge-contracts/scripts/oft/configure-oft.ts new file mode 100644 index 0000000..c4d2142 --- /dev/null +++ b/packages/bridge-contracts/scripts/oft/configure-oft.ts @@ -0,0 +1,139 @@ +/*** + * Orchestrates post-deploy OFT configuration for a single Hardhat network. + * + * Steps (in order): + * 1. Grant MINTER_ROLE to GoodDollarOFTMinterBurner + * 2. Set bridge limits from scripts/oft/oft.config.json + * 3. Wire LayerZero peers / DVNs / enforced options (lz:oapp:wire --ci) + * 4. Transfer GoodDollarOFTAdapter ownership to DAO Avatar + * + * Usage: + * yarn oft:configure --network production-xdc + * yarn oft:configure --network production-celo + * yarn oft:configure --network development-xdc --skip-wire + * + * Flags (forwarded by scripts/oft/run-configure.js): + * --skip-minter + * --skip-limits + * --skip-wire + * --skip-adapter-ownership + */ + +import { spawnSync } from "child_process"; +import path from "path"; +import { network } from "hardhat"; +import { main as grantMinterRole } from "./grant-minter-role"; +import { main as setBridgeLimits } from "./set-bridge-limits"; +import { main as transferAdapterOwnership } from "./adapter-ownership"; + +type ConfigureFlags = { + skipMinter: boolean; + skipLimits: boolean; + skipWire: boolean; + skipAdapterOwnership: boolean; +}; + +function flagEnabled(name: string): boolean { + const value = process.env[name]; + return value === "1" || value === "true"; +} + +function parseFlags(): ConfigureFlags { + return { + skipMinter: flagEnabled("SKIP_MINTER"), + skipLimits: flagEnabled("SKIP_LIMITS"), + skipWire: flagEnabled("SKIP_WIRE"), + skipAdapterOwnership: flagEnabled("SKIP_ADAPTER_OWNERSHIP"), + }; +} + +function resolveOftPairNetworks(networkName: string): { xdc: string; celo: string } { + const isProduction = networkName.includes("production"); + return { + xdc: isProduction ? "production-xdc" : "development-xdc", + celo: isProduction ? "production-celo" : "development-celo", + }; +} + +function wireLayerZero(networkName: string) { + const pair = resolveOftPairNetworks(networkName); + const packageRoot = path.resolve(__dirname, "../.."); + console.log("\n=== Wire LayerZero OApp ==="); + console.log("Network (signer/RPC):", networkName); + console.log("OFT_XDC_NETWORK:", pair.xdc); + console.log("OFT_CELO_NETWORK:", pair.celo); + + const result = spawnSync( + "npx", + [ + "hardhat", + "lz:oapp:wire", + "--oapp-config", + "./layerzero.config.ts", + "--network", + networkName, + "--ci", + ], + { + cwd: packageRoot, + stdio: "inherit", + env: { + ...process.env, + OFT_XDC_NETWORK: pair.xdc, + OFT_CELO_NETWORK: pair.celo, + }, + } + ); + + if (result.error) { + throw result.error; + } + if (result.status !== 0) { + throw new Error(`LayerZero wire failed with exit code ${result.status ?? "unknown"}`); + } + console.log("✅ LayerZero wire completed"); +} + +export const main = async () => { + const flags = parseFlags(); + const networkName = network.name; + + console.log("=== Configure GoodDollar OFT ==="); + console.log("Network:", networkName); + console.log("Flags:", flags); + + if (!flags.skipMinter) { + await grantMinterRole(); + } else { + console.log("\nSkipping grant MINTER_ROLE (--skip-minter)"); + } + + if (!flags.skipLimits) { + await setBridgeLimits(); + } else { + console.log("\nSkipping set bridge limits (--skip-limits)"); + } + + if (!flags.skipWire) { + wireLayerZero(networkName); + } else { + console.log("\nSkipping LayerZero wire (--skip-wire)"); + } + + if (!flags.skipAdapterOwnership) { + await transferAdapterOwnership(); + } else { + console.log("\nSkipping adapter ownership transfer (--skip-adapter-ownership)"); + } + + console.log("\n=== OFT configuration complete ==="); +}; + +if (require.main === module) { + main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); +} diff --git a/packages/bridge-contracts/test/oft/grant-minter-role.ts b/packages/bridge-contracts/scripts/oft/grant-minter-role.ts similarity index 97% rename from packages/bridge-contracts/test/oft/grant-minter-role.ts rename to packages/bridge-contracts/scripts/oft/grant-minter-role.ts index 3dd3e01..8f109d0 100644 --- a/packages/bridge-contracts/test/oft/grant-minter-role.ts +++ b/packages/bridge-contracts/scripts/oft/grant-minter-role.ts @@ -1,10 +1,10 @@ /*** - * Script to grant MINTER_ROLE to GoodDollarOFTMinterBurner contract on development-celo + * Script to grant MINTER_ROLE to GoodDollarOFTMinterBurner contract * Uses genericCall through Avatar/Controller to execute the transaction - * + * * Usage: - * npx hardhat run test/oft/grant-minter-role.ts --network development-celo - * + * npx hardhat run scripts/oft/grant-minter-role.ts --network development-celo + * * Note: This script must be run by a guardian or address with permissions to execute via Controller */ @@ -12,7 +12,7 @@ import { network, ethers } from "hardhat"; import Contracts from "@gooddollar/goodprotocol/releases/deployment.json"; import { getOftDeploymentAddresses } from "../../deploy/utils/getOftDeploymentAddresses"; -const main = async () => { +export const main = async () => { const networkName = network.name; const [signer] = await ethers.getSigners(); diff --git a/packages/bridge-contracts/test/oft/oft.config.json b/packages/bridge-contracts/scripts/oft/oft.config.json similarity index 100% rename from packages/bridge-contracts/test/oft/oft.config.json rename to packages/bridge-contracts/scripts/oft/oft.config.json diff --git a/packages/bridge-contracts/scripts/oft/run-configure.js b/packages/bridge-contracts/scripts/oft/run-configure.js new file mode 100644 index 0000000..72ca3d2 --- /dev/null +++ b/packages/bridge-contracts/scripts/oft/run-configure.js @@ -0,0 +1,34 @@ +const { spawnSync } = require("child_process"); +const path = require("path"); + +const SKIP_FLAGS = new Set([ + "--skip-minter", + "--skip-limits", + "--skip-wire", + "--skip-adapter-ownership", +]); + +const args = process.argv.slice(2); +const hardhatArgs = []; +const env = { ...process.env }; + +for (const arg of args) { + if (SKIP_FLAGS.has(arg)) { + const key = arg.slice(2).toUpperCase().replace(/-/g, "_"); + env[key] = "1"; + } else { + hardhatArgs.push(arg); + } +} + +const result = spawnSync( + "npx", + ["hardhat", "run", "scripts/oft/configure-oft.ts", ...hardhatArgs], + { + cwd: path.resolve(__dirname, "../.."), + stdio: "inherit", + env, + } +); + +process.exit(result.status ?? 1); diff --git a/packages/bridge-contracts/test/oft/set-bridge-limits.ts b/packages/bridge-contracts/scripts/oft/set-bridge-limits.ts similarity index 95% rename from packages/bridge-contracts/test/oft/set-bridge-limits.ts rename to packages/bridge-contracts/scripts/oft/set-bridge-limits.ts index 2e295ea..186db71 100644 --- a/packages/bridge-contracts/test/oft/set-bridge-limits.ts +++ b/packages/bridge-contracts/scripts/oft/set-bridge-limits.ts @@ -1,16 +1,16 @@ /*** * Script to set bridge limits for GoodDollarOFTAdapter contract * Calls setBridgeLimits directly on the OFT adapter (not via Controller/Avatar). - * - * Note: Limits are now managed in GoodDollarOFTAdapter, not GoodDollarOFTMinterBurner - * + * + * Note: Limits are managed in GoodDollarOFTAdapter, not GoodDollarOFTMinterBurner + * * Usage: - * npx hardhat run test/oft/set-bridge-limits.ts --network development-celo - * + * npx hardhat run scripts/oft/set-bridge-limits.ts --network development-celo + * * Configuration: - * All limit values are read from test/oft/oft.config.json + * Limit values are read from scripts/oft/oft.config.json * Each network/env has its entry in the config file. - * + * * Note: This script must be run by the OFT adapter owner (usually the deployer or DAO Avatar) */ @@ -20,7 +20,7 @@ import Contracts from "@gooddollar/goodprotocol/releases/deployment.json"; import { getOftDeploymentAddresses } from "../../deploy/utils/getOftDeploymentAddresses"; import config from "./oft.config.json"; -const main = async () => { +export const main = async () => { const networkName = network.name; const [signer] = await ethers.getSigners(); @@ -72,7 +72,7 @@ const main = async () => { const networkConfig = (config as any)[networkName]; if (!networkConfig || !networkConfig.limits) { console.log("\n⚠️ No limits configuration found for this network."); - console.log(`Please add a "limits" entry for "${networkName}" in test/oft/oft.config.json`); + console.log(`Please add a "limits" entry for "${networkName}" in scripts/oft/oft.config.json`); return; } diff --git a/packages/bridge-contracts/test/oft/set-oft-operator.ts b/packages/bridge-contracts/scripts/oft/set-oft-operator.ts similarity index 100% rename from packages/bridge-contracts/test/oft/set-oft-operator.ts rename to packages/bridge-contracts/scripts/oft/set-oft-operator.ts diff --git a/packages/bridge-contracts/test/oft/OFT_CONFIGURING_GUIDE.md b/packages/bridge-contracts/test/oft/OFT_CONFIGURING_GUIDE.md deleted file mode 100644 index 005d389..0000000 --- a/packages/bridge-contracts/test/oft/OFT_CONFIGURING_GUIDE.md +++ /dev/null @@ -1,153 +0,0 @@ -# OFT (Omnichain Fungible Token) Configuration Guide - -This guide explains how to configure the GoodDollar OFT bridge between XDC and CELO networks using LayerZero. - -## Overview - -The OFT bridge enables cross-chain transfers of GoodDollar (G$) tokens between XDC and CELO networks. The setup involves deploying contracts, configuring permissions, setting up LayerZero connections, and configuring bridge limits. - - -## Configuration File - -All configuration values are stored in `test/oft/oft.config.json`. Each network has its own configuration entry. - -### Configuration Structure - -```json -{ - "development-xdc": { - "skipTransferOwnership": false, - "skipWiring": false, - "skipLimits": false, - "skipBridgeTest": true, - "limits": { - "dailyLimit": "5000", - "txLimit": "1000", - "accountDailyLimit": "1000", - "minAmount": "1", - "onlyWhitelisted": false - } - }, - "development-celo": { - // ... same structure - }, - "production-xdc": { - // ... same structure - }, - "production-celo": { - // ... same structure - } -} -``` - -## Manual Configuration: Step-by-Step - -If you prefer to configure each network individually or need more control, follow these steps: - -### Step 1: Deploy OFT Contracts - -Deploy the GoodDollarOFTMinterBurner and GoodDollarOFTAdapter contracts on each network using hardhat-deploy: - -```bash -# Deploy on XDC -npx hardhat deploy --tags OFT --network development-xdc - -# Deploy on CELO -npx hardhat deploy --tags OFT --network development-celo -``` - -This deployment script will: -- Deploy `GoodDollarOFTMinterBurner` (upgradeable proxy) -- Deploy `GoodDollarOFTAdapter` (upgradeable proxy) -- Save contract addresses to hardhat-deploy's `deployments/` artifacts -- Save deployments to hardhat-deploy's deployment system - -**Note**: The deployment uses hardhat-deploy for better deployment management and tracking. - -### Step 2: Grant MINTER_ROLE - -`GoodDollarOFTMinterBurner.initialize(nameService, adapter)` automatically sets the OFT adapter as an operator, -so there is no separate “set operator” step required. - -Grant the MINTER_ROLE to GoodDollarOFTMinterBurner on the GoodDollar token: - -```bash -# Grant on XDC -yarn hardhat run test/oft/grant-minter-role.ts --network development-xdc - -# Grant on CELO -yarn hardhat run test/oft/grant-minter-role.ts --network development-celo -``` - -This executes via DAO governance (Controller/Avatar) to grant the minter role. - -### Step 3: Wire LayerZero Connections - -Configure LayerZero messaging libraries, DVNs, executors, and enforced options: - -```bash -# Wire on XDC -yarn hardhat lz:oapp:wire --oapp-config ./layerzero.config.ts --network development-xdc - -# Wire on CELO -yarn hardhat lz:oapp:wire --oapp-config ./layerzero.config.ts --network development-celo -``` - -**Important**: -- Wiring may fail with permission errors (0xc4c52593) if the OApp owner doesn't have delegate permissions on the LayerZero endpoint -- If wiring fails, you may need to manually configure enforced options or contact LayerZero support - -### Step 4: Set Bridge Limits (Optional) - -Configure bridge limits using values from `oft.config.json` (only needed if you want to set the bridge limits): - -```bash -# Set limits on XDC -yarn hardhat run test/oft/set-bridge-limits.ts --network development-xdc - -# Set limits on CELO -yarn hardhat run test/oft/set-bridge-limits.ts --network development-celo -``` - -The script reads limit values from `oft.config.json` for the specified network and sets them on the OFT adapter. - -### Step 5: Test Bridge Functionality (Optional) - -Test the bridge by sending tokens from one chain to another: - -```bash -# Bridge from XDC to CELO -yarn hardhat run test/oft/bridge-oft-token.ts --network development-xdc - -# Bridge from CELO to XDC -yarn hardhat run test/oft/bridge-oft-token.ts --network development-celo -``` - -**Requirements**: -- Sufficient G$ balance on the source chain -- Sufficient native token (XDC/CELO) for gas and LayerZero fees -- GoodDollarOFTMinterBurner approval for token burning - -### Step 6: Transfer Ownership (Optional) - -Transfer OFT adapter ownership to DAO Avatar. This should be done as the final step: - -```bash -# Transfer on XDC -yarn hardhat run test/oft/transfer-oft-adapter-ownership.ts --network development-xdc - -# Transfer on CELO -yarn hardhat run test/oft/transfer-oft-adapter-ownership.ts --network development-celo -``` - -**Note**: Only transfer ownership if you want the DAO Avatar to be the long-term owner (recommended for production). This must be done by the current owner of the OFT adapter (usually the deployer), and is performed after all other configuration. - -## Configuration Verification - -After configuration, verify the setup: - -1. **Check contract deployments**: Verify addresses in hardhat-deploy's `deployments/` artifacts -2. **Check MINTER_ROLE**: Verify GoodDollarOFTMinterBurner has minter role on GoodDollar token -3. **Check ownership**: Verify OFT adapter is owned by DAO Avatar -4. **Check LayerZero peers**: Verify peer connections are set between chains -5. **Check limits**: Verify bridge limits are set correctly diff --git a/packages/bridge-contracts/test/oft/transfer-oft-adapter-ownership-from-avatar.ts b/packages/bridge-contracts/test/oft/transfer-oft-adapter-ownership-from-avatar.ts deleted file mode 100644 index 551a271..0000000 --- a/packages/bridge-contracts/test/oft/transfer-oft-adapter-ownership-from-avatar.ts +++ /dev/null @@ -1,164 +0,0 @@ -/*** - * Script to transfer ownership of GoodDollarOFTAdapter from DAO Avatar to current signer - * - * Usage: - * yarn hardhat run test/oft/transfer-oft-adapter-ownership-from-avatar.ts --network development-celo - * yarn hardhat run test/oft/transfer-oft-adapter-ownership-from-avatar.ts --network development-xdc - * - * Note: This script must be run by an account that can execute DAO proposals. - * If the Avatar owns the contract, ownership transfer must go through the Controller. - */ - -import { network, ethers } from "hardhat"; -import Contracts from "@gooddollar/goodprotocol/releases/deployment.json"; -import { getOftDeploymentAddresses } from "../../deploy/utils/getOftDeploymentAddresses"; - -const main = async () => { - const networkName = network.name; - const [signer] = await ethers.getSigners(); - - console.log("=== Transfer GoodDollarOFTAdapter Ownership from Avatar to Signer ==="); - console.log("Network:", networkName); - console.log("Signer:", signer.address); - console.log("Signer balance:", ethers.utils.formatEther(await ethers.provider.getBalance(signer.address)), "ETH/CELO/XDC"); - console.log(""); - - // Get deployment info - const { GoodDollarOFTAdapter: oftAdapterAddress } = getOftDeploymentAddresses(networkName); - const goodProtocolContracts = Contracts[networkName as keyof typeof Contracts] as any; - - if (!goodProtocolContracts) { - throw new Error(`No GoodProtocol contracts found for network: ${networkName}`); - } - - const avatarAddress = goodProtocolContracts.Avatar; - const controllerAddress = goodProtocolContracts.Controller; - - if (!avatarAddress) { - throw new Error(`Avatar not found in GoodProtocol deployment.json for ${networkName}`); - } - - if (!controllerAddress) { - throw new Error(`Controller not found in GoodProtocol deployment.json for ${networkName}`); - } - - console.log("Contract addresses:"); - console.log("GoodDollarOFTAdapter:", oftAdapterAddress); - console.log("Avatar:", avatarAddress); - console.log("Controller:", controllerAddress); - console.log(""); - - // Get OFT adapter contract - const oftAdapter = await ethers.getContractAt("GoodDollarOFTAdapter", oftAdapterAddress); - - // Get current owner - let currentOwner: string; - try { - currentOwner = await oftAdapter.owner(); - console.log("Current owner:", currentOwner); - } catch (e: any) { - throw new Error(`Could not read owner: ${e.message}`); - } - - // Check if already owned by signer - if (currentOwner.toLowerCase() === signer.address.toLowerCase()) { - console.log("\n✅ GoodDollarOFTAdapter is already owned by signer. No action needed."); - return; - } - - // Check if owned by Avatar - const isAvatarOwner = currentOwner.toLowerCase() === avatarAddress.toLowerCase(); - - if (!isAvatarOwner) { - console.log("\n❌ Error: Current owner is not the Avatar."); - console.log(`Current owner: ${currentOwner}`); - console.log(`Expected owner (Avatar): ${avatarAddress}`); - console.log("\nThis script can only transfer ownership FROM the Avatar."); - console.log("If you need to transfer from a different owner, use transferOwnership directly."); - throw new Error("Current owner is not the Avatar"); - } - - console.log("✅ Current owner is Avatar. Proceeding with ownership transfer..."); - console.log("Target owner (signer):", signer.address); - console.log(""); - - // Transfer ownership from Avatar to signer - // Since Avatar is the owner, we need to call through Controller - try { - console.log("Transferring ownership from Avatar to signer through Controller..."); - - const Controller = await ethers.getContractAt("Controller", controllerAddress); - - // Encode transferOwnership(address newOwner) - const functionSignature = "transferOwnership(address)"; - const functionSelector = ethers.utils.id(functionSignature).slice(0, 10); - const encodedParams = ethers.utils.defaultAbiCoder.encode( - ["address"], - [signer.address] - ); - const encodedCall = ethers.utils.hexConcat([functionSelector, encodedParams]); - - console.log("Encoded function call:", encodedCall); - console.log("Calling Controller.genericCall..."); - - const tx = await Controller.genericCall( - oftAdapterAddress, - encodedCall, - avatarAddress, - 0 - ); - - console.log("Transaction hash:", tx.hash); - console.log("Waiting for confirmation..."); - - const receipt = await tx.wait(); - console.log("✅ Transaction confirmed!"); - console.log("Block number:", receipt.blockNumber); - console.log("Gas used:", receipt.gasUsed.toString()); - - // Verify ownership was transferred - console.log("\nVerifying ownership transfer..."); - const newOwner = await oftAdapter.owner(); - console.log("New owner:", newOwner); - - if (newOwner.toLowerCase() === signer.address.toLowerCase()) { - console.log("\n✅ Successfully transferred ownership from Avatar to signer!"); - console.log(`Contract is now owned by: ${signer.address}`); - } else { - console.log("\n⚠️ Warning: Ownership was not transferred correctly."); - console.log("Expected:", signer.address); - console.log("Got:", newOwner); - throw new Error("Ownership transfer verification failed"); - } - - } catch (error: any) { - console.error("\n❌ Error transferring ownership:"); - if (error.message) { - console.error("Error message:", error.message); - } - if (error.reason) { - console.error("Reason:", error.reason); - } - if (error.data) { - console.error("Error data:", error.data); - } - - // Provide helpful error messages - if (error.message?.includes("genericCall") || error.message?.includes("Controller")) { - console.error("\n💡 Tip: Make sure you have permission to execute DAO proposals."); - console.error(" The Controller.genericCall requires proper DAO permissions."); - } - - throw error; - } -}; - -if (require.main === module) { - main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error); - process.exit(1); - }); -} - diff --git a/packages/bridge-contracts/test/oft/transfer-oft-adapter-ownership.ts b/packages/bridge-contracts/test/oft/transfer-oft-adapter-ownership.ts deleted file mode 100644 index 0fe808d..0000000 --- a/packages/bridge-contracts/test/oft/transfer-oft-adapter-ownership.ts +++ /dev/null @@ -1,201 +0,0 @@ -/*** - * Script to transfer ownership of GoodDollarOFTAdapter to DAO Avatar - * - * Usage: - * npx hardhat run test/oft/transfer-oft-adapter-ownership.ts --network development-celo - * - * Note: This script must be run by the current owner of the OFT adapter. - * If the current owner is not the signer, you'll need to run this script from the owner's account. - */ - -import { network, ethers } from "hardhat"; -import Contracts from "@gooddollar/goodprotocol/releases/deployment.json"; -import { getOftDeploymentAddresses } from "../../deploy/utils/getOftDeploymentAddresses"; - -const main = async () => { - const networkName = network.name; - const [signer] = await ethers.getSigners(); - - console.log("=== Transfer GoodDollarOFTAdapter Ownership to Avatar ==="); - console.log("Network:", networkName); - console.log("Signer:", signer.address); - console.log("Signer balance:", ethers.utils.formatEther(await ethers.provider.getBalance(signer.address)), "ETH/CELO"); - - // Get deployment info - const { GoodDollarOFTAdapter: oftAdapterAddress, GoodDollarOFTMinterBurner: minterBurnerAddress } = - getOftDeploymentAddresses(networkName); - const goodProtocolContracts = Contracts[networkName as keyof typeof Contracts] as any; - - if (!goodProtocolContracts) { - throw new Error(`No GoodProtocol contracts found for network: ${networkName}`); - } - - const avatarAddress = goodProtocolContracts.Avatar; - - - if (!avatarAddress) { - throw new Error(`Avatar not found in GoodProtocol deployment.json for ${networkName}`); - } - - console.log("\nContract addresses:"); - console.log("GoodDollarOFTAdapter:", oftAdapterAddress); - console.log("Avatar:", avatarAddress); - - // Get OFT adapter contract - const oftAdapter = await ethers.getContractAt("GoodDollarOFTAdapter", oftAdapterAddress); - - // Get current owner - let currentOwner: string; - try { - currentOwner = await oftAdapter.owner(); - } catch (e: any) { - // If owner() fails, contract might not be initialized - console.log("⚠️ Warning: Could not read owner. Contract may not be initialized."); - currentOwner = ethers.constants.AddressZero; - } - - console.log("\nCurrent owner:", currentOwner); - console.log("Target owner (Avatar):", avatarAddress); - - // Check if already owned by Avatar - if (currentOwner.toLowerCase() === avatarAddress.toLowerCase()) { - console.log("\n✅ GoodDollarOFTAdapter is already owned by Avatar. No action needed."); - return; - } - - // If owner is zero, try to initialize the contract first - if (currentOwner === ethers.constants.AddressZero) { - console.log("\n⚠️ Owner is zero address. Contract may not be initialized."); - console.log("Attempting to initialize the contract..."); - - // Get required addresses for initialization - const tokenAddress = goodProtocolContracts.GoodDollar || goodProtocolContracts.SuperGoodDollar; - // Loaded from hardhat-deploy artifacts - const nameServiceAddress = goodProtocolContracts.NameService; - const controllerAddress = goodProtocolContracts.Controller; - - if (!tokenAddress || !minterBurnerAddress || !nameServiceAddress) { - throw new Error( - `Cannot initialize: Missing required addresses. ` + - `Token: ${tokenAddress}, MinterBurner: ${minterBurnerAddress}, NameService: ${nameServiceAddress}` - ); - } - - // Get LayerZero endpoint (from network config or environment) - const lzEndpoint = networkName.includes("xdc") - ? "0x9740FF91F1985D8d2B71494aE1A2f723bb3Ed9E4" // XDC endpoint - : "0x3A73033C0b1407574C76BdBAc67f126f6b4a9AA9"; // CELO endpoint - - const feeRecipient = avatarAddress; // Use Avatar as fee recipient - - try { - console.log("Initializing with parameters:"); - console.log(" Token:", tokenAddress); - console.log(" MinterBurner:", minterBurnerAddress); - console.log(" LZ Endpoint:", lzEndpoint); - console.log(" Owner (Avatar):", avatarAddress); - console.log(" Fee Recipient:", feeRecipient); - console.log(" NameService:", nameServiceAddress); - - const initTx = await oftAdapter.initialize( - tokenAddress, - minterBurnerAddress, - lzEndpoint, - avatarAddress, // Set Avatar as owner during initialization - feeRecipient, - ); - await initTx.wait(); - console.log("✅ Contract initialized successfully!"); - console.log("Transaction hash:", initTx.hash); - - // Verify owner was set - const newOwner = await oftAdapter.owner(); - console.log("New owner after initialization:", newOwner); - - if (newOwner.toLowerCase() === avatarAddress.toLowerCase()) { - console.log("\n✅ Contract initialized and ownership set to Avatar!"); - return; - } else { - console.log("\n⚠️ Warning: Initialization completed but owner is not Avatar."); - console.log("Expected:", avatarAddress); - console.log("Got:", newOwner); - // Continue to try transfer ownership below - currentOwner = newOwner; - } - } catch (initError: any) { - const errorMsg = initError.message || initError.reason || ""; - if (errorMsg.includes("already initialized") || errorMsg.includes("Initializable: contract is already initialized")) { - console.log("⚠️ Contract is already initialized (detected via error)"); - } else { - console.error("❌ Initialization failed:", errorMsg); - throw initError; - } - } - } - - // Check if signer is the current owner (after potential initialization) - if (currentOwner !== ethers.constants.AddressZero && currentOwner.toLowerCase() !== signer.address.toLowerCase()) { - console.log("\n❌ Error: Current owner is not the signer."); - console.log(`Current owner: ${currentOwner}`); - console.log(`Signer: ${signer.address}`); - console.log("\nTo transfer ownership, you must run this script from the owner's account."); - console.log("Alternatively, the current owner can manually call:"); - console.log(` oftAdapter.transferOwnership("${avatarAddress}")`); - throw new Error("Signer is not the current owner"); - } - - // If we reach here and owner is still zero, we can't transfer - if (currentOwner === ethers.constants.AddressZero) { - throw new Error( - "Cannot transfer ownership: Contract owner is zero address and initialization failed.\n" + - "The contract may need to be redeployed. Please check the contract deployment and initialization." - ); - } - - console.log("\n✅ Signer is the current owner. Proceeding with ownership transfer..."); - - // Transfer ownership to Avatar - try { - console.log("\nTransferring ownership to Avatar..."); - const tx = await oftAdapter.transferOwnership(avatarAddress); - console.log("Transaction hash:", tx.hash); - console.log("Waiting for confirmation..."); - - const receipt = await tx.wait(); - console.log("✅ Transaction confirmed!"); - console.log("Block number:", receipt.blockNumber); - console.log("Gas used:", receipt.gasUsed.toString()); - - // Verify ownership was transferred - console.log("\nVerifying ownership transfer..."); - const newOwner = await oftAdapter.owner(); - console.log("New owner:", newOwner); - - if (newOwner.toLowerCase() === avatarAddress.toLowerCase()) { - console.log("\n✅ Successfully transferred ownership to Avatar!"); - } else { - console.log("\n⚠️ Warning: Ownership was not transferred correctly."); - console.log("Expected:", avatarAddress); - console.log("Got:", newOwner); - } - - } catch (error: any) { - console.error("\n❌ Error transferring ownership:"); - if (error.message) { - console.error("Error message:", error.message); - } - if (error.reason) { - console.error("Reason:", error.reason); - } - throw error; - } -}; - -if (require.main === module) { - main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error); - process.exit(1); - }); -} From 4b5a80f765fcde7097a65742881c0cceb609f43e Mon Sep 17 00:00:00 2001 From: blueogin Date: Fri, 10 Jul 2026 14:02:42 -0400 Subject: [PATCH 2/7] Remove detailed error handling and diagnostics from bridge OFT token script --- .../scripts/oft/bridge-oft-token.ts | 50 +------------------ 1 file changed, 1 insertion(+), 49 deletions(-) diff --git a/packages/bridge-contracts/scripts/oft/bridge-oft-token.ts b/packages/bridge-contracts/scripts/oft/bridge-oft-token.ts index c61f149..365ccc6 100644 --- a/packages/bridge-contracts/scripts/oft/bridge-oft-token.ts +++ b/packages/bridge-contracts/scripts/oft/bridge-oft-token.ts @@ -333,55 +333,7 @@ const main = async () => { } catch (error: any) { console.error("\n❌ Error during bridge:"); - - // Provide helpful error messages for common issues - if (error.code === 'CALL_EXCEPTION' || error.reason || error.data) { - const errorData = error.data || error.error?.data || ''; - - // Check for invalid worker options error (error code 0x6592671c = LZ_ULN_InvalidWorkerOptions) - if (errorData.includes('6592671c')) { - console.error("\n🔍 DIAGNOSIS: Invalid Worker Options"); - console.error("The error code 0x6592671c = LZ_ULN_InvalidWorkerOptions indicates invalid extraOptions."); - console.error("This happens when enforced options are required but not properly configured."); - console.error("\nWhat's configured:"); - console.error(" ✅ Send library: Found"); - console.error(" ✅ Peer connection: Set"); - console.error("\nWhat's likely missing:"); - console.error(" ❌ DVN (Data Verification Network) configuration"); - console.error(" ❌ Executor configuration"); - console.error(" ❌ Receive library configuration on destination"); - console.error(" ❌ Complete wiring configuration"); - console.error("\nROOT CAUSE:"); - console.error("The 'lz:oapp:wire' command failed earlier, so enforced options weren't configured."); - console.error("The OApp requires specific worker options (gas limits, etc.) but they're not set."); - console.error("\nSOLUTION:"); - console.error("1. The wiring command MUST succeed to configure enforced options:"); - console.error(` yarn hardhat lz:oapp:wire --oapp-config ./layerzero.config.ts --network ${networkName}`); - console.error(` yarn hardhat lz:oapp:wire --oapp-config ./layerzero.config.ts --network ${destNetworkName}`); - console.error("\n2. If wiring fails with permission errors (0xc4c52593), you need to:"); - console.error(" - Run wiring from an account that has delegate permissions on the endpoint"); - console.error(" - The OApp owner must be set as a delegate on the endpoint"); - console.error(" - Contact LayerZero support if you need help with endpoint permissions"); - console.error("\n3. Alternative: Manually configure enforced options:"); - console.error(" - Use the OApp's setEnforcedOptions function if available"); - console.error(" - Or check LayerZero documentation for manual option configuration"); - console.error("\n4. Check LayerZero Scan for default configurations:"); - console.error(` Visit: https://layerzeroscan.com/tools/defaults?version=V2`); - } else if (error.message?.includes('send library') || error.message?.includes('SendLib') || error.message?.includes('receive library') || error.message?.includes('ReceiveLib')) { - console.error("\n🔍 DIAGNOSIS: LayerZero library configuration issue"); - console.error("Check library configuration using:"); - console.error(` npx hardhat lz:oapp:config:get --oapp-config ./layerzero.config.ts --network ${networkName}`); - } - - // Check for peer errors - if (errorData.includes('NoPeer') || error.message?.includes('peer')) { - console.error("\n🔍 DIAGNOSIS: Peer not configured"); - console.error("The peer connection between chains is not set."); - console.error("\nSOLUTION:"); - console.error(`Run: npx hardhat lz:oapp:wire --oapp-config ./layerzero.config.ts --network ${networkName} --ci`); - } - } - + throw error; } }; From 04bce8aebd70f8045715baa92d55fd7a6061ea0d Mon Sep 17 00:00:00 2001 From: blueogin Date: Fri, 10 Jul 2026 14:22:52 -0400 Subject: [PATCH 3/7] Refactor OFT configuration scripts and restructure LayerZero settings --- packages/bridge-contracts/layerzero.config.ts | 91 --------- .../scripts/oft/OFT_CONFIGURING_GUIDE.md | 185 ++++++------------ .../scripts/oft/adapter-ownership.ts | 104 ---------- .../scripts/oft/bridge-oft-token.ts | 6 +- .../scripts/oft/configure-oft.ts | 85 ++------ .../scripts/oft/grant-minter-role.ts | 137 ------------- .../scripts/oft/layerzero.config.ts | 51 +++++ .../scripts/oft/set-bridge-limits.ts | 172 ---------------- .../scripts/oft/set-oft-operator.ts | 132 ------------- .../scripts/oft/steps/adapter-ownership.ts | 44 +++++ .../scripts/oft/steps/grant-minter-role.ts | 51 +++++ .../scripts/oft/steps/set-bridge-limits.ts | 60 ++++++ 12 files changed, 285 insertions(+), 833 deletions(-) delete mode 100644 packages/bridge-contracts/layerzero.config.ts delete mode 100644 packages/bridge-contracts/scripts/oft/adapter-ownership.ts delete mode 100644 packages/bridge-contracts/scripts/oft/grant-minter-role.ts create mode 100644 packages/bridge-contracts/scripts/oft/layerzero.config.ts delete mode 100644 packages/bridge-contracts/scripts/oft/set-bridge-limits.ts delete mode 100644 packages/bridge-contracts/scripts/oft/set-oft-operator.ts create mode 100644 packages/bridge-contracts/scripts/oft/steps/adapter-ownership.ts create mode 100644 packages/bridge-contracts/scripts/oft/steps/grant-minter-role.ts create mode 100644 packages/bridge-contracts/scripts/oft/steps/set-bridge-limits.ts diff --git a/packages/bridge-contracts/layerzero.config.ts b/packages/bridge-contracts/layerzero.config.ts deleted file mode 100644 index c1c6003..0000000 --- a/packages/bridge-contracts/layerzero.config.ts +++ /dev/null @@ -1,91 +0,0 @@ -/** - * LayerZero OApp Configuration - * - * This config file is used by LayerZero Hardhat tools to wire connections - * between chains and configure messaging libraries, DVNs, and executors. - * - * Pair networks (defaults are development): - * OFT_XDC_NETWORK=production-xdc OFT_CELO_NETWORK=production-celo - * - * Usage: - * npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts --ci - * npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts - */ - -import { EndpointId } from "@layerzerolabs/lz-definitions"; -import type { OmniPointHardhat } from "@layerzerolabs/toolbox-hardhat"; -import { OAppEnforcedOption } from "@layerzerolabs/toolbox-hardhat"; -import { ExecutorOptionType } from "@layerzerolabs/lz-v2-utilities"; -import { TwoWayConfig, generateConnectionsConfig } from "@layerzerolabs/metadata-tools"; -import { getOftDeploymentAddresses } from "./deploy/utils/getOftDeploymentAddresses"; - -const XDC_NETWORK = process.env.OFT_XDC_NETWORK || "development-xdc"; -const CELO_NETWORK = process.env.OFT_CELO_NETWORK || "development-celo"; - -// Get contract addresses from hardhat-deploy artifacts (`deployments/`) -const { GoodDollarOFTAdapter: xdcOftAdapterAddress } = getOftDeploymentAddresses(XDC_NETWORK); -const { GoodDollarOFTAdapter: celoOftAdapterAddress } = getOftDeploymentAddresses(CELO_NETWORK); - -// XDC Network contract -const xdcContract: OmniPointHardhat = { - eid: EndpointId.XDC_V2_MAINNET, // XDC endpoint ID - contractName: "GoodDollarOFTAdapter", - address: xdcOftAdapterAddress, // Will be updated after deployment -}; - -// CELO Network contract -const celoContract: OmniPointHardhat = { - eid: EndpointId.CELO_V2_MAINNET, // CELO endpoint ID - contractName: "GoodDollarOFTAdapter", - address: celoOftAdapterAddress, // Will be updated after deployment -}; - -// Enforced execution options for EVM chains -// These set the minimum gas limits for lzReceive on the destination chain -const EVM_ENFORCED_OPTIONS: OAppEnforcedOption[] = [ - { - msgType: 1, // SEND message type - optionType: ExecutorOptionType.LZ_RECEIVE, - gas: 200000, // Gas limit for lzReceive execution on destination - value: 0, // Native token value to send (0 for now) - }, -]; - -// Define the pathways between chains -// This creates a bidirectional connection: XDC <-> CELO -const pathways: TwoWayConfig[] = [ - [ - // 1) Destination chain contract (CELO) - celoContract, - - // 2) Source chain contract (XDC) - xdcContract, - - // 3) Channel security settings: - // • first array = "required" DVN names (must sign) - // • second array = "optional" DVN names array + threshold - // • third value = threshold (number of optionalDVNs that must sign) - [["LayerZero Labs" /* Add more DVN names here if needed */], []], - - // 4) Block confirmations: - // [confirmations for CELO → XDC, confirmations for XDC → CELO] - [20, 20], - - // 5) Enforced execution options: - // [options for CELO → XDC, options for XDC → CELO] - [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], - ], -]; - -export default async function () { - // Generate the connections config based on the pathways - const connections = await generateConnectionsConfig(pathways); - return { - contracts: [ - { contract: xdcContract }, - { contract: celoContract }, - ], - connections, - }; -} - diff --git a/packages/bridge-contracts/scripts/oft/OFT_CONFIGURING_GUIDE.md b/packages/bridge-contracts/scripts/oft/OFT_CONFIGURING_GUIDE.md index 3b02960..a1fd59d 100644 --- a/packages/bridge-contracts/scripts/oft/OFT_CONFIGURING_GUIDE.md +++ b/packages/bridge-contracts/scripts/oft/OFT_CONFIGURING_GUIDE.md @@ -1,162 +1,105 @@ -# OFT (Omnichain Fungible Token) Configuration Guide +# OFT bridge deploy & configure -Configure the GoodDollar OFT bridge between XDC and CELO (LayerZero). Post-deploy steps live under `scripts/oft/` and are orchestrated by `yarn oft:configure`. +From `packages/bridge-contracts`. Addresses are written to git-ignored hardhat-deploy artifacts: -## Overview +`deployments//GoodDollarOFTAdapter.json` +`deployments//GoodDollarOFTMinterBurner.json` -1. Deploy OFT proxies/implementations (`deploy/deployOFT.ts`) -2. Configure each network with `yarn oft:configure` (grant minter → limits → LayerZero wire → adapter ownership) -3. Later upgrades use `deploy/upgradeOFT.ts` (`OFT-Upgrade` tag); **proxy addresses stay unchanged** - -## Config values explained - -### CREATE2 salts (`deploy/deployOFT.ts`) - -| Salt | How it is derived | Purpose | -|------|-------------------|---------| -| Proxy salt (MinterBurner) | `keccak256("Development-GoodDollarOFTMinterBurnerV1")` or `Production-...V1` | Deterministic **proxy** address per env | -| Proxy salt (OFT Adapter) | `keccak256("Development-GoodDollarOFTAdapterV1")` or `Production-...V1` | Deterministic **proxy** address per env | -| Implementation salt | `keccak256(compiled bytecode)` of each contract | Deterministic **implementation** address; changes when bytecode changes | - -Proxy salts are fixed strings so redeploys of the same env target the same proxy addresses. Implementation salts track bytecode so a new implementation gets a new CREATE2 address without moving the proxy. - -### LayerZero endpoints (`deploy/deployOFT.ts`) - -Passed as constructor args to `GoodDollarOFTAdapter` implementation: - -| Hardhat network | Endpoint | -|-----------------|----------| -| `development-celo` / `production-celo` | `0x1a44076050125825900e736c501f859c50fE728c` | -| `development-xdc` / `production-xdc` | `0xcb566e3B6934Fa77258d68ea18E931fa75e1aaAa` | - -Override with `LAYERZERO_ENDPOINT` if needed. Wiring peers/DVNs uses `layerzero.config.ts` (not these constructor args alone). - -`layerzero.config.ts` loads adapter addresses from hardhat-deploy artifacts. Pair networks via env (set automatically by `oft:configure`): - -- `OFT_XDC_NETWORK` — default `development-xdc` -- `OFT_CELO_NETWORK` — default `development-celo` - -### Bridge limits (`scripts/oft/oft.config.json`) - -Per-network `limits` object (human-readable G$ amounts; the script converts to 18-decimal wei when the string is short / decimal): - -| Field | Meaning | -|-------|---------| -| `dailyLimit` | Max total G$ bridged in a rolling 24h window (global) | -| `txLimit` | Max G$ per single bridge transaction | -| `accountDailyLimit` | Max G$ one account can bridge in a rolling 24h window | -| `minAmount` | Minimum G$ amount per bridge transaction | -| `onlyWhitelisted` | If `true`, only Identity-whitelisted senders may bridge | - -Skip behavior is controlled by CLI flags on `yarn oft:configure`, not by this JSON file. - -Example: - -```json -{ - "production-xdc": { - "limits": { - "dailyLimit": "1000000", - "txLimit": "100000", - "accountDailyLimit": "50000", - "minAmount": "10", - "onlyWhitelisted": false - } - } -} -``` - -## Step 1: Deploy OFT contracts +## Deploy ```bash -npx hardhat deploy --tags OFT --network development-xdc -npx hardhat deploy --tags OFT --network development-celo - npx hardhat deploy --tags OFT --network production-xdc npx hardhat deploy --tags OFT --network production-celo ``` -This deploys upgradeable proxies for `GoodDollarOFTMinterBurner` and `GoodDollarOFTAdapter` and writes addresses under `deployments//` (git-ignored). +Use `development-xdc` / `development-celo` for staging. Deploy **both** chains before configure (wiring needs both adapters). -`GoodDollarOFTMinterBurner.initialize(nameService, adapter)` sets the OFT adapter as operator; no separate set-operator step is required for the happy path. +### Deploy parameters (`deploy/deployOFT.ts`) -## Step 2: Configure (`yarn oft:configure`) +| Parameter | Source | Notes | +|-----------|--------|--------| +| Token / NameService / Controller | `@gooddollar/goodprotocol` `deployment.json` for the Hardhat network | Required | +| LayerZero endpoint | Built-in map, or `LAYERZERO_ENDPOINT` | Celo `0x1a44…728c`, XDC `0xcb56…aaAa` | +| Proxy CREATE2 salt | `Development-…V1` or `Production-…V1` string hash | Fixed proxy address per env | +| Implementation CREATE2 salt | `keccak256(bytecode)` | Changes when code changes | -Run on **each** network after both sides are deployed (wire needs both adapters in artifacts): +## Configure ```bash yarn oft:configure --network production-xdc yarn oft:configure --network production-celo ``` -Development: +### Steps (in order) -```bash -yarn oft:configure --network development-xdc -yarn oft:configure --network development-celo -``` - -### Order of steps - -1. Grant `MINTER_ROLE` to `GoodDollarOFTMinterBurner` (via Controller/Avatar) -2. Set bridge limits from `oft.config.json` -3. Wire LayerZero (`lz:oapp:wire --ci`) using `layerzero.config.ts` -4. Transfer `GoodDollarOFTAdapter` ownership to DAO Avatar (`adapter-ownership.ts`) - -Ownership transfer is last so limits and wiring still run as the deployer owner. +1. **Minter role** — grant `MINTER_ROLE` on G$ to `GoodDollarOFTMinterBurner` via Controller/Avatar +2. **Bridge limits** — `setBridgeLimits` from `scripts/oft/oft.config.json` +3. **LayerZero wire** — `lz:oapp:wire --ci` using `scripts/oft/layerzero.config.ts` (peers / DVNs / options) +4. **Adapter ownership** — transfer `GoodDollarOFTAdapter` owner to DAO Avatar -### CLI flags +### Skip flags ```bash yarn oft:configure --network production-xdc --skip-wire -yarn oft:configure --network production-celo --skip-minter --skip-limits -yarn oft:configure --network production-xdc --skip-adapter-ownership +yarn oft:configure --network production-celo --skip-minter --skip-limits --skip-adapter-ownership ``` -| Flag | Effect | +| Flag | Skips | |------|--------| -| `--skip-minter` | Skip granting MINTER_ROLE | -| `--skip-limits` | Skip `setBridgeLimits` | -| `--skip-wire` | Skip `lz:oapp:wire` | -| `--skip-adapter-ownership` | Skip ownership transfer to Avatar | +| `--skip-minter` | Step 1 | +| `--skip-limits` | Step 2 | +| `--skip-wire` | Step 3 | +| `--skip-adapter-ownership` | Step 4 | -### Individual helpers (optional) +### Limit parameters (`oft.config.json`) -Helpers used by `oft:configure` can still be run alone: +Values are **G$ amounts** (e.g. `"100"` → 100 G$), not wei: -```bash -npx hardhat run scripts/oft/grant-minter-role.ts --network production-xdc -npx hardhat run scripts/oft/set-bridge-limits.ts --network production-xdc -npx hardhat lz:oapp:wire --oapp-config ./layerzero.config.ts --network production-xdc --ci -npx hardhat run scripts/oft/adapter-ownership.ts --network production-xdc -``` - -Other utilities: +| Field | Meaning | +|-------|---------| +| `dailyLimit` | Max G$ bridged per 24h (all users) | +| `txLimit` | Max G$ in one tx | +| `accountDailyLimit` | Max G$ per account per 24h | +| `minAmount` | Min G$ per tx | +| `onlyWhitelisted` | `true` → only Identity-whitelisted senders | -- `scripts/oft/adapter-ownership.ts` — transfer adapter ownership to Avatar (also step 4 of `oft:configure`) -- `scripts/oft/bridge-oft-token.ts` — manual bridge smoke test -- `scripts/oft/set-oft-operator.ts` — set operator if needed outside initialize -- `scripts/oft/transfer-oft-adapter-ownership-from-avatar.ts` — transfer ownership away from Avatar via Controller +### LayerZero pair env -### LayerZero wire notes +`oft:configure` sets these from the network name (`production` vs `development`): -- Wiring may fail with permission errors (`0xc4c52593`) if the OApp owner lacks endpoint delegate permissions -- Prefer running wire **before** transferring ownership to Avatar -- `oft:configure` sets `OFT_XDC_NETWORK` / `OFT_CELO_NETWORK` from whether the Hardhat network name contains `production` +- `OFT_XDC_NETWORK` +- `OFT_CELO_NETWORK` -## Upgrades +## Upgrade -Proxy addresses remain fixed. Deploy new implementations and point proxies at them: +Proxy addresses stay the same: ```bash npx hardhat deploy --tags OFT-Upgrade --network production-xdc npx hardhat deploy --tags OFT-Upgrade --network production-celo ``` -## Verification checklist +## Bridge (smoke test) + +After deploy + configure on **both** chains, send ~1 G$ from the source network: + +```bash +npx hardhat run scripts/oft/bridge-oft-token.ts --network production-xdc +npx hardhat run scripts/oft/bridge-oft-token.ts --network production-celo +``` + +`--network` is the **source** chain (XDC → CELO or CELO → XDC). Needs G$ on source, native gas for the tx + LayerZero fee, peers wired, and minter role on both sides. Track delivery on [LayerZero Scan](https://layerzeroscan.com/). + +## Add a network for LayerZero wiring + +Today wiring is **XDC ↔ CELO** in `scripts/oft/layerzero.config.ts`. To add another chain: + +1. **Deploy** OFT on the new Hardhat network (`--tags OFT`) so `deployments//GoodDollarOFTAdapter.json` exists +2. **Hardhat** — network entry in `hardhat.config.ts` (RPC + accounts) +3. **Endpoint** — add the chain’s LZ V2 endpoint in `deploy/deployOFT.ts` (or set `LAYERZERO_ENDPOINT`) +4. **`layerzero.config.ts`** — new `OmniPointHardhat` with `EndpointId._V2_…` + adapter address from `getOftDeploymentAddresses` +5. **Pathway** — append a `pathways` entry: `[dest, src, DVNs, confirmations, enforcedOptions]` (same shape as the existing XDC↔CELO pair) +6. **Configure helpers** — limits in `oft.config.json`; if `oft:configure` pair env only knows xdc/celo, extend `resolveOftPairNetworks` (or set `OFT_*_NETWORK` yourself) +7. **Wire** — `yarn oft:configure --network ` (and on each peer you care about) -1. Addresses present in `deployments//GoodDollarOFTAdapter.json` and `GoodDollarOFTMinterBurner.json` -2. `GoodDollarOFTMinterBurner` has minter role on GoodDollar -3. LayerZero peers configured between XDC and CELO -4. Bridge limits match `oft.config.json` -5. OFT adapter `owner()` is DAO Avatar (after configure without `--skip-adapter-ownership`) +`EndpointId` values come from `@layerzerolabs/lz-definitions`. DVN names must match LayerZero metadata for that chain. diff --git a/packages/bridge-contracts/scripts/oft/adapter-ownership.ts b/packages/bridge-contracts/scripts/oft/adapter-ownership.ts deleted file mode 100644 index 12ade89..0000000 --- a/packages/bridge-contracts/scripts/oft/adapter-ownership.ts +++ /dev/null @@ -1,104 +0,0 @@ -/*** - * Transfer GoodDollarOFTAdapter ownership to DAO Avatar. - * - * Used by configure-oft.ts (step 4). Standalone: - * npx hardhat run scripts/oft/adapter-ownership.ts --network development-celo - * - * Must be run by the current OFT adapter owner. - */ - -import { network, ethers } from "hardhat"; -import Contracts from "@gooddollar/goodprotocol/releases/deployment.json"; -import { getOftDeploymentAddresses } from "../../deploy/utils/getOftDeploymentAddresses"; - -export const main = async () => { - const networkName = network.name; - const [signer] = await ethers.getSigners(); - - console.log("=== Transfer GoodDollarOFTAdapter Ownership to Avatar ==="); - console.log("Network:", networkName); - console.log("Signer:", signer.address); - console.log( - "Signer balance:", - ethers.utils.formatEther(await ethers.provider.getBalance(signer.address)), - "ETH/CELO" - ); - - const { GoodDollarOFTAdapter: oftAdapterAddress } = getOftDeploymentAddresses(networkName); - const goodProtocolContracts = Contracts[networkName as keyof typeof Contracts] as any; - - if (!goodProtocolContracts) { - throw new Error(`No GoodProtocol contracts found for network: ${networkName}`); - } - - const avatarAddress = goodProtocolContracts.Avatar; - if (!avatarAddress) { - throw new Error(`Avatar not found in GoodProtocol deployment.json for ${networkName}`); - } - - console.log("\nContract addresses:"); - console.log("GoodDollarOFTAdapter:", oftAdapterAddress); - console.log("Avatar:", avatarAddress); - - const oftAdapter = await ethers.getContractAt("GoodDollarOFTAdapter", oftAdapterAddress); - const currentOwner = await oftAdapter.owner(); - - console.log("\nCurrent owner:", currentOwner); - console.log("Target owner (Avatar):", avatarAddress); - - if (currentOwner.toLowerCase() === avatarAddress.toLowerCase()) { - console.log("\n✅ GoodDollarOFTAdapter is already owned by Avatar. No action needed."); - return; - } - - if (currentOwner.toLowerCase() !== signer.address.toLowerCase()) { - console.log("\n❌ Error: Current owner is not the signer."); - console.log(`Current owner: ${currentOwner}`); - console.log(`Signer: ${signer.address}`); - console.log(`Manually call: oftAdapter.transferOwnership("${avatarAddress}")`); - throw new Error("Signer is not the current owner"); - } - - console.log("\n✅ Signer is the current owner. Proceeding with ownership transfer..."); - - try { - console.log("\nTransferring ownership to Avatar..."); - const tx = await oftAdapter.transferOwnership(avatarAddress); - console.log("Transaction hash:", tx.hash); - console.log("Waiting for confirmation..."); - - const receipt = await tx.wait(); - console.log("✅ Transaction confirmed!"); - console.log("Block number:", receipt.blockNumber); - console.log("Gas used:", receipt.gasUsed.toString()); - - const newOwner = await oftAdapter.owner(); - console.log("\nNew owner:", newOwner); - - if (newOwner.toLowerCase() === avatarAddress.toLowerCase()) { - console.log("\n✅ Successfully transferred ownership to Avatar!"); - } else { - console.log("\n⚠️ Warning: Ownership was not transferred correctly."); - console.log("Expected:", avatarAddress); - console.log("Got:", newOwner); - } - } catch (error: any) { - console.error("\n❌ Error transferring ownership:"); - if (error.message) { - console.error("Error message:", error.message); - } - if (error.reason) { - console.error("Reason:", error.reason); - } - throw error; - } -}; - -if (require.main === module) { - main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error); - process.exit(1); - }); -} diff --git a/packages/bridge-contracts/scripts/oft/bridge-oft-token.ts b/packages/bridge-contracts/scripts/oft/bridge-oft-token.ts index 365ccc6..2400fbf 100644 --- a/packages/bridge-contracts/scripts/oft/bridge-oft-token.ts +++ b/packages/bridge-contracts/scripts/oft/bridge-oft-token.ts @@ -153,7 +153,7 @@ const main = async () => { console.log("You need to set the peer before bridging. Run this command:"); console.log(` oftAdapter.setPeer(${destEndpointId}, "${expectedPeer}")`); console.log("\nOr use the LayerZero wire command:"); - console.log(` npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts --network ${networkName}`); + console.log(` npx hardhat lz:oapp:wire --oapp-config ./scripts/oft/layerzero.config.ts --network ${networkName}`); throw new Error(`NoPeer: ${destNetwork} peer (endpoint ${destEndpointId}) is not set. Expected: ${destOFTAdapter}`); } @@ -216,7 +216,7 @@ const main = async () => { throw new Error( `No send library configured for ${destNetwork} (eid ${destEndpointId}). ` + `You need to run the LayerZero wiring command: ` + - `yarn hardhat lz:oapp:wire --oapp-config ./layerzero.config.ts --network ${networkName}` + `yarn hardhat lz:oapp:wire --oapp-config ./scripts/oft/layerzero.config.ts --network ${networkName}` ); } else { console.log("ℹ️ Using default send library. Consider configuring a specific send library for better control."); @@ -226,7 +226,7 @@ const main = async () => { `Send library not configured for ${destNetwork} (eid ${destEndpointId}). ` + `Error: ${e.message}. ` + `You need to run the LayerZero wiring command: ` + - `yarn hardhat lz:oapp:wire --oapp-config ./layerzero.config.ts --network ${networkName}` + `yarn hardhat lz:oapp:wire --oapp-config ./scripts/oft/layerzero.config.ts --network ${networkName}` ); } } else { diff --git a/packages/bridge-contracts/scripts/oft/configure-oft.ts b/packages/bridge-contracts/scripts/oft/configure-oft.ts index c4d2142..3b2bc7e 100644 --- a/packages/bridge-contracts/scripts/oft/configure-oft.ts +++ b/packages/bridge-contracts/scripts/oft/configure-oft.ts @@ -1,30 +1,9 @@ -/*** - * Orchestrates post-deploy OFT configuration for a single Hardhat network. - * - * Steps (in order): - * 1. Grant MINTER_ROLE to GoodDollarOFTMinterBurner - * 2. Set bridge limits from scripts/oft/oft.config.json - * 3. Wire LayerZero peers / DVNs / enforced options (lz:oapp:wire --ci) - * 4. Transfer GoodDollarOFTAdapter ownership to DAO Avatar - * - * Usage: - * yarn oft:configure --network production-xdc - * yarn oft:configure --network production-celo - * yarn oft:configure --network development-xdc --skip-wire - * - * Flags (forwarded by scripts/oft/run-configure.js): - * --skip-minter - * --skip-limits - * --skip-wire - * --skip-adapter-ownership - */ - import { spawnSync } from "child_process"; import path from "path"; import { network } from "hardhat"; -import { main as grantMinterRole } from "./grant-minter-role"; -import { main as setBridgeLimits } from "./set-bridge-limits"; -import { main as transferAdapterOwnership } from "./adapter-ownership"; +import { main as grantMinterRole } from "./steps/grant-minter-role"; +import { main as setBridgeLimits } from "./steps/set-bridge-limits"; +import { main as transferAdapterOwnership } from "./steps/adapter-ownership"; type ConfigureFlags = { skipMinter: boolean; @@ -57,25 +36,11 @@ function resolveOftPairNetworks(networkName: string): { xdc: string; celo: strin function wireLayerZero(networkName: string) { const pair = resolveOftPairNetworks(networkName); - const packageRoot = path.resolve(__dirname, "../.."); - console.log("\n=== Wire LayerZero OApp ==="); - console.log("Network (signer/RPC):", networkName); - console.log("OFT_XDC_NETWORK:", pair.xdc); - console.log("OFT_CELO_NETWORK:", pair.celo); - const result = spawnSync( "npx", - [ - "hardhat", - "lz:oapp:wire", - "--oapp-config", - "./layerzero.config.ts", - "--network", - networkName, - "--ci", - ], + ["hardhat", "lz:oapp:wire", "--oapp-config", "./scripts/oft/layerzero.config.ts", "--network", networkName, "--ci"], { - cwd: packageRoot, + cwd: path.resolve(__dirname, "../.."), stdio: "inherit", env: { ...process.env, @@ -84,49 +49,23 @@ function wireLayerZero(networkName: string) { }, } ); - - if (result.error) { - throw result.error; - } + if (result.error) throw result.error; if (result.status !== 0) { throw new Error(`LayerZero wire failed with exit code ${result.status ?? "unknown"}`); } - console.log("✅ LayerZero wire completed"); } export const main = async () => { const flags = parseFlags(); const networkName = network.name; + console.log(`oft:configure ${networkName}`, flags); - console.log("=== Configure GoodDollar OFT ==="); - console.log("Network:", networkName); - console.log("Flags:", flags); - - if (!flags.skipMinter) { - await grantMinterRole(); - } else { - console.log("\nSkipping grant MINTER_ROLE (--skip-minter)"); - } - - if (!flags.skipLimits) { - await setBridgeLimits(); - } else { - console.log("\nSkipping set bridge limits (--skip-limits)"); - } - - if (!flags.skipWire) { - wireLayerZero(networkName); - } else { - console.log("\nSkipping LayerZero wire (--skip-wire)"); - } - - if (!flags.skipAdapterOwnership) { - await transferAdapterOwnership(); - } else { - console.log("\nSkipping adapter ownership transfer (--skip-adapter-ownership)"); - } + if (!flags.skipMinter) await grantMinterRole(); + if (!flags.skipLimits) await setBridgeLimits(); + if (!flags.skipWire) wireLayerZero(networkName); + if (!flags.skipAdapterOwnership) await transferAdapterOwnership(); - console.log("\n=== OFT configuration complete ==="); + console.log(`oft:configure done (${networkName})`); }; if (require.main === module) { diff --git a/packages/bridge-contracts/scripts/oft/grant-minter-role.ts b/packages/bridge-contracts/scripts/oft/grant-minter-role.ts deleted file mode 100644 index 8f109d0..0000000 --- a/packages/bridge-contracts/scripts/oft/grant-minter-role.ts +++ /dev/null @@ -1,137 +0,0 @@ -/*** - * Script to grant MINTER_ROLE to GoodDollarOFTMinterBurner contract - * Uses genericCall through Avatar/Controller to execute the transaction - * - * Usage: - * npx hardhat run scripts/oft/grant-minter-role.ts --network development-celo - * - * Note: This script must be run by a guardian or address with permissions to execute via Controller - */ - -import { network, ethers } from "hardhat"; -import Contracts from "@gooddollar/goodprotocol/releases/deployment.json"; -import { getOftDeploymentAddresses } from "../../deploy/utils/getOftDeploymentAddresses"; - -export const main = async () => { - const networkName = network.name; - const [signer] = await ethers.getSigners(); - - console.log("=== Grant MINTER_ROLE to GoodDollarOFTMinterBurner ==="); - console.log("Network:", networkName); - console.log("Signer:", signer.address); - - // Derive native token name from network - const nativeTokenName = networkName.includes("celo") ? "CELO" : networkName.includes("xdc") ? "XDC" : "native token"; - console.log("Signer balance:", ethers.utils.formatEther(await ethers.provider.getBalance(signer.address)), nativeTokenName); - - // Get deployment info from GoodProtocol and GoodBridge - const goodProtocolContracts = Contracts[networkName as keyof typeof Contracts] as any; - if (!goodProtocolContracts) { - throw new Error(`No GoodProtocol contracts found for network: ${networkName}`); - } - - const tokenAddress = goodProtocolContracts.GoodDollar || goodProtocolContracts.SuperGoodDollar; - const { GoodDollarOFTMinterBurner: minterBurnerAddress } = getOftDeploymentAddresses(networkName); - const controllerAddress = goodProtocolContracts.Controller; - const avatarAddress = goodProtocolContracts.Avatar; - - if (!tokenAddress) { - throw new Error(`GoodDollar token not found in GoodProtocol deployment.json for ${networkName}`); - } - - // `getOftDeploymentAddresses` throws if missing - - if (!controllerAddress) { - throw new Error(`Controller not found in GoodProtocol deployment.json for ${networkName}`); - } - - if (!avatarAddress) { - throw new Error(`Avatar not found in GoodProtocol deployment.json for ${networkName}`); - } - - console.log("\nContract addresses:"); - console.log("GoodDollar token:", tokenAddress); - console.log("GoodDollarOFTMinterBurner:", minterBurnerAddress); - console.log("Controller:", controllerAddress); - console.log("Avatar:", avatarAddress); - - // Get token contract to check current status - // Use the local interface file - const token = await ethers.getContractAt( - "contracts/oft/interfaces/ISuperGoodDollar.sol:ISuperGoodDollar", - tokenAddress - ); - - // Check if MinterBurner already has minter role - const isMinter = await token.isMinter(minterBurnerAddress); - console.log("\nCurrent status:"); - console.log("MinterBurner has MINTER_ROLE:", isMinter); - - if (isMinter) { - console.log("\n✅ GoodDollarOFTMinterBurner already has MINTER_ROLE. No action needed."); - return; - } - - // Prepare the generic call through Avatar - // Function signature: addMinter(address) - const functionSignature = "addMinter(address)"; - - // Encode the function input (minterBurnerAddress) - const abiCoder = ethers.utils.defaultAbiCoder; - const functionInputs = abiCoder.encode(["address"], [minterBurnerAddress]); - - console.log("\nPreparing generic call:"); - console.log("Function:", functionSignature); - console.log("Target contract:", tokenAddress); - console.log("Parameter (minterBurner):", minterBurnerAddress); - - // Execute via Controller/Avatar - try { - console.log("\nExecuting via Controller/Avatar..."); - const Controller = await ethers.getContractAt("Controller", controllerAddress); - - // Use genericCall to execute through Avatar - // Encode the function call: function selector + parameters - const functionSelector = ethers.utils.id(functionSignature).slice(0, 10); - const encodedCall = ethers.utils.hexConcat([functionSelector, functionInputs]); - - const tx = await Controller.genericCall( - tokenAddress, - encodedCall, - avatarAddress, - 0 - ); - await tx.wait(); - console.log("Transaction hash:", tx.hash); - - // Verify the role was granted - console.log("\nVerifying role was granted..."); - const isMinterAfter = await token.isMinter(minterBurnerAddress); - console.log("MinterBurner has MINTER_ROLE:", isMinterAfter); - - if (isMinterAfter) { - console.log("\n✅ Successfully granted MINTER_ROLE to GoodDollarOFTMinterBurner via Avatar!"); - } else { - console.log("\n⚠️ Warning: MINTER_ROLE was not granted. Please check the transaction."); - } - - } catch (error: any) { - console.error("\n❌ Error granting MINTER_ROLE:"); - if (error.message) { - console.error("Error message:", error.message); - } - if (error.reason) { - console.error("Reason:", error.reason); - } - throw error; - } -}; - -if (require.main === module) { - main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error); - process.exit(1); - }); -} diff --git a/packages/bridge-contracts/scripts/oft/layerzero.config.ts b/packages/bridge-contracts/scripts/oft/layerzero.config.ts new file mode 100644 index 0000000..7c0ef4c --- /dev/null +++ b/packages/bridge-contracts/scripts/oft/layerzero.config.ts @@ -0,0 +1,51 @@ +import { EndpointId } from "@layerzerolabs/lz-definitions"; +import type { OmniPointHardhat } from "@layerzerolabs/toolbox-hardhat"; +import { OAppEnforcedOption } from "@layerzerolabs/toolbox-hardhat"; +import { ExecutorOptionType } from "@layerzerolabs/lz-v2-utilities"; +import { TwoWayConfig, generateConnectionsConfig } from "@layerzerolabs/metadata-tools"; +import { getOftDeploymentAddresses } from "../../deploy/utils/getOftDeploymentAddresses"; + +const XDC_NETWORK = process.env.OFT_XDC_NETWORK || "development-xdc"; +const CELO_NETWORK = process.env.OFT_CELO_NETWORK || "development-celo"; + +const { GoodDollarOFTAdapter: xdcOftAdapterAddress } = getOftDeploymentAddresses(XDC_NETWORK); +const { GoodDollarOFTAdapter: celoOftAdapterAddress } = getOftDeploymentAddresses(CELO_NETWORK); + +const xdcContract: OmniPointHardhat = { + eid: EndpointId.XDC_V2_MAINNET, + contractName: "GoodDollarOFTAdapter", + address: xdcOftAdapterAddress, +}; + +const celoContract: OmniPointHardhat = { + eid: EndpointId.CELO_V2_MAINNET, + contractName: "GoodDollarOFTAdapter", + address: celoOftAdapterAddress, +}; + +const EVM_ENFORCED_OPTIONS: OAppEnforcedOption[] = [ + { + msgType: 1, + optionType: ExecutorOptionType.LZ_RECEIVE, + gas: 200000, + value: 0, + }, +]; + +const pathways: TwoWayConfig[] = [ + [ + celoContract, + xdcContract, + [["LayerZero Labs"], []], + [20, 20], + [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], + ], +]; + +export default async function () { + const connections = await generateConnectionsConfig(pathways); + return { + contracts: [{ contract: xdcContract }, { contract: celoContract }], + connections, + }; +} diff --git a/packages/bridge-contracts/scripts/oft/set-bridge-limits.ts b/packages/bridge-contracts/scripts/oft/set-bridge-limits.ts deleted file mode 100644 index 186db71..0000000 --- a/packages/bridge-contracts/scripts/oft/set-bridge-limits.ts +++ /dev/null @@ -1,172 +0,0 @@ -/*** - * Script to set bridge limits for GoodDollarOFTAdapter contract - * Calls setBridgeLimits directly on the OFT adapter (not via Controller/Avatar). - * - * Note: Limits are managed in GoodDollarOFTAdapter, not GoodDollarOFTMinterBurner - * - * Usage: - * npx hardhat run scripts/oft/set-bridge-limits.ts --network development-celo - * - * Configuration: - * Limit values are read from scripts/oft/oft.config.json - * Each network/env has its entry in the config file. - * - * Note: This script must be run by the OFT adapter owner (usually the deployer or DAO Avatar) - */ - -import { network, ethers } from "hardhat"; -import { BigNumber } from "ethers"; -import Contracts from "@gooddollar/goodprotocol/releases/deployment.json"; -import { getOftDeploymentAddresses } from "../../deploy/utils/getOftDeploymentAddresses"; -import config from "./oft.config.json"; - -export const main = async () => { - const networkName = network.name; - const [signer] = await ethers.getSigners(); - - console.log("=== Set Bridge Limits for GoodDollarOFTAdapter ==="); - console.log("Network:", networkName); - console.log("Signer:", signer.address); - - // Derive native token name from network - const nativeTokenName = networkName.includes("celo") ? "CELO" : networkName.includes("xdc") ? "XDC" : "native token"; - console.log("Signer balance:", ethers.utils.formatEther(await ethers.provider.getBalance(signer.address)), nativeTokenName); - - // Get deployment info (from hardhat-deploy artifacts) - const { GoodDollarOFTAdapter: oftAdapterAddress } = getOftDeploymentAddresses(networkName); - - // Get GoodProtocol contracts for Controller and Avatar - const goodProtocolContracts = Contracts[networkName as keyof typeof Contracts] as any; - if (!goodProtocolContracts) { - throw new Error(`No GoodProtocol contracts found for network: ${networkName}`); - } - - const controllerAddress = goodProtocolContracts.Controller; - const avatarAddress = goodProtocolContracts.Avatar; - - if (!controllerAddress) { - throw new Error(`Controller not found in GoodProtocol deployment.json for ${networkName}`); - } - - if (!avatarAddress) { - throw new Error(`Avatar not found in GoodProtocol deployment.json for ${networkName}`); - } - - console.log("\nContract addresses:"); - console.log("GoodDollarOFTAdapter:", oftAdapterAddress); - console.log("Controller:", controllerAddress); - console.log("Avatar:", avatarAddress); - - // Get current limits from OFTAdapter contract - const oftAdapter = await ethers.getContractAt("GoodDollarOFTAdapter", oftAdapterAddress); - const currentLimits = await oftAdapter.bridgeLimits(); - - console.log("\nCurrent bridge limits:"); - console.log("Daily Limit:", ethers.utils.formatEther(currentLimits.dailyLimit), "G$"); - console.log("Transaction Limit:", ethers.utils.formatEther(currentLimits.txLimit), "G$"); - console.log("Account Daily Limit:", ethers.utils.formatEther(currentLimits.accountDailyLimit), "G$"); - console.log("Min Amount:", ethers.utils.formatEther(currentLimits.minAmount), "G$"); - console.log("Only Whitelisted:", currentLimits.onlyWhitelisted); - - // Get config for this network - const networkConfig = (config as any)[networkName]; - if (!networkConfig || !networkConfig.limits) { - console.log("\n⚠️ No limits configuration found for this network."); - console.log(`Please add a "limits" entry for "${networkName}" in scripts/oft/oft.config.json`); - return; - } - - const limitsConfig = networkConfig.limits; - - // Parse limit values (values can be in decimal format, e.g., "1000000" for 1M G$) - // The script will automatically convert them to wei (18 decimals) - const parseLimit = (value: string | undefined): BigNumber | null => { - if (!value) return null; - // Check if value contains a decimal point or is a simple number - // If it's a simple number string, treat it as G$ and convert to wei - // If it's already in wei format (very large number), use it as-is - const numValue = value.trim(); - if (numValue.includes('.') || numValue.length < 15) { - // Treat as decimal G$ value and convert to wei - return ethers.utils.parseEther(numValue); - } else { - // Assume it's already in wei format - return ethers.BigNumber.from(numValue); - } - }; - - const dailyLimit = parseLimit(limitsConfig.dailyLimit); - const txLimit = parseLimit(limitsConfig.txLimit); - const accountDailyLimit = parseLimit(limitsConfig.accountDailyLimit); - const minAmount = parseLimit(limitsConfig.minAmount); - const onlyWhitelisted = limitsConfig.onlyWhitelisted !== undefined ? limitsConfig.onlyWhitelisted : null; - - // Prepare new limits struct (use current values if not provided) - const newLimits = { - dailyLimit: dailyLimit !== null ? dailyLimit : currentLimits.dailyLimit, - txLimit: txLimit !== null ? txLimit : currentLimits.txLimit, - accountDailyLimit: accountDailyLimit !== null ? accountDailyLimit : currentLimits.accountDailyLimit, - minAmount: minAmount !== null ? minAmount : currentLimits.minAmount, - onlyWhitelisted: onlyWhitelisted !== null ? onlyWhitelisted : currentLimits.onlyWhitelisted - }; - - // Check if limits are changing - const limitsChanged = - !newLimits.dailyLimit.eq(currentLimits.dailyLimit) || - !newLimits.txLimit.eq(currentLimits.txLimit) || - !newLimits.accountDailyLimit.eq(currentLimits.accountDailyLimit) || - !newLimits.minAmount.eq(currentLimits.minAmount) || - newLimits.onlyWhitelisted !== currentLimits.onlyWhitelisted; - - if (!limitsChanged) { - console.log("\n✅ All limits are already set to the requested values. No transactions needed."); - return; - } - - console.log("\n📝 New bridge limits to set:"); - console.log("Daily Limit:", ethers.utils.formatEther(newLimits.dailyLimit), "G$"); - console.log("Transaction Limit:", ethers.utils.formatEther(newLimits.txLimit), "G$"); - console.log("Account Daily Limit:", ethers.utils.formatEther(newLimits.accountDailyLimit), "G$"); - console.log("Min Amount:", ethers.utils.formatEther(newLimits.minAmount), "G$"); - console.log("Only Whitelisted:", newLimits.onlyWhitelisted); - - // Execute setBridgeLimits directly (signer must be the OFT adapter owner) - try { - console.log("\nExecuting setBridgeLimits..."); - const tx = await oftAdapter.setBridgeLimits(newLimits); - await tx.wait(); - console.log("✅ Transaction confirmed:", tx.hash); - - // Verify the limits were set - console.log("\nVerifying limits were set..."); - const updatedLimits = await oftAdapter.bridgeLimits(); - - console.log("\nUpdated bridge limits:"); - console.log("Daily Limit:", ethers.utils.formatEther(updatedLimits.dailyLimit), "G$"); - console.log("Transaction Limit:", ethers.utils.formatEther(updatedLimits.txLimit), "G$"); - console.log("Account Daily Limit:", ethers.utils.formatEther(updatedLimits.accountDailyLimit), "G$"); - console.log("Min Amount:", ethers.utils.formatEther(updatedLimits.minAmount), "G$"); - console.log("Only Whitelisted:", updatedLimits.onlyWhitelisted); - - console.log("\n✅ Successfully set bridge limits!"); - - } catch (error: any) { - console.error("\n❌ Error setting limits:"); - if (error.message) { - console.error("Error message:", error.message); - } - if (error.reason) { - console.error("Reason:", error.reason); - } - throw error; - } -}; - -if (require.main === module) { - main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error); - process.exit(1); - }); -} diff --git a/packages/bridge-contracts/scripts/oft/set-oft-operator.ts b/packages/bridge-contracts/scripts/oft/set-oft-operator.ts deleted file mode 100644 index 8e8d8cc..0000000 --- a/packages/bridge-contracts/scripts/oft/set-oft-operator.ts +++ /dev/null @@ -1,132 +0,0 @@ -/*** - * Script to set OFT adapter as operator on GoodDollarOFTMinterBurner via DAO governance - * - * This script must be run after deploying the OFT contracts. - * It sets the GoodDollarOFTAdapter as an operator on GoodDollarOFTMinterBurner, - * which allows the adapter to mint and burn tokens for cross-chain transfers. - * - * This operation requires DAO governance since MinterBurner is DAO-controlled. - */ - -import { network, ethers } from 'hardhat'; -import Contracts from '@gooddollar/goodprotocol/releases/deployment.json'; -import { getOftDeploymentAddresses } from '../../deploy/utils/getOftDeploymentAddresses'; - -const { name: networkName } = network; - -export const setOFTOperator = async () => { - const [root] = await ethers.getSigners(); - - console.log('Setting OFT operator:', { - networkName, - root: root.address, - balance: await ethers.provider.getBalance(root.address).then((_) => _.toString()), - }); - - // Get contract addresses from GoodProtocol deployment - const goodProtocolContracts = Contracts[networkName as keyof typeof Contracts] as any; - if (!goodProtocolContracts) { - throw new Error( - `No GoodProtocol contracts found for network ${networkName}. Please check @gooddollar/goodprotocol/releases/deployment.json`, - ); - } - - // Get Controller address directly from GoodProtocol contracts (or via NameService if needed) - let controllerAddress = goodProtocolContracts.Controller; - if (!controllerAddress) { - // Fallback: try to get Controller via NameService interface - const nameServiceAddress = goodProtocolContracts.NameService; - if (!nameServiceAddress) { - throw new Error( - `NameService address not found in GoodProtocol deployment for network ${networkName}. Please deploy NameService first.`, - ); - } - const INameService = await ethers.getContractAt( - '@gooddollar/goodprotocol/contracts/Interfaces.sol:INameService', - nameServiceAddress, - ); - controllerAddress = await INameService.getAddress('CONTROLLER'); - if (!controllerAddress || controllerAddress === ethers.constants.AddressZero) { - throw new Error(`Controller address not found in GoodProtocol deployment for network ${networkName}`); - } - } - - // Get deployed contract addresses (from hardhat-deploy `deployments/` artifacts) - const { GoodDollarOFTMinterBurner: minterBurnerAddress, GoodDollarOFTAdapter: oftAdapterAddress } = - getOftDeploymentAddresses(networkName); - - console.log('Contract addresses:', { - MinterBurner: minterBurnerAddress, - OFTAdapter: oftAdapterAddress, - }); - - // Get Controller and Avatar addresses - const Controller = await ethers.getContractAt('Controller', controllerAddress); - const avatarAddress = await Controller.avatar(); - - if (!avatarAddress || avatarAddress === ethers.constants.AddressZero) { - throw new Error(`Avatar address is invalid: ${avatarAddress}`); - } - - // Get MinterBurner contract - const MinterBurner = await ethers.getContractAt('GoodDollarOFTMinterBurner', minterBurnerAddress); - - // Check if OFT adapter is already an operator - const isOperator = await MinterBurner.operators(oftAdapterAddress); - - if (!isOperator) { - console.log('Setting OFT adapter as operator on MinterBurner via DAO...'); - console.log(` MinterBurner address: ${minterBurnerAddress}`); - console.log(` OFTAdapter address: ${oftAdapterAddress}`); - - // Encode the setOperator function call - const setOperatorEncoded = MinterBurner.interface.encodeFunctionData('setOperator', [oftAdapterAddress, true]); - - // Execute via Controller/Avatar - try { - const tx = await Controller.genericCall(minterBurnerAddress, setOperatorEncoded, avatarAddress, 0); - await tx.wait(); - console.log('✅ Successfully set OFT adapter as operator on MinterBurner'); - console.log('Transaction hash:', tx.hash); - - // Verify it was set - const isOperatorAfter = await MinterBurner.operators(oftAdapterAddress); - if (isOperatorAfter) { - console.log('✅ Verified: OFT adapter is now an operator'); - } else { - console.log('⚠️ Warning: Operator status not set. Please check the transaction.'); - } - } catch (error: any) { - console.error('❌ Error setting operator:'); - if (error.message) { - console.error('Error message:', error.message); - } - if (error.reason) { - console.error('Reason:', error.reason); - } - throw error; - } - } else { - console.log('✅ OFT adapter is already an operator on MinterBurner'); - } - - console.log('\n=== Operator Setup Summary ==='); - console.log('Network:', networkName); - console.log('GoodDollarOFTMinterBurner:', minterBurnerAddress); - console.log('GoodDollarOFTAdapter:', oftAdapterAddress); - console.log('Operator Status:', isOperator ? 'Already set' : 'Set successfully'); - console.log('==============================\n'); -}; - -export const main = async () => { - await setOFTOperator(); -}; - -if (require.main === module) { - main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error); - process.exit(1); - }); -} diff --git a/packages/bridge-contracts/scripts/oft/steps/adapter-ownership.ts b/packages/bridge-contracts/scripts/oft/steps/adapter-ownership.ts new file mode 100644 index 0000000..86be3fb --- /dev/null +++ b/packages/bridge-contracts/scripts/oft/steps/adapter-ownership.ts @@ -0,0 +1,44 @@ +import { network, ethers } from "hardhat"; +import Contracts from "@gooddollar/goodprotocol/releases/deployment.json"; +import { getOftDeploymentAddresses } from "../../../deploy/utils/getOftDeploymentAddresses"; + +export const main = async () => { + const networkName = network.name; + const [signer] = await ethers.getSigners(); + const goodProtocolContracts = Contracts[networkName as keyof typeof Contracts] as any; + if (!goodProtocolContracts?.Avatar) { + throw new Error(`Avatar not found for ${networkName}`); + } + + const avatarAddress = goodProtocolContracts.Avatar as string; + const { GoodDollarOFTAdapter: oftAdapterAddress } = getOftDeploymentAddresses(networkName); + const oftAdapter = await ethers.getContractAt("GoodDollarOFTAdapter", oftAdapterAddress); + const currentOwner = await oftAdapter.owner(); + + if (currentOwner.toLowerCase() === avatarAddress.toLowerCase()) { + console.log("adapter ownership: already Avatar"); + return; + } + + if (currentOwner.toLowerCase() !== signer.address.toLowerCase()) { + throw new Error(`Signer ${signer.address} is not owner ${currentOwner}`); + } + + const tx = await oftAdapter.transferOwnership(avatarAddress); + await tx.wait(); + + const newOwner = await oftAdapter.owner(); + if (newOwner.toLowerCase() !== avatarAddress.toLowerCase()) { + throw new Error(`Ownership transfer failed (tx ${tx.hash})`); + } + console.log(`adapter ownership: transferred to Avatar (${tx.hash})`); +}; + +if (require.main === module) { + main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); +} diff --git a/packages/bridge-contracts/scripts/oft/steps/grant-minter-role.ts b/packages/bridge-contracts/scripts/oft/steps/grant-minter-role.ts new file mode 100644 index 0000000..ec4251b --- /dev/null +++ b/packages/bridge-contracts/scripts/oft/steps/grant-minter-role.ts @@ -0,0 +1,51 @@ +import { network, ethers } from "hardhat"; +import Contracts from "@gooddollar/goodprotocol/releases/deployment.json"; +import { getOftDeploymentAddresses } from "../../../deploy/utils/getOftDeploymentAddresses"; + +export const main = async () => { + const networkName = network.name; + const goodProtocolContracts = Contracts[networkName as keyof typeof Contracts] as any; + if (!goodProtocolContracts) { + throw new Error(`No GoodProtocol contracts for ${networkName}`); + } + + const tokenAddress = goodProtocolContracts.GoodDollar || goodProtocolContracts.SuperGoodDollar; + const controllerAddress = goodProtocolContracts.Controller; + const avatarAddress = goodProtocolContracts.Avatar; + if (!tokenAddress || !controllerAddress || !avatarAddress) { + throw new Error(`Missing GoodDollar/Controller/Avatar for ${networkName}`); + } + + const { GoodDollarOFTMinterBurner: minterBurnerAddress } = getOftDeploymentAddresses(networkName); + const token = await ethers.getContractAt( + "contracts/oft/interfaces/ISuperGoodDollar.sol:ISuperGoodDollar", + tokenAddress + ); + + if (await token.isMinter(minterBurnerAddress)) { + console.log("minter role: already granted"); + return; + } + + const Controller = await ethers.getContractAt("Controller", controllerAddress); + const encodedCall = ethers.utils.hexConcat([ + ethers.utils.id("addMinter(address)").slice(0, 10), + ethers.utils.defaultAbiCoder.encode(["address"], [minterBurnerAddress]), + ]); + const tx = await Controller.genericCall(tokenAddress, encodedCall, avatarAddress, 0); + await tx.wait(); + + if (!(await token.isMinter(minterBurnerAddress))) { + throw new Error(`MINTER_ROLE not granted (tx ${tx.hash})`); + } + console.log(`minter role: granted (${tx.hash})`); +}; + +if (require.main === module) { + main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); +} diff --git a/packages/bridge-contracts/scripts/oft/steps/set-bridge-limits.ts b/packages/bridge-contracts/scripts/oft/steps/set-bridge-limits.ts new file mode 100644 index 0000000..9af6837 --- /dev/null +++ b/packages/bridge-contracts/scripts/oft/steps/set-bridge-limits.ts @@ -0,0 +1,60 @@ +import { network, ethers } from "hardhat"; +import { BigNumber } from "ethers"; +import { getOftDeploymentAddresses } from "../../../deploy/utils/getOftDeploymentAddresses"; +import config from "../oft.config.json"; + +function parseLimit(value: string | undefined): BigNumber | null { + if (!value) return null; + const numValue = value.trim(); + if (numValue.includes(".") || numValue.length < 15) { + return ethers.utils.parseEther(numValue); + } + return ethers.BigNumber.from(numValue); +} + +export const main = async () => { + const networkName = network.name; + const networkConfig = (config as any)[networkName]; + if (!networkConfig?.limits) { + throw new Error(`No limits in oft.config.json for ${networkName}`); + } + + const { GoodDollarOFTAdapter: oftAdapterAddress } = getOftDeploymentAddresses(networkName); + const oftAdapter = await ethers.getContractAt("GoodDollarOFTAdapter", oftAdapterAddress); + const current = await oftAdapter.bridgeLimits(); + const limits = networkConfig.limits; + + const next = { + dailyLimit: parseLimit(limits.dailyLimit) ?? current.dailyLimit, + txLimit: parseLimit(limits.txLimit) ?? current.txLimit, + accountDailyLimit: parseLimit(limits.accountDailyLimit) ?? current.accountDailyLimit, + minAmount: parseLimit(limits.minAmount) ?? current.minAmount, + onlyWhitelisted: + limits.onlyWhitelisted !== undefined ? limits.onlyWhitelisted : current.onlyWhitelisted, + }; + + const unchanged = + next.dailyLimit.eq(current.dailyLimit) && + next.txLimit.eq(current.txLimit) && + next.accountDailyLimit.eq(current.accountDailyLimit) && + next.minAmount.eq(current.minAmount) && + next.onlyWhitelisted === current.onlyWhitelisted; + + if (unchanged) { + console.log("bridge limits: already set"); + return; + } + + const tx = await oftAdapter.setBridgeLimits(next); + await tx.wait(); + console.log(`bridge limits: set (${tx.hash})`); +}; + +if (require.main === module) { + main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); +} From e3b0649e19fa56f57983fd9435569401f805c08f Mon Sep 17 00:00:00 2001 From: blueogin Date: Fri, 10 Jul 2026 14:35:11 -0400 Subject: [PATCH 4/7] Enhance OFT deployment scripts by introducing a utility for LayerZero endpoint retrieval and updating configuration references in documentation. --- packages/bridge-contracts/deploy/deployOFT.ts | 17 ++------------ .../bridge-contracts/deploy/upgradeOFT.ts | 15 ++----------- .../deploy/utils/getLzEndpoint.ts | 22 +++++++++++++++++++ packages/bridge-contracts/package.json | 1 + .../scripts/oft/OFT_CONFIGURING_GUIDE.md | 4 ++-- 5 files changed, 29 insertions(+), 30 deletions(-) create mode 100644 packages/bridge-contracts/deploy/utils/getLzEndpoint.ts diff --git a/packages/bridge-contracts/deploy/deployOFT.ts b/packages/bridge-contracts/deploy/deployOFT.ts index b58c616..a7c3889 100644 --- a/packages/bridge-contracts/deploy/deployOFT.ts +++ b/packages/bridge-contracts/deploy/deployOFT.ts @@ -18,14 +18,7 @@ import { ethers } from 'hardhat'; import Contracts from '@gooddollar/goodprotocol/releases/deployment.json'; import { getImplementationAddress } from '@openzeppelin/upgrades-core'; import { verifyContract } from './utils/verifyContract'; - -// Network-specific LayerZero endpoints -const lzEndpoints: { [key: string]: string } = { - 'development-celo': '0x1a44076050125825900e736c501f859c50fE728c', - 'production-celo': '0x1a44076050125825900e736c501f859c50fE728c', - 'development-xdc': '0xcb566e3B6934Fa77258d68ea18E931fa75e1aaAa', - 'production-xdc': '0xcb566e3B6934Fa77258d68ea18E931fa75e1aaAa', -}; +import { getLzEndpoint } from './utils/getLzEndpoint'; const func: DeployFunction = async function (hre) { const { deployments, network } = hre; @@ -77,13 +70,7 @@ const func: DeployFunction = async function (hre) { } } - // Get LayerZero endpoint - const lzEndpoint = lzEndpoints[networkName] || process.env.LAYERZERO_ENDPOINT; - if (!lzEndpoint) { - throw new Error( - `LayerZero endpoint not found. Please set LAYERZERO_ENDPOINT environment variable or add default for network ${networkName}`, - ); - } + const lzEndpoint = getLzEndpoint(networkName); console.log('Deployment parameters:', { tokenAddress, diff --git a/packages/bridge-contracts/deploy/upgradeOFT.ts b/packages/bridge-contracts/deploy/upgradeOFT.ts index 6b98fdf..a547976 100644 --- a/packages/bridge-contracts/deploy/upgradeOFT.ts +++ b/packages/bridge-contracts/deploy/upgradeOFT.ts @@ -19,13 +19,7 @@ import { ethers } from "hardhat"; import Contracts from "@gooddollar/goodprotocol/releases/deployment.json"; import { getImplementationAddress } from "@openzeppelin/upgrades-core"; import { getOftDeploymentAddresses } from "./utils/getOftDeploymentAddresses"; - -const lzEndpoints: { [key: string]: string } = { - "development-celo": "0x1a44076050125825900e736c501f859c50fE728c", - "production-celo": "0x1a44076050125825900e736c501f859c50fE728c", - "development-xdc": "0xcb566e3B6934Fa77258d68ea18E931fa75e1aaAa", - "production-xdc": "0xcb566e3B6934Fa77258d68ea18E931fa75e1aaAa", -}; +import { getLzEndpoint } from "./utils/getLzEndpoint"; const func: DeployFunction = async function (hre) { const { deployments, network } = hre; @@ -61,12 +55,7 @@ const func: DeployFunction = async function (hre) { ); } - const lzEndpoint = lzEndpoints[networkName] || process.env.LAYERZERO_ENDPOINT; - if (!lzEndpoint) { - throw new Error( - `LayerZero endpoint not found. Please set LAYERZERO_ENDPOINT or add default for network ${networkName}` - ); - } + const lzEndpoint = getLzEndpoint(networkName); console.log("\nDeployment parameters (same as deployOFT):"); console.log("Token:", tokenAddress); diff --git a/packages/bridge-contracts/deploy/utils/getLzEndpoint.ts b/packages/bridge-contracts/deploy/utils/getLzEndpoint.ts new file mode 100644 index 0000000..b3f6c61 --- /dev/null +++ b/packages/bridge-contracts/deploy/utils/getLzEndpoint.ts @@ -0,0 +1,22 @@ +const celoMainnetEndpoint = require("@layerzerolabs/lz-evm-sdk-v2/deployments/celo-mainnet/EndpointV2.json"); +const xdcMainnetEndpoint = require("@layerzerolabs/lz-evm-sdk-v2/deployments/xdc-mainnet/EndpointV2.json"); + +const LZ_ENDPOINT_BY_NETWORK: Record = { + "development-celo": celoMainnetEndpoint.address, + "production-celo": celoMainnetEndpoint.address, + "development-xdc": xdcMainnetEndpoint.address, + "production-xdc": xdcMainnetEndpoint.address, +}; + +export function getLzEndpoint(networkName: string): string { + const fromEnv = process.env.LAYERZERO_ENDPOINT; + if (fromEnv) return fromEnv; + + const endpoint = LZ_ENDPOINT_BY_NETWORK[networkName]; + if (!endpoint) { + throw new Error( + `LayerZero EndpointV2 not mapped for "${networkName}". Set LAYERZERO_ENDPOINT or add a mapping in deploy/utils/getLzEndpoint.ts` + ); + } + return endpoint; +} diff --git a/packages/bridge-contracts/package.json b/packages/bridge-contracts/package.json index 739081b..4ba7b64 100644 --- a/packages/bridge-contracts/package.json +++ b/packages/bridge-contracts/package.json @@ -31,6 +31,7 @@ "@layerzerolabs/devtools-evm-hardhat": "^4.0.4", "@layerzerolabs/lz-definitions": "^3.0.151", "@layerzerolabs/lz-evm-protocol-v2": "^3.0.151", + "@layerzerolabs/lz-evm-sdk-v2": "^3.0.148", "@layerzerolabs/lz-v2-utilities": "^3.0.156", "@layerzerolabs/metadata-tools": "^3.0.3", "@layerzerolabs/oapp-evm": "^0.4.1", diff --git a/packages/bridge-contracts/scripts/oft/OFT_CONFIGURING_GUIDE.md b/packages/bridge-contracts/scripts/oft/OFT_CONFIGURING_GUIDE.md index a1fd59d..44e6eab 100644 --- a/packages/bridge-contracts/scripts/oft/OFT_CONFIGURING_GUIDE.md +++ b/packages/bridge-contracts/scripts/oft/OFT_CONFIGURING_GUIDE.md @@ -19,7 +19,7 @@ Use `development-xdc` / `development-celo` for staging. Deploy **both** chains b | Parameter | Source | Notes | |-----------|--------|--------| | Token / NameService / Controller | `@gooddollar/goodprotocol` `deployment.json` for the Hardhat network | Required | -| LayerZero endpoint | Built-in map, or `LAYERZERO_ENDPOINT` | Celo `0x1a44…728c`, XDC `0xcb56…aaAa` | +| LayerZero endpoint | `@layerzerolabs/lz-evm-sdk-v2` `EndpointV2.json` via `deploy/utils/getLzEndpoint.ts` (override: `LAYERZERO_ENDPOINT`) | Celo/XDC mainnet deployments | | Proxy CREATE2 salt | `Development-…V1` or `Production-…V1` string hash | Fixed proxy address per env | | Implementation CREATE2 salt | `keccak256(bytecode)` | Changes when code changes | @@ -96,7 +96,7 @@ Today wiring is **XDC ↔ CELO** in `scripts/oft/layerzero.config.ts`. To add an 1. **Deploy** OFT on the new Hardhat network (`--tags OFT`) so `deployments//GoodDollarOFTAdapter.json` exists 2. **Hardhat** — network entry in `hardhat.config.ts` (RPC + accounts) -3. **Endpoint** — add the chain’s LZ V2 endpoint in `deploy/deployOFT.ts` (or set `LAYERZERO_ENDPOINT`) +3. **Endpoint** — map the Hardhat network → `@layerzerolabs/lz-evm-sdk-v2` `deployments//EndpointV2.json` in `deploy/utils/getLzEndpoint.ts` (or set `LAYERZERO_ENDPOINT`) 4. **`layerzero.config.ts`** — new `OmniPointHardhat` with `EndpointId._V2_…` + adapter address from `getOftDeploymentAddresses` 5. **Pathway** — append a `pathways` entry: `[dest, src, DVNs, confirmations, enforcedOptions]` (same shape as the existing XDC↔CELO pair) 6. **Configure helpers** — limits in `oft.config.json`; if `oft:configure` pair env only knows xdc/celo, extend `resolveOftPairNetworks` (or set `OFT_*_NETWORK` yourself) From be38f72e8e5256c2d3090d18b52e00f030453f7b Mon Sep 17 00:00:00 2001 From: blueogin Date: Fri, 10 Jul 2026 14:40:35 -0400 Subject: [PATCH 5/7] Update yarn.lock to include @layerzerolabs/lz-evm-sdk-v2 version 3.0.148 --- yarn.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/yarn.lock b/yarn.lock index 10bd1dd..7fed285 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2099,6 +2099,7 @@ __metadata: "@layerzerolabs/devtools-evm-hardhat": "npm:^4.0.4" "@layerzerolabs/lz-definitions": "npm:^3.0.151" "@layerzerolabs/lz-evm-protocol-v2": "npm:^3.0.151" + "@layerzerolabs/lz-evm-sdk-v2": "npm:^3.0.148" "@layerzerolabs/lz-v2-utilities": "npm:^3.0.156" "@layerzerolabs/metadata-tools": "npm:^3.0.3" "@layerzerolabs/oapp-evm": "npm:^0.4.1" From 760083f2580b3c71ba2d6210e8e83d03399562a3 Mon Sep 17 00:00:00 2001 From: blueogin Date: Fri, 10 Jul 2026 15:00:14 -0400 Subject: [PATCH 6/7] Refactor OFT configuration process by introducing a Hardhat task for streamlined setup, enhancing LayerZero endpoint retrieval, and updating related documentation. Remove deprecated run-configure.js script. --- .../deploy/utils/getLzEndpoint.ts | 5 +- packages/bridge-contracts/hardhat.config.ts | 2 + packages/bridge-contracts/package.json | 2 +- .../scripts/oft/OFT_CONFIGURING_GUIDE.md | 10 ++- .../scripts/oft/configure-oft.ts | 89 +++++++++++-------- .../scripts/oft/run-configure.js | 34 ------- 6 files changed, 60 insertions(+), 82 deletions(-) delete mode 100644 packages/bridge-contracts/scripts/oft/run-configure.js diff --git a/packages/bridge-contracts/deploy/utils/getLzEndpoint.ts b/packages/bridge-contracts/deploy/utils/getLzEndpoint.ts index b3f6c61..fb718bb 100644 --- a/packages/bridge-contracts/deploy/utils/getLzEndpoint.ts +++ b/packages/bridge-contracts/deploy/utils/getLzEndpoint.ts @@ -9,10 +9,7 @@ const LZ_ENDPOINT_BY_NETWORK: Record = { }; export function getLzEndpoint(networkName: string): string { - const fromEnv = process.env.LAYERZERO_ENDPOINT; - if (fromEnv) return fromEnv; - - const endpoint = LZ_ENDPOINT_BY_NETWORK[networkName]; + const endpoint = LZ_ENDPOINT_BY_NETWORK[networkName] || process.env.LAYERZERO_ENDPOINT; if (!endpoint) { throw new Error( `LayerZero EndpointV2 not mapped for "${networkName}". Set LAYERZERO_ENDPOINT or add a mapping in deploy/utils/getLzEndpoint.ts` diff --git a/packages/bridge-contracts/hardhat.config.ts b/packages/bridge-contracts/hardhat.config.ts index 21040d8..4874435 100644 --- a/packages/bridge-contracts/hardhat.config.ts +++ b/packages/bridge-contracts/hardhat.config.ts @@ -15,6 +15,8 @@ import * as envEnc from '@chainlink/env-enc'; import '@layerzerolabs/toolbox-hardhat' import { EndpointId } from '@layerzerolabs/lz-definitions' +import './scripts/oft/configure-oft'; + configDotenv(); envEnc.config(); diff --git a/packages/bridge-contracts/package.json b/packages/bridge-contracts/package.json index 4ba7b64..65bc4e5 100644 --- a/packages/bridge-contracts/package.json +++ b/packages/bridge-contracts/package.json @@ -8,7 +8,7 @@ "build": "hardhat compile", "test": "hardhat test", "test:oft": "hardhat test ./test/GoodDollarOFT.test.ts", - "oft:configure": "node scripts/oft/run-configure.js", + "oft:configure": "hardhat oft:configure", "updateabi": "find ./artifacts/contracts/ -iregex '.*/[a-z]*.json' -exec cp \"{}\" ../bridge-app/src/abi \\;", "start:dev": "hardhat node & yarn build && yarn updateabi && hardhat run scripts/deployDevEnv.ts --network localhost", "mpb:deploy": "hardhat deploy --export-all ./release/mpb.json" diff --git a/packages/bridge-contracts/scripts/oft/OFT_CONFIGURING_GUIDE.md b/packages/bridge-contracts/scripts/oft/OFT_CONFIGURING_GUIDE.md index 44e6eab..f523fa2 100644 --- a/packages/bridge-contracts/scripts/oft/OFT_CONFIGURING_GUIDE.md +++ b/packages/bridge-contracts/scripts/oft/OFT_CONFIGURING_GUIDE.md @@ -12,19 +12,21 @@ npx hardhat deploy --tags OFT --network production-xdc npx hardhat deploy --tags OFT --network production-celo ``` -Use `development-xdc` / `development-celo` for staging. Deploy **both** chains before configure (wiring needs both adapters). +Use `development-xdc` / `development-celo` for staging. Deploy **both** chains before configuring (wiring needs both adapters). ### Deploy parameters (`deploy/deployOFT.ts`) | Parameter | Source | Notes | |-----------|--------|--------| | Token / NameService / Controller | `@gooddollar/goodprotocol` `deployment.json` for the Hardhat network | Required | -| LayerZero endpoint | `@layerzerolabs/lz-evm-sdk-v2` `EndpointV2.json` via `deploy/utils/getLzEndpoint.ts` (override: `LAYERZERO_ENDPOINT`) | Celo/XDC mainnet deployments | +| LayerZero endpoint | `@layerzerolabs/lz-evm-sdk-v2` `EndpointV2.json` via `deploy/utils/getLzEndpoint.ts` (fallback: `LAYERZERO_ENDPOINT` if network unmapped) | Celo/XDC mainnet deployments | | Proxy CREATE2 salt | `Development-…V1` or `Production-…V1` string hash | Fixed proxy address per env | | Implementation CREATE2 salt | `keccak256(bytecode)` | Changes when code changes | ## Configure +Hardhat task (`package.json` → `hardhat oft:configure`): + ```bash yarn oft:configure --network production-xdc yarn oft:configure --network production-celo @@ -65,7 +67,7 @@ Values are **G$ amounts** (e.g. `"100"` → 100 G$), not wei: ### LayerZero pair env -`oft:configure` sets these from the network name (`production` vs `development`): +`oft:configure` sets these from the explicit network pair map in `configure-oft.ts`: - `OFT_XDC_NETWORK` - `OFT_CELO_NETWORK` @@ -99,7 +101,7 @@ Today wiring is **XDC ↔ CELO** in `scripts/oft/layerzero.config.ts`. To add an 3. **Endpoint** — map the Hardhat network → `@layerzerolabs/lz-evm-sdk-v2` `deployments//EndpointV2.json` in `deploy/utils/getLzEndpoint.ts` (or set `LAYERZERO_ENDPOINT`) 4. **`layerzero.config.ts`** — new `OmniPointHardhat` with `EndpointId._V2_…` + adapter address from `getOftDeploymentAddresses` 5. **Pathway** — append a `pathways` entry: `[dest, src, DVNs, confirmations, enforcedOptions]` (same shape as the existing XDC↔CELO pair) -6. **Configure helpers** — limits in `oft.config.json`; if `oft:configure` pair env only knows xdc/celo, extend `resolveOftPairNetworks` (or set `OFT_*_NETWORK` yourself) +6. **Configure helpers** — limits in `oft.config.json`; extend `OFT_NETWORK_PAIRS` in `configure-oft.ts` (and `OFT_*_NETWORK` if wiring manually) 7. **Wire** — `yarn oft:configure --network ` (and on each peer you care about) `EndpointId` values come from `@layerzerolabs/lz-definitions`. DVN names must match LayerZero metadata for that chain. diff --git a/packages/bridge-contracts/scripts/oft/configure-oft.ts b/packages/bridge-contracts/scripts/oft/configure-oft.ts index 3b2bc7e..19ffc5b 100644 --- a/packages/bridge-contracts/scripts/oft/configure-oft.ts +++ b/packages/bridge-contracts/scripts/oft/configure-oft.ts @@ -1,51 +1,49 @@ import { spawnSync } from "child_process"; import path from "path"; -import { network } from "hardhat"; -import { main as grantMinterRole } from "./steps/grant-minter-role"; -import { main as setBridgeLimits } from "./steps/set-bridge-limits"; -import { main as transferAdapterOwnership } from "./steps/adapter-ownership"; +import { task } from "hardhat/config"; +import type { HardhatRuntimeEnvironment } from "hardhat/types"; -type ConfigureFlags = { +export type ConfigureFlags = { skipMinter: boolean; skipLimits: boolean; skipWire: boolean; skipAdapterOwnership: boolean; }; -function flagEnabled(name: string): boolean { - const value = process.env[name]; - return value === "1" || value === "true"; -} - -function parseFlags(): ConfigureFlags { - return { - skipMinter: flagEnabled("SKIP_MINTER"), - skipLimits: flagEnabled("SKIP_LIMITS"), - skipWire: flagEnabled("SKIP_WIRE"), - skipAdapterOwnership: flagEnabled("SKIP_ADAPTER_OWNERSHIP"), - }; -} +const OFT_NETWORK_PAIRS: Record = { + "production-xdc": { xdc: "production-xdc", celo: "production-celo" }, + "production-celo": { xdc: "production-xdc", celo: "production-celo" }, + "development-xdc": { xdc: "development-xdc", celo: "development-celo" }, + "development-celo": { xdc: "development-xdc", celo: "development-celo" }, +}; function resolveOftPairNetworks(networkName: string): { xdc: string; celo: string } { - const isProduction = networkName.includes("production"); - return { - xdc: isProduction ? "production-xdc" : "development-xdc", - celo: isProduction ? "production-celo" : "development-celo", - }; + const pair = OFT_NETWORK_PAIRS[networkName]; + if (!pair) { + throw new Error(`Unknown network pairing for "${networkName}"`); + } + return pair; } -function wireLayerZero(networkName: string) { - const pair = resolveOftPairNetworks(networkName); +function runLayerZeroWire(networkName: string, xdcNetwork: string, celoNetwork: string) { const result = spawnSync( "npx", - ["hardhat", "lz:oapp:wire", "--oapp-config", "./scripts/oft/layerzero.config.ts", "--network", networkName, "--ci"], + [ + "hardhat", + "lz:oapp:wire", + "--oapp-config", + "./scripts/oft/layerzero.config.ts", + "--network", + networkName, + "--ci", + ], { cwd: path.resolve(__dirname, "../.."), stdio: "inherit", env: { ...process.env, - OFT_XDC_NETWORK: pair.xdc, - OFT_CELO_NETWORK: pair.celo, + OFT_XDC_NETWORK: xdcNetwork, + OFT_CELO_NETWORK: celoNetwork, }, } ); @@ -55,9 +53,17 @@ function wireLayerZero(networkName: string) { } } -export const main = async () => { - const flags = parseFlags(); - const networkName = network.name; +function wireLayerZero(networkName: string) { + const pair = resolveOftPairNetworks(networkName); + runLayerZeroWire(networkName, pair.xdc, pair.celo); +} + +export async function configureOft(hre: HardhatRuntimeEnvironment, flags: ConfigureFlags) { + const { main: grantMinterRole } = await import("./steps/grant-minter-role"); + const { main: setBridgeLimits } = await import("./steps/set-bridge-limits"); + const { main: transferAdapterOwnership } = await import("./steps/adapter-ownership"); + + const networkName = hre.network.name; console.log(`oft:configure ${networkName}`, flags); if (!flags.skipMinter) await grantMinterRole(); @@ -66,13 +72,18 @@ export const main = async () => { if (!flags.skipAdapterOwnership) await transferAdapterOwnership(); console.log(`oft:configure done (${networkName})`); -}; +} -if (require.main === module) { - main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error); - process.exit(1); +task("oft:configure", "Configure GoodDollar OFT after deploy (minter, limits, wire, ownership)") + .addFlag("skipMinter", "Skip granting MINTER_ROLE") + .addFlag("skipLimits", "Skip setBridgeLimits") + .addFlag("skipWire", "Skip LayerZero lz:oapp:wire") + .addFlag("skipAdapterOwnership", "Skip ownership transfer to Avatar") + .setAction(async (taskArgs, hre) => { + await configureOft(hre, { + skipMinter: taskArgs.skipMinter, + skipLimits: taskArgs.skipLimits, + skipWire: taskArgs.skipWire, + skipAdapterOwnership: taskArgs.skipAdapterOwnership, }); -} + }); diff --git a/packages/bridge-contracts/scripts/oft/run-configure.js b/packages/bridge-contracts/scripts/oft/run-configure.js deleted file mode 100644 index 72ca3d2..0000000 --- a/packages/bridge-contracts/scripts/oft/run-configure.js +++ /dev/null @@ -1,34 +0,0 @@ -const { spawnSync } = require("child_process"); -const path = require("path"); - -const SKIP_FLAGS = new Set([ - "--skip-minter", - "--skip-limits", - "--skip-wire", - "--skip-adapter-ownership", -]); - -const args = process.argv.slice(2); -const hardhatArgs = []; -const env = { ...process.env }; - -for (const arg of args) { - if (SKIP_FLAGS.has(arg)) { - const key = arg.slice(2).toUpperCase().replace(/-/g, "_"); - env[key] = "1"; - } else { - hardhatArgs.push(arg); - } -} - -const result = spawnSync( - "npx", - ["hardhat", "run", "scripts/oft/configure-oft.ts", ...hardhatArgs], - { - cwd: path.resolve(__dirname, "../.."), - stdio: "inherit", - env, - } -); - -process.exit(result.status ?? 1); From 5d38e692a80a901c562afad9fa27d542c66c89d5 Mon Sep 17 00:00:00 2001 From: blueogin Date: Fri, 10 Jul 2026 15:09:12 -0400 Subject: [PATCH 7/7] Update OFT configuration script to ensure LayerZero wiring occurs before ownership transfer, and revise documentation for clarity on configuration steps and skip flags. --- .../scripts/oft/OFT_CONFIGURING_GUIDE.md | 36 ++++++++++--------- .../scripts/oft/configure-oft.ts | 4 +-- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/packages/bridge-contracts/scripts/oft/OFT_CONFIGURING_GUIDE.md b/packages/bridge-contracts/scripts/oft/OFT_CONFIGURING_GUIDE.md index f523fa2..79daab7 100644 --- a/packages/bridge-contracts/scripts/oft/OFT_CONFIGURING_GUIDE.md +++ b/packages/bridge-contracts/scripts/oft/OFT_CONFIGURING_GUIDE.md @@ -12,7 +12,7 @@ npx hardhat deploy --tags OFT --network production-xdc npx hardhat deploy --tags OFT --network production-celo ``` -Use `development-xdc` / `development-celo` for staging. Deploy **both** chains before configuring (wiring needs both adapters). +Use `development-xdc` / `development-celo` for staging. Deploy **both** chains before configuring (LayerZero wire needs both adapters, and must run while the deployer still owns the OApp). ### Deploy parameters (`deploy/deployOFT.ts`) @@ -34,10 +34,10 @@ yarn oft:configure --network production-celo ### Steps (in order) -1. **Minter role** — grant `MINTER_ROLE` on G$ to `GoodDollarOFTMinterBurner` via Controller/Avatar -2. **Bridge limits** — `setBridgeLimits` from `scripts/oft/oft.config.json` -3. **LayerZero wire** — `lz:oapp:wire --ci` using `scripts/oft/layerzero.config.ts` (peers / DVNs / options) -4. **Adapter ownership** — transfer `GoodDollarOFTAdapter` owner to DAO Avatar +1. **LayerZero wire** — `lz:oapp:wire --ci` using `scripts/oft/layerzero.config.ts` (peers / DVNs / options). Must run **before** ownership transfer; after Avatar owns the adapter, wiring is not possible from the deployer key. +2. **Minter role** — grant `MINTER_ROLE` on G$ to `GoodDollarOFTMinterBurner` via Controller/Avatar +3. **Bridge limits** — `setBridgeLimits` from `scripts/oft/oft.config.json` (still as deployer owner) +4. **Adapter ownership** — transfer `GoodDollarOFTAdapter` owner to DAO Avatar (**last**) ### Skip flags @@ -48,9 +48,9 @@ yarn oft:configure --network production-celo --skip-minter --skip-limits --skip- | Flag | Skips | |------|--------| -| `--skip-minter` | Step 1 | -| `--skip-limits` | Step 2 | -| `--skip-wire` | Step 3 | +| `--skip-wire` | Step 1 | +| `--skip-minter` | Step 2 | +| `--skip-limits` | Step 3 | | `--skip-adapter-ownership` | Step 4 | ### Limit parameters (`oft.config.json`) @@ -94,14 +94,16 @@ npx hardhat run scripts/oft/bridge-oft-token.ts --network production-celo ## Add a network for LayerZero wiring -Today wiring is **XDC ↔ CELO** in `scripts/oft/layerzero.config.ts`. To add another chain: +Today the stack is built for **XDC ↔ CELO** (`layerzero.config.ts`, `OFT_NETWORK_PAIRS`). Adding a third chain needs more than a one-line append — those two-chain helpers must be extended or refactored. -1. **Deploy** OFT on the new Hardhat network (`--tags OFT`) so `deployments//GoodDollarOFTAdapter.json` exists -2. **Hardhat** — network entry in `hardhat.config.ts` (RPC + accounts) -3. **Endpoint** — map the Hardhat network → `@layerzerolabs/lz-evm-sdk-v2` `deployments//EndpointV2.json` in `deploy/utils/getLzEndpoint.ts` (or set `LAYERZERO_ENDPOINT`) -4. **`layerzero.config.ts`** — new `OmniPointHardhat` with `EndpointId._V2_…` + adapter address from `getOftDeploymentAddresses` -5. **Pathway** — append a `pathways` entry: `[dest, src, DVNs, confirmations, enforcedOptions]` (same shape as the existing XDC↔CELO pair) -6. **Configure helpers** — limits in `oft.config.json`; extend `OFT_NETWORK_PAIRS` in `configure-oft.ts` (and `OFT_*_NETWORK` if wiring manually) -7. **Wire** — `yarn oft:configure --network ` (and on each peer you care about) +Suggested order: -`EndpointId` values come from `@layerzerolabs/lz-definitions`. DVN names must match LayerZero metadata for that chain. +1. **Hardhat** — add the network in `hardhat.config.ts` (RPC, accounts, and `eid: EndpointId._V2_…` like the existing celo/xdc entries) +2. **Deploy endpoint** — map Hardhat name → `@layerzerolabs/lz-evm-sdk-v2` `deployments//EndpointV2.json` in `getLzEndpoint.ts`, or set `LAYERZERO_ENDPOINT` for that run (required by the OFT adapter **constructor**, not by `lz:oapp:wire`) +3. **GoodProtocol** — ensure `deployment.json` (or local overrides) has GoodDollar / NameService / Controller / Avatar for that network (configure steps need them) +4. **Deploy** — `npx hardhat deploy --tags OFT --network ` → `deployments//GoodDollarOFTAdapter.json` +5. **`layerzero.config.ts`** — add an `OmniPointHardhat` (eid + adapter from `getOftDeploymentAddresses`); add `pathways` for each pair you want (e.g. new↔CELO, new↔XDC). The current file is a single XDC↔CELO pathway driven by `OFT_XDC_NETWORK` / `OFT_CELO_NETWORK` — multi-chain needs that structure updated, not only one new pathway line +6. **Configure helpers** — `oft.config.json` limits; extend/refactor `OFT_NETWORK_PAIRS` in `configure-oft.ts` so `oft:configure` knows how to set peer env for wiring +7. **Wire then configure** — `yarn oft:configure --network ` (and on each peer). Wire runs **first** (deployer must still own the OApp); ownership transfer to Avatar is **last** + +`EndpointId` / DVN names: `@layerzerolabs/lz-definitions` and LayerZero metadata for that chain. diff --git a/packages/bridge-contracts/scripts/oft/configure-oft.ts b/packages/bridge-contracts/scripts/oft/configure-oft.ts index 19ffc5b..bfe8a79 100644 --- a/packages/bridge-contracts/scripts/oft/configure-oft.ts +++ b/packages/bridge-contracts/scripts/oft/configure-oft.ts @@ -66,15 +66,15 @@ export async function configureOft(hre: HardhatRuntimeEnvironment, flags: Config const networkName = hre.network.name; console.log(`oft:configure ${networkName}`, flags); + if (!flags.skipWire) wireLayerZero(networkName); if (!flags.skipMinter) await grantMinterRole(); if (!flags.skipLimits) await setBridgeLimits(); - if (!flags.skipWire) wireLayerZero(networkName); if (!flags.skipAdapterOwnership) await transferAdapterOwnership(); console.log(`oft:configure done (${networkName})`); } -task("oft:configure", "Configure GoodDollar OFT after deploy (minter, limits, wire, ownership)") +task("oft:configure", "Configure GoodDollar OFT after deploy (wire, minter, limits, ownership)") .addFlag("skipMinter", "Skip granting MINTER_ROLE") .addFlag("skipLimits", "Skip setBridgeLimits") .addFlag("skipWire", "Skip LayerZero lz:oapp:wire")