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..fb718bb --- /dev/null +++ b/packages/bridge-contracts/deploy/utils/getLzEndpoint.ts @@ -0,0 +1,19 @@ +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 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` + ); + } + return endpoint; +} 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/layerzero.config.ts b/packages/bridge-contracts/layerzero.config.ts deleted file mode 100644 index 8f86282..0000000 --- a/packages/bridge-contracts/layerzero.config.ts +++ /dev/null @@ -1,89 +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. - * - * Usage: - * npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts - * 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"; - -// Network names - adjust these based on your deployment -const XDC_NETWORK = "development-xdc"; -const 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/package.json b/packages/bridge-contracts/package.json index 4b0d479..65bc4e5 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": "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" @@ -30,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 new file mode 100644 index 0000000..79daab7 --- /dev/null +++ b/packages/bridge-contracts/scripts/oft/OFT_CONFIGURING_GUIDE.md @@ -0,0 +1,109 @@ +# OFT bridge deploy & configure + +From `packages/bridge-contracts`. Addresses are written to git-ignored hardhat-deploy artifacts: + +`deployments//GoodDollarOFTAdapter.json` +`deployments//GoodDollarOFTMinterBurner.json` + +## Deploy + +```bash +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 (LayerZero wire needs both adapters, and must run while the deployer still owns the OApp). + +### 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` (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 +``` + +### Steps (in order) + +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 + +```bash +yarn oft:configure --network production-xdc --skip-wire +yarn oft:configure --network production-celo --skip-minter --skip-limits --skip-adapter-ownership +``` + +| Flag | Skips | +|------|--------| +| `--skip-wire` | Step 1 | +| `--skip-minter` | Step 2 | +| `--skip-limits` | Step 3 | +| `--skip-adapter-ownership` | Step 4 | + +### Limit parameters (`oft.config.json`) + +Values are **G$ amounts** (e.g. `"100"` → 100 G$), not wei: + +| 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 | + +### LayerZero pair env + +`oft:configure` sets these from the explicit network pair map in `configure-oft.ts`: + +- `OFT_XDC_NETWORK` +- `OFT_CELO_NETWORK` + +## Upgrade + +Proxy addresses stay the same: + +```bash +npx hardhat deploy --tags OFT-Upgrade --network production-xdc +npx hardhat deploy --tags OFT-Upgrade --network production-celo +``` + +## 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 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. + +Suggested order: + +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/test/oft/bridge-oft-token.ts b/packages/bridge-contracts/scripts/oft/bridge-oft-token.ts similarity index 78% rename from packages/bridge-contracts/test/oft/bridge-oft-token.ts rename to packages/bridge-contracts/scripts/oft/bridge-oft-token.ts index 45ad1ee..2400fbf 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 @@ -157,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}`); } @@ -220,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."); @@ -230,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 { @@ -337,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(` yarn hardhat run test/oft/check-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: yarn hardhat run test/oft/set-layerzero-peers.ts --network ${networkName}`); - } - } - + throw error; } }; 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..bfe8a79 --- /dev/null +++ b/packages/bridge-contracts/scripts/oft/configure-oft.ts @@ -0,0 +1,89 @@ +import { spawnSync } from "child_process"; +import path from "path"; +import { task } from "hardhat/config"; +import type { HardhatRuntimeEnvironment } from "hardhat/types"; + +export type ConfigureFlags = { + skipMinter: boolean; + skipLimits: boolean; + skipWire: boolean; + skipAdapterOwnership: boolean; +}; + +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 pair = OFT_NETWORK_PAIRS[networkName]; + if (!pair) { + throw new Error(`Unknown network pairing for "${networkName}"`); + } + return pair; +} + +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", + ], + { + cwd: path.resolve(__dirname, "../.."), + stdio: "inherit", + env: { + ...process.env, + OFT_XDC_NETWORK: xdcNetwork, + OFT_CELO_NETWORK: celoNetwork, + }, + } + ); + if (result.error) throw result.error; + if (result.status !== 0) { + throw new Error(`LayerZero wire failed with exit code ${result.status ?? "unknown"}`); + } +} + +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.skipWire) wireLayerZero(networkName); + if (!flags.skipMinter) await grantMinterRole(); + if (!flags.skipLimits) await setBridgeLimits(); + if (!flags.skipAdapterOwnership) await transferAdapterOwnership(); + + console.log(`oft:configure done (${networkName})`); +} + +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") + .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/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/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/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); + }); +} 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/grant-minter-role.ts b/packages/bridge-contracts/test/oft/grant-minter-role.ts deleted file mode 100644 index 3dd3e01..0000000 --- a/packages/bridge-contracts/test/oft/grant-minter-role.ts +++ /dev/null @@ -1,137 +0,0 @@ -/*** - * Script to grant MINTER_ROLE to GoodDollarOFTMinterBurner contract on development-celo - * Uses genericCall through Avatar/Controller to execute the transaction - * - * Usage: - * npx hardhat run test/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"; - -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/test/oft/set-bridge-limits.ts b/packages/bridge-contracts/test/oft/set-bridge-limits.ts deleted file mode 100644 index 2e295ea..0000000 --- a/packages/bridge-contracts/test/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 now managed in GoodDollarOFTAdapter, not GoodDollarOFTMinterBurner - * - * Usage: - * npx hardhat run test/oft/set-bridge-limits.ts --network development-celo - * - * Configuration: - * All limit values are read from test/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"; - -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 test/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/test/oft/set-oft-operator.ts b/packages/bridge-contracts/test/oft/set-oft-operator.ts deleted file mode 100644 index 8e8d8cc..0000000 --- a/packages/bridge-contracts/test/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/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); - }); -} 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"