From 48c1051cea668b2f1d18b79fdff14ec2a77b657f Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Mon, 27 Nov 2023 13:53:12 +1000 Subject: [PATCH 001/155] Support ledger & switch to ts --- package.json | 2 + scripts/bootstrap/.env.example | 8 + ...loyer_funding.js => 1_deployer_funding.ts} | 40 +-- scripts/bootstrap/2_deployment_validation.js | 60 ---- scripts/bootstrap/2_deployment_validation.ts | 62 ++++ ...ld_deployment.js => 3_child_deployment.ts} | 6 +- ...oot_deployment.js => 4_root_deployment.ts} | 6 +- ...alisation.js => 5_child_initialisation.ts} | 6 +- .../{6_imx_burning.js => 6_imx_burning.ts} | 38 +-- ...mx_rebalancing.js => 7_imx_rebalancing.ts} | 38 +-- ...ialisation.js => 8_root_initialisation.ts} | 6 +- scripts/bootstrap/README.md | 26 +- scripts/deploy/.env.example | 6 + scripts/deploy/README.md | 8 +- ...hild_deployment.js => child_deployment.ts} | 80 ++--- ...tialisation.js => child_initialisation.ts} | 66 ++-- scripts/deploy/deployAndInit.js | 14 - scripts/deploy/deployAndInit.ts | 14 + ...{root_deployment.js => root_deployment.ts} | 69 ++-- ...itialisation.js => root_initialisation.ts} | 126 ++++---- scripts/e2e/{e2e.js => e2e.ts} | 146 ++++----- scripts/helpers/helpers.js | 66 ---- scripts/helpers/helpers.ts | 90 ++++++ scripts/helpers/ledger_signer.ts | 145 +++++++++ .../{axelar_setup.js => axelar_setup.ts} | 46 +-- ...ldchain.config.js => childchain.config.ts} | 7 +- ...hildchain_setup.js => childchain_setup.ts} | 16 +- scripts/localdev/ci.sh | 2 +- scripts/localdev/deploy.sh | 14 +- ...ootchain.config.js => rootchain.config.ts} | 9 +- ...{rootchain_setup.js => rootchain_setup.ts} | 51 ++- scripts/localdev/start.sh | 14 +- tsconfig.json | 11 + yarn.lock | 302 +++++++++++++++++- 34 files changed, 1041 insertions(+), 559 deletions(-) rename scripts/bootstrap/{1_deployer_funding.js => 1_deployer_funding.ts} (62%) delete mode 100644 scripts/bootstrap/2_deployment_validation.js create mode 100644 scripts/bootstrap/2_deployment_validation.ts rename scripts/bootstrap/{3_child_deployment.js => 3_child_deployment.ts} (55%) rename scripts/bootstrap/{4_root_deployment.js => 4_root_deployment.ts} (55%) rename scripts/bootstrap/{5_child_initialisation.js => 5_child_initialisation.ts} (56%) rename scripts/bootstrap/{6_imx_burning.js => 6_imx_burning.ts} (74%) rename scripts/bootstrap/{7_imx_rebalancing.js => 7_imx_rebalancing.ts} (88%) rename scripts/bootstrap/{8_root_initialisation.js => 8_root_initialisation.ts} (56%) rename scripts/deploy/{child_deployment.js => child_deployment.ts} (52%) rename scripts/deploy/{child_initialisation.js => child_initialisation.ts} (52%) delete mode 100644 scripts/deploy/deployAndInit.js create mode 100644 scripts/deploy/deployAndInit.ts rename scripts/deploy/{root_deployment.js => root_deployment.ts} (51%) rename scripts/deploy/{root_initialisation.js => root_initialisation.ts} (54%) rename scripts/e2e/{e2e.js => e2e.ts} (80%) delete mode 100644 scripts/helpers/helpers.js create mode 100644 scripts/helpers/helpers.ts create mode 100644 scripts/helpers/ledger_signer.ts rename scripts/localdev/{axelar_setup.js => axelar_setup.ts} (80%) rename scripts/localdev/{childchain.config.js => childchain.config.ts} (60%) rename scripts/localdev/{childchain_setup.js => childchain_setup.ts} (62%) rename scripts/localdev/{rootchain.config.js => rootchain.config.ts} (60%) rename scripts/localdev/{rootchain_setup.js => rootchain_setup.ts} (63%) create mode 100644 tsconfig.json diff --git a/package.json b/package.json index 98ec7265..b4e5ca0a 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,8 @@ "@axelar-network/axelar-local-dev": "^2.1.1-alpha.2", "@axelar-network/axelarjs-sdk": "^0.12.8", "@ethersproject/hardware-wallets": "^5.7.0", + "@ledgerhq/hw-app-eth": "^6.35.0", + "@ledgerhq/hw-transport-node-hid": "^6.28.0", "@openzeppelin/contracts": "^4.5.0", "axios": "^0.27.2", "bip39": "^3.0.4", diff --git a/scripts/bootstrap/.env.example b/scripts/bootstrap/.env.example index ad143850..1806dcaf 100644 --- a/scripts/bootstrap/.env.example +++ b/scripts/bootstrap/.env.example @@ -9,18 +9,26 @@ ROOT_CHAIN_ID= CHILD_ADMIN_ADDR= ## The private key for the admin EOA or "ledger" if using hardware wallet. CHILD_ADMIN_EOA_SECRET= +## The ledger index for the admin EOA, required if using ledger. +CHILD_ADMIN_EOA_LEDGER_INDEX= ## The deployer address on child chain. CHILD_DEPLOYER_ADDR= ## The private key for the deployer on child chain or "ledger" if using hardware wallet. CHILD_DEPLOYER_SECRET= +## The ledger index for the deployer on child chain, required if using ledger. +CHILD_DEPLOYER_LEDGER_INDEX= ## The amount of fund deployer required on L2, unit is in IMX or 10^18 Wei. CHILD_DEPLOYER_FUND= ## The deployer address on root chain. ROOT_DEPLOYER_ADDR= ## The private key for the deployer on root chain or "ledger" if using hardware wallet. ROOT_DEPLOYER_SECRET= +## The ledger index for the deployer on root chain, required if using ledger. +ROOT_DEPLOYER_LEDGER_INDEX= ## The private key for rate admin or "ledger" if using hardware wallet. ROOT_BRIDGE_RATE_ADMIN_SECRET= +## The ledger index for the rate admin, required if using ledger. +ROOT_BRIDGE_RATE_ADMIN_LEDGER_INDEX= ## The IMX token address on root chain. ROOT_IMX_ADDR= ## The Wrapped ETH token address on the root chain. diff --git a/scripts/bootstrap/1_deployer_funding.js b/scripts/bootstrap/1_deployer_funding.ts similarity index 62% rename from scripts/bootstrap/1_deployer_funding.js rename to scripts/bootstrap/1_deployer_funding.ts index 7266624f..8e3b1341 100644 --- a/scripts/bootstrap/1_deployer_funding.js +++ b/scripts/bootstrap/1_deployer_funding.ts @@ -1,27 +1,29 @@ // Deployer funding -'use strict'; -require('dotenv').config(); -const { ethers } = require("ethers"); -const helper = require("../helpers/helpers.js"); -const { LedgerSigner } = require('@ethersproject/hardware-wallets') +import * as dotenv from "dotenv"; +dotenv.config(); +import { ethers } from "ethers"; +import { requireEnv, waitForConfirmation, waitForReceipt, getFee, hasDuplicates } from "../helpers/helpers"; +import { LedgerSigner } from "../helpers/ledger_signer"; async function run() { console.log("=======Start Deployer Funding======="); // Check environment variables - let childRPCURL = helper.requireEnv("CHILD_RPC_URL"); - let childChainID = helper.requireEnv("CHILD_CHAIN_ID"); - let adminEOASecret = helper.requireEnv("CHILD_ADMIN_EOA_SECRET"); - let axelarEOA = helper.requireEnv("AXELAR_EOA"); - let axelarFund = helper.requireEnv("AXELAR_FUND"); - let deployerEOA = helper.requireEnv("CHILD_DEPLOYER_ADDR"); - let deployerFund = helper.requireEnv("CHILD_DEPLOYER_FUND"); + let childRPCURL = requireEnv("CHILD_RPC_URL"); + let childChainID = requireEnv("CHILD_CHAIN_ID"); + let adminEOASecret = requireEnv("CHILD_ADMIN_EOA_SECRET"); + let axelarEOA = requireEnv("AXELAR_EOA"); + let axelarFund = requireEnv("AXELAR_FUND"); + let deployerEOA = requireEnv("CHILD_DEPLOYER_ADDR"); + let deployerFund = requireEnv("CHILD_DEPLOYER_FUND"); // Get admin EOA address const childProvider = new ethers.providers.JsonRpcProvider(childRPCURL, Number(childChainID)); let adminWallet; if (adminEOASecret == "ledger") { - adminWallet = new LedgerSigner(childProvider); + let index = requireEnv("CHILD_ADMIN_EOA_LEDGER_INDEX"); + const derivationPath = `m/44'/60'/${parseInt(index)}'/0/0`; + adminWallet = new LedgerSigner(childProvider, derivationPath); } else { adminWallet = new ethers.Wallet(adminEOASecret, childProvider); } @@ -29,7 +31,7 @@ async function run() { console.log("Admin address is: ", adminAddr); // Check duplicates - if (helper.hasDuplicates([adminAddr, axelarEOA, deployerEOA])) { + if (hasDuplicates([adminAddr, axelarEOA, deployerEOA])) { throw("Duplicate address detected!"); } @@ -37,9 +39,9 @@ async function run() { console.log("Axelar EOA now has: ", ethers.utils.formatEther(await childProvider.getBalance(axelarEOA))); console.log("Deployer EOA now has: ", ethers.utils.formatEther(await childProvider.getBalance(deployerEOA))); console.log("Fund Axelar and deployer on child chain in..."); - await helper.waitForConfirmation(); + await waitForConfirmation(); - let [priorityFee, maxFee] = await helper.getFee(adminWallet); + let [priorityFee, maxFee] = await getFee(childProvider); console.log("Transfer value to axelar..."); let resp = await adminWallet.sendTransaction({ to: axelarEOA, @@ -48,9 +50,9 @@ async function run() { maxFeePerGas: maxFee, }) console.log("Transaction submitted: " + JSON.stringify(resp, null, 2)); - await helper.waitForReceipt(resp.hash, childProvider); + await waitForReceipt(resp.hash, childProvider); - [priorityFee, maxFee] = await helper.getFee(adminWallet); + [priorityFee, maxFee] = await getFee(childProvider); console.log("Transfer value to deployer..."); resp = await adminWallet.sendTransaction({ to: deployerEOA, @@ -59,7 +61,7 @@ async function run() { maxFeePerGas: maxFee, }) console.log("Transaction submitted: " + JSON.stringify(resp, null, 2)); - await helper.waitForReceipt(resp.hash, childProvider); + await waitForReceipt(resp.hash, childProvider); // Print target balance console.log("Axelar EOA now has: ", ethers.utils.formatEther(await childProvider.getBalance(axelarEOA))); diff --git a/scripts/bootstrap/2_deployment_validation.js b/scripts/bootstrap/2_deployment_validation.js deleted file mode 100644 index ea763836..00000000 --- a/scripts/bootstrap/2_deployment_validation.js +++ /dev/null @@ -1,60 +0,0 @@ -// Deployment validation -'use strict'; -require('dotenv').config(); -const { ethers } = require("ethers"); -const helper = require("../helpers/helpers.js"); - -async function run() { - console.log("=======Start Deployment Validation======="); - - // Check environment variables - let childRPCURL = helper.requireEnv("CHILD_RPC_URL"); - let childChainID = helper.requireEnv("CHILD_CHAIN_ID"); - let rootRPCURL = helper.requireEnv("ROOT_RPC_URL"); - let rootChainID = helper.requireEnv("ROOT_CHAIN_ID"); - let childGatewayAddr = helper.requireEnv("CHILD_GATEWAY_ADDRESS"); - let childGasServiceAddr = helper.requireEnv("CHILD_GAS_SERVICE_ADDRESS"); - let multisigAddr = helper.requireEnv("MULTISIG_CONTRACT_ADDRESS"); - let rootGatewayAddr = helper.requireEnv("ROOT_GATEWAY_ADDRESS"); - let rootGasService = helper.requireEnv("ROOT_GAS_SERVICE_ADDRESS"); - - // Check duplicates - if (helper.hasDuplicates([childGatewayAddr, childGasServiceAddr, multisigAddr])) { - throw("Duplicate address detected!"); - } - if (helper.hasDuplicates([rootGatewayAddr, rootGasService])) { - throw("Duplicate address detected!"); - } - - const childProvider = new ethers.providers.JsonRpcProvider(childRPCURL, Number(childChainID)); - const rootProvider = new ethers.providers.JsonRpcProvider(rootRPCURL, Number(rootChainID)); - - // Check child chain. - console.log("Check contracts on child chain..."); - console.log("Check gateway contract...") - await helper.requireNonEmptyCode(childProvider, childGatewayAddr); - console.log("Succeed."); - console.log("Check gas service contract...") - await helper.requireNonEmptyCode(childProvider, childGasServiceAddr); - console.log("Succeed."); - if (process.env["SKIP_MULTISIG_CHECK"] != null) { - console.log("Skip multisig contract check..."); - } else { - console.log("Check multisig contract..."); - await helper.requireNonEmptyCode(childProvider, multisigAddr); - console.log("Succeed."); - } - - // Check root chain. - console.log("Check contracts on root chain..."); - console.log("Check gateway contract..."); - await helper.requireNonEmptyCode(rootProvider, rootGatewayAddr); - console.log("Succeed."); - console.log("Check gas service contract..."); - await helper.requireNonEmptyCode(rootProvider, rootGasService); - console.log("Succeed."); - - console.log("=======End Deployment Validation======="); -} - -run(); \ No newline at end of file diff --git a/scripts/bootstrap/2_deployment_validation.ts b/scripts/bootstrap/2_deployment_validation.ts new file mode 100644 index 00000000..b6b2835b --- /dev/null +++ b/scripts/bootstrap/2_deployment_validation.ts @@ -0,0 +1,62 @@ +// Deployment validation +import * as dotenv from "dotenv"; +dotenv.config(); +import { ethers } from "ethers"; +import { requireEnv, hasDuplicates, requireNonEmptyCode } from "../helpers/helpers"; +import { LedgerSigner } from "../helpers/ledger_signer"; +import * as fs from "fs"; + +async function run() { + console.log("=======Start Deployment Validation======="); + + // Check environment variables + let childRPCURL = requireEnv("CHILD_RPC_URL"); + let childChainID = requireEnv("CHILD_CHAIN_ID"); + let rootRPCURL = requireEnv("ROOT_RPC_URL"); + let rootChainID = requireEnv("ROOT_CHAIN_ID"); + let childGatewayAddr = requireEnv("CHILD_GATEWAY_ADDRESS"); + let childGasServiceAddr = requireEnv("CHILD_GAS_SERVICE_ADDRESS"); + let multisigAddr = requireEnv("MULTISIG_CONTRACT_ADDRESS"); + let rootGatewayAddr = requireEnv("ROOT_GATEWAY_ADDRESS"); + let rootGasService = requireEnv("ROOT_GAS_SERVICE_ADDRESS"); + + // Check duplicates + if (hasDuplicates([childGatewayAddr, childGasServiceAddr, multisigAddr])) { + throw("Duplicate address detected!"); + } + if (hasDuplicates([rootGatewayAddr, rootGasService])) { + throw("Duplicate address detected!"); + } + + const childProvider = new ethers.providers.JsonRpcProvider(childRPCURL, Number(childChainID)); + const rootProvider = new ethers.providers.JsonRpcProvider(rootRPCURL, Number(rootChainID)); + + // Check child chain. + console.log("Check contracts on child chain..."); + console.log("Check gateway contract...") + await requireNonEmptyCode(childProvider, childGatewayAddr); + console.log("Succeed."); + console.log("Check gas service contract...") + await requireNonEmptyCode(childProvider, childGasServiceAddr); + console.log("Succeed."); + if (process.env["SKIP_MULTISIG_CHECK"] != null) { + console.log("Skip multisig contract check..."); + } else { + console.log("Check multisig contract..."); + await requireNonEmptyCode(childProvider, multisigAddr); + console.log("Succeed."); + } + + // Check root chain. + console.log("Check contracts on root chain..."); + console.log("Check gateway contract..."); + await requireNonEmptyCode(rootProvider, rootGatewayAddr); + console.log("Succeed."); + console.log("Check gas service contract..."); + await requireNonEmptyCode(rootProvider, rootGasService); + console.log("Succeed."); + + console.log("=======End Deployment Validation======="); +} + +run(); \ No newline at end of file diff --git a/scripts/bootstrap/3_child_deployment.js b/scripts/bootstrap/3_child_deployment.ts similarity index 55% rename from scripts/bootstrap/3_child_deployment.js rename to scripts/bootstrap/3_child_deployment.ts index 509ff107..37af73e2 100644 --- a/scripts/bootstrap/3_child_deployment.js +++ b/scripts/bootstrap/3_child_deployment.ts @@ -1,12 +1,10 @@ // Deploy child contracts -'use strict'; -require('dotenv').config(); -const deploy = require("../deploy/child_deployment.js"); +import { deployChildContracts } from "../deploy/child_deployment"; async function run() { console.log("=======Start Child Deployment======="); - await deploy.deployChildContracts(); + await deployChildContracts(); console.log("=======End Child Deployment======="); } diff --git a/scripts/bootstrap/4_root_deployment.js b/scripts/bootstrap/4_root_deployment.ts similarity index 55% rename from scripts/bootstrap/4_root_deployment.js rename to scripts/bootstrap/4_root_deployment.ts index 48ea2d4f..546aaf11 100644 --- a/scripts/bootstrap/4_root_deployment.js +++ b/scripts/bootstrap/4_root_deployment.ts @@ -1,12 +1,10 @@ // Deploy root contracts -'use strict'; -require('dotenv').config(); -const deploy = require("../deploy/root_deployment.js"); +import { deployRootContracts } from "../deploy/root_deployment"; async function run() { console.log("=======Start Root Deployment======="); - await deploy.deployRootContracts(); + await deployRootContracts(); console.log("=======End Root Deployment======="); } diff --git a/scripts/bootstrap/5_child_initialisation.js b/scripts/bootstrap/5_child_initialisation.ts similarity index 56% rename from scripts/bootstrap/5_child_initialisation.js rename to scripts/bootstrap/5_child_initialisation.ts index 09fc59e6..47fa9c60 100644 --- a/scripts/bootstrap/5_child_initialisation.js +++ b/scripts/bootstrap/5_child_initialisation.ts @@ -1,12 +1,10 @@ // Initialise child contracts -'use strict'; -require('dotenv').config(); -const init = require("../deploy/child_initialisation.js"); +import { initialiseChildContracts } from "../deploy/child_initialisation"; async function run() { console.log("=======Start Child Initialisation======="); - await init.initialiseChildContracts(); + await initialiseChildContracts(); console.log("=======End Child Initialisation======="); } diff --git a/scripts/bootstrap/6_imx_burning.js b/scripts/bootstrap/6_imx_burning.ts similarity index 74% rename from scripts/bootstrap/6_imx_burning.js rename to scripts/bootstrap/6_imx_burning.ts index 57e0bfb0..d67ddbd4 100644 --- a/scripts/bootstrap/6_imx_burning.js +++ b/scripts/bootstrap/6_imx_burning.ts @@ -1,20 +1,20 @@ // IMX burning -'use strict'; -require('dotenv').config(); -const { ethers } = require("ethers"); -const helper = require("../helpers/helpers.js"); -const { LedgerSigner } = require('@ethersproject/hardware-wallets') -const fs = require('fs'); +import * as dotenv from "dotenv"; +dotenv.config(); +import { ethers } from "ethers"; +import { requireEnv, waitForConfirmation, hasDuplicates, waitForReceipt, getFee } from "../helpers/helpers"; +import { LedgerSigner } from "../helpers/ledger_signer"; +import * as fs from "fs"; async function run() { console.log("=======Start IMX Burning======="); // Check environment variables - let childRPCURL = helper.requireEnv("CHILD_RPC_URL"); - let childChainID = helper.requireEnv("CHILD_CHAIN_ID"); - let adminEOASecret = helper.requireEnv("CHILD_ADMIN_EOA_SECRET"); - let multisigAddr = helper.requireEnv("MULTISIG_CONTRACT_ADDRESS"); - let imxDepositLimit = helper.requireEnv("IMX_DEPOSIT_LIMIT"); + let childRPCURL = requireEnv("CHILD_RPC_URL"); + let childChainID = requireEnv("CHILD_CHAIN_ID"); + let adminEOASecret = requireEnv("CHILD_ADMIN_EOA_SECRET"); + let multisigAddr = requireEnv("MULTISIG_CONTRACT_ADDRESS"); + let imxDepositLimit = requireEnv("IMX_DEPOSIT_LIMIT"); // Read from contract file. let data = fs.readFileSync(".child.bridge.contracts.json", 'utf-8'); @@ -25,7 +25,9 @@ async function run() { const childProvider = new ethers.providers.JsonRpcProvider(childRPCURL, Number(childChainID)); let adminWallet; if (adminEOASecret == "ledger") { - adminWallet = new LedgerSigner(childProvider); + let index = requireEnv("CHILD_ADMIN_EOA_LEDGER_INDEX"); + const derivationPath = `m/44'/60'/${parseInt(index)}'/0/0`; + adminWallet = new LedgerSigner(childProvider, derivationPath); } else { adminWallet = new ethers.Wallet(adminEOASecret, childProvider); } @@ -33,7 +35,7 @@ async function run() { console.log("Admin address is: ", adminAddr); // Check duplicates - if (helper.hasDuplicates([adminAddr, childBridgeAddr, multisigAddr])) { + if (hasDuplicates([adminAddr, childBridgeAddr, multisigAddr])) { throw("Duplicate address detected!"); } @@ -51,20 +53,20 @@ async function run() { } console.log("Burn IMX in..."); - await helper.waitForConfirmation(); + await waitForConfirmation(); let childBridgeObj = JSON.parse(fs.readFileSync('../../out/ChildERC20Bridge.sol/ChildERC20Bridge.json', 'utf8')); let childBridge = new ethers.Contract(childBridgeAddr, childBridgeObj.abi, childProvider); console.log("Transfer " + imxDepositLimit + " IMX to child bridge..."); - let [priorityFee, maxFee] = await helper.getFee(adminWallet); + let [priorityFee, maxFee] = await getFee(childProvider); let resp = await childBridge.connect(adminWallet).privilegedDeposit({ value: ethers.utils.parseEther(imxDepositLimit), maxPriorityFeePerGas: priorityFee, maxFeePerGas: maxFee, }) console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)) - await helper.waitForReceipt(resp.hash, childProvider); + await waitForReceipt(resp.hash, childProvider); adminBal = await childProvider.getBalance(adminAddr); bridgeBal = await childProvider.getBalance(childBridgeAddr); @@ -75,7 +77,7 @@ async function run() { // Transfer to multisig console.log("Transfer remaining to multisig..."); - [priorityFee, maxFee] = await helper.getFee(adminWallet); + [priorityFee, maxFee] = await getFee(childProvider); resp = await adminWallet.sendTransaction({ to: multisigAddr, value: adminBal.sub(ethers.utils.parseEther("0.01")), @@ -83,7 +85,7 @@ async function run() { maxFeePerGas: maxFee, }); console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)) - await helper.waitForReceipt(resp.hash, childProvider); + await waitForReceipt(resp.hash, childProvider); adminBal = await childProvider.getBalance(adminAddr); bridgeBal = await childProvider.getBalance(childBridgeAddr); diff --git a/scripts/bootstrap/7_imx_rebalancing.js b/scripts/bootstrap/7_imx_rebalancing.ts similarity index 88% rename from scripts/bootstrap/7_imx_rebalancing.js rename to scripts/bootstrap/7_imx_rebalancing.ts index e2bf463c..01f7d805 100644 --- a/scripts/bootstrap/7_imx_rebalancing.js +++ b/scripts/bootstrap/7_imx_rebalancing.ts @@ -1,10 +1,10 @@ // IMX rebalancing -'use strict'; -require('dotenv').config(); -const { ethers } = require("ethers"); -const helper = require("../helpers/helpers.js"); -const { LedgerSigner } = require('@ethersproject/hardware-wallets') -const fs = require('fs'); +import * as dotenv from "dotenv"; +dotenv.config(); +import { ethers } from "ethers"; +import { requireEnv, waitForConfirmation, waitForReceipt, getFee, hasDuplicates } from "../helpers/helpers"; +import { LedgerSigner } from "../helpers/ledger_signer"; +import * as fs from "fs"; // The total supply of IMX const TOTAL_SUPPLY = "2000000000"; @@ -16,13 +16,13 @@ async function run() { console.log("=======Start IMX Rebalancing======="); // Check environment variables - let childRPCURL = helper.requireEnv("CHILD_RPC_URL"); - let childChainID = helper.requireEnv("CHILD_CHAIN_ID"); - let rootRPCURL = helper.requireEnv("ROOT_RPC_URL"); - let rootChainID = helper.requireEnv("ROOT_CHAIN_ID"); - let rootDeployerSecret = helper.requireEnv("ROOT_DEPLOYER_SECRET"); - let multisigAddr = helper.requireEnv("MULTISIG_CONTRACT_ADDRESS"); - let rootIMXAddr = helper.requireEnv("ROOT_IMX_ADDR"); + let childRPCURL = requireEnv("CHILD_RPC_URL"); + let childChainID = requireEnv("CHILD_CHAIN_ID"); + let rootRPCURL = requireEnv("ROOT_RPC_URL"); + let rootChainID = requireEnv("ROOT_CHAIN_ID"); + let rootDeployerSecret = requireEnv("ROOT_DEPLOYER_SECRET"); + let multisigAddr = requireEnv("MULTISIG_CONTRACT_ADDRESS"); + let rootIMXAddr = requireEnv("ROOT_IMX_ADDR"); // Read from contract file. let data = fs.readFileSync(".child.bridge.contracts.json", 'utf-8'); @@ -37,7 +37,9 @@ async function run() { const rootProvider = new ethers.providers.JsonRpcProvider(rootRPCURL, Number(rootChainID)); let adminWallet; if (rootDeployerSecret == "ledger") { - adminWallet = new LedgerSigner(rootProvider); + let index = requireEnv("ROOT_DEPLOYER_LEDGER_INDEX"); + const derivationPath = `m/44'/60'/${parseInt(index)}'/0/0`; + adminWallet = new LedgerSigner(rootProvider, derivationPath); } else { adminWallet = new ethers.Wallet(rootDeployerSecret, rootProvider); } @@ -45,10 +47,10 @@ async function run() { console.log("Deployer address is: ", adminAddr); // Check duplicates - if (helper.hasDuplicates([adminAddr, childBridgeAddr, multisigAddr])) { + if (hasDuplicates([adminAddr, childBridgeAddr, multisigAddr])) { throw("Duplicate address detected!"); } - if (helper.hasDuplicates([adminAddr, rootBridgeAddr, rootIMXAddr])) { + if (hasDuplicates([adminAddr, rootBridgeAddr, rootIMXAddr])) { throw("Duplicate address detected!"); } @@ -72,13 +74,13 @@ async function run() { } console.log("Rebalance in..."); - await helper.waitForConfirmation(); + await waitForConfirmation(); // Rebalancing console.log("Transfer...") let resp = await IMX.connect(adminWallet).transfer(rootBridgeAddr, balanceAmt); console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)) - await helper.waitForReceipt(resp.hash, rootProvider); + await waitForReceipt(resp.hash, rootProvider); adminL1Balance = await IMX.balanceOf(adminAddr); rootBridgeBalance = await IMX.balanceOf(rootBridgeAddr); diff --git a/scripts/bootstrap/8_root_initialisation.js b/scripts/bootstrap/8_root_initialisation.ts similarity index 56% rename from scripts/bootstrap/8_root_initialisation.js rename to scripts/bootstrap/8_root_initialisation.ts index a193b67c..3693345b 100644 --- a/scripts/bootstrap/8_root_initialisation.js +++ b/scripts/bootstrap/8_root_initialisation.ts @@ -1,12 +1,10 @@ // Initialise root contracts -'use strict'; -require('dotenv').config(); -const init = require("../deploy/root_initialisation.js"); +import { initialiseRootContracts } from "../deploy/root_initialisation"; async function run() { console.log("=======Start Root Initialisation======="); - await init.initialiseRootContracts(); + await initialiseRootContracts(); console.log("=======End Root Initialisation======="); } diff --git a/scripts/bootstrap/README.md b/scripts/bootstrap/README.md index e5ca044e..2dda4bc7 100644 --- a/scripts/bootstrap/README.md +++ b/scripts/bootstrap/README.md @@ -34,18 +34,26 @@ ROOT_CHAIN_ID= CHILD_ADMIN_ADDR= ## The private key for the admin EOA or "ledger" if using hardware wallet. CHILD_ADMIN_EOA_SECRET= +## The ledger index for the admin EOA, required if using ledger. +CHILD_ADMIN_EOA_LEDGER_INDEX= ## The deployer address on child chain. CHILD_DEPLOYER_ADDR= ## The private key for the deployer on child chain or "ledger" if using hardware wallet. CHILD_DEPLOYER_SECRET= +## The ledger index for the deployer on child chain, required if using ledger. +CHILD_DEPLOYER_LEDGER_INDEX= ## The amount of fund deployer required on L2, unit is in IMX or 10^18 Wei. CHILD_DEPLOYER_FUND= ## The deployer address on root chain. ROOT_DEPLOYER_ADDR= ## The private key for the deployer on root chain or "ledger" if using hardware wallet. ROOT_DEPLOYER_SECRET= +## The ledger index for the deployer on root chain, required if using ledger. +ROOT_DEPLOYER_LEDGER_INDEX= ## The private key for rate admin or "ledger" if using hardware wallet. ROOT_BRIDGE_RATE_ADMIN_SECRET= +## The ledger index for the rate admin, required if using ledger. +ROOT_BRIDGE_RATE_ADMIN_LEDGER_INDEX= ## The IMX token address on root chain. ROOT_IMX_ADDR= ## The Wrapped ETH token address on the root chain. @@ -141,7 +149,7 @@ RATE_LIMIT_GOG_LARGE_THRESHOLD= ``` 3. Fund deployer ``` -node 1_deployer_funding.js 2>&1 | tee bootstrap.out +npx ts-node 1_deployer_funding.ts 2>&1 | tee bootstrap.out ``` 4. Wait for Axelar to deploy & setup their system and Security team to deploy & setup multisig wallet. 5. Set the following environment variables @@ -156,32 +164,32 @@ ROOT_GAS_SERVICE_ADDRESS= If multisig is deployed: ``` -node 2_deployment_validation.js 2>&1 | tee -a bootstrap.out +npx ts-node 2_deployment_validation.ts 2>&1 | tee -a bootstrap.out ``` If multisig isn't deployed: ``` -SKIP_MULTISIG_CHECK=true node 2_deployment_validation.js 2>&1 | tee -a bootstrap.out +SKIP_MULTISIG_CHECK=true npx ts-node 2_deployment_validation.ts 2>&1 | tee -a bootstrap.out ``` 7. Deploy bridge contracts on child and root chain. ``` -node 3_child_deployment.js 2>&1 | tee -a bootstrap.out -node 4_root_deployment.js 2>&1 | tee -a bootstrap.out +npx ts-node 3_child_deployment.ts 2>&1 | tee -a bootstrap.out +npx ts-node 4_root_deployment.ts 2>&1 | tee -a bootstrap.out ``` 8. Initialise bridge contracts on child chain. ``` -node 5_child_initialisation.js 2>&1 | tee -a bootstrap.out +npx ts-node 5_child_initialisation.ts 2>&1 | tee -a bootstrap.out ``` 9. IMX Burning ``` -node 6_imx_burning.js 2>&1 | tee -a bootstrap.out +npx ts-node 6_imx_burning.ts 2>&1 | tee -a bootstrap.out ``` 10. IMX Rebalancing ``` -node 7_imx_rebalancing.js 2>&1 | tee -a bootstrap.out +npx ts-node 7_imx_rebalancing.ts 2>&1 | tee -a bootstrap.out ``` 11. Initialise bridge contracts on root chain. ``` -node 8_root_initialisation.js 2>&1 | tee -a bootstrap.out +npx ts-node 8_root_initialisation.ts 2>&1 | tee -a bootstrap.out ``` 12. Set the following environment variable ``` diff --git a/scripts/deploy/.env.example b/scripts/deploy/.env.example index dd94dd60..0fcc7293 100644 --- a/scripts/deploy/.env.example +++ b/scripts/deploy/.env.example @@ -11,10 +11,16 @@ CHILD_ADMIN_ADDR= MULTISIG_CONTRACT_ADDRESS= ## The private key for the deployer on child chain or "ledger" if using hardware wallet. CHILD_DEPLOYER_SECRET= +## The ledger index for the deployer on child chain, required if using ledger. +CHILD_DEPLOYER_LEDGER_INDEX= ## The private key for the deployer on root chain or "ledger" if using hardware wallet. ROOT_DEPLOYER_SECRET= +## The ledger index for the deployer on root chain, required if using ledger. +ROOT_DEPLOYER_LEDGER_INDEX= ## The private key for rate admin or "ledger" if using hardware wallet. ROOT_BRIDGE_RATE_ADMIN_SECRET= +## The ledger index for the rate admin, required if using ledger. +ROOT_BRIDGE_RATE_ADMIN_LEDGER_INDEX= ## The IMX token address on root chain. ROOT_IMX_ADDR= ## The Wrapped ETH token address on the root chain. diff --git a/scripts/deploy/README.md b/scripts/deploy/README.md index 6b1e2d8e..0251cc9f 100644 --- a/scripts/deploy/README.md +++ b/scripts/deploy/README.md @@ -24,10 +24,16 @@ CHILD_ADMIN_ADDR= MULTISIG_CONTRACT_ADDRESS= ## The private key for the deployer on child chain or "ledger" if using hardware wallet. CHILD_DEPLOYER_SECRET= +## The ledger index for the deployer on child chain, required if using ledger. +CHILD_DEPLOYER_LEDGER_INDEX= ## The private key for the deployer on root chain or "ledger" if using hardware wallet. ROOT_DEPLOYER_SECRET= +## The ledger index for the deployer on root chain, required if using ledger. +ROOT_DEPLOYER_LEDGER_INDEX= ## The private key for rate admin or "ledger" if using hardware wallet. ROOT_BRIDGE_RATE_ADMIN_SECRET= +## The ledger index for the rate admin, required if using ledger. +ROOT_BRIDGE_RATE_ADMIN_LEDGER_INDEX= ## The IMX token address on root chain. ROOT_IMX_ADDR= ## The Wrapped ETH token address on the root chain. @@ -128,5 +134,5 @@ ROOT_GAS_SERVICE_ADDRESS= 3. Deploy and setup contracts: ``` -node deployAndInit.js +npx ts-node deployAndInit.ts ``` \ No newline at end of file diff --git a/scripts/deploy/child_deployment.js b/scripts/deploy/child_deployment.ts similarity index 52% rename from scripts/deploy/child_deployment.js rename to scripts/deploy/child_deployment.ts index 45f47ad4..2ac569a0 100644 --- a/scripts/deploy/child_deployment.js +++ b/scripts/deploy/child_deployment.ts @@ -1,24 +1,26 @@ // Deploy child contracts -'use strict'; -require('dotenv').config(); -const { ethers } = require("ethers"); -const helper = require("../helpers/helpers.js"); -const { LedgerSigner } = require('@ethersproject/hardware-wallets') -const fs = require('fs'); +import * as dotenv from "dotenv"; +dotenv.config(); +import { ethers } from "ethers"; +import { requireEnv, waitForConfirmation, deployChildContract, waitForReceipt, getFee } from "../helpers/helpers"; +import { LedgerSigner } from "../helpers/ledger_signer"; +import * as fs from "fs"; -exports.deployChildContracts = async () => { +export async function deployChildContracts() { // Check environment variables - let childRPCURL = helper.requireEnv("CHILD_RPC_URL"); - let childChainID = helper.requireEnv("CHILD_CHAIN_ID"); - let childDeployerSecret = helper.requireEnv("CHILD_DEPLOYER_SECRET"); - let childGatewayAddr = helper.requireEnv("CHILD_GATEWAY_ADDRESS"); - let childProxyAdmin = helper.requireEnv("CHILD_PROXY_ADMIN"); + let childRPCURL = requireEnv("CHILD_RPC_URL"); + let childChainID = requireEnv("CHILD_CHAIN_ID"); + let childDeployerSecret = requireEnv("CHILD_DEPLOYER_SECRET"); + let childGatewayAddr = requireEnv("CHILD_GATEWAY_ADDRESS"); + let childProxyAdmin = requireEnv("CHILD_PROXY_ADMIN"); // Get admin address const childProvider = new ethers.providers.JsonRpcProvider(childRPCURL, Number(childChainID)); let adminWallet; if (childDeployerSecret == "ledger") { - adminWallet = new LedgerSigner(childProvider); + let index = requireEnv("CHILD_DEPLOYER_LEDGER_INDEX"); + const derivationPath = `m/44'/60'/${parseInt(index)}'/0/0`; + adminWallet = new LedgerSigner(childProvider, derivationPath); } else { adminWallet = new ethers.Wallet(childDeployerSecret, childProvider); } @@ -27,87 +29,85 @@ exports.deployChildContracts = async () => { // Execute console.log("Deploy child contracts in..."); - await helper.waitForConfirmation(); + await waitForConfirmation(); // Deploy child token template - let childTokenTemplateObj = JSON.parse(fs.readFileSync('../../out/ChildERC20.sol/ChildERC20.json', 'utf8')); console.log("Deploy child token template..."); - let childTokenTemplate = await helper.deployChildContract(childTokenTemplateObj, adminWallet); + let childTokenTemplate = await deployChildContract("ChildERC20", adminWallet); console.log("Transaction submitted: ", JSON.stringify(childTokenTemplate.deployTransaction, null, 2)); - await helper.waitForReceipt(childTokenTemplate.deployTransaction.hash, childProvider); + await waitForReceipt(childTokenTemplate.deployTransaction.hash, childProvider); // Initialise template console.log("Initialise child token template..."); - let [priorityFee, maxFee] = await helper.getFee(adminWallet); + let [priorityFee, maxFee] = await getFee(childProvider); let resp = await childTokenTemplate.connect(adminWallet).initialize("000000000000000000000000000000000000007B", "TEMPLATE", "TPT", 18, { maxPriorityFeePerGas: priorityFee, maxFeePerGas: maxFee, }); console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); - await helper.waitForReceipt(resp.hash, childProvider); + await waitForReceipt(resp.hash, childProvider); console.log("Deployed to CHILD_TOKEN_TEMPLATE: ", childTokenTemplate.address); // Deploy wrapped IMX - let wrappedIMXObj = JSON.parse(fs.readFileSync('../../out/WIMX.sol/WIMX.json', 'utf8')); console.log("Deploy wrapped IMX..."); - let wrappedIMX = await helper.deployChildContract(wrappedIMXObj, adminWallet); + let wrappedIMX = await deployChildContract("WIMX", adminWallet); console.log("Transaction submitted: ", JSON.stringify(wrappedIMX.deployTransaction, null, 2)); - await helper.waitForReceipt(wrappedIMX.deployTransaction.hash, childProvider); + await waitForReceipt(wrappedIMX.deployTransaction.hash, childProvider); console.log("Deployed to WRAPPED_IMX_ADDRESS: ", wrappedIMX.address); // Deploy proxy admin - let proxyAdminObj = JSON.parse(fs.readFileSync('../../out/ProxyAdmin.sol/ProxyAdmin.json', 'utf8')); console.log("Deploy proxy admin..."); - let proxyAdmin = await helper.deployChildContract(proxyAdminObj, adminWallet); + let proxyAdmin = await deployChildContract("ProxyAdmin", adminWallet); console.log("Transaction submitted: ", JSON.stringify(proxyAdmin.deployTransaction, null, 2)); - await helper.waitForReceipt(proxyAdmin.deployTransaction.hash, childProvider); + await waitForReceipt(proxyAdmin.deployTransaction.hash, childProvider); // Change owner console.log("Change ownership..."); - [priorityFee, maxFee] = await helper.getFee(adminWallet); + [priorityFee, maxFee] = await getFee(childProvider); resp = await proxyAdmin.connect(adminWallet).transferOwnership(childProxyAdmin, { maxPriorityFeePerGas: priorityFee, maxFeePerGas: maxFee, }); console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); - await helper.waitForReceipt(resp.hash, childProvider); + await waitForReceipt(resp.hash, childProvider); console.log("Deployed to CHILD_PROXY_ADMIN: ", proxyAdmin.address); // Deploy child bridge impl - let childBridgeImplObj = JSON.parse(fs.readFileSync('../../out/ChildERC20Bridge.sol/ChildERC20Bridge.json', 'utf8')); console.log("Deploy child bridge impl..."); - let childBridgeImpl = await helper.deployChildContract(childBridgeImplObj, adminWallet); + let childBridgeImpl = await deployChildContract("ChildERC20Bridge", adminWallet); console.log("Transaction submitted: ", JSON.stringify(childBridgeImpl.deployTransaction, null, 2)); - await helper.waitForReceipt(childBridgeImpl.deployTransaction.hash, childProvider); + await waitForReceipt(childBridgeImpl.deployTransaction.hash, childProvider); console.log("Deployed to CHILD_BRIDGE_IMPL_ADDRESS: ", childBridgeImpl.address); // Deploy child bridge proxy - let childBridgeProxyObj = JSON.parse(fs.readFileSync('../../out/TransparentUpgradeableProxy.sol/TransparentUpgradeableProxy.json', 'utf8')); console.log("Deploy child bridge proxy..."); - let childBridgeProxy = await helper.deployChildContract(childBridgeProxyObj, adminWallet, childBridgeImpl.address, proxyAdmin.address, []); + let childBridgeProxy = await deployChildContract("TransparentUpgradeableProxy", adminWallet, childBridgeImpl.address, proxyAdmin.address, []); console.log("Transaction submitted: ", JSON.stringify(childBridgeProxy.deployTransaction, null, 2)); - await helper.waitForReceipt(childBridgeProxy.deployTransaction.hash, childProvider); + await waitForReceipt(childBridgeProxy.deployTransaction.hash, childProvider); console.log("Deployed to CHILD_BRIDGE_PROXY_ADDRESS: ", childBridgeProxy.address); // Deploy child adaptor impl - let childAdaptorImplObj = JSON.parse(fs.readFileSync('../../out/ChildAxelarBridgeAdaptor.sol/ChildAxelarBridgeAdaptor.json', 'utf8')); console.log("Deploy child adaptor impl..."); - let childAdaptorImpl = await helper.deployChildContract(childAdaptorImplObj, adminWallet, childGatewayAddr); + let childAdaptorImpl = await deployChildContract("ChildAxelarBridgeAdaptor", adminWallet, childGatewayAddr); console.log("Transaction submitted: ", JSON.stringify(childAdaptorImpl.deployTransaction, null, 2)); - await helper.waitForReceipt(childAdaptorImpl.deployTransaction.hash, childProvider); + await waitForReceipt(childAdaptorImpl.deployTransaction.hash, childProvider); console.log("Deployed to CHILD_ADAPTOR_IMPL_ADDRESS: ", childAdaptorImpl.address); // Deploy child adaptor proxy - let childAdaptorProxyObj = JSON.parse(fs.readFileSync('../../out/TransparentUpgradeableProxy.sol/TransparentUpgradeableProxy.json', 'utf8')); console.log("Deploy child adaptor proxy..."); - let childAdaptorProxy = await helper.deployChildContract(childAdaptorProxyObj, adminWallet, childAdaptorImpl.address, proxyAdmin.address, []); + let childAdaptorProxy = await deployChildContract("TransparentUpgradeableProxy", adminWallet, childAdaptorImpl.address, proxyAdmin.address, []); console.log("Transaction submitted: ", JSON.stringify(childAdaptorProxy.deployTransaction, null, 2)); - await helper.waitForReceipt(childAdaptorProxy.deployTransaction.hash, childProvider); + await waitForReceipt(childAdaptorProxy.deployTransaction.hash, childProvider); console.log("Deployed to CHILD_ADAPTOR_PROXY_ADDRESS: ", childAdaptorProxy.address); let contractData = { + CHILD_PROXY_ADMIN: proxyAdmin.address, + CHILD_BRIDGE_IMPL_ADDRESS: childBridgeImpl.address, + CHILD_BRIDGE_PROXY_ADDRESS: childBridgeProxy.address, CHILD_BRIDGE_ADDRESS: childBridgeProxy.address, + CHILD_ADAPTOR_IMPL_ADDRESS: childAdaptorImpl.address, + CHILD_ADAPTOR_PROXY_ADDRESS: childAdaptorProxy.address, CHILD_ADAPTOR_ADDRESS: childAdaptorProxy.address, - WRAPPED_IMX_ADDRESS: wrappedIMX.address, CHILD_TOKEN_TEMPLATE: childTokenTemplate.address, + WRAPPED_IMX_ADDRESS: wrappedIMX.address, }; fs.writeFileSync(".child.bridge.contracts.json", JSON.stringify(contractData, null, 2)); } \ No newline at end of file diff --git a/scripts/deploy/child_initialisation.js b/scripts/deploy/child_initialisation.ts similarity index 52% rename from scripts/deploy/child_initialisation.js rename to scripts/deploy/child_initialisation.ts index 2aed4ce7..996e50a7 100644 --- a/scripts/deploy/child_initialisation.js +++ b/scripts/deploy/child_initialisation.ts @@ -1,28 +1,29 @@ // Initialise child contracts 'use strict'; -require('dotenv').config(); -const { ethers } = require("ethers"); -const helper = require("../helpers/helpers.js"); -const { LedgerSigner } = require('@ethersproject/hardware-wallets') -const fs = require('fs'); +import * as dotenv from "dotenv"; +dotenv.config(); +import { ethers } from "ethers"; +import { requireEnv, waitForConfirmation, waitForReceipt, getFee, getContract } from "../helpers/helpers"; +import { LedgerSigner } from "../helpers/ledger_signer"; +import * as fs from "fs"; -exports.initialiseChildContracts = async () => { - let rootChainName = helper.requireEnv("ROOT_CHAIN_NAME"); - let childRPCURL = helper.requireEnv("CHILD_RPC_URL"); - let childChainID = helper.requireEnv("CHILD_CHAIN_ID"); - let adminEOAAddr = helper.requireEnv("CHILD_ADMIN_ADDR"); - let childBridgeDefaultAdmin = helper.requireEnv("CHILD_BRIDGE_DEFAULT_ADMIN"); - let childBridgePauser = helper.requireEnv("CHILD_BRIDGE_PAUSER"); - let childBridgeUnpauser = helper.requireEnv("CHILD_BRIDGE_UNPAUSER"); - let childBridgeAdaptorManager = helper.requireEnv("CHILD_BRIDGE_ADAPTOR_MANAGER"); - let childAdaptorDefaultAdmin = helper.requireEnv("CHILD_ADAPTOR_DEFAULT_ADMIN"); - let childAdaptorBridgeManager = helper.requireEnv("CHILD_ADAPTOR_BRIDGE_MANAGER"); - let childAdaptorGasServiceManager = helper.requireEnv("CHILD_ADAPTOR_GAS_SERVICE_MANAGER"); - let childAdaptorTargetManager = helper.requireEnv("CHILD_ADAPTOR_TARGET_MANAGER"); - let childDeployerSecret = helper.requireEnv("CHILD_DEPLOYER_SECRET"); - let childGasServiceAddr = helper.requireEnv("CHILD_GAS_SERVICE_ADDRESS"); - let multisigAddr = helper.requireEnv("MULTISIG_CONTRACT_ADDRESS"); - let rootIMXAddr = helper.requireEnv("ROOT_IMX_ADDR"); +export async function initialiseChildContracts() { + let rootChainName = requireEnv("ROOT_CHAIN_NAME"); + let childRPCURL = requireEnv("CHILD_RPC_URL"); + let childChainID = requireEnv("CHILD_CHAIN_ID"); + let adminEOAAddr = requireEnv("CHILD_ADMIN_ADDR"); + let childBridgeDefaultAdmin = requireEnv("CHILD_BRIDGE_DEFAULT_ADMIN"); + let childBridgePauser = requireEnv("CHILD_BRIDGE_PAUSER"); + let childBridgeUnpauser = requireEnv("CHILD_BRIDGE_UNPAUSER"); + let childBridgeAdaptorManager = requireEnv("CHILD_BRIDGE_ADAPTOR_MANAGER"); + let childAdaptorDefaultAdmin = requireEnv("CHILD_ADAPTOR_DEFAULT_ADMIN"); + let childAdaptorBridgeManager = requireEnv("CHILD_ADAPTOR_BRIDGE_MANAGER"); + let childAdaptorGasServiceManager = requireEnv("CHILD_ADAPTOR_GAS_SERVICE_MANAGER"); + let childAdaptorTargetManager = requireEnv("CHILD_ADAPTOR_TARGET_MANAGER"); + let childDeployerSecret = requireEnv("CHILD_DEPLOYER_SECRET"); + let childGasServiceAddr = requireEnv("CHILD_GAS_SERVICE_ADDRESS"); + let multisigAddr = requireEnv("MULTISIG_CONTRACT_ADDRESS"); + let rootIMXAddr = requireEnv("ROOT_IMX_ADDR"); // Read from contract file. let data = fs.readFileSync(".child.bridge.contracts.json", 'utf-8'); @@ -39,7 +40,9 @@ exports.initialiseChildContracts = async () => { const childProvider = new ethers.providers.JsonRpcProvider(childRPCURL, Number(childChainID)); let adminWallet; if (childDeployerSecret == "ledger") { - adminWallet = new LedgerSigner(childProvider); + let index = requireEnv("CHILD_DEPLOYER_LEDGER_INDEX"); + const derivationPath = `m/44'/60'/${parseInt(index)}'/0/0`; + adminWallet = new LedgerSigner(childProvider, derivationPath); } else { adminWallet = new ethers.Wallet(childDeployerSecret, childProvider); } @@ -48,13 +51,12 @@ exports.initialiseChildContracts = async () => { // Execute console.log("Initialise child contracts in..."); - await helper.waitForConfirmation(); + await waitForConfirmation(); // Initialise child bridge - let childBridgeObj = JSON.parse(fs.readFileSync('../../out/ChildERC20Bridge.sol/ChildERC20Bridge.json', 'utf8')); console.log("Initialise child bridge..."); - let childBridge = new ethers.Contract(childBridgeAddr, childBridgeObj.abi, childProvider); - let [priorityFee, maxFee] = await helper.getFee(adminWallet); + let childBridge = getContract("ChildERC20Bridge", childBridgeAddr, childProvider); + let [priorityFee, maxFee] = await getFee(childProvider); let resp = await childBridge.connect(adminWallet).initialize( { defaultAdmin: childBridgeDefaultAdmin, @@ -73,13 +75,13 @@ exports.initialiseChildContracts = async () => { maxFeePerGas: maxFee, }); console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); - await helper.waitForReceipt(resp.hash, childProvider); + await waitForReceipt(resp.hash, childProvider); // Initialise child adaptor - let childAdaptorObj = JSON.parse(fs.readFileSync('../../out/ChildAxelarBridgeAdaptor.sol/ChildAxelarBridgeAdaptor.json', 'utf8')); console.log("Initialise child adaptor..."); - let childAdaptor = new ethers.Contract(childAdaptorAddr, childAdaptorObj.abi, childProvider); - [priorityFee, maxFee] = await helper.getFee(adminWallet); + let childAdaptor = getContract("ChildAxelarBridgeAdaptor", childAdaptorAddr, childProvider); + // let childAdaptor = new ethers.Contract(childAdaptorAddr, childAdaptorObj.abi, childProvider); + [priorityFee, maxFee] = await getFee(childProvider); resp = await childAdaptor.connect(adminWallet).initialize( { defaultAdmin: childAdaptorDefaultAdmin, @@ -96,5 +98,5 @@ exports.initialiseChildContracts = async () => { maxFeePerGas: maxFee, }); console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); - await helper.waitForReceipt(resp.hash, childProvider); + await waitForReceipt(resp.hash, childProvider); } \ No newline at end of file diff --git a/scripts/deploy/deployAndInit.js b/scripts/deploy/deployAndInit.js deleted file mode 100644 index 78243245..00000000 --- a/scripts/deploy/deployAndInit.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; -require('dotenv').config(); -const deployChild = require("./child_deployment.js"); -const initChild = require("./child_initialisation.js"); -const deployRoot = require("./root_deployment.js"); -const initRoot = require("./root_initialisation.js"); - -async function run() { - await deployChild.deployChildContracts(); - await deployRoot.deployRootContracts(); - await initChild.initialiseChildContracts(); - await initRoot.initialiseRootContracts(); -} -run(); \ No newline at end of file diff --git a/scripts/deploy/deployAndInit.ts b/scripts/deploy/deployAndInit.ts new file mode 100644 index 00000000..b34a0cd8 --- /dev/null +++ b/scripts/deploy/deployAndInit.ts @@ -0,0 +1,14 @@ +'use strict'; +require('dotenv').config(); +import { deployChildContracts } from "./child_deployment"; +import { initialiseChildContracts } from "./child_initialisation"; +import { deployRootContracts } from "./root_deployment"; +import { initialiseRootContracts } from "./root_initialisation"; + +async function run() { + await deployChildContracts(); + await deployRootContracts(); + await initialiseChildContracts(); + await initialiseRootContracts(); +} +run(); \ No newline at end of file diff --git a/scripts/deploy/root_deployment.js b/scripts/deploy/root_deployment.ts similarity index 51% rename from scripts/deploy/root_deployment.js rename to scripts/deploy/root_deployment.ts index 6363d2be..11cca6a7 100644 --- a/scripts/deploy/root_deployment.js +++ b/scripts/deploy/root_deployment.ts @@ -1,24 +1,26 @@ // Deploy root contracts -'use strict'; -require('dotenv').config(); -const { ethers } = require("ethers"); -const helper = require("../helpers/helpers.js"); -const { LedgerSigner } = require('@ethersproject/hardware-wallets') -const fs = require('fs'); +import * as dotenv from "dotenv"; +dotenv.config(); +import { ethers } from "ethers"; +import { requireEnv, waitForConfirmation, deployRootContract, waitForReceipt } from "../helpers/helpers"; +import { LedgerSigner } from "../helpers/ledger_signer"; +import * as fs from "fs"; -exports.deployRootContracts = async () => { +export async function deployRootContracts() { // Check environment variables - let rootRPCURL = helper.requireEnv("ROOT_RPC_URL"); - let rootChainID = helper.requireEnv("ROOT_CHAIN_ID"); - let rootDeployerSecret = helper.requireEnv("ROOT_DEPLOYER_SECRET"); - let rootProxyAdmin = helper.requireEnv("ROOT_PROXY_ADMIN"); - let rootGatewayAddr = helper.requireEnv("ROOT_GATEWAY_ADDRESS"); + let rootRPCURL = requireEnv("ROOT_RPC_URL"); + let rootChainID = requireEnv("ROOT_CHAIN_ID"); + let rootDeployerSecret = requireEnv("ROOT_DEPLOYER_SECRET"); + let rootProxyAdmin = requireEnv("ROOT_PROXY_ADMIN"); + let rootGatewayAddr = requireEnv("ROOT_GATEWAY_ADDRESS"); // Get admin address const rootProvider = new ethers.providers.JsonRpcProvider(rootRPCURL, Number(rootChainID)); let adminWallet; if (rootDeployerSecret == "ledger") { - adminWallet = new LedgerSigner(rootProvider); + let index = requireEnv("ROOT_DEPLOYER_LEDGER_INDEX"); + const derivationPath = `m/44'/60'/${parseInt(index)}'/0/0`; + adminWallet = new LedgerSigner(rootProvider, derivationPath); } else { adminWallet = new ethers.Wallet(rootDeployerSecret, rootProvider); } @@ -27,68 +29,67 @@ exports.deployRootContracts = async () => { // Execute console.log("Deploy root contracts in..."); - await helper.waitForConfirmation(); + await waitForConfirmation(); // Deploy root token template - let rootTokenTemplateObj = JSON.parse(fs.readFileSync('../../out/ChildERC20.sol/ChildERC20.json', 'utf8')); console.log("Deploy root token template..."); - let rootTokenTemplate = await helper.deployRootContract(rootTokenTemplateObj, adminWallet); + let rootTokenTemplate = await deployRootContract("ChildERC20", adminWallet); console.log("Transaction submitted: ", JSON.stringify(rootTokenTemplate.deployTransaction, null, 2)); - await helper.waitForReceipt(rootTokenTemplate.deployTransaction.hash, rootProvider); + await waitForReceipt(rootTokenTemplate.deployTransaction.hash, rootProvider); // Initialise template console.log("Initialise root token template..."); let resp = await rootTokenTemplate.connect(adminWallet).initialize("000000000000000000000000000000000000007B", "TEMPLATE", "TPT", 18); console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); - await helper.waitForReceipt(resp.hash, rootProvider); + await waitForReceipt(resp.hash, rootProvider); console.log("Deployed to ROOT_TOKEN_TEMPLATE: ", rootTokenTemplate.address); // Deploy proxy admin - let proxyAdminObj = JSON.parse(fs.readFileSync('../../out/ProxyAdmin.sol/ProxyAdmin.json', 'utf8')); console.log("Deploy proxy admin..."); - let proxyAdmin = await helper.deployRootContract(proxyAdminObj, adminWallet); + let proxyAdmin = await deployRootContract("ProxyAdmin", adminWallet); console.log("Transaction submitted: ", JSON.stringify(proxyAdmin.deployTransaction, null, 2)); - await helper.waitForReceipt(proxyAdmin.deployTransaction.hash, rootProvider); + await waitForReceipt(proxyAdmin.deployTransaction.hash, rootProvider); // Change owner console.log("Change ownership...") resp = await proxyAdmin.connect(adminWallet).transferOwnership(rootProxyAdmin); console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); - await helper.waitForReceipt(resp.hash, rootProvider); + await waitForReceipt(resp.hash, rootProvider); console.log("Deployed to ROOT_PROXY_ADMIN: ", proxyAdmin.address); // Deploy root bridge impl - let rootBridgeImplObj = JSON.parse(fs.readFileSync('../../out/RootERC20BridgeFlowRate.sol/RootERC20BridgeFlowRate.json', 'utf8')); console.log("Deploy root bridge impl..."); - let rootBridgeImpl = await helper.deployRootContract(rootBridgeImplObj, adminWallet); + let rootBridgeImpl = await deployRootContract("RootERC20BridgeFlowRate", adminWallet); console.log("Transaction submitted: ", JSON.stringify(rootBridgeImpl.deployTransaction, null, 2)); - await helper.waitForReceipt(rootBridgeImpl.deployTransaction.hash, rootProvider); + await waitForReceipt(rootBridgeImpl.deployTransaction.hash, rootProvider); console.log("Deployed to ROOT_BRIDGE_IMPL_ADDRESS: ", rootBridgeImpl.address); // Deploy root bridge proxy - let rootBridgeProxyObj = JSON.parse(fs.readFileSync('../../out/TransparentUpgradeableProxy.sol/TransparentUpgradeableProxy.json', 'utf8')); console.log("Deploy root bridge proxy..."); - let rootBridgeProxy = await helper.deployRootContract(rootBridgeProxyObj, adminWallet, rootBridgeImpl.address, proxyAdmin.address, []); + let rootBridgeProxy = await deployRootContract("TransparentUpgradeableProxy", adminWallet, rootBridgeImpl.address, proxyAdmin.address, []); console.log("Transaction submitted: ", JSON.stringify(rootBridgeProxy.deployTransaction, null, 2)); - await helper.waitForReceipt(rootBridgeProxy.deployTransaction.hash, rootProvider); + await waitForReceipt(rootBridgeProxy.deployTransaction.hash, rootProvider); console.log("Deployed to ROOT_BRIDGE_PROXY_ADDRESS: ", rootBridgeProxy.address); // Deploy root adaptor impl - let rootAdaptorImplObj = JSON.parse(fs.readFileSync('../../out/RootAxelarBridgeAdaptor.sol/RootAxelarBridgeAdaptor.json', 'utf8')); console.log("Deploy root adaptor impl..."); - let rootAdaptorImpl = await helper.deployRootContract(rootAdaptorImplObj, adminWallet, rootGatewayAddr); + let rootAdaptorImpl = await deployRootContract("RootAxelarBridgeAdaptor", adminWallet, rootGatewayAddr); console.log("Transaction submitted: ", JSON.stringify(rootAdaptorImpl.deployTransaction, null, 2)); - await helper.waitForReceipt(rootAdaptorImpl.deployTransaction.hash, rootProvider); + await waitForReceipt(rootAdaptorImpl.deployTransaction.hash, rootProvider); console.log("Deployed to ROOT_ADAPTOR_IMPL_ADDRESS: ", rootAdaptorImpl.address); // Deploy root adaptor proxy - let rootAdaptorProxyObj = JSON.parse(fs.readFileSync('../../out/TransparentUpgradeableProxy.sol/TransparentUpgradeableProxy.json', 'utf8')); console.log("Deploy root adaptor proxy..."); - let rootAdaptorProxy = await helper.deployRootContract(rootAdaptorProxyObj, adminWallet, rootAdaptorImpl.address, proxyAdmin.address, []); + let rootAdaptorProxy = await deployRootContract("TransparentUpgradeableProxy", adminWallet, rootAdaptorImpl.address, proxyAdmin.address, []); console.log("Transaction submitted: ", JSON.stringify(rootAdaptorProxy.deployTransaction, null, 2)); - await helper.waitForReceipt(rootAdaptorProxy.deployTransaction.hash, rootProvider); + await waitForReceipt(rootAdaptorProxy.deployTransaction.hash, rootProvider); console.log("Deployed to ROOT_ADAPTOR_PROXY_ADDRESS: ", rootAdaptorProxy.address); let contractData = { + ROOT_PROXY_ADMIN: rootBridgeImpl.address, + ROOT_BRIDGE_IMPL_ADDRESS: rootBridgeImpl.address, + ROOT_BRIDGE_PROXY_ADDRESS: rootBridgeProxy.address, ROOT_BRIDGE_ADDRESS: rootBridgeProxy.address, + ROOT_ADAPTOR_IMPL_ADDRESS: rootAdaptorImpl.address, + ROOT_ADAPTOR_PROXY_ADDRESS: rootAdaptorProxy.address, ROOT_ADAPTOR_ADDRESS: rootAdaptorProxy.address, ROOT_TOKEN_TEMPLATE: rootTokenTemplate.address, }; diff --git a/scripts/deploy/root_initialisation.js b/scripts/deploy/root_initialisation.ts similarity index 54% rename from scripts/deploy/root_initialisation.js rename to scripts/deploy/root_initialisation.ts index bf49cc6d..1f61002f 100644 --- a/scripts/deploy/root_initialisation.js +++ b/scripts/deploy/root_initialisation.ts @@ -1,53 +1,53 @@ // Initialise root contracts -'use strict'; -require('dotenv').config(); -const { ethers } = require("ethers"); -const helper = require("../helpers/helpers.js"); -const { LedgerSigner } = require('@ethersproject/hardware-wallets') -const fs = require('fs'); +import * as dotenv from "dotenv"; +dotenv.config(); +import { ethers } from "ethers"; +import { requireEnv, waitForConfirmation, waitForReceipt, getContract } from "../helpers/helpers"; +import { LedgerSigner } from "../helpers/ledger_signer"; +import * as fs from "fs"; -exports.initialiseRootContracts = async() => { +export async function initialiseRootContracts() { // Check environment variables - let childChainName = helper.requireEnv("CHILD_CHAIN_NAME"); - let rootRPCURL = helper.requireEnv("ROOT_RPC_URL"); - let rootChainID = helper.requireEnv("ROOT_CHAIN_ID"); - let rootDeployerSecret = helper.requireEnv("ROOT_DEPLOYER_SECRET"); - let rootRateAdminSecret = helper.requireEnv("ROOT_BRIDGE_RATE_ADMIN_SECRET"); - let rootBridgeDefaultAdmin = helper.requireEnv("ROOT_BRIDGE_DEFAULT_ADMIN"); - let rootBridgePauser = helper.requireEnv("ROOT_BRIDGE_PAUSER"); - let rootBridgeUnpauser = helper.requireEnv("ROOT_BRIDGE_UNPAUSER"); - let rootBridgeVariableManager = helper.requireEnv("ROOT_BRIDGE_VARIABLE_MANAGER"); - let rootBridgeAdaptorManager = helper.requireEnv("ROOT_BRIDGE_ADAPTOR_MANAGER"); - let rootAdaptorDefaultAdmin = helper.requireEnv("ROOT_ADAPTOR_DEFAULT_ADMIN"); - let rootAdaptorBridgeManager = helper.requireEnv("ROOT_ADAPTOR_BRIDGE_MANAGER"); - let rootAdaptorGasServiceManager = helper.requireEnv("ROOT_ADAPTOR_GAS_SERVICE_MANAGER"); - let rootAdaptorTargetManager = helper.requireEnv("ROOT_ADAPTOR_TARGET_MANAGER"); - let rootGasServiceAddr = helper.requireEnv("ROOT_GAS_SERVICE_ADDRESS"); - let rootIMXAddr = helper.requireEnv("ROOT_IMX_ADDR"); - let rootWETHAddr = helper.requireEnv("ROOT_WETH_ADDR"); - let imxDepositLimit = helper.requireEnv("IMX_DEPOSIT_LIMIT"); - let rateLimitIMXCap = helper.requireEnv("RATE_LIMIT_IMX_CAPACITY"); - let rateLimitIMXRefill = helper.requireEnv("RATE_LIMIT_IMX_REFILL_RATE"); - let rateLimitIMXLargeThreshold = helper.requireEnv("RATE_LIMIT_IMX_LARGE_THRESHOLD"); - let rateLimitETHCap = helper.requireEnv("RATE_LIMIT_ETH_CAPACITY"); - let rateLimitETHRefill = helper.requireEnv("RATE_LIMIT_ETH_REFILL_RATE"); - let rateLimitETHLargeThreshold = helper.requireEnv("RATE_LIMIT_ETH_LARGE_THRESHOLD"); - let rateLimitUSDCAddr = helper.requireEnv("RATE_LIMIT_USDC_ADDR"); - let rateLimitUSDCCap = helper.requireEnv("RATE_LIMIT_USDC_CAPACITY"); - let rateLimitUSDCRefill = helper.requireEnv("RATE_LIMIT_USDC_REFILL_RATE"); - let rateLimitUSDCLargeThreshold = helper.requireEnv("RATE_LIMIT_USDC_LARGE_THRESHOLD"); - let rateLimitGUAddr = helper.requireEnv("RATE_LIMIT_GU_ADDR"); - let rateLimitGUCap = helper.requireEnv("RATE_LIMIT_GU_CAPACITY"); - let rateLimitGURefill = helper.requireEnv("RATE_LIMIT_GU_REFILL_RATE"); - let rateLimitGULargeThreshold = helper.requireEnv("RATE_LIMIT_GU_LARGE_THRESHOLD"); - let rateLimitCheckMateAddr = helper.requireEnv("RATE_LIMIT_CHECKMATE_ADDR"); - let rateLimitCheckMateCap = helper.requireEnv("RATE_LIMIT_CHECKMATE_CAPACITY"); - let rateLimitCheckMateRefill = helper.requireEnv("RATE_LIMIT_CHECKMATE_REFILL_RATE"); - let rateLimitCheckMateLargeThreshold = helper.requireEnv("RATE_LIMIT_CHECKMATE_LARGE_THRESHOLD"); - let rateLimitGOGAddr = helper.requireEnv("RATE_LIMIT_GOG_ADDR"); - let rateLimitGOGCap = helper.requireEnv("RATE_LIMIT_GOG_CAPACITY"); - let rateLimitGOGRefill = helper.requireEnv("RATE_LIMIT_GOG_REFILL_RATE"); - let rateLimitGOGLargeThreshold = helper.requireEnv("RATE_LIMIT_GOG_LARGE_THRESHOLD"); + let childChainName = requireEnv("CHILD_CHAIN_NAME"); + let rootRPCURL = requireEnv("ROOT_RPC_URL"); + let rootChainID = requireEnv("ROOT_CHAIN_ID"); + let rootDeployerSecret = requireEnv("ROOT_DEPLOYER_SECRET"); + let rootRateAdminSecret = requireEnv("ROOT_BRIDGE_RATE_ADMIN_SECRET"); + let rootBridgeDefaultAdmin = requireEnv("ROOT_BRIDGE_DEFAULT_ADMIN"); + let rootBridgePauser = requireEnv("ROOT_BRIDGE_PAUSER"); + let rootBridgeUnpauser = requireEnv("ROOT_BRIDGE_UNPAUSER"); + let rootBridgeVariableManager = requireEnv("ROOT_BRIDGE_VARIABLE_MANAGER"); + let rootBridgeAdaptorManager = requireEnv("ROOT_BRIDGE_ADAPTOR_MANAGER"); + let rootAdaptorDefaultAdmin = requireEnv("ROOT_ADAPTOR_DEFAULT_ADMIN"); + let rootAdaptorBridgeManager = requireEnv("ROOT_ADAPTOR_BRIDGE_MANAGER"); + let rootAdaptorGasServiceManager = requireEnv("ROOT_ADAPTOR_GAS_SERVICE_MANAGER"); + let rootAdaptorTargetManager = requireEnv("ROOT_ADAPTOR_TARGET_MANAGER"); + let rootGasServiceAddr = requireEnv("ROOT_GAS_SERVICE_ADDRESS"); + let rootIMXAddr = requireEnv("ROOT_IMX_ADDR"); + let rootWETHAddr = requireEnv("ROOT_WETH_ADDR"); + let imxDepositLimit = requireEnv("IMX_DEPOSIT_LIMIT"); + let rateLimitIMXCap = requireEnv("RATE_LIMIT_IMX_CAPACITY"); + let rateLimitIMXRefill = requireEnv("RATE_LIMIT_IMX_REFILL_RATE"); + let rateLimitIMXLargeThreshold = requireEnv("RATE_LIMIT_IMX_LARGE_THRESHOLD"); + let rateLimitETHCap = requireEnv("RATE_LIMIT_ETH_CAPACITY"); + let rateLimitETHRefill = requireEnv("RATE_LIMIT_ETH_REFILL_RATE"); + let rateLimitETHLargeThreshold = requireEnv("RATE_LIMIT_ETH_LARGE_THRESHOLD"); + let rateLimitUSDCAddr = requireEnv("RATE_LIMIT_USDC_ADDR"); + let rateLimitUSDCCap = requireEnv("RATE_LIMIT_USDC_CAPACITY"); + let rateLimitUSDCRefill = requireEnv("RATE_LIMIT_USDC_REFILL_RATE"); + let rateLimitUSDCLargeThreshold = requireEnv("RATE_LIMIT_USDC_LARGE_THRESHOLD"); + let rateLimitGUAddr = requireEnv("RATE_LIMIT_GU_ADDR"); + let rateLimitGUCap = requireEnv("RATE_LIMIT_GU_CAPACITY"); + let rateLimitGURefill = requireEnv("RATE_LIMIT_GU_REFILL_RATE"); + let rateLimitGULargeThreshold = requireEnv("RATE_LIMIT_GU_LARGE_THRESHOLD"); + let rateLimitCheckMateAddr = requireEnv("RATE_LIMIT_CHECKMATE_ADDR"); + let rateLimitCheckMateCap = requireEnv("RATE_LIMIT_CHECKMATE_CAPACITY"); + let rateLimitCheckMateRefill = requireEnv("RATE_LIMIT_CHECKMATE_REFILL_RATE"); + let rateLimitCheckMateLargeThreshold = requireEnv("RATE_LIMIT_CHECKMATE_LARGE_THRESHOLD"); + let rateLimitGOGAddr = requireEnv("RATE_LIMIT_GOG_ADDR"); + let rateLimitGOGCap = requireEnv("RATE_LIMIT_GOG_CAPACITY"); + let rateLimitGOGRefill = requireEnv("RATE_LIMIT_GOG_REFILL_RATE"); + let rateLimitGOGLargeThreshold = requireEnv("RATE_LIMIT_GOG_LARGE_THRESHOLD"); // Read from contract file. let data = fs.readFileSync(".child.bridge.contracts.json", 'utf-8'); @@ -64,7 +64,9 @@ exports.initialiseRootContracts = async() => { const rootProvider = new ethers.providers.JsonRpcProvider(rootRPCURL, Number(rootChainID)); let adminWallet; if (rootDeployerSecret == "ledger") { - adminWallet = new LedgerSigner(rootProvider); + let index = requireEnv("ROOT_DEPLOYER_LEDGER_INDEX"); + const derivationPath = `m/44'/60'/${parseInt(index)}'/0/0`; + adminWallet = new LedgerSigner(rootProvider, derivationPath); } else { adminWallet = new ethers.Wallet(rootDeployerSecret, rootProvider); } @@ -74,7 +76,9 @@ exports.initialiseRootContracts = async() => { // Get rate admin address let rateAdminWallet; if (rootRateAdminSecret == "ledger") { - rateAdminWallet = new LedgerSigner(rateAdminWallet); + let index = requireEnv("ROOT_BRIDGE_RATE_ADMIN_LEDGER_INDEX"); + const derivationPath = `m/44'/60'/${parseInt(index)}'/0/0`; + rateAdminWallet = new LedgerSigner(rootProvider, derivationPath); } else { rateAdminWallet = new ethers.Wallet(rootRateAdminSecret, rootProvider); } @@ -84,12 +88,11 @@ exports.initialiseRootContracts = async() => { // Execute console.log("Initialise root contracts in..."); - await helper.waitForConfirmation(); + await waitForConfirmation(); // Initialise root bridge - let rootBridgeObj = JSON.parse(fs.readFileSync('../../out/RootERC20BridgeFlowRate.sol/RootERC20BridgeFlowRate.json', 'utf8')); console.log("Initialise root bridge..."); - let rootBridge = new ethers.Contract(rootBridgeAddr, rootBridgeObj.abi, rootProvider); + let rootBridge = getContract("RootERC20BridgeFlowRate", rootBridgeAddr, rootProvider); let resp = await rootBridge.connect(adminWallet)["initialize((address,address,address,address,address),address,address,address,address,address,uint256,address)"]( { defaultAdmin: rootBridgeDefaultAdmin, @@ -106,7 +109,7 @@ exports.initialiseRootContracts = async() => { ethers.utils.parseEther(imxDepositLimit), rateAdminAddr); console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); - await helper.waitForReceipt(resp.hash, rootProvider); + await waitForReceipt(resp.hash, rootProvider); // Configure rate // IMX @@ -118,7 +121,7 @@ exports.initialiseRootContracts = async() => { ethers.utils.parseEther(rateLimitIMXLargeThreshold) ); console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); - await helper.waitForReceipt(resp.hash, rootProvider); + await waitForReceipt(resp.hash, rootProvider); // ETH console.log("Configure rate limiting for ETH...") @@ -129,7 +132,7 @@ exports.initialiseRootContracts = async() => { ethers.utils.parseEther(rateLimitETHLargeThreshold) ); console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); - await helper.waitForReceipt(resp.hash, rootProvider); + await waitForReceipt(resp.hash, rootProvider); // USDC console.log("Configure rate limiting for USDC...") @@ -140,7 +143,7 @@ exports.initialiseRootContracts = async() => { ethers.utils.parseEther(rateLimitUSDCLargeThreshold) ); console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); - await helper.waitForReceipt(resp.hash, rootProvider); + await waitForReceipt(resp.hash, rootProvider); // GU console.log("Configure rate limiting for GU...") @@ -151,7 +154,7 @@ exports.initialiseRootContracts = async() => { ethers.utils.parseEther(rateLimitGULargeThreshold) ); console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); - await helper.waitForReceipt(resp.hash, rootProvider); + await waitForReceipt(resp.hash, rootProvider); // Checkmate console.log("Configure rate limiting for CheckMate...") @@ -162,7 +165,7 @@ exports.initialiseRootContracts = async() => { ethers.utils.parseEther(rateLimitCheckMateLargeThreshold) ); console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); - await helper.waitForReceipt(resp.hash, rootProvider); + await waitForReceipt(resp.hash, rootProvider); // GOG console.log("Configure rate limiting for GOG...") @@ -173,12 +176,11 @@ exports.initialiseRootContracts = async() => { ethers.utils.parseEther(rateLimitGOGLargeThreshold) ); console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); - await helper.waitForReceipt(resp.hash, rootProvider); + await waitForReceipt(resp.hash, rootProvider); // Initialise root adaptor - let rootAdaptorObj = JSON.parse(fs.readFileSync('../../out/RootAxelarBridgeAdaptor.sol/RootAxelarBridgeAdaptor.json', 'utf8')); console.log("Initialise root adaptor..."); - let rootAdaptor = new ethers.Contract(rootAdaptorAddr, rootAdaptorObj.abi, rootProvider); + let rootAdaptor = getContract("RootAxelarBridgeAdaptor", rootAdaptorAddr, rootProvider); resp = await rootAdaptor.connect(adminWallet).initialize( { defaultAdmin: rootAdaptorDefaultAdmin, @@ -191,5 +193,5 @@ exports.initialiseRootContracts = async() => { childAdaptorAddr, rootGasServiceAddr); console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); - await helper.waitForReceipt(resp.hash, rootProvider); + await waitForReceipt(resp.hash, rootProvider); } \ No newline at end of file diff --git a/scripts/e2e/e2e.js b/scripts/e2e/e2e.ts similarity index 80% rename from scripts/e2e/e2e.js rename to scripts/e2e/e2e.ts index 964432e2..d62e0f49 100644 --- a/scripts/e2e/e2e.js +++ b/scripts/e2e/e2e.ts @@ -1,38 +1,38 @@ // End to end tests -'use strict'; -require('dotenv').config(); -const { ethers, ContractFactory } = require("ethers"); -const helper = require("../helpers/helpers.js"); -const fs = require('fs'); -const { expect } = require("chai"); +import * as dotenv from "dotenv"; +dotenv.config(); +import { ethers, providers } from "ethers"; +import { requireEnv, waitForReceipt, getFee, getContract, deployRootContract, delay } from "../helpers/helpers"; +import * as fs from "fs"; +import { expect } from "chai"; // The contract ABI of IMX on L1. const IMX_ABI = `[{"inputs":[{"internalType":"address","name":"minter","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"previousAdminRole","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"newAdminRole","type":"bytes32"}],"name":"RoleAdminChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleGranted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleRevoked","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[],"name":"DEFAULT_ADMIN_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MINTER_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"cap","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"subtractedValue","type":"uint256"}],"name":"decreaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"}],"name":"getRoleAdmin","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"grantRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"hasRole","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"addedValue","type":"uint256"}],"name":"increaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"mint","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"renounceRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"revokeRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}]`; describe("Bridge e2e test", () => { - let rootProvider; - let childProvider; - let rootTestWallet; - let childTestWallet; - let rootBridge; - let rootWETH; - let rootIMX; - let childBridge; - let childETH; - let childWIMX; - let rootCustomToken; - let childCustomToken; + let rootProvider: providers.JsonRpcProvider; + let childProvider: providers.JsonRpcProvider; + let rootTestWallet: ethers.Wallet; + let childTestWallet: ethers.Wallet; + let rootBridge: ethers.Contract; + let rootWETH: ethers.Contract; + let rootIMX: ethers.Contract; + let childBridge: ethers.Contract; + let childETH: ethers.Contract; + let childWIMX: ethers.Contract; + let rootCustomToken: ethers.Contract; + let childCustomToken: ethers.Contract; before(async function () { this.timeout(30000); - let rootRPCURL = helper.requireEnv("ROOT_RPC_URL"); - let rootChainID = helper.requireEnv("ROOT_CHAIN_ID"); - let childRPCURL = helper.requireEnv("CHILD_RPC_URL"); - let childChainID = helper.requireEnv("CHILD_CHAIN_ID"); - let rootRateAdminSecret = helper.requireEnv("ROOT_BRIDGE_RATE_ADMIN_SECRET"); - let testAccountKey = helper.requireEnv("TEST_ACCOUNT_SECRET"); - let rootIMXAddr = helper.requireEnv("ROOT_IMX_ADDR"); - let rootWETHAddr = helper.requireEnv("ROOT_WETH_ADDR"); + let rootRPCURL = requireEnv("ROOT_RPC_URL"); + let rootChainID = requireEnv("ROOT_CHAIN_ID"); + let childRPCURL = requireEnv("CHILD_RPC_URL"); + let childChainID = requireEnv("CHILD_CHAIN_ID"); + let rootRateAdminSecret = requireEnv("ROOT_BRIDGE_RATE_ADMIN_SECRET"); + let testAccountKey = requireEnv("TEST_ACCOUNT_SECRET"); + let rootIMXAddr = requireEnv("ROOT_IMX_ADDR"); + let rootWETHAddr = requireEnv("ROOT_WETH_ADDR"); // Read from contract file. let data = fs.readFileSync(".child.bridge.contracts.json", 'utf-8'); @@ -49,32 +49,19 @@ describe("Bridge e2e test", () => { childTestWallet = new ethers.Wallet(testAccountKey, childProvider); let rootRateAdminWallet = new ethers.Wallet(rootRateAdminSecret, rootProvider); - let rootBridgeObj = JSON.parse(fs.readFileSync('../../out/RootERC20BridgeFlowRate.sol/RootERC20BridgeFlowRate.json', 'utf8')); - rootBridge = new ethers.Contract(rootBridgeAddr, rootBridgeObj.abi, rootProvider); - - let WETHObj = JSON.parse(fs.readFileSync('../../out/WETH.sol/WETH.json', 'utf8')) - rootWETH = new ethers.Contract(rootWETHAddr, WETHObj.abi, rootProvider); - + rootBridge = getContract("RootERC20BridgeFlowRate", rootBridgeAddr, rootProvider); + rootWETH = getContract("WETH", rootWETHAddr, rootProvider); rootIMX = new ethers.Contract(rootIMXAddr, IMX_ABI, rootProvider); - - let childBridgeObj = JSON.parse(fs.readFileSync('../../out/ChildERC20Bridge.sol/ChildERC20Bridge.json', 'utf8')); - childBridge = new ethers.Contract(childBridgeAddr, childBridgeObj.abi, childProvider); - - let childEthTokenAddr = await childBridge.childETHToken(); - let childTokenTemplateObj = JSON.parse(fs.readFileSync('../../out/ChildERC20.sol/ChildERC20.json', 'utf8')); - childETH = new ethers.Contract(childEthTokenAddr, childTokenTemplateObj.abi, childProvider); - - let wrappedIMXObj = JSON.parse(fs.readFileSync('../../out/WIMX.sol/WIMX.json', 'utf8')); - childWIMX = new ethers.Contract(childWIMXAddr, wrappedIMXObj.abi, childProvider); + childBridge = getContract("ChildERC20Bridge", childBridgeAddr, childProvider); + childETH = getContract("ChildERC20", await childBridge.childETHToken(), childProvider); + childWIMX = getContract("WIMX", childWIMXAddr, childProvider); // Deploy a custom token - let customTokenObj = JSON.parse(fs.readFileSync('../../out/ERC20PresetMinterPauser.sol/ERC20PresetMinterPauser.json', 'utf8')); - let factory = new ContractFactory(customTokenObj.abi, customTokenObj.bytecode, rootTestWallet); - rootCustomToken = await factory.deploy("Custom Token", "CTK"); - await helper.waitForReceipt(rootCustomToken.deployTransaction.hash, rootProvider); + rootCustomToken = await deployRootContract("ERC20PresetMinterPauser", rootTestWallet, "Custom Token", "CTK"); + await waitForReceipt(rootCustomToken.deployTransaction.hash, rootProvider); // Mint tokens let resp = await rootCustomToken.connect(rootTestWallet).mint(rootTestWallet.address, ethers.utils.parseEther("1000.0").toBigInt()); - await helper.waitForReceipt(resp.hash, rootProvider); + await waitForReceipt(resp.hash, rootProvider); // Set rate control resp = await rootBridge.connect(rootRateAdminWallet).setRateControlThreshold( rootCustomToken.address, @@ -82,7 +69,7 @@ describe("Bridge e2e test", () => { ethers.utils.parseEther("5.56"), ethers.utils.parseEther("10008.0") ); - await helper.waitForReceipt(resp.hash, rootProvider); + await waitForReceipt(resp.hash, rootProvider); }) it("should successfully deposit IMX to self from L1 to L2", async() => { @@ -95,20 +82,20 @@ describe("Bridge e2e test", () => { // Approve let resp = await rootIMX.connect(rootTestWallet).approve(rootBridge.address, amt); - await helper.waitForReceipt(resp.hash, rootProvider); + await waitForReceipt(resp.hash, rootProvider); // IMX deposit L1 to L2 resp = await rootBridge.connect(rootTestWallet).deposit(rootIMX.address, amt, { value: bridgeFee, }); - await helper.waitForReceipt(resp.hash, rootProvider); + await waitForReceipt(resp.hash, rootProvider); let postBalL1 = await rootIMX.balanceOf(rootTestWallet.address); let postBalL2 = preBalL2; while (postBalL2.eq(preBalL2)) { postBalL2 = await childProvider.getBalance(childTestWallet.address); - await helper.delay(1000); + await delay(1000); } // Verify @@ -127,20 +114,20 @@ describe("Bridge e2e test", () => { let bridgeFee = ethers.utils.parseEther("1.0"); // IMX withdraw L2 to L1 - let [priorityFee, maxFee] = await helper.getFee(childTestWallet); + let [priorityFee, maxFee] = await getFee(childProvider); let resp = await childBridge.connect(childTestWallet).withdrawIMX(amt, { value: amt.add(bridgeFee), maxPriorityFeePerGas: priorityFee, maxFeePerGas: maxFee, }); - await helper.waitForReceipt(resp.hash, childProvider); + await waitForReceipt(resp.hash, childProvider); let postBalL1 = preBalL1; let postBalL2 = await childProvider.getBalance(childTestWallet.address); while (postBalL1.eq(preBalL1)) { postBalL1 = await rootIMX.balanceOf(rootTestWallet.address); - await helper.delay(1000); + await delay(1000); } // Verify @@ -154,13 +141,13 @@ describe("Bridge e2e test", () => { it("should successfully withdraw wIMX to self from L2 to L1", async() => { // Wrap 1 IMX - let [priorityFee, maxFee] = await helper.getFee(childTestWallet); + let [priorityFee, maxFee] = await getFee(childProvider); let resp = await childWIMX.connect(childTestWallet).deposit({ value: ethers.utils.parseEther("1.0"), maxPriorityFeePerGas: priorityFee, maxFeePerGas: maxFee, }); - await helper.waitForReceipt(resp.hash, childProvider); + await waitForReceipt(resp.hash, childProvider); // Get IMX balance on root & child chains before withdraw let preBalL1 = await rootIMX.balanceOf(rootTestWallet.address); @@ -170,28 +157,28 @@ describe("Bridge e2e test", () => { let bridgeFee = ethers.utils.parseEther("1.0"); // Approve - [priorityFee, maxFee] = await helper.getFee(childTestWallet); + [priorityFee, maxFee] = await getFee(childProvider); resp = await childWIMX.connect(childTestWallet).approve(childBridge.address, amt, { maxPriorityFeePerGas: priorityFee, maxFeePerGas: maxFee, }); - await helper.waitForReceipt(resp.hash, childProvider); + await waitForReceipt(resp.hash, childProvider); // wIMX withdraw L2 to L1 - [priorityFee, maxFee] = await helper.getFee(childTestWallet); + [priorityFee, maxFee] = await getFee(childProvider); resp = await childBridge.connect(childTestWallet).withdrawWIMX(amt, { value: bridgeFee, maxPriorityFeePerGas: priorityFee, maxFeePerGas: maxFee, }); - await helper.waitForReceipt(resp.hash, childProvider); + await waitForReceipt(resp.hash, childProvider); let postBalL1 = preBalL1; let postBalL2 = await childWIMX.balanceOf(childTestWallet.address); while (postBalL1.eq(preBalL1)) { postBalL1 = await rootIMX.balanceOf(rootTestWallet.address); - await helper.delay(1000); + await delay(1000); } // Verify @@ -213,14 +200,14 @@ describe("Bridge e2e test", () => { let resp = await rootBridge.connect(rootTestWallet).depositETH(amt, { value: amt.add(bridgeFee), }); - await helper.waitForReceipt(resp.hash, rootProvider); + await waitForReceipt(resp.hash, rootProvider); let postBalL1 = await rootProvider.getBalance(rootTestWallet.address); let postBalL2 = preBalL2; while (postBalL2.eq(preBalL2)) { postBalL2 = await childETH.balanceOf(childTestWallet.address); - await helper.delay(1000); + await delay(1000); } // Verify @@ -237,7 +224,7 @@ describe("Bridge e2e test", () => { let resp = await rootWETH.connect(rootTestWallet).deposit({ value: ethers.utils.parseEther("0.01"), }) - await helper.waitForReceipt(resp.hash, rootProvider); + await waitForReceipt(resp.hash, rootProvider); // Get ETH balance on root & child chains before withdraw let preBalL1 = await rootWETH.balanceOf(rootTestWallet.address); @@ -248,20 +235,20 @@ describe("Bridge e2e test", () => { // Approve resp = await rootWETH.connect(rootTestWallet).approve(rootBridge.address, amt); - await helper.waitForReceipt(resp.hash, rootProvider); + await waitForReceipt(resp.hash, rootProvider); // wETH deposit L1 to L2 resp = await rootBridge.connect(rootTestWallet).deposit(rootWETH.address, amt, { value: bridgeFee, }) - await helper.waitForReceipt(resp.hash, rootProvider); + await waitForReceipt(resp.hash, rootProvider); let postBalL1 = await rootWETH.balanceOf(rootTestWallet.address); let postBalL2 = preBalL2; while (postBalL2.eq(preBalL2)) { postBalL2 = await childETH.balanceOf(childTestWallet.address); - await helper.delay(1000); + await delay(1000); } // Verify @@ -280,20 +267,20 @@ describe("Bridge e2e test", () => { let bridgeFee = ethers.utils.parseEther("1.0"); // ETH withdraw L2 to L1 - let [priorityFee, maxFee] = await helper.getFee(childTestWallet); + let [priorityFee, maxFee] = await getFee(childProvider); let resp = await childBridge.connect(childTestWallet).withdrawETH(amt, { value: bridgeFee, maxPriorityFeePerGas: priorityFee, maxFeePerGas: maxFee, }); - await helper.waitForReceipt(resp.hash, childProvider); + await waitForReceipt(resp.hash, childProvider); let postBalL1 = preBalL1; let postBalL2 = await childETH.balanceOf(childTestWallet.address); while (postBalL1.eq(preBalL1)) { postBalL1 = await rootProvider.getBalance(rootTestWallet.address); - await helper.delay(1000); + await delay(1000); } // Verify @@ -312,15 +299,14 @@ describe("Bridge e2e test", () => { let resp = await rootBridge.connect(rootTestWallet).mapToken(rootCustomToken.address, { value: bridgeFee, }) - await helper.waitForReceipt(resp.hash, rootProvider); + await waitForReceipt(resp.hash, rootProvider); let childTokenAddr = await childBridge.rootTokenToChildToken(rootCustomToken.address); while (childTokenAddr == ethers.constants.AddressZero) { childTokenAddr = await childBridge.rootTokenToChildToken(rootCustomToken.address); - await helper.delay(1000); + await delay(1000); } - let childTokenTemplateObj = JSON.parse(fs.readFileSync('../../out/ChildERC20.sol/ChildERC20.json', 'utf8')); - childCustomToken = new ethers.Contract(childTokenAddr, childTokenTemplateObj.abi, childProvider); + childCustomToken = getContract("ChildERC20", childTokenAddr, childProvider); // Verify expect(childTokenAddr).to.equal(expectedChildTokenAddr); @@ -336,19 +322,19 @@ describe("Bridge e2e test", () => { // Approve let resp = await rootCustomToken.connect(rootTestWallet).approve(rootBridge.address, amt); - await helper.waitForReceipt(resp.hash, rootProvider); + await waitForReceipt(resp.hash, rootProvider); // Token deposit L1 to L2 resp = await rootBridge.connect(rootTestWallet).deposit(rootCustomToken.address, amt, { value: bridgeFee, }) - await helper.waitForReceipt(resp.hash, rootProvider); + await waitForReceipt(resp.hash, rootProvider); let postBalL1 = await rootCustomToken.balanceOf(rootTestWallet.address); let postBalL2 = preBalL2; while (postBalL2.eq(preBalL2)) { postBalL2 = await childCustomToken.balanceOf(childTestWallet.address); - await helper.delay(1000); + await delay(1000); } // Verify @@ -367,20 +353,20 @@ describe("Bridge e2e test", () => { let bridgeFee = ethers.utils.parseEther("1.0"); // Token withdraw L2 to L1 - let [priorityFee, maxFee] = await helper.getFee(childTestWallet); + let [priorityFee, maxFee] = await getFee(childProvider); let resp = await childBridge.connect(childTestWallet).withdraw(childCustomToken.address, amt, { value: bridgeFee, maxPriorityFeePerGas: priorityFee, maxFeePerGas: maxFee, }) - await helper.waitForReceipt(resp.hash, childProvider); + await waitForReceipt(resp.hash, childProvider); let postBalL1 = preBalL1; let postBalL2 = await childCustomToken.balanceOf(childTestWallet.address); while (postBalL1.eq(preBalL1)) { postBalL1 = await rootCustomToken.balanceOf(rootTestWallet.address); - await helper.delay(1000); + await delay(1000); } // Verify diff --git a/scripts/helpers/helpers.js b/scripts/helpers/helpers.js deleted file mode 100644 index 776846cc..00000000 --- a/scripts/helpers/helpers.js +++ /dev/null @@ -1,66 +0,0 @@ -const { ContractFactory } = require("ethers"); - -exports.delay = (time) => { - return new Promise(resolve => setTimeout(resolve, time)); -} -exports.requireEnv = (envName) => { - let val = process.env[envName]; - if (val == null || val == "") { - throw(envName + " not set!"); - } - if (!envName.includes("SECRET")) { - console.log(envName + ": ", val); - } else { - console.log(envName + " is set."); - } - return val -} -exports.waitForReceipt = async (txHash, provider) => { - let receipt; - while (receipt == null) { - receipt = await provider.getTransactionReceipt(txHash) - await exports.delay(1000); - } - console.log("Receipt: " + JSON.stringify(receipt, null, 2)); - if (receipt.status != 1) { - throw("Fail to execute: " + txHash); - } - console.log("Tx " + txHash + " succeed."); -} -exports.waitForConfirmation = async () => { - if (process.env["SKIP_WAIT_FOR_CONFIRMATION"] == null) { - for (let i = 10; i >= 0; i--) { - console.log(i) - await exports.delay(1000); - } - } -} -exports.getFee = async (wallet) => { - let feeData = await wallet.getFeeData(); - let baseFee = feeData.lastBaseFeePerGas; - let gasPrice = feeData.gasPrice; - let priorityFee = Math.round(gasPrice * 150 / 100); - let maxFee = Math.round(1.13 * baseFee + priorityFee); - return [priorityFee, maxFee]; -} -exports.requireNonEmptyCode = async (provider, addr) => { - if (await provider.getCode(addr) == "0x") { - throw(addr + " has empty code!"); - } - console.log(addr + " has code."); -} -exports.hasDuplicates = (array) => { - return (new Set(array)).size !== array.length; -} -exports.deployChildContract = async (contractObj, adminWallet, ...args) => { - let [priorityFee, maxFee] = await exports.getFee(adminWallet); - let factory = new ContractFactory(contractObj.abi, contractObj.bytecode, adminWallet); - return await factory.deploy(...args, { - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }); -} -exports.deployRootContract = async (contractObj, adminWallet, ...args) => { - let factory = new ContractFactory(contractObj.abi, contractObj.bytecode, adminWallet); - return await factory.deploy(...args); -} \ No newline at end of file diff --git a/scripts/helpers/helpers.ts b/scripts/helpers/helpers.ts new file mode 100644 index 00000000..678a4be1 --- /dev/null +++ b/scripts/helpers/helpers.ts @@ -0,0 +1,90 @@ +import { ContractFactory, providers, ethers } from "ethers"; +import { LedgerSigner } from "./ledger_signer"; +import * as fs from "fs"; + +export function delay(time: number) { + return new Promise(resolve => setTimeout(resolve, time)); +} + +export function requireEnv(envName: string) { + let val = process.env[envName]; + if (val == null || val == "") { + throw(envName + " not set!"); + } + if (!envName.includes("SECRET")) { + console.log(envName + ": ", val); + } else { + console.log(envName + " is set."); + } + return val +} + +export async function waitForReceipt(txHash: string, provider: providers.JsonRpcProvider) { + let receipt; + while (receipt == null) { + receipt = await provider.getTransactionReceipt(txHash) + await exports.delay(1000); + } + console.log("Receipt: " + JSON.stringify(receipt, null, 2)); + if (receipt.status != 1) { + throw("Fail to execute: " + txHash); + } + console.log("Tx " + txHash + " succeed."); +} + +export async function waitForConfirmation() { + if (process.env["SKIP_WAIT_FOR_CONFIRMATION"] == null) { + for (let i = 10; i >= 0; i--) { + console.log(i) + await exports.delay(1000); + } + } +} + +export async function getFee(provider: providers.JsonRpcProvider) { + let feeData = await provider.getFeeData(); + let baseFee = feeData.lastBaseFeePerGas; + let gasPrice = feeData.gasPrice; + let priorityFee; + let maxFee; + if (gasPrice && baseFee) { + priorityFee = gasPrice.mul(150).div(100); + maxFee = baseFee.mul(113).div(100).add(priorityFee); + } else { + priorityFee = ethers.utils.parseUnits("110", "gwei"); + maxFee = ethers.utils.parseUnits("120", "gwei"); + } + return [priorityFee, maxFee]; +} + +export async function requireNonEmptyCode(provider: providers.JsonRpcProvider, addr: string) { + if (await provider.getCode(addr) == "0x") { + throw(addr + " has empty code!"); + } + console.log(addr + " has code."); +} + +export function hasDuplicates(array: string[]) { + return (new Set(array)).size !== array.length; +} + +export async function deployChildContract(contract: string, adminWallet: ethers.Wallet | LedgerSigner, ...args: any) { + let contractObj = JSON.parse(fs.readFileSync(`../../out/${contract}.sol/${contract}.json`, 'utf8')); + let [priorityFee, maxFee] = await exports.getFee(adminWallet); + let factory = new ContractFactory(contractObj.abi, contractObj.bytecode, adminWallet); + return await factory.deploy(...args, { + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); +} + +export async function deployRootContract(contract: string, adminWallet: ethers.Wallet | LedgerSigner, ...args: any) { + let contractObj = JSON.parse(fs.readFileSync(`../../out/${contract}.sol/${contract}.json`, 'utf8')); + let factory = new ContractFactory(contractObj.abi, contractObj.bytecode, adminWallet); + return await factory.deploy(...args); +} + +export function getContract(contract: string, contractAddr: string, provider: providers.JsonRpcProvider) { + let contractObj = JSON.parse(fs.readFileSync(`../../out/${contract}.sol/${contract}.json`, 'utf8')); + return new ethers.Contract(contractAddr, contractObj.abi, provider); +} \ No newline at end of file diff --git a/scripts/helpers/ledger_signer.ts b/scripts/helpers/ledger_signer.ts new file mode 100644 index 00000000..cb81315f --- /dev/null +++ b/scripts/helpers/ledger_signer.ts @@ -0,0 +1,145 @@ +// Copied from https://github.com/immutable/imx-engine/blob/77b8a62e6ac0baf033519e0ed533316eead3bc23/services/order-book-mr/e2e/scripts/ledger-signer.ts +import { ethers } from "ethers"; +import Eth from "@ledgerhq/hw-app-eth"; +import TransportNodeHid from "@ledgerhq/hw-transport-node-hid"; +import { + defineReadOnly, + hexlify, + resolveProperties, + serializeTransaction, + toUtf8Bytes, + UnsignedTransaction, +} from "ethers/lib/utils"; +import { toBuffer, toRpcSig } from "@nomicfoundation/ethereumjs-util"; +import ledgerService from "@ledgerhq/hw-app-eth/lib/services/ledger"; + +const DEFAULT_LEDGER_PATH = "m/44'/60'/0'/0/0"; + +function toHex(value: string | Buffer): string { + const stringValue = typeof value === "string" ? value : value.toString("hex"); + return stringValue.startsWith("0x") ? stringValue : `0x${stringValue}`; +} + +// Simple LedgerSigner that wraps @ledgerhq/hw-transport-node-hid to deploy +// contracts using hardware wallet. +export class LedgerSigner extends ethers.Signer { + readonly path: string; + readonly _eth: Promise | undefined; + + constructor( + provider?: ethers.providers.Provider, + path: string = DEFAULT_LEDGER_PATH + ) { + super(); + this.path = path || DEFAULT_LEDGER_PATH; + + defineReadOnly(this, "path", path); + defineReadOnly(this, "provider", provider || undefined); + defineReadOnly( + this, + "_eth", + TransportNodeHid.create().then(async (transport) => { + try { + const eth = new Eth(transport); + await eth.getAppConfiguration(); + return eth; + } catch (error) { + throw "LedgerSigner: unable to initialize TransportNodeHid: " + error; + } + }) + ); + } + + private async _withConfirmation any>( + func: T + ): Promise> { + try { + const result = await func(); + + return result; + } catch (error) { + throw new Error("LedgerSigner: confirmation_failure: " + error); + } + } + + public async getAddress(): Promise { + const eth = await this._eth; + + const MAX_RETRY_COUNT = 50; + const WAIT_INTERVAL = 100; + + for (let i = 0; i < MAX_RETRY_COUNT; i++) { + try { + const account = await eth!.getAddress(this.path); + return ethers.utils.getAddress(account.address); + } catch (error) { + if ((error as any).id !== "TransportLocked") { + throw error; + } + } + await new Promise((resolve) => setTimeout(resolve, WAIT_INTERVAL)); + } + + throw new Error("LedgerSigner: getAddress timed out"); + } + + public async signMessage( + message: ethers.utils.Bytes | string + ): Promise { + const resolvedMessage = + typeof message === "string" ? toUtf8Bytes(message) : message; + + const eth = await this._eth; + const signature = await this._withConfirmation(() => + eth!.signPersonalMessage(this.path, hexlify(resolvedMessage)) + ); + + return toRpcSig( + BigInt(signature.v - 27), + toBuffer(toHex(signature.r)), + toBuffer(toHex(signature.s)) + ); + } + + async signTransaction( + transaction: ethers.providers.TransactionRequest + ): Promise { + const txRequest = await resolveProperties(transaction); + + const baseTx: UnsignedTransaction = { + type: txRequest.type, + data: txRequest.data, + chainId: txRequest.chainId, + gasLimit: txRequest.gasLimit, + gasPrice: txRequest.gasPrice, + nonce: Number(txRequest.nonce), + value: txRequest.value, + to: txRequest.to, + }; + + // Type-2 transaction, with tip + if (txRequest.type === 2) { + baseTx.maxFeePerGas = txRequest.maxFeePerGas; + baseTx.maxPriorityFeePerGas = txRequest.maxPriorityFeePerGas; + } + + const txToSign = serializeTransaction(baseTx).substring(2); + + const resolution = await ledgerService.resolveTransaction(txToSign, {}, {}); + + const eth = await this._eth; + const signature = await this._withConfirmation(() => + eth!.signTransaction(this.path, txToSign, resolution) + ); + + return serializeTransaction(baseTx, { + v: Number(signature.v), + r: toHex(signature.r), + s: toHex(signature.s), + }); + } + + connect(provider: ethers.providers.Provider): ethers.Signer { + return new LedgerSigner(provider, this.path); + } +} \ No newline at end of file diff --git a/scripts/localdev/axelar_setup.js b/scripts/localdev/axelar_setup.ts similarity index 80% rename from scripts/localdev/axelar_setup.js rename to scripts/localdev/axelar_setup.ts index 81a467af..b8b0c6f1 100644 --- a/scripts/localdev/axelar_setup.js +++ b/scripts/localdev/axelar_setup.ts @@ -1,23 +1,23 @@ -'use strict'; -const { Network, networks, EvmRelayer, relay } = require('@axelar-network/axelar-local-dev'); -const helper = require("../helpers/helpers.js"); -const { ethers } = require("ethers"); -const fs = require('fs'); -require('dotenv').config(); +import * as dotenv from "dotenv"; +dotenv.config(); +import { Network, networks, EvmRelayer, relay } from '@axelar-network/axelar-local-dev'; +import { requireEnv, waitForReceipt } from "../helpers/helpers"; +import { ethers } from "ethers"; +import * as fs from "fs"; let relaying = false; const defaultEvmRelayer = new EvmRelayer(); async function main() { - let rootChainName = helper.requireEnv("ROOT_CHAIN_NAME"); - let rootRPCURL = helper.requireEnv("ROOT_RPC_URL"); - let rootChainID = helper.requireEnv("ROOT_CHAIN_ID"); - let childChainName = helper.requireEnv("CHILD_CHAIN_NAME"); - let childRPCURL = helper.requireEnv("CHILD_RPC_URL"); - let childChainID = helper.requireEnv("CHILD_CHAIN_ID"); - let axelarRootEOAKey = helper.requireEnv("AXELAR_ROOT_EOA_SECRET"); - let axelarChildEOAKey = helper.requireEnv("AXELAR_CHILD_EOA_SECRET"); - let axelarDeployerKey = helper.requireEnv("AXELAR_DEPLOYER_SECRET"); + let rootChainName = requireEnv("ROOT_CHAIN_NAME"); + let rootRPCURL = requireEnv("ROOT_RPC_URL"); + let rootChainID = requireEnv("ROOT_CHAIN_ID"); + let childChainName = requireEnv("CHILD_CHAIN_NAME"); + let childRPCURL = requireEnv("CHILD_RPC_URL"); + let childChainID = requireEnv("CHILD_CHAIN_ID"); + let axelarRootEOAKey = requireEnv("AXELAR_ROOT_EOA_SECRET"); + let axelarChildEOAKey = requireEnv("AXELAR_CHILD_EOA_SECRET"); + let axelarDeployerKey = requireEnv("AXELAR_DEPLOYER_SECRET"); // Create root chain. let rootProvider = new ethers.providers.JsonRpcProvider(rootRPCURL, Number(rootChainID)); @@ -75,23 +75,23 @@ async function main() { to: childChain.ownerWallet.address, value: ethers.utils.parseEther("35.0"), }) - await helper.waitForReceipt(resp.hash, childProvider); + await waitForReceipt(resp.hash, childProvider); resp = await axelarChildEOA.sendTransaction({ to: childChain.operatorWallet.address, value: ethers.utils.parseEther("35.0"), }) - await helper.waitForReceipt(resp.hash, childProvider); + await waitForReceipt(resp.hash, childProvider); resp = await axelarChildEOA.sendTransaction({ to: childChain.relayerWallet.address, value: ethers.utils.parseEther("35.0"), }) - await helper.waitForReceipt(resp.hash, childProvider); + await waitForReceipt(resp.hash, childProvider); for (let i = 0; i < 10; i++) { resp = await axelarChildEOA.sendTransaction({ to: childChain.adminWallets[i].address, value: ethers.utils.parseEther("35.0"), }) - await helper.waitForReceipt(resp.hash, childProvider); + await waitForReceipt(resp.hash, childProvider); } // Deploy child contracts. await childChain.deployConstAddressDeployer(); @@ -106,23 +106,23 @@ async function main() { to: rootChain.ownerWallet.address, value: ethers.utils.parseEther("35.0"), }) - await helper.waitForReceipt(resp.hash, rootProvider); + await waitForReceipt(resp.hash, rootProvider); resp = await axelarRootEOA.sendTransaction({ to: rootChain.operatorWallet.address, value: ethers.utils.parseEther("35.0"), }) - await helper.waitForReceipt(resp.hash, rootProvider); + await waitForReceipt(resp.hash, rootProvider); resp = await axelarRootEOA.sendTransaction({ to: rootChain.relayerWallet.address, value: ethers.utils.parseEther("35.0"), }) - await helper.waitForReceipt(resp.hash, rootProvider); + await waitForReceipt(resp.hash, rootProvider); for (let i = 0; i < 10; i++) { resp = await axelarRootEOA.sendTransaction({ to: rootChain.adminWallets[i].address, value: ethers.utils.parseEther("35.0"), }) - await helper.waitForReceipt(resp.hash, rootProvider); + await waitForReceipt(resp.hash, rootProvider); } // Deploy root contracts. await rootChain.deployConstAddressDeployer(); diff --git a/scripts/localdev/childchain.config.js b/scripts/localdev/childchain.config.ts similarity index 60% rename from scripts/localdev/childchain.config.js rename to scripts/localdev/childchain.config.ts index 9d968f23..b7df4ed5 100644 --- a/scripts/localdev/childchain.config.js +++ b/scripts/localdev/childchain.config.ts @@ -1,7 +1,7 @@ -/** @type import('hardhat/config').HardhatUserConfig */ -require("@nomicfoundation/hardhat-toolbox"); +import { HardhatUserConfig } from "hardhat/config"; +import "@nomicfoundation/hardhat-toolbox"; -module.exports = { +const config: HardhatUserConfig = { networks: { hardhat: { mining: { @@ -17,3 +17,4 @@ module.exports = { }, solidity: "0.8.19", }; +export default config; \ No newline at end of file diff --git a/scripts/localdev/childchain_setup.js b/scripts/localdev/childchain_setup.ts similarity index 62% rename from scripts/localdev/childchain_setup.js rename to scripts/localdev/childchain_setup.ts index e1f74557..889ffbd5 100644 --- a/scripts/localdev/childchain_setup.js +++ b/scripts/localdev/childchain_setup.ts @@ -1,13 +1,13 @@ -'use strict'; -const { ethers: hardhat } = require("hardhat"); -const { ethers } = require("ethers"); -const helper = require("../helpers/helpers.js"); -require('dotenv').config(); +import * as dotenv from "dotenv"; +dotenv.config(); +import { ethers as hardhat } from "hardhat"; +import { ethers } from "ethers"; +import { requireEnv } from "../helpers/helpers"; async function main() { - let childRPCURL = helper.requireEnv("CHILD_RPC_URL"); - let childChainID = helper.requireEnv("CHILD_CHAIN_ID"); - let childEOAKey = helper.requireEnv("CHILD_ADMIN_EOA_SECRET"); + let childRPCURL = requireEnv("CHILD_RPC_URL"); + let childChainID = requireEnv("CHILD_CHAIN_ID"); + let childEOAKey = requireEnv("CHILD_ADMIN_EOA_SECRET"); // Get child provider. let childProvider = new ethers.providers.JsonRpcProvider(childRPCURL, Number(childChainID)); diff --git a/scripts/localdev/ci.sh b/scripts/localdev/ci.sh index 003ea77a..b510e099 100755 --- a/scripts/localdev/ci.sh +++ b/scripts/localdev/ci.sh @@ -5,7 +5,7 @@ counter=1 while [ $counter -le 300 ] do echo "Waiting for chain and axelar setup... ${counter}" - SKIP_WAIT_FOR_CONFIRMATION=true SKIP_MULTISIG_CHECK=true node ../bootstrap/2_deployment_validation.js > /dev/null 2>&1 + SKIP_WAIT_FOR_CONFIRMATION=true SKIP_MULTISIG_CHECK=true npx ts-node ../bootstrap/2_deployment_validation.ts > /dev/null 2>&1 if [ $? -ne 0 ]; then sleep 1 ((counter++)) diff --git a/scripts/localdev/deploy.sh b/scripts/localdev/deploy.sh index 3fef152b..a770cc33 100755 --- a/scripts/localdev/deploy.sh +++ b/scripts/localdev/deploy.sh @@ -3,22 +3,22 @@ set -ex set -o pipefail # Verify deployment -SKIP_WAIT_FOR_CONFIRMATION=true SKIP_MULTISIG_CHECK=true node ../bootstrap/2_deployment_validation.js 2>&1 | tee -a bootstrap.out +SKIP_WAIT_FOR_CONFIRMATION=true SKIP_MULTISIG_CHECK=true npx ts-node ../bootstrap/2_deployment_validation.ts 2>&1 | tee -a bootstrap.out # Deploy child contracts -SKIP_WAIT_FOR_CONFIRMATION=true node ../bootstrap/3_child_deployment.js 2>&1 | tee -a bootstrap.out +SKIP_WAIT_FOR_CONFIRMATION=true npx ts-node ../bootstrap/3_child_deployment.ts 2>&1 | tee -a bootstrap.out # Deploy root contracts -SKIP_WAIT_FOR_CONFIRMATION=true node ../bootstrap/4_root_deployment.js 2>&1 | tee -a bootstrap.out +SKIP_WAIT_FOR_CONFIRMATION=true npx ts-node ../bootstrap/4_root_deployment.ts 2>&1 | tee -a bootstrap.out # Initialise child contracts -SKIP_WAIT_FOR_CONFIRMATION=true node ../bootstrap/5_child_initialisation.js 2>&1 | tee -a bootstrap.out +SKIP_WAIT_FOR_CONFIRMATION=true npx ts-node ../bootstrap/5_child_initialisation.ts 2>&1 | tee -a bootstrap.out # IMX Burning -SKIP_WAIT_FOR_CONFIRMATION=true node ../bootstrap/6_imx_burning.js 2>&1 | tee -a bootstrap.out +SKIP_WAIT_FOR_CONFIRMATION=true npx ts-node ../bootstrap/6_imx_burning.ts 2>&1 | tee -a bootstrap.out # IMX Rebalancing -SKIP_WAIT_FOR_CONFIRMATION=true node ../bootstrap/7_imx_rebalancing.js 2>&1 | tee -a bootstrap.out +SKIP_WAIT_FOR_CONFIRMATION=true npx ts-node ../bootstrap/7_imx_rebalancing.ts 2>&1 | tee -a bootstrap.out # Initialise root contracts -SKIP_WAIT_FOR_CONFIRMATION=true node ../bootstrap/8_root_initialisation.js 2>&1 | tee -a bootstrap.out \ No newline at end of file +SKIP_WAIT_FOR_CONFIRMATION=true npx ts-node ../bootstrap/8_root_initialisation.ts 2>&1 | tee -a bootstrap.out \ No newline at end of file diff --git a/scripts/localdev/rootchain.config.js b/scripts/localdev/rootchain.config.ts similarity index 60% rename from scripts/localdev/rootchain.config.js rename to scripts/localdev/rootchain.config.ts index 1177b63f..db622ae6 100644 --- a/scripts/localdev/rootchain.config.js +++ b/scripts/localdev/rootchain.config.ts @@ -1,7 +1,7 @@ -/** @type import('hardhat/config').HardhatUserConfig */ -require("@nomicfoundation/hardhat-toolbox"); +import { HardhatUserConfig } from "hardhat/config"; +import "@nomicfoundation/hardhat-toolbox"; -module.exports = { +const config: HardhatUserConfig = { networks: { hardhat: { mining: { @@ -16,4 +16,5 @@ module.exports = { } }, solidity: "0.8.19", -}; \ No newline at end of file +}; +export default config; \ No newline at end of file diff --git a/scripts/localdev/rootchain_setup.js b/scripts/localdev/rootchain_setup.ts similarity index 63% rename from scripts/localdev/rootchain_setup.js rename to scripts/localdev/rootchain_setup.ts index cc7e5b35..0bd6f26d 100644 --- a/scripts/localdev/rootchain_setup.js +++ b/scripts/localdev/rootchain_setup.ts @@ -1,18 +1,18 @@ -'use strict'; -const { ethers: hardhat } = require("hardhat"); -const { ethers, ContractFactory } = require("ethers"); -const helper = require("../helpers/helpers.js"); -const fs = require('fs'); -require('dotenv').config(); +import * as dotenv from "dotenv"; +dotenv.config(); +import { ethers as hardhat } from "hardhat"; +import { ethers } from "ethers"; +import { requireEnv, deployRootContract, waitForReceipt } from "../helpers/helpers"; +import * as fs from "fs"; async function main() { - let rootRPCURL = helper.requireEnv("ROOT_RPC_URL"); - let rootChainID = helper.requireEnv("ROOT_CHAIN_ID"); - let rootAdminKey = helper.requireEnv("ROOT_EOA_SECRET"); - let rootDeployerKey = helper.requireEnv("ROOT_DEPLOYER_SECRET"); - let axelarDeployerKey = helper.requireEnv("AXELAR_ROOT_EOA_SECRET"); - let rootTestKey = helper.requireEnv("TEST_ACCOUNT_SECRET"); - let rootRateAdminKey = helper.requireEnv("ROOT_BRIDGE_RATE_ADMIN_SECRET"); + let rootRPCURL = requireEnv("ROOT_RPC_URL"); + let rootChainID = requireEnv("ROOT_CHAIN_ID"); + let rootAdminKey = requireEnv("ROOT_EOA_SECRET"); + let rootDeployerKey = requireEnv("ROOT_DEPLOYER_SECRET"); + let axelarDeployerKey = requireEnv("AXELAR_ROOT_EOA_SECRET"); + let rootTestKey = requireEnv("TEST_ACCOUNT_SECRET"); + let rootRateAdminKey = requireEnv("ROOT_BRIDGE_RATE_ADMIN_SECRET"); // Get root provider. let rootProvider = new ethers.providers.JsonRpcProvider(rootRPCURL, Number(rootChainID)); @@ -39,38 +39,31 @@ async function main() { ]); // Deploy IMX contract - let IMXObj = JSON.parse(fs.readFileSync('../../out/ERC20PresetMinterPauser.sol/ERC20PresetMinterPauser.json', 'utf8')); console.log("Deploy IMX contract on root chain..."); - - let IMXFactory = new ContractFactory(IMXObj.abi, IMXObj.bytecode, admin); - let IMX = await IMXFactory.deploy("IMX Token", "IMX"); - let txn = IMX.deployTransaction; - await helper.waitForReceipt(txn.hash, rootProvider); + let IMX = await deployRootContract("ERC20PresetMinterPauser", admin, "IMX Token", "IMX"); + await waitForReceipt(IMX.deployTransaction.hash, rootProvider); console.log("IMX deployed at: " + IMX.address); // Deploy WETH contract - let WETHObj = JSON.parse(fs.readFileSync('../../out/WETH.sol/WETH.json', 'utf8')) console.log("Deploy WETH contract on root chain..."); - let WETHFactory = new ContractFactory(WETHObj.abi, WETHObj.bytecode, admin); - let WETH = await WETHFactory.deploy(); - txn = WETH.deployTransaction; - await helper.waitForReceipt(txn.hash, rootProvider); + let WETH = await deployRootContract("WETH", admin); + await waitForReceipt(WETH.deployTransaction.hash, rootProvider); console.log("WETH deployed at: " + WETH.address); // Mint 1100 IMX to root deployer let resp = await IMX.connect(admin).mint(rootDeployer.address, ethers.utils.parseEther("1100.0")); - await helper.waitForReceipt(resp.hash, rootProvider); + await waitForReceipt(resp.hash, rootProvider); // Transfer 1000 IMX to test wallet resp = await IMX.connect(admin).mint(testWallet.address, ethers.utils.parseEther("1000.0")) - await helper.waitForReceipt(resp.hash, rootProvider); + await waitForReceipt(resp.hash, rootProvider); // Transfer 0.1 ETH to root deployer resp = await admin.sendTransaction({ to: rootDeployer.address, value: ethers.utils.parseEther("0.1"), }) - await helper.waitForReceipt(resp.hash, rootProvider); + await waitForReceipt(resp.hash, rootProvider); // Transfer 500 ETH to axelar deployer resp = await admin.sendTransaction({ @@ -83,14 +76,14 @@ async function main() { to: testWallet.address, value: ethers.utils.parseEther("10.0"), }) - await helper.waitForReceipt(resp.hash, rootProvider); + await waitForReceipt(resp.hash, rootProvider); // Transfer 0.1 ETH to rate admin resp = await admin.sendTransaction({ to: rateAdminWallet.address, value: ethers.utils.parseEther("10.0"), }) - await helper.waitForReceipt(resp.hash, rootProvider); + await waitForReceipt(resp.hash, rootProvider); console.log("Root deployer now has " + ethers.utils.formatEther(await IMX.balanceOf(rootDeployer.address)) + " IMX."); console.log("Root deployer now has " + ethers.utils.formatEther(await rootProvider.getBalance(rootDeployer.address)) + " ETH."); diff --git a/scripts/localdev/start.sh b/scripts/localdev/start.sh index 47f7b972..74205d77 100755 --- a/scripts/localdev/start.sh +++ b/scripts/localdev/start.sh @@ -8,8 +8,8 @@ set -o pipefail cp .env.local .env # Start root & child chain. -npx hardhat node --config ./rootchain.config.js --port 8500 > /dev/null 2>&1 & -npx hardhat node --config ./childchain.config.js --port 8501 > /dev/null 2>&1 & +npx hardhat node --config ./rootchain.config.ts --port 8500 > /dev/null 2>&1 & +npx hardhat node --config ./childchain.config.ts --port 8501 > /dev/null 2>&1 & sleep 10 # trap ctrl-c and call ctrl_c() @@ -20,18 +20,18 @@ function ctrl_c() { } # Setup root & child chain. -npx hardhat run ./rootchain_setup.js --config ./rootchain.config.js --network localhost -npx hardhat run ./childchain_setup.js --config ./childchain.config.js --network localhost +npx hardhat run ./rootchain_setup.ts --config ./rootchain.config.ts --network localhost +npx hardhat run ./childchain_setup.ts --config ./childchain.config.ts --network localhost echo "Successfully setup root chain and child chain..." if [ -z ${LOCAL_CHAIN_ONLY+x} ]; then # Fund accounts - SKIP_WAIT_FOR_CONFIRMATION=true node ../bootstrap/1_deployer_funding.js 2>&1 | tee bootstrap.out - echo "Successfully run 1_deployer_funding.js..." + SKIP_WAIT_FOR_CONFIRMATION=true npx ts-node ../bootstrap/1_deployer_funding.ts 2>&1 | tee bootstrap.out + echo "Successfully run 1_deployer_funding.ts..." # Setup axelar - node axelar_setup.js + npx ts-node axelar_setup.ts ./stop.sh fi \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..0b9e4592 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2018", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "outDir": "dist", + "declaration": true, + "resolveJsonModule": true + } +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 130cde6f..e8988aaf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -336,7 +336,7 @@ ethereum-cryptography "^2.0.0" micro-ftch "^0.3.1" -"@ethersproject/abi@5.7.0", "@ethersproject/abi@^5.0.9", "@ethersproject/abi@^5.1.2", "@ethersproject/abi@^5.7.0": +"@ethersproject/abi@5.7.0", "@ethersproject/abi@^5.0.9", "@ethersproject/abi@^5.1.2", "@ethersproject/abi@^5.5.0", "@ethersproject/abi@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.7.0.tgz#b3f3e045bbbeed1af3947335c247ad625a44e449" integrity sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA== @@ -573,7 +573,7 @@ "@ethersproject/bytes" "^5.7.0" "@ethersproject/logger" "^5.7.0" -"@ethersproject/rlp@5.7.0", "@ethersproject/rlp@^5.7.0": +"@ethersproject/rlp@5.7.0", "@ethersproject/rlp@^5.5.0", "@ethersproject/rlp@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.7.0.tgz#de39e4d5918b9d74d46de93af80b7685a9c21304" integrity sha512-rBxzX2vK8mVF7b0Tol44t5Tb8gomOHkj5guL+HhzQ1yBh/ydjGnpw6at+X6Iw0Kp3OzzzkcKp8N9r0W4kYSs9w== @@ -713,6 +713,15 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@ledgerhq/cryptoassets@^11.2.0": + version "11.2.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/cryptoassets/-/cryptoassets-11.2.0.tgz#5594e262fc0aab6e02c1c2bdd950c019712fdaf2" + integrity sha512-O5fVIxzlwyR3YNEJJKUcGNyZW5Nf2lJtm0CPDWIfPaKERwvLPLfuJ5yUSHYBqpvYMGCCFldykiPdZ9XS3+fRaA== + dependencies: + axios "^1.6.0" + bs58check "^2.1.2" + invariant "2" + "@ledgerhq/cryptoassets@^5.27.2": version "5.53.0" resolved "https://registry.yarnpkg.com/@ledgerhq/cryptoassets/-/cryptoassets-5.53.0.tgz#11dcc93211960c6fd6620392e4dd91896aaabe58" @@ -730,11 +739,50 @@ rxjs "6" semver "^7.3.5" +"@ledgerhq/devices@^8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/devices/-/devices-8.1.0.tgz#39b12feabe1c7a99b86667bedf2eafbd125cf217" + integrity sha512-Vsdv84Nwzee0qhObdwVzhkxW1+h2cFoD1AWuU8N1V/2OJKiVS35A1qloSCF0oHapg+KTJvim8tr5rRvlkCYyzQ== + dependencies: + "@ledgerhq/errors" "^6.16.0" + "@ledgerhq/logs" "^6.12.0" + rxjs "^7.8.1" + semver "^7.3.5" + +"@ledgerhq/domain-service@^1.1.15": + version "1.1.15" + resolved "https://registry.yarnpkg.com/@ledgerhq/domain-service/-/domain-service-1.1.15.tgz#fb7664c61c83c0230f8aec35861ac81bdb605c0b" + integrity sha512-1X4MvNhVDTXCfOQckaUHsq/Qzn8xhFMcHjnLKOuCR5zNB8hYuTyg9e7JXURZ8W7/Qcn41rvIPxXBHwvMjWQBMA== + dependencies: + "@ledgerhq/errors" "^6.16.0" + "@ledgerhq/logs" "^6.12.0" + "@ledgerhq/types-live" "^6.43.0" + axios "^1.3.4" + eip55 "^2.1.1" + react "^18.2.0" + react-dom "^18.2.0" + "@ledgerhq/errors@^5.26.0", "@ledgerhq/errors@^5.50.0": version "5.50.0" resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-5.50.0.tgz#e3a6834cb8c19346efca214c1af84ed28e69dad9" integrity sha512-gu6aJ/BHuRlpU7kgVpy2vcYk6atjB4iauP2ymF7Gk0ez0Y/6VSMVSJvubeEQN+IV60+OBK0JgeIZG7OiHaw8ow== +"@ledgerhq/errors@^6.16.0": + version "6.16.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-6.16.0.tgz#0aaf16bbf649a3b43867746781b2e3adebf7fe3a" + integrity sha512-vnew6lf4jN6E+WI0DFhD4WY0uM8LYL8HCumtUr86hNwvmEfebi7LxxpJGmYfVQD5TgEC7NibYnQ+2q9XWAc02A== + +"@ledgerhq/evm-tools@^1.0.11": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@ledgerhq/evm-tools/-/evm-tools-1.0.11.tgz#d1352dcd3a8e971c5808f6a7e65439277bd1ee66" + integrity sha512-XfOQvEAzT3iD0hd7zNg8kioRXHnWdeLgs2/bwHeI9/pttzE+kTCjLhvIipYAeYVHg0gKaqecoygKdsuh6kS1fw== + dependencies: + "@ledgerhq/cryptoassets" "^11.2.0" + "@ledgerhq/live-env" "^0.7.0" + "@ledgerhq/live-network" "^1.1.9" + crypto-js "4.2.0" + ethers "5.7.2" + "@ledgerhq/hw-app-eth@5.27.2": version "5.27.2" resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-eth/-/hw-app-eth-5.27.2.tgz#65a2ed613a69340e0cd69c942147455ec513d006" @@ -746,6 +794,33 @@ bignumber.js "^9.0.1" rlp "^2.2.6" +"@ledgerhq/hw-app-eth@^6.35.0": + version "6.35.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-eth/-/hw-app-eth-6.35.0.tgz#30486e0e9221de92653985af08a6208fde770a9c" + integrity sha512-BJ39+biwuTXmiKuO2c5PbjJBdGMOSl7nHncuLFCwBXi0hYlHiELHQgEOjjPon418ltuCQyuDBiNMyIFOLikIRQ== + dependencies: + "@ethersproject/abi" "^5.5.0" + "@ethersproject/rlp" "^5.5.0" + "@ledgerhq/cryptoassets" "^11.2.0" + "@ledgerhq/domain-service" "^1.1.15" + "@ledgerhq/errors" "^6.16.0" + "@ledgerhq/evm-tools" "^1.0.11" + "@ledgerhq/hw-transport" "^6.30.0" + "@ledgerhq/hw-transport-mocker" "^6.28.0" + "@ledgerhq/logs" "^6.12.0" + "@ledgerhq/types-live" "^6.43.0" + axios "^1.3.4" + bignumber.js "^9.1.2" + +"@ledgerhq/hw-transport-mocker@^6.28.0": + version "6.28.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-mocker/-/hw-transport-mocker-6.28.0.tgz#a664220338b56d62c8aca0a10c98e28484e4a889" + integrity sha512-svUgIRdoc69b49MHncKikoRgWIqn7ZR3IHP+nq4TCTYn2nm5LILJYyf8osnCg8brsXdEY68z++fr++GyF9vUIw== + dependencies: + "@ledgerhq/hw-transport" "^6.30.0" + "@ledgerhq/logs" "^6.12.0" + rxjs "^7.8.1" + "@ledgerhq/hw-transport-node-hid-noevents@^5.26.0": version "5.51.1" resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-node-hid-noevents/-/hw-transport-node-hid-noevents-5.51.1.tgz#71f37f812e448178ad0bcc2258982150d211c1ab" @@ -757,6 +832,17 @@ "@ledgerhq/logs" "^5.50.0" node-hid "2.1.1" +"@ledgerhq/hw-transport-node-hid-noevents@^6.29.0": + version "6.29.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-node-hid-noevents/-/hw-transport-node-hid-noevents-6.29.0.tgz#10cbb260d9af3e961675ca88695d521dbc8c5964" + integrity sha512-JJM0NGOmFxCJ0IvbGlCo3KHYhkckn7QPNgBlGTrV/UDoMZdtDfp3R971jGUVInUmqYmHRDCGXRpjwgZRI7MJhg== + dependencies: + "@ledgerhq/devices" "^8.1.0" + "@ledgerhq/errors" "^6.16.0" + "@ledgerhq/hw-transport" "^6.30.0" + "@ledgerhq/logs" "^6.12.0" + node-hid "^2.1.2" + "@ledgerhq/hw-transport-node-hid@5.26.0": version "5.26.0" resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-node-hid/-/hw-transport-node-hid-5.26.0.tgz#69bc4f8067cdd9c09ef4aed0e0b3c58328936e4b" @@ -771,6 +857,20 @@ node-hid "1.3.0" usb "^1.6.3" +"@ledgerhq/hw-transport-node-hid@^6.28.0": + version "6.28.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-node-hid/-/hw-transport-node-hid-6.28.0.tgz#d8e67b1b8acd1110bcecff11326eb37a7f1f2475" + integrity sha512-kRGsT9YkudP8TbiaBWOtpgMje3gp7CbNHgAA4gdGM5Xri5Li0foEoIFqYZfWCS44NrPbDrsalWqj03HmQ2LDpg== + dependencies: + "@ledgerhq/devices" "^8.1.0" + "@ledgerhq/errors" "^6.16.0" + "@ledgerhq/hw-transport" "^6.30.0" + "@ledgerhq/hw-transport-node-hid-noevents" "^6.29.0" + "@ledgerhq/logs" "^6.12.0" + lodash "^4.17.21" + node-hid "^2.1.2" + usb "2.9.0" + "@ledgerhq/hw-transport-u2f@5.26.0": version "5.26.0" resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-u2f/-/hw-transport-u2f-5.26.0.tgz#b7d9d13193eb82b051fd7a838cd652372f907ec5" @@ -799,11 +899,62 @@ "@ledgerhq/errors" "^5.50.0" events "^3.3.0" +"@ledgerhq/hw-transport@^6.30.0": + version "6.30.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport/-/hw-transport-6.30.0.tgz#9c8a8f2c8281fbc4a3db1d1f3ac44a456b38281a" + integrity sha512-wrAwn/wCAaGP2Yuy78cLyqmQNzbuDvUv4gJYF/UO4djvUz0jjvD2w5kxRWxF/W93vyKT+/RplRtFk3CJzD3e3A== + dependencies: + "@ledgerhq/devices" "^8.1.0" + "@ledgerhq/errors" "^6.16.0" + "@ledgerhq/logs" "^6.12.0" + events "^3.3.0" + +"@ledgerhq/live-env@^0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/live-env/-/live-env-0.7.0.tgz#b1fd3922111cb9c9410ffbed2010e506adacd093" + integrity sha512-Q77gmJLafjKmc23CbRgBD1Bm1MVatISo0JEWDX/nWZnWUK3IVwp8VxxJDHW4P7TlpsuCKCgCtd0C1gxZDWI/RA== + dependencies: + rxjs "^7.8.1" + utility-types "^3.10.0" + +"@ledgerhq/live-network@^1.1.9": + version "1.1.9" + resolved "https://registry.yarnpkg.com/@ledgerhq/live-network/-/live-network-1.1.9.tgz#ecf6b14cb382384665b29ffb6d8541044515b8e2" + integrity sha512-uwtVSzL88VtClmfkUTW5plEgdBqXnmT1vhTC7k/bCOf3CPpvkPQ2NLuutT1GHPkHUu+BjAweM6uUKl9JDwGs1g== + dependencies: + "@ledgerhq/errors" "^6.16.0" + "@ledgerhq/live-env" "^0.7.0" + "@ledgerhq/live-promise" "^0.0.3" + "@ledgerhq/logs" "^6.12.0" + axios "0.26.1" + invariant "^2.2.2" + lru-cache "^7.14.1" + +"@ledgerhq/live-promise@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@ledgerhq/live-promise/-/live-promise-0.0.3.tgz#432693468ddd48f94a24437c01791d59d393adbc" + integrity sha512-/49dRz5XoxUw4TFq0kytU2Vz9w+FoGgG28U8RH9nuUWVPjVhAPvhY/QXUQA+7qqaorEIAYPHF0Rappalawhr+g== + dependencies: + "@ledgerhq/logs" "^6.12.0" + "@ledgerhq/logs@^5.26.0", "@ledgerhq/logs@^5.50.0": version "5.50.0" resolved "https://registry.yarnpkg.com/@ledgerhq/logs/-/logs-5.50.0.tgz#29c6419e8379d496ab6d0426eadf3c4d100cd186" integrity sha512-swKHYCOZUGyVt4ge0u8a7AwNcA//h4nx5wIi0sruGye1IJ5Cva0GyK9L2/WdX+kWVTKp92ZiEo1df31lrWGPgA== +"@ledgerhq/logs@^6.12.0": + version "6.12.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/logs/-/logs-6.12.0.tgz#ad903528bf3687a44da435d7b2479d724d374f5d" + integrity sha512-ExDoj1QV5eC6TEbMdLUMMk9cfvNKhhv5gXol4SmULRVCx/3iyCPhJ74nsb3S0Vb+/f+XujBEj3vQn5+cwS0fNA== + +"@ledgerhq/types-live@^6.43.0": + version "6.43.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/types-live/-/types-live-6.43.0.tgz#48491fb6f1a24b012e0b78188de7ad1cd1814bab" + integrity sha512-NvSWPefZ54BLTTMdljO2eS3j1Jbj4O+j/2OWZfyt6T1qMrU1OwORkIn7weuyqR0Y01mTos0sjST7r10MqtauJg== + dependencies: + bignumber.js "^9.1.2" + rxjs "^7.8.1" + "@metamask/eth-sig-util@^4.0.0": version "4.0.1" resolved "https://registry.yarnpkg.com/@metamask/eth-sig-util/-/eth-sig-util-4.0.1.tgz#3ad61f6ea9ad73ba5b19db780d40d9aae5157088" @@ -1626,6 +1777,11 @@ resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== +"@types/w3c-web-usb@^1.0.6": + version "1.0.10" + resolved "https://registry.yarnpkg.com/@types/w3c-web-usb/-/w3c-web-usb-1.0.10.tgz#cf89cccd2d93b6245e784c19afe0a9f5038d4528" + integrity sha512-CHgUI5kTc/QLMP8hODUHhge0D4vx+9UiAwIGiT0sTy/B2XpdX1U5rJt6JSISgr6ikRT7vxV9EVAFeYZqUnl1gQ== + abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" @@ -1968,6 +2124,13 @@ available-typed-arrays@^1.0.5: resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== +axios@0.26.1: + version "0.26.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9" + integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA== + dependencies: + follow-redirects "^1.14.8" + axios@0.27.2, axios@^0.27.2: version "0.27.2" resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" @@ -1983,7 +2146,7 @@ axios@^0.21.2: dependencies: follow-redirects "^1.14.0" -axios@^1.5.1: +axios@^1.3.4, axios@^1.5.1, axios@^1.6.0: version "1.6.2" resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.2.tgz#de67d42c755b571d3e698df1b6504cde9b0ee9f2" integrity sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A== @@ -2034,7 +2197,7 @@ bigint-crypto-utils@^3.0.23: resolved "https://registry.yarnpkg.com/bigint-crypto-utils/-/bigint-crypto-utils-3.3.0.tgz#72ad00ae91062cf07f2b1def9594006c279c1d77" integrity sha512-jOTSb+drvEDxEq6OuUybOAv/xxoh3cuYRUIPyu8sSHQNKM303UQ2R1DAo45o1AkcIXw6fzbaFI1+xGGdaXs2lg== -bignumber.js@^9.0.1: +bignumber.js@^9.0.1, bignumber.js@^9.1.2: version "9.1.2" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug== @@ -2608,6 +2771,11 @@ cross-fetch@^3.1.5: resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow== +crypto-js@4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631" + integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q== + death@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/death/-/death-1.1.0.tgz#01aa9c401edd92750514470b8266390c66c67318" @@ -2716,6 +2884,11 @@ detect-libc@^1.0.3: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== +detect-libc@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.2.tgz#8ccf2ba9315350e1241b88d0ac3b0e1fbd99605d" + integrity sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw== + detect-port@^1.3.0: version "1.5.1" resolved "https://registry.yarnpkg.com/detect-port/-/detect-port-1.5.1.tgz#451ca9b6eaf20451acb0799b8ab40dff7718727b" @@ -2760,6 +2933,13 @@ dotenv@^16.0.2: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== +eip55@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/eip55/-/eip55-2.1.1.tgz#28b743c4701ac3c811b1e9fe67e39cf1d0781b96" + integrity sha512-WcagVAmNu2Ww2cDUfzuWVntYwFxbvZ5MvIyLZpMjTTkjD6sCvkGOiS86jTppzu9/gWsc8isLHAeMBWK02OnZmA== + dependencies: + keccak "^3.0.3" + elliptic@6.5.4, elliptic@^6.5.2, elliptic@^6.5.3, elliptic@^6.5.4: version "6.5.4" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" @@ -3312,7 +3492,7 @@ flat@^5.0.2: resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== -follow-redirects@^1.12.1, follow-redirects@^1.14.0, follow-redirects@^1.14.9, follow-redirects@^1.15.0: +follow-redirects@^1.12.1, follow-redirects@^1.14.0, follow-redirects@^1.14.8, follow-redirects@^1.14.9, follow-redirects@^1.15.0: version "1.15.3" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== @@ -3984,7 +4164,7 @@ interpret@^1.0.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== -invariant@2: +invariant@2, invariant@^2.2.2: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== @@ -4303,7 +4483,7 @@ keccak@3.0.2: node-gyp-build "^4.2.0" readable-stream "^3.6.0" -keccak@^3.0.0, keccak@^3.0.2: +keccak@^3.0.0, keccak@^3.0.2, keccak@^3.0.3: version "3.0.4" resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.4.tgz#edc09b89e633c0549da444432ecf062ffadee86d" integrity sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q== @@ -4459,7 +4639,7 @@ long@^4.0.0: resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== -loose-envify@^1.0.0: +loose-envify@^1.0.0, loose-envify@^1.1.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -4492,6 +4672,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lru-cache@^7.14.1: + version "7.18.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" + integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== + lru_map@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" @@ -4802,6 +4987,13 @@ node-abi@^2.18.0, node-abi@^2.21.0, node-abi@^2.7.0: dependencies: semver "^5.4.1" +node-abi@^3.3.0: + version "3.51.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.51.0.tgz#970bf595ef5a26a271307f8a4befa02823d4e87d" + integrity sha512-SQkEP4hmNWjlniS5zdnfIXTk1x7Ome85RDzHlTbBtzE97Gfwz/Ipw4v/Ryk20DWIy3yCNVLVlGKApCnmvYoJbA== + dependencies: + semver "^7.3.5" + node-addon-api@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32" @@ -4817,6 +5009,11 @@ node-addon-api@^4.2.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f" integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ== +node-addon-api@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-6.1.0.tgz#ac8470034e58e67d0c6f1204a18ae6995d9c0d76" + integrity sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA== + node-emoji@^1.10.0: version "1.11.0" resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.11.0.tgz#69a0150e6946e2f115e9d7ea4df7971e2628301c" @@ -4841,6 +5038,11 @@ node-gyp-build@^4.2.0, node-gyp-build@^4.3.0: resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.7.0.tgz#749f0033590b2a89ac8edb5e0775f95f5ae86d15" integrity sha512-PbZERfeFdrHQOOXiAKOY0VPbykZy90ndPKk0d+CFDegTKmWp1VgOTz2xACVbr1BjCWxrQp68CXtvNsveFhqDJg== +node-gyp-build@^4.5.0: + version "4.7.1" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.7.1.tgz#cd7d2eb48e594874053150a9418ac85af83ca8f7" + integrity sha512-wTSrZ+8lsRRa3I3H8Xr65dLWSgCvY2l4AOnaeKdPA9TB/WYMPaTcrzf3rXvFoVvjKNVnu0CcWSx54qq9GKRUYg== + node-hid@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/node-hid/-/node-hid-1.3.0.tgz#346a468505cee13d69ccd760052cbaf749f66a41" @@ -4860,6 +5062,15 @@ node-hid@2.1.1: node-addon-api "^3.0.2" prebuild-install "^6.0.0" +node-hid@^2.1.2: + version "2.2.0" + resolved "https://registry.yarnpkg.com/node-hid/-/node-hid-2.2.0.tgz#33e039e7530a7bfe2b7a25f0a2f9496af8b02236" + integrity sha512-vj48zh9j555DZzUhMc8tk/qw6xPFrDyPBH1ST1Z/hWaA/juBJw7IuSxPeOgpzNFNU36mGYj+THioRMt1xOdm/g== + dependencies: + bindings "^1.5.0" + node-addon-api "^3.0.2" + prebuild-install "^7.1.1" + node-port-check@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/node-port-check/-/node-port-check-2.0.1.tgz#72cae367d3ca906b0903b261ce818c297aade11f" @@ -5172,6 +5383,24 @@ prebuild-install@^6.0.0: tar-fs "^2.0.0" tunnel-agent "^0.6.0" +prebuild-install@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45" + integrity sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw== + dependencies: + detect-libc "^2.0.0" + expand-template "^2.0.3" + github-from-package "0.0.0" + minimist "^1.2.3" + mkdirp-classic "^0.5.3" + napi-build-utils "^1.0.1" + node-abi "^3.3.0" + pump "^3.0.0" + rc "^1.2.7" + simple-get "^4.0.0" + tar-fs "^2.0.0" + tunnel-agent "^0.6.0" + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -5299,6 +5528,21 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-dom@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" + integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.0" + +react@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" + integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== + dependencies: + loose-envify "^1.1.0" + readable-stream@^2.0.6, readable-stream@^2.2.2: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" @@ -5493,6 +5737,13 @@ rxjs@6: dependencies: tslib "^1.9.0" +rxjs@^7.8.1: + version "7.8.1" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" + integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== + dependencies: + tslib "^2.1.0" + safe-array-concat@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.1.tgz#91686a63ce3adbea14d61b14c99572a8ff84754c" @@ -5547,6 +5798,13 @@ sc-istanbul@^0.4.5: which "^1.1.1" wordwrap "^1.0.0" +scheduler@^0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" + integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== + dependencies: + loose-envify "^1.1.0" + scrypt-js@3.0.1, scrypt-js@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/scrypt-js/-/scrypt-js-3.0.1.tgz#d314a57c2aef69d1ad98a138a21fe9eafa9ee312" @@ -5684,6 +5942,15 @@ simple-get@^3.0.3: once "^1.3.1" simple-concat "^1.0.0" +simple-get@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543" + integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA== + dependencies: + decompress-response "^6.0.0" + once "^1.3.1" + simple-concat "^1.0.0" + slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" @@ -6174,6 +6441,11 @@ tslib@^1.9.0, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@^2.1.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + tsort@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/tsort/-/tsort-0.0.1.tgz#e2280f5e817f8bf4275657fd0f9aebd44f5a2786" @@ -6362,6 +6634,15 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +usb@2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/usb/-/usb-2.9.0.tgz#8ae3b175f93bee559400bff33491eee63406b6a2" + integrity sha512-G0I/fPgfHUzWH8xo2KkDxTTFruUWfppgSFJ+bQxz/kVY2x15EQ/XDB7dqD1G432G4gBG4jYQuF3U7j/orSs5nw== + dependencies: + "@types/w3c-web-usb" "^1.0.6" + node-addon-api "^6.0.0" + node-gyp-build "^4.5.0" + usb@^1.6.3: version "1.9.2" resolved "https://registry.yarnpkg.com/usb/-/usb-1.9.2.tgz#fb6b36f744ecc707a196c45a6ec72442cb6f2b73" @@ -6394,6 +6675,11 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== +utility-types@^3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.10.0.tgz#ea4148f9a741015f05ed74fd615e1d20e6bed82b" + integrity sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg== + uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" From 68faca9dc1a71653f90e76b1d53dac4290a5c496 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Mon, 27 Nov 2023 16:27:25 +1000 Subject: [PATCH 002/155] Fix mocha issue --- .mocharc.json | 7 +++++++ package.json | 4 ++-- scripts/bootstrap/README.md | 2 +- scripts/e2e/README.md | 2 +- scripts/localdev/README.md | 2 +- 5 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 .mocharc.json diff --git a/.mocharc.json b/.mocharc.json new file mode 100644 index 00000000..5c5a28b0 --- /dev/null +++ b/.mocharc.json @@ -0,0 +1,7 @@ +{ + "extensions": ["ts"], + "node-option": [ + "experimental-specifier-resolution=node", + "loader=ts-node/esm" + ] +} \ No newline at end of file diff --git a/package.json b/package.json index b4e5ca0a..2e6cab40 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ "lint": "forge fmt", "local:start": "cd scripts/localdev; ./start.sh", "local:setup": "cd scripts/localdev; ./deploy.sh", - "local:test": "cd scripts/localdev; npx mocha --require mocha-suppress-logs ../e2e/", - "local:ci": "cd scripts/localdev; ./ci.sh && ./deploy.sh && npx mocha --require mocha-suppress-logs ../e2e/ && ./stop.sh", + "local:test": "cd scripts/localdev; npx mocha --require mocha-suppress-logs ../e2e/e2e.ts", + "local:ci": "cd scripts/localdev; ./ci.sh && ./deploy.sh && npx mocha --require mocha-suppress-logs ../e2e/e2e.ts && ./stop.sh", "local:chainonly": "cd scripts/localdev; LOCAL_CHAIN_ONLY=true ./start.sh", "local:axelaronly": "cd scripts/localdev; node axelar_setup.js", "stop": "cd scripts/localdev; ./stop.sh" diff --git a/scripts/bootstrap/README.md b/scripts/bootstrap/README.md index 2dda4bc7..f6b9d3df 100644 --- a/scripts/bootstrap/README.md +++ b/scripts/bootstrap/README.md @@ -197,5 +197,5 @@ TEST_ACCOUNT_SECRET= ``` 13. Test bridge functions ``` -npx mocha --require mocha-suppress-logs ../e2e/ 2>&1 | tee -a bootstrap.out +npx mocha --require mocha-suppress-logs ../e2e/e2e.ts 2>&1 | tee -a bootstrap.out ``` \ No newline at end of file diff --git a/scripts/e2e/README.md b/scripts/e2e/README.md index 8fb2cd88..e3c39e80 100644 --- a/scripts/e2e/README.md +++ b/scripts/e2e/README.md @@ -51,5 +51,5 @@ TEST_ACCOUNT_SECRET= 3. Run end to end tests ``` -npx mocha --require mocha-suppress-logs . +npx mocha --require mocha-suppress-logs ./e2e.ts ``` \ No newline at end of file diff --git a/scripts/localdev/README.md b/scripts/localdev/README.md index fb294cab..00c6d7d9 100644 --- a/scripts/localdev/README.md +++ b/scripts/localdev/README.md @@ -31,5 +31,5 @@ The addresses of deployed contracts will be saved in: To run end to end tests against local development network: ``` -npx mocha --require mocha-suppress-logs ../e2e/ +npx mocha --require mocha-suppress-logs ../e2e/e2e.ts ``` \ No newline at end of file From ac227d3eaccc21a9de9ec30cb5a2ebb14f34f8d4 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Sun, 3 Dec 2023 00:13:52 +1000 Subject: [PATCH 003/155] Fix issues --- package.json | 8 +- scripts/bootstrap/.env.example | 90 +++----- scripts/bootstrap/0_pre_validation.ts | 148 +++++++++++++ scripts/bootstrap/1_deployer_funding.ts | 76 ++++--- scripts/bootstrap/2_deployment_validation.ts | 2 - scripts/bootstrap/6_imx_burning.ts | 100 ++++----- scripts/bootstrap/7_imx_rebalancing.ts | 53 +++-- scripts/bootstrap/9_test_preparation.ts | 71 ++++++ scripts/bootstrap/README.md | 134 +++++------- scripts/deploy/.env.example | 83 +++---- scripts/deploy/README.md | 82 +++---- scripts/deploy/child_deployment.ts | 207 ++++++++++++------ scripts/deploy/child_initialisation.ts | 65 +++--- scripts/deploy/root_deployment.ts | 176 ++++++++++----- scripts/deploy/root_initialisation.ts | 86 +++----- .../e2e/.root.bridge.contracts.json.example | 3 +- scripts/e2e/README.md | 5 +- scripts/e2e/e2e.ts | 110 +++++++--- scripts/helpers/helpers.ts | 81 ++++++- scripts/localdev/.env.local | 84 +++---- scripts/localdev/README.md | 2 +- scripts/localdev/childchain.config.ts | 1 + scripts/localdev/childchain_setup.ts | 9 +- scripts/localdev/deploy.sh | 5 +- scripts/localdev/rootchain.config.ts | 1 + scripts/localdev/rootchain_setup.ts | 49 ++--- scripts/localdev/start.sh | 5 +- 27 files changed, 1001 insertions(+), 735 deletions(-) create mode 100644 scripts/bootstrap/0_pre_validation.ts create mode 100644 scripts/bootstrap/9_test_preparation.ts diff --git a/package.json b/package.json index 2e6cab40..10de3b11 100644 --- a/package.json +++ b/package.json @@ -10,11 +10,11 @@ "test": "forge test", "lint": "forge fmt", "local:start": "cd scripts/localdev; ./start.sh", - "local:setup": "cd scripts/localdev; ./deploy.sh", - "local:test": "cd scripts/localdev; npx mocha --require mocha-suppress-logs ../e2e/e2e.ts", - "local:ci": "cd scripts/localdev; ./ci.sh && ./deploy.sh && npx mocha --require mocha-suppress-logs ../e2e/e2e.ts && ./stop.sh", + "local:setup": "cd scripts/localdev; rm -rf .child.bridge.contracts.json .root.bridge.contracts.json; ./deploy.sh", + "local:test": "cd scripts/localdev; LONG_WAIT=0 SHORT_WAIT=0 npx mocha --require mocha-suppress-logs ../e2e/e2e.ts", + "local:ci": "cd scripts/localdev; rm -rf .child.bridge.contracts.json .root.bridge.contracts.json; ./ci.sh && ./deploy.sh && LONG_WAIT=0 SHORT_WAIT=0 npx mocha --require mocha-suppress-logs ../e2e/e2e.ts && ./stop.sh", "local:chainonly": "cd scripts/localdev; LOCAL_CHAIN_ONLY=true ./start.sh", - "local:axelaronly": "cd scripts/localdev; node axelar_setup.js", + "local:axelaronly": "cd scripts/localdev; npx ts-node axelar_setup.ts", "stop": "cd scripts/localdev; ./stop.sh" }, "author": "", diff --git a/scripts/bootstrap/.env.example b/scripts/bootstrap/.env.example index 1806dcaf..c64b22bb 100644 --- a/scripts/bootstrap/.env.example +++ b/scripts/bootstrap/.env.example @@ -1,82 +1,44 @@ -# Set prior to 1_deployer_funding.js +# Set prior to 0_pre_validation.js +# Name of the child chain MUST match Axelar's definition. CHILD_CHAIN_NAME= +# The RPC URL of child chain. CHILD_RPC_URL= +# The chain ID of the child chain. CHILD_CHAIN_ID= +# Name of the root chain MUST match Axelar's definition. ROOT_CHAIN_NAME= +# The RPC URL of root chain. ROOT_RPC_URL= +# The chain ID of the root chain. ROOT_CHAIN_ID= -## The admin EOA address on the child chain. -CHILD_ADMIN_ADDR= -## The private key for the admin EOA or "ledger" if using hardware wallet. -CHILD_ADMIN_EOA_SECRET= -## The ledger index for the admin EOA, required if using ledger. -CHILD_ADMIN_EOA_LEDGER_INDEX= -## The deployer address on child chain. -CHILD_DEPLOYER_ADDR= -## The private key for the deployer on child chain or "ledger" if using hardware wallet. -CHILD_DEPLOYER_SECRET= -## The ledger index for the deployer on child chain, required if using ledger. -CHILD_DEPLOYER_LEDGER_INDEX= -## The amount of fund deployer required on L2, unit is in IMX or 10^18 Wei. -CHILD_DEPLOYER_FUND= -## The deployer address on root chain. -ROOT_DEPLOYER_ADDR= -## The private key for the deployer on root chain or "ledger" if using hardware wallet. -ROOT_DEPLOYER_SECRET= -## The ledger index for the deployer on root chain, required if using ledger. -ROOT_DEPLOYER_LEDGER_INDEX= -## The private key for rate admin or "ledger" if using hardware wallet. -ROOT_BRIDGE_RATE_ADMIN_SECRET= -## The ledger index for the rate admin, required if using ledger. -ROOT_BRIDGE_RATE_ADMIN_LEDGER_INDEX= +## The deployer address on child & root chains. +DEPLOYER_ADDR= +## The private key for the deployer on child & root chains or "ledger" if using hardware wallet. +DEPLOYER_SECRET= +## The ledger index for the deployer on child & root chains, required if using ledger. +DEPLOYER_LEDGER_INDEX= +## The nonce reserved deployer address on child & root chains. +NONCE_RESERVED_DEPLOYER_ADDR= +## The nonce reserved deployer, or "ledger" if using hardware wallet. +NONCE_RESERVED_DEPLOYER_SECRET= +## The ledger index for the nonce reserved deployer. +NONCE_RESERVED_DEPLOYER_INDEX= +## The reserved nonce for token template deployment. +NONCE_RESERVED= ## The IMX token address on root chain. ROOT_IMX_ADDR= ## The Wrapped ETH token address on the root chain. ROOT_WETH_ADDR= -## The Axelar address for receive initial funding on the child chain. +## The Axelar address to receive initial funding on the child chain. AXELAR_EOA= ## The amount of fund Axelar requested, unit is in IMX or 10^18 Wei. AXELAR_FUND= +## The amount of fund deployer to be left with after bootstrapping on L2, unit is in IMX or 10^18 Wei. +CHILD_DEPLOYER_FUND= +## The amount of fund nonce reserved deployer required on L2, unit is in IMX or 10^18 Wei. +CHILD_NONCE_RESERVED_DEPLOYER_FUND= ## The maximum amount of IMX that can be deposited to L2, unit is in IMX or 10^18 Wei. IMX_DEPOSIT_LIMIT= -## The address to perform child bridge upgrade. -CHILD_PROXY_ADMIN= -## The address to be assigned with DEFAULT_ADMIN_ROLE in child bridge. -CHILD_BRIDGE_DEFAULT_ADMIN= -## The address to be assigned with PAUSER_ROLE in child bridge. -CHILD_BRIDGE_PAUSER= -## The address to be assigned with UNPAUSER_ROLE in child bridge. -CHILD_BRIDGE_UNPAUSER= -## The address to be assigned with ADAPTOR_MANAGER_ROLE in child bridge. -CHILD_BRIDGE_ADAPTOR_MANAGER= -## The address to be assigned with DEFAULT_ADMIN_ROLE in child adaptor. -CHILD_ADAPTOR_DEFAULT_ADMIN= -## The address to be assigned with BRIDGE_MANAGER_ROLE in child adaptor. -CHILD_ADAPTOR_BRIDGE_MANAGER= -## The address to be assigned with GAS_SERVICE_MANAGER_ROLE in child adaptor. -CHILD_ADAPTOR_GAS_SERVICE_MANAGER= -## The address to be assigned with TARGET_MANAGER_ROLE in child adaptor. -CHILD_ADAPTOR_TARGET_MANAGER= -## The address to perform root adaptor upgrade. -ROOT_PROXY_ADMIN= -## The address to be assigned with DEFAULT_ADMIN_ROLE in root bridge. -ROOT_BRIDGE_DEFAULT_ADMIN= -## The address to be assigned with PAUSER_ROLE in root bridge. -ROOT_BRIDGE_PAUSER= -## The address to be assigned with UNPAUSER_ROLE in root bridge. -ROOT_BRIDGE_UNPAUSER= -## The address to be assigned with VARIABLE_MANAGER_ROLE in root bridge. -ROOT_BRIDGE_VARIABLE_MANAGER= -## The address to be assigned with ADAPTOR_MANAGER_ROLE in root bridge. -ROOT_BRIDGE_ADAPTOR_MANAGER= -## The address to be assigned with DEFAULT_ADMIN_ROLE in root adaptor. -ROOT_ADAPTOR_DEFAULT_ADMIN= -## The address to be assigned with BRIDGE_MANAGER_ROLE in root adaptor. -ROOT_ADAPTOR_BRIDGE_MANAGER= -## The address to be assigned with GAS_SERVICE_MANAGER_ROLE in root adaptor. -ROOT_ADAPTOR_GAS_SERVICE_MANAGER= -## The address to be assigned with TARGET_MANAGER_ROLE in root adaptor. -ROOT_ADAPTOR_TARGET_MANAGER= ## The capacity of the rate limit policy of IMX token, unit is in 10^18. RATE_LIMIT_IMX_CAPACITY= ## The refill rate of the rate limit policy of IMX token, unit is in 10^18. diff --git a/scripts/bootstrap/0_pre_validation.ts b/scripts/bootstrap/0_pre_validation.ts new file mode 100644 index 00000000..874a5515 --- /dev/null +++ b/scripts/bootstrap/0_pre_validation.ts @@ -0,0 +1,148 @@ +// Pre validation +import * as dotenv from "dotenv"; +dotenv.config(); +import { ethers } from "ethers"; +import { requireEnv, hasDuplicates } from "../helpers/helpers"; +import { LedgerSigner } from "../helpers/ledger_signer"; + +// The total supply of IMX +const TOTAL_SUPPLY = "2000000000"; + +// The contract ABI of IMX on L1. +const IMX_ABI = `[{"inputs":[{"internalType":"address","name":"minter","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"previousAdminRole","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"newAdminRole","type":"bytes32"}],"name":"RoleAdminChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleGranted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleRevoked","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[],"name":"DEFAULT_ADMIN_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MINTER_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"cap","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"subtractedValue","type":"uint256"}],"name":"decreaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"}],"name":"getRoleAdmin","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"grantRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"hasRole","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"addedValue","type":"uint256"}],"name":"increaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"mint","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"renounceRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"revokeRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}]`; + +function tryThrow(errorMsg: string) { + if (process.env["THROW_ON_FAIL"] != null) { + throw(errorMsg); + } else { + console.log(errorMsg); + } +} + +async function run() { + console.log("=======Start Pre Validation======="); + + // Check environment variables + requireEnv("CHILD_CHAIN_NAME"); + let childRPCURL = requireEnv("CHILD_RPC_URL"); + let childChainID = requireEnv("CHILD_CHAIN_ID"); + requireEnv("ROOT_CHAIN_NAME"); + let rootRPCURL = requireEnv("ROOT_RPC_URL"); + let rootChainID = requireEnv("ROOT_CHAIN_ID"); + let deployerAddr = requireEnv("DEPLOYER_ADDR"); + let deployerSecret = requireEnv("DEPLOYER_SECRET"); + let reservedDeployerAddr = requireEnv("NONCE_RESERVED_DEPLOYER_ADDR"); + let reservedDeployerSecret = requireEnv("NONCE_RESERVED_DEPLOYER_SECRET"); + Number(requireEnv("NONCE_RESERVED")); + let rootIMXAddr = requireEnv("ROOT_IMX_ADDR"); + let rootWETHAddr = requireEnv("ROOT_WETH_ADDR"); + let axelarEOA = requireEnv("AXELAR_EOA"); + let axelarFund = requireEnv("AXELAR_FUND"); + let childDeployerFund = requireEnv("CHILD_DEPLOYER_FUND"); + let childReservedDeployerFund = requireEnv("CHILD_NONCE_RESERVED_DEPLOYER_FUND"); + let imxDepositLimit = requireEnv("IMX_DEPOSIT_LIMIT"); + requireEnv("RATE_LIMIT_IMX_CAPACITY"); + requireEnv("RATE_LIMIT_IMX_REFILL_RATE"); + requireEnv("RATE_LIMIT_IMX_LARGE_THRESHOLD"); + requireEnv("RATE_LIMIT_ETH_CAPACITY"); + requireEnv("RATE_LIMIT_ETH_REFILL_RATE"); + requireEnv("RATE_LIMIT_ETH_LARGE_THRESHOLD"); + requireEnv("RATE_LIMIT_USDC_ADDR"); + requireEnv("RATE_LIMIT_USDC_CAPACITY"); + requireEnv("RATE_LIMIT_USDC_REFILL_RATE"); + requireEnv("RATE_LIMIT_USDC_LARGE_THRESHOLD"); + requireEnv("RATE_LIMIT_GU_ADDR"); + requireEnv("RATE_LIMIT_GU_CAPACITY"); + requireEnv("RATE_LIMIT_GU_REFILL_RATE"); + requireEnv("RATE_LIMIT_GU_LARGE_THRESHOLD"); + requireEnv("RATE_LIMIT_CHECKMATE_ADDR"); + requireEnv("RATE_LIMIT_CHECKMATE_CAPACITY"); + requireEnv("RATE_LIMIT_CHECKMATE_REFILL_RATE"); + requireEnv("RATE_LIMIT_CHECKMATE_LARGE_THRESHOLD"); + requireEnv("RATE_LIMIT_GOG_ADDR"); + requireEnv("RATE_LIMIT_GOG_CAPACITY"); + requireEnv("RATE_LIMIT_GOG_REFILL_RATE"); + requireEnv("RATE_LIMIT_GOG_LARGE_THRESHOLD"); + + const childProvider = new ethers.providers.JsonRpcProvider(childRPCURL, Number(childChainID)); + const rootProvider = new ethers.providers.JsonRpcProvider(rootRPCURL, Number(rootChainID)); + let deployerWallet; + if (deployerSecret == "ledger") { + let index = requireEnv("DEPLOYER_LEDGER_INDEX"); + const derivationPath = `m/44'/60'/${parseInt(index)}'/0/0`; + deployerWallet = new LedgerSigner(childProvider, derivationPath); + } else { + deployerWallet = new ethers.Wallet(deployerSecret, childProvider); + } + let reservedWallet; + if (reservedDeployerSecret == "ledger") { + let index = requireEnv("NONCE_RESERVED_DEPLOYER_INDEX"); + const derivationPath = `m/44'/60'/${parseInt(index)}'/0/0`; + reservedWallet = new LedgerSigner(childProvider, derivationPath); + } else { + reservedWallet = new ethers.Wallet(reservedDeployerSecret, childProvider); + } + + // Check deployer address matches deployer addr + let actualDeployerAddress = await deployerWallet.getAddress(); + if (actualDeployerAddress != deployerAddr) { + tryThrow("Deployer addresses mismatch, expect " + deployerAddr + " actual " + actualDeployerAddress); + } + let actualReservedDeployerAddress = await reservedWallet.getAddress(); + if (actualReservedDeployerAddress != reservedDeployerAddr) { + tryThrow("Reserved Nonce deployer addresses mismatch, expect " + reservedDeployerAddr + " actual " + actualReservedDeployerAddress); + } + + // Check duplicates + if (hasDuplicates([actualDeployerAddress, actualReservedDeployerAddress, axelarEOA])) { + throw("Duplicate address detected!"); + } + if (hasDuplicates([rootIMXAddr, rootWETHAddr])) { + throw("Duplicate address detected!"); + } + + // Check deployer fund on root chain and child chain. + let IMX = new ethers.Contract(rootIMXAddr, IMX_ABI, rootProvider); + let rootDeployerETHBalance = await rootProvider.getBalance(actualDeployerAddress); + let rootReservedDeployerETHBalance = await rootProvider.getBalance(actualReservedDeployerAddress); + if (rootDeployerETHBalance.lt(ethers.utils.parseEther("0.1"))) { + tryThrow("Deployer on root chain needs to have at least 0.1 ETH, got " + ethers.utils.formatEther(rootDeployerETHBalance)); + } + if (rootReservedDeployerETHBalance.lt(ethers.utils.parseEther("0.1"))) { + tryThrow("Reserved deployer on root chain needs to have at least 0.1 ETH, got " + ethers.utils.formatEther(rootReservedDeployerETHBalance)); + } + let rootDeployerIMXBalance = await IMX.balanceOf(actualDeployerAddress); + let axelarRequiredIMX = ethers.utils.parseEther(axelarFund); + let deployerRequiredIMX = ethers.utils.parseEther(childDeployerFund); + let reservedDeployerRequiredIMX = ethers.utils.parseEther(childReservedDeployerFund); + if (axelarRequiredIMX.lt(ethers.utils.parseEther("500.0"))) { + tryThrow("Axelar on child chain should request at least 500 IMX, got" + ethers.utils.formatEther(axelarRequiredIMX)); + } + if (deployerRequiredIMX.lt(ethers.utils.parseEther("500.0"))) { + tryThrow("Deployer on child chain should request at least 500 IMX, got" + ethers.utils.formatEther(deployerRequiredIMX)); + } + if (reservedDeployerRequiredIMX.lt(ethers.utils.parseEther("10.0"))) { + tryThrow("Reserved deployer on child chain should request at least 10 IMX, got" + ethers.utils.formatEther(reservedDeployerRequiredIMX)); + } + let extraIMX = ethers.utils.parseEther("100.0"); + let requiredIMX = axelarRequiredIMX.add(deployerRequiredIMX).add(reservedDeployerRequiredIMX).add(extraIMX); + if (rootDeployerIMXBalance.lt(requiredIMX)) { + tryThrow("Deployer on root chain needs to have at least " + ethers.utils.formatEther(requiredIMX) + " IMX, got " + ethers.utils.formatEther(rootDeployerIMXBalance)); + } + let childDeployerIMXBalance = await childProvider.getBalance(actualDeployerAddress); + if (!childDeployerIMXBalance.eq(ethers.utils.parseEther(TOTAL_SUPPLY))) { + tryThrow("Deployer on child chain needs to have 2B units of pre-mined IMX, got " + ethers.utils.formatEther(childDeployerIMXBalance)); + } + + // Check IMX deposit limit + let depositLimit = ethers.utils.parseEther(imxDepositLimit); + if (depositLimit.gt(ethers.utils.parseEther("200000000"))) { + tryThrow("Deposit limit should be at most 200m, got " + ethers.utils.formatEther(depositLimit)); + } + if (depositLimit.lt(ethers.utils.parseEther("2000000"))) { + tryThrow("Deposit limit should be at least 2m, got " + ethers.utils.formatEther(depositLimit)); + } + + console.log("=======End Pre Validation======="); +} +run(); \ No newline at end of file diff --git a/scripts/bootstrap/1_deployer_funding.ts b/scripts/bootstrap/1_deployer_funding.ts index 8e3b1341..a6a9aaca 100644 --- a/scripts/bootstrap/1_deployer_funding.ts +++ b/scripts/bootstrap/1_deployer_funding.ts @@ -11,61 +11,69 @@ async function run() { // Check environment variables let childRPCURL = requireEnv("CHILD_RPC_URL"); let childChainID = requireEnv("CHILD_CHAIN_ID"); - let adminEOASecret = requireEnv("CHILD_ADMIN_EOA_SECRET"); + let deployerSecret = requireEnv("DEPLOYER_SECRET"); + let reservedDeployerAddr = requireEnv("NONCE_RESERVED_DEPLOYER_ADDR"); + let reservedDeployerFund = requireEnv("CHILD_NONCE_RESERVED_DEPLOYER_FUND"); let axelarEOA = requireEnv("AXELAR_EOA"); let axelarFund = requireEnv("AXELAR_FUND"); - let deployerEOA = requireEnv("CHILD_DEPLOYER_ADDR"); - let deployerFund = requireEnv("CHILD_DEPLOYER_FUND"); - // Get admin EOA address + // Get deployer address const childProvider = new ethers.providers.JsonRpcProvider(childRPCURL, Number(childChainID)); - let adminWallet; - if (adminEOASecret == "ledger") { - let index = requireEnv("CHILD_ADMIN_EOA_LEDGER_INDEX"); + let childDeployerWallet; + if (deployerSecret == "ledger") { + let index = requireEnv("DEPLOYER_LEDGER_INDEX"); const derivationPath = `m/44'/60'/${parseInt(index)}'/0/0`; - adminWallet = new LedgerSigner(childProvider, derivationPath); + childDeployerWallet = new LedgerSigner(childProvider, derivationPath); } else { - adminWallet = new ethers.Wallet(adminEOASecret, childProvider); + childDeployerWallet = new ethers.Wallet(deployerSecret, childProvider); } - let adminAddr = await adminWallet.getAddress(); - console.log("Admin address is: ", adminAddr); + let deployerAddr = await childDeployerWallet.getAddress(); + console.log("Deployer address is: ", deployerAddr); // Check duplicates - if (hasDuplicates([adminAddr, axelarEOA, deployerEOA])) { + if (hasDuplicates([deployerAddr, axelarEOA, reservedDeployerAddr])) { throw("Duplicate address detected!"); } // Execute + console.log("Nonce reserved deployer now has: ", ethers.utils.formatEther(await childProvider.getBalance(reservedDeployerAddr))); console.log("Axelar EOA now has: ", ethers.utils.formatEther(await childProvider.getBalance(axelarEOA))); - console.log("Deployer EOA now has: ", ethers.utils.formatEther(await childProvider.getBalance(deployerEOA))); console.log("Fund Axelar and deployer on child chain in..."); await waitForConfirmation(); - let [priorityFee, maxFee] = await getFee(childProvider); - console.log("Transfer value to axelar..."); - let resp = await adminWallet.sendTransaction({ - to: axelarEOA, - value: ethers.utils.parseEther(axelarFund), - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }) - console.log("Transaction submitted: " + JSON.stringify(resp, null, 2)); - await waitForReceipt(resp.hash, childProvider); + if ((await childProvider.getBalance(reservedDeployerAddr)).gte(ethers.utils.parseEther(reservedDeployerFund))) { + console.log("Nonce reserved deployer has already got requested amount, skip."); + } else { + let [priorityFee, maxFee] = await getFee(childProvider); + console.log("Transfer value to reserved nonce deployer..."); + let resp = await childDeployerWallet.sendTransaction({ + to: reservedDeployerAddr, + value: ethers.utils.parseEther(reservedDeployerFund), + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }) + console.log("Transaction submitted: " + JSON.stringify(resp, null, 2)); + await waitForReceipt(resp.hash, childProvider); + } - [priorityFee, maxFee] = await getFee(childProvider); - console.log("Transfer value to deployer..."); - resp = await adminWallet.sendTransaction({ - to: deployerEOA, - value: ethers.utils.parseEther(deployerFund), - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }) - console.log("Transaction submitted: " + JSON.stringify(resp, null, 2)); - await waitForReceipt(resp.hash, childProvider); + if ((await childProvider.getBalance(axelarEOA)).gte(ethers.utils.parseEther(axelarFund))) { + console.log("Axelar has already got requested amount, skip."); + } else { + let [priorityFee, maxFee] = await getFee(childProvider); + console.log("Transfer value to axelar..."); + let resp = await childDeployerWallet.sendTransaction({ + to: axelarEOA, + value: ethers.utils.parseEther(axelarFund), + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }) + console.log("Transaction submitted: " + JSON.stringify(resp, null, 2)); + await waitForReceipt(resp.hash, childProvider); + } // Print target balance + console.log("Nonce reserved deployer now has: ", ethers.utils.formatEther(await childProvider.getBalance(reservedDeployerAddr))); console.log("Axelar EOA now has: ", ethers.utils.formatEther(await childProvider.getBalance(axelarEOA))); - console.log("Deployer EOA now has: ", ethers.utils.formatEther(await childProvider.getBalance(deployerEOA))); console.log("=======End Deployer Funding======="); } diff --git a/scripts/bootstrap/2_deployment_validation.ts b/scripts/bootstrap/2_deployment_validation.ts index b6b2835b..d49d8469 100644 --- a/scripts/bootstrap/2_deployment_validation.ts +++ b/scripts/bootstrap/2_deployment_validation.ts @@ -3,8 +3,6 @@ import * as dotenv from "dotenv"; dotenv.config(); import { ethers } from "ethers"; import { requireEnv, hasDuplicates, requireNonEmptyCode } from "../helpers/helpers"; -import { LedgerSigner } from "../helpers/ledger_signer"; -import * as fs from "fs"; async function run() { console.log("=======Start Deployment Validation======="); diff --git a/scripts/bootstrap/6_imx_burning.ts b/scripts/bootstrap/6_imx_burning.ts index d67ddbd4..6dd1b16d 100644 --- a/scripts/bootstrap/6_imx_burning.ts +++ b/scripts/bootstrap/6_imx_burning.ts @@ -2,9 +2,8 @@ import * as dotenv from "dotenv"; dotenv.config(); import { ethers } from "ethers"; -import { requireEnv, waitForConfirmation, hasDuplicates, waitForReceipt, getFee } from "../helpers/helpers"; +import { requireEnv, waitForConfirmation, hasDuplicates, waitForReceipt, getFee, getContract, getChildContracts } from "../helpers/helpers"; import { LedgerSigner } from "../helpers/ledger_signer"; -import * as fs from "fs"; async function run() { console.log("=======Start IMX Burning======="); @@ -12,85 +11,82 @@ async function run() { // Check environment variables let childRPCURL = requireEnv("CHILD_RPC_URL"); let childChainID = requireEnv("CHILD_CHAIN_ID"); - let adminEOASecret = requireEnv("CHILD_ADMIN_EOA_SECRET"); + let deployerSecret = requireEnv("DEPLOYER_SECRET"); let multisigAddr = requireEnv("MULTISIG_CONTRACT_ADDRESS"); let imxDepositLimit = requireEnv("IMX_DEPOSIT_LIMIT"); + let deployerFund = requireEnv("CHILD_DEPLOYER_FUND"); // Read from contract file. - let data = fs.readFileSync(".child.bridge.contracts.json", 'utf-8'); - let childContracts = JSON.parse(data); + let childContracts = getChildContracts(); let childBridgeAddr = childContracts.CHILD_BRIDGE_ADDRESS; - // Get admin address + // Get deployer address const childProvider = new ethers.providers.JsonRpcProvider(childRPCURL, Number(childChainID)); - let adminWallet; - if (adminEOASecret == "ledger") { - let index = requireEnv("CHILD_ADMIN_EOA_LEDGER_INDEX"); + let childDeployerWallet; + if (deployerSecret == "ledger") { + let index = requireEnv("DEPLOYER_LEDGER_INDEX"); const derivationPath = `m/44'/60'/${parseInt(index)}'/0/0`; - adminWallet = new LedgerSigner(childProvider, derivationPath); + childDeployerWallet = new LedgerSigner(childProvider, derivationPath); } else { - adminWallet = new ethers.Wallet(adminEOASecret, childProvider); + childDeployerWallet = new ethers.Wallet(deployerSecret, childProvider); } - let adminAddr = await adminWallet.getAddress(); - console.log("Admin address is: ", adminAddr); + let deployerAddr = await childDeployerWallet.getAddress(); + console.log("Deployer address is: ", deployerAddr); // Check duplicates - if (hasDuplicates([adminAddr, childBridgeAddr, multisigAddr])) { + if (hasDuplicates([deployerAddr, childBridgeAddr, multisigAddr])) { throw("Duplicate address detected!"); } // Execute - let adminBal = await childProvider.getBalance(adminAddr); + let deployerBal = await childProvider.getBalance(deployerAddr); let bridgeBal = await childProvider.getBalance(childBridgeAddr); let multisigBal = await childProvider.getBalance(multisigAddr); - console.log("Admin balance: ", ethers.utils.formatEther(adminBal)); + console.log("Deployer balance: ", ethers.utils.formatEther(deployerBal)); console.log("Bridge balance: ", ethers.utils.formatEther(bridgeBal)); console.log("Multisig balance: ", ethers.utils.formatEther(multisigBal)); - if (adminBal.lt(ethers.utils.parseEther("0.01"))) { - console.log("IMX Burning has already been done, skip.") - return; - } - console.log("Burn IMX in..."); await waitForConfirmation(); - let childBridgeObj = JSON.parse(fs.readFileSync('../../out/ChildERC20Bridge.sol/ChildERC20Bridge.json', 'utf8')); - let childBridge = new ethers.Contract(childBridgeAddr, childBridgeObj.abi, childProvider); - - console.log("Transfer " + imxDepositLimit + " IMX to child bridge..."); - let [priorityFee, maxFee] = await getFee(childProvider); - let resp = await childBridge.connect(adminWallet).privilegedDeposit({ - value: ethers.utils.parseEther(imxDepositLimit), - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }) - console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)) - await waitForReceipt(resp.hash, childProvider); - - adminBal = await childProvider.getBalance(adminAddr); - bridgeBal = await childProvider.getBalance(childBridgeAddr); - multisigBal = await childProvider.getBalance(multisigAddr); - console.log("Admin balance: ", ethers.utils.formatEther(adminBal)); - console.log("Bridge balance: ", ethers.utils.formatEther(bridgeBal)); - console.log("Multisig balance: ", ethers.utils.formatEther(multisigBal)); + if ((await childProvider.getBalance(childBridgeAddr)).gte(ethers.utils.parseEther(imxDepositLimit))) { + console.log("Child bridge has already got burned IMX, skip."); + } else { + console.log("Transfer " + imxDepositLimit + " IMX to child bridge..."); + let [priorityFee, maxFee] = await getFee(childProvider); + let childBridge = getContract("ChildERC20Bridge", childBridgeAddr, childProvider); + let resp = await childBridge.connect(childDeployerWallet).privilegedDeposit({ + value: ethers.utils.parseEther(imxDepositLimit), + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }) + console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)) + await waitForReceipt(resp.hash, childProvider); + } // Transfer to multisig - console.log("Transfer remaining to multisig..."); - [priorityFee, maxFee] = await getFee(childProvider); - resp = await adminWallet.sendTransaction({ - to: multisigAddr, - value: adminBal.sub(ethers.utils.parseEther("0.01")), - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }); - console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)) - await waitForReceipt(resp.hash, childProvider); + let remain = ethers.utils.parseEther(deployerFund); + deployerBal = await childProvider.getBalance(deployerAddr); + if (deployerBal.lte(remain)) { + console.log("Multisig has already got remaining burned IMX, skip."); + } else { + console.log("Transfer remaining to multisig..."); + let toTransfer = deployerBal.sub(remain); + let [priorityFee, maxFee] = await getFee(childProvider); + let resp = await childDeployerWallet.sendTransaction({ + to: multisigAddr, + value: toTransfer, + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)) + await waitForReceipt(resp.hash, childProvider); + } - adminBal = await childProvider.getBalance(adminAddr); + deployerBal = await childProvider.getBalance(deployerAddr); bridgeBal = await childProvider.getBalance(childBridgeAddr); multisigBal = await childProvider.getBalance(multisigAddr); - console.log("Admin balance: ", ethers.utils.formatEther(adminBal)); + console.log("Deployer balance: ", ethers.utils.formatEther(deployerBal)); console.log("Bridge balance: ", ethers.utils.formatEther(bridgeBal)); console.log("Multisig balance: ", ethers.utils.formatEther(multisigBal)); diff --git a/scripts/bootstrap/7_imx_rebalancing.ts b/scripts/bootstrap/7_imx_rebalancing.ts index 01f7d805..4d18e9c3 100644 --- a/scripts/bootstrap/7_imx_rebalancing.ts +++ b/scripts/bootstrap/7_imx_rebalancing.ts @@ -2,9 +2,8 @@ import * as dotenv from "dotenv"; dotenv.config(); import { ethers } from "ethers"; -import { requireEnv, waitForConfirmation, waitForReceipt, getFee, hasDuplicates } from "../helpers/helpers"; +import { requireEnv, waitForConfirmation, waitForReceipt, getFee, hasDuplicates, getChildContracts, getRootContracts } from "../helpers/helpers"; import { LedgerSigner } from "../helpers/ledger_signer"; -import * as fs from "fs"; // The total supply of IMX const TOTAL_SUPPLY = "2000000000"; @@ -20,37 +19,35 @@ async function run() { let childChainID = requireEnv("CHILD_CHAIN_ID"); let rootRPCURL = requireEnv("ROOT_RPC_URL"); let rootChainID = requireEnv("ROOT_CHAIN_ID"); - let rootDeployerSecret = requireEnv("ROOT_DEPLOYER_SECRET"); + let deployerSecret = requireEnv("DEPLOYER_SECRET"); let multisigAddr = requireEnv("MULTISIG_CONTRACT_ADDRESS"); let rootIMXAddr = requireEnv("ROOT_IMX_ADDR"); // Read from contract file. - let data = fs.readFileSync(".child.bridge.contracts.json", 'utf-8'); - let childContracts = JSON.parse(data); + let childContracts = getChildContracts(); let childBridgeAddr = childContracts.CHILD_BRIDGE_ADDRESS; - data = fs.readFileSync(".root.bridge.contracts.json", 'utf-8'); - let rootContracts = JSON.parse(data); + let rootContracts = getRootContracts(); let rootBridgeAddr = rootContracts.ROOT_BRIDGE_ADDRESS; - // Get admin address + // Get deployer address const childProvider = new ethers.providers.JsonRpcProvider(childRPCURL, Number(childChainID)); const rootProvider = new ethers.providers.JsonRpcProvider(rootRPCURL, Number(rootChainID)); - let adminWallet; - if (rootDeployerSecret == "ledger") { - let index = requireEnv("ROOT_DEPLOYER_LEDGER_INDEX"); + let rootDeployerWallet; + if (deployerSecret == "ledger") { + let index = requireEnv("DEPLOYER_LEDGER_INDEX"); const derivationPath = `m/44'/60'/${parseInt(index)}'/0/0`; - adminWallet = new LedgerSigner(rootProvider, derivationPath); + rootDeployerWallet = new LedgerSigner(rootProvider, derivationPath); } else { - adminWallet = new ethers.Wallet(rootDeployerSecret, rootProvider); + rootDeployerWallet = new ethers.Wallet(deployerSecret, rootProvider); } - let adminAddr = await adminWallet.getAddress(); - console.log("Deployer address is: ", adminAddr); + let deployerAddr = await rootDeployerWallet.getAddress(); + console.log("Deployer address is: ", deployerAddr); // Check duplicates - if (hasDuplicates([adminAddr, childBridgeAddr, multisigAddr])) { + if (hasDuplicates([deployerAddr, childBridgeAddr, multisigAddr])) { throw("Duplicate address detected!"); } - if (hasDuplicates([adminAddr, rootBridgeAddr, rootIMXAddr])) { + if (hasDuplicates([deployerAddr, rootBridgeAddr, rootIMXAddr])) { throw("Duplicate address detected!"); } @@ -62,29 +59,29 @@ async function run() { console.log("The amount to balance on L1 is: ", ethers.utils.formatEther(balanceAmt)); let IMX = new ethers.Contract(rootIMXAddr, IMX_ABI, rootProvider); - let adminL1Balance = await IMX.balanceOf(adminAddr); + let deployerL1Balance = await IMX.balanceOf(deployerAddr); let rootBridgeBalance = await IMX.balanceOf(rootBridgeAddr); - console.log("Admin L1 IMX balance: ", ethers.utils.formatEther(adminL1Balance)); + console.log("Deployer L1 IMX balance: ", ethers.utils.formatEther(deployerL1Balance)); console.log("Root bridge L1 IMX balance: ", ethers.utils.formatEther(rootBridgeBalance)); - - if (rootBridgeBalance.gt(ethers.utils.parseEther("1.0"))) { - console.log("IMX Rebalancing has already been done, skip.") - return; - } console.log("Rebalance in..."); await waitForConfirmation(); + if (deployerL1Balance.lt(balanceAmt)) { + console.log("Insufficient balance to rebalance (already balanced?), skip."); + return; + } + // Rebalancing - console.log("Transfer...") - let resp = await IMX.connect(adminWallet).transfer(rootBridgeAddr, balanceAmt); + console.log("Rebalancing...") + let resp = await IMX.connect(rootDeployerWallet).transfer(rootBridgeAddr, balanceAmt); console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)) await waitForReceipt(resp.hash, rootProvider); - adminL1Balance = await IMX.balanceOf(adminAddr); + deployerL1Balance = await IMX.balanceOf(deployerAddr); rootBridgeBalance = await IMX.balanceOf(rootBridgeAddr); - console.log("Admin L1 IMX balance: ", ethers.utils.formatEther(adminL1Balance)); + console.log("Deployer L1 IMX balance: ", ethers.utils.formatEther(deployerL1Balance)); console.log("Root bridge L1 IMX balance: ", ethers.utils.formatEther(rootBridgeBalance)); console.log("=======End IMX Rebalancing======="); diff --git a/scripts/bootstrap/9_test_preparation.ts b/scripts/bootstrap/9_test_preparation.ts new file mode 100644 index 00000000..49bac642 --- /dev/null +++ b/scripts/bootstrap/9_test_preparation.ts @@ -0,0 +1,71 @@ +// Prepare for test +import * as dotenv from "dotenv"; +dotenv.config(); +import { ethers } from "ethers"; +import { deployRootContract, getContract, getRootContracts, requireEnv, saveRootContracts, waitForConfirmation, waitForReceipt } from "../helpers/helpers"; +import { LedgerSigner } from "../helpers/ledger_signer"; + +async function run() { + console.log("=======Start Test Preparation======="); + + // Check environment variables + let rootRPCURL = requireEnv("ROOT_RPC_URL"); + let rootChainID = requireEnv("ROOT_CHAIN_ID"); + let deployerSecret = requireEnv("DEPLOYER_SECRET"); + let testAccountKey = requireEnv("TEST_ACCOUNT_SECRET"); + + // Get deployer address + const rootProvider = new ethers.providers.JsonRpcProvider(rootRPCURL, Number(rootChainID)); + let rootDeployerWallet; + if (deployerSecret == "ledger") { + let index = requireEnv("DEPLOYER_LEDGER_INDEX"); + const derivationPath = `m/44'/60'/${parseInt(index)}'/0/0`; + rootDeployerWallet = new LedgerSigner(rootProvider, derivationPath); + } else { + rootDeployerWallet = new ethers.Wallet(deployerSecret, rootProvider); + } + let deployerAddr = await rootDeployerWallet.getAddress(); + console.log("Deployer address is: ", deployerAddr); + + let rootTestWallet = new ethers.Wallet(testAccountKey, rootProvider); + + let rootContracts = getRootContracts(); + let rootBridge = getContract("RootERC20BridgeFlowRate", rootContracts.ROOT_BRIDGE_ADDRESS, rootProvider); + + // Execute + console.log("Prepare test in..."); + await waitForConfirmation(); + + // Deploy a custom token + let rootCustomToken; + if (rootContracts.ROOT_TEST_CUSTOM_TOKEN != "") { + console.log("Root test custom token has already been deployed to: " + rootContracts.ROOT_TEST_CUSTOM_TOKEN + ", skip."); + rootCustomToken = getContract("ERC20PresetMinterPauser", rootContracts.ROOT_TEST_CUSTOM_TOKEN, rootProvider); + } else { + console.log("Deploy root test custom token..."); + rootCustomToken = await deployRootContract("ERC20PresetMinterPauser", rootDeployerWallet, null, "Custom Token", "CTK"); + await waitForReceipt(rootCustomToken.deployTransaction.hash, rootProvider); + console.log("Custom token deployed to: ", rootCustomToken) + } + rootContracts.ROOT_TEST_CUSTOM_TOKEN=rootCustomToken.address; + saveRootContracts(rootContracts); + console.log("Deployed to ROOT_TEST_CUSTOM_TOKEN: ", rootCustomToken.address); + + // Mint tokens + console.log("Mint tokens..."); + let resp = await rootCustomToken.connect(rootDeployerWallet).mint(rootTestWallet.address, ethers.utils.parseEther("1000.0").toBigInt()); + await waitForReceipt(resp.hash, rootProvider); + + console.log("Set rate control..."); + // Set rate control + resp = await rootBridge.connect(rootDeployerWallet).setRateControlThreshold( + rootCustomToken.address, + ethers.utils.parseEther("20016.0"), + ethers.utils.parseEther("5.56"), + ethers.utils.parseEther("10008.0") + ); + await waitForReceipt(resp.hash, rootProvider); + + console.log("=======End Test Preparation======="); +} +run(); \ No newline at end of file diff --git a/scripts/bootstrap/README.md b/scripts/bootstrap/README.md index f6b9d3df..25d7242c 100644 --- a/scripts/bootstrap/README.md +++ b/scripts/bootstrap/README.md @@ -3,12 +3,11 @@ ## Prerequisite 1. Coordinate with Axelar to obtain their admin address for initial funding as well as the desired amount in $IMX. (500 IMX in previous discussion). 2. Obtain the deployer account on both child chain and root chain. -3. Obtain the amount to fund deployer on child chain in $IMX. (500 IMX by default). -4. Coordinate with security to obtain the addresses for different roles. -5. Fund deployer with `ETH` and `IMX` on root chain. (As a rule of thumb, _0.1 ETH and 1100 IMX_ (TBD)). -6. Fund a test account with `ETH` and `IMX` on root chain. (As a rule of thumb, _0.1 ETH and 50 IMX_ (TBD)). -7. Fund a rate admin account with `ETH` on root chain. (As a rule of thumb, _0.1 ETH_ (TBD)). - +3. Obtain the nonce reserved deployer account and reserved nonce on both child chain and root chain. +4. Obtain the amount to fund reserved deployer on child chain in $IMX. (10 IMX by default). +5. Obtain the amount to fund deployer on child chain in $IMX. (500 IMX by default). +6. Fund deployer with `ETH` and `IMX` on root chain. (As a rule of thumb, _0.1 ETH and 1100 IMX_ (TBD)). +7. Fund a test account with `ETH` and `IMX` on root chain. (As a rule of thumb, _0.1 ETH and 50 IMX_ (TBD)). ## Bootstrapping 0. Install dependency and compile contracts (Run in root directory) @@ -23,85 +22,47 @@ cp .env.example .env ``` 2. Set the following environment variables ``` -# Set prior to 1_deployer_funding.js +# Set prior to 0_pre_validation.js +# Name of the child chain MUST match Axelar's definition. CHILD_CHAIN_NAME= +# The RPC URL of child chain. CHILD_RPC_URL= +# The chain ID of the child chain. CHILD_CHAIN_ID= +# Name of the root chain MUST match Axelar's definition. ROOT_CHAIN_NAME= +# The RPC URL of root chain. ROOT_RPC_URL= +# The chain ID of the root chain. ROOT_CHAIN_ID= -## The admin EOA address on the child chain. -CHILD_ADMIN_ADDR= -## The private key for the admin EOA or "ledger" if using hardware wallet. -CHILD_ADMIN_EOA_SECRET= -## The ledger index for the admin EOA, required if using ledger. -CHILD_ADMIN_EOA_LEDGER_INDEX= -## The deployer address on child chain. -CHILD_DEPLOYER_ADDR= -## The private key for the deployer on child chain or "ledger" if using hardware wallet. -CHILD_DEPLOYER_SECRET= -## The ledger index for the deployer on child chain, required if using ledger. -CHILD_DEPLOYER_LEDGER_INDEX= -## The amount of fund deployer required on L2, unit is in IMX or 10^18 Wei. -CHILD_DEPLOYER_FUND= -## The deployer address on root chain. -ROOT_DEPLOYER_ADDR= -## The private key for the deployer on root chain or "ledger" if using hardware wallet. -ROOT_DEPLOYER_SECRET= -## The ledger index for the deployer on root chain, required if using ledger. -ROOT_DEPLOYER_LEDGER_INDEX= -## The private key for rate admin or "ledger" if using hardware wallet. -ROOT_BRIDGE_RATE_ADMIN_SECRET= -## The ledger index for the rate admin, required if using ledger. -ROOT_BRIDGE_RATE_ADMIN_LEDGER_INDEX= +## The deployer address on child & root chains. +DEPLOYER_ADDR= +## The private key for the deployer on child & root chains or "ledger" if using hardware wallet. +DEPLOYER_SECRET= +## The ledger index for the deployer on child & root chains, required if using ledger. +DEPLOYER_LEDGER_INDEX= +## The nonce reserved deployer address on child & root chains. +NONCE_RESERVED_DEPLOYER_ADDR= +## The nonce reserved deployer, or "ledger" if using hardware wallet. +NONCE_RESERVED_DEPLOYER_SECRET= +## The ledger index for the nonce reserved deployer. +NONCE_RESERVED_DEPLOYER_INDEX= +## The reserved nonce for token template deployment. +NONCE_RESERVED= ## The IMX token address on root chain. ROOT_IMX_ADDR= ## The Wrapped ETH token address on the root chain. ROOT_WETH_ADDR= -## The Axelar address for receive initial funding on the child chain. +## The Axelar address to receive initial funding on the child chain. AXELAR_EOA= ## The amount of fund Axelar requested, unit is in IMX or 10^18 Wei. AXELAR_FUND= +## The amount of fund deployer to be left with after bootstrapping on L2, unit is in IMX or 10^18 Wei. +CHILD_DEPLOYER_FUND= +## The amount of fund nonce reserved deployer required on L2, unit is in IMX or 10^18 Wei. +CHILD_NONCE_RESERVED_DEPLOYER_FUND= ## The maximum amount of IMX that can be deposited to L2, unit is in IMX or 10^18 Wei. IMX_DEPOSIT_LIMIT= -## The address to perform child bridge upgrade. -CHILD_PROXY_ADMIN= -## The address to be assigned with DEFAULT_ADMIN_ROLE in child bridge. -CHILD_BRIDGE_DEFAULT_ADMIN= -## The address to be assigned with PAUSER_ROLE in child bridge. -CHILD_BRIDGE_PAUSER= -## The address to be assigned with UNPAUSER_ROLE in child bridge. -CHILD_BRIDGE_UNPAUSER= -## The address to be assigned with ADAPTOR_MANAGER_ROLE in child bridge. -CHILD_BRIDGE_ADAPTOR_MANAGER= -## The address to be assigned with DEFAULT_ADMIN_ROLE in child adaptor. -CHILD_ADAPTOR_DEFAULT_ADMIN= -## The address to be assigned with BRIDGE_MANAGER_ROLE in child adaptor. -CHILD_ADAPTOR_BRIDGE_MANAGER= -## The address to be assigned with GAS_SERVICE_MANAGER_ROLE in child adaptor. -CHILD_ADAPTOR_GAS_SERVICE_MANAGER= -## The address to be assigned with TARGET_MANAGER_ROLE in child adaptor. -CHILD_ADAPTOR_TARGET_MANAGER= -## The address to perform root adaptor upgrade. -ROOT_PROXY_ADMIN= -## The address to be assigned with DEFAULT_ADMIN_ROLE in root bridge. -ROOT_BRIDGE_DEFAULT_ADMIN= -## The address to be assigned with PAUSER_ROLE in root bridge. -ROOT_BRIDGE_PAUSER= -## The address to be assigned with UNPAUSER_ROLE in root bridge. -ROOT_BRIDGE_UNPAUSER= -## The address to be assigned with VARIABLE_MANAGER_ROLE in root bridge. -ROOT_BRIDGE_VARIABLE_MANAGER= -## The address to be assigned with ADAPTOR_MANAGER_ROLE in root bridge. -ROOT_BRIDGE_ADAPTOR_MANAGER= -## The address to be assigned with DEFAULT_ADMIN_ROLE in root adaptor. -ROOT_ADAPTOR_DEFAULT_ADMIN= -## The address to be assigned with BRIDGE_MANAGER_ROLE in root adaptor. -ROOT_ADAPTOR_BRIDGE_MANAGER= -## The address to be assigned with GAS_SERVICE_MANAGER_ROLE in root adaptor. -ROOT_ADAPTOR_GAS_SERVICE_MANAGER= -## The address to be assigned with TARGET_MANAGER_ROLE in root adaptor. -ROOT_ADAPTOR_TARGET_MANAGER= ## The capacity of the rate limit policy of IMX token, unit is in 10^18. RATE_LIMIT_IMX_CAPACITY= ## The refill rate of the rate limit policy of IMX token, unit is in 10^18. @@ -147,12 +108,16 @@ RATE_LIMIT_GOG_REFILL_RATE= ## The large threshold of the rate limit policy of GOG token, unit is in 10^18. RATE_LIMIT_GOG_LARGE_THRESHOLD= ``` -3. Fund deployer +3. Pre validation +``` +npx ts-node 0_pre_validation.ts 2>&1 | tee bootstrap.out +``` +4. Fund deployer ``` npx ts-node 1_deployer_funding.ts 2>&1 | tee bootstrap.out ``` -4. Wait for Axelar to deploy & setup their system and Security team to deploy & setup multisig wallet. -5. Set the following environment variables +5. Wait for Axelar to deploy & setup their system and Security team to deploy & setup multisig wallet. +6. Set the following environment variables ``` CHILD_GATEWAY_ADDRESS= CHILD_GAS_SERVICE_ADDRESS= @@ -160,8 +125,7 @@ MULTISIG_CONTRACT_ADDRESS= ROOT_GATEWAY_ADDRESS= ROOT_GAS_SERVICE_ADDRESS= ``` -6. Basic contract validation - +7. Basic contract validation If multisig is deployed: ``` npx ts-node 2_deployment_validation.ts 2>&1 | tee -a bootstrap.out @@ -170,32 +134,36 @@ If multisig isn't deployed: ``` SKIP_MULTISIG_CHECK=true npx ts-node 2_deployment_validation.ts 2>&1 | tee -a bootstrap.out ``` -7. Deploy bridge contracts on child and root chain. +8. Deploy bridge contracts on child and root chain. ``` npx ts-node 3_child_deployment.ts 2>&1 | tee -a bootstrap.out npx ts-node 4_root_deployment.ts 2>&1 | tee -a bootstrap.out ``` -8. Initialise bridge contracts on child chain. +9. Initialise bridge contracts on child chain. ``` npx ts-node 5_child_initialisation.ts 2>&1 | tee -a bootstrap.out ``` -9. IMX Burning +10. IMX Burning ``` npx ts-node 6_imx_burning.ts 2>&1 | tee -a bootstrap.out ``` -10. IMX Rebalancing +11. IMX Rebalancing ``` npx ts-node 7_imx_rebalancing.ts 2>&1 | tee -a bootstrap.out ``` -11. Initialise bridge contracts on root chain. +12. Initialise bridge contracts on root chain. ``` npx ts-node 8_root_initialisation.ts 2>&1 | tee -a bootstrap.out ``` -12. Set the following environment variable +13. Set the following environment variable ``` TEST_ACCOUNT_SECRET= ``` -13. Test bridge functions +14. Prepare for test +``` +npx ts-node 9_test_preparation.ts 2>&1 | tee -a bootstrap.out +``` +15. Test bridge functions ``` -npx mocha --require mocha-suppress-logs ../e2e/e2e.ts 2>&1 | tee -a bootstrap.out +LONG_WAIT=1200000 SHORT_WAIT=300000 npx mocha --require mocha-suppress-logs ../e2e/e2e.ts 2>&1 | tee -a bootstrap.out ``` \ No newline at end of file diff --git a/scripts/deploy/.env.example b/scripts/deploy/.env.example index 0fcc7293..d0b6c81f 100644 --- a/scripts/deploy/.env.example +++ b/scripts/deploy/.env.example @@ -1,70 +1,43 @@ -# Access to child and root chains. +# Name of the child chain MUST match Axelar's definition. CHILD_CHAIN_NAME= +# The RPC URL of child chain. CHILD_RPC_URL= +# The chain ID of the child chain. CHILD_CHAIN_ID= +# Name of the root chain MUST match Axelar's definition. ROOT_CHAIN_NAME= +# The RPC URL of root chain. ROOT_RPC_URL= +# The chain ID of the root chain. ROOT_CHAIN_ID= -## The admin EOA address on the child chain that is allowed to top up the child bridge contract. -CHILD_ADMIN_ADDR= -## The multisig contract that is allowed to top up the child bridge contract. -MULTISIG_CONTRACT_ADDRESS= -## The private key for the deployer on child chain or "ledger" if using hardware wallet. -CHILD_DEPLOYER_SECRET= -## The ledger index for the deployer on child chain, required if using ledger. -CHILD_DEPLOYER_LEDGER_INDEX= -## The private key for the deployer on root chain or "ledger" if using hardware wallet. -ROOT_DEPLOYER_SECRET= -## The ledger index for the deployer on root chain, required if using ledger. -ROOT_DEPLOYER_LEDGER_INDEX= -## The private key for rate admin or "ledger" if using hardware wallet. -ROOT_BRIDGE_RATE_ADMIN_SECRET= -## The ledger index for the rate admin, required if using ledger. -ROOT_BRIDGE_RATE_ADMIN_LEDGER_INDEX= +## The deployer address on child & root chains. +DEPLOYER_ADDR= +## The private key for the deployer on child & root chains or "ledger" if using hardware wallet. +DEPLOYER_SECRET= +## The ledger index for the deployer on child & root chains, required if using ledger. +DEPLOYER_LEDGER_INDEX= +## The nonce reserved deployer address on child & root chains. +NONCE_RESERVED_DEPLOYER_ADDR= +## The nonce reserved deployer, or "ledger" if using hardware wallet. +NONCE_RESERVED_DEPLOYER_SECRET= +## The ledger index for the nonce reserved deployer. +NONCE_RESERVED_DEPLOYER_INDEX= +## The reserved nonce for token template deployment. +NONCE_RESERVED= ## The IMX token address on root chain. ROOT_IMX_ADDR= ## The Wrapped ETH token address on the root chain. ROOT_WETH_ADDR= +## The Axelar address to receive initial funding on the child chain. +AXELAR_EOA= +## The amount of fund Axelar requested, unit is in IMX or 10^18 Wei. +AXELAR_FUND= +## The amount of fund deployer to be left with after bootstrapping on L2, unit is in IMX or 10^18 Wei. +CHILD_DEPLOYER_FUND= +## The amount of fund nonce reserved deployer required on L2, unit is in IMX or 10^18 Wei. +CHILD_NONCE_RESERVED_DEPLOYER_FUND= ## The maximum amount of IMX that can be deposited to L2, unit is in IMX or 10^18 Wei. IMX_DEPOSIT_LIMIT= -## The address to perform child bridge upgrade. -CHILD_PROXY_ADMIN= -## The address to be assigned with DEFAULT_ADMIN_ROLE in child bridge. -CHILD_BRIDGE_DEFAULT_ADMIN= -## The address to be assigned with PAUSER_ROLE in child bridge. -CHILD_BRIDGE_PAUSER= -## The address to be assigned with UNPAUSER_ROLE in child bridge. -CHILD_BRIDGE_UNPAUSER= -## The address to be assigned with ADAPTOR_MANAGER_ROLE in child bridge. -CHILD_BRIDGE_ADAPTOR_MANAGER= -## The address to be assigned with DEFAULT_ADMIN_ROLE in child adaptor. -CHILD_ADAPTOR_DEFAULT_ADMIN= -## The address to be assigned with BRIDGE_MANAGER_ROLE in child adaptor. -CHILD_ADAPTOR_BRIDGE_MANAGER= -## The address to be assigned with GAS_SERVICE_MANAGER_ROLE in child adaptor. -CHILD_ADAPTOR_GAS_SERVICE_MANAGER= -## The address to be assigned with TARGET_MANAGER_ROLE in child adaptor. -CHILD_ADAPTOR_TARGET_MANAGER= -## The address to perform root adaptor upgrade. -ROOT_PROXY_ADMIN= -## The address to be assigned with DEFAULT_ADMIN_ROLE in root bridge. -ROOT_BRIDGE_DEFAULT_ADMIN= -## The address to be assigned with PAUSER_ROLE in root bridge. -ROOT_BRIDGE_PAUSER= -## The address to be assigned with UNPAUSER_ROLE in root bridge. -ROOT_BRIDGE_UNPAUSER= -## The address to be assigned with VARIABLE_MANAGER_ROLE in root bridge. -ROOT_BRIDGE_VARIABLE_MANAGER= -## The address to be assigned with ADAPTOR_MANAGER_ROLE in root bridge. -ROOT_BRIDGE_ADAPTOR_MANAGER= -## The address to be assigned with DEFAULT_ADMIN_ROLE in root adaptor. -ROOT_ADAPTOR_DEFAULT_ADMIN= -## The address to be assigned with BRIDGE_MANAGER_ROLE in root adaptor. -ROOT_ADAPTOR_BRIDGE_MANAGER= -## The address to be assigned with GAS_SERVICE_MANAGER_ROLE in root adaptor. -ROOT_ADAPTOR_GAS_SERVICE_MANAGER= -## The address to be assigned with TARGET_MANAGER_ROLE in root adaptor. -ROOT_ADAPTOR_TARGET_MANAGER= ## The capacity of the rate limit policy of IMX token, unit is in 10^18. RATE_LIMIT_IMX_CAPACITY= ## The refill rate of the rate limit policy of IMX token, unit is in 10^18. diff --git a/scripts/deploy/README.md b/scripts/deploy/README.md index 0251cc9f..b9b6f4af 100644 --- a/scripts/deploy/README.md +++ b/scripts/deploy/README.md @@ -12,72 +12,46 @@ cp .env.example .env 2. Set the following fields inside `.env` ``` # Access to child and root chains. +# Name of the child chain MUST match Axelar's definition. CHILD_CHAIN_NAME= +# The RPC URL of child chain. CHILD_RPC_URL= +# The chain ID of the child chain. CHILD_CHAIN_ID= +# Name of the root chain MUST match Axelar's definition. ROOT_CHAIN_NAME= +# The RPC URL of root chain. ROOT_RPC_URL= +# The chain ID of the root chain. ROOT_CHAIN_ID= -## The admin EOA address on the child chain that is allowed to top up the child bridge contract. -CHILD_ADMIN_ADDR= -## The multisig contract that is allowed to top up the child bridge contract. -MULTISIG_CONTRACT_ADDRESS= -## The private key for the deployer on child chain or "ledger" if using hardware wallet. -CHILD_DEPLOYER_SECRET= -## The ledger index for the deployer on child chain, required if using ledger. -CHILD_DEPLOYER_LEDGER_INDEX= -## The private key for the deployer on root chain or "ledger" if using hardware wallet. -ROOT_DEPLOYER_SECRET= -## The ledger index for the deployer on root chain, required if using ledger. -ROOT_DEPLOYER_LEDGER_INDEX= -## The private key for rate admin or "ledger" if using hardware wallet. -ROOT_BRIDGE_RATE_ADMIN_SECRET= -## The ledger index for the rate admin, required if using ledger. -ROOT_BRIDGE_RATE_ADMIN_LEDGER_INDEX= +## The deployer address on child & root chains. +DEPLOYER_ADDR= +## The private key for the deployer on child & root chains or "ledger" if using hardware wallet. +DEPLOYER_SECRET= +## The ledger index for the deployer on child & root chains, required if using ledger. +DEPLOYER_LEDGER_INDEX= +## The nonce reserved deployer address on child & root chains. +NONCE_RESERVED_DEPLOYER_ADDR= +## The nonce reserved deployer, or "ledger" if using hardware wallet. +NONCE_RESERVED_DEPLOYER_SECRET= +## The ledger index for the nonce reserved deployer. +NONCE_RESERVED_DEPLOYER_INDEX= +## The reserved nonce for token template deployment. +NONCE_RESERVED= ## The IMX token address on root chain. ROOT_IMX_ADDR= ## The Wrapped ETH token address on the root chain. ROOT_WETH_ADDR= +## The Axelar address to receive initial funding on the child chain. +AXELAR_EOA= +## The amount of fund Axelar requested, unit is in IMX or 10^18 Wei. +AXELAR_FUND= +## The amount of fund deployer to be left with after bootstrapping on L2, unit is in IMX or 10^18 Wei. +CHILD_DEPLOYER_FUND= +## The amount of fund nonce reserved deployer required on L2, unit is in IMX or 10^18 Wei. +CHILD_NONCE_RESERVED_DEPLOYER_FUND= ## The maximum amount of IMX that can be deposited to L2, unit is in IMX or 10^18 Wei. IMX_DEPOSIT_LIMIT= -## The address to perform child bridge upgrade. -CHILD_PROXY_ADMIN= -## The address to be assigned with DEFAULT_ADMIN_ROLE in child bridge. -CHILD_BRIDGE_DEFAULT_ADMIN= -## The address to be assigned with PAUSER_ROLE in child bridge. -CHILD_BRIDGE_PAUSER= -## The address to be assigned with UNPAUSER_ROLE in child bridge. -CHILD_BRIDGE_UNPAUSER= -## The address to be assigned with ADAPTOR_MANAGER_ROLE in child bridge. -CHILD_BRIDGE_ADAPTOR_MANAGER= -## The address to be assigned with DEFAULT_ADMIN_ROLE in child adaptor. -CHILD_ADAPTOR_DEFAULT_ADMIN= -## The address to be assigned with BRIDGE_MANAGER_ROLE in child adaptor. -CHILD_ADAPTOR_BRIDGE_MANAGER= -## The address to be assigned with GAS_SERVICE_MANAGER_ROLE in child adaptor. -CHILD_ADAPTOR_GAS_SERVICE_MANAGER= -## The address to be assigned with TARGET_MANAGER_ROLE in child adaptor. -CHILD_ADAPTOR_TARGET_MANAGER= -## The address to perform root adaptor upgrade. -ROOT_PROXY_ADMIN= -## The address to be assigned with DEFAULT_ADMIN_ROLE in root bridge. -ROOT_BRIDGE_DEFAULT_ADMIN= -## The address to be assigned with PAUSER_ROLE in root bridge. -ROOT_BRIDGE_PAUSER= -## The address to be assigned with UNPAUSER_ROLE in root bridge. -ROOT_BRIDGE_UNPAUSER= -## The address to be assigned with VARIABLE_MANAGER_ROLE in root bridge. -ROOT_BRIDGE_VARIABLE_MANAGER= -## The address to be assigned with ADAPTOR_MANAGER_ROLE in root bridge. -ROOT_BRIDGE_ADAPTOR_MANAGER= -## The address to be assigned with DEFAULT_ADMIN_ROLE in root adaptor. -ROOT_ADAPTOR_DEFAULT_ADMIN= -## The address to be assigned with BRIDGE_MANAGER_ROLE in root adaptor. -ROOT_ADAPTOR_BRIDGE_MANAGER= -## The address to be assigned with GAS_SERVICE_MANAGER_ROLE in root adaptor. -ROOT_ADAPTOR_GAS_SERVICE_MANAGER= -## The address to be assigned with TARGET_MANAGER_ROLE in root adaptor. -ROOT_ADAPTOR_TARGET_MANAGER= ## The capacity of the rate limit policy of IMX token, unit is in 10^18. RATE_LIMIT_IMX_CAPACITY= ## The refill rate of the rate limit policy of IMX token, unit is in 10^18. diff --git a/scripts/deploy/child_deployment.ts b/scripts/deploy/child_deployment.ts index 2ac569a0..71de36b2 100644 --- a/scripts/deploy/child_deployment.ts +++ b/scripts/deploy/child_deployment.ts @@ -2,112 +2,177 @@ import * as dotenv from "dotenv"; dotenv.config(); import { ethers } from "ethers"; -import { requireEnv, waitForConfirmation, deployChildContract, waitForReceipt, getFee } from "../helpers/helpers"; +import { requireEnv, waitForConfirmation, deployChildContract, waitForReceipt, getFee, getChildContracts, getContract, saveChildContracts } from "../helpers/helpers"; import { LedgerSigner } from "../helpers/ledger_signer"; -import * as fs from "fs"; export async function deployChildContracts() { // Check environment variables let childRPCURL = requireEnv("CHILD_RPC_URL"); let childChainID = requireEnv("CHILD_CHAIN_ID"); - let childDeployerSecret = requireEnv("CHILD_DEPLOYER_SECRET"); + let deployerSecret = requireEnv("DEPLOYER_SECRET"); + let nonceReservedDeployerSecret = requireEnv("NONCE_RESERVED_DEPLOYER_SECRET"); + let nonceReserved = Number(requireEnv("NONCE_RESERVED")); let childGatewayAddr = requireEnv("CHILD_GATEWAY_ADDRESS"); - let childProxyAdmin = requireEnv("CHILD_PROXY_ADMIN"); - // Get admin address + // Read from contract file. + let childContracts = getChildContracts(); + + // Get deployer address const childProvider = new ethers.providers.JsonRpcProvider(childRPCURL, Number(childChainID)); - let adminWallet; - if (childDeployerSecret == "ledger") { - let index = requireEnv("CHILD_DEPLOYER_LEDGER_INDEX"); + let childDeployerWallet; + if (deployerSecret == "ledger") { + let index = requireEnv("DEPLOYER_LEDGER_INDEX"); const derivationPath = `m/44'/60'/${parseInt(index)}'/0/0`; - adminWallet = new LedgerSigner(childProvider, derivationPath); + childDeployerWallet = new LedgerSigner(childProvider, derivationPath); } else { - adminWallet = new ethers.Wallet(childDeployerSecret, childProvider); + childDeployerWallet = new ethers.Wallet(deployerSecret, childProvider); + } + let deployerAddr = await childDeployerWallet.getAddress(); + console.log("Deployer address is: ", deployerAddr); + + // Get reserved wallet + let reservedDeployerWallet; + if (nonceReservedDeployerSecret == "ledger") { + let index = requireEnv("NONCE_RESERVED_DEPLOYER_INDEX"); + const derivationPath = `m/44'/60'/${parseInt(index)}'/0/0`; + reservedDeployerWallet = new LedgerSigner(childProvider, derivationPath); + } else { + reservedDeployerWallet = new ethers.Wallet(nonceReservedDeployerSecret, childProvider); + } + let reservedDeployerAddr = await reservedDeployerWallet.getAddress(); + console.log("Reserved deployer address is: ", reservedDeployerAddr); + + // Check the current nonce matches the reserved nonce + let currentNonce = await childProvider.getTransactionCount(reservedDeployerAddr); + if (nonceReserved != currentNonce) { + throw("Nonce mismatch, expected " + nonceReserved + " actual " + currentNonce); } - let adminAddr = await adminWallet.getAddress(); - console.log("Deployer address is: ", adminAddr); // Execute console.log("Deploy child contracts in..."); await waitForConfirmation(); // Deploy child token template - console.log("Deploy child token template..."); - let childTokenTemplate = await deployChildContract("ChildERC20", adminWallet); - console.log("Transaction submitted: ", JSON.stringify(childTokenTemplate.deployTransaction, null, 2)); - await waitForReceipt(childTokenTemplate.deployTransaction.hash, childProvider); - // Initialise template - console.log("Initialise child token template..."); - let [priorityFee, maxFee] = await getFee(childProvider); - let resp = await childTokenTemplate.connect(adminWallet).initialize("000000000000000000000000000000000000007B", "TEMPLATE", "TPT", 18, { - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }); - console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); - await waitForReceipt(resp.hash, childProvider); + let childTokenTemplate; + if (childContracts.CHILD_TOKEN_TEMPLATE != "") { + console.log("Child token template has already been deployed to: " + childContracts.CHILD_TOKEN_TEMPLATE + ", skip."); + childTokenTemplate = getContract("ChildERC20", childContracts.CHILD_TOKEN_TEMPLATE, childProvider); + } else { + console.log("Deploy child token template..."); + childTokenTemplate = await deployChildContract("ChildERC20", reservedDeployerWallet, nonceReserved); + console.log("Transaction submitted: ", JSON.stringify(childTokenTemplate.deployTransaction, null, 2)); + await waitForReceipt(childTokenTemplate.deployTransaction.hash, childProvider); + } + childContracts.CHILD_TOKEN_TEMPLATE = childTokenTemplate.address; + saveChildContracts(childContracts); console.log("Deployed to CHILD_TOKEN_TEMPLATE: ", childTokenTemplate.address); + + // Initialise template + if (await childTokenTemplate.name() == "TEMPLATE") { + console.log("Child token template has already been initialised, skip."); + } else { + console.log("Initialise child token template..."); + let [priorityFee, maxFee] = await getFee(childProvider); + let resp = await childTokenTemplate.connect(reservedDeployerWallet).initialize("000000000000000000000000000000000000007B", "TEMPLATE", "TPT", 18, { + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); + await waitForReceipt(resp.hash, childProvider); + } + console.log("Initialised CHILD_TOKEN_TEMPLATE at: ", childTokenTemplate.address); // Deploy wrapped IMX - console.log("Deploy wrapped IMX..."); - let wrappedIMX = await deployChildContract("WIMX", adminWallet); - console.log("Transaction submitted: ", JSON.stringify(wrappedIMX.deployTransaction, null, 2)); - await waitForReceipt(wrappedIMX.deployTransaction.hash, childProvider); + let wrappedIMX; + if (childContracts.WRAPPED_IMX_ADDRESS != "") { + console.log("Wrapped IMX has already been deployed to: " + childContracts.WRAPPED_IMX_ADDRESS + ", skip."); + wrappedIMX = getContract("WIMX", childContracts.WRAPPED_IMX_ADDRESS, childProvider); + } else { + console.log("Deploy wrapped IMX..."); + wrappedIMX = await deployChildContract("WIMX", childDeployerWallet, null); + console.log("Transaction submitted: ", JSON.stringify(wrappedIMX.deployTransaction, null, 2)); + await waitForReceipt(wrappedIMX.deployTransaction.hash, childProvider); + } + childContracts.WRAPPED_IMX_ADDRESS = wrappedIMX.address; + saveChildContracts(childContracts); console.log("Deployed to WRAPPED_IMX_ADDRESS: ", wrappedIMX.address); // Deploy proxy admin - console.log("Deploy proxy admin..."); - let proxyAdmin = await deployChildContract("ProxyAdmin", adminWallet); - console.log("Transaction submitted: ", JSON.stringify(proxyAdmin.deployTransaction, null, 2)); - await waitForReceipt(proxyAdmin.deployTransaction.hash, childProvider); - // Change owner - console.log("Change ownership..."); - [priorityFee, maxFee] = await getFee(childProvider); - resp = await proxyAdmin.connect(adminWallet).transferOwnership(childProxyAdmin, { - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }); - console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); - await waitForReceipt(resp.hash, childProvider); + let proxyAdmin; + if (childContracts.CHILD_PROXY_ADMIN != "") { + console.log("Proxy admin has already been deployed to: " + childContracts.CHILD_PROXY_ADMIN + ", skip."); + proxyAdmin = getContract("ProxyAdmin", childContracts.CHILD_PROXY_ADMIN, childProvider); + } else { + console.log("Deploy proxy admin..."); + proxyAdmin = await deployChildContract("ProxyAdmin", childDeployerWallet, null); + console.log("Transaction submitted: ", JSON.stringify(proxyAdmin.deployTransaction, null, 2)); + await waitForReceipt(proxyAdmin.deployTransaction.hash, childProvider); + } + childContracts.CHILD_PROXY_ADMIN = proxyAdmin.address; + saveChildContracts(childContracts); console.log("Deployed to CHILD_PROXY_ADMIN: ", proxyAdmin.address); // Deploy child bridge impl - console.log("Deploy child bridge impl..."); - let childBridgeImpl = await deployChildContract("ChildERC20Bridge", adminWallet); - console.log("Transaction submitted: ", JSON.stringify(childBridgeImpl.deployTransaction, null, 2)); - await waitForReceipt(childBridgeImpl.deployTransaction.hash, childProvider); + let childBridgeImpl; + if (childContracts.CHILD_BRIDGE_IMPL_ADDRESS != "") { + console.log("Child bridge impl has already been deployed to: " + childContracts.CHILD_BRIDGE_IMPL_ADDRESS + ", skip."); + childBridgeImpl = getContract("ChildERC20Bridge", childContracts.CHILD_BRIDGE_IMPL_ADDRESS, childProvider); + } else { + console.log("Deploy child bridge impl..."); + childBridgeImpl = await deployChildContract("ChildERC20Bridge", childDeployerWallet, null); + console.log("Transaction submitted: ", JSON.stringify(childBridgeImpl.deployTransaction, null, 2)); + await waitForReceipt(childBridgeImpl.deployTransaction.hash, childProvider); + } + childContracts.CHILD_BRIDGE_IMPL_ADDRESS = childBridgeImpl.address; + saveChildContracts(childContracts); console.log("Deployed to CHILD_BRIDGE_IMPL_ADDRESS: ", childBridgeImpl.address); // Deploy child bridge proxy - console.log("Deploy child bridge proxy..."); - let childBridgeProxy = await deployChildContract("TransparentUpgradeableProxy", adminWallet, childBridgeImpl.address, proxyAdmin.address, []); - console.log("Transaction submitted: ", JSON.stringify(childBridgeProxy.deployTransaction, null, 2)); - await waitForReceipt(childBridgeProxy.deployTransaction.hash, childProvider); + let childBridgeProxy; + if (childContracts.CHILD_BRIDGE_PROXY_ADDRESS != "") { + console.log("Child bridge proxy has already been deployed to: " + childContracts.CHILD_BRIDGE_PROXY_ADDRESS + ", skip."); + childBridgeProxy = getContract("TransparentUpgradeableProxy", childContracts.CHILD_BRIDGE_PROXY_ADDRESS, childProvider); + } else { + console.log("Deploy child bridge proxy..."); + childBridgeProxy = await deployChildContract("TransparentUpgradeableProxy", childDeployerWallet, null, childBridgeImpl.address, proxyAdmin.address, []); + console.log("Transaction submitted: ", JSON.stringify(childBridgeProxy.deployTransaction, null, 2)); + await waitForReceipt(childBridgeProxy.deployTransaction.hash, childProvider); + } + childContracts.CHILD_BRIDGE_PROXY_ADDRESS = childBridgeProxy.address; + saveChildContracts(childContracts); console.log("Deployed to CHILD_BRIDGE_PROXY_ADDRESS: ", childBridgeProxy.address); - + // Deploy child adaptor impl - console.log("Deploy child adaptor impl..."); - let childAdaptorImpl = await deployChildContract("ChildAxelarBridgeAdaptor", adminWallet, childGatewayAddr); - console.log("Transaction submitted: ", JSON.stringify(childAdaptorImpl.deployTransaction, null, 2)); - await waitForReceipt(childAdaptorImpl.deployTransaction.hash, childProvider); + let childAdaptorImpl; + if (childContracts.CHILD_ADAPTOR_IMPL_ADDRESS != "") { + console.log("Child adaptor impl has already been deployed to: " + childContracts.CHILD_ADAPTOR_IMPL_ADDRESS + ", skip."); + childAdaptorImpl = getContract("ChildAxelarBridgeAdaptor", childContracts.CHILD_ADAPTOR_IMPL_ADDRESS, childProvider); + } else { + console.log("Deploy child adaptor impl..."); + childAdaptorImpl = await deployChildContract("ChildAxelarBridgeAdaptor", childDeployerWallet, null, childGatewayAddr); + console.log("Transaction submitted: ", JSON.stringify(childAdaptorImpl.deployTransaction, null, 2)); + await waitForReceipt(childAdaptorImpl.deployTransaction.hash, childProvider); + } + childContracts.CHILD_ADAPTOR_IMPL_ADDRESS = childAdaptorImpl.address; + saveChildContracts(childContracts); console.log("Deployed to CHILD_ADAPTOR_IMPL_ADDRESS: ", childAdaptorImpl.address); // Deploy child adaptor proxy - console.log("Deploy child adaptor proxy..."); - let childAdaptorProxy = await deployChildContract("TransparentUpgradeableProxy", adminWallet, childAdaptorImpl.address, proxyAdmin.address, []); - console.log("Transaction submitted: ", JSON.stringify(childAdaptorProxy.deployTransaction, null, 2)); - await waitForReceipt(childAdaptorProxy.deployTransaction.hash, childProvider); + let childAdaptorProxy; + if (childContracts.CHILD_ADAPTOR_PROXY_ADDRESS != "") { + console.log("Child adaptor proxy has already been deployed to: " + childContracts.CHILD_ADAPTOR_PROXY_ADDRESS + ", skip."); + childAdaptorProxy = getContract("TransparentUpgradeableProxy", childContracts.CHILD_ADAPTOR_PROXY_ADDRESS, childProvider); + } else { + console.log("Deploy child adaptor proxy..."); + childAdaptorProxy = await deployChildContract("TransparentUpgradeableProxy", childDeployerWallet, null, childAdaptorImpl.address, proxyAdmin.address, []); + console.log("Transaction submitted: ", JSON.stringify(childAdaptorProxy.deployTransaction, null, 2)); + await waitForReceipt(childAdaptorProxy.deployTransaction.hash, childProvider); + } + childContracts.CHILD_ADAPTOR_PROXY_ADDRESS = childAdaptorProxy.address; + saveChildContracts(childContracts); console.log("Deployed to CHILD_ADAPTOR_PROXY_ADDRESS: ", childAdaptorProxy.address); - let contractData = { - CHILD_PROXY_ADMIN: proxyAdmin.address, - CHILD_BRIDGE_IMPL_ADDRESS: childBridgeImpl.address, - CHILD_BRIDGE_PROXY_ADDRESS: childBridgeProxy.address, - CHILD_BRIDGE_ADDRESS: childBridgeProxy.address, - CHILD_ADAPTOR_IMPL_ADDRESS: childAdaptorImpl.address, - CHILD_ADAPTOR_PROXY_ADDRESS: childAdaptorProxy.address, - CHILD_ADAPTOR_ADDRESS: childAdaptorProxy.address, - CHILD_TOKEN_TEMPLATE: childTokenTemplate.address, - WRAPPED_IMX_ADDRESS: wrappedIMX.address, - }; - fs.writeFileSync(".child.bridge.contracts.json", JSON.stringify(contractData, null, 2)); + childContracts.CHILD_BRIDGE_ADDRESS = childBridgeProxy.address; + childContracts.CHILD_ADAPTOR_ADDRESS = childAdaptorProxy.address, + saveChildContracts(childContracts); } \ No newline at end of file diff --git a/scripts/deploy/child_initialisation.ts b/scripts/deploy/child_initialisation.ts index 996e50a7..c7d8cc0d 100644 --- a/scripts/deploy/child_initialisation.ts +++ b/scripts/deploy/child_initialisation.ts @@ -3,51 +3,45 @@ import * as dotenv from "dotenv"; dotenv.config(); import { ethers } from "ethers"; -import { requireEnv, waitForConfirmation, waitForReceipt, getFee, getContract } from "../helpers/helpers"; +import { requireEnv, waitForConfirmation, waitForReceipt, getFee, getContract, getChildContracts, getRootContracts } from "../helpers/helpers"; import { LedgerSigner } from "../helpers/ledger_signer"; -import * as fs from "fs"; export async function initialiseChildContracts() { let rootChainName = requireEnv("ROOT_CHAIN_NAME"); let childRPCURL = requireEnv("CHILD_RPC_URL"); let childChainID = requireEnv("CHILD_CHAIN_ID"); - let adminEOAAddr = requireEnv("CHILD_ADMIN_ADDR"); - let childBridgeDefaultAdmin = requireEnv("CHILD_BRIDGE_DEFAULT_ADMIN"); - let childBridgePauser = requireEnv("CHILD_BRIDGE_PAUSER"); - let childBridgeUnpauser = requireEnv("CHILD_BRIDGE_UNPAUSER"); - let childBridgeAdaptorManager = requireEnv("CHILD_BRIDGE_ADAPTOR_MANAGER"); - let childAdaptorDefaultAdmin = requireEnv("CHILD_ADAPTOR_DEFAULT_ADMIN"); - let childAdaptorBridgeManager = requireEnv("CHILD_ADAPTOR_BRIDGE_MANAGER"); - let childAdaptorGasServiceManager = requireEnv("CHILD_ADAPTOR_GAS_SERVICE_MANAGER"); - let childAdaptorTargetManager = requireEnv("CHILD_ADAPTOR_TARGET_MANAGER"); - let childDeployerSecret = requireEnv("CHILD_DEPLOYER_SECRET"); + let deployerSecret = requireEnv("DEPLOYER_SECRET"); let childGasServiceAddr = requireEnv("CHILD_GAS_SERVICE_ADDRESS"); let multisigAddr = requireEnv("MULTISIG_CONTRACT_ADDRESS"); let rootIMXAddr = requireEnv("ROOT_IMX_ADDR"); // Read from contract file. - let data = fs.readFileSync(".child.bridge.contracts.json", 'utf-8'); - let childContracts = JSON.parse(data); + let childContracts = getChildContracts(); + let rootContracts = getRootContracts(); let childBridgeAddr = childContracts.CHILD_BRIDGE_ADDRESS; let childAdaptorAddr = childContracts.CHILD_ADAPTOR_ADDRESS; let childWIMXAddr = childContracts.WRAPPED_IMX_ADDRESS; let childTemplateAddr = childContracts.CHILD_TOKEN_TEMPLATE; - data = fs.readFileSync(".root.bridge.contracts.json", 'utf-8'); - let rootContracts = JSON.parse(data); let rootAdaptorAddr = rootContracts.ROOT_ADAPTOR_ADDRESS; + let rootTemplateAddr = rootContracts.ROOT_TOKEN_TEMPLATE; - // Get admin address + // Root token template must have the same address as child token template + if (childTemplateAddr != rootTemplateAddr) { + throw("Token template contract address mismatch: root " + rootTemplateAddr + " child " + childTemplateAddr); + } + + // Get deployer address const childProvider = new ethers.providers.JsonRpcProvider(childRPCURL, Number(childChainID)); - let adminWallet; - if (childDeployerSecret == "ledger") { - let index = requireEnv("CHILD_DEPLOYER_LEDGER_INDEX"); + let childDeployerWallet; + if (deployerSecret == "ledger") { + let index = requireEnv("DEPLOYER_LEDGER_INDEX"); const derivationPath = `m/44'/60'/${parseInt(index)}'/0/0`; - adminWallet = new LedgerSigner(childProvider, derivationPath); + childDeployerWallet = new LedgerSigner(childProvider, derivationPath); } else { - adminWallet = new ethers.Wallet(childDeployerSecret, childProvider); + childDeployerWallet = new ethers.Wallet(deployerSecret, childProvider); } - let adminAddr = await adminWallet.getAddress(); - console.log("Deployer address is: ", adminAddr); + let deployerAddr = await childDeployerWallet.getAddress(); + console.log("Deployer address is: ", deployerAddr); // Execute console.log("Initialise child contracts in..."); @@ -57,13 +51,13 @@ export async function initialiseChildContracts() { console.log("Initialise child bridge..."); let childBridge = getContract("ChildERC20Bridge", childBridgeAddr, childProvider); let [priorityFee, maxFee] = await getFee(childProvider); - let resp = await childBridge.connect(adminWallet).initialize( + let resp = await childBridge.connect(childDeployerWallet).initialize( { - defaultAdmin: childBridgeDefaultAdmin, - pauser: childBridgePauser, - unpauser: childBridgeUnpauser, - adaptorManager: childBridgeAdaptorManager, - initialDepositor: adminEOAAddr, + defaultAdmin: deployerAddr, + pauser: deployerAddr, + unpauser: deployerAddr, + adaptorManager: deployerAddr, + initialDepositor: deployerAddr, treasuryManager: multisigAddr, }, childAdaptorAddr, @@ -80,14 +74,13 @@ export async function initialiseChildContracts() { // Initialise child adaptor console.log("Initialise child adaptor..."); let childAdaptor = getContract("ChildAxelarBridgeAdaptor", childAdaptorAddr, childProvider); - // let childAdaptor = new ethers.Contract(childAdaptorAddr, childAdaptorObj.abi, childProvider); [priorityFee, maxFee] = await getFee(childProvider); - resp = await childAdaptor.connect(adminWallet).initialize( + resp = await childAdaptor.connect(childDeployerWallet).initialize( { - defaultAdmin: childAdaptorDefaultAdmin, - bridgeManager: childAdaptorBridgeManager, - gasServiceManager: childAdaptorGasServiceManager, - targetManager: childAdaptorTargetManager, + defaultAdmin: deployerAddr, + bridgeManager: deployerAddr, + gasServiceManager: deployerAddr, + targetManager: deployerAddr, }, childBridgeAddr, rootChainName, diff --git a/scripts/deploy/root_deployment.ts b/scripts/deploy/root_deployment.ts index 11cca6a7..68cac805 100644 --- a/scripts/deploy/root_deployment.ts +++ b/scripts/deploy/root_deployment.ts @@ -2,96 +2,158 @@ import * as dotenv from "dotenv"; dotenv.config(); import { ethers } from "ethers"; -import { requireEnv, waitForConfirmation, deployRootContract, waitForReceipt } from "../helpers/helpers"; +import { requireEnv, waitForConfirmation, deployRootContract, waitForReceipt, getRootContracts, getContract, saveRootContracts } from "../helpers/helpers"; import { LedgerSigner } from "../helpers/ledger_signer"; -import * as fs from "fs"; export async function deployRootContracts() { // Check environment variables let rootRPCURL = requireEnv("ROOT_RPC_URL"); let rootChainID = requireEnv("ROOT_CHAIN_ID"); - let rootDeployerSecret = requireEnv("ROOT_DEPLOYER_SECRET"); - let rootProxyAdmin = requireEnv("ROOT_PROXY_ADMIN"); + let deployerSecret = requireEnv("DEPLOYER_SECRET"); + let nonceReservedDeployerSecret = requireEnv("NONCE_RESERVED_DEPLOYER_SECRET"); + let nonceReserved = Number(requireEnv("NONCE_RESERVED")); let rootGatewayAddr = requireEnv("ROOT_GATEWAY_ADDRESS"); - // Get admin address + // Read from contract file. + let rootContracts = getRootContracts(); + + // Get deployer address const rootProvider = new ethers.providers.JsonRpcProvider(rootRPCURL, Number(rootChainID)); - let adminWallet; - if (rootDeployerSecret == "ledger") { - let index = requireEnv("ROOT_DEPLOYER_LEDGER_INDEX"); + let rootDeployerWallet; + if (deployerSecret == "ledger") { + let index = requireEnv("DEPLOYER_LEDGER_INDEX"); const derivationPath = `m/44'/60'/${parseInt(index)}'/0/0`; - adminWallet = new LedgerSigner(rootProvider, derivationPath); + rootDeployerWallet = new LedgerSigner(rootProvider, derivationPath); } else { - adminWallet = new ethers.Wallet(rootDeployerSecret, rootProvider); + rootDeployerWallet = new ethers.Wallet(deployerSecret, rootProvider); + } + let deployerAddr = await rootDeployerWallet.getAddress(); + console.log("Deployer address is: ", deployerAddr); + + // Get reserved wallet + let reservedDeployerWallet; + if (nonceReservedDeployerSecret == "ledger") { + let index = requireEnv("NONCE_RESERVED_DEPLOYER_INDEX"); + const derivationPath = `m/44'/60'/${parseInt(index)}'/0/0`; + reservedDeployerWallet = new LedgerSigner(rootProvider, derivationPath); + } else { + reservedDeployerWallet = new ethers.Wallet(nonceReservedDeployerSecret, rootProvider); + } + let reservedDeployerAddr = await reservedDeployerWallet.getAddress(); + console.log("Reserved deployer address is: ", reservedDeployerAddr); + + // Check the current nonce matches the reserved nonce + let currentNonce = await rootProvider.getTransactionCount(reservedDeployerAddr); + if (nonceReserved != currentNonce) { + throw("Nonce mismatch, expected " + nonceReserved + " actual " + currentNonce); } - let adminAddr = await adminWallet.getAddress(); - console.log("Deployer address is: ", adminAddr); // Execute console.log("Deploy root contracts in..."); await waitForConfirmation(); // Deploy root token template - console.log("Deploy root token template..."); - let rootTokenTemplate = await deployRootContract("ChildERC20", adminWallet); - console.log("Transaction submitted: ", JSON.stringify(rootTokenTemplate.deployTransaction, null, 2)); - await waitForReceipt(rootTokenTemplate.deployTransaction.hash, rootProvider); + let rootTokenTemplate; + if (rootContracts.ROOT_TOKEN_TEMPLATE != "") { + console.log("Root token template has already been deployed to: " + rootContracts.ROOT_TOKEN_TEMPLATE + ", skip."); + rootTokenTemplate = getContract("ChildERC20", rootContracts.ROOT_TOKEN_TEMPLATE, rootProvider); + } else { + console.log("Deploy root token template..."); + rootTokenTemplate = await deployRootContract("ChildERC20", reservedDeployerWallet, nonceReserved); + console.log("Transaction submitted: ", JSON.stringify(rootTokenTemplate.deployTransaction, null, 2)); + await waitForReceipt(rootTokenTemplate.deployTransaction.hash, rootProvider); + } + rootContracts.ROOT_TOKEN_TEMPLATE = rootTokenTemplate.address; + saveRootContracts(rootContracts); + console.log("Deployed to ROOT_TOKEN_TEMPLATE: ", rootTokenTemplate.address); + // Initialise template - console.log("Initialise root token template..."); - let resp = await rootTokenTemplate.connect(adminWallet).initialize("000000000000000000000000000000000000007B", "TEMPLATE", "TPT", 18); - console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); - await waitForReceipt(resp.hash, rootProvider); + if (await rootTokenTemplate.name() == "TEMPLATE") { + console.log("Root token template has already been initialised, skip."); + } else { + console.log("Initialise root token template..."); + let resp = await rootTokenTemplate.connect(reservedDeployerWallet).initialize("000000000000000000000000000000000000007B", "TEMPLATE", "TPT", 18); + console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); + await waitForReceipt(resp.hash, rootProvider); + } console.log("Deployed to ROOT_TOKEN_TEMPLATE: ", rootTokenTemplate.address); - + // Deploy proxy admin - console.log("Deploy proxy admin..."); - let proxyAdmin = await deployRootContract("ProxyAdmin", adminWallet); - console.log("Transaction submitted: ", JSON.stringify(proxyAdmin.deployTransaction, null, 2)); - await waitForReceipt(proxyAdmin.deployTransaction.hash, rootProvider); - // Change owner - console.log("Change ownership...") - resp = await proxyAdmin.connect(adminWallet).transferOwnership(rootProxyAdmin); - console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); - await waitForReceipt(resp.hash, rootProvider); + let proxyAdmin; + if (rootContracts.ROOT_PROXY_ADMIN != "") { + console.log("Proxy admin has already been deployed to: " + rootContracts.ROOT_PROXY_ADMIN + ", skip."); + proxyAdmin = getContract("ProxyAdmin", rootContracts.ROOT_PROXY_ADMIN, rootProvider); + } else { + console.log("Deploy proxy admin..."); + proxyAdmin = await deployRootContract("ProxyAdmin", rootDeployerWallet, null); + console.log("Transaction submitted: ", JSON.stringify(proxyAdmin.deployTransaction, null, 2)); + await waitForReceipt(proxyAdmin.deployTransaction.hash, rootProvider); + } + rootContracts.ROOT_PROXY_ADMIN = proxyAdmin.address; + saveRootContracts(rootContracts); console.log("Deployed to ROOT_PROXY_ADMIN: ", proxyAdmin.address); // Deploy root bridge impl - console.log("Deploy root bridge impl..."); - let rootBridgeImpl = await deployRootContract("RootERC20BridgeFlowRate", adminWallet); - console.log("Transaction submitted: ", JSON.stringify(rootBridgeImpl.deployTransaction, null, 2)); - await waitForReceipt(rootBridgeImpl.deployTransaction.hash, rootProvider); + let rootBridgeImpl; + if (rootContracts.ROOT_BRIDGE_IMPL_ADDRESS != "") { + console.log("Root bridge impl has already been deployed to: " + rootContracts.ROOT_BRIDGE_IMPL_ADDRESS + ", skip."); + rootBridgeImpl = getContract("RootERC20BridgeFlowRate", rootContracts.ROOT_BRIDGE_IMPL_ADDRESS, rootProvider); + } else { + console.log("Deploy root bridge impl..."); + rootBridgeImpl = await deployRootContract("RootERC20BridgeFlowRate", rootDeployerWallet, null); + console.log("Transaction submitted: ", JSON.stringify(rootBridgeImpl.deployTransaction, null, 2)); + await waitForReceipt(rootBridgeImpl.deployTransaction.hash, rootProvider); + } + rootContracts.ROOT_BRIDGE_IMPL_ADDRESS = rootBridgeImpl.address; + saveRootContracts(rootContracts); console.log("Deployed to ROOT_BRIDGE_IMPL_ADDRESS: ", rootBridgeImpl.address); // Deploy root bridge proxy - console.log("Deploy root bridge proxy..."); - let rootBridgeProxy = await deployRootContract("TransparentUpgradeableProxy", adminWallet, rootBridgeImpl.address, proxyAdmin.address, []); - console.log("Transaction submitted: ", JSON.stringify(rootBridgeProxy.deployTransaction, null, 2)); - await waitForReceipt(rootBridgeProxy.deployTransaction.hash, rootProvider); + let rootBridgeProxy; + if (rootContracts.ROOT_BRIDGE_PROXY_ADDRESS != "") { + console.log("Root bridge proxy has already been deployed to: " + rootContracts.ROOT_BRIDGE_PROXY_ADDRESS + ", skip."); + rootBridgeProxy = getContract("TransparentUpgradeableProxy", rootContracts.ROOT_BRIDGE_PROXY_ADDRESS, rootProvider); + } else { + console.log("Deploy root bridge proxy..."); + rootBridgeProxy = await deployRootContract("TransparentUpgradeableProxy", rootDeployerWallet, null, rootBridgeImpl.address, proxyAdmin.address, []); + console.log("Transaction submitted: ", JSON.stringify(rootBridgeProxy.deployTransaction, null, 2)); + await waitForReceipt(rootBridgeProxy.deployTransaction.hash, rootProvider); + } + rootContracts.ROOT_BRIDGE_PROXY_ADDRESS = rootBridgeProxy.address; + saveRootContracts(rootContracts); console.log("Deployed to ROOT_BRIDGE_PROXY_ADDRESS: ", rootBridgeProxy.address); // Deploy root adaptor impl - console.log("Deploy root adaptor impl..."); - let rootAdaptorImpl = await deployRootContract("RootAxelarBridgeAdaptor", adminWallet, rootGatewayAddr); - console.log("Transaction submitted: ", JSON.stringify(rootAdaptorImpl.deployTransaction, null, 2)); - await waitForReceipt(rootAdaptorImpl.deployTransaction.hash, rootProvider); + let rootAdaptorImpl; + if (rootContracts.ROOT_ADAPTOR_IMPL_ADDRESS != "") { + console.log("Root adaptor impl has already been deployed to: " + rootContracts.ROOT_ADAPTOR_IMPL_ADDRESS + ", skip."); + rootAdaptorImpl = getContract("RootAxelarBridgeAdaptor", rootContracts.ROOT_ADAPTOR_IMPL_ADDRESS, rootProvider); + } else { + console.log("Deploy root adaptor impl..."); + rootAdaptorImpl = await deployRootContract("RootAxelarBridgeAdaptor", rootDeployerWallet, null, rootGatewayAddr); + console.log("Transaction submitted: ", JSON.stringify(rootAdaptorImpl.deployTransaction, null, 2)); + await waitForReceipt(rootAdaptorImpl.deployTransaction.hash, rootProvider); + } + rootContracts.ROOT_ADAPTOR_IMPL_ADDRESS = rootAdaptorImpl.address; + saveRootContracts(rootContracts); console.log("Deployed to ROOT_ADAPTOR_IMPL_ADDRESS: ", rootAdaptorImpl.address); // Deploy root adaptor proxy - console.log("Deploy root adaptor proxy..."); - let rootAdaptorProxy = await deployRootContract("TransparentUpgradeableProxy", adminWallet, rootAdaptorImpl.address, proxyAdmin.address, []); - console.log("Transaction submitted: ", JSON.stringify(rootAdaptorProxy.deployTransaction, null, 2)); - await waitForReceipt(rootAdaptorProxy.deployTransaction.hash, rootProvider); + let rootAdaptorProxy; + if (rootContracts.ROOT_ADAPTOR_PROXY_ADDRESS != "") { + console.log("Root adaptor proxy has already been deployed to: " + rootContracts.ROOT_ADAPTOR_PROXY_ADDRESS + ", skip."); + rootAdaptorProxy = getContract("TransparentUpgradeableProxy", rootContracts.ROOT_ADAPTOR_PROXY_ADDRESS, rootProvider); + } else { + console.log("Deploy root adaptor proxy..."); + rootAdaptorProxy = await deployRootContract("TransparentUpgradeableProxy", rootDeployerWallet, null, rootAdaptorImpl.address, proxyAdmin.address, []); + console.log("Transaction submitted: ", JSON.stringify(rootAdaptorProxy.deployTransaction, null, 2)); + await waitForReceipt(rootAdaptorProxy.deployTransaction.hash, rootProvider); + } + rootContracts.ROOT_ADAPTOR_PROXY_ADDRESS = rootAdaptorProxy.address; + saveRootContracts(rootContracts); console.log("Deployed to ROOT_ADAPTOR_PROXY_ADDRESS: ", rootAdaptorProxy.address); - let contractData = { - ROOT_PROXY_ADMIN: rootBridgeImpl.address, - ROOT_BRIDGE_IMPL_ADDRESS: rootBridgeImpl.address, - ROOT_BRIDGE_PROXY_ADDRESS: rootBridgeProxy.address, - ROOT_BRIDGE_ADDRESS: rootBridgeProxy.address, - ROOT_ADAPTOR_IMPL_ADDRESS: rootAdaptorImpl.address, - ROOT_ADAPTOR_PROXY_ADDRESS: rootAdaptorProxy.address, - ROOT_ADAPTOR_ADDRESS: rootAdaptorProxy.address, - ROOT_TOKEN_TEMPLATE: rootTokenTemplate.address, - }; - fs.writeFileSync(".root.bridge.contracts.json", JSON.stringify(contractData, null, 2)); + rootContracts.ROOT_BRIDGE_ADDRESS = rootBridgeProxy.address; + rootContracts.ROOT_ADAPTOR_ADDRESS = rootAdaptorProxy.address; + saveRootContracts(rootContracts); } \ No newline at end of file diff --git a/scripts/deploy/root_initialisation.ts b/scripts/deploy/root_initialisation.ts index 1f61002f..a11448b4 100644 --- a/scripts/deploy/root_initialisation.ts +++ b/scripts/deploy/root_initialisation.ts @@ -2,26 +2,15 @@ import * as dotenv from "dotenv"; dotenv.config(); import { ethers } from "ethers"; -import { requireEnv, waitForConfirmation, waitForReceipt, getContract } from "../helpers/helpers"; +import { requireEnv, waitForConfirmation, waitForReceipt, getContract, getChildContracts, getRootContracts } from "../helpers/helpers"; import { LedgerSigner } from "../helpers/ledger_signer"; -import * as fs from "fs"; export async function initialiseRootContracts() { // Check environment variables let childChainName = requireEnv("CHILD_CHAIN_NAME"); let rootRPCURL = requireEnv("ROOT_RPC_URL"); let rootChainID = requireEnv("ROOT_CHAIN_ID"); - let rootDeployerSecret = requireEnv("ROOT_DEPLOYER_SECRET"); - let rootRateAdminSecret = requireEnv("ROOT_BRIDGE_RATE_ADMIN_SECRET"); - let rootBridgeDefaultAdmin = requireEnv("ROOT_BRIDGE_DEFAULT_ADMIN"); - let rootBridgePauser = requireEnv("ROOT_BRIDGE_PAUSER"); - let rootBridgeUnpauser = requireEnv("ROOT_BRIDGE_UNPAUSER"); - let rootBridgeVariableManager = requireEnv("ROOT_BRIDGE_VARIABLE_MANAGER"); - let rootBridgeAdaptorManager = requireEnv("ROOT_BRIDGE_ADAPTOR_MANAGER"); - let rootAdaptorDefaultAdmin = requireEnv("ROOT_ADAPTOR_DEFAULT_ADMIN"); - let rootAdaptorBridgeManager = requireEnv("ROOT_ADAPTOR_BRIDGE_MANAGER"); - let rootAdaptorGasServiceManager = requireEnv("ROOT_ADAPTOR_GAS_SERVICE_MANAGER"); - let rootAdaptorTargetManager = requireEnv("ROOT_ADAPTOR_TARGET_MANAGER"); + let deployerSecret = requireEnv("DEPLOYER_SECRET"); let rootGasServiceAddr = requireEnv("ROOT_GAS_SERVICE_ADDRESS"); let rootIMXAddr = requireEnv("ROOT_IMX_ADDR"); let rootWETHAddr = requireEnv("ROOT_WETH_ADDR"); @@ -50,41 +39,26 @@ export async function initialiseRootContracts() { let rateLimitGOGLargeThreshold = requireEnv("RATE_LIMIT_GOG_LARGE_THRESHOLD"); // Read from contract file. - let data = fs.readFileSync(".child.bridge.contracts.json", 'utf-8'); - let childContracts = JSON.parse(data); + let childContracts = getChildContracts(); let childBridgeAddr = childContracts.CHILD_BRIDGE_ADDRESS; let childAdaptorAddr = childContracts.CHILD_ADAPTOR_ADDRESS; - data = fs.readFileSync(".root.bridge.contracts.json", 'utf-8'); - let rootContracts = JSON.parse(data); + let rootContracts = getRootContracts(); let rootBridgeAddr = rootContracts.ROOT_BRIDGE_ADDRESS; let rootAdaptorAddr = rootContracts.ROOT_ADAPTOR_ADDRESS; let rootTemplateAddr = rootContracts.ROOT_TOKEN_TEMPLATE; - // Get admin address + // Get deployer address const rootProvider = new ethers.providers.JsonRpcProvider(rootRPCURL, Number(rootChainID)); - let adminWallet; - if (rootDeployerSecret == "ledger") { - let index = requireEnv("ROOT_DEPLOYER_LEDGER_INDEX"); + let rootDeployerWallet; + if (deployerSecret == "ledger") { + let index = requireEnv("DEPLOYER_LEDGER_INDEX"); const derivationPath = `m/44'/60'/${parseInt(index)}'/0/0`; - adminWallet = new LedgerSigner(rootProvider, derivationPath); + rootDeployerWallet = new LedgerSigner(rootProvider, derivationPath); } else { - adminWallet = new ethers.Wallet(rootDeployerSecret, rootProvider); + rootDeployerWallet = new ethers.Wallet(deployerSecret, rootProvider); } - let adminAddr = await adminWallet.getAddress(); - console.log("Deployer address is: ", adminAddr); - - // Get rate admin address - let rateAdminWallet; - if (rootRateAdminSecret == "ledger") { - let index = requireEnv("ROOT_BRIDGE_RATE_ADMIN_LEDGER_INDEX"); - const derivationPath = `m/44'/60'/${parseInt(index)}'/0/0`; - rateAdminWallet = new LedgerSigner(rootProvider, derivationPath); - } else { - rateAdminWallet = new ethers.Wallet(rootRateAdminSecret, rootProvider); - } - let rateAdminAddr = await rateAdminWallet.getAddress(); - console.log("Rate admin address is: ", rateAdminAddr); - + let deployerAddr = await rootDeployerWallet.getAddress(); + console.log("Deployer address is: ", deployerAddr); // Execute console.log("Initialise root contracts in..."); @@ -93,13 +67,13 @@ export async function initialiseRootContracts() { // Initialise root bridge console.log("Initialise root bridge..."); let rootBridge = getContract("RootERC20BridgeFlowRate", rootBridgeAddr, rootProvider); - let resp = await rootBridge.connect(adminWallet)["initialize((address,address,address,address,address),address,address,address,address,address,uint256,address)"]( + let resp = await rootBridge.connect(rootDeployerWallet)["initialize((address,address,address,address,address),address,address,address,address,address,uint256,address)"]( { - defaultAdmin: rootBridgeDefaultAdmin, - pauser: rootBridgePauser, - unpauser: rootBridgeUnpauser, - variableManager: rootBridgeVariableManager, - adaptorManager: rootBridgeAdaptorManager, + defaultAdmin: deployerAddr, + pauser: deployerAddr, + unpauser: deployerAddr, + variableManager: deployerAddr, + adaptorManager: deployerAddr, }, rootAdaptorAddr, childBridgeAddr, @@ -107,14 +81,14 @@ export async function initialiseRootContracts() { rootIMXAddr, rootWETHAddr, ethers.utils.parseEther(imxDepositLimit), - rateAdminAddr); + deployerAddr); console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); await waitForReceipt(resp.hash, rootProvider); // Configure rate // IMX console.log("Configure rate limiting for IMX...") - resp = await rootBridge.connect(rateAdminWallet).setRateControlThreshold( + resp = await rootBridge.connect(rootDeployerWallet).setRateControlThreshold( rootIMXAddr, ethers.utils.parseEther(rateLimitIMXCap), ethers.utils.parseEther(rateLimitIMXRefill), @@ -125,7 +99,7 @@ export async function initialiseRootContracts() { // ETH console.log("Configure rate limiting for ETH...") - resp = await rootBridge.connect(rateAdminWallet).setRateControlThreshold( + resp = await rootBridge.connect(rootDeployerWallet).setRateControlThreshold( await rootBridge.NATIVE_ETH(), ethers.utils.parseEther(rateLimitETHCap), ethers.utils.parseEther(rateLimitETHRefill), @@ -136,7 +110,7 @@ export async function initialiseRootContracts() { // USDC console.log("Configure rate limiting for USDC...") - resp = await rootBridge.connect(rateAdminWallet).setRateControlThreshold( + resp = await rootBridge.connect(rootDeployerWallet).setRateControlThreshold( rateLimitUSDCAddr, ethers.utils.parseEther(rateLimitUSDCCap), ethers.utils.parseEther(rateLimitUSDCRefill), @@ -147,7 +121,7 @@ export async function initialiseRootContracts() { // GU console.log("Configure rate limiting for GU...") - resp = await rootBridge.connect(rateAdminWallet).setRateControlThreshold( + resp = await rootBridge.connect(rootDeployerWallet).setRateControlThreshold( rateLimitGUAddr, ethers.utils.parseEther(rateLimitGUCap), ethers.utils.parseEther(rateLimitGURefill), @@ -158,7 +132,7 @@ export async function initialiseRootContracts() { // Checkmate console.log("Configure rate limiting for CheckMate...") - resp = await rootBridge.connect(rateAdminWallet).setRateControlThreshold( + resp = await rootBridge.connect(rootDeployerWallet).setRateControlThreshold( rateLimitCheckMateAddr, ethers.utils.parseEther(rateLimitCheckMateCap), ethers.utils.parseEther(rateLimitCheckMateRefill), @@ -169,7 +143,7 @@ export async function initialiseRootContracts() { // GOG console.log("Configure rate limiting for GOG...") - resp = await rootBridge.connect(rateAdminWallet).setRateControlThreshold( + resp = await rootBridge.connect(rootDeployerWallet).setRateControlThreshold( rateLimitGOGAddr, ethers.utils.parseEther(rateLimitGOGCap), ethers.utils.parseEther(rateLimitGOGRefill), @@ -181,12 +155,12 @@ export async function initialiseRootContracts() { // Initialise root adaptor console.log("Initialise root adaptor..."); let rootAdaptor = getContract("RootAxelarBridgeAdaptor", rootAdaptorAddr, rootProvider); - resp = await rootAdaptor.connect(adminWallet).initialize( + resp = await rootAdaptor.connect(rootDeployerWallet).initialize( { - defaultAdmin: rootAdaptorDefaultAdmin, - bridgeManager: rootAdaptorBridgeManager, - gasServiceManager: rootAdaptorGasServiceManager, - targetManager: rootAdaptorTargetManager, + defaultAdmin: deployerAddr, + bridgeManager: deployerAddr, + gasServiceManager: deployerAddr, + targetManager: deployerAddr, }, rootBridgeAddr, childChainName, diff --git a/scripts/e2e/.root.bridge.contracts.json.example b/scripts/e2e/.root.bridge.contracts.json.example index 4a593209..8484c978 100644 --- a/scripts/e2e/.root.bridge.contracts.json.example +++ b/scripts/e2e/.root.bridge.contracts.json.example @@ -1,5 +1,6 @@ { "ROOT_BRIDGE_ADDRESS": "", "ROOT_ADAPTOR_ADDRESS": "", - "ROOT_TOKEN_TEMPLATE": "" + "ROOT_TOKEN_TEMPLATE": "", + "ROOT_TEST_CUSTOM_TOKEN": "" } \ No newline at end of file diff --git a/scripts/e2e/README.md b/scripts/e2e/README.md index e3c39e80..3989fbb5 100644 --- a/scripts/e2e/README.md +++ b/scripts/e2e/README.md @@ -45,11 +45,12 @@ TEST_ACCOUNT_SECRET= { "ROOT_BRIDGE_ADDRESS": "", "ROOT_ADAPTOR_ADDRESS": "", - "ROOT_TOKEN_TEMPLATE": "" + "ROOT_TOKEN_TEMPLATE": "", + "ROOT_TEST_CUSTOM_TOKEN": "" } ``` 3. Run end to end tests ``` -npx mocha --require mocha-suppress-logs ./e2e.ts +LONG_WAIT=1200000 SHORT_WAIT=300000 npx mocha --require mocha-suppress-logs ./e2e.ts ``` \ No newline at end of file diff --git a/scripts/e2e/e2e.ts b/scripts/e2e/e2e.ts index d62e0f49..7adba223 100644 --- a/scripts/e2e/e2e.ts +++ b/scripts/e2e/e2e.ts @@ -2,8 +2,7 @@ import * as dotenv from "dotenv"; dotenv.config(); import { ethers, providers } from "ethers"; -import { requireEnv, waitForReceipt, getFee, getContract, deployRootContract, delay } from "../helpers/helpers"; -import * as fs from "fs"; +import { requireEnv, waitForReceipt, getFee, getContract, delay, getChildContracts, getRootContracts, saveChildContracts } from "../helpers/helpers"; import { expect } from "chai"; // The contract ABI of IMX on L1. @@ -22,54 +21,50 @@ describe("Bridge e2e test", () => { let childWIMX: ethers.Contract; let rootCustomToken: ethers.Contract; let childCustomToken: ethers.Contract; + let longWait: number; + let shortWait: number; before(async function () { - this.timeout(30000); + this.timeout(300000); let rootRPCURL = requireEnv("ROOT_RPC_URL"); let rootChainID = requireEnv("ROOT_CHAIN_ID"); let childRPCURL = requireEnv("CHILD_RPC_URL"); let childChainID = requireEnv("CHILD_CHAIN_ID"); - let rootRateAdminSecret = requireEnv("ROOT_BRIDGE_RATE_ADMIN_SECRET"); let testAccountKey = requireEnv("TEST_ACCOUNT_SECRET"); let rootIMXAddr = requireEnv("ROOT_IMX_ADDR"); let rootWETHAddr = requireEnv("ROOT_WETH_ADDR"); + if (process.env["LONG_WAIT"] == null || process.env["LONG_WAIT"] == "") { + longWait = 1200000; + } else { + longWait = Number(process.env["LONG_WAIT"]) + } + if (process.env["SHORT_WAIT"] == null || process.env["SHORT_WAIT"] == "") { + longWait = 300000; + } else { + longWait = Number(process.env["SHORT_WAIT"]) + } + // Read from contract file. - let data = fs.readFileSync(".child.bridge.contracts.json", 'utf-8'); - let childContracts = JSON.parse(data); + let childContracts = getChildContracts(); let childBridgeAddr = childContracts.CHILD_BRIDGE_ADDRESS; let childWIMXAddr = childContracts.WRAPPED_IMX_ADDRESS; - data = fs.readFileSync(".root.bridge.contracts.json", 'utf-8'); - let rootContracts = JSON.parse(data); + let rootContracts = getRootContracts(); let rootBridgeAddr = rootContracts.ROOT_BRIDGE_ADDRESS; + let rootCustomTokenAddr = rootContracts.ROOT_TEST_CUSTOM_TOKEN; rootProvider = new ethers.providers.JsonRpcProvider(rootRPCURL, Number(rootChainID)); childProvider = new ethers.providers.JsonRpcProvider(childRPCURL, Number(childChainID)); rootTestWallet = new ethers.Wallet(testAccountKey, rootProvider); childTestWallet = new ethers.Wallet(testAccountKey, childProvider); - let rootRateAdminWallet = new ethers.Wallet(rootRateAdminSecret, rootProvider); rootBridge = getContract("RootERC20BridgeFlowRate", rootBridgeAddr, rootProvider); rootWETH = getContract("WETH", rootWETHAddr, rootProvider); rootIMX = new ethers.Contract(rootIMXAddr, IMX_ABI, rootProvider); + rootCustomToken = getContract("ChildERC20", rootCustomTokenAddr, rootProvider); childBridge = getContract("ChildERC20Bridge", childBridgeAddr, childProvider); childETH = getContract("ChildERC20", await childBridge.childETHToken(), childProvider); childWIMX = getContract("WIMX", childWIMXAddr, childProvider); - - // Deploy a custom token - rootCustomToken = await deployRootContract("ERC20PresetMinterPauser", rootTestWallet, "Custom Token", "CTK"); - await waitForReceipt(rootCustomToken.deployTransaction.hash, rootProvider); - // Mint tokens - let resp = await rootCustomToken.connect(rootTestWallet).mint(rootTestWallet.address, ethers.utils.parseEther("1000.0").toBigInt()); - await waitForReceipt(resp.hash, rootProvider); - // Set rate control - resp = await rootBridge.connect(rootRateAdminWallet).setRateControlThreshold( - rootCustomToken.address, - ethers.utils.parseEther("20016.0"), - ethers.utils.parseEther("5.56"), - ethers.utils.parseEther("10008.0") - ); - await waitForReceipt(resp.hash, rootProvider); }) it("should successfully deposit IMX to self from L1 to L2", async() => { @@ -93,6 +88,10 @@ describe("Bridge e2e test", () => { let postBalL1 = await rootIMX.balanceOf(rootTestWallet.address); let postBalL2 = preBalL2; + console.log("Wait " + longWait + " ms"); + await delay(longWait); + console.log("Done"); + while (postBalL2.eq(preBalL2)) { postBalL2 = await childProvider.getBalance(childTestWallet.address); await delay(1000); @@ -103,7 +102,7 @@ describe("Bridge e2e test", () => { let expectedPostL2 = preBalL2.add(amt); expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); - }).timeout(60000) + }).timeout(1800000) it("should successfully withdraw IMX to self from L2 to L1", async() => { // Get IMX balance on root & child chains before withdraw @@ -125,6 +124,10 @@ describe("Bridge e2e test", () => { let postBalL1 = preBalL1; let postBalL2 = await childProvider.getBalance(childTestWallet.address); + console.log("Wait " + shortWait + " ms"); + await delay(shortWait); + console.log("Done"); + while (postBalL1.eq(preBalL1)) { postBalL1 = await rootIMX.balanceOf(rootTestWallet.address); await delay(1000); @@ -132,12 +135,12 @@ describe("Bridge e2e test", () => { // Verify let receipt = await childProvider.getTransactionReceipt(resp.hash); - let txFee = receipt.cumulativeGasUsed.mul(receipt.effectiveGasPrice); + let txFee = receipt.gasUsed.mul(receipt.effectiveGasPrice); let expectedPostL1 = preBalL1.add(amt); let expectedPostL2 = preBalL2.sub(txFee).sub(amt).sub(bridgeFee); expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); - }).timeout(120000) + }).timeout(1800000) it("should successfully withdraw wIMX to self from L2 to L1", async() => { // Wrap 1 IMX @@ -176,6 +179,10 @@ describe("Bridge e2e test", () => { let postBalL1 = preBalL1; let postBalL2 = await childWIMX.balanceOf(childTestWallet.address); + console.log("Wait " + shortWait + " ms"); + await delay(shortWait); + console.log("Done"); + while (postBalL1.eq(preBalL1)) { postBalL1 = await rootIMX.balanceOf(rootTestWallet.address); await delay(1000); @@ -186,7 +193,7 @@ describe("Bridge e2e test", () => { let expectedPostL2 = preBalL2.sub(amt); expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); - }).timeout(120000) + }).timeout(1800000) it("should successfully deposit ETH to self from L1 to L2", async() => { // Get ETH balance on root & child chains before deposit @@ -205,6 +212,10 @@ describe("Bridge e2e test", () => { let postBalL1 = await rootProvider.getBalance(rootTestWallet.address); let postBalL2 = preBalL2; + console.log("Wait " + longWait + " ms"); + await delay(longWait); + console.log("Done"); + while (postBalL2.eq(preBalL2)) { postBalL2 = await childETH.balanceOf(childTestWallet.address); await delay(1000); @@ -217,7 +228,7 @@ describe("Bridge e2e test", () => { let expectedPostL2 = preBalL2.add(amt); expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); - }).timeout(60000) + }).timeout(1800000) it("should successfully deposit wETH to self from L1 to L2", async() => { // Wrap 0.01 ETH @@ -246,6 +257,10 @@ describe("Bridge e2e test", () => { let postBalL1 = await rootWETH.balanceOf(rootTestWallet.address); let postBalL2 = preBalL2; + console.log("Wait " + longWait + " ms"); + await delay(longWait); + console.log("Done"); + while (postBalL2.eq(preBalL2)) { postBalL2 = await childETH.balanceOf(childTestWallet.address); await delay(1000); @@ -256,7 +271,7 @@ describe("Bridge e2e test", () => { let expectedPostL2 = preBalL2.add(amt); expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); - }).timeout(60000) + }).timeout(1800000) it("should successfully withdraw ETH to self from L2 to L1", async() => { // Get ETH balance on root & child chains before withdraw @@ -278,6 +293,10 @@ describe("Bridge e2e test", () => { let postBalL1 = preBalL1; let postBalL2 = await childETH.balanceOf(childTestWallet.address); + console.log("Wait " + shortWait + " ms"); + await delay(shortWait); + console.log("Done"); + while (postBalL1.eq(preBalL1)) { postBalL1 = await rootProvider.getBalance(rootTestWallet.address); await delay(1000); @@ -288,9 +307,16 @@ describe("Bridge e2e test", () => { let expectedPostL2 = preBalL2.sub(amt); expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); - }).timeout(120000) + }).timeout(1800000) it("should successfully map a ERC20 Token", async() => { + let childContracts = getChildContracts(); + let childCustomTokenAddr = childContracts.CHILD_TEST_CUSTOM_TOKEN; + if (childCustomTokenAddr != "") { + childCustomToken = getContract("ChildERC20", childCustomTokenAddr, childProvider); + console.log("Custom token has already been mapped, skip."); + return; + } // Map token let bridgeFee = ethers.utils.parseEther("0.001"); let expectedChildTokenAddr = await rootBridge.callStatic.mapToken(rootCustomToken.address, { @@ -302,15 +328,22 @@ describe("Bridge e2e test", () => { await waitForReceipt(resp.hash, rootProvider); let childTokenAddr = await childBridge.rootTokenToChildToken(rootCustomToken.address); + + console.log("Wait " + longWait + " ms"); + await delay(longWait); + console.log("Done"); + while (childTokenAddr == ethers.constants.AddressZero) { childTokenAddr = await childBridge.rootTokenToChildToken(rootCustomToken.address); await delay(1000); } childCustomToken = getContract("ChildERC20", childTokenAddr, childProvider); + childContracts.CHILD_TEST_CUSTOM_TOKEN = childTokenAddr; + saveChildContracts(childContracts); // Verify expect(childTokenAddr).to.equal(expectedChildTokenAddr); - }).timeout(60000) + }).timeout(1800000) it("should successfully deposit mapped ERC20 Token to self from L1 to L2", async() => { // Get token balance on root & child chains before deposit @@ -332,6 +365,11 @@ describe("Bridge e2e test", () => { let postBalL1 = await rootCustomToken.balanceOf(rootTestWallet.address); let postBalL2 = preBalL2; + + console.log("Wait " + longWait + " ms"); + await delay(longWait); + console.log("Done"); + while (postBalL2.eq(preBalL2)) { postBalL2 = await childCustomToken.balanceOf(childTestWallet.address); await delay(1000); @@ -342,7 +380,7 @@ describe("Bridge e2e test", () => { let expectedPostL2 = preBalL2.add(amt); expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); - }).timeout(60000) + }).timeout(1800000) it("should successfully withdraw mapped ERC20 Token to self from L2 to L1", async() => { // Get token balance on root & child chains before deposit @@ -364,6 +402,10 @@ describe("Bridge e2e test", () => { let postBalL1 = preBalL1; let postBalL2 = await childCustomToken.balanceOf(childTestWallet.address); + console.log("Wait " + shortWait + " ms"); + await delay(shortWait); + console.log("Done"); + while (postBalL1.eq(preBalL1)) { postBalL1 = await rootCustomToken.balanceOf(rootTestWallet.address); await delay(1000); @@ -374,5 +416,5 @@ describe("Bridge e2e test", () => { let expectedPostL2 = preBalL2.sub(amt); expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); - }).timeout(120000) + }).timeout(1800000) }) \ No newline at end of file diff --git a/scripts/helpers/helpers.ts b/scripts/helpers/helpers.ts index 678a4be1..10f238ba 100644 --- a/scripts/helpers/helpers.ts +++ b/scripts/helpers/helpers.ts @@ -68,23 +68,90 @@ export function hasDuplicates(array: string[]) { return (new Set(array)).size !== array.length; } -export async function deployChildContract(contract: string, adminWallet: ethers.Wallet | LedgerSigner, ...args: any) { +export async function deployChildContract(contract: string, adminWallet: ethers.Wallet | LedgerSigner, reservedNonce: number | null, ...args: any) { let contractObj = JSON.parse(fs.readFileSync(`../../out/${contract}.sol/${contract}.json`, 'utf8')); let [priorityFee, maxFee] = await exports.getFee(adminWallet); let factory = new ContractFactory(contractObj.abi, contractObj.bytecode, adminWallet); - return await factory.deploy(...args, { - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }); + let overrides; + if (reservedNonce != null) { + overrides = { + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + nonce: reservedNonce, + } + } else { + overrides = { + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + } + } + return await factory.deploy(...args, overrides); } -export async function deployRootContract(contract: string, adminWallet: ethers.Wallet | LedgerSigner, ...args: any) { +export async function deployRootContract(contract: string, adminWallet: ethers.Wallet | LedgerSigner, reservedNonce: number | null, ...args: any) { let contractObj = JSON.parse(fs.readFileSync(`../../out/${contract}.sol/${contract}.json`, 'utf8')); let factory = new ContractFactory(contractObj.abi, contractObj.bytecode, adminWallet); - return await factory.deploy(...args); + if (reservedNonce == null) { + return await factory.deploy(...args); + } else { + return await factory.deploy(...args, { + nonce: reservedNonce, + }) + } } export function getContract(contract: string, contractAddr: string, provider: providers.JsonRpcProvider) { let contractObj = JSON.parse(fs.readFileSync(`../../out/${contract}.sol/${contract}.json`, 'utf8')); return new ethers.Contract(contractAddr, contractObj.abi, provider); +} + +export function getChildContracts() { + let childContracts; + if (fs.existsSync(".child.bridge.contracts.json")) { + let data = fs.readFileSync(".child.bridge.contracts.json", 'utf-8'); + childContracts = JSON.parse(data); + } else { + childContracts = { + CHILD_PROXY_ADMIN: "", + CHILD_BRIDGE_IMPL_ADDRESS: "", + CHILD_BRIDGE_PROXY_ADDRESS: "", + CHILD_BRIDGE_ADDRESS: "", + CHILD_ADAPTOR_IMPL_ADDRESS: "", + CHILD_ADAPTOR_PROXY_ADDRESS: "", + CHILD_ADAPTOR_ADDRESS: "", + CHILD_TOKEN_TEMPLATE: "", + WRAPPED_IMX_ADDRESS: "", + CHILD_TEST_CUSTOM_TOKEN: "", + }; + } + return childContracts; +} + +export function saveChildContracts(contractData: any) { + fs.writeFileSync(".child.bridge.contracts.json", JSON.stringify(contractData, null, 2)); +} + +export function getRootContracts() { + let rootContracts; + if (fs.existsSync(".root.bridge.contracts.json")) { + let data = fs.readFileSync(".root.bridge.contracts.json", 'utf-8'); + rootContracts = JSON.parse(data); + } else { + rootContracts = { + ROOT_PROXY_ADMIN: "", + ROOT_BRIDGE_IMPL_ADDRESS: "", + ROOT_BRIDGE_PROXY_ADDRESS: "", + ROOT_BRIDGE_ADDRESS: "", + ROOT_ADAPTOR_IMPL_ADDRESS: "", + ROOT_ADAPTOR_PROXY_ADDRESS: "", + ROOT_ADAPTOR_ADDRESS: "", + ROOT_TOKEN_TEMPLATE: "", + ROOT_TEST_CUSTOM_TOKEN: "", + }; + } + return rootContracts; +} + +export function saveRootContracts(contractData: any) { + fs.writeFileSync(".root.bridge.contracts.json", JSON.stringify(contractData, null, 2)); } \ No newline at end of file diff --git a/scripts/localdev/.env.local b/scripts/localdev/.env.local index e4572792..af4b312b 100644 --- a/scripts/localdev/.env.local +++ b/scripts/localdev/.env.local @@ -1,74 +1,44 @@ -# Set prior to 1_deployer_funding.js +# Set prior to 0_pre_validation.js +# Name of the child chain MUST match Axelar's definition. CHILD_CHAIN_NAME="Immutable zkEVM E2E" +# The RPC URL of child chain. CHILD_RPC_URL=http://127.0.0.1:8501 +# The chain ID of the child chain. CHILD_CHAIN_ID=2501 +# Name of the root chain MUST match Axelar's definition. ROOT_CHAIN_NAME="Ethereum E2E" +# The RPC URL of root chain. ROOT_RPC_URL=http://127.0.0.1:8500 +# The chain ID of the root chain. ROOT_CHAIN_ID=2500 -## The admin EOA address on the child chain. -CHILD_ADMIN_ADDR=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 -## The private key for the admin EOA or "ledger" if using hardware wallet. -CHILD_ADMIN_EOA_SECRET=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 -## The deployer address on child chain. -CHILD_DEPLOYER_ADDR=0x70997970C51812dc3A010C7d01b50e0d17dc79C8 -## The private key for the deployer on child chain or "ledger" if using hardware wallet. -CHILD_DEPLOYER_SECRET=59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d -## The amount of fund deployer required on L2, unit is in IMX or 10^18 Wei. -CHILD_DEPLOYER_FUND=500 -## The deployer address on root chain. -ROOT_DEPLOYER_ADDR=0x70997970C51812dc3A010C7d01b50e0d17dc79C8 -## The private key for the deployer on root chain or "ledger" if using hardware wallet. -ROOT_DEPLOYER_SECRET=59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d -## The private key for rate admin or "ledger" if using hardware wallet. -ROOT_BRIDGE_RATE_ADMIN_SECRET=8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba +## The deployer address on child & root chains. +DEPLOYER_ADDR=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 +## The private key for the deployer on child & root chains or "ledger" if using hardware wallet. +DEPLOYER_SECRET=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 +## The ledger index for the deployer on child & root chains, required if using ledger. +DEPLOYER_LEDGER_INDEX= +## The nonce reserved deployer address on child & root chains. +NONCE_RESERVED_DEPLOYER_ADDR=0x70997970C51812dc3A010C7d01b50e0d17dc79C8 +## The nonce reserved deployer, or "ledger" if using hardware wallet. +NONCE_RESERVED_DEPLOYER_SECRET=59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d +## The ledger index for the nonce reserved deployer. +NONCE_RESERVED_DEPLOYER_INDEX= +## The reserved nonce for token template deployment. +NONCE_RESERVED=0 ## The IMX token address on root chain. ROOT_IMX_ADDR=0x73511669fd4dE447feD18BB79bAFeAC93aB7F31f ## The Wrapped ETH token address on the root chain. ROOT_WETH_ADDR=0xB581C9264f59BF0289fA76D61B2D0746dCE3C30D -## The Axelar address for receive initial funding on the child chain. +## The Axelar address to receive initial funding on the child chain. AXELAR_EOA=0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC ## The amount of fund Axelar requested, unit is in IMX or 10^18 Wei. AXELAR_FUND=500 +## The amount of fund deployer to be left with after bootstrapping on L2, unit is in IMX or 10^18 Wei. +CHILD_DEPLOYER_FUND=500 +## The amount of fund nonce reserved deployer required on L2, unit is in IMX or 10^18 Wei. +CHILD_NONCE_RESERVED_DEPLOYER_FUND=10 ## The maximum amount of IMX that can be deposited to L2, unit is in IMX or 10^18 Wei. -IMX_DEPOSIT_LIMIT=200000000 -## The address to perform child bridge upgrade. -CHILD_PROXY_ADMIN=0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 -## The address to be assigned with DEFAULT_ADMIN_ROLE in child bridge. -CHILD_BRIDGE_DEFAULT_ADMIN=0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc -## The address to be assigned with PAUSER_ROLE in child bridge. -CHILD_BRIDGE_PAUSER=0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc -## The address to be assigned with UNPAUSER_ROLE in child bridge. -CHILD_BRIDGE_UNPAUSER=0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc -## The address to be assigned with ADAPTOR_MANAGER_ROLE in child bridge. -CHILD_BRIDGE_ADAPTOR_MANAGER=0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc -## The address to be assigned with DEFAULT_ADMIN_ROLE in child adaptor. -CHILD_ADAPTOR_DEFAULT_ADMIN=0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc -## The address to be assigned with BRIDGE_MANAGER_ROLE in child adaptor. -CHILD_ADAPTOR_BRIDGE_MANAGER=0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc -## The address to be assigned with GAS_SERVICE_MANAGER_ROLE in child adaptor. -CHILD_ADAPTOR_GAS_SERVICE_MANAGER=0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc -## The address to be assigned with TARGET_MANAGER_ROLE in child adaptor. -CHILD_ADAPTOR_TARGET_MANAGER=0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc -## The address to perform root adaptor upgrade. -ROOT_PROXY_ADMIN=0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 -## The address to be assigned with DEFAULT_ADMIN_ROLE in root bridge. -ROOT_BRIDGE_DEFAULT_ADMIN=0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc -## The address to be assigned with PAUSER_ROLE in root bridge. -ROOT_BRIDGE_PAUSER=0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc -## The address to be assigned with UNPAUSER_ROLE in root bridge. -ROOT_BRIDGE_UNPAUSER=0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc -## The address to be assigned with VARIABLE_MANAGER_ROLE in root bridge. -ROOT_BRIDGE_VARIABLE_MANAGER=0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc -## The address to be assigned with ADAPTOR_MANAGER_ROLE in root bridge. -ROOT_BRIDGE_ADAPTOR_MANAGER=0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc -## The address to be assigned with DEFAULT_ADMIN_ROLE in root adaptor. -ROOT_ADAPTOR_DEFAULT_ADMIN=0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc -## The address to be assigned with BRIDGE_MANAGER_ROLE in root adaptor. -ROOT_ADAPTOR_BRIDGE_MANAGER=0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc -## The address to be assigned with GAS_SERVICE_MANAGER_ROLE in root adaptor. -ROOT_ADAPTOR_GAS_SERVICE_MANAGER=0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc -## The address to be assigned with TARGET_MANAGER_ROLE in root adaptor. -ROOT_ADAPTOR_TARGET_MANAGER=0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc +IMX_DEPOSIT_LIMIT=100000000 ## The capacity of the rate limit policy of IMX token, unit is in 10^18. RATE_LIMIT_IMX_CAPACITY=15516 ## The refill rate of the rate limit policy of IMX token, unit is in 10^18. diff --git a/scripts/localdev/README.md b/scripts/localdev/README.md index 00c6d7d9..407fee87 100644 --- a/scripts/localdev/README.md +++ b/scripts/localdev/README.md @@ -31,5 +31,5 @@ The addresses of deployed contracts will be saved in: To run end to end tests against local development network: ``` -npx mocha --require mocha-suppress-logs ../e2e/e2e.ts +LONG_WAIT=0 SHORT_WAIT=0 npx mocha --require mocha-suppress-logs ../e2e/e2e.ts ``` \ No newline at end of file diff --git a/scripts/localdev/childchain.config.ts b/scripts/localdev/childchain.config.ts index b7df4ed5..c91065e3 100644 --- a/scripts/localdev/childchain.config.ts +++ b/scripts/localdev/childchain.config.ts @@ -4,6 +4,7 @@ import "@nomicfoundation/hardhat-toolbox"; const config: HardhatUserConfig = { networks: { hardhat: { + hardfork: "grayGlacier", mining: { auto: false, interval: 200 diff --git a/scripts/localdev/childchain_setup.ts b/scripts/localdev/childchain_setup.ts index 889ffbd5..93a0abd1 100644 --- a/scripts/localdev/childchain_setup.ts +++ b/scripts/localdev/childchain_setup.ts @@ -7,21 +7,18 @@ import { requireEnv } from "../helpers/helpers"; async function main() { let childRPCURL = requireEnv("CHILD_RPC_URL"); let childChainID = requireEnv("CHILD_CHAIN_ID"); - let childEOAKey = requireEnv("CHILD_ADMIN_EOA_SECRET"); + let deployerAddr = requireEnv("DEPLOYER_ADDR"); // Get child provider. let childProvider = new ethers.providers.JsonRpcProvider(childRPCURL, Number(childChainID)); - // Get admin EOA on the child chain. - let childEOA = new ethers.Wallet(childEOAKey); - // Give admin EOA account 2B IMX. await hardhat.provider.send("hardhat_setBalance", [ - childEOA.address, + deployerAddr, "0x6765c793fa10079d0000000", ]); - console.log("Child admin EOA now has " + ethers.utils.formatEther(await childProvider.getBalance(childEOA.address)) + " IMX."); + console.log("Child admin EOA now has " + ethers.utils.formatEther(await childProvider.getBalance(deployerAddr)) + " IMX."); console.log("Finished setting up on child chain.") } main(); \ No newline at end of file diff --git a/scripts/localdev/deploy.sh b/scripts/localdev/deploy.sh index a770cc33..c5e2b24f 100755 --- a/scripts/localdev/deploy.sh +++ b/scripts/localdev/deploy.sh @@ -21,4 +21,7 @@ SKIP_WAIT_FOR_CONFIRMATION=true npx ts-node ../bootstrap/6_imx_burning.ts 2>&1 | SKIP_WAIT_FOR_CONFIRMATION=true npx ts-node ../bootstrap/7_imx_rebalancing.ts 2>&1 | tee -a bootstrap.out # Initialise root contracts -SKIP_WAIT_FOR_CONFIRMATION=true npx ts-node ../bootstrap/8_root_initialisation.ts 2>&1 | tee -a bootstrap.out \ No newline at end of file +SKIP_WAIT_FOR_CONFIRMATION=true npx ts-node ../bootstrap/8_root_initialisation.ts 2>&1 | tee -a bootstrap.out + +# Prepare for test +SKIP_WAIT_FOR_CONFIRMATION=true npx ts-node ../bootstrap/9_test_preparation.ts 2>&1 | tee -a bootstrap.out \ No newline at end of file diff --git a/scripts/localdev/rootchain.config.ts b/scripts/localdev/rootchain.config.ts index db622ae6..6ec638b6 100644 --- a/scripts/localdev/rootchain.config.ts +++ b/scripts/localdev/rootchain.config.ts @@ -4,6 +4,7 @@ import "@nomicfoundation/hardhat-toolbox"; const config: HardhatUserConfig = { networks: { hardhat: { + hardfork: "shanghai", mining: { auto: false, interval: 1200 diff --git a/scripts/localdev/rootchain_setup.ts b/scripts/localdev/rootchain_setup.ts index 0bd6f26d..abaaa503 100644 --- a/scripts/localdev/rootchain_setup.ts +++ b/scripts/localdev/rootchain_setup.ts @@ -2,32 +2,23 @@ import * as dotenv from "dotenv"; dotenv.config(); import { ethers as hardhat } from "hardhat"; import { ethers } from "ethers"; -import { requireEnv, deployRootContract, waitForReceipt } from "../helpers/helpers"; +import { requireEnv, deployRootContract, waitForReceipt, saveRootContracts } from "../helpers/helpers"; import * as fs from "fs"; async function main() { let rootRPCURL = requireEnv("ROOT_RPC_URL"); let rootChainID = requireEnv("ROOT_CHAIN_ID"); let rootAdminKey = requireEnv("ROOT_EOA_SECRET"); - let rootDeployerKey = requireEnv("ROOT_DEPLOYER_SECRET"); - let axelarDeployerKey = requireEnv("AXELAR_ROOT_EOA_SECRET"); + let deployerAddr = requireEnv("DEPLOYER_ADDR"); + let reservedAddr = requireEnv("NONCE_RESERVED_DEPLOYER_ADDR"); + let axelarEOA = requireEnv("AXELAR_EOA"); let rootTestKey = requireEnv("TEST_ACCOUNT_SECRET"); - let rootRateAdminKey = requireEnv("ROOT_BRIDGE_RATE_ADMIN_SECRET"); // Get root provider. let rootProvider = new ethers.providers.JsonRpcProvider(rootRPCURL, Number(rootChainID)); - // Get deployer wallet on the root chain. - let rootDeployer = new ethers.Wallet(rootDeployerKey, rootProvider); - - // Get axelar wallet on the root chain. - let axelarDeployer = new ethers.Wallet(axelarDeployerKey, rootProvider); - // Get test wwallet on the root chain. let testWallet = new ethers.Wallet(rootTestKey, rootProvider); - - // Get rate admin wallet on the root chain. - let rateAdminWallet = new ethers.Wallet(rootRateAdminKey, rootProvider); // Get root admin eoa wallet. let admin = new ethers.Wallet(rootAdminKey, rootProvider); @@ -40,18 +31,18 @@ async function main() { // Deploy IMX contract console.log("Deploy IMX contract on root chain..."); - let IMX = await deployRootContract("ERC20PresetMinterPauser", admin, "IMX Token", "IMX"); + let IMX = await deployRootContract("ERC20PresetMinterPauser", admin, null, "IMX Token", "IMX"); await waitForReceipt(IMX.deployTransaction.hash, rootProvider); console.log("IMX deployed at: " + IMX.address); // Deploy WETH contract console.log("Deploy WETH contract on root chain..."); - let WETH = await deployRootContract("WETH", admin); + let WETH = await deployRootContract("WETH", admin, null); await waitForReceipt(WETH.deployTransaction.hash, rootProvider); console.log("WETH deployed at: " + WETH.address); - // Mint 1100 IMX to root deployer - let resp = await IMX.connect(admin).mint(rootDeployer.address, ethers.utils.parseEther("1100.0")); + // Mint 1110 IMX to root deployer + let resp = await IMX.connect(admin).mint(deployerAddr, ethers.utils.parseEther("1110.0")); await waitForReceipt(resp.hash, rootProvider); // Transfer 1000 IMX to test wallet @@ -60,14 +51,21 @@ async function main() { // Transfer 0.1 ETH to root deployer resp = await admin.sendTransaction({ - to: rootDeployer.address, + to: deployerAddr, + value: ethers.utils.parseEther("0.1"), + }) + await waitForReceipt(resp.hash, rootProvider); + + // Transfer 0.1 ETH to nonce reserved root deployer + resp = await admin.sendTransaction({ + to: reservedAddr, value: ethers.utils.parseEther("0.1"), }) await waitForReceipt(resp.hash, rootProvider); // Transfer 500 ETH to axelar deployer resp = await admin.sendTransaction({ - to: axelarDeployer.address, + to: axelarEOA, value: ethers.utils.parseEther("500.0"), }) @@ -78,16 +76,9 @@ async function main() { }) await waitForReceipt(resp.hash, rootProvider); - // Transfer 0.1 ETH to rate admin - resp = await admin.sendTransaction({ - to: rateAdminWallet.address, - value: ethers.utils.parseEther("10.0"), - }) - await waitForReceipt(resp.hash, rootProvider); - - console.log("Root deployer now has " + ethers.utils.formatEther(await IMX.balanceOf(rootDeployer.address)) + " IMX."); - console.log("Root deployer now has " + ethers.utils.formatEther(await rootProvider.getBalance(rootDeployer.address)) + " ETH."); - console.log("Root axelar now has " + ethers.utils.formatEther(await rootProvider.getBalance(axelarDeployer.address)) + " ETH."); + console.log("Root deployer now has " + ethers.utils.formatEther(await IMX.balanceOf(deployerAddr)) + " IMX."); + console.log("Root deployer now has " + ethers.utils.formatEther(await rootProvider.getBalance(deployerAddr)) + " ETH."); + console.log("Root axelar now has " + ethers.utils.formatEther(await rootProvider.getBalance(axelarEOA)) + " ETH."); console.log("Finished setting up on root chain."); let contractData = { diff --git a/scripts/localdev/start.sh b/scripts/localdev/start.sh index 74205d77..bad47111 100755 --- a/scripts/localdev/start.sh +++ b/scripts/localdev/start.sh @@ -26,8 +26,11 @@ npx hardhat run ./childchain_setup.ts --config ./childchain.config.ts --network echo "Successfully setup root chain and child chain..." if [ -z ${LOCAL_CHAIN_ONLY+x} ]; then + # Pre validation + SKIP_WAIT_FOR_CONFIRMATION=true npx ts-node ../bootstrap/0_pre_validation.ts 2>&1 | tee bootstrap.out + # Fund accounts - SKIP_WAIT_FOR_CONFIRMATION=true npx ts-node ../bootstrap/1_deployer_funding.ts 2>&1 | tee bootstrap.out + SKIP_WAIT_FOR_CONFIRMATION=true npx ts-node ../bootstrap/1_deployer_funding.ts 2>&1 | tee -a bootstrap.out echo "Successfully run 1_deployer_funding.ts..." # Setup axelar From d90f3b43bdd8fc61650e208d5d15da6136fa2710 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Mon, 4 Dec 2023 10:45:35 +1000 Subject: [PATCH 004/155] Fix ledge issue with multiple accounts --- scripts/bootstrap/0_pre_validation.ts | 8 ++++++-- scripts/deploy/child_deployment.ts | 28 +++++++++++++++------------ scripts/deploy/root_deployment.ts | 28 +++++++++++++++------------ scripts/helpers/ledger_signer.ts | 4 ++++ 4 files changed, 42 insertions(+), 26 deletions(-) diff --git a/scripts/bootstrap/0_pre_validation.ts b/scripts/bootstrap/0_pre_validation.ts index 874a5515..746d6f64 100644 --- a/scripts/bootstrap/0_pre_validation.ts +++ b/scripts/bootstrap/0_pre_validation.ts @@ -74,6 +74,11 @@ async function run() { } else { deployerWallet = new ethers.Wallet(deployerSecret, childProvider); } + let actualDeployerAddress = await deployerWallet.getAddress(); + if (deployerWallet instanceof LedgerSigner) { + deployerWallet.close(); + } + let reservedWallet; if (reservedDeployerSecret == "ledger") { let index = requireEnv("NONCE_RESERVED_DEPLOYER_INDEX"); @@ -82,13 +87,12 @@ async function run() { } else { reservedWallet = new ethers.Wallet(reservedDeployerSecret, childProvider); } + let actualReservedDeployerAddress = await reservedWallet.getAddress(); // Check deployer address matches deployer addr - let actualDeployerAddress = await deployerWallet.getAddress(); if (actualDeployerAddress != deployerAddr) { tryThrow("Deployer addresses mismatch, expect " + deployerAddr + " actual " + actualDeployerAddress); } - let actualReservedDeployerAddress = await reservedWallet.getAddress(); if (actualReservedDeployerAddress != reservedDeployerAddr) { tryThrow("Reserved Nonce deployer addresses mismatch, expect " + reservedDeployerAddr + " actual " + actualReservedDeployerAddress); } diff --git a/scripts/deploy/child_deployment.ts b/scripts/deploy/child_deployment.ts index 71de36b2..41aa8477 100644 --- a/scripts/deploy/child_deployment.ts +++ b/scripts/deploy/child_deployment.ts @@ -17,19 +17,7 @@ export async function deployChildContracts() { // Read from contract file. let childContracts = getChildContracts(); - // Get deployer address const childProvider = new ethers.providers.JsonRpcProvider(childRPCURL, Number(childChainID)); - let childDeployerWallet; - if (deployerSecret == "ledger") { - let index = requireEnv("DEPLOYER_LEDGER_INDEX"); - const derivationPath = `m/44'/60'/${parseInt(index)}'/0/0`; - childDeployerWallet = new LedgerSigner(childProvider, derivationPath); - } else { - childDeployerWallet = new ethers.Wallet(deployerSecret, childProvider); - } - let deployerAddr = await childDeployerWallet.getAddress(); - console.log("Deployer address is: ", deployerAddr); - // Get reserved wallet let reservedDeployerWallet; if (nonceReservedDeployerSecret == "ledger") { @@ -82,6 +70,22 @@ export async function deployChildContracts() { } console.log("Initialised CHILD_TOKEN_TEMPLATE at: ", childTokenTemplate.address); + if (reservedDeployerWallet instanceof LedgerSigner) { + reservedDeployerWallet.close(); + } + + // Get deployer address + let childDeployerWallet; + if (deployerSecret == "ledger") { + let index = requireEnv("DEPLOYER_LEDGER_INDEX"); + const derivationPath = `m/44'/60'/${parseInt(index)}'/0/0`; + childDeployerWallet = new LedgerSigner(childProvider, derivationPath); + } else { + childDeployerWallet = new ethers.Wallet(deployerSecret, childProvider); + } + let deployerAddr = await childDeployerWallet.getAddress(); + console.log("Deployer address is: ", deployerAddr); + // Deploy wrapped IMX let wrappedIMX; if (childContracts.WRAPPED_IMX_ADDRESS != "") { diff --git a/scripts/deploy/root_deployment.ts b/scripts/deploy/root_deployment.ts index 68cac805..2b22136e 100644 --- a/scripts/deploy/root_deployment.ts +++ b/scripts/deploy/root_deployment.ts @@ -17,19 +17,7 @@ export async function deployRootContracts() { // Read from contract file. let rootContracts = getRootContracts(); - // Get deployer address const rootProvider = new ethers.providers.JsonRpcProvider(rootRPCURL, Number(rootChainID)); - let rootDeployerWallet; - if (deployerSecret == "ledger") { - let index = requireEnv("DEPLOYER_LEDGER_INDEX"); - const derivationPath = `m/44'/60'/${parseInt(index)}'/0/0`; - rootDeployerWallet = new LedgerSigner(rootProvider, derivationPath); - } else { - rootDeployerWallet = new ethers.Wallet(deployerSecret, rootProvider); - } - let deployerAddr = await rootDeployerWallet.getAddress(); - console.log("Deployer address is: ", deployerAddr); - // Get reserved wallet let reservedDeployerWallet; if (nonceReservedDeployerSecret == "ledger") { @@ -78,6 +66,22 @@ export async function deployRootContracts() { } console.log("Deployed to ROOT_TOKEN_TEMPLATE: ", rootTokenTemplate.address); + if (reservedDeployerWallet instanceof LedgerSigner) { + reservedDeployerWallet.close(); + } + + // Get deployer address + let rootDeployerWallet; + if (deployerSecret == "ledger") { + let index = requireEnv("DEPLOYER_LEDGER_INDEX"); + const derivationPath = `m/44'/60'/${parseInt(index)}'/0/0`; + rootDeployerWallet = new LedgerSigner(rootProvider, derivationPath); + } else { + rootDeployerWallet = new ethers.Wallet(deployerSecret, rootProvider); + } + let deployerAddr = await rootDeployerWallet.getAddress(); + console.log("Deployer address is: ", deployerAddr); + // Deploy proxy admin let proxyAdmin; if (rootContracts.ROOT_PROXY_ADMIN != "") { diff --git a/scripts/helpers/ledger_signer.ts b/scripts/helpers/ledger_signer.ts index cb81315f..58b3e1c3 100644 --- a/scripts/helpers/ledger_signer.ts +++ b/scripts/helpers/ledger_signer.ts @@ -142,4 +142,8 @@ export class LedgerSigner extends ethers.Signer { connect(provider: ethers.providers.Provider): ethers.Signer { return new LedgerSigner(provider, this.path); } + + public async close() { + (await this._eth)?.transport.close(); + } } \ No newline at end of file From acdade4519d9bc71db77a049b483def677c77b18 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Tue, 5 Dec 2023 17:44:14 +1000 Subject: [PATCH 005/155] Fix role control --- scripts/bootstrap/.env.example | 4 +++ scripts/bootstrap/9_test_preparation.ts | 22 +++++++++++++-- scripts/bootstrap/README.md | 4 +++ scripts/deploy/.env.example | 4 +++ scripts/deploy/README.md | 4 +++ scripts/deploy/root_initialisation.ts | 37 +++++++++++++++++++------ scripts/localdev/.env.local | 4 +++ 7 files changed, 68 insertions(+), 11 deletions(-) diff --git a/scripts/bootstrap/.env.example b/scripts/bootstrap/.env.example index c64b22bb..00e96827 100644 --- a/scripts/bootstrap/.env.example +++ b/scripts/bootstrap/.env.example @@ -39,6 +39,10 @@ CHILD_DEPLOYER_FUND= CHILD_NONCE_RESERVED_DEPLOYER_FUND= ## The maximum amount of IMX that can be deposited to L2, unit is in IMX or 10^18 Wei. IMX_DEPOSIT_LIMIT= +## The privileged transaction Multisig address on the root chain. +PRIVILEGED_ROOT_MULTISIG_ADDR= +# The pauser address on the root chain. +ROOT_PAUSER_ADDR= ## The capacity of the rate limit policy of IMX token, unit is in 10^18. RATE_LIMIT_IMX_CAPACITY= ## The refill rate of the rate limit policy of IMX token, unit is in 10^18. diff --git a/scripts/bootstrap/9_test_preparation.ts b/scripts/bootstrap/9_test_preparation.ts index 49bac642..d0c3a72a 100644 --- a/scripts/bootstrap/9_test_preparation.ts +++ b/scripts/bootstrap/9_test_preparation.ts @@ -1,7 +1,7 @@ // Prepare for test import * as dotenv from "dotenv"; dotenv.config(); -import { ethers } from "ethers"; +import { ethers, utils } from "ethers"; import { deployRootContract, getContract, getRootContracts, requireEnv, saveRootContracts, waitForConfirmation, waitForReceipt } from "../helpers/helpers"; import { LedgerSigner } from "../helpers/ledger_signer"; @@ -13,6 +13,7 @@ async function run() { let rootChainID = requireEnv("ROOT_CHAIN_ID"); let deployerSecret = requireEnv("DEPLOYER_SECRET"); let testAccountKey = requireEnv("TEST_ACCOUNT_SECRET"); + let rootMultisigAddr = requireEnv("PRIVILEGED_ROOT_MULTISIG_ADDR"); // Get deployer address const rootProvider = new ethers.providers.JsonRpcProvider(rootRPCURL, Number(rootChainID)); @@ -45,7 +46,7 @@ async function run() { console.log("Deploy root test custom token..."); rootCustomToken = await deployRootContract("ERC20PresetMinterPauser", rootDeployerWallet, null, "Custom Token", "CTK"); await waitForReceipt(rootCustomToken.deployTransaction.hash, rootProvider); - console.log("Custom token deployed to: ", rootCustomToken) + console.log("Custom token deployed to: ", rootCustomToken.address); } rootContracts.ROOT_TEST_CUSTOM_TOKEN=rootCustomToken.address; saveRootContracts(rootContracts); @@ -66,6 +67,23 @@ async function run() { ); await waitForReceipt(resp.hash, rootProvider); + // Revoke roles + console.log("Revoke RATE_CONTROL_ROLE of deployer...") + resp = await rootBridge.connect(rootDeployerWallet).revokeRole(utils.keccak256(utils.toUtf8Bytes("RATE")), deployerAddr); + console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); + await waitForReceipt(resp.hash, rootProvider); + + console.log("Revoke DEFAULT_ADMIN of deployer...") + resp = await rootBridge.connect(rootDeployerWallet).revokeRole(await rootBridge.DEFAULT_ADMIN_ROLE(), deployerAddr); + console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); + await waitForReceipt(resp.hash, rootProvider); + + // Print summary + console.log("Does multisig have DEFAULT_ADMIN: ", await rootBridge.hasRole(await rootBridge.DEFAULT_ADMIN_ROLE(), rootMultisigAddr)); + console.log("Does deployer have DEFAULT_ADMIN: ", await rootBridge.hasRole(await rootBridge.DEFAULT_ADMIN_ROLE(), deployerAddr)); + console.log("Does multisig have RATE_ADMIN: ", await rootBridge.hasRole(utils.keccak256(utils.toUtf8Bytes("RATE")), rootMultisigAddr)); + console.log("Does deployer have RATE_ADMIN: ", await rootBridge.hasRole(utils.keccak256(utils.toUtf8Bytes("RATE")), deployerAddr)); + console.log("=======End Test Preparation======="); } run(); \ No newline at end of file diff --git a/scripts/bootstrap/README.md b/scripts/bootstrap/README.md index 25d7242c..0c106618 100644 --- a/scripts/bootstrap/README.md +++ b/scripts/bootstrap/README.md @@ -63,6 +63,10 @@ CHILD_DEPLOYER_FUND= CHILD_NONCE_RESERVED_DEPLOYER_FUND= ## The maximum amount of IMX that can be deposited to L2, unit is in IMX or 10^18 Wei. IMX_DEPOSIT_LIMIT= +## The privileged transaction Multisig address on the root chain. +PRIVILEGED_ROOT_MULTISIG_ADDR= +# The pauser address on the root chain. +ROOT_PAUSER_ADDR= ## The capacity of the rate limit policy of IMX token, unit is in 10^18. RATE_LIMIT_IMX_CAPACITY= ## The refill rate of the rate limit policy of IMX token, unit is in 10^18. diff --git a/scripts/deploy/.env.example b/scripts/deploy/.env.example index d0b6c81f..2fa71b06 100644 --- a/scripts/deploy/.env.example +++ b/scripts/deploy/.env.example @@ -38,6 +38,10 @@ CHILD_DEPLOYER_FUND= CHILD_NONCE_RESERVED_DEPLOYER_FUND= ## The maximum amount of IMX that can be deposited to L2, unit is in IMX or 10^18 Wei. IMX_DEPOSIT_LIMIT= +## The privileged transaction Multisig address on the root chain. +PRIVILEGED_ROOT_MULTISIG_ADDR= +# The pauser address on the root chain. +ROOT_PAUSER_ADDR= ## The capacity of the rate limit policy of IMX token, unit is in 10^18. RATE_LIMIT_IMX_CAPACITY= ## The refill rate of the rate limit policy of IMX token, unit is in 10^18. diff --git a/scripts/deploy/README.md b/scripts/deploy/README.md index b9b6f4af..3c38f3f6 100644 --- a/scripts/deploy/README.md +++ b/scripts/deploy/README.md @@ -52,6 +52,10 @@ CHILD_DEPLOYER_FUND= CHILD_NONCE_RESERVED_DEPLOYER_FUND= ## The maximum amount of IMX that can be deposited to L2, unit is in IMX or 10^18 Wei. IMX_DEPOSIT_LIMIT= +## The privileged transaction Multisig address on the root chain. +PRIVILEGED_ROOT_MULTISIG_ADDR= +# The pauser address on the root chain. +ROOT_PAUSER_ADDR= ## The capacity of the rate limit policy of IMX token, unit is in 10^18. RATE_LIMIT_IMX_CAPACITY= ## The refill rate of the rate limit policy of IMX token, unit is in 10^18. diff --git a/scripts/deploy/root_initialisation.ts b/scripts/deploy/root_initialisation.ts index a11448b4..4051d25e 100644 --- a/scripts/deploy/root_initialisation.ts +++ b/scripts/deploy/root_initialisation.ts @@ -1,7 +1,7 @@ // Initialise root contracts import * as dotenv from "dotenv"; dotenv.config(); -import { ethers } from "ethers"; +import { ethers, utils } from "ethers"; import { requireEnv, waitForConfirmation, waitForReceipt, getContract, getChildContracts, getRootContracts } from "../helpers/helpers"; import { LedgerSigner } from "../helpers/ledger_signer"; @@ -15,6 +15,8 @@ export async function initialiseRootContracts() { let rootIMXAddr = requireEnv("ROOT_IMX_ADDR"); let rootWETHAddr = requireEnv("ROOT_WETH_ADDR"); let imxDepositLimit = requireEnv("IMX_DEPOSIT_LIMIT"); + let rootMultisigAddr = requireEnv("PRIVILEGED_ROOT_MULTISIG_ADDR"); + let rootPauser = requireEnv("ROOT_PAUSER_ADDR"); let rateLimitIMXCap = requireEnv("RATE_LIMIT_IMX_CAPACITY"); let rateLimitIMXRefill = requireEnv("RATE_LIMIT_IMX_REFILL_RATE"); let rateLimitIMXLargeThreshold = requireEnv("RATE_LIMIT_IMX_LARGE_THRESHOLD"); @@ -70,10 +72,10 @@ export async function initialiseRootContracts() { let resp = await rootBridge.connect(rootDeployerWallet)["initialize((address,address,address,address,address),address,address,address,address,address,uint256,address)"]( { defaultAdmin: deployerAddr, - pauser: deployerAddr, - unpauser: deployerAddr, - variableManager: deployerAddr, - adaptorManager: deployerAddr, + pauser: rootPauser, + unpauser: rootPauser, + variableManager: rootMultisigAddr, + adaptorManager: rootMultisigAddr, }, rootAdaptorAddr, childBridgeAddr, @@ -152,15 +154,32 @@ export async function initialiseRootContracts() { console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); await waitForReceipt(resp.hash, rootProvider); + // Grant roles + console.log("Grant RATE_CONTROL_ROLE to multisig...") + resp = await rootBridge.connect(rootDeployerWallet).grantRole(utils.keccak256(utils.toUtf8Bytes("RATE")), rootMultisigAddr); + console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); + await waitForReceipt(resp.hash, rootProvider); + + console.log("Grant DEFAULT_ADMIN to multisig...") + resp = await rootBridge.connect(rootDeployerWallet).grantRole(await rootBridge.DEFAULT_ADMIN_ROLE(), rootMultisigAddr); + console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); + await waitForReceipt(resp.hash, rootProvider); + + // Print summary + console.log("Does multisig have DEFAULT_ADMIN: ", await rootBridge.hasRole(await rootBridge.DEFAULT_ADMIN_ROLE(), rootMultisigAddr)); + console.log("Does deployer have DEFAULT_ADMIN: ", await rootBridge.hasRole(await rootBridge.DEFAULT_ADMIN_ROLE(), deployerAddr)); + console.log("Does multisig have RATE_ADMIN: ", await rootBridge.hasRole(utils.keccak256(utils.toUtf8Bytes("RATE")), rootMultisigAddr)); + console.log("Does deployer have RATE_ADMIN: ", await rootBridge.hasRole(utils.keccak256(utils.toUtf8Bytes("RATE")), deployerAddr)); + // Initialise root adaptor console.log("Initialise root adaptor..."); let rootAdaptor = getContract("RootAxelarBridgeAdaptor", rootAdaptorAddr, rootProvider); resp = await rootAdaptor.connect(rootDeployerWallet).initialize( { - defaultAdmin: deployerAddr, - bridgeManager: deployerAddr, - gasServiceManager: deployerAddr, - targetManager: deployerAddr, + defaultAdmin: rootMultisigAddr, + bridgeManager: rootMultisigAddr, + gasServiceManager: rootMultisigAddr, + targetManager: rootMultisigAddr, }, rootBridgeAddr, childChainName, diff --git a/scripts/localdev/.env.local b/scripts/localdev/.env.local index af4b312b..fd3713fe 100644 --- a/scripts/localdev/.env.local +++ b/scripts/localdev/.env.local @@ -39,6 +39,10 @@ CHILD_DEPLOYER_FUND=500 CHILD_NONCE_RESERVED_DEPLOYER_FUND=10 ## The maximum amount of IMX that can be deposited to L2, unit is in IMX or 10^18 Wei. IMX_DEPOSIT_LIMIT=100000000 +## The privileged transaction multisig address on the root chain. +PRIVILEGED_ROOT_MULTISIG_ADDR=0x14dC79964da2C08b23698B3D3cc7Ca32193d9955 +# The pauser address on the root chain. +ROOT_PAUSER_ADDR=0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f ## The capacity of the rate limit policy of IMX token, unit is in 10^18. RATE_LIMIT_IMX_CAPACITY=15516 ## The refill rate of the rate limit policy of IMX token, unit is in 10^18. From 5b7ced75084c5411948cf23bae5713c88a752caa Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Wed, 6 Dec 2023 08:24:02 +1100 Subject: [PATCH 006/155] Add create2 contract deployer with access control --- src/deploy/OwnableCreate2Deployer.sol | 46 +++++ test/unit/deploy/OwnableCreate2Deployer.t.sol | 172 ++++++++++++++++++ 2 files changed, 218 insertions(+) create mode 100644 src/deploy/OwnableCreate2Deployer.sol create mode 100644 test/unit/deploy/OwnableCreate2Deployer.t.sol diff --git a/src/deploy/OwnableCreate2Deployer.sol b/src/deploy/OwnableCreate2Deployer.sol new file mode 100644 index 00000000..f938a688 --- /dev/null +++ b/src/deploy/OwnableCreate2Deployer.sol @@ -0,0 +1,46 @@ +// Copyright Immutable Pty Ltd 2018 - 2023 +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import {Deployer} from "@axelar-gmp-sdk-solidity/contracts/deploy/Deployer.sol"; +import {Create2} from "@axelar-gmp-sdk-solidity/contracts/deploy/Create2.sol"; + +/** + * @title OwnableCreate2Deployer + * @notice Deploys and optionally initializes contracts using the `CREATE2` opcode. + * @dev This contract extends the {Deployer} contract from the Axelar SDK, by adding basic access control to the deployment functions. + * The contract has an owner, which is the only entity that can deploy new contracts. + * The owner is initially set to the deployer of this contract and can be changed using {transferOwnership}. + * + * @dev The contract deploys a contract with the same bytecode, salt, and sender(owner) to the same address. + * Attempting to deploy a contract with the same bytecode, salt, and sender(owner) will revert. + * The address where the contract will be deployed can be found using {deployedAddress}. + */ +contract OwnableCreate2Deployer is Ownable, Create2, Deployer { + constructor() Ownable() {} + + /** + * @dev Deploys a contract using the `CREATE2` opcode. + * This function is called by {deploy} and {deployAndInit} external functions in the {Deployer} contract. + * This function can only be called by the owner of this contract, hence {deploy} and {deployAndInit} can only be called by the owner. + * The address where the contract will be deployed can be found using {deployedAddress}. + * @param bytecode The bytecode of the contract to be deployed + * @param deploySalt A salt which is a hash of the salt provided by the sender and the sender's address. + * @return The address of the deployed contract + */ + function _deploy(bytes memory bytecode, bytes32 deploySalt) internal override onlyOwner returns (address) { + return _create2(bytecode, deploySalt); + } + + /** + * @dev Returns the address where a contract will be stored if deployed via {deploy} or {deployAndInit}. + * This function is called by the {deployedAddress} external functions in the {Deployer} contract. + * @param bytecode The bytecode of the contract to be deployed + * @param deploySalt A salt which is a hash of the salt provided by the sender and the sender's address. + * @return The predicted deployment address of the contract + */ + function _deployedAddress(bytes memory bytecode, bytes32 deploySalt) internal view override returns (address) { + return _create2Address(bytecode, deploySalt); + } +} diff --git a/test/unit/deploy/OwnableCreate2Deployer.t.sol b/test/unit/deploy/OwnableCreate2Deployer.t.sol new file mode 100644 index 00000000..37dc1834 --- /dev/null +++ b/test/unit/deploy/OwnableCreate2Deployer.t.sol @@ -0,0 +1,172 @@ +// Copyright Immutable Pty Ltd 2018 - 2023 +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import "forge-std/Test.sol"; +import {IDeploy} from "@axelar-gmp-sdk-solidity/contracts/interfaces/IDeploy.sol"; +import {IDeployer} from "@axelar-gmp-sdk-solidity/contracts/interfaces/IDeployer.sol"; + +import {ChildERC20} from "../../../src/child/ChildERC20.sol"; +import {OwnableCreate2Deployer} from "../../../src/deploy/OwnableCreate2Deployer.sol"; + +contract OwnableCreate2DeployerTest is Test { + OwnableCreate2Deployer private deployer; + ChildERC20 private childERC20; + + bytes private childERC20Bytecode; + bytes32 private salt; + + event Deployed(address indexed deployedAddress, address indexed sender, bytes32 indexed salt, bytes32 bytecodeHash); + + function setUp() public { + // create a new deployer that is owned by this contract + deployer = new OwnableCreate2Deployer(); + + childERC20 = new ChildERC20(); + childERC20Bytecode = type(ChildERC20).creationCode; + + salt = createSaltFromKey("test-salt"); + } + + function test_RevertIf_DeployWithEmptyByteCode() public { + vm.expectRevert(IDeploy.EmptyBytecode.selector); + deployer.deploy("", salt); + } + + function test_RevertIf_DeployWithNonOwner() public { + address nonOwner = address(0x1); + vm.startPrank(nonOwner); + vm.expectRevert("Ownable: caller is not the owner"); + deployer.deploy(childERC20Bytecode, salt); + } + + /// @dev deploying with the same bytecode, salt and sender should revert + function test_RevertIf_DeployAlreadyDeployedCreate2Contract() public { + deployer.deploy(childERC20Bytecode, salt); + + vm.expectRevert(IDeploy.AlreadyDeployed.selector); + deployer.deploy(childERC20Bytecode, salt); + } + + function test_deploy_DeploysContract() public { + address expectedAddress = predictCreate2Address(childERC20Bytecode, address(deployer), address(this), salt); + + vm.expectEmit(); + emit Deployed(expectedAddress, address(this), salt, keccak256(childERC20Bytecode)); + address deployed = deployer.deploy(childERC20Bytecode, salt); + + assertEq(deployed.code, address(childERC20).code, "deployed contract code does not match expected"); + + ChildERC20 deployedChildERC20 = ChildERC20(deployed); + assertEq(deployedChildERC20.name(), "", "deployed contract should have empty name"); + assertEq(deployedChildERC20.symbol(), "", "deployed contract should have empty symbol"); + assertEq(deployedChildERC20.decimals(), 0, "deployed contract should have 0 decimals"); + } + + function test_deploy_DeploysToPredictedAddress() public { + address deployedAddress = deployer.deploy(childERC20Bytecode, salt); + address expectedAddress = predictCreate2Address(childERC20Bytecode, address(deployer), address(this), salt); + assertEq(deployedAddress, expectedAddress, "deployed address does not match expected address"); + } + + function test_deploy_DeploysSameContractToDifferentAddresses_GivenDifferentSalts() public { + address deployed1 = deployer.deploy(childERC20Bytecode, salt); + + bytes32 newSalt = createSaltFromKey("new-salt"); + address deployed2 = deployer.deploy(childERC20Bytecode, newSalt); + + assertEq(deployed1.code, deployed2.code, "bytecode of deployed contracts do not match"); + assertNotEq(deployed1, deployed2, "deployed contracts should not have the same address"); + } + + function test_deploy_DeploysContractGivenNewOwner() public { + address newOwner = address(0x1); + + deployer.transferOwnership(newOwner); + assertEq(deployer.owner(), newOwner, "owner did not change as expected"); + + // check that the old owner cannot deploy + vm.expectRevert("Ownable: caller is not the owner"); + deployer.deploy(childERC20Bytecode, salt); + + // test that the new owner can deploy + vm.startPrank(newOwner); + address expectedAddress = predictCreate2Address(childERC20Bytecode, address(deployer), address(newOwner), salt); + + vm.expectEmit(); + emit Deployed(expectedAddress, address(newOwner), salt, keccak256(childERC20Bytecode)); + address deployed = deployer.deploy(childERC20Bytecode, salt); + + assertEq(deployed.code, address(childERC20).code, "deployed contract should match expected"); + } + + /** + * deployAndInit + */ + + function test_RevertIf_DeployAndInitWithNonOwner() public { + address nonOwner = address(0x1); + vm.startPrank(nonOwner); + vm.expectRevert("Ownable: caller is not the owner"); + deployer.deployAndInit(childERC20Bytecode, salt, ""); + } + + function test_deployAndInit_DeploysAndInitsContract() public { + address expectedAddress = predictCreate2Address(childERC20Bytecode, address(deployer), address(this), salt); + address rootToken = address(0x1); + string memory name = "Test-Token"; + string memory symbol = "TST"; + uint8 decimals = 18; + bytes memory initPayload = + abi.encodeWithSelector(ChildERC20.initialize.selector, rootToken, name, symbol, decimals); + + vm.expectEmit(); + emit Deployed(expectedAddress, address(this), salt, keccak256(childERC20Bytecode)); + address deployed = deployer.deployAndInit(childERC20Bytecode, salt, initPayload); + + // regardless of init data, the deployed address should match expected deployment + assertEq(deployed, expectedAddress, "deployed address should match expected address"); + + assertEq(deployed.code, address(childERC20).code, "deployed contract should match expected"); + + // verify initialisation + ChildERC20 deployedChildERC20 = ChildERC20(deployed); + assertEq(deployedChildERC20.rootToken(), rootToken, "rootToken does not match expected"); + assertEq(deployedChildERC20.name(), name, "name does not match expected"); + assertEq(deployedChildERC20.symbol(), symbol, "symbol does not match expected"); + assertEq(deployedChildERC20.decimals(), decimals, "decimals does not match expected"); + } + + /** + * deployedAddress + */ + + function test_deployedAddress_ReturnsPredictedAddress() public { + address deployAddress = deployer.deployedAddress(childERC20Bytecode, address(this), salt); + + address predictedAddress = predictCreate2Address(childERC20Bytecode, address(deployer), address(this), salt); + address deployedAddress = deployer.deploy(childERC20Bytecode, salt); + + assertEq(deployAddress, predictedAddress, "deployment address did not match predicted address"); + assertEq(deployAddress, deployedAddress, "deployment address did not match deployed address"); + } + + /** + * private helper functions + */ + + function predictCreate2Address(bytes memory _bytecode, address _deployer, address _sender, bytes32 _salt) + private + pure + returns (address) + { + bytes32 deploySalt = keccak256(abi.encode(_sender, _salt)); + return address( + uint160(uint256(keccak256(abi.encodePacked(hex"ff", address(_deployer), deploySalt, keccak256(_bytecode))))) + ); + } + + function createSaltFromKey(string memory key) private view returns (bytes32) { + return keccak256(abi.encode(address(this), key)); + } +} From 81747bd5049454d4633655f7c83ffd480b637073 Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Wed, 6 Dec 2023 10:51:16 +1100 Subject: [PATCH 007/155] Accept intended owner in constructor --- src/deploy/OwnableCreate2Deployer.sol | 5 ++-- test/unit/deploy/OwnableCreate2Deployer.t.sol | 26 ++++++++++++------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/deploy/OwnableCreate2Deployer.sol b/src/deploy/OwnableCreate2Deployer.sol index f938a688..8aa5de05 100644 --- a/src/deploy/OwnableCreate2Deployer.sol +++ b/src/deploy/OwnableCreate2Deployer.sol @@ -11,14 +11,15 @@ import {Create2} from "@axelar-gmp-sdk-solidity/contracts/deploy/Create2.sol"; * @notice Deploys and optionally initializes contracts using the `CREATE2` opcode. * @dev This contract extends the {Deployer} contract from the Axelar SDK, by adding basic access control to the deployment functions. * The contract has an owner, which is the only entity that can deploy new contracts. - * The owner is initially set to the deployer of this contract and can be changed using {transferOwnership}. * * @dev The contract deploys a contract with the same bytecode, salt, and sender(owner) to the same address. * Attempting to deploy a contract with the same bytecode, salt, and sender(owner) will revert. * The address where the contract will be deployed can be found using {deployedAddress}. */ contract OwnableCreate2Deployer is Ownable, Create2, Deployer { - constructor() Ownable() {} + constructor(address owner) Ownable() { + transferOwnership(owner); + } /** * @dev Deploys a contract using the `CREATE2` opcode. diff --git a/test/unit/deploy/OwnableCreate2Deployer.t.sol b/test/unit/deploy/OwnableCreate2Deployer.t.sol index 37dc1834..a0bf0bce 100644 --- a/test/unit/deploy/OwnableCreate2Deployer.t.sol +++ b/test/unit/deploy/OwnableCreate2Deployer.t.sol @@ -15,17 +15,21 @@ contract OwnableCreate2DeployerTest is Test { bytes private childERC20Bytecode; bytes32 private salt; + address private owner; event Deployed(address indexed deployedAddress, address indexed sender, bytes32 indexed salt, bytes32 bytecodeHash); function setUp() public { + owner = address(0x12345); + // create a new deployer that is owned by this contract - deployer = new OwnableCreate2Deployer(); + deployer = new OwnableCreate2Deployer(owner); childERC20 = new ChildERC20(); childERC20Bytecode = type(ChildERC20).creationCode; salt = createSaltFromKey("test-salt"); + vm.startPrank(owner); } function test_RevertIf_DeployWithEmptyByteCode() public { @@ -34,6 +38,8 @@ contract OwnableCreate2DeployerTest is Test { } function test_RevertIf_DeployWithNonOwner() public { + vm.stopPrank(); + address nonOwner = address(0x1); vm.startPrank(nonOwner); vm.expectRevert("Ownable: caller is not the owner"); @@ -49,10 +55,10 @@ contract OwnableCreate2DeployerTest is Test { } function test_deploy_DeploysContract() public { - address expectedAddress = predictCreate2Address(childERC20Bytecode, address(deployer), address(this), salt); + address expectedAddress = predictCreate2Address(childERC20Bytecode, address(deployer), address(owner), salt); vm.expectEmit(); - emit Deployed(expectedAddress, address(this), salt, keccak256(childERC20Bytecode)); + emit Deployed(expectedAddress, address(owner), salt, keccak256(childERC20Bytecode)); address deployed = deployer.deploy(childERC20Bytecode, salt); assertEq(deployed.code, address(childERC20).code, "deployed contract code does not match expected"); @@ -65,7 +71,7 @@ contract OwnableCreate2DeployerTest is Test { function test_deploy_DeploysToPredictedAddress() public { address deployedAddress = deployer.deploy(childERC20Bytecode, salt); - address expectedAddress = predictCreate2Address(childERC20Bytecode, address(deployer), address(this), salt); + address expectedAddress = predictCreate2Address(childERC20Bytecode, address(deployer), address(owner), salt); assertEq(deployedAddress, expectedAddress, "deployed address does not match expected address"); } @@ -105,6 +111,8 @@ contract OwnableCreate2DeployerTest is Test { */ function test_RevertIf_DeployAndInitWithNonOwner() public { + vm.stopPrank(); + address nonOwner = address(0x1); vm.startPrank(nonOwner); vm.expectRevert("Ownable: caller is not the owner"); @@ -112,7 +120,7 @@ contract OwnableCreate2DeployerTest is Test { } function test_deployAndInit_DeploysAndInitsContract() public { - address expectedAddress = predictCreate2Address(childERC20Bytecode, address(deployer), address(this), salt); + address expectedAddress = predictCreate2Address(childERC20Bytecode, address(deployer), address(owner), salt); address rootToken = address(0x1); string memory name = "Test-Token"; string memory symbol = "TST"; @@ -121,7 +129,7 @@ contract OwnableCreate2DeployerTest is Test { abi.encodeWithSelector(ChildERC20.initialize.selector, rootToken, name, symbol, decimals); vm.expectEmit(); - emit Deployed(expectedAddress, address(this), salt, keccak256(childERC20Bytecode)); + emit Deployed(expectedAddress, address(owner), salt, keccak256(childERC20Bytecode)); address deployed = deployer.deployAndInit(childERC20Bytecode, salt, initPayload); // regardless of init data, the deployed address should match expected deployment @@ -142,9 +150,9 @@ contract OwnableCreate2DeployerTest is Test { */ function test_deployedAddress_ReturnsPredictedAddress() public { - address deployAddress = deployer.deployedAddress(childERC20Bytecode, address(this), salt); + address deployAddress = deployer.deployedAddress(childERC20Bytecode, address(owner), salt); - address predictedAddress = predictCreate2Address(childERC20Bytecode, address(deployer), address(this), salt); + address predictedAddress = predictCreate2Address(childERC20Bytecode, address(deployer), address(owner), salt); address deployedAddress = deployer.deploy(childERC20Bytecode, salt); assertEq(deployAddress, predictedAddress, "deployment address did not match predicted address"); @@ -167,6 +175,6 @@ contract OwnableCreate2DeployerTest is Test { } function createSaltFromKey(string memory key) private view returns (bytes32) { - return keccak256(abi.encode(address(this), key)); + return keccak256(abi.encode(address(owner), key)); } } From 42debb974a9c522a42d59fa78d81fb940d5bcf77 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Wed, 6 Dec 2023 14:24:05 +1000 Subject: [PATCH 008/155] Fix issue --- scripts/deploy/child_deployment.ts | 11 +++++------ scripts/deploy/root_deployment.ts | 11 +++++------ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/scripts/deploy/child_deployment.ts b/scripts/deploy/child_deployment.ts index 41aa8477..df7166c3 100644 --- a/scripts/deploy/child_deployment.ts +++ b/scripts/deploy/child_deployment.ts @@ -30,12 +30,6 @@ export async function deployChildContracts() { let reservedDeployerAddr = await reservedDeployerWallet.getAddress(); console.log("Reserved deployer address is: ", reservedDeployerAddr); - // Check the current nonce matches the reserved nonce - let currentNonce = await childProvider.getTransactionCount(reservedDeployerAddr); - if (nonceReserved != currentNonce) { - throw("Nonce mismatch, expected " + nonceReserved + " actual " + currentNonce); - } - // Execute console.log("Deploy child contracts in..."); await waitForConfirmation(); @@ -46,6 +40,11 @@ export async function deployChildContracts() { console.log("Child token template has already been deployed to: " + childContracts.CHILD_TOKEN_TEMPLATE + ", skip."); childTokenTemplate = getContract("ChildERC20", childContracts.CHILD_TOKEN_TEMPLATE, childProvider); } else { + // Check the current nonce matches the reserved nonce + let currentNonce = await childProvider.getTransactionCount(reservedDeployerAddr); + if (nonceReserved != currentNonce) { + throw("Nonce mismatch, expected " + nonceReserved + " actual " + currentNonce); + } console.log("Deploy child token template..."); childTokenTemplate = await deployChildContract("ChildERC20", reservedDeployerWallet, nonceReserved); console.log("Transaction submitted: ", JSON.stringify(childTokenTemplate.deployTransaction, null, 2)); diff --git a/scripts/deploy/root_deployment.ts b/scripts/deploy/root_deployment.ts index 2b22136e..c0561f04 100644 --- a/scripts/deploy/root_deployment.ts +++ b/scripts/deploy/root_deployment.ts @@ -30,12 +30,6 @@ export async function deployRootContracts() { let reservedDeployerAddr = await reservedDeployerWallet.getAddress(); console.log("Reserved deployer address is: ", reservedDeployerAddr); - // Check the current nonce matches the reserved nonce - let currentNonce = await rootProvider.getTransactionCount(reservedDeployerAddr); - if (nonceReserved != currentNonce) { - throw("Nonce mismatch, expected " + nonceReserved + " actual " + currentNonce); - } - // Execute console.log("Deploy root contracts in..."); await waitForConfirmation(); @@ -46,6 +40,11 @@ export async function deployRootContracts() { console.log("Root token template has already been deployed to: " + rootContracts.ROOT_TOKEN_TEMPLATE + ", skip."); rootTokenTemplate = getContract("ChildERC20", rootContracts.ROOT_TOKEN_TEMPLATE, rootProvider); } else { + // Check the current nonce matches the reserved nonce + let currentNonce = await rootProvider.getTransactionCount(reservedDeployerAddr); + if (nonceReserved != currentNonce) { + throw("Nonce mismatch, expected " + nonceReserved + " actual " + currentNonce); + } console.log("Deploy root token template..."); rootTokenTemplate = await deployRootContract("ChildERC20", reservedDeployerWallet, nonceReserved); console.log("Transaction submitted: ", JSON.stringify(rootTokenTemplate.deployTransaction, null, 2)); From 9e5dddc4fcb1e53ebc840a2229e0f2ec7ec1f2bd Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Wed, 6 Dec 2023 16:32:51 +1000 Subject: [PATCH 009/155] Fix defaults --- scripts/bootstrap/0_pre_validation.ts | 4 +- scripts/deploy/child_deployment.ts | 152 ++++++++++++++------------ scripts/deploy/root_deployment.ts | 124 ++++++++++++--------- scripts/localdev/.env.local | 4 +- 4 files changed, 158 insertions(+), 126 deletions(-) diff --git a/scripts/bootstrap/0_pre_validation.ts b/scripts/bootstrap/0_pre_validation.ts index 746d6f64..23cd044d 100644 --- a/scripts/bootstrap/0_pre_validation.ts +++ b/scripts/bootstrap/0_pre_validation.ts @@ -122,10 +122,10 @@ async function run() { if (axelarRequiredIMX.lt(ethers.utils.parseEther("500.0"))) { tryThrow("Axelar on child chain should request at least 500 IMX, got" + ethers.utils.formatEther(axelarRequiredIMX)); } - if (deployerRequiredIMX.lt(ethers.utils.parseEther("500.0"))) { + if (deployerRequiredIMX.lt(ethers.utils.parseEther("250.0"))) { tryThrow("Deployer on child chain should request at least 500 IMX, got" + ethers.utils.formatEther(deployerRequiredIMX)); } - if (reservedDeployerRequiredIMX.lt(ethers.utils.parseEther("10.0"))) { + if (reservedDeployerRequiredIMX.lt(ethers.utils.parseEther("250.0"))) { tryThrow("Reserved deployer on child chain should request at least 10 IMX, got" + ethers.utils.formatEther(reservedDeployerRequiredIMX)); } let extraIMX = ethers.utils.parseEther("100.0"); diff --git a/scripts/deploy/child_deployment.ts b/scripts/deploy/child_deployment.ts index df7166c3..ff17a8b6 100644 --- a/scripts/deploy/child_deployment.ts +++ b/scripts/deploy/child_deployment.ts @@ -18,6 +18,72 @@ export async function deployChildContracts() { let childContracts = getChildContracts(); const childProvider = new ethers.providers.JsonRpcProvider(childRPCURL, Number(childChainID)); + + // Get deployer address + let childDeployerWallet; + if (deployerSecret == "ledger") { + let index = requireEnv("DEPLOYER_LEDGER_INDEX"); + const derivationPath = `m/44'/60'/${parseInt(index)}'/0/0`; + childDeployerWallet = new LedgerSigner(childProvider, derivationPath); + } else { + childDeployerWallet = new ethers.Wallet(deployerSecret, childProvider); + } + let deployerAddr = await childDeployerWallet.getAddress(); + console.log("Deployer address is: ", deployerAddr); + + // Execute + console.log("Deploy child contracts in..."); + await waitForConfirmation(); + + // Deploy wrapped IMX + let wrappedIMX; + if (childContracts.WRAPPED_IMX_ADDRESS != "") { + console.log("Wrapped IMX has already been deployed to: " + childContracts.WRAPPED_IMX_ADDRESS + ", skip."); + wrappedIMX = getContract("WIMX", childContracts.WRAPPED_IMX_ADDRESS, childProvider); + } else { + console.log("Deploy wrapped IMX..."); + wrappedIMX = await deployChildContract("WIMX", childDeployerWallet, null); + console.log("Transaction submitted: ", JSON.stringify(wrappedIMX.deployTransaction, null, 2)); + await waitForReceipt(wrappedIMX.deployTransaction.hash, childProvider); + } + childContracts.WRAPPED_IMX_ADDRESS = wrappedIMX.address; + saveChildContracts(childContracts); + console.log("Deployed to WRAPPED_IMX_ADDRESS: ", wrappedIMX.address); + + // Deploy child bridge impl + let childBridgeImpl; + if (childContracts.CHILD_BRIDGE_IMPL_ADDRESS != "") { + console.log("Child bridge impl has already been deployed to: " + childContracts.CHILD_BRIDGE_IMPL_ADDRESS + ", skip."); + childBridgeImpl = getContract("ChildERC20Bridge", childContracts.CHILD_BRIDGE_IMPL_ADDRESS, childProvider); + } else { + console.log("Deploy child bridge impl..."); + childBridgeImpl = await deployChildContract("ChildERC20Bridge", childDeployerWallet, null); + console.log("Transaction submitted: ", JSON.stringify(childBridgeImpl.deployTransaction, null, 2)); + await waitForReceipt(childBridgeImpl.deployTransaction.hash, childProvider); + } + childContracts.CHILD_BRIDGE_IMPL_ADDRESS = childBridgeImpl.address; + saveChildContracts(childContracts); + console.log("Deployed to CHILD_BRIDGE_IMPL_ADDRESS: ", childBridgeImpl.address); + + // Deploy child adaptor impl + let childAdaptorImpl; + if (childContracts.CHILD_ADAPTOR_IMPL_ADDRESS != "") { + console.log("Child adaptor impl has already been deployed to: " + childContracts.CHILD_ADAPTOR_IMPL_ADDRESS + ", skip."); + childAdaptorImpl = getContract("ChildAxelarBridgeAdaptor", childContracts.CHILD_ADAPTOR_IMPL_ADDRESS, childProvider); + } else { + console.log("Deploy child adaptor impl..."); + childAdaptorImpl = await deployChildContract("ChildAxelarBridgeAdaptor", childDeployerWallet, null, childGatewayAddr); + console.log("Transaction submitted: ", JSON.stringify(childAdaptorImpl.deployTransaction, null, 2)); + await waitForReceipt(childAdaptorImpl.deployTransaction.hash, childProvider); + } + childContracts.CHILD_ADAPTOR_IMPL_ADDRESS = childAdaptorImpl.address; + saveChildContracts(childContracts); + console.log("Deployed to CHILD_ADAPTOR_IMPL_ADDRESS: ", childAdaptorImpl.address); + + if (childDeployerWallet instanceof LedgerSigner) { + childDeployerWallet.close(); + } + // Get reserved wallet let reservedDeployerWallet; if (nonceReservedDeployerSecret == "ledger") { @@ -30,10 +96,6 @@ export async function deployChildContracts() { let reservedDeployerAddr = await reservedDeployerWallet.getAddress(); console.log("Reserved deployer address is: ", reservedDeployerAddr); - // Execute - console.log("Deploy child contracts in..."); - await waitForConfirmation(); - // Deploy child token template let childTokenTemplate; if (childContracts.CHILD_TOKEN_TEMPLATE != "") { @@ -69,45 +131,19 @@ export async function deployChildContracts() { } console.log("Initialised CHILD_TOKEN_TEMPLATE at: ", childTokenTemplate.address); - if (reservedDeployerWallet instanceof LedgerSigner) { - reservedDeployerWallet.close(); - } - - // Get deployer address - let childDeployerWallet; - if (deployerSecret == "ledger") { - let index = requireEnv("DEPLOYER_LEDGER_INDEX"); - const derivationPath = `m/44'/60'/${parseInt(index)}'/0/0`; - childDeployerWallet = new LedgerSigner(childProvider, derivationPath); - } else { - childDeployerWallet = new ethers.Wallet(deployerSecret, childProvider); - } - let deployerAddr = await childDeployerWallet.getAddress(); - console.log("Deployer address is: ", deployerAddr); - - // Deploy wrapped IMX - let wrappedIMX; - if (childContracts.WRAPPED_IMX_ADDRESS != "") { - console.log("Wrapped IMX has already been deployed to: " + childContracts.WRAPPED_IMX_ADDRESS + ", skip."); - wrappedIMX = getContract("WIMX", childContracts.WRAPPED_IMX_ADDRESS, childProvider); - } else { - console.log("Deploy wrapped IMX..."); - wrappedIMX = await deployChildContract("WIMX", childDeployerWallet, null); - console.log("Transaction submitted: ", JSON.stringify(wrappedIMX.deployTransaction, null, 2)); - await waitForReceipt(wrappedIMX.deployTransaction.hash, childProvider); - } - childContracts.WRAPPED_IMX_ADDRESS = wrappedIMX.address; - saveChildContracts(childContracts); - console.log("Deployed to WRAPPED_IMX_ADDRESS: ", wrappedIMX.address); - // Deploy proxy admin let proxyAdmin; if (childContracts.CHILD_PROXY_ADMIN != "") { console.log("Proxy admin has already been deployed to: " + childContracts.CHILD_PROXY_ADMIN + ", skip."); proxyAdmin = getContract("ProxyAdmin", childContracts.CHILD_PROXY_ADMIN, childProvider); } else { + // Check the current nonce matches the reserved nonce + let currentNonce = await childProvider.getTransactionCount(reservedDeployerAddr); + if (nonceReserved + 2 != currentNonce) { + throw("Nonce mismatch, expected " + (nonceReserved + 2) + " actual " + currentNonce); + } console.log("Deploy proxy admin..."); - proxyAdmin = await deployChildContract("ProxyAdmin", childDeployerWallet, null); + proxyAdmin = await deployChildContract("ProxyAdmin", reservedDeployerWallet, null); console.log("Transaction submitted: ", JSON.stringify(proxyAdmin.deployTransaction, null, 2)); await waitForReceipt(proxyAdmin.deployTransaction.hash, childProvider); } @@ -115,50 +151,25 @@ export async function deployChildContracts() { saveChildContracts(childContracts); console.log("Deployed to CHILD_PROXY_ADMIN: ", proxyAdmin.address); - // Deploy child bridge impl - let childBridgeImpl; - if (childContracts.CHILD_BRIDGE_IMPL_ADDRESS != "") { - console.log("Child bridge impl has already been deployed to: " + childContracts.CHILD_BRIDGE_IMPL_ADDRESS + ", skip."); - childBridgeImpl = getContract("ChildERC20Bridge", childContracts.CHILD_BRIDGE_IMPL_ADDRESS, childProvider); - } else { - console.log("Deploy child bridge impl..."); - childBridgeImpl = await deployChildContract("ChildERC20Bridge", childDeployerWallet, null); - console.log("Transaction submitted: ", JSON.stringify(childBridgeImpl.deployTransaction, null, 2)); - await waitForReceipt(childBridgeImpl.deployTransaction.hash, childProvider); - } - childContracts.CHILD_BRIDGE_IMPL_ADDRESS = childBridgeImpl.address; - saveChildContracts(childContracts); - console.log("Deployed to CHILD_BRIDGE_IMPL_ADDRESS: ", childBridgeImpl.address); - // Deploy child bridge proxy let childBridgeProxy; if (childContracts.CHILD_BRIDGE_PROXY_ADDRESS != "") { console.log("Child bridge proxy has already been deployed to: " + childContracts.CHILD_BRIDGE_PROXY_ADDRESS + ", skip."); childBridgeProxy = getContract("TransparentUpgradeableProxy", childContracts.CHILD_BRIDGE_PROXY_ADDRESS, childProvider); } else { + // Check the current nonce matches the reserved nonce + let currentNonce = await childProvider.getTransactionCount(reservedDeployerAddr); + if (nonceReserved + 3 != currentNonce) { + throw("Nonce mismatch, expected " + (nonceReserved + 3) + " actual " + currentNonce); + } console.log("Deploy child bridge proxy..."); - childBridgeProxy = await deployChildContract("TransparentUpgradeableProxy", childDeployerWallet, null, childBridgeImpl.address, proxyAdmin.address, []); + childBridgeProxy = await deployChildContract("TransparentUpgradeableProxy", reservedDeployerWallet, null, childBridgeImpl.address, proxyAdmin.address, []); console.log("Transaction submitted: ", JSON.stringify(childBridgeProxy.deployTransaction, null, 2)); await waitForReceipt(childBridgeProxy.deployTransaction.hash, childProvider); } childContracts.CHILD_BRIDGE_PROXY_ADDRESS = childBridgeProxy.address; saveChildContracts(childContracts); console.log("Deployed to CHILD_BRIDGE_PROXY_ADDRESS: ", childBridgeProxy.address); - - // Deploy child adaptor impl - let childAdaptorImpl; - if (childContracts.CHILD_ADAPTOR_IMPL_ADDRESS != "") { - console.log("Child adaptor impl has already been deployed to: " + childContracts.CHILD_ADAPTOR_IMPL_ADDRESS + ", skip."); - childAdaptorImpl = getContract("ChildAxelarBridgeAdaptor", childContracts.CHILD_ADAPTOR_IMPL_ADDRESS, childProvider); - } else { - console.log("Deploy child adaptor impl..."); - childAdaptorImpl = await deployChildContract("ChildAxelarBridgeAdaptor", childDeployerWallet, null, childGatewayAddr); - console.log("Transaction submitted: ", JSON.stringify(childAdaptorImpl.deployTransaction, null, 2)); - await waitForReceipt(childAdaptorImpl.deployTransaction.hash, childProvider); - } - childContracts.CHILD_ADAPTOR_IMPL_ADDRESS = childAdaptorImpl.address; - saveChildContracts(childContracts); - console.log("Deployed to CHILD_ADAPTOR_IMPL_ADDRESS: ", childAdaptorImpl.address); // Deploy child adaptor proxy let childAdaptorProxy; @@ -166,8 +177,13 @@ export async function deployChildContracts() { console.log("Child adaptor proxy has already been deployed to: " + childContracts.CHILD_ADAPTOR_PROXY_ADDRESS + ", skip."); childAdaptorProxy = getContract("TransparentUpgradeableProxy", childContracts.CHILD_ADAPTOR_PROXY_ADDRESS, childProvider); } else { + // Check the current nonce matches the reserved nonce + let currentNonce = await childProvider.getTransactionCount(reservedDeployerAddr); + if (nonceReserved + 4 != currentNonce) { + throw("Nonce mismatch, expected " + (nonceReserved + 4) + " actual " + currentNonce); + } console.log("Deploy child adaptor proxy..."); - childAdaptorProxy = await deployChildContract("TransparentUpgradeableProxy", childDeployerWallet, null, childAdaptorImpl.address, proxyAdmin.address, []); + childAdaptorProxy = await deployChildContract("TransparentUpgradeableProxy", reservedDeployerWallet, null, childAdaptorImpl.address, proxyAdmin.address, []); console.log("Transaction submitted: ", JSON.stringify(childAdaptorProxy.deployTransaction, null, 2)); await waitForReceipt(childAdaptorProxy.deployTransaction.hash, childProvider); } diff --git a/scripts/deploy/root_deployment.ts b/scripts/deploy/root_deployment.ts index c0561f04..59de8e83 100644 --- a/scripts/deploy/root_deployment.ts +++ b/scripts/deploy/root_deployment.ts @@ -18,6 +18,57 @@ export async function deployRootContracts() { let rootContracts = getRootContracts(); const rootProvider = new ethers.providers.JsonRpcProvider(rootRPCURL, Number(rootChainID)); + + // Get deployer address + let rootDeployerWallet; + if (deployerSecret == "ledger") { + let index = requireEnv("DEPLOYER_LEDGER_INDEX"); + const derivationPath = `m/44'/60'/${parseInt(index)}'/0/0`; + rootDeployerWallet = new LedgerSigner(rootProvider, derivationPath); + } else { + rootDeployerWallet = new ethers.Wallet(deployerSecret, rootProvider); + } + let deployerAddr = await rootDeployerWallet.getAddress(); + console.log("Deployer address is: ", deployerAddr); + + // Execute + console.log("Deploy root contracts in..."); + await waitForConfirmation(); + + // Deploy root bridge impl + let rootBridgeImpl; + if (rootContracts.ROOT_BRIDGE_IMPL_ADDRESS != "") { + console.log("Root bridge impl has already been deployed to: " + rootContracts.ROOT_BRIDGE_IMPL_ADDRESS + ", skip."); + rootBridgeImpl = getContract("RootERC20BridgeFlowRate", rootContracts.ROOT_BRIDGE_IMPL_ADDRESS, rootProvider); + } else { + console.log("Deploy root bridge impl..."); + rootBridgeImpl = await deployRootContract("RootERC20BridgeFlowRate", rootDeployerWallet, null); + console.log("Transaction submitted: ", JSON.stringify(rootBridgeImpl.deployTransaction, null, 2)); + await waitForReceipt(rootBridgeImpl.deployTransaction.hash, rootProvider); + } + rootContracts.ROOT_BRIDGE_IMPL_ADDRESS = rootBridgeImpl.address; + saveRootContracts(rootContracts); + console.log("Deployed to ROOT_BRIDGE_IMPL_ADDRESS: ", rootBridgeImpl.address); + + // Deploy root adaptor impl + let rootAdaptorImpl; + if (rootContracts.ROOT_ADAPTOR_IMPL_ADDRESS != "") { + console.log("Root adaptor impl has already been deployed to: " + rootContracts.ROOT_ADAPTOR_IMPL_ADDRESS + ", skip."); + rootAdaptorImpl = getContract("RootAxelarBridgeAdaptor", rootContracts.ROOT_ADAPTOR_IMPL_ADDRESS, rootProvider); + } else { + console.log("Deploy root adaptor impl..."); + rootAdaptorImpl = await deployRootContract("RootAxelarBridgeAdaptor", rootDeployerWallet, null, rootGatewayAddr); + console.log("Transaction submitted: ", JSON.stringify(rootAdaptorImpl.deployTransaction, null, 2)); + await waitForReceipt(rootAdaptorImpl.deployTransaction.hash, rootProvider); + } + rootContracts.ROOT_ADAPTOR_IMPL_ADDRESS = rootAdaptorImpl.address; + saveRootContracts(rootContracts); + console.log("Deployed to ROOT_ADAPTOR_IMPL_ADDRESS: ", rootAdaptorImpl.address); + + if (rootDeployerWallet instanceof LedgerSigner) { + rootDeployerWallet.close(); + } + // Get reserved wallet let reservedDeployerWallet; if (nonceReservedDeployerSecret == "ledger") { @@ -30,10 +81,6 @@ export async function deployRootContracts() { let reservedDeployerAddr = await reservedDeployerWallet.getAddress(); console.log("Reserved deployer address is: ", reservedDeployerAddr); - // Execute - console.log("Deploy root contracts in..."); - await waitForConfirmation(); - // Deploy root token template let rootTokenTemplate; if (rootContracts.ROOT_TOKEN_TEMPLATE != "") { @@ -65,60 +112,39 @@ export async function deployRootContracts() { } console.log("Deployed to ROOT_TOKEN_TEMPLATE: ", rootTokenTemplate.address); - if (reservedDeployerWallet instanceof LedgerSigner) { - reservedDeployerWallet.close(); - } - - // Get deployer address - let rootDeployerWallet; - if (deployerSecret == "ledger") { - let index = requireEnv("DEPLOYER_LEDGER_INDEX"); - const derivationPath = `m/44'/60'/${parseInt(index)}'/0/0`; - rootDeployerWallet = new LedgerSigner(rootProvider, derivationPath); - } else { - rootDeployerWallet = new ethers.Wallet(deployerSecret, rootProvider); - } - let deployerAddr = await rootDeployerWallet.getAddress(); - console.log("Deployer address is: ", deployerAddr); - // Deploy proxy admin let proxyAdmin; if (rootContracts.ROOT_PROXY_ADMIN != "") { console.log("Proxy admin has already been deployed to: " + rootContracts.ROOT_PROXY_ADMIN + ", skip."); proxyAdmin = getContract("ProxyAdmin", rootContracts.ROOT_PROXY_ADMIN, rootProvider); } else { + // Check the current nonce matches the reserved nonce + let currentNonce = await rootProvider.getTransactionCount(reservedDeployerAddr); + if (nonceReserved + 2 != currentNonce) { + throw("Nonce mismatch, expected " + (nonceReserved + 2) + " actual " + currentNonce); + } console.log("Deploy proxy admin..."); - proxyAdmin = await deployRootContract("ProxyAdmin", rootDeployerWallet, null); + proxyAdmin = await deployRootContract("ProxyAdmin", reservedDeployerWallet, null); console.log("Transaction submitted: ", JSON.stringify(proxyAdmin.deployTransaction, null, 2)); - await waitForReceipt(proxyAdmin.deployTransaction.hash, rootProvider); + await waitForReceipt(proxyAdmin.deployTransaction.hash, rootProvider); } rootContracts.ROOT_PROXY_ADMIN = proxyAdmin.address; saveRootContracts(rootContracts); console.log("Deployed to ROOT_PROXY_ADMIN: ", proxyAdmin.address); - // Deploy root bridge impl - let rootBridgeImpl; - if (rootContracts.ROOT_BRIDGE_IMPL_ADDRESS != "") { - console.log("Root bridge impl has already been deployed to: " + rootContracts.ROOT_BRIDGE_IMPL_ADDRESS + ", skip."); - rootBridgeImpl = getContract("RootERC20BridgeFlowRate", rootContracts.ROOT_BRIDGE_IMPL_ADDRESS, rootProvider); - } else { - console.log("Deploy root bridge impl..."); - rootBridgeImpl = await deployRootContract("RootERC20BridgeFlowRate", rootDeployerWallet, null); - console.log("Transaction submitted: ", JSON.stringify(rootBridgeImpl.deployTransaction, null, 2)); - await waitForReceipt(rootBridgeImpl.deployTransaction.hash, rootProvider); - } - rootContracts.ROOT_BRIDGE_IMPL_ADDRESS = rootBridgeImpl.address; - saveRootContracts(rootContracts); - console.log("Deployed to ROOT_BRIDGE_IMPL_ADDRESS: ", rootBridgeImpl.address); - // Deploy root bridge proxy let rootBridgeProxy; if (rootContracts.ROOT_BRIDGE_PROXY_ADDRESS != "") { console.log("Root bridge proxy has already been deployed to: " + rootContracts.ROOT_BRIDGE_PROXY_ADDRESS + ", skip."); rootBridgeProxy = getContract("TransparentUpgradeableProxy", rootContracts.ROOT_BRIDGE_PROXY_ADDRESS, rootProvider); } else { + // Check the current nonce matches the reserved nonce + let currentNonce = await rootProvider.getTransactionCount(reservedDeployerAddr); + if (nonceReserved + 3 != currentNonce) { + throw("Nonce mismatch, expected " + (nonceReserved + 3) + " actual " + currentNonce); + } console.log("Deploy root bridge proxy..."); - rootBridgeProxy = await deployRootContract("TransparentUpgradeableProxy", rootDeployerWallet, null, rootBridgeImpl.address, proxyAdmin.address, []); + rootBridgeProxy = await deployRootContract("TransparentUpgradeableProxy", reservedDeployerWallet, null, rootBridgeImpl.address, proxyAdmin.address, []); console.log("Transaction submitted: ", JSON.stringify(rootBridgeProxy.deployTransaction, null, 2)); await waitForReceipt(rootBridgeProxy.deployTransaction.hash, rootProvider); } @@ -126,29 +152,19 @@ export async function deployRootContracts() { saveRootContracts(rootContracts); console.log("Deployed to ROOT_BRIDGE_PROXY_ADDRESS: ", rootBridgeProxy.address); - // Deploy root adaptor impl - let rootAdaptorImpl; - if (rootContracts.ROOT_ADAPTOR_IMPL_ADDRESS != "") { - console.log("Root adaptor impl has already been deployed to: " + rootContracts.ROOT_ADAPTOR_IMPL_ADDRESS + ", skip."); - rootAdaptorImpl = getContract("RootAxelarBridgeAdaptor", rootContracts.ROOT_ADAPTOR_IMPL_ADDRESS, rootProvider); - } else { - console.log("Deploy root adaptor impl..."); - rootAdaptorImpl = await deployRootContract("RootAxelarBridgeAdaptor", rootDeployerWallet, null, rootGatewayAddr); - console.log("Transaction submitted: ", JSON.stringify(rootAdaptorImpl.deployTransaction, null, 2)); - await waitForReceipt(rootAdaptorImpl.deployTransaction.hash, rootProvider); - } - rootContracts.ROOT_ADAPTOR_IMPL_ADDRESS = rootAdaptorImpl.address; - saveRootContracts(rootContracts); - console.log("Deployed to ROOT_ADAPTOR_IMPL_ADDRESS: ", rootAdaptorImpl.address); - // Deploy root adaptor proxy let rootAdaptorProxy; if (rootContracts.ROOT_ADAPTOR_PROXY_ADDRESS != "") { console.log("Root adaptor proxy has already been deployed to: " + rootContracts.ROOT_ADAPTOR_PROXY_ADDRESS + ", skip."); rootAdaptorProxy = getContract("TransparentUpgradeableProxy", rootContracts.ROOT_ADAPTOR_PROXY_ADDRESS, rootProvider); } else { + // Check the current nonce matches the reserved nonce + let currentNonce = await rootProvider.getTransactionCount(reservedDeployerAddr); + if (nonceReserved + 4 != currentNonce) { + throw("Nonce mismatch, expected " + (nonceReserved + 4) + " actual " + currentNonce); + } console.log("Deploy root adaptor proxy..."); - rootAdaptorProxy = await deployRootContract("TransparentUpgradeableProxy", rootDeployerWallet, null, rootAdaptorImpl.address, proxyAdmin.address, []); + rootAdaptorProxy = await deployRootContract("TransparentUpgradeableProxy", reservedDeployerWallet, null, rootAdaptorImpl.address, proxyAdmin.address, []); console.log("Transaction submitted: ", JSON.stringify(rootAdaptorProxy.deployTransaction, null, 2)); await waitForReceipt(rootAdaptorProxy.deployTransaction.hash, rootProvider); } diff --git a/scripts/localdev/.env.local b/scripts/localdev/.env.local index fd3713fe..9b4c5fbd 100644 --- a/scripts/localdev/.env.local +++ b/scripts/localdev/.env.local @@ -34,9 +34,9 @@ AXELAR_EOA=0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC ## The amount of fund Axelar requested, unit is in IMX or 10^18 Wei. AXELAR_FUND=500 ## The amount of fund deployer to be left with after bootstrapping on L2, unit is in IMX or 10^18 Wei. -CHILD_DEPLOYER_FUND=500 +CHILD_DEPLOYER_FUND=250 ## The amount of fund nonce reserved deployer required on L2, unit is in IMX or 10^18 Wei. -CHILD_NONCE_RESERVED_DEPLOYER_FUND=10 +CHILD_NONCE_RESERVED_DEPLOYER_FUND=250 ## The maximum amount of IMX that can be deposited to L2, unit is in IMX or 10^18 Wei. IMX_DEPOSIT_LIMIT=100000000 ## The privileged transaction multisig address on the root chain. From e7d2c761f06c3bbb96d605d0ea190cdd6dd03b74 Mon Sep 17 00:00:00 2001 From: Craig M Date: Fri, 8 Dec 2023 10:48:35 +1300 Subject: [PATCH 010/155] test init, mint, burn --- test/unit/child/ChildERC20.t.sol | 96 ++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 test/unit/child/ChildERC20.t.sol diff --git a/test/unit/child/ChildERC20.t.sol b/test/unit/child/ChildERC20.t.sol new file mode 100644 index 00000000..353a2e52 --- /dev/null +++ b/test/unit/child/ChildERC20.t.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {ChildERC20} from "../../../src/child/ChildERC20.sol"; + +contract ChildERC20Test is Test { + string constant DEFAULT_CHILDERC20_NAME = "Child ERC20"; + string constant DEFAULT_CHILDERC20_SYMBOL = "CERC"; + uint8 constant DEFAULT_CHILDERC20_DECIMALS = 18; + address constant DEFAULT_CHILDERC20_ADDRESS = address(111); + + ChildERC20 public childToken; + + function setUp() public { + childToken = new ChildERC20(); + childToken.initialize(DEFAULT_CHILDERC20_ADDRESS, DEFAULT_CHILDERC20_NAME, DEFAULT_CHILDERC20_SYMBOL, DEFAULT_CHILDERC20_DECIMALS); + } + + function test_InitialState() public { + assertEq(childToken.name(), DEFAULT_CHILDERC20_NAME, "Incorrect token name"); + assertEq(childToken.symbol(), DEFAULT_CHILDERC20_SYMBOL, "Incorrect token symbol"); + assertEq(childToken.decimals(), DEFAULT_CHILDERC20_DECIMALS, "Incorrect token decimals"); + assertEq(childToken.totalSupply(), 0, "Incorrect token supply"); + } + + function test_FailInitialisationBadAddress() public { + ChildERC20 failedToken = new ChildERC20(); + vm.expectRevert("ChildERC20: BAD_INITIALIZATION"); + failedToken.initialize(address(0), DEFAULT_CHILDERC20_NAME, DEFAULT_CHILDERC20_SYMBOL, DEFAULT_CHILDERC20_DECIMALS); + } + + function test_FailInitialisationBadName() public { + ChildERC20 failedToken = new ChildERC20(); + vm.expectRevert("ChildERC20: BAD_INITIALIZATION"); + failedToken.initialize(DEFAULT_CHILDERC20_ADDRESS, "", DEFAULT_CHILDERC20_SYMBOL, DEFAULT_CHILDERC20_DECIMALS); + } + + function test_FailInitialisationBadSymbol() public { + ChildERC20 failedToken = new ChildERC20(); + vm.expectRevert("ChildERC20: BAD_INITIALIZATION"); + failedToken.initialize(DEFAULT_CHILDERC20_ADDRESS, DEFAULT_CHILDERC20_NAME, "", DEFAULT_CHILDERC20_DECIMALS); + } + + function test_RevertIf_InitializeTwice() public { + vm.expectRevert("Initializable: contract is already initialized"); + childToken.initialize(DEFAULT_CHILDERC20_ADDRESS, DEFAULT_CHILDERC20_NAME, DEFAULT_CHILDERC20_SYMBOL, DEFAULT_CHILDERC20_DECIMALS); + } + + function test_RevertIf_MintTokensByNonDeployer() public { + address nonDeployer = address(333); + address receiver = address(222); + vm.prank(nonDeployer); + vm.expectRevert("ChildERC20: Only bridge can call"); + childToken.mint(receiver, 100); + } + + function test_MintSuccess() public { + uint256 mintAmount = 1000000; + address receiver = address(222); + + uint256 receiverPreBal = childToken.balanceOf(receiver); + + childToken.mint(receiver, mintAmount); + + uint256 receiverPostBal = childToken.balanceOf(receiver); + + assertEq(childToken.totalSupply(), mintAmount, "Incorrect token supply"); + assertEq(receiverPostBal, receiverPreBal + mintAmount, "Incorrect token balance"); + } + + function test_RevertIf_BurnTokensByNonDeployer() public { + address nonDeployer = address(333); + address receiver = address(222); + vm.prank(nonDeployer); + vm.expectRevert("ChildERC20: Only bridge can call"); + childToken.burn(receiver, 100); + } + + function test_BurnSuccess() public { + uint256 mintAmount = 1000000; + uint256 burnAmount = 1000; + address receiver = address(222); + + childToken.mint(receiver, mintAmount); + + uint256 receiverPreBurnBal = childToken.balanceOf(receiver); + + childToken.burn(receiver, burnAmount); + + uint256 receiverPostBurnBal = childToken.balanceOf(receiver); + + assertEq(childToken.totalSupply(), mintAmount - burnAmount, "Incorrect token supply"); + assertEq(receiverPostBurnBal, receiverPreBurnBal - burnAmount, "Incorrect token balance"); + } +} From a686dc6e317c9079a5a92cc1e2b1b0b9c98fb51e Mon Sep 17 00:00:00 2001 From: Craig M Date: Fri, 8 Dec 2023 13:24:03 +1300 Subject: [PATCH 011/155] naming update --- test/unit/child/ChildERC20.t.sol | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/unit/child/ChildERC20.t.sol b/test/unit/child/ChildERC20.t.sol index 353a2e52..669f1cf1 100644 --- a/test/unit/child/ChildERC20.t.sol +++ b/test/unit/child/ChildERC20.t.sol @@ -47,10 +47,10 @@ contract ChildERC20Test is Test { childToken.initialize(DEFAULT_CHILDERC20_ADDRESS, DEFAULT_CHILDERC20_NAME, DEFAULT_CHILDERC20_SYMBOL, DEFAULT_CHILDERC20_DECIMALS); } - function test_RevertIf_MintTokensByNonDeployer() public { - address nonDeployer = address(333); + function test_RevertIf_MintTokensByNotDeployer() public { + address notDeployer = address(333); address receiver = address(222); - vm.prank(nonDeployer); + vm.prank(notDeployer); vm.expectRevert("ChildERC20: Only bridge can call"); childToken.mint(receiver, 100); } @@ -69,10 +69,10 @@ contract ChildERC20Test is Test { assertEq(receiverPostBal, receiverPreBal + mintAmount, "Incorrect token balance"); } - function test_RevertIf_BurnTokensByNonDeployer() public { - address nonDeployer = address(333); + function test_RevertIf_BurnTokensByNotDeployer() public { + address notDeployer = address(333); address receiver = address(222); - vm.prank(nonDeployer); + vm.prank(notDeployer); vm.expectRevert("ChildERC20: Only bridge can call"); childToken.burn(receiver, 100); } From bad1f45f6b52cd3d98793fde752def7c7b280ba1 Mon Sep 17 00:00:00 2001 From: Craig M Date: Fri, 8 Dec 2023 13:27:50 +1300 Subject: [PATCH 012/155] formatting --- test/unit/child/ChildERC20.t.sol | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/test/unit/child/ChildERC20.t.sol b/test/unit/child/ChildERC20.t.sol index 669f1cf1..17996e10 100644 --- a/test/unit/child/ChildERC20.t.sol +++ b/test/unit/child/ChildERC20.t.sol @@ -14,7 +14,9 @@ contract ChildERC20Test is Test { function setUp() public { childToken = new ChildERC20(); - childToken.initialize(DEFAULT_CHILDERC20_ADDRESS, DEFAULT_CHILDERC20_NAME, DEFAULT_CHILDERC20_SYMBOL, DEFAULT_CHILDERC20_DECIMALS); + childToken.initialize( + DEFAULT_CHILDERC20_ADDRESS, DEFAULT_CHILDERC20_NAME, DEFAULT_CHILDERC20_SYMBOL, DEFAULT_CHILDERC20_DECIMALS + ); } function test_InitialState() public { @@ -27,7 +29,9 @@ contract ChildERC20Test is Test { function test_FailInitialisationBadAddress() public { ChildERC20 failedToken = new ChildERC20(); vm.expectRevert("ChildERC20: BAD_INITIALIZATION"); - failedToken.initialize(address(0), DEFAULT_CHILDERC20_NAME, DEFAULT_CHILDERC20_SYMBOL, DEFAULT_CHILDERC20_DECIMALS); + failedToken.initialize( + address(0), DEFAULT_CHILDERC20_NAME, DEFAULT_CHILDERC20_SYMBOL, DEFAULT_CHILDERC20_DECIMALS + ); } function test_FailInitialisationBadName() public { @@ -44,7 +48,9 @@ contract ChildERC20Test is Test { function test_RevertIf_InitializeTwice() public { vm.expectRevert("Initializable: contract is already initialized"); - childToken.initialize(DEFAULT_CHILDERC20_ADDRESS, DEFAULT_CHILDERC20_NAME, DEFAULT_CHILDERC20_SYMBOL, DEFAULT_CHILDERC20_DECIMALS); + childToken.initialize( + DEFAULT_CHILDERC20_ADDRESS, DEFAULT_CHILDERC20_NAME, DEFAULT_CHILDERC20_SYMBOL, DEFAULT_CHILDERC20_DECIMALS + ); } function test_RevertIf_MintTokensByNotDeployer() public { @@ -52,7 +58,7 @@ contract ChildERC20Test is Test { address receiver = address(222); vm.prank(notDeployer); vm.expectRevert("ChildERC20: Only bridge can call"); - childToken.mint(receiver, 100); + childToken.mint(receiver, 100); } function test_MintSuccess() public { @@ -61,7 +67,7 @@ contract ChildERC20Test is Test { uint256 receiverPreBal = childToken.balanceOf(receiver); - childToken.mint(receiver, mintAmount); + childToken.mint(receiver, mintAmount); uint256 receiverPostBal = childToken.balanceOf(receiver); @@ -74,7 +80,7 @@ contract ChildERC20Test is Test { address receiver = address(222); vm.prank(notDeployer); vm.expectRevert("ChildERC20: Only bridge can call"); - childToken.burn(receiver, 100); + childToken.burn(receiver, 100); } function test_BurnSuccess() public { @@ -82,11 +88,11 @@ contract ChildERC20Test is Test { uint256 burnAmount = 1000; address receiver = address(222); - childToken.mint(receiver, mintAmount); + childToken.mint(receiver, mintAmount); uint256 receiverPreBurnBal = childToken.balanceOf(receiver); - childToken.burn(receiver, burnAmount); + childToken.burn(receiver, burnAmount); uint256 receiverPostBurnBal = childToken.balanceOf(receiver); From 6e04ccb310ed5c04092db67775897f0cf6cffc44 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Fri, 8 Dec 2023 11:01:49 +1000 Subject: [PATCH 013/155] Update root_initialisation.ts --- scripts/deploy/root_initialisation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/deploy/root_initialisation.ts b/scripts/deploy/root_initialisation.ts index 4051d25e..e95dd67d 100644 --- a/scripts/deploy/root_initialisation.ts +++ b/scripts/deploy/root_initialisation.ts @@ -73,7 +73,7 @@ export async function initialiseRootContracts() { { defaultAdmin: deployerAddr, pauser: rootPauser, - unpauser: rootPauser, + unpauser: rootMultisigAddr, variableManager: rootMultisigAddr, adaptorManager: rootMultisigAddr, }, From 50bbcc490fb29d57d866fe95916bcf62ea60b667 Mon Sep 17 00:00:00 2001 From: Craig M Date: Fri, 8 Dec 2023 14:42:58 +1300 Subject: [PATCH 014/155] update private function to have underscore --- src/child/ChildERC20Bridge.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/child/ChildERC20Bridge.sol b/src/child/ChildERC20Bridge.sol index 06e3051b..6c0c2f07 100644 --- a/src/child/ChildERC20Bridge.sol +++ b/src/child/ChildERC20Bridge.sol @@ -459,10 +459,10 @@ contract ChildERC20Bridge is revert ZeroAddress(); } - transferTokensAndEmitEvent(rootToken, rootTokenToChildToken[rootToken], sender, receiver, amount); + _transferTokensAndEmitEvent(rootToken, rootTokenToChildToken[rootToken], sender, receiver, amount); } - function transferTokensAndEmitEvent( + function _transferTokensAndEmitEvent( address rootToken, address childToken, address sender, From 6384295727dd7651679727702f62c7eaa5e1f2a4 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Fri, 8 Dec 2023 12:00:17 +1000 Subject: [PATCH 015/155] Fix e2e --- package.json | 4 +- scripts/bootstrap/README.md | 2 +- scripts/e2e/README.md | 2 +- scripts/e2e/e2e.ts | 103 ++++++++++++++++++------------------ scripts/localdev/README.md | 2 +- 5 files changed, 57 insertions(+), 56 deletions(-) diff --git a/package.json b/package.json index 10de3b11..fd4617d7 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ "lint": "forge fmt", "local:start": "cd scripts/localdev; ./start.sh", "local:setup": "cd scripts/localdev; rm -rf .child.bridge.contracts.json .root.bridge.contracts.json; ./deploy.sh", - "local:test": "cd scripts/localdev; LONG_WAIT=0 SHORT_WAIT=0 npx mocha --require mocha-suppress-logs ../e2e/e2e.ts", - "local:ci": "cd scripts/localdev; rm -rf .child.bridge.contracts.json .root.bridge.contracts.json; ./ci.sh && ./deploy.sh && LONG_WAIT=0 SHORT_WAIT=0 npx mocha --require mocha-suppress-logs ../e2e/e2e.ts && ./stop.sh", + "local:test": "cd scripts/localdev; AXELAR_API_URL=skip npx mocha --require mocha-suppress-logs ../e2e/e2e.ts", + "local:ci": "cd scripts/localdev; rm -rf .child.bridge.contracts.json .root.bridge.contracts.json; ./ci.sh && ./deploy.sh && AXELAR_API_URL=skip npx mocha --require mocha-suppress-logs ../e2e/e2e.ts && ./stop.sh", "local:chainonly": "cd scripts/localdev; LOCAL_CHAIN_ONLY=true ./start.sh", "local:axelaronly": "cd scripts/localdev; npx ts-node axelar_setup.ts", "stop": "cd scripts/localdev; ./stop.sh" diff --git a/scripts/bootstrap/README.md b/scripts/bootstrap/README.md index 0c106618..6113ca4c 100644 --- a/scripts/bootstrap/README.md +++ b/scripts/bootstrap/README.md @@ -169,5 +169,5 @@ npx ts-node 9_test_preparation.ts 2>&1 | tee -a bootstrap.out ``` 15. Test bridge functions ``` -LONG_WAIT=1200000 SHORT_WAIT=300000 npx mocha --require mocha-suppress-logs ../e2e/e2e.ts 2>&1 | tee -a bootstrap.out +AXELAR_API_URL=${Axelar API URL} npx mocha --require mocha-suppress-logs ../e2e/e2e.ts 2>&1 | tee -a bootstrap.out ``` \ No newline at end of file diff --git a/scripts/e2e/README.md b/scripts/e2e/README.md index 3989fbb5..914ea035 100644 --- a/scripts/e2e/README.md +++ b/scripts/e2e/README.md @@ -52,5 +52,5 @@ TEST_ACCOUNT_SECRET= 3. Run end to end tests ``` -LONG_WAIT=1200000 SHORT_WAIT=300000 npx mocha --require mocha-suppress-logs ./e2e.ts +AXELAR_API_URL=${Axelar API URL or "skip" if run on local} npx mocha --require mocha-suppress-logs ./e2e.ts ``` \ No newline at end of file diff --git a/scripts/e2e/e2e.ts b/scripts/e2e/e2e.ts index 7adba223..d6a0ba13 100644 --- a/scripts/e2e/e2e.ts +++ b/scripts/e2e/e2e.ts @@ -21,8 +21,7 @@ describe("Bridge e2e test", () => { let childWIMX: ethers.Contract; let rootCustomToken: ethers.Contract; let childCustomToken: ethers.Contract; - let longWait: number; - let shortWait: number; + let axelarAPI: string; before(async function () { this.timeout(300000); @@ -33,17 +32,7 @@ describe("Bridge e2e test", () => { let testAccountKey = requireEnv("TEST_ACCOUNT_SECRET"); let rootIMXAddr = requireEnv("ROOT_IMX_ADDR"); let rootWETHAddr = requireEnv("ROOT_WETH_ADDR"); - - if (process.env["LONG_WAIT"] == null || process.env["LONG_WAIT"] == "") { - longWait = 1200000; - } else { - longWait = Number(process.env["LONG_WAIT"]) - } - if (process.env["SHORT_WAIT"] == null || process.env["SHORT_WAIT"] == "") { - longWait = 300000; - } else { - longWait = Number(process.env["SHORT_WAIT"]) - } + axelarAPI = requireEnv("AXELAR_API_URL"); // Read from contract file. let childContracts = getChildContracts(); @@ -88,9 +77,7 @@ describe("Bridge e2e test", () => { let postBalL1 = await rootIMX.balanceOf(rootTestWallet.address); let postBalL2 = preBalL2; - console.log("Wait " + longWait + " ms"); - await delay(longWait); - console.log("Done"); + await waitUntilSucceed(axelarAPI, resp.hash); while (postBalL2.eq(preBalL2)) { postBalL2 = await childProvider.getBalance(childTestWallet.address); @@ -102,7 +89,7 @@ describe("Bridge e2e test", () => { let expectedPostL2 = preBalL2.add(amt); expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); - }).timeout(1800000) + }).timeout(2400000) it("should successfully withdraw IMX to self from L2 to L1", async() => { // Get IMX balance on root & child chains before withdraw @@ -124,9 +111,7 @@ describe("Bridge e2e test", () => { let postBalL1 = preBalL1; let postBalL2 = await childProvider.getBalance(childTestWallet.address); - console.log("Wait " + shortWait + " ms"); - await delay(shortWait); - console.log("Done"); + await waitUntilSucceed(axelarAPI, resp.hash); while (postBalL1.eq(preBalL1)) { postBalL1 = await rootIMX.balanceOf(rootTestWallet.address); @@ -140,7 +125,7 @@ describe("Bridge e2e test", () => { let expectedPostL2 = preBalL2.sub(txFee).sub(amt).sub(bridgeFee); expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); - }).timeout(1800000) + }).timeout(2400000) it("should successfully withdraw wIMX to self from L2 to L1", async() => { // Wrap 1 IMX @@ -179,9 +164,7 @@ describe("Bridge e2e test", () => { let postBalL1 = preBalL1; let postBalL2 = await childWIMX.balanceOf(childTestWallet.address); - console.log("Wait " + shortWait + " ms"); - await delay(shortWait); - console.log("Done"); + await waitUntilSucceed(axelarAPI, resp.hash); while (postBalL1.eq(preBalL1)) { postBalL1 = await rootIMX.balanceOf(rootTestWallet.address); @@ -193,7 +176,7 @@ describe("Bridge e2e test", () => { let expectedPostL2 = preBalL2.sub(amt); expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); - }).timeout(1800000) + }).timeout(2400000) it("should successfully deposit ETH to self from L1 to L2", async() => { // Get ETH balance on root & child chains before deposit @@ -212,9 +195,7 @@ describe("Bridge e2e test", () => { let postBalL1 = await rootProvider.getBalance(rootTestWallet.address); let postBalL2 = preBalL2; - console.log("Wait " + longWait + " ms"); - await delay(longWait); - console.log("Done"); + await waitUntilSucceed(axelarAPI, resp.hash); while (postBalL2.eq(preBalL2)) { postBalL2 = await childETH.balanceOf(childTestWallet.address); @@ -223,12 +204,12 @@ describe("Bridge e2e test", () => { // Verify let receipt = await rootProvider.getTransactionReceipt(resp.hash); - let txFee = receipt.cumulativeGasUsed.mul(receipt.effectiveGasPrice); + let txFee = receipt.gasUsed.mul(receipt.effectiveGasPrice); let expectedPostL1 = preBalL1.sub(txFee).sub(amt).sub(bridgeFee); let expectedPostL2 = preBalL2.add(amt); expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); - }).timeout(1800000) + }).timeout(2400000) it("should successfully deposit wETH to self from L1 to L2", async() => { // Wrap 0.01 ETH @@ -257,9 +238,7 @@ describe("Bridge e2e test", () => { let postBalL1 = await rootWETH.balanceOf(rootTestWallet.address); let postBalL2 = preBalL2; - console.log("Wait " + longWait + " ms"); - await delay(longWait); - console.log("Done"); + await waitUntilSucceed(axelarAPI, resp.hash); while (postBalL2.eq(preBalL2)) { postBalL2 = await childETH.balanceOf(childTestWallet.address); @@ -271,7 +250,7 @@ describe("Bridge e2e test", () => { let expectedPostL2 = preBalL2.add(amt); expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); - }).timeout(1800000) + }).timeout(2400000) it("should successfully withdraw ETH to self from L2 to L1", async() => { // Get ETH balance on root & child chains before withdraw @@ -293,9 +272,7 @@ describe("Bridge e2e test", () => { let postBalL1 = preBalL1; let postBalL2 = await childETH.balanceOf(childTestWallet.address); - console.log("Wait " + shortWait + " ms"); - await delay(shortWait); - console.log("Done"); + await waitUntilSucceed(axelarAPI, resp.hash); while (postBalL1.eq(preBalL1)) { postBalL1 = await rootProvider.getBalance(rootTestWallet.address); @@ -307,7 +284,7 @@ describe("Bridge e2e test", () => { let expectedPostL2 = preBalL2.sub(amt); expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); - }).timeout(1800000) + }).timeout(2400000) it("should successfully map a ERC20 Token", async() => { let childContracts = getChildContracts(); @@ -329,9 +306,7 @@ describe("Bridge e2e test", () => { let childTokenAddr = await childBridge.rootTokenToChildToken(rootCustomToken.address); - console.log("Wait " + longWait + " ms"); - await delay(longWait); - console.log("Done"); + await waitUntilSucceed(axelarAPI, resp.hash); while (childTokenAddr == ethers.constants.AddressZero) { childTokenAddr = await childBridge.rootTokenToChildToken(rootCustomToken.address); @@ -343,7 +318,7 @@ describe("Bridge e2e test", () => { // Verify expect(childTokenAddr).to.equal(expectedChildTokenAddr); - }).timeout(1800000) + }).timeout(2400000) it("should successfully deposit mapped ERC20 Token to self from L1 to L2", async() => { // Get token balance on root & child chains before deposit @@ -366,9 +341,7 @@ describe("Bridge e2e test", () => { let postBalL1 = await rootCustomToken.balanceOf(rootTestWallet.address); let postBalL2 = preBalL2; - console.log("Wait " + longWait + " ms"); - await delay(longWait); - console.log("Done"); + await waitUntilSucceed(axelarAPI, resp.hash); while (postBalL2.eq(preBalL2)) { postBalL2 = await childCustomToken.balanceOf(childTestWallet.address); @@ -380,7 +353,7 @@ describe("Bridge e2e test", () => { let expectedPostL2 = preBalL2.add(amt); expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); - }).timeout(1800000) + }).timeout(2400000) it("should successfully withdraw mapped ERC20 Token to self from L2 to L1", async() => { // Get token balance on root & child chains before deposit @@ -402,9 +375,7 @@ describe("Bridge e2e test", () => { let postBalL1 = preBalL1; let postBalL2 = await childCustomToken.balanceOf(childTestWallet.address); - console.log("Wait " + shortWait + " ms"); - await delay(shortWait); - console.log("Done"); + await waitUntilSucceed(axelarAPI, resp.hash); while (postBalL1.eq(preBalL1)) { postBalL1 = await rootCustomToken.balanceOf(rootTestWallet.address); @@ -416,5 +387,35 @@ describe("Bridge e2e test", () => { let expectedPostL2 = preBalL2.sub(amt); expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); - }).timeout(1800000) -}) \ No newline at end of file + }).timeout(2400000) +}) + +async function waitUntilSucceed(axelarURL: string, txHash: any) { + if (axelarURL == "skip") { + return; + } + console.log("Wait until succeed... tx hash: ", txHash) + let response; + let req = '{"method": "searchGMP", "txHash": "' + txHash + '"}' + while (true) { + response = await fetch(axelarURL, { + method: 'POST', + body: req, + headers: {'Content-Type': 'application/json; charset=UTF-8'} }); + if (!response.ok) {} + if (response.body !== null) { + const asString = new TextDecoder("utf-8").decode(await response.arrayBuffer()); + const asJSON = JSON.parse(asString); + if (asJSON.data[0] == undefined) { + console.log("Waiting for " + txHash + " to become available..."); + } else { + console.log("Current status of " + txHash + ": " + asJSON.data[0].status); + if (asJSON.data[0].status == "executed") { + console.log("Done"); + return; + } + } + } + await delay(60000); + } +} \ No newline at end of file diff --git a/scripts/localdev/README.md b/scripts/localdev/README.md index 407fee87..5d47f518 100644 --- a/scripts/localdev/README.md +++ b/scripts/localdev/README.md @@ -31,5 +31,5 @@ The addresses of deployed contracts will be saved in: To run end to end tests against local development network: ``` -LONG_WAIT=0 SHORT_WAIT=0 npx mocha --require mocha-suppress-logs ../e2e/e2e.ts +AXELAR_API_URL=skip npx mocha --require mocha-suppress-logs ../e2e/e2e.ts ``` \ No newline at end of file From 56672af1fe9610970397acbc970313afe024ebd3 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Fri, 8 Dec 2023 12:11:33 +1000 Subject: [PATCH 016/155] Fix CI --- scripts/e2e/e2e.ts | 34 ++-------------------------------- scripts/helpers/helpers.ts | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/scripts/e2e/e2e.ts b/scripts/e2e/e2e.ts index d6a0ba13..4ef15db9 100644 --- a/scripts/e2e/e2e.ts +++ b/scripts/e2e/e2e.ts @@ -2,7 +2,7 @@ import * as dotenv from "dotenv"; dotenv.config(); import { ethers, providers } from "ethers"; -import { requireEnv, waitForReceipt, getFee, getContract, delay, getChildContracts, getRootContracts, saveChildContracts } from "../helpers/helpers"; +import { requireEnv, waitForReceipt, getFee, getContract, delay, getChildContracts, getRootContracts, saveChildContracts, waitUntilSucceed } from "../helpers/helpers"; import { expect } from "chai"; // The contract ABI of IMX on L1. @@ -388,34 +388,4 @@ describe("Bridge e2e test", () => { expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); }).timeout(2400000) -}) - -async function waitUntilSucceed(axelarURL: string, txHash: any) { - if (axelarURL == "skip") { - return; - } - console.log("Wait until succeed... tx hash: ", txHash) - let response; - let req = '{"method": "searchGMP", "txHash": "' + txHash + '"}' - while (true) { - response = await fetch(axelarURL, { - method: 'POST', - body: req, - headers: {'Content-Type': 'application/json; charset=UTF-8'} }); - if (!response.ok) {} - if (response.body !== null) { - const asString = new TextDecoder("utf-8").decode(await response.arrayBuffer()); - const asJSON = JSON.parse(asString); - if (asJSON.data[0] == undefined) { - console.log("Waiting for " + txHash + " to become available..."); - } else { - console.log("Current status of " + txHash + ": " + asJSON.data[0].status); - if (asJSON.data[0].status == "executed") { - console.log("Done"); - return; - } - } - } - await delay(60000); - } -} \ No newline at end of file +}) \ No newline at end of file diff --git a/scripts/helpers/helpers.ts b/scripts/helpers/helpers.ts index 10f238ba..6ad30056 100644 --- a/scripts/helpers/helpers.ts +++ b/scripts/helpers/helpers.ts @@ -154,4 +154,34 @@ export function getRootContracts() { export function saveRootContracts(contractData: any) { fs.writeFileSync(".root.bridge.contracts.json", JSON.stringify(contractData, null, 2)); +} + +export async function waitUntilSucceed(axelarURL: string, txHash: any) { + if (axelarURL == "skip") { + return; + } + console.log("Wait until succeed... tx hash: ", txHash) + let response; + let req = '{"method": "searchGMP", "txHash": "' + txHash + '"}' + while (true) { + response = await fetch(axelarURL, { + method: 'POST', + body: req, + headers: {'Content-Type': 'application/json; charset=UTF-8'} }); + if (!response.ok) {} + if (response.body !== null) { + const asString = new TextDecoder("utf-8").decode(await response.arrayBuffer()); + const asJSON = JSON.parse(asString); + if (asJSON.data[0] == undefined) { + console.log("Waiting for " + txHash + " to become available..."); + } else { + console.log("Current status of " + txHash + ": " + asJSON.data[0].status); + if (asJSON.data[0].status == "executed") { + console.log("Done"); + return; + } + } + } + await delay(60000); + } } \ No newline at end of file From ba2ea53e22e326bb80770d37198ed1bf0c6050b1 Mon Sep 17 00:00:00 2001 From: Craig M Date: Fri, 8 Dec 2023 15:45:00 +1300 Subject: [PATCH 017/155] added role to role function names --- src/common/AdaptorRoles.sol | 4 ++-- test/unit/common/AdaptorRoles.t.sol | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/common/AdaptorRoles.sol b/src/common/AdaptorRoles.sol index 18784f45..519fbf2e 100644 --- a/src/common/AdaptorRoles.sol +++ b/src/common/AdaptorRoles.sol @@ -24,14 +24,14 @@ abstract contract AdaptorRoles is AccessControlUpgradeable { /** * @notice Function to grant bridge manager role to an address */ - function grantBridgeManager(address account) external onlyRole(DEFAULT_ADMIN_ROLE) { + function grantBridgeManagerRole(address account) external onlyRole(DEFAULT_ADMIN_ROLE) { grantRole(BRIDGE_MANAGER_ROLE, account); } /** * @notice Function to grant gas service manager role to an address */ - function grantGasServiceManager(address account) external onlyRole(DEFAULT_ADMIN_ROLE) { + function grantGasServiceManagerRole(address account) external onlyRole(DEFAULT_ADMIN_ROLE) { grantRole(GAS_SERVICE_MANAGER_ROLE, account); } diff --git a/test/unit/common/AdaptorRoles.t.sol b/test/unit/common/AdaptorRoles.t.sol index 30760944..4ee88508 100644 --- a/test/unit/common/AdaptorRoles.t.sol +++ b/test/unit/common/AdaptorRoles.t.sol @@ -21,8 +21,8 @@ contract Setup is Test { contract AdaptorRoles is Setup { function grantRoles() internal { vm.startPrank(admin); - mockAdaptorRoles.grantBridgeManager(bridgeManager); - mockAdaptorRoles.grantGasServiceManager(gasServiceManager); + mockAdaptorRoles.grantBridgeManagerRole(bridgeManager); + mockAdaptorRoles.grantGasServiceManagerRole(gasServiceManager); mockAdaptorRoles.grantTargetManagerRole(targetManager); vm.stopPrank(); } From 8ae3a5c0c0e49727208a9d72606a2100ff504f9b Mon Sep 17 00:00:00 2001 From: Craig M Date: Fri, 8 Dec 2023 15:50:51 +1300 Subject: [PATCH 018/155] renamed _roles to unique name spaces --- src/child/ChildAxelarBridgeAdaptor.sol | 20 +++++++++++--------- src/root/RootAxelarBridgeAdaptor.sol | 19 ++++++++++--------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/child/ChildAxelarBridgeAdaptor.sol b/src/child/ChildAxelarBridgeAdaptor.sol index ebef9f72..87d54cd6 100644 --- a/src/child/ChildAxelarBridgeAdaptor.sol +++ b/src/child/ChildAxelarBridgeAdaptor.sol @@ -53,23 +53,25 @@ contract ChildAxelarBridgeAdaptor is /** * @notice Initialization function for ChildAxelarBridgeAdaptor. - * @param _roles Struct containing addresses of roles. + * @param _childAxelarAdapterRoles Struct containing addresses of roles. * @param _childBridge Address of child bridge contract. * @param _rootChainId Axelar's string ID for the root chain. * @param _rootBridgeAdaptor Address of the bridge adaptor on the root chain. * @param _gasService Address of Axelar Gas Service contract. */ function initialize( - InitializationRoles memory _roles, + InitializationRoles memory _childAxelarAdapterRoles, address _childBridge, string memory _rootChainId, string memory _rootBridgeAdaptor, address _gasService ) external initializer { if ( - _childBridge == address(0) || _gasService == address(0) || _roles.defaultAdmin == address(0) - || _roles.bridgeManager == address(0) || _roles.gasServiceManager == address(0) - || _roles.targetManager == address(0) + _childBridge == address(0) || _gasService == address(0) + || _childAxelarAdapterRoles.defaultAdmin == address(0) + || _childAxelarAdapterRoles.bridgeManager == address(0) + || _childAxelarAdapterRoles.gasServiceManager == address(0) + || _childAxelarAdapterRoles.targetManager == address(0) ) { revert ZeroAddress(); } @@ -83,10 +85,10 @@ contract ChildAxelarBridgeAdaptor is } __AccessControl_init(); - _grantRole(DEFAULT_ADMIN_ROLE, _roles.defaultAdmin); - _grantRole(BRIDGE_MANAGER_ROLE, _roles.bridgeManager); - _grantRole(GAS_SERVICE_MANAGER_ROLE, _roles.gasServiceManager); - _grantRole(TARGET_MANAGER_ROLE, _roles.targetManager); + _grantRole(DEFAULT_ADMIN_ROLE, _childAxelarAdapterRoles.defaultAdmin); + _grantRole(BRIDGE_MANAGER_ROLE, _childAxelarAdapterRoles.bridgeManager); + _grantRole(GAS_SERVICE_MANAGER_ROLE, _childAxelarAdapterRoles.gasServiceManager); + _grantRole(TARGET_MANAGER_ROLE, _childAxelarAdapterRoles.targetManager); childBridge = IChildERC20Bridge(_childBridge); rootChainId = _rootChainId; diff --git a/src/root/RootAxelarBridgeAdaptor.sol b/src/root/RootAxelarBridgeAdaptor.sol index 22266d82..f0c38bf3 100644 --- a/src/root/RootAxelarBridgeAdaptor.sol +++ b/src/root/RootAxelarBridgeAdaptor.sol @@ -54,23 +54,24 @@ contract RootAxelarBridgeAdaptor is /** * @notice Initialization function for RootAxelarBridgeAdaptor. - * @param _roles Struct containing addresses of roles. + * @param _rootAxelarAdapterRoles Struct containing addresses of roles. * @param _rootBridge Address of root bridge contract. * @param _childChainId Axelar's ID for the child chain. * @param _childBridgeAdaptor Address of the bridge adaptor on the child chain. * @param _gasService Address of Axelar Gas Service contract. */ function initialize( - InitializationRoles memory _roles, + InitializationRoles memory _rootAxelarAdapterRoles, address _rootBridge, string memory _childChainId, string memory _childBridgeAdaptor, address _gasService ) public initializer { if ( - _rootBridge == address(0) || _gasService == address(0) || _roles.defaultAdmin == address(0) - || _roles.bridgeManager == address(0) || _roles.gasServiceManager == address(0) - || _roles.targetManager == address(0) + _rootBridge == address(0) || _gasService == address(0) || _rootAxelarAdapterRoles.defaultAdmin == address(0) + || _rootAxelarAdapterRoles.bridgeManager == address(0) + || _rootAxelarAdapterRoles.gasServiceManager == address(0) + || _rootAxelarAdapterRoles.targetManager == address(0) ) { revert ZeroAddresses(); } @@ -85,10 +86,10 @@ contract RootAxelarBridgeAdaptor is __AccessControl_init(); - _grantRole(DEFAULT_ADMIN_ROLE, _roles.defaultAdmin); - _grantRole(BRIDGE_MANAGER_ROLE, _roles.bridgeManager); - _grantRole(GAS_SERVICE_MANAGER_ROLE, _roles.gasServiceManager); - _grantRole(TARGET_MANAGER_ROLE, _roles.targetManager); + _grantRole(DEFAULT_ADMIN_ROLE, _rootAxelarAdapterRoles.defaultAdmin); + _grantRole(BRIDGE_MANAGER_ROLE, _rootAxelarAdapterRoles.bridgeManager); + _grantRole(GAS_SERVICE_MANAGER_ROLE, _rootAxelarAdapterRoles.gasServiceManager); + _grantRole(TARGET_MANAGER_ROLE, _rootAxelarAdapterRoles.targetManager); rootBridge = IRootERC20Bridge(_rootBridge); childChainId = _childChainId; From e90aa439bb6b897ee31d40d82a6148265de3660f Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Fri, 8 Dec 2023 12:54:41 +1000 Subject: [PATCH 019/155] Update bootstrap --- scripts/bootstrap/.env.example | 4 ++++ scripts/bootstrap/0_pre_validation.ts | 12 +++++++++--- scripts/bootstrap/1_deployer_funding.ts | 23 +++++++++++++++++++++-- scripts/bootstrap/README.md | 4 ++++ scripts/deploy/.env.example | 4 ++++ scripts/deploy/README.md | 4 ++++ scripts/localdev/.env.local | 8 ++++++-- 7 files changed, 52 insertions(+), 7 deletions(-) diff --git a/scripts/bootstrap/.env.example b/scripts/bootstrap/.env.example index 00e96827..5c6944ed 100644 --- a/scripts/bootstrap/.env.example +++ b/scripts/bootstrap/.env.example @@ -31,12 +31,16 @@ ROOT_IMX_ADDR= ROOT_WETH_ADDR= ## The Axelar address to receive initial funding on the child chain. AXELAR_EOA= +## The passport nonce reserver +PASSPORT_NONCE_RESERVER_ADDR= ## The amount of fund Axelar requested, unit is in IMX or 10^18 Wei. AXELAR_FUND= ## The amount of fund deployer to be left with after bootstrapping on L2, unit is in IMX or 10^18 Wei. CHILD_DEPLOYER_FUND= ## The amount of fund nonce reserved deployer required on L2, unit is in IMX or 10^18 Wei. CHILD_NONCE_RESERVED_DEPLOYER_FUND= +## The amount of fund passport reserver required on L2, unit is in IMX or 10^18 Wei. +PASSPORT_NONCE_RESERVER_FUND= ## The maximum amount of IMX that can be deposited to L2, unit is in IMX or 10^18 Wei. IMX_DEPOSIT_LIMIT= ## The privileged transaction Multisig address on the root chain. diff --git a/scripts/bootstrap/0_pre_validation.ts b/scripts/bootstrap/0_pre_validation.ts index 23cd044d..7b717d6d 100644 --- a/scripts/bootstrap/0_pre_validation.ts +++ b/scripts/bootstrap/0_pre_validation.ts @@ -37,9 +37,11 @@ async function run() { let rootIMXAddr = requireEnv("ROOT_IMX_ADDR"); let rootWETHAddr = requireEnv("ROOT_WETH_ADDR"); let axelarEOA = requireEnv("AXELAR_EOA"); + let passportDeployer = requireEnv("PASSPORT_NONCE_RESERVER_ADDR"); let axelarFund = requireEnv("AXELAR_FUND"); let childDeployerFund = requireEnv("CHILD_DEPLOYER_FUND"); let childReservedDeployerFund = requireEnv("CHILD_NONCE_RESERVED_DEPLOYER_FUND"); + let passportDeployerFund = requireEnv("PASSPORT_NONCE_RESERVER_FUND"); let imxDepositLimit = requireEnv("IMX_DEPOSIT_LIMIT"); requireEnv("RATE_LIMIT_IMX_CAPACITY"); requireEnv("RATE_LIMIT_IMX_REFILL_RATE"); @@ -98,7 +100,7 @@ async function run() { } // Check duplicates - if (hasDuplicates([actualDeployerAddress, actualReservedDeployerAddress, axelarEOA])) { + if (hasDuplicates([actualDeployerAddress, actualReservedDeployerAddress, axelarEOA, passportDeployer])) { throw("Duplicate address detected!"); } if (hasDuplicates([rootIMXAddr, rootWETHAddr])) { @@ -119,14 +121,18 @@ async function run() { let axelarRequiredIMX = ethers.utils.parseEther(axelarFund); let deployerRequiredIMX = ethers.utils.parseEther(childDeployerFund); let reservedDeployerRequiredIMX = ethers.utils.parseEther(childReservedDeployerFund); + let passportRequiredIMX = ethers.utils.parseEther(passportDeployerFund); if (axelarRequiredIMX.lt(ethers.utils.parseEther("500.0"))) { tryThrow("Axelar on child chain should request at least 500 IMX, got" + ethers.utils.formatEther(axelarRequiredIMX)); } if (deployerRequiredIMX.lt(ethers.utils.parseEther("250.0"))) { tryThrow("Deployer on child chain should request at least 500 IMX, got" + ethers.utils.formatEther(deployerRequiredIMX)); } - if (reservedDeployerRequiredIMX.lt(ethers.utils.parseEther("250.0"))) { - tryThrow("Reserved deployer on child chain should request at least 10 IMX, got" + ethers.utils.formatEther(reservedDeployerRequiredIMX)); + if (reservedDeployerRequiredIMX.lt(ethers.utils.parseEther("100.0"))) { + tryThrow("Reserved deployer on child chain should request at least 100 IMX, got" + ethers.utils.formatEther(reservedDeployerRequiredIMX)); + } + if (passportRequiredIMX.lt(ethers.utils.parseEther("100.0"))) { + tryThrow("Passport deployer on child chain should request at least 100 IMX, got" + ethers.utils.formatEther(passportRequiredIMX)); } let extraIMX = ethers.utils.parseEther("100.0"); let requiredIMX = axelarRequiredIMX.add(deployerRequiredIMX).add(reservedDeployerRequiredIMX).add(extraIMX); diff --git a/scripts/bootstrap/1_deployer_funding.ts b/scripts/bootstrap/1_deployer_funding.ts index a6a9aaca..08890323 100644 --- a/scripts/bootstrap/1_deployer_funding.ts +++ b/scripts/bootstrap/1_deployer_funding.ts @@ -15,7 +15,9 @@ async function run() { let reservedDeployerAddr = requireEnv("NONCE_RESERVED_DEPLOYER_ADDR"); let reservedDeployerFund = requireEnv("CHILD_NONCE_RESERVED_DEPLOYER_FUND"); let axelarEOA = requireEnv("AXELAR_EOA"); + let passportDeployer = requireEnv("PASSPORT_NONCE_RESERVER_ADDR"); let axelarFund = requireEnv("AXELAR_FUND"); + let passportDeployerFund = requireEnv("PASSPORT_NONCE_RESERVER_FUND"); // Get deployer address const childProvider = new ethers.providers.JsonRpcProvider(childRPCURL, Number(childChainID)); @@ -31,14 +33,15 @@ async function run() { console.log("Deployer address is: ", deployerAddr); // Check duplicates - if (hasDuplicates([deployerAddr, axelarEOA, reservedDeployerAddr])) { + if (hasDuplicates([deployerAddr, axelarEOA, reservedDeployerAddr, passportDeployer])) { throw("Duplicate address detected!"); } // Execute console.log("Nonce reserved deployer now has: ", ethers.utils.formatEther(await childProvider.getBalance(reservedDeployerAddr))); console.log("Axelar EOA now has: ", ethers.utils.formatEther(await childProvider.getBalance(axelarEOA))); - console.log("Fund Axelar and deployer on child chain in..."); + console.log("Passport deployer now has: ", ethers.utils.formatEther(await childProvider.getBalance(passportDeployer))); + console.log("Fund Axelar, deployers on child chain in..."); await waitForConfirmation(); if ((await childProvider.getBalance(reservedDeployerAddr)).gte(ethers.utils.parseEther(reservedDeployerFund))) { @@ -71,9 +74,25 @@ async function run() { await waitForReceipt(resp.hash, childProvider); } + if ((await childProvider.getBalance(passportDeployer)).gte(ethers.utils.parseEther(passportDeployerFund))) { + console.log("Passport deployer has already got requested amount, skip."); + } else { + let [priorityFee, maxFee] = await getFee(childProvider); + console.log("Transfer value to Passport deployer..."); + let resp = await childDeployerWallet.sendTransaction({ + to: passportDeployer, + value: ethers.utils.parseEther(passportDeployerFund), + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }) + console.log("Transaction submitted: " + JSON.stringify(resp, null, 2)); + await waitForReceipt(resp.hash, childProvider); + } + // Print target balance console.log("Nonce reserved deployer now has: ", ethers.utils.formatEther(await childProvider.getBalance(reservedDeployerAddr))); console.log("Axelar EOA now has: ", ethers.utils.formatEther(await childProvider.getBalance(axelarEOA))); + console.log("Passport deployer now has: ", ethers.utils.formatEther(await childProvider.getBalance(passportDeployer))); console.log("=======End Deployer Funding======="); } diff --git a/scripts/bootstrap/README.md b/scripts/bootstrap/README.md index 6113ca4c..40f68e72 100644 --- a/scripts/bootstrap/README.md +++ b/scripts/bootstrap/README.md @@ -55,12 +55,16 @@ ROOT_IMX_ADDR= ROOT_WETH_ADDR= ## The Axelar address to receive initial funding on the child chain. AXELAR_EOA= +## The passport nonce reserver +PASSPORT_NONCE_RESERVER_ADDR= ## The amount of fund Axelar requested, unit is in IMX or 10^18 Wei. AXELAR_FUND= ## The amount of fund deployer to be left with after bootstrapping on L2, unit is in IMX or 10^18 Wei. CHILD_DEPLOYER_FUND= ## The amount of fund nonce reserved deployer required on L2, unit is in IMX or 10^18 Wei. CHILD_NONCE_RESERVED_DEPLOYER_FUND= +## The amount of fund passport reserver required on L2, unit is in IMX or 10^18 Wei. +PASSPORT_NONCE_RESERVER_FUND= ## The maximum amount of IMX that can be deposited to L2, unit is in IMX or 10^18 Wei. IMX_DEPOSIT_LIMIT= ## The privileged transaction Multisig address on the root chain. diff --git a/scripts/deploy/.env.example b/scripts/deploy/.env.example index 2fa71b06..d58b72f7 100644 --- a/scripts/deploy/.env.example +++ b/scripts/deploy/.env.example @@ -30,12 +30,16 @@ ROOT_IMX_ADDR= ROOT_WETH_ADDR= ## The Axelar address to receive initial funding on the child chain. AXELAR_EOA= +## The passport nonce reserver +PASSPORT_NONCE_RESERVER_ADDR= ## The amount of fund Axelar requested, unit is in IMX or 10^18 Wei. AXELAR_FUND= ## The amount of fund deployer to be left with after bootstrapping on L2, unit is in IMX or 10^18 Wei. CHILD_DEPLOYER_FUND= ## The amount of fund nonce reserved deployer required on L2, unit is in IMX or 10^18 Wei. CHILD_NONCE_RESERVED_DEPLOYER_FUND= +## The amount of fund passport reserver required on L2, unit is in IMX or 10^18 Wei. +PASSPORT_NONCE_RESERVER_FUND= ## The maximum amount of IMX that can be deposited to L2, unit is in IMX or 10^18 Wei. IMX_DEPOSIT_LIMIT= ## The privileged transaction Multisig address on the root chain. diff --git a/scripts/deploy/README.md b/scripts/deploy/README.md index 3c38f3f6..75789ae3 100644 --- a/scripts/deploy/README.md +++ b/scripts/deploy/README.md @@ -44,12 +44,16 @@ ROOT_IMX_ADDR= ROOT_WETH_ADDR= ## The Axelar address to receive initial funding on the child chain. AXELAR_EOA= +## The passport nonce reserver +PASSPORT_NONCE_RESERVER_ADDR= ## The amount of fund Axelar requested, unit is in IMX or 10^18 Wei. AXELAR_FUND= ## The amount of fund deployer to be left with after bootstrapping on L2, unit is in IMX or 10^18 Wei. CHILD_DEPLOYER_FUND= ## The amount of fund nonce reserved deployer required on L2, unit is in IMX or 10^18 Wei. CHILD_NONCE_RESERVED_DEPLOYER_FUND= +## The amount of fund passport reserver required on L2, unit is in IMX or 10^18 Wei. +PASSPORT_NONCE_RESERVER_FUND= ## The maximum amount of IMX that can be deposited to L2, unit is in IMX or 10^18 Wei. IMX_DEPOSIT_LIMIT= ## The privileged transaction Multisig address on the root chain. diff --git a/scripts/localdev/.env.local b/scripts/localdev/.env.local index 9b4c5fbd..da35f139 100644 --- a/scripts/localdev/.env.local +++ b/scripts/localdev/.env.local @@ -31,12 +31,16 @@ ROOT_IMX_ADDR=0x73511669fd4dE447feD18BB79bAFeAC93aB7F31f ROOT_WETH_ADDR=0xB581C9264f59BF0289fA76D61B2D0746dCE3C30D ## The Axelar address to receive initial funding on the child chain. AXELAR_EOA=0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC +## The passport nonce reserver +PASSPORT_NONCE_RESERVER_ADDR=0xa0Ee7A142d267C1f36714E4a8F75612F20a79720 ## The amount of fund Axelar requested, unit is in IMX or 10^18 Wei. AXELAR_FUND=500 ## The amount of fund deployer to be left with after bootstrapping on L2, unit is in IMX or 10^18 Wei. -CHILD_DEPLOYER_FUND=250 +CHILD_DEPLOYER_FUND=200 ## The amount of fund nonce reserved deployer required on L2, unit is in IMX or 10^18 Wei. -CHILD_NONCE_RESERVED_DEPLOYER_FUND=250 +CHILD_NONCE_RESERVED_DEPLOYER_FUND=200 +## The amount of fund passport reserver required on L2, unit is in IMX or 10^18 Wei. +PASSPORT_NONCE_RESERVER_FUND=100 ## The maximum amount of IMX that can be deposited to L2, unit is in IMX or 10^18 Wei. IMX_DEPOSIT_LIMIT=100000000 ## The privileged transaction multisig address on the root chain. From 842a0f14fdc249b3fb93fef6c53035572bfdc53e Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Mon, 11 Dec 2023 08:22:01 +1100 Subject: [PATCH 020/155] Conform WETH withdraw to official implementation --- src/lib/WETH.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/WETH.sol b/src/lib/WETH.sol index 4bdf6c15..88c5d5f9 100644 --- a/src/lib/WETH.sol +++ b/src/lib/WETH.sol @@ -39,7 +39,7 @@ contract WETH is IWETH { require(balanceOf[msg.sender] >= wad, "Wrapped ETH: Insufficient balance"); balanceOf[msg.sender] -= wad; - Address.sendValue(payable(msg.sender), wad); + payable(msg.sender).transfer(wad); emit Withdrawal(msg.sender, wad); } From 88f93746f9f172873cfd8a94b9c5482c01dac0df Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Mon, 11 Dec 2023 08:22:47 +1100 Subject: [PATCH 021/155] Remove redundant pause guard in receive fn --- src/root/RootERC20Bridge.sol | 2 +- test/unit/root/RootERC20Bridge.t.sol | 30 ---------------------------- 2 files changed, 1 insertion(+), 31 deletions(-) diff --git a/src/root/RootERC20Bridge.sol b/src/root/RootERC20Bridge.sol index 1f6cb5dd..4bf78d9b 100644 --- a/src/root/RootERC20Bridge.sol +++ b/src/root/RootERC20Bridge.sol @@ -230,7 +230,7 @@ contract RootERC20Bridge is * The unwrapping is done through the WETH contract's `withdraw()` function, which sends the native ETH to this bridge contract. * The only reason this `receive()` function is needed is for this process, hence the validation ensures that the sender is the WETH contract. */ - receive() external payable whenNotPaused { + receive() external payable { // Revert if sender is not the WETH token address if (msg.sender != rootWETHToken) { revert NonWrappedNativeTransfer(); diff --git a/test/unit/root/RootERC20Bridge.t.sol b/test/unit/root/RootERC20Bridge.t.sol index 586cef74..d1c0469f 100644 --- a/test/unit/root/RootERC20Bridge.t.sol +++ b/test/unit/root/RootERC20Bridge.t.sol @@ -97,42 +97,12 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid assert(rootBridge.rootTokenToChildToken(NATIVE_ETH) != address(0)); } - function test_NativeTransferFromWETH() public { - address caller = address(0x123a); - payable(caller).transfer(2 ether); - // forge inspect src/root/RootERC20Bridge.sol:RootERC20Bridge storageLayout | grep -B3 -A5 -i "rootWETHToken" - uint256 wETHStorageSlot = 307; - vm.store(address(rootBridge), bytes32(wETHStorageSlot), bytes32(uint256(uint160(caller)))); - - vm.startPrank(caller); - uint256 bal = address(rootBridge).balance; - (bool ok,) = address(rootBridge).call{value: 1 ether}(""); - assert(ok); - uint256 postBal = address(rootBridge).balance; - - assertEq(bal + 1 ether, postBal, "balance not increased"); - } - function test_RevertI_fNativeTransferIsFromNonWETH() public { vm.expectRevert(NonWrappedNativeTransfer.selector); (bool ok,) = address(rootBridge).call{value: 1 ether}(""); assert(ok); } - function test_RevertIf_NativeTransferWhenPaused() public { - pause(IPausable(address(rootBridge))); - vm.expectRevert("Pausable: paused"); - (bool ok,) = address(rootBridge).call{value: 1 ether}(""); - assert(ok); - } - - function test_NativeTransferResumesFunctionalityAfterUnpausing() public { - test_RevertIf_NativeTransferWhenPaused(); - unpause(IPausable(address(rootBridge))); - // Expect success case to pass - test_NativeTransferFromWETH(); - } - function test_RevertIf_InitializeTwice() public { IRootERC20Bridge.InitializationRoles memory roles = IRootERC20Bridge.InitializationRoles({ defaultAdmin: address(this), From bcbb845d13dee617e3f7e7a3ca80e92526e3053e Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Mon, 11 Dec 2023 09:51:43 +0800 Subject: [PATCH 022/155] Try catch to revert with custom error --- src/child/ChildERC20Bridge.sol | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/child/ChildERC20Bridge.sol b/src/child/ChildERC20Bridge.sol index 06e3051b..ceff0ed7 100644 --- a/src/child/ChildERC20Bridge.sol +++ b/src/child/ChildERC20Bridge.sol @@ -314,9 +314,12 @@ contract ChildERC20Bridge is * @notice Private function to handle withdrawal of L1 native ETH. */ function _withdrawETH(uint256 amount) private returns (address) { - if (!IChildERC20(childETHToken).burn(msg.sender, amount)) { + try IChildERC20(childETHToken).burn(msg.sender, amount) returns (bool success) { + if (!success) revert BurnFailed(); + } catch { revert BurnFailed(); } + return NATIVE_ETH; } @@ -330,7 +333,9 @@ contract ChildERC20Bridge is IWIMX wIMX = IWIMX(wIMXToken); // Transfer to contract - if (!wIMX.transferFrom(msg.sender, address(this), amount)) { + try wIMX.transferFrom(msg.sender, address(this), amount) returns (bool success) { + if (!success) revert TransferWIMXFailed(); + } catch { revert TransferWIMXFailed(); } @@ -372,7 +377,9 @@ contract ChildERC20Bridge is } // Burn tokens - if (!IChildERC20(childToken).burn(msg.sender, amount)) { + try IChildERC20(childToken).burn(msg.sender, amount) returns (bool success) { + if (!success) revert BurnFailed(); + } catch { revert BurnFailed(); } @@ -486,7 +493,9 @@ contract ChildERC20Bridge is revert EmptyTokenContract(); } - if (!IChildERC20(childToken).mint(receiver, amount)) { + try IChildERC20(childToken).mint(receiver, amount) returns (bool success) { + if (!success) revert MintFailed(); + } catch { revert MintFailed(); } From f4ad658bd4418f303e5421f746210956a6ab4324 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Mon, 11 Dec 2023 10:05:15 +0800 Subject: [PATCH 023/155] Fix tests --- test/unit/child/ChildERC20Bridge.t.sol | 2 +- test/unit/child/withdrawals/ChildERC20BridgeWithdrawETH.t.sol | 2 +- .../child/withdrawals/ChildERC20BridgeWithdrawETHTo.t.sol | 2 +- .../unit/child/withdrawals/ChildERC20BridgeWithdrawWIMX.t.sol | 4 ++-- .../child/withdrawals/ChildERC20BridgeWithdrawWIMXTo.t.sol | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/unit/child/ChildERC20Bridge.t.sol b/test/unit/child/ChildERC20Bridge.t.sol index 85386706..1b1db243 100644 --- a/test/unit/child/ChildERC20Bridge.t.sol +++ b/test/unit/child/ChildERC20Bridge.t.sol @@ -628,7 +628,7 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B changePrank(attacker); // Execute withdraw - vm.expectRevert("ReentrancyGuard: reentrant call"); + vm.expectRevert(BurnFailed.selector); childBridge.withdraw{value: 1 ether}(ChildERC20(address(attackToken)), 100); } } diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETH.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETH.t.sol index 92680ec5..7c88aa08 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETH.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETH.t.sol @@ -74,7 +74,7 @@ contract ChildERC20BridgeWithdrawETHUnitTest is Test, IChildERC20BridgeEvents, I uint256 withdrawAmount = 101 ether; uint256 withdrawFee = 300; - vm.expectRevert(bytes("ERC20: burn amount exceeds balance")); + vm.expectRevert(BurnFailed.selector); childBridge.withdrawETH{value: withdrawFee}(withdrawAmount); } diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETHTo.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETHTo.t.sol index 7c474695..d2589b54 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETHTo.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETHTo.t.sol @@ -94,7 +94,7 @@ contract ChildERC20BridgeWithdrawETHToUnitTest is Test, IChildERC20BridgeEvents, uint256 withdrawAmount = 101 ether; uint256 withdrawFee = 300; - vm.expectRevert(bytes("ERC20: burn amount exceeds balance")); + vm.expectRevert(BurnFailed.selector); childBridge.withdrawETHTo{value: withdrawFee}(address(this), withdrawAmount); } diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMX.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMX.t.sol index a14fb6a1..6e30a934 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMX.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMX.t.sol @@ -74,7 +74,7 @@ contract ChildERC20BridgeWithdrawWIMXUnitTest is Test, IChildERC20BridgeEvents, uint256 withdrawFee = 300; wIMXToken.approve(address(childBridge), withdrawAmount); - vm.expectRevert(bytes("Wrapped IMX: Insufficient balance")); + vm.expectRevert(TransferWIMXFailed.selector); childBridge.withdrawWIMX{value: withdrawFee}(withdrawAmount); } @@ -83,7 +83,7 @@ contract ChildERC20BridgeWithdrawWIMXUnitTest is Test, IChildERC20BridgeEvents, uint256 withdrawFee = 300; wIMXToken.approve(address(childBridge), withdrawAmount - 1); - vm.expectRevert(bytes("Wrapped IMX: Insufficient allowance")); + vm.expectRevert(TransferWIMXFailed.selector); childBridge.withdrawWIMX{value: withdrawFee}(withdrawAmount); } diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMXTo.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMXTo.t.sol index 0ffe48c7..02c10e95 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMXTo.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMXTo.t.sol @@ -83,7 +83,7 @@ contract ChildERC20BridgeWithdrawWIMXToUnitTest is Test, IChildERC20BridgeEvents uint256 withdrawFee = 300; wIMXToken.approve(address(childBridge), withdrawAmount); - vm.expectRevert(bytes("Wrapped IMX: Insufficient balance")); + vm.expectRevert(TransferWIMXFailed.selector); childBridge.withdrawWIMXTo{value: withdrawFee}(address(this), withdrawAmount); } @@ -92,7 +92,7 @@ contract ChildERC20BridgeWithdrawWIMXToUnitTest is Test, IChildERC20BridgeEvents uint256 withdrawFee = 300; wIMXToken.approve(address(childBridge), withdrawAmount - 1); - vm.expectRevert(bytes("Wrapped IMX: Insufficient allowance")); + vm.expectRevert(TransferWIMXFailed.selector); childBridge.withdrawWIMXTo{value: withdrawFee}(address(this), withdrawAmount); } From 8ed3ec732941db6b0bb562f8261fcdd2918cbfeb Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Mon, 11 Dec 2023 13:34:03 +1100 Subject: [PATCH 024/155] Disable call to executeWithToken on adaptors --- src/child/ChildAxelarBridgeAdaptor.sol | 13 +++++++++++++ .../child/IChildAxelarBridgeAdaptor.sol | 2 ++ .../root/IRootAxelarBridgeAdaptor.sol | 2 ++ src/root/RootAxelarBridgeAdaptor.sol | 13 +++++++++++++ test/mocks/child/MockChildAxelarGateway.sol | 8 ++++++++ test/mocks/root/MockAxelarGateway.sol | 8 ++++++++ test/unit/child/ChildAxelarBridgeAdaptor.t.sol | 15 +++++++++++++++ test/unit/root/RootAxelarBridgeAdaptor.t.sol | 17 +++++++++++++++++ 8 files changed, 78 insertions(+) diff --git a/src/child/ChildAxelarBridgeAdaptor.sol b/src/child/ChildAxelarBridgeAdaptor.sol index ebef9f72..4636697c 100644 --- a/src/child/ChildAxelarBridgeAdaptor.sol +++ b/src/child/ChildAxelarBridgeAdaptor.sol @@ -197,6 +197,19 @@ contract ChildAxelarBridgeAdaptor is childBridge.onMessageReceive(_payload); } + /** + * @inheritdoc AxelarExecutable + * @dev This function is called by the parent `AxelarExecutable` contract's `executeWithToken()` function. + * However, this function is not required for the bridge, and thus reverts with an `UnsupportedOperation` error. + */ + function _executeWithToken(string calldata, string calldata, bytes calldata, string calldata, uint256) + internal + pure + override + { + revert UnsupportedOperation(); + } + // slither-disable-next-line unused-state,naming-convention uint256[50] private __gapChildAxelarBridgeAdaptor; } diff --git a/src/interfaces/child/IChildAxelarBridgeAdaptor.sol b/src/interfaces/child/IChildAxelarBridgeAdaptor.sol index ce565ab0..9198d586 100644 --- a/src/interfaces/child/IChildAxelarBridgeAdaptor.sol +++ b/src/interfaces/child/IChildAxelarBridgeAdaptor.sol @@ -71,6 +71,8 @@ interface IChildAxelarBridgeAdaptorErrors { error InvalidSourceChain(); /// @notice Error when the source chain's message sender is not a recognised address. error InvalidSourceAddress(); + /// @notice Error when a function that isn't supported by the adaptor is called. + error UnsupportedOperation(); } /** diff --git a/src/interfaces/root/IRootAxelarBridgeAdaptor.sol b/src/interfaces/root/IRootAxelarBridgeAdaptor.sol index e789a555..eb85bc50 100644 --- a/src/interfaces/root/IRootAxelarBridgeAdaptor.sol +++ b/src/interfaces/root/IRootAxelarBridgeAdaptor.sol @@ -75,6 +75,8 @@ interface IRootAxelarBridgeAdaptorErrors { error InvalidSourceAddress(); /// @notice Error when a message received has invalid source chain. error InvalidSourceChain(); + /// @notice Error when a function that isn't supported by the adaptor is called. + error UnsupportedOperation(); } /** diff --git a/src/root/RootAxelarBridgeAdaptor.sol b/src/root/RootAxelarBridgeAdaptor.sol index 22266d82..70356d5c 100644 --- a/src/root/RootAxelarBridgeAdaptor.sol +++ b/src/root/RootAxelarBridgeAdaptor.sol @@ -198,6 +198,19 @@ contract RootAxelarBridgeAdaptor is rootBridge.onMessageReceive(_payload); } + /** + * @inheritdoc AxelarExecutable + * @dev This function is called by the parent `AxelarExecutable` contract's `executeWithToken()` function. + * However, this function is not required for the bridge, and thus reverts with an `UnsupportedOperation` error. + */ + function _executeWithToken(string calldata, string calldata, bytes calldata, string calldata, uint256) + internal + pure + override + { + revert UnsupportedOperation(); + } + // slither-disable-next-line unused-state,naming-convention uint256[50] private __gapRootAxelarBridgeAdaptor; } diff --git a/test/mocks/child/MockChildAxelarGateway.sol b/test/mocks/child/MockChildAxelarGateway.sol index 15408557..7ec086b7 100644 --- a/test/mocks/child/MockChildAxelarGateway.sol +++ b/test/mocks/child/MockChildAxelarGateway.sol @@ -6,5 +6,13 @@ contract MockChildAxelarGateway { return true; } + function validateContractCallAndMint(bytes32, string calldata, string calldata, bytes32, string calldata, uint256) + external + pure + returns (bool) + { + return true; + } + function callContract(string memory childChain, string memory childBridgeAdaptor, bytes memory payload) external {} } diff --git a/test/mocks/root/MockAxelarGateway.sol b/test/mocks/root/MockAxelarGateway.sol index 2d3328d5..6c42e433 100644 --- a/test/mocks/root/MockAxelarGateway.sol +++ b/test/mocks/root/MockAxelarGateway.sol @@ -8,4 +8,12 @@ contract MockAxelarGateway { function validateContractCall(bytes32, string calldata, string calldata, bytes32) external pure returns (bool) { return true; } + + function validateContractCallAndMint(bytes32, string calldata, string calldata, bytes32, string calldata, uint256) + external + pure + returns (bool) + { + return true; + } } diff --git a/test/unit/child/ChildAxelarBridgeAdaptor.t.sol b/test/unit/child/ChildAxelarBridgeAdaptor.t.sol index 94e505e8..be279c4c 100644 --- a/test/unit/child/ChildAxelarBridgeAdaptor.t.sol +++ b/test/unit/child/ChildAxelarBridgeAdaptor.t.sol @@ -489,4 +489,19 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro axelarAdaptor.updateGasService(address(0)); vm.stopPrank(); } + + /** + * UNSUPPORTED OPERATION + */ + + /// Check that executeWithToken function in AxelarExecutable cannot be called + function test_RevertIf_executeWithTokenCalled() public { + bytes32 commandId = bytes32("testCommandId"); + bytes memory payload = abi.encodePacked("payload"); + string memory tokenSymbol = "TST"; + uint256 amount = 100; + + vm.expectRevert(UnsupportedOperation.selector); + axelarAdaptor.executeWithToken(commandId, ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, payload, tokenSymbol, amount); + } } diff --git a/test/unit/root/RootAxelarBridgeAdaptor.t.sol b/test/unit/root/RootAxelarBridgeAdaptor.t.sol index 1ac939ef..c80e0d2d 100644 --- a/test/unit/root/RootAxelarBridgeAdaptor.t.sol +++ b/test/unit/root/RootAxelarBridgeAdaptor.t.sol @@ -450,4 +450,21 @@ contract RootAxelarBridgeAdaptorTest is Test, IRootAxelarBridgeAdaptorEvents, IR vm.expectRevert(ZeroAddresses.selector); axelarAdaptor.updateGasService(address(0)); } + + /** + * UNSUPPORTED OPERATION + */ + + /// Check that executeWithToken function in AxelarExecutable cannot be called + function test_RevertIf_executeWithTokenCalled() public { + bytes32 commandId = bytes32("testCommandId"); + bytes memory payload = abi.encodePacked("payload"); + string memory tokenSymbol = "TST"; + uint256 amount = 100; + + vm.expectRevert(UnsupportedOperation.selector); + axelarAdaptor.executeWithToken( + commandId, CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, payload, tokenSymbol, amount + ); + } } From d0bdd2d1c88caf3abfc424ed079189a5fc2445dc Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Mon, 11 Dec 2023 10:53:14 +0800 Subject: [PATCH 025/155] Detect unsupported token --- src/interfaces/root/IRootERC20Bridge.sol | 2 ++ src/root/RootERC20Bridge.sol | 25 ++++++++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/interfaces/root/IRootERC20Bridge.sol b/src/interfaces/root/IRootERC20Bridge.sol index cd0ea05d..798405cc 100644 --- a/src/interfaces/root/IRootERC20Bridge.sol +++ b/src/interfaces/root/IRootERC20Bridge.sol @@ -189,4 +189,6 @@ interface IRootERC20BridgeErrors { error ImxDepositLimitTooLow(); /// @notice Error when native transfer is sent to contract from non wrapped-token address. error NonWrappedNativeTransfer(); + /// @notice Error when attempt to map a ERC20 token that doesn't support name(), symbol() or decimals(). + error NotSupportedToken(); } diff --git a/src/root/RootERC20Bridge.sol b/src/root/RootERC20Bridge.sol index 1f6cb5dd..dcce6be7 100644 --- a/src/root/RootERC20Bridge.sol +++ b/src/root/RootERC20Bridge.sol @@ -373,8 +373,29 @@ contract RootERC20Bridge is rootTokenToChildToken[address(rootToken)] = childToken; - bytes memory payload = - abi.encode(MAP_TOKEN_SIG, rootToken, rootToken.name(), rootToken.symbol(), rootToken.decimals()); + string memory tokenName; + string memory tokenSymbol; + uint8 tokenDecimals; + + try rootToken.name() returns (string memory name) { + tokenName = name; + } catch { + revert NotSupportedToken(); + } + + try rootToken.symbol() returns (string memory symbol) { + tokenSymbol = symbol; + } catch { + revert NotSupportedToken(); + } + + try rootToken.decimals() returns (uint8 decimals) { + tokenDecimals = decimals; + } catch { + revert NotSupportedToken(); + } + + bytes memory payload = abi.encode(MAP_TOKEN_SIG, rootToken, tokenName, tokenSymbol, tokenDecimals); rootBridgeAdaptor.sendMessage{value: msg.value}(payload, msg.sender); emit L1TokenMapped(address(rootToken), childToken); From f7622714ad3e7091478cf98abedae17146a24ec8 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Mon, 11 Dec 2023 12:21:11 +0800 Subject: [PATCH 026/155] Update tests --- test/unit/root/RootERC20Bridge.t.sol | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/unit/root/RootERC20Bridge.t.sol b/test/unit/root/RootERC20Bridge.t.sol index 586cef74..fb4e7946 100644 --- a/test/unit/root/RootERC20Bridge.t.sol +++ b/test/unit/root/RootERC20Bridge.t.sol @@ -441,6 +441,32 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid rootBridge.mapToken{value: 300}(IERC20Metadata(NATIVE_ETH)); } + function test_SucceedIf_mapTokenWithSupportedMethods() public { + rootBridge.mapToken{value: 300}(token); + } + + function test_RevertIf_mapTokenWithoutName() public { + vm.mockCallRevert(address(token), abi.encodeWithSelector(IERC20Metadata.name.selector), "Unsupported operation"); + vm.expectRevert(NotSupportedToken.selector); + rootBridge.mapToken{value: 300}(token); + } + + function test_RevertIf_mapTokenWithoutSymbol() public { + vm.mockCallRevert( + address(token), abi.encodeWithSelector(IERC20Metadata.symbol.selector), "Unsupported operation" + ); + vm.expectRevert(NotSupportedToken.selector); + rootBridge.mapToken{value: 300}(token); + } + + function test_RevertIf_mapTokenWithoutDecimals() public { + vm.mockCallRevert( + address(token), abi.encodeWithSelector(IERC20Metadata.decimals.selector), "Unsupported operation" + ); + vm.expectRevert(NotSupportedToken.selector); + rootBridge.mapToken{value: 300}(token); + } + function test_updateRootBridgeAdaptor_UpdatesRootBridgeAdaptor() public { address newAdaptorAddress = address(0x11111); From 0e7eaa5f34aa62846fb559fca00c7854305b7631 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Mon, 11 Dec 2023 15:53:14 +0800 Subject: [PATCH 027/155] Add authorized initializer --- src/child/ChildAxelarBridgeAdaptor.sol | 10 +++++++++- src/child/ChildERC20Bridge.sol | 9 +++++++++ src/interfaces/child/IChildAxelarBridgeAdaptor.sol | 2 ++ src/interfaces/child/IChildERC20Bridge.sol | 2 ++ src/interfaces/root/IRootAxelarBridgeAdaptor.sol | 2 ++ src/interfaces/root/IRootERC20Bridge.sol | 2 ++ src/root/RootAxelarBridgeAdaptor.sol | 10 +++++++++- src/root/RootERC20Bridge.sol | 9 +++++++++ src/root/flowrate/RootERC20BridgeFlowRate.sol | 2 ++ 9 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/child/ChildAxelarBridgeAdaptor.sol b/src/child/ChildAxelarBridgeAdaptor.sol index ebef9f72..9fa9dd13 100644 --- a/src/child/ChildAxelarBridgeAdaptor.sol +++ b/src/child/ChildAxelarBridgeAdaptor.sol @@ -49,7 +49,12 @@ contract ChildAxelarBridgeAdaptor is IAxelarGasService public gasService; - constructor(address _gateway) AxelarExecutable(_gateway) {} + /// @notice Address of the authorized initializer. + address public immutable initializerAddress; + + constructor(address _gateway) AxelarExecutable(_gateway) { + initializerAddress = msg.sender; + } /** * @notice Initialization function for ChildAxelarBridgeAdaptor. @@ -66,6 +71,9 @@ contract ChildAxelarBridgeAdaptor is string memory _rootBridgeAdaptor, address _gasService ) external initializer { + if (msg.sender != initializerAddress) { + revert UnauthorizedInitializer(); + } if ( _childBridge == address(0) || _gasService == address(0) || _roles.defaultAdmin == address(0) || _roles.bridgeManager == address(0) || _roles.gasServiceManager == address(0) diff --git a/src/child/ChildERC20Bridge.sol b/src/child/ChildERC20Bridge.sol index 06e3051b..b5948576 100644 --- a/src/child/ChildERC20Bridge.sol +++ b/src/child/ChildERC20Bridge.sol @@ -70,6 +70,8 @@ contract ChildERC20Bridge is address public childETHToken; /// @dev The address of the wrapped IMX token on L2. address public wIMXToken; + /// @dev Address of the authorized initializer. + address public immutable initializerAddress; /** * @notice Modifier to ensure that the caller is the registered child bridge adaptor. @@ -81,6 +83,10 @@ contract ChildERC20Bridge is _; } + constructor() { + initializerAddress = msg.sender; + } + /** * @notice Initialization function for ChildERC20Bridge. * @param newRoles Struct containing addresses of roles. @@ -97,6 +103,9 @@ contract ChildERC20Bridge is address newRootIMXToken, address newWIMXToken ) public initializer { + if (msg.sender != initializerAddress) { + revert UnauthorizedInitializer(); + } if ( newBridgeAdaptor == address(0) || newChildTokenTemplate == address(0) || newRootIMXToken == address(0) || newRoles.defaultAdmin == address(0) || newRoles.pauser == address(0) || newRoles.unpauser == address(0) diff --git a/src/interfaces/child/IChildAxelarBridgeAdaptor.sol b/src/interfaces/child/IChildAxelarBridgeAdaptor.sol index ce565ab0..47692aa5 100644 --- a/src/interfaces/child/IChildAxelarBridgeAdaptor.sol +++ b/src/interfaces/child/IChildAxelarBridgeAdaptor.sol @@ -71,6 +71,8 @@ interface IChildAxelarBridgeAdaptorErrors { error InvalidSourceChain(); /// @notice Error when the source chain's message sender is not a recognised address. error InvalidSourceAddress(); + /// @notice Error when the an unauthorized initializer tries to initialize the contract. + error UnauthorizedInitializer(); } /** diff --git a/src/interfaces/child/IChildERC20Bridge.sol b/src/interfaces/child/IChildERC20Bridge.sol index 6c3ba9bb..db898eaa 100644 --- a/src/interfaces/child/IChildERC20Bridge.sol +++ b/src/interfaces/child/IChildERC20Bridge.sol @@ -196,4 +196,6 @@ interface IChildERC20BridgeErrors { error NonWrappedNativeTransfer(); /// @notice Error when the bridge doesn't have enough native IMX to support the deposit. error InsufficientIMX(); + /// @notice Error when the an unauthorized initializer tries to initialize the contract. + error UnauthorizedInitializer(); } diff --git a/src/interfaces/root/IRootAxelarBridgeAdaptor.sol b/src/interfaces/root/IRootAxelarBridgeAdaptor.sol index e789a555..e733cea9 100644 --- a/src/interfaces/root/IRootAxelarBridgeAdaptor.sol +++ b/src/interfaces/root/IRootAxelarBridgeAdaptor.sol @@ -75,6 +75,8 @@ interface IRootAxelarBridgeAdaptorErrors { error InvalidSourceAddress(); /// @notice Error when a message received has invalid source chain. error InvalidSourceChain(); + /// @notice Error when the an unauthorized initializer tries to initialize the contract. + error UnauthorizedInitializer(); } /** diff --git a/src/interfaces/root/IRootERC20Bridge.sol b/src/interfaces/root/IRootERC20Bridge.sol index cd0ea05d..8fed4e5b 100644 --- a/src/interfaces/root/IRootERC20Bridge.sol +++ b/src/interfaces/root/IRootERC20Bridge.sol @@ -189,4 +189,6 @@ interface IRootERC20BridgeErrors { error ImxDepositLimitTooLow(); /// @notice Error when native transfer is sent to contract from non wrapped-token address. error NonWrappedNativeTransfer(); + /// @notice Error when the an unauthorized initializer tries to initialize the contract. + error UnauthorizedInitializer(); } diff --git a/src/root/RootAxelarBridgeAdaptor.sol b/src/root/RootAxelarBridgeAdaptor.sol index 22266d82..99b56bdb 100644 --- a/src/root/RootAxelarBridgeAdaptor.sol +++ b/src/root/RootAxelarBridgeAdaptor.sol @@ -50,7 +50,12 @@ contract RootAxelarBridgeAdaptor is /// @notice Address of the Axelar Gas Service contract. IAxelarGasService public gasService; - constructor(address _gateway) AxelarExecutable(_gateway) {} + /// @notice Address of the authorized initializer. + address public immutable initializerAddress; + + constructor(address _gateway) AxelarExecutable(_gateway) { + initializerAddress = msg.sender; + } /** * @notice Initialization function for RootAxelarBridgeAdaptor. @@ -67,6 +72,9 @@ contract RootAxelarBridgeAdaptor is string memory _childBridgeAdaptor, address _gasService ) public initializer { + if (msg.sender != initializerAddress) { + revert UnauthorizedInitializer(); + } if ( _rootBridge == address(0) || _gasService == address(0) || _roles.defaultAdmin == address(0) || _roles.bridgeManager == address(0) || _roles.gasServiceManager == address(0) diff --git a/src/root/RootERC20Bridge.sol b/src/root/RootERC20Bridge.sol index 1f6cb5dd..bb723879 100644 --- a/src/root/RootERC20Bridge.sol +++ b/src/root/RootERC20Bridge.sol @@ -82,6 +82,8 @@ contract RootERC20Bridge is /// @dev The maximum cumulative amount of IMX that can be deposited into the bridge. /// @dev A limit of zero indicates unlimited. uint256 public imxCumulativeDepositLimit; + /// @dev Address of the authorized initializer. + address public immutable initializerAddress; /** * @notice Modifier to ensure that the caller is the registered root bridge adaptor. @@ -93,6 +95,10 @@ contract RootERC20Bridge is _; } + constructor() { + initializerAddress = msg.sender; + } + /** * @notice Initialization function for RootERC20Bridge. * @param newRoles Struct containing addresses of roles. @@ -143,6 +149,9 @@ contract RootERC20Bridge is address newRootWETHToken, uint256 newImxCumulativeDepositLimit ) internal { + if (msg.sender != initializerAddress) { + revert UnauthorizedInitializer(); + } if ( newRootBridgeAdaptor == address(0) || newChildERC20Bridge == address(0) || newChildTokenTemplate == address(0) || newRootIMXToken == address(0) || newRootWETHToken == address(0) diff --git a/src/root/flowrate/RootERC20BridgeFlowRate.sol b/src/root/flowrate/RootERC20BridgeFlowRate.sol index dd7bf0e0..f439dd29 100644 --- a/src/root/flowrate/RootERC20BridgeFlowRate.sol +++ b/src/root/flowrate/RootERC20BridgeFlowRate.sol @@ -80,6 +80,8 @@ contract RootERC20BridgeFlowRate is // Map ERC 20 token address to threshold mapping(address => uint256) public largeTransferThresholds; + constructor() RootERC20Bridge() {} + function initialize( InitializationRoles memory newRoles, address newRootBridgeAdaptor, From 95f896becdb1e9f442c5eccffc7cf76ffbc3cc88 Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Tue, 12 Dec 2023 07:13:54 +1100 Subject: [PATCH 028/155] Document risk of locked funds if receiver lacks fallback --- src/child/ChildERC20Bridge.sol | 18 ++++++++++++++++-- src/root/RootERC20Bridge.sol | 22 ++++++++++++++++++---- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/child/ChildERC20Bridge.sol b/src/child/ChildERC20Bridge.sol index 06e3051b..e6a7fd29 100644 --- a/src/child/ChildERC20Bridge.sol +++ b/src/child/ChildERC20Bridge.sol @@ -35,10 +35,16 @@ import {BridgeRoles} from "../common/BridgeRoles.sol"; * - An account with an UNPAUSER_ROLE can unpause the contract. * - An account with an ADAPTOR_MANAGER_ROLE can update the root bridge adaptor address. * - An account with a DEFAULT_ADMIN_ROLE can grant and revoke roles. - * @dev Note: + * + * @dev Caution: + * - When withdrawing ETH (L2 -> L1), it's crucial to make sure that the receiving address on the root chain, + * if it's a contract, has a receive or fallback function that allows it to accept native ETH on the root chain. + * If this isn't the case, the transaction on the root chain could revert, potentially locking the user's funds indefinitely. * - There is undefined behaviour for bridging non-standard ERC20 tokens (e.g. rebasing tokens). Please approach such cases with great care. - * - This is an upgradeable contract that should be operated behind OpenZeppelin's TransparentUpgradeableProxy. * - The initialize function is susceptible to front running, so precautions should be taken to account for this scenario. + * + * @dev Note: + * - This is an upgradeable contract that should be operated behind OpenZeppelin's TransparentUpgradeableProxy. */ contract ChildERC20Bridge is BridgeRoles, @@ -238,6 +244,10 @@ contract ChildERC20Bridge is /** * @inheritdoc IChildERC20Bridge + * @dev Caution: + * When withdrawing ETH, it's crucial to make sure that the receiving address (`msg.sender`) on the root chain, + * if it's a contract, has a receive or fallback function that allows it to accept native ETH. + * If this isn't the case, the transaction on the root chain could revert, potentially locking the user's funds indefinitely. */ function withdrawETH(uint256 amount) external payable { _withdraw(childETHToken, msg.sender, amount); @@ -245,6 +255,10 @@ contract ChildERC20Bridge is /** * @inheritdoc IChildERC20Bridge + * @dev Caution: + * When withdrawing ETH, it's crucial to make sure that the receiving address (`receiver`) on the root chain, + * if it's a contract, has a receive or fallback function that allows it to accept native ETH. + * If this isn't the case, the transaction on the root chain could revert, potentially locking the user's funds indefinitely. */ function withdrawETHTo(address receiver, uint256 amount) external payable { _withdraw(childETHToken, receiver, amount); diff --git a/src/root/RootERC20Bridge.sol b/src/root/RootERC20Bridge.sol index 1f6cb5dd..6e082abb 100644 --- a/src/root/RootERC20Bridge.sol +++ b/src/root/RootERC20Bridge.sol @@ -39,10 +39,16 @@ import {BridgeRoles} from "../common/BridgeRoles.sol"; * - An account with a VARIABLE_MANAGER_ROLE can update the cumulative IMX deposit limit. * - An account with an ADAPTOR_MANAGER_ROLE can update the root bridge adaptor address. * - An account with a DEFAULT_ADMIN_ROLE can grant and revoke roles. - * @dev Note: + * + * @dev Caution: + * - When depositing IMX (L1 -> L2) it's crucial to make sure that the receiving address on the child chain, + * if it's a contract, has a receive or fallback function that allows it to accept native IMX on the child chain. + * If this isn't the case, the transaction on the child chain could revert, potentially locking the user's funds indefinitely. * - There is undefined behaviour for bridging non-standard ERC20 tokens (e.g. rebasing tokens). Please approach such cases with great care. - * - This is an upgradeable contract that should be operated behind OpenZeppelin's TransparentUpgradeableProxy. * - The initialize function is susceptible to front running, so precautions should be taken to account for this scenario. + * + * @dev Note: + * - This is an upgradeable contract that should be operated behind OpenZeppelin's TransparentUpgradeableProxy. */ contract RootERC20Bridge is BridgeRoles, @@ -281,7 +287,11 @@ contract RootERC20Bridge is /** * @inheritdoc IRootERC20Bridge - * @dev Note that there is undefined behaviour for bridging non-standard ERC20 tokens (e.g. rebasing tokens). Please approach such cases with great care. + * @dev Caution: + * - When depositing IMX, it's crucial to make sure that the receiving address (`msg.sender`) on the child chain, + * if it's a contract, has a receive or fallback function that allows it to accept native IMX. + * If this isn't the case, the transaction on the child chain could revert, potentially locking the user's funds indefinitely. + * - Note that there is undefined behaviour for bridging non-standard ERC20 tokens (e.g. rebasing tokens). Please approach such cases with great care. */ function deposit(IERC20Metadata rootToken, uint256 amount) external payable override { _depositToken(rootToken, msg.sender, amount); @@ -289,7 +299,11 @@ contract RootERC20Bridge is /** * @inheritdoc IRootERC20Bridge - * @dev Note that there is undefined behaviour for bridging non-standard ERC20 tokens (e.g. rebasing tokens). Please approach such cases with great care. + * @dev Caution: + * - When depositing IMX, it's crucial to make sure that the receiving address (`receiver`) on the child chain, + * if it's a contract, has a receive or fallback function that allows it to accept native IMX. + * If this isn't the case, the transaction on the child chain could revert, potentially locking the user's funds indefinitely. + * - Note that there is undefined behaviour for bridging non-standard ERC20 tokens (e.g. rebasing tokens). Please approach such cases with great care. */ function depositTo(IERC20Metadata rootToken, address receiver, uint256 amount) external payable override { _depositToken(rootToken, receiver, amount); From 2eeb10bda30730135b47d56ce62140b6cedb4b5c Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Tue, 12 Dec 2023 08:37:12 +1100 Subject: [PATCH 029/155] Minor refactor and add comments --- src/interfaces/root/IRootERC20Bridge.sol | 19 +++++++--- src/root/RootERC20Bridge.sol | 48 +++++++++++++----------- test/unit/root/RootERC20Bridge.t.sol | 6 +-- 3 files changed, 43 insertions(+), 30 deletions(-) diff --git a/src/interfaces/root/IRootERC20Bridge.sol b/src/interfaces/root/IRootERC20Bridge.sol index 798405cc..659b8801 100644 --- a/src/interfaces/root/IRootERC20Bridge.sol +++ b/src/interfaces/root/IRootERC20Bridge.sol @@ -50,14 +50,21 @@ interface IRootERC20Bridge { function onMessageReceive(bytes calldata data) external; /** - * @notice Initiate sending a mapToken message to the child chain. - * This is done when a token hasn't been mapped before. - * @dev Populates a root token => child token mapping on parent chain before - * sending a message telling child chain to do the same. + * @notice Initiates sending a mapToken message to the child chain, if the token hasn't been mapped before. + * This operation requires the `rootToken` to have the following public getter functions: `name()`, `symbol()`, and `decimals()`. + * These functions are optional in the ERC20 standard. If the token does not provide these functions, + * the mapping operation will fail and return a `TokenNotSupported` error. + * + * @dev The function: + * - fails with a `AlreadyMapped` error if the token has already been mapped. + * - populates a root token => child token mapping on the root chain before + * sending a message telling the child chain to do the same. + * - is `payable` because the message passing protocol requires a fee to be paid. + * * @dev The address of the child chain token is deterministic using CREATE2. + * * @param rootToken The address of the token on the root chain. * @return childToken The address of the token to be deployed on the child chain. - * @dev The function is `payable` because the message passing protocol requires a fee to be paid. */ function mapToken(IERC20Metadata rootToken) external payable returns (address); @@ -190,5 +197,5 @@ interface IRootERC20BridgeErrors { /// @notice Error when native transfer is sent to contract from non wrapped-token address. error NonWrappedNativeTransfer(); /// @notice Error when attempt to map a ERC20 token that doesn't support name(), symbol() or decimals(). - error NotSupportedToken(); + error TokenNotSupported(); } diff --git a/src/root/RootERC20Bridge.sol b/src/root/RootERC20Bridge.sol index dcce6be7..0011ed32 100644 --- a/src/root/RootERC20Bridge.sol +++ b/src/root/RootERC20Bridge.sol @@ -16,6 +16,7 @@ import { import {IRootBridgeAdaptor} from "../interfaces/root/IRootBridgeAdaptor.sol"; import {IWETH} from "../interfaces/root/IWETH.sol"; import {BridgeRoles} from "../common/BridgeRoles.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; /** * @title Root ERC20 Bridge @@ -373,27 +374,7 @@ contract RootERC20Bridge is rootTokenToChildToken[address(rootToken)] = childToken; - string memory tokenName; - string memory tokenSymbol; - uint8 tokenDecimals; - - try rootToken.name() returns (string memory name) { - tokenName = name; - } catch { - revert NotSupportedToken(); - } - - try rootToken.symbol() returns (string memory symbol) { - tokenSymbol = symbol; - } catch { - revert NotSupportedToken(); - } - - try rootToken.decimals() returns (uint8 decimals) { - tokenDecimals = decimals; - } catch { - revert NotSupportedToken(); - } + (string memory tokenName, string memory tokenSymbol, uint8 tokenDecimals) = _getTokenDetails(rootToken); bytes memory payload = abi.encode(MAP_TOKEN_SIG, rootToken, tokenName, tokenSymbol, tokenDecimals); rootBridgeAdaptor.sendMessage{value: msg.value}(payload, msg.sender); @@ -505,6 +486,31 @@ contract RootERC20Bridge is } } + function _getTokenDetails(IERC20Metadata token) private view returns (string memory, string memory, uint8) { + string memory tokenName; + try token.name() returns (string memory name) { + tokenName = name; + } catch { + revert TokenNotSupported(); + } + + string memory tokenSymbol; + try token.symbol() returns (string memory symbol) { + tokenSymbol = symbol; + } catch { + revert TokenNotSupported(); + } + + uint8 tokenDecimals; + try token.decimals() returns (uint8 decimals) { + tokenDecimals = decimals; + } catch { + revert TokenNotSupported(); + } + + return (tokenName, tokenSymbol, tokenDecimals); + } + modifier wontIMXOverflow(address rootToken, uint256 amount) { // Assert whether the deposit is root IMX address imxToken = rootIMXToken; diff --git a/test/unit/root/RootERC20Bridge.t.sol b/test/unit/root/RootERC20Bridge.t.sol index fb4e7946..93a7a159 100644 --- a/test/unit/root/RootERC20Bridge.t.sol +++ b/test/unit/root/RootERC20Bridge.t.sol @@ -447,7 +447,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid function test_RevertIf_mapTokenWithoutName() public { vm.mockCallRevert(address(token), abi.encodeWithSelector(IERC20Metadata.name.selector), "Unsupported operation"); - vm.expectRevert(NotSupportedToken.selector); + vm.expectRevert(TokenNotSupported.selector); rootBridge.mapToken{value: 300}(token); } @@ -455,7 +455,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid vm.mockCallRevert( address(token), abi.encodeWithSelector(IERC20Metadata.symbol.selector), "Unsupported operation" ); - vm.expectRevert(NotSupportedToken.selector); + vm.expectRevert(TokenNotSupported.selector); rootBridge.mapToken{value: 300}(token); } @@ -463,7 +463,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid vm.mockCallRevert( address(token), abi.encodeWithSelector(IERC20Metadata.decimals.selector), "Unsupported operation" ); - vm.expectRevert(NotSupportedToken.selector); + vm.expectRevert(TokenNotSupported.selector); rootBridge.mapToken{value: 300}(token); } From a5b05448a65cf2d2b5de5729810d0c4c6b8936d2 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Tue, 12 Dec 2023 07:10:47 +0800 Subject: [PATCH 030/155] Add tests --- test/unit/child/ChildAxelarBridgeAdaptor.t.sol | 13 +++++++++++++ test/unit/child/ChildERC20Bridge.t.sol | 7 +++++++ test/unit/root/RootAxelarBridgeAdaptor.t.sol | 9 +++++++++ test/unit/root/RootERC20Bridge.t.sol | 15 +++++++++++++++ 4 files changed, 44 insertions(+) diff --git a/test/unit/child/ChildAxelarBridgeAdaptor.t.sol b/test/unit/child/ChildAxelarBridgeAdaptor.t.sol index 94e505e8..c358ebff 100644 --- a/test/unit/child/ChildAxelarBridgeAdaptor.t.sol +++ b/test/unit/child/ChildAxelarBridgeAdaptor.t.sol @@ -76,6 +76,19 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro ); } + function test_RevertIf_InitializeWithUnauthorizedInitializer() public { + ChildAxelarBridgeAdaptor newAdaptor = new ChildAxelarBridgeAdaptor(GATEWAY_ADDRESS); + vm.prank(address(0x1234)); + vm.expectRevert(UnauthorizedInitializer.selector); + newAdaptor.initialize( + roles, + address(mockChildERC20Bridge), + ROOT_CHAIN_NAME, + ROOT_BRIDGE_ADAPTOR, + address(mockChildAxelarGasService) + ); + } + function test_RevertIf_InitializeWithZeroAdmin() public { ChildAxelarBridgeAdaptor newAdaptor = new ChildAxelarBridgeAdaptor(GATEWAY_ADDRESS); vm.expectRevert(ZeroAddress.selector); diff --git a/test/unit/child/ChildERC20Bridge.t.sol b/test/unit/child/ChildERC20Bridge.t.sol index 85386706..aa7cffbb 100644 --- a/test/unit/child/ChildERC20Bridge.t.sol +++ b/test/unit/child/ChildERC20Bridge.t.sol @@ -164,6 +164,13 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B assert(childBridge.rootTokenToChildToken(NATIVE_ETH) != address(0)); } + function test_RevertIf_InitializeWithUnauthorizedInitializer() public { + ChildERC20Bridge bridge = new ChildERC20Bridge(); + vm.prank(address(0x1234)); + vm.expectRevert(UnauthorizedInitializer.selector); + bridge.initialize(roles, address(1), address(1), address(1), address(1)); + } + function test_RevertIfInitializeTwice() public { vm.expectRevert("Initializable: contract is already initialized"); childBridge.initialize(roles, address(this), address(childTokenTemplate), ROOT_IMX_TOKEN, CHILD_WIMX_TOKEN); diff --git a/test/unit/root/RootAxelarBridgeAdaptor.t.sol b/test/unit/root/RootAxelarBridgeAdaptor.t.sol index 1ac939ef..87923bc5 100644 --- a/test/unit/root/RootAxelarBridgeAdaptor.t.sol +++ b/test/unit/root/RootAxelarBridgeAdaptor.t.sol @@ -76,6 +76,15 @@ contract RootAxelarBridgeAdaptorTest is Test, IRootAxelarBridgeAdaptorEvents, IR ); } + function test_RevertIf_InitializeWithUnauthorizedInitializer() public { + RootAxelarBridgeAdaptor newAdaptor = new RootAxelarBridgeAdaptor(address(mockAxelarGateway)); + vm.prank(address(0x1234)); + vm.expectRevert(UnauthorizedInitializer.selector); + newAdaptor.initialize( + roles, address(this), CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, address(axelarGasService) + ); + } + function test_RevertIf_InitializeWithZeroAdmin() public { RootAxelarBridgeAdaptor newAdaptor = new RootAxelarBridgeAdaptor(address(mockAxelarGateway)); vm.expectRevert(ZeroAddresses.selector); diff --git a/test/unit/root/RootERC20Bridge.t.sol b/test/unit/root/RootERC20Bridge.t.sol index 586cef74..9d59f961 100644 --- a/test/unit/root/RootERC20Bridge.t.sol +++ b/test/unit/root/RootERC20Bridge.t.sol @@ -133,6 +133,21 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid test_NativeTransferFromWETH(); } + function test_RevertIf_InitializeWithUnauthorizedInitializer() public { + RootERC20Bridge bridge = new RootERC20Bridge(); + IRootERC20Bridge.InitializationRoles memory roles = IRootERC20Bridge.InitializationRoles({ + defaultAdmin: address(this), + pauser: address(this), + unpauser: address(this), + variableManager: address(this), + adaptorManager: address(this) + }); + + vm.prank(address(0x1234)); + vm.expectRevert(UnauthorizedInitializer.selector); + bridge.initialize(roles, address(1), address(1), address(1), address(1), address(1), UNLIMITED_IMX_DEPOSITS); + } + function test_RevertIf_InitializeTwice() public { IRootERC20Bridge.InitializationRoles memory roles = IRootERC20Bridge.InitializationRoles({ defaultAdmin: address(this), From 2929d67ba93ecb3c343b01775ae65b01c3e0e458 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Tue, 12 Dec 2023 08:12:44 +0800 Subject: [PATCH 031/155] Use additional parameter --- src/child/ChildAxelarBridgeAdaptor.sol | 4 +-- src/child/ChildERC20Bridge.sol | 4 +-- src/root/RootAxelarBridgeAdaptor.sol | 4 +-- src/root/RootERC20Bridge.sol | 4 +-- src/root/flowrate/RootERC20BridgeFlowRate.sol | 2 +- .../integration/child/ChildAxelarBridge.t.sol | 4 +-- .../unit/child/ChildAxelarBridgeAdaptor.t.sol | 20 +++++++------- test/unit/child/ChildERC20Bridge.t.sol | 20 +++++++------- .../ChildERC20BridgeWithdraw.t.sol | 2 +- .../ChildERC20BridgeWithdrawETH.t.sol | 2 +- .../ChildERC20BridgeWithdrawETHTo.t.sol | 2 +- .../ChildERC20BridgeWithdrawIMX.t.sol | 2 +- .../ChildERC20BridgeWithdrawIMXTo.t.sol | 2 +- .../ChildERC20BridgeWithdrawTo.t.sol | 2 +- .../ChildERC20BridgeWithdrawWIMX.t.sol | 2 +- .../ChildERC20BridgeWithdrawWIMXTo.t.sol | 2 +- test/unit/root/RootAxelarBridgeAdaptor.t.sol | 18 ++++++------- test/unit/root/RootERC20Bridge.t.sol | 26 +++++++++---------- .../flowrate/RootERC20BridgeFlowRate.t.sol | 4 +-- .../withdrawals/RootERC20BridgeWithdraw.t.sol | 2 +- test/utils.t.sol | 9 ++++--- 21 files changed, 69 insertions(+), 68 deletions(-) diff --git a/src/child/ChildAxelarBridgeAdaptor.sol b/src/child/ChildAxelarBridgeAdaptor.sol index 9fa9dd13..00f7515b 100644 --- a/src/child/ChildAxelarBridgeAdaptor.sol +++ b/src/child/ChildAxelarBridgeAdaptor.sol @@ -52,8 +52,8 @@ contract ChildAxelarBridgeAdaptor is /// @notice Address of the authorized initializer. address public immutable initializerAddress; - constructor(address _gateway) AxelarExecutable(_gateway) { - initializerAddress = msg.sender; + constructor(address _gateway, address _initializerAddress) AxelarExecutable(_gateway) { + initializerAddress = _initializerAddress; } /** diff --git a/src/child/ChildERC20Bridge.sol b/src/child/ChildERC20Bridge.sol index b5948576..5cb96ce5 100644 --- a/src/child/ChildERC20Bridge.sol +++ b/src/child/ChildERC20Bridge.sol @@ -83,8 +83,8 @@ contract ChildERC20Bridge is _; } - constructor() { - initializerAddress = msg.sender; + constructor(address _initializerAddress) { + initializerAddress = _initializerAddress; } /** diff --git a/src/root/RootAxelarBridgeAdaptor.sol b/src/root/RootAxelarBridgeAdaptor.sol index 99b56bdb..d01633b0 100644 --- a/src/root/RootAxelarBridgeAdaptor.sol +++ b/src/root/RootAxelarBridgeAdaptor.sol @@ -53,8 +53,8 @@ contract RootAxelarBridgeAdaptor is /// @notice Address of the authorized initializer. address public immutable initializerAddress; - constructor(address _gateway) AxelarExecutable(_gateway) { - initializerAddress = msg.sender; + constructor(address _gateway, address _initializerAddress) AxelarExecutable(_gateway) { + initializerAddress = _initializerAddress; } /** diff --git a/src/root/RootERC20Bridge.sol b/src/root/RootERC20Bridge.sol index bb723879..e8eb6660 100644 --- a/src/root/RootERC20Bridge.sol +++ b/src/root/RootERC20Bridge.sol @@ -95,8 +95,8 @@ contract RootERC20Bridge is _; } - constructor() { - initializerAddress = msg.sender; + constructor(address _initializerAddress) { + initializerAddress = _initializerAddress; } /** diff --git a/src/root/flowrate/RootERC20BridgeFlowRate.sol b/src/root/flowrate/RootERC20BridgeFlowRate.sol index f439dd29..1f05a872 100644 --- a/src/root/flowrate/RootERC20BridgeFlowRate.sol +++ b/src/root/flowrate/RootERC20BridgeFlowRate.sol @@ -80,7 +80,7 @@ contract RootERC20BridgeFlowRate is // Map ERC 20 token address to threshold mapping(address => uint256) public largeTransferThresholds; - constructor() RootERC20Bridge() {} + constructor(address _initializerAddress) RootERC20Bridge(_initializerAddress) {} function initialize( InitializationRoles memory newRoles, diff --git a/test/integration/child/ChildAxelarBridge.t.sol b/test/integration/child/ChildAxelarBridge.t.sol index c142e21b..2b5e538c 100644 --- a/test/integration/child/ChildAxelarBridge.t.sol +++ b/test/integration/child/ChildAxelarBridge.t.sol @@ -39,10 +39,10 @@ contract ChildERC20BridgeIntegrationTest is Test, IChildERC20BridgeEvents, IChil childERC20 = new ChildERC20(); childERC20.initialize(address(123), "Test", "TST", 18); - childERC20Bridge = new ChildERC20Bridge(); + childERC20Bridge = new ChildERC20Bridge(address(this)); mockChildAxelarGateway = new MockChildAxelarGateway(); mockChildAxelarGasService = new MockChildAxelarGasService(); - childAxelarBridgeAdaptor = new ChildAxelarBridgeAdaptor(address(mockChildAxelarGateway)); + childAxelarBridgeAdaptor = new ChildAxelarBridgeAdaptor(address(mockChildAxelarGateway), address(this)); IChildERC20Bridge.InitializationRoles memory roles = IChildERC20Bridge.InitializationRoles({ defaultAdmin: address(this), diff --git a/test/unit/child/ChildAxelarBridgeAdaptor.t.sol b/test/unit/child/ChildAxelarBridgeAdaptor.t.sol index c358ebff..7934e048 100644 --- a/test/unit/child/ChildAxelarBridgeAdaptor.t.sol +++ b/test/unit/child/ChildAxelarBridgeAdaptor.t.sol @@ -43,7 +43,7 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro mockChildERC20Bridge = new MockChildERC20Bridge(); mockChildAxelarGateway = new MockChildAxelarGateway(); mockChildAxelarGasService = new MockChildAxelarGasService(); - axelarAdaptor = new ChildAxelarBridgeAdaptor(address(mockChildAxelarGateway)); + axelarAdaptor = new ChildAxelarBridgeAdaptor(address(mockChildAxelarGateway), address(this)); axelarAdaptor.initialize( roles, address(mockChildERC20Bridge), @@ -77,7 +77,7 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro } function test_RevertIf_InitializeWithUnauthorizedInitializer() public { - ChildAxelarBridgeAdaptor newAdaptor = new ChildAxelarBridgeAdaptor(GATEWAY_ADDRESS); + ChildAxelarBridgeAdaptor newAdaptor = new ChildAxelarBridgeAdaptor(GATEWAY_ADDRESS, address(this)); vm.prank(address(0x1234)); vm.expectRevert(UnauthorizedInitializer.selector); newAdaptor.initialize( @@ -90,7 +90,7 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro } function test_RevertIf_InitializeWithZeroAdmin() public { - ChildAxelarBridgeAdaptor newAdaptor = new ChildAxelarBridgeAdaptor(GATEWAY_ADDRESS); + ChildAxelarBridgeAdaptor newAdaptor = new ChildAxelarBridgeAdaptor(GATEWAY_ADDRESS, address(this)); vm.expectRevert(ZeroAddress.selector); roles.defaultAdmin = address(0); newAdaptor.initialize( @@ -103,7 +103,7 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro } function test_RevertIf_InitializeWithZeroBridgeManager() public { - ChildAxelarBridgeAdaptor newAdaptor = new ChildAxelarBridgeAdaptor(GATEWAY_ADDRESS); + ChildAxelarBridgeAdaptor newAdaptor = new ChildAxelarBridgeAdaptor(GATEWAY_ADDRESS, address(this)); vm.expectRevert(ZeroAddress.selector); roles.bridgeManager = address(0); newAdaptor.initialize( @@ -116,7 +116,7 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro } function test_RevertIf_InitializeWithZeroGasServiceManager() public { - ChildAxelarBridgeAdaptor newAdaptor = new ChildAxelarBridgeAdaptor(GATEWAY_ADDRESS); + ChildAxelarBridgeAdaptor newAdaptor = new ChildAxelarBridgeAdaptor(GATEWAY_ADDRESS, address(this)); vm.expectRevert(ZeroAddress.selector); roles.gasServiceManager = address(0); newAdaptor.initialize( @@ -129,7 +129,7 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro } function test_RevertIf_InitializeWithZeroTargetManager() public { - ChildAxelarBridgeAdaptor newAdaptor = new ChildAxelarBridgeAdaptor(GATEWAY_ADDRESS); + ChildAxelarBridgeAdaptor newAdaptor = new ChildAxelarBridgeAdaptor(GATEWAY_ADDRESS, address(this)); vm.expectRevert(ZeroAddress.selector); roles.targetManager = address(0); newAdaptor.initialize( @@ -142,7 +142,7 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro } function test_RevertIf_InitializeGivenZeroAddress() public { - ChildAxelarBridgeAdaptor newAdaptor = new ChildAxelarBridgeAdaptor(GATEWAY_ADDRESS); + ChildAxelarBridgeAdaptor newAdaptor = new ChildAxelarBridgeAdaptor(GATEWAY_ADDRESS, address(this)); vm.expectRevert(ZeroAddress.selector); newAdaptor.initialize( roles, address(0), ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, address(mockChildAxelarGasService) @@ -150,7 +150,7 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro } function test_RevertIf_InitializeGivenEmptyRootChain() public { - ChildAxelarBridgeAdaptor newAdaptor = new ChildAxelarBridgeAdaptor(GATEWAY_ADDRESS); + ChildAxelarBridgeAdaptor newAdaptor = new ChildAxelarBridgeAdaptor(GATEWAY_ADDRESS, address(this)); vm.expectRevert(InvalidRootChain.selector); newAdaptor.initialize( roles, address(mockChildERC20Bridge), "", ROOT_BRIDGE_ADAPTOR, address(mockChildAxelarGasService) @@ -158,7 +158,7 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro } function test_RevertIf_InitializeGivenAnEmptyBridgeAdaptorString() public { - ChildAxelarBridgeAdaptor newAdaptor = new ChildAxelarBridgeAdaptor(GATEWAY_ADDRESS); + ChildAxelarBridgeAdaptor newAdaptor = new ChildAxelarBridgeAdaptor(GATEWAY_ADDRESS, address(this)); vm.expectRevert(InvalidRootBridgeAdaptor.selector); newAdaptor.initialize( roles, address(mockChildERC20Bridge), ROOT_CHAIN_NAME, "", address(mockChildAxelarGasService) @@ -166,7 +166,7 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro } function test_RevertIf_InitializeGivenZeroGasService() public { - ChildAxelarBridgeAdaptor newAdaptor = new ChildAxelarBridgeAdaptor(GATEWAY_ADDRESS); + ChildAxelarBridgeAdaptor newAdaptor = new ChildAxelarBridgeAdaptor(GATEWAY_ADDRESS, address(this)); vm.expectRevert(ZeroAddress.selector); newAdaptor.initialize(roles, address(mockChildERC20Bridge), ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, address(0)); } diff --git a/test/unit/child/ChildERC20Bridge.t.sol b/test/unit/child/ChildERC20Bridge.t.sol index aa7cffbb..b0a5bd2a 100644 --- a/test/unit/child/ChildERC20Bridge.t.sol +++ b/test/unit/child/ChildERC20Bridge.t.sol @@ -59,7 +59,7 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B childTokenTemplate = new ChildERC20(); childTokenTemplate.initialize(address(123), "Test", "TST", 18); - childBridge = new ChildERC20Bridge(); + childBridge = new ChildERC20Bridge(address(this)); childBridge.initialize(roles, address(this), address(childTokenTemplate), ROOT_IMX_TOKEN, CHILD_WIMX_TOKEN); } @@ -165,7 +165,7 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B } function test_RevertIf_InitializeWithUnauthorizedInitializer() public { - ChildERC20Bridge bridge = new ChildERC20Bridge(); + ChildERC20Bridge bridge = new ChildERC20Bridge(address(this)); vm.prank(address(0x1234)); vm.expectRevert(UnauthorizedInitializer.selector); bridge.initialize(roles, address(1), address(1), address(1), address(1)); @@ -177,54 +177,54 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B } function test_RevertIf_InitializeWithAZeroAddressDefaultAdmin() public { - ChildERC20Bridge bridge = new ChildERC20Bridge(); + ChildERC20Bridge bridge = new ChildERC20Bridge(address(this)); roles.defaultAdmin = address(0); vm.expectRevert(ZeroAddress.selector); bridge.initialize(roles, address(1), address(1), address(1), address(1)); } function test_RevertIf_InitializeWithAZeroAddressPauser() public { - ChildERC20Bridge bridge = new ChildERC20Bridge(); + ChildERC20Bridge bridge = new ChildERC20Bridge(address(this)); roles.pauser = address(0); vm.expectRevert(ZeroAddress.selector); bridge.initialize(roles, address(1), address(1), address(1), address(1)); } function test_RevertIf_InitializeWithAZeroAddressUnpauser() public { - ChildERC20Bridge bridge = new ChildERC20Bridge(); + ChildERC20Bridge bridge = new ChildERC20Bridge(address(this)); roles.unpauser = address(0); vm.expectRevert(ZeroAddress.selector); bridge.initialize(roles, address(1), address(1), address(1), address(1)); } function test_RevertIf_InitializeWithAZeroAddressAdapter() public { - ChildERC20Bridge bridge = new ChildERC20Bridge(); + ChildERC20Bridge bridge = new ChildERC20Bridge(address(this)); roles.adaptorManager = address(0); vm.expectRevert(ZeroAddress.selector); bridge.initialize(roles, address(0), address(1), address(1), address(1)); } function test_RevertIf_InitializeWithAZeroAddressTreasuryManager() public { - ChildERC20Bridge bridge = new ChildERC20Bridge(); + ChildERC20Bridge bridge = new ChildERC20Bridge(address(this)); roles.treasuryManager = address(0); vm.expectRevert(ZeroAddress.selector); bridge.initialize(roles, address(1), address(1), address(1), address(1)); } function test_RevertIf_InitializeWithAZeroAddressChildTemplate() public { - ChildERC20Bridge bridge = new ChildERC20Bridge(); + ChildERC20Bridge bridge = new ChildERC20Bridge(address(this)); vm.expectRevert(ZeroAddress.selector); bridge.initialize(roles, address(1), address(0), address(1), address(1)); } function test_RevertIf_InitializeWithAZeroAddressIMXToken() public { - ChildERC20Bridge bridge = new ChildERC20Bridge(); + ChildERC20Bridge bridge = new ChildERC20Bridge(address(this)); vm.expectRevert(ZeroAddress.selector); bridge.initialize(roles, address(1), address(1), address(0), address(1)); } function test_RevertIf_InitializeWithAZeroAddressAll() public { - ChildERC20Bridge bridge = new ChildERC20Bridge(); + ChildERC20Bridge bridge = new ChildERC20Bridge(address(this)); vm.expectRevert(ZeroAddress.selector); roles.defaultAdmin = address(0); roles.pauser = address(0); diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol index 2af7e3fa..dd3dea5e 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol @@ -42,7 +42,7 @@ contract ChildERC20BridgeWithdrawUnitTest is Test, IChildERC20BridgeEvents, IChi initialDepositor: address(this), treasuryManager: address(this) }); - childBridge = new ChildERC20Bridge(); + childBridge = new ChildERC20Bridge(address(this)); childBridge.initialize( roles, address(mockAdaptor), address(childTokenTemplate), ROOT_IMX_TOKEN, WIMX_TOKEN_ADDRESS ); diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETH.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETH.t.sol index 92680ec5..223d70dc 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETH.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETH.t.sol @@ -28,7 +28,7 @@ contract ChildERC20BridgeWithdrawETHUnitTest is Test, IChildERC20BridgeEvents, I mockAdaptor = new MockAdaptor(); - childBridge = new ChildERC20Bridge(); + childBridge = new ChildERC20Bridge(address(this)); IChildERC20Bridge.InitializationRoles memory roles = IChildERC20Bridge.InitializationRoles({ defaultAdmin: address(this), pauser: pauser, diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETHTo.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETHTo.t.sol index 7c474695..79260e46 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETHTo.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETHTo.t.sol @@ -37,7 +37,7 @@ contract ChildERC20BridgeWithdrawETHToUnitTest is Test, IChildERC20BridgeEvents, mockAdaptor = new MockAdaptor(); - childBridge = new ChildERC20Bridge(); + childBridge = new ChildERC20Bridge(address(this)); IChildERC20Bridge.InitializationRoles memory roles = IChildERC20Bridge.InitializationRoles({ defaultAdmin: address(this), pauser: pauser, diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawIMX.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawIMX.t.sol index 914665a9..c48c08d8 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawIMX.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawIMX.t.sol @@ -27,7 +27,7 @@ contract ChildERC20BridgeWithdrawIMXUnitTest is Test, IChildERC20BridgeEvents, I mockAdaptor = new MockAdaptor(); - childBridge = new ChildERC20Bridge(); + childBridge = new ChildERC20Bridge(address(this)); IChildERC20Bridge.InitializationRoles memory roles = IChildERC20Bridge.InitializationRoles({ defaultAdmin: address(this), pauser: pauser, diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawIMXTo.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawIMXTo.t.sol index 218a35b7..b2fa6b72 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawIMXTo.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawIMXTo.t.sol @@ -31,7 +31,7 @@ contract ChildERC20BridgeWithdrawIMXToUnitTest is Test, IChildERC20BridgeEvents, mockAdaptor = new MockAdaptor(); - childBridge = new ChildERC20Bridge(); + childBridge = new ChildERC20Bridge(address(this)); IChildERC20Bridge.InitializationRoles memory roles = IChildERC20Bridge.InitializationRoles({ defaultAdmin: address(this), pauser: pauser, diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol index cecc7bb0..88cc2b63 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol @@ -42,7 +42,7 @@ contract ChildERC20BridgeWithdrawToUnitTest is Test, IChildERC20BridgeEvents, IC initialDepositor: address(this), treasuryManager: address(this) }); - childBridge = new ChildERC20Bridge(); + childBridge = new ChildERC20Bridge(address(this)); childBridge.initialize( roles, address(mockAdaptor), address(childTokenTemplate), ROOT_IMX_TOKEN, WIMX_TOKEN_ADDRESS ); diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMX.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMX.t.sol index a14fb6a1..a9a40c8e 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMX.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMX.t.sol @@ -31,7 +31,7 @@ contract ChildERC20BridgeWithdrawWIMXUnitTest is Test, IChildERC20BridgeEvents, wIMXToken = new WIMX(); Address.sendValue(payable(wIMXToken), 100 ether); - childBridge = new ChildERC20Bridge(); + childBridge = new ChildERC20Bridge(address(this)); IChildERC20Bridge.InitializationRoles memory roles = IChildERC20Bridge.InitializationRoles({ defaultAdmin: address(this), pauser: pauser, diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMXTo.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMXTo.t.sol index 0ffe48c7..94d2a3db 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMXTo.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMXTo.t.sol @@ -33,7 +33,7 @@ contract ChildERC20BridgeWithdrawWIMXToUnitTest is Test, IChildERC20BridgeEvents wIMXToken = new WIMX(); Address.sendValue(payable(wIMXToken), 100 ether); - childBridge = new ChildERC20Bridge(); + childBridge = new ChildERC20Bridge(address(this)); IChildERC20Bridge.InitializationRoles memory roles = IChildERC20Bridge.InitializationRoles({ defaultAdmin: address(this), pauser: pauser, diff --git a/test/unit/root/RootAxelarBridgeAdaptor.t.sol b/test/unit/root/RootAxelarBridgeAdaptor.t.sol index 87923bc5..302fb75d 100644 --- a/test/unit/root/RootAxelarBridgeAdaptor.t.sol +++ b/test/unit/root/RootAxelarBridgeAdaptor.t.sol @@ -44,7 +44,7 @@ contract RootAxelarBridgeAdaptorTest is Test, IRootAxelarBridgeAdaptorEvents, IR token = new ERC20PresetMinterPauser("Test", "TST"); mockAxelarGateway = new MockAxelarGateway(); axelarGasService = new MockAxelarGasService(); - axelarAdaptor = new RootAxelarBridgeAdaptor(address(mockAxelarGateway)); + axelarAdaptor = new RootAxelarBridgeAdaptor(address(mockAxelarGateway), address(this)); stubRootBridge = new StubRootBridge(); axelarAdaptor.initialize( roles, address(stubRootBridge), CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, address(axelarGasService) @@ -77,7 +77,7 @@ contract RootAxelarBridgeAdaptorTest is Test, IRootAxelarBridgeAdaptorEvents, IR } function test_RevertIf_InitializeWithUnauthorizedInitializer() public { - RootAxelarBridgeAdaptor newAdaptor = new RootAxelarBridgeAdaptor(address(mockAxelarGateway)); + RootAxelarBridgeAdaptor newAdaptor = new RootAxelarBridgeAdaptor(address(mockAxelarGateway), address(this)); vm.prank(address(0x1234)); vm.expectRevert(UnauthorizedInitializer.selector); newAdaptor.initialize( @@ -86,7 +86,7 @@ contract RootAxelarBridgeAdaptorTest is Test, IRootAxelarBridgeAdaptorEvents, IR } function test_RevertIf_InitializeWithZeroAdmin() public { - RootAxelarBridgeAdaptor newAdaptor = new RootAxelarBridgeAdaptor(address(mockAxelarGateway)); + RootAxelarBridgeAdaptor newAdaptor = new RootAxelarBridgeAdaptor(address(mockAxelarGateway), address(this)); vm.expectRevert(ZeroAddresses.selector); roles.defaultAdmin = address(0); newAdaptor.initialize( @@ -95,7 +95,7 @@ contract RootAxelarBridgeAdaptorTest is Test, IRootAxelarBridgeAdaptorEvents, IR } function test_RevertIf_InitializeWithZeroBridgeManager() public { - RootAxelarBridgeAdaptor newAdaptor = new RootAxelarBridgeAdaptor(address(mockAxelarGateway)); + RootAxelarBridgeAdaptor newAdaptor = new RootAxelarBridgeAdaptor(address(mockAxelarGateway), address(this)); vm.expectRevert(ZeroAddresses.selector); roles.bridgeManager = address(0); newAdaptor.initialize( @@ -104,7 +104,7 @@ contract RootAxelarBridgeAdaptorTest is Test, IRootAxelarBridgeAdaptorEvents, IR } function test_RevertIf_InitializeWithZeroGasServiceManager() public { - RootAxelarBridgeAdaptor newAdaptor = new RootAxelarBridgeAdaptor(address(mockAxelarGateway)); + RootAxelarBridgeAdaptor newAdaptor = new RootAxelarBridgeAdaptor(address(mockAxelarGateway), address(this)); vm.expectRevert(ZeroAddresses.selector); roles.gasServiceManager = address(0); newAdaptor.initialize( @@ -113,7 +113,7 @@ contract RootAxelarBridgeAdaptorTest is Test, IRootAxelarBridgeAdaptorEvents, IR } function test_RevertIf_InitializeWithZeroTargetManager() public { - RootAxelarBridgeAdaptor newAdaptor = new RootAxelarBridgeAdaptor(address(mockAxelarGateway)); + RootAxelarBridgeAdaptor newAdaptor = new RootAxelarBridgeAdaptor(address(mockAxelarGateway), address(this)); vm.expectRevert(ZeroAddresses.selector); roles.targetManager = address(0); newAdaptor.initialize( @@ -122,7 +122,7 @@ contract RootAxelarBridgeAdaptorTest is Test, IRootAxelarBridgeAdaptorEvents, IR } function test_RevertIf_InitializeGivenZeroAddress() public { - RootAxelarBridgeAdaptor newAdaptor = new RootAxelarBridgeAdaptor(address(mockAxelarGateway)); + RootAxelarBridgeAdaptor newAdaptor = new RootAxelarBridgeAdaptor(address(mockAxelarGateway), address(this)); vm.expectRevert(ZeroAddresses.selector); newAdaptor.initialize( roles, address(0), CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, address(axelarGasService) @@ -130,13 +130,13 @@ contract RootAxelarBridgeAdaptorTest is Test, IRootAxelarBridgeAdaptorEvents, IR } function test_RevertIf_InitializeGivenEmptyChildChainName() public { - RootAxelarBridgeAdaptor newAdaptor = new RootAxelarBridgeAdaptor(address(mockAxelarGateway)); + RootAxelarBridgeAdaptor newAdaptor = new RootAxelarBridgeAdaptor(address(mockAxelarGateway), address(this)); vm.expectRevert(InvalidChildChain.selector); newAdaptor.initialize(roles, address(this), "", CHILD_BRIDGE_ADAPTOR_STRING, address(axelarGasService)); } function test_RevertIf_InitializeGivenEmptyChildAdapter() public { - RootAxelarBridgeAdaptor newAdaptor = new RootAxelarBridgeAdaptor(address(mockAxelarGateway)); + RootAxelarBridgeAdaptor newAdaptor = new RootAxelarBridgeAdaptor(address(mockAxelarGateway), address(this)); vm.expectRevert(InvalidChildBridgeAdaptor.selector); newAdaptor.initialize(roles, address(this), CHILD_CHAIN_NAME, "", address(axelarGasService)); } diff --git a/test/unit/root/RootERC20Bridge.t.sol b/test/unit/root/RootERC20Bridge.t.sol index 9d59f961..999bc330 100644 --- a/test/unit/root/RootERC20Bridge.t.sol +++ b/test/unit/root/RootERC20Bridge.t.sol @@ -55,7 +55,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid deployCodeTo("WETH.sol", abi.encode("Wrapped ETH", "WETH"), WRAPPED_ETH); - rootBridge = new RootERC20Bridge(); + rootBridge = new RootERC20Bridge(address(this)); mockAxelarGateway = new MockAxelarGateway(); axelarGasService = new MockAxelarGasService(); @@ -134,7 +134,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid } function test_RevertIf_InitializeWithUnauthorizedInitializer() public { - RootERC20Bridge bridge = new RootERC20Bridge(); + RootERC20Bridge bridge = new RootERC20Bridge(address(this)); IRootERC20Bridge.InitializationRoles memory roles = IRootERC20Bridge.InitializationRoles({ defaultAdmin: address(this), pauser: address(this), @@ -170,7 +170,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid } function test_RevertIf_InitializeWithAZeroAddressDefaultAdmin() public { - RootERC20Bridge bridge = new RootERC20Bridge(); + RootERC20Bridge bridge = new RootERC20Bridge(address(this)); IRootERC20Bridge.InitializationRoles memory roles = IRootERC20Bridge.InitializationRoles({ defaultAdmin: address(0), pauser: address(this), @@ -184,7 +184,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid } function test_RevertIf_InitializeWithAZeroAddressPauser() public { - RootERC20Bridge bridge = new RootERC20Bridge(); + RootERC20Bridge bridge = new RootERC20Bridge(address(this)); IRootERC20Bridge.InitializationRoles memory roles = IRootERC20Bridge.InitializationRoles({ defaultAdmin: address(this), pauser: address(0), @@ -198,7 +198,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid } function test_RevertIf_InitializeWithAZeroAddressUnpauser() public { - RootERC20Bridge bridge = new RootERC20Bridge(); + RootERC20Bridge bridge = new RootERC20Bridge(address(this)); IRootERC20Bridge.InitializationRoles memory roles = IRootERC20Bridge.InitializationRoles({ defaultAdmin: address(this), pauser: address(this), @@ -211,7 +211,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid } function test_RevertIf_InitializeWithAZeroAddressVariableManager() public { - RootERC20Bridge bridge = new RootERC20Bridge(); + RootERC20Bridge bridge = new RootERC20Bridge(address(this)); IRootERC20Bridge.InitializationRoles memory roles = IRootERC20Bridge.InitializationRoles({ defaultAdmin: address(this), pauser: address(this), @@ -224,7 +224,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid } function test_RevertIf_InitializeWithAZeroAddressAdaptorManager() public { - RootERC20Bridge bridge = new RootERC20Bridge(); + RootERC20Bridge bridge = new RootERC20Bridge(address(this)); IRootERC20Bridge.InitializationRoles memory roles = IRootERC20Bridge.InitializationRoles({ defaultAdmin: address(this), pauser: address(this), @@ -237,7 +237,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid } function test_RevertIf_InitializeWithAZeroAddressRootAdapter() public { - RootERC20Bridge bridge = new RootERC20Bridge(); + RootERC20Bridge bridge = new RootERC20Bridge(address(this)); IRootERC20Bridge.InitializationRoles memory roles = IRootERC20Bridge.InitializationRoles({ defaultAdmin: address(this), pauser: address(this), @@ -250,7 +250,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid } function test_RevertIf_InitializeWithAZeroAddressChildBridge() public { - RootERC20Bridge bridge = new RootERC20Bridge(); + RootERC20Bridge bridge = new RootERC20Bridge(address(this)); IRootERC20Bridge.InitializationRoles memory roles = IRootERC20Bridge.InitializationRoles({ defaultAdmin: address(this), pauser: address(this), @@ -263,7 +263,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid } function test_RevertIf_InitializeWithAZeroAddressTokenTemplate() public { - RootERC20Bridge bridge = new RootERC20Bridge(); + RootERC20Bridge bridge = new RootERC20Bridge(address(this)); IRootERC20Bridge.InitializationRoles memory roles = IRootERC20Bridge.InitializationRoles({ defaultAdmin: address(this), pauser: address(this), @@ -276,7 +276,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid } function test_RevertIf_InitializeWithAZeroAddressIMXToken() public { - RootERC20Bridge bridge = new RootERC20Bridge(); + RootERC20Bridge bridge = new RootERC20Bridge(address(this)); IRootERC20Bridge.InitializationRoles memory roles = IRootERC20Bridge.InitializationRoles({ defaultAdmin: address(this), pauser: address(this), @@ -296,7 +296,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid variableManager: address(this), adaptorManager: address(this) }); - RootERC20Bridge bridge = new RootERC20Bridge(); + RootERC20Bridge bridge = new RootERC20Bridge(address(this)); vm.expectRevert(ZeroAddress.selector); bridge.initialize(roles, address(1), address(1), address(1), address(1), address(0), UNLIMITED_IMX_DEPOSITS); } @@ -309,7 +309,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid variableManager: address(0), adaptorManager: address(0) }); - RootERC20Bridge bridge = new RootERC20Bridge(); + RootERC20Bridge bridge = new RootERC20Bridge(address(this)); vm.expectRevert(ZeroAddress.selector); bridge.initialize(roles, address(0), address(0), address(0), address(0), address(0), UNLIMITED_IMX_DEPOSITS); } diff --git a/test/unit/root/flowrate/RootERC20BridgeFlowRate.t.sol b/test/unit/root/flowrate/RootERC20BridgeFlowRate.t.sol index 9f76f984..1f435afb 100644 --- a/test/unit/root/flowrate/RootERC20BridgeFlowRate.t.sol +++ b/test/unit/root/flowrate/RootERC20BridgeFlowRate.t.sol @@ -136,7 +136,7 @@ contract RootERC20BridgeFlowRateUnitTest is bob = makeAddr("bob"); charlie = makeAddr("charlie"); - rootBridgeFlowRate = new RootERC20BridgeFlowRate(); + rootBridgeFlowRate = new RootERC20BridgeFlowRate(address(this)); mockAxelarGateway = new MockAxelarGateway(); axelarGasService = new MockAxelarGasService(); @@ -257,7 +257,7 @@ contract RootERC20BridgeFlowRateUnitTest is } function test_RevertIf_InitializeWithAZeroAddressRateAdmin() public { - RootERC20BridgeFlowRate newRootBridgeFlowRate = new RootERC20BridgeFlowRate(); + RootERC20BridgeFlowRate newRootBridgeFlowRate = new RootERC20BridgeFlowRate(address(this)); IRootERC20Bridge.InitializationRoles memory roles = IRootERC20Bridge.InitializationRoles({ defaultAdmin: address(this), pauser: address(this), diff --git a/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol b/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol index 274b7871..7a8ae5b8 100644 --- a/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol +++ b/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol @@ -45,7 +45,7 @@ contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootE deployCodeTo("WETH.sol", abi.encode("Wrapped ETH", "WETH"), WRAPPED_ETH); - rootBridge = new RootERC20Bridge(); + rootBridge = new RootERC20Bridge(address(this)); mockAxelarGateway = new MockAxelarGateway(); axelarGasService = new MockAxelarGasService(); diff --git a/test/utils.t.sol b/test/utils.t.sol index 77c117db..e557600e 100644 --- a/test/utils.t.sol +++ b/test/utils.t.sol @@ -66,8 +66,8 @@ contract Utils is Test { mockAxelarGateway = new MockAxelarGateway(); childTokenTemplate = new ChildERC20(); childTokenTemplate.initialize(address(1), "Test", "TST", 18); - childBridge = new ChildERC20Bridge(); - childBridgeAdaptor = new ChildAxelarBridgeAdaptor(address(mockAxelarGateway)); + childBridge = new ChildERC20Bridge(address(this)); + childBridgeAdaptor = new ChildAxelarBridgeAdaptor(address(mockAxelarGateway), address(this)); IChildERC20Bridge.InitializationRoles memory roles = IChildERC20Bridge.InitializationRoles({ defaultAdmin: address(this), pauser: address(this), @@ -124,11 +124,12 @@ contract Utils is Test { integrationTest.imxToken = ERC20PresetMinterPauser(imxTokenAddress); integrationTest.imxToken.mint(address(this), 1000000 ether); - integrationTest.rootBridgeFlowRate = new RootERC20BridgeFlowRate(); + integrationTest.rootBridgeFlowRate = new RootERC20BridgeFlowRate(address(this)); integrationTest.mockAxelarGateway = new MockAxelarGateway(); integrationTest.axelarGasService = new MockAxelarGasService(); - integrationTest.axelarAdaptor = new RootAxelarBridgeAdaptor(address(integrationTest.mockAxelarGateway)); + integrationTest.axelarAdaptor = + new RootAxelarBridgeAdaptor(address(integrationTest.mockAxelarGateway), address(this)); IRootERC20Bridge.InitializationRoles memory roles = IRootERC20Bridge.InitializationRoles({ defaultAdmin: address(this), From 2d2e1d6dcb9611fa52827385ed6ec1c3f81bf661 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Tue, 12 Dec 2023 08:14:14 +0800 Subject: [PATCH 032/155] Update script --- scripts/deploy/child_deployment.js | 4 ++-- scripts/deploy/root_deployment.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/deploy/child_deployment.js b/scripts/deploy/child_deployment.js index 45f47ad4..6065ceb0 100644 --- a/scripts/deploy/child_deployment.js +++ b/scripts/deploy/child_deployment.js @@ -74,7 +74,7 @@ exports.deployChildContracts = async () => { // Deploy child bridge impl let childBridgeImplObj = JSON.parse(fs.readFileSync('../../out/ChildERC20Bridge.sol/ChildERC20Bridge.json', 'utf8')); console.log("Deploy child bridge impl..."); - let childBridgeImpl = await helper.deployChildContract(childBridgeImplObj, adminWallet); + let childBridgeImpl = await helper.deployChildContract(childBridgeImplObj, adminWallet, adminWallet.address); console.log("Transaction submitted: ", JSON.stringify(childBridgeImpl.deployTransaction, null, 2)); await helper.waitForReceipt(childBridgeImpl.deployTransaction.hash, childProvider); console.log("Deployed to CHILD_BRIDGE_IMPL_ADDRESS: ", childBridgeImpl.address); @@ -90,7 +90,7 @@ exports.deployChildContracts = async () => { // Deploy child adaptor impl let childAdaptorImplObj = JSON.parse(fs.readFileSync('../../out/ChildAxelarBridgeAdaptor.sol/ChildAxelarBridgeAdaptor.json', 'utf8')); console.log("Deploy child adaptor impl..."); - let childAdaptorImpl = await helper.deployChildContract(childAdaptorImplObj, adminWallet, childGatewayAddr); + let childAdaptorImpl = await helper.deployChildContract(childAdaptorImplObj, adminWallet, childGatewayAddr, adminWallet.address); console.log("Transaction submitted: ", JSON.stringify(childAdaptorImpl.deployTransaction, null, 2)); await helper.waitForReceipt(childAdaptorImpl.deployTransaction.hash, childProvider); console.log("Deployed to CHILD_ADAPTOR_IMPL_ADDRESS: ", childAdaptorImpl.address); diff --git a/scripts/deploy/root_deployment.js b/scripts/deploy/root_deployment.js index 6363d2be..730c5d9c 100644 --- a/scripts/deploy/root_deployment.js +++ b/scripts/deploy/root_deployment.js @@ -58,7 +58,7 @@ exports.deployRootContracts = async () => { // Deploy root bridge impl let rootBridgeImplObj = JSON.parse(fs.readFileSync('../../out/RootERC20BridgeFlowRate.sol/RootERC20BridgeFlowRate.json', 'utf8')); console.log("Deploy root bridge impl..."); - let rootBridgeImpl = await helper.deployRootContract(rootBridgeImplObj, adminWallet); + let rootBridgeImpl = await helper.deployRootContract(rootBridgeImplObj, adminWallet, adminWallet.address); console.log("Transaction submitted: ", JSON.stringify(rootBridgeImpl.deployTransaction, null, 2)); await helper.waitForReceipt(rootBridgeImpl.deployTransaction.hash, rootProvider); console.log("Deployed to ROOT_BRIDGE_IMPL_ADDRESS: ", rootBridgeImpl.address); @@ -74,7 +74,7 @@ exports.deployRootContracts = async () => { // Deploy root adaptor impl let rootAdaptorImplObj = JSON.parse(fs.readFileSync('../../out/RootAxelarBridgeAdaptor.sol/RootAxelarBridgeAdaptor.json', 'utf8')); console.log("Deploy root adaptor impl..."); - let rootAdaptorImpl = await helper.deployRootContract(rootAdaptorImplObj, adminWallet, rootGatewayAddr); + let rootAdaptorImpl = await helper.deployRootContract(rootAdaptorImplObj, adminWallet, rootGatewayAddr, adminWallet.address); console.log("Transaction submitted: ", JSON.stringify(rootAdaptorImpl.deployTransaction, null, 2)); await helper.waitForReceipt(rootAdaptorImpl.deployTransaction.hash, rootProvider); console.log("Deployed to ROOT_ADAPTOR_IMPL_ADDRESS: ", rootAdaptorImpl.address); From 8c5a91b04e5bf5966f83cec1533e105d1e065c83 Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Tue, 12 Dec 2023 12:37:30 +1100 Subject: [PATCH 033/155] Add comments to EIP712MetaTransaction --- src/lib/EIP712MetaTransaction.sol | 33 ++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/lib/EIP712MetaTransaction.sol b/src/lib/EIP712MetaTransaction.sol index 309f9b41..b42f8ced 100644 --- a/src/lib/EIP712MetaTransaction.sol +++ b/src/lib/EIP712MetaTransaction.sol @@ -8,6 +8,7 @@ contract EIP712MetaTransaction is EIP712Upgradeable { bytes32 private constant META_TRANSACTION_TYPEHASH = keccak256(bytes("MetaTransaction(uint256 nonce,address from,bytes functionSignature)")); + /// @dev Event emitted when a meta transaction is successfully executed. event MetaTransactionExecuted(address userAddress, address relayerAddress, bytes functionSignature); mapping(address => uint256) private nonces; @@ -23,6 +24,16 @@ contract EIP712MetaTransaction is EIP712Upgradeable { bytes functionSignature; } + /** + * @notice Executes a meta transaction on behalf of the user. + * @dev This function allows a user to sign a transaction off-chain and have it executed by another entity. + * @param userAddress The address of the user initiating the transaction. + * @param functionSignature The signature of the function to be executed. + * @param sigR Part of the signature data. + * @param sigS Part of the signature data. + * @param sigV Recovery byte of the signature. + * @return returnData The bytes returned from the executed function. + */ function executeMetaTransaction( address userAddress, bytes calldata functionSignature, @@ -53,12 +64,18 @@ contract EIP712MetaTransaction is EIP712Upgradeable { } /** - * @dev Invalidates next "offset" number of nonces for the calling address + * @notice Invalidates next "offset" number of nonces for the calling address + * @param offset The number of nonces, from the current nonce, to invalidate. */ function invalidateNext(uint256 offset) external { nonces[msg.sender] += offset; } + /** + * @notice Retrieves the current nonce for a user. + * @param user The address of the user. + * @return nonce The current nonce of the user. + */ function getNonce(address user) external view returns (uint256 nonce) { nonce = nonces[user]; } @@ -79,11 +96,21 @@ contract EIP712MetaTransaction is EIP712Upgradeable { return sender; } + /** + * @notice Verifies the signature of a meta transaction. + * @param user The address of the user. + * @param metaTx The meta transaction struct. + * @param sigR Part of the signature data. + * @param sigS Part of the signature data. + * @param sigV Recovery byte of the signature. + * @return True if the signature is valid, false otherwise. + */ function _verify(address user, MetaTransaction memory metaTx, bytes32 sigR, bytes32 sigS, uint8 sigV) private view returns (bool) { + // The inclusion of a user specific nonce removes signature malleability concerns address signer = ecrecover(_hashTypedDataV4(_hashMetaTransaction(metaTx)), sigV, sigR, sigS); require(signer != address(0), "Invalid signature"); return signer == user; @@ -95,12 +122,16 @@ contract EIP712MetaTransaction is EIP712Upgradeable { ); } + /** + * @dev Extract the first four bytes from `inBytes` + */ function _convertBytesToBytes4(bytes memory inBytes) private pure returns (bytes4 outBytes4) { if (inBytes.length == 0) { return 0x0; } // slither-disable-next-line assembly assembly { + // extract the first 4 bytes from inBytes // solhint-disable no-inline-assembly outBytes4 := mload(add(inBytes, 32)) } From e0009abc2b4b0dc36306e50685ada82b22b290c8 Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Tue, 12 Dec 2023 12:58:35 +1100 Subject: [PATCH 034/155] Rename for brevity --- src/child/ChildAxelarBridgeAdaptor.sol | 20 +++++++++----------- src/root/RootAxelarBridgeAdaptor.sol | 19 +++++++++---------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/src/child/ChildAxelarBridgeAdaptor.sol b/src/child/ChildAxelarBridgeAdaptor.sol index 87d54cd6..a67878ad 100644 --- a/src/child/ChildAxelarBridgeAdaptor.sol +++ b/src/child/ChildAxelarBridgeAdaptor.sol @@ -53,25 +53,23 @@ contract ChildAxelarBridgeAdaptor is /** * @notice Initialization function for ChildAxelarBridgeAdaptor. - * @param _childAxelarAdapterRoles Struct containing addresses of roles. + * @param _adaptorRoles Struct containing addresses of roles. * @param _childBridge Address of child bridge contract. * @param _rootChainId Axelar's string ID for the root chain. * @param _rootBridgeAdaptor Address of the bridge adaptor on the root chain. * @param _gasService Address of Axelar Gas Service contract. */ function initialize( - InitializationRoles memory _childAxelarAdapterRoles, + InitializationRoles memory _adaptorRoles, address _childBridge, string memory _rootChainId, string memory _rootBridgeAdaptor, address _gasService ) external initializer { if ( - _childBridge == address(0) || _gasService == address(0) - || _childAxelarAdapterRoles.defaultAdmin == address(0) - || _childAxelarAdapterRoles.bridgeManager == address(0) - || _childAxelarAdapterRoles.gasServiceManager == address(0) - || _childAxelarAdapterRoles.targetManager == address(0) + _childBridge == address(0) || _gasService == address(0) || _adaptorRoles.defaultAdmin == address(0) + || _adaptorRoles.bridgeManager == address(0) || _adaptorRoles.gasServiceManager == address(0) + || _adaptorRoles.targetManager == address(0) ) { revert ZeroAddress(); } @@ -85,10 +83,10 @@ contract ChildAxelarBridgeAdaptor is } __AccessControl_init(); - _grantRole(DEFAULT_ADMIN_ROLE, _childAxelarAdapterRoles.defaultAdmin); - _grantRole(BRIDGE_MANAGER_ROLE, _childAxelarAdapterRoles.bridgeManager); - _grantRole(GAS_SERVICE_MANAGER_ROLE, _childAxelarAdapterRoles.gasServiceManager); - _grantRole(TARGET_MANAGER_ROLE, _childAxelarAdapterRoles.targetManager); + _grantRole(DEFAULT_ADMIN_ROLE, _adaptorRoles.defaultAdmin); + _grantRole(BRIDGE_MANAGER_ROLE, _adaptorRoles.bridgeManager); + _grantRole(GAS_SERVICE_MANAGER_ROLE, _adaptorRoles.gasServiceManager); + _grantRole(TARGET_MANAGER_ROLE, _adaptorRoles.targetManager); childBridge = IChildERC20Bridge(_childBridge); rootChainId = _rootChainId; diff --git a/src/root/RootAxelarBridgeAdaptor.sol b/src/root/RootAxelarBridgeAdaptor.sol index f0c38bf3..bc32f711 100644 --- a/src/root/RootAxelarBridgeAdaptor.sol +++ b/src/root/RootAxelarBridgeAdaptor.sol @@ -54,24 +54,23 @@ contract RootAxelarBridgeAdaptor is /** * @notice Initialization function for RootAxelarBridgeAdaptor. - * @param _rootAxelarAdapterRoles Struct containing addresses of roles. + * @param _adaptorRoles Struct containing addresses of roles. * @param _rootBridge Address of root bridge contract. * @param _childChainId Axelar's ID for the child chain. * @param _childBridgeAdaptor Address of the bridge adaptor on the child chain. * @param _gasService Address of Axelar Gas Service contract. */ function initialize( - InitializationRoles memory _rootAxelarAdapterRoles, + InitializationRoles memory _adaptorRoles, address _rootBridge, string memory _childChainId, string memory _childBridgeAdaptor, address _gasService ) public initializer { if ( - _rootBridge == address(0) || _gasService == address(0) || _rootAxelarAdapterRoles.defaultAdmin == address(0) - || _rootAxelarAdapterRoles.bridgeManager == address(0) - || _rootAxelarAdapterRoles.gasServiceManager == address(0) - || _rootAxelarAdapterRoles.targetManager == address(0) + _rootBridge == address(0) || _gasService == address(0) || _adaptorRoles.defaultAdmin == address(0) + || _adaptorRoles.bridgeManager == address(0) || _adaptorRoles.gasServiceManager == address(0) + || _adaptorRoles.targetManager == address(0) ) { revert ZeroAddresses(); } @@ -86,10 +85,10 @@ contract RootAxelarBridgeAdaptor is __AccessControl_init(); - _grantRole(DEFAULT_ADMIN_ROLE, _rootAxelarAdapterRoles.defaultAdmin); - _grantRole(BRIDGE_MANAGER_ROLE, _rootAxelarAdapterRoles.bridgeManager); - _grantRole(GAS_SERVICE_MANAGER_ROLE, _rootAxelarAdapterRoles.gasServiceManager); - _grantRole(TARGET_MANAGER_ROLE, _rootAxelarAdapterRoles.targetManager); + _grantRole(DEFAULT_ADMIN_ROLE, _adaptorRoles.defaultAdmin); + _grantRole(BRIDGE_MANAGER_ROLE, _adaptorRoles.bridgeManager); + _grantRole(GAS_SERVICE_MANAGER_ROLE, _adaptorRoles.gasServiceManager); + _grantRole(TARGET_MANAGER_ROLE, _adaptorRoles.targetManager); rootBridge = IRootERC20Bridge(_rootBridge); childChainId = _childChainId; From 6886523ac28046ddb029f047af4ca52fbebfb1a2 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Tue, 12 Dec 2023 13:53:04 +0800 Subject: [PATCH 035/155] Add comments --- src/child/ChildAxelarBridgeAdaptor.sol | 5 +++++ src/child/ChildERC20Bridge.sol | 4 ++++ src/root/RootAxelarBridgeAdaptor.sol | 5 +++++ src/root/RootERC20Bridge.sol | 4 ++++ 4 files changed, 18 insertions(+) diff --git a/src/child/ChildAxelarBridgeAdaptor.sol b/src/child/ChildAxelarBridgeAdaptor.sol index 9fecc251..ef706b42 100644 --- a/src/child/ChildAxelarBridgeAdaptor.sol +++ b/src/child/ChildAxelarBridgeAdaptor.sol @@ -52,6 +52,11 @@ contract ChildAxelarBridgeAdaptor is /// @notice Address of the authorized initializer. address public immutable initializerAddress; + /** + * @notice Constructs the ChildAxelarBridgeAdaptor contract. + * @param _gateway The address of the Axelar gateway contract. + * @param _initializerAddress The address of the authorized initializer. + */ constructor(address _gateway, address _initializerAddress) AxelarExecutable(_gateway) { initializerAddress = _initializerAddress; } diff --git a/src/child/ChildERC20Bridge.sol b/src/child/ChildERC20Bridge.sol index c84de57d..c12b6b3f 100644 --- a/src/child/ChildERC20Bridge.sol +++ b/src/child/ChildERC20Bridge.sol @@ -83,6 +83,10 @@ contract ChildERC20Bridge is _; } + /** + * @notice Constructs the ChildERC20Bridge contract. + * @param _initializerAddress The address of the authorized initializer. + */ constructor(address _initializerAddress) { initializerAddress = _initializerAddress; } diff --git a/src/root/RootAxelarBridgeAdaptor.sol b/src/root/RootAxelarBridgeAdaptor.sol index 3dad0e17..de20c109 100644 --- a/src/root/RootAxelarBridgeAdaptor.sol +++ b/src/root/RootAxelarBridgeAdaptor.sol @@ -53,6 +53,11 @@ contract RootAxelarBridgeAdaptor is /// @notice Address of the authorized initializer. address public immutable initializerAddress; + /** + * @notice Constructs the RootAxelarBridgeAdaptor contract. + * @param _gateway The address of the Axelar gateway contract. + * @param _initializerAddress The address of the authorized initializer. + */ constructor(address _gateway, address _initializerAddress) AxelarExecutable(_gateway) { initializerAddress = _initializerAddress; } diff --git a/src/root/RootERC20Bridge.sol b/src/root/RootERC20Bridge.sol index b5ef7419..89c0271b 100644 --- a/src/root/RootERC20Bridge.sol +++ b/src/root/RootERC20Bridge.sol @@ -96,6 +96,10 @@ contract RootERC20Bridge is _; } + /** + * @notice Constructs the RootERC20Bridge contract. + * @param _initializerAddress The address of the authorized initializer. + */ constructor(address _initializerAddress) { initializerAddress = _initializerAddress; } From 69aadb260bffad032e7e5a46e8e4b417a3361eb3 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Tue, 12 Dec 2023 15:11:15 +0800 Subject: [PATCH 036/155] Check for zero address --- src/child/ChildAxelarBridgeAdaptor.sol | 3 +++ src/child/ChildERC20Bridge.sol | 3 +++ src/root/RootAxelarBridgeAdaptor.sol | 3 +++ src/root/RootERC20Bridge.sol | 3 +++ test/unit/child/ChildAxelarBridgeAdaptor.t.sol | 5 +++++ test/unit/child/ChildERC20Bridge.t.sol | 5 +++++ test/unit/root/RootAxelarBridgeAdaptor.t.sol | 5 +++++ test/unit/root/RootERC20Bridge.t.sol | 5 +++++ 8 files changed, 32 insertions(+) diff --git a/src/child/ChildAxelarBridgeAdaptor.sol b/src/child/ChildAxelarBridgeAdaptor.sol index ef706b42..9888d4f2 100644 --- a/src/child/ChildAxelarBridgeAdaptor.sol +++ b/src/child/ChildAxelarBridgeAdaptor.sol @@ -58,6 +58,9 @@ contract ChildAxelarBridgeAdaptor is * @param _initializerAddress The address of the authorized initializer. */ constructor(address _gateway, address _initializerAddress) AxelarExecutable(_gateway) { + if (_initializerAddress == address(0)) { + revert ZeroAddress(); + } initializerAddress = _initializerAddress; } diff --git a/src/child/ChildERC20Bridge.sol b/src/child/ChildERC20Bridge.sol index c12b6b3f..15a42018 100644 --- a/src/child/ChildERC20Bridge.sol +++ b/src/child/ChildERC20Bridge.sol @@ -88,6 +88,9 @@ contract ChildERC20Bridge is * @param _initializerAddress The address of the authorized initializer. */ constructor(address _initializerAddress) { + if (_initializerAddress == address(0)) { + revert ZeroAddress(); + } initializerAddress = _initializerAddress; } diff --git a/src/root/RootAxelarBridgeAdaptor.sol b/src/root/RootAxelarBridgeAdaptor.sol index de20c109..df31dea1 100644 --- a/src/root/RootAxelarBridgeAdaptor.sol +++ b/src/root/RootAxelarBridgeAdaptor.sol @@ -59,6 +59,9 @@ contract RootAxelarBridgeAdaptor is * @param _initializerAddress The address of the authorized initializer. */ constructor(address _gateway, address _initializerAddress) AxelarExecutable(_gateway) { + if (_initializerAddress == address(0)) { + revert ZeroAddresses(); + } initializerAddress = _initializerAddress; } diff --git a/src/root/RootERC20Bridge.sol b/src/root/RootERC20Bridge.sol index 89c0271b..a68bd1b0 100644 --- a/src/root/RootERC20Bridge.sol +++ b/src/root/RootERC20Bridge.sol @@ -101,6 +101,9 @@ contract RootERC20Bridge is * @param _initializerAddress The address of the authorized initializer. */ constructor(address _initializerAddress) { + if (_initializerAddress == address(0)) { + revert ZeroAddress(); + } initializerAddress = _initializerAddress; } diff --git a/test/unit/child/ChildAxelarBridgeAdaptor.t.sol b/test/unit/child/ChildAxelarBridgeAdaptor.t.sol index 67ac77f5..87010030 100644 --- a/test/unit/child/ChildAxelarBridgeAdaptor.t.sol +++ b/test/unit/child/ChildAxelarBridgeAdaptor.t.sol @@ -76,6 +76,11 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro ); } + function test_RevertIf_ZeroInitializerIsGiven() public { + vm.expectRevert(ZeroAddress.selector); + new ChildAxelarBridgeAdaptor(GATEWAY_ADDRESS, address(0)); + } + function test_RevertIf_InitializeWithUnauthorizedInitializer() public { ChildAxelarBridgeAdaptor newAdaptor = new ChildAxelarBridgeAdaptor(GATEWAY_ADDRESS, address(this)); vm.prank(address(0x1234)); diff --git a/test/unit/child/ChildERC20Bridge.t.sol b/test/unit/child/ChildERC20Bridge.t.sol index 52a08a6f..e93f334e 100644 --- a/test/unit/child/ChildERC20Bridge.t.sol +++ b/test/unit/child/ChildERC20Bridge.t.sol @@ -164,6 +164,11 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B assert(childBridge.rootTokenToChildToken(NATIVE_ETH) != address(0)); } + function test_RevertIf_ZeroInitializerIsGiven() public { + vm.expectRevert(ZeroAddress.selector); + new ChildERC20Bridge(address(0)); + } + function test_RevertIf_InitializeWithUnauthorizedInitializer() public { ChildERC20Bridge bridge = new ChildERC20Bridge(address(this)); vm.prank(address(0x1234)); diff --git a/test/unit/root/RootAxelarBridgeAdaptor.t.sol b/test/unit/root/RootAxelarBridgeAdaptor.t.sol index cf9e2be6..a9c3451c 100644 --- a/test/unit/root/RootAxelarBridgeAdaptor.t.sol +++ b/test/unit/root/RootAxelarBridgeAdaptor.t.sol @@ -76,6 +76,11 @@ contract RootAxelarBridgeAdaptorTest is Test, IRootAxelarBridgeAdaptorEvents, IR ); } + function test_RevertIf_ZeroInitializerIsGiven() public { + vm.expectRevert(ZeroAddresses.selector); + new RootAxelarBridgeAdaptor(address(mockAxelarGateway), address(0)); + } + function test_RevertIf_InitializeWithUnauthorizedInitializer() public { RootAxelarBridgeAdaptor newAdaptor = new RootAxelarBridgeAdaptor(address(mockAxelarGateway), address(this)); vm.prank(address(0x1234)); diff --git a/test/unit/root/RootERC20Bridge.t.sol b/test/unit/root/RootERC20Bridge.t.sol index 8885346e..0f20e18e 100644 --- a/test/unit/root/RootERC20Bridge.t.sol +++ b/test/unit/root/RootERC20Bridge.t.sol @@ -133,6 +133,11 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid test_NativeTransferFromWETH(); } + function test_RevertIf_ZeroInitializerIsGiven() public { + vm.expectRevert(ZeroAddress.selector); + new RootERC20Bridge(address(0)); + } + function test_RevertIf_InitializeWithUnauthorizedInitializer() public { RootERC20Bridge bridge = new RootERC20Bridge(address(this)); IRootERC20Bridge.InitializationRoles memory roles = IRootERC20Bridge.InitializationRoles({ From f9f1c8a71cf1d037445f404f4643411b58820d00 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Wed, 13 Dec 2023 08:28:35 +0800 Subject: [PATCH 037/155] Fix CI --- scripts/deploy/child_deployment.ts | 4 ++-- scripts/deploy/root_deployment.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/deploy/child_deployment.ts b/scripts/deploy/child_deployment.ts index ff17a8b6..a201f44e 100644 --- a/scripts/deploy/child_deployment.ts +++ b/scripts/deploy/child_deployment.ts @@ -57,7 +57,7 @@ export async function deployChildContracts() { childBridgeImpl = getContract("ChildERC20Bridge", childContracts.CHILD_BRIDGE_IMPL_ADDRESS, childProvider); } else { console.log("Deploy child bridge impl..."); - childBridgeImpl = await deployChildContract("ChildERC20Bridge", childDeployerWallet, null); + childBridgeImpl = await deployChildContract("ChildERC20Bridge", childDeployerWallet, null, deployerAddr); console.log("Transaction submitted: ", JSON.stringify(childBridgeImpl.deployTransaction, null, 2)); await waitForReceipt(childBridgeImpl.deployTransaction.hash, childProvider); } @@ -72,7 +72,7 @@ export async function deployChildContracts() { childAdaptorImpl = getContract("ChildAxelarBridgeAdaptor", childContracts.CHILD_ADAPTOR_IMPL_ADDRESS, childProvider); } else { console.log("Deploy child adaptor impl..."); - childAdaptorImpl = await deployChildContract("ChildAxelarBridgeAdaptor", childDeployerWallet, null, childGatewayAddr); + childAdaptorImpl = await deployChildContract("ChildAxelarBridgeAdaptor", childDeployerWallet, null, childGatewayAddr, deployerAddr); console.log("Transaction submitted: ", JSON.stringify(childAdaptorImpl.deployTransaction, null, 2)); await waitForReceipt(childAdaptorImpl.deployTransaction.hash, childProvider); } diff --git a/scripts/deploy/root_deployment.ts b/scripts/deploy/root_deployment.ts index 59de8e83..9a56f0cc 100644 --- a/scripts/deploy/root_deployment.ts +++ b/scripts/deploy/root_deployment.ts @@ -42,7 +42,7 @@ export async function deployRootContracts() { rootBridgeImpl = getContract("RootERC20BridgeFlowRate", rootContracts.ROOT_BRIDGE_IMPL_ADDRESS, rootProvider); } else { console.log("Deploy root bridge impl..."); - rootBridgeImpl = await deployRootContract("RootERC20BridgeFlowRate", rootDeployerWallet, null); + rootBridgeImpl = await deployRootContract("RootERC20BridgeFlowRate", rootDeployerWallet, null, deployerAddr); console.log("Transaction submitted: ", JSON.stringify(rootBridgeImpl.deployTransaction, null, 2)); await waitForReceipt(rootBridgeImpl.deployTransaction.hash, rootProvider); } @@ -57,7 +57,7 @@ export async function deployRootContracts() { rootAdaptorImpl = getContract("RootAxelarBridgeAdaptor", rootContracts.ROOT_ADAPTOR_IMPL_ADDRESS, rootProvider); } else { console.log("Deploy root adaptor impl..."); - rootAdaptorImpl = await deployRootContract("RootAxelarBridgeAdaptor", rootDeployerWallet, null, rootGatewayAddr); + rootAdaptorImpl = await deployRootContract("RootAxelarBridgeAdaptor", rootDeployerWallet, null, rootGatewayAddr, deployerAddr); console.log("Transaction submitted: ", JSON.stringify(rootAdaptorImpl.deployTransaction, null, 2)); await waitForReceipt(rootAdaptorImpl.deployTransaction.hash, rootProvider); } From e5e48a87fd9309f391ebdbe3d7e7062aa20af373 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Wed, 13 Dec 2023 14:56:29 +0800 Subject: [PATCH 038/155] Add role on L2 --- scripts/bootstrap/.env.example | 10 ++++++--- scripts/bootstrap/9_test_preparation.ts | 6 +++--- scripts/bootstrap/README.md | 10 ++++++--- scripts/deploy/.env.example | 10 ++++++--- scripts/deploy/README.md | 10 ++++++--- scripts/deploy/child_initialisation.ts | 18 +++++++++------- scripts/deploy/root_initialisation.ts | 28 ++++++++++++------------- scripts/localdev/.env.local | 10 ++++++--- 8 files changed, 62 insertions(+), 40 deletions(-) diff --git a/scripts/bootstrap/.env.example b/scripts/bootstrap/.env.example index 5c6944ed..2afff782 100644 --- a/scripts/bootstrap/.env.example +++ b/scripts/bootstrap/.env.example @@ -44,9 +44,13 @@ PASSPORT_NONCE_RESERVER_FUND= ## The maximum amount of IMX that can be deposited to L2, unit is in IMX or 10^18 Wei. IMX_DEPOSIT_LIMIT= ## The privileged transaction Multisig address on the root chain. -PRIVILEGED_ROOT_MULTISIG_ADDR= -# The pauser address on the root chain. -ROOT_PAUSER_ADDR= +ROOT_PRIVILEGED_MULTISIG_ADDR= +# The break glass signer address on the root chain. +ROOT_BREAKGLASS_ADDR= +## The privileged transaction Multisig address on the child chain. +CHILD_PRIVILEGED_MULTISIG_ADDR= +# The break glass signer address on the child chain. +CHILD_BREAKGLASS_ADDR= ## The capacity of the rate limit policy of IMX token, unit is in 10^18. RATE_LIMIT_IMX_CAPACITY= ## The refill rate of the rate limit policy of IMX token, unit is in 10^18. diff --git a/scripts/bootstrap/9_test_preparation.ts b/scripts/bootstrap/9_test_preparation.ts index d0c3a72a..01ed75c2 100644 --- a/scripts/bootstrap/9_test_preparation.ts +++ b/scripts/bootstrap/9_test_preparation.ts @@ -13,7 +13,7 @@ async function run() { let rootChainID = requireEnv("ROOT_CHAIN_ID"); let deployerSecret = requireEnv("DEPLOYER_SECRET"); let testAccountKey = requireEnv("TEST_ACCOUNT_SECRET"); - let rootMultisigAddr = requireEnv("PRIVILEGED_ROOT_MULTISIG_ADDR"); + let rootPrivilegedMultisig = requireEnv("ROOT_PRIVILEGED_MULTISIG_ADDR"); // Get deployer address const rootProvider = new ethers.providers.JsonRpcProvider(rootRPCURL, Number(rootChainID)); @@ -79,9 +79,9 @@ async function run() { await waitForReceipt(resp.hash, rootProvider); // Print summary - console.log("Does multisig have DEFAULT_ADMIN: ", await rootBridge.hasRole(await rootBridge.DEFAULT_ADMIN_ROLE(), rootMultisigAddr)); + console.log("Does multisig have DEFAULT_ADMIN: ", await rootBridge.hasRole(await rootBridge.DEFAULT_ADMIN_ROLE(), rootPrivilegedMultisig)); console.log("Does deployer have DEFAULT_ADMIN: ", await rootBridge.hasRole(await rootBridge.DEFAULT_ADMIN_ROLE(), deployerAddr)); - console.log("Does multisig have RATE_ADMIN: ", await rootBridge.hasRole(utils.keccak256(utils.toUtf8Bytes("RATE")), rootMultisigAddr)); + console.log("Does multisig have RATE_ADMIN: ", await rootBridge.hasRole(utils.keccak256(utils.toUtf8Bytes("RATE")), rootPrivilegedMultisig)); console.log("Does deployer have RATE_ADMIN: ", await rootBridge.hasRole(utils.keccak256(utils.toUtf8Bytes("RATE")), deployerAddr)); console.log("=======End Test Preparation======="); diff --git a/scripts/bootstrap/README.md b/scripts/bootstrap/README.md index 40f68e72..078950e7 100644 --- a/scripts/bootstrap/README.md +++ b/scripts/bootstrap/README.md @@ -68,9 +68,13 @@ PASSPORT_NONCE_RESERVER_FUND= ## The maximum amount of IMX that can be deposited to L2, unit is in IMX or 10^18 Wei. IMX_DEPOSIT_LIMIT= ## The privileged transaction Multisig address on the root chain. -PRIVILEGED_ROOT_MULTISIG_ADDR= -# The pauser address on the root chain. -ROOT_PAUSER_ADDR= +ROOT_PRIVILEGED_MULTISIG_ADDR= +# The break glass signer address on the root chain. +ROOT_BREAKGLASS_ADDR= +## The privileged transaction Multisig address on the child chain. +CHILD_PRIVILEGED_MULTISIG_ADDR= +# The break glass signer address on the child chain. +CHILD_BREAKGLASS_ADDR= ## The capacity of the rate limit policy of IMX token, unit is in 10^18. RATE_LIMIT_IMX_CAPACITY= ## The refill rate of the rate limit policy of IMX token, unit is in 10^18. diff --git a/scripts/deploy/.env.example b/scripts/deploy/.env.example index d58b72f7..3e5f83a2 100644 --- a/scripts/deploy/.env.example +++ b/scripts/deploy/.env.example @@ -43,9 +43,13 @@ PASSPORT_NONCE_RESERVER_FUND= ## The maximum amount of IMX that can be deposited to L2, unit is in IMX or 10^18 Wei. IMX_DEPOSIT_LIMIT= ## The privileged transaction Multisig address on the root chain. -PRIVILEGED_ROOT_MULTISIG_ADDR= -# The pauser address on the root chain. -ROOT_PAUSER_ADDR= +ROOT_PRIVILEGED_MULTISIG_ADDR= +# The break glass signer address on the root chain. +ROOT_BREAKGLASS_ADDR= +## The privileged transaction Multisig address on the child chain. +CHILD_PRIVILEGED_MULTISIG_ADDR= +# The break glass signer address on the child chain. +CHILD_BREAKGLASS_ADDR= ## The capacity of the rate limit policy of IMX token, unit is in 10^18. RATE_LIMIT_IMX_CAPACITY= ## The refill rate of the rate limit policy of IMX token, unit is in 10^18. diff --git a/scripts/deploy/README.md b/scripts/deploy/README.md index 75789ae3..6884186f 100644 --- a/scripts/deploy/README.md +++ b/scripts/deploy/README.md @@ -57,9 +57,13 @@ PASSPORT_NONCE_RESERVER_FUND= ## The maximum amount of IMX that can be deposited to L2, unit is in IMX or 10^18 Wei. IMX_DEPOSIT_LIMIT= ## The privileged transaction Multisig address on the root chain. -PRIVILEGED_ROOT_MULTISIG_ADDR= -# The pauser address on the root chain. -ROOT_PAUSER_ADDR= +ROOT_PRIVILEGED_MULTISIG_ADDR= +# The break glass signer address on the root chain. +ROOT_BREAKGLASS_ADDR= +## The privileged transaction Multisig address on the child chain. +CHILD_PRIVILEGED_MULTISIG_ADDR= +# The break glass signer address on the child chain. +CHILD_BREAKGLASS_ADDR= ## The capacity of the rate limit policy of IMX token, unit is in 10^18. RATE_LIMIT_IMX_CAPACITY= ## The refill rate of the rate limit policy of IMX token, unit is in 10^18. diff --git a/scripts/deploy/child_initialisation.ts b/scripts/deploy/child_initialisation.ts index c7d8cc0d..8be95c1a 100644 --- a/scripts/deploy/child_initialisation.ts +++ b/scripts/deploy/child_initialisation.ts @@ -14,6 +14,8 @@ export async function initialiseChildContracts() { let childGasServiceAddr = requireEnv("CHILD_GAS_SERVICE_ADDRESS"); let multisigAddr = requireEnv("MULTISIG_CONTRACT_ADDRESS"); let rootIMXAddr = requireEnv("ROOT_IMX_ADDR"); + let childPrivilegedMultisig = requireEnv("CHILD_PRIVILEGED_MULTISIG_ADDR"); + let childBreakglass = requireEnv("CHILD_BREAKGLASS_ADDR"); // Read from contract file. let childContracts = getChildContracts(); @@ -53,10 +55,10 @@ export async function initialiseChildContracts() { let [priorityFee, maxFee] = await getFee(childProvider); let resp = await childBridge.connect(childDeployerWallet).initialize( { - defaultAdmin: deployerAddr, - pauser: deployerAddr, - unpauser: deployerAddr, - adaptorManager: deployerAddr, + defaultAdmin: childPrivilegedMultisig, + pauser: childBreakglass, + unpauser: childPrivilegedMultisig, + adaptorManager: childPrivilegedMultisig, initialDepositor: deployerAddr, treasuryManager: multisigAddr, }, @@ -77,10 +79,10 @@ export async function initialiseChildContracts() { [priorityFee, maxFee] = await getFee(childProvider); resp = await childAdaptor.connect(childDeployerWallet).initialize( { - defaultAdmin: deployerAddr, - bridgeManager: deployerAddr, - gasServiceManager: deployerAddr, - targetManager: deployerAddr, + defaultAdmin: childPrivilegedMultisig, + bridgeManager: childPrivilegedMultisig, + gasServiceManager: childPrivilegedMultisig, + targetManager: childPrivilegedMultisig, }, childBridgeAddr, rootChainName, diff --git a/scripts/deploy/root_initialisation.ts b/scripts/deploy/root_initialisation.ts index e95dd67d..4b542e8f 100644 --- a/scripts/deploy/root_initialisation.ts +++ b/scripts/deploy/root_initialisation.ts @@ -15,8 +15,8 @@ export async function initialiseRootContracts() { let rootIMXAddr = requireEnv("ROOT_IMX_ADDR"); let rootWETHAddr = requireEnv("ROOT_WETH_ADDR"); let imxDepositLimit = requireEnv("IMX_DEPOSIT_LIMIT"); - let rootMultisigAddr = requireEnv("PRIVILEGED_ROOT_MULTISIG_ADDR"); - let rootPauser = requireEnv("ROOT_PAUSER_ADDR"); + let rootPrivilegedMultisig = requireEnv("ROOT_PRIVILEGED_MULTISIG_ADDR"); + let rootBreakglass = requireEnv("ROOT_BREAKGLASS_ADDR"); let rateLimitIMXCap = requireEnv("RATE_LIMIT_IMX_CAPACITY"); let rateLimitIMXRefill = requireEnv("RATE_LIMIT_IMX_REFILL_RATE"); let rateLimitIMXLargeThreshold = requireEnv("RATE_LIMIT_IMX_LARGE_THRESHOLD"); @@ -72,10 +72,10 @@ export async function initialiseRootContracts() { let resp = await rootBridge.connect(rootDeployerWallet)["initialize((address,address,address,address,address),address,address,address,address,address,uint256,address)"]( { defaultAdmin: deployerAddr, - pauser: rootPauser, - unpauser: rootMultisigAddr, - variableManager: rootMultisigAddr, - adaptorManager: rootMultisigAddr, + pauser: rootBreakglass, + unpauser: rootPrivilegedMultisig, + variableManager: rootPrivilegedMultisig, + adaptorManager: rootPrivilegedMultisig, }, rootAdaptorAddr, childBridgeAddr, @@ -156,19 +156,19 @@ export async function initialiseRootContracts() { // Grant roles console.log("Grant RATE_CONTROL_ROLE to multisig...") - resp = await rootBridge.connect(rootDeployerWallet).grantRole(utils.keccak256(utils.toUtf8Bytes("RATE")), rootMultisigAddr); + resp = await rootBridge.connect(rootDeployerWallet).grantRole(utils.keccak256(utils.toUtf8Bytes("RATE")), rootPrivilegedMultisig); console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); await waitForReceipt(resp.hash, rootProvider); console.log("Grant DEFAULT_ADMIN to multisig...") - resp = await rootBridge.connect(rootDeployerWallet).grantRole(await rootBridge.DEFAULT_ADMIN_ROLE(), rootMultisigAddr); + resp = await rootBridge.connect(rootDeployerWallet).grantRole(await rootBridge.DEFAULT_ADMIN_ROLE(), rootPrivilegedMultisig); console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); await waitForReceipt(resp.hash, rootProvider); // Print summary - console.log("Does multisig have DEFAULT_ADMIN: ", await rootBridge.hasRole(await rootBridge.DEFAULT_ADMIN_ROLE(), rootMultisigAddr)); + console.log("Does multisig have DEFAULT_ADMIN: ", await rootBridge.hasRole(await rootBridge.DEFAULT_ADMIN_ROLE(), rootPrivilegedMultisig)); console.log("Does deployer have DEFAULT_ADMIN: ", await rootBridge.hasRole(await rootBridge.DEFAULT_ADMIN_ROLE(), deployerAddr)); - console.log("Does multisig have RATE_ADMIN: ", await rootBridge.hasRole(utils.keccak256(utils.toUtf8Bytes("RATE")), rootMultisigAddr)); + console.log("Does multisig have RATE_ADMIN: ", await rootBridge.hasRole(utils.keccak256(utils.toUtf8Bytes("RATE")), rootPrivilegedMultisig)); console.log("Does deployer have RATE_ADMIN: ", await rootBridge.hasRole(utils.keccak256(utils.toUtf8Bytes("RATE")), deployerAddr)); // Initialise root adaptor @@ -176,10 +176,10 @@ export async function initialiseRootContracts() { let rootAdaptor = getContract("RootAxelarBridgeAdaptor", rootAdaptorAddr, rootProvider); resp = await rootAdaptor.connect(rootDeployerWallet).initialize( { - defaultAdmin: rootMultisigAddr, - bridgeManager: rootMultisigAddr, - gasServiceManager: rootMultisigAddr, - targetManager: rootMultisigAddr, + defaultAdmin: rootPrivilegedMultisig, + bridgeManager: rootPrivilegedMultisig, + gasServiceManager: rootPrivilegedMultisig, + targetManager: rootPrivilegedMultisig, }, rootBridgeAddr, childChainName, diff --git a/scripts/localdev/.env.local b/scripts/localdev/.env.local index da35f139..8633ddc7 100644 --- a/scripts/localdev/.env.local +++ b/scripts/localdev/.env.local @@ -44,9 +44,13 @@ PASSPORT_NONCE_RESERVER_FUND=100 ## The maximum amount of IMX that can be deposited to L2, unit is in IMX or 10^18 Wei. IMX_DEPOSIT_LIMIT=100000000 ## The privileged transaction multisig address on the root chain. -PRIVILEGED_ROOT_MULTISIG_ADDR=0x14dC79964da2C08b23698B3D3cc7Ca32193d9955 -# The pauser address on the root chain. -ROOT_PAUSER_ADDR=0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f +ROOT_PRIVILEGED_MULTISIG_ADDR=0x14dC79964da2C08b23698B3D3cc7Ca32193d9955 +# The break glass signer address on the root chain. +ROOT_BREAKGLASS_ADDR=0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f +## The privileged transaction Multisig address on the child chain. +CHILD_PRIVILEGED_MULTISIG_ADDR=0x14dC79964da2C08b23698B3D3cc7Ca32193d9955 +# The break glass signer address on the child chain. +CHILD_BREAKGLASS_ADDR=0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f ## The capacity of the rate limit policy of IMX token, unit is in 10^18. RATE_LIMIT_IMX_CAPACITY=15516 ## The refill rate of the rate limit policy of IMX token, unit is in 10^18. From 78ac63d12fa9b5d0e531e0020cdcf9d353798736 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Wed, 13 Dec 2023 22:25:16 +0800 Subject: [PATCH 039/155] Update e2e.yml --- .github/workflows/e2e.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 2f261b0e..17a3cdde 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -13,10 +13,10 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set Node.js 18.x + - name: Set Node.js 18.18.x uses: actions/setup-node@v3 with: - node-version: 18.x + node-version: 18.18.x - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 From b58a234ccf75e59433b8f5d081628ec1f390451c Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Thu, 14 Dec 2023 07:33:59 +0800 Subject: [PATCH 040/155] Add verification of contracts --- scripts/bootstrap/.env.example | 4 ++++ scripts/bootstrap/README.md | 4 ++++ scripts/bootstrap/verify.txt | 1 + scripts/deploy/child_deployment.ts | 9 ++++++++- scripts/deploy/root_deployment.ts | 10 ++++++++-- scripts/helpers/helpers.ts | 30 ++++++++++++++++++++++++++++++ 6 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 scripts/bootstrap/verify.txt diff --git a/scripts/bootstrap/.env.example b/scripts/bootstrap/.env.example index 2afff782..a8ed55a2 100644 --- a/scripts/bootstrap/.env.example +++ b/scripts/bootstrap/.env.example @@ -102,6 +102,10 @@ CHILD_GAS_SERVICE_ADDRESS= MULTISIG_CONTRACT_ADDRESS= ROOT_GATEWAY_ADDRESS= ROOT_GAS_SERVICE_ADDRESS= +## (Optional) to verify child contract after deployment +CHILD_CHAIN_BLOCKSCOUT_API_URL= +## (Optional) to verify root contract after deployment +ROOT_CHAIN_ETHERSCAN_API_KEY= # Set prior to bridge testing TEST_ACCOUNT_SECRET= \ No newline at end of file diff --git a/scripts/bootstrap/README.md b/scripts/bootstrap/README.md index 078950e7..e510bb20 100644 --- a/scripts/bootstrap/README.md +++ b/scripts/bootstrap/README.md @@ -136,6 +136,10 @@ CHILD_GAS_SERVICE_ADDRESS= MULTISIG_CONTRACT_ADDRESS= ROOT_GATEWAY_ADDRESS= ROOT_GAS_SERVICE_ADDRESS= +## (Optional) to verify child contract after deployment +CHILD_CHAIN_BLOCKSCOUT_API_URL= +## (Optional) to verify root contract after deployment +ROOT_CHAIN_ETHERSCAN_API_KEY= ``` 7. Basic contract validation If multisig is deployed: diff --git a/scripts/bootstrap/verify.txt b/scripts/bootstrap/verify.txt new file mode 100644 index 00000000..902153db --- /dev/null +++ b/scripts/bootstrap/verify.txt @@ -0,0 +1 @@ +forge verify-contract --verifier blockscout --verifier-url https://explorer.testnet.immutable.com/api
SomeContract \ No newline at end of file diff --git a/scripts/deploy/child_deployment.ts b/scripts/deploy/child_deployment.ts index a201f44e..ce8201f1 100644 --- a/scripts/deploy/child_deployment.ts +++ b/scripts/deploy/child_deployment.ts @@ -2,7 +2,7 @@ import * as dotenv from "dotenv"; dotenv.config(); import { ethers } from "ethers"; -import { requireEnv, waitForConfirmation, deployChildContract, waitForReceipt, getFee, getChildContracts, getContract, saveChildContracts } from "../helpers/helpers"; +import { requireEnv, waitForConfirmation, deployChildContract, waitForReceipt, getFee, getChildContracts, getContract, saveChildContracts, verifyChildContract } from "../helpers/helpers"; import { LedgerSigner } from "../helpers/ledger_signer"; export async function deployChildContracts() { @@ -45,6 +45,7 @@ export async function deployChildContracts() { wrappedIMX = await deployChildContract("WIMX", childDeployerWallet, null); console.log("Transaction submitted: ", JSON.stringify(wrappedIMX.deployTransaction, null, 2)); await waitForReceipt(wrappedIMX.deployTransaction.hash, childProvider); + verifyChildContract("WIMX", wrappedIMX.address); } childContracts.WRAPPED_IMX_ADDRESS = wrappedIMX.address; saveChildContracts(childContracts); @@ -60,6 +61,7 @@ export async function deployChildContracts() { childBridgeImpl = await deployChildContract("ChildERC20Bridge", childDeployerWallet, null, deployerAddr); console.log("Transaction submitted: ", JSON.stringify(childBridgeImpl.deployTransaction, null, 2)); await waitForReceipt(childBridgeImpl.deployTransaction.hash, childProvider); + verifyChildContract("ChildERC20Bridge", childBridgeImpl.address); } childContracts.CHILD_BRIDGE_IMPL_ADDRESS = childBridgeImpl.address; saveChildContracts(childContracts); @@ -75,6 +77,7 @@ export async function deployChildContracts() { childAdaptorImpl = await deployChildContract("ChildAxelarBridgeAdaptor", childDeployerWallet, null, childGatewayAddr, deployerAddr); console.log("Transaction submitted: ", JSON.stringify(childAdaptorImpl.deployTransaction, null, 2)); await waitForReceipt(childAdaptorImpl.deployTransaction.hash, childProvider); + verifyChildContract("ChildAxelarBridgeAdaptor", childAdaptorImpl.address); } childContracts.CHILD_ADAPTOR_IMPL_ADDRESS = childAdaptorImpl.address; saveChildContracts(childContracts); @@ -111,6 +114,7 @@ export async function deployChildContracts() { childTokenTemplate = await deployChildContract("ChildERC20", reservedDeployerWallet, nonceReserved); console.log("Transaction submitted: ", JSON.stringify(childTokenTemplate.deployTransaction, null, 2)); await waitForReceipt(childTokenTemplate.deployTransaction.hash, childProvider); + verifyChildContract("ChildERC20", childTokenTemplate.address); } childContracts.CHILD_TOKEN_TEMPLATE = childTokenTemplate.address; saveChildContracts(childContracts); @@ -146,6 +150,7 @@ export async function deployChildContracts() { proxyAdmin = await deployChildContract("ProxyAdmin", reservedDeployerWallet, null); console.log("Transaction submitted: ", JSON.stringify(proxyAdmin.deployTransaction, null, 2)); await waitForReceipt(proxyAdmin.deployTransaction.hash, childProvider); + verifyChildContract("ProxyAdmin", proxyAdmin.address); } childContracts.CHILD_PROXY_ADMIN = proxyAdmin.address; saveChildContracts(childContracts); @@ -166,6 +171,7 @@ export async function deployChildContracts() { childBridgeProxy = await deployChildContract("TransparentUpgradeableProxy", reservedDeployerWallet, null, childBridgeImpl.address, proxyAdmin.address, []); console.log("Transaction submitted: ", JSON.stringify(childBridgeProxy.deployTransaction, null, 2)); await waitForReceipt(childBridgeProxy.deployTransaction.hash, childProvider); + verifyChildContract("TransparentUpgradeableProxy", childBridgeProxy.address); } childContracts.CHILD_BRIDGE_PROXY_ADDRESS = childBridgeProxy.address; saveChildContracts(childContracts); @@ -186,6 +192,7 @@ export async function deployChildContracts() { childAdaptorProxy = await deployChildContract("TransparentUpgradeableProxy", reservedDeployerWallet, null, childAdaptorImpl.address, proxyAdmin.address, []); console.log("Transaction submitted: ", JSON.stringify(childAdaptorProxy.deployTransaction, null, 2)); await waitForReceipt(childAdaptorProxy.deployTransaction.hash, childProvider); + verifyChildContract("TransparentUpgradeableProxy", childAdaptorProxy.address); } childContracts.CHILD_ADAPTOR_PROXY_ADDRESS = childAdaptorProxy.address; saveChildContracts(childContracts); diff --git a/scripts/deploy/root_deployment.ts b/scripts/deploy/root_deployment.ts index 9a56f0cc..da7b01ef 100644 --- a/scripts/deploy/root_deployment.ts +++ b/scripts/deploy/root_deployment.ts @@ -2,7 +2,7 @@ import * as dotenv from "dotenv"; dotenv.config(); import { ethers } from "ethers"; -import { requireEnv, waitForConfirmation, deployRootContract, waitForReceipt, getRootContracts, getContract, saveRootContracts } from "../helpers/helpers"; +import { requireEnv, waitForConfirmation, deployRootContract, waitForReceipt, getRootContracts, getContract, saveRootContracts, verifyRootContract } from "../helpers/helpers"; import { LedgerSigner } from "../helpers/ledger_signer"; export async function deployRootContracts() { @@ -45,6 +45,7 @@ export async function deployRootContracts() { rootBridgeImpl = await deployRootContract("RootERC20BridgeFlowRate", rootDeployerWallet, null, deployerAddr); console.log("Transaction submitted: ", JSON.stringify(rootBridgeImpl.deployTransaction, null, 2)); await waitForReceipt(rootBridgeImpl.deployTransaction.hash, rootProvider); + verifyRootContract("RootERC20BridgeFlowRate", rootBridgeImpl.address); } rootContracts.ROOT_BRIDGE_IMPL_ADDRESS = rootBridgeImpl.address; saveRootContracts(rootContracts); @@ -59,7 +60,8 @@ export async function deployRootContracts() { console.log("Deploy root adaptor impl..."); rootAdaptorImpl = await deployRootContract("RootAxelarBridgeAdaptor", rootDeployerWallet, null, rootGatewayAddr, deployerAddr); console.log("Transaction submitted: ", JSON.stringify(rootAdaptorImpl.deployTransaction, null, 2)); - await waitForReceipt(rootAdaptorImpl.deployTransaction.hash, rootProvider); + await waitForReceipt(rootAdaptorImpl.deployTransaction.hash, rootProvider); + verifyRootContract("RootAxelarBridgeAdaptor", rootAdaptorImpl.address); } rootContracts.ROOT_ADAPTOR_IMPL_ADDRESS = rootAdaptorImpl.address; saveRootContracts(rootContracts); @@ -96,6 +98,7 @@ export async function deployRootContracts() { rootTokenTemplate = await deployRootContract("ChildERC20", reservedDeployerWallet, nonceReserved); console.log("Transaction submitted: ", JSON.stringify(rootTokenTemplate.deployTransaction, null, 2)); await waitForReceipt(rootTokenTemplate.deployTransaction.hash, rootProvider); + verifyRootContract("ChildERC20", rootTokenTemplate.address); } rootContracts.ROOT_TOKEN_TEMPLATE = rootTokenTemplate.address; saveRootContracts(rootContracts); @@ -127,6 +130,7 @@ export async function deployRootContracts() { proxyAdmin = await deployRootContract("ProxyAdmin", reservedDeployerWallet, null); console.log("Transaction submitted: ", JSON.stringify(proxyAdmin.deployTransaction, null, 2)); await waitForReceipt(proxyAdmin.deployTransaction.hash, rootProvider); + verifyRootContract("ProxyAdmin", proxyAdmin.address); } rootContracts.ROOT_PROXY_ADMIN = proxyAdmin.address; saveRootContracts(rootContracts); @@ -147,6 +151,7 @@ export async function deployRootContracts() { rootBridgeProxy = await deployRootContract("TransparentUpgradeableProxy", reservedDeployerWallet, null, rootBridgeImpl.address, proxyAdmin.address, []); console.log("Transaction submitted: ", JSON.stringify(rootBridgeProxy.deployTransaction, null, 2)); await waitForReceipt(rootBridgeProxy.deployTransaction.hash, rootProvider); + verifyRootContract("TransparentUpgradeableProxy", rootBridgeProxy.address); } rootContracts.ROOT_BRIDGE_PROXY_ADDRESS = rootBridgeProxy.address; saveRootContracts(rootContracts); @@ -167,6 +172,7 @@ export async function deployRootContracts() { rootAdaptorProxy = await deployRootContract("TransparentUpgradeableProxy", reservedDeployerWallet, null, rootAdaptorImpl.address, proxyAdmin.address, []); console.log("Transaction submitted: ", JSON.stringify(rootAdaptorProxy.deployTransaction, null, 2)); await waitForReceipt(rootAdaptorProxy.deployTransaction.hash, rootProvider); + verifyRootContract("TransparentUpgradeableProxy", rootAdaptorProxy.address); } rootContracts.ROOT_ADAPTOR_PROXY_ADDRESS = rootAdaptorProxy.address; saveRootContracts(rootContracts); diff --git a/scripts/helpers/helpers.ts b/scripts/helpers/helpers.ts index 6ad30056..81c936b9 100644 --- a/scripts/helpers/helpers.ts +++ b/scripts/helpers/helpers.ts @@ -1,6 +1,8 @@ import { ContractFactory, providers, ethers } from "ethers"; import { LedgerSigner } from "./ledger_signer"; import * as fs from "fs"; +import util from 'util'; +const exec = util.promisify(require('child_process').exec); export function delay(time: number) { return new Promise(resolve => setTimeout(resolve, time)); @@ -184,4 +186,32 @@ export async function waitUntilSucceed(axelarURL: string, txHash: any) { } await delay(60000); } +} + +export async function verifyChildContract(contract: string, contractAddr: string) { + let url = process.env["CHILD_CHAIN_BLOCKSCOUT_API_URL"]; + if (url == null) { + console.log("CHILD_CHAIN_BLOCKSCOUT_API_URL not set, skip contract verification..."); + return; + } + let cmd = `forge verify-contract --verifier blockscout --verifier-url ${url} ${contractAddr} ${contract}`; + const { stdout, stderr } = await exec(cmd); + if (stderr != "") { + throw(stderr); + } + console.log(stdout); +} + +export async function verifyRootContract(contract: string, contractAddr: string) { + let key = process.env["ROOT_CHAIN_ETHERSCAN_API_KEY"]; + if (key == null) { + console.log("ROOT_CHAIN_ETHERSCAN_API_KEY not set, skip contract verification..."); + } + let chainID = requireEnv("ROOT_CHAIN_ID"); + let cmd = `ETHER_SCAN_API_KEY=${key} forge verify-contract ${contractAddr} ${contract} --chain-id ${chainID}`; + const { stdout, stderr } = await exec(cmd); + if (stderr != "") { + throw(stderr); + } + console.log(stdout); } \ No newline at end of file From 3ea5908f100daafbd974d35b909ac96aa98ae809 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Thu, 14 Dec 2023 07:46:47 +0800 Subject: [PATCH 041/155] Fix CI --- scripts/deploy/child_deployment.ts | 14 +++++++------- scripts/deploy/root_deployment.ts | 12 ++++++------ scripts/helpers/helpers.ts | 5 +++-- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/scripts/deploy/child_deployment.ts b/scripts/deploy/child_deployment.ts index ce8201f1..d25c287c 100644 --- a/scripts/deploy/child_deployment.ts +++ b/scripts/deploy/child_deployment.ts @@ -45,7 +45,7 @@ export async function deployChildContracts() { wrappedIMX = await deployChildContract("WIMX", childDeployerWallet, null); console.log("Transaction submitted: ", JSON.stringify(wrappedIMX.deployTransaction, null, 2)); await waitForReceipt(wrappedIMX.deployTransaction.hash, childProvider); - verifyChildContract("WIMX", wrappedIMX.address); + await verifyChildContract("WIMX", wrappedIMX.address); } childContracts.WRAPPED_IMX_ADDRESS = wrappedIMX.address; saveChildContracts(childContracts); @@ -61,7 +61,7 @@ export async function deployChildContracts() { childBridgeImpl = await deployChildContract("ChildERC20Bridge", childDeployerWallet, null, deployerAddr); console.log("Transaction submitted: ", JSON.stringify(childBridgeImpl.deployTransaction, null, 2)); await waitForReceipt(childBridgeImpl.deployTransaction.hash, childProvider); - verifyChildContract("ChildERC20Bridge", childBridgeImpl.address); + await verifyChildContract("ChildERC20Bridge", childBridgeImpl.address); } childContracts.CHILD_BRIDGE_IMPL_ADDRESS = childBridgeImpl.address; saveChildContracts(childContracts); @@ -77,7 +77,7 @@ export async function deployChildContracts() { childAdaptorImpl = await deployChildContract("ChildAxelarBridgeAdaptor", childDeployerWallet, null, childGatewayAddr, deployerAddr); console.log("Transaction submitted: ", JSON.stringify(childAdaptorImpl.deployTransaction, null, 2)); await waitForReceipt(childAdaptorImpl.deployTransaction.hash, childProvider); - verifyChildContract("ChildAxelarBridgeAdaptor", childAdaptorImpl.address); + await verifyChildContract("ChildAxelarBridgeAdaptor", childAdaptorImpl.address); } childContracts.CHILD_ADAPTOR_IMPL_ADDRESS = childAdaptorImpl.address; saveChildContracts(childContracts); @@ -114,7 +114,7 @@ export async function deployChildContracts() { childTokenTemplate = await deployChildContract("ChildERC20", reservedDeployerWallet, nonceReserved); console.log("Transaction submitted: ", JSON.stringify(childTokenTemplate.deployTransaction, null, 2)); await waitForReceipt(childTokenTemplate.deployTransaction.hash, childProvider); - verifyChildContract("ChildERC20", childTokenTemplate.address); + await verifyChildContract("ChildERC20", childTokenTemplate.address); } childContracts.CHILD_TOKEN_TEMPLATE = childTokenTemplate.address; saveChildContracts(childContracts); @@ -150,7 +150,7 @@ export async function deployChildContracts() { proxyAdmin = await deployChildContract("ProxyAdmin", reservedDeployerWallet, null); console.log("Transaction submitted: ", JSON.stringify(proxyAdmin.deployTransaction, null, 2)); await waitForReceipt(proxyAdmin.deployTransaction.hash, childProvider); - verifyChildContract("ProxyAdmin", proxyAdmin.address); + await verifyChildContract("ProxyAdmin", proxyAdmin.address); } childContracts.CHILD_PROXY_ADMIN = proxyAdmin.address; saveChildContracts(childContracts); @@ -171,7 +171,7 @@ export async function deployChildContracts() { childBridgeProxy = await deployChildContract("TransparentUpgradeableProxy", reservedDeployerWallet, null, childBridgeImpl.address, proxyAdmin.address, []); console.log("Transaction submitted: ", JSON.stringify(childBridgeProxy.deployTransaction, null, 2)); await waitForReceipt(childBridgeProxy.deployTransaction.hash, childProvider); - verifyChildContract("TransparentUpgradeableProxy", childBridgeProxy.address); + await verifyChildContract("TransparentUpgradeableProxy", childBridgeProxy.address); } childContracts.CHILD_BRIDGE_PROXY_ADDRESS = childBridgeProxy.address; saveChildContracts(childContracts); @@ -192,7 +192,7 @@ export async function deployChildContracts() { childAdaptorProxy = await deployChildContract("TransparentUpgradeableProxy", reservedDeployerWallet, null, childAdaptorImpl.address, proxyAdmin.address, []); console.log("Transaction submitted: ", JSON.stringify(childAdaptorProxy.deployTransaction, null, 2)); await waitForReceipt(childAdaptorProxy.deployTransaction.hash, childProvider); - verifyChildContract("TransparentUpgradeableProxy", childAdaptorProxy.address); + await verifyChildContract("TransparentUpgradeableProxy", childAdaptorProxy.address); } childContracts.CHILD_ADAPTOR_PROXY_ADDRESS = childAdaptorProxy.address; saveChildContracts(childContracts); diff --git a/scripts/deploy/root_deployment.ts b/scripts/deploy/root_deployment.ts index da7b01ef..3690c963 100644 --- a/scripts/deploy/root_deployment.ts +++ b/scripts/deploy/root_deployment.ts @@ -45,7 +45,7 @@ export async function deployRootContracts() { rootBridgeImpl = await deployRootContract("RootERC20BridgeFlowRate", rootDeployerWallet, null, deployerAddr); console.log("Transaction submitted: ", JSON.stringify(rootBridgeImpl.deployTransaction, null, 2)); await waitForReceipt(rootBridgeImpl.deployTransaction.hash, rootProvider); - verifyRootContract("RootERC20BridgeFlowRate", rootBridgeImpl.address); + await verifyRootContract("RootERC20BridgeFlowRate", rootBridgeImpl.address); } rootContracts.ROOT_BRIDGE_IMPL_ADDRESS = rootBridgeImpl.address; saveRootContracts(rootContracts); @@ -61,7 +61,7 @@ export async function deployRootContracts() { rootAdaptorImpl = await deployRootContract("RootAxelarBridgeAdaptor", rootDeployerWallet, null, rootGatewayAddr, deployerAddr); console.log("Transaction submitted: ", JSON.stringify(rootAdaptorImpl.deployTransaction, null, 2)); await waitForReceipt(rootAdaptorImpl.deployTransaction.hash, rootProvider); - verifyRootContract("RootAxelarBridgeAdaptor", rootAdaptorImpl.address); + await verifyRootContract("RootAxelarBridgeAdaptor", rootAdaptorImpl.address); } rootContracts.ROOT_ADAPTOR_IMPL_ADDRESS = rootAdaptorImpl.address; saveRootContracts(rootContracts); @@ -98,7 +98,7 @@ export async function deployRootContracts() { rootTokenTemplate = await deployRootContract("ChildERC20", reservedDeployerWallet, nonceReserved); console.log("Transaction submitted: ", JSON.stringify(rootTokenTemplate.deployTransaction, null, 2)); await waitForReceipt(rootTokenTemplate.deployTransaction.hash, rootProvider); - verifyRootContract("ChildERC20", rootTokenTemplate.address); + await verifyRootContract("ChildERC20", rootTokenTemplate.address); } rootContracts.ROOT_TOKEN_TEMPLATE = rootTokenTemplate.address; saveRootContracts(rootContracts); @@ -130,7 +130,7 @@ export async function deployRootContracts() { proxyAdmin = await deployRootContract("ProxyAdmin", reservedDeployerWallet, null); console.log("Transaction submitted: ", JSON.stringify(proxyAdmin.deployTransaction, null, 2)); await waitForReceipt(proxyAdmin.deployTransaction.hash, rootProvider); - verifyRootContract("ProxyAdmin", proxyAdmin.address); + await verifyRootContract("ProxyAdmin", proxyAdmin.address); } rootContracts.ROOT_PROXY_ADMIN = proxyAdmin.address; saveRootContracts(rootContracts); @@ -151,7 +151,7 @@ export async function deployRootContracts() { rootBridgeProxy = await deployRootContract("TransparentUpgradeableProxy", reservedDeployerWallet, null, rootBridgeImpl.address, proxyAdmin.address, []); console.log("Transaction submitted: ", JSON.stringify(rootBridgeProxy.deployTransaction, null, 2)); await waitForReceipt(rootBridgeProxy.deployTransaction.hash, rootProvider); - verifyRootContract("TransparentUpgradeableProxy", rootBridgeProxy.address); + await verifyRootContract("TransparentUpgradeableProxy", rootBridgeProxy.address); } rootContracts.ROOT_BRIDGE_PROXY_ADDRESS = rootBridgeProxy.address; saveRootContracts(rootContracts); @@ -172,7 +172,7 @@ export async function deployRootContracts() { rootAdaptorProxy = await deployRootContract("TransparentUpgradeableProxy", reservedDeployerWallet, null, rootAdaptorImpl.address, proxyAdmin.address, []); console.log("Transaction submitted: ", JSON.stringify(rootAdaptorProxy.deployTransaction, null, 2)); await waitForReceipt(rootAdaptorProxy.deployTransaction.hash, rootProvider); - verifyRootContract("TransparentUpgradeableProxy", rootAdaptorProxy.address); + await verifyRootContract("TransparentUpgradeableProxy", rootAdaptorProxy.address); } rootContracts.ROOT_ADAPTOR_PROXY_ADDRESS = rootAdaptorProxy.address; saveRootContracts(rootContracts); diff --git a/scripts/helpers/helpers.ts b/scripts/helpers/helpers.ts index 81c936b9..e7d27981 100644 --- a/scripts/helpers/helpers.ts +++ b/scripts/helpers/helpers.ts @@ -190,7 +190,7 @@ export async function waitUntilSucceed(axelarURL: string, txHash: any) { export async function verifyChildContract(contract: string, contractAddr: string) { let url = process.env["CHILD_CHAIN_BLOCKSCOUT_API_URL"]; - if (url == null) { + if (url == null || url == undefined) { console.log("CHILD_CHAIN_BLOCKSCOUT_API_URL not set, skip contract verification..."); return; } @@ -204,8 +204,9 @@ export async function verifyChildContract(contract: string, contractAddr: string export async function verifyRootContract(contract: string, contractAddr: string) { let key = process.env["ROOT_CHAIN_ETHERSCAN_API_KEY"]; - if (key == null) { + if (key == null || key == undefined) { console.log("ROOT_CHAIN_ETHERSCAN_API_KEY not set, skip contract verification..."); + return; } let chainID = requireEnv("ROOT_CHAIN_ID"); let cmd = `ETHER_SCAN_API_KEY=${key} forge verify-contract ${contractAddr} ${contract} --chain-id ${chainID}`; From 25a5a75b2d1b8f15ce8d20a6229ec793d3874815 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Thu, 14 Dec 2023 08:02:19 +0800 Subject: [PATCH 042/155] Delete verify.txt --- scripts/bootstrap/verify.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 scripts/bootstrap/verify.txt diff --git a/scripts/bootstrap/verify.txt b/scripts/bootstrap/verify.txt deleted file mode 100644 index 902153db..00000000 --- a/scripts/bootstrap/verify.txt +++ /dev/null @@ -1 +0,0 @@ -forge verify-contract --verifier blockscout --verifier-url https://explorer.testnet.immutable.com/api
SomeContract \ No newline at end of file From 44cf8046c67a5cadcd88f3fb6b4bc8b238d9a5d1 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Thu, 14 Dec 2023 08:45:30 +0800 Subject: [PATCH 043/155] Update helpers.ts --- scripts/helpers/helpers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/helpers/helpers.ts b/scripts/helpers/helpers.ts index e7d27981..bb5846cd 100644 --- a/scripts/helpers/helpers.ts +++ b/scripts/helpers/helpers.ts @@ -190,7 +190,7 @@ export async function waitUntilSucceed(axelarURL: string, txHash: any) { export async function verifyChildContract(contract: string, contractAddr: string) { let url = process.env["CHILD_CHAIN_BLOCKSCOUT_API_URL"]; - if (url == null || url == undefined) { + if (url == null || url == "") { console.log("CHILD_CHAIN_BLOCKSCOUT_API_URL not set, skip contract verification..."); return; } @@ -204,7 +204,7 @@ export async function verifyChildContract(contract: string, contractAddr: string export async function verifyRootContract(contract: string, contractAddr: string) { let key = process.env["ROOT_CHAIN_ETHERSCAN_API_KEY"]; - if (key == null || key == undefined) { + if (key == null || key == "") { console.log("ROOT_CHAIN_ETHERSCAN_API_KEY not set, skip contract verification..."); return; } From e989792821311afe3e50e959cad9d7d3e8ae5871 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Thu, 14 Dec 2023 08:48:42 +0800 Subject: [PATCH 044/155] Update helpers.ts --- scripts/helpers/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/helpers/helpers.ts b/scripts/helpers/helpers.ts index bb5846cd..a982e899 100644 --- a/scripts/helpers/helpers.ts +++ b/scripts/helpers/helpers.ts @@ -209,7 +209,7 @@ export async function verifyRootContract(contract: string, contractAddr: string) return; } let chainID = requireEnv("ROOT_CHAIN_ID"); - let cmd = `ETHER_SCAN_API_KEY=${key} forge verify-contract ${contractAddr} ${contract} --chain-id ${chainID}`; + let cmd = `ETHERSCAN_API_KEY=${key} forge verify-contract ${contractAddr} ${contract} --chain-id ${chainID}`; const { stdout, stderr } = await exec(cmd); if (stderr != "") { throw(stderr); From 156005a1b561f91b53e442595c329a24b5de4f41 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Thu, 14 Dec 2023 09:39:51 +0800 Subject: [PATCH 045/155] Improve stability --- scripts/deploy/child_initialisation.ts | 86 ++++----- scripts/deploy/root_initialisation.ts | 230 +++++++++++++++---------- 2 files changed, 182 insertions(+), 134 deletions(-) diff --git a/scripts/deploy/child_initialisation.ts b/scripts/deploy/child_initialisation.ts index 8be95c1a..f42d3ce2 100644 --- a/scripts/deploy/child_initialisation.ts +++ b/scripts/deploy/child_initialisation.ts @@ -50,48 +50,56 @@ export async function initialiseChildContracts() { await waitForConfirmation(); // Initialise child bridge - console.log("Initialise child bridge..."); let childBridge = getContract("ChildERC20Bridge", childBridgeAddr, childProvider); - let [priorityFee, maxFee] = await getFee(childProvider); - let resp = await childBridge.connect(childDeployerWallet).initialize( + if (await childBridge.rootIMXToken() != "0x0000000000000000000000000000000000000000") { + console.log("Child bridge has already been initialised, skip."); + } else { + console.log("Initialise child bridge..."); + let [priorityFee, maxFee] = await getFee(childProvider); + let resp = await childBridge.connect(childDeployerWallet).initialize( + { + defaultAdmin: childPrivilegedMultisig, + pauser: childBreakglass, + unpauser: childPrivilegedMultisig, + adaptorManager: childPrivilegedMultisig, + initialDepositor: deployerAddr, + treasuryManager: multisigAddr, + }, + childAdaptorAddr, + childTemplateAddr, + rootIMXAddr, + childWIMXAddr, { - defaultAdmin: childPrivilegedMultisig, - pauser: childBreakglass, - unpauser: childPrivilegedMultisig, - adaptorManager: childPrivilegedMultisig, - initialDepositor: deployerAddr, - treasuryManager: multisigAddr, - }, - childAdaptorAddr, - childTemplateAddr, - rootIMXAddr, - childWIMXAddr, - { - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }); - console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); - await waitForReceipt(resp.hash, childProvider); - + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); + await waitForReceipt(resp.hash, childProvider); + } + // Initialise child adaptor - console.log("Initialise child adaptor..."); let childAdaptor = getContract("ChildAxelarBridgeAdaptor", childAdaptorAddr, childProvider); - [priorityFee, maxFee] = await getFee(childProvider); - resp = await childAdaptor.connect(childDeployerWallet).initialize( + if (await childAdaptor.gasService() != "0x0000000000000000000000000000000000000000") { + console.log("Child adaptor has already been initialized, skip."); + } else { + console.log("Initialise child adaptor..."); + let [priorityFee, maxFee] = await getFee(childProvider); + let resp = await childAdaptor.connect(childDeployerWallet).initialize( + { + defaultAdmin: childPrivilegedMultisig, + bridgeManager: childPrivilegedMultisig, + gasServiceManager: childPrivilegedMultisig, + targetManager: childPrivilegedMultisig, + }, + childBridgeAddr, + rootChainName, + rootAdaptorAddr, + childGasServiceAddr, { - defaultAdmin: childPrivilegedMultisig, - bridgeManager: childPrivilegedMultisig, - gasServiceManager: childPrivilegedMultisig, - targetManager: childPrivilegedMultisig, - }, - childBridgeAddr, - rootChainName, - rootAdaptorAddr, - childGasServiceAddr, - { - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }); - console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); - await waitForReceipt(resp.hash, childProvider); + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); + await waitForReceipt(resp.hash, childProvider); + } } \ No newline at end of file diff --git a/scripts/deploy/root_initialisation.ts b/scripts/deploy/root_initialisation.ts index 4b542e8f..17d1c4d9 100644 --- a/scripts/deploy/root_initialisation.ts +++ b/scripts/deploy/root_initialisation.ts @@ -67,103 +67,139 @@ export async function initialiseRootContracts() { await waitForConfirmation(); // Initialise root bridge - console.log("Initialise root bridge..."); let rootBridge = getContract("RootERC20BridgeFlowRate", rootBridgeAddr, rootProvider); - let resp = await rootBridge.connect(rootDeployerWallet)["initialize((address,address,address,address,address),address,address,address,address,address,uint256,address)"]( - { - defaultAdmin: deployerAddr, - pauser: rootBreakglass, - unpauser: rootPrivilegedMultisig, - variableManager: rootPrivilegedMultisig, - adaptorManager: rootPrivilegedMultisig, - }, - rootAdaptorAddr, - childBridgeAddr, - rootTemplateAddr, - rootIMXAddr, - rootWETHAddr, - ethers.utils.parseEther(imxDepositLimit), - deployerAddr); - console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); - await waitForReceipt(resp.hash, rootProvider); + if (await rootBridge.rootIMXToken() != "0x0000000000000000000000000000000000000000") { + console.log("Root bridge has already been initialised, skip."); + } else { + console.log("Initialise root bridge..."); + let resp = await rootBridge.connect(rootDeployerWallet)["initialize((address,address,address,address,address),address,address,address,address,address,uint256,address)"]( + { + defaultAdmin: deployerAddr, + pauser: rootBreakglass, + unpauser: rootPrivilegedMultisig, + variableManager: rootPrivilegedMultisig, + adaptorManager: rootPrivilegedMultisig, + }, + rootAdaptorAddr, + childBridgeAddr, + rootTemplateAddr, + rootIMXAddr, + rootWETHAddr, + ethers.utils.parseEther(imxDepositLimit), + deployerAddr); + console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); + await waitForReceipt(resp.hash, rootProvider); + } // Configure rate // IMX - console.log("Configure rate limiting for IMX...") - resp = await rootBridge.connect(rootDeployerWallet).setRateControlThreshold( - rootIMXAddr, - ethers.utils.parseEther(rateLimitIMXCap), - ethers.utils.parseEther(rateLimitIMXRefill), - ethers.utils.parseEther(rateLimitIMXLargeThreshold) - ); - console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); - await waitForReceipt(resp.hash, rootProvider); + if ((await rootBridge.largeTransferThresholds(rootIMXAddr)).toString() != "0") { + console.log("IMX rate limiting has already been configured, skip."); + } else { + console.log("Configure rate limiting for IMX...") + let resp = await rootBridge.connect(rootDeployerWallet).setRateControlThreshold( + rootIMXAddr, + ethers.utils.parseEther(rateLimitIMXCap), + ethers.utils.parseEther(rateLimitIMXRefill), + ethers.utils.parseEther(rateLimitIMXLargeThreshold) + ); + console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); + await waitForReceipt(resp.hash, rootProvider); + } // ETH - console.log("Configure rate limiting for ETH...") - resp = await rootBridge.connect(rootDeployerWallet).setRateControlThreshold( - await rootBridge.NATIVE_ETH(), - ethers.utils.parseEther(rateLimitETHCap), - ethers.utils.parseEther(rateLimitETHRefill), - ethers.utils.parseEther(rateLimitETHLargeThreshold) - ); - console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); - await waitForReceipt(resp.hash, rootProvider); + if ((await rootBridge.largeTransferThresholds(await rootBridge.NATIVE_ETH())).toString() != "0") { + console.log("ETH rate limiting has already been configured, skip."); + } else { + console.log("Configure rate limiting for ETH...") + let resp = await rootBridge.connect(rootDeployerWallet).setRateControlThreshold( + await rootBridge.NATIVE_ETH(), + ethers.utils.parseEther(rateLimitETHCap), + ethers.utils.parseEther(rateLimitETHRefill), + ethers.utils.parseEther(rateLimitETHLargeThreshold) + ); + console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); + await waitForReceipt(resp.hash, rootProvider); + } // USDC - console.log("Configure rate limiting for USDC...") - resp = await rootBridge.connect(rootDeployerWallet).setRateControlThreshold( - rateLimitUSDCAddr, - ethers.utils.parseEther(rateLimitUSDCCap), - ethers.utils.parseEther(rateLimitUSDCRefill), - ethers.utils.parseEther(rateLimitUSDCLargeThreshold) - ); - console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); - await waitForReceipt(resp.hash, rootProvider); + if ((await rootBridge.largeTransferThresholds(rateLimitUSDCAddr)).toString() != "0") { + console.log("USDC rate limiting has already been configured, skip."); + } else { + console.log("Configure rate limiting for USDC...") + let resp = await rootBridge.connect(rootDeployerWallet).setRateControlThreshold( + rateLimitUSDCAddr, + ethers.utils.parseEther(rateLimitUSDCCap), + ethers.utils.parseEther(rateLimitUSDCRefill), + ethers.utils.parseEther(rateLimitUSDCLargeThreshold) + ); + console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); + await waitForReceipt(resp.hash, rootProvider); + } // GU - console.log("Configure rate limiting for GU...") - resp = await rootBridge.connect(rootDeployerWallet).setRateControlThreshold( - rateLimitGUAddr, - ethers.utils.parseEther(rateLimitGUCap), - ethers.utils.parseEther(rateLimitGURefill), - ethers.utils.parseEther(rateLimitGULargeThreshold) - ); - console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); - await waitForReceipt(resp.hash, rootProvider); + if ((await rootBridge.largeTransferThresholds(rateLimitGUAddr)).toString() != "0") { + console.log("GU rate limiting has already been configured, skip."); + } else { + console.log("Configure rate limiting for GU...") + let resp = await rootBridge.connect(rootDeployerWallet).setRateControlThreshold( + rateLimitGUAddr, + ethers.utils.parseEther(rateLimitGUCap), + ethers.utils.parseEther(rateLimitGURefill), + ethers.utils.parseEther(rateLimitGULargeThreshold) + ); + console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); + await waitForReceipt(resp.hash, rootProvider); + } // Checkmate - console.log("Configure rate limiting for CheckMate...") - resp = await rootBridge.connect(rootDeployerWallet).setRateControlThreshold( - rateLimitCheckMateAddr, - ethers.utils.parseEther(rateLimitCheckMateCap), - ethers.utils.parseEther(rateLimitCheckMateRefill), - ethers.utils.parseEther(rateLimitCheckMateLargeThreshold) - ); - console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); - await waitForReceipt(resp.hash, rootProvider); + if ((await rootBridge.largeTransferThresholds(rateLimitCheckMateAddr)).toString() != "0") { + console.log("CheckMate rate limiting has already been configured, skip."); + } else { + console.log("Configure rate limiting for CheckMate...") + let resp = await rootBridge.connect(rootDeployerWallet).setRateControlThreshold( + rateLimitCheckMateAddr, + ethers.utils.parseEther(rateLimitCheckMateCap), + ethers.utils.parseEther(rateLimitCheckMateRefill), + ethers.utils.parseEther(rateLimitCheckMateLargeThreshold) + ); + console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); + await waitForReceipt(resp.hash, rootProvider); + } // GOG - console.log("Configure rate limiting for GOG...") - resp = await rootBridge.connect(rootDeployerWallet).setRateControlThreshold( - rateLimitGOGAddr, - ethers.utils.parseEther(rateLimitGOGCap), - ethers.utils.parseEther(rateLimitGOGRefill), - ethers.utils.parseEther(rateLimitGOGLargeThreshold) - ); - console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); - await waitForReceipt(resp.hash, rootProvider); - + if ((await rootBridge.largeTransferThresholds(rateLimitGOGAddr)).toString() != "0") { + console.log("GOG rate limiting has already been configured, skip."); + } else { + console.log("Configure rate limiting for GOG...") + let resp = await rootBridge.connect(rootDeployerWallet).setRateControlThreshold( + rateLimitGOGAddr, + ethers.utils.parseEther(rateLimitGOGCap), + ethers.utils.parseEther(rateLimitGOGRefill), + ethers.utils.parseEther(rateLimitGOGLargeThreshold) + ); + console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); + await waitForReceipt(resp.hash, rootProvider); + } + // Grant roles - console.log("Grant RATE_CONTROL_ROLE to multisig...") - resp = await rootBridge.connect(rootDeployerWallet).grantRole(utils.keccak256(utils.toUtf8Bytes("RATE")), rootPrivilegedMultisig); - console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); - await waitForReceipt(resp.hash, rootProvider); + if (await rootBridge.hasRole(utils.keccak256(utils.toUtf8Bytes("RATE")), rootPrivilegedMultisig)) { + console.log("Multisig has already obtained RATE_CONTROL_ROLE..., skip."); + } else { + console.log("Grant RATE_CONTROL_ROLE to multisig..."); + let resp = await rootBridge.connect(rootDeployerWallet).grantRole(utils.keccak256(utils.toUtf8Bytes("RATE")), rootPrivilegedMultisig); + console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); + await waitForReceipt(resp.hash, rootProvider); + } - console.log("Grant DEFAULT_ADMIN to multisig...") - resp = await rootBridge.connect(rootDeployerWallet).grantRole(await rootBridge.DEFAULT_ADMIN_ROLE(), rootPrivilegedMultisig); - console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); - await waitForReceipt(resp.hash, rootProvider); + if (await rootBridge.hasRole(await rootBridge.DEFAULT_ADMIN_ROLE(), rootPrivilegedMultisig)) { + console.log("Multisig has already obtained DEFAULT_ADMIN..., skip."); + } else { + console.log("Grant DEFAULT_ADMIN to multisig...") + let resp = await rootBridge.connect(rootDeployerWallet).grantRole(await rootBridge.DEFAULT_ADMIN_ROLE(), rootPrivilegedMultisig); + console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); + await waitForReceipt(resp.hash, rootProvider); + } // Print summary console.log("Does multisig have DEFAULT_ADMIN: ", await rootBridge.hasRole(await rootBridge.DEFAULT_ADMIN_ROLE(), rootPrivilegedMultisig)); @@ -172,19 +208,23 @@ export async function initialiseRootContracts() { console.log("Does deployer have RATE_ADMIN: ", await rootBridge.hasRole(utils.keccak256(utils.toUtf8Bytes("RATE")), deployerAddr)); // Initialise root adaptor - console.log("Initialise root adaptor..."); let rootAdaptor = getContract("RootAxelarBridgeAdaptor", rootAdaptorAddr, rootProvider); - resp = await rootAdaptor.connect(rootDeployerWallet).initialize( - { - defaultAdmin: rootPrivilegedMultisig, - bridgeManager: rootPrivilegedMultisig, - gasServiceManager: rootPrivilegedMultisig, - targetManager: rootPrivilegedMultisig, - }, - rootBridgeAddr, - childChainName, - childAdaptorAddr, - rootGasServiceAddr); - console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); - await waitForReceipt(resp.hash, rootProvider); + if (await rootAdaptor.gasService() != "0x0000000000000000000000000000000000000000") { + console.log("Root adaptor has already been initialized, skip."); + } else { + console.log("Initialise root adaptor..."); + let resp = await rootAdaptor.connect(rootDeployerWallet).initialize( + { + defaultAdmin: rootPrivilegedMultisig, + bridgeManager: rootPrivilegedMultisig, + gasServiceManager: rootPrivilegedMultisig, + targetManager: rootPrivilegedMultisig, + }, + rootBridgeAddr, + childChainName, + childAdaptorAddr, + rootGasServiceAddr); + console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); + await waitForReceipt(resp.hash, rootProvider); + } } \ No newline at end of file From bcac3a94f25a611ab0e530759175394e49896b32 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Thu, 14 Dec 2023 09:47:07 +0800 Subject: [PATCH 046/155] Improve stability again --- scripts/bootstrap/9_test_preparation.ts | 56 ++++++++++++++++--------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/scripts/bootstrap/9_test_preparation.ts b/scripts/bootstrap/9_test_preparation.ts index 01ed75c2..b4e2b4ab 100644 --- a/scripts/bootstrap/9_test_preparation.ts +++ b/scripts/bootstrap/9_test_preparation.ts @@ -53,30 +53,46 @@ async function run() { console.log("Deployed to ROOT_TEST_CUSTOM_TOKEN: ", rootCustomToken.address); // Mint tokens - console.log("Mint tokens..."); - let resp = await rootCustomToken.connect(rootDeployerWallet).mint(rootTestWallet.address, ethers.utils.parseEther("1000.0").toBigInt()); - await waitForReceipt(resp.hash, rootProvider); + if ((await rootCustomToken.balanceOf(rootTestWallet.address)).toString() != "0") { + console.log("Test account has already been given test tokens, skip."); + } else { + console.log("Mint tokens..."); + let resp = await rootCustomToken.connect(rootDeployerWallet).mint(rootTestWallet.address, ethers.utils.parseEther("1000.0").toBigInt()); + await waitForReceipt(resp.hash, rootProvider); + } - console.log("Set rate control..."); - // Set rate control - resp = await rootBridge.connect(rootDeployerWallet).setRateControlThreshold( - rootCustomToken.address, - ethers.utils.parseEther("20016.0"), - ethers.utils.parseEther("5.56"), - ethers.utils.parseEther("10008.0") - ); - await waitForReceipt(resp.hash, rootProvider); + if ((await rootBridge.largeTransferThresholds(rootCustomToken.address)).toString() != "0") { + console.log("Rate limiting has already been configured for custom token, skip."); + } else { + console.log("Set rate control..."); + // Set rate control + let resp = await rootBridge.connect(rootDeployerWallet).setRateControlThreshold( + rootCustomToken.address, + ethers.utils.parseEther("20016.0"), + ethers.utils.parseEther("5.56"), + ethers.utils.parseEther("10008.0") + ); + await waitForReceipt(resp.hash, rootProvider); + } // Revoke roles - console.log("Revoke RATE_CONTROL_ROLE of deployer...") - resp = await rootBridge.connect(rootDeployerWallet).revokeRole(utils.keccak256(utils.toUtf8Bytes("RATE")), deployerAddr); - console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); - await waitForReceipt(resp.hash, rootProvider); + if (!await rootBridge.hasRole(utils.keccak256(utils.toUtf8Bytes("RATE")), deployerAddr)) { + console.log("Deployer has already revoked RATE_CONTROL_ROLE..., skip."); + } else { + console.log("Revoke RATE_CONTROL_ROLE of deployer...") + let resp = await rootBridge.connect(rootDeployerWallet).revokeRole(utils.keccak256(utils.toUtf8Bytes("RATE")), deployerAddr); + console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); + await waitForReceipt(resp.hash, rootProvider); + } - console.log("Revoke DEFAULT_ADMIN of deployer...") - resp = await rootBridge.connect(rootDeployerWallet).revokeRole(await rootBridge.DEFAULT_ADMIN_ROLE(), deployerAddr); - console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); - await waitForReceipt(resp.hash, rootProvider); + if (!await rootBridge.hasRole(await rootBridge.DEFAULT_ADMIN_ROLE(), deployerAddr)) { + console.log("Deployer has already revoked DEFAULT_ADMIN..., skip."); + } else { + console.log("Revoke DEFAULT_ADMIN of deployer...") + let resp = await rootBridge.connect(rootDeployerWallet).revokeRole(await rootBridge.DEFAULT_ADMIN_ROLE(), deployerAddr); + console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); + await waitForReceipt(resp.hash, rootProvider); + } // Print summary console.log("Does multisig have DEFAULT_ADMIN: ", await rootBridge.hasRole(await rootBridge.DEFAULT_ADMIN_ROLE(), rootPrivilegedMultisig)); From 9b3f21ba7c1bd5d06f87b2bd481cce850c7f1453 Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Thu, 14 Dec 2023 13:54:29 +1100 Subject: [PATCH 047/155] Update README --- README.md | 128 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 91 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index e30dd77a..28a7c9ac 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,80 @@ -# Immutable zkEVM Bridge Contracts - -(Work in progress doc) - -

- -zkevm-bridge-contracts is a repository of smart contracts for bridging in the Immutable zkEVM, a general-purpose permissionless L2 zero-knowledge rollup. - -These contracts are used in the ERC20 and native ETH bridging functionality of the Immutable zkEVM. - -The main development toolkit for this repository is [Foundry](https://book.getfoundry.sh) -Foundry consists of: - -- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). -- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. -- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. -- **Chisel**: Fast, utilitarian, and verbose solidity REPL. - -## Usage - +# Immutable Token Bridge + +---- +The Immutable token bridge facilitates the transfer of assets between two chains, namely Ethereum (the Root chain) and the Immutable chain (the Child chain). At present, the bridge only supports the transfer of standard ERC20 tokens originating from Ethereum, as well as native assets (ETH and IMX). Other types of assets (such as ERC721) and assets originating from the Child chain are not currently supported. + +## Contents + +* [Features](#features) +* [Build and Test](#build-and-test) +* [Contract Deployment](#deployment) +* [Deployed Contract Addresses](#deployed-contract-addresses) + + +## Features +### Core Features +The bridge provides two key functions, **deposits** and **withdrawals**. + +#### Deposit Assets (Root Chain → Child Chain) +When a user wishes to transfer assets from Ethereum to Immutable, they initiate a deposit. This deposit moves an asset from the Root chain to the Child chain. It does so by first transferring the user's asset to the bridge (Root chain), then minting and transferring corresponding representation tokens of that asset to the user, on the Child chain. The following types of asset deposits flows are supported: +1. Native ETH on Ethereum → Wrapped ETH on Immutable zkEVM (ERC20 token) +2. Wrapped ETH on Ethereum → Wrapped ETH on Immutable zkEVM (ERC20 token) +3. ERC20 IMX on Ethereum → Native IMX on Immutable zkEVM. IMX is represented on Immutable zkEVM as the native gas token, see [here](https://etherscan.io/token/0xf57e7e7c23978c3caec3c3548e3d615c346e79ff) +4. Standard ERC20 tokens → Wrapped equivalents on Immutable zkEVM (ERC20 token) + +#### Withdraw Assets (Child Chain → Root Chain) +When a user wants to transfer bridged assets from Immutable back to Ethereum, they start a withdrawal. This process moves an asset from the Child chain to the Root chain. It includes burning the user's bridged tokens on the child chain and unlocking the corresponding asset on the Root chain. Only assets that were bridged using the deposit flow described above can be withdrawn. Therefore, the available withdrawal flows are as follows: +1. Native IMX on Immutable zkEVM → for ERC20 IMX on Ethereum. +2. Wrapped ETH on Immutable zkEVM → Native ETH on Ethereum +3. Wrapped IMX on Immutable zkEVM → for ERC20 IMX on Ethereum +4. Wrapped ERC20 on Immutable zkEVM → Original ERC20 on Ethereum + +**Not supported:** +The following capabilities are not currently supported by the Immutable bridge: +- Bridging of tokens that were originally deployed on the Child chain (i.e. ones that do not originate from the Root chain). +- Bridging of non-standard ERC20 tokens +- Bridging of ERC721 or other tokens standards + +### Security Features +The bridge employs a number of security features to mitigate the likelihood and impact of potential exploits. These are discussed further in subsections below. + +#### IMX Deposit Limit +The total amount of IMX that can be deposited (i.e. sent from the Root chain to the Child chain), is capped at a configurable threshold. In addition to mitigating the potential impact of an exploit, this limit serves to reduce the likelihood of scenarios where the bridge might not have sufficient native IMX to process the deposits on the child chain. + +#### Withdrawal Delays +To mitigate the impact of potential exploits, withdrawal transactions (token transfers from the Child chain to the Root chain) may be automatically delayed under certain conditions. By default, this delay is one day. The delay is implemented as a withdrawal queue, which is an array of withdrawal transactions for each user. Once the required delay has passed, a user can finalize a queued withdrawal. The conditions that trigger a withdrawal delay are as follows: +- Specific flow rates can be set for individual tokens. These rates regulate the amount that can be withdrawn over a period of time. If a token's withdrawal rate exceeds its specific threshold, all subsequent withdrawals from the bridge are queued. +- Any withdrawal that exceeds a token-specific amount is queued. This only affects the individual withdrawal in question and does not impact other withdrawals by the same user or others. +- If no thresholds are defined for a given token, all withdrawals relating to that token are queued. + +For further details, see the [withdrawal delay mechanism section](#withdrawal-delay-mechanism). + +#### Emergency Pause +In the event of an emergency, the bridge can be paused to mitigate the potential impact of an incident. This suspends all user-accessible capabilities, including token mapping, deposits, and withdrawals, until the bridge is resumed. However, this doesn't restrict privileged functions accessible by accounts with certain roles. It allows administrators to perform necessary operations that can address the incident (e.g., bridge parameter changes, upgrades). The specific functions that are halted by the emergency pause mechanism for each contract are listed below: +- **Root Chain** + - `RootERC20Bridge`: `mapToken()`, `deposit()`, `depositTo()`, `depositETH()`, `depositToETH()`, `onMessageReceive()` + - `RootERC20BridgeFlowRate` contract: `finaliseQueuedWithdrawal()`, `finaliseQueuedWithdrawalsAggregated()`, as well as all functions from `RootERC20Bridge`. +- **Child Chain:** + - `ChildERC20Bridge`: `withdraw()`, `withdrawTo()`, `withdrawIMX()`, `withdrawIMXTo()`, `withdrawWIMX()`, `withdrawWIMXTo()`,`onMessageReceive()` + +#### Role-Based-Access-Control +The bridge employs fine-grained Role-Based-Access-Controls (RBAC), for privileged operations that control various parameters of the bridge. These include: +- `DEFAULT_ADMIN_ROLE`: Can manage granting and revoking of roles to accounts. +- `VARIABLE_MANAGER_ROLE`: Can update the cumulative IMX deposit limit. +- `RATE_MANAGER_ROLE`: Can enable or disable the withdrawal queue, and configure parameter for each token related to the withdrawal queue. +- `BRIDGE_MANAGER_ROLE`: Can update the bridge used by the adaptor. +- `ADAPTOR_MANAGER_ROLE`: Can update the bridge adaptor. +- `TARGET_MANAGER_ROLE`: Can update targeted bridge used by the adaptor (e.g. target is child chain on root adaptors). +- `GAS_SERVICE_MANAGER_ROLE`: Role identifier for those who can update the gas service used by the adaptor. +- `PAUSER_ROLE`: Role identifier for those who can pause functionanlity. +- `UNPAUSER_ROLE`: Role identifier for those who can unpause functionality + +## Build and Test ### Install Dependencies ```shell $ yarn install $ forge install ``` - ### Build ```shell @@ -31,26 +82,12 @@ $ forge build ``` ### Test - ```shell $ forge test ``` -### Format - -```shell -$ forge fmt -``` - -### Gas Snapshots - -```shell -$ forge snapshot -``` - +## Contract Deployment ### Local Deployment - -##### Instructions To set up the contracts on two separate local networks, we need to start running the local networks, then deploy and initialize the contracts. 1. Set up the two local networks and axelar network @@ -72,4 +109,21 @@ yarn local:test ### Remote Deployment -When deploying these contracts on remote networks (i.e. testnets or mainnets). Refer to [deployment](./scripts/deploy/README.md) or [bootstrap](./scripts/bootstrap/README.md). \ No newline at end of file +When deploying these contracts on remote networks (i.e. testnets or mainnets). Refer to [deployment](./scripts/deploy/README.md) or [bootstrap](./scripts/bootstrap/README.md). + +## Deployed Contract Addresses + +| | Mainnet | Testnet | Devnet | +|---------------------------|---------|---------|---------| +| `RootERC20BridgeFlowRate` | [TBA]() | [TBA]() | [TBA]() | +| `RootAxelarBridgeAdaptor` | [TBA]() | [TBA]() | [TBA]() | +| `wIMX` | [TBA]() | [TBA]() | [TBA]() | + + +| | Mainnet | Testnet | Devnet | +|----------------------------|---------|---------|---------| +| `ChildERC20Bridge` | [TBA]() | [TBA]() | [TBA]() | +| `ChildAxelarBridgeAdaptor` | [TBA]() | [TBA]() | [TBA]() | +| `wETH` | [TBA]() | [TBA]() | [TBA]() | + + From 6abb1f0953843bd81c6bf33a6ecaed13ced0457a Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Fri, 15 Dec 2023 09:53:42 +1100 Subject: [PATCH 048/155] Add contract addresses --- README.md | 33 ++++++++++--------- deployment/devnet/child-chain-addresses.json | 12 +++++++ deployment/devnet/root-chain-addresses.json | 11 +++++++ deployment/mainnet/child-chain-addresses.json | 12 +++++++ deployment/mainnet/root-chain-addresses.json | 11 +++++++ deployment/testnet/child-chain-addresses.json | 12 +++++++ deployment/testnet/root-chain-addresses.json | 11 +++++++ 7 files changed, 87 insertions(+), 15 deletions(-) create mode 100644 deployment/devnet/child-chain-addresses.json create mode 100644 deployment/devnet/root-chain-addresses.json create mode 100644 deployment/mainnet/child-chain-addresses.json create mode 100644 deployment/mainnet/root-chain-addresses.json create mode 100644 deployment/testnet/child-chain-addresses.json create mode 100644 deployment/testnet/root-chain-addresses.json diff --git a/README.md b/README.md index 28a7c9ac..bf40003a 100644 --- a/README.md +++ b/README.md @@ -112,18 +112,21 @@ yarn local:test When deploying these contracts on remote networks (i.e. testnets or mainnets). Refer to [deployment](./scripts/deploy/README.md) or [bootstrap](./scripts/bootstrap/README.md). ## Deployed Contract Addresses - -| | Mainnet | Testnet | Devnet | -|---------------------------|---------|---------|---------| -| `RootERC20BridgeFlowRate` | [TBA]() | [TBA]() | [TBA]() | -| `RootAxelarBridgeAdaptor` | [TBA]() | [TBA]() | [TBA]() | -| `wIMX` | [TBA]() | [TBA]() | [TBA]() | - - -| | Mainnet | Testnet | Devnet | -|----------------------------|---------|---------|---------| -| `ChildERC20Bridge` | [TBA]() | [TBA]() | [TBA]() | -| `ChildAxelarBridgeAdaptor` | [TBA]() | [TBA]() | [TBA]() | -| `wETH` | [TBA]() | [TBA]() | [TBA]() | - - +Addresses for the core bridge contracts are listed below. For a full list of deployed contracts, see [deployments/](./deployments/). +ABIs for contracts can be obtained from the blockchain explorer links for each contract provided below. + +### Root Chain +| | Mainnet | Testnet | Devnet | +|-------------------------------|---------|--------------------------------------------------------------------------------------------------------------------------------------|--------| +| Bridge Proxy | TBA | [`0x0D3C59c779Fd552C27b23F723E80246c840100F5`](https://sepolia.etherscan.io/address/0x0d3c59c779fd552c27b23f723e80246c840100f5) | TBA | +| Bridge Implementation | TBA | [`0xac88a57943b5BBa1ecd931F8494cAd0B7F717590`](https://sepolia.etherscan.io/address/0xac88a57943b5bba1ecd931f8494cad0b7f717590#code) | TBA | +| Bridge Adaptor Proxy | TBA | [`0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab`](https://sepolia.etherscan.io/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | +| Bridge Adaptor Implementation | TBA | [`0xe9ec55e1fC90AB69B2Fb4C029d24a4622B94042e`](https://sepolia.etherscan.io/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | + +### Child Chain +| | Mainnet | Testnet | Devnet | +|-------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------|--------| +| Bridge Proxy | TBA | [`0x0D3C59c779Fd552C27b23F723E80246c840100F5`](https://explorer.testnet.immutable.com/address/0x0D3C59c779Fd552C27b23F723E80246c840100F5) | TBA | +| Bridge Implementation | TBA | [`0xA554Cf58b9524d43F1dee2fE1b0C928f18A93FE9`](https://explorer.testnet.immutable.com/address/0xA554Cf58b9524d43F1dee2fE1b0C928f18A93FE9) | TBA | +| Bridge Adaptor Proxy | TBA | [`0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab`](https://explorer.testnet.immutable.com/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | +| Bridge Adaptor Implementation | TBA | [`0xac88a57943b5BBa1ecd931F8494cAd0B7F717590`](https://explorer.testnet.immutable.com/address/0xac88a57943b5BBa1ecd931F8494cAd0B7F717590) | TBA | \ No newline at end of file diff --git a/deployment/devnet/child-chain-addresses.json b/deployment/devnet/child-chain-addresses.json new file mode 100644 index 00000000..b953719b --- /dev/null +++ b/deployment/devnet/child-chain-addresses.json @@ -0,0 +1,12 @@ +{ + "CHILD_PROXY_ADMIN": "", + "CHILD_BRIDGE_IMPL_ADDRESS": "", + "CHILD_BRIDGE_PROXY_ADDRESS": "", + "CHILD_BRIDGE_ADDRESS": "", + "CHILD_ADAPTOR_IMPL_ADDRESS": "", + "CHILD_ADAPTOR_PROXY_ADDRESS": "", + "CHILD_ADAPTOR_ADDRESS": "", + "CHILD_TOKEN_TEMPLATE": "", + "WRAPPED_IMX_ADDRESS": "", + "CHILD_TEST_CUSTOM_TOKEN": "" +} \ No newline at end of file diff --git a/deployment/devnet/root-chain-addresses.json b/deployment/devnet/root-chain-addresses.json new file mode 100644 index 00000000..0be8a3f5 --- /dev/null +++ b/deployment/devnet/root-chain-addresses.json @@ -0,0 +1,11 @@ +{ + "ROOT_PROXY_ADMIN": "", + "ROOT_BRIDGE_IMPL_ADDRESS": "", + "ROOT_BRIDGE_PROXY_ADDRESS": "", + "ROOT_BRIDGE_ADDRESS": "", + "ROOT_ADAPTOR_IMPL_ADDRESS": "", + "ROOT_ADAPTOR_PROXY_ADDRESS": "", + "ROOT_ADAPTOR_ADDRESS": "", + "ROOT_TOKEN_TEMPLATE": "", + "ROOT_TEST_CUSTOM_TOKEN": "" +} \ No newline at end of file diff --git a/deployment/mainnet/child-chain-addresses.json b/deployment/mainnet/child-chain-addresses.json new file mode 100644 index 00000000..b953719b --- /dev/null +++ b/deployment/mainnet/child-chain-addresses.json @@ -0,0 +1,12 @@ +{ + "CHILD_PROXY_ADMIN": "", + "CHILD_BRIDGE_IMPL_ADDRESS": "", + "CHILD_BRIDGE_PROXY_ADDRESS": "", + "CHILD_BRIDGE_ADDRESS": "", + "CHILD_ADAPTOR_IMPL_ADDRESS": "", + "CHILD_ADAPTOR_PROXY_ADDRESS": "", + "CHILD_ADAPTOR_ADDRESS": "", + "CHILD_TOKEN_TEMPLATE": "", + "WRAPPED_IMX_ADDRESS": "", + "CHILD_TEST_CUSTOM_TOKEN": "" +} \ No newline at end of file diff --git a/deployment/mainnet/root-chain-addresses.json b/deployment/mainnet/root-chain-addresses.json new file mode 100644 index 00000000..0be8a3f5 --- /dev/null +++ b/deployment/mainnet/root-chain-addresses.json @@ -0,0 +1,11 @@ +{ + "ROOT_PROXY_ADMIN": "", + "ROOT_BRIDGE_IMPL_ADDRESS": "", + "ROOT_BRIDGE_PROXY_ADDRESS": "", + "ROOT_BRIDGE_ADDRESS": "", + "ROOT_ADAPTOR_IMPL_ADDRESS": "", + "ROOT_ADAPTOR_PROXY_ADDRESS": "", + "ROOT_ADAPTOR_ADDRESS": "", + "ROOT_TOKEN_TEMPLATE": "", + "ROOT_TEST_CUSTOM_TOKEN": "" +} \ No newline at end of file diff --git a/deployment/testnet/child-chain-addresses.json b/deployment/testnet/child-chain-addresses.json new file mode 100644 index 00000000..02efd0bb --- /dev/null +++ b/deployment/testnet/child-chain-addresses.json @@ -0,0 +1,12 @@ +{ + "CHILD_PROXY_ADMIN": "0x8841Bfb811e30c791E262B7900Ab1Ff60e939282", + "CHILD_BRIDGE_IMPL_ADDRESS": "0xA554Cf58b9524d43F1dee2fE1b0C928f18A93FE9", + "CHILD_BRIDGE_PROXY_ADDRESS": "0x0D3C59c779Fd552C27b23F723E80246c840100F5", + "CHILD_BRIDGE_ADDRESS": "0x0D3C59c779Fd552C27b23F723E80246c840100F5", + "CHILD_ADAPTOR_IMPL_ADDRESS": "0xac88a57943b5BBa1ecd931F8494cAd0B7F717590", + "CHILD_ADAPTOR_PROXY_ADDRESS": "0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab", + "CHILD_ADAPTOR_ADDRESS": "0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab", + "CHILD_TOKEN_TEMPLATE": "0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6", + "WRAPPED_IMX_ADDRESS": "0x1CcCa691501174B4A623CeDA58cC8f1a76dc3439", + "CHILD_TEST_CUSTOM_TOKEN": "" +} \ No newline at end of file diff --git a/deployment/testnet/root-chain-addresses.json b/deployment/testnet/root-chain-addresses.json new file mode 100644 index 00000000..b7de4a59 --- /dev/null +++ b/deployment/testnet/root-chain-addresses.json @@ -0,0 +1,11 @@ +{ + "ROOT_PROXY_ADMIN": "0x8841Bfb811e30c791E262B7900Ab1Ff60e939282", + "ROOT_BRIDGE_IMPL_ADDRESS": "0xac88a57943b5BBa1ecd931F8494cAd0B7F717590", + "ROOT_BRIDGE_PROXY_ADDRESS": "0x0D3C59c779Fd552C27b23F723E80246c840100F5", + "ROOT_BRIDGE_ADDRESS": "0x0D3C59c779Fd552C27b23F723E80246c840100F5", + "ROOT_ADAPTOR_IMPL_ADDRESS": "0xe9ec55e1fC90AB69B2Fb4C029d24a4622B94042e", + "ROOT_ADAPTOR_PROXY_ADDRESS": "0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab", + "ROOT_ADAPTOR_ADDRESS": "0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab", + "ROOT_TOKEN_TEMPLATE": "0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6", + "ROOT_TEST_CUSTOM_TOKEN": "" +} \ No newline at end of file From 0da37287b8347dc4e08e75774b1d6c982e618bb3 Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Fri, 15 Dec 2023 10:09:45 +1100 Subject: [PATCH 049/155] Add audit report --- README.md | 2 +- audits/Trail-of-Bits-2023-12-14.pdf | Bin 0 -> 562760 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 audits/Trail-of-Bits-2023-12-14.pdf diff --git a/README.md b/README.md index bf40003a..7a56cb43 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ yarn local:setup 3. Get contract addresses from `./scripts/localdev/.child.bridge.contracts.json` and `./scripts/localdev/.root.bridge.contracts.json`. -4. (Optional) Run end to end tests +4. (Optional) Run end-to-end tests ``` yarn local:test ``` diff --git a/audits/Trail-of-Bits-2023-12-14.pdf b/audits/Trail-of-Bits-2023-12-14.pdf new file mode 100644 index 0000000000000000000000000000000000000000..dc978eaee90aae779f1fcdd6c718cef5fc5efc52 GIT binary patch literal 562760 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%0A5(`&qk^C-~eaDQTxcUAJR7$MGR%@ORg` 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

$dhs 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

@xoVMGg^0c3r!&8V?(U!w~Q{MmP!n9I_~ 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 am$QqJlgqE~dtkNW<>SX-V31Oh#`r%Tj5bjK literal 0 HcmV?d00001 From 496c00296cd02e73949a58687e08a605df59be76 Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Fri, 15 Dec 2023 10:36:20 +1100 Subject: [PATCH 050/155] Add link to audit in readme --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7a56cb43..8c8b7829 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ The Immutable token bridge facilitates the transfer of assets between two chains * [Build and Test](#build-and-test) * [Contract Deployment](#deployment) * [Deployed Contract Addresses](#deployed-contract-addresses) +* [Audits](#audits) ## Features @@ -129,4 +130,7 @@ ABIs for contracts can be obtained from the blockchain explorer links for each c | Bridge Proxy | TBA | [`0x0D3C59c779Fd552C27b23F723E80246c840100F5`](https://explorer.testnet.immutable.com/address/0x0D3C59c779Fd552C27b23F723E80246c840100F5) | TBA | | Bridge Implementation | TBA | [`0xA554Cf58b9524d43F1dee2fE1b0C928f18A93FE9`](https://explorer.testnet.immutable.com/address/0xA554Cf58b9524d43F1dee2fE1b0C928f18A93FE9) | TBA | | Bridge Adaptor Proxy | TBA | [`0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab`](https://explorer.testnet.immutable.com/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | -| Bridge Adaptor Implementation | TBA | [`0xac88a57943b5BBa1ecd931F8494cAd0B7F717590`](https://explorer.testnet.immutable.com/address/0xac88a57943b5BBa1ecd931F8494cAd0B7F717590) | TBA | \ No newline at end of file +| Bridge Adaptor Implementation | TBA | [`0xac88a57943b5BBa1ecd931F8494cAd0B7F717590`](https://explorer.testnet.immutable.com/address/0xac88a57943b5BBa1ecd931F8494cAd0B7F717590) | TBA | + +## Audits +The Immutable token bridge has been audited by [Trail of Bits](https://www.trailofbits.com/). The audit report can be found [here](./audits/Trail-of-Bits-2023-12-14.pdf). \ No newline at end of file From 4c812970a339c9b170f920a4c02a4d33e5bcd1d8 Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Fri, 15 Dec 2023 10:37:27 +1100 Subject: [PATCH 051/155] Fix broken link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8c8b7829..25ae400a 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ The Immutable token bridge facilitates the transfer of assets between two chains * [Features](#features) * [Build and Test](#build-and-test) -* [Contract Deployment](#deployment) +* [Contract Deployment](#contract-deployment) * [Deployed Contract Addresses](#deployed-contract-addresses) * [Audits](#audits) From 11359b3e009fdcf4e09a44d725f672efbfddf7a4 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Fri, 15 Dec 2023 13:25:28 +0800 Subject: [PATCH 052/155] Update verification --- scripts/deploy/child_deployment.ts | 44 +++++++++++++------------- scripts/deploy/root_deployment.ts | 50 +++++++++++++++--------------- scripts/helpers/helpers.ts | 7 ++++- 3 files changed, 53 insertions(+), 48 deletions(-) diff --git a/scripts/deploy/child_deployment.ts b/scripts/deploy/child_deployment.ts index d25c287c..beb62976 100644 --- a/scripts/deploy/child_deployment.ts +++ b/scripts/deploy/child_deployment.ts @@ -45,11 +45,11 @@ export async function deployChildContracts() { wrappedIMX = await deployChildContract("WIMX", childDeployerWallet, null); console.log("Transaction submitted: ", JSON.stringify(wrappedIMX.deployTransaction, null, 2)); await waitForReceipt(wrappedIMX.deployTransaction.hash, childProvider); + childContracts.WRAPPED_IMX_ADDRESS = wrappedIMX.address; + saveChildContracts(childContracts); + console.log("Deployed to WRAPPED_IMX_ADDRESS: ", wrappedIMX.address); await verifyChildContract("WIMX", wrappedIMX.address); } - childContracts.WRAPPED_IMX_ADDRESS = wrappedIMX.address; - saveChildContracts(childContracts); - console.log("Deployed to WRAPPED_IMX_ADDRESS: ", wrappedIMX.address); // Deploy child bridge impl let childBridgeImpl; @@ -61,11 +61,11 @@ export async function deployChildContracts() { childBridgeImpl = await deployChildContract("ChildERC20Bridge", childDeployerWallet, null, deployerAddr); console.log("Transaction submitted: ", JSON.stringify(childBridgeImpl.deployTransaction, null, 2)); await waitForReceipt(childBridgeImpl.deployTransaction.hash, childProvider); + childContracts.CHILD_BRIDGE_IMPL_ADDRESS = childBridgeImpl.address; + saveChildContracts(childContracts); + console.log("Deployed to CHILD_BRIDGE_IMPL_ADDRESS: ", childBridgeImpl.address); await verifyChildContract("ChildERC20Bridge", childBridgeImpl.address); } - childContracts.CHILD_BRIDGE_IMPL_ADDRESS = childBridgeImpl.address; - saveChildContracts(childContracts); - console.log("Deployed to CHILD_BRIDGE_IMPL_ADDRESS: ", childBridgeImpl.address); // Deploy child adaptor impl let childAdaptorImpl; @@ -77,11 +77,11 @@ export async function deployChildContracts() { childAdaptorImpl = await deployChildContract("ChildAxelarBridgeAdaptor", childDeployerWallet, null, childGatewayAddr, deployerAddr); console.log("Transaction submitted: ", JSON.stringify(childAdaptorImpl.deployTransaction, null, 2)); await waitForReceipt(childAdaptorImpl.deployTransaction.hash, childProvider); + childContracts.CHILD_ADAPTOR_IMPL_ADDRESS = childAdaptorImpl.address; + saveChildContracts(childContracts); + console.log("Deployed to CHILD_ADAPTOR_IMPL_ADDRESS: ", childAdaptorImpl.address); await verifyChildContract("ChildAxelarBridgeAdaptor", childAdaptorImpl.address); } - childContracts.CHILD_ADAPTOR_IMPL_ADDRESS = childAdaptorImpl.address; - saveChildContracts(childContracts); - console.log("Deployed to CHILD_ADAPTOR_IMPL_ADDRESS: ", childAdaptorImpl.address); if (childDeployerWallet instanceof LedgerSigner) { childDeployerWallet.close(); @@ -114,11 +114,11 @@ export async function deployChildContracts() { childTokenTemplate = await deployChildContract("ChildERC20", reservedDeployerWallet, nonceReserved); console.log("Transaction submitted: ", JSON.stringify(childTokenTemplate.deployTransaction, null, 2)); await waitForReceipt(childTokenTemplate.deployTransaction.hash, childProvider); + childContracts.CHILD_TOKEN_TEMPLATE = childTokenTemplate.address; + saveChildContracts(childContracts); + console.log("Deployed to CHILD_TOKEN_TEMPLATE: ", childTokenTemplate.address); await verifyChildContract("ChildERC20", childTokenTemplate.address); } - childContracts.CHILD_TOKEN_TEMPLATE = childTokenTemplate.address; - saveChildContracts(childContracts); - console.log("Deployed to CHILD_TOKEN_TEMPLATE: ", childTokenTemplate.address); // Initialise template if (await childTokenTemplate.name() == "TEMPLATE") { @@ -132,8 +132,8 @@ export async function deployChildContracts() { }); console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); await waitForReceipt(resp.hash, childProvider); + console.log("Initialised CHILD_TOKEN_TEMPLATE at: ", childTokenTemplate.address); } - console.log("Initialised CHILD_TOKEN_TEMPLATE at: ", childTokenTemplate.address); // Deploy proxy admin let proxyAdmin; @@ -150,11 +150,11 @@ export async function deployChildContracts() { proxyAdmin = await deployChildContract("ProxyAdmin", reservedDeployerWallet, null); console.log("Transaction submitted: ", JSON.stringify(proxyAdmin.deployTransaction, null, 2)); await waitForReceipt(proxyAdmin.deployTransaction.hash, childProvider); + childContracts.CHILD_PROXY_ADMIN = proxyAdmin.address; + saveChildContracts(childContracts); + console.log("Deployed to CHILD_PROXY_ADMIN: ", proxyAdmin.address); await verifyChildContract("ProxyAdmin", proxyAdmin.address); } - childContracts.CHILD_PROXY_ADMIN = proxyAdmin.address; - saveChildContracts(childContracts); - console.log("Deployed to CHILD_PROXY_ADMIN: ", proxyAdmin.address); // Deploy child bridge proxy let childBridgeProxy; @@ -171,11 +171,11 @@ export async function deployChildContracts() { childBridgeProxy = await deployChildContract("TransparentUpgradeableProxy", reservedDeployerWallet, null, childBridgeImpl.address, proxyAdmin.address, []); console.log("Transaction submitted: ", JSON.stringify(childBridgeProxy.deployTransaction, null, 2)); await waitForReceipt(childBridgeProxy.deployTransaction.hash, childProvider); + childContracts.CHILD_BRIDGE_PROXY_ADDRESS = childBridgeProxy.address; + saveChildContracts(childContracts); + console.log("Deployed to CHILD_BRIDGE_PROXY_ADDRESS: ", childBridgeProxy.address); await verifyChildContract("TransparentUpgradeableProxy", childBridgeProxy.address); } - childContracts.CHILD_BRIDGE_PROXY_ADDRESS = childBridgeProxy.address; - saveChildContracts(childContracts); - console.log("Deployed to CHILD_BRIDGE_PROXY_ADDRESS: ", childBridgeProxy.address); // Deploy child adaptor proxy let childAdaptorProxy; @@ -192,11 +192,11 @@ export async function deployChildContracts() { childAdaptorProxy = await deployChildContract("TransparentUpgradeableProxy", reservedDeployerWallet, null, childAdaptorImpl.address, proxyAdmin.address, []); console.log("Transaction submitted: ", JSON.stringify(childAdaptorProxy.deployTransaction, null, 2)); await waitForReceipt(childAdaptorProxy.deployTransaction.hash, childProvider); + childContracts.CHILD_ADAPTOR_PROXY_ADDRESS = childAdaptorProxy.address; + saveChildContracts(childContracts); + console.log("Deployed to CHILD_ADAPTOR_PROXY_ADDRESS: ", childAdaptorProxy.address); await verifyChildContract("TransparentUpgradeableProxy", childAdaptorProxy.address); } - childContracts.CHILD_ADAPTOR_PROXY_ADDRESS = childAdaptorProxy.address; - saveChildContracts(childContracts); - console.log("Deployed to CHILD_ADAPTOR_PROXY_ADDRESS: ", childAdaptorProxy.address); childContracts.CHILD_BRIDGE_ADDRESS = childBridgeProxy.address; childContracts.CHILD_ADAPTOR_ADDRESS = childAdaptorProxy.address, diff --git a/scripts/deploy/root_deployment.ts b/scripts/deploy/root_deployment.ts index 3690c963..cd19f6df 100644 --- a/scripts/deploy/root_deployment.ts +++ b/scripts/deploy/root_deployment.ts @@ -45,11 +45,11 @@ export async function deployRootContracts() { rootBridgeImpl = await deployRootContract("RootERC20BridgeFlowRate", rootDeployerWallet, null, deployerAddr); console.log("Transaction submitted: ", JSON.stringify(rootBridgeImpl.deployTransaction, null, 2)); await waitForReceipt(rootBridgeImpl.deployTransaction.hash, rootProvider); - await verifyRootContract("RootERC20BridgeFlowRate", rootBridgeImpl.address); + rootContracts.ROOT_BRIDGE_IMPL_ADDRESS = rootBridgeImpl.address; + saveRootContracts(rootContracts); + console.log("Deployed to ROOT_BRIDGE_IMPL_ADDRESS: ", rootBridgeImpl.address); + await verifyRootContract("RootERC20BridgeFlowRate", rootBridgeImpl.address, `"constructor(address)" "${deployerAddr}"`); } - rootContracts.ROOT_BRIDGE_IMPL_ADDRESS = rootBridgeImpl.address; - saveRootContracts(rootContracts); - console.log("Deployed to ROOT_BRIDGE_IMPL_ADDRESS: ", rootBridgeImpl.address); // Deploy root adaptor impl let rootAdaptorImpl; @@ -61,11 +61,11 @@ export async function deployRootContracts() { rootAdaptorImpl = await deployRootContract("RootAxelarBridgeAdaptor", rootDeployerWallet, null, rootGatewayAddr, deployerAddr); console.log("Transaction submitted: ", JSON.stringify(rootAdaptorImpl.deployTransaction, null, 2)); await waitForReceipt(rootAdaptorImpl.deployTransaction.hash, rootProvider); - await verifyRootContract("RootAxelarBridgeAdaptor", rootAdaptorImpl.address); + rootContracts.ROOT_ADAPTOR_IMPL_ADDRESS = rootAdaptorImpl.address; + saveRootContracts(rootContracts); + console.log("Deployed to ROOT_ADAPTOR_IMPL_ADDRESS: ", rootAdaptorImpl.address); + await verifyRootContract("RootAxelarBridgeAdaptor", rootAdaptorImpl.address, `"constructor(address,address)" "${rootGatewayAddr}" "${deployerAddr}"`); } - rootContracts.ROOT_ADAPTOR_IMPL_ADDRESS = rootAdaptorImpl.address; - saveRootContracts(rootContracts); - console.log("Deployed to ROOT_ADAPTOR_IMPL_ADDRESS: ", rootAdaptorImpl.address); if (rootDeployerWallet instanceof LedgerSigner) { rootDeployerWallet.close(); @@ -98,11 +98,11 @@ export async function deployRootContracts() { rootTokenTemplate = await deployRootContract("ChildERC20", reservedDeployerWallet, nonceReserved); console.log("Transaction submitted: ", JSON.stringify(rootTokenTemplate.deployTransaction, null, 2)); await waitForReceipt(rootTokenTemplate.deployTransaction.hash, rootProvider); - await verifyRootContract("ChildERC20", rootTokenTemplate.address); + rootContracts.ROOT_TOKEN_TEMPLATE = rootTokenTemplate.address; + saveRootContracts(rootContracts); + console.log("Deployed to ROOT_TOKEN_TEMPLATE: ", rootTokenTemplate.address); + await verifyRootContract("ChildERC20", rootTokenTemplate.address, null); } - rootContracts.ROOT_TOKEN_TEMPLATE = rootTokenTemplate.address; - saveRootContracts(rootContracts); - console.log("Deployed to ROOT_TOKEN_TEMPLATE: ", rootTokenTemplate.address); // Initialise template if (await rootTokenTemplate.name() == "TEMPLATE") { @@ -113,7 +113,7 @@ export async function deployRootContracts() { console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); await waitForReceipt(resp.hash, rootProvider); } - console.log("Deployed to ROOT_TOKEN_TEMPLATE: ", rootTokenTemplate.address); + console.log("Initialized ROOT_TOKEN_TEMPLATE at: ", rootTokenTemplate.address); // Deploy proxy admin let proxyAdmin; @@ -130,11 +130,11 @@ export async function deployRootContracts() { proxyAdmin = await deployRootContract("ProxyAdmin", reservedDeployerWallet, null); console.log("Transaction submitted: ", JSON.stringify(proxyAdmin.deployTransaction, null, 2)); await waitForReceipt(proxyAdmin.deployTransaction.hash, rootProvider); - await verifyRootContract("ProxyAdmin", proxyAdmin.address); + rootContracts.ROOT_PROXY_ADMIN = proxyAdmin.address; + saveRootContracts(rootContracts); + console.log("Deployed to ROOT_PROXY_ADMIN: ", proxyAdmin.address); + await verifyRootContract("ProxyAdmin", proxyAdmin.address, null); } - rootContracts.ROOT_PROXY_ADMIN = proxyAdmin.address; - saveRootContracts(rootContracts); - console.log("Deployed to ROOT_PROXY_ADMIN: ", proxyAdmin.address); // Deploy root bridge proxy let rootBridgeProxy; @@ -151,11 +151,11 @@ export async function deployRootContracts() { rootBridgeProxy = await deployRootContract("TransparentUpgradeableProxy", reservedDeployerWallet, null, rootBridgeImpl.address, proxyAdmin.address, []); console.log("Transaction submitted: ", JSON.stringify(rootBridgeProxy.deployTransaction, null, 2)); await waitForReceipt(rootBridgeProxy.deployTransaction.hash, rootProvider); - await verifyRootContract("TransparentUpgradeableProxy", rootBridgeProxy.address); + rootContracts.ROOT_BRIDGE_PROXY_ADDRESS = rootBridgeProxy.address; + saveRootContracts(rootContracts); + console.log("Deployed to ROOT_BRIDGE_PROXY_ADDRESS: ", rootBridgeProxy.address); + await verifyRootContract("TransparentUpgradeableProxy", rootBridgeProxy.address, `"constructor(address,address,bytes)" "${rootBridgeImpl.address}" "${proxyAdmin.address}" ""`); } - rootContracts.ROOT_BRIDGE_PROXY_ADDRESS = rootBridgeProxy.address; - saveRootContracts(rootContracts); - console.log("Deployed to ROOT_BRIDGE_PROXY_ADDRESS: ", rootBridgeProxy.address); // Deploy root adaptor proxy let rootAdaptorProxy; @@ -172,11 +172,11 @@ export async function deployRootContracts() { rootAdaptorProxy = await deployRootContract("TransparentUpgradeableProxy", reservedDeployerWallet, null, rootAdaptorImpl.address, proxyAdmin.address, []); console.log("Transaction submitted: ", JSON.stringify(rootAdaptorProxy.deployTransaction, null, 2)); await waitForReceipt(rootAdaptorProxy.deployTransaction.hash, rootProvider); - await verifyRootContract("TransparentUpgradeableProxy", rootAdaptorProxy.address); + rootContracts.ROOT_ADAPTOR_PROXY_ADDRESS = rootAdaptorProxy.address; + saveRootContracts(rootContracts); + console.log("Deployed to ROOT_ADAPTOR_PROXY_ADDRESS: ", rootAdaptorProxy.address); + await verifyRootContract("TransparentUpgradeableProxy", rootAdaptorProxy.address, `"constructor(address,address,bytes)" "${rootAdaptorImpl.address}" "${proxyAdmin.address}" ""`); } - rootContracts.ROOT_ADAPTOR_PROXY_ADDRESS = rootAdaptorProxy.address; - saveRootContracts(rootContracts); - console.log("Deployed to ROOT_ADAPTOR_PROXY_ADDRESS: ", rootAdaptorProxy.address); rootContracts.ROOT_BRIDGE_ADDRESS = rootBridgeProxy.address; rootContracts.ROOT_ADAPTOR_ADDRESS = rootAdaptorProxy.address; diff --git a/scripts/helpers/helpers.ts b/scripts/helpers/helpers.ts index a982e899..015478b2 100644 --- a/scripts/helpers/helpers.ts +++ b/scripts/helpers/helpers.ts @@ -189,6 +189,7 @@ export async function waitUntilSucceed(axelarURL: string, txHash: any) { } export async function verifyChildContract(contract: string, contractAddr: string) { + console.log("Verifying " + contract + " at " + contractAddr + " on child chain..."); let url = process.env["CHILD_CHAIN_BLOCKSCOUT_API_URL"]; if (url == null || url == "") { console.log("CHILD_CHAIN_BLOCKSCOUT_API_URL not set, skip contract verification..."); @@ -202,7 +203,8 @@ export async function verifyChildContract(contract: string, contractAddr: string console.log(stdout); } -export async function verifyRootContract(contract: string, contractAddr: string) { +export async function verifyRootContract(contract: string, contractAddr: string, args: string | null) { + console.log("Verifying " + contract + " at " + contractAddr + " on root chain..."); let key = process.env["ROOT_CHAIN_ETHERSCAN_API_KEY"]; if (key == null || key == "") { console.log("ROOT_CHAIN_ETHERSCAN_API_KEY not set, skip contract verification..."); @@ -210,6 +212,9 @@ export async function verifyRootContract(contract: string, contractAddr: string) } let chainID = requireEnv("ROOT_CHAIN_ID"); let cmd = `ETHERSCAN_API_KEY=${key} forge verify-contract ${contractAddr} ${contract} --chain-id ${chainID}`; + if (args != null) { + cmd += ` --constructor-args $(cast abi-encode ${args})` + } const { stdout, stderr } = await exec(cmd); if (stderr != "") { throw(stderr); From 0e5b545f401708edc94b4aacc6dcedf0842f303b Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Mon, 18 Dec 2023 20:55:16 +1100 Subject: [PATCH 053/155] Add wETH and wIMX addresses --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 25ae400a..987a7250 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,7 @@ ABIs for contracts can be obtained from the blockchain explorer links for each c | Bridge Implementation | TBA | [`0xac88a57943b5BBa1ecd931F8494cAd0B7F717590`](https://sepolia.etherscan.io/address/0xac88a57943b5bba1ecd931f8494cad0b7f717590#code) | TBA | | Bridge Adaptor Proxy | TBA | [`0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab`](https://sepolia.etherscan.io/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | | Bridge Adaptor Implementation | TBA | [`0xe9ec55e1fC90AB69B2Fb4C029d24a4622B94042e`](https://sepolia.etherscan.io/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | +| Bridge Adaptor Implementation | TBA | [`0xe9ec55e1fC90AB69B2Fb4C029d24a4622B94042e`](https://sepolia.etherscan.io/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | ### Child Chain | | Mainnet | Testnet | Devnet | @@ -131,6 +132,8 @@ ABIs for contracts can be obtained from the blockchain explorer links for each c | Bridge Implementation | TBA | [`0xA554Cf58b9524d43F1dee2fE1b0C928f18A93FE9`](https://explorer.testnet.immutable.com/address/0xA554Cf58b9524d43F1dee2fE1b0C928f18A93FE9) | TBA | | Bridge Adaptor Proxy | TBA | [`0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab`](https://explorer.testnet.immutable.com/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | | Bridge Adaptor Implementation | TBA | [`0xac88a57943b5BBa1ecd931F8494cAd0B7F717590`](https://explorer.testnet.immutable.com/address/0xac88a57943b5BBa1ecd931F8494cAd0B7F717590) | TBA | +| Wrapped ETH | TBA | [`0xe9E96d1aad82562b7588F03f49aD34186f996478`](https://explorer.testnet.immutable.com/address/0xe9E96d1aad82562b7588F03f49aD34186f996478) | TBA | +| Wrapped IMX | TBA | [`0x1CcCa691501174B4A623CeDA58cC8f1a76dc3439`](https://explorer.testnet.immutable.com/address/0x1CcCa691501174B4A623CeDA58cC8f1a76dc3439) | TBA | ## Audits The Immutable token bridge has been audited by [Trail of Bits](https://www.trailofbits.com/). The audit report can be found [here](./audits/Trail-of-Bits-2023-12-14.pdf). \ No newline at end of file From aef6b28bdf298484bac876091b5125a1e14ab100 Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Tue, 19 Dec 2023 05:53:18 +1100 Subject: [PATCH 054/155] Add wETH and wIMX addresses on Root chain --- README.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 987a7250..26ef6885 100644 --- a/README.md +++ b/README.md @@ -117,13 +117,15 @@ Addresses for the core bridge contracts are listed below. For a full list of dep ABIs for contracts can be obtained from the blockchain explorer links for each contract provided below. ### Root Chain -| | Mainnet | Testnet | Devnet | -|-------------------------------|---------|--------------------------------------------------------------------------------------------------------------------------------------|--------| -| Bridge Proxy | TBA | [`0x0D3C59c779Fd552C27b23F723E80246c840100F5`](https://sepolia.etherscan.io/address/0x0d3c59c779fd552c27b23f723e80246c840100f5) | TBA | -| Bridge Implementation | TBA | [`0xac88a57943b5BBa1ecd931F8494cAd0B7F717590`](https://sepolia.etherscan.io/address/0xac88a57943b5bba1ecd931f8494cad0b7f717590#code) | TBA | -| Bridge Adaptor Proxy | TBA | [`0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab`](https://sepolia.etherscan.io/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | -| Bridge Adaptor Implementation | TBA | [`0xe9ec55e1fC90AB69B2Fb4C029d24a4622B94042e`](https://sepolia.etherscan.io/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | -| Bridge Adaptor Implementation | TBA | [`0xe9ec55e1fC90AB69B2Fb4C029d24a4622B94042e`](https://sepolia.etherscan.io/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | +| | Mainnet | Testnet | Devnet | +|-------------------------------|-----------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|--------| +| Bridge Proxy | TBA | [`0x0D3C59c779Fd552C27b23F723E80246c840100F5`](https://sepolia.etherscan.io/address/0x0d3c59c779fd552c27b23f723e80246c840100f5) | TBA | +| Bridge Implementation | TBA | [`0xac88a57943b5BBa1ecd931F8494cAd0B7F717590`](https://sepolia.etherscan.io/address/0xac88a57943b5bba1ecd931f8494cad0b7f717590#code) | TBA | +| Bridge Adaptor Proxy | TBA | [`0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab`](https://sepolia.etherscan.io/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | +| Bridge Adaptor Implementation | TBA | [`0xe9ec55e1fC90AB69B2Fb4C029d24a4622B94042e`](https://sepolia.etherscan.io/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | +| Bridge Adaptor Implementation | TBA | [`0xe9ec55e1fC90AB69B2Fb4C029d24a4622B94042e`](https://sepolia.etherscan.io/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | +| Wrapped ETH | [`0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2`](https://etherscan.io/token/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2) | [`0x7b79995e5f793a07bc00c21412e50ecae098e7f9`](https://sepolia.etherscan.io/address/0x7b79995e5f793a07bc00c21412e50ecae098e7f9) | TBA | +| Wrapped IMX | [`0xf57e7e7c23978c3caec3c3548e3d615c346e79ff`](https://etherscan.io/token/0xf57e7e7c23978c3caec3c3548e3d615c346e79ff) | [`0xe2629e08f4125d14e446660028bd98ee60ee69f2`](https://sepolia.etherscan.io/address/0xe2629e08f4125d14e446660028bd98ee60ee69f2) | TBA | ### Child Chain | | Mainnet | Testnet | Devnet | From 606ea343bc3b3d3203a4cc7d211058ab540e9f8d Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Tue, 19 Dec 2023 09:41:33 +1100 Subject: [PATCH 055/155] Add wETH address for Root chain on Devnet --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 26ef6885..a825f1aa 100644 --- a/README.md +++ b/README.md @@ -117,15 +117,15 @@ Addresses for the core bridge contracts are listed below. For a full list of dep ABIs for contracts can be obtained from the blockchain explorer links for each contract provided below. ### Root Chain -| | Mainnet | Testnet | Devnet | -|-------------------------------|-----------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|--------| -| Bridge Proxy | TBA | [`0x0D3C59c779Fd552C27b23F723E80246c840100F5`](https://sepolia.etherscan.io/address/0x0d3c59c779fd552c27b23f723e80246c840100f5) | TBA | -| Bridge Implementation | TBA | [`0xac88a57943b5BBa1ecd931F8494cAd0B7F717590`](https://sepolia.etherscan.io/address/0xac88a57943b5bba1ecd931f8494cad0b7f717590#code) | TBA | -| Bridge Adaptor Proxy | TBA | [`0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab`](https://sepolia.etherscan.io/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | -| Bridge Adaptor Implementation | TBA | [`0xe9ec55e1fC90AB69B2Fb4C029d24a4622B94042e`](https://sepolia.etherscan.io/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | -| Bridge Adaptor Implementation | TBA | [`0xe9ec55e1fC90AB69B2Fb4C029d24a4622B94042e`](https://sepolia.etherscan.io/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | -| Wrapped ETH | [`0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2`](https://etherscan.io/token/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2) | [`0x7b79995e5f793a07bc00c21412e50ecae098e7f9`](https://sepolia.etherscan.io/address/0x7b79995e5f793a07bc00c21412e50ecae098e7f9) | TBA | -| Wrapped IMX | [`0xf57e7e7c23978c3caec3c3548e3d615c346e79ff`](https://etherscan.io/token/0xf57e7e7c23978c3caec3c3548e3d615c346e79ff) | [`0xe2629e08f4125d14e446660028bd98ee60ee69f2`](https://sepolia.etherscan.io/address/0xe2629e08f4125d14e446660028bd98ee60ee69f2) | TBA | +| | Mainnet | Testnet | Devnet | +|-------------------------------|-----------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------| +| Bridge Proxy | TBA | [`0x0D3C59c779Fd552C27b23F723E80246c840100F5`](https://sepolia.etherscan.io/address/0x0d3c59c779fd552c27b23f723e80246c840100f5) | TBA | +| Bridge Implementation | TBA | [`0xac88a57943b5BBa1ecd931F8494cAd0B7F717590`](https://sepolia.etherscan.io/address/0xac88a57943b5bba1ecd931f8494cad0b7f717590#code) | TBA | +| Bridge Adaptor Proxy | TBA | [`0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab`](https://sepolia.etherscan.io/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | +| Bridge Adaptor Implementation | TBA | [`0xe9ec55e1fC90AB69B2Fb4C029d24a4622B94042e`](https://sepolia.etherscan.io/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | +| Bridge Adaptor Implementation | TBA | [`0xe9ec55e1fC90AB69B2Fb4C029d24a4622B94042e`](https://sepolia.etherscan.io/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | +| Wrapped ETH | [`0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2`](https://etherscan.io/token/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2) | [`0x7b79995e5f793a07bc00c21412e50ecae098e7f9`](https://sepolia.etherscan.io/address/0x7b79995e5f793a07bc00c21412e50ecae098e7f9) | [`0x7b79995e5f793a07bc00c21412e50ecae098e7f9`](https://sepolia.etherscan.io/address/0x7b79995e5f793a07bc00c21412e50ecae098e7f9) | +| Wrapped IMX | [`0xf57e7e7c23978c3caec3c3548e3d615c346e79ff`](https://etherscan.io/token/0xf57e7e7c23978c3caec3c3548e3d615c346e79ff) | [`0xe2629e08f4125d14e446660028bd98ee60ee69f2`](https://sepolia.etherscan.io/address/0xe2629e08f4125d14e446660028bd98ee60ee69f2) | TBA | ### Child Chain | | Mainnet | Testnet | Devnet | From 8e16f64c1a646d61f8117f55e12b4345ff69b012 Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Tue, 19 Dec 2023 09:45:09 +1100 Subject: [PATCH 056/155] Rename for clarity --- README.md | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index a825f1aa..435a22ae 100644 --- a/README.md +++ b/README.md @@ -117,25 +117,24 @@ Addresses for the core bridge contracts are listed below. For a full list of dep ABIs for contracts can be obtained from the blockchain explorer links for each contract provided below. ### Root Chain -| | Mainnet | Testnet | Devnet | -|-------------------------------|-----------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------| -| Bridge Proxy | TBA | [`0x0D3C59c779Fd552C27b23F723E80246c840100F5`](https://sepolia.etherscan.io/address/0x0d3c59c779fd552c27b23f723e80246c840100f5) | TBA | -| Bridge Implementation | TBA | [`0xac88a57943b5BBa1ecd931F8494cAd0B7F717590`](https://sepolia.etherscan.io/address/0xac88a57943b5bba1ecd931f8494cad0b7f717590#code) | TBA | -| Bridge Adaptor Proxy | TBA | [`0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab`](https://sepolia.etherscan.io/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | -| Bridge Adaptor Implementation | TBA | [`0xe9ec55e1fC90AB69B2Fb4C029d24a4622B94042e`](https://sepolia.etherscan.io/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | -| Bridge Adaptor Implementation | TBA | [`0xe9ec55e1fC90AB69B2Fb4C029d24a4622B94042e`](https://sepolia.etherscan.io/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | -| Wrapped ETH | [`0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2`](https://etherscan.io/token/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2) | [`0x7b79995e5f793a07bc00c21412e50ecae098e7f9`](https://sepolia.etherscan.io/address/0x7b79995e5f793a07bc00c21412e50ecae098e7f9) | [`0x7b79995e5f793a07bc00c21412e50ecae098e7f9`](https://sepolia.etherscan.io/address/0x7b79995e5f793a07bc00c21412e50ecae098e7f9) | -| Wrapped IMX | [`0xf57e7e7c23978c3caec3c3548e3d615c346e79ff`](https://etherscan.io/token/0xf57e7e7c23978c3caec3c3548e3d615c346e79ff) | [`0xe2629e08f4125d14e446660028bd98ee60ee69f2`](https://sepolia.etherscan.io/address/0xe2629e08f4125d14e446660028bd98ee60ee69f2) | TBA | +| | Mainnet | Testnet | Devnet | +|------------------------|-----------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------| +| Bridge Proxy | TBA | [`0x0D3C59c779Fd552C27b23F723E80246c840100F5`](https://sepolia.etherscan.io/address/0x0d3c59c779fd552c27b23f723e80246c840100f5) | TBA | +| Bridge Implementation | TBA | [`0xac88a57943b5BBa1ecd931F8494cAd0B7F717590`](https://sepolia.etherscan.io/address/0xac88a57943b5bba1ecd931f8494cad0b7f717590#code) | TBA | +| Adaptor Proxy | TBA | [`0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab`](https://sepolia.etherscan.io/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | +| Adaptor Implementation | TBA | [`0xe9ec55e1fC90AB69B2Fb4C029d24a4622B94042e`](https://sepolia.etherscan.io/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | +| Wrapped ETH | [`0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2`](https://etherscan.io/token/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2) | [`0x7b79995e5f793a07bc00c21412e50ecae098e7f9`](https://sepolia.etherscan.io/address/0x7b79995e5f793a07bc00c21412e50ecae098e7f9) | [`0x7b79995e5f793a07bc00c21412e50ecae098e7f9`](https://sepolia.etherscan.io/address/0x7b79995e5f793a07bc00c21412e50ecae098e7f9) | +| Wrapped IMX | [`0xf57e7e7c23978c3caec3c3548e3d615c346e79ff`](https://etherscan.io/token/0xf57e7e7c23978c3caec3c3548e3d615c346e79ff) | [`0xe2629e08f4125d14e446660028bd98ee60ee69f2`](https://sepolia.etherscan.io/address/0xe2629e08f4125d14e446660028bd98ee60ee69f2) | TBA | ### Child Chain -| | Mainnet | Testnet | Devnet | -|-------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------|--------| -| Bridge Proxy | TBA | [`0x0D3C59c779Fd552C27b23F723E80246c840100F5`](https://explorer.testnet.immutable.com/address/0x0D3C59c779Fd552C27b23F723E80246c840100F5) | TBA | -| Bridge Implementation | TBA | [`0xA554Cf58b9524d43F1dee2fE1b0C928f18A93FE9`](https://explorer.testnet.immutable.com/address/0xA554Cf58b9524d43F1dee2fE1b0C928f18A93FE9) | TBA | -| Bridge Adaptor Proxy | TBA | [`0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab`](https://explorer.testnet.immutable.com/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | -| Bridge Adaptor Implementation | TBA | [`0xac88a57943b5BBa1ecd931F8494cAd0B7F717590`](https://explorer.testnet.immutable.com/address/0xac88a57943b5BBa1ecd931F8494cAd0B7F717590) | TBA | -| Wrapped ETH | TBA | [`0xe9E96d1aad82562b7588F03f49aD34186f996478`](https://explorer.testnet.immutable.com/address/0xe9E96d1aad82562b7588F03f49aD34186f996478) | TBA | -| Wrapped IMX | TBA | [`0x1CcCa691501174B4A623CeDA58cC8f1a76dc3439`](https://explorer.testnet.immutable.com/address/0x1CcCa691501174B4A623CeDA58cC8f1a76dc3439) | TBA | +| | Mainnet | Testnet | Devnet | +|------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------|--------| +| Bridge Proxy | TBA | [`0x0D3C59c779Fd552C27b23F723E80246c840100F5`](https://explorer.testnet.immutable.com/address/0x0D3C59c779Fd552C27b23F723E80246c840100F5) | TBA | +| Bridge Implementation | TBA | [`0xA554Cf58b9524d43F1dee2fE1b0C928f18A93FE9`](https://explorer.testnet.immutable.com/address/0xA554Cf58b9524d43F1dee2fE1b0C928f18A93FE9) | TBA | +| Adaptor Proxy | TBA | [`0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab`](https://explorer.testnet.immutable.com/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | +| Adaptor Implementation | TBA | [`0xac88a57943b5BBa1ecd931F8494cAd0B7F717590`](https://explorer.testnet.immutable.com/address/0xac88a57943b5BBa1ecd931F8494cAd0B7F717590) | TBA | +| Wrapped ETH | TBA | [`0xe9E96d1aad82562b7588F03f49aD34186f996478`](https://explorer.testnet.immutable.com/address/0xe9E96d1aad82562b7588F03f49aD34186f996478) | TBA | +| Wrapped IMX | TBA | [`0x1CcCa691501174B4A623CeDA58cC8f1a76dc3439`](https://explorer.testnet.immutable.com/address/0x1CcCa691501174B4A623CeDA58cC8f1a76dc3439) | TBA | ## Audits The Immutable token bridge has been audited by [Trail of Bits](https://www.trailofbits.com/). The audit report can be found [here](./audits/Trail-of-Bits-2023-12-14.pdf). \ No newline at end of file From 480d7daf3941741621aeb81d3be49f736fafff9c Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Tue, 19 Dec 2023 08:26:40 +0800 Subject: [PATCH 057/155] Use Retry provider & Improve stability --- scripts/bootstrap/0_pre_validation.ts | 5 ++- scripts/bootstrap/1_deployer_funding.ts | 3 +- scripts/bootstrap/2_deployment_validation.ts | 5 ++- scripts/bootstrap/6_imx_burning.ts | 3 +- scripts/bootstrap/7_imx_rebalancing.ts | 5 ++- scripts/bootstrap/9_test_preparation.ts | 3 +- scripts/deploy/child_deployment.ts | 3 +- scripts/deploy/child_initialisation.ts | 3 +- scripts/deploy/root_deployment.ts | 3 +- scripts/deploy/root_initialisation.ts | 3 +- scripts/e2e/e2e.ts | 5 ++- scripts/helpers/helpers.ts | 24 ++++++++---- scripts/helpers/retry.ts | 39 ++++++++++++++++++++ scripts/localdev/axelar_setup.ts | 5 ++- scripts/localdev/childchain_setup.ts | 3 +- scripts/localdev/rootchain_setup.ts | 3 +- 16 files changed, 88 insertions(+), 27 deletions(-) create mode 100644 scripts/helpers/retry.ts diff --git a/scripts/bootstrap/0_pre_validation.ts b/scripts/bootstrap/0_pre_validation.ts index 7b717d6d..34b5dd42 100644 --- a/scripts/bootstrap/0_pre_validation.ts +++ b/scripts/bootstrap/0_pre_validation.ts @@ -4,6 +4,7 @@ dotenv.config(); import { ethers } from "ethers"; import { requireEnv, hasDuplicates } from "../helpers/helpers"; import { LedgerSigner } from "../helpers/ledger_signer"; +import { RetryProvider } from "../helpers/retry"; // The total supply of IMX const TOTAL_SUPPLY = "2000000000"; @@ -66,8 +67,8 @@ async function run() { requireEnv("RATE_LIMIT_GOG_REFILL_RATE"); requireEnv("RATE_LIMIT_GOG_LARGE_THRESHOLD"); - const childProvider = new ethers.providers.JsonRpcProvider(childRPCURL, Number(childChainID)); - const rootProvider = new ethers.providers.JsonRpcProvider(rootRPCURL, Number(rootChainID)); + const childProvider = new RetryProvider(childRPCURL, Number(childChainID)); + const rootProvider = new RetryProvider(rootRPCURL, Number(rootChainID)); let deployerWallet; if (deployerSecret == "ledger") { let index = requireEnv("DEPLOYER_LEDGER_INDEX"); diff --git a/scripts/bootstrap/1_deployer_funding.ts b/scripts/bootstrap/1_deployer_funding.ts index 08890323..2d1622a5 100644 --- a/scripts/bootstrap/1_deployer_funding.ts +++ b/scripts/bootstrap/1_deployer_funding.ts @@ -4,6 +4,7 @@ dotenv.config(); import { ethers } from "ethers"; import { requireEnv, waitForConfirmation, waitForReceipt, getFee, hasDuplicates } from "../helpers/helpers"; import { LedgerSigner } from "../helpers/ledger_signer"; +import { RetryProvider } from "../helpers/retry"; async function run() { console.log("=======Start Deployer Funding======="); @@ -20,7 +21,7 @@ async function run() { let passportDeployerFund = requireEnv("PASSPORT_NONCE_RESERVER_FUND"); // Get deployer address - const childProvider = new ethers.providers.JsonRpcProvider(childRPCURL, Number(childChainID)); + const childProvider = new RetryProvider(childRPCURL, Number(childChainID)); let childDeployerWallet; if (deployerSecret == "ledger") { let index = requireEnv("DEPLOYER_LEDGER_INDEX"); diff --git a/scripts/bootstrap/2_deployment_validation.ts b/scripts/bootstrap/2_deployment_validation.ts index d49d8469..a006067c 100644 --- a/scripts/bootstrap/2_deployment_validation.ts +++ b/scripts/bootstrap/2_deployment_validation.ts @@ -3,6 +3,7 @@ import * as dotenv from "dotenv"; dotenv.config(); import { ethers } from "ethers"; import { requireEnv, hasDuplicates, requireNonEmptyCode } from "../helpers/helpers"; +import { RetryProvider } from "../helpers/retry"; async function run() { console.log("=======Start Deployment Validation======="); @@ -26,8 +27,8 @@ async function run() { throw("Duplicate address detected!"); } - const childProvider = new ethers.providers.JsonRpcProvider(childRPCURL, Number(childChainID)); - const rootProvider = new ethers.providers.JsonRpcProvider(rootRPCURL, Number(rootChainID)); + const childProvider = new RetryProvider(childRPCURL, Number(childChainID)); + const rootProvider = new RetryProvider(rootRPCURL, Number(rootChainID)); // Check child chain. console.log("Check contracts on child chain..."); diff --git a/scripts/bootstrap/6_imx_burning.ts b/scripts/bootstrap/6_imx_burning.ts index 6dd1b16d..632d23da 100644 --- a/scripts/bootstrap/6_imx_burning.ts +++ b/scripts/bootstrap/6_imx_burning.ts @@ -4,6 +4,7 @@ dotenv.config(); import { ethers } from "ethers"; import { requireEnv, waitForConfirmation, hasDuplicates, waitForReceipt, getFee, getContract, getChildContracts } from "../helpers/helpers"; import { LedgerSigner } from "../helpers/ledger_signer"; +import { RetryProvider } from "../helpers/retry"; async function run() { console.log("=======Start IMX Burning======="); @@ -21,7 +22,7 @@ async function run() { let childBridgeAddr = childContracts.CHILD_BRIDGE_ADDRESS; // Get deployer address - const childProvider = new ethers.providers.JsonRpcProvider(childRPCURL, Number(childChainID)); + const childProvider = new RetryProvider(childRPCURL, Number(childChainID)); let childDeployerWallet; if (deployerSecret == "ledger") { let index = requireEnv("DEPLOYER_LEDGER_INDEX"); diff --git a/scripts/bootstrap/7_imx_rebalancing.ts b/scripts/bootstrap/7_imx_rebalancing.ts index 4d18e9c3..27c43ea6 100644 --- a/scripts/bootstrap/7_imx_rebalancing.ts +++ b/scripts/bootstrap/7_imx_rebalancing.ts @@ -4,6 +4,7 @@ dotenv.config(); import { ethers } from "ethers"; import { requireEnv, waitForConfirmation, waitForReceipt, getFee, hasDuplicates, getChildContracts, getRootContracts } from "../helpers/helpers"; import { LedgerSigner } from "../helpers/ledger_signer"; +import { RetryProvider } from "../helpers/retry"; // The total supply of IMX const TOTAL_SUPPLY = "2000000000"; @@ -30,8 +31,8 @@ async function run() { let rootBridgeAddr = rootContracts.ROOT_BRIDGE_ADDRESS; // Get deployer address - const childProvider = new ethers.providers.JsonRpcProvider(childRPCURL, Number(childChainID)); - const rootProvider = new ethers.providers.JsonRpcProvider(rootRPCURL, Number(rootChainID)); + const childProvider = new RetryProvider(childRPCURL, Number(childChainID)); + const rootProvider = new RetryProvider(rootRPCURL, Number(rootChainID)); let rootDeployerWallet; if (deployerSecret == "ledger") { let index = requireEnv("DEPLOYER_LEDGER_INDEX"); diff --git a/scripts/bootstrap/9_test_preparation.ts b/scripts/bootstrap/9_test_preparation.ts index b4e2b4ab..7fcbac83 100644 --- a/scripts/bootstrap/9_test_preparation.ts +++ b/scripts/bootstrap/9_test_preparation.ts @@ -4,6 +4,7 @@ dotenv.config(); import { ethers, utils } from "ethers"; import { deployRootContract, getContract, getRootContracts, requireEnv, saveRootContracts, waitForConfirmation, waitForReceipt } from "../helpers/helpers"; import { LedgerSigner } from "../helpers/ledger_signer"; +import { RetryProvider } from "../helpers/retry"; async function run() { console.log("=======Start Test Preparation======="); @@ -16,7 +17,7 @@ async function run() { let rootPrivilegedMultisig = requireEnv("ROOT_PRIVILEGED_MULTISIG_ADDR"); // Get deployer address - const rootProvider = new ethers.providers.JsonRpcProvider(rootRPCURL, Number(rootChainID)); + const rootProvider = new RetryProvider(rootRPCURL, Number(rootChainID)); let rootDeployerWallet; if (deployerSecret == "ledger") { let index = requireEnv("DEPLOYER_LEDGER_INDEX"); diff --git a/scripts/deploy/child_deployment.ts b/scripts/deploy/child_deployment.ts index beb62976..c62f3b42 100644 --- a/scripts/deploy/child_deployment.ts +++ b/scripts/deploy/child_deployment.ts @@ -4,6 +4,7 @@ dotenv.config(); import { ethers } from "ethers"; import { requireEnv, waitForConfirmation, deployChildContract, waitForReceipt, getFee, getChildContracts, getContract, saveChildContracts, verifyChildContract } from "../helpers/helpers"; import { LedgerSigner } from "../helpers/ledger_signer"; +import { RetryProvider } from "../helpers/retry"; export async function deployChildContracts() { // Check environment variables @@ -17,7 +18,7 @@ export async function deployChildContracts() { // Read from contract file. let childContracts = getChildContracts(); - const childProvider = new ethers.providers.JsonRpcProvider(childRPCURL, Number(childChainID)); + const childProvider = new RetryProvider(childRPCURL, Number(childChainID)); // Get deployer address let childDeployerWallet; diff --git a/scripts/deploy/child_initialisation.ts b/scripts/deploy/child_initialisation.ts index f42d3ce2..50154a7a 100644 --- a/scripts/deploy/child_initialisation.ts +++ b/scripts/deploy/child_initialisation.ts @@ -5,6 +5,7 @@ dotenv.config(); import { ethers } from "ethers"; import { requireEnv, waitForConfirmation, waitForReceipt, getFee, getContract, getChildContracts, getRootContracts } from "../helpers/helpers"; import { LedgerSigner } from "../helpers/ledger_signer"; +import { RetryProvider } from "../helpers/retry"; export async function initialiseChildContracts() { let rootChainName = requireEnv("ROOT_CHAIN_NAME"); @@ -33,7 +34,7 @@ export async function initialiseChildContracts() { } // Get deployer address - const childProvider = new ethers.providers.JsonRpcProvider(childRPCURL, Number(childChainID)); + const childProvider = new RetryProvider(childRPCURL, Number(childChainID)); let childDeployerWallet; if (deployerSecret == "ledger") { let index = requireEnv("DEPLOYER_LEDGER_INDEX"); diff --git a/scripts/deploy/root_deployment.ts b/scripts/deploy/root_deployment.ts index cd19f6df..a519d8e5 100644 --- a/scripts/deploy/root_deployment.ts +++ b/scripts/deploy/root_deployment.ts @@ -4,6 +4,7 @@ dotenv.config(); import { ethers } from "ethers"; import { requireEnv, waitForConfirmation, deployRootContract, waitForReceipt, getRootContracts, getContract, saveRootContracts, verifyRootContract } from "../helpers/helpers"; import { LedgerSigner } from "../helpers/ledger_signer"; +import { RetryProvider } from "../helpers/retry"; export async function deployRootContracts() { // Check environment variables @@ -17,7 +18,7 @@ export async function deployRootContracts() { // Read from contract file. let rootContracts = getRootContracts(); - const rootProvider = new ethers.providers.JsonRpcProvider(rootRPCURL, Number(rootChainID)); + const rootProvider = new RetryProvider(rootRPCURL, Number(rootChainID)); // Get deployer address let rootDeployerWallet; diff --git a/scripts/deploy/root_initialisation.ts b/scripts/deploy/root_initialisation.ts index 17d1c4d9..8e7bf7d2 100644 --- a/scripts/deploy/root_initialisation.ts +++ b/scripts/deploy/root_initialisation.ts @@ -4,6 +4,7 @@ dotenv.config(); import { ethers, utils } from "ethers"; import { requireEnv, waitForConfirmation, waitForReceipt, getContract, getChildContracts, getRootContracts } from "../helpers/helpers"; import { LedgerSigner } from "../helpers/ledger_signer"; +import { RetryProvider } from "../helpers/retry"; export async function initialiseRootContracts() { // Check environment variables @@ -50,7 +51,7 @@ export async function initialiseRootContracts() { let rootTemplateAddr = rootContracts.ROOT_TOKEN_TEMPLATE; // Get deployer address - const rootProvider = new ethers.providers.JsonRpcProvider(rootRPCURL, Number(rootChainID)); + const rootProvider = new RetryProvider(rootRPCURL, Number(rootChainID)); let rootDeployerWallet; if (deployerSecret == "ledger") { let index = requireEnv("DEPLOYER_LEDGER_INDEX"); diff --git a/scripts/e2e/e2e.ts b/scripts/e2e/e2e.ts index 4ef15db9..7cf347da 100644 --- a/scripts/e2e/e2e.ts +++ b/scripts/e2e/e2e.ts @@ -4,6 +4,7 @@ dotenv.config(); import { ethers, providers } from "ethers"; import { requireEnv, waitForReceipt, getFee, getContract, delay, getChildContracts, getRootContracts, saveChildContracts, waitUntilSucceed } from "../helpers/helpers"; import { expect } from "chai"; +import { RetryProvider } from "../helpers/retry"; // The contract ABI of IMX on L1. const IMX_ABI = `[{"inputs":[{"internalType":"address","name":"minter","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"previousAdminRole","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"newAdminRole","type":"bytes32"}],"name":"RoleAdminChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleGranted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleRevoked","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[],"name":"DEFAULT_ADMIN_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MINTER_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"cap","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"subtractedValue","type":"uint256"}],"name":"decreaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"}],"name":"getRoleAdmin","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"grantRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"hasRole","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"addedValue","type":"uint256"}],"name":"increaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"mint","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"renounceRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"revokeRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}]`; @@ -42,8 +43,8 @@ describe("Bridge e2e test", () => { let rootBridgeAddr = rootContracts.ROOT_BRIDGE_ADDRESS; let rootCustomTokenAddr = rootContracts.ROOT_TEST_CUSTOM_TOKEN; - rootProvider = new ethers.providers.JsonRpcProvider(rootRPCURL, Number(rootChainID)); - childProvider = new ethers.providers.JsonRpcProvider(childRPCURL, Number(childChainID)); + rootProvider = new RetryProvider(rootRPCURL, Number(rootChainID)); + childProvider = new RetryProvider(childRPCURL, Number(childChainID)); rootTestWallet = new ethers.Wallet(testAccountKey, rootProvider); childTestWallet = new ethers.Wallet(testAccountKey, childProvider); diff --git a/scripts/helpers/helpers.ts b/scripts/helpers/helpers.ts index 015478b2..c0ed555e 100644 --- a/scripts/helpers/helpers.ts +++ b/scripts/helpers/helpers.ts @@ -196,11 +196,15 @@ export async function verifyChildContract(contract: string, contractAddr: string return; } let cmd = `forge verify-contract --verifier blockscout --verifier-url ${url} ${contractAddr} ${contract}`; - const { stdout, stderr } = await exec(cmd); - if (stderr != "") { - throw(stderr); + try { + const { stdout, stderr } = await exec(cmd); + if (stderr != "") { + console.log(stderr); + } + console.log(stdout); + } catch (e) { + console.log(e); } - console.log(stdout); } export async function verifyRootContract(contract: string, contractAddr: string, args: string | null) { @@ -215,9 +219,13 @@ export async function verifyRootContract(contract: string, contractAddr: string, if (args != null) { cmd += ` --constructor-args $(cast abi-encode ${args})` } - const { stdout, stderr } = await exec(cmd); - if (stderr != "") { - throw(stderr); + try { + const { stdout, stderr } = await exec(cmd); + if (stderr != "") { + console.log(stderr); + } + console.log(stdout); + } catch (e) { + console.log(e); } - console.log(stdout); } \ No newline at end of file diff --git a/scripts/helpers/retry.ts b/scripts/helpers/retry.ts new file mode 100644 index 00000000..9e0d4cb5 --- /dev/null +++ b/scripts/helpers/retry.ts @@ -0,0 +1,39 @@ +import { providers, utils } from "ethers"; + +const MAX_ATTEMPT = 20; + +export class RetryProvider extends providers.JsonRpcProvider { + + constructor( + url?: utils.ConnectionInfo | string, + network?: providers.Networkish + ) { + super(url, network); + } + + public perform(method: string, params: any) { + let attempts = 0; + return utils.poll(() => { + if (attempts != 0) { + console.log("Retry RPC Request: " + attempts); + } + attempts++; + return super.perform(method, params) + .then(result => { + return result; + }, (error: any) => { + if (error.statusCode !== 429) { + return Promise.reject(error); + } else { + return Promise.resolve(undefined); + } + }) + .catch(error => { + console.log(error); + return Promise.resolve(undefined); + }) + }, { + retryLimit: MAX_ATTEMPT, + }); + } +} diff --git a/scripts/localdev/axelar_setup.ts b/scripts/localdev/axelar_setup.ts index b8b0c6f1..4450f3bd 100644 --- a/scripts/localdev/axelar_setup.ts +++ b/scripts/localdev/axelar_setup.ts @@ -4,6 +4,7 @@ import { Network, networks, EvmRelayer, relay } from '@axelar-network/axelar-loc import { requireEnv, waitForReceipt } from "../helpers/helpers"; import { ethers } from "ethers"; import * as fs from "fs"; +import { RetryProvider } from "../helpers/retry"; let relaying = false; const defaultEvmRelayer = new EvmRelayer(); @@ -20,7 +21,7 @@ async function main() { let axelarDeployerKey = requireEnv("AXELAR_DEPLOYER_SECRET"); // Create root chain. - let rootProvider = new ethers.providers.JsonRpcProvider(rootRPCURL, Number(rootChainID)); + let rootProvider = new RetryProvider(rootRPCURL, Number(rootChainID)); let rootChain = new Network(); rootChain.name = rootChainName; rootChain.chainId = Number(rootChainID); @@ -45,7 +46,7 @@ async function main() { rootChain.lastExpressedBlock = rootChain.lastRelayedBlock; // Create child chain. - let childProvider = new ethers.providers.JsonRpcProvider(childRPCURL, Number(childChainID)); + let childProvider = new RetryProvider(childRPCURL, Number(childChainID)); let childChain = new Network(); childChain.name = childChainName; childChain.chainId = Number(childChainID); diff --git a/scripts/localdev/childchain_setup.ts b/scripts/localdev/childchain_setup.ts index 93a0abd1..6a0db0a8 100644 --- a/scripts/localdev/childchain_setup.ts +++ b/scripts/localdev/childchain_setup.ts @@ -3,6 +3,7 @@ dotenv.config(); import { ethers as hardhat } from "hardhat"; import { ethers } from "ethers"; import { requireEnv } from "../helpers/helpers"; +import { RetryProvider } from "../helpers/retry"; async function main() { let childRPCURL = requireEnv("CHILD_RPC_URL"); @@ -10,7 +11,7 @@ async function main() { let deployerAddr = requireEnv("DEPLOYER_ADDR"); // Get child provider. - let childProvider = new ethers.providers.JsonRpcProvider(childRPCURL, Number(childChainID)); + let childProvider = new RetryProvider(childRPCURL, Number(childChainID)); // Give admin EOA account 2B IMX. await hardhat.provider.send("hardhat_setBalance", [ diff --git a/scripts/localdev/rootchain_setup.ts b/scripts/localdev/rootchain_setup.ts index abaaa503..c31fe00f 100644 --- a/scripts/localdev/rootchain_setup.ts +++ b/scripts/localdev/rootchain_setup.ts @@ -4,6 +4,7 @@ import { ethers as hardhat } from "hardhat"; import { ethers } from "ethers"; import { requireEnv, deployRootContract, waitForReceipt, saveRootContracts } from "../helpers/helpers"; import * as fs from "fs"; +import { RetryProvider } from "../helpers/retry"; async function main() { let rootRPCURL = requireEnv("ROOT_RPC_URL"); @@ -15,7 +16,7 @@ async function main() { let rootTestKey = requireEnv("TEST_ACCOUNT_SECRET"); // Get root provider. - let rootProvider = new ethers.providers.JsonRpcProvider(rootRPCURL, Number(rootChainID)); + let rootProvider = new RetryProvider(rootRPCURL, Number(rootChainID)); // Get test wwallet on the root chain. let testWallet = new ethers.Wallet(rootTestKey, rootProvider); From 6f5d84ad9325f73a49a3140eacb78fc36d7cded7 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Tue, 19 Dec 2023 11:22:57 +0800 Subject: [PATCH 058/155] Update rate configuration --- scripts/bootstrap/0_pre_validation.ts | 24 +++--- scripts/deploy/root_initialisation.ts | 108 +++++++++++++------------- 2 files changed, 66 insertions(+), 66 deletions(-) diff --git a/scripts/bootstrap/0_pre_validation.ts b/scripts/bootstrap/0_pre_validation.ts index 34b5dd42..d909b1ec 100644 --- a/scripts/bootstrap/0_pre_validation.ts +++ b/scripts/bootstrap/0_pre_validation.ts @@ -54,18 +54,18 @@ async function run() { requireEnv("RATE_LIMIT_USDC_CAPACITY"); requireEnv("RATE_LIMIT_USDC_REFILL_RATE"); requireEnv("RATE_LIMIT_USDC_LARGE_THRESHOLD"); - requireEnv("RATE_LIMIT_GU_ADDR"); - requireEnv("RATE_LIMIT_GU_CAPACITY"); - requireEnv("RATE_LIMIT_GU_REFILL_RATE"); - requireEnv("RATE_LIMIT_GU_LARGE_THRESHOLD"); - requireEnv("RATE_LIMIT_CHECKMATE_ADDR"); - requireEnv("RATE_LIMIT_CHECKMATE_CAPACITY"); - requireEnv("RATE_LIMIT_CHECKMATE_REFILL_RATE"); - requireEnv("RATE_LIMIT_CHECKMATE_LARGE_THRESHOLD"); - requireEnv("RATE_LIMIT_GOG_ADDR"); - requireEnv("RATE_LIMIT_GOG_CAPACITY"); - requireEnv("RATE_LIMIT_GOG_REFILL_RATE"); - requireEnv("RATE_LIMIT_GOG_LARGE_THRESHOLD"); + // requireEnv("RATE_LIMIT_GU_ADDR"); + // requireEnv("RATE_LIMIT_GU_CAPACITY"); + // requireEnv("RATE_LIMIT_GU_REFILL_RATE"); + // requireEnv("RATE_LIMIT_GU_LARGE_THRESHOLD"); + // requireEnv("RATE_LIMIT_CHECKMATE_ADDR"); + // requireEnv("RATE_LIMIT_CHECKMATE_CAPACITY"); + // requireEnv("RATE_LIMIT_CHECKMATE_REFILL_RATE"); + // requireEnv("RATE_LIMIT_CHECKMATE_LARGE_THRESHOLD"); + // requireEnv("RATE_LIMIT_GOG_ADDR"); + // requireEnv("RATE_LIMIT_GOG_CAPACITY"); + // requireEnv("RATE_LIMIT_GOG_REFILL_RATE"); + // requireEnv("RATE_LIMIT_GOG_LARGE_THRESHOLD"); const childProvider = new RetryProvider(childRPCURL, Number(childChainID)); const rootProvider = new RetryProvider(rootRPCURL, Number(rootChainID)); diff --git a/scripts/deploy/root_initialisation.ts b/scripts/deploy/root_initialisation.ts index 8e7bf7d2..1d550ffe 100644 --- a/scripts/deploy/root_initialisation.ts +++ b/scripts/deploy/root_initialisation.ts @@ -28,18 +28,18 @@ export async function initialiseRootContracts() { let rateLimitUSDCCap = requireEnv("RATE_LIMIT_USDC_CAPACITY"); let rateLimitUSDCRefill = requireEnv("RATE_LIMIT_USDC_REFILL_RATE"); let rateLimitUSDCLargeThreshold = requireEnv("RATE_LIMIT_USDC_LARGE_THRESHOLD"); - let rateLimitGUAddr = requireEnv("RATE_LIMIT_GU_ADDR"); - let rateLimitGUCap = requireEnv("RATE_LIMIT_GU_CAPACITY"); - let rateLimitGURefill = requireEnv("RATE_LIMIT_GU_REFILL_RATE"); - let rateLimitGULargeThreshold = requireEnv("RATE_LIMIT_GU_LARGE_THRESHOLD"); - let rateLimitCheckMateAddr = requireEnv("RATE_LIMIT_CHECKMATE_ADDR"); - let rateLimitCheckMateCap = requireEnv("RATE_LIMIT_CHECKMATE_CAPACITY"); - let rateLimitCheckMateRefill = requireEnv("RATE_LIMIT_CHECKMATE_REFILL_RATE"); - let rateLimitCheckMateLargeThreshold = requireEnv("RATE_LIMIT_CHECKMATE_LARGE_THRESHOLD"); - let rateLimitGOGAddr = requireEnv("RATE_LIMIT_GOG_ADDR"); - let rateLimitGOGCap = requireEnv("RATE_LIMIT_GOG_CAPACITY"); - let rateLimitGOGRefill = requireEnv("RATE_LIMIT_GOG_REFILL_RATE"); - let rateLimitGOGLargeThreshold = requireEnv("RATE_LIMIT_GOG_LARGE_THRESHOLD"); + // let rateLimitGUAddr = requireEnv("RATE_LIMIT_GU_ADDR"); + // let rateLimitGUCap = requireEnv("RATE_LIMIT_GU_CAPACITY"); + // let rateLimitGURefill = requireEnv("RATE_LIMIT_GU_REFILL_RATE"); + // let rateLimitGULargeThreshold = requireEnv("RATE_LIMIT_GU_LARGE_THRESHOLD"); + // let rateLimitCheckMateAddr = requireEnv("RATE_LIMIT_CHECKMATE_ADDR"); + // let rateLimitCheckMateCap = requireEnv("RATE_LIMIT_CHECKMATE_CAPACITY"); + // let rateLimitCheckMateRefill = requireEnv("RATE_LIMIT_CHECKMATE_REFILL_RATE"); + // let rateLimitCheckMateLargeThreshold = requireEnv("RATE_LIMIT_CHECKMATE_LARGE_THRESHOLD"); + // let rateLimitGOGAddr = requireEnv("RATE_LIMIT_GOG_ADDR"); + // let rateLimitGOGCap = requireEnv("RATE_LIMIT_GOG_CAPACITY"); + // let rateLimitGOGRefill = requireEnv("RATE_LIMIT_GOG_REFILL_RATE"); + // let rateLimitGOGLargeThreshold = requireEnv("RATE_LIMIT_GOG_LARGE_THRESHOLD"); // Read from contract file. let childContracts = getChildContracts(); @@ -138,50 +138,50 @@ export async function initialiseRootContracts() { await waitForReceipt(resp.hash, rootProvider); } - // GU - if ((await rootBridge.largeTransferThresholds(rateLimitGUAddr)).toString() != "0") { - console.log("GU rate limiting has already been configured, skip."); - } else { - console.log("Configure rate limiting for GU...") - let resp = await rootBridge.connect(rootDeployerWallet).setRateControlThreshold( - rateLimitGUAddr, - ethers.utils.parseEther(rateLimitGUCap), - ethers.utils.parseEther(rateLimitGURefill), - ethers.utils.parseEther(rateLimitGULargeThreshold) - ); - console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); - await waitForReceipt(resp.hash, rootProvider); - } + // // GU + // if ((await rootBridge.largeTransferThresholds(rateLimitGUAddr)).toString() != "0") { + // console.log("GU rate limiting has already been configured, skip."); + // } else { + // console.log("Configure rate limiting for GU...") + // let resp = await rootBridge.connect(rootDeployerWallet).setRateControlThreshold( + // rateLimitGUAddr, + // ethers.utils.parseEther(rateLimitGUCap), + // ethers.utils.parseEther(rateLimitGURefill), + // ethers.utils.parseEther(rateLimitGULargeThreshold) + // ); + // console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); + // await waitForReceipt(resp.hash, rootProvider); + // } - // Checkmate - if ((await rootBridge.largeTransferThresholds(rateLimitCheckMateAddr)).toString() != "0") { - console.log("CheckMate rate limiting has already been configured, skip."); - } else { - console.log("Configure rate limiting for CheckMate...") - let resp = await rootBridge.connect(rootDeployerWallet).setRateControlThreshold( - rateLimitCheckMateAddr, - ethers.utils.parseEther(rateLimitCheckMateCap), - ethers.utils.parseEther(rateLimitCheckMateRefill), - ethers.utils.parseEther(rateLimitCheckMateLargeThreshold) - ); - console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); - await waitForReceipt(resp.hash, rootProvider); - } + // // Checkmate + // if ((await rootBridge.largeTransferThresholds(rateLimitCheckMateAddr)).toString() != "0") { + // console.log("CheckMate rate limiting has already been configured, skip."); + // } else { + // console.log("Configure rate limiting for CheckMate...") + // let resp = await rootBridge.connect(rootDeployerWallet).setRateControlThreshold( + // rateLimitCheckMateAddr, + // ethers.utils.parseEther(rateLimitCheckMateCap), + // ethers.utils.parseEther(rateLimitCheckMateRefill), + // ethers.utils.parseEther(rateLimitCheckMateLargeThreshold) + // ); + // console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); + // await waitForReceipt(resp.hash, rootProvider); + // } - // GOG - if ((await rootBridge.largeTransferThresholds(rateLimitGOGAddr)).toString() != "0") { - console.log("GOG rate limiting has already been configured, skip."); - } else { - console.log("Configure rate limiting for GOG...") - let resp = await rootBridge.connect(rootDeployerWallet).setRateControlThreshold( - rateLimitGOGAddr, - ethers.utils.parseEther(rateLimitGOGCap), - ethers.utils.parseEther(rateLimitGOGRefill), - ethers.utils.parseEther(rateLimitGOGLargeThreshold) - ); - console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); - await waitForReceipt(resp.hash, rootProvider); - } + // // GOG + // if ((await rootBridge.largeTransferThresholds(rateLimitGOGAddr)).toString() != "0") { + // console.log("GOG rate limiting has already been configured, skip."); + // } else { + // console.log("Configure rate limiting for GOG...") + // let resp = await rootBridge.connect(rootDeployerWallet).setRateControlThreshold( + // rateLimitGOGAddr, + // ethers.utils.parseEther(rateLimitGOGCap), + // ethers.utils.parseEther(rateLimitGOGRefill), + // ethers.utils.parseEther(rateLimitGOGLargeThreshold) + // ); + // console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); + // await waitForReceipt(resp.hash, rootProvider); + // } // Grant roles if (await rootBridge.hasRole(utils.keccak256(utils.toUtf8Bytes("RATE")), rootPrivilegedMultisig)) { From 49f7af91516a587e122352f50adb2563600f3e29 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Tue, 19 Dec 2023 12:22:11 +0800 Subject: [PATCH 059/155] Fix rate limit for USDC --- scripts/deploy/root_initialisation.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/deploy/root_initialisation.ts b/scripts/deploy/root_initialisation.ts index 1d550ffe..4f826bf5 100644 --- a/scripts/deploy/root_initialisation.ts +++ b/scripts/deploy/root_initialisation.ts @@ -130,9 +130,9 @@ export async function initialiseRootContracts() { console.log("Configure rate limiting for USDC...") let resp = await rootBridge.connect(rootDeployerWallet).setRateControlThreshold( rateLimitUSDCAddr, - ethers.utils.parseEther(rateLimitUSDCCap), - ethers.utils.parseEther(rateLimitUSDCRefill), - ethers.utils.parseEther(rateLimitUSDCLargeThreshold) + ethers.utils.parseUnits(rateLimitUSDCCap, 6), + ethers.utils.parseUnits(rateLimitUSDCRefill, 6), + ethers.utils.parseUnits(rateLimitUSDCLargeThreshold, 6) ); console.log("Transaction submitted: ", JSON.stringify(resp, null, 2)); await waitForReceipt(resp.hash, rootProvider); From faa009368c8be1747b731ba319ba9a2d8ce0a59a Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Tue, 19 Dec 2023 21:36:42 +1100 Subject: [PATCH 060/155] Add mainnet deployment addresses --- README.md | 36 +++++++++---------- deployment/mainnet/child-chain-addresses.json | 12 ------- deployment/mainnet/root-chain-addresses.json | 11 ------ .../devnet/child-chain-addresses.json | 0 .../devnet/root-chain-addresses.json | 0 .../mainnet/child-chain-addresses.json | 12 +++++++ deployments/mainnet/root-chain-addresses.json | 11 ++++++ .../testnet/child-chain-addresses.json | 0 .../testnet/root-chain-addresses.json | 0 9 files changed, 41 insertions(+), 41 deletions(-) delete mode 100644 deployment/mainnet/child-chain-addresses.json delete mode 100644 deployment/mainnet/root-chain-addresses.json rename {deployment => deployments}/devnet/child-chain-addresses.json (100%) rename {deployment => deployments}/devnet/root-chain-addresses.json (100%) create mode 100644 deployments/mainnet/child-chain-addresses.json create mode 100644 deployments/mainnet/root-chain-addresses.json rename {deployment => deployments}/testnet/child-chain-addresses.json (100%) rename {deployment => deployments}/testnet/root-chain-addresses.json (100%) diff --git a/README.md b/README.md index 435a22ae..49d78f99 100644 --- a/README.md +++ b/README.md @@ -110,31 +110,31 @@ yarn local:test ### Remote Deployment -When deploying these contracts on remote networks (i.e. testnets or mainnets). Refer to [deployment](./scripts/deploy/README.md) or [bootstrap](./scripts/bootstrap/README.md). +When deploying these contracts on remote networks (i.e. testnet or mainnet), refer to the documentation in [deployment](./scripts/deploy/README.md) or [bootstrap](./scripts/bootstrap/README.md). ## Deployed Contract Addresses -Addresses for the core bridge contracts are listed below. For a full list of deployed contracts, see [deployments/](./deployments/). +Addresses for the core bridge contracts are listed below. For a full list of deployed contracts see [deployments/](./deployments) directory. ABIs for contracts can be obtained from the blockchain explorer links for each contract provided below. ### Root Chain -| | Mainnet | Testnet | Devnet | -|------------------------|-----------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------| -| Bridge Proxy | TBA | [`0x0D3C59c779Fd552C27b23F723E80246c840100F5`](https://sepolia.etherscan.io/address/0x0d3c59c779fd552c27b23f723e80246c840100f5) | TBA | -| Bridge Implementation | TBA | [`0xac88a57943b5BBa1ecd931F8494cAd0B7F717590`](https://sepolia.etherscan.io/address/0xac88a57943b5bba1ecd931f8494cad0b7f717590#code) | TBA | -| Adaptor Proxy | TBA | [`0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab`](https://sepolia.etherscan.io/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | -| Adaptor Implementation | TBA | [`0xe9ec55e1fC90AB69B2Fb4C029d24a4622B94042e`](https://sepolia.etherscan.io/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | -| Wrapped ETH | [`0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2`](https://etherscan.io/token/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2) | [`0x7b79995e5f793a07bc00c21412e50ecae098e7f9`](https://sepolia.etherscan.io/address/0x7b79995e5f793a07bc00c21412e50ecae098e7f9) | [`0x7b79995e5f793a07bc00c21412e50ecae098e7f9`](https://sepolia.etherscan.io/address/0x7b79995e5f793a07bc00c21412e50ecae098e7f9) | -| Wrapped IMX | [`0xf57e7e7c23978c3caec3c3548e3d615c346e79ff`](https://etherscan.io/token/0xf57e7e7c23978c3caec3c3548e3d615c346e79ff) | [`0xe2629e08f4125d14e446660028bd98ee60ee69f2`](https://sepolia.etherscan.io/address/0xe2629e08f4125d14e446660028bd98ee60ee69f2) | TBA | +| | Mainnet | Testnet | Devnet | +|------------------------|-------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------| +| Bridge Proxy | [`0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6`](https://etherscan.io/address/0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6) | [`0x0D3C59c779Fd552C27b23F723E80246c840100F5`](https://sepolia.etherscan.io/address/0x0d3c59c779fd552c27b23f723e80246c840100f5) | TBA | +| Bridge Implementation | [`0x177EaFe0f1F3359375B1728dae0530a75C83E154`](https://etherscan.io/address/0x177EaFe0f1F3359375B1728dae0530a75C83E154) | [`0xac88a57943b5BBa1ecd931F8494cAd0B7F717590`](https://sepolia.etherscan.io/address/0xac88a57943b5bba1ecd931f8494cad0b7f717590#code) | TBA | +| Adaptor Proxy | [`0x4f49B53928A71E553bB1B0F66a5BcB54Fd4E8932`](https://etherscan.io/address/0x4f49b53928a71e553bb1b0f66a5bcb54fd4e8932) | [`0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab`](https://sepolia.etherscan.io/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | +| Adaptor Implementation | [`0xE2E91C1Ae2873720C3b975a8034e887A35323345`](https://etherscan.io/address/0xE2E91C1Ae2873720C3b975a8034e887A35323345) | [`0xe9ec55e1fC90AB69B2Fb4C029d24a4622B94042e`](https://sepolia.etherscan.io/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | +| Wrapped ETH | [`0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2`](https://etherscan.io/token/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2) | [`0x7b79995e5f793a07bc00c21412e50ecae098e7f9`](https://sepolia.etherscan.io/address/0x7b79995e5f793a07bc00c21412e50ecae098e7f9) | [`0x7b79995e5f793a07bc00c21412e50ecae098e7f9`](https://sepolia.etherscan.io/address/0x7b79995e5f793a07bc00c21412e50ecae098e7f9) | +| IMX | [`0xf57e7e7c23978c3caec3c3548e3d615c346e79ff`](https://etherscan.io/token/0xf57e7e7c23978c3caec3c3548e3d615c346e79ff) | [`0xe2629e08f4125d14e446660028bd98ee60ee69f2`](https://sepolia.etherscan.io/address/0xe2629e08f4125d14e446660028bd98ee60ee69f2) | TBA | ### Child Chain -| | Mainnet | Testnet | Devnet | -|------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------|--------| -| Bridge Proxy | TBA | [`0x0D3C59c779Fd552C27b23F723E80246c840100F5`](https://explorer.testnet.immutable.com/address/0x0D3C59c779Fd552C27b23F723E80246c840100F5) | TBA | -| Bridge Implementation | TBA | [`0xA554Cf58b9524d43F1dee2fE1b0C928f18A93FE9`](https://explorer.testnet.immutable.com/address/0xA554Cf58b9524d43F1dee2fE1b0C928f18A93FE9) | TBA | -| Adaptor Proxy | TBA | [`0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab`](https://explorer.testnet.immutable.com/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | -| Adaptor Implementation | TBA | [`0xac88a57943b5BBa1ecd931F8494cAd0B7F717590`](https://explorer.testnet.immutable.com/address/0xac88a57943b5BBa1ecd931F8494cAd0B7F717590) | TBA | -| Wrapped ETH | TBA | [`0xe9E96d1aad82562b7588F03f49aD34186f996478`](https://explorer.testnet.immutable.com/address/0xe9E96d1aad82562b7588F03f49aD34186f996478) | TBA | -| Wrapped IMX | TBA | [`0x1CcCa691501174B4A623CeDA58cC8f1a76dc3439`](https://explorer.testnet.immutable.com/address/0x1CcCa691501174B4A623CeDA58cC8f1a76dc3439) | TBA | +| | Mainnet | Testnet | Devnet | +|------------------------|-----------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------|--------| +| Bridge Proxy | [`0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6`](https://explorer.immutable.com/address/0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6) | [`0x0D3C59c779Fd552C27b23F723E80246c840100F5`](https://explorer.testnet.immutable.com/address/0x0D3C59c779Fd552C27b23F723E80246c840100F5) | TBA | +| Bridge Implementation | [`0xb4c3597e6b090A2f6117780cEd103FB16B071A84`](https://explorer.immutable.com/address/0xb4c3597e6b090A2f6117780cEd103FB16B071A84) | [`0xA554Cf58b9524d43F1dee2fE1b0C928f18A93FE9`](https://explorer.testnet.immutable.com/address/0xA554Cf58b9524d43F1dee2fE1b0C928f18A93FE9) | TBA | +| Adaptor Proxy | [`0x4f49B53928A71E553bB1B0F66a5BcB54Fd4E8932`](https://explorer.immutable.com/address/0x4f49B53928A71E553bB1B0F66a5BcB54Fd4E8932) | [`0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab`](https://explorer.testnet.immutable.com/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | +| Adaptor Implementation | [`0x1d49c44dc4BbDE68D8D51a9C5732f3a24e48EFA6`](https://explorer.immutable.com/address/0x1d49c44dc4BbDE68D8D51a9C5732f3a24e48EFA6) | [`0xac88a57943b5BBa1ecd931F8494cAd0B7F717590`](https://explorer.testnet.immutable.com/address/0xac88a57943b5BBa1ecd931F8494cAd0B7F717590) | TBA | +| Wrapped ETH | [`0x52a6c53869ce09a731cd772f245b97a4401d3348`](https://explorer.immutable.com/address/0x52a6c53869ce09a731cd772f245b97a4401d3348) | [`0xe9E96d1aad82562b7588F03f49aD34186f996478`](https://explorer.testnet.immutable.com/address/0xe9E96d1aad82562b7588F03f49aD34186f996478) | TBA | +| Wrapped IMX | [`0x3a0c2ba54d6cbd3121f01b96dfd20e99d1696c9d`](https://explorer.immutable.com/address/0x3a0c2ba54d6cbd3121f01b96dfd20e99d1696c9d) | [`0x1CcCa691501174B4A623CeDA58cC8f1a76dc3439`](https://explorer.testnet.immutable.com/address/0x1CcCa691501174B4A623CeDA58cC8f1a76dc3439) | TBA | ## Audits The Immutable token bridge has been audited by [Trail of Bits](https://www.trailofbits.com/). The audit report can be found [here](./audits/Trail-of-Bits-2023-12-14.pdf). \ No newline at end of file diff --git a/deployment/mainnet/child-chain-addresses.json b/deployment/mainnet/child-chain-addresses.json deleted file mode 100644 index b953719b..00000000 --- a/deployment/mainnet/child-chain-addresses.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "CHILD_PROXY_ADMIN": "", - "CHILD_BRIDGE_IMPL_ADDRESS": "", - "CHILD_BRIDGE_PROXY_ADDRESS": "", - "CHILD_BRIDGE_ADDRESS": "", - "CHILD_ADAPTOR_IMPL_ADDRESS": "", - "CHILD_ADAPTOR_PROXY_ADDRESS": "", - "CHILD_ADAPTOR_ADDRESS": "", - "CHILD_TOKEN_TEMPLATE": "", - "WRAPPED_IMX_ADDRESS": "", - "CHILD_TEST_CUSTOM_TOKEN": "" -} \ No newline at end of file diff --git a/deployment/mainnet/root-chain-addresses.json b/deployment/mainnet/root-chain-addresses.json deleted file mode 100644 index 0be8a3f5..00000000 --- a/deployment/mainnet/root-chain-addresses.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "ROOT_PROXY_ADMIN": "", - "ROOT_BRIDGE_IMPL_ADDRESS": "", - "ROOT_BRIDGE_PROXY_ADDRESS": "", - "ROOT_BRIDGE_ADDRESS": "", - "ROOT_ADAPTOR_IMPL_ADDRESS": "", - "ROOT_ADAPTOR_PROXY_ADDRESS": "", - "ROOT_ADAPTOR_ADDRESS": "", - "ROOT_TOKEN_TEMPLATE": "", - "ROOT_TEST_CUSTOM_TOKEN": "" -} \ No newline at end of file diff --git a/deployment/devnet/child-chain-addresses.json b/deployments/devnet/child-chain-addresses.json similarity index 100% rename from deployment/devnet/child-chain-addresses.json rename to deployments/devnet/child-chain-addresses.json diff --git a/deployment/devnet/root-chain-addresses.json b/deployments/devnet/root-chain-addresses.json similarity index 100% rename from deployment/devnet/root-chain-addresses.json rename to deployments/devnet/root-chain-addresses.json diff --git a/deployments/mainnet/child-chain-addresses.json b/deployments/mainnet/child-chain-addresses.json new file mode 100644 index 00000000..358718f3 --- /dev/null +++ b/deployments/mainnet/child-chain-addresses.json @@ -0,0 +1,12 @@ +{ + "CHILD_PROXY_ADMIN": "0xdE2BCd3F0297d29c25e83228E5A33C0b43b51Ec8", + "CHILD_BRIDGE_IMPL_ADDRESS": "0xb4c3597e6b090A2f6117780cEd103FB16B071A84", + "CHILD_BRIDGE_PROXY_ADDRESS": "0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6", + "CHILD_BRIDGE_ADDRESS": "0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6", + "CHILD_ADAPTOR_IMPL_ADDRESS": "0x1d49c44dc4BbDE68D8D51a9C5732f3a24e48EFA6", + "CHILD_ADAPTOR_PROXY_ADDRESS": "0x4f49B53928A71E553bB1B0F66a5BcB54Fd4E8932", + "CHILD_ADAPTOR_ADDRESS": "0x4f49B53928A71E553bB1B0F66a5BcB54Fd4E8932", + "CHILD_TOKEN_TEMPLATE": "0x8804A8aA1F18f23aE8A456dD73806FdA3219FaD1", + "WRAPPED_IMX_ADDRESS": "0x3A0C2Ba54D6CBd3121F01b96dFd20e99D1696C9D", + "CHILD_TEST_CUSTOM_TOKEN": "" +} \ No newline at end of file diff --git a/deployments/mainnet/root-chain-addresses.json b/deployments/mainnet/root-chain-addresses.json new file mode 100644 index 00000000..8a53397e --- /dev/null +++ b/deployments/mainnet/root-chain-addresses.json @@ -0,0 +1,11 @@ +{ + "ROOT_PROXY_ADMIN": "0xdE2BCd3F0297d29c25e83228E5A33C0b43b51Ec8", + "ROOT_BRIDGE_IMPL_ADDRESS": "0x177EaFe0f1F3359375B1728dae0530a75C83E154", + "ROOT_BRIDGE_PROXY_ADDRESS": "0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6", + "ROOT_BRIDGE_ADDRESS": "0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6", + "ROOT_ADAPTOR_IMPL_ADDRESS": "0xE2E91C1Ae2873720C3b975a8034e887A35323345", + "ROOT_ADAPTOR_PROXY_ADDRESS": "0x4f49B53928A71E553bB1B0F66a5BcB54Fd4E8932", + "ROOT_ADAPTOR_ADDRESS": "0x4f49B53928A71E553bB1B0F66a5BcB54Fd4E8932", + "ROOT_TOKEN_TEMPLATE": "0x8804A8aA1F18f23aE8A456dD73806FdA3219FaD1", + "ROOT_TEST_CUSTOM_TOKEN": "0xA060151cF8c803202d3A6182bDfEF019C8d836e2" +} \ No newline at end of file diff --git a/deployment/testnet/child-chain-addresses.json b/deployments/testnet/child-chain-addresses.json similarity index 100% rename from deployment/testnet/child-chain-addresses.json rename to deployments/testnet/child-chain-addresses.json diff --git a/deployment/testnet/root-chain-addresses.json b/deployments/testnet/root-chain-addresses.json similarity index 100% rename from deployment/testnet/root-chain-addresses.json rename to deployments/testnet/root-chain-addresses.json From c66b5e2637d7f91ba9da4d05c017e4a850cc9e3c Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Tue, 19 Dec 2023 21:38:56 +1100 Subject: [PATCH 061/155] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 49d78f99..aff8f932 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ Addresses for the core bridge contracts are listed below. For a full list of dep ABIs for contracts can be obtained from the blockchain explorer links for each contract provided below. ### Root Chain -| | Mainnet | Testnet | Devnet | +| | Mainnet (Ethereum) | Testnet (Sepolia) | Devnet (Sepolia) | |------------------------|-------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------| | Bridge Proxy | [`0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6`](https://etherscan.io/address/0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6) | [`0x0D3C59c779Fd552C27b23F723E80246c840100F5`](https://sepolia.etherscan.io/address/0x0d3c59c779fd552c27b23f723e80246c840100f5) | TBA | | Bridge Implementation | [`0x177EaFe0f1F3359375B1728dae0530a75C83E154`](https://etherscan.io/address/0x177EaFe0f1F3359375B1728dae0530a75C83E154) | [`0xac88a57943b5BBa1ecd931F8494cAd0B7F717590`](https://sepolia.etherscan.io/address/0xac88a57943b5bba1ecd931f8494cad0b7f717590#code) | TBA | From 7b37f2aadaa7ad262e930ed26e09f03e7aee5f23 Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Tue, 9 Jan 2024 10:05:33 +1100 Subject: [PATCH 062/155] Add USDC USDT and wBTC mapped addresses --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index aff8f932..7ee496be 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,10 @@ ABIs for contracts can be obtained from the blockchain explorer links for each c | Adaptor Implementation | [`0x1d49c44dc4BbDE68D8D51a9C5732f3a24e48EFA6`](https://explorer.immutable.com/address/0x1d49c44dc4BbDE68D8D51a9C5732f3a24e48EFA6) | [`0xac88a57943b5BBa1ecd931F8494cAd0B7F717590`](https://explorer.testnet.immutable.com/address/0xac88a57943b5BBa1ecd931F8494cAd0B7F717590) | TBA | | Wrapped ETH | [`0x52a6c53869ce09a731cd772f245b97a4401d3348`](https://explorer.immutable.com/address/0x52a6c53869ce09a731cd772f245b97a4401d3348) | [`0xe9E96d1aad82562b7588F03f49aD34186f996478`](https://explorer.testnet.immutable.com/address/0xe9E96d1aad82562b7588F03f49aD34186f996478) | TBA | | Wrapped IMX | [`0x3a0c2ba54d6cbd3121f01b96dfd20e99d1696c9d`](https://explorer.immutable.com/address/0x3a0c2ba54d6cbd3121f01b96dfd20e99d1696c9d) | [`0x1CcCa691501174B4A623CeDA58cC8f1a76dc3439`](https://explorer.testnet.immutable.com/address/0x1CcCa691501174B4A623CeDA58cC8f1a76dc3439) | TBA | +| USDC | [`0x6de8aCC0D406837030CE4dd28e7c08C5a96a30d2`](https://explorer.immutable.com/address/0x6de8aCC0D406837030CE4dd28e7c08C5a96a30d2) | TBA | TBA | +| USDT | [`0x68bcc7F1190AF20e7b572BCfb431c3Ac10A936Ab`](https://explorer.immutable.com/address/0x68bcc7F1190AF20e7b572BCfb431c3Ac10A936Ab) | TBA | TBA | +| Wrapped BTC | [`0x235F9A2Dc29E51cE7D103bcC5Dfb4F5c9c3371De`](https://explorer.immutable.com/address/0x235F9A2Dc29E51cE7D103bcC5Dfb4F5c9c3371De) | TBA | TBA | + ## Audits The Immutable token bridge has been audited by [Trail of Bits](https://www.trailofbits.com/). The audit report can be found [here](./audits/Trail-of-Bits-2023-12-14.pdf). \ No newline at end of file From 280aac92de7bcf9c60dbf348404e5354385a11ba Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Tue, 9 Jan 2024 10:28:23 +1100 Subject: [PATCH 063/155] Fix the Foundry version used for builds The most recent version of Foundry seems to have an open bug. This bug causes the formatter check to produce a different result compared to previous versions. This commit reverts to the version of Foundry that was used before the bug was introduced. https://github.com/foundry-rs/foundry/issues/6726 --- .github/workflows/coverage.yml | 2 +- .github/workflows/e2e.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/test.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index c1ba6d25..3a4d7b13 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -25,7 +25,7 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: - version: nightly + version: nightly-34f684ddfacc5b2ed371353ba6f730c485616ffe - name: Run Forge build run: | diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 2f261b0e..5faa70b4 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -21,7 +21,7 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: - version: nightly + version: nightly-34f684ddfacc5b2ed371353ba6f730c485616ffe - name: Run install uses: borales/actions-yarn@v4 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d5804b89..73c7580d 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -20,7 +20,7 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: - version: nightly + version: nightly-34f684ddfacc5b2ed371353ba6f730c485616ffe - name: Run Forge fmt --check run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 921bdba7..52f77ec5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,7 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: - version: nightly + version: nightly-34f684ddfacc5b2ed371353ba6f730c485616ffe - name: Run Forge build run: | From 2e799fe0753c3696c29894aac5bfe2e136884d14 Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Tue, 9 Jan 2024 10:39:30 +1100 Subject: [PATCH 064/155] Refactor table structure for clarity --- README.md | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 7ee496be..58f466e3 100644 --- a/README.md +++ b/README.md @@ -117,28 +117,38 @@ Addresses for the core bridge contracts are listed below. For a full list of dep ABIs for contracts can be obtained from the blockchain explorer links for each contract provided below. ### Root Chain +#### Core Contracts + +| | Mainnet (Ethereum) | Testnet (Sepolia) | Devnet (Sepolia) | +|------------------------|-------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|------------------| +| Bridge Proxy | [`0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6`](https://etherscan.io/address/0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6) | [`0x0D3C59c779Fd552C27b23F723E80246c840100F5`](https://sepolia.etherscan.io/address/0x0d3c59c779fd552c27b23f723e80246c840100f5) | TBA | +| Bridge Implementation | [`0x177EaFe0f1F3359375B1728dae0530a75C83E154`](https://etherscan.io/address/0x177EaFe0f1F3359375B1728dae0530a75C83E154) | [`0xac88a57943b5BBa1ecd931F8494cAd0B7F717590`](https://sepolia.etherscan.io/address/0xac88a57943b5bba1ecd931f8494cad0b7f717590#code) | TBA | +| Adaptor Proxy | [`0x4f49B53928A71E553bB1B0F66a5BcB54Fd4E8932`](https://etherscan.io/address/0x4f49b53928a71e553bb1b0f66a5bcb54fd4e8932) | [`0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab`](https://sepolia.etherscan.io/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | +| Adaptor Implementation | [`0xE2E91C1Ae2873720C3b975a8034e887A35323345`](https://etherscan.io/address/0xE2E91C1Ae2873720C3b975a8034e887A35323345) | [`0xe9ec55e1fC90AB69B2Fb4C029d24a4622B94042e`](https://sepolia.etherscan.io/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | + +#### Token Addresses | | Mainnet (Ethereum) | Testnet (Sepolia) | Devnet (Sepolia) | |------------------------|-------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------| -| Bridge Proxy | [`0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6`](https://etherscan.io/address/0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6) | [`0x0D3C59c779Fd552C27b23F723E80246c840100F5`](https://sepolia.etherscan.io/address/0x0d3c59c779fd552c27b23f723e80246c840100f5) | TBA | -| Bridge Implementation | [`0x177EaFe0f1F3359375B1728dae0530a75C83E154`](https://etherscan.io/address/0x177EaFe0f1F3359375B1728dae0530a75C83E154) | [`0xac88a57943b5BBa1ecd931F8494cAd0B7F717590`](https://sepolia.etherscan.io/address/0xac88a57943b5bba1ecd931f8494cad0b7f717590#code) | TBA | -| Adaptor Proxy | [`0x4f49B53928A71E553bB1B0F66a5BcB54Fd4E8932`](https://etherscan.io/address/0x4f49b53928a71e553bb1b0f66a5bcb54fd4e8932) | [`0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab`](https://sepolia.etherscan.io/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | -| Adaptor Implementation | [`0xE2E91C1Ae2873720C3b975a8034e887A35323345`](https://etherscan.io/address/0xE2E91C1Ae2873720C3b975a8034e887A35323345) | [`0xe9ec55e1fC90AB69B2Fb4C029d24a4622B94042e`](https://sepolia.etherscan.io/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | +| | Wrapped ETH | [`0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2`](https://etherscan.io/token/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2) | [`0x7b79995e5f793a07bc00c21412e50ecae098e7f9`](https://sepolia.etherscan.io/address/0x7b79995e5f793a07bc00c21412e50ecae098e7f9) | [`0x7b79995e5f793a07bc00c21412e50ecae098e7f9`](https://sepolia.etherscan.io/address/0x7b79995e5f793a07bc00c21412e50ecae098e7f9) | | IMX | [`0xf57e7e7c23978c3caec3c3548e3d615c346e79ff`](https://etherscan.io/token/0xf57e7e7c23978c3caec3c3548e3d615c346e79ff) | [`0xe2629e08f4125d14e446660028bd98ee60ee69f2`](https://sepolia.etherscan.io/address/0xe2629e08f4125d14e446660028bd98ee60ee69f2) | TBA | ### Child Chain +#### Core Contracts | | Mainnet | Testnet | Devnet | |------------------------|-----------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------|--------| | Bridge Proxy | [`0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6`](https://explorer.immutable.com/address/0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6) | [`0x0D3C59c779Fd552C27b23F723E80246c840100F5`](https://explorer.testnet.immutable.com/address/0x0D3C59c779Fd552C27b23F723E80246c840100F5) | TBA | | Bridge Implementation | [`0xb4c3597e6b090A2f6117780cEd103FB16B071A84`](https://explorer.immutable.com/address/0xb4c3597e6b090A2f6117780cEd103FB16B071A84) | [`0xA554Cf58b9524d43F1dee2fE1b0C928f18A93FE9`](https://explorer.testnet.immutable.com/address/0xA554Cf58b9524d43F1dee2fE1b0C928f18A93FE9) | TBA | | Adaptor Proxy | [`0x4f49B53928A71E553bB1B0F66a5BcB54Fd4E8932`](https://explorer.immutable.com/address/0x4f49B53928A71E553bB1B0F66a5BcB54Fd4E8932) | [`0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab`](https://explorer.testnet.immutable.com/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | | Adaptor Implementation | [`0x1d49c44dc4BbDE68D8D51a9C5732f3a24e48EFA6`](https://explorer.immutable.com/address/0x1d49c44dc4BbDE68D8D51a9C5732f3a24e48EFA6) | [`0xac88a57943b5BBa1ecd931F8494cAd0B7F717590`](https://explorer.testnet.immutable.com/address/0xac88a57943b5BBa1ecd931F8494cAd0B7F717590) | TBA | + +#### Token Addresses +| | Mainnet | Testnet | Devnet | +|------------------------|-----------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------|--------| | Wrapped ETH | [`0x52a6c53869ce09a731cd772f245b97a4401d3348`](https://explorer.immutable.com/address/0x52a6c53869ce09a731cd772f245b97a4401d3348) | [`0xe9E96d1aad82562b7588F03f49aD34186f996478`](https://explorer.testnet.immutable.com/address/0xe9E96d1aad82562b7588F03f49aD34186f996478) | TBA | | Wrapped IMX | [`0x3a0c2ba54d6cbd3121f01b96dfd20e99d1696c9d`](https://explorer.immutable.com/address/0x3a0c2ba54d6cbd3121f01b96dfd20e99d1696c9d) | [`0x1CcCa691501174B4A623CeDA58cC8f1a76dc3439`](https://explorer.testnet.immutable.com/address/0x1CcCa691501174B4A623CeDA58cC8f1a76dc3439) | TBA | | USDC | [`0x6de8aCC0D406837030CE4dd28e7c08C5a96a30d2`](https://explorer.immutable.com/address/0x6de8aCC0D406837030CE4dd28e7c08C5a96a30d2) | TBA | TBA | | USDT | [`0x68bcc7F1190AF20e7b572BCfb431c3Ac10A936Ab`](https://explorer.immutable.com/address/0x68bcc7F1190AF20e7b572BCfb431c3Ac10A936Ab) | TBA | TBA | | Wrapped BTC | [`0x235F9A2Dc29E51cE7D103bcC5Dfb4F5c9c3371De`](https://explorer.immutable.com/address/0x235F9A2Dc29E51cE7D103bcC5Dfb4F5c9c3371De) | TBA | TBA | - - ## Audits The Immutable token bridge has been audited by [Trail of Bits](https://www.trailofbits.com/). The audit report can be found [here](./audits/Trail-of-Bits-2023-12-14.pdf). \ No newline at end of file From 288271d68c08e6c2d384b21e84b0c57924c30d80 Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Tue, 9 Jan 2024 10:43:02 +1100 Subject: [PATCH 065/155] Fix table formatting --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 58f466e3..eaa9cebe 100644 --- a/README.md +++ b/README.md @@ -127,11 +127,10 @@ ABIs for contracts can be obtained from the blockchain explorer links for each c | Adaptor Implementation | [`0xE2E91C1Ae2873720C3b975a8034e887A35323345`](https://etherscan.io/address/0xE2E91C1Ae2873720C3b975a8034e887A35323345) | [`0xe9ec55e1fC90AB69B2Fb4C029d24a4622B94042e`](https://sepolia.etherscan.io/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | #### Token Addresses -| | Mainnet (Ethereum) | Testnet (Sepolia) | Devnet (Sepolia) | -|------------------------|-------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------| -| -| Wrapped ETH | [`0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2`](https://etherscan.io/token/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2) | [`0x7b79995e5f793a07bc00c21412e50ecae098e7f9`](https://sepolia.etherscan.io/address/0x7b79995e5f793a07bc00c21412e50ecae098e7f9) | [`0x7b79995e5f793a07bc00c21412e50ecae098e7f9`](https://sepolia.etherscan.io/address/0x7b79995e5f793a07bc00c21412e50ecae098e7f9) | -| IMX | [`0xf57e7e7c23978c3caec3c3548e3d615c346e79ff`](https://etherscan.io/token/0xf57e7e7c23978c3caec3c3548e3d615c346e79ff) | [`0xe2629e08f4125d14e446660028bd98ee60ee69f2`](https://sepolia.etherscan.io/address/0xe2629e08f4125d14e446660028bd98ee60ee69f2) | TBA | +| | Mainnet (Ethereum) | Testnet (Sepolia) | Devnet (Sepolia) | +|-------------|-----------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------| +| Wrapped ETH | [`0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2`](https://etherscan.io/token/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2) | [`0x7b79995e5f793a07bc00c21412e50ecae098e7f9`](https://sepolia.etherscan.io/address/0x7b79995e5f793a07bc00c21412e50ecae098e7f9) | [`0x7b79995e5f793a07bc00c21412e50ecae098e7f9`](https://sepolia.etherscan.io/address/0x7b79995e5f793a07bc00c21412e50ecae098e7f9) | +| IMX | [`0xf57e7e7c23978c3caec3c3548e3d615c346e79ff`](https://etherscan.io/token/0xf57e7e7c23978c3caec3c3548e3d615c346e79ff) | [`0xe2629e08f4125d14e446660028bd98ee60ee69f2`](https://sepolia.etherscan.io/address/0xe2629e08f4125d14e446660028bd98ee60ee69f2) | TBA | ### Child Chain #### Core Contracts From 108df4ce3468e9400eadbcfe8b1cbcea4521ffc0 Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Tue, 9 Jan 2024 19:05:14 +1100 Subject: [PATCH 066/155] Remove devnet addresses --- README.md | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index eaa9cebe..e9993825 100644 --- a/README.md +++ b/README.md @@ -119,35 +119,35 @@ ABIs for contracts can be obtained from the blockchain explorer links for each c ### Root Chain #### Core Contracts -| | Mainnet (Ethereum) | Testnet (Sepolia) | Devnet (Sepolia) | -|------------------------|-------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|------------------| -| Bridge Proxy | [`0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6`](https://etherscan.io/address/0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6) | [`0x0D3C59c779Fd552C27b23F723E80246c840100F5`](https://sepolia.etherscan.io/address/0x0d3c59c779fd552c27b23f723e80246c840100f5) | TBA | -| Bridge Implementation | [`0x177EaFe0f1F3359375B1728dae0530a75C83E154`](https://etherscan.io/address/0x177EaFe0f1F3359375B1728dae0530a75C83E154) | [`0xac88a57943b5BBa1ecd931F8494cAd0B7F717590`](https://sepolia.etherscan.io/address/0xac88a57943b5bba1ecd931f8494cad0b7f717590#code) | TBA | -| Adaptor Proxy | [`0x4f49B53928A71E553bB1B0F66a5BcB54Fd4E8932`](https://etherscan.io/address/0x4f49b53928a71e553bb1b0f66a5bcb54fd4e8932) | [`0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab`](https://sepolia.etherscan.io/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | -| Adaptor Implementation | [`0xE2E91C1Ae2873720C3b975a8034e887A35323345`](https://etherscan.io/address/0xE2E91C1Ae2873720C3b975a8034e887A35323345) | [`0xe9ec55e1fC90AB69B2Fb4C029d24a4622B94042e`](https://sepolia.etherscan.io/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | +| | Mainnet (Ethereum) | Testnet (Sepolia) | +|------------------------|-------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------| +| Bridge Proxy | [`0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6`](https://etherscan.io/address/0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6) | [`0x0D3C59c779Fd552C27b23F723E80246c840100F5`](https://sepolia.etherscan.io/address/0x0d3c59c779fd552c27b23f723e80246c840100f5) | +| Bridge Implementation | [`0x177EaFe0f1F3359375B1728dae0530a75C83E154`](https://etherscan.io/address/0x177EaFe0f1F3359375B1728dae0530a75C83E154) | [`0xac88a57943b5BBa1ecd931F8494cAd0B7F717590`](https://sepolia.etherscan.io/address/0xac88a57943b5bba1ecd931f8494cad0b7f717590#code) | +| Adaptor Proxy | [`0x4f49B53928A71E553bB1B0F66a5BcB54Fd4E8932`](https://etherscan.io/address/0x4f49b53928a71e553bb1b0f66a5bcb54fd4e8932) | [`0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab`](https://sepolia.etherscan.io/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | +| Adaptor Implementation | [`0xE2E91C1Ae2873720C3b975a8034e887A35323345`](https://etherscan.io/address/0xE2E91C1Ae2873720C3b975a8034e887A35323345) | [`0xe9ec55e1fC90AB69B2Fb4C029d24a4622B94042e`](https://sepolia.etherscan.io/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | #### Token Addresses -| | Mainnet (Ethereum) | Testnet (Sepolia) | Devnet (Sepolia) | -|-------------|-----------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------| -| Wrapped ETH | [`0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2`](https://etherscan.io/token/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2) | [`0x7b79995e5f793a07bc00c21412e50ecae098e7f9`](https://sepolia.etherscan.io/address/0x7b79995e5f793a07bc00c21412e50ecae098e7f9) | [`0x7b79995e5f793a07bc00c21412e50ecae098e7f9`](https://sepolia.etherscan.io/address/0x7b79995e5f793a07bc00c21412e50ecae098e7f9) | -| IMX | [`0xf57e7e7c23978c3caec3c3548e3d615c346e79ff`](https://etherscan.io/token/0xf57e7e7c23978c3caec3c3548e3d615c346e79ff) | [`0xe2629e08f4125d14e446660028bd98ee60ee69f2`](https://sepolia.etherscan.io/address/0xe2629e08f4125d14e446660028bd98ee60ee69f2) | TBA | +| | Mainnet (Ethereum) | Testnet (Sepolia) | +|-------------|-----------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------| +| Wrapped ETH | [`0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2`](https://etherscan.io/token/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2) | [`0x7b79995e5f793a07bc00c21412e50ecae098e7f9`](https://sepolia.etherscan.io/address/0x7b79995e5f793a07bc00c21412e50ecae098e7f9) | +| IMX | [`0xf57e7e7c23978c3caec3c3548e3d615c346e79ff`](https://etherscan.io/token/0xf57e7e7c23978c3caec3c3548e3d615c346e79ff) | [`0xe2629e08f4125d14e446660028bd98ee60ee69f2`](https://sepolia.etherscan.io/address/0xe2629e08f4125d14e446660028bd98ee60ee69f2) | ### Child Chain #### Core Contracts -| | Mainnet | Testnet | Devnet | -|------------------------|-----------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------|--------| -| Bridge Proxy | [`0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6`](https://explorer.immutable.com/address/0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6) | [`0x0D3C59c779Fd552C27b23F723E80246c840100F5`](https://explorer.testnet.immutable.com/address/0x0D3C59c779Fd552C27b23F723E80246c840100F5) | TBA | -| Bridge Implementation | [`0xb4c3597e6b090A2f6117780cEd103FB16B071A84`](https://explorer.immutable.com/address/0xb4c3597e6b090A2f6117780cEd103FB16B071A84) | [`0xA554Cf58b9524d43F1dee2fE1b0C928f18A93FE9`](https://explorer.testnet.immutable.com/address/0xA554Cf58b9524d43F1dee2fE1b0C928f18A93FE9) | TBA | -| Adaptor Proxy | [`0x4f49B53928A71E553bB1B0F66a5BcB54Fd4E8932`](https://explorer.immutable.com/address/0x4f49B53928A71E553bB1B0F66a5BcB54Fd4E8932) | [`0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab`](https://explorer.testnet.immutable.com/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | TBA | -| Adaptor Implementation | [`0x1d49c44dc4BbDE68D8D51a9C5732f3a24e48EFA6`](https://explorer.immutable.com/address/0x1d49c44dc4BbDE68D8D51a9C5732f3a24e48EFA6) | [`0xac88a57943b5BBa1ecd931F8494cAd0B7F717590`](https://explorer.testnet.immutable.com/address/0xac88a57943b5BBa1ecd931F8494cAd0B7F717590) | TBA | +| | Mainnet | Testnet | +|------------------------|-----------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------| +| Bridge Proxy | [`0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6`](https://explorer.immutable.com/address/0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6) | [`0x0D3C59c779Fd552C27b23F723E80246c840100F5`](https://explorer.testnet.immutable.com/address/0x0D3C59c779Fd552C27b23F723E80246c840100F5) | +| Bridge Implementation | [`0xb4c3597e6b090A2f6117780cEd103FB16B071A84`](https://explorer.immutable.com/address/0xb4c3597e6b090A2f6117780cEd103FB16B071A84) | [`0xA554Cf58b9524d43F1dee2fE1b0C928f18A93FE9`](https://explorer.testnet.immutable.com/address/0xA554Cf58b9524d43F1dee2fE1b0C928f18A93FE9) | +| Adaptor Proxy | [`0x4f49B53928A71E553bB1B0F66a5BcB54Fd4E8932`](https://explorer.immutable.com/address/0x4f49B53928A71E553bB1B0F66a5BcB54Fd4E8932) | [`0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab`](https://explorer.testnet.immutable.com/address/0x6328Ac88ba8D466a0F551FC7C42C61d1aC7f92ab) | +| Adaptor Implementation | [`0x1d49c44dc4BbDE68D8D51a9C5732f3a24e48EFA6`](https://explorer.immutable.com/address/0x1d49c44dc4BbDE68D8D51a9C5732f3a24e48EFA6) | [`0xac88a57943b5BBa1ecd931F8494cAd0B7F717590`](https://explorer.testnet.immutable.com/address/0xac88a57943b5BBa1ecd931F8494cAd0B7F717590) | #### Token Addresses -| | Mainnet | Testnet | Devnet | -|------------------------|-----------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------|--------| -| Wrapped ETH | [`0x52a6c53869ce09a731cd772f245b97a4401d3348`](https://explorer.immutable.com/address/0x52a6c53869ce09a731cd772f245b97a4401d3348) | [`0xe9E96d1aad82562b7588F03f49aD34186f996478`](https://explorer.testnet.immutable.com/address/0xe9E96d1aad82562b7588F03f49aD34186f996478) | TBA | +| | Mainnet | Testnet | +|------------------------|-----------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------| +| Wrapped ETH | [`0x52a6c53869ce09a731cd772f245b97a4401d3348`](https://explorer.immutable.com/address/0x52a6c53869ce09a731cd772f245b97a4401d3348) | [`0xe9E96d1aad82562b7588F03f49aD34186f996478`](https://explorer.testnet.immutable.com/address/0xe9E96d1aad82562b7588F03f49aD34186f996478) | | Wrapped IMX | [`0x3a0c2ba54d6cbd3121f01b96dfd20e99d1696c9d`](https://explorer.immutable.com/address/0x3a0c2ba54d6cbd3121f01b96dfd20e99d1696c9d) | [`0x1CcCa691501174B4A623CeDA58cC8f1a76dc3439`](https://explorer.testnet.immutable.com/address/0x1CcCa691501174B4A623CeDA58cC8f1a76dc3439) | TBA | -| USDC | [`0x6de8aCC0D406837030CE4dd28e7c08C5a96a30d2`](https://explorer.immutable.com/address/0x6de8aCC0D406837030CE4dd28e7c08C5a96a30d2) | TBA | TBA | -| USDT | [`0x68bcc7F1190AF20e7b572BCfb431c3Ac10A936Ab`](https://explorer.immutable.com/address/0x68bcc7F1190AF20e7b572BCfb431c3Ac10A936Ab) | TBA | TBA | -| Wrapped BTC | [`0x235F9A2Dc29E51cE7D103bcC5Dfb4F5c9c3371De`](https://explorer.immutable.com/address/0x235F9A2Dc29E51cE7D103bcC5Dfb4F5c9c3371De) | TBA | TBA | +| USDC | [`0x6de8aCC0D406837030CE4dd28e7c08C5a96a30d2`](https://explorer.immutable.com/address/0x6de8aCC0D406837030CE4dd28e7c08C5a96a30d2) | TBA | +| USDT | [`0x68bcc7F1190AF20e7b572BCfb431c3Ac10A936Ab`](https://explorer.immutable.com/address/0x68bcc7F1190AF20e7b572BCfb431c3Ac10A936Ab) | TBA | +| Wrapped BTC | [`0x235F9A2Dc29E51cE7D103bcC5Dfb4F5c9c3371De`](https://explorer.immutable.com/address/0x235F9A2Dc29E51cE7D103bcC5Dfb4F5c9c3371De) | TBA | ## Audits The Immutable token bridge has been audited by [Trail of Bits](https://www.trailofbits.com/). The audit report can be found [here](./audits/Trail-of-Bits-2023-12-14.pdf). \ No newline at end of file From 3fb47c8c2f0a1a81b6f7e587eb77790bef255dcf Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Tue, 9 Jan 2024 19:10:04 +1100 Subject: [PATCH 067/155] Remove redundant section marker --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index e9993825..78724bf8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ # Immutable Token Bridge ----- The Immutable token bridge facilitates the transfer of assets between two chains, namely Ethereum (the Root chain) and the Immutable chain (the Child chain). At present, the bridge only supports the transfer of standard ERC20 tokens originating from Ethereum, as well as native assets (ETH and IMX). Other types of assets (such as ERC721) and assets originating from the Child chain are not currently supported. ## Contents From 2b0efca341199d8db3781022cf586a14c36ff292 Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Wed, 17 Jan 2024 11:26:26 +1100 Subject: [PATCH 068/155] Document USDC token addresses for testnet --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 78724bf8..2213bfc9 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,7 @@ ABIs for contracts can be obtained from the blockchain explorer links for each c |-------------|-----------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------| | Wrapped ETH | [`0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2`](https://etherscan.io/token/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2) | [`0x7b79995e5f793a07bc00c21412e50ecae098e7f9`](https://sepolia.etherscan.io/address/0x7b79995e5f793a07bc00c21412e50ecae098e7f9) | | IMX | [`0xf57e7e7c23978c3caec3c3548e3d615c346e79ff`](https://etherscan.io/token/0xf57e7e7c23978c3caec3c3548e3d615c346e79ff) | [`0xe2629e08f4125d14e446660028bd98ee60ee69f2`](https://sepolia.etherscan.io/address/0xe2629e08f4125d14e446660028bd98ee60ee69f2) | +| USDC | [`0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48`](https://etherscan.io/token/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) | [`0xe2629e08f4125d14e446660028bd98ee60ee69f2`](https://sepolia.etherscan.io/address/0x40b87d235A5B010a20A241F15797C9debf1ecd01) | ### Child Chain #### Core Contracts @@ -145,8 +146,8 @@ ABIs for contracts can be obtained from the blockchain explorer links for each c |------------------------|-----------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------| | Wrapped ETH | [`0x52a6c53869ce09a731cd772f245b97a4401d3348`](https://explorer.immutable.com/address/0x52a6c53869ce09a731cd772f245b97a4401d3348) | [`0xe9E96d1aad82562b7588F03f49aD34186f996478`](https://explorer.testnet.immutable.com/address/0xe9E96d1aad82562b7588F03f49aD34186f996478) | | Wrapped IMX | [`0x3a0c2ba54d6cbd3121f01b96dfd20e99d1696c9d`](https://explorer.immutable.com/address/0x3a0c2ba54d6cbd3121f01b96dfd20e99d1696c9d) | [`0x1CcCa691501174B4A623CeDA58cC8f1a76dc3439`](https://explorer.testnet.immutable.com/address/0x1CcCa691501174B4A623CeDA58cC8f1a76dc3439) | TBA | -| USDC | [`0x6de8aCC0D406837030CE4dd28e7c08C5a96a30d2`](https://explorer.immutable.com/address/0x6de8aCC0D406837030CE4dd28e7c08C5a96a30d2) | TBA | +| USDC | [`0x6de8aCC0D406837030CE4dd28e7c08C5a96a30d2`](https://explorer.immutable.com/address/0x6de8aCC0D406837030CE4dd28e7c08C5a96a30d2) | [`0x3B2d8A1931736Fc321C24864BceEe981B11c3c57`](https://explorer.testnet.immutable.com/address/0x3B2d8A1931736Fc321C24864BceEe981B11c3c57) | | USDT | [`0x68bcc7F1190AF20e7b572BCfb431c3Ac10A936Ab`](https://explorer.immutable.com/address/0x68bcc7F1190AF20e7b572BCfb431c3Ac10A936Ab) | TBA | | Wrapped BTC | [`0x235F9A2Dc29E51cE7D103bcC5Dfb4F5c9c3371De`](https://explorer.immutable.com/address/0x235F9A2Dc29E51cE7D103bcC5Dfb4F5c9c3371De) | TBA | ## Audits -The Immutable token bridge has been audited by [Trail of Bits](https://www.trailofbits.com/). The audit report can be found [here](./audits/Trail-of-Bits-2023-12-14.pdf). \ No newline at end of file +The Immutable token bridge has been audited by [Trail of Bits](https://www.trailofbits.com/). The audit report can be found [here](./audits/Trail-of-Bits-2023-12-14.pdf). From b7f329a2224a87d50403a0e2a8c49a08ef08400c Mon Sep 17 00:00:00 2001 From: Craig M Date: Tue, 23 Jan 2024 18:33:45 +1300 Subject: [PATCH 069/155] WIP --- test/fork/root/RootERC20BridgeFlowRate.t.sol | 25 ++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 test/fork/root/RootERC20BridgeFlowRate.t.sol diff --git a/test/fork/root/RootERC20BridgeFlowRate.t.sol b/test/fork/root/RootERC20BridgeFlowRate.t.sol new file mode 100644 index 00000000..7f25ce42 --- /dev/null +++ b/test/fork/root/RootERC20BridgeFlowRate.t.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import {Test, console2} from "forge-std/Test.sol"; +import {RootERC20BridgeFlowRate} from "../../../src/root/flowrate/RootERC20BridgeFlowRate.sol"; +import {Utils} from "../../utils.t.sol"; + +contract RootERC20BridgeFlowRateForkTest is Test, Utils { + uint256 mainnetFork; + address payable rootBridgeAddress = payable(0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6); + RootERC20BridgeFlowRate public rootBridgeFlowRate; + string MAINNET_RPC_URL = vm.envString("FORK_MAINNET_RPC_URL"); + + function setUp() public { + mainnetFork = vm.createFork(MAINNET_RPC_URL); + rootBridgeFlowRate = RootERC20BridgeFlowRate(rootBridgeAddress); + } + + function test_getWithdrawalDelay() public { + uint256 withdrawDelay = rootBridgeFlowRate.withdrawalDelay(); + console2.log("withdrawDelay"); + console2.logUint(withdrawDelay); + assertEq(withdrawDelay, uint256(86400)); + } +} From f00f34bdf4e72108b52949a8b3d3bd93a75afef3 Mon Sep 17 00:00:00 2001 From: Craig M Date: Wed, 24 Jan 2024 10:34:46 +1300 Subject: [PATCH 070/155] basic fork test working --- test/fork/root/RootERC20BridgeFlowRate.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/fork/root/RootERC20BridgeFlowRate.t.sol b/test/fork/root/RootERC20BridgeFlowRate.t.sol index 7f25ce42..e8831b13 100644 --- a/test/fork/root/RootERC20BridgeFlowRate.t.sol +++ b/test/fork/root/RootERC20BridgeFlowRate.t.sol @@ -13,12 +13,12 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils { function setUp() public { mainnetFork = vm.createFork(MAINNET_RPC_URL); + vm.selectFork(mainnetFork); rootBridgeFlowRate = RootERC20BridgeFlowRate(rootBridgeAddress); } function test_getWithdrawalDelay() public { uint256 withdrawDelay = rootBridgeFlowRate.withdrawalDelay(); - console2.log("withdrawDelay"); console2.logUint(withdrawDelay); assertEq(withdrawDelay, uint256(86400)); } From fb5df13bb72ed3adfc3a26a8b65834c91b2507ab Mon Sep 17 00:00:00 2001 From: Craig M Date: Wed, 24 Jan 2024 17:59:05 +1300 Subject: [PATCH 071/155] first tests passes --- test/fork/root/RootERC20BridgeFlowRate.t.sol | 134 ++++++++++++++++++- 1 file changed, 129 insertions(+), 5 deletions(-) diff --git a/test/fork/root/RootERC20BridgeFlowRate.t.sol b/test/fork/root/RootERC20BridgeFlowRate.t.sol index e8831b13..5baec1e1 100644 --- a/test/fork/root/RootERC20BridgeFlowRate.t.sol +++ b/test/fork/root/RootERC20BridgeFlowRate.t.sol @@ -3,23 +3,147 @@ pragma solidity 0.8.19; import {Test, console2} from "forge-std/Test.sol"; import {RootERC20BridgeFlowRate} from "../../../src/root/flowrate/RootERC20BridgeFlowRate.sol"; +import {IFlowRateWithdrawalQueueErrors} from "../../../src/root/flowrate/FlowRateWithdrawalQueue.sol"; + import {Utils} from "../../utils.t.sol"; contract RootERC20BridgeFlowRateForkTest is Test, Utils { uint256 mainnetFork; - address payable rootBridgeAddress = payable(0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6); RootERC20BridgeFlowRate public rootBridgeFlowRate; string MAINNET_RPC_URL = vm.envString("FORK_MAINNET_RPC_URL"); + address NATIVE_ETH = address(0x0000000000000000000000000000000000000Eee); + uint256 withdrawDelay; + + // move to .env + address payable rootBridgeAddress = payable(0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6); + address rootAdapter = address(0x4f49B53928A71E553bB1B0F66a5BcB54Fd4E8932); + address receiver1 = address(0x111); + address receiver2 = address(0x222); + + + struct Bucket { + uint256 capacity; + uint256 depth; + uint256 refillTime; + uint256 refillRate; + } function setUp() public { mainnetFork = vm.createFork(MAINNET_RPC_URL); vm.selectFork(mainnetFork); rootBridgeFlowRate = RootERC20BridgeFlowRate(rootBridgeAddress); + withdrawDelay = rootBridgeFlowRate.withdrawalDelay(); + assertGt(withdrawDelay, 0); } - function test_getWithdrawalDelay() public { - uint256 withdrawDelay = rootBridgeFlowRate.withdrawalDelay(); - console2.logUint(withdrawDelay); - assertEq(withdrawDelay, uint256(86400)); + function test_flowRateETH() public { + uint256 largeThreshold = rootBridgeFlowRate.largeTransferThresholds(NATIVE_ETH); + (uint256 capacity, uint256 depth, uint256 refillTime, uint256 refillRate) = rootBridgeFlowRate.flowRateBuckets(NATIVE_ETH); + + // send 75% of the largeThreshold value + uint256 txValue = ((largeThreshold / 100) * 75); + + uint256 numTxs = (depth / txValue) + 2; + + uint256 numTxsReceiver1 = 0; + + //deal enough ETH to the bridge to cover all the txs + vm.deal(rootBridgeAddress, txValue * numTxs); + + while(depth > 0) { + //prank as axelar sending a message to the adapter + vm.startPrank(rootAdapter); + + bytes memory predictedPayload1 = + abi.encode(rootBridgeFlowRate.WITHDRAW_SIG(), NATIVE_ETH, receiver1, receiver1, txValue); + rootBridgeFlowRate.onMessageReceive(predictedPayload1); + vm.stopPrank(); + + (capacity, depth, refillTime, refillRate) = rootBridgeFlowRate.flowRateBuckets(NATIVE_ETH); + + bool queueActivated = rootBridgeFlowRate.withdrawalQueueActivated(); + + if (depth > 0) { + assertFalse(queueActivated); + } else { + assertTrue(queueActivated); + } + + numTxsReceiver1 += 1; + } + + //sanity check we dealt the enough eth + assertEq(numTxsReceiver1+1, numTxs); + + //send one more tx to receiver2 and make sure it gets queued + vm.startPrank(rootAdapter); + bytes memory predictedPayload2 = + abi.encode(rootBridgeFlowRate.WITHDRAW_SIG(), NATIVE_ETH, receiver2, receiver2, txValue); + rootBridgeFlowRate.onMessageReceive(predictedPayload2); + vm.stopPrank(); + + uint256 pendingLength1 = rootBridgeFlowRate.getPendingWithdrawalsLength(receiver1); + uint256 pendingLength2 = rootBridgeFlowRate.getPendingWithdrawalsLength(receiver2); + + //each receiver should have 1 queued tx + assertEq(pendingLength1, 1); + assertEq(pendingLength2, 1); + + uint256[] memory indices1 = new uint256[](1); + indices1[0] = 0; + + RootERC20BridgeFlowRate.PendingWithdrawal[] memory pending1 = + rootBridgeFlowRate.getPendingWithdrawals(receiver1, indices1); + + assertEq(pending1.length, 1); + assertEq(pending1[0].withdrawer, receiver1); + assertEq(pending1[0].token, NATIVE_ETH); + assertEq(pending1[0].amount, txValue); + uint256 timestamp1 = pending1[0].timestamp; + + uint256 okTime1 = timestamp1 + withdrawDelay; + + //deal some eth to pay withdraw gas + vm.deal(address(this), 1 ether); + + //try to process withdraw 1 + vm.expectRevert( + abi.encodeWithSelector(IFlowRateWithdrawalQueueErrors.WithdrawalRequestTooEarly.selector, timestamp1, okTime1) + ); + rootBridgeFlowRate.finaliseQueuedWithdrawal(receiver1, 0); + + uint256[] memory indices2 = new uint256[](1); + indices2[0] = 0; + + RootERC20BridgeFlowRate.PendingWithdrawal[] memory pending2 = + rootBridgeFlowRate.getPendingWithdrawals(receiver2, indices2); + + assertEq(pending2.length, 1); + assertEq(pending2[0].withdrawer, receiver2); + assertEq(pending2[0].token, NATIVE_ETH); + assertEq(pending2[0].amount, txValue); + uint256 timestamp2 = pending2[0].timestamp; + + uint256 okTime2 = timestamp2 + withdrawDelay; + + //try to process withdraw 2 + vm.expectRevert( + abi.encodeWithSelector(IFlowRateWithdrawalQueueErrors.WithdrawalRequestTooEarly.selector, timestamp2, okTime2) + ); + rootBridgeFlowRate.finaliseQueuedWithdrawal(receiver2, 0); + + vm.warp(okTime1+1); + + rootBridgeFlowRate.finaliseQueuedWithdrawal(receiver1, 0); + + vm.warp(okTime2+1); + + rootBridgeFlowRate.finaliseQueuedWithdrawal(receiver2, 0); + + console2.log('success'); + + //warp to time when withdraw can be processed + + //try to withdraw again } } From b8a2603d6e20a852ebc6d9a434e880b2354d51be Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Mon, 29 Jan 2024 12:50:15 +1000 Subject: [PATCH 072/155] Add more e2e test cases --- package.json | 1 + scripts/e2e/e2e.ts | 1678 +++++++++++++++++++++++++-- scripts/localdev/.env.local | 4 +- scripts/localdev/rootchain_setup.ts | 10 +- yarn.lock | 2 +- 5 files changed, 1598 insertions(+), 97 deletions(-) diff --git a/package.json b/package.json index fd4617d7..aa213c2b 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@ledgerhq/hw-app-eth": "^6.35.0", "@ledgerhq/hw-transport-node-hid": "^6.28.0", "@openzeppelin/contracts": "^4.5.0", + "@types/chai-as-promised": "^7.1.8", "axios": "^0.27.2", "bip39": "^3.0.4", "config": "^3.3.9", diff --git a/scripts/e2e/e2e.ts b/scripts/e2e/e2e.ts index 7cf347da..7e0175bb 100644 --- a/scripts/e2e/e2e.ts +++ b/scripts/e2e/e2e.ts @@ -3,8 +3,9 @@ import * as dotenv from "dotenv"; dotenv.config(); import { ethers, providers } from "ethers"; import { requireEnv, waitForReceipt, getFee, getContract, delay, getChildContracts, getRootContracts, saveChildContracts, waitUntilSucceed } from "../helpers/helpers"; -import { expect } from "chai"; -import { RetryProvider } from "../helpers/retry"; +import * as chai from "chai"; +chai.use(require('chai-as-promised')); +const { expect } = chai; // The contract ABI of IMX on L1. const IMX_ABI = `[{"inputs":[{"internalType":"address","name":"minter","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"previousAdminRole","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"newAdminRole","type":"bytes32"}],"name":"RoleAdminChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleGranted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleRevoked","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[],"name":"DEFAULT_ADMIN_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MINTER_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"cap","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"subtractedValue","type":"uint256"}],"name":"decreaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"}],"name":"getRoleAdmin","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"grantRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"hasRole","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"addedValue","type":"uint256"}],"name":"increaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"mint","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"renounceRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"revokeRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}]`; @@ -14,6 +15,10 @@ describe("Bridge e2e test", () => { let childProvider: providers.JsonRpcProvider; let rootTestWallet: ethers.Wallet; let childTestWallet: ethers.Wallet; + let rootPauserWallet: ethers.Wallet; + let childPauserWallet: ethers.Wallet; + let rootPrivilegedWallet: ethers.Wallet; + let childPrivilegedWallet: ethers.Wallet; let rootBridge: ethers.Contract; let rootWETH: ethers.Contract; let rootIMX: ethers.Contract; @@ -30,6 +35,8 @@ describe("Bridge e2e test", () => { let rootChainID = requireEnv("ROOT_CHAIN_ID"); let childRPCURL = requireEnv("CHILD_RPC_URL"); let childChainID = requireEnv("CHILD_CHAIN_ID"); + let testBreakGlassKey = requireEnv("BREAKGLASS_EOA_SECRET"); + let testPriviledgeKey = requireEnv("PRIVILEGED_EOA_SECRET"); let testAccountKey = requireEnv("TEST_ACCOUNT_SECRET"); let rootIMXAddr = requireEnv("ROOT_IMX_ADDR"); let rootWETHAddr = requireEnv("ROOT_WETH_ADDR"); @@ -43,10 +50,14 @@ describe("Bridge e2e test", () => { let rootBridgeAddr = rootContracts.ROOT_BRIDGE_ADDRESS; let rootCustomTokenAddr = rootContracts.ROOT_TEST_CUSTOM_TOKEN; - rootProvider = new RetryProvider(rootRPCURL, Number(rootChainID)); - childProvider = new RetryProvider(childRPCURL, Number(childChainID)); + rootProvider = new providers.JsonRpcProvider(rootRPCURL, Number(rootChainID)); + childProvider = new providers.JsonRpcProvider(childRPCURL, Number(childChainID)); rootTestWallet = new ethers.Wallet(testAccountKey, rootProvider); childTestWallet = new ethers.Wallet(testAccountKey, childProvider); + rootPauserWallet = new ethers.Wallet(testBreakGlassKey, rootProvider); + childPauserWallet = new ethers.Wallet(testBreakGlassKey, childProvider); + rootPrivilegedWallet = new ethers.Wallet(testPriviledgeKey, rootProvider); + childPrivilegedWallet = new ethers.Wallet(testPriviledgeKey, childProvider); rootBridge = getContract("RootERC20BridgeFlowRate", rootBridgeAddr, rootProvider); rootWETH = getContract("WETH", rootWETHAddr, rootProvider); @@ -57,12 +68,101 @@ describe("Bridge e2e test", () => { childWIMX = getContract("WIMX", childWIMXAddr, childProvider); }) + it("should not deposit IMX if allowance is insufficient", async() => { + let amt = ethers.utils.parseEther("50.0"); + let bridgeFee = ethers.utils.parseEther("0.001"); + + // Approve + let resp = await rootIMX.connect(rootTestWallet).approve(rootBridge.address, amt.sub(1)); + await waitForReceipt(resp.hash, rootProvider); + + // Fail to deposit on L1 + await expect(rootBridge.connect(rootTestWallet).deposit(rootIMX.address, amt, { + value: bridgeFee, + })).to.be.rejectedWith("UNPREDICTABLE_GAS_LIMIT"); + }).timeout(2400000) + + it("should not deposit IMX if balance is insufficient", async() => { + let balance = await rootIMX.balanceOf(rootTestWallet.address); + + let amt = balance.add(1); + let bridgeFee = ethers.utils.parseEther("0.001"); + + // Approve + let resp = await rootIMX.connect(rootTestWallet).approve(rootBridge.address, amt); + await waitForReceipt(resp.hash, rootProvider); + + // Fail to deposit on L1 + await expect(rootBridge.connect(rootTestWallet).deposit(rootIMX.address, amt, { + value: bridgeFee, + })).to.be.rejectedWith("UNPREDICTABLE_GAS_LIMIT"); + }).timeout(2400000) + + // Local only + it("should not deposit IMX if root bridge is paused", async() => { + // Transfer 0.1 ETH to root pauser + let resp = await rootTestWallet.sendTransaction({ + to: rootPauserWallet.address, + value: ethers.utils.parseEther("0.1"), + }) + await waitForReceipt(resp.hash, rootProvider); + + // Transfer 0.1 ETH to root unpauser + resp = await rootTestWallet.sendTransaction({ + to: rootPrivilegedWallet.address, + value: ethers.utils.parseEther("0.1"), + }) + await waitForReceipt(resp.hash, rootProvider); + + // Pause root bridge + if (!await rootBridge.paused()) { + resp = await rootBridge.connect(rootPauserWallet).pause(); + await waitForReceipt(resp.hash, rootProvider); + expect(await rootBridge.paused()).to.true; + } + + // Try to deposit. + let amt = ethers.utils.parseEther("10.0"); + let bridgeFee = ethers.utils.parseEther("0.001"); + + // Approve + resp = await rootIMX.connect(rootTestWallet).approve(rootBridge.address, amt); + await waitForReceipt(resp.hash, rootProvider); + + // Fail to deposit on L1 + await expect(rootBridge.connect(rootTestWallet).deposit(rootIMX.address, amt, { + value: bridgeFee, + })).to.be.rejectedWith("Pausable: paused"); + + // Unpause root bridge + resp = await rootBridge.connect(rootPrivilegedWallet).unpause(); + await waitForReceipt(resp.hash, rootProvider); + expect(await rootBridge.paused()).to.false; + }).timeout(2400000) + + // Local only + it("should not deposit IMX if deposit limit is reached", async() => { + let limit = ethers.utils.parseEther("100000000.0"); + + let amt = limit.add(1); + let bridgeFee = ethers.utils.parseEther("0.001"); + + // Approve + let resp = await rootIMX.connect(rootTestWallet).approve(rootBridge.address, amt); + await waitForReceipt(resp.hash, rootProvider); + + // Fail to deposit on L1 + await expect(rootBridge.connect(rootTestWallet).deposit(rootIMX.address, amt, { + value: bridgeFee, + })).to.be.rejectedWith(rootBridge.interface.getSighash('ImxDepositLimitExceeded()')); + }).timeout(2400000) + it("should successfully deposit IMX to self from L1 to L2", async() => { // Get IMX balance on root & child chains before deposit let preBalL1 = await rootIMX.balanceOf(rootTestWallet.address); let preBalL2 = await childProvider.getBalance(childTestWallet.address); - let amt = ethers.utils.parseEther("10.0"); + let amt = ethers.utils.parseEther("50.0"); let bridgeFee = ethers.utils.parseEther("0.001"); // Approve @@ -92,6 +192,155 @@ describe("Bridge e2e test", () => { expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); }).timeout(2400000) + it("should successfully deposit IMX to others from L1 to L2", async() => { + let childRecipient = childPrivilegedWallet.address; + // Get IMX balance on root & child chains before deposit + let preBalL1 = await rootIMX.balanceOf(rootTestWallet.address); + let preBalL2 = await childProvider.getBalance(childRecipient); + + let amt = ethers.utils.parseEther("50.0"); + let bridgeFee = ethers.utils.parseEther("0.001"); + + // Approve + let resp = await rootIMX.connect(rootTestWallet).approve(rootBridge.address, amt); + await waitForReceipt(resp.hash, rootProvider); + + // IMX deposit L1 to L2 + resp = await rootBridge.connect(rootTestWallet).depositTo(rootIMX.address, childRecipient, amt, { + value: bridgeFee, + }); + await waitForReceipt(resp.hash, rootProvider); + + let postBalL1 = await rootIMX.balanceOf(rootTestWallet.address); + let postBalL2 = preBalL2; + + await waitUntilSucceed(axelarAPI, resp.hash); + + while (postBalL2.eq(preBalL2)) { + postBalL2 = await childProvider.getBalance(childRecipient); + await delay(1000); + } + + // Verify + let expectedPostL1 = preBalL1.sub(amt); + let expectedPostL2 = preBalL2.add(amt); + expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); + expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); + }).timeout(2400000) + + // Local only + it("should not deposit IMX on L2 if child bridge is paused", async() => { + // Transfer 0.1 IMX to child pauser + let resp = await childTestWallet.sendTransaction({ + to: childPauserWallet.address, + value: ethers.utils.parseEther("0.1"), + }) + await waitForReceipt(resp.hash, childProvider); + + // Transfer 0.1 IMX to child unpauser + resp = await childTestWallet.sendTransaction({ + to: childPrivilegedWallet.address, + value: ethers.utils.parseEther("0.1"), + }) + await waitForReceipt(resp.hash, childProvider); + + // Pause child bridge + if (!await childBridge.paused()) { + resp = await childBridge.connect(childPauserWallet).pause(); + await waitForReceipt(resp.hash, childProvider); + expect(await childBridge.paused()).to.true; + } + + // Get IMX balance on root & child chains before deposit + let preBalL1 = await rootIMX.balanceOf(rootTestWallet.address); + let preBalL2 = await childProvider.getBalance(childTestWallet.address); + + let amt = ethers.utils.parseEther("10.0"); + let bridgeFee = ethers.utils.parseEther("0.001"); + + // Approve + resp = await rootIMX.connect(rootTestWallet).approve(rootBridge.address, amt); + await waitForReceipt(resp.hash, rootProvider); + + // Try to deposit + resp = await rootBridge.connect(rootTestWallet).deposit(rootIMX.address, amt, { + value: bridgeFee, + }); + await waitForReceipt(resp.hash, rootProvider); + await waitUntilSucceed(axelarAPI, resp.hash); + + // Balance on L2 should not change. + await delay(10000); + let postBalL1 = await rootIMX.balanceOf(rootTestWallet.address); + let postBalL2 = await childProvider.getBalance(childTestWallet.address); + + // Verify + let expectedPostL1 = preBalL1.sub(amt); + let expectedPostL2 = preBalL2; + expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); + expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); + + // Unpause child bridge + resp = await childBridge.connect(childPrivilegedWallet).unpause(); + await waitForReceipt(resp.hash, childProvider); + expect(await childBridge.paused()).to.false; + }).timeout(2400000) + + // Local only + it("should not withdraw IMX if child bridge is paused", async() => { + // Transfer 0.1 IMX to child pauser + let resp = await childTestWallet.sendTransaction({ + to: childPauserWallet.address, + value: ethers.utils.parseEther("0.1"), + }) + await waitForReceipt(resp.hash, childProvider); + + // Transfer 0.1 IMX to child unpauser + resp = await childTestWallet.sendTransaction({ + to: childPrivilegedWallet.address, + value: ethers.utils.parseEther("0.1"), + }) + await waitForReceipt(resp.hash, childProvider); + + // Pause child bridge + if (!await childBridge.paused()) { + resp = await childBridge.connect(childPauserWallet).pause(); + await waitForReceipt(resp.hash, childProvider); + expect(await childBridge.paused()).to.true; + } + + let amt = ethers.utils.parseEther("1.0"); + let bridgeFee = ethers.utils.parseEther("1.0"); + + // IMX withdraw L2 to L1 + let [priorityFee, maxFee] = await getFee(childProvider); + await expect(childBridge.connect(childTestWallet).withdrawIMX(amt, { + value: amt.add(bridgeFee), + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + })).to.be.rejectedWith("Pausable: paused"); + + // Unpause child bridge + resp = await childBridge.connect(childPrivilegedWallet).unpause(); + await waitForReceipt(resp.hash, childProvider); + expect(await childBridge.paused()).to.false; + }).timeout(2400000) + + it("should not withdraw IMX if balance is insufficient", async() => { + let balance = await childProvider.getBalance(childTestWallet.address); + + let amt = balance; + let bridgeFee = ethers.utils.parseEther("1.0"); + + // IMX withdraw L2 to L1 + let [priorityFee, maxFee] = await getFee(childProvider); + await expect(childBridge.connect(childTestWallet).withdrawIMX(amt, { + value: amt.add(bridgeFee), + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + })).to.be.rejectedWith("sender doesn't have enough funds to send tx"); + }).timeout(2400000) + it("should successfully withdraw IMX to self from L2 to L1", async() => { // Get IMX balance on root & child chains before withdraw let preBalL1 = await rootIMX.balanceOf(rootTestWallet.address); @@ -128,163 +377,1104 @@ describe("Bridge e2e test", () => { expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); }).timeout(2400000) - it("should successfully withdraw wIMX to self from L2 to L1", async() => { - // Wrap 1 IMX - let [priorityFee, maxFee] = await getFee(childProvider); - let resp = await childWIMX.connect(childTestWallet).deposit({ - value: ethers.utils.parseEther("1.0"), - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }); - await waitForReceipt(resp.hash, childProvider); - + it("should successfully withdraw IMX to others from L2 to L1", async() => { + let rootRecipient = rootPrivilegedWallet.address; // Get IMX balance on root & child chains before withdraw - let preBalL1 = await rootIMX.balanceOf(rootTestWallet.address); - let preBalL2 = await childWIMX.balanceOf(childTestWallet.address); + let preBalL1 = await rootIMX.balanceOf(rootRecipient); + let preBalL2 = await childProvider.getBalance(childTestWallet.address); - let amt = ethers.utils.parseEther("0.5"); + let amt = ethers.utils.parseEther("1.0"); let bridgeFee = ethers.utils.parseEther("1.0"); - // Approve - [priorityFee, maxFee] = await getFee(childProvider); - resp = await childWIMX.connect(childTestWallet).approve(childBridge.address, amt, { - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }); - await waitForReceipt(resp.hash, childProvider); - - // wIMX withdraw L2 to L1 - [priorityFee, maxFee] = await getFee(childProvider); - resp = await childBridge.connect(childTestWallet).withdrawWIMX(amt, { - value: bridgeFee, + // IMX withdraw L2 to L1 + let [priorityFee, maxFee] = await getFee(childProvider); + let resp = await childBridge.connect(childTestWallet).withdrawIMXTo(rootRecipient, amt, { + value: amt.add(bridgeFee), maxPriorityFeePerGas: priorityFee, maxFeePerGas: maxFee, }); await waitForReceipt(resp.hash, childProvider); let postBalL1 = preBalL1; - let postBalL2 = await childWIMX.balanceOf(childTestWallet.address); + let postBalL2 = await childProvider.getBalance(childTestWallet.address); await waitUntilSucceed(axelarAPI, resp.hash); while (postBalL1.eq(preBalL1)) { - postBalL1 = await rootIMX.balanceOf(rootTestWallet.address); + postBalL1 = await rootIMX.balanceOf(rootRecipient); await delay(1000); } // Verify + let receipt = await childProvider.getTransactionReceipt(resp.hash); + let txFee = receipt.gasUsed.mul(receipt.effectiveGasPrice); let expectedPostL1 = preBalL1.add(amt); - let expectedPostL2 = preBalL2.sub(amt); + let expectedPostL2 = preBalL2.sub(txFee).sub(amt).sub(bridgeFee); expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); }).timeout(2400000) - it("should successfully deposit ETH to self from L1 to L2", async() => { - // Get ETH balance on root & child chains before deposit - let preBalL1 = await rootProvider.getBalance(rootTestWallet.address); - let preBalL2 = await childETH.balanceOf(childTestWallet.address); - - let amt = ethers.utils.parseEther("0.001"); - let bridgeFee = ethers.utils.parseEther("0.001"); + // Local only + it("should not withdraw IMX on L1 if root bridge is paused", async() => { + // Transfer 0.1 ETH to root pauser + let resp = await rootTestWallet.sendTransaction({ + to: rootPauserWallet.address, + value: ethers.utils.parseEther("0.1"), + }) + await waitForReceipt(resp.hash, rootProvider); - // ETH deposit L1 to L2 - let resp = await rootBridge.connect(rootTestWallet).depositETH(amt, { - value: amt.add(bridgeFee), - }); + // Transfer 0.1 ETH to root unpauser + resp = await rootTestWallet.sendTransaction({ + to: rootPrivilegedWallet.address, + value: ethers.utils.parseEther("0.1"), + }) await waitForReceipt(resp.hash, rootProvider); - let postBalL1 = await rootProvider.getBalance(rootTestWallet.address); - let postBalL2 = preBalL2; + // Pause root bridge + if (!await rootBridge.paused()) { + resp = await rootBridge.connect(rootPauserWallet).pause(); + await waitForReceipt(resp.hash, rootProvider); + expect(await rootBridge.paused()).to.true; + } + + // Get IMX balance on root & child chains before withdraw + let preBalL1 = await rootIMX.balanceOf(rootTestWallet.address); + let preBalL2 = await childProvider.getBalance(childTestWallet.address); + let amt = ethers.utils.parseEther("1.0"); + let bridgeFee = ethers.utils.parseEther("1.0"); + + // IMX withdraw L2 to L1 + let [priorityFee, maxFee] = await getFee(childProvider); + resp = await childBridge.connect(childTestWallet).withdrawIMX(amt, { + value: amt.add(bridgeFee), + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); await waitUntilSucceed(axelarAPI, resp.hash); - while (postBalL2.eq(preBalL2)) { - postBalL2 = await childETH.balanceOf(childTestWallet.address); - await delay(1000); - } + // Balance on L1 should not change. + await delay(10000); + let postBalL1 = await rootIMX.balanceOf(rootTestWallet.address); + let postBalL2 = await childProvider.getBalance(childTestWallet.address); // Verify - let receipt = await rootProvider.getTransactionReceipt(resp.hash); + let receipt = await childProvider.getTransactionReceipt(resp.hash); let txFee = receipt.gasUsed.mul(receipt.effectiveGasPrice); - let expectedPostL1 = preBalL1.sub(txFee).sub(amt).sub(bridgeFee); - let expectedPostL2 = preBalL2.add(amt); + let expectedPostL1 = preBalL1; + let expectedPostL2 = preBalL2.sub(txFee).sub(amt).sub(bridgeFee); expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); + + // Unpause root bridge + resp = await rootBridge.connect(rootPrivilegedWallet).unpause(); + await waitForReceipt(resp.hash, rootProvider); + expect(await rootBridge.paused()).to.false; }).timeout(2400000) - it("should successfully deposit wETH to self from L1 to L2", async() => { - // Wrap 0.01 ETH - let resp = await rootWETH.connect(rootTestWallet).deposit({ - value: ethers.utils.parseEther("0.01"), - }) + // Local only + it("should put IMX withdrawal in pending when violating rate limit policy", async() => { + // Set new rate limit + let resp = await rootBridge.connect(rootPrivilegedWallet).setRateControlThreshold(rootIMX.address, ethers.utils.parseEther("2.016"), ethers.utils.parseEther("0.00056"), ethers.utils.parseEther("1.008")); await waitForReceipt(resp.hash, rootProvider); - // Get ETH balance on root & child chains before withdraw - let preBalL1 = await rootWETH.balanceOf(rootTestWallet.address); - let preBalL2 = await childETH.balanceOf(childTestWallet.address); + // Withdraw of IMX exceeding large threshold + // Get IMX balance on root & child chains before withdraw + let preBalL1 = await rootIMX.balanceOf(rootTestWallet.address); + let preBalL2 = await childProvider.getBalance(childTestWallet.address); + let preLength = await rootBridge.getPendingWithdrawalsLength(rootTestWallet.address); - let amt = ethers.utils.parseEther("0.001"); - let bridgeFee = ethers.utils.parseEther("0.001"); + let amt1 = ethers.utils.parseEther("1.1"); + let bridgeFee1 = ethers.utils.parseEther("1.0"); - // Approve - resp = await rootWETH.connect(rootTestWallet).approve(rootBridge.address, amt); - await waitForReceipt(resp.hash, rootProvider); + // IMX withdraw L2 to L1 + let [priorityFee, maxFee] = await getFee(childProvider); + resp = await childBridge.connect(childTestWallet).withdrawIMX(amt1, { + value: amt1.add(bridgeFee1), + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); + await waitUntilSucceed(axelarAPI, resp.hash); - // wETH deposit L1 to L2 - resp = await rootBridge.connect(rootTestWallet).deposit(rootWETH.address, amt, { - value: bridgeFee, - }) - await waitForReceipt(resp.hash, rootProvider); + let receipt = await childProvider.getTransactionReceipt(resp.hash); + let txFee1 = receipt.gasUsed.mul(receipt.effectiveGasPrice); - let postBalL1 = await rootWETH.balanceOf(rootTestWallet.address); - let postBalL2 = preBalL2; + while ((await rootBridge.getPendingWithdrawalsLength(rootTestWallet.address)).eq(preLength)) { + await delay(1000); + } + // Withdraw of IMX exceeding rate limit + let amt2 = ethers.utils.parseEther("1.0"); + let bridgeFee2 = ethers.utils.parseEther("1.0"); + + // IMX withdraw L2 to L1 + [priorityFee, maxFee] = await getFee(childProvider); + resp = await childBridge.connect(childTestWallet).withdrawIMX(amt2, { + value: amt2.add(bridgeFee2), + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); + await waitUntilSucceed(axelarAPI, resp.hash); + receipt = await childProvider.getTransactionReceipt(resp.hash); + let txFee2 = receipt.gasUsed.mul(receipt.effectiveGasPrice); + + while ((await rootBridge.getPendingWithdrawalsLength(rootTestWallet.address)).eq(preLength.add(1))) { + await delay(1000); + } + + // Try to withdraw + await expect(rootBridge.connect(rootTestWallet).finaliseQueuedWithdrawal(rootTestWallet.address, preLength.add(1))).to.be.rejectedWith( + "UNPREDICTABLE_GAS_LIMIT" + ); + + // Fast-forward to 24 hours later. + await rootProvider.send( + "hardhat_mine", [ + "0x15181", // 24 hours + ]); + + // Withdraw again + resp = await rootBridge.connect(rootTestWallet).finaliseQueuedWithdrawal(rootTestWallet.address, preLength.add(1)) + await waitForReceipt(resp.hash, rootProvider); + + resp = await rootBridge.connect(rootTestWallet).finaliseQueuedWithdrawal(rootTestWallet.address, preLength) + await waitForReceipt(resp.hash, rootProvider); + + let postBalL1 = await rootIMX.balanceOf(rootTestWallet.address); + let postBalL2 = await childProvider.getBalance(childTestWallet.address); + + // Verify + let expectedPostL1 = preBalL1.add(amt1).add(amt2); + let expectedPostL2 = preBalL2.sub(amt1).sub(amt2).sub(txFee1).sub(txFee2).sub(bridgeFee1).sub(bridgeFee2); + expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); + expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); + + // Recover rate limit + resp = await rootBridge.connect(rootPrivilegedWallet).setRateControlThreshold(rootIMX.address, ethers.utils.parseEther("15516"), ethers.utils.parseEther("4.31"), ethers.utils.parseEther("7758")); + await waitForReceipt(resp.hash, rootProvider); + + // Deactive withdraw queue + resp = await rootBridge.connect(rootPrivilegedWallet).deactivateWithdrawalQueue(); + await waitForReceipt(resp.hash, rootProvider); + }).timeout(2400000) + + // Local only + it("should not withdraw WIMX if child bridge is paused", async() => { + // Transfer 0.1 IMX to child pauser + let resp = await childTestWallet.sendTransaction({ + to: childPauserWallet.address, + value: ethers.utils.parseEther("0.1"), + }) + await waitForReceipt(resp.hash, childProvider); + + // Transfer 0.1 IMX to child unpauser + resp = await childTestWallet.sendTransaction({ + to: childPrivilegedWallet.address, + value: ethers.utils.parseEther("0.1"), + }) + await waitForReceipt(resp.hash, childProvider); + + // Pause child bridge + if (!await childBridge.paused()) { + resp = await childBridge.connect(childPauserWallet).pause(); + await waitForReceipt(resp.hash, childProvider); + expect(await childBridge.paused()).to.true; + } + + // Wrap 1 IMX + let [priorityFee, maxFee] = await getFee(childProvider); + resp = await childWIMX.connect(childTestWallet).deposit({ + value: ethers.utils.parseEther("1.0"), + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); + + let amt = ethers.utils.parseEther("0.5"); + let bridgeFee = ethers.utils.parseEther("1.0"); + + // Approve + [priorityFee, maxFee] = await getFee(childProvider); + resp = await childWIMX.connect(childTestWallet).approve(childBridge.address, amt, { + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); + + await expect(childBridge.connect(childTestWallet).withdrawWIMX(amt, { + value: amt.add(bridgeFee), + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + })).to.be.rejectedWith("Pausable: paused"); + + // Unpause child bridge + resp = await childBridge.connect(childPrivilegedWallet).unpause(); + await waitForReceipt(resp.hash, childProvider); + expect(await childBridge.paused()).to.false; + }).timeout(2400000) + + it("should not withdraw wIMX if allowance is insufficient", async() => { + // Wrap 1 IMX + let [priorityFee, maxFee] = await getFee(childProvider); + let resp = await childWIMX.connect(childTestWallet).deposit({ + value: ethers.utils.parseEther("1.0"), + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); + + let amt = ethers.utils.parseEther("0.5"); + let bridgeFee = ethers.utils.parseEther("1.0"); + + // Approve + [priorityFee, maxFee] = await getFee(childProvider); + resp = await childWIMX.connect(childTestWallet).approve(childBridge.address, amt.sub(1), { + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); + + // wIMX withdraw L2 to L1 + [priorityFee, maxFee] = await getFee(childProvider); + await expect(childBridge.connect(childTestWallet).withdrawWIMX(amt, { + value: bridgeFee, + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + })).to.be.rejectedWith("UNPREDICTABLE_GAS_LIMIT"); + }).timeout(2400000) + + it("should not withdraw wIMX if balance is insufficient", async() => { + let balance = await childWIMX.balanceOf(childTestWallet.address); + + let amt = balance; + let bridgeFee = ethers.utils.parseEther("1.0"); + + // wIMX withdraw L2 to L1 + let [priorityFee, maxFee] = await getFee(childProvider); + await expect(childBridge.connect(childTestWallet).withdrawWIMX(amt.add(1), { + value: bridgeFee, + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + })).to.be.rejectedWith("UNPREDICTABLE_GAS_LIMIT"); + }).timeout(2400000) + + it("should successfully withdraw wIMX to self from L2 to L1", async() => { + // Wrap 1 IMX + let [priorityFee, maxFee] = await getFee(childProvider); + let resp = await childWIMX.connect(childTestWallet).deposit({ + value: ethers.utils.parseEther("1.0"), + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); + + // Get IMX balance on root & child chains before withdraw + let preBalL1 = await rootIMX.balanceOf(rootTestWallet.address); + let preBalL2 = await childWIMX.balanceOf(childTestWallet.address); + + let amt = ethers.utils.parseEther("0.5"); + let bridgeFee = ethers.utils.parseEther("1.0"); + + // Approve + [priorityFee, maxFee] = await getFee(childProvider); + resp = await childWIMX.connect(childTestWallet).approve(childBridge.address, amt, { + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); + + // wIMX withdraw L2 to L1 + [priorityFee, maxFee] = await getFee(childProvider); + resp = await childBridge.connect(childTestWallet).withdrawWIMX(amt, { + value: bridgeFee, + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); + + let postBalL1 = preBalL1; + let postBalL2 = await childWIMX.balanceOf(childTestWallet.address); + + await waitUntilSucceed(axelarAPI, resp.hash); + + while (postBalL1.eq(preBalL1)) { + postBalL1 = await rootIMX.balanceOf(rootTestWallet.address); + await delay(1000); + } + + // Verify + let expectedPostL1 = preBalL1.add(amt); + let expectedPostL2 = preBalL2.sub(amt); + expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); + expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); + }).timeout(2400000) + + it("should successfully withdraw wIMX to others from L2 to L1", async() => { + let rootRecipient = rootPrivilegedWallet.address; + // Wrap 1 IMX + let [priorityFee, maxFee] = await getFee(childProvider); + let resp = await childWIMX.connect(childTestWallet).deposit({ + value: ethers.utils.parseEther("1.0"), + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); + + // Get IMX balance on root & child chains before withdraw + let preBalL1 = await rootIMX.balanceOf(rootRecipient); + let preBalL2 = await childWIMX.balanceOf(childTestWallet.address); + + let amt = ethers.utils.parseEther("0.5"); + let bridgeFee = ethers.utils.parseEther("1.0"); + + // Approve + [priorityFee, maxFee] = await getFee(childProvider); + resp = await childWIMX.connect(childTestWallet).approve(childBridge.address, amt, { + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); + + // wIMX withdraw L2 to L1 + [priorityFee, maxFee] = await getFee(childProvider); + resp = await childBridge.connect(childTestWallet).withdrawWIMXTo(rootRecipient, amt, { + value: bridgeFee, + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); + + let postBalL1 = preBalL1; + let postBalL2 = await childWIMX.balanceOf(childTestWallet.address); + + await waitUntilSucceed(axelarAPI, resp.hash); + + while (postBalL1.eq(preBalL1)) { + postBalL1 = await rootIMX.balanceOf(rootRecipient); + await delay(1000); + } + + // Verify + let expectedPostL1 = preBalL1.add(amt); + let expectedPostL2 = preBalL2.sub(amt); + expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); + expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); + }).timeout(2400000) + + // Local only + it("should not withdraw wIMX on L1 if root bridge is paused", async() => { + // Transfer 0.1 ETH to root pauser + let resp = await rootTestWallet.sendTransaction({ + to: rootPauserWallet.address, + value: ethers.utils.parseEther("0.1"), + }) + await waitForReceipt(resp.hash, rootProvider); + + // Transfer 0.1 ETH to root unpauser + resp = await rootTestWallet.sendTransaction({ + to: rootPrivilegedWallet.address, + value: ethers.utils.parseEther("0.1"), + }) + await waitForReceipt(resp.hash, rootProvider); + + // Pause root bridge + if (!await rootBridge.paused()) { + resp = await rootBridge.connect(rootPauserWallet).pause(); + await waitForReceipt(resp.hash, rootProvider); + expect(await rootBridge.paused()).to.true; + } + + // Wrap 1 IMX + let [priorityFee, maxFee] = await getFee(childProvider); + resp = await childWIMX.connect(childTestWallet).deposit({ + value: ethers.utils.parseEther("1.0"), + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); + + // Get IMX balance on root & child chains before withdraw + let preBalL1 = await rootIMX.balanceOf(rootTestWallet.address); + let preBalL2 = await childWIMX.balanceOf(childTestWallet.address); + + let amt = ethers.utils.parseEther("0.5"); + let bridgeFee = ethers.utils.parseEther("1.0"); + + // Approve + [priorityFee, maxFee] = await getFee(childProvider); + resp = await childWIMX.connect(childTestWallet).approve(childBridge.address, amt, { + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); + + // wIMX withdraw L2 to L1 + [priorityFee, maxFee] = await getFee(childProvider); + resp = await childBridge.connect(childTestWallet).withdrawWIMX(amt, { + value: bridgeFee, + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); + await waitUntilSucceed(axelarAPI, resp.hash); + + // Balance on L1 should not change. + await delay(10000); + let postBalL1 = await rootIMX.balanceOf(rootTestWallet.address); + let postBalL2 = await childWIMX.balanceOf(childTestWallet.address); + + // Verify + let expectedPostL1 = preBalL1; + let expectedPostL2 = preBalL2.sub(amt); + expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); + expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); + + // Unpause root bridge + resp = await rootBridge.connect(rootPrivilegedWallet).unpause(); + await waitForReceipt(resp.hash, rootProvider); + expect(await rootBridge.paused()).to.false; + }).timeout(2400000) + + // Local only + it("should put wIMX withdrawal in pending when violating rate limit policy", async() => { + // Set new rate limit + let resp = await rootBridge.connect(rootPrivilegedWallet).setRateControlThreshold(rootIMX.address, ethers.utils.parseEther("2.016"), ethers.utils.parseEther("0.00056"), ethers.utils.parseEther("1.008")); + await waitForReceipt(resp.hash, rootProvider); + + // Wrap 3 IMX + let [priorityFee, maxFee] = await getFee(childProvider); + resp = await childWIMX.connect(childTestWallet).deposit({ + value: ethers.utils.parseEther("3.0"), + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); + + // Withdraw of IMX exceeding large threshold + // Get IMX balance on root & child chains before withdraw + let preBalL1 = await rootIMX.balanceOf(rootTestWallet.address); + let preBalL2 = await childWIMX.balanceOf(childTestWallet.address); + let preLength = await rootBridge.getPendingWithdrawalsLength(rootTestWallet.address); + + let amt1 = ethers.utils.parseEther("1.1"); + let bridgeFee1 = ethers.utils.parseEther("1.0"); + + // Approve + [priorityFee, maxFee] = await getFee(childProvider); + resp = await childWIMX.connect(childTestWallet).approve(childBridge.address, amt1, { + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); + + // wIMX withdraw L2 to L1 + [priorityFee, maxFee] = await getFee(childProvider); + resp = await childBridge.connect(childTestWallet).withdrawWIMX(amt1, { + value: bridgeFee1, + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); + await waitUntilSucceed(axelarAPI, resp.hash); + + while ((await rootBridge.getPendingWithdrawalsLength(rootTestWallet.address)).eq(preLength)) { + await delay(1000); + } + + // Withdraw of IMX exceeding rate limit + let amt2 = ethers.utils.parseEther("1.0"); + let bridgeFee2 = ethers.utils.parseEther("1.0"); + + // Approve + [priorityFee, maxFee] = await getFee(childProvider); + resp = await childWIMX.connect(childTestWallet).approve(childBridge.address, amt2, { + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); + + // IMX withdraw L2 to L1 + [priorityFee, maxFee] = await getFee(childProvider); + resp = await childBridge.connect(childTestWallet).withdrawWIMX(amt2, { + value: bridgeFee2, + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); + await waitUntilSucceed(axelarAPI, resp.hash); + + while ((await rootBridge.getPendingWithdrawalsLength(rootTestWallet.address)).eq(preLength.add(1))) { + await delay(1000); + } + + // Try to withdraw + await expect(rootBridge.connect(rootTestWallet).finaliseQueuedWithdrawal(rootTestWallet.address, preLength.add(1))).to.be.rejectedWith( + "UNPREDICTABLE_GAS_LIMIT" + ); + + // Fast-forward to 24 hours later. + await rootProvider.send( + "hardhat_mine", [ + "0x15181", // 24 hours + ]); + + // Withdraw again + resp = await rootBridge.connect(rootTestWallet).finaliseQueuedWithdrawal(rootTestWallet.address, preLength.add(1)) + await waitForReceipt(resp.hash, rootProvider); + + resp = await rootBridge.connect(rootTestWallet).finaliseQueuedWithdrawal(rootTestWallet.address, preLength) + await waitForReceipt(resp.hash, rootProvider); + + let postBalL1 = await rootIMX.balanceOf(rootTestWallet.address); + let postBalL2 = await childWIMX.balanceOf(childTestWallet.address); + + // Verify + let expectedPostL1 = preBalL1.add(amt1).add(amt2); + let expectedPostL2 = preBalL2.sub(amt1).sub(amt2); + expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); + expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); + + // Recover rate limit + resp = await rootBridge.connect(rootPrivilegedWallet).setRateControlThreshold(rootIMX.address, ethers.utils.parseEther("15516"), ethers.utils.parseEther("4.31"), ethers.utils.parseEther("7758")); + await waitForReceipt(resp.hash, rootProvider); + + // Deactive withdraw queue + resp = await rootBridge.connect(rootPrivilegedWallet).deactivateWithdrawalQueue(); + await waitForReceipt(resp.hash, rootProvider); + }).timeout(2400000) + + it("should not deposit ETH if balance is insufficient", async() => { + let balance = await rootProvider.getBalance(rootTestWallet.address); + + let amt = balance; + let bridgeFee = ethers.utils.parseEther("0.001"); + + await expect(rootBridge.connect(rootTestWallet).depositETH(amt, { + value: amt.add(bridgeFee), + })).to.be.rejectedWith("sender doesn't have enough funds to send tx"); + }).timeout(2400000) + + // Local only + it("should not deposit ETH if root bridge is paused", async() => { + // Transfer 0.1 ETH to root pauser + let resp = await rootTestWallet.sendTransaction({ + to: rootPauserWallet.address, + value: ethers.utils.parseEther("0.1"), + }) + await waitForReceipt(resp.hash, rootProvider); + + // Transfer 0.1 ETH to root unpauser + resp = await rootTestWallet.sendTransaction({ + to: rootPrivilegedWallet.address, + value: ethers.utils.parseEther("0.1"), + }) + await waitForReceipt(resp.hash, rootProvider); + + // Pause root bridge + if (!await rootBridge.paused()) { + resp = await rootBridge.connect(rootPauserWallet).pause(); + await waitForReceipt(resp.hash, rootProvider); + expect(await rootBridge.paused()).to.true; + } + + let amt = ethers.utils.parseEther("0.001"); + let bridgeFee = ethers.utils.parseEther("0.001"); + + // Fail to deposit on L1 + await expect(rootBridge.connect(rootTestWallet).depositETH(amt, { + value: amt.add(bridgeFee), + })).to.be.rejectedWith("Pausable: paused"); + + // Unpause root bridge + resp = await rootBridge.connect(rootPrivilegedWallet).unpause(); + await waitForReceipt(resp.hash, rootProvider); + expect(await rootBridge.paused()).to.false; + }).timeout(2400000) + + it("should successfully deposit ETH to self from L1 to L2", async() => { + // Get ETH balance on root & child chains before deposit + let preBalL1 = await rootProvider.getBalance(rootTestWallet.address); + let preBalL2 = await childETH.balanceOf(childTestWallet.address); + + let amt = ethers.utils.parseEther("1.0"); + let bridgeFee = ethers.utils.parseEther("0.001"); + + // ETH deposit L1 to L2 + let resp = await rootBridge.connect(rootTestWallet).depositETH(amt, { + value: amt.add(bridgeFee), + }); + await waitForReceipt(resp.hash, rootProvider); + + let postBalL1 = await rootProvider.getBalance(rootTestWallet.address); + let postBalL2 = preBalL2; + + await waitUntilSucceed(axelarAPI, resp.hash); + + while (postBalL2.eq(preBalL2)) { + postBalL2 = await childETH.balanceOf(childTestWallet.address); + await delay(1000); + } + + // Verify + let receipt = await rootProvider.getTransactionReceipt(resp.hash); + let txFee = receipt.gasUsed.mul(receipt.effectiveGasPrice); + let expectedPostL1 = preBalL1.sub(txFee).sub(amt).sub(bridgeFee); + let expectedPostL2 = preBalL2.add(amt); + expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); + expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); + }).timeout(2400000) + + it("should successfully deposit ETH to others from L1 to L2", async() => { + let childRecipient = childPrivilegedWallet.address; + // Get ETH balance on root & child chains before deposit + let preBalL1 = await rootProvider.getBalance(rootTestWallet.address); + let preBalL2 = await childETH.balanceOf(childRecipient); + + let amt = ethers.utils.parseEther("0.001"); + let bridgeFee = ethers.utils.parseEther("0.001"); + + // ETH deposit L1 to L2 + let resp = await rootBridge.connect(rootTestWallet).depositToETH(childRecipient, amt, { + value: amt.add(bridgeFee), + }); + await waitForReceipt(resp.hash, rootProvider); + + let postBalL1 = await rootProvider.getBalance(rootTestWallet.address); + let postBalL2 = preBalL2; + + await waitUntilSucceed(axelarAPI, resp.hash); + + while (postBalL2.eq(preBalL2)) { + postBalL2 = await childETH.balanceOf(childRecipient); + await delay(1000); + } + + // Verify + let receipt = await rootProvider.getTransactionReceipt(resp.hash); + let txFee = receipt.gasUsed.mul(receipt.effectiveGasPrice); + let expectedPostL1 = preBalL1.sub(txFee).sub(amt).sub(bridgeFee); + let expectedPostL2 = preBalL2.add(amt); + expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); + expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); + }).timeout(2400000) + + // Local only + it("should not deposit ETH on L2 if child bridge is paused", async() => { + // Transfer 0.1 IMX to child pauser + let resp = await childTestWallet.sendTransaction({ + to: childPauserWallet.address, + value: ethers.utils.parseEther("0.1"), + }) + await waitForReceipt(resp.hash, childProvider); + + // Transfer 0.1 IMX to child unpauser + resp = await childTestWallet.sendTransaction({ + to: childPrivilegedWallet.address, + value: ethers.utils.parseEther("0.1"), + }) + await waitForReceipt(resp.hash, childProvider); + + // Pause child bridge + if (!await childBridge.paused()) { + resp = await childBridge.connect(childPauserWallet).pause(); + await waitForReceipt(resp.hash, childProvider); + expect(await childBridge.paused()).to.true; + } + + // Get ETH balance on root & child chains before deposit + let preBalL1 = await rootProvider.getBalance(rootTestWallet.address); + let preBalL2 = await childETH.balanceOf(childTestWallet.address); + + let amt = ethers.utils.parseEther("0.001"); + let bridgeFee = ethers.utils.parseEther("0.001"); + + // Try to deposit + resp = await rootBridge.connect(rootTestWallet).depositETH(amt, { + value: amt.add(bridgeFee), + }); + await waitForReceipt(resp.hash, rootProvider); + await waitUntilSucceed(axelarAPI, resp.hash); + + // Balance on L2 should not change. + await delay(10000); + let postBalL1 = await rootProvider.getBalance(rootTestWallet.address); + let postBalL2 = await childETH.balanceOf(childTestWallet.address); + + // Verify + let receipt = await rootProvider.getTransactionReceipt(resp.hash); + let txFee = receipt.gasUsed.mul(receipt.effectiveGasPrice); + let expectedPostL1 = preBalL1.sub(txFee).sub(amt).sub(bridgeFee); + let expectedPostL2 = preBalL2; + expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); + expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); + + // Unpause child bridge + resp = await childBridge.connect(childPrivilegedWallet).unpause(); + await waitForReceipt(resp.hash, childProvider); + expect(await childBridge.paused()).to.false; + }).timeout(2400000) + + it("should successfully deposit wETH to self from L1 to L2", async() => { + // Wrap 0.01 ETH + let resp = await rootWETH.connect(rootTestWallet).deposit({ + value: ethers.utils.parseEther("0.01"), + }) + await waitForReceipt(resp.hash, rootProvider); + + // Get ETH balance on root & child chains before withdraw + let preBalL1 = await rootWETH.balanceOf(rootTestWallet.address); + let preBalL2 = await childETH.balanceOf(childTestWallet.address); + + let amt = ethers.utils.parseEther("0.001"); + let bridgeFee = ethers.utils.parseEther("0.001"); + + // Approve + resp = await rootWETH.connect(rootTestWallet).approve(rootBridge.address, amt); + await waitForReceipt(resp.hash, rootProvider); + + // wETH deposit L1 to L2 + resp = await rootBridge.connect(rootTestWallet).deposit(rootWETH.address, amt, { + value: bridgeFee, + }) + await waitForReceipt(resp.hash, rootProvider); + + let postBalL1 = await rootWETH.balanceOf(rootTestWallet.address); + let postBalL2 = preBalL2; + + await waitUntilSucceed(axelarAPI, resp.hash); + + while (postBalL2.eq(preBalL2)) { + postBalL2 = await childETH.balanceOf(childTestWallet.address); + await delay(1000); + } + + // Verify + let expectedPostL1 = preBalL1.sub(amt); + let expectedPostL2 = preBalL2.add(amt); + expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); + expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); + }).timeout(2400000) + + it("should successfully deposit wETH to others from L1 to L2", async() => { + let childRecipient = childPrivilegedWallet.address; + // Wrap 0.01 ETH + let resp = await rootWETH.connect(rootTestWallet).deposit({ + value: ethers.utils.parseEther("0.01"), + }) + await waitForReceipt(resp.hash, rootProvider); + + // Get ETH balance on root & child chains before withdraw + let preBalL1 = await rootWETH.balanceOf(rootTestWallet.address); + let preBalL2 = await childETH.balanceOf(childRecipient); + + let amt = ethers.utils.parseEther("0.001"); + let bridgeFee = ethers.utils.parseEther("0.001"); + + // Approve + resp = await rootWETH.connect(rootTestWallet).approve(rootBridge.address, amt); + await waitForReceipt(resp.hash, rootProvider); + + // wETH deposit L1 to L2 + resp = await rootBridge.connect(rootTestWallet).depositTo(rootWETH.address, childRecipient, amt, { + value: bridgeFee, + }) + await waitForReceipt(resp.hash, rootProvider); + + let postBalL1 = await rootWETH.balanceOf(rootTestWallet.address); + let postBalL2 = preBalL2; + + await waitUntilSucceed(axelarAPI, resp.hash); + + while (postBalL2.eq(preBalL2)) { + postBalL2 = await childETH.balanceOf(childRecipient); + await delay(1000); + } + + // Verify + let expectedPostL1 = preBalL1.sub(amt); + let expectedPostL2 = preBalL2.add(amt); + expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); + expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); + }).timeout(2400000) + + // Local only + it("should not withdraw ETH if child bridge is paused", async() => { + // Transfer 0.1 IMX to child pauser + let resp = await childTestWallet.sendTransaction({ + to: childPauserWallet.address, + value: ethers.utils.parseEther("0.1"), + }) + await waitForReceipt(resp.hash, childProvider); + + // Transfer 0.1 IMX to child unpauser + resp = await childTestWallet.sendTransaction({ + to: childPrivilegedWallet.address, + value: ethers.utils.parseEther("0.1"), + }) + await waitForReceipt(resp.hash, childProvider); + + // Pause child bridge + if (!await childBridge.paused()) { + resp = await childBridge.connect(childPauserWallet).pause(); + await waitForReceipt(resp.hash, childProvider); + expect(await childBridge.paused()).to.true; + } + + let amt = ethers.utils.parseEther("0.0005"); + let bridgeFee = ethers.utils.parseEther("1.0"); + + // ETH withdraw L2 to L1 + let [priorityFee, maxFee] = await getFee(childProvider); + await expect(childBridge.connect(childTestWallet).withdrawETH(amt, { + value: bridgeFee, + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + })).to.be.rejectedWith("Pausable: paused"); + + // Unpause child bridge + resp = await childBridge.connect(childPrivilegedWallet).unpause(); + await waitForReceipt(resp.hash, childProvider); + expect(await childBridge.paused()).to.false; + }).timeout(2400000) + + it("should not withdraw ETH if balance is insufficient", async() => { + let amt = await childETH.balanceOf(childTestWallet.address); + let bridgeFee = ethers.utils.parseEther("1.0"); + + // ETH withdraw L2 to L1 + let [priorityFee, maxFee] = await getFee(childProvider); + await expect(childBridge.connect(childTestWallet).withdrawETH(amt.add(1), { + value: bridgeFee, + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + })).to.be.rejectedWith("UNPREDICTABLE_GAS_LIMIT"); + }).timeout(2400000) + + it("should successfully withdraw ETH to self from L2 to L1", async() => { + // Get ETH balance on root & child chains before withdraw + let preBalL1 = await rootProvider.getBalance(rootTestWallet.address); + let preBalL2 = await childETH.balanceOf(childTestWallet.address); + + let amt = ethers.utils.parseEther("0.0005"); + let bridgeFee = ethers.utils.parseEther("1.0"); + + // ETH withdraw L2 to L1 + let [priorityFee, maxFee] = await getFee(childProvider); + let resp = await childBridge.connect(childTestWallet).withdrawETH(amt, { + value: bridgeFee, + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); + + let postBalL1 = preBalL1; + let postBalL2 = await childETH.balanceOf(childTestWallet.address); + + await waitUntilSucceed(axelarAPI, resp.hash); + + while (postBalL1.eq(preBalL1)) { + postBalL1 = await rootProvider.getBalance(rootTestWallet.address); + await delay(1000); + } + + // Verify + let expectedPostL1 = preBalL1.add(amt); + let expectedPostL2 = preBalL2.sub(amt); + expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); + expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); + }).timeout(2400000) + + it("should successfully withdraw ETH to others from L2 to L1", async() => { + let rootRecipient = rootPrivilegedWallet.address; + // Get ETH balance on root & child chains before withdraw + let preBalL1 = await rootProvider.getBalance(rootRecipient); + let preBalL2 = await childETH.balanceOf(childTestWallet.address); + + let amt = ethers.utils.parseEther("0.0005"); + let bridgeFee = ethers.utils.parseEther("1.0"); + + // ETH withdraw L2 to L1 + let [priorityFee, maxFee] = await getFee(childProvider); + let resp = await childBridge.connect(childTestWallet).withdrawETHTo(rootRecipient, amt, { + value: bridgeFee, + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); + + let postBalL1 = preBalL1; + let postBalL2 = await childETH.balanceOf(childTestWallet.address); + + await waitUntilSucceed(axelarAPI, resp.hash); + + while (postBalL1.eq(preBalL1)) { + postBalL1 = await rootProvider.getBalance(rootRecipient); + await delay(1000); + } + + // Verify + let expectedPostL1 = preBalL1.add(amt); + let expectedPostL2 = preBalL2.sub(amt); + expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); + expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); + }).timeout(2400000) + + // Local only + it("should not withdraw ETH on L1 if root bridge is paused", async() => { + // Transfer 0.1 ETH to root pauser + let resp = await rootTestWallet.sendTransaction({ + to: rootPauserWallet.address, + value: ethers.utils.parseEther("0.1"), + }) + await waitForReceipt(resp.hash, rootProvider); + + // Transfer 0.1 ETH to root unpauser + resp = await rootTestWallet.sendTransaction({ + to: rootPrivilegedWallet.address, + value: ethers.utils.parseEther("0.1"), + }) + await waitForReceipt(resp.hash, rootProvider); + + // Pause root bridge + if (!await rootBridge.paused()) { + resp = await rootBridge.connect(rootPauserWallet).pause(); + await waitForReceipt(resp.hash, rootProvider); + expect(await rootBridge.paused()).to.true; + } + + // Get ETH balance on root & child chains before withdraw + let preBalL1 = await rootProvider.getBalance(rootTestWallet.address); + let preBalL2 = await childETH.balanceOf(childTestWallet.address); + + let amt = ethers.utils.parseEther("0.0005"); + let bridgeFee = ethers.utils.parseEther("1.0"); + + // ETH withdraw L2 to L1 + let [priorityFee, maxFee] = await getFee(childProvider); + resp = await childBridge.connect(childTestWallet).withdrawETH(amt, { + value: bridgeFee, + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); await waitUntilSucceed(axelarAPI, resp.hash); - while (postBalL2.eq(preBalL2)) { - postBalL2 = await childETH.balanceOf(childTestWallet.address); - await delay(1000); - } + // Balance on L1 should not change. + await delay(10000); + let postBalL1 = await rootProvider.getBalance(rootTestWallet.address); + let postBalL2 = await childETH.balanceOf(childTestWallet.address); // Verify - let expectedPostL1 = preBalL1.sub(amt); - let expectedPostL2 = preBalL2.add(amt); + let expectedPostL1 = preBalL1; + let expectedPostL2 = preBalL2.sub(amt); expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); + + // Unpause root bridge + resp = await rootBridge.connect(rootPrivilegedWallet).unpause(); + await waitForReceipt(resp.hash, rootProvider); + expect(await rootBridge.paused()).to.false; }).timeout(2400000) - it("should successfully withdraw ETH to self from L2 to L1", async() => { + // Local only + it("should put ETH withdrawal in pending when violating rate limit policy", async() => { + // Set new rate limit + let resp = await rootBridge.connect(rootPrivilegedWallet).setRateControlThreshold(await rootBridge.NATIVE_ETH(), ethers.utils.parseEther("0.0010008"), ethers.utils.parseEther("0.000000278"), ethers.utils.parseEther("0.0005004")); + await waitForReceipt(resp.hash, rootProvider); + + // Withdraw of ETH exceeding large threshold // Get ETH balance on root & child chains before withdraw let preBalL1 = await rootProvider.getBalance(rootTestWallet.address); let preBalL2 = await childETH.balanceOf(childTestWallet.address); + let preLength = await rootBridge.getPendingWithdrawalsLength(rootTestWallet.address); - let amt = ethers.utils.parseEther("0.0005"); - let bridgeFee = ethers.utils.parseEther("1.0"); + let amt1 = ethers.utils.parseEther("0.0006"); + let bridgeFee1 = ethers.utils.parseEther("1.0"); // ETH withdraw L2 to L1 let [priorityFee, maxFee] = await getFee(childProvider); - let resp = await childBridge.connect(childTestWallet).withdrawETH(amt, { - value: bridgeFee, + resp = await childBridge.connect(childTestWallet).withdrawETH(amt1, { + value: bridgeFee1, maxPriorityFeePerGas: priorityFee, maxFeePerGas: maxFee, }); await waitForReceipt(resp.hash, childProvider); + await waitUntilSucceed(axelarAPI, resp.hash); - let postBalL1 = preBalL1; - let postBalL2 = await childETH.balanceOf(childTestWallet.address); + while ((await rootBridge.getPendingWithdrawalsLength(rootTestWallet.address)).eq(preLength)) { + await delay(1000); + } + + // Withdraw of ETH exceeding rate limit + let amt2 = ethers.utils.parseEther("0.0005"); + let bridgeFee2 = ethers.utils.parseEther("1.0"); + // ETH withdraw L2 to L1 + [priorityFee, maxFee] = await getFee(childProvider); + resp = await childBridge.connect(childTestWallet).withdrawETH(amt2, { + value: bridgeFee2, + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); await waitUntilSucceed(axelarAPI, resp.hash); - while (postBalL1.eq(preBalL1)) { - postBalL1 = await rootProvider.getBalance(rootTestWallet.address); + while ((await rootBridge.getPendingWithdrawalsLength(rootTestWallet.address)).eq(preLength.add(1))) { await delay(1000); } + // Try to withdraw + await expect(rootBridge.connect(rootTestWallet).finaliseQueuedWithdrawal(rootTestWallet.address, preLength.add(1))).to.be.rejectedWith( + "UNPREDICTABLE_GAS_LIMIT" + ); + + // Fast-forward to 24 hours later. + await rootProvider.send( + "hardhat_mine", [ + "0x15181", // 24 hours + ]); + + // Withdraw again + resp = await rootBridge.connect(rootTestWallet).finaliseQueuedWithdrawal(rootTestWallet.address, preLength.add(1)) + await waitForReceipt(resp.hash, rootProvider); + let receipt = await rootProvider.getTransactionReceipt(resp.hash); + let txFee1 = receipt.gasUsed.mul(receipt.effectiveGasPrice); + + resp = await rootBridge.connect(rootTestWallet).finaliseQueuedWithdrawal(rootTestWallet.address, preLength) + await waitForReceipt(resp.hash, rootProvider); + receipt = await rootProvider.getTransactionReceipt(resp.hash); + let txFee2 = receipt.gasUsed.mul(receipt.effectiveGasPrice); + + let postBalL1 = await rootProvider.getBalance(rootTestWallet.address); + let postBalL2 = await childETH.balanceOf(childTestWallet.address); + // Verify - let expectedPostL1 = preBalL1.add(amt); - let expectedPostL2 = preBalL2.sub(amt); + let expectedPostL1 = preBalL1.sub(txFee1).sub(txFee2).add(amt1).add(amt2); + let expectedPostL2 = preBalL2.sub(amt1).sub(amt2); expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); + + // Recover rate limit + resp = await rootBridge.connect(rootPrivilegedWallet).setRateControlThreshold(await rootBridge.NATIVE_ETH(), ethers.utils.parseEther("10.08"), ethers.utils.parseEther("0.0028"), ethers.utils.parseEther("5.04")); + await waitForReceipt(resp.hash, rootProvider); + + // Deactive withdraw queue + resp = await rootBridge.connect(rootPrivilegedWallet).deactivateWithdrawalQueue(); + await waitForReceipt(resp.hash, rootProvider); + }).timeout(2400000) + + it("should not deposit unmapped token", async() => { + let unMappedToken = ethers.utils.getAddress("0xccC8cb5229B0ac8069C51fd58367Fd1e622aFD97"); + let amt = ethers.utils.parseEther("1.0"); + let bridgeFee = ethers.utils.parseEther("0.001"); + + // Token deposit L1 to L2 + await expect(rootBridge.connect(rootTestWallet).deposit(unMappedToken, amt, { + value: bridgeFee, + })).to.be.rejectedWith("UNPREDICTABLE_GAS_LIMIT"); }).timeout(2400000) it("should successfully map a ERC20 Token", async() => { @@ -321,12 +1511,98 @@ describe("Bridge e2e test", () => { expect(childTokenAddr).to.equal(expectedChildTokenAddr); }).timeout(2400000) + it("should not map a mapped ERC20 Token", async() => { + let childContracts = getChildContracts(); + let childCustomTokenAddr = childContracts.CHILD_TEST_CUSTOM_TOKEN; + if (childCustomTokenAddr == "") { + childCustomToken = getContract("ChildERC20", childCustomTokenAddr, childProvider); + console.log("Custom token has not been mapped yet, skip."); + return; + } + // Map token + let bridgeFee = ethers.utils.parseEther("0.001"); + await expect(rootBridge.connect(rootTestWallet).mapToken(rootCustomToken.address, { + value: bridgeFee, + })).to.be.rejectedWith("UNPREDICTABLE_GAS_LIMIT"); + }).timeout(2400000) + + it("should not deposit mapped ERC20 Token if allowance is insufficient", async() => { + let amt = ethers.utils.parseEther("1.0"); + let bridgeFee = ethers.utils.parseEther("0.001"); + + // Approve + let resp = await rootCustomToken.connect(rootTestWallet).approve(rootBridge.address, amt.sub(1)); + await waitForReceipt(resp.hash, rootProvider); + + // Fail to deposit on L1 + await expect(rootBridge.connect(rootTestWallet).deposit(rootCustomToken.address, amt, { + value: bridgeFee, + })).to.be.rejectedWith("UNPREDICTABLE_GAS_LIMIT"); + }).timeout(2400000) + + it("should not deposit mapped ERC20 Token if balance is insufficient", async() => { + let balance = await rootCustomToken.balanceOf(rootTestWallet.address); + + let amt = balance.add(1); + let bridgeFee = ethers.utils.parseEther("0.001"); + + // Approve + let resp = await rootCustomToken.connect(rootTestWallet).approve(rootBridge.address, amt); + await waitForReceipt(resp.hash, rootProvider); + + await expect(rootBridge.connect(rootTestWallet).deposit(rootCustomToken.address, amt, { + value: bridgeFee, + })).to.be.rejectedWith("UNPREDICTABLE_GAS_LIMIT"); + }).timeout(2400000) + + // Local only + it("should not deposit mapped ERC20 Token if root bridge is paused", async() => { + // Transfer 0.1 ETH to root pauser + let resp = await rootTestWallet.sendTransaction({ + to: rootPauserWallet.address, + value: ethers.utils.parseEther("0.1"), + }) + await waitForReceipt(resp.hash, rootProvider); + + // Transfer 0.1 ETH to root unpauser + resp = await rootTestWallet.sendTransaction({ + to: rootPrivilegedWallet.address, + value: ethers.utils.parseEther("0.1"), + }) + await waitForReceipt(resp.hash, rootProvider); + + // Pause root bridge + if (!await rootBridge.paused()) { + resp = await rootBridge.connect(rootPauserWallet).pause(); + await waitForReceipt(resp.hash, rootProvider); + expect(await rootBridge.paused()).to.true; + } + + // Try to deposit. + let amt = ethers.utils.parseEther("1.0"); + let bridgeFee = ethers.utils.parseEther("0.001"); + + // Approve + resp = await rootCustomToken.connect(rootTestWallet).approve(rootBridge.address, amt); + await waitForReceipt(resp.hash, rootProvider); + + // Fail to deposit on L1 + await expect(rootBridge.connect(rootTestWallet).deposit(rootCustomToken.address, amt, { + value: bridgeFee, + })).to.be.rejectedWith("Pausable: paused"); + + // Unpause root bridge + resp = await rootBridge.connect(rootPrivilegedWallet).unpause(); + await waitForReceipt(resp.hash, rootProvider); + expect(await rootBridge.paused()).to.false; + }).timeout(2400000) + it("should successfully deposit mapped ERC20 Token to self from L1 to L2", async() => { // Get token balance on root & child chains before deposit let preBalL1 = await rootCustomToken.balanceOf(rootTestWallet.address); let preBalL2 = await childCustomToken.balanceOf(childTestWallet.address); - let amt = ethers.utils.parseEther("1.0"); + let amt = ethers.utils.parseEther("10.0"); let bridgeFee = ethers.utils.parseEther("0.001"); // Approve @@ -356,6 +1632,109 @@ describe("Bridge e2e test", () => { expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); }).timeout(2400000) + it("should successfully deposit mapped ERC20 Token to others from L1 to L2", async() => { + let childRecipient = childPrivilegedWallet.address; + // Get token balance on root & child chains before deposit + let preBalL1 = await rootCustomToken.balanceOf(rootTestWallet.address); + let preBalL2 = await childCustomToken.balanceOf(childRecipient); + + let amt = ethers.utils.parseEther("1.0"); + let bridgeFee = ethers.utils.parseEther("0.001"); + + // Approve + let resp = await rootCustomToken.connect(rootTestWallet).approve(rootBridge.address, amt); + await waitForReceipt(resp.hash, rootProvider); + + // Token deposit L1 to L2 + resp = await rootBridge.connect(rootTestWallet).depositTo(rootCustomToken.address, childRecipient, amt, { + value: bridgeFee, + }) + await waitForReceipt(resp.hash, rootProvider); + + let postBalL1 = await rootCustomToken.balanceOf(rootTestWallet.address); + let postBalL2 = preBalL2; + + await waitUntilSucceed(axelarAPI, resp.hash); + + while (postBalL2.eq(preBalL2)) { + postBalL2 = await childCustomToken.balanceOf(childRecipient); + await delay(1000); + } + + // Verify + let expectedPostL1 = preBalL1.sub(amt); + let expectedPostL2 = preBalL2.add(amt); + expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); + expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); + }).timeout(2400000) + + it("should not withdraw unmapped token", async() => { + let unMappedToken = ethers.utils.getAddress("0xccC8cb5229B0ac8069C51fd58367Fd1e622aFD97"); + let amt = ethers.utils.parseEther("0.5"); + let bridgeFee = ethers.utils.parseEther("1.0"); + + // Token withdraw L2 to L1 + let [priorityFee, maxFee] = await getFee(childProvider); + await expect(childBridge.connect(childTestWallet).withdraw(unMappedToken, amt, { + value: bridgeFee, + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + })).to.be.rejectedWith("UNPREDICTABLE_GAS_LIMIT"); + }).timeout(2400000) + + // Local only + it("should not withdraw mapped ERC20 Token if child bridge is paused", async() => { + // Transfer 0.1 IMX to child pauser + let resp = await childTestWallet.sendTransaction({ + to: childPauserWallet.address, + value: ethers.utils.parseEther("0.1"), + }) + await waitForReceipt(resp.hash, childProvider); + + // Transfer 0.1 IMX to child unpauser + resp = await childTestWallet.sendTransaction({ + to: childPrivilegedWallet.address, + value: ethers.utils.parseEther("0.1"), + }) + await waitForReceipt(resp.hash, childProvider); + + // Pause child bridge + if (!await childBridge.paused()) { + resp = await childBridge.connect(childPauserWallet).pause(); + await waitForReceipt(resp.hash, childProvider); + expect(await childBridge.paused()).to.true; + } + + let amt = ethers.utils.parseEther("0.5"); + let bridgeFee = ethers.utils.parseEther("1.0"); + + // Token withdraw L2 to L1 + let [priorityFee, maxFee] = await getFee(childProvider); + await expect(childBridge.connect(childTestWallet).withdraw(childCustomToken.address, amt, { + value: bridgeFee, + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + })).to.be.rejectedWith("Pausable: paused"); + + // Unpause child bridge + resp = await childBridge.connect(childPrivilegedWallet).unpause(); + await waitForReceipt(resp.hash, childProvider); + expect(await childBridge.paused()).to.false; + }).timeout(2400000) + + it("should not withdraw mapped ERC20 Token if balance is insufficient", async() => { + let amt = await childCustomToken.balanceOf(childTestWallet.address); + let bridgeFee = ethers.utils.parseEther("1.0"); + + // ETH withdraw L2 to L1 + let [priorityFee, maxFee] = await getFee(childProvider); + await expect(childBridge.connect(childTestWallet).withdraw(childCustomToken.address, amt.add(1), { + value: bridgeFee, + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + })).to.be.rejectedWith("UNPREDICTABLE_GAS_LIMIT"); + }).timeout(2400000) + it("should successfully withdraw mapped ERC20 Token to self from L2 to L1", async() => { // Get token balance on root & child chains before deposit let preBalL1 = await rootCustomToken.balanceOf(rootTestWallet.address); @@ -389,4 +1768,121 @@ describe("Bridge e2e test", () => { expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); }).timeout(2400000) + + it("should successfully withdraw mapped ERC20 Token to others from L2 to L1", async() => { + let rootRecipient = rootPrivilegedWallet.address; + // Get token balance on root & child chains before deposit + let preBalL1 = await rootCustomToken.balanceOf(rootRecipient); + let preBalL2 = await childCustomToken.balanceOf(childTestWallet.address); + + let amt = ethers.utils.parseEther("0.5"); + let bridgeFee = ethers.utils.parseEther("1.0"); + + // Token withdraw L2 to L1 + let [priorityFee, maxFee] = await getFee(childProvider); + let resp = await childBridge.connect(childTestWallet).withdrawTo(childCustomToken.address, rootRecipient, amt, { + value: bridgeFee, + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }) + await waitForReceipt(resp.hash, childProvider); + + let postBalL1 = preBalL1; + let postBalL2 = await childCustomToken.balanceOf(childTestWallet.address); + + await waitUntilSucceed(axelarAPI, resp.hash); + + while (postBalL1.eq(preBalL1)) { + postBalL1 = await rootCustomToken.balanceOf(rootRecipient); + await delay(1000); + } + + // Verify + let expectedPostL1 = preBalL1.add(amt); + let expectedPostL2 = preBalL2.sub(amt); + expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); + expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); + }).timeout(2400000) + + it("should put mapped ERC20 Token withdrawal in pending when violating rate limit policy", async() => { + // Set new rate limit + let resp = await rootBridge.connect(rootPrivilegedWallet).setRateControlThreshold(rootCustomToken.address, ethers.utils.parseEther("1.0008"), ethers.utils.parseEther("0.000278"), ethers.utils.parseEther("0.5004")); + await waitForReceipt(resp.hash, rootProvider); + + // Withdraw of ERC20 exceeding large threshold + // Get ERC20 balance on root & child chains before withdraw + let preBalL1 = await rootCustomToken.balanceOf(rootTestWallet.address); + let preBalL2 = await childCustomToken.balanceOf(childTestWallet.address); + let preLength = await rootBridge.getPendingWithdrawalsLength(rootTestWallet.address); + + let amt1 = ethers.utils.parseEther("0.6"); + let bridgeFee1 = ethers.utils.parseEther("1.0"); + + // ERC20 withdraw L2 to L1 + let [priorityFee, maxFee] = await getFee(childProvider); + resp = await childBridge.connect(childTestWallet).withdraw(childCustomToken.address, amt1, { + value: bridgeFee1, + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); + await waitUntilSucceed(axelarAPI, resp.hash); + + while ((await rootBridge.getPendingWithdrawalsLength(rootTestWallet.address)).eq(preLength)) { + await delay(1000); + } + + // Withdraw of ERC20 exceeding rate limit + let amt2 = ethers.utils.parseEther("0.5"); + let bridgeFee2 = ethers.utils.parseEther("1.0"); + + // ERC20 withdraw L2 to L1 + [priorityFee, maxFee] = await getFee(childProvider); + resp = await childBridge.connect(childTestWallet).withdraw(childCustomToken.address, amt2, { + value: bridgeFee2, + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); + await waitUntilSucceed(axelarAPI, resp.hash); + + while ((await rootBridge.getPendingWithdrawalsLength(rootTestWallet.address)).eq(preLength.add(1))) { + await delay(1000); + } + + // Try to withdraw + await expect(rootBridge.connect(rootTestWallet).finaliseQueuedWithdrawal(rootTestWallet.address, preLength.add(1))).to.be.rejectedWith( + "UNPREDICTABLE_GAS_LIMIT" + ); + + // Fast-forward to 24 hours later. + await rootProvider.send( + "hardhat_mine", [ + "0x15181", // 24 hours + ]); + + // Withdraw again + resp = await rootBridge.connect(rootTestWallet).finaliseQueuedWithdrawal(rootTestWallet.address, preLength.add(1)) + await waitForReceipt(resp.hash, rootProvider); + + resp = await rootBridge.connect(rootTestWallet).finaliseQueuedWithdrawal(rootTestWallet.address, preLength) + await waitForReceipt(resp.hash, rootProvider); + + let postBalL1 = await rootCustomToken.balanceOf(rootTestWallet.address); + let postBalL2 = await childCustomToken.balanceOf(childTestWallet.address); + + // Verify + let expectedPostL1 = preBalL1.add(amt1).add(amt2); + let expectedPostL2 = preBalL2.sub(amt1).sub(amt2); + expect(postBalL1.toBigInt()).to.equal(expectedPostL1.toBigInt()); + expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); + + // Recover rate limit + resp = await rootBridge.connect(rootPrivilegedWallet).setRateControlThreshold(rootCustomToken.address, ethers.utils.parseEther("20016.0"), ethers.utils.parseEther("5.56"), ethers.utils.parseEther("10008.0")); + await waitForReceipt(resp.hash, rootProvider); + + // Deactive withdraw queue + resp = await rootBridge.connect(rootPrivilegedWallet).deactivateWithdrawalQueue(); + await waitForReceipt(resp.hash, rootProvider); + }).timeout(2400000) }) \ No newline at end of file diff --git a/scripts/localdev/.env.local b/scripts/localdev/.env.local index 8633ddc7..da8b99ad 100644 --- a/scripts/localdev/.env.local +++ b/scripts/localdev/.env.local @@ -110,4 +110,6 @@ TEST_ACCOUNT_SECRET=92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1 ROOT_EOA_SECRET=df57089febbacf7ba0bc227dafbffa9fc08a93fdc68e1e42411a14efcf23656e AXELAR_ROOT_EOA_SECRET=5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a AXELAR_CHILD_EOA_SECRET=5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a -AXELAR_DEPLOYER_SECRET=7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6 \ No newline at end of file +AXELAR_DEPLOYER_SECRET=7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6 +BREAKGLASS_EOA_SECRET=dbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97 +PRIVILEGED_EOA_SECRET=4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356 \ No newline at end of file diff --git a/scripts/localdev/rootchain_setup.ts b/scripts/localdev/rootchain_setup.ts index c31fe00f..523b528e 100644 --- a/scripts/localdev/rootchain_setup.ts +++ b/scripts/localdev/rootchain_setup.ts @@ -14,6 +14,8 @@ async function main() { let reservedAddr = requireEnv("NONCE_RESERVED_DEPLOYER_ADDR"); let axelarEOA = requireEnv("AXELAR_EOA"); let rootTestKey = requireEnv("TEST_ACCOUNT_SECRET"); + let rootBreakGlassAddr = requireEnv("ROOT_BREAKGLASS_ADDR"); + let rootPrivilegedAddr = requireEnv("ROOT_PRIVILEGED_MULTISIG_ADDR"); // Get root provider. let rootProvider = new RetryProvider(rootRPCURL, Number(rootChainID)); @@ -46,8 +48,8 @@ async function main() { let resp = await IMX.connect(admin).mint(deployerAddr, ethers.utils.parseEther("1110.0")); await waitForReceipt(resp.hash, rootProvider); - // Transfer 1000 IMX to test wallet - resp = await IMX.connect(admin).mint(testWallet.address, ethers.utils.parseEther("1000.0")) + // Transfer 1000000000 IMX to test wallet + resp = await IMX.connect(admin).mint(testWallet.address, ethers.utils.parseEther("1000000000.0")) await waitForReceipt(resp.hash, rootProvider); // Transfer 0.1 ETH to root deployer @@ -70,10 +72,10 @@ async function main() { value: ethers.utils.parseEther("500.0"), }) - // Transfer 10 ETH to test wallet + // Transfer 1000 ETH to test wallet resp = await admin.sendTransaction({ to: testWallet.address, - value: ethers.utils.parseEther("10.0"), + value: ethers.utils.parseEther("1000.0"), }) await waitForReceipt(resp.hash, rootProvider); diff --git a/yarn.lock b/yarn.lock index e8988aaf..dd5c0ce9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1640,7 +1640,7 @@ "@types/node" "*" "@types/responselike" "^1.0.0" -"@types/chai-as-promised@^7.1.3": +"@types/chai-as-promised@^7.1.3", "@types/chai-as-promised@^7.1.8": version "7.1.8" resolved "https://registry.yarnpkg.com/@types/chai-as-promised/-/chai-as-promised-7.1.8.tgz#f2b3d82d53c59626b5d6bbc087667ccb4b677fe9" integrity sha512-ThlRVIJhr69FLlh6IctTXFkmhtP3NpMZ2QGq69StYLyKZFp/HOp1VdKZj7RvfNWYYcJ1xlbLGLLWj1UvP5u/Gw== From 1f3db34c7ec17bc019cc675f05f462a6ae5bc0f1 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Mon, 29 Jan 2024 14:40:34 +1000 Subject: [PATCH 073/155] Fix CI --- .github/workflows/coverage.yml | 2 +- .github/workflows/e2e.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/test.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 3a4d7b13..2176a7b0 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -25,7 +25,7 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: - version: nightly-34f684ddfacc5b2ed371353ba6f730c485616ffe + version: nightly-caef1360e29dfefb1723fa501f425e6f7824bf7f - name: Run Forge build run: | diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index fde67cd7..dcc988ff 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -21,7 +21,7 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: - version: nightly-34f684ddfacc5b2ed371353ba6f730c485616ffe + version: nightly-caef1360e29dfefb1723fa501f425e6f7824bf7f - name: Run install uses: borales/actions-yarn@v4 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 73c7580d..418e54cb 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -20,7 +20,7 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: - version: nightly-34f684ddfacc5b2ed371353ba6f730c485616ffe + version: nightly-caef1360e29dfefb1723fa501f425e6f7824bf7f - name: Run Forge fmt --check run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 52f77ec5..c8879c5d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,7 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: - version: nightly-34f684ddfacc5b2ed371353ba6f730c485616ffe + version: nightly-caef1360e29dfefb1723fa501f425e6f7824bf7f - name: Run Forge build run: | From 2a730d2e851e48c3948a68ab46f47708150b0899 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Mon, 29 Jan 2024 14:49:29 +1000 Subject: [PATCH 074/155] Lint --- test/integration/root/RootERC20BridgeFlowRate.t.sol | 5 ----- test/unit/child/ChildAxelarBridgeAdaptor.t.sol | 7 ------- test/unit/child/ChildERC20Bridge.t.sol | 8 -------- .../unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol | 1 - .../child/withdrawals/ChildERC20BridgeWithdrawETH.t.sol | 1 - .../child/withdrawals/ChildERC20BridgeWithdrawETHTo.t.sol | 1 - .../child/withdrawals/ChildERC20BridgeWithdrawIMX.t.sol | 1 - .../child/withdrawals/ChildERC20BridgeWithdrawIMXTo.t.sol | 1 - .../child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol | 1 - .../child/withdrawals/ChildERC20BridgeWithdrawWIMX.t.sol | 1 - .../withdrawals/ChildERC20BridgeWithdrawWIMXTo.t.sol | 1 - test/unit/deploy/OwnableCreate2Deployer.t.sol | 3 --- test/unit/root/RootAxelarBridgeAdaptor.t.sol | 6 ------ test/unit/root/RootERC20Bridge.t.sol | 8 -------- test/unit/root/flowrate/RootERC20BridgeFlowRate.t.sol | 3 --- 15 files changed, 48 deletions(-) diff --git a/test/integration/root/RootERC20BridgeFlowRate.t.sol b/test/integration/root/RootERC20BridgeFlowRate.t.sol index cd12454f..17b00b3e 100644 --- a/test/integration/root/RootERC20BridgeFlowRate.t.sol +++ b/test/integration/root/RootERC20BridgeFlowRate.t.sol @@ -61,7 +61,6 @@ contract RootERC20BridgeFlowRateIntegrationTest is * This test uses the same code as the mapToken function does to calculate this address, so we can * not consider it sufficient. */ - function test_mapTokenTransfersValue() public { address childToken = Clones.predictDeterministicAddress(address(token), keccak256(abi.encodePacked(token)), CHILD_BRIDGE); @@ -135,7 +134,6 @@ contract RootERC20BridgeFlowRateIntegrationTest is /** * DEPOSIT ETH */ - function test_depositETHTransfersValue() public { uint256 tokenAmount = 300; setupDeposit(NATIVE_ETH, rootBridgeFlowRate, mapTokenFee, depositFee, tokenAmount, false); @@ -211,7 +209,6 @@ contract RootERC20BridgeFlowRateIntegrationTest is /** * DEPOSIT IMX */ - function test_depositIMXTokenTransfersValue() public { uint256 tokenAmount = 300; @@ -292,7 +289,6 @@ contract RootERC20BridgeFlowRateIntegrationTest is /** * DEPOSIT WETH */ - function test_depositWETHTransfersValue() public { uint256 tokenAmount = 300; setupDeposit(WRAPPED_ETH, rootBridgeFlowRate, mapTokenFee, depositFee, tokenAmount, false); @@ -446,7 +442,6 @@ contract RootERC20BridgeFlowRateIntegrationTest is /** * DEPOSIT TO */ - function test_depositToTransfersValue() public { uint256 tokenAmount = 300; address recipient = address(9876); diff --git a/test/unit/child/ChildAxelarBridgeAdaptor.t.sol b/test/unit/child/ChildAxelarBridgeAdaptor.t.sol index 87010030..73182c59 100644 --- a/test/unit/child/ChildAxelarBridgeAdaptor.t.sol +++ b/test/unit/child/ChildAxelarBridgeAdaptor.t.sol @@ -56,7 +56,6 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro /** * INITIALIZE */ - function test_Initialize() public { assertEq(address(axelarAdaptor.childBridge()), address(mockChildERC20Bridge), "childBridge not set"); assertEq(axelarAdaptor.rootChainId(), ROOT_CHAIN_NAME, "rootChain not set"); @@ -179,7 +178,6 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro /** * EXECUTE */ - function test_RevertIf_executeCalledWithInvalidSourceChain() public { bytes32 commandId = bytes32("testCommandId"); bytes memory payload = abi.encodePacked("payload"); @@ -221,7 +219,6 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro /** * SEND MESSAGE */ - function test_sendMessage_CallsGasService() public { address refundRecipient = address(123); bytes memory payload = abi.encode(WITHDRAW_SIG, address(token), address(this), address(999), 11111); @@ -342,7 +339,6 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro /** * UPDATE CHILD BRIDGE */ - function test_updateChildBridge_UpdatesChildBridge() public { vm.startPrank(bridgeManager); address newChildBridge = address(0x123); @@ -384,7 +380,6 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro /** * UPDATE ROOT CHAIN */ - function test_updateRootChain_UpdatesRootChain() public { vm.startPrank(targetManager); string memory newRootChain = "newRoot"; @@ -425,7 +420,6 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro /** * UPDATE ROOT BRIDGE ADAPTOR */ - function test_updateRootBridgeAdaptor_UpdatesRootBridgeAdaptor() public { vm.startPrank(targetManager); string memory newAdaptor = "newAdaptor"; @@ -470,7 +464,6 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro /** * UPDATE GAS SERVICE */ - function test_updateGasService_UpdatesGasService() public { vm.startPrank(gasServiceManager); address newGasService = address(0x123); diff --git a/test/unit/child/ChildERC20Bridge.t.sol b/test/unit/child/ChildERC20Bridge.t.sol index e93f334e..cc70efe2 100644 --- a/test/unit/child/ChildERC20Bridge.t.sol +++ b/test/unit/child/ChildERC20Bridge.t.sol @@ -67,7 +67,6 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B /** * RECEIVE */ - function test_NativeTransferFromWIMX() public { address caller = address(0x123a); payable(caller).transfer(2 ether); @@ -107,7 +106,6 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B /** * TREASURY DEPOSIT */ - function test_treasuryDepostIncreasesBalance() public { vm.deal(treasuryManager, 100 ether); vm.startPrank(treasuryManager); @@ -150,7 +148,6 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B /** * INITIALIZE */ - function test_Initialize() public { assertEq(address(childBridge.childBridgeAdaptor()), address(address(this)), "bridgeAdaptor not set"); assertEq(childBridge.childTokenTemplate(), address(childTokenTemplate), "childTokenTemplate not set"); @@ -257,7 +254,6 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B /** * UPDATE CHILD BRIDGE ADAPTOR */ - function test_updateChildBridgeAdaptor_UpdatesChildBridgeAdaptor() public { address newAdaptorAddress = address(0x11111); @@ -298,7 +294,6 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B /** * ON MESSAGE RECIEVE */ - function test_onMessageReceive_SetsTokenMapping() public { address predictedChildToken = Clones.predictDeterministicAddress( address(childTokenTemplate), keccak256(abi.encodePacked(rootToken)), address(childBridge) @@ -387,7 +382,6 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B /** * DEPOSIT ETH */ - function test_RevertsIf_OnMessageReceiveWhenPaused() public { pause(IPausable(address(childBridge))); bytes memory depositData = @@ -461,7 +455,6 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B /** * DEPOSIT */ - function test_onMessageReceive_DepositIMX_EmitsIMXDepositEvent() public { uint256 fundedAmount = 10 ether; vm.deal(address(childBridge), fundedAmount); @@ -616,7 +609,6 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B /** * WITHDRAW */ - function test_RevertIf_WithdrawReentered() public { // Create attack token vm.startPrank(address(childBridge)); diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol index dd3dea5e..9b564788 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol @@ -62,7 +62,6 @@ contract ChildERC20BridgeWithdrawUnitTest is Test, IChildERC20BridgeEvents, IChi /** * WITHDRAW */ - function test_RevertsIf_WithdrawWhenPaused() public { pause(IPausable(address(childBridge))); vm.expectRevert("Pausable: paused"); diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETH.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETH.t.sol index b564870f..c4fff9d4 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETH.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETH.t.sol @@ -49,7 +49,6 @@ contract ChildERC20BridgeWithdrawETHUnitTest is Test, IChildERC20BridgeEvents, I /** * WITHDRAW ETH */ - function test_RevertsIf_WithdrawETHWhenPaused() public { pause(IPausable(address(childBridge))); vm.expectRevert("Pausable: paused"); diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETHTo.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETHTo.t.sol index 21708c0f..efb39dd7 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETHTo.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETHTo.t.sol @@ -69,7 +69,6 @@ contract ChildERC20BridgeWithdrawETHToUnitTest is Test, IChildERC20BridgeEvents, /** * WITHDRAW ETH TO */ - function test_RevertsIf_WithdrawETHToWhenPaused() public { pause(IPausable(address(childBridge))); vm.expectRevert("Pausable: paused"); diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawIMX.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawIMX.t.sol index c48c08d8..31653971 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawIMX.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawIMX.t.sol @@ -44,7 +44,6 @@ contract ChildERC20BridgeWithdrawIMXUnitTest is Test, IChildERC20BridgeEvents, I /** * WITHDRAW IMX */ - function test_RevertIf_WithdrawIMXWhenPaused() public { pause(IPausable(address(childBridge))); vm.expectRevert("Pausable: paused"); diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawIMXTo.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawIMXTo.t.sol index b2fa6b72..8676cdb9 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawIMXTo.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawIMXTo.t.sol @@ -48,7 +48,6 @@ contract ChildERC20BridgeWithdrawIMXToUnitTest is Test, IChildERC20BridgeEvents, /** * WITHDRAW IMX TO */ - function test_RevertsIf_WithdrawIMXToWhenPaused() public { pause(IPausable(address(childBridge))); vm.expectRevert("Pausable: paused"); diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol index 88cc2b63..9bc0dc90 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol @@ -62,7 +62,6 @@ contract ChildERC20BridgeWithdrawToUnitTest is Test, IChildERC20BridgeEvents, IC /** * WITHDRAW TO */ - function test_RevertsIf_WithdrawToWhenPaused() public { pause(IPausable(address(childBridge))); vm.expectRevert("Pausable: paused"); diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMX.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMX.t.sol index 0d1b314c..f7a38208 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMX.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMX.t.sol @@ -48,7 +48,6 @@ contract ChildERC20BridgeWithdrawWIMXUnitTest is Test, IChildERC20BridgeEvents, /** * WITHDRAW WIMX */ - function test_RevertsIf_WithdrawWIMXWhenPaused() public { pause(IPausable(address(childBridge))); vm.expectRevert("Pausable: paused"); diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMXTo.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMXTo.t.sol index 83a6da3f..070cec06 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMXTo.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMXTo.t.sol @@ -50,7 +50,6 @@ contract ChildERC20BridgeWithdrawWIMXToUnitTest is Test, IChildERC20BridgeEvents /** * WITHDRAW WIMX TO */ - function test_RevertsIf_WithdrawWIMXToWhenPaused() public { pause(IPausable(address(childBridge))); vm.expectRevert("Pausable: paused"); diff --git a/test/unit/deploy/OwnableCreate2Deployer.t.sol b/test/unit/deploy/OwnableCreate2Deployer.t.sol index a0bf0bce..1b89ef8b 100644 --- a/test/unit/deploy/OwnableCreate2Deployer.t.sol +++ b/test/unit/deploy/OwnableCreate2Deployer.t.sol @@ -109,7 +109,6 @@ contract OwnableCreate2DeployerTest is Test { /** * deployAndInit */ - function test_RevertIf_DeployAndInitWithNonOwner() public { vm.stopPrank(); @@ -148,7 +147,6 @@ contract OwnableCreate2DeployerTest is Test { /** * deployedAddress */ - function test_deployedAddress_ReturnsPredictedAddress() public { address deployAddress = deployer.deployedAddress(childERC20Bytecode, address(owner), salt); @@ -162,7 +160,6 @@ contract OwnableCreate2DeployerTest is Test { /** * private helper functions */ - function predictCreate2Address(bytes memory _bytecode, address _deployer, address _sender, bytes32 _salt) private pure diff --git a/test/unit/root/RootAxelarBridgeAdaptor.t.sol b/test/unit/root/RootAxelarBridgeAdaptor.t.sol index a9c3451c..949afd50 100644 --- a/test/unit/root/RootAxelarBridgeAdaptor.t.sol +++ b/test/unit/root/RootAxelarBridgeAdaptor.t.sol @@ -55,7 +55,6 @@ contract RootAxelarBridgeAdaptorTest is Test, IRootAxelarBridgeAdaptorEvents, IR /** * INITIALIZE */ - function test_Initialize() public { assertEq(address(axelarAdaptor.rootBridge()), address(stubRootBridge), "rootBridge not set"); assertEq(axelarAdaptor.childChainId(), CHILD_CHAIN_NAME, "childChain not set"); @@ -281,7 +280,6 @@ contract RootAxelarBridgeAdaptorTest is Test, IRootAxelarBridgeAdaptorEvents, IR /** * MAP TOKEN */ - function test_RevertIf_mapTokenCalledByNonRootBridge() public { address payable prankster = payable(address(0x33)); uint256 value = 300; @@ -304,7 +302,6 @@ contract RootAxelarBridgeAdaptorTest is Test, IRootAxelarBridgeAdaptorEvents, IR /** * UPDATE ROOT BRIDGE */ - function test_updateRootBridge_UpdatesRootBridge() public { vm.startPrank(bridgeManager); address newRootBridge = address(0x3333); @@ -344,7 +341,6 @@ contract RootAxelarBridgeAdaptorTest is Test, IRootAxelarBridgeAdaptorEvents, IR /** * UPDATE CHILD CHAIN */ - function test_updateChildChain_UpdatesChildChain() public { vm.startPrank(targetManager); string memory newChildChain = "newChildChain"; @@ -384,7 +380,6 @@ contract RootAxelarBridgeAdaptorTest is Test, IRootAxelarBridgeAdaptorEvents, IR /** * UPDATE CHILD BRIDGE ADAPTOR */ - function test_updateChildBridgeAdaptor_UpdatesChildBridgeAdaptor() public { vm.startPrank(targetManager); @@ -428,7 +423,6 @@ contract RootAxelarBridgeAdaptorTest is Test, IRootAxelarBridgeAdaptorEvents, IR /** * UPDATE GAS SERVICE */ - function test_updateGasService_UpdatesGasService() public { vm.startPrank(gasServiceManager); address newGasService = address(0x3333); diff --git a/test/unit/root/RootERC20Bridge.t.sol b/test/unit/root/RootERC20Bridge.t.sol index 51ab4364..8b63e1e2 100644 --- a/test/unit/root/RootERC20Bridge.t.sol +++ b/test/unit/root/RootERC20Bridge.t.sol @@ -84,7 +84,6 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid /** * INITIALIZE */ - function test_InitializeBridge() public { assertEq(address(rootBridge.rootBridgeAdaptor()), address(mockAxelarAdaptor), "bridgeAdaptor not set"); assertEq(rootBridge.childERC20Bridge(), CHILD_BRIDGE, "childERC20Bridge not set"); @@ -339,7 +338,6 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid /** * MAP TOKEN */ - function test_RevertsIf_MapTokenWhenPaused() public { pause(IPausable(address(rootBridge))); vm.expectRevert("Pausable: paused"); @@ -497,7 +495,6 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid /** * DEPOSIT ETH */ - function test_RevertsIf_DepositETHWhenPaused() public { pause(IPausable(address(rootBridge))); vm.expectRevert("Pausable: paused"); @@ -544,7 +541,6 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid /** * DEPOSIT TO ETH */ - function test_RevertsIf_DepositToETHWhenPaused() public { pause(IPausable(address(rootBridge))); vm.expectRevert("Pausable: paused"); @@ -594,7 +590,6 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid /** * ZERO AMOUNT */ - function test_RevertIf_depositETHAmountIsZero() public { uint256 amount = 0; setupDeposit(NATIVE_ETH, rootBridge, mapTokenFee, depositFee, amount, false); @@ -633,7 +628,6 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid /** * DEPOSIT WETH */ - function test_depositWETHCallsSendMessage() public { uint256 amount = 100; (, bytes memory predictedPayload) = @@ -708,7 +702,6 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid /** * DEPOSIT TOKEN */ - function test_RevertsIf_DepositReentered() public { // Create attack token ReentrancyAttackDeposit attackToken = new ReentrancyAttackDeposit(address(rootBridge)); @@ -916,7 +909,6 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid /** * DEPOSIT TO */ - function test_RevertsIf_DepositToWhenPaused() public { pause(IPausable(address(rootBridge))); vm.expectRevert("Pausable: paused"); diff --git a/test/unit/root/flowrate/RootERC20BridgeFlowRate.t.sol b/test/unit/root/flowrate/RootERC20BridgeFlowRate.t.sol index 1f435afb..d7f4d664 100644 --- a/test/unit/root/flowrate/RootERC20BridgeFlowRate.t.sol +++ b/test/unit/root/flowrate/RootERC20BridgeFlowRate.t.sol @@ -203,7 +203,6 @@ contract RootERC20BridgeFlowRateUnitTest is /** * INITIALIZE */ - function test_InitializeBridgeFlowRate() public { assertEq(address(rootBridgeFlowRate.rootBridgeAdaptor()), address(mockAxelarAdaptor), "bridgeAdaptor not set"); assertEq(rootBridgeFlowRate.childERC20Bridge(), CHILD_BRIDGE, "childERC20Bridge not set"); @@ -281,7 +280,6 @@ contract RootERC20BridgeFlowRateUnitTest is /** * RATE ROLE ACTIONS */ - function testActivateWithdrawalQueue() public { vm.prank(rateAdmin); rootBridgeFlowRate.activateWithdrawalQueue(); @@ -504,7 +502,6 @@ contract RootERC20BridgeFlowRateUnitTest is /** * FLOW RATE WITHDRAW */ - function testWithdrawalUnconfiguredToken() public { transferTokensToChild(); From 3fbb2e4846755882fd95fd8921960ff004eb81d0 Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Tue, 30 Jan 2024 08:06:24 +1100 Subject: [PATCH 075/155] Document flow rate parameters --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.md b/README.md index 2213bfc9..98697e70 100644 --- a/README.md +++ b/README.md @@ -149,5 +149,30 @@ ABIs for contracts can be obtained from the blockchain explorer links for each c | USDC | [`0x6de8aCC0D406837030CE4dd28e7c08C5a96a30d2`](https://explorer.immutable.com/address/0x6de8aCC0D406837030CE4dd28e7c08C5a96a30d2) | [`0x3B2d8A1931736Fc321C24864BceEe981B11c3c57`](https://explorer.testnet.immutable.com/address/0x3B2d8A1931736Fc321C24864BceEe981B11c3c57) | | USDT | [`0x68bcc7F1190AF20e7b572BCfb431c3Ac10A936Ab`](https://explorer.immutable.com/address/0x68bcc7F1190AF20e7b572BCfb431c3Ac10A936Ab) | TBA | | Wrapped BTC | [`0x235F9A2Dc29E51cE7D103bcC5Dfb4F5c9c3371De`](https://explorer.immutable.com/address/0x235F9A2Dc29E51cE7D103bcC5Dfb4F5c9c3371De) | TBA | + +## Flow Rate Parameters +Below are the flow rate parameters that have been configured on the L1 Mainnet and Testnet for different tokens. + +**Mainnet** + +| Token | Units | Capacity | Refill Rate | Large Transfer Threshold | +|-------------------------------------------------------------------------------|:------|----------|-------------|--------------------------| +| ETH | 10^18 | 10.08 | 0.0028 | 5.04 | +| [IMX](https://etherscan.io/token/0xf57e7e7c23978c3caec3c3548e3d615c346e79f) | 10^18 | 10008 | 2.78 | 5004 | +| [USDC](https://etherscan.io/token/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) | 10^6 | 20016 | 5.56 | 10008 | + +**Testnet** + +| Token | Units | Capacity | Refill Rate | Large Transfer Threshold | +|----------------------------------------------------------------------------------------------|:------|------------|-------------|--------------------------| +| ETH | 10^18 | 10.08 | 0.0028 | 5.04 | +| [IMX](https://sepolia.etherscan.io/address/0xe2629e08f4125d14e446660028bd98ee60ee69ff) | 10^18 | 68,976 | 19.16 | 34,488 | +| [GODS](https://sepolia.etherscan.io/address/0x5c9f1680bb6a4b4fc698e0cf702e0cc34aed91b7) | 10^18 | 10,008 | 2.78 | 5,004 | +| [GOG](https://sepolia.etherscan.io/address/0x9AB7bb7FdC60f4357ECFef43986818A2A3569c62) | 10^18 | 25,5816 | 71.06 | 127,908 | +| [CHECKMATE](https://sepolia.etherscan.io/address/0xE910c2a090516Fb7a7Be07f96a464785f2D5Dc18) | 10^18 | 12,276,000 | 3410 | 6,138,000 | + +*Note: USDC flow rate parameters have not yet been configured on Testnet.* + + ## Audits The Immutable token bridge has been audited by [Trail of Bits](https://www.trailofbits.com/). The audit report can be found [here](./audits/Trail-of-Bits-2023-12-14.pdf). From 46aeae79b07b16546b8c9902677faa4a244e24e5 Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Tue, 30 Jan 2024 08:13:10 +1100 Subject: [PATCH 076/155] Add link to flow rate documentation --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 98697e70..8494f97f 100644 --- a/README.md +++ b/README.md @@ -151,15 +151,15 @@ ABIs for contracts can be obtained from the blockchain explorer links for each c | Wrapped BTC | [`0x235F9A2Dc29E51cE7D103bcC5Dfb4F5c9c3371De`](https://explorer.immutable.com/address/0x235F9A2Dc29E51cE7D103bcC5Dfb4F5c9c3371De) | TBA | ## Flow Rate Parameters -Below are the flow rate parameters that have been configured on the L1 Mainnet and Testnet for different tokens. +Below are the [flow rate](https://github.com/immutable/zkevm-bridge-contracts/blob/documentation/docs/HLA-and-Threat-Model.md#flow-rate-detection) parameters that have been configured on the L1 Mainnet and Testnet deployments. **Mainnet** | Token | Units | Capacity | Refill Rate | Large Transfer Threshold | |-------------------------------------------------------------------------------|:------|----------|-------------|--------------------------| | ETH | 10^18 | 10.08 | 0.0028 | 5.04 | -| [IMX](https://etherscan.io/token/0xf57e7e7c23978c3caec3c3548e3d615c346e79f) | 10^18 | 10008 | 2.78 | 5004 | -| [USDC](https://etherscan.io/token/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) | 10^6 | 20016 | 5.56 | 10008 | +| [IMX](https://etherscan.io/token/0xf57e7e7c23978c3caec3c3548e3d615c346e79f) | 10^18 | 10,008 | 2.78 | 5,004 | +| [USDC](https://etherscan.io/token/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) | 10^6 | 20,016 | 5.56 | 10,008 | **Testnet** From 417e8d4bebee8c5fbf6589dc3217adaf41bbfb94 Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Tue, 30 Jan 2024 08:15:40 +1100 Subject: [PATCH 077/155] Fix formatting issue caused by latest forge version --- test/integration/root/RootERC20BridgeFlowRate.t.sol | 5 ----- test/unit/child/ChildAxelarBridgeAdaptor.t.sol | 7 ------- test/unit/child/ChildERC20Bridge.t.sol | 8 -------- .../unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol | 1 - .../child/withdrawals/ChildERC20BridgeWithdrawETH.t.sol | 1 - .../child/withdrawals/ChildERC20BridgeWithdrawETHTo.t.sol | 1 - .../child/withdrawals/ChildERC20BridgeWithdrawIMX.t.sol | 1 - .../child/withdrawals/ChildERC20BridgeWithdrawIMXTo.t.sol | 1 - .../child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol | 1 - .../child/withdrawals/ChildERC20BridgeWithdrawWIMX.t.sol | 1 - .../withdrawals/ChildERC20BridgeWithdrawWIMXTo.t.sol | 1 - test/unit/root/RootAxelarBridgeAdaptor.t.sol | 6 ------ test/unit/root/RootERC20Bridge.t.sol | 8 -------- test/unit/root/flowrate/RootERC20BridgeFlowRate.t.sol | 3 --- 14 files changed, 45 deletions(-) diff --git a/test/integration/root/RootERC20BridgeFlowRate.t.sol b/test/integration/root/RootERC20BridgeFlowRate.t.sol index cd12454f..17b00b3e 100644 --- a/test/integration/root/RootERC20BridgeFlowRate.t.sol +++ b/test/integration/root/RootERC20BridgeFlowRate.t.sol @@ -61,7 +61,6 @@ contract RootERC20BridgeFlowRateIntegrationTest is * This test uses the same code as the mapToken function does to calculate this address, so we can * not consider it sufficient. */ - function test_mapTokenTransfersValue() public { address childToken = Clones.predictDeterministicAddress(address(token), keccak256(abi.encodePacked(token)), CHILD_BRIDGE); @@ -135,7 +134,6 @@ contract RootERC20BridgeFlowRateIntegrationTest is /** * DEPOSIT ETH */ - function test_depositETHTransfersValue() public { uint256 tokenAmount = 300; setupDeposit(NATIVE_ETH, rootBridgeFlowRate, mapTokenFee, depositFee, tokenAmount, false); @@ -211,7 +209,6 @@ contract RootERC20BridgeFlowRateIntegrationTest is /** * DEPOSIT IMX */ - function test_depositIMXTokenTransfersValue() public { uint256 tokenAmount = 300; @@ -292,7 +289,6 @@ contract RootERC20BridgeFlowRateIntegrationTest is /** * DEPOSIT WETH */ - function test_depositWETHTransfersValue() public { uint256 tokenAmount = 300; setupDeposit(WRAPPED_ETH, rootBridgeFlowRate, mapTokenFee, depositFee, tokenAmount, false); @@ -446,7 +442,6 @@ contract RootERC20BridgeFlowRateIntegrationTest is /** * DEPOSIT TO */ - function test_depositToTransfersValue() public { uint256 tokenAmount = 300; address recipient = address(9876); diff --git a/test/unit/child/ChildAxelarBridgeAdaptor.t.sol b/test/unit/child/ChildAxelarBridgeAdaptor.t.sol index 87010030..73182c59 100644 --- a/test/unit/child/ChildAxelarBridgeAdaptor.t.sol +++ b/test/unit/child/ChildAxelarBridgeAdaptor.t.sol @@ -56,7 +56,6 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro /** * INITIALIZE */ - function test_Initialize() public { assertEq(address(axelarAdaptor.childBridge()), address(mockChildERC20Bridge), "childBridge not set"); assertEq(axelarAdaptor.rootChainId(), ROOT_CHAIN_NAME, "rootChain not set"); @@ -179,7 +178,6 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro /** * EXECUTE */ - function test_RevertIf_executeCalledWithInvalidSourceChain() public { bytes32 commandId = bytes32("testCommandId"); bytes memory payload = abi.encodePacked("payload"); @@ -221,7 +219,6 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro /** * SEND MESSAGE */ - function test_sendMessage_CallsGasService() public { address refundRecipient = address(123); bytes memory payload = abi.encode(WITHDRAW_SIG, address(token), address(this), address(999), 11111); @@ -342,7 +339,6 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro /** * UPDATE CHILD BRIDGE */ - function test_updateChildBridge_UpdatesChildBridge() public { vm.startPrank(bridgeManager); address newChildBridge = address(0x123); @@ -384,7 +380,6 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro /** * UPDATE ROOT CHAIN */ - function test_updateRootChain_UpdatesRootChain() public { vm.startPrank(targetManager); string memory newRootChain = "newRoot"; @@ -425,7 +420,6 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro /** * UPDATE ROOT BRIDGE ADAPTOR */ - function test_updateRootBridgeAdaptor_UpdatesRootBridgeAdaptor() public { vm.startPrank(targetManager); string memory newAdaptor = "newAdaptor"; @@ -470,7 +464,6 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro /** * UPDATE GAS SERVICE */ - function test_updateGasService_UpdatesGasService() public { vm.startPrank(gasServiceManager); address newGasService = address(0x123); diff --git a/test/unit/child/ChildERC20Bridge.t.sol b/test/unit/child/ChildERC20Bridge.t.sol index e93f334e..cc70efe2 100644 --- a/test/unit/child/ChildERC20Bridge.t.sol +++ b/test/unit/child/ChildERC20Bridge.t.sol @@ -67,7 +67,6 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B /** * RECEIVE */ - function test_NativeTransferFromWIMX() public { address caller = address(0x123a); payable(caller).transfer(2 ether); @@ -107,7 +106,6 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B /** * TREASURY DEPOSIT */ - function test_treasuryDepostIncreasesBalance() public { vm.deal(treasuryManager, 100 ether); vm.startPrank(treasuryManager); @@ -150,7 +148,6 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B /** * INITIALIZE */ - function test_Initialize() public { assertEq(address(childBridge.childBridgeAdaptor()), address(address(this)), "bridgeAdaptor not set"); assertEq(childBridge.childTokenTemplate(), address(childTokenTemplate), "childTokenTemplate not set"); @@ -257,7 +254,6 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B /** * UPDATE CHILD BRIDGE ADAPTOR */ - function test_updateChildBridgeAdaptor_UpdatesChildBridgeAdaptor() public { address newAdaptorAddress = address(0x11111); @@ -298,7 +294,6 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B /** * ON MESSAGE RECIEVE */ - function test_onMessageReceive_SetsTokenMapping() public { address predictedChildToken = Clones.predictDeterministicAddress( address(childTokenTemplate), keccak256(abi.encodePacked(rootToken)), address(childBridge) @@ -387,7 +382,6 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B /** * DEPOSIT ETH */ - function test_RevertsIf_OnMessageReceiveWhenPaused() public { pause(IPausable(address(childBridge))); bytes memory depositData = @@ -461,7 +455,6 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B /** * DEPOSIT */ - function test_onMessageReceive_DepositIMX_EmitsIMXDepositEvent() public { uint256 fundedAmount = 10 ether; vm.deal(address(childBridge), fundedAmount); @@ -616,7 +609,6 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B /** * WITHDRAW */ - function test_RevertIf_WithdrawReentered() public { // Create attack token vm.startPrank(address(childBridge)); diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol index dd3dea5e..9b564788 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol @@ -62,7 +62,6 @@ contract ChildERC20BridgeWithdrawUnitTest is Test, IChildERC20BridgeEvents, IChi /** * WITHDRAW */ - function test_RevertsIf_WithdrawWhenPaused() public { pause(IPausable(address(childBridge))); vm.expectRevert("Pausable: paused"); diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETH.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETH.t.sol index b564870f..c4fff9d4 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETH.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETH.t.sol @@ -49,7 +49,6 @@ contract ChildERC20BridgeWithdrawETHUnitTest is Test, IChildERC20BridgeEvents, I /** * WITHDRAW ETH */ - function test_RevertsIf_WithdrawETHWhenPaused() public { pause(IPausable(address(childBridge))); vm.expectRevert("Pausable: paused"); diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETHTo.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETHTo.t.sol index 21708c0f..efb39dd7 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETHTo.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETHTo.t.sol @@ -69,7 +69,6 @@ contract ChildERC20BridgeWithdrawETHToUnitTest is Test, IChildERC20BridgeEvents, /** * WITHDRAW ETH TO */ - function test_RevertsIf_WithdrawETHToWhenPaused() public { pause(IPausable(address(childBridge))); vm.expectRevert("Pausable: paused"); diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawIMX.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawIMX.t.sol index c48c08d8..31653971 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawIMX.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawIMX.t.sol @@ -44,7 +44,6 @@ contract ChildERC20BridgeWithdrawIMXUnitTest is Test, IChildERC20BridgeEvents, I /** * WITHDRAW IMX */ - function test_RevertIf_WithdrawIMXWhenPaused() public { pause(IPausable(address(childBridge))); vm.expectRevert("Pausable: paused"); diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawIMXTo.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawIMXTo.t.sol index b2fa6b72..8676cdb9 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawIMXTo.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawIMXTo.t.sol @@ -48,7 +48,6 @@ contract ChildERC20BridgeWithdrawIMXToUnitTest is Test, IChildERC20BridgeEvents, /** * WITHDRAW IMX TO */ - function test_RevertsIf_WithdrawIMXToWhenPaused() public { pause(IPausable(address(childBridge))); vm.expectRevert("Pausable: paused"); diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol index 88cc2b63..9bc0dc90 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol @@ -62,7 +62,6 @@ contract ChildERC20BridgeWithdrawToUnitTest is Test, IChildERC20BridgeEvents, IC /** * WITHDRAW TO */ - function test_RevertsIf_WithdrawToWhenPaused() public { pause(IPausable(address(childBridge))); vm.expectRevert("Pausable: paused"); diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMX.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMX.t.sol index 0d1b314c..f7a38208 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMX.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMX.t.sol @@ -48,7 +48,6 @@ contract ChildERC20BridgeWithdrawWIMXUnitTest is Test, IChildERC20BridgeEvents, /** * WITHDRAW WIMX */ - function test_RevertsIf_WithdrawWIMXWhenPaused() public { pause(IPausable(address(childBridge))); vm.expectRevert("Pausable: paused"); diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMXTo.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMXTo.t.sol index 83a6da3f..070cec06 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMXTo.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMXTo.t.sol @@ -50,7 +50,6 @@ contract ChildERC20BridgeWithdrawWIMXToUnitTest is Test, IChildERC20BridgeEvents /** * WITHDRAW WIMX TO */ - function test_RevertsIf_WithdrawWIMXToWhenPaused() public { pause(IPausable(address(childBridge))); vm.expectRevert("Pausable: paused"); diff --git a/test/unit/root/RootAxelarBridgeAdaptor.t.sol b/test/unit/root/RootAxelarBridgeAdaptor.t.sol index a9c3451c..949afd50 100644 --- a/test/unit/root/RootAxelarBridgeAdaptor.t.sol +++ b/test/unit/root/RootAxelarBridgeAdaptor.t.sol @@ -55,7 +55,6 @@ contract RootAxelarBridgeAdaptorTest is Test, IRootAxelarBridgeAdaptorEvents, IR /** * INITIALIZE */ - function test_Initialize() public { assertEq(address(axelarAdaptor.rootBridge()), address(stubRootBridge), "rootBridge not set"); assertEq(axelarAdaptor.childChainId(), CHILD_CHAIN_NAME, "childChain not set"); @@ -281,7 +280,6 @@ contract RootAxelarBridgeAdaptorTest is Test, IRootAxelarBridgeAdaptorEvents, IR /** * MAP TOKEN */ - function test_RevertIf_mapTokenCalledByNonRootBridge() public { address payable prankster = payable(address(0x33)); uint256 value = 300; @@ -304,7 +302,6 @@ contract RootAxelarBridgeAdaptorTest is Test, IRootAxelarBridgeAdaptorEvents, IR /** * UPDATE ROOT BRIDGE */ - function test_updateRootBridge_UpdatesRootBridge() public { vm.startPrank(bridgeManager); address newRootBridge = address(0x3333); @@ -344,7 +341,6 @@ contract RootAxelarBridgeAdaptorTest is Test, IRootAxelarBridgeAdaptorEvents, IR /** * UPDATE CHILD CHAIN */ - function test_updateChildChain_UpdatesChildChain() public { vm.startPrank(targetManager); string memory newChildChain = "newChildChain"; @@ -384,7 +380,6 @@ contract RootAxelarBridgeAdaptorTest is Test, IRootAxelarBridgeAdaptorEvents, IR /** * UPDATE CHILD BRIDGE ADAPTOR */ - function test_updateChildBridgeAdaptor_UpdatesChildBridgeAdaptor() public { vm.startPrank(targetManager); @@ -428,7 +423,6 @@ contract RootAxelarBridgeAdaptorTest is Test, IRootAxelarBridgeAdaptorEvents, IR /** * UPDATE GAS SERVICE */ - function test_updateGasService_UpdatesGasService() public { vm.startPrank(gasServiceManager); address newGasService = address(0x3333); diff --git a/test/unit/root/RootERC20Bridge.t.sol b/test/unit/root/RootERC20Bridge.t.sol index 51ab4364..8b63e1e2 100644 --- a/test/unit/root/RootERC20Bridge.t.sol +++ b/test/unit/root/RootERC20Bridge.t.sol @@ -84,7 +84,6 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid /** * INITIALIZE */ - function test_InitializeBridge() public { assertEq(address(rootBridge.rootBridgeAdaptor()), address(mockAxelarAdaptor), "bridgeAdaptor not set"); assertEq(rootBridge.childERC20Bridge(), CHILD_BRIDGE, "childERC20Bridge not set"); @@ -339,7 +338,6 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid /** * MAP TOKEN */ - function test_RevertsIf_MapTokenWhenPaused() public { pause(IPausable(address(rootBridge))); vm.expectRevert("Pausable: paused"); @@ -497,7 +495,6 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid /** * DEPOSIT ETH */ - function test_RevertsIf_DepositETHWhenPaused() public { pause(IPausable(address(rootBridge))); vm.expectRevert("Pausable: paused"); @@ -544,7 +541,6 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid /** * DEPOSIT TO ETH */ - function test_RevertsIf_DepositToETHWhenPaused() public { pause(IPausable(address(rootBridge))); vm.expectRevert("Pausable: paused"); @@ -594,7 +590,6 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid /** * ZERO AMOUNT */ - function test_RevertIf_depositETHAmountIsZero() public { uint256 amount = 0; setupDeposit(NATIVE_ETH, rootBridge, mapTokenFee, depositFee, amount, false); @@ -633,7 +628,6 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid /** * DEPOSIT WETH */ - function test_depositWETHCallsSendMessage() public { uint256 amount = 100; (, bytes memory predictedPayload) = @@ -708,7 +702,6 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid /** * DEPOSIT TOKEN */ - function test_RevertsIf_DepositReentered() public { // Create attack token ReentrancyAttackDeposit attackToken = new ReentrancyAttackDeposit(address(rootBridge)); @@ -916,7 +909,6 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid /** * DEPOSIT TO */ - function test_RevertsIf_DepositToWhenPaused() public { pause(IPausable(address(rootBridge))); vm.expectRevert("Pausable: paused"); diff --git a/test/unit/root/flowrate/RootERC20BridgeFlowRate.t.sol b/test/unit/root/flowrate/RootERC20BridgeFlowRate.t.sol index 1f435afb..d7f4d664 100644 --- a/test/unit/root/flowrate/RootERC20BridgeFlowRate.t.sol +++ b/test/unit/root/flowrate/RootERC20BridgeFlowRate.t.sol @@ -203,7 +203,6 @@ contract RootERC20BridgeFlowRateUnitTest is /** * INITIALIZE */ - function test_InitializeBridgeFlowRate() public { assertEq(address(rootBridgeFlowRate.rootBridgeAdaptor()), address(mockAxelarAdaptor), "bridgeAdaptor not set"); assertEq(rootBridgeFlowRate.childERC20Bridge(), CHILD_BRIDGE, "childERC20Bridge not set"); @@ -281,7 +280,6 @@ contract RootERC20BridgeFlowRateUnitTest is /** * RATE ROLE ACTIONS */ - function testActivateWithdrawalQueue() public { vm.prank(rateAdmin); rootBridgeFlowRate.activateWithdrawalQueue(); @@ -504,7 +502,6 @@ contract RootERC20BridgeFlowRateUnitTest is /** * FLOW RATE WITHDRAW */ - function testWithdrawalUnconfiguredToken() public { transferTokensToChild(); From ca5a72671753c88535588b3b67c9fbaab1c74a3e Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Tue, 30 Jan 2024 08:32:31 +1100 Subject: [PATCH 078/155] Fix issue with pruned foundry nightly version --- .github/workflows/coverage.yml | 2 +- .github/workflows/e2e.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/test.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 3a4d7b13..c1ba6d25 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -25,7 +25,7 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: - version: nightly-34f684ddfacc5b2ed371353ba6f730c485616ffe + version: nightly - name: Run Forge build run: | diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 5faa70b4..2f261b0e 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -21,7 +21,7 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: - version: nightly-34f684ddfacc5b2ed371353ba6f730c485616ffe + version: nightly - name: Run install uses: borales/actions-yarn@v4 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 73c7580d..d5804b89 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -20,7 +20,7 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: - version: nightly-34f684ddfacc5b2ed371353ba6f730c485616ffe + version: nightly - name: Run Forge fmt --check run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 52f77ec5..921bdba7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,7 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: - version: nightly-34f684ddfacc5b2ed371353ba6f730c485616ffe + version: nightly - name: Run Forge build run: | From e08fdd64f78a94b4951e9052d3f20bb329e03818 Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Tue, 30 Jan 2024 08:38:01 +1100 Subject: [PATCH 079/155] Update ToC --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8494f97f..109ed666 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ The Immutable token bridge facilitates the transfer of assets between two chains * [Build and Test](#build-and-test) * [Contract Deployment](#contract-deployment) * [Deployed Contract Addresses](#deployed-contract-addresses) +* [Flow Rate Parameters](#flow-rate-parameters) * [Audits](#audits) From 8bcd395a406d00ffd4595f9d8314034a7412afeb Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Tue, 30 Jan 2024 08:51:42 +1100 Subject: [PATCH 080/155] List game tokens that will be configured --- README.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 109ed666..c0c3c067 100644 --- a/README.md +++ b/README.md @@ -156,11 +156,13 @@ Below are the [flow rate](https://github.com/immutable/zkevm-bridge-contracts/bl **Mainnet** -| Token | Units | Capacity | Refill Rate | Large Transfer Threshold | -|-------------------------------------------------------------------------------|:------|----------|-------------|--------------------------| -| ETH | 10^18 | 10.08 | 0.0028 | 5.04 | -| [IMX](https://etherscan.io/token/0xf57e7e7c23978c3caec3c3548e3d615c346e79f) | 10^18 | 10,008 | 2.78 | 5,004 | -| [USDC](https://etherscan.io/token/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) | 10^6 | 20,016 | 5.56 | 10,008 | +| Token | Units | Capacity | Refill Rate | Large Transfer Threshold | +|-----------------------------------------------------------------------------------------------------|:------|----------|-------------|--------------------------| +| ETH | 10^18 | 10.08 | 0.0028 | 5.04 | +| [IMX](https://etherscan.io/token/0xf57e7e7c23978c3caec3c3548e3d615c346e79f) | 10^18 | 10,008 | 2.78 | 5,004 | +| [USDC](https://etherscan.io/token/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) | 10^6 | 20,016 | 5.56 | 10,008 | +| [Gods Unchained (GODS)](https://etherscan.io/address/0xccC8cb5229B0ac8069C51fd58367Fd1e622aFD97) | 10^18 | TBA | TBA | TBA | +| [Guild of Guardians (GOG)](https://etherscan.io/address/0x9AB7bb7FdC60f4357ECFef43986818A2A3569c62) | 10^18 | TBA | TBA | TBA | **Testnet** @@ -168,11 +170,10 @@ Below are the [flow rate](https://github.com/immutable/zkevm-bridge-contracts/bl |----------------------------------------------------------------------------------------------|:------|------------|-------------|--------------------------| | ETH | 10^18 | 10.08 | 0.0028 | 5.04 | | [IMX](https://sepolia.etherscan.io/address/0xe2629e08f4125d14e446660028bd98ee60ee69ff) | 10^18 | 68,976 | 19.16 | 34,488 | -| [GODS](https://sepolia.etherscan.io/address/0x5c9f1680bb6a4b4fc698e0cf702e0cc34aed91b7) | 10^18 | 10,008 | 2.78 | 5,004 | -| [GOG](https://sepolia.etherscan.io/address/0x9AB7bb7FdC60f4357ECFef43986818A2A3569c62) | 10^18 | 25,5816 | 71.06 | 127,908 | -| [CHECKMATE](https://sepolia.etherscan.io/address/0xE910c2a090516Fb7a7Be07f96a464785f2D5Dc18) | 10^18 | 12,276,000 | 3410 | 6,138,000 | +| [USDC](https://etherscan.io/token/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) | 10^6 | TBA | TBA | TBA | +| [Gods Unchained (GODS)]() | 10^18 | TBA | TBA | TBA | +| [Guild of Guardians (GOG)]() | 10^18 | TBA | TBA | TBA | -*Note: USDC flow rate parameters have not yet been configured on Testnet.* ## Audits From fd3eef1613340aa2d7104164e5652b3d69eb47ae Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Tue, 30 Jan 2024 08:52:46 +1100 Subject: [PATCH 081/155] Update table formatting --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index c0c3c067..64274934 100644 --- a/README.md +++ b/README.md @@ -166,13 +166,13 @@ Below are the [flow rate](https://github.com/immutable/zkevm-bridge-contracts/bl **Testnet** -| Token | Units | Capacity | Refill Rate | Large Transfer Threshold | -|----------------------------------------------------------------------------------------------|:------|------------|-------------|--------------------------| -| ETH | 10^18 | 10.08 | 0.0028 | 5.04 | -| [IMX](https://sepolia.etherscan.io/address/0xe2629e08f4125d14e446660028bd98ee60ee69ff) | 10^18 | 68,976 | 19.16 | 34,488 | -| [USDC](https://etherscan.io/token/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) | 10^6 | TBA | TBA | TBA | -| [Gods Unchained (GODS)]() | 10^18 | TBA | TBA | TBA | -| [Guild of Guardians (GOG)]() | 10^18 | TBA | TBA | TBA | +| Token | Units | Capacity | Refill Rate | Large Transfer Threshold | +|----------------------------------------------------------------------------------------|:------|----------|-------------|--------------------------| +| ETH | 10^18 | 10.08 | 0.0028 | 5.04 | +| [IMX](https://sepolia.etherscan.io/address/0xe2629e08f4125d14e446660028bd98ee60ee69ff) | 10^18 | 68,976 | 19.16 | 34,488 | +| [USDC](https://etherscan.io/token/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) | 10^6 | TBA | TBA | TBA | +| [Gods Unchained (GODS)]() | 10^18 | TBA | TBA | TBA | +| [Guild of Guardians (GOG)]() | 10^18 | TBA | TBA | TBA | From c6ab8293c55ffa22d88d5ad10fcbe42cd84defd0 Mon Sep 17 00:00:00 2001 From: Craig M Date: Tue, 30 Jan 2024 11:55:28 +1300 Subject: [PATCH 082/155] some refactoring --- test/fork/root/RootERC20BridgeFlowRate.t.sol | 141 ++++++++----------- 1 file changed, 60 insertions(+), 81 deletions(-) diff --git a/test/fork/root/RootERC20BridgeFlowRate.t.sol b/test/fork/root/RootERC20BridgeFlowRate.t.sol index 5baec1e1..d97f67c9 100644 --- a/test/fork/root/RootERC20BridgeFlowRate.t.sol +++ b/test/fork/root/RootERC20BridgeFlowRate.t.sol @@ -16,6 +16,7 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils { // move to .env address payable rootBridgeAddress = payable(0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6); + address IMX = address(0xF57e7e7C23978C3cAEC3C3548E3D615c346e79fF); address rootAdapter = address(0x4f49B53928A71E553bB1B0F66a5BcB54Fd4E8932); address receiver1 = address(0x111); address receiver2 = address(0x222); @@ -37,113 +38,91 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils { } function test_flowRateETH() public { - uint256 largeThreshold = rootBridgeFlowRate.largeTransferThresholds(NATIVE_ETH); - (uint256 capacity, uint256 depth, uint256 refillTime, uint256 refillRate) = rootBridgeFlowRate.flowRateBuckets(NATIVE_ETH); + _flowRate(NATIVE_ETH); + } + + function _flowRate(address token) public { + uint256 largeThreshold = rootBridgeFlowRate.largeTransferThresholds(token); + + //only need depth returned + (, uint256 depth, ,) = rootBridgeFlowRate.flowRateBuckets(token); // send 75% of the largeThreshold value uint256 txValue = ((largeThreshold / 100) * 75); uint256 numTxs = (depth / txValue) + 2; - uint256 numTxsReceiver1 = 0; - - //deal enough ETH to the bridge to cover all the txs - vm.deal(rootBridgeAddress, txValue * numTxs); + if (token == NATIVE_ETH) { + //deal enough ETH to the bridge to cover all the txs + vm.deal(rootBridgeAddress, txValue * numTxs); + } else { + //@TODO ensure the bridge has enough tokens to cover all the txs + console2.log('not eth'); + } - while(depth > 0) { - //prank as axelar sending a message to the adapter - vm.startPrank(rootAdapter); + //withdraw until queue is activated + bool queueActivated = rootBridgeFlowRate.withdrawalQueueActivated(); + while(queueActivated == false) { + _sendWithdrawMessage(token, receiver1, receiver1, txValue); + queueActivated = rootBridgeFlowRate.withdrawalQueueActivated(); + } - bytes memory predictedPayload1 = - abi.encode(rootBridgeFlowRate.WITHDRAW_SIG(), NATIVE_ETH, receiver1, receiver1, txValue); - rootBridgeFlowRate.onMessageReceive(predictedPayload1); - vm.stopPrank(); + //send one more tx to receiver2 and make sure it gets queued + _sendWithdrawMessage(token, receiver2, receiver2, txValue); - (capacity, depth, refillTime, refillRate) = rootBridgeFlowRate.flowRateBuckets(NATIVE_ETH); + //attempt to withdraw for receiver 1 + uint256 okTime1 = _attemptEarlyWithdraw(token, receiver1, txValue); - bool queueActivated = rootBridgeFlowRate.withdrawalQueueActivated(); + //attempt to withdraw for receiver 2 + uint256 okTime2 = _attemptEarlyWithdraw(token, receiver2, txValue); - if (depth > 0) { - assertFalse(queueActivated); - } else { - assertTrue(queueActivated); - } + //fast forward past withdrawal delay time and withdraw for receiver 1 + vm.warp(okTime1+1); + rootBridgeFlowRate.finaliseQueuedWithdrawal(receiver1, 0); - numTxsReceiver1 += 1; - } + //fast forward past withdrawal delay time and withdraw for receiver 2 + vm.warp(okTime2+1); + rootBridgeFlowRate.finaliseQueuedWithdrawal(receiver2, 0); - //sanity check we dealt the enough eth - assertEq(numTxsReceiver1+1, numTxs); + } - //send one more tx to receiver2 and make sure it gets queued - vm.startPrank(rootAdapter); - bytes memory predictedPayload2 = - abi.encode(rootBridgeFlowRate.WITHDRAW_SIG(), NATIVE_ETH, receiver2, receiver2, txValue); - rootBridgeFlowRate.onMessageReceive(predictedPayload2); - vm.stopPrank(); + function _attemptEarlyWithdraw(address token, address receiver, uint256 txValue) + public returns (uint256 okTime){ - uint256 pendingLength1 = rootBridgeFlowRate.getPendingWithdrawalsLength(receiver1); - uint256 pendingLength2 = rootBridgeFlowRate.getPendingWithdrawalsLength(receiver2); + uint256 pendingLength = rootBridgeFlowRate.getPendingWithdrawalsLength(receiver); - //each receiver should have 1 queued tx - assertEq(pendingLength1, 1); - assertEq(pendingLength2, 1); + assertEq(pendingLength, 1); - uint256[] memory indices1 = new uint256[](1); - indices1[0] = 0; + uint256[] memory indices = new uint256[](1); + indices[0] = 0; - RootERC20BridgeFlowRate.PendingWithdrawal[] memory pending1 = - rootBridgeFlowRate.getPendingWithdrawals(receiver1, indices1); + RootERC20BridgeFlowRate.PendingWithdrawal[] memory pending = + rootBridgeFlowRate.getPendingWithdrawals(receiver, indices); - assertEq(pending1.length, 1); - assertEq(pending1[0].withdrawer, receiver1); - assertEq(pending1[0].token, NATIVE_ETH); - assertEq(pending1[0].amount, txValue); - uint256 timestamp1 = pending1[0].timestamp; + assertEq(pending.length, 1); + assertEq(pending[0].withdrawer, receiver); + assertEq(pending[0].token, token); + assertEq(pending[0].amount, txValue); + uint256 timestamp = pending[0].timestamp; - uint256 okTime1 = timestamp1 + withdrawDelay; + okTime = timestamp + withdrawDelay; //deal some eth to pay withdraw gas vm.deal(address(this), 1 ether); - //try to process withdraw 1 - vm.expectRevert( - abi.encodeWithSelector(IFlowRateWithdrawalQueueErrors.WithdrawalRequestTooEarly.selector, timestamp1, okTime1) - ); - rootBridgeFlowRate.finaliseQueuedWithdrawal(receiver1, 0); - - uint256[] memory indices2 = new uint256[](1); - indices2[0] = 0; - - RootERC20BridgeFlowRate.PendingWithdrawal[] memory pending2 = - rootBridgeFlowRate.getPendingWithdrawals(receiver2, indices2); - - assertEq(pending2.length, 1); - assertEq(pending2[0].withdrawer, receiver2); - assertEq(pending2[0].token, NATIVE_ETH); - assertEq(pending2[0].amount, txValue); - uint256 timestamp2 = pending2[0].timestamp; - - uint256 okTime2 = timestamp2 + withdrawDelay; - - //try to process withdraw 2 + //try to process the withdrawal vm.expectRevert( - abi.encodeWithSelector(IFlowRateWithdrawalQueueErrors.WithdrawalRequestTooEarly.selector, timestamp2, okTime2) + abi.encodeWithSelector(IFlowRateWithdrawalQueueErrors.WithdrawalRequestTooEarly.selector, timestamp, okTime) ); - rootBridgeFlowRate.finaliseQueuedWithdrawal(receiver2, 0); - - vm.warp(okTime1+1); - - rootBridgeFlowRate.finaliseQueuedWithdrawal(receiver1, 0); - - vm.warp(okTime2+1); - - rootBridgeFlowRate.finaliseQueuedWithdrawal(receiver2, 0); - - console2.log('success'); - - //warp to time when withdraw can be processed + rootBridgeFlowRate.finaliseQueuedWithdrawal(receiver, 0); + } - //try to withdraw again + function _sendWithdrawMessage(address token, address sender, address receiver, uint256 txValue) public { + //prank as axelar sending a message to the adapter + vm.startPrank(rootAdapter); + bytes memory predictedPayload = + abi.encode(rootBridgeFlowRate.WITHDRAW_SIG(), token, sender, receiver, txValue); + rootBridgeFlowRate.onMessageReceive(predictedPayload); + vm.stopPrank(); } } From 9cfd57fe72650aa747381c8262e0d81b69ac5142 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Wed, 31 Jan 2024 13:37:32 +1000 Subject: [PATCH 083/155] Add test case --- package.json | 2 +- scripts/e2e/e2e.ts | 142 +++++++++++++++++++++++++++++++++++++ scripts/helpers/helpers.ts | 1 - 3 files changed, 143 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index aa213c2b..f3648521 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "lint": "forge fmt", "local:start": "cd scripts/localdev; ./start.sh", "local:setup": "cd scripts/localdev; rm -rf .child.bridge.contracts.json .root.bridge.contracts.json; ./deploy.sh", - "local:test": "cd scripts/localdev; AXELAR_API_URL=skip npx mocha --require mocha-suppress-logs ../e2e/e2e.ts", + "local:test": "cd scripts/localdev; AXELAR_API_URL=skip npx mocha ../e2e/e2e.ts", "local:ci": "cd scripts/localdev; rm -rf .child.bridge.contracts.json .root.bridge.contracts.json; ./ci.sh && ./deploy.sh && AXELAR_API_URL=skip npx mocha --require mocha-suppress-logs ../e2e/e2e.ts && ./stop.sh", "local:chainonly": "cd scripts/localdev; LOCAL_CHAIN_ONLY=true ./start.sh", "local:axelaronly": "cd scripts/localdev; npx ts-node axelar_setup.ts", diff --git a/scripts/e2e/e2e.ts b/scripts/e2e/e2e.ts index 7e0175bb..712fbe1a 100644 --- a/scripts/e2e/e2e.ts +++ b/scripts/e2e/e2e.ts @@ -1804,6 +1804,7 @@ describe("Bridge e2e test", () => { expect(postBalL2.toBigInt()).to.equal(expectedPostL2.toBigInt()); }).timeout(2400000) + // Local only it("should put mapped ERC20 Token withdrawal in pending when violating rate limit policy", async() => { // Set new rate limit let resp = await rootBridge.connect(rootPrivilegedWallet).setRateControlThreshold(rootCustomToken.address, ethers.utils.parseEther("1.0008"), ethers.utils.parseEther("0.000278"), ethers.utils.parseEther("0.5004")); @@ -1885,4 +1886,145 @@ describe("Bridge e2e test", () => { resp = await rootBridge.connect(rootPrivilegedWallet).deactivateWithdrawalQueue(); await waitForReceipt(resp.hash, rootProvider); }).timeout(2400000) + + // Local only + it("should successfully process multiple deposit and withdrawal requests in parallel", async() => { + // Deposit & withdrawal amount + let amtL1 = ethers.utils.parseEther("1.0"); + let bridgeFeeL1 = ethers.utils.parseEther("0.001"); + let amtL2 = ethers.utils.parseEther("0.5"); + let bridgeFeeL2 = ethers.utils.parseEther("1.0"); + + // Wrap & Approval + let resp = await rootIMX.connect(rootTestWallet).approve(rootBridge.address, amtL1); + await waitForReceipt(resp.hash, rootProvider); + + resp = await rootWETH.connect(rootTestWallet).deposit({ value: amtL1 }); + await waitForReceipt(resp.hash, rootProvider); + resp = await rootWETH.connect(rootTestWallet).approve(rootBridge.address, amtL1); + await waitForReceipt(resp.hash, rootProvider); + + resp = await rootCustomToken.connect(rootTestWallet).approve(rootBridge.address, amtL1); + await waitForReceipt(resp.hash, rootProvider); + + resp = await childWIMX.connect(childTestWallet).deposit( {value: amtL2 }); + await waitForReceipt(resp.hash, childProvider); + resp = await childWIMX.connect(childTestWallet).approve(childBridge.address, amtL2); + await waitForReceipt(resp.hash, childProvider); + + // Deposit IMX, ETH, WETH, ERC20, and withdraw IMX, WIMX, ETH, ERC20 + let preIMXBalL1 = await rootIMX.balanceOf(rootTestWallet.address); + let preETHBalL1 = await rootProvider.getBalance(rootTestWallet.address); + let preWETHBalL1 = await rootWETH.balanceOf(rootTestWallet.address); + let preERC20BalL1 = await rootCustomToken.balanceOf(rootTestWallet.address); + let preIMXBalL2 = await childProvider.getBalance(childTestWallet.address); + let preWIMXBalL2 = await childWIMX.balanceOf(childTestWallet.address); + let preETHBalL2 = await childETH.balanceOf(childTestWallet.address); + let preERC20BalL2 = await childCustomToken.balanceOf(childTestWallet.address); + + // Stop mining + await rootProvider.send( + "evm_setIntervalMining", [ + 0, + ]); + await childProvider.send( + "evm_setIntervalMining", [ + 0, + ]); + + // Calls on L1 & L2 + let resp1 = await rootBridge.connect(rootTestWallet).deposit(rootIMX.address, amtL1, { + value: bridgeFeeL1, + }); + let resp2 = await rootBridge.connect(rootTestWallet).depositETH(amtL1, { + value: bridgeFeeL1.add(amtL1), + }); + let resp3 = await rootBridge.connect(rootTestWallet).deposit(rootWETH.address, amtL1, { + value: bridgeFeeL1, + }); + let resp4 = await rootBridge.connect(rootTestWallet).deposit(rootCustomToken.address, amtL1, { + value: bridgeFeeL1, + }); + let resp5 = await childBridge.connect(childTestWallet).withdrawIMX(amtL2, { + value: bridgeFeeL2.add(amtL2), + }); + let resp6 = await childBridge.connect(childTestWallet).withdrawWIMX(amtL2, { + value: bridgeFeeL2, + }); + let resp7 = await childBridge.connect(childTestWallet).withdrawETH(amtL2, { + value: bridgeFeeL2, + }); + let resp8 = await childBridge.connect(childTestWallet).withdraw(childCustomToken.address, amtL2, { + value: bridgeFeeL2, + }); + + + // Enable mining + await rootProvider.send( + "evm_setIntervalMining", [ + 1200, + ]); + await childProvider.send( + "evm_setIntervalMining", [ + 200, + ]); + + // Wait for transactions to be mined. + await waitForReceipt(resp1.hash, rootProvider); + await waitForReceipt(resp2.hash, rootProvider); + await waitForReceipt(resp3.hash, rootProvider); + await waitForReceipt(resp4.hash, rootProvider); + await waitForReceipt(resp5.hash, childProvider); + await waitForReceipt(resp6.hash, childProvider); + await waitForReceipt(resp7.hash, childProvider); + await waitForReceipt(resp8.hash, childProvider); + + // Wait for 30 seconds + await delay(30000); + let receipt = await rootProvider.getTransactionReceipt(resp1.hash); + let txFee1 = receipt.gasUsed.mul(receipt.effectiveGasPrice); + receipt = await rootProvider.getTransactionReceipt(resp2.hash); + let txFee2 = receipt.gasUsed.mul(receipt.effectiveGasPrice); + receipt = await rootProvider.getTransactionReceipt(resp3.hash); + let txFee3 = receipt.gasUsed.mul(receipt.effectiveGasPrice); + receipt = await rootProvider.getTransactionReceipt(resp4.hash); + let txFee4 = receipt.gasUsed.mul(receipt.effectiveGasPrice); + receipt = await childProvider.getTransactionReceipt(resp5.hash); + let txFee5 = receipt.gasUsed.mul(receipt.effectiveGasPrice); + receipt = await childProvider.getTransactionReceipt(resp6.hash); + let txFee6 = receipt.gasUsed.mul(receipt.effectiveGasPrice); + receipt = await childProvider.getTransactionReceipt(resp7.hash); + let txFee7 = receipt.gasUsed.mul(receipt.effectiveGasPrice); + receipt = await childProvider.getTransactionReceipt(resp8.hash); + let txFee8 = receipt.gasUsed.mul(receipt.effectiveGasPrice); + + let postIMXBalL1 = await rootIMX.balanceOf(rootTestWallet.address); + let postETHBalL1 = await rootProvider.getBalance(rootTestWallet.address); + let postWETHBalL1 = await rootWETH.balanceOf(rootTestWallet.address); + let postERC20BalL1 = await rootCustomToken.balanceOf(rootTestWallet.address); + let postIMXBalL2 = await childProvider.getBalance(childTestWallet.address); + let postWIMXBalL2 = await childWIMX.balanceOf(childTestWallet.address); + let postETHBalL2 = await childETH.balanceOf(childTestWallet.address); + let postERC20BalL2 = await childCustomToken.balanceOf(childTestWallet.address); + + // Verify + let expectedIMXBalL1 = preIMXBalL1.sub(amtL1).add(amtL2.mul(2)); + let expectedETHBalL1 = preETHBalL1.sub(amtL1).add(amtL2).sub(bridgeFeeL1.mul(4)).sub(txFee1).sub(txFee2).sub(txFee3).sub(txFee4); + let expectedWETHBalL1 = preWETHBalL1.sub(amtL1); + let expectedERC20BalL1 = preERC20BalL1.sub(amtL1).add(amtL2); + let expectedIMXBalL2 = preIMXBalL2.add(amtL1).sub(amtL2).sub(bridgeFeeL2.mul(4)).sub(txFee5).sub(txFee6).sub(txFee7).sub(txFee8); + let expectedWIMXBalL2 = preWIMXBalL2.sub(amtL2); + let expectedETHBalL2 = preETHBalL2.add(amtL1.mul(2)).sub(amtL2); + let expectedERC20BalL2 = preERC20BalL2.add(amtL1).sub(amtL2); + expect(postIMXBalL1.toBigInt()).to.equal(expectedIMXBalL1.toBigInt()); + expect(postETHBalL1.toBigInt()).to.equal(expectedETHBalL1.toBigInt()); + expect(postWETHBalL1.toBigInt()).to.equal(expectedWETHBalL1.toBigInt()); + expect(postERC20BalL1.toBigInt()).to.equal(expectedERC20BalL1.toBigInt()); + expect(postIMXBalL2.toBigInt()).to.equal(expectedIMXBalL2.toBigInt()); + expect(postWIMXBalL2.toBigInt()).to.equal(expectedWIMXBalL2.toBigInt()); + expect(postETHBalL2.toBigInt()).to.equal(expectedETHBalL2.toBigInt()); + expect(postERC20BalL2.toBigInt()).to.equal(expectedERC20BalL2.toBigInt()); + + // Test balance. + }).timeout(2400000) }) \ No newline at end of file diff --git a/scripts/helpers/helpers.ts b/scripts/helpers/helpers.ts index c0ed555e..18992b13 100644 --- a/scripts/helpers/helpers.ts +++ b/scripts/helpers/helpers.ts @@ -27,7 +27,6 @@ export async function waitForReceipt(txHash: string, provider: providers.JsonRpc receipt = await provider.getTransactionReceipt(txHash) await exports.delay(1000); } - console.log("Receipt: " + JSON.stringify(receipt, null, 2)); if (receipt.status != 1) { throw("Fail to execute: " + txHash); } From ba857665b41fb5e3b4782c0f05feb6e8dea4d6bc Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Wed, 31 Jan 2024 14:25:37 +1000 Subject: [PATCH 084/155] Update e2e.yml --- .github/workflows/e2e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 17a3cdde..099f4a7b 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -9,7 +9,7 @@ jobs: build: name: End to End Test runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 30 steps: - uses: actions/checkout@v3 From 84c262761d082bbb74d4c493002bdd3aeab4dd69 Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Wed, 31 Jan 2024 16:49:58 +1100 Subject: [PATCH 085/155] Implement flowrate fork test --- test/fork/root/RootERC20BridgeFlowRate.t.sol | 266 +++++++++++++------ 1 file changed, 179 insertions(+), 87 deletions(-) diff --git a/test/fork/root/RootERC20BridgeFlowRate.t.sol b/test/fork/root/RootERC20BridgeFlowRate.t.sol index d97f67c9..01cba67f 100644 --- a/test/fork/root/RootERC20BridgeFlowRate.t.sol +++ b/test/fork/root/RootERC20BridgeFlowRate.t.sol @@ -4,125 +4,217 @@ pragma solidity 0.8.19; import {Test, console2} from "forge-std/Test.sol"; import {RootERC20BridgeFlowRate} from "../../../src/root/flowrate/RootERC20BridgeFlowRate.sol"; import {IFlowRateWithdrawalQueueErrors} from "../../../src/root/flowrate/FlowRateWithdrawalQueue.sol"; +import {console} from "forge-std/console.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {Utils} from "../../utils.t.sol"; contract RootERC20BridgeFlowRateForkTest is Test, Utils { - uint256 mainnetFork; - RootERC20BridgeFlowRate public rootBridgeFlowRate; - string MAINNET_RPC_URL = vm.envString("FORK_MAINNET_RPC_URL"); - address NATIVE_ETH = address(0x0000000000000000000000000000000000000Eee); - uint256 withdrawDelay; - - // move to .env - address payable rootBridgeAddress = payable(0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6); - address IMX = address(0xF57e7e7C23978C3cAEC3C3548E3D615c346e79fF); - address rootAdapter = address(0x4f49B53928A71E553bB1B0F66a5BcB54Fd4E8932); - address receiver1 = address(0x111); - address receiver2 = address(0x222); - - - struct Bucket { - uint256 capacity; - uint256 depth; - uint256 refillTime; - uint256 refillRate; + address public constant NATIVE_ETH = address(0xeee); + + string[] private deployments = vm.envString("DEPLOYMENTS", ","); + mapping(string => string) private rpcURLForEnv; + mapping(string => uint256) private forkOfEnv; + mapping(string => RootERC20BridgeFlowRate) private bridgeForEnv; + mapping(string => address[]) private tokensForEnv; + + string private deployment; + modifier forEachDeployment() { + for (uint256 i; i < deployments.length; i++) { + deployment = deployments[i]; + vm.selectFork(forkOfEnv[deployment]); + _; + } } function setUp() public { - mainnetFork = vm.createFork(MAINNET_RPC_URL); - vm.selectFork(mainnetFork); - rootBridgeFlowRate = RootERC20BridgeFlowRate(rootBridgeAddress); - withdrawDelay = rootBridgeFlowRate.withdrawalDelay(); - assertGt(withdrawDelay, 0); + for (uint256 i; i < deployments.length; i++) { + string memory dep = deployments[i]; + rpcURLForEnv[dep] = vm.envString(string.concat(dep, "_RPC_URL")); + bridgeForEnv[dep] = RootERC20BridgeFlowRate(payable(vm.envAddress(string.concat(dep, "_BRIDGE_ADDRESS")))); + tokensForEnv[dep] = vm.envAddress(string.concat(dep, "_FLOW_RATED_TOKENS"), ","); + forkOfEnv[dep] = vm.createFork(rpcURLForEnv[dep]); + } } - function test_flowRateETH() public { - _flowRate(NATIVE_ETH); + function test_withdrawalQueueEnforcedWhenFlowRateExceeded() public forEachDeployment { + console.log("Testing deployment: ", deployment); + vm.selectFork(forkOfEnv[deployment]); + _verifyWithdrawalQueueEnforcedForAllTokens(bridgeForEnv[deployment], tokensForEnv[deployment]); } - function _flowRate(address token) public { - uint256 largeThreshold = rootBridgeFlowRate.largeTransferThresholds(token); + function test_nonFlowRatedTokenIsQueued() public forEachDeployment { + vm.selectFork(forkOfEnv[deployment]); + RootERC20BridgeFlowRate bridge = bridgeForEnv[deployment]; - //only need depth returned - (, uint256 depth, ,) = rootBridgeFlowRate.flowRateBuckets(token); + // preconditions + assertFalse(bridge.withdrawalQueueActivated()); - // send 75% of the largeThreshold value - uint256 txValue = ((largeThreshold / 100) * 75); + // deploy and map a token + ERC20 erc20 = new ERC20("Test Token", "TEST"); + bridge.mapToken{value: 100 gwei}(erc20); + _giveBridgeFunds(address(erc20), address(erc20), 1 ether); - uint256 numTxs = (depth / txValue) + 2; + // ensure withdrawals for the token, which does not have flow rate configured, is queued + _sendWithdrawalMessage(bridge, address(erc20), address(1), 1 ether); + _verifyWithdrawalWasQueued(bridge, address(erc20), address(1), 1 ether); - if (token == NATIVE_ETH) { - //deal enough ETH to the bridge to cover all the txs - vm.deal(rootBridgeAddress, txValue * numTxs); - } else { - //@TODO ensure the bridge has enough tokens to cover all the txs - console2.log('not eth'); - } + // The queue should only affect the specific token + assertFalse(bridge.withdrawalQueueActivated()); + } - //withdraw until queue is activated - bool queueActivated = rootBridgeFlowRate.withdrawalQueueActivated(); - while(queueActivated == false) { - _sendWithdrawMessage(token, receiver1, receiver1, txValue); - queueActivated = rootBridgeFlowRate.withdrawalQueueActivated(); - } + function test_withdrawalQueueDelayEnforced() public {} - //send one more tx to receiver2 and make sure it gets queued - _sendWithdrawMessage(token, receiver2, receiver2, txValue); + function test_withdrawalQueuedIfTransferIsTooLarge() public { - //attempt to withdraw for receiver 1 - uint256 okTime1 = _attemptEarlyWithdraw(token, receiver1, txValue); + } - //attempt to withdraw for receiver 2 - uint256 okTime2 = _attemptEarlyWithdraw(token, receiver2, txValue); + function test_queuedWithdrawalsCanBeFinalised() public {} + + function _verifyWithdrawalQueueEnforcedForAllTokens(RootERC20BridgeFlowRate bridge, address[] memory tokens) + private + { + // preconditions + assertFalse(bridge.withdrawalQueueActivated()); + assertGt(bridge.withdrawalDelay(), 0); + + uint256 snapshotId = vm.snapshot(); + for (uint256 i; i < tokens.length; i++) { + address token = tokens[i]; + console.log("Testing flow rate for token: ", token); + // exceed flow rate for any token + _exceedFlowRateParameters(bridge, token, address(11)); + + // Verify that any subsequent withdrawal by other users for other tokens gets queued + address otherToken = tokens[(i + 1) % tokens.length]; + _sendWithdrawalMessage(bridge, otherToken, address(12), 1 ether); + _verifyWithdrawalWasQueued(bridge, otherToken, address(12), 1 ether); + + // roll back state for subsequent test + vm.revertTo(snapshotId); + } + } - //fast forward past withdrawal delay time and withdraw for receiver 1 - vm.warp(okTime1+1); - rootBridgeFlowRate.finaliseQueuedWithdrawal(receiver1, 0); + function _exceedFlowRateParameters(RootERC20BridgeFlowRate bridge, address token, address receiver) private { + (uint256 capacity, uint256 depth,, uint256 refillRate) = bridge.flowRateBuckets(token); + // check preconditions + assertGt(bridge.largeTransferThresholds(token), 0); + assertGt(capacity, 0); + assertGt(refillRate, 0); + assertEq(bridge.getPendingWithdrawalsLength(receiver), 0); - //fast forward past withdrawal delay time and withdraw for receiver 2 - vm.warp(okTime2+1); - rootBridgeFlowRate.finaliseQueuedWithdrawal(receiver2, 0); + uint256 txValue = bridge.largeTransferThresholds(token) - 1; + uint256 numTxs = depth > txValue ? (depth / txValue) + 2 : 1; - } + _giveBridgeFunds(address(bridge), token, numTxs * txValue * 2); - function _attemptEarlyWithdraw(address token, address receiver, uint256 txValue) - public returns (uint256 okTime){ + // withdraw until flow rate is exceeded + for (uint256 i = 0; i < numTxs; i++) { + (, depth,,) = bridge.flowRateBuckets(token); + _sendWithdrawalMessage(bridge, token, receiver, txValue); + } - uint256 pendingLength = rootBridgeFlowRate.getPendingWithdrawalsLength(receiver); + assertTrue(bridge.withdrawalQueueActivated()); + _verifyWithdrawalWasQueued(bridge, token, receiver, txValue); + } - assertEq(pendingLength, 1); + function _giveBridgeFunds(address bridge, address token, uint256 amount) private { + if (token == NATIVE_ETH) { + deal(bridge, amount); + } else { + deal(token, bridge, amount); + } + } + function _verifyWithdrawalWasQueued( + RootERC20BridgeFlowRate bridge, + address token, + address receiver, + uint256 txValue + ) private { uint256[] memory indices = new uint256[](1); indices[0] = 0; + assertEq(bridge.getPendingWithdrawalsLength(receiver), 1, "Expected 1 pending withdrawal"); + RootERC20BridgeFlowRate.PendingWithdrawal[] memory pending = bridge.getPendingWithdrawals(receiver, indices); + assertEq(pending[0].withdrawer, receiver, "Unexpected withdrawer"); + assertEq(pending[0].token, token, "Unexpected token"); + assertEq(pending[0].amount, txValue, "Unexpected amount"); + } - RootERC20BridgeFlowRate.PendingWithdrawal[] memory pending = - rootBridgeFlowRate.getPendingWithdrawals(receiver, indices); - - assertEq(pending.length, 1); - assertEq(pending[0].withdrawer, receiver); - assertEq(pending[0].token, token); - assertEq(pending[0].amount, txValue); - uint256 timestamp = pending[0].timestamp; - - okTime = timestamp + withdrawDelay; - - //deal some eth to pay withdraw gas - vm.deal(address(this), 1 ether); - - //try to process the withdrawal - vm.expectRevert( - abi.encodeWithSelector(IFlowRateWithdrawalQueueErrors.WithdrawalRequestTooEarly.selector, timestamp, okTime) - ); - rootBridgeFlowRate.finaliseQueuedWithdrawal(receiver, 0); + // ensure subsequent withdrawals for any token, by any entity are queued + // _sendWithdrawMessage(NATIVE_ETH, receiver, receiver, txValue); + + /* + Check that the withdrawal queue has been activated + Check that the user has a pending withdrawal for the specified token + */ + // + // assertEq(bridge.getPendingWithdrawalsLength(receiver1), 0); + // + // //send one more tx to receiver2 and make sure it gets queued + // _sendWithdrawMessage(token, receiver2, receiver2, txValue); + // + // //attempt to withdraw for receiver 1 + // uint256 okTime1 = _attemptEarlyWithdraw(token, receiver1, txValue); + // + // //attempt to withdraw for receiver 2 + // uint256 okTime2 = _attemptEarlyWithdraw(token, receiver2, txValue); + // + // //fast forward past withdrawal delay time and withdraw for receiver 1 + // vm.warp(okTime1 + 1); + // bridge.finaliseQueuedWithdrawal(receiver1, 0); + // + // //fast forward past withdrawal delay time and withdraw for receiver 2 + // vm.warp(okTime2 + 1); + // bridge.finaliseQueuedWithdrawal(receiver2, 0); + + // function _attemptEarlyWithdraw(address token, address receiver, uint256 txValue) public returns (uint256 okTime) { + // uint withdrawDelay = bridge.withdrawalDelay(); + // uint256 pendingLength = bridge.getPendingWithdrawalsLength(receiver); + // + // assertEq(pendingLength, 1); + // + // uint256[] memory indices = new uint256[](1); + // indices[0] = 0; + // + // RootERC20BridgeFlowRate.PendingWithdrawal[] memory pending = + // bridge.getPendingWithdrawals(receiver, indices); + // + // assertEq(pending.length, 1); + // assertEq(pending[0].withdrawer, receiver); + // assertEq(pending[0].token, token); + // assertEq(pending[0].amount, txValue); + // uint256 timestamp = pending[0].timestamp; + // + // okTime = timestamp + withdrawDelay; + // + // //deal some eth to pay withdraw gas + // vm.deal(address(this), 1 ether); + // + // //try to process the withdrawal + // vm.expectRevert( + // abi.encodeWithSelector(IFlowRateWithdrawalQueueErrors.WithdrawalRequestTooEarly.selector, timestamp, okTime) + // ); + // bridge.finaliseQueuedWithdrawal(receiver, 0); + // } + + function _sendWithdrawalMessage(RootERC20BridgeFlowRate bridge, address token, address sender, uint256 txValue) + private + { + _sendWithdrawMessage(bridge, token, sender, sender, txValue); } - function _sendWithdrawMessage(address token, address sender, address receiver, uint256 txValue) public { + function _sendWithdrawMessage( + RootERC20BridgeFlowRate bridge, + address token, + address sender, + address receiver, + uint256 txValue + ) private { //prank as axelar sending a message to the adapter - vm.startPrank(rootAdapter); - bytes memory predictedPayload = - abi.encode(rootBridgeFlowRate.WITHDRAW_SIG(), token, sender, receiver, txValue); - rootBridgeFlowRate.onMessageReceive(predictedPayload); + vm.startPrank(address(bridge.rootBridgeAdaptor())); + bytes memory predictedPayload = abi.encode(bridge.WITHDRAW_SIG(), token, sender, receiver, txValue); + bridge.onMessageReceive(predictedPayload); vm.stopPrank(); } } From f1c08fd96b291697cf35b91f369f19063a057c04 Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Wed, 31 Jan 2024 22:12:54 +1100 Subject: [PATCH 086/155] Refactor test --- test/fork/root/RootERC20BridgeFlowRate.t.sol | 251 +++++++++++-------- 1 file changed, 150 insertions(+), 101 deletions(-) diff --git a/test/fork/root/RootERC20BridgeFlowRate.t.sol b/test/fork/root/RootERC20BridgeFlowRate.t.sol index 01cba67f..cc8e4a9d 100644 --- a/test/fork/root/RootERC20BridgeFlowRate.t.sol +++ b/test/fork/root/RootERC20BridgeFlowRate.t.sol @@ -4,13 +4,34 @@ pragma solidity 0.8.19; import {Test, console2} from "forge-std/Test.sol"; import {RootERC20BridgeFlowRate} from "../../../src/root/flowrate/RootERC20BridgeFlowRate.sol"; import {IFlowRateWithdrawalQueueErrors} from "../../../src/root/flowrate/FlowRateWithdrawalQueue.sol"; -import {console} from "forge-std/console.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {console} from "forge-std/Console.sol"; import {Utils} from "../../utils.t.sol"; - +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +/** + * @dev This test suite evaluates the flow rate functionality of already deployed RootERC20BridgeFlowRate contracts. + * The tests are executed against forked chains for each deployment (e.g., mainnet, testnet). + * This test suite's objective is not to exhaustively test the flow rate functionality, as this is adequately + * addressed in unit and integration tests. Instead, it aims to ensure that the functionality works as expected + * in each deployed environment. Conducting live E2E tests on the flow rate capability in a mainnet environment + * for each configured token would be complex, expensive, and potentially disruptive. + * Therefore, these tests provide an alternative way to verify that these capabilities function correctly in + * the deployed environment. They can help identify any issues related to deployment parameters or + * specific tokens or deployment conditions that may not have been captured in unit and integration tests. + * + * The test suite is parameterized by the following environment variables: + * - DEPLOYMENTS: comma-separated list of deployments to test (e.g., MAINNET, TESTNET) + * - _RPC_URL: RPC URL for the forked chain for the deployment (e.g., MAINNET_RPC_URL) + * - _BRIDGE_ADDRESS: address of the RootERC20BridgeFlowRate contract for the deployment (e.g., MAINNET_BRIDGE_ADDRESS) + * - _FLOW_RATED_TOKENS: comma-separated list of tokens to test for the deployment (e.g., MAINNET_FLOW_RATED_TOKENS) + * + * NOTE: Foundry's deal() function does not currently support contracts that use the proxy pattern, such as USDC. + * Hence this test is limited to ETH and ERC20 tokens that do not use the proxy pattern. + */ contract RootERC20BridgeFlowRateForkTest is Test, Utils { - address public constant NATIVE_ETH = address(0xeee); + address private constant ETH = address(0xeee); string[] private deployments = vm.envString("DEPLOYMENTS", ","); mapping(string => string) private rpcURLForEnv; @@ -19,6 +40,10 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils { mapping(string => address[]) private tokensForEnv; string private deployment; + + /** + * @dev runs a given test function for each deployment in the DEPLOYMENTS environment variable (e.g. MAINNET, TESTNET) + */ modifier forEachDeployment() { for (uint256 i; i < deployments.length; i++) { deployment = deployments[i]; @@ -37,18 +62,47 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils { } } + /** + * @dev Tests that that exceeding the flow rate parameters for any of the configured tokens in a given deployment, triggers the withdrawal delays. + * This test is run for each deployment environment, and against each configured token. + * The test also checks that flow rate parameters configured are valid. + */ function test_withdrawalQueueEnforcedWhenFlowRateExceeded() public forEachDeployment { - console.log("Testing deployment: ", deployment); - vm.selectFork(forkOfEnv[deployment]); - _verifyWithdrawalQueueEnforcedForAllTokens(bridgeForEnv[deployment], tokensForEnv[deployment]); + RootERC20BridgeFlowRate bridge = bridgeForEnv[deployment]; + address[] memory tokens = tokensForEnv[deployment]; + // preconditions + assertFalse(bridge.withdrawalQueueActivated()); + assertGt(bridge.withdrawalDelay(), 0); + + address receiver1 = createAddress(1); + address receiver2 = createAddress(2); + uint256 snapshotId = vm.snapshot(); + for (uint256 i; i < tokens.length; i++) { + address token = tokens[i]; + // exceed flow rate for any token + _exceedFlowRateParameters(bridge, token, receiver1); + + // Verify that any subsequent withdrawal by other users for other tokens gets queued + address otherToken = tokens[(i + 1) % tokens.length]; + _sendWithdrawalMessage(bridge, otherToken, receiver2, 1 ether); + _verifyWithdrawalWasQueued(bridge, otherToken, receiver2, 1 ether); + _verifyBalance(otherToken, receiver2, 0); + + // roll back state for subsequent test + vm.revertTo(snapshotId); + } } - function test_nonFlowRatedTokenIsQueued() public forEachDeployment { - vm.selectFork(forkOfEnv[deployment]); + /** + * @dev Tests that withdrawal of non-flow rated tokens are queued. + * This test is run for each deployment environment. + */ + function test_nonFlowRatedTokenWithdrawalsAreQueued() public forEachDeployment { RootERC20BridgeFlowRate bridge = bridgeForEnv[deployment]; + address receiver = createAddress(1); // preconditions - assertFalse(bridge.withdrawalQueueActivated()); + assertFalse(bridge.withdrawalQueueActivated(), "Precondition: Withdrawal queue should not activate"); // deploy and map a token ERC20 erc20 = new ERC20("Test Token", "TEST"); @@ -56,57 +110,104 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils { _giveBridgeFunds(address(erc20), address(erc20), 1 ether); // ensure withdrawals for the token, which does not have flow rate configured, is queued - _sendWithdrawalMessage(bridge, address(erc20), address(1), 1 ether); - _verifyWithdrawalWasQueued(bridge, address(erc20), address(1), 1 ether); + _sendWithdrawalMessage(bridge, address(erc20), receiver, 1 ether); + _verifyWithdrawalWasQueued(bridge, address(erc20), receiver, 1 ether); // The queue should only affect the specific token assertFalse(bridge.withdrawalQueueActivated()); } - function test_withdrawalQueueDelayEnforced() public {} + /** + * @dev Tests that for queued withdrawal the mandatory delay is enforced. Also ensures that the withdrawal delay parameters for a deployment are valid. + */ + function test_withdrawalQueueDelayEnforced() public forEachDeployment { + uint256 snapshotId = vm.snapshot(); + address[] memory tokens = tokensForEnv[deployment]; + for (uint256 i; i < tokens.length; i++) { + address token = tokens[i]; + RootERC20BridgeFlowRate bridge = bridgeForEnv[deployment]; + + assertTrue( + bridge.withdrawalDelay() > 0 days && bridge.withdrawalDelay() <= 3 days, + "Precondition: Withdrawal delay appears either too low or too high" + ); - function test_withdrawalQueuedIfTransferIsTooLarge() public { + address receiver = address(12234); + uint256 amount = bridge.largeTransferThresholds(token) + 1; - } + _sendWithdrawalMessage(bridge, token, receiver, amount); + _verifyWithdrawalWasQueued(bridge, token, receiver, amount); - function test_queuedWithdrawalsCanBeFinalised() public {} + // check that early withdrawal attempt fails + vm.expectRevert(); + bridge.finaliseQueuedWithdrawal(receiver, 0); - function _verifyWithdrawalQueueEnforcedForAllTokens(RootERC20BridgeFlowRate bridge, address[] memory tokens) - private - { - // preconditions - assertFalse(bridge.withdrawalQueueActivated()); - assertGt(bridge.withdrawalDelay(), 0); + // check that timely withdrawal succeeds + vm.warp(block.timestamp + bridge.withdrawalDelay()); + bridge.finaliseQueuedWithdrawal(receiver, 0); + _verifyBalance(token, receiver, amount); + + // roll back state for subsequent test + vm.revertTo(snapshotId); + } + } + + function test_withdrawalIsQueuedIfSizeThresholdForTokenExceeded() public forEachDeployment { + address[] memory tokens = tokensForEnv[deployment]; + RootERC20BridgeFlowRate bridge = bridgeForEnv[deployment]; + address receiver = createAddress(1); uint256 snapshotId = vm.snapshot(); for (uint256 i; i < tokens.length; i++) { address token = tokens[i]; - console.log("Testing flow rate for token: ", token); - // exceed flow rate for any token - _exceedFlowRateParameters(bridge, token, address(11)); + uint256 largeAmount = bridge.largeTransferThresholds(token) + 1; - // Verify that any subsequent withdrawal by other users for other tokens gets queued - address otherToken = tokens[(i + 1) % tokens.length]; - _sendWithdrawalMessage(bridge, otherToken, address(12), 1 ether); - _verifyWithdrawalWasQueued(bridge, otherToken, address(12), 1 ether); + // preconditions + _checkIsValidLargeThreshold(token, largeAmount); + + _sendWithdrawalMessage(bridge, token, receiver, largeAmount); + _verifyWithdrawalWasQueued(bridge, token, receiver, largeAmount); + + // The queue should only affect the specific token + assertFalse(bridge.withdrawalQueueActivated()); // roll back state for subsequent test vm.revertTo(snapshotId); } } + // check that the large transfer threshold for the token is at least greater than 1 whole unit of the token + function _checkIsValidLargeThreshold(address token, uint256 amount) private { + if (token == ETH) { + assertGe(amount, 1 ether); + } else { + assertGe(amount, 1 ^ IERC20Metadata(token).decimals()); + } + } + function _exceedFlowRateParameters(RootERC20BridgeFlowRate bridge, address token, address receiver) private { (uint256 capacity, uint256 depth,, uint256 refillRate) = bridge.flowRateBuckets(token); - // check preconditions - assertGt(bridge.largeTransferThresholds(token), 0); - assertGt(capacity, 0); + + uint256 oneUnit = token == ETH ? 1 ether : 1 ^ IERC20Metadata(token).decimals(); + // Check if the thresholds are within reasonable range + assertGt( + bridge.largeTransferThresholds(token), + oneUnit, + "Precondition: Large transfer threshold should be greater than 1 unit of token" + ); + assertLt( + bridge.largeTransferThresholds(token), + capacity, + "Precondition: Large transfer threshold should be less than capacity" + ); + assertGt(capacity, oneUnit); assertGt(refillRate, 0); assertEq(bridge.getPendingWithdrawalsLength(receiver), 0); uint256 txValue = bridge.largeTransferThresholds(token) - 1; uint256 numTxs = depth > txValue ? (depth / txValue) + 2 : 1; - _giveBridgeFunds(address(bridge), token, numTxs * txValue * 2); + _giveBridgeFunds(address(bridge), token, numTxs * txValue); // withdraw until flow rate is exceeded for (uint256 i = 0; i < numTxs; i++) { @@ -116,10 +217,11 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils { assertTrue(bridge.withdrawalQueueActivated()); _verifyWithdrawalWasQueued(bridge, token, receiver, txValue); + _verifyBalance(token, receiver, (numTxs - 1) * txValue); } function _giveBridgeFunds(address bridge, address token, uint256 amount) private { - if (token == NATIVE_ETH) { + if (token == ETH) { deal(bridge, amount); } else { deal(token, bridge, amount); @@ -141,80 +243,27 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils { assertEq(pending[0].amount, txValue, "Unexpected amount"); } - // ensure subsequent withdrawals for any token, by any entity are queued - // _sendWithdrawMessage(NATIVE_ETH, receiver, receiver, txValue); - - /* - Check that the withdrawal queue has been activated - Check that the user has a pending withdrawal for the specified token - */ - // - // assertEq(bridge.getPendingWithdrawalsLength(receiver1), 0); - // - // //send one more tx to receiver2 and make sure it gets queued - // _sendWithdrawMessage(token, receiver2, receiver2, txValue); - // - // //attempt to withdraw for receiver 1 - // uint256 okTime1 = _attemptEarlyWithdraw(token, receiver1, txValue); - // - // //attempt to withdraw for receiver 2 - // uint256 okTime2 = _attemptEarlyWithdraw(token, receiver2, txValue); - // - // //fast forward past withdrawal delay time and withdraw for receiver 1 - // vm.warp(okTime1 + 1); - // bridge.finaliseQueuedWithdrawal(receiver1, 0); - // - // //fast forward past withdrawal delay time and withdraw for receiver 2 - // vm.warp(okTime2 + 1); - // bridge.finaliseQueuedWithdrawal(receiver2, 0); - - // function _attemptEarlyWithdraw(address token, address receiver, uint256 txValue) public returns (uint256 okTime) { - // uint withdrawDelay = bridge.withdrawalDelay(); - // uint256 pendingLength = bridge.getPendingWithdrawalsLength(receiver); - // - // assertEq(pendingLength, 1); - // - // uint256[] memory indices = new uint256[](1); - // indices[0] = 0; - // - // RootERC20BridgeFlowRate.PendingWithdrawal[] memory pending = - // bridge.getPendingWithdrawals(receiver, indices); - // - // assertEq(pending.length, 1); - // assertEq(pending[0].withdrawer, receiver); - // assertEq(pending[0].token, token); - // assertEq(pending[0].amount, txValue); - // uint256 timestamp = pending[0].timestamp; - // - // okTime = timestamp + withdrawDelay; - // - // //deal some eth to pay withdraw gas - // vm.deal(address(this), 1 ether); - // - // //try to process the withdrawal - // vm.expectRevert( - // abi.encodeWithSelector(IFlowRateWithdrawalQueueErrors.WithdrawalRequestTooEarly.selector, timestamp, okTime) - // ); - // bridge.finaliseQueuedWithdrawal(receiver, 0); - // } + function _verifyBalance(address token, address receiver, uint256 expectedAmount) private { + if (token == ETH) { + assertEq(receiver.balance, expectedAmount); + } else { + assertEq(ERC20(token).balanceOf(receiver), expectedAmount); + } + } function _sendWithdrawalMessage(RootERC20BridgeFlowRate bridge, address token, address sender, uint256 txValue) - private + private { - _sendWithdrawMessage(bridge, token, sender, sender, txValue); - } - - function _sendWithdrawMessage( - RootERC20BridgeFlowRate bridge, - address token, - address sender, - address receiver, - uint256 txValue - ) private { //prank as axelar sending a message to the adapter vm.startPrank(address(bridge.rootBridgeAdaptor())); - bytes memory predictedPayload = abi.encode(bridge.WITHDRAW_SIG(), token, sender, receiver, txValue); + bytes memory predictedPayload = abi.encode(bridge.WITHDRAW_SIG(), token, sender, sender, txValue); bridge.onMessageReceive(predictedPayload); vm.stopPrank(); } + + function createAddress(uint256 index) private view returns (address) { + return address( + uint160(uint256(keccak256(abi.encodePacked("root-bridge-fork-test", index, blockhash(block.number))))) + ); + } } From 4276e17683f4762b26b9ddf7c9f5983ae27a79e1 Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Thu, 1 Feb 2024 09:03:03 +1100 Subject: [PATCH 087/155] Upgrade slither-action --- .github/workflows/static-analysis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 58281c32..265dbed1 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: crytic/slither-action@v0.3.0 + - uses: crytic/slither-action@v0.3.1 with: fail-on: high slither-args: --filter-paths "./lib|./test" From e8998cdec3f8570b397fed250738aabea1fa422c Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Thu, 1 Feb 2024 13:31:57 +1100 Subject: [PATCH 088/155] Refactor and improve comments --- test/fork/root/RootERC20BridgeFlowRate.t.sol | 133 +++++++++++-------- 1 file changed, 75 insertions(+), 58 deletions(-) diff --git a/test/fork/root/RootERC20BridgeFlowRate.t.sol b/test/fork/root/RootERC20BridgeFlowRate.t.sol index cc8e4a9d..934383a2 100644 --- a/test/fork/root/RootERC20BridgeFlowRate.t.sol +++ b/test/fork/root/RootERC20BridgeFlowRate.t.sol @@ -29,36 +29,44 @@ import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IER * * NOTE: Foundry's deal() function does not currently support contracts that use the proxy pattern, such as USDC. * Hence this test is limited to ETH and ERC20 tokens that do not use the proxy pattern. + * Details: https://github.com/foundry-rs/forge-std/issues/318#issuecomment-1452463876 */ contract RootERC20BridgeFlowRateForkTest is Test, Utils { address private constant ETH = address(0xeee); string[] private deployments = vm.envString("DEPLOYMENTS", ","); + + // rpc endpoints to the root chain for each environment mapping(string => string) private rpcURLForEnv; - mapping(string => uint256) private forkOfEnv; - mapping(string => RootERC20BridgeFlowRate) private bridgeForEnv; + // the fork id for each environment + mapping(string => uint256) private forkIdForEnv; + // the list of tokens for which flow rate parameters have been configured, in each environment mapping(string => address[]) private tokensForEnv; + // the root bridge address in each environment + mapping(string => RootERC20BridgeFlowRate) private bridgeInEnv; + // the deployment environment currently being tested string private deployment; /** - * @dev runs a given test function for each deployment in the DEPLOYMENTS environment variable (e.g. MAINNET, TESTNET) + * @dev Runs a test function against each deployment listed in the DEPLOYMENTS environment variable (e.g. MAINNET, TESTNET) */ modifier forEachDeployment() { for (uint256 i; i < deployments.length; i++) { deployment = deployments[i]; - vm.selectFork(forkOfEnv[deployment]); + vm.selectFork(forkIdForEnv[deployment]); _; } } function setUp() public { + // extract the rpc endpoint, bridge address, and tokens for each deployment for (uint256 i; i < deployments.length; i++) { string memory dep = deployments[i]; rpcURLForEnv[dep] = vm.envString(string.concat(dep, "_RPC_URL")); - bridgeForEnv[dep] = RootERC20BridgeFlowRate(payable(vm.envAddress(string.concat(dep, "_BRIDGE_ADDRESS")))); + bridgeInEnv[dep] = RootERC20BridgeFlowRate(payable(vm.envAddress(string.concat(dep, "_BRIDGE_ADDRESS")))); tokensForEnv[dep] = vm.envAddress(string.concat(dep, "_FLOW_RATED_TOKENS"), ","); - forkOfEnv[dep] = vm.createFork(rpcURLForEnv[dep]); + forkIdForEnv[dep] = vm.createFork(rpcURLForEnv[dep]); } } @@ -68,25 +76,25 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils { * The test also checks that flow rate parameters configured are valid. */ function test_withdrawalQueueEnforcedWhenFlowRateExceeded() public forEachDeployment { - RootERC20BridgeFlowRate bridge = bridgeForEnv[deployment]; + RootERC20BridgeFlowRate bridge = bridgeInEnv[deployment]; address[] memory tokens = tokensForEnv[deployment]; - // preconditions - assertFalse(bridge.withdrawalQueueActivated()); - assertGt(bridge.withdrawalDelay(), 0); + // precondition: sanity check on the the current state and parameters of the bridge + assertFalse(bridge.withdrawalQueueActivated(), "Precondition: Withdrawal queue should not already be active"); + assertGt(bridge.withdrawalDelay(), 0, "Precondition: Withdrawal delay should be greater than 0"); - address receiver1 = createAddress(1); - address receiver2 = createAddress(2); + address withdrawer1 = createAddress(1); + address withdrawer2 = createAddress(2); uint256 snapshotId = vm.snapshot(); for (uint256 i; i < tokens.length; i++) { address token = tokens[i]; - // exceed flow rate for any token - _exceedFlowRateParameters(bridge, token, receiver1); + // exceed flow rate for token + _exceedFlowRateParameters(bridge, token, withdrawer1); // Verify that any subsequent withdrawal by other users for other tokens gets queued address otherToken = tokens[(i + 1) % tokens.length]; - _sendWithdrawalMessage(bridge, otherToken, receiver2, 1 ether); - _verifyWithdrawalWasQueued(bridge, otherToken, receiver2, 1 ether); - _verifyBalance(otherToken, receiver2, 0); + _sendWithdrawalMessage(bridge, otherToken, withdrawer2, 1 ether); + _verifyWithdrawalWasQueued(bridge, otherToken, withdrawer2, 1 ether); + _verifyBalance(otherToken, withdrawer2, 0); // roll back state for subsequent test vm.revertTo(snapshotId); @@ -94,68 +102,74 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils { } /** - * @dev Tests that withdrawal of non-flow rated tokens are queued. - * This test is run for each deployment environment. + * @dev Tests that withdrawal of non-flow rated tokens get queued. + * This test is run for each deployment environment. */ function test_nonFlowRatedTokenWithdrawalsAreQueued() public forEachDeployment { - RootERC20BridgeFlowRate bridge = bridgeForEnv[deployment]; - address receiver = createAddress(1); + RootERC20BridgeFlowRate bridge = bridgeInEnv[deployment]; + address withdrawer = createAddress(1); // preconditions assertFalse(bridge.withdrawalQueueActivated(), "Precondition: Withdrawal queue should not activate"); // deploy and map a token - ERC20 erc20 = new ERC20("Test Token", "TEST"); - bridge.mapToken{value: 100 gwei}(erc20); - _giveBridgeFunds(address(erc20), address(erc20), 1 ether); + ERC20 nonFlowRatedToken = new ERC20("Test Token", "TEST"); + bridge.mapToken{value: 100 gwei}(nonFlowRatedToken); + _giveBridgeFunds(address(bridge), address(nonFlowRatedToken), 1 ether); // ensure withdrawals for the token, which does not have flow rate configured, is queued - _sendWithdrawalMessage(bridge, address(erc20), receiver, 1 ether); - _verifyWithdrawalWasQueued(bridge, address(erc20), receiver, 1 ether); + _sendWithdrawalMessage(bridge, address(nonFlowRatedToken), withdrawer, 1 ether); + _verifyWithdrawalWasQueued(bridge, address(nonFlowRatedToken), withdrawer, 1 ether); // The queue should only affect the specific token assertFalse(bridge.withdrawalQueueActivated()); } /** - * @dev Tests that for queued withdrawal the mandatory delay is enforced. Also ensures that the withdrawal delay parameters for a deployment are valid. + * @dev Tests that for queued withdrawal the mandatory delay is enforced. + * Also ensures that the withdrawal delay parameters for a deployment are valid. */ function test_withdrawalQueueDelayEnforced() public forEachDeployment { - uint256 snapshotId = vm.snapshot(); + RootERC20BridgeFlowRate bridge = bridgeInEnv[deployment]; + // preconditions: sanity check on the current state and parameters of the bridge + assertGt(bridge.withdrawalDelay(), 0 days, "Precondition: Withdrawal delay should be greater than 0"); + address[] memory tokens = tokensForEnv[deployment]; + uint256 snapshotId = vm.snapshot(); for (uint256 i; i < tokens.length; i++) { address token = tokens[i]; - RootERC20BridgeFlowRate bridge = bridgeForEnv[deployment]; - - assertTrue( - bridge.withdrawalDelay() > 0 days && bridge.withdrawalDelay() <= 3 days, - "Precondition: Withdrawal delay appears either too low or too high" - ); - - address receiver = address(12234); uint256 amount = bridge.largeTransferThresholds(token) + 1; + _giveBridgeFunds(address(bridge), token, amount); + + address withdrawer = createAddress(1); + _sendWithdrawalMessage(bridge, token, withdrawer, amount); + _verifyWithdrawalWasQueued(bridge, token, withdrawer, amount); - _sendWithdrawalMessage(bridge, token, receiver, amount); - _verifyWithdrawalWasQueued(bridge, token, receiver, amount); + console.log("Bridge balance: ", address(bridge).balance); + console.log("Test contract balance: ", address(this).balance); // check that early withdrawal attempt fails vm.expectRevert(); - bridge.finaliseQueuedWithdrawal(receiver, 0); + bridge.finaliseQueuedWithdrawal(withdrawer, 0); // check that timely withdrawal succeeds vm.warp(block.timestamp + bridge.withdrawalDelay()); - bridge.finaliseQueuedWithdrawal(receiver, 0); - _verifyBalance(token, receiver, amount); + bridge.finaliseQueuedWithdrawal(withdrawer, 0); + _verifyBalance(token, withdrawer, amount); // roll back state for subsequent test vm.revertTo(snapshotId); } } + /** + * @dev Tests that withdrawals that exceed the size threshold for a given token get queued. + * Also ensures that the threshold parameters for a token are valid. + */ function test_withdrawalIsQueuedIfSizeThresholdForTokenExceeded() public forEachDeployment { address[] memory tokens = tokensForEnv[deployment]; - RootERC20BridgeFlowRate bridge = bridgeForEnv[deployment]; - address receiver = createAddress(1); + RootERC20BridgeFlowRate bridge = bridgeInEnv[deployment]; + address withdrawer = createAddress(1); uint256 snapshotId = vm.snapshot(); for (uint256 i; i < tokens.length; i++) { @@ -165,8 +179,8 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils { // preconditions _checkIsValidLargeThreshold(token, largeAmount); - _sendWithdrawalMessage(bridge, token, receiver, largeAmount); - _verifyWithdrawalWasQueued(bridge, token, receiver, largeAmount); + _sendWithdrawalMessage(bridge, token, withdrawer, largeAmount); + _verifyWithdrawalWasQueued(bridge, token, withdrawer, largeAmount); // The queue should only affect the specific token assertFalse(bridge.withdrawalQueueActivated()); @@ -176,7 +190,7 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils { } } - // check that the large transfer threshold for the token is at least greater than 1 whole unit of the token + /// @dev check that the large transfer threshold for the token is at least greater than 1 whole unit of the token function _checkIsValidLargeThreshold(address token, uint256 amount) private { if (token == ETH) { assertGe(amount, 1 ether); @@ -185,7 +199,8 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils { } } - function _exceedFlowRateParameters(RootERC20BridgeFlowRate bridge, address token, address receiver) private { + /// @dev sends a number of withdrawal messages to the bridge that exceeds the flow rate parameters for a given token + function _exceedFlowRateParameters(RootERC20BridgeFlowRate bridge, address token, address withdrawer) private { (uint256 capacity, uint256 depth,, uint256 refillRate) = bridge.flowRateBuckets(token); uint256 oneUnit = token == ETH ? 1 ether : 1 ^ IERC20Metadata(token).decimals(); @@ -202,7 +217,7 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils { ); assertGt(capacity, oneUnit); assertGt(refillRate, 0); - assertEq(bridge.getPendingWithdrawalsLength(receiver), 0); + assertEq(bridge.getPendingWithdrawalsLength(withdrawer), 0); uint256 txValue = bridge.largeTransferThresholds(token) - 1; uint256 numTxs = depth > txValue ? (depth / txValue) + 2 : 1; @@ -212,14 +227,15 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils { // withdraw until flow rate is exceeded for (uint256 i = 0; i < numTxs; i++) { (, depth,,) = bridge.flowRateBuckets(token); - _sendWithdrawalMessage(bridge, token, receiver, txValue); + _sendWithdrawalMessage(bridge, token, withdrawer, txValue); } assertTrue(bridge.withdrawalQueueActivated()); - _verifyWithdrawalWasQueued(bridge, token, receiver, txValue); - _verifyBalance(token, receiver, (numTxs - 1) * txValue); + _verifyWithdrawalWasQueued(bridge, token, withdrawer, txValue); + _verifyBalance(token, withdrawer, (numTxs - 1) * txValue); } + /// @dev sends an amount of a given token to the bridge. function _giveBridgeFunds(address bridge, address token, uint256 amount) private { if (token == ETH) { deal(bridge, amount); @@ -228,26 +244,27 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils { } } + /// @dev checks that a withdrawal was queued for a given token and user function _verifyWithdrawalWasQueued( RootERC20BridgeFlowRate bridge, address token, - address receiver, + address withdrawer, uint256 txValue ) private { uint256[] memory indices = new uint256[](1); indices[0] = 0; - assertEq(bridge.getPendingWithdrawalsLength(receiver), 1, "Expected 1 pending withdrawal"); - RootERC20BridgeFlowRate.PendingWithdrawal[] memory pending = bridge.getPendingWithdrawals(receiver, indices); - assertEq(pending[0].withdrawer, receiver, "Unexpected withdrawer"); + assertEq(bridge.getPendingWithdrawalsLength(withdrawer), 1, "Expected 1 pending withdrawal"); + RootERC20BridgeFlowRate.PendingWithdrawal[] memory pending = bridge.getPendingWithdrawals(withdrawer, indices); + assertEq(pending[0].withdrawer, withdrawer, "Unexpected withdrawer"); assertEq(pending[0].token, token, "Unexpected token"); assertEq(pending[0].amount, txValue, "Unexpected amount"); } - function _verifyBalance(address token, address receiver, uint256 expectedAmount) private { + function _verifyBalance(address token, address withdrawer, uint256 expectedAmount) private { if (token == ETH) { - assertEq(receiver.balance, expectedAmount); + assertEq(withdrawer.balance, expectedAmount); } else { - assertEq(ERC20(token).balanceOf(receiver), expectedAmount); + assertEq(ERC20(token).balanceOf(withdrawer), expectedAmount); } } From 2d07c828797e64b007080e328be14be78c94241e Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Thu, 1 Feb 2024 13:40:26 +1100 Subject: [PATCH 089/155] Fix formatting --- test/fork/root/RootERC20BridgeFlowRate.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/fork/root/RootERC20BridgeFlowRate.t.sol b/test/fork/root/RootERC20BridgeFlowRate.t.sol index 934383a2..6c11d7ca 100644 --- a/test/fork/root/RootERC20BridgeFlowRate.t.sol +++ b/test/fork/root/RootERC20BridgeFlowRate.t.sol @@ -35,7 +35,7 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils { address private constant ETH = address(0xeee); string[] private deployments = vm.envString("DEPLOYMENTS", ","); - + // rpc endpoints to the root chain for each environment mapping(string => string) private rpcURLForEnv; // the fork id for each environment From 16b67292751389134001a896c4a534b18f68a354 Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Thu, 1 Feb 2024 14:10:31 +1100 Subject: [PATCH 090/155] Skip fork tests in CI --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 921bdba7..09d7d256 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,5 +30,5 @@ jobs: - name: Run Forge tests run: | - forge test -vvv + forge test --no-match-path "test/fork/**" -vvv id: test From 6e18d514f45da7165cca78b84f544ecf852d8bfa Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Thu, 1 Feb 2024 14:25:21 +1100 Subject: [PATCH 091/155] Remove unused imports --- test/fork/root/RootERC20BridgeFlowRate.t.sol | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test/fork/root/RootERC20BridgeFlowRate.t.sol b/test/fork/root/RootERC20BridgeFlowRate.t.sol index 6c11d7ca..f6280334 100644 --- a/test/fork/root/RootERC20BridgeFlowRate.t.sol +++ b/test/fork/root/RootERC20BridgeFlowRate.t.sol @@ -1,11 +1,10 @@ // SPDX-License-Identifier: Apache 2.0 pragma solidity 0.8.19; -import {Test, console2} from "forge-std/Test.sol"; +import {Test} from "forge-std/Test.sol"; import {RootERC20BridgeFlowRate} from "../../../src/root/flowrate/RootERC20BridgeFlowRate.sol"; import {IFlowRateWithdrawalQueueErrors} from "../../../src/root/flowrate/FlowRateWithdrawalQueue.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import {console} from "forge-std/Console.sol"; import {Utils} from "../../utils.t.sol"; import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; @@ -145,9 +144,6 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils { _sendWithdrawalMessage(bridge, token, withdrawer, amount); _verifyWithdrawalWasQueued(bridge, token, withdrawer, amount); - console.log("Bridge balance: ", address(bridge).balance); - console.log("Test contract balance: ", address(this).balance); - // check that early withdrawal attempt fails vm.expectRevert(); bridge.finaliseQueuedWithdrawal(withdrawer, 0); From 4bfdc8d7722dbf1c97530a021d848247c61d8519 Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Thu, 1 Feb 2024 14:36:12 +1100 Subject: [PATCH 092/155] Make error verification more precise --- test/fork/root/RootERC20BridgeFlowRate.t.sol | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/test/fork/root/RootERC20BridgeFlowRate.t.sol b/test/fork/root/RootERC20BridgeFlowRate.t.sol index f6280334..7cd14fbd 100644 --- a/test/fork/root/RootERC20BridgeFlowRate.t.sol +++ b/test/fork/root/RootERC20BridgeFlowRate.t.sol @@ -142,10 +142,17 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils { address withdrawer = createAddress(1); _sendWithdrawalMessage(bridge, token, withdrawer, amount); - _verifyWithdrawalWasQueued(bridge, token, withdrawer, amount); + RootERC20BridgeFlowRate.PendingWithdrawal memory pending = + _verifyWithdrawalWasQueued(bridge, token, withdrawer, amount); // check that early withdrawal attempt fails - vm.expectRevert(); + vm.expectRevert( + abi.encodeWithSelector( + IFlowRateWithdrawalQueueErrors.WithdrawalRequestTooEarly.selector, + block.timestamp, + pending.timestamp + bridge.withdrawalDelay() + ) + ); bridge.finaliseQueuedWithdrawal(withdrawer, 0); // check that timely withdrawal succeeds @@ -246,7 +253,7 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils { address token, address withdrawer, uint256 txValue - ) private { + ) private returns (RootERC20BridgeFlowRate.PendingWithdrawal memory) { uint256[] memory indices = new uint256[](1); indices[0] = 0; assertEq(bridge.getPendingWithdrawalsLength(withdrawer), 1, "Expected 1 pending withdrawal"); @@ -254,6 +261,7 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils { assertEq(pending[0].withdrawer, withdrawer, "Unexpected withdrawer"); assertEq(pending[0].token, token, "Unexpected token"); assertEq(pending[0].amount, txValue, "Unexpected amount"); + return pending[0]; } function _verifyBalance(address token, address withdrawer, uint256 expectedAmount) private { From 13efe03cb5c671eb709a316aac94d5489ec50537 Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Thu, 1 Feb 2024 14:42:15 +1100 Subject: [PATCH 093/155] Minor edits to comments --- test/fork/root/RootERC20BridgeFlowRate.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/fork/root/RootERC20BridgeFlowRate.t.sol b/test/fork/root/RootERC20BridgeFlowRate.t.sol index 7cd14fbd..cfe14e99 100644 --- a/test/fork/root/RootERC20BridgeFlowRate.t.sol +++ b/test/fork/root/RootERC20BridgeFlowRate.t.sol @@ -10,9 +10,9 @@ import {Utils} from "../../utils.t.sol"; import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; /** - * @dev This test suite evaluates the flow rate functionality of already deployed RootERC20BridgeFlowRate contracts. + * @dev This test suite tests the flow rate control functionality of already deployed RootERC20BridgeFlowRate contracts. * The tests are executed against forked chains for each deployment (e.g., mainnet, testnet). - * This test suite's objective is not to exhaustively test the flow rate functionality, as this is adequately + * The objective of this test suite is not to exhaustively test the flow rate functionality, as this is adequately * addressed in unit and integration tests. Instead, it aims to ensure that the functionality works as expected * in each deployed environment. Conducting live E2E tests on the flow rate capability in a mainnet environment * for each configured token would be complex, expensive, and potentially disruptive. From b381ee9183fa2800db122fc5fd21d620fda40596 Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Thu, 1 Feb 2024 15:28:10 +1100 Subject: [PATCH 094/155] Fix incorrect USDC address on Testnet --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 64274934..8dff631a 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ ABIs for contracts can be obtained from the blockchain explorer links for each c |-------------|-----------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------| | Wrapped ETH | [`0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2`](https://etherscan.io/token/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2) | [`0x7b79995e5f793a07bc00c21412e50ecae098e7f9`](https://sepolia.etherscan.io/address/0x7b79995e5f793a07bc00c21412e50ecae098e7f9) | | IMX | [`0xf57e7e7c23978c3caec3c3548e3d615c346e79ff`](https://etherscan.io/token/0xf57e7e7c23978c3caec3c3548e3d615c346e79ff) | [`0xe2629e08f4125d14e446660028bd98ee60ee69f2`](https://sepolia.etherscan.io/address/0xe2629e08f4125d14e446660028bd98ee60ee69f2) | -| USDC | [`0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48`](https://etherscan.io/token/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) | [`0xe2629e08f4125d14e446660028bd98ee60ee69f2`](https://sepolia.etherscan.io/address/0x40b87d235A5B010a20A241F15797C9debf1ecd01) | +| USDC | [`0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48`](https://etherscan.io/token/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) | [`0x40b87d235A5B010a20A241F15797C9debf1ecd01`](https://sepolia.etherscan.io/address/0x40b87d235A5B010a20A241F15797C9debf1ecd01) | ### Child Chain #### Core Contracts From 7234cdd1bd720cfa2b5a4bf7fbe12cc4b91b7b84 Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Fri, 2 Feb 2024 11:25:12 +1100 Subject: [PATCH 095/155] Relax flow rate parameter range check --- test/fork/root/RootERC20BridgeFlowRate.t.sol | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/fork/root/RootERC20BridgeFlowRate.t.sol b/test/fork/root/RootERC20BridgeFlowRate.t.sol index cfe14e99..1bd4430e 100644 --- a/test/fork/root/RootERC20BridgeFlowRate.t.sol +++ b/test/fork/root/RootERC20BridgeFlowRate.t.sol @@ -206,19 +206,18 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils { function _exceedFlowRateParameters(RootERC20BridgeFlowRate bridge, address token, address withdrawer) private { (uint256 capacity, uint256 depth,, uint256 refillRate) = bridge.flowRateBuckets(token); - uint256 oneUnit = token == ETH ? 1 ether : 1 ^ IERC20Metadata(token).decimals(); // Check if the thresholds are within reasonable range assertGt( bridge.largeTransferThresholds(token), - oneUnit, - "Precondition: Large transfer threshold should be greater than 1 unit of token" + 0, + "Precondition: Large transfer threshold should be greater than zero" ); assertLt( bridge.largeTransferThresholds(token), capacity, "Precondition: Large transfer threshold should be less than capacity" ); - assertGt(capacity, oneUnit); + assertGt(capacity, 0); assertGt(refillRate, 0); assertEq(bridge.getPendingWithdrawalsLength(withdrawer), 0); From 011781b2ab15f9acfc5a75b6e8795ca5e0ed7117 Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Tue, 6 Feb 2024 14:16:11 +1100 Subject: [PATCH 096/155] Add token addresses and update flow rate parameters --- README.md | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 64274934..2b332ebc 100644 --- a/README.md +++ b/README.md @@ -143,26 +143,30 @@ ABIs for contracts can be obtained from the blockchain explorer links for each c | Adaptor Implementation | [`0x1d49c44dc4BbDE68D8D51a9C5732f3a24e48EFA6`](https://explorer.immutable.com/address/0x1d49c44dc4BbDE68D8D51a9C5732f3a24e48EFA6) | [`0xac88a57943b5BBa1ecd931F8494cAd0B7F717590`](https://explorer.testnet.immutable.com/address/0xac88a57943b5BBa1ecd931F8494cAd0B7F717590) | #### Token Addresses -| | Mainnet | Testnet | -|------------------------|-----------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------| -| Wrapped ETH | [`0x52a6c53869ce09a731cd772f245b97a4401d3348`](https://explorer.immutable.com/address/0x52a6c53869ce09a731cd772f245b97a4401d3348) | [`0xe9E96d1aad82562b7588F03f49aD34186f996478`](https://explorer.testnet.immutable.com/address/0xe9E96d1aad82562b7588F03f49aD34186f996478) | -| Wrapped IMX | [`0x3a0c2ba54d6cbd3121f01b96dfd20e99d1696c9d`](https://explorer.immutable.com/address/0x3a0c2ba54d6cbd3121f01b96dfd20e99d1696c9d) | [`0x1CcCa691501174B4A623CeDA58cC8f1a76dc3439`](https://explorer.testnet.immutable.com/address/0x1CcCa691501174B4A623CeDA58cC8f1a76dc3439) | TBA | -| USDC | [`0x6de8aCC0D406837030CE4dd28e7c08C5a96a30d2`](https://explorer.immutable.com/address/0x6de8aCC0D406837030CE4dd28e7c08C5a96a30d2) | [`0x3B2d8A1931736Fc321C24864BceEe981B11c3c57`](https://explorer.testnet.immutable.com/address/0x3B2d8A1931736Fc321C24864BceEe981B11c3c57) | -| USDT | [`0x68bcc7F1190AF20e7b572BCfb431c3Ac10A936Ab`](https://explorer.immutable.com/address/0x68bcc7F1190AF20e7b572BCfb431c3Ac10A936Ab) | TBA | -| Wrapped BTC | [`0x235F9A2Dc29E51cE7D103bcC5Dfb4F5c9c3371De`](https://explorer.immutable.com/address/0x235F9A2Dc29E51cE7D103bcC5Dfb4F5c9c3371De) | TBA | +| | Mainnet | Testnet | +|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------| +| Wrapped ETH | [`0x52a6c53869ce09a731cd772f245b97a4401d3348`](https://explorer.immutable.com/address/0x52a6c53869ce09a731cd772f245b97a4401d3348) | [`0xe9E96d1aad82562b7588F03f49aD34186f996478`](https://explorer.testnet.immutable.com/address/0xe9E96d1aad82562b7588F03f49aD34186f996478) | +| Wrapped IMX | [`0x3a0c2ba54d6cbd3121f01b96dfd20e99d1696c9d`](https://explorer.immutable.com/address/0x3a0c2ba54d6cbd3121f01b96dfd20e99d1696c9d) | [`0x1CcCa691501174B4A623CeDA58cC8f1a76dc3439`](https://explorer.testnet.immutable.com/address/0x1CcCa691501174B4A623CeDA58cC8f1a76dc3439) | TBA | +| USDC | [`0x6de8aCC0D406837030CE4dd28e7c08C5a96a30d2`](https://explorer.immutable.com/address/0x6de8aCC0D406837030CE4dd28e7c08C5a96a30d2) | [`0x3B2d8A1931736Fc321C24864BceEe981B11c3c57`](https://explorer.testnet.immutable.com/address/0x3B2d8A1931736Fc321C24864BceEe981B11c3c57) | +| USDT | [`0x68bcc7F1190AF20e7b572BCfb431c3Ac10A936Ab`](https://explorer.immutable.com/address/0x68bcc7F1190AF20e7b572BCfb431c3Ac10A936Ab) | TBA | +| Wrapped BTC | [`0x235F9A2Dc29E51cE7D103bcC5Dfb4F5c9c3371De`](https://explorer.immutable.com/address/0x235F9A2Dc29E51cE7D103bcC5Dfb4F5c9c3371De) | TBA | +| Gods Unchained (GODS) | [`0xccC8cb5229B0ac8069C51fd58367Fd1e622aFD97`](https://explorer.immutable.com/address/0xccC8cb5229B0ac8069C51fd58367Fd1e622aFD97) | TBA | +| Guild of Guardians (GOG) | [`0x9AB7bb7FdC60f4357ECFef43986818A2A3569c62`](https://explorer.immutable.com/address/0x9AB7bb7FdC60f4357ECFef43986818A2A3569c62) | TBA | ## Flow Rate Parameters Below are the [flow rate](https://github.com/immutable/zkevm-bridge-contracts/blob/documentation/docs/HLA-and-Threat-Model.md#flow-rate-detection) parameters that have been configured on the L1 Mainnet and Testnet deployments. **Mainnet** -| Token | Units | Capacity | Refill Rate | Large Transfer Threshold | -|-----------------------------------------------------------------------------------------------------|:------|----------|-------------|--------------------------| -| ETH | 10^18 | 10.08 | 0.0028 | 5.04 | -| [IMX](https://etherscan.io/token/0xf57e7e7c23978c3caec3c3548e3d615c346e79f) | 10^18 | 10,008 | 2.78 | 5,004 | -| [USDC](https://etherscan.io/token/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) | 10^6 | 20,016 | 5.56 | 10,008 | -| [Gods Unchained (GODS)](https://etherscan.io/address/0xccC8cb5229B0ac8069C51fd58367Fd1e622aFD97) | 10^18 | TBA | TBA | TBA | -| [Guild of Guardians (GOG)](https://etherscan.io/address/0x9AB7bb7FdC60f4357ECFef43986818A2A3569c62) | 10^18 | TBA | TBA | TBA | +| Token | Units | Capacity | Refill Rate | Large Transfer Threshold | +|-----------------------------------------------------------------------------------------------------|:------|------------|-------------|--------------------------| +| ETH | 10^18 | 10.08 | 0.0028 | 5.04 | +| [IMX](https://etherscan.io/token/0xf57e7e7c23978c3caec3c3548e3d615c346e79f) | 10^18 | 10,008 | 2.78 | 5,004 | +| [USDC](https://etherscan.io/token/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) | 10^6 | 20,016 | 5.56 | 10,008 | +| [USDT](https://etherscan.io/token/0xdAC17F958D2ee523a2206206994597C13D831ec7) | 10^6 | 20,000 | 5.56 | 10,000 | +| [wBTC](https://etherscan.io/token/0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599) | 10^8 | 0.470 | 0.000131 | 0.235 | +| [Gods Unchained (GODS)](https://etherscan.io/address/0xccC8cb5229B0ac8069C51fd58367Fd1e622aFD97) | 10^18 | 69,108.50 | 19.20 | 34,554.25 | +| [Guild of Guardians (GOG)](https://etherscan.io/address/0x9AB7bb7FdC60f4357ECFef43986818A2A3569c62) | 10^18 | 119,118.52 | 33.09 | 59,559.26 | **Testnet** @@ -170,9 +174,6 @@ Below are the [flow rate](https://github.com/immutable/zkevm-bridge-contracts/bl |----------------------------------------------------------------------------------------|:------|----------|-------------|--------------------------| | ETH | 10^18 | 10.08 | 0.0028 | 5.04 | | [IMX](https://sepolia.etherscan.io/address/0xe2629e08f4125d14e446660028bd98ee60ee69ff) | 10^18 | 68,976 | 19.16 | 34,488 | -| [USDC](https://etherscan.io/token/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) | 10^6 | TBA | TBA | TBA | -| [Gods Unchained (GODS)]() | 10^18 | TBA | TBA | TBA | -| [Guild of Guardians (GOG)]() | 10^18 | TBA | TBA | TBA | From 00ffbb78e5f2b4e1d7fe8a66068b782afc19bad0 Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Wed, 7 Feb 2024 10:49:35 +1100 Subject: [PATCH 097/155] Refactor test --- test/fork/root/RootERC20BridgeFlowRate.t.sol | 25 ++++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/test/fork/root/RootERC20BridgeFlowRate.t.sol b/test/fork/root/RootERC20BridgeFlowRate.t.sol index 1bd4430e..ca5b409e 100644 --- a/test/fork/root/RootERC20BridgeFlowRate.t.sol +++ b/test/fork/root/RootERC20BridgeFlowRate.t.sol @@ -5,7 +5,6 @@ import {Test} from "forge-std/Test.sol"; import {RootERC20BridgeFlowRate} from "../../../src/root/flowrate/RootERC20BridgeFlowRate.sol"; import {IFlowRateWithdrawalQueueErrors} from "../../../src/root/flowrate/FlowRateWithdrawalQueue.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; - import {Utils} from "../../utils.t.sol"; import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; @@ -221,20 +220,20 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils { assertGt(refillRate, 0); assertEq(bridge.getPendingWithdrawalsLength(withdrawer), 0); - uint256 txValue = bridge.largeTransferThresholds(token) - 1; - uint256 numTxs = depth > txValue ? (depth / txValue) + 2 : 1; - - _giveBridgeFunds(address(bridge), token, numTxs * txValue); - - // withdraw until flow rate is exceeded - for (uint256 i = 0; i < numTxs; i++) { + uint256 largeTransferThreshold = bridge.largeTransferThresholds(token); + uint256 totalWithdrawals; + uint256 amount; + while (depth > 0) { + amount = depth > largeTransferThreshold ? largeTransferThreshold - 1 : depth + 1; + _giveBridgeFunds(address(bridge), token, amount); + _sendWithdrawalMessage(bridge, token, withdrawer, amount); (, depth,,) = bridge.flowRateBuckets(token); - _sendWithdrawalMessage(bridge, token, withdrawer, txValue); + totalWithdrawals += amount; } assertTrue(bridge.withdrawalQueueActivated()); - _verifyWithdrawalWasQueued(bridge, token, withdrawer, txValue); - _verifyBalance(token, withdrawer, (numTxs - 1) * txValue); + _verifyWithdrawalWasQueued(bridge, token, withdrawer, amount); + _verifyBalance(token, withdrawer, totalWithdrawals - amount); } /// @dev sends an amount of a given token to the bridge. @@ -265,9 +264,9 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils { function _verifyBalance(address token, address withdrawer, uint256 expectedAmount) private { if (token == ETH) { - assertEq(withdrawer.balance, expectedAmount); + assertEq(withdrawer.balance, expectedAmount, "Balance does not match expected"); } else { - assertEq(ERC20(token).balanceOf(withdrawer), expectedAmount); + assertEq(ERC20(token).balanceOf(withdrawer), expectedAmount, "Balance does not match expected"); } } From 6ec59a1e1d5ab48de3f17e7aaa076d60738e04be Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Wed, 7 Feb 2024 10:49:50 +1100 Subject: [PATCH 098/155] Add Fork tests to CI --- .github/workflows/test.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 09d7d256..9d072230 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,7 +28,12 @@ jobs: forge build --sizes id: build - - name: Run Forge tests + - name: Run unit and integration tests run: | forge test --no-match-path "test/fork/**" -vvv id: test + + - name: Run Fork Tests + run: | + forge test --match-path "test/fork/**" -vvv + id: test From d58f54f41c0a0341d3fc0f3f7934d90e1b9cd8be Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Wed, 7 Feb 2024 10:58:11 +1100 Subject: [PATCH 099/155] Fix ID conflict in CI job --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9d072230..161f7202 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,9 +31,9 @@ jobs: - name: Run unit and integration tests run: | forge test --no-match-path "test/fork/**" -vvv - id: test + id: unit_integration_test - name: Run Fork Tests run: | forge test --match-path "test/fork/**" -vvv - id: test + id: fork_test From a919ae4853fccb7d806e3581b6876ff5e5911b77 Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Wed, 7 Feb 2024 11:06:06 +1100 Subject: [PATCH 100/155] Increase CI logging for fork tests --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 161f7202..15e33296 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,5 +35,5 @@ jobs: - name: Run Fork Tests run: | - forge test --match-path "test/fork/**" -vvv + forge test --match-path "test/fork/**" -vvvvv id: fork_test From e6363d92b2f9d0ef3df28cf55af8aeac96cc793d Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Wed, 7 Feb 2024 11:25:33 +1100 Subject: [PATCH 101/155] Add Fork test env variables --- .github/workflows/test.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 15e33296..ede69bd0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,9 +7,14 @@ env: jobs: check: + env: + DEPLOYMENTS: MAINNET,TESTNET + MAINNET_BRIDGE_ADDRESS: 0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6 + MAINNET_FLOW_RATED_TOKENS: 0x0000000000000000000000000000000000000Eee,0xF57e7e7C23978C3cAEC3C3548E3D615c346e79fF,0xdAC17F958D2ee523a2206206994597C13D831ec7,0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599,0xccC8cb5229B0ac8069C51fd58367Fd1e622aFD97,0x9AB7bb7FdC60f4357ECFef43986818A2A3569c62 + TESTNET_BRIDGE_ADDRESS: 0x0D3C59c779Fd552C27b23F723E80246c840100F5 + TESTNET_FLOW_RATED_TOKENS: 0x0000000000000000000000000000000000000Eee,0xe2629e08f4125d14e446660028bd98ee60ee69f2 strategy: fail-fast: true - name: Foundry project runs-on: ubuntu-latest steps: From e30f2abc4dae344590067a9cdd151d926f8a3d0e Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Wed, 7 Feb 2024 11:36:23 +1100 Subject: [PATCH 102/155] Update test.yml --- .github/workflows/test.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ede69bd0..7fe5285c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,11 +8,11 @@ env: jobs: check: env: - DEPLOYMENTS: MAINNET,TESTNET - MAINNET_BRIDGE_ADDRESS: 0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6 - MAINNET_FLOW_RATED_TOKENS: 0x0000000000000000000000000000000000000Eee,0xF57e7e7C23978C3cAEC3C3548E3D615c346e79fF,0xdAC17F958D2ee523a2206206994597C13D831ec7,0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599,0xccC8cb5229B0ac8069C51fd58367Fd1e622aFD97,0x9AB7bb7FdC60f4357ECFef43986818A2A3569c62 - TESTNET_BRIDGE_ADDRESS: 0x0D3C59c779Fd552C27b23F723E80246c840100F5 - TESTNET_FLOW_RATED_TOKENS: 0x0000000000000000000000000000000000000Eee,0xe2629e08f4125d14e446660028bd98ee60ee69f2 + DEPLOYMENTS: "MAINNET,TESTNET" + MAINNET_BRIDGE_ADDRESS: "0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6" + MAINNET_FLOW_RATED_TOKENS: "0x0000000000000000000000000000000000000Eee,0xF57e7e7C23978C3cAEC3C3548E3D615c346e79fF,0xdAC17F958D2ee523a2206206994597C13D831ec7,0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599,0xccC8cb5229B0ac8069C51fd58367Fd1e622aFD97,0x9AB7bb7FdC60f4357ECFef43986818A2A3569c62" + TESTNET_BRIDGE_ADDRESS: "0x0D3C59c779Fd552C27b23F723E80246c840100F5" + TESTNET_FLOW_RATED_TOKENS: "0x0000000000000000000000000000000000000Eee,0xe2629e08f4125d14e446660028bd98ee60ee69f2" strategy: fail-fast: true name: Foundry project From 062cffbc4cb65e4e5816d774c57cb2cc97d4549c Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Wed, 7 Feb 2024 11:40:37 +1100 Subject: [PATCH 103/155] Update test.yml --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7fe5285c..33264fb9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,6 +13,8 @@ jobs: MAINNET_FLOW_RATED_TOKENS: "0x0000000000000000000000000000000000000Eee,0xF57e7e7C23978C3cAEC3C3548E3D615c346e79fF,0xdAC17F958D2ee523a2206206994597C13D831ec7,0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599,0xccC8cb5229B0ac8069C51fd58367Fd1e622aFD97,0x9AB7bb7FdC60f4357ECFef43986818A2A3569c62" TESTNET_BRIDGE_ADDRESS: "0x0D3C59c779Fd552C27b23F723E80246c840100F5" TESTNET_FLOW_RATED_TOKENS: "0x0000000000000000000000000000000000000Eee,0xe2629e08f4125d14e446660028bd98ee60ee69f2" + MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }} + TESTNET_RPC_URL: ${{ secrets.TESTNET_RPC_URL }} strategy: fail-fast: true name: Foundry project From bd0b8e6b68e52472fed6936aceab57adb62e77f5 Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Wed, 7 Feb 2024 11:58:48 +1100 Subject: [PATCH 104/155] Update job name for clarity --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 33264fb9..740d9140 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,7 @@ jobs: TESTNET_RPC_URL: ${{ secrets.TESTNET_RPC_URL }} strategy: fail-fast: true - name: Foundry project + name: Build and Test runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 From a6bc3024869b27278884edd8facdc2a028ac15cd Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Wed, 7 Feb 2024 12:13:29 +1100 Subject: [PATCH 105/155] Use configured repository variables in CI --- .github/workflows/test.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 740d9140..eba27536 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,11 +8,11 @@ env: jobs: check: env: - DEPLOYMENTS: "MAINNET,TESTNET" - MAINNET_BRIDGE_ADDRESS: "0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6" - MAINNET_FLOW_RATED_TOKENS: "0x0000000000000000000000000000000000000Eee,0xF57e7e7C23978C3cAEC3C3548E3D615c346e79fF,0xdAC17F958D2ee523a2206206994597C13D831ec7,0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599,0xccC8cb5229B0ac8069C51fd58367Fd1e622aFD97,0x9AB7bb7FdC60f4357ECFef43986818A2A3569c62" - TESTNET_BRIDGE_ADDRESS: "0x0D3C59c779Fd552C27b23F723E80246c840100F5" - TESTNET_FLOW_RATED_TOKENS: "0x0000000000000000000000000000000000000Eee,0xe2629e08f4125d14e446660028bd98ee60ee69f2" + DEPLOYMENTS: ${{ vars.DEPLOYMENTS }} + MAINNET_BRIDGE_ADDRESS: ${{ vars.MAINNET_BRIDGE_ADDRESS }} + MAINNET_FLOW_RATED_TOKENS: ${{ vars.MAINNET_FLOW_RATED_TOKENS }} + TESTNET_BRIDGE_ADDRESS: ${{ vars.TESTNET_BRIDGE_ADDRESS }} + TESTNET_FLOW_RATED_TOKENS: ${{ vars.TESTNET_FLOW_RATED_TOKENS }} MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }} TESTNET_RPC_URL: ${{ secrets.TESTNET_RPC_URL }} strategy: From 4e5d1f1f7bdd7b097fed5c48260f94f246bf3c7f Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Wed, 7 Feb 2024 12:29:28 +1100 Subject: [PATCH 106/155] Update README test instructions --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8dff631a..dc582c08 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ $ forge build ### Test ```shell -$ forge test +$ forge test --no-match-path "test/fork/**" ``` ## Contract Deployment From 833ef70bbdf65de9a42e252a3991bd6c897298da Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Wed, 7 Feb 2024 17:01:06 +1100 Subject: [PATCH 107/155] Add example .env --- .env.example | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..9ef3e283 --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +WITHDRAWAL_DELAY=1 +DEPLOYMENTS=MAINNET,TESTNET +MAINNET_BRIDGE_ADDRESS=0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6 +MAINNET_FLOW_RATED_TOKENS=0x0000000000000000000000000000000000000Eee,0xF57e7e7C23978C3cAEC3C3548E3D615c346e79fF,0xdAC17F958D2ee523a2206206994597C13D831ec7,0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599,0xccC8cb5229B0ac8069C51fd58367Fd1e622aFD97,0x9AB7bb7FdC60f4357ECFef43986818A2A3569c62 +MAINNET_RPC_URL= +TESTNET_RPC_URL= +TESTNET_FLOW_RATED_TOKENS=0x0000000000000000000000000000000000000Eee,0xe2629e08f4125d14e446660028bd98ee60ee69f2 +TESTNET_BRIDGE_ADDRESS=0x0D3C59c779Fd552C27b23F723E80246c840100F5 \ No newline at end of file From 0adf2b311d59c406328356a4a79e8ca820ff8166 Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Wed, 7 Feb 2024 17:12:24 +1100 Subject: [PATCH 108/155] Document details for running fork test --- README.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c2c812d7..4ab5e90a 100644 --- a/README.md +++ b/README.md @@ -82,11 +82,28 @@ $ forge install $ forge build ``` -### Test +### Testing +To run all tests (unit, integration, and fork tests), run the following command: +```shell +$ forge test +``` +This requires setting the relevant environment variables as described in the "Fork Test" section below. + +**Unit and Integration Tests Only** ```shell $ forge test --no-match-path "test/fork/**" ``` +**Fork Tests Only** + +The fork tests run a suite of tests against one or more deployments of the bridge. +To run these tests copy [`.env.example`](.env.example) file to a `.env` file and set the `MAINNET_RPC_URL` and `TESTNET_RPC_URL` environment variables. Set or update any other environment variables as required. +Then run the following command to run the fork tests. + +```shell +$ forge test --match-path "test/fork/**" +``` + ## Contract Deployment ### Local Deployment To set up the contracts on two separate local networks, we need to start running the local networks, then deploy and initialize the contracts. From a63e6cc6a9baeacba77be9ba7db49335b9903c8d Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Thu, 8 Feb 2024 07:59:06 +1000 Subject: [PATCH 109/155] Add fuzz testing --- .../fuzz/child/ChildAxelarBridgeAdaptor.t.sol | 94 +++++ test/fuzz/child/ChildERC20.t.sol | 55 +++ test/fuzz/child/ChildERC20Bridge.t.sol | 322 ++++++++++++++++ test/fuzz/child/WIMX.t.sol | 161 ++++++++ test/fuzz/root/FlowRateDetection.t.sol | 62 +++ test/fuzz/root/RootAxelarBridgeAdaptor.t.sol | 94 +++++ test/fuzz/root/RootERC20Bridge.t.sol | 363 ++++++++++++++++++ test/fuzz/root/RootERC20BridgeFlowRate.t.sol | 69 ++++ 8 files changed, 1220 insertions(+) create mode 100644 test/fuzz/child/ChildAxelarBridgeAdaptor.t.sol create mode 100644 test/fuzz/child/ChildERC20.t.sol create mode 100644 test/fuzz/child/ChildERC20Bridge.t.sol create mode 100644 test/fuzz/child/WIMX.t.sol create mode 100644 test/fuzz/root/FlowRateDetection.t.sol create mode 100644 test/fuzz/root/RootAxelarBridgeAdaptor.t.sol create mode 100644 test/fuzz/root/RootERC20Bridge.t.sol create mode 100644 test/fuzz/root/RootERC20BridgeFlowRate.t.sol diff --git a/test/fuzz/child/ChildAxelarBridgeAdaptor.t.sol b/test/fuzz/child/ChildAxelarBridgeAdaptor.t.sol new file mode 100644 index 00000000..16824341 --- /dev/null +++ b/test/fuzz/child/ChildAxelarBridgeAdaptor.t.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import { + IChildAxelarBridgeAdaptorErrors, + IChildAxelarBridgeAdaptorEvents, + IChildAxelarBridgeAdaptor +} from "../../../src/interfaces/child/IChildAxelarBridgeAdaptor.sol"; +import {ChildAxelarBridgeAdaptor} from "../../../src/child/ChildAxelarBridgeAdaptor.sol"; +import {MockChildERC20Bridge} from "../../mocks/child/MockChildERC20Bridge.sol"; +import {MockChildAxelarGateway} from "../../mocks/child/MockChildAxelarGateway.sol"; +import {MockChildAxelarGasService} from "../../mocks/child/MockChildAxelarGasService.sol"; + +contract ChildAxelarBridgeAdaptorTest is Test, IChildAxelarBridgeAdaptorErrors, IChildAxelarBridgeAdaptorEvents { + string public constant ROOT_CHAIN_NAME = "root"; + string public ROOT_BRIDGE_ADAPTOR = Strings.toHexString(address(4)); + + ChildAxelarBridgeAdaptor public axelarAdaptor; + MockChildERC20Bridge public mockChildERC20Bridge; + MockChildAxelarGateway public mockChildAxelarGateway; + MockChildAxelarGasService public mockChildAxelarGasService; + + function setUp() public { + IChildAxelarBridgeAdaptor.InitializationRoles memory roles = IChildAxelarBridgeAdaptor.InitializationRoles({ + defaultAdmin: address(this), + bridgeManager: address(this), + gasServiceManager: address(this), + targetManager: address(this) + }); + + mockChildERC20Bridge = new MockChildERC20Bridge(); + mockChildAxelarGateway = new MockChildAxelarGateway(); + mockChildAxelarGasService = new MockChildAxelarGasService(); + + axelarAdaptor = new ChildAxelarBridgeAdaptor(address(mockChildAxelarGateway), address(this)); + axelarAdaptor.initialize( + roles, + address(mockChildERC20Bridge), + ROOT_CHAIN_NAME, + ROOT_BRIDGE_ADAPTOR, + address(mockChildAxelarGasService) + ); + } + + function testFuzz_SendMessage(uint256 callValue, bytes calldata payload, address refundRecipient) public { + vm.assume(callValue > 0 && callValue < type(uint256).max); + + vm.startPrank(address(mockChildERC20Bridge)); + vm.deal(address(mockChildERC20Bridge), callValue); + + // Send message called with insufficient balance should revert + vm.expectRevert(); + axelarAdaptor.sendMessage{value: callValue + 1}(payload, refundRecipient); + + // Send message correctly should call gas service and gateway with expected data + vm.expectCall( + address(mockChildAxelarGasService), + callValue, + abi.encodeWithSelector( + mockChildAxelarGasService.payNativeGasForContractCall.selector, + address(axelarAdaptor), + ROOT_CHAIN_NAME, + axelarAdaptor.rootBridgeAdaptor(), + payload, + refundRecipient + ) + ); + vm.expectCall( + address(mockChildAxelarGateway), + abi.encodeWithSelector( + mockChildAxelarGateway.callContract.selector, + ROOT_CHAIN_NAME, + axelarAdaptor.rootBridgeAdaptor(), + payload + ) + ); + axelarAdaptor.sendMessage{value: callValue}(payload, refundRecipient); + + vm.stopPrank(); + } + + function testFuzz_Execute(bytes32 commandId, bytes calldata payload) public { + // Execute should emit event and call bridge. + vm.expectEmit(); + emit AdaptorExecute(ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, payload); + vm.expectCall( + address(mockChildERC20Bridge), + abi.encodeWithSelector(mockChildERC20Bridge.onMessageReceive.selector, payload) + ); + axelarAdaptor.execute(commandId, ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, payload); + } +} diff --git a/test/fuzz/child/ChildERC20.t.sol b/test/fuzz/child/ChildERC20.t.sol new file mode 100644 index 00000000..33faba5d --- /dev/null +++ b/test/fuzz/child/ChildERC20.t.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {ChildERC20} from "../../../src/child/ChildERC20.sol"; + +contract ChildERC20Test is Test { + ChildERC20 public childToken; + + address constant DEFAULT_ROOT_ADDRESS = address(111); + string constant DEFAULT_NAME = "Test ERC20"; + string constant DEFAULT_SYMBOL = "TEST"; + uint8 constant DEFAULT_DECIMALS = 18; + + function setUp() public { + childToken = new ChildERC20(); + childToken.initialize(DEFAULT_ROOT_ADDRESS, DEFAULT_NAME, DEFAULT_SYMBOL, DEFAULT_DECIMALS); + } + + function testFuzz_Mint(address user, uint256 amount) public { + vm.assume(user != address(0)); + + assertEq(childToken.balanceOf(user), 0, "User should not have balance before mint"); + + // Unauthorised mint should revert + vm.prank(user); + vm.expectRevert("ChildERC20: Only bridge can call"); + childToken.mint(user, amount); + + childToken.mint(user, amount); + assertEq(childToken.balanceOf(user), amount, "User should have given amount of balance after mint"); + } + + function testFuzz_Burn(address user, uint256 balance, uint256 burnAmt) public { + vm.assume(user != address(0)); + vm.assume(balance < type(uint256).max); + vm.assume(burnAmt < balance); + + childToken.mint(user, balance); + assertEq(childToken.balanceOf(user), balance, "User should have given amount of balance before burn"); + + // Unauthorised burn should revert + vm.prank(user); + vm.expectRevert("ChildERC20: Only bridge can call"); + childToken.burn(user, burnAmt); + + // Over burn should revert + vm.expectRevert("ERC20: burn amount exceeds balance"); + childToken.burn(user, balance + 1); + + // Burn should decrease balance + childToken.burn(user, burnAmt); + assertEq(childToken.balanceOf(user), balance - burnAmt, "User should have balance - burnAmt after burn"); + } +} diff --git a/test/fuzz/child/ChildERC20Bridge.t.sol b/test/fuzz/child/ChildERC20Bridge.t.sol new file mode 100644 index 00000000..dfddc41f --- /dev/null +++ b/test/fuzz/child/ChildERC20Bridge.t.sol @@ -0,0 +1,322 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import { + ChildERC20Bridge, + IChildERC20Bridge, + IChildERC20BridgeEvents, + IChildERC20BridgeErrors +} from "../../../src/child/ChildERC20Bridge.sol"; +import {ChildERC20} from "../../../src/child/ChildERC20.sol"; +import {MockAdaptor} from "../../mocks/root/MockAdaptor.sol"; +import {WIMX} from "../../../src/child/WIMX.sol"; +import {IChildERC20} from "../../../src/interfaces/child/IChildERC20.sol"; + +contract ChildERC20BridgeTest is Test, IChildERC20BridgeEvents { + bytes32 public constant MAP_TOKEN_SIG = keccak256("MAP_TOKEN"); + bytes32 public constant DEPOSIT_SIG = keccak256("DEPOSIT"); + bytes32 public constant WITHDRAW_SIG = keccak256("WITHDRAW"); + address public constant NATIVE_ETH = address(0xeee); + address public constant NATIVE_IMX = address(0xfff); + + address constant ROOT_IMX_TOKEN = address(0xccc); + ChildERC20 public childTokenTemplate; + WIMX public wIMX; + MockAdaptor public mockAdaptor; + + ChildERC20Bridge bridge; + + receive() external payable {} + + function setUp() public { + IChildERC20Bridge.InitializationRoles memory roles = IChildERC20Bridge.InitializationRoles({ + defaultAdmin: address(this), + pauser: address(this), + unpauser: address(this), + adaptorManager: address(this), + initialDepositor: address(this), + treasuryManager: address(this) + }); + + bridge = new ChildERC20Bridge(address(this)); + + wIMX = new WIMX(); + + mockAdaptor = new MockAdaptor(); + + childTokenTemplate = new ChildERC20(); + childTokenTemplate.initialize(address(123), "Test", "TST", 18); + + bridge.initialize(roles, address(mockAdaptor), address(childTokenTemplate), ROOT_IMX_TOKEN, address(wIMX)); + } + + function testFuzz_MapToken(address rootToken, string memory name, string memory symbol, uint8 decimals) public { + vm.assume(rootToken != address(0) && bytes(name).length != 0 && bytes(symbol).length != 0 && decimals > 0); + vm.assume(rootToken != bridge.NATIVE_ETH() && rootToken != bridge.NATIVE_IMX()); + + // Map token on L1 triggers call on child bridge. + bytes memory data = abi.encode(MAP_TOKEN_SIG, rootToken, name, symbol, decimals); + + address childTokenAddress = Clones.predictDeterministicAddress( + address(childTokenTemplate), keccak256(abi.encodePacked(rootToken)), address(bridge) + ); + vm.expectEmit(address(bridge)); + emit L2TokenMapped(rootToken, childTokenAddress); + + vm.startPrank(address(mockAdaptor)); + bridge.onMessageReceive(data); + + assertEq( + bridge.rootTokenToChildToken(rootToken), + childTokenAddress, + "Child actual token address should match predicated address" + ); + + vm.stopPrank(); + } + + function testFuzz_DepositIMX(address sender, address recipient, uint256 depositAmt) public { + vm.assume(sender != address(0) && recipient != address(0) && depositAmt > 0); + vm.deal(address(bridge), depositAmt); + + assertEq(address(bridge).balance, depositAmt, "Bridge should have depositAmt of IMX"); + assertEq(recipient.balance, 0, "Recipient should have 0 IMX"); + + // Deposit IMX on L1 triggers call on child bridge. + bytes memory data = abi.encode(DEPOSIT_SIG, bridge.rootIMXToken(), sender, recipient, depositAmt); + + vm.expectEmit(address(bridge)); + emit IMXDeposit(bridge.rootIMXToken(), sender, recipient, depositAmt); + vm.startPrank(address(mockAdaptor)); + bridge.onMessageReceive(data); + + assertEq(address(bridge).balance, 0, "Bridge should have 0 IMX"); + assertEq(recipient.balance, depositAmt, "User should have depositAmt of IMX"); + } + + function testFuzz_WithdrawIMX(address user, uint256 balance, uint256 gasAmt, uint256 withdrawAmt) public { + vm.assume(user != address(0)); + vm.assume(balance > 0 && withdrawAmt > 0 && gasAmt > 0); + vm.assume(balance < type(uint256).max - gasAmt); + vm.assume(balance > withdrawAmt && balance - withdrawAmt > gasAmt); + + // Fund user + vm.deal(user, balance); + vm.startPrank(user); + + // Before withdraw + assertEq(user.balance, balance, "User should have given balance of IMX"); + assertEq(address(bridge).balance, 0, "Bridge should have 0 balance of IMX"); + + // Over-withdraw should fail + vm.expectRevert(); + bridge.withdrawIMX{value: gasAmt + balance}(balance); + + // Normal withdraw should succeed + bytes memory predictedPayload = abi.encode(WITHDRAW_SIG, bridge.rootIMXToken(), user, user, withdrawAmt); + vm.expectCall( + address(mockAdaptor), + gasAmt, + abi.encodeWithSelector(mockAdaptor.sendMessage.selector, predictedPayload, user) + ); + vm.expectEmit(address(bridge)); + emit ChildChainNativeIMXWithdraw(bridge.rootIMXToken(), user, user, withdrawAmt); + bridge.withdrawIMX{value: gasAmt + withdrawAmt}(withdrawAmt); + + assertEq( + user.balance, balance - gasAmt - withdrawAmt, "User should have balance - gasAmt - withdrawAmt of balance" + ); + + vm.stopPrank(); + } + + function testFuzz_WithdrawWIMX(address user, uint256 balance, uint256 gasAmt, uint256 withdrawAmt) public { + vm.assume(user != address(0)); + vm.assume(balance > 0 && withdrawAmt > 0 && gasAmt > 0); + vm.assume(balance < type(uint256).max); + vm.assume(balance > withdrawAmt && balance - withdrawAmt > gasAmt); + + // Fund user + vm.deal(user, balance); + vm.startPrank(user); + + // Wrap IMX + wIMX.deposit{value: balance}(); + + vm.deal(user, gasAmt); + + assertEq(user.balance, gasAmt, "User should have gasAmt of balance"); + assertEq(wIMX.balanceOf(user), balance, "User should have given balance"); + + // Withdraw without approval should fail + vm.expectRevert(); + bridge.withdrawWIMX{value: gasAmt}(withdrawAmt); + + // Over-withdraw should fail + wIMX.approve(address(bridge), balance + 1); + vm.expectRevert(); + bridge.withdrawWIMX{value: gasAmt}(balance + 1); + + // Withdraw within balance and allowance should go through + wIMX.approve(address(bridge), withdrawAmt); + + bytes memory predictedPayload = abi.encode(WITHDRAW_SIG, bridge.rootIMXToken(), user, user, withdrawAmt); + vm.expectCall( + address(mockAdaptor), + gasAmt, + abi.encodeWithSelector(mockAdaptor.sendMessage.selector, predictedPayload, user) + ); + + vm.expectEmit(address(bridge)); + emit ChildChainWrappedIMXWithdraw(bridge.rootIMXToken(), user, user, withdrawAmt); + + bridge.withdrawWIMX{value: gasAmt}(withdrawAmt); + + assertEq(user.balance, 0, "User should have 0 balance"); + assertEq(wIMX.balanceOf(user), balance - withdrawAmt, "User should have balance - withdrawAmt of wIMX"); + + vm.stopPrank(); + } + + function testFuzz_DepositETH(address sender, address recipient, uint256 depositAmt) public { + vm.assume(sender != address(0) && recipient != address(0) && depositAmt > 0); + + assertEq(IChildERC20(bridge.childETHToken()).balanceOf(recipient), 0, "Recipient should have 0 ETH"); + + // Deposit ETH on L1 triggers call on child bridge. + bytes memory data = abi.encode(DEPOSIT_SIG, bridge.NATIVE_ETH(), sender, recipient, depositAmt); + + vm.expectEmit(address(bridge)); + emit NativeEthDeposit(bridge.NATIVE_ETH(), bridge.childETHToken(), sender, recipient, depositAmt); + vm.startPrank(address(mockAdaptor)); + bridge.onMessageReceive(data); + + assertEq( + IChildERC20(bridge.childETHToken()).balanceOf(recipient), + depositAmt, + "Recipient should have depositAmt of ETH" + ); + } + + function testFuzz_WithdrawETH(address user, uint256 balance, uint256 gasAmt, uint256 withdrawAmt) public { + vm.assume(user != address(0)); + vm.assume(balance > 0 && withdrawAmt > 0 && gasAmt > 0); + vm.assume(balance < type(uint256).max); + vm.assume(balance > withdrawAmt); + + // Fund user + vm.deal(user, gasAmt); + // Mint token to user + vm.startPrank(address(bridge)); + IChildERC20 childETH = IChildERC20(bridge.childETHToken()); + childETH.mint(user, balance); + + assertEq(user.balance, gasAmt, "User should have given gasAmt of balance"); + assertEq(childETH.balanceOf(user), balance, "User should have given balance of ETH"); + + vm.startPrank(user); + + // Over-withdraw should fail + vm.expectRevert(); + bridge.withdrawETH{value: gasAmt}(balance + 1); + + // Withdraw within balance + bytes memory predictedPayload = abi.encode(WITHDRAW_SIG, bridge.NATIVE_ETH(), user, user, withdrawAmt); + vm.expectCall( + address(mockAdaptor), + gasAmt, + abi.encodeWithSelector(mockAdaptor.sendMessage.selector, predictedPayload, user) + ); + + vm.expectEmit(address(bridge)); + emit ChildChainEthWithdraw(user, user, withdrawAmt); + + bridge.withdrawETH{value: gasAmt}(withdrawAmt); + + assertEq(user.balance, 0, "User should have 0 balance"); + assertEq(childETH.balanceOf(user), balance - withdrawAmt, "User should have balance - withdrawAmt of ETH"); + + vm.stopPrank(); + } + + function testFuzz_DepositERC20(address rootToken, address sender, address recipient, uint256 depositAmt) public { + vm.assume(rootToken != bridge.NATIVE_ETH() && rootToken != bridge.NATIVE_IMX()); + vm.assume(rootToken != address(0) && rootToken != bridge.NATIVE_ETH() && rootToken != bridge.NATIVE_IMX()); + vm.assume(sender != address(0) && recipient != address(0) && depositAmt > 0); + + // Map + bytes memory data = abi.encode(MAP_TOKEN_SIG, rootToken, "Test token", "Test", 18); + vm.startPrank(address(mockAdaptor)); + bridge.onMessageReceive(data); + + address childTokenAddr = bridge.rootTokenToChildToken(rootToken); + IChildERC20 childToken = IChildERC20(childTokenAddr); + + assertEq(childToken.balanceOf(recipient), 0, "Recipient should have 0 token"); + + // Map token on L1 triggers call on child bridge. + data = abi.encode(DEPOSIT_SIG, rootToken, sender, recipient, depositAmt); + + vm.expectEmit(address(bridge)); + emit ChildChainERC20Deposit(rootToken, childTokenAddr, sender, recipient, depositAmt); + vm.startPrank(address(mockAdaptor)); + bridge.onMessageReceive(data); + + assertEq(childToken.balanceOf(recipient), depositAmt, "Recipient should have depositAmt token"); + } + + function testFuzz_WithdrawERC20( + address rootToken, + address user, + uint256 balance, + uint256 gasAmt, + uint256 withdrawAmt + ) public { + vm.assume(rootToken != bridge.NATIVE_ETH() && rootToken != bridge.NATIVE_IMX()); + vm.assume(rootToken != address(0) && user != address(0)); + vm.assume(balance > 0 && withdrawAmt > 0 && gasAmt > 0); + vm.assume(balance < type(uint256).max); + vm.assume(balance > withdrawAmt); + + // Map + bytes memory data = abi.encode(MAP_TOKEN_SIG, rootToken, "Test token", "Test", 18); + vm.startPrank(address(mockAdaptor)); + bridge.onMessageReceive(data); + + address childTokenAddr = bridge.rootTokenToChildToken(rootToken); + + vm.deal(user, gasAmt); + // Mint token to user + vm.startPrank(address(bridge)); + IChildERC20 childToken = IChildERC20(childTokenAddr); + childToken.mint(user, balance); + + assertEq(user.balance, gasAmt, "User should have given gasAmt of balance"); + assertEq(childToken.balanceOf(user), balance, "User should have given balance of token"); + + // Over-withdraw + vm.startPrank(user); + vm.expectRevert(); + bridge.withdraw{value: gasAmt}(childToken, balance + 1); + + // Withdraw within balance + bytes memory predictedPayload = abi.encode(WITHDRAW_SIG, rootToken, user, user, withdrawAmt); + vm.expectCall( + address(mockAdaptor), + gasAmt, + abi.encodeWithSelector(mockAdaptor.sendMessage.selector, predictedPayload, user) + ); + + vm.expectEmit(address(bridge)); + emit ChildChainERC20Withdraw(rootToken, childTokenAddr, user, user, withdrawAmt); + + bridge.withdraw{value: gasAmt}(childToken, withdrawAmt); + + assertEq(user.balance, 0, "User should have 0 balance"); + assertEq(childToken.balanceOf(user), balance - withdrawAmt, "User should have balance - withdrawAmt of token"); + + vm.stopPrank(); + } +} diff --git a/test/fuzz/child/WIMX.t.sol b/test/fuzz/child/WIMX.t.sol new file mode 100644 index 00000000..75866a01 --- /dev/null +++ b/test/fuzz/child/WIMX.t.sol @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {WIMX} from "../../../src/child/WIMX.sol"; + +contract WIMXTest is Test { + WIMX public wIMX; + + event Approval(address indexed src, address indexed guy, uint256 wad); + event Transfer(address indexed src, address indexed dst, uint256 wad); + event Deposit(address indexed dst, uint256 wad); + event Withdrawal(address indexed src, uint256 wad); + + function setUp() public { + wIMX = new WIMX(); + } + + function testFuzz_Deposit(uint256 depositAmt) public { + // Create a user and fund it + address user = address(1234); + vm.deal(user, depositAmt); + vm.startPrank(user); + + // Before deposit + assertEq(user.balance, depositAmt, "User should have given depositAmt of IMX"); + assertEq(wIMX.balanceOf(user), 0, "User should have 0 wIMX"); + assertEq(wIMX.totalSupply(), 0, "Total supply should be 0"); + + if (depositAmt != type(uint256).max) { + vm.expectRevert(); + wIMX.deposit{value: depositAmt + 1}(); + } + + // Deposit + vm.expectEmit(address(wIMX)); + emit Deposit(user, depositAmt); + wIMX.deposit{value: depositAmt}(); + + // After deposit + assertEq(user.balance, 0, "User should have 0 of IMX"); + assertEq(wIMX.balanceOf(user), depositAmt, "User should have given depositAmt wIMX"); + assertEq(wIMX.totalSupply(), depositAmt, "Total supply should be given depositAmt"); + + vm.stopPrank(); + } + + function testFuzz_Withdraw(uint256 depositAmt, uint256 withdrawAmt) public { + vm.assume(depositAmt >= withdrawAmt); + + // Create a user and fund it + address user = address(1234); + vm.deal(user, depositAmt); + vm.startPrank(user); + + // Deposit + wIMX.deposit{value: depositAmt}(); + + assertEq(wIMX.totalSupply(), depositAmt, "Total supply should be given depositAmt"); + + // Withdraw more than depositAmt + if (depositAmt != type(uint256).max) { + vm.expectRevert("Wrapped IMX: Insufficient balance"); + wIMX.withdraw(depositAmt + 1); + } + + vm.expectEmit(address(wIMX)); + emit Withdrawal(user, withdrawAmt); + wIMX.withdraw(withdrawAmt); + + assertEq(user.balance, withdrawAmt, "User should have withdrawAmt of IMX"); + assertEq(wIMX.balanceOf(user), depositAmt - withdrawAmt, "User should have depositAmt - withdrawAmt wIMX"); + assertEq(wIMX.totalSupply(), depositAmt - withdrawAmt, "Total supply should be depositAmt - withdrawAmt"); + + vm.stopPrank(); + } + + function testFuzz_Approve(address user, address approved, uint256 approvalAmt) public { + vm.startPrank(user); + + // Approve + vm.expectEmit(address(wIMX)); + emit Approval(user, approved, approvalAmt); + wIMX.approve(approved, approvalAmt); + + assertEq(wIMX.allowance(user, approved), approvalAmt, "Allowance should be given approvalAmt"); + + vm.stopPrank(); + } + + function testFuzz_Transfer(address from, address to, uint256 depositAmt, uint256 transferAmt) public { + vm.assume(depositAmt >= transferAmt); + vm.assume(from != to); + + // Fund sender + vm.deal(from, depositAmt); + vm.startPrank(from); + + // Deposit + wIMX.deposit{value: depositAmt}(); + + // Transfer out of balance + if (depositAmt != type(uint256).max) { + vm.expectRevert("Wrapped IMX: Insufficient balance"); + wIMX.transfer(to, depositAmt + 1); + } + + vm.expectEmit(address(wIMX)); + emit Transfer(from, to, transferAmt); + wIMX.transfer(to, transferAmt); + + assertEq(wIMX.balanceOf(from), depositAmt - transferAmt, "Sender should have depositAmt - transferAmt of IMX"); + assertEq(wIMX.balanceOf(to), transferAmt, "User should have transferAmt wIMX"); + assertEq(wIMX.totalSupply(), depositAmt, "Total supply should be depositAmt"); + + vm.stopPrank(); + } + + function testFuzz_TransferFrom(address from, address to, address operator, uint256 depositAmt, uint256 transferAmt) + public + { + vm.assume(depositAmt != type(uint256).max && depositAmt >= transferAmt && transferAmt > 1); + vm.assume(from != to && from != operator && to != operator); + + // Fund sender + vm.deal(from, depositAmt); + vm.startPrank(from); + + // Deposit + wIMX.deposit{value: depositAmt}(); + + // Insufficient allowance + wIMX.approve(operator, transferAmt - 1); + + // Transfer + vm.startPrank(operator); + + vm.expectRevert("Wrapped IMX: Insufficient allowance"); + wIMX.transferFrom(from, to, transferAmt); + + // Approve sufficient amount + vm.startPrank(from); + wIMX.approve(operator, depositAmt); + + vm.startPrank(operator); + vm.expectEmit(address(wIMX)); + emit Transfer(from, to, transferAmt); + wIMX.transferFrom(from, to, transferAmt); + + assertEq(wIMX.balanceOf(from), depositAmt - transferAmt, "Sender should have depositAmt - transferAmt of IMX"); + assertEq(wIMX.balanceOf(to), transferAmt, "User should have transferAmt wIMX"); + assertEq(wIMX.totalSupply(), depositAmt, "Total supply should be depositAmt"); + assertEq( + wIMX.allowance(from, operator), + depositAmt - transferAmt, + "Allowance should have depositAmt - transferAmt of IMX" + ); + + vm.stopPrank(); + } +} diff --git a/test/fuzz/root/FlowRateDetection.t.sol b/test/fuzz/root/FlowRateDetection.t.sol new file mode 100644 index 00000000..dffee508 --- /dev/null +++ b/test/fuzz/root/FlowRateDetection.t.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import "forge-std/Test.sol"; + +import {FlowRateDetection} from "../../../src/root/flowrate/FlowRateDetection.sol"; + +contract FlowRateDetectionTest is Test, FlowRateDetection { + function activateWithdrawalQueue() external { + _activateWithdrawalQueue(); + } + + function deactivateWithdrawalQueue() external { + _deactivateWithdrawalQueue(); + } + + function setFlowRateThreshold(address token, uint256 capacity, uint256 refillRate) external { + _setFlowRateThreshold(token, capacity, refillRate); + } + + function updateFlowRateBucket(address token, uint256 amount) external returns (bool delayWithdrawal) { + return _updateFlowRateBucket(token, amount); + } + + function setUp() public {} + + function testFuzz_SetFlowRateThreshold(address token, uint256 capacity, uint256 refillRate) public { + vm.assume(token != address(0)); + vm.assume(capacity > 0); + vm.assume(refillRate > 0); + + this.setFlowRateThreshold(token, capacity, refillRate); + (uint256 currentCapacity,,, uint256 currentRefillRate) = this.flowRateBuckets(token); + assertEq(currentCapacity, capacity, "Capacity should match"); + assertEq(currentRefillRate, refillRate, "Refill rate should match"); + } + + function testFuzz_RateLimit(address token, uint256 capacity, uint256 refillRate) public { + vm.assume(token != address(0)); + vm.assume(refillRate > 0 && refillRate < type(uint256).max / 86400); + vm.assume(capacity > 86400 * refillRate && capacity < type(uint256).max / 86400); + + this.setFlowRateThreshold(token, capacity, refillRate); + (, uint256 depth,,) = this.flowRateBuckets(token); + assertEq(depth, capacity, "Depth should match capacity"); + assertFalse(this.withdrawalQueueActivated(), "Withdrawal queue should not activate"); + + // Use half capacity + bool delay = this.updateFlowRateBucket(token, capacity / 2); + assertFalse(delay, "Should not be delayed"); + (, depth,,) = this.flowRateBuckets(token); + assertEq(depth, capacity - capacity / 2, "Depth should match half capacity"); + assertFalse(this.withdrawalQueueActivated(), "Withdrawal queue should not activate"); + + // Use the other half capacity + delay = this.updateFlowRateBucket(token, capacity / 2 + 2); + assertFalse(delay, "Should not be delayed"); + (, depth,,) = this.flowRateBuckets(token); + assertEq(depth, 0, "Depth should be 0"); + assertTrue(this.withdrawalQueueActivated(), "Withdrawal queue should activate"); + } +} diff --git a/test/fuzz/root/RootAxelarBridgeAdaptor.t.sol b/test/fuzz/root/RootAxelarBridgeAdaptor.t.sol new file mode 100644 index 00000000..7af7e469 --- /dev/null +++ b/test/fuzz/root/RootAxelarBridgeAdaptor.t.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import { + RootAxelarBridgeAdaptor, + IRootAxelarBridgeAdaptorEvents, + IRootAxelarBridgeAdaptorErrors, + IRootAxelarBridgeAdaptor +} from "../../../src/root/RootAxelarBridgeAdaptor.sol"; +import {MockAxelarGateway} from "../../mocks/root/MockAxelarGateway.sol"; +import {MockAxelarGasService} from "../../mocks/root/MockAxelarGasService.sol"; +import {StubRootBridge} from "../../mocks/root/StubRootBridge.sol"; +import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; + +contract RootAxelarBridgeAdaptorTest is Test, IRootAxelarBridgeAdaptorErrors, IRootAxelarBridgeAdaptorEvents { + string public constant CHILD_CHAIN_NAME = "child"; + string public CHILD_BRIDGE_ADAPTOR = Strings.toHexString(address(4)); + + RootAxelarBridgeAdaptor public axelarAdaptor; + MockAxelarGateway public mockRootAxelarGateway; + MockAxelarGasService public mockRootAxelarGasService; + StubRootBridge public mockRootERC20Bridge; + + function setUp() public { + IRootAxelarBridgeAdaptor.InitializationRoles memory roles = IRootAxelarBridgeAdaptor.InitializationRoles({ + defaultAdmin: address(this), + bridgeManager: address(this), + gasServiceManager: address(this), + targetManager: address(this) + }); + + mockRootERC20Bridge = new StubRootBridge(); + mockRootAxelarGateway = new MockAxelarGateway(); + mockRootAxelarGasService = new MockAxelarGasService(); + + axelarAdaptor = new RootAxelarBridgeAdaptor(address(mockRootAxelarGateway), address(this)); + axelarAdaptor.initialize( + roles, + address(mockRootERC20Bridge), + CHILD_CHAIN_NAME, + CHILD_BRIDGE_ADAPTOR, + address(mockRootAxelarGasService) + ); + } + + function testFuzz_SendMessage(uint256 callValue, bytes calldata payload, address refundRecipient) public { + vm.assume(callValue > 0 && callValue < type(uint256).max); + + vm.startPrank(address(mockRootERC20Bridge)); + vm.deal(address(mockRootERC20Bridge), callValue); + + // Send message called with insufficient balance should revert + vm.expectRevert(); + axelarAdaptor.sendMessage{value: callValue + 1}(payload, refundRecipient); + + // Send message correctly should call gas service and gateway with expected data + vm.expectCall( + address(mockRootAxelarGasService), + callValue, + abi.encodeWithSelector( + mockRootAxelarGasService.payNativeGasForContractCall.selector, + address(axelarAdaptor), + CHILD_CHAIN_NAME, + axelarAdaptor.childBridgeAdaptor(), + payload, + refundRecipient + ) + ); + vm.expectCall( + address(mockRootAxelarGateway), + abi.encodeWithSelector( + mockRootAxelarGateway.callContract.selector, + CHILD_CHAIN_NAME, + axelarAdaptor.childBridgeAdaptor(), + payload + ) + ); + axelarAdaptor.sendMessage{value: callValue}(payload, refundRecipient); + + vm.stopPrank(); + } + + function testFuzz_Execute(bytes32 commandId, bytes calldata payload) public { + // Execute should emit event and call bridge. + vm.expectEmit(); + emit AdaptorExecute(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR, payload); + vm.expectCall( + address(mockRootERC20Bridge), abi.encodeWithSelector(mockRootERC20Bridge.onMessageReceive.selector, payload) + ); + axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR, payload); + } +} diff --git a/test/fuzz/root/RootERC20Bridge.t.sol b/test/fuzz/root/RootERC20Bridge.t.sol new file mode 100644 index 00000000..8c23662b --- /dev/null +++ b/test/fuzz/root/RootERC20Bridge.t.sol @@ -0,0 +1,363 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import { + RootERC20Bridge, + IRootERC20BridgeEvents, + IERC20Metadata, + IRootERC20BridgeErrors, + IRootERC20Bridge +} from "../../../src/root/RootERC20Bridge.sol"; +import {ChildERC20} from "../../../src/child/ChildERC20.sol"; +import {WETH} from "../../../src/lib/WETH.sol"; +import {MockAdaptor} from "../../mocks/root/MockAdaptor.sol"; + +contract RootERC20BridgeTest is Test, IRootERC20BridgeEvents { + bytes32 public constant MAP_TOKEN_SIG = keccak256("MAP_TOKEN"); + bytes32 public constant DEPOSIT_SIG = keccak256("DEPOSIT"); + bytes32 public constant WITHDRAW_SIG = keccak256("WITHDRAW"); + address constant CHILD_BRIDGE = address(3); + uint256 constant IMX_DEPOSITS_LIMIT = 10000 ether; + + ChildERC20 childTokenTemplate; + ChildERC20 imxToken; + WETH wETH; + MockAdaptor mockAdaptor; + RootERC20Bridge bridge; + + function setUp() public { + mockAdaptor = new MockAdaptor(); + + childTokenTemplate = new ChildERC20(); + childTokenTemplate.initialize(address(123), "Test", "TST", 18); + + imxToken = new ChildERC20(); + imxToken.initialize(address(234), "IMX Token", "IMX", 18); + + wETH = new WETH(); + + IRootERC20Bridge.InitializationRoles memory roles = IRootERC20Bridge.InitializationRoles({ + defaultAdmin: address(this), + pauser: address(this), + unpauser: address(this), + variableManager: address(this), + adaptorManager: address(this) + }); + + bridge = new RootERC20Bridge(address(this)); + bridge.initialize( + roles, + address(mockAdaptor), + CHILD_BRIDGE, + address(childTokenTemplate), + address(imxToken), + address(wETH), + IMX_DEPOSITS_LIMIT + ); + } + + function testFuzz_MapToken(address user, uint256 gasAmt, string memory name, string memory symbol, uint8 decimals) + public + { + vm.assume(user != address(0)); + vm.assume(gasAmt > 0); + vm.assume(bytes(name).length != 0 && bytes(symbol).length != 0 && decimals > 0); + + ChildERC20 rootToken = new ChildERC20(); + rootToken.initialize(address(123), name, symbol, decimals); + + // Map token on L1 triggers call to child bridge. + vm.deal(user, gasAmt); + vm.startPrank(user); + + address childTokenAddress = Clones.predictDeterministicAddress( + address(childTokenTemplate), keccak256(abi.encodePacked(rootToken)), CHILD_BRIDGE + ); + + bytes memory predictedPayload = abi.encode(MAP_TOKEN_SIG, address(rootToken), name, symbol, decimals); + vm.expectCall( + address(mockAdaptor), + gasAmt, + abi.encodeWithSelector(mockAdaptor.sendMessage.selector, predictedPayload, user) + ); + vm.expectEmit(address(bridge)); + emit L1TokenMapped(address(rootToken), childTokenAddress); + bridge.mapToken{value: gasAmt}(IERC20Metadata(address(rootToken))); + + vm.stopPrank(); + } + + function testFuzz_DepositIMX(address sender, address recipient, uint256 balance, uint256 gasAmt, uint256 depositAmt) + public + { + vm.assume(sender != address(0) && recipient != address(0)); + vm.assume(balance > 0 && depositAmt > 0 && gasAmt > 0); + vm.assume(balance > depositAmt && balance < type(uint256).max); + vm.assume(depositAmt <= IMX_DEPOSITS_LIMIT); + + // Fund user + vm.deal(sender, gasAmt); + imxToken.mint(sender, balance); + vm.startPrank(sender); + + // Before deposit + assertEq(sender.balance, gasAmt, "Sender should have gasAmt of balance"); + assertEq(imxToken.balanceOf(sender), balance, "Sender should have given balance of IMX"); + + // Deposit without approval should fail + vm.expectRevert(); + bridge.depositTo{value: gasAmt}(IERC20Metadata(address(imxToken)), recipient, depositAmt); + + // Deposit out of balance should fail + imxToken.approve(address(bridge), balance + 1); + vm.expectRevert(); + bridge.depositTo{value: gasAmt}(IERC20Metadata(address(imxToken)), recipient, balance + 1); + + // Deposit within balance and allowance should go through + imxToken.approve(address(bridge), depositAmt); + + bytes memory predictedPayload = abi.encode(DEPOSIT_SIG, address(imxToken), sender, recipient, depositAmt); + vm.expectCall( + address(mockAdaptor), + gasAmt, + abi.encodeWithSelector(mockAdaptor.sendMessage.selector, predictedPayload, sender) + ); + vm.expectEmit(address(bridge)); + emit IMXDeposit(address(imxToken), sender, recipient, depositAmt); + bridge.depositTo{value: gasAmt}(IERC20Metadata(address(imxToken)), recipient, depositAmt); + + // After deposit + assertEq(sender.balance, 0, "Sender should have 0 balance"); + assertEq(imxToken.balanceOf(sender), balance - depositAmt, "Sender should have balance - depositAmt of IMX"); + + assertEq(address(imxToken), bridge.rootIMXToken()); + + vm.stopPrank(); + } + + function testFuzz_WithdrawIMX(address sender, address recipient, uint256 withdrawAmt) public { + vm.assume(sender != address(0) && recipient != address(0) && withdrawAmt > 0); + + imxToken.mint(address(bridge), withdrawAmt); + + assertEq(imxToken.balanceOf(address(bridge)), withdrawAmt, "Bridge should have withdrawAmt balance"); + assertEq(imxToken.balanceOf(recipient), 0, "Recipient should have 0 balance"); + + bytes memory data = abi.encode(WITHDRAW_SIG, address(imxToken), sender, recipient, withdrawAmt); + + vm.expectEmit(address(bridge)); + emit RootChainERC20Withdraw(address(imxToken), bridge.NATIVE_IMX(), sender, recipient, withdrawAmt); + + vm.startPrank(address(mockAdaptor)); + + bridge.onMessageReceive(data); + + assertEq(imxToken.balanceOf(address(bridge)), 0, "Bridge should have 0 balance"); + assertEq(imxToken.balanceOf(recipient), withdrawAmt, "Recipient should have withdrawAmt balance"); + + vm.stopPrank(); + } + + function testFuzz_DepositETH(address sender, address recipient, uint256 balance, uint256 gasAmt, uint256 depositAmt) + public + { + vm.assume(sender != address(0) && recipient != address(0)); + vm.assume(balance > 0 && depositAmt > 0 && gasAmt > 0); + vm.assume(balance > depositAmt && balance < type(uint256).max - gasAmt && balance - depositAmt > gasAmt); + + // Fund user + vm.deal(sender, balance); + vm.startPrank(sender); + + // Before deposit + assertEq(sender.balance, balance, "Sender should have given balance"); + assertEq(address(bridge).balance, 0, "Bridge should have 0 balance"); + + // Deposit out of balance should fail + vm.expectRevert(); + bridge.depositToETH{value: balance + gasAmt + 1}(recipient, balance + 1); + + // Deposit within balance should go through + bytes memory predictedPayload = abi.encode(DEPOSIT_SIG, bridge.NATIVE_ETH(), sender, recipient, depositAmt); + vm.expectCall( + address(mockAdaptor), + gasAmt, + abi.encodeWithSelector(mockAdaptor.sendMessage.selector, predictedPayload, sender) + ); + vm.expectEmit(address(bridge)); + emit NativeEthDeposit(bridge.NATIVE_ETH(), bridge.childETHToken(), sender, recipient, depositAmt); + bridge.depositToETH{value: depositAmt + gasAmt}(recipient, depositAmt); + + // Before deposit + assertEq(sender.balance, balance - gasAmt - depositAmt, "Sender should have balance - gasAmt - depositAmt"); + assertEq(address(bridge).balance, depositAmt, "Bridge should have depositAmt"); + + vm.stopPrank(); + } + + function testFuzz_DepositWETH( + address sender, + address recipient, + uint256 balance, + uint256 gasAmt, + uint256 depositAmt + ) public { + vm.assume(sender != address(0) && recipient != address(0)); + vm.assume(balance > 0 && depositAmt > 0 && gasAmt > 0); + vm.assume(balance > depositAmt && balance < type(uint256).max - gasAmt && balance - depositAmt > gasAmt); + + // Fund user + vm.deal(sender, balance); + vm.startPrank(sender); + wETH.deposit{value: balance}(); + + vm.deal(sender, gasAmt); + + // Before deposit + assertEq(sender.balance, gasAmt, "Sender should have gasAmt"); + assertEq(wETH.balanceOf(sender), balance, "Sender should have given balance of WETH"); + assertEq(wETH.balanceOf(address(bridge)), 0, "Bridge should have 0 balance of WETH"); + + // Deposit without approval should fail + vm.expectRevert(); + bridge.depositTo{value: gasAmt}(IERC20Metadata(address(wETH)), recipient, depositAmt); + + // Deposit out of balance should fail + wETH.approve(address(bridge), balance + 1); + vm.expectRevert(); + bridge.depositTo{value: gasAmt}(IERC20Metadata(address(wETH)), recipient, balance + 1); + + // Deposit within balance and allowance should go through + wETH.approve(address(bridge), depositAmt); + bytes memory predictedPayload = abi.encode(DEPOSIT_SIG, bridge.NATIVE_ETH(), sender, recipient, depositAmt); + vm.expectCall( + address(mockAdaptor), + gasAmt, + abi.encodeWithSelector(mockAdaptor.sendMessage.selector, predictedPayload, sender) + ); + vm.expectEmit(address(bridge)); + emit WETHDeposit(address(wETH), bridge.childETHToken(), sender, recipient, depositAmt); + bridge.depositTo{value: gasAmt}(IERC20Metadata(address(wETH)), recipient, depositAmt); + + // After deposit + assertEq(sender.balance, 0, "Sender should have 0"); + assertEq(wETH.balanceOf(sender), balance - depositAmt, "Sender should have balance - depositAmt of WETH"); + assertEq(address(bridge).balance, depositAmt, "Bridge should have depositAmt of ETH"); + + vm.stopPrank(); + } + + function testFuzz_WithdrawETH(address sender, address recipient, uint256 withdrawAmt) public { + vm.assume(sender != address(0) && recipient != address(0) && withdrawAmt > 0); + + vm.deal(address(bridge), withdrawAmt); + + assertEq(address(bridge).balance, withdrawAmt, "Bridge should have withdrawAmt balance"); + assertEq(recipient.balance, 0, "Recipient should have 0 balance"); + + bytes memory data = abi.encode(WITHDRAW_SIG, bridge.NATIVE_ETH(), sender, recipient, withdrawAmt); + + vm.expectEmit(address(bridge)); + emit RootChainETHWithdraw(bridge.NATIVE_ETH(), bridge.childETHToken(), sender, recipient, withdrawAmt); + + vm.startPrank(address(mockAdaptor)); + + bridge.onMessageReceive(data); + + assertEq(address(bridge).balance, 0, "Bridge should have 0 balance"); + assertEq(recipient.balance, withdrawAmt, "Recipient should have withdrawAmt balance"); + + vm.stopPrank(); + } + + function testFuzz_DepositERC20( + address sender, + address recipient, + uint256 balance, + uint256 gasAmt, + uint256 depositAmt + ) public { + vm.assume(sender != address(0) && recipient != address(0)); + vm.assume(balance > 0 && depositAmt > 0 && gasAmt > 0); + vm.assume(balance > depositAmt && balance < type(uint256).max); + vm.assume(gasAmt < 100); + + // Map token + ChildERC20 rootToken = new ChildERC20(); + rootToken.initialize(address(123), "Test token", "TEST", 18); + + vm.deal(sender, gasAmt); + rootToken.mint(sender, balance); + vm.startPrank(sender); + + bridge.mapToken{value: gasAmt}(IERC20Metadata(address(rootToken))); + + vm.deal(sender, gasAmt); + + // Before deposit + assertEq(rootToken.balanceOf(sender), balance, "Sender should have given balance of ERC20"); + assertEq(rootToken.balanceOf(address(bridge)), 0, "Bridge should have 0 balance of ERC20"); + + // Deposit without approval should fail + vm.expectRevert(); + bridge.depositTo{value: gasAmt}(IERC20Metadata(address(rootToken)), recipient, depositAmt); + + // Deposit out of balance should fail + rootToken.approve(address(bridge), balance + 1); + vm.expectRevert(); + bridge.depositTo{value: gasAmt}(IERC20Metadata(address(rootToken)), recipient, balance + 1); + + // Deposit within balance and allowance should go through + rootToken.approve(address(bridge), depositAmt); + bytes memory predictedPayload = abi.encode(DEPOSIT_SIG, address(rootToken), sender, recipient, depositAmt); + vm.expectCall( + address(mockAdaptor), + gasAmt, + abi.encodeWithSelector(mockAdaptor.sendMessage.selector, predictedPayload, sender) + ); + vm.expectEmit(address(bridge)); + emit ChildChainERC20Deposit( + address(rootToken), bridge.rootTokenToChildToken(address(rootToken)), sender, recipient, depositAmt + ); + bridge.depositTo{value: gasAmt}(IERC20Metadata(address(rootToken)), recipient, depositAmt); + + // After deposit + assertEq(rootToken.balanceOf(sender), balance - depositAmt, "Sender should have balance - depositAmt of ERC20"); + assertEq(rootToken.balanceOf(address(bridge)), depositAmt, "Bridge should have depositAmt of ERC20"); + + vm.stopPrank(); + } + + function testFuzz_WithdrawERC20(address sender, address recipient, uint256 withdrawAmt) public { + vm.assume(sender != address(0) && recipient != address(0) && withdrawAmt > 0); + + // Map token + ChildERC20 rootToken = new ChildERC20(); + rootToken.initialize(address(123), "Test token", "TEST", 18); + rootToken.mint(address(bridge), withdrawAmt); + vm.deal(sender, 100); + vm.startPrank(sender); + bridge.mapToken{value: 100}(IERC20Metadata(address(rootToken))); + + address childTokenAddr = bridge.rootTokenToChildToken(address(rootToken)); + + assertEq(rootToken.balanceOf(recipient), 0, "Recipient should have 0 balance of ERC20"); + assertEq(rootToken.balanceOf(address(bridge)), withdrawAmt, "Bridge should have withdrawAmt of ERC20"); + + bytes memory data = abi.encode(WITHDRAW_SIG, address(rootToken), sender, recipient, withdrawAmt); + + vm.expectEmit(address(bridge)); + emit RootChainERC20Withdraw(address(rootToken), childTokenAddr, sender, recipient, withdrawAmt); + + vm.startPrank(address(mockAdaptor)); + + bridge.onMessageReceive(data); + + assertEq(rootToken.balanceOf(recipient), withdrawAmt, "Recipient should have withdrawAmt of ERC20"); + assertEq(rootToken.balanceOf(address(bridge)), 0, "Bridge should have 0 of ERC20"); + + vm.stopPrank(); + } +} diff --git a/test/fuzz/root/RootERC20BridgeFlowRate.t.sol b/test/fuzz/root/RootERC20BridgeFlowRate.t.sol new file mode 100644 index 00000000..051beac7 --- /dev/null +++ b/test/fuzz/root/RootERC20BridgeFlowRate.t.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import { + IRootERC20BridgeEvents, + IERC20Metadata, + IRootERC20BridgeErrors, + IRootERC20Bridge +} from "../../../src/root/RootERC20Bridge.sol"; +import {RootERC20BridgeFlowRate} from "../../../src/root/flowrate/RootERC20BridgeFlowRate.sol"; +import {ChildERC20} from "../../../src/child/ChildERC20.sol"; +import {WETH} from "../../../src/lib/WETH.sol"; +import {MockAdaptor} from "../../mocks/root/MockAdaptor.sol"; + +contract RootERC20BridgeFlowRateTest is Test { + bytes32 public constant MAP_TOKEN_SIG = keccak256("MAP_TOKEN"); + bytes32 public constant DEPOSIT_SIG = keccak256("DEPOSIT"); + bytes32 public constant WITHDRAW_SIG = keccak256("WITHDRAW"); + address constant CHILD_BRIDGE = address(3); + uint256 constant IMX_DEPOSITS_LIMIT = 10000 ether; + + ChildERC20 childTokenTemplate; + ChildERC20 imxToken; + WETH wETH; + MockAdaptor mockAdaptor; + RootERC20BridgeFlowRate bridge; + + function setUp() public { + mockAdaptor = new MockAdaptor(); + + childTokenTemplate = new ChildERC20(); + childTokenTemplate.initialize(address(123), "Test", "TST", 18); + + imxToken = new ChildERC20(); + imxToken.initialize(address(234), "IMX Token", "IMX", 18); + + wETH = new WETH(); + + IRootERC20Bridge.InitializationRoles memory roles = IRootERC20Bridge.InitializationRoles({ + defaultAdmin: address(this), + pauser: address(this), + unpauser: address(this), + variableManager: address(this), + adaptorManager: address(this) + }); + + bridge = new RootERC20BridgeFlowRate(address(this)); + bridge.initialize( + roles, + address(mockAdaptor), + CHILD_BRIDGE, + address(childTokenTemplate), + address(imxToken), + address(wETH), + IMX_DEPOSITS_LIMIT, + address(this) + ); + } + + function testFuzz_RateLimitForIMX(uint256 capacity) public { + vm.assume(capacity < 1000 ether && capacity > 86400); + uint256 refillRate = capacity / 86400; + uint256 largeTransferThreshold = capacity / 2; + + // bridge.setRateControlThreshold(, capacity, refillRate, largeTransferThreshold); + } +} From 9f06a96b94e2f6c20c3243c444508e64cbf0e69d Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Thu, 8 Feb 2024 09:19:01 +1000 Subject: [PATCH 110/155] Delete RootERC20BridgeFlowRate.t.sol --- test/fuzz/root/RootERC20BridgeFlowRate.t.sol | 69 -------------------- 1 file changed, 69 deletions(-) delete mode 100644 test/fuzz/root/RootERC20BridgeFlowRate.t.sol diff --git a/test/fuzz/root/RootERC20BridgeFlowRate.t.sol b/test/fuzz/root/RootERC20BridgeFlowRate.t.sol deleted file mode 100644 index 051beac7..00000000 --- a/test/fuzz/root/RootERC20BridgeFlowRate.t.sol +++ /dev/null @@ -1,69 +0,0 @@ -// SPDX-License-Identifier: Apache 2.0 -pragma solidity 0.8.19; - -import {Test} from "forge-std/Test.sol"; -import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; -import { - IRootERC20BridgeEvents, - IERC20Metadata, - IRootERC20BridgeErrors, - IRootERC20Bridge -} from "../../../src/root/RootERC20Bridge.sol"; -import {RootERC20BridgeFlowRate} from "../../../src/root/flowrate/RootERC20BridgeFlowRate.sol"; -import {ChildERC20} from "../../../src/child/ChildERC20.sol"; -import {WETH} from "../../../src/lib/WETH.sol"; -import {MockAdaptor} from "../../mocks/root/MockAdaptor.sol"; - -contract RootERC20BridgeFlowRateTest is Test { - bytes32 public constant MAP_TOKEN_SIG = keccak256("MAP_TOKEN"); - bytes32 public constant DEPOSIT_SIG = keccak256("DEPOSIT"); - bytes32 public constant WITHDRAW_SIG = keccak256("WITHDRAW"); - address constant CHILD_BRIDGE = address(3); - uint256 constant IMX_DEPOSITS_LIMIT = 10000 ether; - - ChildERC20 childTokenTemplate; - ChildERC20 imxToken; - WETH wETH; - MockAdaptor mockAdaptor; - RootERC20BridgeFlowRate bridge; - - function setUp() public { - mockAdaptor = new MockAdaptor(); - - childTokenTemplate = new ChildERC20(); - childTokenTemplate.initialize(address(123), "Test", "TST", 18); - - imxToken = new ChildERC20(); - imxToken.initialize(address(234), "IMX Token", "IMX", 18); - - wETH = new WETH(); - - IRootERC20Bridge.InitializationRoles memory roles = IRootERC20Bridge.InitializationRoles({ - defaultAdmin: address(this), - pauser: address(this), - unpauser: address(this), - variableManager: address(this), - adaptorManager: address(this) - }); - - bridge = new RootERC20BridgeFlowRate(address(this)); - bridge.initialize( - roles, - address(mockAdaptor), - CHILD_BRIDGE, - address(childTokenTemplate), - address(imxToken), - address(wETH), - IMX_DEPOSITS_LIMIT, - address(this) - ); - } - - function testFuzz_RateLimitForIMX(uint256 capacity) public { - vm.assume(capacity < 1000 ether && capacity > 86400); - uint256 refillRate = capacity / 86400; - uint256 largeTransferThreshold = capacity / 2; - - // bridge.setRateControlThreshold(, capacity, refillRate, largeTransferThreshold); - } -} From 9a678f60d2506a72e1a66239b1743946e2684727 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Mon, 12 Feb 2024 08:09:43 +1000 Subject: [PATCH 111/155] Delete OwnableCreate2Deployer.sol --- src/deploy/OwnableCreate2Deployer.sol | 47 --------------------------- 1 file changed, 47 deletions(-) delete mode 100644 src/deploy/OwnableCreate2Deployer.sol diff --git a/src/deploy/OwnableCreate2Deployer.sol b/src/deploy/OwnableCreate2Deployer.sol deleted file mode 100644 index 8aa5de05..00000000 --- a/src/deploy/OwnableCreate2Deployer.sol +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright Immutable Pty Ltd 2018 - 2023 -// SPDX-License-Identifier: Apache 2.0 -pragma solidity 0.8.19; - -import "@openzeppelin/contracts/access/Ownable.sol"; -import {Deployer} from "@axelar-gmp-sdk-solidity/contracts/deploy/Deployer.sol"; -import {Create2} from "@axelar-gmp-sdk-solidity/contracts/deploy/Create2.sol"; - -/** - * @title OwnableCreate2Deployer - * @notice Deploys and optionally initializes contracts using the `CREATE2` opcode. - * @dev This contract extends the {Deployer} contract from the Axelar SDK, by adding basic access control to the deployment functions. - * The contract has an owner, which is the only entity that can deploy new contracts. - * - * @dev The contract deploys a contract with the same bytecode, salt, and sender(owner) to the same address. - * Attempting to deploy a contract with the same bytecode, salt, and sender(owner) will revert. - * The address where the contract will be deployed can be found using {deployedAddress}. - */ -contract OwnableCreate2Deployer is Ownable, Create2, Deployer { - constructor(address owner) Ownable() { - transferOwnership(owner); - } - - /** - * @dev Deploys a contract using the `CREATE2` opcode. - * This function is called by {deploy} and {deployAndInit} external functions in the {Deployer} contract. - * This function can only be called by the owner of this contract, hence {deploy} and {deployAndInit} can only be called by the owner. - * The address where the contract will be deployed can be found using {deployedAddress}. - * @param bytecode The bytecode of the contract to be deployed - * @param deploySalt A salt which is a hash of the salt provided by the sender and the sender's address. - * @return The address of the deployed contract - */ - function _deploy(bytes memory bytecode, bytes32 deploySalt) internal override onlyOwner returns (address) { - return _create2(bytecode, deploySalt); - } - - /** - * @dev Returns the address where a contract will be stored if deployed via {deploy} or {deployAndInit}. - * This function is called by the {deployedAddress} external functions in the {Deployer} contract. - * @param bytecode The bytecode of the contract to be deployed - * @param deploySalt A salt which is a hash of the salt provided by the sender and the sender's address. - * @return The predicted deployment address of the contract - */ - function _deployedAddress(bytes memory bytecode, bytes32 deploySalt) internal view override returns (address) { - return _create2Address(bytecode, deploySalt); - } -} From f4c7278a670860c597972298add54ee10c857702 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Mon, 12 Feb 2024 08:47:40 +1000 Subject: [PATCH 112/155] Delete OwnableCreate2Deployer.t.sol --- test/unit/deploy/OwnableCreate2Deployer.t.sol | 177 ------------------ 1 file changed, 177 deletions(-) delete mode 100644 test/unit/deploy/OwnableCreate2Deployer.t.sol diff --git a/test/unit/deploy/OwnableCreate2Deployer.t.sol b/test/unit/deploy/OwnableCreate2Deployer.t.sol deleted file mode 100644 index 1b89ef8b..00000000 --- a/test/unit/deploy/OwnableCreate2Deployer.t.sol +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright Immutable Pty Ltd 2018 - 2023 -// SPDX-License-Identifier: Apache 2.0 -pragma solidity 0.8.19; - -import "forge-std/Test.sol"; -import {IDeploy} from "@axelar-gmp-sdk-solidity/contracts/interfaces/IDeploy.sol"; -import {IDeployer} from "@axelar-gmp-sdk-solidity/contracts/interfaces/IDeployer.sol"; - -import {ChildERC20} from "../../../src/child/ChildERC20.sol"; -import {OwnableCreate2Deployer} from "../../../src/deploy/OwnableCreate2Deployer.sol"; - -contract OwnableCreate2DeployerTest is Test { - OwnableCreate2Deployer private deployer; - ChildERC20 private childERC20; - - bytes private childERC20Bytecode; - bytes32 private salt; - address private owner; - - event Deployed(address indexed deployedAddress, address indexed sender, bytes32 indexed salt, bytes32 bytecodeHash); - - function setUp() public { - owner = address(0x12345); - - // create a new deployer that is owned by this contract - deployer = new OwnableCreate2Deployer(owner); - - childERC20 = new ChildERC20(); - childERC20Bytecode = type(ChildERC20).creationCode; - - salt = createSaltFromKey("test-salt"); - vm.startPrank(owner); - } - - function test_RevertIf_DeployWithEmptyByteCode() public { - vm.expectRevert(IDeploy.EmptyBytecode.selector); - deployer.deploy("", salt); - } - - function test_RevertIf_DeployWithNonOwner() public { - vm.stopPrank(); - - address nonOwner = address(0x1); - vm.startPrank(nonOwner); - vm.expectRevert("Ownable: caller is not the owner"); - deployer.deploy(childERC20Bytecode, salt); - } - - /// @dev deploying with the same bytecode, salt and sender should revert - function test_RevertIf_DeployAlreadyDeployedCreate2Contract() public { - deployer.deploy(childERC20Bytecode, salt); - - vm.expectRevert(IDeploy.AlreadyDeployed.selector); - deployer.deploy(childERC20Bytecode, salt); - } - - function test_deploy_DeploysContract() public { - address expectedAddress = predictCreate2Address(childERC20Bytecode, address(deployer), address(owner), salt); - - vm.expectEmit(); - emit Deployed(expectedAddress, address(owner), salt, keccak256(childERC20Bytecode)); - address deployed = deployer.deploy(childERC20Bytecode, salt); - - assertEq(deployed.code, address(childERC20).code, "deployed contract code does not match expected"); - - ChildERC20 deployedChildERC20 = ChildERC20(deployed); - assertEq(deployedChildERC20.name(), "", "deployed contract should have empty name"); - assertEq(deployedChildERC20.symbol(), "", "deployed contract should have empty symbol"); - assertEq(deployedChildERC20.decimals(), 0, "deployed contract should have 0 decimals"); - } - - function test_deploy_DeploysToPredictedAddress() public { - address deployedAddress = deployer.deploy(childERC20Bytecode, salt); - address expectedAddress = predictCreate2Address(childERC20Bytecode, address(deployer), address(owner), salt); - assertEq(deployedAddress, expectedAddress, "deployed address does not match expected address"); - } - - function test_deploy_DeploysSameContractToDifferentAddresses_GivenDifferentSalts() public { - address deployed1 = deployer.deploy(childERC20Bytecode, salt); - - bytes32 newSalt = createSaltFromKey("new-salt"); - address deployed2 = deployer.deploy(childERC20Bytecode, newSalt); - - assertEq(deployed1.code, deployed2.code, "bytecode of deployed contracts do not match"); - assertNotEq(deployed1, deployed2, "deployed contracts should not have the same address"); - } - - function test_deploy_DeploysContractGivenNewOwner() public { - address newOwner = address(0x1); - - deployer.transferOwnership(newOwner); - assertEq(deployer.owner(), newOwner, "owner did not change as expected"); - - // check that the old owner cannot deploy - vm.expectRevert("Ownable: caller is not the owner"); - deployer.deploy(childERC20Bytecode, salt); - - // test that the new owner can deploy - vm.startPrank(newOwner); - address expectedAddress = predictCreate2Address(childERC20Bytecode, address(deployer), address(newOwner), salt); - - vm.expectEmit(); - emit Deployed(expectedAddress, address(newOwner), salt, keccak256(childERC20Bytecode)); - address deployed = deployer.deploy(childERC20Bytecode, salt); - - assertEq(deployed.code, address(childERC20).code, "deployed contract should match expected"); - } - - /** - * deployAndInit - */ - function test_RevertIf_DeployAndInitWithNonOwner() public { - vm.stopPrank(); - - address nonOwner = address(0x1); - vm.startPrank(nonOwner); - vm.expectRevert("Ownable: caller is not the owner"); - deployer.deployAndInit(childERC20Bytecode, salt, ""); - } - - function test_deployAndInit_DeploysAndInitsContract() public { - address expectedAddress = predictCreate2Address(childERC20Bytecode, address(deployer), address(owner), salt); - address rootToken = address(0x1); - string memory name = "Test-Token"; - string memory symbol = "TST"; - uint8 decimals = 18; - bytes memory initPayload = - abi.encodeWithSelector(ChildERC20.initialize.selector, rootToken, name, symbol, decimals); - - vm.expectEmit(); - emit Deployed(expectedAddress, address(owner), salt, keccak256(childERC20Bytecode)); - address deployed = deployer.deployAndInit(childERC20Bytecode, salt, initPayload); - - // regardless of init data, the deployed address should match expected deployment - assertEq(deployed, expectedAddress, "deployed address should match expected address"); - - assertEq(deployed.code, address(childERC20).code, "deployed contract should match expected"); - - // verify initialisation - ChildERC20 deployedChildERC20 = ChildERC20(deployed); - assertEq(deployedChildERC20.rootToken(), rootToken, "rootToken does not match expected"); - assertEq(deployedChildERC20.name(), name, "name does not match expected"); - assertEq(deployedChildERC20.symbol(), symbol, "symbol does not match expected"); - assertEq(deployedChildERC20.decimals(), decimals, "decimals does not match expected"); - } - - /** - * deployedAddress - */ - function test_deployedAddress_ReturnsPredictedAddress() public { - address deployAddress = deployer.deployedAddress(childERC20Bytecode, address(owner), salt); - - address predictedAddress = predictCreate2Address(childERC20Bytecode, address(deployer), address(owner), salt); - address deployedAddress = deployer.deploy(childERC20Bytecode, salt); - - assertEq(deployAddress, predictedAddress, "deployment address did not match predicted address"); - assertEq(deployAddress, deployedAddress, "deployment address did not match deployed address"); - } - - /** - * private helper functions - */ - function predictCreate2Address(bytes memory _bytecode, address _deployer, address _sender, bytes32 _salt) - private - pure - returns (address) - { - bytes32 deploySalt = keccak256(abi.encode(_sender, _salt)); - return address( - uint160(uint256(keccak256(abi.encodePacked(hex"ff", address(_deployer), deploySalt, keccak256(_bytecode))))) - ); - } - - function createSaltFromKey(string memory key) private view returns (bytes32) { - return keccak256(abi.encode(address(owner), key)); - } -} From 1e83d42e1fdc594038f50e055265f7a0571ae93e Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Mon, 12 Feb 2024 12:00:06 +1000 Subject: [PATCH 113/155] Refactor --- scripts/e2e/e2e.ts | 611 ++++++++++++++++----------------------------- 1 file changed, 215 insertions(+), 396 deletions(-) diff --git a/scripts/e2e/e2e.ts b/scripts/e2e/e2e.ts index 712fbe1a..6badcd99 100644 --- a/scripts/e2e/e2e.ts +++ b/scripts/e2e/e2e.ts @@ -66,6 +66,34 @@ describe("Bridge e2e test", () => { childBridge = getContract("ChildERC20Bridge", childBridgeAddr, childProvider); childETH = getContract("ChildERC20", await childBridge.childETHToken(), childProvider); childWIMX = getContract("WIMX", childWIMXAddr, childProvider); + + // Transfer 0.5 ETH to root pauser + let resp = await rootTestWallet.sendTransaction({ + to: rootPauserWallet.address, + value: ethers.utils.parseEther("0.5"), + }) + await waitForReceipt(resp.hash, rootProvider); + + // Transfer 0.5 ETH to root unpauser + resp = await rootTestWallet.sendTransaction({ + to: rootPrivilegedWallet.address, + value: ethers.utils.parseEther("0.5"), + }) + await waitForReceipt(resp.hash, rootProvider); + + // Transfer 5 IMX to child pauser + resp = await childTestWallet.sendTransaction({ + to: childPauserWallet.address, + value: ethers.utils.parseEther("5"), + }) + await waitForReceipt(resp.hash, childProvider); + + // Transfer 5 IMX to child unpauser + resp = await childTestWallet.sendTransaction({ + to: childPrivilegedWallet.address, + value: ethers.utils.parseEther("5"), + }) + await waitForReceipt(resp.hash, childProvider); }) it("should not deposit IMX if allowance is insufficient", async() => { @@ -82,38 +110,20 @@ describe("Bridge e2e test", () => { })).to.be.rejectedWith("UNPREDICTABLE_GAS_LIMIT"); }).timeout(2400000) - it("should not deposit IMX if balance is insufficient", async() => { + it.only("should not deposit IMX if balance is insufficient", async() => { let balance = await rootIMX.balanceOf(rootTestWallet.address); let amt = balance.add(1); let bridgeFee = ethers.utils.parseEther("0.001"); - // Approve - let resp = await rootIMX.connect(rootTestWallet).approve(rootBridge.address, amt); - await waitForReceipt(resp.hash, rootProvider); - - // Fail to deposit on L1 - await expect(rootBridge.connect(rootTestWallet).deposit(rootIMX.address, amt, { - value: bridgeFee, - })).to.be.rejectedWith("UNPREDICTABLE_GAS_LIMIT"); + await expect( + depositIMX(rootTestWallet, amt, bridgeFee, null) + ).to.be.rejectedWith("UNPREDICTABLE_GAS_LIMIT"); }).timeout(2400000) // Local only it("should not deposit IMX if root bridge is paused", async() => { - // Transfer 0.1 ETH to root pauser - let resp = await rootTestWallet.sendTransaction({ - to: rootPauserWallet.address, - value: ethers.utils.parseEther("0.1"), - }) - await waitForReceipt(resp.hash, rootProvider); - - // Transfer 0.1 ETH to root unpauser - resp = await rootTestWallet.sendTransaction({ - to: rootPrivilegedWallet.address, - value: ethers.utils.parseEther("0.1"), - }) - await waitForReceipt(resp.hash, rootProvider); - + let resp; // Pause root bridge if (!await rootBridge.paused()) { resp = await rootBridge.connect(rootPauserWallet).pause(); @@ -125,14 +135,9 @@ describe("Bridge e2e test", () => { let amt = ethers.utils.parseEther("10.0"); let bridgeFee = ethers.utils.parseEther("0.001"); - // Approve - resp = await rootIMX.connect(rootTestWallet).approve(rootBridge.address, amt); - await waitForReceipt(resp.hash, rootProvider); - - // Fail to deposit on L1 - await expect(rootBridge.connect(rootTestWallet).deposit(rootIMX.address, amt, { - value: bridgeFee, - })).to.be.rejectedWith("Pausable: paused"); + await expect( + depositIMX(rootTestWallet, amt, bridgeFee, null) + ).to.be.rejectedWith("Pausable: paused"); // Unpause root bridge resp = await rootBridge.connect(rootPrivilegedWallet).unpause(); @@ -147,14 +152,9 @@ describe("Bridge e2e test", () => { let amt = limit.add(1); let bridgeFee = ethers.utils.parseEther("0.001"); - // Approve - let resp = await rootIMX.connect(rootTestWallet).approve(rootBridge.address, amt); - await waitForReceipt(resp.hash, rootProvider); - - // Fail to deposit on L1 - await expect(rootBridge.connect(rootTestWallet).deposit(rootIMX.address, amt, { - value: bridgeFee, - })).to.be.rejectedWith(rootBridge.interface.getSighash('ImxDepositLimitExceeded()')); + await expect( + depositIMX(rootTestWallet, amt, bridgeFee, null) + ).to.be.rejectedWith("ImxDepositLimitExceeded()"); }).timeout(2400000) it("should successfully deposit IMX to self from L1 to L2", async() => { @@ -165,14 +165,7 @@ describe("Bridge e2e test", () => { let amt = ethers.utils.parseEther("50.0"); let bridgeFee = ethers.utils.parseEther("0.001"); - // Approve - let resp = await rootIMX.connect(rootTestWallet).approve(rootBridge.address, amt); - await waitForReceipt(resp.hash, rootProvider); - - // IMX deposit L1 to L2 - resp = await rootBridge.connect(rootTestWallet).deposit(rootIMX.address, amt, { - value: bridgeFee, - }); + let resp = await depositIMX(rootTestWallet, amt, bridgeFee, null); await waitForReceipt(resp.hash, rootProvider); let postBalL1 = await rootIMX.balanceOf(rootTestWallet.address); @@ -201,14 +194,7 @@ describe("Bridge e2e test", () => { let amt = ethers.utils.parseEther("50.0"); let bridgeFee = ethers.utils.parseEther("0.001"); - // Approve - let resp = await rootIMX.connect(rootTestWallet).approve(rootBridge.address, amt); - await waitForReceipt(resp.hash, rootProvider); - - // IMX deposit L1 to L2 - resp = await rootBridge.connect(rootTestWallet).depositTo(rootIMX.address, childRecipient, amt, { - value: bridgeFee, - }); + let resp = await depositIMX(rootTestWallet, amt, bridgeFee, childRecipient); await waitForReceipt(resp.hash, rootProvider); let postBalL1 = await rootIMX.balanceOf(rootTestWallet.address); @@ -257,15 +243,7 @@ describe("Bridge e2e test", () => { let amt = ethers.utils.parseEther("10.0"); let bridgeFee = ethers.utils.parseEther("0.001"); - - // Approve - resp = await rootIMX.connect(rootTestWallet).approve(rootBridge.address, amt); - await waitForReceipt(resp.hash, rootProvider); - - // Try to deposit - resp = await rootBridge.connect(rootTestWallet).deposit(rootIMX.address, amt, { - value: bridgeFee, - }); + resp = await depositIMX(rootTestWallet, amt, bridgeFee, null); await waitForReceipt(resp.hash, rootProvider); await waitUntilSucceed(axelarAPI, resp.hash); @@ -288,20 +266,7 @@ describe("Bridge e2e test", () => { // Local only it("should not withdraw IMX if child bridge is paused", async() => { - // Transfer 0.1 IMX to child pauser - let resp = await childTestWallet.sendTransaction({ - to: childPauserWallet.address, - value: ethers.utils.parseEther("0.1"), - }) - await waitForReceipt(resp.hash, childProvider); - - // Transfer 0.1 IMX to child unpauser - resp = await childTestWallet.sendTransaction({ - to: childPrivilegedWallet.address, - value: ethers.utils.parseEther("0.1"), - }) - await waitForReceipt(resp.hash, childProvider); - + let resp; // Pause child bridge if (!await childBridge.paused()) { resp = await childBridge.connect(childPauserWallet).pause(); @@ -333,12 +298,9 @@ describe("Bridge e2e test", () => { let bridgeFee = ethers.utils.parseEther("1.0"); // IMX withdraw L2 to L1 - let [priorityFee, maxFee] = await getFee(childProvider); - await expect(childBridge.connect(childTestWallet).withdrawIMX(amt, { - value: amt.add(bridgeFee), - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - })).to.be.rejectedWith("sender doesn't have enough funds to send tx"); + await expect( + withdrawIMX(childTestWallet, amt, bridgeFee, null) + ).to.be.rejectedWith("sender doesn't have enough funds to send tx"); }).timeout(2400000) it("should successfully withdraw IMX to self from L2 to L1", async() => { @@ -350,12 +312,7 @@ describe("Bridge e2e test", () => { let bridgeFee = ethers.utils.parseEther("1.0"); // IMX withdraw L2 to L1 - let [priorityFee, maxFee] = await getFee(childProvider); - let resp = await childBridge.connect(childTestWallet).withdrawIMX(amt, { - value: amt.add(bridgeFee), - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }); + let resp = await withdrawIMX(childTestWallet, amt, bridgeFee, null); await waitForReceipt(resp.hash, childProvider); let postBalL1 = preBalL1; @@ -387,12 +344,7 @@ describe("Bridge e2e test", () => { let bridgeFee = ethers.utils.parseEther("1.0"); // IMX withdraw L2 to L1 - let [priorityFee, maxFee] = await getFee(childProvider); - let resp = await childBridge.connect(childTestWallet).withdrawIMXTo(rootRecipient, amt, { - value: amt.add(bridgeFee), - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }); + let resp = await withdrawIMX(childTestWallet, amt, bridgeFee, rootRecipient); await waitForReceipt(resp.hash, childProvider); let postBalL1 = preBalL1; @@ -416,20 +368,7 @@ describe("Bridge e2e test", () => { // Local only it("should not withdraw IMX on L1 if root bridge is paused", async() => { - // Transfer 0.1 ETH to root pauser - let resp = await rootTestWallet.sendTransaction({ - to: rootPauserWallet.address, - value: ethers.utils.parseEther("0.1"), - }) - await waitForReceipt(resp.hash, rootProvider); - - // Transfer 0.1 ETH to root unpauser - resp = await rootTestWallet.sendTransaction({ - to: rootPrivilegedWallet.address, - value: ethers.utils.parseEther("0.1"), - }) - await waitForReceipt(resp.hash, rootProvider); - + let resp; // Pause root bridge if (!await rootBridge.paused()) { resp = await rootBridge.connect(rootPauserWallet).pause(); @@ -445,12 +384,7 @@ describe("Bridge e2e test", () => { let bridgeFee = ethers.utils.parseEther("1.0"); // IMX withdraw L2 to L1 - let [priorityFee, maxFee] = await getFee(childProvider); - resp = await childBridge.connect(childTestWallet).withdrawIMX(amt, { - value: amt.add(bridgeFee), - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }); + resp = await withdrawIMX(childTestWallet, amt, bridgeFee, null); await waitForReceipt(resp.hash, childProvider); await waitUntilSucceed(axelarAPI, resp.hash); @@ -489,12 +423,7 @@ describe("Bridge e2e test", () => { let bridgeFee1 = ethers.utils.parseEther("1.0"); // IMX withdraw L2 to L1 - let [priorityFee, maxFee] = await getFee(childProvider); - resp = await childBridge.connect(childTestWallet).withdrawIMX(amt1, { - value: amt1.add(bridgeFee1), - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }); + resp = await withdrawIMX(childTestWallet, amt1, bridgeFee1, null); await waitForReceipt(resp.hash, childProvider); await waitUntilSucceed(axelarAPI, resp.hash); @@ -510,12 +439,7 @@ describe("Bridge e2e test", () => { let bridgeFee2 = ethers.utils.parseEther("1.0"); // IMX withdraw L2 to L1 - [priorityFee, maxFee] = await getFee(childProvider); - resp = await childBridge.connect(childTestWallet).withdrawIMX(amt2, { - value: amt2.add(bridgeFee2), - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }); + resp = await withdrawIMX(childTestWallet, amt2, bridgeFee2, null); await waitForReceipt(resp.hash, childProvider); await waitUntilSucceed(axelarAPI, resp.hash); receipt = await childProvider.getTransactionReceipt(resp.hash); @@ -563,20 +487,7 @@ describe("Bridge e2e test", () => { // Local only it("should not withdraw WIMX if child bridge is paused", async() => { - // Transfer 0.1 IMX to child pauser - let resp = await childTestWallet.sendTransaction({ - to: childPauserWallet.address, - value: ethers.utils.parseEther("0.1"), - }) - await waitForReceipt(resp.hash, childProvider); - - // Transfer 0.1 IMX to child unpauser - resp = await childTestWallet.sendTransaction({ - to: childPrivilegedWallet.address, - value: ethers.utils.parseEther("0.1"), - }) - await waitForReceipt(resp.hash, childProvider); - + let resp; // Pause child bridge if (!await childBridge.paused()) { resp = await childBridge.connect(childPauserWallet).pause(); @@ -584,31 +495,12 @@ describe("Bridge e2e test", () => { expect(await childBridge.paused()).to.true; } - // Wrap 1 IMX - let [priorityFee, maxFee] = await getFee(childProvider); - resp = await childWIMX.connect(childTestWallet).deposit({ - value: ethers.utils.parseEther("1.0"), - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }); - await waitForReceipt(resp.hash, childProvider); - let amt = ethers.utils.parseEther("0.5"); let bridgeFee = ethers.utils.parseEther("1.0"); - // Approve - [priorityFee, maxFee] = await getFee(childProvider); - resp = await childWIMX.connect(childTestWallet).approve(childBridge.address, amt, { - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }); - await waitForReceipt(resp.hash, childProvider); - - await expect(childBridge.connect(childTestWallet).withdrawWIMX(amt, { - value: amt.add(bridgeFee), - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - })).to.be.rejectedWith("Pausable: paused"); + await expect( + withdrawWIMX(childTestWallet, amt, bridgeFee, null) + ).to.be.rejectedWith("Pausable: paused"); // Unpause child bridge resp = await childBridge.connect(childPrivilegedWallet).unpause(); @@ -649,50 +541,23 @@ describe("Bridge e2e test", () => { it("should not withdraw wIMX if balance is insufficient", async() => { let balance = await childWIMX.balanceOf(childTestWallet.address); - let amt = balance; + let amt = balance.add(1); let bridgeFee = ethers.utils.parseEther("1.0"); - // wIMX withdraw L2 to L1 - let [priorityFee, maxFee] = await getFee(childProvider); - await expect(childBridge.connect(childTestWallet).withdrawWIMX(amt.add(1), { - value: bridgeFee, - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - })).to.be.rejectedWith("UNPREDICTABLE_GAS_LIMIT"); + await expect( + withdrawWIMX(childTestWallet, amt, bridgeFee, null) + ).to.be.rejectedWith("UNPREDICTABLE_GAS_LIMIT"); }).timeout(2400000) it("should successfully withdraw wIMX to self from L2 to L1", async() => { - // Wrap 1 IMX - let [priorityFee, maxFee] = await getFee(childProvider); - let resp = await childWIMX.connect(childTestWallet).deposit({ - value: ethers.utils.parseEther("1.0"), - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }); - await waitForReceipt(resp.hash, childProvider); + let amt = ethers.utils.parseEther("0.5"); + let bridgeFee = ethers.utils.parseEther("1.0"); // Get IMX balance on root & child chains before withdraw let preBalL1 = await rootIMX.balanceOf(rootTestWallet.address); let preBalL2 = await childWIMX.balanceOf(childTestWallet.address); - let amt = ethers.utils.parseEther("0.5"); - let bridgeFee = ethers.utils.parseEther("1.0"); - - // Approve - [priorityFee, maxFee] = await getFee(childProvider); - resp = await childWIMX.connect(childTestWallet).approve(childBridge.address, amt, { - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }); - await waitForReceipt(resp.hash, childProvider); - - // wIMX withdraw L2 to L1 - [priorityFee, maxFee] = await getFee(childProvider); - resp = await childBridge.connect(childTestWallet).withdrawWIMX(amt, { - value: bridgeFee, - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }); + let resp = await withdrawWIMX(rootTestWallet, amt, bridgeFee, null); await waitForReceipt(resp.hash, childProvider); let postBalL1 = preBalL1; @@ -714,37 +579,13 @@ describe("Bridge e2e test", () => { it("should successfully withdraw wIMX to others from L2 to L1", async() => { let rootRecipient = rootPrivilegedWallet.address; - // Wrap 1 IMX - let [priorityFee, maxFee] = await getFee(childProvider); - let resp = await childWIMX.connect(childTestWallet).deposit({ - value: ethers.utils.parseEther("1.0"), - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }); - await waitForReceipt(resp.hash, childProvider); - + let amt = ethers.utils.parseEther("0.5"); + let bridgeFee = ethers.utils.parseEther("1.0"); // Get IMX balance on root & child chains before withdraw let preBalL1 = await rootIMX.balanceOf(rootRecipient); let preBalL2 = await childWIMX.balanceOf(childTestWallet.address); - let amt = ethers.utils.parseEther("0.5"); - let bridgeFee = ethers.utils.parseEther("1.0"); - - // Approve - [priorityFee, maxFee] = await getFee(childProvider); - resp = await childWIMX.connect(childTestWallet).approve(childBridge.address, amt, { - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }); - await waitForReceipt(resp.hash, childProvider); - - // wIMX withdraw L2 to L1 - [priorityFee, maxFee] = await getFee(childProvider); - resp = await childBridge.connect(childTestWallet).withdrawWIMXTo(rootRecipient, amt, { - value: bridgeFee, - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }); + let resp = await withdrawWIMX(rootTestWallet, amt, bridgeFee, rootRecipient); await waitForReceipt(resp.hash, childProvider); let postBalL1 = preBalL1; @@ -766,20 +607,7 @@ describe("Bridge e2e test", () => { // Local only it("should not withdraw wIMX on L1 if root bridge is paused", async() => { - // Transfer 0.1 ETH to root pauser - let resp = await rootTestWallet.sendTransaction({ - to: rootPauserWallet.address, - value: ethers.utils.parseEther("0.1"), - }) - await waitForReceipt(resp.hash, rootProvider); - - // Transfer 0.1 ETH to root unpauser - resp = await rootTestWallet.sendTransaction({ - to: rootPrivilegedWallet.address, - value: ethers.utils.parseEther("0.1"), - }) - await waitForReceipt(resp.hash, rootProvider); - + let resp; // Pause root bridge if (!await rootBridge.paused()) { resp = await rootBridge.connect(rootPauserWallet).pause(); @@ -787,37 +615,14 @@ describe("Bridge e2e test", () => { expect(await rootBridge.paused()).to.true; } - // Wrap 1 IMX - let [priorityFee, maxFee] = await getFee(childProvider); - resp = await childWIMX.connect(childTestWallet).deposit({ - value: ethers.utils.parseEther("1.0"), - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }); - await waitForReceipt(resp.hash, childProvider); + let amt = ethers.utils.parseEther("0.5"); + let bridgeFee = ethers.utils.parseEther("1.0"); // Get IMX balance on root & child chains before withdraw let preBalL1 = await rootIMX.balanceOf(rootTestWallet.address); let preBalL2 = await childWIMX.balanceOf(childTestWallet.address); - let amt = ethers.utils.parseEther("0.5"); - let bridgeFee = ethers.utils.parseEther("1.0"); - - // Approve - [priorityFee, maxFee] = await getFee(childProvider); - resp = await childWIMX.connect(childTestWallet).approve(childBridge.address, amt, { - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }); - await waitForReceipt(resp.hash, childProvider); - - // wIMX withdraw L2 to L1 - [priorityFee, maxFee] = await getFee(childProvider); - resp = await childBridge.connect(childTestWallet).withdrawWIMX(amt, { - value: bridgeFee, - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }); + resp = await withdrawWIMX(rootTestWallet, amt, bridgeFee, null); await waitForReceipt(resp.hash, childProvider); await waitUntilSucceed(axelarAPI, resp.hash); @@ -959,20 +764,7 @@ describe("Bridge e2e test", () => { // Local only it("should not deposit ETH if root bridge is paused", async() => { - // Transfer 0.1 ETH to root pauser - let resp = await rootTestWallet.sendTransaction({ - to: rootPauserWallet.address, - value: ethers.utils.parseEther("0.1"), - }) - await waitForReceipt(resp.hash, rootProvider); - - // Transfer 0.1 ETH to root unpauser - resp = await rootTestWallet.sendTransaction({ - to: rootPrivilegedWallet.address, - value: ethers.utils.parseEther("0.1"), - }) - await waitForReceipt(resp.hash, rootProvider); - + let resp; // Pause root bridge if (!await rootBridge.paused()) { resp = await rootBridge.connect(rootPauserWallet).pause(); @@ -984,9 +776,9 @@ describe("Bridge e2e test", () => { let bridgeFee = ethers.utils.parseEther("0.001"); // Fail to deposit on L1 - await expect(rootBridge.connect(rootTestWallet).depositETH(amt, { - value: amt.add(bridgeFee), - })).to.be.rejectedWith("Pausable: paused"); + await expect( + depositETH(rootTestWallet, amt, bridgeFee, null) + ).to.be.rejectedWith("Pausable: paused"); // Unpause root bridge resp = await rootBridge.connect(rootPrivilegedWallet).unpause(); @@ -1003,9 +795,7 @@ describe("Bridge e2e test", () => { let bridgeFee = ethers.utils.parseEther("0.001"); // ETH deposit L1 to L2 - let resp = await rootBridge.connect(rootTestWallet).depositETH(amt, { - value: amt.add(bridgeFee), - }); + let resp = await depositETH(rootTestWallet, amt, bridgeFee, null); await waitForReceipt(resp.hash, rootProvider); let postBalL1 = await rootProvider.getBalance(rootTestWallet.address); @@ -1037,9 +827,7 @@ describe("Bridge e2e test", () => { let bridgeFee = ethers.utils.parseEther("0.001"); // ETH deposit L1 to L2 - let resp = await rootBridge.connect(rootTestWallet).depositToETH(childRecipient, amt, { - value: amt.add(bridgeFee), - }); + let resp = await depositETH(rootTestWallet, amt, bridgeFee, childRecipient); await waitForReceipt(resp.hash, rootProvider); let postBalL1 = await rootProvider.getBalance(rootTestWallet.address); @@ -1063,20 +851,7 @@ describe("Bridge e2e test", () => { // Local only it("should not deposit ETH on L2 if child bridge is paused", async() => { - // Transfer 0.1 IMX to child pauser - let resp = await childTestWallet.sendTransaction({ - to: childPauserWallet.address, - value: ethers.utils.parseEther("0.1"), - }) - await waitForReceipt(resp.hash, childProvider); - - // Transfer 0.1 IMX to child unpauser - resp = await childTestWallet.sendTransaction({ - to: childPrivilegedWallet.address, - value: ethers.utils.parseEther("0.1"), - }) - await waitForReceipt(resp.hash, childProvider); - + let resp; // Pause child bridge if (!await childBridge.paused()) { resp = await childBridge.connect(childPauserWallet).pause(); @@ -1092,9 +867,7 @@ describe("Bridge e2e test", () => { let bridgeFee = ethers.utils.parseEther("0.001"); // Try to deposit - resp = await rootBridge.connect(rootTestWallet).depositETH(amt, { - value: amt.add(bridgeFee), - }); + resp = await depositETH(rootTestWallet, amt, bridgeFee, null); await waitForReceipt(resp.hash, rootProvider); await waitUntilSucceed(axelarAPI, resp.hash); @@ -1118,27 +891,14 @@ describe("Bridge e2e test", () => { }).timeout(2400000) it("should successfully deposit wETH to self from L1 to L2", async() => { - // Wrap 0.01 ETH - let resp = await rootWETH.connect(rootTestWallet).deposit({ - value: ethers.utils.parseEther("0.01"), - }) - await waitForReceipt(resp.hash, rootProvider); + let amt = ethers.utils.parseEther("0.001"); + let bridgeFee = ethers.utils.parseEther("0.001"); // Get ETH balance on root & child chains before withdraw let preBalL1 = await rootWETH.balanceOf(rootTestWallet.address); let preBalL2 = await childETH.balanceOf(childTestWallet.address); - let amt = ethers.utils.parseEther("0.001"); - let bridgeFee = ethers.utils.parseEther("0.001"); - - // Approve - resp = await rootWETH.connect(rootTestWallet).approve(rootBridge.address, amt); - await waitForReceipt(resp.hash, rootProvider); - - // wETH deposit L1 to L2 - resp = await rootBridge.connect(rootTestWallet).deposit(rootWETH.address, amt, { - value: bridgeFee, - }) + let resp = await depositWETH(rootTestWallet, amt, bridgeFee, null); await waitForReceipt(resp.hash, rootProvider); let postBalL1 = await rootWETH.balanceOf(rootTestWallet.address); @@ -1160,27 +920,14 @@ describe("Bridge e2e test", () => { it("should successfully deposit wETH to others from L1 to L2", async() => { let childRecipient = childPrivilegedWallet.address; - // Wrap 0.01 ETH - let resp = await rootWETH.connect(rootTestWallet).deposit({ - value: ethers.utils.parseEther("0.01"), - }) - await waitForReceipt(resp.hash, rootProvider); + let amt = ethers.utils.parseEther("0.001"); + let bridgeFee = ethers.utils.parseEther("0.001"); // Get ETH balance on root & child chains before withdraw let preBalL1 = await rootWETH.balanceOf(rootTestWallet.address); let preBalL2 = await childETH.balanceOf(childRecipient); - let amt = ethers.utils.parseEther("0.001"); - let bridgeFee = ethers.utils.parseEther("0.001"); - - // Approve - resp = await rootWETH.connect(rootTestWallet).approve(rootBridge.address, amt); - await waitForReceipt(resp.hash, rootProvider); - - // wETH deposit L1 to L2 - resp = await rootBridge.connect(rootTestWallet).depositTo(rootWETH.address, childRecipient, amt, { - value: bridgeFee, - }) + let resp = await depositWETH(rootTestWallet, amt, bridgeFee, childRecipient); await waitForReceipt(resp.hash, rootProvider); let postBalL1 = await rootWETH.balanceOf(rootTestWallet.address); @@ -1202,20 +949,7 @@ describe("Bridge e2e test", () => { // Local only it("should not withdraw ETH if child bridge is paused", async() => { - // Transfer 0.1 IMX to child pauser - let resp = await childTestWallet.sendTransaction({ - to: childPauserWallet.address, - value: ethers.utils.parseEther("0.1"), - }) - await waitForReceipt(resp.hash, childProvider); - - // Transfer 0.1 IMX to child unpauser - resp = await childTestWallet.sendTransaction({ - to: childPrivilegedWallet.address, - value: ethers.utils.parseEther("0.1"), - }) - await waitForReceipt(resp.hash, childProvider); - + let resp; // Pause child bridge if (!await childBridge.paused()) { resp = await childBridge.connect(childPauserWallet).pause(); @@ -1227,12 +961,9 @@ describe("Bridge e2e test", () => { let bridgeFee = ethers.utils.parseEther("1.0"); // ETH withdraw L2 to L1 - let [priorityFee, maxFee] = await getFee(childProvider); - await expect(childBridge.connect(childTestWallet).withdrawETH(amt, { - value: bridgeFee, - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - })).to.be.rejectedWith("Pausable: paused"); + await expect( + withdrawETH(childTestWallet, amt, bridgeFee, null) + ).to.be.rejectedWith("Pausable: paused"); // Unpause child bridge resp = await childBridge.connect(childPrivilegedWallet).unpause(); @@ -1241,16 +972,13 @@ describe("Bridge e2e test", () => { }).timeout(2400000) it("should not withdraw ETH if balance is insufficient", async() => { - let amt = await childETH.balanceOf(childTestWallet.address); + let amt = await childETH.balanceOf(childTestWallet.address).add(1); let bridgeFee = ethers.utils.parseEther("1.0"); // ETH withdraw L2 to L1 - let [priorityFee, maxFee] = await getFee(childProvider); - await expect(childBridge.connect(childTestWallet).withdrawETH(amt.add(1), { - value: bridgeFee, - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - })).to.be.rejectedWith("UNPREDICTABLE_GAS_LIMIT"); + await expect( + withdrawETH(childTestWallet, amt, bridgeFee, null) + ).to.be.rejectedWith("UNPREDICTABLE_GAS_LIMIT"); }).timeout(2400000) it("should successfully withdraw ETH to self from L2 to L1", async() => { @@ -1262,12 +990,7 @@ describe("Bridge e2e test", () => { let bridgeFee = ethers.utils.parseEther("1.0"); // ETH withdraw L2 to L1 - let [priorityFee, maxFee] = await getFee(childProvider); - let resp = await childBridge.connect(childTestWallet).withdrawETH(amt, { - value: bridgeFee, - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }); + let resp = await withdrawETH(childTestWallet, amt, bridgeFee, null); await waitForReceipt(resp.hash, childProvider); let postBalL1 = preBalL1; @@ -1297,12 +1020,7 @@ describe("Bridge e2e test", () => { let bridgeFee = ethers.utils.parseEther("1.0"); // ETH withdraw L2 to L1 - let [priorityFee, maxFee] = await getFee(childProvider); - let resp = await childBridge.connect(childTestWallet).withdrawETHTo(rootRecipient, amt, { - value: bridgeFee, - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }); + let resp = await withdrawETH(childTestWallet, amt, bridgeFee, rootRecipient); await waitForReceipt(resp.hash, childProvider); let postBalL1 = preBalL1; @@ -1324,20 +1042,7 @@ describe("Bridge e2e test", () => { // Local only it("should not withdraw ETH on L1 if root bridge is paused", async() => { - // Transfer 0.1 ETH to root pauser - let resp = await rootTestWallet.sendTransaction({ - to: rootPauserWallet.address, - value: ethers.utils.parseEther("0.1"), - }) - await waitForReceipt(resp.hash, rootProvider); - - // Transfer 0.1 ETH to root unpauser - resp = await rootTestWallet.sendTransaction({ - to: rootPrivilegedWallet.address, - value: ethers.utils.parseEther("0.1"), - }) - await waitForReceipt(resp.hash, rootProvider); - + let resp; // Pause root bridge if (!await rootBridge.paused()) { resp = await rootBridge.connect(rootPauserWallet).pause(); @@ -1353,12 +1058,7 @@ describe("Bridge e2e test", () => { let bridgeFee = ethers.utils.parseEther("1.0"); // ETH withdraw L2 to L1 - let [priorityFee, maxFee] = await getFee(childProvider); - resp = await childBridge.connect(childTestWallet).withdrawETH(amt, { - value: bridgeFee, - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }); + resp = await withdrawETH(childTestWallet, amt, bridgeFee, null); await waitForReceipt(resp.hash, childProvider); await waitUntilSucceed(axelarAPI, resp.hash); @@ -2027,4 +1727,123 @@ describe("Bridge e2e test", () => { // Test balance. }).timeout(2400000) + + async function depositIMX(sender: ethers.Wallet, amt: ethers.BigNumber, bridgeFee: ethers.BigNumber, recipient: string | null) { + // Approve + let resp = await rootIMX.connect(rootTestWallet).approve(rootBridge.address, amt); + await waitForReceipt(resp.hash, rootProvider); + + if (recipient == null) { + return rootBridge.connect(sender).deposit(rootIMX.address, amt, { + value: bridgeFee, + }); + } else { + return rootBridge.connect(sender).depositTo(rootIMX.address, recipient, amt, { + value: bridgeFee, + }); + } + } + + async function withdrawIMX(sender: ethers.Wallet, amt: ethers.BigNumber, bridgeFee: ethers.BigNumber, recipient: string | null) { + let [priorityFee, maxFee] = await getFee(childProvider); + + if (recipient == null) { + return childBridge.connect(sender).withdrawIMX(amt, { + value: amt.add(bridgeFee), + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + } else { + return childBridge.connect(sender).withdrawIMXTo(recipient, amt, { + value: amt.add(bridgeFee), + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + } + } + + async function withdrawWIMX(sender: ethers.Wallet, amt: ethers.BigNumber, bridgeFee: ethers.BigNumber, recipient: string | null) { + let [priorityFee, maxFee] = await getFee(childProvider); + + // Wrap IMX + let resp = await childWIMX.connect(sender).deposit({ + value: amt, + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); + + // Approve + resp = await childWIMX.connect(sender).approve(childBridge.address, amt, { + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); + + if (recipient == null) { + return childBridge.connect(sender).withdrawWIMX(amt, { + value: bridgeFee, + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + } else { + return childBridge.connect(sender).withdrawWIMXTo(recipient, amt, { + value: bridgeFee, + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + } + } + + async function depositETH(sender: ethers.Wallet, amt: ethers.BigNumber, bridgeFee: ethers.BigNumber, recipient: string | null) { + if (recipient == null) { + return rootBridge.connect(sender).depositETH(amt, { + value: amt.add(bridgeFee), + }); + } else { + return rootBridge.connect(sender).depositToETH(recipient, amt, { + value: amt.add(bridgeFee), + }); + } + } + + async function depositWETH(sender: ethers.Wallet, amt: ethers.BigNumber, bridgeFee: ethers.BigNumber, recipient: string | null) { + // Wrap ETH + let resp = await rootWETH.connect(sender).deposit({ + value: amt, + }) + await waitForReceipt(resp.hash, rootProvider); + + // Approve + resp = await rootBridge.connect(sender).approve(rootBridge.address, amt); + await waitForReceipt(resp.hash, rootProvider); + + if (recipient == null) { + return rootBridge.connect(sender).deposit(rootWETH.address, amt, { + value: bridgeFee, + }); + } else { + return rootBridge.connect(sender).depositTo(rootWETH.address, recipient, amt, { + value: bridgeFee, + }); + } + } + + async function withdrawETH(sender: ethers.Wallet, amt: ethers.BigNumber, bridgeFee: ethers.BigNumber, recipient: string | null) { + let [priorityFee, maxFee] = await getFee(childProvider); + + if (recipient == null) { + return childBridge.connect(sender).withdrawETH(amt, { + value: bridgeFee, + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + } else { + return childBridge.connect(sender).withdrawETHTo(recipient, amt, { + value: bridgeFee, + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }) + } + } }) \ No newline at end of file From f102a3e4492c0f9a6642d1f0d1913952ab092c35 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Mon, 12 Feb 2024 12:00:17 +1000 Subject: [PATCH 114/155] Update e2e.ts --- scripts/e2e/e2e.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/e2e/e2e.ts b/scripts/e2e/e2e.ts index 6badcd99..a99e519b 100644 --- a/scripts/e2e/e2e.ts +++ b/scripts/e2e/e2e.ts @@ -110,7 +110,7 @@ describe("Bridge e2e test", () => { })).to.be.rejectedWith("UNPREDICTABLE_GAS_LIMIT"); }).timeout(2400000) - it.only("should not deposit IMX if balance is insufficient", async() => { + it("should not deposit IMX if balance is insufficient", async() => { let balance = await rootIMX.balanceOf(rootTestWallet.address); let amt = balance.add(1); From b3bd1132c762d94d2c7a2da3da6d1bf3f431142c Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Mon, 12 Feb 2024 13:43:23 +1000 Subject: [PATCH 115/155] Update e2e.ts --- scripts/e2e/e2e.ts | 136 +++++++++++++++++---------------------------- 1 file changed, 52 insertions(+), 84 deletions(-) diff --git a/scripts/e2e/e2e.ts b/scripts/e2e/e2e.ts index a99e519b..258e7e5c 100644 --- a/scripts/e2e/e2e.ts +++ b/scripts/e2e/e2e.ts @@ -1246,31 +1246,14 @@ describe("Bridge e2e test", () => { let amt = balance.add(1); let bridgeFee = ethers.utils.parseEther("0.001"); - // Approve - let resp = await rootCustomToken.connect(rootTestWallet).approve(rootBridge.address, amt); - await waitForReceipt(resp.hash, rootProvider); - - await expect(rootBridge.connect(rootTestWallet).deposit(rootCustomToken.address, amt, { - value: bridgeFee, - })).to.be.rejectedWith("UNPREDICTABLE_GAS_LIMIT"); + await expect( + depositERC20(rootTestWallet, amt, bridgeFee, null) + ).to.be.rejectedWith("UNPREDICTABLE_GAS_LIMIT"); }).timeout(2400000) // Local only it("should not deposit mapped ERC20 Token if root bridge is paused", async() => { - // Transfer 0.1 ETH to root pauser - let resp = await rootTestWallet.sendTransaction({ - to: rootPauserWallet.address, - value: ethers.utils.parseEther("0.1"), - }) - await waitForReceipt(resp.hash, rootProvider); - - // Transfer 0.1 ETH to root unpauser - resp = await rootTestWallet.sendTransaction({ - to: rootPrivilegedWallet.address, - value: ethers.utils.parseEther("0.1"), - }) - await waitForReceipt(resp.hash, rootProvider); - + let resp; // Pause root bridge if (!await rootBridge.paused()) { resp = await rootBridge.connect(rootPauserWallet).pause(); @@ -1282,14 +1265,10 @@ describe("Bridge e2e test", () => { let amt = ethers.utils.parseEther("1.0"); let bridgeFee = ethers.utils.parseEther("0.001"); - // Approve - resp = await rootCustomToken.connect(rootTestWallet).approve(rootBridge.address, amt); - await waitForReceipt(resp.hash, rootProvider); - // Fail to deposit on L1 - await expect(rootBridge.connect(rootTestWallet).deposit(rootCustomToken.address, amt, { - value: bridgeFee, - })).to.be.rejectedWith("Pausable: paused"); + await expect( + depositERC20(rootTestWallet, amt, bridgeFee, null) + ).to.be.rejectedWith("Pausable: paused"); // Unpause root bridge resp = await rootBridge.connect(rootPrivilegedWallet).unpause(); @@ -1305,14 +1284,7 @@ describe("Bridge e2e test", () => { let amt = ethers.utils.parseEther("10.0"); let bridgeFee = ethers.utils.parseEther("0.001"); - // Approve - let resp = await rootCustomToken.connect(rootTestWallet).approve(rootBridge.address, amt); - await waitForReceipt(resp.hash, rootProvider); - - // Token deposit L1 to L2 - resp = await rootBridge.connect(rootTestWallet).deposit(rootCustomToken.address, amt, { - value: bridgeFee, - }) + let resp = await depositERC20(rootTestWallet, amt, bridgeFee, null); await waitForReceipt(resp.hash, rootProvider); let postBalL1 = await rootCustomToken.balanceOf(rootTestWallet.address); @@ -1341,14 +1313,7 @@ describe("Bridge e2e test", () => { let amt = ethers.utils.parseEther("1.0"); let bridgeFee = ethers.utils.parseEther("0.001"); - // Approve - let resp = await rootCustomToken.connect(rootTestWallet).approve(rootBridge.address, amt); - await waitForReceipt(resp.hash, rootProvider); - - // Token deposit L1 to L2 - resp = await rootBridge.connect(rootTestWallet).depositTo(rootCustomToken.address, childRecipient, amt, { - value: bridgeFee, - }) + let resp = await depositERC20(rootTestWallet, amt, bridgeFee, childRecipient); await waitForReceipt(resp.hash, rootProvider); let postBalL1 = await rootCustomToken.balanceOf(rootTestWallet.address); @@ -1384,20 +1349,7 @@ describe("Bridge e2e test", () => { // Local only it("should not withdraw mapped ERC20 Token if child bridge is paused", async() => { - // Transfer 0.1 IMX to child pauser - let resp = await childTestWallet.sendTransaction({ - to: childPauserWallet.address, - value: ethers.utils.parseEther("0.1"), - }) - await waitForReceipt(resp.hash, childProvider); - - // Transfer 0.1 IMX to child unpauser - resp = await childTestWallet.sendTransaction({ - to: childPrivilegedWallet.address, - value: ethers.utils.parseEther("0.1"), - }) - await waitForReceipt(resp.hash, childProvider); - + let resp; // Pause child bridge if (!await childBridge.paused()) { resp = await childBridge.connect(childPauserWallet).pause(); @@ -1409,12 +1361,9 @@ describe("Bridge e2e test", () => { let bridgeFee = ethers.utils.parseEther("1.0"); // Token withdraw L2 to L1 - let [priorityFee, maxFee] = await getFee(childProvider); - await expect(childBridge.connect(childTestWallet).withdraw(childCustomToken.address, amt, { - value: bridgeFee, - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - })).to.be.rejectedWith("Pausable: paused"); + await expect( + withdrawERC20(childTestWallet, amt, bridgeFee, null) + ).to.be.rejectedWith("Pausable: paused"); // Unpause child bridge resp = await childBridge.connect(childPrivilegedWallet).unpause(); @@ -1427,12 +1376,9 @@ describe("Bridge e2e test", () => { let bridgeFee = ethers.utils.parseEther("1.0"); // ETH withdraw L2 to L1 - let [priorityFee, maxFee] = await getFee(childProvider); - await expect(childBridge.connect(childTestWallet).withdraw(childCustomToken.address, amt.add(1), { - value: bridgeFee, - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - })).to.be.rejectedWith("UNPREDICTABLE_GAS_LIMIT"); + await expect( + withdrawERC20(childTestWallet, amt, bridgeFee, null) + ).to.be.rejectedWith("UNPREDICTABLE_GAS_LIMIT"); }).timeout(2400000) it("should successfully withdraw mapped ERC20 Token to self from L2 to L1", async() => { @@ -1443,13 +1389,7 @@ describe("Bridge e2e test", () => { let amt = ethers.utils.parseEther("0.5"); let bridgeFee = ethers.utils.parseEther("1.0"); - // Token withdraw L2 to L1 - let [priorityFee, maxFee] = await getFee(childProvider); - let resp = await childBridge.connect(childTestWallet).withdraw(childCustomToken.address, amt, { - value: bridgeFee, - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }) + let resp = await withdrawERC20(childTestWallet, amt, bridgeFee, null); await waitForReceipt(resp.hash, childProvider); let postBalL1 = preBalL1; @@ -1478,13 +1418,7 @@ describe("Bridge e2e test", () => { let amt = ethers.utils.parseEther("0.5"); let bridgeFee = ethers.utils.parseEther("1.0"); - // Token withdraw L2 to L1 - let [priorityFee, maxFee] = await getFee(childProvider); - let resp = await childBridge.connect(childTestWallet).withdrawTo(childCustomToken.address, rootRecipient, amt, { - value: bridgeFee, - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }) + let resp = await withdrawERC20(childTestWallet, amt, bridgeFee, rootRecipient); await waitForReceipt(resp.hash, childProvider); let postBalL1 = preBalL1; @@ -1846,4 +1780,38 @@ describe("Bridge e2e test", () => { }) } } + + async function depositERC20(sender: ethers.Wallet, amt: ethers.BigNumber, bridgeFee: ethers.BigNumber, recipient: string | null) { + // Approve + let resp = await rootCustomToken.connect(rootTestWallet).approve(rootBridge.address, amt); + await waitForReceipt(resp.hash, rootProvider); + + if (recipient == null) { + return rootBridge.connect(sender).deposit(rootTestWallet.address, amt, { + value: bridgeFee, + }); + } else { + return rootBridge.connect(sender).depositTo(rootTestWallet.address, recipient, amt, { + value: bridgeFee, + }); + } + } + + async function withdrawERC20(sender: ethers.Wallet, amt: ethers.BigNumber, bridgeFee: ethers.BigNumber, recipient: string | null) { + let [priorityFee, maxFee] = await getFee(childProvider); + + if (recipient == null) { + return childBridge.connect(sender).withdraw(childCustomToken.address, amt, { + value: bridgeFee, + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + } else { + return childBridge.connect(sender).withdrawTo(childCustomToken.address, recipient, amt, { + value: bridgeFee, + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + } + } }) \ No newline at end of file From ece226001f866cd63ce3e04413a1e0d019f57ad9 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Mon, 12 Feb 2024 13:55:13 +1000 Subject: [PATCH 116/155] Update e2e.ts --- scripts/e2e/e2e.ts | 36 +++++++++++++----------------------- 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/scripts/e2e/e2e.ts b/scripts/e2e/e2e.ts index 258e7e5c..ba87e0ef 100644 --- a/scripts/e2e/e2e.ts +++ b/scripts/e2e/e2e.ts @@ -80,20 +80,6 @@ describe("Bridge e2e test", () => { value: ethers.utils.parseEther("0.5"), }) await waitForReceipt(resp.hash, rootProvider); - - // Transfer 5 IMX to child pauser - resp = await childTestWallet.sendTransaction({ - to: childPauserWallet.address, - value: ethers.utils.parseEther("5"), - }) - await waitForReceipt(resp.hash, childProvider); - - // Transfer 5 IMX to child unpauser - resp = await childTestWallet.sendTransaction({ - to: childPrivilegedWallet.address, - value: ethers.utils.parseEther("5"), - }) - await waitForReceipt(resp.hash, childProvider); }) it("should not deposit IMX if allowance is insufficient", async() => { @@ -154,7 +140,7 @@ describe("Bridge e2e test", () => { await expect( depositIMX(rootTestWallet, amt, bridgeFee, null) - ).to.be.rejectedWith("ImxDepositLimitExceeded()"); + ).to.be.rejectedWith("UNPREDICTABLE_GAS_LIMIT"); }).timeout(2400000) it("should successfully deposit IMX to self from L1 to L2", async() => { @@ -216,17 +202,17 @@ describe("Bridge e2e test", () => { // Local only it("should not deposit IMX on L2 if child bridge is paused", async() => { - // Transfer 0.1 IMX to child pauser + // Transfer 5 IMX to child pauser let resp = await childTestWallet.sendTransaction({ to: childPauserWallet.address, - value: ethers.utils.parseEther("0.1"), + value: ethers.utils.parseEther("5"), }) await waitForReceipt(resp.hash, childProvider); - // Transfer 0.1 IMX to child unpauser + // Transfer 5 IMX to child unpauser resp = await childTestWallet.sendTransaction({ to: childPrivilegedWallet.address, - value: ethers.utils.parseEther("0.1"), + value: ethers.utils.parseEther("5"), }) await waitForReceipt(resp.hash, childProvider); @@ -541,12 +527,16 @@ describe("Bridge e2e test", () => { it("should not withdraw wIMX if balance is insufficient", async() => { let balance = await childWIMX.balanceOf(childTestWallet.address); - let amt = balance.add(1); + let amt = balance; let bridgeFee = ethers.utils.parseEther("1.0"); - await expect( - withdrawWIMX(childTestWallet, amt, bridgeFee, null) - ).to.be.rejectedWith("UNPREDICTABLE_GAS_LIMIT"); + // wIMX withdraw L2 to L1 + let [priorityFee, maxFee] = await getFee(childProvider); + await expect(childBridge.connect(childTestWallet).withdrawWIMX(amt.add(1), { + value: bridgeFee, + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + })).to.be.rejectedWith("UNPREDICTABLE_GAS_LIMIT"); }).timeout(2400000) it("should successfully withdraw wIMX to self from L2 to L1", async() => { From e7f5e3765713c84d6b94baa7a4468fc0378cabec Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Mon, 12 Feb 2024 14:04:45 +1000 Subject: [PATCH 117/155] Fix e2e --- scripts/e2e/e2e.ts | 206 +++++++++++++++++++++++++++++---------------- 1 file changed, 133 insertions(+), 73 deletions(-) diff --git a/scripts/e2e/e2e.ts b/scripts/e2e/e2e.ts index ba87e0ef..cc5db60d 100644 --- a/scripts/e2e/e2e.ts +++ b/scripts/e2e/e2e.ts @@ -481,12 +481,31 @@ describe("Bridge e2e test", () => { expect(await childBridge.paused()).to.true; } + // Wrap 1 IMX + let [priorityFee, maxFee] = await getFee(childProvider); + resp = await childWIMX.connect(childTestWallet).deposit({ + value: ethers.utils.parseEther("1.0"), + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); + let amt = ethers.utils.parseEther("0.5"); let bridgeFee = ethers.utils.parseEther("1.0"); - await expect( - withdrawWIMX(childTestWallet, amt, bridgeFee, null) - ).to.be.rejectedWith("Pausable: paused"); + // Approve + [priorityFee, maxFee] = await getFee(childProvider); + resp = await childWIMX.connect(childTestWallet).approve(childBridge.address, amt, { + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); + + await expect(childBridge.connect(childTestWallet).withdrawWIMX(amt, { + value: amt.add(bridgeFee), + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + })).to.be.rejectedWith("Pausable: paused"); // Unpause child bridge resp = await childBridge.connect(childPrivilegedWallet).unpause(); @@ -540,14 +559,37 @@ describe("Bridge e2e test", () => { }).timeout(2400000) it("should successfully withdraw wIMX to self from L2 to L1", async() => { - let amt = ethers.utils.parseEther("0.5"); - let bridgeFee = ethers.utils.parseEther("1.0"); + // Wrap 1 IMX + let [priorityFee, maxFee] = await getFee(childProvider); + let resp = await childWIMX.connect(childTestWallet).deposit({ + value: ethers.utils.parseEther("1.0"), + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); // Get IMX balance on root & child chains before withdraw let preBalL1 = await rootIMX.balanceOf(rootTestWallet.address); let preBalL2 = await childWIMX.balanceOf(childTestWallet.address); - let resp = await withdrawWIMX(rootTestWallet, amt, bridgeFee, null); + let amt = ethers.utils.parseEther("0.5"); + let bridgeFee = ethers.utils.parseEther("1.0"); + + // Approve + [priorityFee, maxFee] = await getFee(childProvider); + resp = await childWIMX.connect(childTestWallet).approve(childBridge.address, amt, { + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); + + // wIMX withdraw L2 to L1 + [priorityFee, maxFee] = await getFee(childProvider); + resp = await childBridge.connect(childTestWallet).withdrawWIMX(amt, { + value: bridgeFee, + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); await waitForReceipt(resp.hash, childProvider); let postBalL1 = preBalL1; @@ -569,13 +611,37 @@ describe("Bridge e2e test", () => { it("should successfully withdraw wIMX to others from L2 to L1", async() => { let rootRecipient = rootPrivilegedWallet.address; - let amt = ethers.utils.parseEther("0.5"); - let bridgeFee = ethers.utils.parseEther("1.0"); + // Wrap 1 IMX + let [priorityFee, maxFee] = await getFee(childProvider); + let resp = await childWIMX.connect(childTestWallet).deposit({ + value: ethers.utils.parseEther("1.0"), + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); + // Get IMX balance on root & child chains before withdraw let preBalL1 = await rootIMX.balanceOf(rootRecipient); let preBalL2 = await childWIMX.balanceOf(childTestWallet.address); - let resp = await withdrawWIMX(rootTestWallet, amt, bridgeFee, rootRecipient); + let amt = ethers.utils.parseEther("0.5"); + let bridgeFee = ethers.utils.parseEther("1.0"); + + // Approve + [priorityFee, maxFee] = await getFee(childProvider); + resp = await childWIMX.connect(childTestWallet).approve(childBridge.address, amt, { + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); + + // wIMX withdraw L2 to L1 + [priorityFee, maxFee] = await getFee(childProvider); + resp = await childBridge.connect(childTestWallet).withdrawWIMXTo(rootRecipient, amt, { + value: bridgeFee, + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); await waitForReceipt(resp.hash, childProvider); let postBalL1 = preBalL1; @@ -605,14 +671,37 @@ describe("Bridge e2e test", () => { expect(await rootBridge.paused()).to.true; } - let amt = ethers.utils.parseEther("0.5"); - let bridgeFee = ethers.utils.parseEther("1.0"); + // Wrap 1 IMX + let [priorityFee, maxFee] = await getFee(childProvider); + resp = await childWIMX.connect(childTestWallet).deposit({ + value: ethers.utils.parseEther("1.0"), + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); // Get IMX balance on root & child chains before withdraw let preBalL1 = await rootIMX.balanceOf(rootTestWallet.address); let preBalL2 = await childWIMX.balanceOf(childTestWallet.address); - resp = await withdrawWIMX(rootTestWallet, amt, bridgeFee, null); + let amt = ethers.utils.parseEther("0.5"); + let bridgeFee = ethers.utils.parseEther("1.0"); + + // Approve + [priorityFee, maxFee] = await getFee(childProvider); + resp = await childWIMX.connect(childTestWallet).approve(childBridge.address, amt, { + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); + await waitForReceipt(resp.hash, childProvider); + + // wIMX withdraw L2 to L1 + [priorityFee, maxFee] = await getFee(childProvider); + resp = await childBridge.connect(childTestWallet).withdrawWIMX(amt, { + value: bridgeFee, + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }); await waitForReceipt(resp.hash, childProvider); await waitUntilSucceed(axelarAPI, resp.hash); @@ -881,14 +970,27 @@ describe("Bridge e2e test", () => { }).timeout(2400000) it("should successfully deposit wETH to self from L1 to L2", async() => { - let amt = ethers.utils.parseEther("0.001"); - let bridgeFee = ethers.utils.parseEther("0.001"); + // Wrap 0.01 ETH + let resp = await rootWETH.connect(rootTestWallet).deposit({ + value: ethers.utils.parseEther("0.01"), + }) + await waitForReceipt(resp.hash, rootProvider); // Get ETH balance on root & child chains before withdraw let preBalL1 = await rootWETH.balanceOf(rootTestWallet.address); let preBalL2 = await childETH.balanceOf(childTestWallet.address); - let resp = await depositWETH(rootTestWallet, amt, bridgeFee, null); + let amt = ethers.utils.parseEther("0.001"); + let bridgeFee = ethers.utils.parseEther("0.001"); + + // Approve + resp = await rootWETH.connect(rootTestWallet).approve(rootBridge.address, amt); + await waitForReceipt(resp.hash, rootProvider); + + // wETH deposit L1 to L2 + resp = await rootBridge.connect(rootTestWallet).deposit(rootWETH.address, amt, { + value: bridgeFee, + }) await waitForReceipt(resp.hash, rootProvider); let postBalL1 = await rootWETH.balanceOf(rootTestWallet.address); @@ -910,14 +1012,27 @@ describe("Bridge e2e test", () => { it("should successfully deposit wETH to others from L1 to L2", async() => { let childRecipient = childPrivilegedWallet.address; - let amt = ethers.utils.parseEther("0.001"); - let bridgeFee = ethers.utils.parseEther("0.001"); + // Wrap 0.01 ETH + let resp = await rootWETH.connect(rootTestWallet).deposit({ + value: ethers.utils.parseEther("0.01"), + }) + await waitForReceipt(resp.hash, rootProvider); // Get ETH balance on root & child chains before withdraw let preBalL1 = await rootWETH.balanceOf(rootTestWallet.address); let preBalL2 = await childETH.balanceOf(childRecipient); - let resp = await depositWETH(rootTestWallet, amt, bridgeFee, childRecipient); + let amt = ethers.utils.parseEther("0.001"); + let bridgeFee = ethers.utils.parseEther("0.001"); + + // Approve + resp = await rootWETH.connect(rootTestWallet).approve(rootBridge.address, amt); + await waitForReceipt(resp.hash, rootProvider); + + // wETH deposit L1 to L2 + resp = await rootBridge.connect(rootTestWallet).depositTo(rootWETH.address, childRecipient, amt, { + value: bridgeFee, + }) await waitForReceipt(resp.hash, rootProvider); let postBalL1 = await rootWETH.balanceOf(rootTestWallet.address); @@ -1686,39 +1801,6 @@ describe("Bridge e2e test", () => { } } - async function withdrawWIMX(sender: ethers.Wallet, amt: ethers.BigNumber, bridgeFee: ethers.BigNumber, recipient: string | null) { - let [priorityFee, maxFee] = await getFee(childProvider); - - // Wrap IMX - let resp = await childWIMX.connect(sender).deposit({ - value: amt, - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }); - await waitForReceipt(resp.hash, childProvider); - - // Approve - resp = await childWIMX.connect(sender).approve(childBridge.address, amt, { - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }); - await waitForReceipt(resp.hash, childProvider); - - if (recipient == null) { - return childBridge.connect(sender).withdrawWIMX(amt, { - value: bridgeFee, - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }); - } else { - return childBridge.connect(sender).withdrawWIMXTo(recipient, amt, { - value: bridgeFee, - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }); - } - } - async function depositETH(sender: ethers.Wallet, amt: ethers.BigNumber, bridgeFee: ethers.BigNumber, recipient: string | null) { if (recipient == null) { return rootBridge.connect(sender).depositETH(amt, { @@ -1731,28 +1813,6 @@ describe("Bridge e2e test", () => { } } - async function depositWETH(sender: ethers.Wallet, amt: ethers.BigNumber, bridgeFee: ethers.BigNumber, recipient: string | null) { - // Wrap ETH - let resp = await rootWETH.connect(sender).deposit({ - value: amt, - }) - await waitForReceipt(resp.hash, rootProvider); - - // Approve - resp = await rootBridge.connect(sender).approve(rootBridge.address, amt); - await waitForReceipt(resp.hash, rootProvider); - - if (recipient == null) { - return rootBridge.connect(sender).deposit(rootWETH.address, amt, { - value: bridgeFee, - }); - } else { - return rootBridge.connect(sender).depositTo(rootWETH.address, recipient, amt, { - value: bridgeFee, - }); - } - } - async function withdrawETH(sender: ethers.Wallet, amt: ethers.BigNumber, bridgeFee: ethers.BigNumber, recipient: string | null) { let [priorityFee, maxFee] = await getFee(childProvider); From edc3b7516c3856c974f8d288e4158c75f3835deb Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Mon, 12 Feb 2024 14:21:59 +1000 Subject: [PATCH 118/155] Fix e2e --- scripts/e2e/e2e.ts | 139 ++++++++++++++++++++++++++++----------------- 1 file changed, 86 insertions(+), 53 deletions(-) diff --git a/scripts/e2e/e2e.ts b/scripts/e2e/e2e.ts index cc5db60d..7998941d 100644 --- a/scripts/e2e/e2e.ts +++ b/scripts/e2e/e2e.ts @@ -1077,7 +1077,8 @@ describe("Bridge e2e test", () => { }).timeout(2400000) it("should not withdraw ETH if balance is insufficient", async() => { - let amt = await childETH.balanceOf(childTestWallet.address).add(1); + let amt = await childETH.balanceOf(childTestWallet.address); + amt = amt.add(1); let bridgeFee = ethers.utils.parseEther("1.0"); // ETH withdraw L2 to L1 @@ -1351,14 +1352,31 @@ describe("Bridge e2e test", () => { let amt = balance.add(1); let bridgeFee = ethers.utils.parseEther("0.001"); - await expect( - depositERC20(rootTestWallet, amt, bridgeFee, null) - ).to.be.rejectedWith("UNPREDICTABLE_GAS_LIMIT"); + // Approve + let resp = await rootCustomToken.connect(rootTestWallet).approve(rootBridge.address, amt); + await waitForReceipt(resp.hash, rootProvider); + + await expect(rootBridge.connect(rootTestWallet).deposit(rootCustomToken.address, amt, { + value: bridgeFee, + })).to.be.rejectedWith("UNPREDICTABLE_GAS_LIMIT"); }).timeout(2400000) // Local only it("should not deposit mapped ERC20 Token if root bridge is paused", async() => { - let resp; + // Transfer 0.1 ETH to root pauser + let resp = await rootTestWallet.sendTransaction({ + to: rootPauserWallet.address, + value: ethers.utils.parseEther("0.1"), + }) + await waitForReceipt(resp.hash, rootProvider); + + // Transfer 0.1 ETH to root unpauser + resp = await rootTestWallet.sendTransaction({ + to: rootPrivilegedWallet.address, + value: ethers.utils.parseEther("0.1"), + }) + await waitForReceipt(resp.hash, rootProvider); + // Pause root bridge if (!await rootBridge.paused()) { resp = await rootBridge.connect(rootPauserWallet).pause(); @@ -1370,10 +1388,14 @@ describe("Bridge e2e test", () => { let amt = ethers.utils.parseEther("1.0"); let bridgeFee = ethers.utils.parseEther("0.001"); + // Approve + resp = await rootCustomToken.connect(rootTestWallet).approve(rootBridge.address, amt); + await waitForReceipt(resp.hash, rootProvider); + // Fail to deposit on L1 - await expect( - depositERC20(rootTestWallet, amt, bridgeFee, null) - ).to.be.rejectedWith("Pausable: paused"); + await expect(rootBridge.connect(rootTestWallet).deposit(rootCustomToken.address, amt, { + value: bridgeFee, + })).to.be.rejectedWith("Pausable: paused"); // Unpause root bridge resp = await rootBridge.connect(rootPrivilegedWallet).unpause(); @@ -1389,7 +1411,14 @@ describe("Bridge e2e test", () => { let amt = ethers.utils.parseEther("10.0"); let bridgeFee = ethers.utils.parseEther("0.001"); - let resp = await depositERC20(rootTestWallet, amt, bridgeFee, null); + // Approve + let resp = await rootCustomToken.connect(rootTestWallet).approve(rootBridge.address, amt); + await waitForReceipt(resp.hash, rootProvider); + + // Token deposit L1 to L2 + resp = await rootBridge.connect(rootTestWallet).deposit(rootCustomToken.address, amt, { + value: bridgeFee, + }) await waitForReceipt(resp.hash, rootProvider); let postBalL1 = await rootCustomToken.balanceOf(rootTestWallet.address); @@ -1418,7 +1447,14 @@ describe("Bridge e2e test", () => { let amt = ethers.utils.parseEther("1.0"); let bridgeFee = ethers.utils.parseEther("0.001"); - let resp = await depositERC20(rootTestWallet, amt, bridgeFee, childRecipient); + // Approve + let resp = await rootCustomToken.connect(rootTestWallet).approve(rootBridge.address, amt); + await waitForReceipt(resp.hash, rootProvider); + + // Token deposit L1 to L2 + resp = await rootBridge.connect(rootTestWallet).depositTo(rootCustomToken.address, childRecipient, amt, { + value: bridgeFee, + }) await waitForReceipt(resp.hash, rootProvider); let postBalL1 = await rootCustomToken.balanceOf(rootTestWallet.address); @@ -1454,7 +1490,20 @@ describe("Bridge e2e test", () => { // Local only it("should not withdraw mapped ERC20 Token if child bridge is paused", async() => { - let resp; + // Transfer 0.1 IMX to child pauser + let resp = await childTestWallet.sendTransaction({ + to: childPauserWallet.address, + value: ethers.utils.parseEther("0.1"), + }) + await waitForReceipt(resp.hash, childProvider); + + // Transfer 0.1 IMX to child unpauser + resp = await childTestWallet.sendTransaction({ + to: childPrivilegedWallet.address, + value: ethers.utils.parseEther("0.1"), + }) + await waitForReceipt(resp.hash, childProvider); + // Pause child bridge if (!await childBridge.paused()) { resp = await childBridge.connect(childPauserWallet).pause(); @@ -1466,9 +1515,12 @@ describe("Bridge e2e test", () => { let bridgeFee = ethers.utils.parseEther("1.0"); // Token withdraw L2 to L1 - await expect( - withdrawERC20(childTestWallet, amt, bridgeFee, null) - ).to.be.rejectedWith("Pausable: paused"); + let [priorityFee, maxFee] = await getFee(childProvider); + await expect(childBridge.connect(childTestWallet).withdraw(childCustomToken.address, amt, { + value: bridgeFee, + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + })).to.be.rejectedWith("Pausable: paused"); // Unpause child bridge resp = await childBridge.connect(childPrivilegedWallet).unpause(); @@ -1481,9 +1533,12 @@ describe("Bridge e2e test", () => { let bridgeFee = ethers.utils.parseEther("1.0"); // ETH withdraw L2 to L1 - await expect( - withdrawERC20(childTestWallet, amt, bridgeFee, null) - ).to.be.rejectedWith("UNPREDICTABLE_GAS_LIMIT"); + let [priorityFee, maxFee] = await getFee(childProvider); + await expect(childBridge.connect(childTestWallet).withdraw(childCustomToken.address, amt.add(1), { + value: bridgeFee, + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + })).to.be.rejectedWith("UNPREDICTABLE_GAS_LIMIT"); }).timeout(2400000) it("should successfully withdraw mapped ERC20 Token to self from L2 to L1", async() => { @@ -1494,7 +1549,13 @@ describe("Bridge e2e test", () => { let amt = ethers.utils.parseEther("0.5"); let bridgeFee = ethers.utils.parseEther("1.0"); - let resp = await withdrawERC20(childTestWallet, amt, bridgeFee, null); + // Token withdraw L2 to L1 + let [priorityFee, maxFee] = await getFee(childProvider); + let resp = await childBridge.connect(childTestWallet).withdraw(childCustomToken.address, amt, { + value: bridgeFee, + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }) await waitForReceipt(resp.hash, childProvider); let postBalL1 = preBalL1; @@ -1523,7 +1584,13 @@ describe("Bridge e2e test", () => { let amt = ethers.utils.parseEther("0.5"); let bridgeFee = ethers.utils.parseEther("1.0"); - let resp = await withdrawERC20(childTestWallet, amt, bridgeFee, rootRecipient); + // Token withdraw L2 to L1 + let [priorityFee, maxFee] = await getFee(childProvider); + let resp = await childBridge.connect(childTestWallet).withdrawTo(childCustomToken.address, rootRecipient, amt, { + value: bridgeFee, + maxPriorityFeePerGas: priorityFee, + maxFeePerGas: maxFee, + }) await waitForReceipt(resp.hash, childProvider); let postBalL1 = preBalL1; @@ -1830,38 +1897,4 @@ describe("Bridge e2e test", () => { }) } } - - async function depositERC20(sender: ethers.Wallet, amt: ethers.BigNumber, bridgeFee: ethers.BigNumber, recipient: string | null) { - // Approve - let resp = await rootCustomToken.connect(rootTestWallet).approve(rootBridge.address, amt); - await waitForReceipt(resp.hash, rootProvider); - - if (recipient == null) { - return rootBridge.connect(sender).deposit(rootTestWallet.address, amt, { - value: bridgeFee, - }); - } else { - return rootBridge.connect(sender).depositTo(rootTestWallet.address, recipient, amt, { - value: bridgeFee, - }); - } - } - - async function withdrawERC20(sender: ethers.Wallet, amt: ethers.BigNumber, bridgeFee: ethers.BigNumber, recipient: string | null) { - let [priorityFee, maxFee] = await getFee(childProvider); - - if (recipient == null) { - return childBridge.connect(sender).withdraw(childCustomToken.address, amt, { - value: bridgeFee, - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }); - } else { - return childBridge.connect(sender).withdrawTo(childCustomToken.address, recipient, amt, { - value: bridgeFee, - maxPriorityFeePerGas: priorityFee, - maxFeePerGas: maxFee, - }); - } - } }) \ No newline at end of file From ce85db39f8597d5bc61b73d14654fbcf32fae898 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Tue, 13 Feb 2024 09:41:48 +1000 Subject: [PATCH 119/155] Fix test --- test/fuzz/child/ChildERC20Bridge.t.sol | 34 +++++++++++++++----------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/test/fuzz/child/ChildERC20Bridge.t.sol b/test/fuzz/child/ChildERC20Bridge.t.sol index dfddc41f..c95369be 100644 --- a/test/fuzz/child/ChildERC20Bridge.t.sol +++ b/test/fuzz/child/ChildERC20Bridge.t.sol @@ -53,8 +53,8 @@ contract ChildERC20BridgeTest is Test, IChildERC20BridgeEvents { } function testFuzz_MapToken(address rootToken, string memory name, string memory symbol, uint8 decimals) public { - vm.assume(rootToken != address(0) && bytes(name).length != 0 && bytes(symbol).length != 0 && decimals > 0); - vm.assume(rootToken != bridge.NATIVE_ETH() && rootToken != bridge.NATIVE_IMX()); + vm.assume(rootToken > address(10) && bytes(name).length != 0 && bytes(symbol).length != 0 && decimals > 0); + vm.assume(rootToken != bridge.NATIVE_ETH() && rootToken != bridge.NATIVE_IMX() && rootToken != ROOT_IMX_TOKEN); // Map token on L1 triggers call on child bridge. bytes memory data = abi.encode(MAP_TOKEN_SIG, rootToken, name, symbol, decimals); @@ -78,11 +78,12 @@ contract ChildERC20BridgeTest is Test, IChildERC20BridgeEvents { } function testFuzz_DepositIMX(address sender, address recipient, uint256 depositAmt) public { - vm.assume(sender != address(0) && recipient != address(0) && depositAmt > 0); + vm.assume(sender > address(10) && recipient > address(10) && depositAmt > 0); + vm.assume(sender.code.length == 0 && recipient.code.length == 0); + vm.assume(recipient.balance == 0); vm.deal(address(bridge), depositAmt); assertEq(address(bridge).balance, depositAmt, "Bridge should have depositAmt of IMX"); - assertEq(recipient.balance, 0, "Recipient should have 0 IMX"); // Deposit IMX on L1 triggers call on child bridge. bytes memory data = abi.encode(DEPOSIT_SIG, bridge.rootIMXToken(), sender, recipient, depositAmt); @@ -97,7 +98,8 @@ contract ChildERC20BridgeTest is Test, IChildERC20BridgeEvents { } function testFuzz_WithdrawIMX(address user, uint256 balance, uint256 gasAmt, uint256 withdrawAmt) public { - vm.assume(user != address(0)); + vm.assume(user > address(10)); + vm.assume(user.code.length == 0); vm.assume(balance > 0 && withdrawAmt > 0 && gasAmt > 0); vm.assume(balance < type(uint256).max - gasAmt); vm.assume(balance > withdrawAmt && balance - withdrawAmt > gasAmt); @@ -133,7 +135,8 @@ contract ChildERC20BridgeTest is Test, IChildERC20BridgeEvents { } function testFuzz_WithdrawWIMX(address user, uint256 balance, uint256 gasAmt, uint256 withdrawAmt) public { - vm.assume(user != address(0)); + vm.assume(user > address(10)); + vm.assume(user.code.length == 0); vm.assume(balance > 0 && withdrawAmt > 0 && gasAmt > 0); vm.assume(balance < type(uint256).max); vm.assume(balance > withdrawAmt && balance - withdrawAmt > gasAmt); @@ -181,8 +184,8 @@ contract ChildERC20BridgeTest is Test, IChildERC20BridgeEvents { } function testFuzz_DepositETH(address sender, address recipient, uint256 depositAmt) public { - vm.assume(sender != address(0) && recipient != address(0) && depositAmt > 0); - + vm.assume(sender > address(10) && recipient > address(10) && depositAmt > 0); + vm.assume(sender.code.length == 0 && recipient.code.length == 0); assertEq(IChildERC20(bridge.childETHToken()).balanceOf(recipient), 0, "Recipient should have 0 ETH"); // Deposit ETH on L1 triggers call on child bridge. @@ -201,7 +204,8 @@ contract ChildERC20BridgeTest is Test, IChildERC20BridgeEvents { } function testFuzz_WithdrawETH(address user, uint256 balance, uint256 gasAmt, uint256 withdrawAmt) public { - vm.assume(user != address(0)); + vm.assume(user > address(10)); + vm.assume(user.code.length == 0); vm.assume(balance > 0 && withdrawAmt > 0 && gasAmt > 0); vm.assume(balance < type(uint256).max); vm.assume(balance > withdrawAmt); @@ -242,9 +246,11 @@ contract ChildERC20BridgeTest is Test, IChildERC20BridgeEvents { } function testFuzz_DepositERC20(address rootToken, address sender, address recipient, uint256 depositAmt) public { - vm.assume(rootToken != bridge.NATIVE_ETH() && rootToken != bridge.NATIVE_IMX()); - vm.assume(rootToken != address(0) && rootToken != bridge.NATIVE_ETH() && rootToken != bridge.NATIVE_IMX()); - vm.assume(sender != address(0) && recipient != address(0) && depositAmt > 0); + vm.assume( + rootToken > address(10) && rootToken != bridge.NATIVE_ETH() && rootToken != bridge.NATIVE_IMX() + && rootToken != ROOT_IMX_TOKEN + ); + vm.assume(sender > address(10) && recipient > address(10) && depositAmt > 0); // Map bytes memory data = abi.encode(MAP_TOKEN_SIG, rootToken, "Test token", "Test", 18); @@ -274,8 +280,8 @@ contract ChildERC20BridgeTest is Test, IChildERC20BridgeEvents { uint256 gasAmt, uint256 withdrawAmt ) public { - vm.assume(rootToken != bridge.NATIVE_ETH() && rootToken != bridge.NATIVE_IMX()); - vm.assume(rootToken != address(0) && user != address(0)); + vm.assume(rootToken != bridge.NATIVE_ETH() && rootToken != bridge.NATIVE_IMX() && rootToken != ROOT_IMX_TOKEN); + vm.assume(rootToken > address(10) && user > address(10)); vm.assume(balance > 0 && withdrawAmt > 0 && gasAmt > 0); vm.assume(balance < type(uint256).max); vm.assume(balance > withdrawAmt); From ebeb6e0e05aa476298b1131b6fd794f47612a00f Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Tue, 13 Feb 2024 09:45:42 +1000 Subject: [PATCH 120/155] Fix test --- test/fuzz/child/ChildERC20.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/fuzz/child/ChildERC20.t.sol b/test/fuzz/child/ChildERC20.t.sol index 33faba5d..cb55e668 100644 --- a/test/fuzz/child/ChildERC20.t.sol +++ b/test/fuzz/child/ChildERC20.t.sol @@ -18,7 +18,7 @@ contract ChildERC20Test is Test { } function testFuzz_Mint(address user, uint256 amount) public { - vm.assume(user != address(0)); + vm.assume(user != address(0) && user != address(this)); assertEq(childToken.balanceOf(user), 0, "User should not have balance before mint"); @@ -32,7 +32,7 @@ contract ChildERC20Test is Test { } function testFuzz_Burn(address user, uint256 balance, uint256 burnAmt) public { - vm.assume(user != address(0)); + vm.assume(user != address(0) && user != address(this)); vm.assume(balance < type(uint256).max); vm.assume(burnAmt < balance); From 6d1bcb2f9c4678c65fe45dffbdfe15c8d9bfcd01 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Tue, 13 Feb 2024 10:17:41 +1000 Subject: [PATCH 121/155] Fix --- test/fuzz/child/ChildERC20Bridge.t.sol | 77 ++++++++++++++------------ test/fuzz/root/RootERC20Bridge.t.sol | 35 +++++++++--- 2 files changed, 69 insertions(+), 43 deletions(-) diff --git a/test/fuzz/child/ChildERC20Bridge.t.sol b/test/fuzz/child/ChildERC20Bridge.t.sol index c95369be..69feaa6f 100644 --- a/test/fuzz/child/ChildERC20Bridge.t.sol +++ b/test/fuzz/child/ChildERC20Bridge.t.sol @@ -53,8 +53,8 @@ contract ChildERC20BridgeTest is Test, IChildERC20BridgeEvents { } function testFuzz_MapToken(address rootToken, string memory name, string memory symbol, uint8 decimals) public { - vm.assume(rootToken > address(10) && bytes(name).length != 0 && bytes(symbol).length != 0 && decimals > 0); - vm.assume(rootToken != bridge.NATIVE_ETH() && rootToken != bridge.NATIVE_IMX() && rootToken != ROOT_IMX_TOKEN); + assumeValidRootToken(rootToken); + vm.assume(bytes(name).length != 0 && bytes(symbol).length != 0 && decimals > 0); // Map token on L1 triggers call on child bridge. bytes memory data = abi.encode(MAP_TOKEN_SIG, rootToken, name, symbol, decimals); @@ -78,11 +78,10 @@ contract ChildERC20BridgeTest is Test, IChildERC20BridgeEvents { } function testFuzz_DepositIMX(address sender, address recipient, uint256 depositAmt) public { - vm.assume(sender > address(10) && recipient > address(10) && depositAmt > 0); - vm.assume(sender.code.length == 0 && recipient.code.length == 0); - vm.assume(recipient.balance == 0); - vm.deal(address(bridge), depositAmt); + assumeValidUsers(sender, recipient); + vm.assume(depositAmt > 0); + vm.deal(address(bridge), depositAmt); assertEq(address(bridge).balance, depositAmt, "Bridge should have depositAmt of IMX"); // Deposit IMX on L1 triggers call on child bridge. @@ -98,11 +97,9 @@ contract ChildERC20BridgeTest is Test, IChildERC20BridgeEvents { } function testFuzz_WithdrawIMX(address user, uint256 balance, uint256 gasAmt, uint256 withdrawAmt) public { - vm.assume(user > address(10)); - vm.assume(user.code.length == 0); - vm.assume(balance > 0 && withdrawAmt > 0 && gasAmt > 0); - vm.assume(balance < type(uint256).max - gasAmt); - vm.assume(balance > withdrawAmt && balance - withdrawAmt > gasAmt); + assumeValidUser(user); + vm.assume(withdrawAmt > 0 && gasAmt > 0); + vm.assume(balance > withdrawAmt && balance - withdrawAmt > gasAmt && balance < type(uint256).max - gasAmt); // Fund user vm.deal(user, balance); @@ -135,11 +132,9 @@ contract ChildERC20BridgeTest is Test, IChildERC20BridgeEvents { } function testFuzz_WithdrawWIMX(address user, uint256 balance, uint256 gasAmt, uint256 withdrawAmt) public { - vm.assume(user > address(10)); - vm.assume(user.code.length == 0); - vm.assume(balance > 0 && withdrawAmt > 0 && gasAmt > 0); - vm.assume(balance < type(uint256).max); - vm.assume(balance > withdrawAmt && balance - withdrawAmt > gasAmt); + assumeValidUser(user); + vm.assume(withdrawAmt > 0 && gasAmt > 0); + vm.assume(balance > withdrawAmt && balance - withdrawAmt > gasAmt && balance < type(uint256).max - gasAmt); // Fund user vm.deal(user, balance); @@ -184,9 +179,8 @@ contract ChildERC20BridgeTest is Test, IChildERC20BridgeEvents { } function testFuzz_DepositETH(address sender, address recipient, uint256 depositAmt) public { - vm.assume(sender > address(10) && recipient > address(10) && depositAmt > 0); - vm.assume(sender.code.length == 0 && recipient.code.length == 0); - assertEq(IChildERC20(bridge.childETHToken()).balanceOf(recipient), 0, "Recipient should have 0 ETH"); + assumeValidUsers(sender, recipient); + vm.assume(depositAmt > 0); // Deposit ETH on L1 triggers call on child bridge. bytes memory data = abi.encode(DEPOSIT_SIG, bridge.NATIVE_ETH(), sender, recipient, depositAmt); @@ -204,11 +198,9 @@ contract ChildERC20BridgeTest is Test, IChildERC20BridgeEvents { } function testFuzz_WithdrawETH(address user, uint256 balance, uint256 gasAmt, uint256 withdrawAmt) public { - vm.assume(user > address(10)); - vm.assume(user.code.length == 0); - vm.assume(balance > 0 && withdrawAmt > 0 && gasAmt > 0); - vm.assume(balance < type(uint256).max); - vm.assume(balance > withdrawAmt); + assumeValidUser(user); + vm.assume(withdrawAmt > 0 && gasAmt > 0); + vm.assume(balance > withdrawAmt && balance - withdrawAmt > gasAmt && balance < type(uint256).max - gasAmt); // Fund user vm.deal(user, gasAmt); @@ -246,11 +238,9 @@ contract ChildERC20BridgeTest is Test, IChildERC20BridgeEvents { } function testFuzz_DepositERC20(address rootToken, address sender, address recipient, uint256 depositAmt) public { - vm.assume( - rootToken > address(10) && rootToken != bridge.NATIVE_ETH() && rootToken != bridge.NATIVE_IMX() - && rootToken != ROOT_IMX_TOKEN - ); - vm.assume(sender > address(10) && recipient > address(10) && depositAmt > 0); + assumeValidRootToken(rootToken); + assumeValidUsers(sender, recipient); + vm.assume(depositAmt > 0); // Map bytes memory data = abi.encode(MAP_TOKEN_SIG, rootToken, "Test token", "Test", 18); @@ -280,11 +270,10 @@ contract ChildERC20BridgeTest is Test, IChildERC20BridgeEvents { uint256 gasAmt, uint256 withdrawAmt ) public { - vm.assume(rootToken != bridge.NATIVE_ETH() && rootToken != bridge.NATIVE_IMX() && rootToken != ROOT_IMX_TOKEN); - vm.assume(rootToken > address(10) && user > address(10)); - vm.assume(balance > 0 && withdrawAmt > 0 && gasAmt > 0); - vm.assume(balance < type(uint256).max); - vm.assume(balance > withdrawAmt); + assumeValidRootToken(rootToken); + assumeValidUser(user); + vm.assume(withdrawAmt > 0 && gasAmt > 0); + vm.assume(balance > withdrawAmt && balance - withdrawAmt > gasAmt && balance < type(uint256).max - gasAmt); // Map bytes memory data = abi.encode(MAP_TOKEN_SIG, rootToken, "Test token", "Test", 18); @@ -325,4 +314,24 @@ contract ChildERC20BridgeTest is Test, IChildERC20BridgeEvents { vm.stopPrank(); } + + function assumeValidRootToken(address rootToken) internal view { + vm.assume(rootToken > address(10)); + vm.assume(rootToken != bridge.NATIVE_ETH() && rootToken != bridge.NATIVE_IMX() && rootToken != ROOT_IMX_TOKEN); + } + + function assumeValidUsers(address user1, address user2) internal view { + vm.assume(user1 != user2); + assumeValidUser(user1); + assumeValidUser(user2); + } + + function assumeValidUser(address user) internal view { + vm.assume(user > address(10)); + vm.assume(user.balance == 0); + vm.assume(user.code.length == 0); + vm.assume(childTokenTemplate.balanceOf(user) == 0); + vm.assume(wIMX.balanceOf(user) == 0); + vm.assume(IChildERC20(bridge.childETHToken()).balanceOf(user) == 0); + } } diff --git a/test/fuzz/root/RootERC20Bridge.t.sol b/test/fuzz/root/RootERC20Bridge.t.sol index 8c23662b..f4e970ea 100644 --- a/test/fuzz/root/RootERC20Bridge.t.sol +++ b/test/fuzz/root/RootERC20Bridge.t.sol @@ -61,7 +61,7 @@ contract RootERC20BridgeTest is Test, IRootERC20BridgeEvents { function testFuzz_MapToken(address user, uint256 gasAmt, string memory name, string memory symbol, uint8 decimals) public { - vm.assume(user != address(0)); + assumeValidUser(user); vm.assume(gasAmt > 0); vm.assume(bytes(name).length != 0 && bytes(symbol).length != 0 && decimals > 0); @@ -92,7 +92,7 @@ contract RootERC20BridgeTest is Test, IRootERC20BridgeEvents { function testFuzz_DepositIMX(address sender, address recipient, uint256 balance, uint256 gasAmt, uint256 depositAmt) public { - vm.assume(sender != address(0) && recipient != address(0)); + assumeValidUsers(sender, recipient); vm.assume(balance > 0 && depositAmt > 0 && gasAmt > 0); vm.assume(balance > depositAmt && balance < type(uint256).max); vm.assume(depositAmt <= IMX_DEPOSITS_LIMIT); @@ -138,7 +138,8 @@ contract RootERC20BridgeTest is Test, IRootERC20BridgeEvents { } function testFuzz_WithdrawIMX(address sender, address recipient, uint256 withdrawAmt) public { - vm.assume(sender != address(0) && recipient != address(0) && withdrawAmt > 0); + assumeValidUsers(sender, recipient); + vm.assume(withdrawAmt > 0); imxToken.mint(address(bridge), withdrawAmt); @@ -163,7 +164,7 @@ contract RootERC20BridgeTest is Test, IRootERC20BridgeEvents { function testFuzz_DepositETH(address sender, address recipient, uint256 balance, uint256 gasAmt, uint256 depositAmt) public { - vm.assume(sender != address(0) && recipient != address(0)); + assumeValidUsers(sender, recipient); vm.assume(balance > 0 && depositAmt > 0 && gasAmt > 0); vm.assume(balance > depositAmt && balance < type(uint256).max - gasAmt && balance - depositAmt > gasAmt); @@ -172,7 +173,6 @@ contract RootERC20BridgeTest is Test, IRootERC20BridgeEvents { vm.startPrank(sender); // Before deposit - assertEq(sender.balance, balance, "Sender should have given balance"); assertEq(address(bridge).balance, 0, "Bridge should have 0 balance"); // Deposit out of balance should fail @@ -204,7 +204,7 @@ contract RootERC20BridgeTest is Test, IRootERC20BridgeEvents { uint256 gasAmt, uint256 depositAmt ) public { - vm.assume(sender != address(0) && recipient != address(0)); + assumeValidUsers(sender, recipient); vm.assume(balance > 0 && depositAmt > 0 && gasAmt > 0); vm.assume(balance > depositAmt && balance < type(uint256).max - gasAmt && balance - depositAmt > gasAmt); @@ -250,7 +250,8 @@ contract RootERC20BridgeTest is Test, IRootERC20BridgeEvents { } function testFuzz_WithdrawETH(address sender, address recipient, uint256 withdrawAmt) public { - vm.assume(sender != address(0) && recipient != address(0) && withdrawAmt > 0); + assumeValidUsers(sender, recipient); + vm.assume(withdrawAmt > 0); vm.deal(address(bridge), withdrawAmt); @@ -279,7 +280,7 @@ contract RootERC20BridgeTest is Test, IRootERC20BridgeEvents { uint256 gasAmt, uint256 depositAmt ) public { - vm.assume(sender != address(0) && recipient != address(0)); + assumeValidUsers(sender, recipient); vm.assume(balance > 0 && depositAmt > 0 && gasAmt > 0); vm.assume(balance > depositAmt && balance < type(uint256).max); vm.assume(gasAmt < 100); @@ -331,7 +332,8 @@ contract RootERC20BridgeTest is Test, IRootERC20BridgeEvents { } function testFuzz_WithdrawERC20(address sender, address recipient, uint256 withdrawAmt) public { - vm.assume(sender != address(0) && recipient != address(0) && withdrawAmt > 0); + assumeValidUsers(sender, recipient); + vm.assume(withdrawAmt > 0); // Map token ChildERC20 rootToken = new ChildERC20(); @@ -360,4 +362,19 @@ contract RootERC20BridgeTest is Test, IRootERC20BridgeEvents { vm.stopPrank(); } + + function assumeValidUsers(address user1, address user2) internal view { + vm.assume(user1 != user2); + assumeValidUser(user1); + assumeValidUser(user2); + } + + function assumeValidUser(address user) internal view { + vm.assume(user > address(10)); + vm.assume(user.balance == 0); + vm.assume(user.code.length == 0); + vm.assume(childTokenTemplate.balanceOf(user) == 0); + vm.assume(imxToken.balanceOf(user) == 0); + vm.assume(wETH.balanceOf(user) == 0); + } } From 4a70af5feb007f671a5a9c6aab27c31c38e4db5d Mon Sep 17 00:00:00 2001 From: Zhenyang Shi Date: Thu, 15 Feb 2024 11:27:12 +1000 Subject: [PATCH 122/155] Update test/fuzz/child/ChildERC20Bridge.t.sol Co-authored-by: Ermyas Abebe --- test/fuzz/child/ChildERC20Bridge.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/fuzz/child/ChildERC20Bridge.t.sol b/test/fuzz/child/ChildERC20Bridge.t.sol index 69feaa6f..9136900e 100644 --- a/test/fuzz/child/ChildERC20Bridge.t.sol +++ b/test/fuzz/child/ChildERC20Bridge.t.sol @@ -71,7 +71,7 @@ contract ChildERC20BridgeTest is Test, IChildERC20BridgeEvents { assertEq( bridge.rootTokenToChildToken(rootToken), childTokenAddress, - "Child actual token address should match predicated address" + "Child actual token address should match predicted address" ); vm.stopPrank(); From b0d64a1a964ec6837a2c91b58beefa1c456debdf Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Wed, 21 Feb 2024 01:44:44 +1000 Subject: [PATCH 123/155] Fix crash --- test/invariant/InvariantBridge.t.sol | 184 ++++++++++++++++++ test/invariant/MockAdaptor.sol | 42 ++++ .../child/ChildERC20BridgeHandler.sol | 89 +++++++++ test/invariant/child/ChildHelper.sol | 25 +++ .../root/RootERC20BridgeFlowRateHandler.sol | 69 +++++++ test/invariant/root/RootHelper.sol | 48 +++++ 6 files changed, 457 insertions(+) create mode 100644 test/invariant/InvariantBridge.t.sol create mode 100644 test/invariant/MockAdaptor.sol create mode 100644 test/invariant/child/ChildERC20BridgeHandler.sol create mode 100644 test/invariant/child/ChildHelper.sol create mode 100644 test/invariant/root/RootERC20BridgeFlowRateHandler.sol create mode 100644 test/invariant/root/RootHelper.sol diff --git a/test/invariant/InvariantBridge.t.sol b/test/invariant/InvariantBridge.t.sol new file mode 100644 index 00000000..9e4945f2 --- /dev/null +++ b/test/invariant/InvariantBridge.t.sol @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {ChildERC20} from "../../src/child/ChildERC20.sol"; +import {WIMX} from "../../src/child/WIMX.sol"; +import {IChildERC20Bridge, ChildERC20Bridge} from "../../src/child/ChildERC20Bridge.sol"; +import {IRootERC20Bridge, IERC20Metadata} from "../../src/root/RootERC20Bridge.sol"; +import {RootERC20BridgeFlowRate} from "../../src/root/flowrate/RootERC20BridgeFlowRate.sol"; +import {MockAdaptor} from "./MockAdaptor.sol"; +import {ChildHelper} from "./child/ChildHelper.sol"; +import {RootHelper} from "./root/RootHelper.sol"; +import {ChildERC20BridgeHandler} from "./child/ChildERC20BridgeHandler.sol"; +import {RootERC20BridgeFlowRateHandler} from "./root/RootERC20BridgeFlowRateHandler.sol"; +import "forge-std/console.sol"; + +contract InvariantBridge is Test { + string public constant CHILD_CHAIN_URL = "http://127.0.0.1:8500"; + string public constant ROOT_CHAIN_URL = "http://127.0.0.1:8501"; + uint256 public constant IMX_DEPOSIT_LIMIT = 10000 ether; + uint256 public constant MAX_AMOUNT = 10000; + address public constant ADMIN = address(0x111); + uint256 public constant NO_OF_USERS = 5; + uint256 public constant NO_OF_TOKENS = 4; + + address[] users; + address[] rootTokens; + + uint256 childId; + uint256 rootId; + ChildERC20Bridge childBridge; + RootERC20BridgeFlowRate rootBridge; + MockAdaptor childAdaptor; + MockAdaptor rootAdaptor; + ChildHelper childHelper; + RootHelper rootHelper; + ChildERC20BridgeHandler childBridgeHandler; + RootERC20BridgeFlowRateHandler rootBridgeHandler; + + function setUp() public { + childId = vm.createFork(CHILD_CHAIN_URL); + rootId = vm.createFork(ROOT_CHAIN_URL); + + // Deploy contracts on child chain. + vm.selectFork(childId); + vm.startPrank(ADMIN); + ChildERC20 childTokenTemplate = new ChildERC20(); + childTokenTemplate.initialize(address(123), "Test", "TST", 18); + childAdaptor = new MockAdaptor(); + vm.stopPrank(); + + childBridge = new ChildERC20Bridge(address(this)); + WIMX wIMX = new WIMX(); + + // Deploy contracts on root chain. + vm.selectFork(rootId); + vm.startPrank(ADMIN); + ChildERC20 rootTokenTemplate = new ChildERC20(); + rootTokenTemplate.initialize(address(123), "Test", "TST", 18); + rootAdaptor = new MockAdaptor(); + vm.stopPrank(); + + rootBridge = new RootERC20BridgeFlowRate(address(this)); + ChildERC20 rootIMXToken = new ChildERC20(); + rootIMXToken.initialize(address(123), "Immutable X", "IMX", 18); + WIMX wETH = new WIMX(); + + // Configure contracts on child chain. + vm.selectFork(childId); + childAdaptor.initialize(rootId, address(childBridge)); + IChildERC20Bridge.InitializationRoles memory childRoles = IChildERC20Bridge.InitializationRoles({ + defaultAdmin: address(this), + pauser: address(this), + unpauser: address(this), + adaptorManager: address(this), + initialDepositor: address(this), + treasuryManager: address(this) + }); + childBridge.initialize( + childRoles, address(childAdaptor), address(childTokenTemplate), address(rootIMXToken), address(wIMX) + ); + vm.deal(address(childBridge), IMX_DEPOSIT_LIMIT); + + // Configure contracts on root chain. + vm.selectFork(rootId); + rootAdaptor.initialize(childId, address(rootBridge)); + IRootERC20Bridge.InitializationRoles memory rootRoles = IRootERC20Bridge.InitializationRoles({ + defaultAdmin: address(this), + pauser: address(this), + unpauser: address(this), + variableManager: address(this), + adaptorManager: address(this) + }); + rootBridge.initialize( + rootRoles, + address(rootAdaptor), + address(childBridge), + address(rootTokenTemplate), + address(rootIMXToken), + address(wETH), + IMX_DEPOSIT_LIMIT, + ADMIN + ); + + // Create users. + vm.selectFork(rootId); + for (uint256 i = 0; i < NO_OF_USERS; i++) { + address user = vm.addr(0x10000 + i); + // Mint ETH token + vm.deal(user, MAX_AMOUNT); + // Mint IMX token + rootIMXToken.mint(user, MAX_AMOUNT); + users.push(user); + } + // Create tokens. + for (uint256 i = 0; i < NO_OF_TOKENS; i++) { + vm.prank(address(0x234)); + ChildERC20 rootToken = new ChildERC20(); + vm.prank(address(0x234)); + rootToken.initialize(address(123), "Test", "TST", 18); + // Mint token to user + for (uint256 j = 0; j < NO_OF_USERS; j++) { + vm.prank(address(0x234)); + rootToken.mint(users[j], MAX_AMOUNT); + } + // Configure rate for half tokens + if (i % 2 == 0) { + vm.prank(ADMIN); + rootBridge.setRateControlThreshold(address(rootToken), MAX_AMOUNT, MAX_AMOUNT / 3600, MAX_AMOUNT / 2); + } + rootTokens.push(address(rootToken)); + } + + // Deploy helpers and handlers on both chains. + vm.selectFork(childId); + vm.startPrank(ADMIN); + childHelper = new ChildHelper(payable(childBridge)); + address temp = address(new RootHelper(ADMIN, payable(rootBridge))); + childBridgeHandler = + new ChildERC20BridgeHandler(childId, rootId, users, rootTokens, address(childHelper), temp); + new RootERC20BridgeFlowRateHandler(childId, rootId, users, rootTokens, address(childHelper), temp); + vm.stopPrank(); + + vm.selectFork(rootId); + vm.startPrank(ADMIN); + new ChildHelper(payable(childBridge)); + rootHelper = new RootHelper(ADMIN, payable(rootBridge)); + new ChildERC20BridgeHandler(childId, rootId, users, rootTokens, address(childHelper), address(rootHelper)); + rootBridgeHandler = + new RootERC20BridgeFlowRateHandler(childId, rootId, users, rootTokens, address(childHelper), address(rootHelper)); + vm.stopPrank(); + + // Map tokens + vm.selectFork(rootId); + for (uint256 i = 0; i < NO_OF_TOKENS; i++) { + address rootToken = rootTokens[i]; + rootBridge.mapToken{value: 1}(IERC20Metadata(rootToken)); + // Verify + address childTokenL1 = rootBridge.rootTokenToChildToken(address(rootToken)); + + vm.selectFork(childId); + address childTokenL2 = childBridge.rootTokenToChildToken(address(rootToken)); + vm.selectFork(rootId); + + assertEq(childTokenL1, childTokenL2, "Child token address mismatch between L1 and L2"); + } + + // Target contracts + bytes4[] memory childSelectors = new bytes4[](1); + childSelectors[0] = childBridgeHandler.withdraw.selector; + targetSelector(FuzzSelector({addr: address(childBridgeHandler), selectors: childSelectors})); + + bytes4[] memory rootSelectors = new bytes4[](1); + rootSelectors[0] = rootBridgeHandler.deposit.selector; + targetSelector(FuzzSelector({addr: address(rootBridgeHandler), selectors: rootSelectors})); + + targetContract(address(childBridgeHandler)); + targetContract(address(rootBridgeHandler)); + } + + /// forge-config: default.invariant.fail-on-revert = false + function invariant_A() external { + } +} \ No newline at end of file diff --git a/test/invariant/MockAdaptor.sol b/test/invariant/MockAdaptor.sol new file mode 100644 index 00000000..3666ccdd --- /dev/null +++ b/test/invariant/MockAdaptor.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {IChildBridgeAdaptor} from "../../src/interfaces/child/IChildBridgeAdaptor.sol"; +import {IRootBridgeAdaptor} from "../../src/interfaces/root/IRootBridgeAdaptor.sol"; +import "forge-std/console.sol"; + +interface MessageReceiver { + function onMessageReceive(bytes calldata data) external; +} + +contract MockAdaptor is Test, IChildBridgeAdaptor, IRootBridgeAdaptor { + uint256 otherChainId; + MessageReceiver messageReceiver; + + constructor() {} + + function initialize(uint256 _otherChainId, address _messageReceiver) public { + otherChainId = _otherChainId; + messageReceiver = MessageReceiver(_messageReceiver); + } + + function sendMessage(bytes calldata payload, address /*refundRecipient*/ ) + external + payable + override(IChildBridgeAdaptor, IRootBridgeAdaptor) + { + uint256 original = vm.activeFork(); + + // Switch to the other chain. + vm.selectFork(otherChainId); + console.log(""); // <= Bug + onMessageReceive(payload); + + vm.selectFork(original); + } + + function onMessageReceive(bytes calldata data) public { + messageReceiver.onMessageReceive(data); + } +} \ No newline at end of file diff --git a/test/invariant/child/ChildERC20BridgeHandler.sol b/test/invariant/child/ChildERC20BridgeHandler.sol new file mode 100644 index 00000000..8b1ff059 --- /dev/null +++ b/test/invariant/child/ChildERC20BridgeHandler.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {ChildERC20} from "../../../src/child/ChildERC20.sol"; +import {ChildHelper} from "./ChildHelper.sol"; +import {RootHelper} from "../root/RootHelper.sol"; + +contract ChildERC20BridgeHandler is Test { + uint256 public constant MAX_AMOUNT = 10000; + uint256 public constant MAX_GAS = 100; + + uint256 childId; + uint256 rootId; + address[] users; + address[] rootTokens; + ChildHelper childHelper; + RootHelper rootHelper; + + constructor( + uint256 _childId, + uint256 _rootId, + address[] memory _users, + address[] memory _rootTokens, + address _childHelper, + address _rootHelper + ) { + childId = _childId; + rootId = _rootId; + users = _users; + rootTokens = _rootTokens; + childHelper = ChildHelper(_childHelper); + rootHelper = RootHelper(_rootHelper); + } + + function initialize( + uint256 _childId, + uint256 _rootId, + address[] memory _users, + address[] memory _rootTokens, + address _childHelper, + address _rootHelper + ) public { + childId = _childId; + rootId = _rootId; + users = _users; + rootTokens = _rootTokens; + childHelper = ChildHelper(_childHelper); + rootHelper = RootHelper(_rootHelper); + } + + function withdraw(uint256 userIndexSeed, uint256 tokenIndexSeed, uint256 amount, uint256 gasAmt) public { + uint256 original = vm.activeFork(); + + // Switch to child chain + vm.selectFork(childId); + + // Bound + address user = users[bound(userIndexSeed, 0, users.length - 1)]; + address rootToken = rootTokens[bound(tokenIndexSeed, 0, rootTokens.length - 1)]; + amount = bound(amount, 1, MAX_AMOUNT); + gasAmt = bound(gasAmt, 1, MAX_GAS); + + // Get child token + address childToken = childHelper.childBridge().rootTokenToChildToken(rootToken); + + // Get current balance + uint256 currentBalance = ChildERC20(childToken).balanceOf(user); + + if (currentBalance < amount) { + // Deposit difference + vm.selectFork(rootId); + rootHelper.deposit(user, rootToken, amount - currentBalance, gasAmt); + vm.selectFork(childId); + } + + vm.selectFork(rootId); + uint256 previousLen = rootHelper.getQueueSize(user); + vm.selectFork(childId); + + childHelper.withdraw(user, childToken, amount, gasAmt); + + vm.selectFork(rootId); + rootHelper.finaliseWithdrawal(user, previousLen); + vm.selectFork(childId); + + vm.selectFork(original); + } +} \ No newline at end of file diff --git a/test/invariant/child/ChildHelper.sol b/test/invariant/child/ChildHelper.sol new file mode 100644 index 00000000..9209b29a --- /dev/null +++ b/test/invariant/child/ChildHelper.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {ChildERC20} from "../../../src/child/ChildERC20.sol"; +import {ChildERC20Bridge} from "../../../src/child/ChildERC20Bridge.sol"; +import {IChildERC20} from "../../../src/interfaces/child/IChildERC20.sol"; + +contract ChildHelper is Test { + ChildERC20Bridge public childBridge; + + uint256 public totalGas; + + constructor(address payable _childBridge) { + childBridge = ChildERC20Bridge(_childBridge); + } + + function withdraw(address user, address childToken, uint256 amount, uint256 gasAmt) public { + vm.deal(user, gasAmt); + totalGas += gasAmt; + + vm.prank(user); + childBridge.withdraw{value: gasAmt}(IChildERC20(childToken), amount); + } +} \ No newline at end of file diff --git a/test/invariant/root/RootERC20BridgeFlowRateHandler.sol b/test/invariant/root/RootERC20BridgeFlowRateHandler.sol new file mode 100644 index 00000000..50d33d2f --- /dev/null +++ b/test/invariant/root/RootERC20BridgeFlowRateHandler.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {ChildERC20} from "../../../src/child/ChildERC20.sol"; +import {ChildHelper} from "../child/ChildHelper.sol"; +import {RootHelper} from "./RootHelper.sol"; + +contract RootERC20BridgeFlowRateHandler is Test { + uint256 public constant MAX_AMOUNT = 10000; + uint256 public constant MAX_GAS = 100; + + uint256 childId; + uint256 rootId; + address[] users; + address[] rootTokens; + ChildHelper childHelper; + RootHelper rootHelper; + + constructor( + uint256 _childId, + uint256 _rootId, + address[] memory _users, + address[] memory _rootTokens, + address _childHelper, + address _rootHelper + ) { + childId = _childId; + rootId = _rootId; + users = _users; + rootTokens = _rootTokens; + childHelper = ChildHelper(_childHelper); + rootHelper = RootHelper(_rootHelper); + } + + function deposit(uint256 userIndexSeed, uint256 tokenIndexSeed, uint256 amount, uint256 gasAmt) public { + uint256 original = vm.activeFork(); + + // Switch to root chain + vm.selectFork(rootId); + + // Bound + address user = users[bound(userIndexSeed, 0, users.length - 1)]; + address rootToken = rootTokens[bound(tokenIndexSeed, 0, rootTokens.length - 1)]; + amount = bound(amount, 1, MAX_AMOUNT); + gasAmt = bound(gasAmt, 1, MAX_GAS); + + // Get child token + address childToken = rootHelper.rootBridge().rootTokenToChildToken(rootToken); + + // Get current balance + uint256 currentBalance = ChildERC20(rootToken).balanceOf(user); + + if (currentBalance < amount) { + // Withdraw difference + uint256 previousLen = rootHelper.getQueueSize(user); + + vm.selectFork(childId); + childHelper.withdraw(user, childToken, amount - currentBalance, gasAmt); + vm.selectFork(rootId); + + rootHelper.finaliseWithdrawal(user, previousLen); + } + + rootHelper.deposit(user, rootToken, amount, gasAmt); + + vm.selectFork(original); + } +} \ No newline at end of file diff --git a/test/invariant/root/RootHelper.sol b/test/invariant/root/RootHelper.sol new file mode 100644 index 00000000..76174c6e --- /dev/null +++ b/test/invariant/root/RootHelper.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {ChildERC20} from "../../../src/child/ChildERC20.sol"; +import {RootERC20BridgeFlowRate} from "../../../src/root/flowrate/RootERC20BridgeFlowRate.sol"; +import {IERC20Metadata} from "../../../src/root/RootERC20Bridge.sol"; + +contract RootHelper is Test { + address admin; + RootERC20BridgeFlowRate public rootBridge; + + uint256 public totalGas; + + constructor(address _admin, address payable _rootBridge) { + admin = _admin; + rootBridge = RootERC20BridgeFlowRate(_rootBridge); + } + + function deposit(address user, address rootToken, uint256 amount, uint256 gasAmt) public { + vm.prank(user); + ChildERC20(rootToken).approve(address(rootBridge), amount); + + vm.deal(user, gasAmt); + totalGas += gasAmt; + + vm.prank(user); + rootBridge.deposit{value: gasAmt}(IERC20Metadata(rootToken), amount); + } + + function getQueueSize(address user) public view returns (uint256) { + return rootBridge.getPendingWithdrawalsLength(user); + } + + function finaliseWithdrawal(address user, uint256 previousLen) public { + // Check if this withdrawal has hit rate limit + if (rootBridge.getPendingWithdrawalsLength(user) > previousLen) { + skip(86401); + vm.prank(user); + rootBridge.finaliseQueuedWithdrawal(user, previousLen); + } + + if (rootBridge.withdrawalQueueActivated()) { + vm.prank(admin); + rootBridge.deactivateWithdrawalQueue(); + } + } +} \ No newline at end of file From 85a58df444173910451b740641a90fd40848408b Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Wed, 21 Feb 2024 02:10:56 +1000 Subject: [PATCH 124/155] Fix --- test/invariant/MockAdaptor.sol | 2 +- .../child/ChildERC20BridgeHandler.sol | 11 ++++++---- .../root/RootERC20BridgeFlowRateHandler.sol | 20 ++++++++++--------- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/test/invariant/MockAdaptor.sol b/test/invariant/MockAdaptor.sol index 3666ccdd..f826fc6e 100644 --- a/test/invariant/MockAdaptor.sol +++ b/test/invariant/MockAdaptor.sol @@ -30,7 +30,7 @@ contract MockAdaptor is Test, IChildBridgeAdaptor, IRootBridgeAdaptor { // Switch to the other chain. vm.selectFork(otherChainId); - console.log(""); // <= Bug + console.log(""); // <= // Due to a foundry bug, remove this logging will very likely cause foundry to crash. onMessageReceive(payload); vm.selectFork(original); diff --git a/test/invariant/child/ChildERC20BridgeHandler.sol b/test/invariant/child/ChildERC20BridgeHandler.sol index 8b1ff059..ce6fa164 100644 --- a/test/invariant/child/ChildERC20BridgeHandler.sol +++ b/test/invariant/child/ChildERC20BridgeHandler.sol @@ -68,10 +68,13 @@ contract ChildERC20BridgeHandler is Test { uint256 currentBalance = ChildERC20(childToken).balanceOf(user); if (currentBalance < amount) { - // Deposit difference - vm.selectFork(rootId); - rootHelper.deposit(user, rootToken, amount - currentBalance, gasAmt); - vm.selectFork(childId); + // // Deposit difference + // vm.selectFork(rootId); + // rootHelper.deposit(user, rootToken, amount - currentBalance, gasAmt); + // vm.selectFork(childId); + vm.selectFork(original); + // TODO: Issue when try to deposit in withdraw flow. + return; } vm.selectFork(rootId); diff --git a/test/invariant/root/RootERC20BridgeFlowRateHandler.sol b/test/invariant/root/RootERC20BridgeFlowRateHandler.sol index 50d33d2f..99e56881 100644 --- a/test/invariant/root/RootERC20BridgeFlowRateHandler.sol +++ b/test/invariant/root/RootERC20BridgeFlowRateHandler.sol @@ -45,21 +45,23 @@ contract RootERC20BridgeFlowRateHandler is Test { amount = bound(amount, 1, MAX_AMOUNT); gasAmt = bound(gasAmt, 1, MAX_GAS); - // Get child token - address childToken = rootHelper.rootBridge().rootTokenToChildToken(rootToken); - // Get current balance uint256 currentBalance = ChildERC20(rootToken).balanceOf(user); if (currentBalance < amount) { - // Withdraw difference - uint256 previousLen = rootHelper.getQueueSize(user); + // // Withdraw difference + // // Get child token + // address childToken = rootHelper.rootBridge().rootTokenToChildToken(rootToken); + // uint256 previousLen = rootHelper.getQueueSize(user); - vm.selectFork(childId); - childHelper.withdraw(user, childToken, amount - currentBalance, gasAmt); - vm.selectFork(rootId); + // vm.selectFork(childId); + // childHelper.withdraw(user, childToken, amount - currentBalance, gasAmt); + // vm.selectFork(rootId); - rootHelper.finaliseWithdrawal(user, previousLen); + // rootHelper.finaliseWithdrawal(user, previousLen); + vm.selectFork(original); + // TODO: Issue when try to withdraw in deposit flow. + return; } rootHelper.deposit(user, rootToken, amount, gasAmt); From f7baef00d19b8fc31604b5cac3f35e5ff2e7468f Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Wed, 21 Feb 2024 09:52:40 +1000 Subject: [PATCH 125/155] Update InvariantBridge.t.sol --- test/invariant/InvariantBridge.t.sol | 78 ++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 4 deletions(-) diff --git a/test/invariant/InvariantBridge.t.sol b/test/invariant/InvariantBridge.t.sol index 9e4945f2..2bdcbd79 100644 --- a/test/invariant/InvariantBridge.t.sol +++ b/test/invariant/InvariantBridge.t.sol @@ -166,19 +166,89 @@ contract InvariantBridge is Test { } // Target contracts - bytes4[] memory childSelectors = new bytes4[](1); - childSelectors[0] = childBridgeHandler.withdraw.selector; - targetSelector(FuzzSelector({addr: address(childBridgeHandler), selectors: childSelectors})); + // bytes4[] memory childSelectors = new bytes4[](1); + // childSelectors[0] = childBridgeHandler.withdraw.selector; + // targetSelector(FuzzSelector({addr: address(childBridgeHandler), selectors: childSelectors})); bytes4[] memory rootSelectors = new bytes4[](1); rootSelectors[0] = rootBridgeHandler.deposit.selector; targetSelector(FuzzSelector({addr: address(rootBridgeHandler), selectors: rootSelectors})); - targetContract(address(childBridgeHandler)); + // targetContract(address(childBridgeHandler)); targetContract(address(rootBridgeHandler)); } + /// forge-config: default.invariant.runs = 1 + /// forge-config: default.invariant.depth = 1 /// forge-config: default.invariant.fail-on-revert = false function invariant_A() external { + vm.selectFork(rootId); + uint256 bridgeBalance0 = ChildERC20(rootTokens[0]).balanceOf(address(rootBridge)); + uint256 bridgeBalance1 = ChildERC20(rootTokens[1]).balanceOf(address(rootBridge)); + uint256 bridgeBalance2 = ChildERC20(rootTokens[2]).balanceOf(address(rootBridge)); + uint256 bridgeBalance3 = ChildERC20(rootTokens[3]).balanceOf(address(rootBridge)); + + address childToken0 = rootBridge.rootTokenToChildToken(rootTokens[0]); + address childToken1 = rootBridge.rootTokenToChildToken(rootTokens[1]); + address childToken2 = rootBridge.rootTokenToChildToken(rootTokens[2]); + address childToken3 = rootBridge.rootTokenToChildToken(rootTokens[3]); + + vm.selectFork(childId); + uint256 totalSupply0 = ChildERC20(childToken0).totalSupply(); + uint256 totalSupply1 = ChildERC20(childToken1).totalSupply(); + uint256 totalSupply2 = ChildERC20(childToken2).totalSupply(); + uint256 totalSupply3 = ChildERC20(childToken3).totalSupply(); + + console.log(string.concat(string.concat(vm.toString(bridgeBalance0), " "), vm.toString(totalSupply0))); + console.log(string.concat(string.concat(vm.toString(bridgeBalance1), " "), vm.toString(totalSupply1))); + console.log(string.concat(string.concat(vm.toString(bridgeBalance2), " "), vm.toString(totalSupply2))); + console.log(string.concat(string.concat(vm.toString(bridgeBalance3), " "), vm.toString(totalSupply3))); + + if (bridgeBalance0 != totalSupply0) { + console.log("000"); + revert(string.concat("**0**",string.concat(string.concat(vm.toString(bridgeBalance0), " "), vm.toString(totalSupply0)))); + } + + if (bridgeBalance1 != totalSupply1) { + console.log("111"); + revert(string.concat("**1**",string.concat(string.concat(vm.toString(bridgeBalance1), " "), vm.toString(totalSupply1)))); + } + + if (bridgeBalance2 != totalSupply2) { + console.log("222"); + revert(string.concat("**2**",string.concat(string.concat(vm.toString(bridgeBalance2), " "), vm.toString(totalSupply2)))); + } + + if (bridgeBalance3 != totalSupply3) { + console.log("333"); + revert(string.concat("**3**",string.concat(string.concat(vm.toString(bridgeBalance3), " "), vm.toString(totalSupply3)))); + } + + // assertEq(bridgeBalance0, totalSupply0); + // assertEq(bridgeBalance1, totalSupply1); + // assertEq(bridgeBalance2, totalSupply2); + // assertEq(bridgeBalance3, totalSupply3); + + // for (uint256 i = 0; i < NO_OF_TOKENS; i++) { + // address rootToken = rootTokens[i]; + + // vm.selectFork(rootId); + // uint256 bridgeBalance = ChildERC20(rootToken).balanceOf(address(rootBridge)); + // address childToken = rootBridge.rootTokenToChildToken(rootToken); + + // vm.selectFork(childId); + // uint256 totalSupply = ChildERC20(childToken).totalSupply(); + + // string memory log1 = string.concat(string.concat(vm.toString(bridgeBalance), " "), vm.toString(totalSupply)); + // console.log(string.concat("!!!", log1)); + // if (bridgeBalance != totalSupply) { + // console.log("I'm here...."); + // // // string memory res = string.concat(string.concat(vm.toString(bridgeBalance), " "), vm.toString(totalSupply)); + // // console.log(); + // revert(string.concat("???", log1)); + // // vm.writeFile("./something.txt", log1); + // } + // // assertEq(bridgeBalance, totalSupply); + // } } } \ No newline at end of file From ccf95ab555ab7b4281e076a62714be21b6dba448 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Wed, 21 Feb 2024 13:42:00 +1000 Subject: [PATCH 126/155] Update --- .../child/ChildERC20BridgeHandler.sol | 11 ++++------- .../root/RootERC20BridgeFlowRateHandler.sol | 19 ++++++++----------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/test/invariant/child/ChildERC20BridgeHandler.sol b/test/invariant/child/ChildERC20BridgeHandler.sol index ce6fa164..8b1ff059 100644 --- a/test/invariant/child/ChildERC20BridgeHandler.sol +++ b/test/invariant/child/ChildERC20BridgeHandler.sol @@ -68,13 +68,10 @@ contract ChildERC20BridgeHandler is Test { uint256 currentBalance = ChildERC20(childToken).balanceOf(user); if (currentBalance < amount) { - // // Deposit difference - // vm.selectFork(rootId); - // rootHelper.deposit(user, rootToken, amount - currentBalance, gasAmt); - // vm.selectFork(childId); - vm.selectFork(original); - // TODO: Issue when try to deposit in withdraw flow. - return; + // Deposit difference + vm.selectFork(rootId); + rootHelper.deposit(user, rootToken, amount - currentBalance, gasAmt); + vm.selectFork(childId); } vm.selectFork(rootId); diff --git a/test/invariant/root/RootERC20BridgeFlowRateHandler.sol b/test/invariant/root/RootERC20BridgeFlowRateHandler.sol index 99e56881..876b2e9e 100644 --- a/test/invariant/root/RootERC20BridgeFlowRateHandler.sol +++ b/test/invariant/root/RootERC20BridgeFlowRateHandler.sol @@ -49,19 +49,16 @@ contract RootERC20BridgeFlowRateHandler is Test { uint256 currentBalance = ChildERC20(rootToken).balanceOf(user); if (currentBalance < amount) { - // // Withdraw difference - // // Get child token - // address childToken = rootHelper.rootBridge().rootTokenToChildToken(rootToken); - // uint256 previousLen = rootHelper.getQueueSize(user); + // Withdraw difference + // Get child token + address childToken = rootHelper.rootBridge().rootTokenToChildToken(rootToken); + uint256 previousLen = rootHelper.getQueueSize(user); - // vm.selectFork(childId); - // childHelper.withdraw(user, childToken, amount - currentBalance, gasAmt); - // vm.selectFork(rootId); + vm.selectFork(childId); + childHelper.withdraw(user, childToken, amount - currentBalance, gasAmt); + vm.selectFork(rootId); - // rootHelper.finaliseWithdrawal(user, previousLen); - vm.selectFork(original); - // TODO: Issue when try to withdraw in deposit flow. - return; + rootHelper.finaliseWithdrawal(user, previousLen); } rootHelper.deposit(user, rootToken, amount, gasAmt); From de5977f1cf4399db173aaf611e3fd382cecd026b Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Wed, 21 Feb 2024 13:54:18 +1000 Subject: [PATCH 127/155] Update MockAdaptor.sol --- test/invariant/MockAdaptor.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/invariant/MockAdaptor.sol b/test/invariant/MockAdaptor.sol index f826fc6e..27624bc3 100644 --- a/test/invariant/MockAdaptor.sol +++ b/test/invariant/MockAdaptor.sol @@ -4,7 +4,6 @@ pragma solidity 0.8.19; import {Test} from "forge-std/Test.sol"; import {IChildBridgeAdaptor} from "../../src/interfaces/child/IChildBridgeAdaptor.sol"; import {IRootBridgeAdaptor} from "../../src/interfaces/root/IRootBridgeAdaptor.sol"; -import "forge-std/console.sol"; interface MessageReceiver { function onMessageReceive(bytes calldata data) external; @@ -30,7 +29,6 @@ contract MockAdaptor is Test, IChildBridgeAdaptor, IRootBridgeAdaptor { // Switch to the other chain. vm.selectFork(otherChainId); - console.log(""); // <= // Due to a foundry bug, remove this logging will very likely cause foundry to crash. onMessageReceive(payload); vm.selectFork(original); From c2f8e47359b1a7610ba9e19fccca11927486c374 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Wed, 21 Feb 2024 13:55:11 +1000 Subject: [PATCH 128/155] Update InvariantBridge.t.sol --- test/invariant/InvariantBridge.t.sol | 37 ++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/test/invariant/InvariantBridge.t.sol b/test/invariant/InvariantBridge.t.sol index 2bdcbd79..9b96ea5b 100644 --- a/test/invariant/InvariantBridge.t.sol +++ b/test/invariant/InvariantBridge.t.sol @@ -17,6 +17,7 @@ import "forge-std/console.sol"; contract InvariantBridge is Test { string public constant CHILD_CHAIN_URL = "http://127.0.0.1:8500"; string public constant ROOT_CHAIN_URL = "http://127.0.0.1:8501"; + string public constant RESET_CHAIN_URL = "http://127.0.0.1:8502"; uint256 public constant IMX_DEPOSIT_LIMIT = 10000 ether; uint256 public constant MAX_AMOUNT = 10000; address public constant ADMIN = address(0x111); @@ -28,6 +29,7 @@ contract InvariantBridge is Test { uint256 childId; uint256 rootId; + uint256 resetId; ChildERC20Bridge childBridge; RootERC20BridgeFlowRate rootBridge; MockAdaptor childAdaptor; @@ -40,6 +42,7 @@ contract InvariantBridge is Test { function setUp() public { childId = vm.createFork(CHILD_CHAIN_URL); rootId = vm.createFork(ROOT_CHAIN_URL); + resetId = vm.createFork(RESET_CHAIN_URL); // Deploy contracts on child chain. vm.selectFork(childId); @@ -65,6 +68,14 @@ contract InvariantBridge is Test { rootIMXToken.initialize(address(123), "Immutable X", "IMX", 18); WIMX wETH = new WIMX(); + // Deploy contracts on reset chain. + vm.selectFork(resetId); + vm.startPrank(ADMIN); + ChildERC20 resetTokenTemplate = new ChildERC20(); + resetTokenTemplate.initialize(address(123), "Test", "TST", 18); + new MockAdaptor(); + vm.stopPrank(); + // Configure contracts on child chain. vm.selectFork(childId); childAdaptor.initialize(rootId, address(childBridge)); @@ -131,7 +142,7 @@ contract InvariantBridge is Test { rootTokens.push(address(rootToken)); } - // Deploy helpers and handlers on both chains. + // Deploy helpers and handlers on all chains. vm.selectFork(childId); vm.startPrank(ADMIN); childHelper = new ChildHelper(payable(childBridge)); @@ -150,6 +161,14 @@ contract InvariantBridge is Test { new RootERC20BridgeFlowRateHandler(childId, rootId, users, rootTokens, address(childHelper), address(rootHelper)); vm.stopPrank(); + vm.selectFork(resetId); + vm.startPrank(ADMIN); + new ChildHelper(payable(childBridge)); + new RootHelper(ADMIN, payable(rootBridge)); + new ChildERC20BridgeHandler(childId, rootId, users, rootTokens, address(childHelper), address(rootHelper)); + new RootERC20BridgeFlowRateHandler(childId, rootId, users, rootTokens, address(childHelper), address(rootHelper)); + vm.stopPrank(); + // Map tokens vm.selectFork(rootId); for (uint256 i = 0; i < NO_OF_TOKENS; i++) { @@ -166,21 +185,23 @@ contract InvariantBridge is Test { } // Target contracts - // bytes4[] memory childSelectors = new bytes4[](1); - // childSelectors[0] = childBridgeHandler.withdraw.selector; - // targetSelector(FuzzSelector({addr: address(childBridgeHandler), selectors: childSelectors})); + bytes4[] memory childSelectors = new bytes4[](1); + childSelectors[0] = childBridgeHandler.withdraw.selector; + targetSelector(FuzzSelector({addr: address(childBridgeHandler), selectors: childSelectors})); bytes4[] memory rootSelectors = new bytes4[](1); rootSelectors[0] = rootBridgeHandler.deposit.selector; targetSelector(FuzzSelector({addr: address(rootBridgeHandler), selectors: rootSelectors})); - // targetContract(address(childBridgeHandler)); + targetContract(address(childBridgeHandler)); targetContract(address(rootBridgeHandler)); + + vm.selectFork(resetId); } - /// forge-config: default.invariant.runs = 1 - /// forge-config: default.invariant.depth = 1 - /// forge-config: default.invariant.fail-on-revert = false + // forge-config: default.invariant.runs = 256 + /// forge-config: default.invariant.depth = 15 + /// forge-config: default.invariant.fail-on-revert = true function invariant_A() external { vm.selectFork(rootId); uint256 bridgeBalance0 = ChildERC20(rootTokens[0]).balanceOf(address(rootBridge)); From 86a629f58d53079916de864330ceacf822a121ea Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Wed, 21 Feb 2024 13:55:42 +1000 Subject: [PATCH 129/155] Fmt --- test/invariant/InvariantBridge.t.sol | 38 ++++++++++++++----- test/invariant/MockAdaptor.sol | 4 +- .../child/ChildERC20BridgeHandler.sol | 2 +- test/invariant/child/ChildHelper.sol | 2 +- .../root/RootERC20BridgeFlowRateHandler.sol | 4 +- test/invariant/root/RootHelper.sol | 4 +- 6 files changed, 36 insertions(+), 18 deletions(-) diff --git a/test/invariant/InvariantBridge.t.sol b/test/invariant/InvariantBridge.t.sol index 9b96ea5b..441c7e41 100644 --- a/test/invariant/InvariantBridge.t.sol +++ b/test/invariant/InvariantBridge.t.sol @@ -147,8 +147,7 @@ contract InvariantBridge is Test { vm.startPrank(ADMIN); childHelper = new ChildHelper(payable(childBridge)); address temp = address(new RootHelper(ADMIN, payable(rootBridge))); - childBridgeHandler = - new ChildERC20BridgeHandler(childId, rootId, users, rootTokens, address(childHelper), temp); + childBridgeHandler = new ChildERC20BridgeHandler(childId, rootId, users, rootTokens, address(childHelper), temp); new RootERC20BridgeFlowRateHandler(childId, rootId, users, rootTokens, address(childHelper), temp); vm.stopPrank(); @@ -157,8 +156,9 @@ contract InvariantBridge is Test { new ChildHelper(payable(childBridge)); rootHelper = new RootHelper(ADMIN, payable(rootBridge)); new ChildERC20BridgeHandler(childId, rootId, users, rootTokens, address(childHelper), address(rootHelper)); - rootBridgeHandler = - new RootERC20BridgeFlowRateHandler(childId, rootId, users, rootTokens, address(childHelper), address(rootHelper)); + rootBridgeHandler = new RootERC20BridgeFlowRateHandler( + childId, rootId, users, rootTokens, address(childHelper), address(rootHelper) + ); vm.stopPrank(); vm.selectFork(resetId); @@ -166,7 +166,9 @@ contract InvariantBridge is Test { new ChildHelper(payable(childBridge)); new RootHelper(ADMIN, payable(rootBridge)); new ChildERC20BridgeHandler(childId, rootId, users, rootTokens, address(childHelper), address(rootHelper)); - new RootERC20BridgeFlowRateHandler(childId, rootId, users, rootTokens, address(childHelper), address(rootHelper)); + new RootERC20BridgeFlowRateHandler( + childId, rootId, users, rootTokens, address(childHelper), address(rootHelper) + ); vm.stopPrank(); // Map tokens @@ -227,22 +229,38 @@ contract InvariantBridge is Test { if (bridgeBalance0 != totalSupply0) { console.log("000"); - revert(string.concat("**0**",string.concat(string.concat(vm.toString(bridgeBalance0), " "), vm.toString(totalSupply0)))); + revert( + string.concat( + "**0**", string.concat(string.concat(vm.toString(bridgeBalance0), " "), vm.toString(totalSupply0)) + ) + ); } if (bridgeBalance1 != totalSupply1) { console.log("111"); - revert(string.concat("**1**",string.concat(string.concat(vm.toString(bridgeBalance1), " "), vm.toString(totalSupply1)))); + revert( + string.concat( + "**1**", string.concat(string.concat(vm.toString(bridgeBalance1), " "), vm.toString(totalSupply1)) + ) + ); } if (bridgeBalance2 != totalSupply2) { console.log("222"); - revert(string.concat("**2**",string.concat(string.concat(vm.toString(bridgeBalance2), " "), vm.toString(totalSupply2)))); + revert( + string.concat( + "**2**", string.concat(string.concat(vm.toString(bridgeBalance2), " "), vm.toString(totalSupply2)) + ) + ); } if (bridgeBalance3 != totalSupply3) { console.log("333"); - revert(string.concat("**3**",string.concat(string.concat(vm.toString(bridgeBalance3), " "), vm.toString(totalSupply3)))); + revert( + string.concat( + "**3**", string.concat(string.concat(vm.toString(bridgeBalance3), " "), vm.toString(totalSupply3)) + ) + ); } // assertEq(bridgeBalance0, totalSupply0); @@ -272,4 +290,4 @@ contract InvariantBridge is Test { // // assertEq(bridgeBalance, totalSupply); // } } -} \ No newline at end of file +} diff --git a/test/invariant/MockAdaptor.sol b/test/invariant/MockAdaptor.sol index 27624bc3..0a5bfe83 100644 --- a/test/invariant/MockAdaptor.sol +++ b/test/invariant/MockAdaptor.sol @@ -30,11 +30,11 @@ contract MockAdaptor is Test, IChildBridgeAdaptor, IRootBridgeAdaptor { // Switch to the other chain. vm.selectFork(otherChainId); onMessageReceive(payload); - + vm.selectFork(original); } function onMessageReceive(bytes calldata data) public { messageReceiver.onMessageReceive(data); } -} \ No newline at end of file +} diff --git a/test/invariant/child/ChildERC20BridgeHandler.sol b/test/invariant/child/ChildERC20BridgeHandler.sol index 8b1ff059..a6a3bab0 100644 --- a/test/invariant/child/ChildERC20BridgeHandler.sol +++ b/test/invariant/child/ChildERC20BridgeHandler.sol @@ -86,4 +86,4 @@ contract ChildERC20BridgeHandler is Test { vm.selectFork(original); } -} \ No newline at end of file +} diff --git a/test/invariant/child/ChildHelper.sol b/test/invariant/child/ChildHelper.sol index 9209b29a..41aedff9 100644 --- a/test/invariant/child/ChildHelper.sol +++ b/test/invariant/child/ChildHelper.sol @@ -22,4 +22,4 @@ contract ChildHelper is Test { vm.prank(user); childBridge.withdraw{value: gasAmt}(IChildERC20(childToken), amount); } -} \ No newline at end of file +} diff --git a/test/invariant/root/RootERC20BridgeFlowRateHandler.sol b/test/invariant/root/RootERC20BridgeFlowRateHandler.sol index 876b2e9e..1c62cd42 100644 --- a/test/invariant/root/RootERC20BridgeFlowRateHandler.sol +++ b/test/invariant/root/RootERC20BridgeFlowRateHandler.sol @@ -53,7 +53,7 @@ contract RootERC20BridgeFlowRateHandler is Test { // Get child token address childToken = rootHelper.rootBridge().rootTokenToChildToken(rootToken); uint256 previousLen = rootHelper.getQueueSize(user); - + vm.selectFork(childId); childHelper.withdraw(user, childToken, amount - currentBalance, gasAmt); vm.selectFork(rootId); @@ -65,4 +65,4 @@ contract RootERC20BridgeFlowRateHandler is Test { vm.selectFork(original); } -} \ No newline at end of file +} diff --git a/test/invariant/root/RootHelper.sol b/test/invariant/root/RootHelper.sol index 76174c6e..fd81acac 100644 --- a/test/invariant/root/RootHelper.sol +++ b/test/invariant/root/RootHelper.sol @@ -39,10 +39,10 @@ contract RootHelper is Test { vm.prank(user); rootBridge.finaliseQueuedWithdrawal(user, previousLen); } - + if (rootBridge.withdrawalQueueActivated()) { vm.prank(admin); rootBridge.deactivateWithdrawalQueue(); } } -} \ No newline at end of file +} From 9178788a0fac0c24064c0181b9574455eb62c9f0 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Wed, 21 Feb 2024 13:56:34 +1000 Subject: [PATCH 130/155] Create resetchain.config.ts --- scripts/localdev/resetchain.config.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 scripts/localdev/resetchain.config.ts diff --git a/scripts/localdev/resetchain.config.ts b/scripts/localdev/resetchain.config.ts new file mode 100644 index 00000000..116a3eba --- /dev/null +++ b/scripts/localdev/resetchain.config.ts @@ -0,0 +1,21 @@ +import { HardhatUserConfig } from "hardhat/config"; +import "@nomicfoundation/hardhat-toolbox"; + +const config: HardhatUserConfig = { + networks: { + hardhat: { + hardfork: "shanghai", + mining: { + auto: false, + interval: 1200 + }, + chainId: 2502, + accounts: [], + }, + localhost: { + url: "http://127.0.0.1:8502/", + } + }, + solidity: "0.8.19", +}; +export default config; \ No newline at end of file From 808187263f94d0908ee310dd997ac4030070476c Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Wed, 21 Feb 2024 14:00:44 +1000 Subject: [PATCH 131/155] Update --- package.json | 1 + scripts/localdev/chains.sh | 12 ++++++++++++ 2 files changed, 13 insertions(+) create mode 100755 scripts/localdev/chains.sh diff --git a/package.json b/package.json index f3648521..fb877879 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "local:ci": "cd scripts/localdev; rm -rf .child.bridge.contracts.json .root.bridge.contracts.json; ./ci.sh && ./deploy.sh && AXELAR_API_URL=skip npx mocha --require mocha-suppress-logs ../e2e/e2e.ts && ./stop.sh", "local:chainonly": "cd scripts/localdev; LOCAL_CHAIN_ONLY=true ./start.sh", "local:axelaronly": "cd scripts/localdev; npx ts-node axelar_setup.ts", + "local:threechains": "cd scripts/localdev; ./chains.sh", "stop": "cd scripts/localdev; ./stop.sh" }, "author": "", diff --git a/scripts/localdev/chains.sh b/scripts/localdev/chains.sh new file mode 100755 index 00000000..31d3cb0b --- /dev/null +++ b/scripts/localdev/chains.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -ex +set -o pipefail + +# Stop previous deployment. +./stop.sh + +# Start root & child chain. +npx hardhat node --config ./rootchain.config.ts --port 8500 > /dev/null 2>&1 & +npx hardhat node --config ./childchain.config.ts --port 8501 > /dev/null 2>&1 & +npx hardhat node --config ./resetchain.config.ts --port 8502 > /dev/null 2>&1 & +sleep 10 From 087864f4a8216955002d639555f1bb36494d2c8f Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Wed, 21 Feb 2024 14:06:32 +1000 Subject: [PATCH 132/155] Update test.yml --- .github/workflows/test.yml | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eba27536..dfefbb8f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,6 +24,11 @@ jobs: with: submodules: recursive + - name: Set Node.js 18.18.x + uses: actions/setup-node@v3 + with: + node-version: 18.18.x + - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: @@ -35,12 +40,38 @@ jobs: forge build --sizes id: build - - name: Run unit and integration tests + - name: Run install + uses: borales/actions-yarn@v4 + with: + cmd: install + + - name: Run build + uses: borales/actions-yarn@v4 + with: + cmd: build + + - name: Run Unit Tests run: | - forge test --no-match-path "test/fork/**" -vvv - id: unit_integration_test + forge test --match-path "test/unit/**" -vvv + id: unit_test + + - name: Run Integration Tests + run: | + forge test --match-path "test/integration/**" -vvv + id: integration_test + + - name: Run Fuzz Tests + run: | + forge test --match-path "test/fuzz/**" -vvv + id: fuzz_test - name: Run Fork Tests run: | forge test --match-path "test/fork/**" -vvvvv id: fork_test + + - name: Run Invariant Tests + run: | + yarn local:threechains + forge test --match-path "test/invariant/**" -vvvvv + id: invariant_test \ No newline at end of file From 8255782849ec5ff901af4015afb244967621e66e Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Wed, 21 Feb 2024 14:15:44 +1000 Subject: [PATCH 133/155] Update InvariantBridge.t.sol --- test/invariant/InvariantBridge.t.sol | 94 ++++------------------------ 1 file changed, 11 insertions(+), 83 deletions(-) diff --git a/test/invariant/InvariantBridge.t.sol b/test/invariant/InvariantBridge.t.sol index 441c7e41..8062a06d 100644 --- a/test/invariant/InvariantBridge.t.sol +++ b/test/invariant/InvariantBridge.t.sol @@ -21,8 +21,8 @@ contract InvariantBridge is Test { uint256 public constant IMX_DEPOSIT_LIMIT = 10000 ether; uint256 public constant MAX_AMOUNT = 10000; address public constant ADMIN = address(0x111); - uint256 public constant NO_OF_USERS = 5; - uint256 public constant NO_OF_TOKENS = 4; + uint256 public constant NO_OF_USERS = 20; + uint256 public constant NO_OF_TOKENS = 10; address[] users; address[] rootTokens; @@ -204,90 +204,18 @@ contract InvariantBridge is Test { // forge-config: default.invariant.runs = 256 /// forge-config: default.invariant.depth = 15 /// forge-config: default.invariant.fail-on-revert = true - function invariant_A() external { - vm.selectFork(rootId); - uint256 bridgeBalance0 = ChildERC20(rootTokens[0]).balanceOf(address(rootBridge)); - uint256 bridgeBalance1 = ChildERC20(rootTokens[1]).balanceOf(address(rootBridge)); - uint256 bridgeBalance2 = ChildERC20(rootTokens[2]).balanceOf(address(rootBridge)); - uint256 bridgeBalance3 = ChildERC20(rootTokens[3]).balanceOf(address(rootBridge)); - - address childToken0 = rootBridge.rootTokenToChildToken(rootTokens[0]); - address childToken1 = rootBridge.rootTokenToChildToken(rootTokens[1]); - address childToken2 = rootBridge.rootTokenToChildToken(rootTokens[2]); - address childToken3 = rootBridge.rootTokenToChildToken(rootTokens[3]); - - vm.selectFork(childId); - uint256 totalSupply0 = ChildERC20(childToken0).totalSupply(); - uint256 totalSupply1 = ChildERC20(childToken1).totalSupply(); - uint256 totalSupply2 = ChildERC20(childToken2).totalSupply(); - uint256 totalSupply3 = ChildERC20(childToken3).totalSupply(); - - console.log(string.concat(string.concat(vm.toString(bridgeBalance0), " "), vm.toString(totalSupply0))); - console.log(string.concat(string.concat(vm.toString(bridgeBalance1), " "), vm.toString(totalSupply1))); - console.log(string.concat(string.concat(vm.toString(bridgeBalance2), " "), vm.toString(totalSupply2))); - console.log(string.concat(string.concat(vm.toString(bridgeBalance3), " "), vm.toString(totalSupply3))); - - if (bridgeBalance0 != totalSupply0) { - console.log("000"); - revert( - string.concat( - "**0**", string.concat(string.concat(vm.toString(bridgeBalance0), " "), vm.toString(totalSupply0)) - ) - ); - } + function invariant_ERC20TokenBalanced() external { + for (uint256 i = 0; i < NO_OF_TOKENS; i++) { + address rootToken = rootTokens[i]; - if (bridgeBalance1 != totalSupply1) { - console.log("111"); - revert( - string.concat( - "**1**", string.concat(string.concat(vm.toString(bridgeBalance1), " "), vm.toString(totalSupply1)) - ) - ); - } + vm.selectFork(rootId); + uint256 bridgeBalance = ChildERC20(rootToken).balanceOf(address(rootBridge)); + address childToken = rootBridge.rootTokenToChildToken(rootToken); - if (bridgeBalance2 != totalSupply2) { - console.log("222"); - revert( - string.concat( - "**2**", string.concat(string.concat(vm.toString(bridgeBalance2), " "), vm.toString(totalSupply2)) - ) - ); - } + vm.selectFork(childId); + uint256 totalSupply = ChildERC20(childToken).totalSupply(); - if (bridgeBalance3 != totalSupply3) { - console.log("333"); - revert( - string.concat( - "**3**", string.concat(string.concat(vm.toString(bridgeBalance3), " "), vm.toString(totalSupply3)) - ) - ); + assertEq(bridgeBalance, totalSupply); } - - // assertEq(bridgeBalance0, totalSupply0); - // assertEq(bridgeBalance1, totalSupply1); - // assertEq(bridgeBalance2, totalSupply2); - // assertEq(bridgeBalance3, totalSupply3); - - // for (uint256 i = 0; i < NO_OF_TOKENS; i++) { - // address rootToken = rootTokens[i]; - - // vm.selectFork(rootId); - // uint256 bridgeBalance = ChildERC20(rootToken).balanceOf(address(rootBridge)); - // address childToken = rootBridge.rootTokenToChildToken(rootToken); - - // vm.selectFork(childId); - // uint256 totalSupply = ChildERC20(childToken).totalSupply(); - - // string memory log1 = string.concat(string.concat(vm.toString(bridgeBalance), " "), vm.toString(totalSupply)); - // console.log(string.concat("!!!", log1)); - // if (bridgeBalance != totalSupply) { - // console.log("I'm here...."); - // // // string memory res = string.concat(string.concat(vm.toString(bridgeBalance), " "), vm.toString(totalSupply)); - // // console.log(); - // revert(string.concat("???", log1)); - // // vm.writeFile("./something.txt", log1); - // } - // // assertEq(bridgeBalance, totalSupply); - // } } } From da6dcf31cb7c5e54907fc50e7c9c175a4b19c5c9 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Wed, 21 Feb 2024 14:17:31 +1000 Subject: [PATCH 134/155] Update InvariantBridge.t.sol --- test/invariant/InvariantBridge.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/invariant/InvariantBridge.t.sol b/test/invariant/InvariantBridge.t.sol index 8062a06d..c9c645df 100644 --- a/test/invariant/InvariantBridge.t.sol +++ b/test/invariant/InvariantBridge.t.sol @@ -201,7 +201,7 @@ contract InvariantBridge is Test { vm.selectFork(resetId); } - // forge-config: default.invariant.runs = 256 + /// forge-config: default.invariant.runs = 256 /// forge-config: default.invariant.depth = 15 /// forge-config: default.invariant.fail-on-revert = true function invariant_ERC20TokenBalanced() external { From 32e49edceb422140c159f709b8715a015d120298 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Wed, 21 Feb 2024 14:20:36 +1000 Subject: [PATCH 135/155] Update InvariantBridge.t.sol --- test/invariant/InvariantBridge.t.sol | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/invariant/InvariantBridge.t.sol b/test/invariant/InvariantBridge.t.sol index c9c645df..650a47d4 100644 --- a/test/invariant/InvariantBridge.t.sol +++ b/test/invariant/InvariantBridge.t.sol @@ -215,7 +215,14 @@ contract InvariantBridge is Test { vm.selectFork(childId); uint256 totalSupply = ChildERC20(childToken).totalSupply(); + uint256 userBalanceSum = 0; + for (uint256 j = 0; j < NO_OF_USERS; j++) { + address user = users[j]; + userBalanceSum += ChildERC20(childToken).balanceOf(user); + } + assertEq(bridgeBalance, totalSupply); + assertEq(bridgeBalance, userBalanceSum); } } } From fcfc46399276c8648c8fdab7294db93f4d535bf0 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Wed, 21 Feb 2024 14:33:49 +1000 Subject: [PATCH 136/155] Update InvariantBridge.t.sol --- test/invariant/InvariantBridge.t.sol | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/invariant/InvariantBridge.t.sol b/test/invariant/InvariantBridge.t.sol index 650a47d4..8fa902c1 100644 --- a/test/invariant/InvariantBridge.t.sol +++ b/test/invariant/InvariantBridge.t.sol @@ -225,4 +225,25 @@ contract InvariantBridge is Test { assertEq(bridgeBalance, userBalanceSum); } } + + /// forge-config: default.invariant.runs = 256 + /// forge-config: default.invariant.depth = 15 + /// forge-config: default.invariant.fail-on-revert = true + function invariant_IndividualERC20Balanced() external { + for (uint256 i = 0; i < NO_OF_TOKENS; i++) { + address rootToken = rootTokens[i]; + for (uint256 j = 0; j < NO_OF_USERS; j++) { + address user = users[j]; + + vm.selectFork(rootId); + uint256 balanceL1 = ChildERC20(rootToken).balanceOf(user); + address childToken = rootBridge.rootTokenToChildToken(rootToken); + + vm.selectFork(childId); + uint256 balanceL2 = ChildERC20(childToken).balanceOf(user); + + assertEq(balanceL1 + balanceL2, MAX_AMOUNT); + } + } + } } From 1a36766f7b75cdfd9eeefe89a4038bee35e87a99 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Wed, 21 Feb 2024 14:34:32 +1000 Subject: [PATCH 137/155] Update InvariantBridge.t.sol --- test/invariant/InvariantBridge.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/invariant/InvariantBridge.t.sol b/test/invariant/InvariantBridge.t.sol index 8fa902c1..546c6945 100644 --- a/test/invariant/InvariantBridge.t.sol +++ b/test/invariant/InvariantBridge.t.sol @@ -229,7 +229,7 @@ contract InvariantBridge is Test { /// forge-config: default.invariant.runs = 256 /// forge-config: default.invariant.depth = 15 /// forge-config: default.invariant.fail-on-revert = true - function invariant_IndividualERC20Balanced() external { + function invariant_IndividualERC20TokenBalanced() external { for (uint256 i = 0; i < NO_OF_TOKENS; i++) { address rootToken = rootTokens[i]; for (uint256 j = 0; j < NO_OF_USERS; j++) { From f49b8f93aba851d5b14110bc79d1552b242451c1 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Wed, 21 Feb 2024 15:41:42 +1000 Subject: [PATCH 138/155] Update InvariantBridge.t.sol --- test/invariant/InvariantBridge.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/invariant/InvariantBridge.t.sol b/test/invariant/InvariantBridge.t.sol index 546c6945..f9afc0cf 100644 --- a/test/invariant/InvariantBridge.t.sol +++ b/test/invariant/InvariantBridge.t.sol @@ -238,7 +238,7 @@ contract InvariantBridge is Test { vm.selectFork(rootId); uint256 balanceL1 = ChildERC20(rootToken).balanceOf(user); address childToken = rootBridge.rootTokenToChildToken(rootToken); - + vm.selectFork(childId); uint256 balanceL2 = ChildERC20(childToken).balanceOf(user); From 5d548198ce3c7d09fa0a28d47dc2ce70f9ed7f10 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Mon, 26 Feb 2024 23:22:20 +1000 Subject: [PATCH 139/155] Withdraw/Deposit --- test/invariant/InvariantBridge.t.sol | 27 ++------ .../child/ChildERC20BridgeHandler.sol | 67 ++++++++++++++++++- test/invariant/child/ChildHelper.sol | 8 +++ .../root/RootERC20BridgeFlowRateHandler.sol | 62 ++++++++++++++++- test/invariant/root/RootHelper.sol | 11 +++ 5 files changed, 150 insertions(+), 25 deletions(-) diff --git a/test/invariant/InvariantBridge.t.sol b/test/invariant/InvariantBridge.t.sol index f9afc0cf..eee4f254 100644 --- a/test/invariant/InvariantBridge.t.sol +++ b/test/invariant/InvariantBridge.t.sol @@ -187,12 +187,14 @@ contract InvariantBridge is Test { } // Target contracts - bytes4[] memory childSelectors = new bytes4[](1); + bytes4[] memory childSelectors = new bytes4[](2); childSelectors[0] = childBridgeHandler.withdraw.selector; + childSelectors[1] = childBridgeHandler.withdrawTo.selector; targetSelector(FuzzSelector({addr: address(childBridgeHandler), selectors: childSelectors})); - bytes4[] memory rootSelectors = new bytes4[](1); + bytes4[] memory rootSelectors = new bytes4[](2); rootSelectors[0] = rootBridgeHandler.deposit.selector; + rootSelectors[1] = rootBridgeHandler.depositTo.selector; targetSelector(FuzzSelector({addr: address(rootBridgeHandler), selectors: rootSelectors})); targetContract(address(childBridgeHandler)); @@ -225,25 +227,4 @@ contract InvariantBridge is Test { assertEq(bridgeBalance, userBalanceSum); } } - - /// forge-config: default.invariant.runs = 256 - /// forge-config: default.invariant.depth = 15 - /// forge-config: default.invariant.fail-on-revert = true - function invariant_IndividualERC20TokenBalanced() external { - for (uint256 i = 0; i < NO_OF_TOKENS; i++) { - address rootToken = rootTokens[i]; - for (uint256 j = 0; j < NO_OF_USERS; j++) { - address user = users[j]; - - vm.selectFork(rootId); - uint256 balanceL1 = ChildERC20(rootToken).balanceOf(user); - address childToken = rootBridge.rootTokenToChildToken(rootToken); - - vm.selectFork(childId); - uint256 balanceL2 = ChildERC20(childToken).balanceOf(user); - - assertEq(balanceL1 + balanceL2, MAX_AMOUNT); - } - } - } } diff --git a/test/invariant/child/ChildERC20BridgeHandler.sol b/test/invariant/child/ChildERC20BridgeHandler.sol index a6a3bab0..49d4a918 100644 --- a/test/invariant/child/ChildERC20BridgeHandler.sol +++ b/test/invariant/child/ChildERC20BridgeHandler.sol @@ -70,7 +70,7 @@ contract ChildERC20BridgeHandler is Test { if (currentBalance < amount) { // Deposit difference vm.selectFork(rootId); - rootHelper.deposit(user, rootToken, amount - currentBalance, gasAmt); + rootHelper.deposit(user, rootToken, amount - currentBalance, 1); vm.selectFork(childId); } @@ -86,4 +86,69 @@ contract ChildERC20BridgeHandler is Test { vm.selectFork(original); } + + function withdrawTo( + uint256 userIndexSeed, + uint256 recipientIndexSeed, + uint256 tokenIndexSeed, + uint256 amount, + uint256 gasAmt + ) public { + uint256 original = vm.activeFork(); + + // Switch to child chain + vm.selectFork(childId); + + // Bound + address user = users[bound(userIndexSeed, 0, users.length - 1)]; + address recipient = users[bound(recipientIndexSeed, 0, users.length - 1)]; + address rootToken = rootTokens[bound(tokenIndexSeed, 0, rootTokens.length - 1)]; + amount = bound(amount, 1, MAX_AMOUNT); + gasAmt = bound(gasAmt, 1, MAX_GAS); + + // Get child token + address childToken = childHelper.childBridge().rootTokenToChildToken(rootToken); + + // Get current balance + uint256 currentBalance = ChildERC20(childToken).balanceOf(user); + + if (currentBalance < amount) { + // Deposit difference + vm.selectFork(rootId); + uint256 offset = bound(userIndexSeed, 0, users.length - 1); + uint256 diff = amount - currentBalance; + address from = findDepositFrom(offset, rootToken, diff); + rootHelper.depositTo(from, user, rootToken, diff, 1); + vm.selectFork(childId); + } + + vm.selectFork(rootId); + uint256 previousLen = rootHelper.getQueueSize(recipient); + vm.selectFork(childId); + + childHelper.withdrawTo(user, recipient, childToken, amount, gasAmt); + + vm.selectFork(rootId); + rootHelper.finaliseWithdrawal(recipient, previousLen); + vm.selectFork(childId); + + vm.selectFork(original); + } + + function findDepositFrom(uint256 offset, address rootToken, uint256 requiredAmt) + public + view + returns (address from) + { + for (uint256 i = 0; i < users.length; i++) { + uint256 index = i + offset; + if (index >= users.length) { + index -= users.length; + } + if (ChildERC20(rootToken).balanceOf(users[index]) >= requiredAmt) { + from = users[index]; + break; + } + } + } } diff --git a/test/invariant/child/ChildHelper.sol b/test/invariant/child/ChildHelper.sol index 41aedff9..bbca00fe 100644 --- a/test/invariant/child/ChildHelper.sol +++ b/test/invariant/child/ChildHelper.sol @@ -22,4 +22,12 @@ contract ChildHelper is Test { vm.prank(user); childBridge.withdraw{value: gasAmt}(IChildERC20(childToken), amount); } + + function withdrawTo(address user, address recipient, address childToken, uint256 amount, uint256 gasAmt) public { + vm.deal(user, gasAmt); + totalGas += gasAmt; + + vm.prank(user); + childBridge.withdrawTo{value: gasAmt}(IChildERC20(childToken), recipient, amount); + } } diff --git a/test/invariant/root/RootERC20BridgeFlowRateHandler.sol b/test/invariant/root/RootERC20BridgeFlowRateHandler.sol index 1c62cd42..5d77a510 100644 --- a/test/invariant/root/RootERC20BridgeFlowRateHandler.sol +++ b/test/invariant/root/RootERC20BridgeFlowRateHandler.sol @@ -55,7 +55,7 @@ contract RootERC20BridgeFlowRateHandler is Test { uint256 previousLen = rootHelper.getQueueSize(user); vm.selectFork(childId); - childHelper.withdraw(user, childToken, amount - currentBalance, gasAmt); + childHelper.withdraw(user, childToken, amount - currentBalance, 1); vm.selectFork(rootId); rootHelper.finaliseWithdrawal(user, previousLen); @@ -65,4 +65,64 @@ contract RootERC20BridgeFlowRateHandler is Test { vm.selectFork(original); } + + function depositTo( + uint256 userIndexSeed, + uint256 recipientIndexSeed, + uint256 tokenIndexSeed, + uint256 amount, + uint256 gasAmt + ) public { + uint256 original = vm.activeFork(); + + // Switch to root chain + vm.selectFork(rootId); + + // Bound + address user = users[bound(userIndexSeed, 0, users.length - 1)]; + address recipient = users[bound(recipientIndexSeed, 0, users.length - 1)]; + address rootToken = rootTokens[bound(tokenIndexSeed, 0, rootTokens.length - 1)]; + amount = bound(amount, 1, MAX_AMOUNT); + gasAmt = bound(gasAmt, 1, MAX_GAS); + + // Get current balance + uint256 currentBalance = ChildERC20(rootToken).balanceOf(user); + + if (currentBalance < amount) { + // Withdraw difference + // Get child token + address childToken = rootHelper.rootBridge().rootTokenToChildToken(rootToken); + uint256 previousLen = rootHelper.getQueueSize(user); + + vm.selectFork(childId); + uint256 offset = bound(userIndexSeed, 0, users.length - 1); + uint256 diff = amount - currentBalance; + address from = findWithdrawFrom(offset, childToken, diff); + childHelper.withdrawTo(from, user, childToken, diff, 1); + vm.selectFork(rootId); + + rootHelper.finaliseWithdrawal(user, previousLen); + } + + rootHelper.depositTo(user, recipient, rootToken, amount, gasAmt); + + vm.selectFork(original); + } + + function findWithdrawFrom(uint256 offset, address childToken, uint256 requiredAmt) + public + view + returns (address from) + { + for (uint256 i = 0; i < users.length; i++) { + uint256 index = i + offset; + if (index >= users.length) { + index -= users.length; + } + if (ChildERC20(childToken).balanceOf(users[index]) >= requiredAmt) { + from = users[index]; + break; + } + } + } } diff --git a/test/invariant/root/RootHelper.sol b/test/invariant/root/RootHelper.sol index fd81acac..634a9e53 100644 --- a/test/invariant/root/RootHelper.sol +++ b/test/invariant/root/RootHelper.sol @@ -28,6 +28,17 @@ contract RootHelper is Test { rootBridge.deposit{value: gasAmt}(IERC20Metadata(rootToken), amount); } + function depositTo(address user, address recipient, address rootToken, uint256 amount, uint256 gasAmt) public { + vm.prank(user); + ChildERC20(rootToken).approve(address(rootBridge), amount); + + vm.deal(user, gasAmt); + totalGas += gasAmt; + + vm.prank(user); + rootBridge.depositTo{value: gasAmt}(IERC20Metadata(rootToken), recipient, amount); + } + function getQueueSize(address user) public view returns (uint256) { return rootBridge.getPendingWithdrawalsLength(user); } From 62e5bc95ceeb1d28e9b5ba480d0d6e0c85dc0231 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Mon, 26 Feb 2024 23:24:19 +1000 Subject: [PATCH 140/155] Update InvariantBridge.t.sol --- test/invariant/InvariantBridge.t.sol | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/invariant/InvariantBridge.t.sol b/test/invariant/InvariantBridge.t.sol index eee4f254..db4f9da3 100644 --- a/test/invariant/InvariantBridge.t.sol +++ b/test/invariant/InvariantBridge.t.sol @@ -227,4 +227,12 @@ contract InvariantBridge is Test { assertEq(bridgeBalance, userBalanceSum); } } + + /// forge-config: default.invariant.runs = 256 + /// forge-config: default.invariant.depth = 15 + /// forge-config: default.invariant.fail-on-revert = true + function invariant_GasBalanced() external { + assertEq(address(rootAdaptor).balance, rootHelper.totalGas()); + assertEq(address(childAdaptor).balance, childHelper.totalGas()); + } } From b77a4821848935ab241013b6039c2895064ece46 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Tue, 27 Feb 2024 10:09:47 +1000 Subject: [PATCH 141/155] Add helper methods --- test/invariant/child/ChildHelper.sol | 59 ++++++++++++++++++++++++ test/invariant/root/RootHelper.sol | 69 ++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/test/invariant/child/ChildHelper.sol b/test/invariant/child/ChildHelper.sol index bbca00fe..c025a8f4 100644 --- a/test/invariant/child/ChildHelper.sol +++ b/test/invariant/child/ChildHelper.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.19; import {Test} from "forge-std/Test.sol"; import {ChildERC20} from "../../../src/child/ChildERC20.sol"; +import {WIMX} from "../../../src/child/WIMX.sol"; import {ChildERC20Bridge} from "../../../src/child/ChildERC20Bridge.sol"; import {IChildERC20} from "../../../src/interfaces/child/IChildERC20.sol"; @@ -30,4 +31,62 @@ contract ChildHelper is Test { vm.prank(user); childBridge.withdrawTo{value: gasAmt}(IChildERC20(childToken), recipient, amount); } + + function withdrawIMX(address user, uint256 amount, uint256 gasAmt) public { + vm.deal(user, gasAmt); + totalGas += gasAmt; + + vm.prank(user); + childBridge.withdrawIMX{value: gasAmt + amount}(amount); + } + + function withdrawIMXTo(address user, address recipient, uint256 amount, uint256 gasAmt) public { + vm.deal(user, gasAmt); + totalGas += gasAmt; + + vm.prank(user); + childBridge.withdrawIMXTo{value: gasAmt + amount}(recipient, amount); + } + + function withdrawWIMX(address user, uint256 amount, uint256 gasAmt) public { + address payable wIMX = payable(childBridge.wIMXToken()); + + vm.prank(user); + WIMX(wIMX).approve(address(childBridge), amount); + + vm.deal(user, gasAmt); + totalGas += gasAmt; + + vm.prank(user); + childBridge.withdrawWIMX{value: gasAmt}(amount); + } + + function withdrawWIMXTo(address user, address recipient, uint256 amount, uint256 gasAmt) public { + address payable wIMX = payable(childBridge.wIMXToken()); + + vm.prank(user); + WIMX(wIMX).approve(address(childBridge), amount); + + vm.deal(user, gasAmt); + totalGas += gasAmt; + + vm.prank(user); + childBridge.withdrawWIMXTo{value: gasAmt}(recipient, amount); + } + + function withdrawETH(address user, uint256 amount, uint256 gasAmt) public { + vm.deal(user, gasAmt); + totalGas += gasAmt; + + vm.prank(user); + childBridge.withdrawETH{value: gasAmt}(amount); + } + + function withdrawETHTo(address user, address recipient, uint256 amount, uint256 gasAmt) public { + vm.deal(user, gasAmt); + totalGas += gasAmt; + + vm.prank(user); + childBridge.withdrawETHTo{value: gasAmt}(recipient, amount); + } } diff --git a/test/invariant/root/RootHelper.sol b/test/invariant/root/RootHelper.sol index 634a9e53..7190f065 100644 --- a/test/invariant/root/RootHelper.sol +++ b/test/invariant/root/RootHelper.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.19; import {Test} from "forge-std/Test.sol"; import {ChildERC20} from "../../../src/child/ChildERC20.sol"; +import {WIMX as WETH} from "../../../src/child/WIMX.sol"; import {RootERC20BridgeFlowRate} from "../../../src/root/flowrate/RootERC20BridgeFlowRate.sol"; import {IERC20Metadata} from "../../../src/root/RootERC20Bridge.sol"; @@ -39,6 +40,74 @@ contract RootHelper is Test { rootBridge.depositTo{value: gasAmt}(IERC20Metadata(rootToken), recipient, amount); } + function depositIMX(address user, uint256 amount, uint256 gasAmt) public { + address IMX = rootBridge.rootIMXToken(); + + vm.prank(user); + ChildERC20(IMX).approve(address(rootBridge), amount); + + vm.deal(user, gasAmt); + totalGas += gasAmt; + + vm.prank(user); + rootBridge.deposit{value: gasAmt}(IERC20Metadata(IMX), amount); + } + + function depositIMXTo(address user, address recipient, uint256 amount, uint256 gasAmt) public { + address IMX = rootBridge.rootIMXToken(); + + vm.prank(user); + ChildERC20(IMX).approve(address(rootBridge), amount); + + vm.deal(user, gasAmt); + totalGas += gasAmt; + + vm.prank(user); + rootBridge.depositTo{value: gasAmt}(IERC20Metadata(IMX), recipient, amount); + } + + function depositETH(address user, uint256 amount, uint256 gasAmt) public { + vm.deal(user, gasAmt); + totalGas += gasAmt; + + vm.prank(user); + rootBridge.depositETH{value: gasAmt + amount}(amount); + } + + function depositETHTo(address user, address recipient, uint256 amount, uint256 gasAmt) public { + vm.deal(user, gasAmt); + totalGas += gasAmt; + + vm.prank(user); + rootBridge.depositToETH{value: gasAmt + amount}(recipient, amount); + } + + function depositWETH(address user, uint256 amount, uint256 gasAmt) public { + address payable wETH = payable(rootBridge.rootWETHToken()); + + vm.prank(user); + WETH(wETH).approve(address(rootBridge), amount); + + vm.deal(user, gasAmt); + totalGas += gasAmt; + + vm.prank(user); + rootBridge.deposit{value: gasAmt}(IERC20Metadata(wETH), amount); + } + + function depositWETHTo(address user, address recipient, uint256 amount, uint256 gasAmt) public { + address payable wETH = payable(rootBridge.rootWETHToken()); + + vm.prank(user); + WETH(wETH).approve(address(rootBridge), amount); + + vm.deal(user, gasAmt); + totalGas += gasAmt; + + vm.prank(user); + rootBridge.depositTo{value: gasAmt}(IERC20Metadata(wETH), recipient, amount); + } + function getQueueSize(address user) public view returns (uint256) { return rootBridge.getPendingWithdrawalsLength(user); } From dde7380d130bd65e78ad5e234076a1ee7feb5d43 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Tue, 27 Feb 2024 11:07:42 +1000 Subject: [PATCH 142/155] Update --- .../child/ChildERC20BridgeHandler.sol | 38 ++++++++------ .../root/RootERC20BridgeFlowRateHandler.sol | 52 ++++++++++--------- 2 files changed, 49 insertions(+), 41 deletions(-) diff --git a/test/invariant/child/ChildERC20BridgeHandler.sol b/test/invariant/child/ChildERC20BridgeHandler.sol index 49d4a918..8c5c57f6 100644 --- a/test/invariant/child/ChildERC20BridgeHandler.sol +++ b/test/invariant/child/ChildERC20BridgeHandler.sol @@ -68,10 +68,8 @@ contract ChildERC20BridgeHandler is Test { uint256 currentBalance = ChildERC20(childToken).balanceOf(user); if (currentBalance < amount) { - // Deposit difference - vm.selectFork(rootId); - rootHelper.deposit(user, rootToken, amount - currentBalance, 1); - vm.selectFork(childId); + // Fund difference + fund(userIndexSeed, rootToken, childToken, amount - currentBalance); } vm.selectFork(rootId); @@ -113,13 +111,8 @@ contract ChildERC20BridgeHandler is Test { uint256 currentBalance = ChildERC20(childToken).balanceOf(user); if (currentBalance < amount) { - // Deposit difference - vm.selectFork(rootId); - uint256 offset = bound(userIndexSeed, 0, users.length - 1); - uint256 diff = amount - currentBalance; - address from = findDepositFrom(offset, rootToken, diff); - rootHelper.depositTo(from, user, rootToken, diff, 1); - vm.selectFork(childId); + // Fund difference + fund(userIndexSeed, rootToken, childToken, amount - currentBalance); } vm.selectFork(rootId); @@ -135,17 +128,28 @@ contract ChildERC20BridgeHandler is Test { vm.selectFork(original); } - function findDepositFrom(uint256 offset, address rootToken, uint256 requiredAmt) - public - view - returns (address from) - { + function fund(uint256 userIndexSeed, address rootToken, address childToken, uint256 diff) public { + uint256 offset = bound(userIndexSeed, 0, users.length - 1); + address user = users[offset]; + address from = findFrom(offset, childToken, diff); + if (from != address(0)) { + vm.prank(from); + ChildERC20(childToken).transfer(user, diff); + } else { + vm.selectFork(rootId); + from = findFrom(offset, rootToken, diff); + rootHelper.depositTo(from, user, rootToken, diff, 1); + vm.selectFork(childId); + } + } + + function findFrom(uint256 offset, address token, uint256 requiredAmt) public view returns (address from) { for (uint256 i = 0; i < users.length; i++) { uint256 index = i + offset; if (index >= users.length) { index -= users.length; } - if (ChildERC20(rootToken).balanceOf(users[index]) >= requiredAmt) { + if (ChildERC20(token).balanceOf(users[index]) >= requiredAmt) { from = users[index]; break; } diff --git a/test/invariant/root/RootERC20BridgeFlowRateHandler.sol b/test/invariant/root/RootERC20BridgeFlowRateHandler.sol index 5d77a510..bbf162ae 100644 --- a/test/invariant/root/RootERC20BridgeFlowRateHandler.sol +++ b/test/invariant/root/RootERC20BridgeFlowRateHandler.sol @@ -45,20 +45,15 @@ contract RootERC20BridgeFlowRateHandler is Test { amount = bound(amount, 1, MAX_AMOUNT); gasAmt = bound(gasAmt, 1, MAX_GAS); + // Get child token + address childToken = rootHelper.rootBridge().rootTokenToChildToken(rootToken); + // Get current balance uint256 currentBalance = ChildERC20(rootToken).balanceOf(user); if (currentBalance < amount) { - // Withdraw difference - // Get child token - address childToken = rootHelper.rootBridge().rootTokenToChildToken(rootToken); - uint256 previousLen = rootHelper.getQueueSize(user); - - vm.selectFork(childId); - childHelper.withdraw(user, childToken, amount - currentBalance, 1); - vm.selectFork(rootId); - - rootHelper.finaliseWithdrawal(user, previousLen); + // Fund difference + fund(userIndexSeed, rootToken, childToken, amount - currentBalance); } rootHelper.deposit(user, rootToken, amount, gasAmt); @@ -85,31 +80,40 @@ contract RootERC20BridgeFlowRateHandler is Test { amount = bound(amount, 1, MAX_AMOUNT); gasAmt = bound(gasAmt, 1, MAX_GAS); + // Get child token + address childToken = rootHelper.rootBridge().rootTokenToChildToken(rootToken); + // Get current balance uint256 currentBalance = ChildERC20(rootToken).balanceOf(user); if (currentBalance < amount) { - // Withdraw difference - // Get child token - address childToken = rootHelper.rootBridge().rootTokenToChildToken(rootToken); - uint256 previousLen = rootHelper.getQueueSize(user); + // Fund difference + fund(userIndexSeed, rootToken, childToken, amount - currentBalance); + } + rootHelper.depositTo(user, recipient, rootToken, amount, gasAmt); + + vm.selectFork(original); + } + + function fund(uint256 userIndexSeed, address rootToken, address childToken, uint256 diff) public { + uint256 offset = bound(userIndexSeed, 0, users.length - 1); + address user = users[offset]; + address from = findFrom(offset, rootToken, diff); + if (from != address(0)) { + vm.prank(from); + ChildERC20(rootToken).transfer(user, diff); + } else { + uint256 previousLen = rootHelper.getQueueSize(user); vm.selectFork(childId); - uint256 offset = bound(userIndexSeed, 0, users.length - 1); - uint256 diff = amount - currentBalance; - address from = findWithdrawFrom(offset, childToken, diff); + from = findFrom(offset, childToken, diff); childHelper.withdrawTo(from, user, childToken, diff, 1); vm.selectFork(rootId); - rootHelper.finaliseWithdrawal(user, previousLen); } - - rootHelper.depositTo(user, recipient, rootToken, amount, gasAmt); - - vm.selectFork(original); } - function findWithdrawFrom(uint256 offset, address childToken, uint256 requiredAmt) + function findFrom(uint256 offset, address token, uint256 requiredAmt) public view returns (address from) @@ -119,7 +123,7 @@ contract RootERC20BridgeFlowRateHandler is Test { if (index >= users.length) { index -= users.length; } - if (ChildERC20(childToken).balanceOf(users[index]) >= requiredAmt) { + if (ChildERC20(token).balanceOf(users[index]) >= requiredAmt) { from = users[index]; break; } From a5c4fb569036e8f152c4ad6955fd9cb65b963876 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Tue, 27 Feb 2024 12:02:00 +1000 Subject: [PATCH 143/155] Update InvariantBridge.t.sol --- test/invariant/InvariantBridge.t.sol | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/invariant/InvariantBridge.t.sol b/test/invariant/InvariantBridge.t.sol index db4f9da3..39d8d496 100644 --- a/test/invariant/InvariantBridge.t.sol +++ b/test/invariant/InvariantBridge.t.sol @@ -39,6 +39,8 @@ contract InvariantBridge is Test { ChildERC20BridgeHandler childBridgeHandler; RootERC20BridgeFlowRateHandler rootBridgeHandler; + uint256 mappingGas; + function setUp() public { childId = vm.createFork(CHILD_CHAIN_URL); rootId = vm.createFork(ROOT_CHAIN_URL); @@ -176,6 +178,7 @@ contract InvariantBridge is Test { for (uint256 i = 0; i < NO_OF_TOKENS; i++) { address rootToken = rootTokens[i]; rootBridge.mapToken{value: 1}(IERC20Metadata(rootToken)); + mappingGas += 1; // Verify address childTokenL1 = rootBridge.rootTokenToChildToken(address(rootToken)); @@ -226,13 +229,17 @@ contract InvariantBridge is Test { assertEq(bridgeBalance, totalSupply); assertEq(bridgeBalance, userBalanceSum); } + vm.selectFork(resetId); } /// forge-config: default.invariant.runs = 256 /// forge-config: default.invariant.depth = 15 /// forge-config: default.invariant.fail-on-revert = true function invariant_GasBalanced() external { - assertEq(address(rootAdaptor).balance, rootHelper.totalGas()); + vm.selectFork(rootId); + assertEq(address(rootAdaptor).balance - mappingGas, rootHelper.totalGas()); + vm.selectFork(childId); assertEq(address(childAdaptor).balance, childHelper.totalGas()); + vm.selectFork(resetId); } } From 1784c9180e022fcc901240b91939dd2d4fe80fbc Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Tue, 27 Feb 2024 12:04:52 +1000 Subject: [PATCH 144/155] Update RootERC20BridgeFlowRateHandler.sol --- test/invariant/root/RootERC20BridgeFlowRateHandler.sol | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test/invariant/root/RootERC20BridgeFlowRateHandler.sol b/test/invariant/root/RootERC20BridgeFlowRateHandler.sol index bbf162ae..60bc7f39 100644 --- a/test/invariant/root/RootERC20BridgeFlowRateHandler.sol +++ b/test/invariant/root/RootERC20BridgeFlowRateHandler.sol @@ -113,11 +113,7 @@ contract RootERC20BridgeFlowRateHandler is Test { } } - function findFrom(uint256 offset, address token, uint256 requiredAmt) - public - view - returns (address from) - { + function findFrom(uint256 offset, address token, uint256 requiredAmt) public view returns (address from) { for (uint256 i = 0; i < users.length; i++) { uint256 index = i + offset; if (index >= users.length) { From 5a47e0f77ebe032f5cc6b23cb8e88b4870f7636f Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Tue, 27 Feb 2024 12:27:04 +1000 Subject: [PATCH 145/155] Simplify testing scenario --- test/invariant/InvariantBridge.t.sol | 21 +++++++++ .../child/ChildERC20BridgeHandler.sol | 41 +++++------------ .../root/RootERC20BridgeFlowRateHandler.sol | 44 +++++++------------ 3 files changed, 47 insertions(+), 59 deletions(-) diff --git a/test/invariant/InvariantBridge.t.sol b/test/invariant/InvariantBridge.t.sol index 39d8d496..3b43463d 100644 --- a/test/invariant/InvariantBridge.t.sol +++ b/test/invariant/InvariantBridge.t.sol @@ -232,6 +232,27 @@ contract InvariantBridge is Test { vm.selectFork(resetId); } + /// forge-config: default.invariant.runs = 256 + /// forge-config: default.invariant.depth = 15 + /// forge-config: default.invariant.fail-on-revert = true + function invariant_IndividualERC20TokenBalanced() external { + for (uint256 i = 0; i < NO_OF_TOKENS; i++) { + address rootToken = rootTokens[i]; + for (uint256 j = 0; j < NO_OF_USERS; j++) { + address user = users[j]; + + vm.selectFork(rootId); + uint256 balanceL1 = ChildERC20(rootToken).balanceOf(user); + address childToken = rootBridge.rootTokenToChildToken(rootToken); + + vm.selectFork(childId); + uint256 balanceL2 = ChildERC20(childToken).balanceOf(user); + + assertEq(balanceL1 + balanceL2, MAX_AMOUNT); + } + } + } + /// forge-config: default.invariant.runs = 256 /// forge-config: default.invariant.depth = 15 /// forge-config: default.invariant.fail-on-revert = true diff --git a/test/invariant/child/ChildERC20BridgeHandler.sol b/test/invariant/child/ChildERC20BridgeHandler.sol index 8c5c57f6..859e99ad 100644 --- a/test/invariant/child/ChildERC20BridgeHandler.sol +++ b/test/invariant/child/ChildERC20BridgeHandler.sol @@ -69,7 +69,9 @@ contract ChildERC20BridgeHandler is Test { if (currentBalance < amount) { // Fund difference - fund(userIndexSeed, rootToken, childToken, amount - currentBalance); + vm.selectFork(rootId); + rootHelper.deposit(user, rootToken, amount - currentBalance, gasAmt); + vm.selectFork(childId); } vm.selectFork(rootId); @@ -112,7 +114,9 @@ contract ChildERC20BridgeHandler is Test { if (currentBalance < amount) { // Fund difference - fund(userIndexSeed, rootToken, childToken, amount - currentBalance); + vm.selectFork(rootId); + rootHelper.deposit(user, rootToken, amount - currentBalance, gasAmt); + vm.selectFork(childId); } vm.selectFork(rootId); @@ -123,36 +127,13 @@ contract ChildERC20BridgeHandler is Test { vm.selectFork(rootId); rootHelper.finaliseWithdrawal(recipient, previousLen); + // If recipient is different, transfer back + if (user != recipient) { + vm.prank(recipient); + ChildERC20(rootToken).transfer(user, amount); + } vm.selectFork(childId); vm.selectFork(original); } - - function fund(uint256 userIndexSeed, address rootToken, address childToken, uint256 diff) public { - uint256 offset = bound(userIndexSeed, 0, users.length - 1); - address user = users[offset]; - address from = findFrom(offset, childToken, diff); - if (from != address(0)) { - vm.prank(from); - ChildERC20(childToken).transfer(user, diff); - } else { - vm.selectFork(rootId); - from = findFrom(offset, rootToken, diff); - rootHelper.depositTo(from, user, rootToken, diff, 1); - vm.selectFork(childId); - } - } - - function findFrom(uint256 offset, address token, uint256 requiredAmt) public view returns (address from) { - for (uint256 i = 0; i < users.length; i++) { - uint256 index = i + offset; - if (index >= users.length) { - index -= users.length; - } - if (ChildERC20(token).balanceOf(users[index]) >= requiredAmt) { - from = users[index]; - break; - } - } - } } diff --git a/test/invariant/root/RootERC20BridgeFlowRateHandler.sol b/test/invariant/root/RootERC20BridgeFlowRateHandler.sol index 60bc7f39..65e30bca 100644 --- a/test/invariant/root/RootERC20BridgeFlowRateHandler.sol +++ b/test/invariant/root/RootERC20BridgeFlowRateHandler.sol @@ -53,7 +53,11 @@ contract RootERC20BridgeFlowRateHandler is Test { if (currentBalance < amount) { // Fund difference - fund(userIndexSeed, rootToken, childToken, amount - currentBalance); + uint256 previousLen = rootHelper.getQueueSize(user); + vm.selectFork(childId); + childHelper.withdraw(user, childToken, amount - currentBalance, gasAmt); + vm.selectFork(rootId); + rootHelper.finaliseWithdrawal(user, previousLen); } rootHelper.deposit(user, rootToken, amount, gasAmt); @@ -88,41 +92,23 @@ contract RootERC20BridgeFlowRateHandler is Test { if (currentBalance < amount) { // Fund difference - fund(userIndexSeed, rootToken, childToken, amount - currentBalance); + uint256 previousLen = rootHelper.getQueueSize(user); + vm.selectFork(childId); + childHelper.withdraw(user, childToken, amount - currentBalance, gasAmt); + vm.selectFork(rootId); + rootHelper.finaliseWithdrawal(user, previousLen); } rootHelper.depositTo(user, recipient, rootToken, amount, gasAmt); - vm.selectFork(original); - } - - function fund(uint256 userIndexSeed, address rootToken, address childToken, uint256 diff) public { - uint256 offset = bound(userIndexSeed, 0, users.length - 1); - address user = users[offset]; - address from = findFrom(offset, rootToken, diff); - if (from != address(0)) { - vm.prank(from); - ChildERC20(rootToken).transfer(user, diff); - } else { - uint256 previousLen = rootHelper.getQueueSize(user); + // If recipient is different, transfer back + if (user != recipient) { vm.selectFork(childId); - from = findFrom(offset, childToken, diff); - childHelper.withdrawTo(from, user, childToken, diff, 1); + vm.prank(recipient); + ChildERC20(childToken).transfer(user, amount); vm.selectFork(rootId); - rootHelper.finaliseWithdrawal(user, previousLen); } - } - function findFrom(uint256 offset, address token, uint256 requiredAmt) public view returns (address from) { - for (uint256 i = 0; i < users.length; i++) { - uint256 index = i + offset; - if (index >= users.length) { - index -= users.length; - } - if (ChildERC20(token).balanceOf(users[index]) >= requiredAmt) { - from = users[index]; - break; - } - } + vm.selectFork(original); } } From 96a9009eaa2cd82abd955e1da31e83bf89e452b5 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Tue, 27 Feb 2024 12:40:36 +1000 Subject: [PATCH 146/155] Update --- test/invariant/InvariantBridge.t.sol | 60 ++++++++++++------- .../child/ChildERC20BridgeHandler.sol | 34 +++++++++++ test/invariant/child/ChildHelper.sol | 16 ++--- test/invariant/root/RootHelper.sol | 16 ++--- 4 files changed, 90 insertions(+), 36 deletions(-) diff --git a/test/invariant/InvariantBridge.t.sol b/test/invariant/InvariantBridge.t.sol index 3b43463d..9cae968d 100644 --- a/test/invariant/InvariantBridge.t.sol +++ b/test/invariant/InvariantBridge.t.sol @@ -190,9 +190,10 @@ contract InvariantBridge is Test { } // Target contracts - bytes4[] memory childSelectors = new bytes4[](2); + bytes4[] memory childSelectors = new bytes4[](3); childSelectors[0] = childBridgeHandler.withdraw.selector; childSelectors[1] = childBridgeHandler.withdrawTo.selector; + childSelectors[2] = childBridgeHandler.withdrawIMX.selector; targetSelector(FuzzSelector({addr: address(childBridgeHandler), selectors: childSelectors})); bytes4[] memory rootSelectors = new bytes4[](2); @@ -232,25 +233,25 @@ contract InvariantBridge is Test { vm.selectFork(resetId); } - /// forge-config: default.invariant.runs = 256 - /// forge-config: default.invariant.depth = 15 - /// forge-config: default.invariant.fail-on-revert = true - function invariant_IndividualERC20TokenBalanced() external { - for (uint256 i = 0; i < NO_OF_TOKENS; i++) { - address rootToken = rootTokens[i]; - for (uint256 j = 0; j < NO_OF_USERS; j++) { - address user = users[j]; - - vm.selectFork(rootId); - uint256 balanceL1 = ChildERC20(rootToken).balanceOf(user); - address childToken = rootBridge.rootTokenToChildToken(rootToken); - - vm.selectFork(childId); - uint256 balanceL2 = ChildERC20(childToken).balanceOf(user); - - assertEq(balanceL1 + balanceL2, MAX_AMOUNT); - } - } + /// forge-config: default.invariant.runs = 256 + /// forge-config: default.invariant.depth = 15 + /// forge-config: default.invariant.fail-on-revert = true + function invariant_IndividualERC20TokenBalanced() external { + for (uint256 i = 0; i < NO_OF_TOKENS; i++) { + address rootToken = rootTokens[i]; + for (uint256 j = 0; j < NO_OF_USERS; j++) { + address user = users[j]; + + vm.selectFork(rootId); + uint256 balanceL1 = ChildERC20(rootToken).balanceOf(user); + address childToken = rootBridge.rootTokenToChildToken(rootToken); + + vm.selectFork(childId); + uint256 balanceL2 = ChildERC20(childToken).balanceOf(user); + + assertEq(balanceL1 + balanceL2, MAX_AMOUNT); + } + } } /// forge-config: default.invariant.runs = 256 @@ -263,4 +264,23 @@ contract InvariantBridge is Test { assertEq(address(childAdaptor).balance, childHelper.totalGas()); vm.selectFork(resetId); } + + /// forge-config: default.invariant.runs = 256 + /// forge-config: default.invariant.depth = 15 + /// forge-config: default.invariant.fail-on-revert = true + function invariant_IMXBalanced() external { + vm.selectFork(rootId); + uint256 bridgeBalance = ChildERC20(rootBridge.rootIMXToken()).balanceOf(address(rootBridge)); + + vm.selectFork(childId); + uint256 userBalanceSum = 0; + for (uint256 j = 0; j < NO_OF_USERS; j++) { + address user = users[j]; + userBalanceSum += user.balance; + } + + assertEq(bridgeBalance, userBalanceSum); + + vm.selectFork(resetId); + } } diff --git a/test/invariant/child/ChildERC20BridgeHandler.sol b/test/invariant/child/ChildERC20BridgeHandler.sol index 859e99ad..7b5e92c6 100644 --- a/test/invariant/child/ChildERC20BridgeHandler.sol +++ b/test/invariant/child/ChildERC20BridgeHandler.sol @@ -136,4 +136,38 @@ contract ChildERC20BridgeHandler is Test { vm.selectFork(original); } + + function withdrawIMX(uint256 userIndexSeed, uint256 amount, uint256 gasAmt) public { + uint256 original = vm.activeFork(); + + // Switch to child chain + vm.selectFork(childId); + + // Bound + address user = users[bound(userIndexSeed, 0, users.length - 1)]; + amount = bound(amount, 1, MAX_AMOUNT); + gasAmt = bound(gasAmt, 1, MAX_GAS); + + // Get current balance + uint256 currentBalance = user.balance; + + if (currentBalance < amount) { + // Fund difference + vm.selectFork(rootId); + rootHelper.depositIMX(user, amount - currentBalance, gasAmt); + vm.selectFork(childId); + } + + vm.selectFork(rootId); + uint256 previousLen = rootHelper.getQueueSize(user); + vm.selectFork(childId); + + childHelper.withdrawIMX(user, amount, gasAmt); + + vm.selectFork(rootId); + rootHelper.finaliseWithdrawal(user, previousLen); + vm.selectFork(childId); + + vm.selectFork(original); + } } diff --git a/test/invariant/child/ChildHelper.sol b/test/invariant/child/ChildHelper.sol index c025a8f4..ac1a8d3e 100644 --- a/test/invariant/child/ChildHelper.sol +++ b/test/invariant/child/ChildHelper.sol @@ -17,7 +17,7 @@ contract ChildHelper is Test { } function withdraw(address user, address childToken, uint256 amount, uint256 gasAmt) public { - vm.deal(user, gasAmt); + vm.deal(user, gasAmt + user.balance); totalGas += gasAmt; vm.prank(user); @@ -25,7 +25,7 @@ contract ChildHelper is Test { } function withdrawTo(address user, address recipient, address childToken, uint256 amount, uint256 gasAmt) public { - vm.deal(user, gasAmt); + vm.deal(user, gasAmt + user.balance); totalGas += gasAmt; vm.prank(user); @@ -33,7 +33,7 @@ contract ChildHelper is Test { } function withdrawIMX(address user, uint256 amount, uint256 gasAmt) public { - vm.deal(user, gasAmt); + vm.deal(user, gasAmt + user.balance); totalGas += gasAmt; vm.prank(user); @@ -41,7 +41,7 @@ contract ChildHelper is Test { } function withdrawIMXTo(address user, address recipient, uint256 amount, uint256 gasAmt) public { - vm.deal(user, gasAmt); + vm.deal(user, gasAmt + user.balance); totalGas += gasAmt; vm.prank(user); @@ -54,7 +54,7 @@ contract ChildHelper is Test { vm.prank(user); WIMX(wIMX).approve(address(childBridge), amount); - vm.deal(user, gasAmt); + vm.deal(user, gasAmt + user.balance); totalGas += gasAmt; vm.prank(user); @@ -67,7 +67,7 @@ contract ChildHelper is Test { vm.prank(user); WIMX(wIMX).approve(address(childBridge), amount); - vm.deal(user, gasAmt); + vm.deal(user, gasAmt + user.balance); totalGas += gasAmt; vm.prank(user); @@ -75,7 +75,7 @@ contract ChildHelper is Test { } function withdrawETH(address user, uint256 amount, uint256 gasAmt) public { - vm.deal(user, gasAmt); + vm.deal(user, gasAmt + user.balance); totalGas += gasAmt; vm.prank(user); @@ -83,7 +83,7 @@ contract ChildHelper is Test { } function withdrawETHTo(address user, address recipient, uint256 amount, uint256 gasAmt) public { - vm.deal(user, gasAmt); + vm.deal(user, gasAmt + user.balance); totalGas += gasAmt; vm.prank(user); diff --git a/test/invariant/root/RootHelper.sol b/test/invariant/root/RootHelper.sol index 7190f065..6f0521c7 100644 --- a/test/invariant/root/RootHelper.sol +++ b/test/invariant/root/RootHelper.sol @@ -22,7 +22,7 @@ contract RootHelper is Test { vm.prank(user); ChildERC20(rootToken).approve(address(rootBridge), amount); - vm.deal(user, gasAmt); + vm.deal(user, gasAmt + user.balance); totalGas += gasAmt; vm.prank(user); @@ -33,7 +33,7 @@ contract RootHelper is Test { vm.prank(user); ChildERC20(rootToken).approve(address(rootBridge), amount); - vm.deal(user, gasAmt); + vm.deal(user, gasAmt + user.balance); totalGas += gasAmt; vm.prank(user); @@ -46,7 +46,7 @@ contract RootHelper is Test { vm.prank(user); ChildERC20(IMX).approve(address(rootBridge), amount); - vm.deal(user, gasAmt); + vm.deal(user, gasAmt + user.balance); totalGas += gasAmt; vm.prank(user); @@ -59,7 +59,7 @@ contract RootHelper is Test { vm.prank(user); ChildERC20(IMX).approve(address(rootBridge), amount); - vm.deal(user, gasAmt); + vm.deal(user, gasAmt + user.balance); totalGas += gasAmt; vm.prank(user); @@ -67,7 +67,7 @@ contract RootHelper is Test { } function depositETH(address user, uint256 amount, uint256 gasAmt) public { - vm.deal(user, gasAmt); + vm.deal(user, gasAmt + user.balance); totalGas += gasAmt; vm.prank(user); @@ -75,7 +75,7 @@ contract RootHelper is Test { } function depositETHTo(address user, address recipient, uint256 amount, uint256 gasAmt) public { - vm.deal(user, gasAmt); + vm.deal(user, gasAmt + user.balance); totalGas += gasAmt; vm.prank(user); @@ -88,7 +88,7 @@ contract RootHelper is Test { vm.prank(user); WETH(wETH).approve(address(rootBridge), amount); - vm.deal(user, gasAmt); + vm.deal(user, gasAmt + user.balance); totalGas += gasAmt; vm.prank(user); @@ -101,7 +101,7 @@ contract RootHelper is Test { vm.prank(user); WETH(wETH).approve(address(rootBridge), amount); - vm.deal(user, gasAmt); + vm.deal(user, gasAmt + user.balance); totalGas += gasAmt; vm.prank(user); From cc7342fce98a009c77160db948b744fd45fbd32e Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Tue, 27 Feb 2024 12:52:44 +1000 Subject: [PATCH 147/155] Update InvariantBridge.t.sol --- test/invariant/InvariantBridge.t.sol | 60 ++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/test/invariant/InvariantBridge.t.sol b/test/invariant/InvariantBridge.t.sol index 9cae968d..7a1bfdd6 100644 --- a/test/invariant/InvariantBridge.t.sol +++ b/test/invariant/InvariantBridge.t.sol @@ -252,6 +252,7 @@ contract InvariantBridge is Test { assertEq(balanceL1 + balanceL2, MAX_AMOUNT); } } + vm.selectFork(resetId); } /// forge-config: default.invariant.runs = 256 @@ -273,14 +274,73 @@ contract InvariantBridge is Test { uint256 bridgeBalance = ChildERC20(rootBridge.rootIMXToken()).balanceOf(address(rootBridge)); vm.selectFork(childId); + uint256 totalSupply = IMX_DEPOSIT_LIMIT - address(childBridge).balance; + uint256 userBalanceSum = 0; for (uint256 j = 0; j < NO_OF_USERS; j++) { address user = users[j]; userBalanceSum += user.balance; } + assertEq(bridgeBalance, totalSupply); assertEq(bridgeBalance, userBalanceSum); + vm.selectFork(resetId); + } + + /// forge-config: default.invariant.runs = 256 + /// forge-config: default.invariant.depth = 15 + /// forge-config: default.invariant.fail-on-revert = true + function invariant_IndividualIMXBalanced() external { + for (uint256 j = 0; j < NO_OF_USERS; j++) { + address user = users[j]; + + vm.selectFork(rootId); + uint256 balanceL1 = ChildERC20(rootBridge.rootIMXToken()).balanceOf(user); + + vm.selectFork(childId); + uint256 balanceL2 = user.balance; + + assertEq(balanceL1 + balanceL2, MAX_AMOUNT); + } + vm.selectFork(resetId); + } + + /// forge-config: default.invariant.runs = 256 + /// forge-config: default.invariant.depth = 15 + /// forge-config: default.invariant.fail-on-revert = true + function invariant_ETHBalanced() external { + vm.selectFork(rootId); + uint256 bridgeBalance = address(rootBridge).balance; + vm.selectFork(childId); + uint256 totalSupply = ChildERC20(childBridge.childETHToken()).totalSupply(); + + uint256 userBalanceSum = 0; + for (uint256 j = 0; j < NO_OF_USERS; j++) { + address user = users[j]; + userBalanceSum += ChildERC20(childBridge.childETHToken()).balanceOf(user); + } + + assertEq(bridgeBalance, totalSupply); + assertEq(bridgeBalance, userBalanceSum); + vm.selectFork(resetId); + } + + /// forge-config: default.invariant.runs = 256 + /// forge-config: default.invariant.depth = 15 + /// forge-config: default.invariant.fail-on-revert = true + function invariant_IndividualETHBalanced() external { + for (uint256 j = 0; j < NO_OF_USERS; j++) { + address user = users[j]; + + vm.selectFork(rootId); + uint256 balanceL1 = user.balance; + + vm.selectFork(childId); + uint256 balanceL2 = ChildERC20(childBridge.childETHToken()).balanceOf(user); + + assertEq(balanceL1 + balanceL2, MAX_AMOUNT); + } vm.selectFork(resetId); } } From 2348071d8a9364f34b0eea4ffadf2245598363f9 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Tue, 27 Feb 2024 12:59:02 +1000 Subject: [PATCH 148/155] Update InvariantBridge.t.sol --- test/invariant/InvariantBridge.t.sol | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/invariant/InvariantBridge.t.sol b/test/invariant/InvariantBridge.t.sol index 7a1bfdd6..11d1019e 100644 --- a/test/invariant/InvariantBridge.t.sol +++ b/test/invariant/InvariantBridge.t.sol @@ -343,4 +343,22 @@ contract InvariantBridge is Test { } vm.selectFork(resetId); } + + /// forge-config: default.invariant.runs = 256 + /// forge-config: default.invariant.depth = 15 + /// forge-config: default.invariant.fail-on-revert = true + function invariant_NoRemainingWETH() external { + vm.selectFork(rootId); + assertEq(rootBridge.rootWETHToken().balance, 0); + vm.selectFork(resetId); + } + + /// forge-config: default.invariant.runs = 256 + /// forge-config: default.invariant.depth = 15 + /// forge-config: default.invariant.fail-on-revert = true + function invariant_NoRemainingWIMX() external { + vm.selectFork(childId); + assertEq(childBridge.wIMXToken().balance, 0); + vm.selectFork(resetId); + } } From 83e4f854a4bcbee8cfea11a72bd0aff0d480d3c5 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Tue, 27 Feb 2024 14:04:50 +1000 Subject: [PATCH 149/155] Add tests --- test/invariant/InvariantBridge.t.sol | 29 +-- .../child/ChildERC20BridgeHandler.sol | 201 ++++++++++++++++++ 2 files changed, 218 insertions(+), 12 deletions(-) diff --git a/test/invariant/InvariantBridge.t.sol b/test/invariant/InvariantBridge.t.sol index 11d1019e..9b5ab6cd 100644 --- a/test/invariant/InvariantBridge.t.sol +++ b/test/invariant/InvariantBridge.t.sol @@ -190,10 +190,15 @@ contract InvariantBridge is Test { } // Target contracts - bytes4[] memory childSelectors = new bytes4[](3); + bytes4[] memory childSelectors = new bytes4[](8); childSelectors[0] = childBridgeHandler.withdraw.selector; childSelectors[1] = childBridgeHandler.withdrawTo.selector; childSelectors[2] = childBridgeHandler.withdrawIMX.selector; + childSelectors[3] = childBridgeHandler.withdrawIMXTo.selector; + childSelectors[4] = childBridgeHandler.withdrawWIMX.selector; + childSelectors[5] = childBridgeHandler.withdrawWIMXTo.selector; + childSelectors[6] = childBridgeHandler.withdrawETH.selector; + childSelectors[7] = childBridgeHandler.withdrawETHTo.selector; targetSelector(FuzzSelector({addr: address(childBridgeHandler), selectors: childSelectors})); bytes4[] memory rootSelectors = new bytes4[](2); @@ -255,17 +260,6 @@ contract InvariantBridge is Test { vm.selectFork(resetId); } - /// forge-config: default.invariant.runs = 256 - /// forge-config: default.invariant.depth = 15 - /// forge-config: default.invariant.fail-on-revert = true - function invariant_GasBalanced() external { - vm.selectFork(rootId); - assertEq(address(rootAdaptor).balance - mappingGas, rootHelper.totalGas()); - vm.selectFork(childId); - assertEq(address(childAdaptor).balance, childHelper.totalGas()); - vm.selectFork(resetId); - } - /// forge-config: default.invariant.runs = 256 /// forge-config: default.invariant.depth = 15 /// forge-config: default.invariant.fail-on-revert = true @@ -361,4 +355,15 @@ contract InvariantBridge is Test { assertEq(childBridge.wIMXToken().balance, 0); vm.selectFork(resetId); } + + /// forge-config: default.invariant.runs = 256 + /// forge-config: default.invariant.depth = 15 + /// forge-config: default.invariant.fail-on-revert = true + function invariant_GasBalanced() external { + vm.selectFork(rootId); + assertEq(address(rootAdaptor).balance - mappingGas, rootHelper.totalGas()); + vm.selectFork(childId); + assertEq(address(childAdaptor).balance, childHelper.totalGas()); + vm.selectFork(resetId); + } } diff --git a/test/invariant/child/ChildERC20BridgeHandler.sol b/test/invariant/child/ChildERC20BridgeHandler.sol index 7b5e92c6..72980c30 100644 --- a/test/invariant/child/ChildERC20BridgeHandler.sol +++ b/test/invariant/child/ChildERC20BridgeHandler.sol @@ -5,6 +5,7 @@ import {Test} from "forge-std/Test.sol"; import {ChildERC20} from "../../../src/child/ChildERC20.sol"; import {ChildHelper} from "./ChildHelper.sol"; import {RootHelper} from "../root/RootHelper.sol"; +import {WIMX} from "../../../src/child/WIMX.sol"; contract ChildERC20BridgeHandler is Test { uint256 public constant MAX_AMOUNT = 10000; @@ -170,4 +171,204 @@ contract ChildERC20BridgeHandler is Test { vm.selectFork(original); } + + function withdrawIMXTo(uint256 userIndexSeed, uint256 recipientIndexSeed, uint256 amount, uint256 gasAmt) public { + uint256 original = vm.activeFork(); + + // Switch to child chain + vm.selectFork(childId); + + // Bound + address user = users[bound(userIndexSeed, 0, users.length - 1)]; + address recipient = users[bound(recipientIndexSeed, 0, users.length - 1)]; + amount = bound(amount, 1, MAX_AMOUNT); + gasAmt = bound(gasAmt, 1, MAX_GAS); + + // Get current balance + uint256 currentBalance = user.balance; + + if (currentBalance < amount) { + // Fund difference + vm.selectFork(rootId); + rootHelper.depositIMX(user, amount - currentBalance, gasAmt); + vm.selectFork(childId); + } + + vm.selectFork(rootId); + uint256 previousLen = rootHelper.getQueueSize(recipient); + vm.selectFork(childId); + + childHelper.withdrawIMXTo(user, recipient, amount, gasAmt); + + vm.selectFork(rootId); + rootHelper.finaliseWithdrawal(recipient, previousLen); + // If recipient is different, transfer back + if (user != recipient) { + address imx = rootHelper.rootBridge().rootIMXToken(); + vm.prank(recipient); + ChildERC20(imx).transfer(user, amount); + } + vm.selectFork(childId); + + vm.selectFork(original); + } + + function withdrawWIMX(uint256 userIndexSeed, uint256 amount, uint256 gasAmt) public { + uint256 original = vm.activeFork(); + + // Switch to child chain + vm.selectFork(childId); + + // Bound + address user = users[bound(userIndexSeed, 0, users.length - 1)]; + amount = bound(amount, 1, MAX_AMOUNT); + gasAmt = bound(gasAmt, 1, MAX_GAS); + + // Get current balance + uint256 currentBalance = user.balance; + + if (currentBalance < amount) { + // Fund difference + vm.selectFork(rootId); + rootHelper.depositIMX(user, amount - currentBalance, gasAmt); + vm.selectFork(childId); + } + + vm.selectFork(rootId); + uint256 previousLen = rootHelper.getQueueSize(user); + vm.selectFork(childId); + + // Wrap IMX + address payable wIMX = payable(childHelper.childBridge().wIMXToken()); + vm.prank(user); + WIMX(wIMX).deposit{value: amount}(); + + childHelper.withdrawWIMX(user, amount, gasAmt); + + vm.selectFork(rootId); + rootHelper.finaliseWithdrawal(user, previousLen); + vm.selectFork(childId); + + vm.selectFork(original); + } + + function withdrawWIMXTo(uint256 userIndexSeed, uint256 recipientIndexSeed, uint256 amount, uint256 gasAmt) public { + uint256 original = vm.activeFork(); + + // Switch to child chain + vm.selectFork(childId); + + // Bound + address user = users[bound(userIndexSeed, 0, users.length - 1)]; + address recipient = users[bound(recipientIndexSeed, 0, users.length - 1)]; + amount = bound(amount, 1, MAX_AMOUNT); + gasAmt = bound(gasAmt, 1, MAX_GAS); + + // Get current balance + uint256 currentBalance = user.balance; + + if (currentBalance < amount) { + // Fund difference + vm.selectFork(rootId); + rootHelper.depositIMX(user, amount - currentBalance, gasAmt); + vm.selectFork(childId); + } + + vm.selectFork(rootId); + uint256 previousLen = rootHelper.getQueueSize(recipient); + vm.selectFork(childId); + + // Wrap IMX + address payable wIMX = payable(childHelper.childBridge().wIMXToken()); + vm.prank(user); + WIMX(wIMX).deposit{value: amount}(); + + childHelper.withdrawWIMXTo(user, recipient, amount, gasAmt); + + vm.selectFork(rootId); + rootHelper.finaliseWithdrawal(recipient, previousLen); + // If recipient is different, transfer back + if (user != recipient) { + address imx = rootHelper.rootBridge().rootIMXToken(); + vm.prank(recipient); + ChildERC20(imx).transfer(user, amount); + } + vm.selectFork(childId); + + vm.selectFork(original); + } + + function withdrawETH(uint256 userIndexSeed, uint256 amount, uint256 gasAmt) public { + uint256 original = vm.activeFork(); + + // Switch to child chain + vm.selectFork(childId); + + // Bound + address user = users[bound(userIndexSeed, 0, users.length - 1)]; + amount = bound(amount, 1, MAX_AMOUNT); + gasAmt = bound(gasAmt, 1, MAX_GAS); + + // Get current balance + uint256 currentBalance = ChildERC20(childHelper.childBridge().childETHToken()).balanceOf(user); + + if (currentBalance < amount) { + // Fund difference + vm.selectFork(rootId); + rootHelper.depositETH(user, amount - currentBalance, gasAmt); + vm.selectFork(childId); + } + + vm.selectFork(rootId); + uint256 previousLen = rootHelper.getQueueSize(user); + vm.selectFork(childId); + + childHelper.withdrawETH(user, amount, gasAmt); + + vm.selectFork(rootId); + rootHelper.finaliseWithdrawal(user, previousLen); + vm.selectFork(childId); + + vm.selectFork(original); + } + + function withdrawETHTo(uint256 userIndexSeed, uint256 recipientIndexSeed, uint256 amount, uint256 gasAmt) public { + uint256 original = vm.activeFork(); + + // Switch to child chain + vm.selectFork(childId); + + // Bound + address user = users[bound(userIndexSeed, 0, users.length - 1)]; + address recipient = users[bound(recipientIndexSeed, 0, users.length - 1)]; + amount = bound(amount, 1, MAX_AMOUNT); + gasAmt = bound(gasAmt, 1, MAX_GAS); + + // Get current balance + uint256 currentBalance = ChildERC20(childHelper.childBridge().childETHToken()).balanceOf(user); + + if (currentBalance < amount) { + // Fund difference + vm.selectFork(rootId); + rootHelper.depositETH(user, amount - currentBalance, gasAmt); + vm.selectFork(childId); + } + + vm.selectFork(rootId); + uint256 previousLen = rootHelper.getQueueSize(recipient); + vm.selectFork(childId); + + childHelper.withdrawETHTo(user, recipient, amount, gasAmt); + + vm.selectFork(rootId); + rootHelper.finaliseWithdrawal(recipient, previousLen); + // If recipient is different, transfer back + if (user != recipient) { + vm.prank(recipient); + user.call{value: amount}(""); + } + vm.selectFork(childId); + + vm.selectFork(original); + } } From d30fdc7e8ef018805272159fe45a0fe0dc6118c7 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Tue, 27 Feb 2024 14:29:06 +1000 Subject: [PATCH 150/155] Add tests --- test/invariant/InvariantBridge.t.sol | 8 +- .../root/RootERC20BridgeFlowRateHandler.sol | 208 ++++++++++++++++++ 2 files changed, 215 insertions(+), 1 deletion(-) diff --git a/test/invariant/InvariantBridge.t.sol b/test/invariant/InvariantBridge.t.sol index 9b5ab6cd..ee52ffaa 100644 --- a/test/invariant/InvariantBridge.t.sol +++ b/test/invariant/InvariantBridge.t.sol @@ -201,9 +201,15 @@ contract InvariantBridge is Test { childSelectors[7] = childBridgeHandler.withdrawETHTo.selector; targetSelector(FuzzSelector({addr: address(childBridgeHandler), selectors: childSelectors})); - bytes4[] memory rootSelectors = new bytes4[](2); + bytes4[] memory rootSelectors = new bytes4[](8); rootSelectors[0] = rootBridgeHandler.deposit.selector; rootSelectors[1] = rootBridgeHandler.depositTo.selector; + rootSelectors[2] = rootBridgeHandler.depositIMX.selector; + rootSelectors[3] = rootBridgeHandler.depositIMXTo.selector; + rootSelectors[4] = rootBridgeHandler.depositETH.selector; + rootSelectors[5] = rootBridgeHandler.depositETHTo.selector; + rootSelectors[6] = rootBridgeHandler.depositWETH.selector; + rootSelectors[7] = rootBridgeHandler.depositWETHTo.selector; targetSelector(FuzzSelector({addr: address(rootBridgeHandler), selectors: rootSelectors})); targetContract(address(childBridgeHandler)); diff --git a/test/invariant/root/RootERC20BridgeFlowRateHandler.sol b/test/invariant/root/RootERC20BridgeFlowRateHandler.sol index 65e30bca..a4334d6d 100644 --- a/test/invariant/root/RootERC20BridgeFlowRateHandler.sol +++ b/test/invariant/root/RootERC20BridgeFlowRateHandler.sol @@ -5,6 +5,7 @@ import {Test} from "forge-std/Test.sol"; import {ChildERC20} from "../../../src/child/ChildERC20.sol"; import {ChildHelper} from "../child/ChildHelper.sol"; import {RootHelper} from "./RootHelper.sol"; +import {WIMX as WETH} from "../../../src/child/WIMX.sol"; contract RootERC20BridgeFlowRateHandler is Test { uint256 public constant MAX_AMOUNT = 10000; @@ -111,4 +112,211 @@ contract RootERC20BridgeFlowRateHandler is Test { vm.selectFork(original); } + + function depositIMX(uint256 userIndexSeed, uint256 amount, uint256 gasAmt) public { + uint256 original = vm.activeFork(); + + // Switch to root chain + vm.selectFork(rootId); + + // Bound + address user = users[bound(userIndexSeed, 0, users.length - 1)]; + amount = bound(amount, 1, MAX_AMOUNT); + gasAmt = bound(gasAmt, 1, MAX_GAS); + + // Get current balance + uint256 currentBalance = ChildERC20(rootHelper.rootBridge().rootIMXToken()).balanceOf(user); + + if (currentBalance < amount) { + // Fund difference + uint256 previousLen = rootHelper.getQueueSize(user); + vm.selectFork(childId); + childHelper.withdrawIMX(user, amount - currentBalance, gasAmt); + vm.selectFork(rootId); + rootHelper.finaliseWithdrawal(user, previousLen); + } + + rootHelper.depositIMX(user, amount, gasAmt); + + vm.selectFork(original); + } + + function depositIMXTo(uint256 userIndexSeed, uint256 recipientIndexSeed, uint256 amount, uint256 gasAmt) public { + uint256 original = vm.activeFork(); + + // Switch to root chain + vm.selectFork(rootId); + + // Bound + address user = users[bound(userIndexSeed, 0, users.length - 1)]; + address recipient = users[bound(recipientIndexSeed, 0, users.length - 1)]; + amount = bound(amount, 1, MAX_AMOUNT); + gasAmt = bound(gasAmt, 1, MAX_GAS); + + // Get current balance + uint256 currentBalance = ChildERC20(rootHelper.rootBridge().rootIMXToken()).balanceOf(user); + + if (currentBalance < amount) { + // Fund difference + uint256 previousLen = rootHelper.getQueueSize(user); + vm.selectFork(childId); + childHelper.withdrawIMX(user, amount - currentBalance, gasAmt); + vm.selectFork(rootId); + rootHelper.finaliseWithdrawal(user, previousLen); + } + + rootHelper.depositIMXTo(user, recipient, amount, gasAmt); + + // If recipient is different, transfer back + if (user != recipient) { + vm.selectFork(childId); + vm.prank(recipient); + user.call{value: amount}(""); + vm.selectFork(rootId); + } + + vm.selectFork(original); + } + + function depositETH(uint256 userIndexSeed, uint256 amount, uint256 gasAmt) public { + uint256 original = vm.activeFork(); + + // Switch to root chain + vm.selectFork(rootId); + + // Bound + address user = users[bound(userIndexSeed, 0, users.length - 1)]; + amount = bound(amount, 1, MAX_AMOUNT); + gasAmt = bound(gasAmt, 1, MAX_GAS); + + // Get current balance + uint256 currentBalance = user.balance; + + if (currentBalance < amount) { + // Fund difference + uint256 previousLen = rootHelper.getQueueSize(user); + vm.selectFork(childId); + childHelper.withdrawETH(user, amount - currentBalance, gasAmt); + vm.selectFork(rootId); + rootHelper.finaliseWithdrawal(user, previousLen); + } + + rootHelper.depositETH(user, amount, gasAmt); + + vm.selectFork(original); + } + + function depositETHTo(uint256 userIndexSeed, uint256 recipientIndexSeed, uint256 amount, uint256 gasAmt) public { + uint256 original = vm.activeFork(); + + // Switch to root chain + vm.selectFork(rootId); + + // Bound + address user = users[bound(userIndexSeed, 0, users.length - 1)]; + address recipient = users[bound(recipientIndexSeed, 0, users.length - 1)]; + amount = bound(amount, 1, MAX_AMOUNT); + gasAmt = bound(gasAmt, 1, MAX_GAS); + + // Get current balance + uint256 currentBalance = user.balance; + + if (currentBalance < amount) { + // Fund difference + uint256 previousLen = rootHelper.getQueueSize(user); + vm.selectFork(childId); + childHelper.withdrawETH(user, amount - currentBalance, gasAmt); + vm.selectFork(rootId); + rootHelper.finaliseWithdrawal(user, previousLen); + } + + rootHelper.depositETHTo(user, recipient, amount, gasAmt); + + // If recipient is different, transfer back + if (user != recipient) { + vm.selectFork(childId); + address eth = childHelper.childBridge().childETHToken(); + vm.prank(recipient); + ChildERC20(eth).transfer(user, amount); + } + vm.selectFork(childId); + + vm.selectFork(original); + } + + function depositWETH(uint256 userIndexSeed, uint256 amount, uint256 gasAmt) public { + uint256 original = vm.activeFork(); + + // Switch to root chain + vm.selectFork(rootId); + + // Bound + address user = users[bound(userIndexSeed, 0, users.length - 1)]; + amount = bound(amount, 1, MAX_AMOUNT); + gasAmt = bound(gasAmt, 1, MAX_GAS); + + // Get current balance + uint256 currentBalance = user.balance; + + if (currentBalance < amount) { + // Fund difference + uint256 previousLen = rootHelper.getQueueSize(user); + vm.selectFork(childId); + childHelper.withdrawETH(user, amount - currentBalance, gasAmt); + vm.selectFork(rootId); + rootHelper.finaliseWithdrawal(user, previousLen); + } + + // Wrap ETH + address payable wETH = payable(rootHelper.rootBridge().rootWETHToken()); + vm.prank(user); + WETH(wETH).deposit{value: amount}(); + + rootHelper.depositWETH(user, amount, gasAmt); + + vm.selectFork(original); + } + + function depositWETHTo(uint256 userIndexSeed, uint256 recipientIndexSeed, uint256 amount, uint256 gasAmt) public { + uint256 original = vm.activeFork(); + + // Switch to root chain + vm.selectFork(rootId); + + // Bound + address user = users[bound(userIndexSeed, 0, users.length - 1)]; + address recipient = users[bound(recipientIndexSeed, 0, users.length - 1)]; + amount = bound(amount, 1, MAX_AMOUNT); + gasAmt = bound(gasAmt, 1, MAX_GAS); + + // Get current balance + uint256 currentBalance = user.balance; + + if (currentBalance < amount) { + // Fund difference + uint256 previousLen = rootHelper.getQueueSize(user); + vm.selectFork(childId); + childHelper.withdrawETH(user, amount - currentBalance, gasAmt); + vm.selectFork(rootId); + rootHelper.finaliseWithdrawal(user, previousLen); + } + + // Wrap ETH + address payable wETH = payable(rootHelper.rootBridge().rootWETHToken()); + vm.prank(user); + WETH(wETH).deposit{value: amount}(); + + rootHelper.depositWETHTo(user, recipient, amount, gasAmt); + + // If recipient is different, transfer back + if (user != recipient) { + vm.selectFork(childId); + address eth = childHelper.childBridge().childETHToken(); + vm.prank(recipient); + ChildERC20(eth).transfer(user, amount); + vm.selectFork(rootId); + } + + vm.selectFork(original); + } } From 8a55ab8e9e17378ee04314815f94691abce8c5fa Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Wed, 28 Feb 2024 13:26:15 +1000 Subject: [PATCH 151/155] Update InvariantBridge.t.sol --- test/invariant/InvariantBridge.t.sol | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/test/invariant/InvariantBridge.t.sol b/test/invariant/InvariantBridge.t.sol index ee52ffaa..795a6258 100644 --- a/test/invariant/InvariantBridge.t.sol +++ b/test/invariant/InvariantBridge.t.sol @@ -17,6 +17,12 @@ import "forge-std/console.sol"; contract InvariantBridge is Test { string public constant CHILD_CHAIN_URL = "http://127.0.0.1:8500"; string public constant ROOT_CHAIN_URL = "http://127.0.0.1:8501"; + // Forge has an issue that fails to reset state at the end of each run. + // For example, we found out that if the context stays at child chain at the end of setUp(), + // the state on child chain will not be reset or if the context stays at root chain, the state + // on the root chain will not be reset, which causes subsequent runs to fail. + // We introduced a third chain called reset chain and we make the context to stay on the reset chain + // in order to reset state on both child chain and root chain. string public constant RESET_CHAIN_URL = "http://127.0.0.1:8502"; uint256 public constant IMX_DEPOSIT_LIMIT = 10000 ether; uint256 public constant MAX_AMOUNT = 10000; @@ -127,15 +133,14 @@ contract InvariantBridge is Test { } // Create tokens. for (uint256 i = 0; i < NO_OF_TOKENS; i++) { - vm.prank(address(0x234)); + vm.startPrank(address(0x234)); ChildERC20 rootToken = new ChildERC20(); - vm.prank(address(0x234)); rootToken.initialize(address(123), "Test", "TST", 18); // Mint token to user for (uint256 j = 0; j < NO_OF_USERS; j++) { - vm.prank(address(0x234)); rootToken.mint(users[j], MAX_AMOUNT); } + vm.stopPrank(); // Configure rate for half tokens if (i % 2 == 0) { vm.prank(ADMIN); @@ -250,12 +255,13 @@ contract InvariantBridge is Test { function invariant_IndividualERC20TokenBalanced() external { for (uint256 i = 0; i < NO_OF_TOKENS; i++) { address rootToken = rootTokens[i]; + vm.selectFork(rootId); + address childToken = rootBridge.rootTokenToChildToken(rootToken); for (uint256 j = 0; j < NO_OF_USERS; j++) { address user = users[j]; vm.selectFork(rootId); uint256 balanceL1 = ChildERC20(rootToken).balanceOf(user); - address childToken = rootBridge.rootTokenToChildToken(rootToken); vm.selectFork(childId); uint256 balanceL2 = ChildERC20(childToken).balanceOf(user); From ea7943f06e2b477372b6e5fa9d209103047d18b0 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Fri, 1 Mar 2024 14:49:15 +1000 Subject: [PATCH 152/155] Cleanup unnecessary code --- test/invariant/InvariantBridge.t.sol | 8 -------- test/invariant/child/ChildERC20BridgeHandler.sol | 8 -------- 2 files changed, 16 deletions(-) diff --git a/test/invariant/InvariantBridge.t.sol b/test/invariant/InvariantBridge.t.sol index 795a6258..6ded7493 100644 --- a/test/invariant/InvariantBridge.t.sol +++ b/test/invariant/InvariantBridge.t.sol @@ -246,7 +246,6 @@ contract InvariantBridge is Test { assertEq(bridgeBalance, totalSupply); assertEq(bridgeBalance, userBalanceSum); } - vm.selectFork(resetId); } /// forge-config: default.invariant.runs = 256 @@ -269,7 +268,6 @@ contract InvariantBridge is Test { assertEq(balanceL1 + balanceL2, MAX_AMOUNT); } } - vm.selectFork(resetId); } /// forge-config: default.invariant.runs = 256 @@ -290,7 +288,6 @@ contract InvariantBridge is Test { assertEq(bridgeBalance, totalSupply); assertEq(bridgeBalance, userBalanceSum); - vm.selectFork(resetId); } /// forge-config: default.invariant.runs = 256 @@ -308,7 +305,6 @@ contract InvariantBridge is Test { assertEq(balanceL1 + balanceL2, MAX_AMOUNT); } - vm.selectFork(resetId); } /// forge-config: default.invariant.runs = 256 @@ -329,7 +325,6 @@ contract InvariantBridge is Test { assertEq(bridgeBalance, totalSupply); assertEq(bridgeBalance, userBalanceSum); - vm.selectFork(resetId); } /// forge-config: default.invariant.runs = 256 @@ -356,7 +351,6 @@ contract InvariantBridge is Test { function invariant_NoRemainingWETH() external { vm.selectFork(rootId); assertEq(rootBridge.rootWETHToken().balance, 0); - vm.selectFork(resetId); } /// forge-config: default.invariant.runs = 256 @@ -365,7 +359,6 @@ contract InvariantBridge is Test { function invariant_NoRemainingWIMX() external { vm.selectFork(childId); assertEq(childBridge.wIMXToken().balance, 0); - vm.selectFork(resetId); } /// forge-config: default.invariant.runs = 256 @@ -376,6 +369,5 @@ contract InvariantBridge is Test { assertEq(address(rootAdaptor).balance - mappingGas, rootHelper.totalGas()); vm.selectFork(childId); assertEq(address(childAdaptor).balance, childHelper.totalGas()); - vm.selectFork(resetId); } } diff --git a/test/invariant/child/ChildERC20BridgeHandler.sol b/test/invariant/child/ChildERC20BridgeHandler.sol index 72980c30..65f9d485 100644 --- a/test/invariant/child/ChildERC20BridgeHandler.sol +++ b/test/invariant/child/ChildERC20BridgeHandler.sol @@ -83,7 +83,6 @@ contract ChildERC20BridgeHandler is Test { vm.selectFork(rootId); rootHelper.finaliseWithdrawal(user, previousLen); - vm.selectFork(childId); vm.selectFork(original); } @@ -133,7 +132,6 @@ contract ChildERC20BridgeHandler is Test { vm.prank(recipient); ChildERC20(rootToken).transfer(user, amount); } - vm.selectFork(childId); vm.selectFork(original); } @@ -167,7 +165,6 @@ contract ChildERC20BridgeHandler is Test { vm.selectFork(rootId); rootHelper.finaliseWithdrawal(user, previousLen); - vm.selectFork(childId); vm.selectFork(original); } @@ -208,7 +205,6 @@ contract ChildERC20BridgeHandler is Test { vm.prank(recipient); ChildERC20(imx).transfer(user, amount); } - vm.selectFork(childId); vm.selectFork(original); } @@ -247,7 +243,6 @@ contract ChildERC20BridgeHandler is Test { vm.selectFork(rootId); rootHelper.finaliseWithdrawal(user, previousLen); - vm.selectFork(childId); vm.selectFork(original); } @@ -293,7 +288,6 @@ contract ChildERC20BridgeHandler is Test { vm.prank(recipient); ChildERC20(imx).transfer(user, amount); } - vm.selectFork(childId); vm.selectFork(original); } @@ -327,7 +321,6 @@ contract ChildERC20BridgeHandler is Test { vm.selectFork(rootId); rootHelper.finaliseWithdrawal(user, previousLen); - vm.selectFork(childId); vm.selectFork(original); } @@ -367,7 +360,6 @@ contract ChildERC20BridgeHandler is Test { vm.prank(recipient); user.call{value: amount}(""); } - vm.selectFork(childId); vm.selectFork(original); } From 23b96bf2856dd3a34f96694b71cba5e1229ad4c7 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Wed, 6 Mar 2024 13:06:40 +1000 Subject: [PATCH 153/155] Update InvariantBridge.t.sol --- test/invariant/InvariantBridge.t.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/invariant/InvariantBridge.t.sol b/test/invariant/InvariantBridge.t.sol index 6ded7493..1b31b684 100644 --- a/test/invariant/InvariantBridge.t.sol +++ b/test/invariant/InvariantBridge.t.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.19; import {Test} from "forge-std/Test.sol"; import {ChildERC20} from "../../src/child/ChildERC20.sol"; import {WIMX} from "../../src/child/WIMX.sol"; +import {WETH} from "../../src/lib/WETH.sol"; import {IChildERC20Bridge, ChildERC20Bridge} from "../../src/child/ChildERC20Bridge.sol"; import {IRootERC20Bridge, IERC20Metadata} from "../../src/root/RootERC20Bridge.sol"; import {RootERC20BridgeFlowRate} from "../../src/root/flowrate/RootERC20BridgeFlowRate.sol"; @@ -74,7 +75,7 @@ contract InvariantBridge is Test { rootBridge = new RootERC20BridgeFlowRate(address(this)); ChildERC20 rootIMXToken = new ChildERC20(); rootIMXToken.initialize(address(123), "Immutable X", "IMX", 18); - WIMX wETH = new WIMX(); + WETH wETH = new WETH(); // Deploy contracts on reset chain. vm.selectFork(resetId); From 7071c46d5f8950f68cbe2bfcb6a6b1948d15a4ca Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Wed, 6 Mar 2024 14:01:28 +1000 Subject: [PATCH 154/155] Cleanup --- test/invariant/InvariantBridge.t.sol | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/test/invariant/InvariantBridge.t.sol b/test/invariant/InvariantBridge.t.sol index 1b31b684..dbcaa09a 100644 --- a/test/invariant/InvariantBridge.t.sol +++ b/test/invariant/InvariantBridge.t.sol @@ -77,14 +77,6 @@ contract InvariantBridge is Test { rootIMXToken.initialize(address(123), "Immutable X", "IMX", 18); WETH wETH = new WETH(); - // Deploy contracts on reset chain. - vm.selectFork(resetId); - vm.startPrank(ADMIN); - ChildERC20 resetTokenTemplate = new ChildERC20(); - resetTokenTemplate.initialize(address(123), "Test", "TST", 18); - new MockAdaptor(); - vm.stopPrank(); - // Configure contracts on child chain. vm.selectFork(childId); childAdaptor.initialize(rootId, address(childBridge)); @@ -169,16 +161,6 @@ contract InvariantBridge is Test { ); vm.stopPrank(); - vm.selectFork(resetId); - vm.startPrank(ADMIN); - new ChildHelper(payable(childBridge)); - new RootHelper(ADMIN, payable(rootBridge)); - new ChildERC20BridgeHandler(childId, rootId, users, rootTokens, address(childHelper), address(rootHelper)); - new RootERC20BridgeFlowRateHandler( - childId, rootId, users, rootTokens, address(childHelper), address(rootHelper) - ); - vm.stopPrank(); - // Map tokens vm.selectFork(rootId); for (uint256 i = 0; i < NO_OF_TOKENS; i++) { @@ -343,7 +325,6 @@ contract InvariantBridge is Test { assertEq(balanceL1 + balanceL2, MAX_AMOUNT); } - vm.selectFork(resetId); } /// forge-config: default.invariant.runs = 256 From d731c783975bdbbf65a80922bfd60e8b5631dba1 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Wed, 6 Mar 2024 14:09:44 +1000 Subject: [PATCH 155/155] Cleanup --- .github/workflows/test.yml | 2 +- package.json | 2 +- scripts/localdev/chains.sh | 2 -- test/invariant/InvariantBridge.t.sol | 22 ++++++++++------------ 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dfefbb8f..c46f5c72 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -72,6 +72,6 @@ jobs: - name: Run Invariant Tests run: | - yarn local:threechains + yarn local:testchain forge test --match-path "test/invariant/**" -vvvvv id: invariant_test \ No newline at end of file diff --git a/package.json b/package.json index fb877879..10840708 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "local:ci": "cd scripts/localdev; rm -rf .child.bridge.contracts.json .root.bridge.contracts.json; ./ci.sh && ./deploy.sh && AXELAR_API_URL=skip npx mocha --require mocha-suppress-logs ../e2e/e2e.ts && ./stop.sh", "local:chainonly": "cd scripts/localdev; LOCAL_CHAIN_ONLY=true ./start.sh", "local:axelaronly": "cd scripts/localdev; npx ts-node axelar_setup.ts", - "local:threechains": "cd scripts/localdev; ./chains.sh", + "local:testchain": "cd scripts/localdev; ./chains.sh", "stop": "cd scripts/localdev; ./stop.sh" }, "author": "", diff --git a/scripts/localdev/chains.sh b/scripts/localdev/chains.sh index 31d3cb0b..d515fdd0 100755 --- a/scripts/localdev/chains.sh +++ b/scripts/localdev/chains.sh @@ -7,6 +7,4 @@ set -o pipefail # Start root & child chain. npx hardhat node --config ./rootchain.config.ts --port 8500 > /dev/null 2>&1 & -npx hardhat node --config ./childchain.config.ts --port 8501 > /dev/null 2>&1 & -npx hardhat node --config ./resetchain.config.ts --port 8502 > /dev/null 2>&1 & sleep 10 diff --git a/test/invariant/InvariantBridge.t.sol b/test/invariant/InvariantBridge.t.sol index dbcaa09a..12ec8695 100644 --- a/test/invariant/InvariantBridge.t.sol +++ b/test/invariant/InvariantBridge.t.sol @@ -16,15 +16,7 @@ import {RootERC20BridgeFlowRateHandler} from "./root/RootERC20BridgeFlowRateHand import "forge-std/console.sol"; contract InvariantBridge is Test { - string public constant CHILD_CHAIN_URL = "http://127.0.0.1:8500"; - string public constant ROOT_CHAIN_URL = "http://127.0.0.1:8501"; - // Forge has an issue that fails to reset state at the end of each run. - // For example, we found out that if the context stays at child chain at the end of setUp(), - // the state on child chain will not be reset or if the context stays at root chain, the state - // on the root chain will not be reset, which causes subsequent runs to fail. - // We introduced a third chain called reset chain and we make the context to stay on the reset chain - // in order to reset state on both child chain and root chain. - string public constant RESET_CHAIN_URL = "http://127.0.0.1:8502"; + string public constant CHAIN_URL = "http://127.0.0.1:8500"; uint256 public constant IMX_DEPOSIT_LIMIT = 10000 ether; uint256 public constant MAX_AMOUNT = 10000; address public constant ADMIN = address(0x111); @@ -49,9 +41,15 @@ contract InvariantBridge is Test { uint256 mappingGas; function setUp() public { - childId = vm.createFork(CHILD_CHAIN_URL); - rootId = vm.createFork(ROOT_CHAIN_URL); - resetId = vm.createFork(RESET_CHAIN_URL); + childId = vm.createFork(CHAIN_URL); + rootId = vm.createFork(CHAIN_URL); + // Forge has an issue that fails to reset state at the end of each run. + // For example, we found out that if the context stays at child chain at the end of setUp(), + // the state on child chain will not be reset or if the context stays at root chain, the state + // on the root chain will not be reset, which causes subsequent runs to fail. + // We introduced a third chain called reset chain and we make the context to stay on the reset chain + // in order to reset state on both child chain and root chain. + resetId = vm.createFork(CHAIN_URL); // Deploy contracts on child chain. vm.selectFork(childId);