diff --git a/packages/bridge-contracts/.env.sample b/packages/bridge-contracts/.env.sample new file mode 100644 index 0000000..125f63c --- /dev/null +++ b/packages/bridge-contracts/.env.sample @@ -0,0 +1,4 @@ +PRIVATE_KEY= +PUBLIC_KEY= + +ETHERSCAN_KEY= diff --git a/packages/bridge-contracts/deploy/deployOFT.ts b/packages/bridge-contracts/deploy/deployOFT.ts new file mode 100644 index 0000000..2c9db27 --- /dev/null +++ b/packages/bridge-contracts/deploy/deployOFT.ts @@ -0,0 +1,256 @@ +/*** + * Hardhat-deploy script for GoodDollar OFT (Omnichain Fungible Token) contracts + * + * Deploys (same pattern as MessageBridge: deterministic proxy + implementation + execute initialize): + * 1. GoodDollarOFTMinterBurner - DAO-upgradeable contract that handles minting and burning of GoodDollar tokens for OFT + * 2. GoodDollarOFTAdapter - Upgradeable LayerZero OFT adapter that wraps GoodDollar token for cross-chain transfers + * + * Steps: + * 1. Deploy ERC1967Proxy (deterministic) for GoodDollarOFTMinterBurner, deploy implementation + * then execute initialize(nameService, adapter) once the adapter proxy address is known + * 2. Deploy ERC1967Proxy (deterministic) for GoodDollarOFTAdapter, deploy implementation (constructor: token, lzEndpoint), execute initialize(token, minterBurner, owner, feeRecipient) + * + * Note: GoodDollarOFTMinterBurner.initialize wires the adapter as operator. + */ + +import { DeployFunction } from 'hardhat-deploy/types'; +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', +}; + +const func: DeployFunction = async function (hre) { + const { deployments, network } = hre; + const [root] = await ethers.getSigners(); + + const networkName = network.name; + + console.log('Deployment signer:', { + 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 token address from GoodProtocol + const tokenAddress = goodProtocolContracts.GoodDollar; + if (!tokenAddress) { + throw new Error( + `Token address not found in GoodProtocol deployment for network ${networkName}. Please deploy SuperGoodDollar or GoodDollar first.`, + ); + } + + // Get NameService for DAO integration from GoodProtocol + const nameServiceAddress = goodProtocolContracts.NameService; + if (!nameServiceAddress) { + throw new Error( + `NameService address not found in GoodProtocol deployment for network ${networkName}. Please deploy NameService first.`, + ); + } + + // 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 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 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}`, + ); + } + + console.log('Deployment parameters:', { + tokenAddress, + nameServiceAddress, + controllerAddress, + lzEndpoint, + networkName, + }); + + // Get Controller and Avatar addresses (used for OFT adapter owner) + 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}`); + } + console.log('✅ Verified Avatar address:', avatarAddress); + + let isDevelopment = false; + if (network.name.includes('development')) { + isDevelopment = true; + } + + // CREATE2 salt for implementations: + // hardhat-deploy uses `deterministicDeployment` as the CREATE2 salt. + // We derive it from the contract's compiled bytecode so version changes + // map to different deterministic implementation addresses. + const minterBurnerArtifact = await hre.artifacts.readArtifact('GoodDollarOFTMinterBurner'); + const minterBurnerImplSalt = ethers.utils.keccak256(minterBurnerArtifact.bytecode); + const oftAdapterArtifact = await hre.artifacts.readArtifact('GoodDollarOFTAdapter'); + const oftAdapterImplSalt = ethers.utils.keccak256(oftAdapterArtifact.bytecode); + + // --- GoodDollarOFTMinterBurner (hardhat-deploy: deterministic proxy + implementation + execute initialize) --- + const minterBurnerProxySalt = ethers.utils.keccak256( + ethers.utils.toUtf8Bytes( + isDevelopment ? 'Development-GoodDollarOFTMinterBurnerV1' : 'Production-GoodDollarOFTMinterBurnerV1' + ), + ); + const minterBurnerProxyDeploy = await deployments.deterministic('GoodDollarOFTMinterBurner', { + contract: 'ERC1967Proxy', + from: root.address, + salt: minterBurnerProxySalt, + log: true, + }); + const minterBurnerProxy = await minterBurnerProxyDeploy.deploy(); + const minterBurnerAddress = minterBurnerProxy.address; + console.log('GoodDollarOFTMinterBurner proxy', minterBurnerAddress); + + const minterBurnerImpl = await deployments.deploy('GoodDollarOFTMinterBurner_Implementation', { + contract: 'GoodDollarOFTMinterBurner', + from: root.address, + deterministicDeployment: minterBurnerImplSalt, + log: true, + }); + console.log('GoodDollarOFTMinterBurner implementation', minterBurnerImpl.address); + + const minterBurnerContract = await ethers.getContractAt('GoodDollarOFTMinterBurner', minterBurnerAddress); + const minterBurnerInitialized = await minterBurnerContract + .token() + .then((addr: string) => addr !== ethers.constants.AddressZero) + .catch(() => false); + + if (!minterBurnerInitialized) { + console.log('GoodDollarOFTMinterBurner not initialized yet; will initialize after deploying adapter...'); + } else { + console.log('GoodDollarOFTMinterBurner already initialized'); + } + + // Verify GoodDollarOFTMinterBurner implementation (no constructor args) on non-local networks (skip: 'hardhat', 'localhost') + if (!['hardhat', 'localhost'].includes(networkName)) { + await verifyContract(hre as any, minterBurnerImpl.address, [], 'GoodDollarOFTMinterBurner'); + } + + // --- GoodDollarOFTAdapter (hardhat-deploy: deterministic proxy + implementation with constructor + execute initialize) --- + const oftAdapterProxySalt = ethers.utils.keccak256( + ethers.utils.toUtf8Bytes(isDevelopment ? 'Development-GoodDollarOFTAdapterV1' : 'Production-GoodDollarOFTAdapterV1'), + ); + const oftAdapterProxyDeploy = await deployments.deterministic('GoodDollarOFTAdapter', { + contract: 'ERC1967Proxy', + from: root.address, + salt: oftAdapterProxySalt, + log: true, + }); + const oftAdapterProxy = await oftAdapterProxyDeploy.deploy(); + const oftAdapterAddress = oftAdapterProxy.address; + console.log('GoodDollarOFTAdapter proxy', oftAdapterAddress); + + // Initialize minter/burner after we know the adapter address + if (!minterBurnerInitialized) { + console.log('Initializing GoodDollarOFTMinterBurner with adapter address...'); + const minterBurnerInitData = minterBurnerContract.interface.encodeFunctionData('initialize', [ + nameServiceAddress, + oftAdapterAddress, + ]); + await deployments.execute( + 'GoodDollarOFTMinterBurner', + { from: root.address }, + 'initialize', + minterBurnerImpl.address, + minterBurnerInitData, + ); + console.log('GoodDollarOFTMinterBurner initialized'); + } + + const oftAdapterImpl = await deployments.deploy('GoodDollarOFTAdapter_Implementation', { + contract: 'GoodDollarOFTAdapter', + from: root.address, + deterministicDeployment: oftAdapterImplSalt, + log: true, + args: [tokenAddress, lzEndpoint], + }); + console.log('GoodDollarOFTAdapter implementation', oftAdapterImpl.address); + + const oftAdapterContract = await ethers.getContractAt('GoodDollarOFTAdapter', oftAdapterAddress); + const oftAdapterInitialized = await oftAdapterContract + .minterBurner() + .then((addr: string) => addr !== ethers.constants.AddressZero) + .catch(() => false); + + if (!oftAdapterInitialized) { + console.log('Initializing GoodDollarOFTAdapter...'); + const oftAdapterInitData = oftAdapterContract.interface.encodeFunctionData('initialize', [ + tokenAddress, + minterBurnerAddress, + root.address, + root.address, + ]); + await deployments.execute( + 'GoodDollarOFTAdapter', + { from: root.address }, + 'initialize', + oftAdapterImpl.address, + oftAdapterInitData, + ); + console.log('GoodDollarOFTAdapter initialized'); + console.log('Fee recipient:', root.address); + } else { + console.log('GoodDollarOFTAdapter already initialized'); + } + + // Verify GoodDollarOFTAdapter implementation (constructor: tokenAddress, lzEndpoint) on non-local networks (skip: 'hardhat', 'localhost') + if (!['hardhat', 'localhost'].includes(networkName)) { + await verifyContract(hre as any, oftAdapterImpl.address, [tokenAddress, lzEndpoint], 'GoodDollarOFTAdapter'); + } + + const minterBurnerImplAddress = await getImplementationAddress(ethers.provider, minterBurnerAddress).catch( + () => undefined, + ); + const oftAdapterImplAddress = await getImplementationAddress(ethers.provider, oftAdapterAddress).catch( + () => undefined, + ); + + console.log('\n=== Deployment Summary ==='); + console.log('Network:', networkName); + console.log('GoodDollarOFTMinterBurner:', minterBurnerAddress, '(upgradeable)'); + if (minterBurnerImplAddress) { + console.log(' Implementation:', minterBurnerImplAddress); + } + console.log('GoodDollarOFTAdapter:', oftAdapterAddress, '(upgradeable)'); + if (oftAdapterImplAddress) { + console.log(' Implementation:', oftAdapterImplAddress); + } + console.log('Token:', tokenAddress); + console.log('OFT Adapter Owner (Avatar):', avatarAddress); + console.log('LayerZero Endpoint:', lzEndpoint); + console.log('========================\n'); +}; + +export default func; +func.tags = ['OFT', 'GoodDollar']; diff --git a/packages/bridge-contracts/deploy/upgradeOFT.ts b/packages/bridge-contracts/deploy/upgradeOFT.ts new file mode 100644 index 0000000..6b98fdf --- /dev/null +++ b/packages/bridge-contracts/deploy/upgradeOFT.ts @@ -0,0 +1,202 @@ +/*** + * Hardhat-deploy script to upgrade existing GoodDollar OFT contracts. + * + * Same flow as deployOFT.ts (no hardhat-upgrades): + * - Reads proxy addresses from hardhat-deploy artifacts (`deployments/`) for the current network + * - Resolves token and LayerZero endpoint from GoodProtocol deployment (same as deployOFT) + * - Deploys new implementations (OFT adapter with constructor args, MinterBurner with none) + * - Calls upgradeTo(newImplementation) on each proxy (UUPS) + * + * Usage: + * npx hardhat deploy --tags OFT-Upgrade --network development-xdc + * npx hardhat deploy --tags OFT-Upgrade --network development-celo + * npx hardhat deploy --tags OFT-Upgrade --network production-xdc + * npx hardhat deploy --tags OFT-Upgrade --network production-celo + */ + +import { DeployFunction } from "hardhat-deploy/types"; +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", +}; + +const func: DeployFunction = async function (hre) { + const { deployments, network } = hre; + const [root] = await ethers.getSigners(); + const networkName = network.name; + + console.log("=== Upgrade GoodDollar OFT Contracts ==="); + console.log("Network:", networkName); + console.log("Deployer:", root.address); + console.log( + "Deployer balance:", + (await ethers.provider.getBalance(root.address)).toString() + ); + + const { GoodDollarOFTAdapter: oftAdapterProxy, GoodDollarOFTMinterBurner: minterBurnerProxy } = + getOftDeploymentAddresses(networkName); + + console.log("\nExisting proxy addresses (from hardhat-deploy artifacts):"); + console.log("GoodDollarOFTAdapter proxy:", oftAdapterProxy); + console.log("GoodDollarOFTMinterBurner proxy:", minterBurnerProxy); + + const goodProtocolContracts = (Contracts as any)[networkName] as any; + if (!goodProtocolContracts) { + throw new Error( + `No GoodProtocol contracts found for network ${networkName}. Please check @gooddollar/goodprotocol/releases/deployment.json` + ); + } + + const tokenAddress = goodProtocolContracts.GoodDollar as string | undefined; + if (!tokenAddress) { + throw new Error( + `Token address not found in GoodProtocol deployment for network ${networkName}. Please deploy SuperGoodDollar or GoodDollar first.` + ); + } + + 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}` + ); + } + + console.log("\nDeployment parameters (same as deployOFT):"); + console.log("Token:", tokenAddress); + console.log("LayerZero endpoint:", lzEndpoint); + + // Controller / Avatar (same pattern as deployOFT) + const nameServiceAddress = goodProtocolContracts.NameService as string | undefined; + if (!nameServiceAddress) { + throw new Error( + `NameService address not found in GoodProtocol deployment for network ${networkName}. Please deploy NameService first.` + ); + } + + let controllerAddress = goodProtocolContracts.Controller as string | undefined; + if (!controllerAddress) { + 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}` + ); + } + } + + 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}`); + } + console.log("Controller:", controllerAddress); + console.log("Avatar:", avatarAddress); + + // CREATE2 salt for implementations: + // hardhat-deploy uses `deterministicDeployment` as the CREATE2 salt. + // We derive it from the contract's compiled bytecode. + const minterBurnerArtifact = await hre.artifacts.readArtifact("GoodDollarOFTMinterBurner"); + const minterBurnerImplSalt = ethers.utils.keccak256(minterBurnerArtifact.bytecode); + const oftAdapterArtifact = await hre.artifacts.readArtifact("GoodDollarOFTAdapter"); + const oftAdapterImplSalt = ethers.utils.keccak256(oftAdapterArtifact.bytecode); + + // --- GoodDollarOFTAdapter: deploy new implementation via hardhat-deploy, then upgrade proxy --- + console.log("\nDeploying new GoodDollarOFTAdapter implementation..."); + + const oftAdapterImpl = await deployments.deploy("GoodDollarOFTAdapter_Implementation", { + contract: "GoodDollarOFTAdapter", + from: root.address, + deterministicDeployment: oftAdapterImplSalt, + log: true, + args: [tokenAddress, lzEndpoint], + }); + console.log("GoodDollarOFTAdapter implementation", oftAdapterImpl.address); + + console.log("Upgrading GoodDollarOFTAdapter proxy via upgradeTo..."); + const oftAdapterProxyContract = await ethers.getContractAt( + "GoodDollarOFTAdapter", + oftAdapterProxy + ); + const txOft = await oftAdapterProxyContract.upgradeTo(oftAdapterImpl.address); + await txOft.wait(); + console.log("✅ GoodDollarOFTAdapter upgraded"); + + // --- GoodDollarOFTMinterBurner: deploy new implementation via hardhat-deploy, then upgrade proxy --- + console.log("\nDeploying new GoodDollarOFTMinterBurner implementation..."); + const minterBurnerImpl = await deployments.deploy( + "GoodDollarOFTMinterBurner_Implementation", + { + contract: "GoodDollarOFTMinterBurner", + from: root.address, + deterministicDeployment: minterBurnerImplSalt, + log: true, + } + ); + console.log("GoodDollarOFTMinterBurner implementation", minterBurnerImpl.address); + + console.log("Upgrading GoodDollarOFTMinterBurner proxy via Controller.genericCall (avatar)..."); + const minterBurnerProxyContract = await ethers.getContractAt( + "GoodDollarOFTMinterBurner", + minterBurnerProxy + ); + const upgradeData = minterBurnerProxyContract.interface.encodeFunctionData("upgradeTo", [ + minterBurnerImpl.address, + ]); + const txMb = await Controller.genericCall( + minterBurnerProxy, + upgradeData, + avatarAddress, + 0 + ); + await txMb.wait(); + console.log("✅ GoodDollarOFTMinterBurner upgraded via DAO avatar"); + + const minterBurnerImplAddress = await getImplementationAddress( + ethers.provider, + minterBurnerProxy + ).catch(() => undefined); + const oftAdapterImplAddress = await getImplementationAddress( + ethers.provider, + oftAdapterProxy + ).catch(() => undefined); + + console.log("\n=== Upgrade Summary ==="); + console.log("Network:", networkName); + console.log("GoodDollarOFTMinterBurner:", minterBurnerProxy, "(upgradeable)"); + if (minterBurnerImplAddress) { + console.log(" Implementation:", minterBurnerImplAddress); + } + console.log("GoodDollarOFTAdapter:", oftAdapterProxy, "(upgradeable)"); + if (oftAdapterImplAddress) { + console.log(" Implementation:", oftAdapterImplAddress); + } + console.log("Token:", tokenAddress); + console.log("LayerZero Endpoint:", lzEndpoint); + console.log("\n--- Verify commands (constructor args for GoodDollarOFTAdapter) ---"); + console.log( + "GoodDollarOFTAdapter implementation:\n npx hardhat verify --network", + networkName, + oftAdapterImpl.address, + tokenAddress, + lzEndpoint + ); + console.log( + "GoodDollarOFTMinterBurner implementation (no constructor args):\n npx hardhat verify --network", + networkName, + minterBurnerImpl.address + ); + console.log("========================\n"); +}; + +export default func; +func.tags = ["OFT-Upgrade", "OFT"]; diff --git a/packages/bridge-contracts/deploy/utils/getOftDeploymentAddresses.ts b/packages/bridge-contracts/deploy/utils/getOftDeploymentAddresses.ts new file mode 100644 index 0000000..77474b2 --- /dev/null +++ b/packages/bridge-contracts/deploy/utils/getOftDeploymentAddresses.ts @@ -0,0 +1,50 @@ +import path from 'path'; +import fse from 'fs-extra'; + +export type OftDeploymentAddresses = { + GoodDollarOFTMinterBurner: string; + GoodDollarOFTAdapter: string; +}; + +/** + * Reads OFT proxy addresses from hardhat-deploy's `deployments/` artifacts. + * + * hardhat-deploy writes: + * deployments//GoodDollarOFTMinterBurner.json + * deployments//GoodDollarOFTAdapter.json + */ +export function getOftDeploymentAddresses(networkName: string): OftDeploymentAddresses { + const deploymentsDir = path.resolve(__dirname, '../../deployments'); + + const minterBurnerPath = path.join(deploymentsDir, networkName, 'GoodDollarOFTMinterBurner.json'); + const oftAdapterPath = path.join(deploymentsDir, networkName, 'GoodDollarOFTAdapter.json'); + + if (!fse.existsSync(minterBurnerPath)) { + throw new Error( + `Missing hardhat-deploy artifact for GoodDollarOFTMinterBurner on network "${networkName}". ` + + `Expected: ${minterBurnerPath}. Make sure you've run: hardhat deploy --tags OFT --network ${networkName}` + ); + } + if (!fse.existsSync(oftAdapterPath)) { + throw new Error( + `Missing hardhat-deploy artifact for GoodDollarOFTAdapter on network "${networkName}". ` + + `Expected: ${oftAdapterPath}. Make sure you've run: hardhat deploy --tags OFT --network ${networkName}` + ); + } + + const minterBurnerDeployment = fse.readJSONSync(minterBurnerPath) as { address?: string }; + const oftAdapterDeployment = fse.readJSONSync(oftAdapterPath) as { address?: string }; + + if (!minterBurnerDeployment.address) { + throw new Error(`Invalid deployment file (no "address") for GoodDollarOFTMinterBurner: ${minterBurnerPath}`); + } + if (!oftAdapterDeployment.address) { + throw new Error(`Invalid deployment file (no "address") for GoodDollarOFTAdapter: ${oftAdapterPath}`); + } + + return { + GoodDollarOFTMinterBurner: minterBurnerDeployment.address, + GoodDollarOFTAdapter: oftAdapterDeployment.address, + }; +} + diff --git a/packages/bridge-contracts/deploy/utils/verifyContract.ts b/packages/bridge-contracts/deploy/utils/verifyContract.ts new file mode 100644 index 0000000..e21b710 --- /dev/null +++ b/packages/bridge-contracts/deploy/utils/verifyContract.ts @@ -0,0 +1,92 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types'; + +/** + * Verify a contract on block explorer (Etherscan, etc.) + * + * @param hre Hardhat runtime environment + * @param contractAddress The address of the contract to verify + * @param constructorArgs Optional constructor arguments array + * @param contractName Optional contract name for logging + * @returns Promise that resolves when verification is complete + */ +export async function verifyContract( + hre: HardhatRuntimeEnvironment, + contractAddress: string, + constructorArgs: any[] = [], + contractName?: string, +): Promise { + const networkName = hre.network.name; + const displayName = contractName || contractAddress; + + // Skip verification for local networks + if (['hardhat', 'localhost'].includes(networkName)) { + console.log(`ℹ️ Skipping verification for ${displayName} on local network: ${networkName}`); + return; + } + + console.log(`\n🔍 Verifying ${displayName}...`); + console.log(` Address: ${contractAddress}`); + if (constructorArgs.length > 0) { + console.log(` Constructor args: ${JSON.stringify(constructorArgs)}`); + } + + try { + await hre.run('verify:verify', { + address: contractAddress, + constructorArguments: constructorArgs, + }); + console.log(`✅ ${displayName} verified successfully`); + } catch (error: any) { + const errorMessage = error.message || error.toString() || ''; + + if (errorMessage.includes('Already Verified') || errorMessage.includes('already verified')) { + console.log(`ℹ️ ${displayName} already verified`); + } else { + console.log(`⚠️ ${displayName} verification error: ${errorMessage}`); + console.log(` You can verify manually using:`); + if (constructorArgs.length > 0) { + console.log(` npx hardhat verify --network ${networkName} ${contractAddress} ${constructorArgs.join(' ')}`); + } else { + console.log(` npx hardhat verify --network ${networkName} ${contractAddress}`); + } + } + } +} + +/** + * Verify multiple contracts in sequence + * + * @param hre Hardhat runtime environment + * @param contracts Array of contract verification configs + */ +export async function verifyContracts( + hre: HardhatRuntimeEnvironment, + contracts: Array<{ + address: string; + constructorArgs?: any[]; + name?: string; + }>, +): Promise { + console.log(`\n=== Starting Contract Verification (${contracts.length} contracts) ===`); + + for (const contract of contracts) { + await verifyContract( + hre, + contract.address, + contract.constructorArgs || [], + contract.name, + ); + } + + // Also verify on Sourcify + console.log('\n🔍 Verifying on Sourcify...'); + try { + await hre.run('sourcify'); + console.log('✅ Sourcify verification completed'); + } catch (error: any) { + console.log('⚠️ Sourcify verification error:', error.message || error.toString()); + } + + console.log('=== Contract Verification Complete ===\n'); +} + diff --git a/packages/bridge-contracts/hardhat.config.ts b/packages/bridge-contracts/hardhat.config.ts index 25a916d..027e47c 100644 --- a/packages/bridge-contracts/hardhat.config.ts +++ b/packages/bridge-contracts/hardhat.config.ts @@ -109,7 +109,7 @@ const config: HardhatUserConfig = { xdc: { accounts: accounts as HttpNetworkAccountsConfig, chainId: 50, - url: 'https://rpc.xinfin.network', + url: 'https://rpc.xdc.org', verify: { etherscan: { apiUrl: 'https://api.etherscan.io/v2/api?chainid=50', @@ -149,6 +149,44 @@ const config: HardhatUserConfig = { gasPrice: 2e9, chainId: 5, }, + "development-celo": { + accounts: accounts as HttpNetworkAccountsConfig, + url: "https://forno.celo.org", + gas: 3000000, + gasPrice: 26e9, + chainId: 42220, + eid: EndpointId.CELO_V2_MAINNET, + } as any, + "production-celo": { + accounts: accounts as HttpNetworkAccountsConfig, + url: "https://forno.celo.org", + gas: 8000000, + gasPrice: 26e9, + chainId: 42220 + }, + "production-xdc": { + accounts: accounts as HttpNetworkAccountsConfig, + chainId: 50, + url: 'https://rpc.xdc.org', + verify: { + etherscan: { + apiUrl: 'https://api.etherscan.io/v2/api?chainid=50', + apiKey: process.env.ETHERSCAN_KEY || '', + }, + }, + }, + "development-xdc": { + accounts: accounts as HttpNetworkAccountsConfig, + chainId: 50, + url: 'https://rpc.xdc.org', + eid: EndpointId.XDC_V2_MAINNET, + verify: { + etherscan: { + apiUrl: 'https://api.etherscan.io/v2/api?chainid=50', + apiKey: process.env.ETHERSCAN_KEY || '', + }, + }, + } as any }, sourcify: { enabled: true, @@ -190,6 +228,16 @@ const config: HardhatUserConfig = { contractSizer: { runOnCompile: true, }, + namedAccounts: { + deployer: { + default: 0, // Use the first account as deployer + }, + }, + // hardhat-deploy configuration + // This ensures LayerZero DevTools can detect hardhat-deploy usage + paths: { + deployments: 'deployments', + }, }; export default config; diff --git a/packages/bridge-contracts/layerzero.config.ts b/packages/bridge-contracts/layerzero.config.ts index 7570da1..8f86282 100644 --- a/packages/bridge-contracts/layerzero.config.ts +++ b/packages/bridge-contracts/layerzero.config.ts @@ -14,36 +14,28 @@ 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 dao from "./releases/deployment.json"; +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 deployment.json -const xdcOftAdapterAddress = (dao[XDC_NETWORK] as any)?.GoodDollarOFTAdapter; -const celoOftAdapterAddress = (dao[CELO_NETWORK] as any)?.GoodDollarOFTAdapter; - -if (!xdcOftAdapterAddress || !celoOftAdapterAddress) { - throw new Error( - `OFT Adapter addresses not found in deployment.json. ` + - `XDC: ${xdcOftAdapterAddress || "missing"}, CELO: ${celoOftAdapterAddress || "missing"}. ` + - `Please deploy them first or adjust XDC_NETWORK and CELO_NETWORK constants.` - ); -} +// 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, + 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, + address: celoOftAdapterAddress, // Will be updated after deployment }; // Enforced execution options for EVM chains diff --git a/packages/bridge-contracts/package.json b/packages/bridge-contracts/package.json index 2057ed1..2c523d5 100644 --- a/packages/bridge-contracts/package.json +++ b/packages/bridge-contracts/package.json @@ -26,8 +26,11 @@ "@axelar-network/axelarjs-sdk": "^0.13.6", "@chainlink/env-enc": "^1.0.5", "@gooddollar/goodprotocol": "2.1.0", + "@layerzerolabs/devtools-evm-hardhat": "^4.0.4", "@layerzerolabs/lz-definitions": "^3.0.151", "@layerzerolabs/lz-evm-protocol-v2": "^3.0.151", + "@layerzerolabs/lz-v2-utilities": "^3.0.156", + "@layerzerolabs/metadata-tools": "^3.0.3", "@layerzerolabs/oapp-evm": "^0.4.1", "@layerzerolabs/oapp-evm-upgradeable": "^0.1.3", "@layerzerolabs/oft-evm": "^4.0.0", @@ -35,6 +38,7 @@ "@layerzerolabs/scan-client": "^0.0.6", "@layerzerolabs/solidity-examples": "^0.0.13", "@layerzerolabs/toolbox-hardhat": "~0.6.13", + "@layerzerolabs/ua-devtools": "^5.0.2", "@nomicfoundation/hardhat-chai-matchers": "^1.0.5", "@nomicfoundation/hardhat-network-helpers": "^1.0.6", "@nomicfoundation/hardhat-verify": "^2.1.0", @@ -56,7 +60,7 @@ "ethers": "^5.*", "hardhat": "2.26", "hardhat-contract-sizer": "^2.6.1", - "hardhat-deploy": "^1.0.4", + "hardhat-deploy": "^0.12.4", "hardhat-gas-reporter": "^1.0.9", "merkle-patricia-tree": "^2.*", "prettier": "^2.5.1", diff --git a/packages/bridge-contracts/release/deployment.json b/packages/bridge-contracts/release/deployment.json index 24f06f7..a93d348 100644 --- a/packages/bridge-contracts/release/deployment.json +++ b/packages/bridge-contracts/release/deployment.json @@ -1,17 +1,17 @@ { - "fuse": { - "fuseBridge": "0x5B7cEfD0e7d952F7E400416F9c98fE36F1043822", - "celoBridge": "0x165aEb4184A0cc4eFb96Cb6035341Ba2265bA564", - "registry": "0x44a1E0A83821E239F9Cef248CECc3AC5b910aeD2" - }, - "staging": { - "fuseBridge": "0x1CD7a472FF2c6826252932CC8aC40473898d90E8", - "celoBridge": "0x0A6538C9DAc037f5313CaAEb42b19081993e3183", - "registry": "0x44a1E0A83821E239F9Cef248CECc3AC5b910aeD2" - }, - "production": { - "fuseBridge": "0x08fdf766694C353401350c225cAEB9C631dC3288", - "celoBridge": "0xfb152Fc469A3E9154f8AA60bbD6700EcBC357A54", - "registry": "0x44a1E0A83821E239F9Cef248CECc3AC5b910aeD2" - } -} \ No newline at end of file + "fuse": { + "fuseBridge": "0x5B7cEfD0e7d952F7E400416F9c98fE36F1043822", + "celoBridge": "0x165aEb4184A0cc4eFb96Cb6035341Ba2265bA564", + "registry": "0x44a1E0A83821E239F9Cef248CECc3AC5b910aeD2" + }, + "staging": { + "fuseBridge": "0x1CD7a472FF2c6826252932CC8aC40473898d90E8", + "celoBridge": "0x0A6538C9DAc037f5313CaAEb42b19081993e3183", + "registry": "0x44a1E0A83821E239F9Cef248CECc3AC5b910aeD2" + }, + "production": { + "fuseBridge": "0x08fdf766694C353401350c225cAEB9C631dC3288", + "celoBridge": "0xfb152Fc469A3E9154f8AA60bbD6700EcBC357A54", + "registry": "0x44a1E0A83821E239F9Cef248CECc3AC5b910aeD2" + } +} diff --git a/packages/bridge-contracts/test/GoodDollarOFT.test.ts b/packages/bridge-contracts/test/GoodDollarOFT.test.ts index 1657625..14942db 100644 --- a/packages/bridge-contracts/test/GoodDollarOFT.test.ts +++ b/packages/bridge-contracts/test/GoodDollarOFT.test.ts @@ -6,7 +6,7 @@ describe("OFT (unit, no fork)", () => { async function fixture() { const [owner, user, feeRecipient, avatar] = await ethers.getSigners(); - // Mocks for DAO stack used by GoodDollarMinterBurner + // Mocks for DAO stack used by GoodDollarOFTMinterBurner const NameService = await ethers.getContractFactory("NameServiceMock"); const nameService = await NameService.deploy(); await nameService.deployed(); diff --git a/packages/bridge-contracts/test/oft/OFT_CONFIGURING_GUIDE.md b/packages/bridge-contracts/test/oft/OFT_CONFIGURING_GUIDE.md new file mode 100644 index 0000000..005d389 --- /dev/null +++ b/packages/bridge-contracts/test/oft/OFT_CONFIGURING_GUIDE.md @@ -0,0 +1,153 @@ +# 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/bridge-oft-token.ts b/packages/bridge-contracts/test/oft/bridge-oft-token.ts new file mode 100644 index 0000000..45ad1ee --- /dev/null +++ b/packages/bridge-contracts/test/oft/bridge-oft-token.ts @@ -0,0 +1,400 @@ +/*** + * 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 + * + * Note: Make sure you have: + * - GoodDollarOFTAdapter deployed on both XDC and CELO + * - Sufficient G$ balance on the source chain + * - Sufficient native token (XDC or CELO) for gas and LayerZero fees + */ + +import { network, ethers } from "hardhat"; +import { Contract } from "ethers"; +import { EndpointId } from "@layerzerolabs/lz-definitions"; +import Contracts from "@gooddollar/goodprotocol/releases/deployment.json"; +import { getOftDeploymentAddresses } from "../../deploy/utils/getOftDeploymentAddresses"; + +// IERC20 interface for token operations +const IERC20_ABI = [ + "function balanceOf(address owner) view returns (uint256)", + "function allowance(address owner, address spender) view returns (uint256)", + "function approve(address spender, uint256 amount) returns (bool)", + "function transfer(address to, uint256 amount) returns (bool)", +]; + +// LayerZero Endpoint IDs (eid) +// These are LayerZero v2 endpoint IDs, not chain IDs +const XDC_ENDPOINT_ID = EndpointId.XDC_V2_MAINNET; +const CELO_ENDPOINT_ID = process.env.CELO_LZ_ENDPOINT_ID + ? parseInt(process.env.CELO_LZ_ENDPOINT_ID) + : EndpointId.CELO_V2_MAINNET; // Default CELO LayerZero endpoint ID + +const main = async () => { + const networkName = network.name; + const [sender] = await ethers.getSigners(); + + // Detect source and destination networks + const isXDC = networkName.includes("xdc"); + const isCELO = networkName.includes("celo"); + + if (!isXDC && !isCELO) { + throw new Error( + `Network must be XDC or CELO. Current network: ${networkName}\n` + + `Supported networks: production-xdc, development-xdc, production-celo, development-celo` + ); + } + + const sourceNetwork = isXDC ? "XDC" : "CELO"; + const destNetwork = isXDC ? "CELO" : "XDC"; + const sourceEndpointId = isXDC ? XDC_ENDPOINT_ID : CELO_ENDPOINT_ID; + const destEndpointId = isXDC ? CELO_ENDPOINT_ID : XDC_ENDPOINT_ID; + const nativeTokenName = isXDC ? "XDC" : "CELO"; + + console.log("=== Bridge G$ ==="); + console.log(`Bridging from ${sourceNetwork} to ${destNetwork}`); + console.log("Source Network:", networkName); + console.log("Sender:", sender.address); + console.log(`Sender balance: ${ethers.utils.formatEther(await ethers.provider.getBalance(sender.address))} ${nativeTokenName}`); + + // Get deployment info for source network + 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 tokenAddress = goodProtocolContracts.GoodDollar || goodProtocolContracts.SuperGoodDollar; + + if (!tokenAddress) { + throw new Error(`GoodDollar token not found in GoodProtocol deployment.json for ${networkName}`); + } + + + console.log("\nSource chain contract addresses:"); + console.log("OFT Adapter:", oftAdapterAddress); + console.log("Token:", tokenAddress); + console.log("MinterBurner:", minterBurnerAddress); + + // Get contracts + const token = new ethers.Contract(tokenAddress, IERC20_ABI, sender); + const oftAdapter = await ethers.getContractAt("GoodDollarOFTAdapter", oftAdapterAddress); + + // Amount to bridge: 1 G$ = 1e18 + const amount = ethers.utils.parseEther("90"); + console.log("\nAmount to bridge:", ethers.utils.formatEther(amount), "G$"); + + // Check token balance + const balance = await token.balanceOf(sender.address); + console.log("Current G$ balance:", ethers.utils.formatEther(balance), "G$"); + + if (balance.lt(amount)) { + throw new Error(`Insufficient balance. Need ${ethers.utils.formatEther(amount)} G$, have ${ethers.utils.formatEther(balance)} G$`); + } + + // Check and approve MinterBurner if needed (required for burning tokens) + // The OFT adapter calls minterBurner.burn(), which calls token.burnFrom() requiring approval + // Note: OFT adapter itself doesn't need approval since approvalRequired() returns false + const minterBurnerAllowance = await token.allowance(sender.address, minterBurnerAddress); + console.log("\nChecking MinterBurner allowance..."); + console.log("Current MinterBurner allowance:", ethers.utils.formatEther(minterBurnerAllowance), "G$"); + + if (minterBurnerAllowance.lt(amount)) { + console.log("\nApproving MinterBurner to burn tokens..."); + const approveMinterBurnerTx = await token.approve(minterBurnerAddress, amount); + await approveMinterBurnerTx.wait(); + console.log("MinterBurner approval confirmed:", approveMinterBurnerTx.hash); + } else { + console.log("Sufficient MinterBurner allowance already set"); + } + + // Recipient address (same address on destination chain) + const recipient = sender.address; + console.log(`\nRecipient on ${destNetwork}:`, recipient); + + // Get destination network OFT adapter address + let destNetworkName: string; + if (isXDC) { + // Bridging to CELO - try production-celo first, then development-celo + destNetworkName = "development-celo"; + } else { + // Bridging to XDC - try production-xdc first, then development-xdc + destNetworkName = "development-xdc"; + } + + const { GoodDollarOFTAdapter: destOFTAdapter } = getOftDeploymentAddresses(destNetworkName); + + console.log(`\nDestination chain (${destNetwork}):`); + console.log(`OFT Adapter: ${destOFTAdapter}`); + console.log(`Network name: ${destNetworkName}`); + + // Check if peer is set for destination chain + console.log(`\nChecking if ${destNetwork} peer is configured...`); + const destPeer = await oftAdapter.peers(destEndpointId); + console.log(`Current ${destNetwork} peer:`, destPeer); + + const expectedPeer = ethers.utils.hexZeroPad(destOFTAdapter, 32); + console.log(`Expected ${destNetwork} peer (OFT adapter on ${destNetwork}):`, destOFTAdapter); + console.log("Expected peer (bytes32):", expectedPeer); + + // Compare case-insensitively (addresses can have different case) + const destPeerLower = destPeer.toLowerCase(); + const expectedPeerLower = expectedPeer.toLowerCase(); + + if (destPeerLower === ethers.constants.HashZero.toLowerCase() || destPeerLower !== expectedPeerLower) { + console.log(`\n⚠️ WARNING: ${destNetwork} peer is not configured correctly!`); + 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}`); + throw new Error(`NoPeer: ${destNetwork} peer (endpoint ${destEndpointId}) is not set. Expected: ${destOFTAdapter}`); + } + + console.log(`✅ ${destNetwork} peer is configured correctly`); + + await token.approve(minterBurnerAddress, amount); + // Double-check MinterBurner approval before calling quoteSend + console.log("\nVerifying MinterBurner approval before quoteSend..."); + const finalMinterBurnerAllowance = await token.allowance(sender.address, minterBurnerAddress); + console.log("Final MinterBurner allowance:", ethers.utils.formatEther(finalMinterBurnerAllowance), "G$"); + + if (finalMinterBurnerAllowance.lt(amount)) { + throw new Error( + `MinterBurner allowance insufficient. Need ${ethers.utils.formatEther(amount)} G$, have ${ethers.utils.formatEther(finalMinterBurnerAllowance)} G$` + ); + } + + // Check send library configuration before attempting quoteSend + console.log("\nChecking send library configuration..."); + try { + // Get the endpoint address from the OFT adapter (OApp has endpoint() function) + let endpointAddress: string; + try { + endpointAddress = await oftAdapter.endpoint(); + } catch { + // Fallback: try to get from the deployment config + const lzEndpoints: { [key: string]: string } = { + "development-celo": "0x1a44076050125825900e736c501f859c50fE728c", + "production-celo": "0x1a44076050125825900e736c501f859c50fE728c", + "development-xdc": "0xcb566e3B6934Fa77258d68ea18E931fa75e1aaAa", + "production-xdc": "0xcb566e3B6934Fa77258d68ea18E931fa75e1aaAa", + }; + endpointAddress = lzEndpoints[networkName] || ""; + if (!endpointAddress) { + throw new Error(`Could not determine endpoint address for ${networkName}`); + } + } + console.log("LayerZero Endpoint:", endpointAddress); + + // Check if send library is configured + // The endpoint's MessageLibManager has getSendLibrary function + const endpointABI = [ + "function getSendLibrary(address _sender, uint32 _dstEid) external view returns (address lib)", + "function defaultSendLibrary(uint32 _dstEid) external view returns (address lib)", + "function defaultReceiveLibrary(uint32 _srcEid) external view returns (address lib)", + "function getReceiveLibrary(address _receiver, uint32 _srcEid) external view returns (address lib)", + ]; + const endpoint = new ethers.Contract(endpointAddress, endpointABI, sender); + + try { + const sendLib = await endpoint.getSendLibrary(oftAdapterAddress, destEndpointId); + console.log(`Send library for ${destNetwork} (eid ${destEndpointId}):`, sendLib); + + if (sendLib === ethers.constants.AddressZero) { + console.log("⚠️ WARNING: No send library configured!"); + try { + const defaultSendLib = await endpoint.defaultSendLibrary(destEndpointId); + console.log(`Default send library for ${destNetwork}:`, defaultSendLib); + if (defaultSendLib === ethers.constants.AddressZero) { + 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}` + ); + } else { + console.log("ℹ️ Using default send library. Consider configuring a specific send library for better control."); + } + } catch (e: any) { + throw new Error( + `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}` + ); + } + } else { + console.log("✅ Send library is configured"); + + // Note: We can't check receive library on destination chain from here (cross-chain calls not supported) + // The error 0x6592671c likely indicates missing DVN/executor configuration from wiring + console.log(`\nℹ️ Note: Cannot check receive library on ${destNetwork} from ${sourceNetwork} network.`); + console.log(` If quoteSend fails, ensure wiring is completed on BOTH networks.`); + } + } catch (e: any) { + console.log("⚠️ Could not check send library configuration:", e.message); + console.log("Proceeding with quoteSend - will fail if send library is not configured..."); + } + } catch (e: any) { + console.log("⚠️ Could not check endpoint configuration:", e.message); + console.log("Proceeding with quoteSend..."); + } + + // Estimate LayerZero fee using quoteSend + console.log("\nEstimating LayerZero fee..."); + try { + // LayerZero v2 OFT uses quoteSend with SendParam struct + // SendParam: { dstEid, to, amountLD, minAmountLD, extraOptions, composeMsg, oftCmd } + // + // For extraOptions: Use combineOptions to build proper options, or use empty bytes + // Since wiring failed, we'll try using the OApp's combineOptions if available, + // otherwise use empty options (but this might fail if enforced options are required) + let extraOptions = "0x"; + + try { + // Try to use combineOptions to build proper options + // combineOptions(msgType, extraOptions) - msgType 1 = SEND + const combineOptionsResult = await oftAdapter.combineOptions(destEndpointId, 1, extraOptions); + if (combineOptionsResult && combineOptionsResult !== "0x") { + extraOptions = combineOptionsResult; + console.log("Using combined options from OApp"); + } + } catch (e: any) { + console.log("Note: Could not use combineOptions, using empty options"); + console.log("If quoteSend fails, enforced options may need to be configured via wiring"); + } + + const sendParam = { + dstEid: destEndpointId, // destination endpoint ID + to: ethers.utils.hexZeroPad(recipient, 32), // recipient address (bytes32 encoded) + amountLD: amount, // amount to send in local decimals + minAmountLD: amount, // minimum amount to receive (slippage protection) + extraOptions: extraOptions, // extra options (may need to be properly encoded) + composeMsg: "0x", // compose message (empty for simple send) + oftCmd: "0x" // OFT command (unused in default) + }; + + // Quote the fee (payInLzToken = false means pay in native token) + const msgFee = await oftAdapter.quoteSend(sendParam, false); + + console.log(`Estimated native fee: ${ethers.utils.formatEther(msgFee.nativeFee)} ${nativeTokenName}`); + console.log("Estimated LZ token fee:", ethers.utils.formatEther(msgFee.lzTokenFee), "LZ"); + + // Check if sender has enough native token for fee + const senderBalance = await ethers.provider.getBalance(sender.address); + if (senderBalance.lt(msgFee.nativeFee)) { + throw new Error( + `Insufficient native token for fee. Need ${ethers.utils.formatEther(msgFee.nativeFee)} ${nativeTokenName}, have ${ethers.utils.formatEther(senderBalance)} ${nativeTokenName}` + ); + } + + // Send tokens + console.log("\nSending tokens via LayerZero OFT..."); + console.log("This may take a few minutes..."); + + const sendTx = await oftAdapter.send( + sendParam, // SendParam struct + msgFee, // MessagingFee struct + sender.address, // refund address + { value: msgFee.nativeFee } // send native fee + ); + + console.log("Transaction sent:", sendTx.hash); + console.log("Waiting for confirmation..."); + + const receipt = await sendTx.wait(); + console.log("\n✅ Transaction confirmed!"); + console.log("Block number:", receipt.blockNumber); + console.log("Gas used:", receipt.gasUsed.toString()); + + // Look for Send event + const sendEvent = receipt.events?.find((e: any) => e.event === "Send"); + if (sendEvent) { + console.log("\nSend event found:"); + console.log(" Amount:", ethers.utils.formatEther(sendEvent.args?.amountLD || 0), "G$"); + console.log(" Recipient:", sendEvent.args?.to); + } + + console.log("\n=== Bridge Initiated Successfully ==="); + console.log(`Bridging from ${sourceNetwork} to ${destNetwork}`); + console.log("Transaction hash:", sendTx.hash); + console.log(`Recipient on ${destNetwork}:`, recipient); + console.log("Amount:", ethers.utils.formatEther(amount), "G$"); + console.log("\nYou can track the cross-chain message at:"); + console.log(`https://layerzeroscan.com/tx/${sendTx.hash}`); + console.log(`\nNote: The tokens will arrive on ${destNetwork} after the LayerZero message is delivered.`); + console.log("This typically takes a few minutes."); + + } 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; + } +}; + +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/test/oft/grant-minter-role.ts new file mode 100644 index 0000000..3dd3e01 --- /dev/null +++ b/packages/bridge-contracts/test/oft/grant-minter-role.ts @@ -0,0 +1,137 @@ +/*** + * 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/oft.config.json b/packages/bridge-contracts/test/oft/oft.config.json new file mode 100644 index 0000000..712b979 --- /dev/null +++ b/packages/bridge-contracts/test/oft/oft.config.json @@ -0,0 +1,38 @@ +{ + "development-xdc": { + "limits": { + "dailyLimit": "1000", + "txLimit": "100", + "accountDailyLimit": "100", + "minAmount": "1", + "onlyWhitelisted": false + } + }, + "development-celo": { + "limits": { + "dailyLimit": "1000", + "txLimit": "95", + "accountDailyLimit": "200", + "minAmount": "1", + "onlyWhitelisted": false + } + }, + "production-xdc": { + "limits": { + "dailyLimit": "1000000", + "txLimit": "100000", + "accountDailyLimit": "50000", + "minAmount": "10", + "onlyWhitelisted": false + } + }, + "production-celo": { + "limits": { + "dailyLimit": "1000000", + "txLimit": "100000", + "accountDailyLimit": "50000", + "minAmount": "10", + "onlyWhitelisted": false + } + } +} diff --git a/packages/bridge-contracts/test/oft/set-bridge-limits.ts b/packages/bridge-contracts/test/oft/set-bridge-limits.ts new file mode 100644 index 0000000..d97b053 --- /dev/null +++ b/packages/bridge-contracts/test/oft/set-bridge-limits.ts @@ -0,0 +1,178 @@ +/*** + * Script to set bridge limits for GoodDollarOFTAdapter contract + * Uses genericCall through Avatar/Controller to execute the transaction + * + * 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 a guardian or address with permissions to execute via Controller + */ + +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); + + // Prepare transaction + const abiCoder = ethers.utils.defaultAbiCoder; + // const setBridgeLimitsEncoded = oftAdapter.interface.encodeFunctionData("setBridgeLimits", [newLimits]); + + + // Execute via Controller/Avatar + try { + console.log("\nExecuting via Controller/Avatar..."); + const Controller = await ethers.getContractAt("Controller", controllerAddress); + 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 via Avatar!"); + + } 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 new file mode 100644 index 0000000..8e8d8cc --- /dev/null +++ b/packages/bridge-contracts/test/oft/set-oft-operator.ts @@ -0,0 +1,132 @@ +/*** + * 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 new file mode 100644 index 0000000..551a271 --- /dev/null +++ b/packages/bridge-contracts/test/oft/transfer-oft-adapter-ownership-from-avatar.ts @@ -0,0 +1,164 @@ +/*** + * 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 new file mode 100644 index 0000000..0fe808d --- /dev/null +++ b/packages/bridge-contracts/test/oft/transfer-oft-adapter-ownership.ts @@ -0,0 +1,201 @@ +/*** + * 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/packages/bridge-contracts/typechain-types/contracts/BlockHeaderRegistry.ts b/packages/bridge-contracts/typechain-types/contracts/BlockHeaderRegistry.ts deleted file mode 100644 index 2e2f33a..0000000 --- a/packages/bridge-contracts/typechain-types/contracts/BlockHeaderRegistry.ts +++ /dev/null @@ -1,612 +0,0 @@ -/* Autogenerated file. Do not edit manually. */ -/* tslint:disable */ -/* eslint-disable */ -import type { - BaseContract, - BigNumber, - BigNumberish, - BytesLike, - CallOverrides, - ContractTransaction, - Overrides, - PopulatedTransaction, - Signer, - utils, -} from "ethers"; -import type { - FunctionFragment, - Result, - EventFragment, -} from "@ethersproject/abi"; -import type { Listener, Provider } from "@ethersproject/providers"; -import type { - TypedEventFilter, - TypedEvent, - TypedListener, - OnEvent, - PromiseOrValue, -} from "../common"; - -export declare namespace BlockHeaderRegistry { - export type SignatureStruct = { - r: PromiseOrValue; - vs: PromiseOrValue; - }; - - export type SignatureStructOutput = [string, string] & { - r: string; - vs: string; - }; - - export type BlockStruct = { - rlpHeader: PromiseOrValue; - signature: BlockHeaderRegistry.SignatureStruct; - chainId: PromiseOrValue; - blockHash: PromiseOrValue; - cycleEnd: PromiseOrValue; - validators: PromiseOrValue[]; - }; - - export type BlockStructOutput = [ - string, - BlockHeaderRegistry.SignatureStructOutput, - BigNumber, - string, - BigNumber, - string[] - ] & { - rlpHeader: string; - signature: BlockHeaderRegistry.SignatureStructOutput; - chainId: BigNumber; - blockHash: string; - cycleEnd: BigNumber; - validators: string[]; - }; - - export type BlockchainStruct = { - rpc: PromiseOrValue; - chainId: PromiseOrValue; - }; - - export type BlockchainStructOutput = [string, BigNumber] & { - rpc: string; - chainId: BigNumber; - }; - - export type SignedBlockStruct = { - signatures: PromiseOrValue[]; - cycleEnd: PromiseOrValue; - validators: PromiseOrValue[]; - blockHash: PromiseOrValue; - }; - - export type SignedBlockStructOutput = [ - string[], - BigNumber, - string[], - string - ] & { - signatures: string[]; - cycleEnd: BigNumber; - validators: string[]; - blockHash: string; - }; -} - -export interface BlockHeaderRegistryInterface extends utils.Interface { - functions: { - "addBlockchain(uint256,string)": FunctionFragment; - "addSignedBlocks((bytes,(bytes32,bytes32),uint256,bytes32,uint256,address[])[])": FunctionFragment; - "blockHashes(uint256,uint256,uint256)": FunctionFragment; - "consensus()": FunctionFragment; - "enabledBlockchains(uint256)": FunctionFragment; - "getBlockHashByPayloadHash(bytes32)": FunctionFragment; - "getRPCs()": FunctionFragment; - "getSignedBlock(uint256,uint256)": FunctionFragment; - "hasValidatorSigned(bytes32,address)": FunctionFragment; - "parseRLPBlockNumber(bytes)": FunctionFragment; - "signedBlocks(bytes32)": FunctionFragment; - "voting()": FunctionFragment; - }; - - getFunction( - nameOrSignatureOrTopic: - | "addBlockchain" - | "addSignedBlocks" - | "blockHashes" - | "consensus" - | "enabledBlockchains" - | "getBlockHashByPayloadHash" - | "getRPCs" - | "getSignedBlock" - | "hasValidatorSigned" - | "parseRLPBlockNumber" - | "signedBlocks" - | "voting" - ): FunctionFragment; - - encodeFunctionData( - functionFragment: "addBlockchain", - values: [PromiseOrValue, PromiseOrValue] - ): string; - encodeFunctionData( - functionFragment: "addSignedBlocks", - values: [BlockHeaderRegistry.BlockStruct[]] - ): string; - encodeFunctionData( - functionFragment: "blockHashes", - values: [ - PromiseOrValue, - PromiseOrValue, - PromiseOrValue - ] - ): string; - encodeFunctionData(functionFragment: "consensus", values?: undefined): string; - encodeFunctionData( - functionFragment: "enabledBlockchains", - values: [PromiseOrValue] - ): string; - encodeFunctionData( - functionFragment: "getBlockHashByPayloadHash", - values: [PromiseOrValue] - ): string; - encodeFunctionData(functionFragment: "getRPCs", values?: undefined): string; - encodeFunctionData( - functionFragment: "getSignedBlock", - values: [PromiseOrValue, PromiseOrValue] - ): string; - encodeFunctionData( - functionFragment: "hasValidatorSigned", - values: [PromiseOrValue, PromiseOrValue] - ): string; - encodeFunctionData( - functionFragment: "parseRLPBlockNumber", - values: [PromiseOrValue] - ): string; - encodeFunctionData( - functionFragment: "signedBlocks", - values: [PromiseOrValue] - ): string; - encodeFunctionData(functionFragment: "voting", values?: undefined): string; - - decodeFunctionResult( - functionFragment: "addBlockchain", - data: BytesLike - ): Result; - decodeFunctionResult( - functionFragment: "addSignedBlocks", - data: BytesLike - ): Result; - decodeFunctionResult( - functionFragment: "blockHashes", - data: BytesLike - ): Result; - decodeFunctionResult(functionFragment: "consensus", data: BytesLike): Result; - decodeFunctionResult( - functionFragment: "enabledBlockchains", - data: BytesLike - ): Result; - decodeFunctionResult( - functionFragment: "getBlockHashByPayloadHash", - data: BytesLike - ): Result; - decodeFunctionResult(functionFragment: "getRPCs", data: BytesLike): Result; - decodeFunctionResult( - functionFragment: "getSignedBlock", - data: BytesLike - ): Result; - decodeFunctionResult( - functionFragment: "hasValidatorSigned", - data: BytesLike - ): Result; - decodeFunctionResult( - functionFragment: "parseRLPBlockNumber", - data: BytesLike - ): Result; - decodeFunctionResult( - functionFragment: "signedBlocks", - data: BytesLike - ): Result; - decodeFunctionResult(functionFragment: "voting", data: BytesLike): Result; - - events: { - "BlockAdded(address,uint256,bytes32,address[],uint256)": EventFragment; - "BlockchainAdded(uint256,string)": EventFragment; - "BlockchainRemoved(uint256)": EventFragment; - }; - - getEvent(nameOrSignatureOrTopic: "BlockAdded"): EventFragment; - getEvent(nameOrSignatureOrTopic: "BlockchainAdded"): EventFragment; - getEvent(nameOrSignatureOrTopic: "BlockchainRemoved"): EventFragment; -} - -export interface BlockAddedEventObject { - validator: string; - chainId: BigNumber; - rlpHeaderHash: string; - validators: string[]; - cycleEnd: BigNumber; -} -export type BlockAddedEvent = TypedEvent< - [string, BigNumber, string, string[], BigNumber], - BlockAddedEventObject ->; - -export type BlockAddedEventFilter = TypedEventFilter; - -export interface BlockchainAddedEventObject { - chainId: BigNumber; - rpc: string; -} -export type BlockchainAddedEvent = TypedEvent< - [BigNumber, string], - BlockchainAddedEventObject ->; - -export type BlockchainAddedEventFilter = TypedEventFilter; - -export interface BlockchainRemovedEventObject { - chainId: BigNumber; -} -export type BlockchainRemovedEvent = TypedEvent< - [BigNumber], - BlockchainRemovedEventObject ->; - -export type BlockchainRemovedEventFilter = - TypedEventFilter; - -export interface BlockHeaderRegistry extends BaseContract { - connect(signerOrProvider: Signer | Provider | string): this; - attach(addressOrName: string): this; - deployed(): Promise; - - interface: BlockHeaderRegistryInterface; - - queryFilter( - event: TypedEventFilter, - fromBlockOrBlockhash?: string | number | undefined, - toBlock?: string | number | undefined - ): Promise>; - - listeners( - eventFilter?: TypedEventFilter - ): Array>; - listeners(eventName?: string): Array; - removeAllListeners( - eventFilter: TypedEventFilter - ): this; - removeAllListeners(eventName?: string): this; - off: OnEvent; - on: OnEvent; - once: OnEvent; - removeListener: OnEvent; - - functions: { - addBlockchain( - chainId: PromiseOrValue, - rpc: PromiseOrValue, - overrides?: Overrides & { from?: PromiseOrValue } - ): Promise; - - addSignedBlocks( - blocks: BlockHeaderRegistry.BlockStruct[], - overrides?: Overrides & { from?: PromiseOrValue } - ): Promise; - - blockHashes( - arg0: PromiseOrValue, - arg1: PromiseOrValue, - arg2: PromiseOrValue, - overrides?: CallOverrides - ): Promise<[string]>; - - consensus(overrides?: CallOverrides): Promise<[string]>; - - enabledBlockchains( - arg0: PromiseOrValue, - overrides?: CallOverrides - ): Promise<[string, BigNumber] & { rpc: string; chainId: BigNumber }>; - - getBlockHashByPayloadHash( - payloadHash: PromiseOrValue, - overrides?: CallOverrides - ): Promise<[string] & { blockHash: string }>; - - getRPCs( - overrides?: CallOverrides - ): Promise<[BlockHeaderRegistry.BlockchainStructOutput[]]>; - - getSignedBlock( - chainId: PromiseOrValue, - number: PromiseOrValue, - overrides?: CallOverrides - ): Promise< - [BlockHeaderRegistry.SignedBlockStructOutput] & { - signedBlock: BlockHeaderRegistry.SignedBlockStructOutput; - } - >; - - hasValidatorSigned( - arg0: PromiseOrValue, - arg1: PromiseOrValue, - overrides?: CallOverrides - ): Promise<[boolean]>; - - parseRLPBlockNumber( - rlpHeader: PromiseOrValue, - overrides?: CallOverrides - ): Promise<[BigNumber] & { blockNumber: BigNumber }>; - - signedBlocks( - arg0: PromiseOrValue, - overrides?: CallOverrides - ): Promise< - [BigNumber, string] & { cycleEnd: BigNumber; blockHash: string } - >; - - voting(overrides?: CallOverrides): Promise<[string]>; - }; - - addBlockchain( - chainId: PromiseOrValue, - rpc: PromiseOrValue, - overrides?: Overrides & { from?: PromiseOrValue } - ): Promise; - - addSignedBlocks( - blocks: BlockHeaderRegistry.BlockStruct[], - overrides?: Overrides & { from?: PromiseOrValue } - ): Promise; - - blockHashes( - arg0: PromiseOrValue, - arg1: PromiseOrValue, - arg2: PromiseOrValue, - overrides?: CallOverrides - ): Promise; - - consensus(overrides?: CallOverrides): Promise; - - enabledBlockchains( - arg0: PromiseOrValue, - overrides?: CallOverrides - ): Promise<[string, BigNumber] & { rpc: string; chainId: BigNumber }>; - - getBlockHashByPayloadHash( - payloadHash: PromiseOrValue, - overrides?: CallOverrides - ): Promise; - - getRPCs( - overrides?: CallOverrides - ): Promise; - - getSignedBlock( - chainId: PromiseOrValue, - number: PromiseOrValue, - overrides?: CallOverrides - ): Promise; - - hasValidatorSigned( - arg0: PromiseOrValue, - arg1: PromiseOrValue, - overrides?: CallOverrides - ): Promise; - - parseRLPBlockNumber( - rlpHeader: PromiseOrValue, - overrides?: CallOverrides - ): Promise; - - signedBlocks( - arg0: PromiseOrValue, - overrides?: CallOverrides - ): Promise<[BigNumber, string] & { cycleEnd: BigNumber; blockHash: string }>; - - voting(overrides?: CallOverrides): Promise; - - callStatic: { - addBlockchain( - chainId: PromiseOrValue, - rpc: PromiseOrValue, - overrides?: CallOverrides - ): Promise; - - addSignedBlocks( - blocks: BlockHeaderRegistry.BlockStruct[], - overrides?: CallOverrides - ): Promise; - - blockHashes( - arg0: PromiseOrValue, - arg1: PromiseOrValue, - arg2: PromiseOrValue, - overrides?: CallOverrides - ): Promise; - - consensus(overrides?: CallOverrides): Promise; - - enabledBlockchains( - arg0: PromiseOrValue, - overrides?: CallOverrides - ): Promise<[string, BigNumber] & { rpc: string; chainId: BigNumber }>; - - getBlockHashByPayloadHash( - payloadHash: PromiseOrValue, - overrides?: CallOverrides - ): Promise; - - getRPCs( - overrides?: CallOverrides - ): Promise; - - getSignedBlock( - chainId: PromiseOrValue, - number: PromiseOrValue, - overrides?: CallOverrides - ): Promise; - - hasValidatorSigned( - arg0: PromiseOrValue, - arg1: PromiseOrValue, - overrides?: CallOverrides - ): Promise; - - parseRLPBlockNumber( - rlpHeader: PromiseOrValue, - overrides?: CallOverrides - ): Promise; - - signedBlocks( - arg0: PromiseOrValue, - overrides?: CallOverrides - ): Promise< - [BigNumber, string] & { cycleEnd: BigNumber; blockHash: string } - >; - - voting(overrides?: CallOverrides): Promise; - }; - - filters: { - "BlockAdded(address,uint256,bytes32,address[],uint256)"( - validator?: PromiseOrValue | null, - chainId?: PromiseOrValue | null, - rlpHeaderHash?: PromiseOrValue | null, - validators?: null, - cycleEnd?: null - ): BlockAddedEventFilter; - BlockAdded( - validator?: PromiseOrValue | null, - chainId?: PromiseOrValue | null, - rlpHeaderHash?: PromiseOrValue | null, - validators?: null, - cycleEnd?: null - ): BlockAddedEventFilter; - - "BlockchainAdded(uint256,string)"( - chainId?: null, - rpc?: null - ): BlockchainAddedEventFilter; - BlockchainAdded(chainId?: null, rpc?: null): BlockchainAddedEventFilter; - - "BlockchainRemoved(uint256)"(chainId?: null): BlockchainRemovedEventFilter; - BlockchainRemoved(chainId?: null): BlockchainRemovedEventFilter; - }; - - estimateGas: { - addBlockchain( - chainId: PromiseOrValue, - rpc: PromiseOrValue, - overrides?: Overrides & { from?: PromiseOrValue } - ): Promise; - - addSignedBlocks( - blocks: BlockHeaderRegistry.BlockStruct[], - overrides?: Overrides & { from?: PromiseOrValue } - ): Promise; - - blockHashes( - arg0: PromiseOrValue, - arg1: PromiseOrValue, - arg2: PromiseOrValue, - overrides?: CallOverrides - ): Promise; - - consensus(overrides?: CallOverrides): Promise; - - enabledBlockchains( - arg0: PromiseOrValue, - overrides?: CallOverrides - ): Promise; - - getBlockHashByPayloadHash( - payloadHash: PromiseOrValue, - overrides?: CallOverrides - ): Promise; - - getRPCs(overrides?: CallOverrides): Promise; - - getSignedBlock( - chainId: PromiseOrValue, - number: PromiseOrValue, - overrides?: CallOverrides - ): Promise; - - hasValidatorSigned( - arg0: PromiseOrValue, - arg1: PromiseOrValue, - overrides?: CallOverrides - ): Promise; - - parseRLPBlockNumber( - rlpHeader: PromiseOrValue, - overrides?: CallOverrides - ): Promise; - - signedBlocks( - arg0: PromiseOrValue, - overrides?: CallOverrides - ): Promise; - - voting(overrides?: CallOverrides): Promise; - }; - - populateTransaction: { - addBlockchain( - chainId: PromiseOrValue, - rpc: PromiseOrValue, - overrides?: Overrides & { from?: PromiseOrValue } - ): Promise; - - addSignedBlocks( - blocks: BlockHeaderRegistry.BlockStruct[], - overrides?: Overrides & { from?: PromiseOrValue } - ): Promise; - - blockHashes( - arg0: PromiseOrValue, - arg1: PromiseOrValue, - arg2: PromiseOrValue, - overrides?: CallOverrides - ): Promise; - - consensus(overrides?: CallOverrides): Promise; - - enabledBlockchains( - arg0: PromiseOrValue, - overrides?: CallOverrides - ): Promise; - - getBlockHashByPayloadHash( - payloadHash: PromiseOrValue, - overrides?: CallOverrides - ): Promise; - - getRPCs(overrides?: CallOverrides): Promise; - - getSignedBlock( - chainId: PromiseOrValue, - number: PromiseOrValue, - overrides?: CallOverrides - ): Promise; - - hasValidatorSigned( - arg0: PromiseOrValue, - arg1: PromiseOrValue, - overrides?: CallOverrides - ): Promise; - - parseRLPBlockNumber( - rlpHeader: PromiseOrValue, - overrides?: CallOverrides - ): Promise; - - signedBlocks( - arg0: PromiseOrValue, - overrides?: CallOverrides - ): Promise; - - voting(overrides?: CallOverrides): Promise; - }; -} diff --git a/yarn.lock b/yarn.lock index dfa99ea..10bd1dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2096,8 +2096,11 @@ __metadata: "@axelar-network/axelarjs-sdk": "npm:^0.13.6" "@chainlink/env-enc": "npm:^1.0.5" "@gooddollar/goodprotocol": "npm:2.1.0" + "@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-v2-utilities": "npm:^3.0.156" + "@layerzerolabs/metadata-tools": "npm:^3.0.3" "@layerzerolabs/oapp-evm": "npm:^0.4.1" "@layerzerolabs/oapp-evm-upgradeable": "npm:^0.1.3" "@layerzerolabs/oft-evm": "npm:^4.0.0" @@ -2105,6 +2108,7 @@ __metadata: "@layerzerolabs/scan-client": "npm:^0.0.6" "@layerzerolabs/solidity-examples": "npm:^0.0.13" "@layerzerolabs/toolbox-hardhat": "npm:~0.6.13" + "@layerzerolabs/ua-devtools": "npm:^5.0.2" "@nomicfoundation/hardhat-chai-matchers": "npm:^1.0.5" "@nomicfoundation/hardhat-network-helpers": "npm:^1.0.6" "@nomicfoundation/hardhat-verify": "npm:^2.1.0" @@ -2126,7 +2130,7 @@ __metadata: ethers: "npm:^5.*" hardhat: "npm:2.26" hardhat-contract-sizer: "npm:^2.6.1" - hardhat-deploy: "npm:^1.0.4" + hardhat-deploy: "npm:^0.12.4" hardhat-gas-reporter: "npm:^1.0.9" merkle-patricia-tree: "npm:^2.*" prettier: "npm:^2.5.1" @@ -2631,7 +2635,7 @@ __metadata: languageName: node linkType: hard -"@layerzerolabs/devtools-evm-hardhat@npm:~4.0.4": +"@layerzerolabs/devtools-evm-hardhat@npm:^4.0.4, @layerzerolabs/devtools-evm-hardhat@npm:~4.0.4": version: 4.0.4 resolution: "@layerzerolabs/devtools-evm-hardhat@npm:4.0.4" dependencies: @@ -2803,7 +2807,7 @@ __metadata: languageName: node linkType: hard -"@layerzerolabs/lz-v2-utilities@npm:^3.0.148": +"@layerzerolabs/lz-v2-utilities@npm:^3.0.148, @layerzerolabs/lz-v2-utilities@npm:^3.0.156": version: 3.0.156 resolution: "@layerzerolabs/lz-v2-utilities@npm:3.0.156" dependencies: @@ -2819,6 +2823,16 @@ __metadata: languageName: node linkType: hard +"@layerzerolabs/metadata-tools@npm:^3.0.3": + version: 3.0.3 + resolution: "@layerzerolabs/metadata-tools@npm:3.0.3" + peerDependencies: + "@layerzerolabs/devtools-evm-hardhat": ~4.0.0 + "@layerzerolabs/ua-devtools": ~5.0.1 + checksum: 10/82e75265a744bdd5ada327b9d1cc6414b5e9f1fa7736662bc393704665e8527fe7e2d665246310d31f970d774c5671f5b2d53dc68d43d505681882ffaef59a0c + languageName: node + linkType: hard + "@layerzerolabs/oapp-evm-upgradeable@npm:^0.1.3": version: 0.1.3 resolution: "@layerzerolabs/oapp-evm-upgradeable@npm:0.1.3" @@ -3039,7 +3053,7 @@ __metadata: languageName: node linkType: hard -"@layerzerolabs/ua-devtools@npm:~5.0.2": +"@layerzerolabs/ua-devtools@npm:^5.0.2, @layerzerolabs/ua-devtools@npm:~5.0.2": version: 5.0.2 resolution: "@layerzerolabs/ua-devtools@npm:5.0.2" peerDependencies: @@ -12418,9 +12432,9 @@ __metadata: languageName: node linkType: hard -"hardhat-deploy@npm:^1.0.4": - version: 1.0.4 - resolution: "hardhat-deploy@npm:1.0.4" +"hardhat-deploy@npm:^0.12.4": + version: 0.12.4 + resolution: "hardhat-deploy@npm:0.12.4" dependencies: "@ethersproject/abi": "npm:^5.7.0" "@ethersproject/abstract-signer": "npm:^5.7.0" @@ -12433,6 +12447,7 @@ __metadata: "@ethersproject/solidity": "npm:^5.7.0" "@ethersproject/transactions": "npm:^5.7.0" "@ethersproject/wallet": "npm:^5.7.0" + "@types/qs": "npm:^6.9.7" axios: "npm:^0.21.1" chalk: "npm:^4.1.2" chokidar: "npm:^3.5.2" @@ -12443,9 +12458,9 @@ __metadata: fs-extra: "npm:^10.0.0" match-all: "npm:^1.2.6" murmur-128: "npm:^0.2.1" - neoqs: "npm:^6.13.0" + qs: "npm:^6.9.4" zksync-ethers: "npm:^5.0.0" - checksum: 10/41b4784703a29a1690578a34fd8bb75c463347e7050c55ef39ca1d183fef5463bc8a1f679d57e5f7228524fc6abf8e499f8edbe1bcbc801abd10e3ac3299f9f3 + checksum: 10/127feddc4f95eaa530e7fe77021f7634e23f7f182a8a2e6d51ef5c7254037b05862c54aaa79dd7e07cac627dbae6bff68f8439851ca048ccc571110704998f9d languageName: node linkType: hard @@ -16401,13 +16416,6 @@ __metadata: languageName: node linkType: hard -"neoqs@npm:^6.13.0": - version: 6.13.0 - resolution: "neoqs@npm:6.13.0" - checksum: 10/222ac1cc370a3906c24cbc3e384dea47f993d593200b35d5f97ccb03e7248b405889d480a668ddb131898ec3d5049dffc4ddc1910d75f96772ded9c6a33f103a - languageName: node - linkType: hard - "next-tick@npm:^1.1.0": version: 1.1.0 resolution: "next-tick@npm:1.1.0"