diff --git a/.changeset/thin-ways-dress.md b/.changeset/thin-ways-dress.md new file mode 100644 index 00000000..33a25e4b --- /dev/null +++ b/.changeset/thin-ways-dress.md @@ -0,0 +1,6 @@ +--- +'@fuel-bridge/solidity-contracts': minor +'@fuel-bridge/test-utils': minor +--- + +ci for contract upgrade test suite diff --git a/.github/actions/setup-rust/action.yml b/.github/actions/setup-rust/action.yml index 7fdbe222..8e2bae1a 100644 --- a/.github/actions/setup-rust/action.yml +++ b/.github/actions/setup-rust/action.yml @@ -23,4 +23,4 @@ runs: uses: FuelLabs/action-fuel-toolchain@v0.6.0 with: name: fuel-bridge - components: ${{ inputs.forc-components }} + components: ${{ inputs.forc-components }} \ No newline at end of file diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 7d0bdfd1..fa5667e6 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -82,4 +82,4 @@ jobs: # TODO issue 115: https://github.com/FuelLabs/fuel-bridge/issues/115 # Run test again to ensure it works with node that already has transactions/messages # - name: Test projects Again - # run: pnpm --filter @fuel-bridge/integration-tests test + # run: pnpm --filter @fuel-bridge/integration-tests test \ No newline at end of file diff --git a/.github/workflows/upgrade-test-suite.yml b/.github/workflows/upgrade-test-suite.yml new file mode 100644 index 00000000..82ec5a43 --- /dev/null +++ b/.github/workflows/upgrade-test-suite.yml @@ -0,0 +1,43 @@ +name: Upgrade Test Suite + +on: + push: + branches: + - main + pull_request: + branches: + - main # Target branch for the PR + release: + types: [published] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + upgrade-test-suite: + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + steps: + - uses: actions/checkout@v3 + - uses: FuelLabs/github-actions/setups/node@master + with: + node-version: 20.16.0 + pnpm-version: 9.0.6 + - uses: FuelLabs/github-actions/setups/docker@master + with: + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - uses: ./.github/actions/setup-rust + - name: Build project + run: pnpm build + - name: Sets the tenderly rpc endpoint in the L1 docker container env + run: | + cat << EOF > l1_chain.env + TENDERLY_RPC_URL=${{ secrets.TENDERLY_RPC_URL }} + EOF + working-directory: ./packages/solidity-contracts/docker/envs + - name: Run integration tests on a L1 fork after upgrading contracts + run: | + pnpm upgrade:test:integration + working-directory: ./packages/solidity-contracts diff --git a/docker/l1-chain/Dockerfile b/docker/l1-chain/Dockerfile index 7970a8b3..a4828892 100644 --- a/docker/l1-chain/Dockerfile +++ b/docker/l1-chain/Dockerfile @@ -10,7 +10,7 @@ RUN apk --no-cache add git curl RUN npm i -g pnpm # clone the contracts repo -ADD ./packages/solidity-contracts/package.json /l1chain/fuel-v2-contracts/ +ADD docker/l1-chain/package.json /l1chain/fuel-v2-contracts/ # copy over the fuel chain and replace consts values WORKDIR /l1chain/fuel-v2-contracts diff --git a/docker/l1-chain/package.json b/docker/l1-chain/package.json new file mode 100644 index 00000000..24b1ad6f --- /dev/null +++ b/docker/l1-chain/package.json @@ -0,0 +1,85 @@ +{ + "name": "@fuel-bridge/solidity-contracts", + "version": "1.0.0", + "description": "The Fuel v2 Solidity smart contracts.", + "license": "APACHE-2.0", + "files": [ + "typechain", + "contracts", + "artifacts", + "dist" + ], + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "typings": "dist/index.d.ts", + "main": "dist/index.js", + "scripts": { + "build": "run-s clean compile build:exports", + "build:exports": "tsup", + "clean": "pnpm hardhat clean", + "compile": "pnpm hardhat compile --show-stack-traces", + "coverage": "pnpm run build && pnpm hardhat coverage --temp artifacts --network hardhat", + "check": "pnpm solhint \"contracts/**/*.sol\"", + "node": "pnpm hardhat node --network hardhat --hostname 0.0.0.0", + "start-mining": "pnpm hardhat run --no-compile --network localhost scripts/startAutoMining.ts", + "serve-deployments": "pnpm ts-node scripts/serveDeployments.ts", + "test": "pnpm hardhat test", + "integration-test": "pnpm mocha -b -r ts-node/register 'integration-tests/**/*.ts'", + "node:up": "sh ./scripts/node:up.sh", + "hardhat:test:integration": "DISABLE_GAS_REPORTER=true pnpm hardhat --network mainnetFork test fork-integration-tests/**/*.ts --bail", + "test-no-compile": "pnpm hardhat test --no-compile", + "test-parallel": "pnpx mocha 'test/**/*.ts' --recursive --parallel --require hardhat/register", + "prettier": "prettier --write --plugin=prettier-plugin-solidity 'contracts/**/*.sol'", + "lint": "prettier --list-different --plugin=prettier-plugin-solidity 'contracts/**/*.sol'" + }, + "devDependencies": { + "@fuel-ts/merkle": "^0.21.2", + "@inquirer/prompts": "^5.3.8", + "@nomicfoundation/hardhat-chai-matchers": "^2.0.4", + "@nomicfoundation/hardhat-ethers": "^3.0.5", + "@nomicfoundation/hardhat-network-helpers": "^1.0.10", + "@nomicfoundation/hardhat-verify": "1.1.1", + "@openzeppelin/contracts": "^4.8.3", + "@openzeppelin/contracts-upgradeable": "^4.8.3", + "@openzeppelin/hardhat-upgrades": "^3.0.4", + "@safe-global/api-kit": "^2.4.6", + "@safe-global/protocol-kit": "^4.1.1", + "@safe-global/safe-core-sdk-types": "^5.1.0", + "@typechain/ethers-v6": "^0.5.1", + "@typechain/hardhat": "^9.1.0", + "@types/chai": "^4.3.4", + "@types/cors": "2.8.17", + "mocha": "^10.0.0", + "@types/express": "^4.17.14", + "@types/lodash": "^4.14.202", + "@types/mocha": "^10.0.0", + "@types/node": "^18.11.9", + "@typescript-eslint/eslint-plugin": "^5.43.0", + "@typescript-eslint/parser": "^5.43.0", + "axios": "^1.7.7", + "chai": "^4.3.7", + "cors": "2.8.5", + "dotenv": "^16.0.3", + "ethers": "6.13.1", + "express": "^4.18.2", + "fuels": "0.96.1", + "hardhat": "^2.20.1", + "hardhat-deploy": "^0.12.4", + "inquirer": "^10.1.8", + "lodash": "^4.17.21", + "markdownlint": "^0.26.2", + "markdownlint-cli": "^0.32.2", + "node-fetch": "^2.6.6", + "npm-run-all": "^4.1.5", + "prettier-plugin-solidity": "^1.1.3", + "solc": "^0.8.17", + "solhint": "3.3.7", + "solidity-coverage": "^0.8.5", + "ts-generator": "^0.1.1", + "ts-node": "^10.9.1", + "tsup": "^7.2.0", + "typechain": "^8.3.2", + "typescript": "^4.9.3" + } + } + \ No newline at end of file diff --git a/packages/solidity-contracts/contracts/test/PlaceHolder.sol b/packages/solidity-contracts/contracts/test/PlaceHolder.sol new file mode 100644 index 00000000..e18ceb0f --- /dev/null +++ b/packages/solidity-contracts/contracts/test/PlaceHolder.sol @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; +/// @dev The only purpose of this contract is to be copied during the Dockerfile build +/// so that hardhat downloads the compiler and it gets cached +contract PlaceHolder {} \ No newline at end of file diff --git a/packages/solidity-contracts/deploy/hardhat/001.chain_state.ts b/packages/solidity-contracts/deploy/hardhat/001.chain_state.ts index 1d9f0e2d..30b4b275 100644 --- a/packages/solidity-contracts/deploy/hardhat/001.chain_state.ts +++ b/packages/solidity-contracts/deploy/hardhat/001.chain_state.ts @@ -13,6 +13,10 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { upgrades: { deployProxy, erc1967 }, deployments: { save }, } = hre; + + const isForking = hre.config.networks[hre.network.name]?.forking?.enabled; + if (isForking) return; + const [deployer] = await ethers.getSigners(); const contract = await deployProxy(new FuelChainState(deployer), [], { diff --git a/packages/solidity-contracts/deploy/hardhat/002.fuel_message_portal_v3.ts b/packages/solidity-contracts/deploy/hardhat/002.fuel_message_portal_v3.ts index 225aaf25..5468571c 100644 --- a/packages/solidity-contracts/deploy/hardhat/002.fuel_message_portal_v3.ts +++ b/packages/solidity-contracts/deploy/hardhat/002.fuel_message_portal_v3.ts @@ -14,6 +14,10 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { upgrades: { deployProxy, erc1967 }, deployments: { get, save }, } = hre; + + const isForking = hre.config.networks[hre.network.name]?.forking?.enabled; + if (isForking) return; + const [deployer] = await ethers.getSigners(); const { address: fuelChainState } = await get('FuelChainState'); diff --git a/packages/solidity-contracts/deploy/hardhat/003.erc20_gateway_v4.ts b/packages/solidity-contracts/deploy/hardhat/003.erc20_gateway_v4.ts index bef7f203..2f117733 100644 --- a/packages/solidity-contracts/deploy/hardhat/003.erc20_gateway_v4.ts +++ b/packages/solidity-contracts/deploy/hardhat/003.erc20_gateway_v4.ts @@ -9,6 +9,10 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { upgrades: { deployProxy, erc1967 }, deployments: { get, save }, } = hre; + + const isForking = hre.config.networks[hre.network.name]?.forking?.enabled; + if (isForking) return; + const [deployer] = await ethers.getSigners(); const fuelMessagePortal = await get('FuelMessagePortal'); diff --git a/packages/solidity-contracts/deploy/hardhat/005.register_block_committer.ts b/packages/solidity-contracts/deploy/hardhat/005.register_block_committer.ts index f0fa19ab..9acd6bbe 100644 --- a/packages/solidity-contracts/deploy/hardhat/005.register_block_committer.ts +++ b/packages/solidity-contracts/deploy/hardhat/005.register_block_committer.ts @@ -7,16 +7,21 @@ const COMMITTER_ADDRESS = '0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc'; const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const { ethers, deployments } = hre; + + const isForking = hre.config.networks[hre.network.name]?.forking?.enabled; + const [deployer] = await ethers.getSigners(); - const { address } = await deployments.get('FuelChainState'); + if (!isForking) { + const { address } = await deployments.get('FuelChainState'); - const fuelChainState = FuelChainState__factory.connect(address, deployer); - const COMMITTER_ROLE = await fuelChainState.COMMITTER_ROLE(); + const fuelChainState = FuelChainState__factory.connect(address, deployer); + const COMMITTER_ROLE = await fuelChainState.COMMITTER_ROLE(); - await fuelChainState - .grantRole(COMMITTER_ROLE, COMMITTER_ADDRESS) - .then((tx) => tx.wait()); + await fuelChainState + .grantRole(COMMITTER_ROLE, COMMITTER_ADDRESS) + .then((tx) => tx.wait()); + } console.log('Granted role COMMITTER_ROLE to', COMMITTER_ADDRESS); }; diff --git a/packages/solidity-contracts/deploy/hardhat/006._erc721_gateway_v3.ts b/packages/solidity-contracts/deploy/hardhat/006._erc721_gateway_v3.ts index 3f4734bf..69129a74 100644 --- a/packages/solidity-contracts/deploy/hardhat/006._erc721_gateway_v3.ts +++ b/packages/solidity-contracts/deploy/hardhat/006._erc721_gateway_v3.ts @@ -1,5 +1,7 @@ +import fs from 'fs'; import type { HardhatRuntimeEnvironment } from 'hardhat/types'; import type { DeployFunction } from 'hardhat-deploy/dist/types'; +import path from 'path'; import { FuelERC721GatewayV2__factory as FuelERC721GatewayV2 } from '../../typechain'; @@ -9,13 +11,33 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { upgrades: { deployProxy, erc1967 }, deployments: { get, save }, } = hre; + + const isForking = hre.config.networks[hre.network.name]?.forking?.enabled; + const [deployer] = await ethers.getSigners(); - const fuelMessagePortal = await get('FuelMessagePortal'); + let portlAddress; + if (isForking) { + const deploymentPath = path.join( + __dirname, + '..', + '..', + '/', + 'deployments', + 'mainnet', + 'FuelMessagePortal.json' + ); + + const deployment = JSON.parse(fs.readFileSync(deploymentPath, 'utf8')); + portlAddress = deployment.address; + } else { + const fuelMessagePortal = await get('FuelMessagePortal'); + portlAddress = fuelMessagePortal.address; + } const contract = await deployProxy( new FuelERC721GatewayV2(deployer), - [fuelMessagePortal.address], + [portlAddress], { initializer: 'initialize', } diff --git a/packages/solidity-contracts/deploy/hardhat/007.set_asset_issuer_id.ts b/packages/solidity-contracts/deploy/hardhat/007.set_asset_issuer_id.ts index 025d7345..f689131a 100644 --- a/packages/solidity-contracts/deploy/hardhat/007.set_asset_issuer_id.ts +++ b/packages/solidity-contracts/deploy/hardhat/007.set_asset_issuer_id.ts @@ -16,6 +16,10 @@ const ASSET_ISSUER_ID = const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const { ethers, deployments } = hre; + + const isForking = hre.config.networks[hre.network.name]?.forking?.enabled; + if (isForking) return; + const [deployer] = await ethers.getSigners(); await deployments.execute( diff --git a/packages/solidity-contracts/deploy/hardhat/009.chain_state_upgrade.ts b/packages/solidity-contracts/deploy/hardhat/009.chain_state_upgrade.ts new file mode 100644 index 00000000..48fdf219 --- /dev/null +++ b/packages/solidity-contracts/deploy/hardhat/009.chain_state_upgrade.ts @@ -0,0 +1,101 @@ +import fs from 'fs'; +import type { HardhatRuntimeEnvironment } from 'hardhat/types'; +import type { DeployFunction } from 'hardhat-deploy/dist/types'; +import path from 'path'; + +import { FuelChainState__factory } from '../../typechain'; + +const BLOCKS_PER_COMMIT_INTERVAL = 30; +const TIME_TO_FINALIZE = 5; +const COMMIT_COOLDOWN = TIME_TO_FINALIZE; + +const ADMIN = '0x32da601374b38154f05904B16F44A1911Aa6f314'; +let COMMITTER_ADDRESS = '0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc'; + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { + ethers, + upgrades: { erc1967 }, + } = hre; + + const [deployer] = await ethers.getSigners(); + + const isForking = hre.config.networks[hre.network.name]?.forking?.enabled; + let address; + + if (isForking) { + const deploymentPath = path.join( + __dirname, + '..', + '..', + '/', + 'deployments', + 'mainnet', + 'FuelChainState.json' + ); + + const deployment = JSON.parse(fs.readFileSync(deploymentPath, 'utf8')); + address = deployment.address; + + const chainState = FuelChainState__factory.connect(address, deployer); + + const factory = await hre.ethers.getContractFactory('FuelChainState'); + + const newImplementation = await factory.deploy( + TIME_TO_FINALIZE, + BLOCKS_PER_COMMIT_INTERVAL, + COMMIT_COOLDOWN + ); + + const newImplementationAddress = await newImplementation.getAddress(); + + let txData = chainState.interface.encodeFunctionData('upgradeTo', [ + newImplementationAddress, + ]); + + await deployer.sendTransaction({ + to: ADMIN, + value: ethers.parseEther('100'), + }); + + const impersonatedSigner = await ethers.getImpersonatedSigner(ADMIN); + await impersonatedSigner.sendTransaction({ + to: address, + data: txData, + }); + + const COMMITTER_ROLE = await chainState.COMMITTER_ROLE(); + + txData = await chainState.interface.encodeFunctionData('grantRole', [ + COMMITTER_ROLE, + COMMITTER_ADDRESS, + ]); + + await impersonatedSigner.sendTransaction({ + to: address, + data: txData, + }); + + COMMITTER_ADDRESS = '0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65'; + + txData = await chainState.interface.encodeFunctionData('grantRole', [ + COMMITTER_ROLE, + COMMITTER_ADDRESS, + ]); + + await impersonatedSigner.sendTransaction({ + to: address, + data: txData, + }); + + const implementation = await erc1967.getImplementationAddress(address); + + console.log('Upgraded FuelChainState to', implementation); + + return true; + } +}; + +func.tags = ['upgrade_chain_state']; +func.id = 'upgrade_chain_state'; +export default func; diff --git a/packages/solidity-contracts/deploy/hardhat/010.portal_upgrade.ts b/packages/solidity-contracts/deploy/hardhat/010.portal_upgrade.ts new file mode 100644 index 00000000..14682a6a --- /dev/null +++ b/packages/solidity-contracts/deploy/hardhat/010.portal_upgrade.ts @@ -0,0 +1,86 @@ +import { MaxUint256 } from 'ethers'; +import fs from 'fs'; +import type { HardhatRuntimeEnvironment } from 'hardhat/types'; +import type { DeployFunction } from 'hardhat-deploy/dist/types'; +import path from 'path'; + +import { FuelMessagePortalV3__factory as FuelMessagePortal } from '../../typechain'; + +const RATE_LIMIT_DURATION = 3600 * 24 * 7; + +const ADMIN = '0x32da601374b38154f05904B16F44A1911Aa6f314'; + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { + ethers, + upgrades: { erc1967 }, + } = hre; + + const [deployer] = await ethers.getSigners(); + + const isForking = hre.config.networks[hre.network.name]?.forking?.enabled; + let address; + + if (isForking) { + const deploymentPath = path.join( + __dirname, + '..', + '..', + '/', + 'deployments', + 'mainnet', + 'FuelMessagePortal.json' + ); + + const deployment = JSON.parse(fs.readFileSync(deploymentPath, 'utf8')); + address = deployment.address; + + const portal = FuelMessagePortal.connect(address, deployer); + + const SET_RATE_LIMITER_ROLE = await portal.SET_RATE_LIMITER_ROLE(); + + const factory = await hre.ethers.getContractFactory('FuelMessagePortalV3'); + + const newImplementation = await factory.deploy( + MaxUint256, + RATE_LIMIT_DURATION + ); + + const newImplementationAddress = await newImplementation.getAddress(); + + let txData = portal.interface.encodeFunctionData('upgradeTo', [ + newImplementationAddress, + ]); + + await deployer.sendTransaction({ + to: ADMIN, + value: ethers.parseEther('100'), + }); + + const impersonatedSigner = await ethers.getImpersonatedSigner(ADMIN); + await impersonatedSigner.sendTransaction({ + to: address, + data: txData, + }); + + txData = await portal.interface.encodeFunctionData('grantRole', [ + SET_RATE_LIMITER_ROLE, + await deployer.getAddress(), + ]); + + await impersonatedSigner.sendTransaction({ + to: address, + data: txData, + }); + + const implementation = await erc1967.getImplementationAddress(address); + + console.log('Upgraded FuelMessagePortal to', implementation); + + return true; + } +}; + +func.tags = ['upgrade_portal']; +func.id = 'upgrade_portal'; +export default func; diff --git a/packages/solidity-contracts/deploy/hardhat/011.gateway_upgrade.ts b/packages/solidity-contracts/deploy/hardhat/011.gateway_upgrade.ts new file mode 100644 index 00000000..9b6e0d82 --- /dev/null +++ b/packages/solidity-contracts/deploy/hardhat/011.gateway_upgrade.ts @@ -0,0 +1,97 @@ +import fs from 'fs'; +import type { HardhatRuntimeEnvironment } from 'hardhat/types'; +import type { DeployFunction } from 'hardhat-deploy/dist/types'; +import path from 'path'; + +import { FuelERC20GatewayV4__factory } from '../../typechain'; + +const ADMIN = '0x32da601374b38154f05904B16F44A1911Aa6f314'; + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { + ethers, + upgrades: { erc1967 }, + } = hre; + + const [deployer] = await ethers.getSigners(); + + const isForking = hre.config.networks[hre.network.name]?.forking?.enabled; + let address; + + if (isForking) { + const deploymentPath = path.join( + __dirname, + '..', + '..', + '/', + 'deployments', + 'mainnet', + 'FuelERC20GatewayV4.json' + ); + + const deployment = JSON.parse(fs.readFileSync(deploymentPath, 'utf8')); + address = deployment.address; + + const portal = FuelERC20GatewayV4__factory.connect(address, deployer); + + const ADMIN_ROLE = await portal.DEFAULT_ADMIN_ROLE(); + const SET_RATE_LIMITER_ROLE = await portal.SET_RATE_LIMITER_ROLE(); + + const factory = await hre.ethers.getContractFactory('FuelERC20GatewayV4'); + + const newImplementation = await factory.deploy(); + + const newImplementationAddress = await newImplementation.getAddress(); + + let txData = portal.interface.encodeFunctionData('upgradeTo', [ + newImplementationAddress, + ]); + + await deployer.sendTransaction({ + to: ADMIN, + value: ethers.parseEther('100'), + }); + + const impersonatedSigner = await ethers.getImpersonatedSigner(ADMIN); + await impersonatedSigner.sendTransaction({ + to: address, + data: txData, + }); + + txData = await portal.interface.encodeFunctionData('grantRole', [ + ADMIN_ROLE, + await deployer.getAddress(), + ]); + await impersonatedSigner.sendTransaction({ + to: address, + data: txData, + }); + + txData = await portal.interface.encodeFunctionData('grantRole', [ + SET_RATE_LIMITER_ROLE, + await deployer.getAddress(), + ]); + await impersonatedSigner.sendTransaction({ + to: address, + data: txData, + }); + + txData = await portal.interface.encodeFunctionData('requireWhitelist', [ + false, + ]); + await impersonatedSigner.sendTransaction({ + to: address, + data: txData, + }); + + const implementation = await erc1967.getImplementationAddress(address); + + console.log('Upgraded FuelGateway to', implementation); + + return true; + } +}; + +func.tags = ['upgrade_gateway']; +func.id = 'upgrade_gateway'; +export default func; diff --git a/packages/solidity-contracts/deploy/hardhat/999.serve_deployment_file.ts b/packages/solidity-contracts/deploy/hardhat/999.serve_deployment_file.ts index 9900c7e0..4a2fbc64 100644 --- a/packages/solidity-contracts/deploy/hardhat/999.serve_deployment_file.ts +++ b/packages/solidity-contracts/deploy/hardhat/999.serve_deployment_file.ts @@ -1,18 +1,78 @@ -import { promises as fs } from 'fs'; +import fs, { writeFile } from 'fs'; import type { HardhatRuntimeEnvironment } from 'hardhat/types'; import type { DeployFunction } from 'hardhat-deploy/dist/types'; +import path from 'path'; +import { promisify } from 'util'; import { FuelChainState__factory } from '../../typechain'; const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const { deployments, ethers } = hre; - const allDeployments = await deployments.all(); + + let address: any; + const isForking = hre.config.networks[hre.network.name]?.forking?.enabled; const deploymentsFile: { [name: string]: string } = {}; - for (const key of Object.keys(allDeployments)) { - deploymentsFile[key] = allDeployments[key].address; + + if (isForking) { + let deploymentDir = path.join( + __dirname, + '..', + '..', + '/', + 'deployments', + 'mainnet' + ); + + fs.readdirSync(deploymentDir) + .filter((file) => path.extname(file) === '.json') + .forEach((file) => { + const filePath = path.join(deploymentDir, file); + try { + const deployment = JSON.parse(fs.readFileSync(filePath, 'utf8')); + // Use filename (without .json) as the key + const contractName = path.basename(file, '.json'); + deploymentsFile[contractName] = deployment.address; + } catch (error) { + console.error(`Error reading deployment file ${file}:`, error); + } + }); + + deploymentDir = path.join( + __dirname, + '..', + '..', + '/', + 'deployments', + 'localhost', + 'FuelERC721Gateway.json' + ); + + const deployment = JSON.parse(fs.readFileSync(deploymentDir, 'utf8')); + deploymentsFile['FuelERC721Gateway'] = deployment.address; + + deploymentDir = path.join( + __dirname, + '..', + '..', + '/', + 'deployments', + 'mainnet', + 'FuelChainState.json' + ); + + const chainStateDeployment = JSON.parse( + fs.readFileSync(deploymentDir, 'utf8') + ); + address = chainStateDeployment.address; + } else { + const allDeployments = await deployments.all(); + for (const key of Object.keys(allDeployments)) { + deploymentsFile[key] = allDeployments[key].address; + } + + ({ address } = await deployments.get('FuelChainState')); } - const { address } = await deployments.get('FuelChainState'); const state = FuelChainState__factory.connect(address, ethers.provider); deploymentsFile['BLOCKS_PER_COMMIT_INTERVAL'] = ( @@ -25,10 +85,12 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { await state.TIME_TO_FINALIZE() ).toString(); - await fs.writeFile( + const writeFileAsync = promisify(writeFile); + + await writeFileAsync( 'deployments/deployments.local.json', - JSON.stringify(deploymentsFile, null, ' '), - 'utf-8' + JSON.stringify(deploymentsFile, null, 2), + 'utf8' ); }; diff --git a/packages/solidity-contracts/docker/block-committer/Dockerfile b/packages/solidity-contracts/docker/block-committer/Dockerfile new file mode 100644 index 00000000..b9399970 --- /dev/null +++ b/packages/solidity-contracts/docker/block-committer/Dockerfile @@ -0,0 +1,21 @@ +FROM ghcr.io/fuellabs/fuel-block-committer:v0.10.4 + +ARG COMMITER_IP=0.0.0.0 +ARG COMMITER_PORT=8888 + +# dependencies +ENV DEBIAN_FRONTEND=noninteractive +RUN apt update && apt install -y curl jq && rm -rf /var/lib/apt/lists/* + +# copy chain config +WORKDIR /block-committer + +# expose fuel node port +ENV HOST="${COMMITER_IP}" +ENV PORT="${COMMITER_PORT}" + +EXPOSE ${PORT} + +# copy over script and run +COPY ./block-committer.sh . +ENTRYPOINT ["sh", "./block-committer.sh"] diff --git a/packages/solidity-contracts/docker/block-committer/block-committer.sh b/packages/solidity-contracts/docker/block-committer/block-committer.sh new file mode 100644 index 00000000..f2be6988 --- /dev/null +++ b/packages/solidity-contracts/docker/block-committer/block-committer.sh @@ -0,0 +1,54 @@ +#!/bin/sh +set -euo + +RETRIES=${RETRIES:-60} +DELAY=${DELAY:-10} +JSON='{"jsonrpc":"2.0","id":0,"method":"net_version","params":[]}' +HEALTH_URL=${HEALTH_URL:-"http://fuel_core:4001/v1/health"} + +if [ -z "$COMMITTER__ETH__RPC" ]; then + echo "Must specify \$ETHEREUM_RPC." + exit 1 +fi +if [ -z "$COMMITTER__FUEL__GRAPHQL_ENDPOINT" ]; then + echo "Must specify \$FUEL_GRAPHQL_ENDPOINT." + exit 1 +fi + +echo $COMMITTER__FUEL__GRAPHQL_ENDPOINT/health + +# wait for the base layer to be up +echo "Waiting for Fuel Core chain." +curl \ + --fail \ + --show-error \ + --silent \ + --retry-connrefused \ + --retry $RETRIES \ + --retry-delay $DELAY \ + $HEALTH_URL > /dev/null +echo "Connected to Fuel Core chain." + +# get the deployments file from the deployer +echo "Waiting for l1 chain deployment data." +curl \ + --fail \ + --show-error \ + --silent \ + --retry-connrefused \ + --retry-all-errors \ + --retry $RETRIES \ + --retry-delay 5 \ + $DEPLOYMENTS_HTTP \ + -o addresses.json +echo "Got l1 chain deployment data." + +# pull data from deployer dump +export COMMITTER__ETH__STATE_CONTRACT_ADDRESS=$(cat "./addresses.json" | jq -r .FuelChainState) +echo "COMMITTER__ETH__STATE_CONTRACT_ADDRESS: $COMMITTER__ETH__STATE_CONTRACT_ADDRESS" +echo "ETHEREUM_RPC: $COMMITTER__ETH__RPC" +echo "FUEL_GRAPHQL_ENDPOINT: $COMMITTER__FUEL__GRAPHQL_ENDPOINT" + +# start the Block Commiter +echo "Starting block commiter" +exec /root/fuel-block-committer diff --git a/packages/solidity-contracts/docker/docker-compose.yml b/packages/solidity-contracts/docker/docker-compose.yml new file mode 100644 index 00000000..b23c6721 --- /dev/null +++ b/packages/solidity-contracts/docker/docker-compose.yml @@ -0,0 +1,99 @@ +version: '3.4' + +services: + db: + image: postgres:14 + environment: + POSTGRES_USER: username + POSTGRES_PASSWORD: password + POSTGRES_DB: committer_db + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U username -d committer_db'] + interval: 5s + timeout: 5s + retries: 5 + + l1_chain: + image: fueldev/l1chain:${DOCKER_TAG_L1_CHAIN:-latest} + build: + dockerfile: ./docker/l1-chain/Dockerfile + # Use build context of the root directory + # to allow copying solidity-contracts on Dockerfile + context: ../ + env_file: + - ./envs/l1_chain.env + ports: + # expose the service to the host for integration testing + - ${L1_CHAIN_HTTP_PORT:-8545}:9545 + - ${DEPLOYMENTS_PORT:-8080}:8081 + stop_grace_period: 1s + + fuel_core: + image: fueldev/fuelcore:${DOCKER_TAG_FUEL_CORE:-latest} + depends_on: + l1_chain: + condition: service_started + platform: linux/amd64 + build: + context: ./fuel-core/ + env_file: + - ./envs/fuel_core.env + environment: + L1_CHAIN_HTTP: http://l1_chain:9545 + DEPLOYMENTS_HTTP: http://l1_chain:8081/deployments.local.json + RUST_LOG: debug + DEBUG: true + DB_PATH: /db + ports: + # expose the service to the host for integration testing + - ${FUEL_CORE_HTTP_PORT:-4000}:4001 + stop_grace_period: 1s + restart: always + + fuel_block_commiter: + image: ghcr.io/fuellabs/fuel-block-committer:v0.10.4 + platform: linux/amd64 + build: + context: ./block-committer/ + environment: + COMMITTER__ETH__RPC: 'ws://l1_chain:9545/' + COMMITTER__ETH__L1_KEYS__MAIN: 'Private(8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba)' + COMMITTER__ETH__L1_KEYS__BLOB: 'Private(59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d)' + COMMITTER__FUEL__GRAPHQL_ENDPOINT: 'http://fuel_core:4001/graphql' + COMMITTER__FUEL__NUM_BUFFERED_REQUESTS: '5' + COMMITTER__APP__DB__PORT: '5432' + COMMITTER__APP__DB__HOST: 'db' + COMMITTER__APP__DB__USERNAME: 'username' + COMMITTER__APP__DB__PASSWORD: 'password' + COMMITTER__APP__DB__MAX_CONNECTIONS: '10' + COMMITTER__APP__DB__USE_SSL: 'false' + COMMITTER__APP__DB__DATABASE: 'committer_db' + COMMITTER__APP__PORT: '8080' + COMMITTER__APP__HOST: '0.0.0.0' + COMMITTER__APP__BLOCK_CHECK_INTERVAL: '5s' + COMMITTER__APP__TX_FINALIZATION_CHECK_INTERVAL: '5s' + COMMITTER__APP__NUM_BLOCKS_TO_FINALIZE_TX: '3' + COMMITTER__APP__GAS_BUMP_TIMEOUT: '300s' + COMMITTER__APP__TX_MAX_FEE: '4000000000000000' + COMMITTER__APP__SEND_TX_REQUEST_TIMEOUT: '10s' + COMMITTER__APP__BUNDLE__ACCUMULATION_TIMEOUT: '3600s' + COMMITTER__APP__BUNDLE__BLOCKS_TO_ACCUMULATE: '400' + COMMITTER__APP__BUNDLE__OPTIMIZATION_TIMEOUT: '60s' + COMMITTER__APP__BUNDLE__BLOCK_HEIGHT_LOOKBACK: '8500' + COMMITTER__APP__BUNDLE__COMPRESSION_LEVEL: 'level6' + COMMITTER__APP__BUNDLE__OPTIMIZATION_STEP: '100' + COMMITTER__APP__BUNDLE__FRAGMENTS_TO_ACCUMULATE: '3' + COMMITTER__APP__BUNDLE__FRAGMENT_ACCUMULATION_TIMEOUT: '10m' + COMMITTER__APP__BUNDLE__NEW_BUNDLE_CHECK_INTERVAL: '3s' + DEPLOYMENTS_HTTP: http://l1_chain:8081/deployments.local.json + ports: + # expose the service to the host for integration testing + - ${COMMITTER_HTTP_PORT:-8888}:8888 + depends_on: + db: + condition: service_healthy + l1_chain: + condition: service_started + fuel_core: + condition: service_started + restart: always diff --git a/packages/solidity-contracts/docker/envs/fuel_core.env b/packages/solidity-contracts/docker/envs/fuel_core.env new file mode 100644 index 00000000..7b0bf02d --- /dev/null +++ b/packages/solidity-contracts/docker/envs/fuel_core.env @@ -0,0 +1 @@ +CONSENSUS_KEY_SECRET="0xa449b1ffee0e2205fa924c6740cc48b3b473aa28587df6dab12abc245d1f5298" \ No newline at end of file diff --git a/packages/solidity-contracts/docker/envs/l1_chain.env b/packages/solidity-contracts/docker/envs/l1_chain.env new file mode 100644 index 00000000..efddf6ab --- /dev/null +++ b/packages/solidity-contracts/docker/envs/l1_chain.env @@ -0,0 +1 @@ +TENDERLY_RPC_URL= \ No newline at end of file diff --git a/packages/solidity-contracts/docker/fuel-core/Dockerfile b/packages/solidity-contracts/docker/fuel-core/Dockerfile new file mode 100644 index 00000000..3d8b69db --- /dev/null +++ b/packages/solidity-contracts/docker/fuel-core/Dockerfile @@ -0,0 +1,55 @@ +# IMPORTANT! +# Make sure to check: +# https://github.com/FuelLabs/chain-configuration/tree/master/upgradelog/ignition-testnet +# and apply the latest state_transition_function and consensus_parameter +# when upgrading fuel-core +FROM ghcr.io/fuellabs/fuel-core:v0.40.0 + +ARG FUEL_IP=0.0.0.0 +ARG FUEL_PORT=4001 +ARG CONSENSUS_KEY_SECRET="" + +# dependencies +ENV DEBIAN_FRONTEND=noninteractive +RUN apt update && apt install -y git curl jq && rm -rf /var/lib/apt/lists/* + +# copy chain config +WORKDIR /fuel + +COPY ./genesis_coins.json . + +RUN git clone \ + https://github.com/FuelLabs/chain-configuration.git \ + /chain-configuration + +# Anchor the chain configuration to a specific commit to avoid CI breaking +RUN cd /chain-configuration && git checkout 8e4f7b52d7112f929a7cd95b988dfebfd10e87ec + +# Copy the base local configuration +RUN cp -R /chain-configuration/local/* ./ + +# Copy the devnet consensus parameters and state transition bytecode +RUN cp /chain-configuration/upgradelog/ignition-devnet/consensus_parameters/14.json \ + ./latest_consensus_parameters.json +RUN cp /chain-configuration/upgradelog/ignition-devnet/state_transition_function/16.wasm \ + ./state_transition_bytecode.wasm + +# update local state_config with custom genesis coins config +RUN jq '.coins = input' \ + state_config.json genesis_coins.json > tmp.json \ + && mv tmp.json state_config.json + +# update local state_config with testnet consensus parameters +RUN jq '.consensus_parameters = input' \ + state_config.json latest_consensus_parameters.json > tmp.json \ + && mv tmp.json state_config.json + +# expose fuel node port +ENV FUEL_IP="${FUEL_IP}" +ENV FUEL_PORT="${FUEL_PORT}" +ENV CONSENSUS_KEY_SECRET="${CONSENSUS_KEY_SECRET}" +EXPOSE ${FUEL_PORT} + +# copy over script and run +COPY ./fuel_core.sh . +CMD ["sh", "./fuel_core.sh"] \ No newline at end of file diff --git a/packages/solidity-contracts/docker/fuel-core/fuel_core.sh b/packages/solidity-contracts/docker/fuel-core/fuel_core.sh new file mode 100644 index 00000000..a1c2c3a3 --- /dev/null +++ b/packages/solidity-contracts/docker/fuel-core/fuel_core.sh @@ -0,0 +1,71 @@ +#!/bin/sh +set -euo + +#!/bin/sh +set -euo + +RETRIES=${RETRIES:-90} +DA_COMPRESSION=${DA_COMPRESSION:-"3600sec"} +GRAPHQL_COMPLEXITY=${GRAPHQL_COMPLEXITY:-500000} +JSON='{"jsonrpc":"2.0","id":0,"method":"net_version","params":[]}' +# FUEL_DB_PATH=./mnt/db/ + +if [ -z "$L1_CHAIN_HTTP" ]; then + echo "Must specify \$L1_CHAIN_HTTP." + exit 1 +fi + +# wait for the base layer to be up +echo "Waiting for l1 chain." +curl \ + --fail \ + --show-error \ + --silent \ + -H "Content-Type: application/json" \ + --retry-connrefused \ + --retry $RETRIES \ + --retry-delay 1 \ + -d $JSON \ + $L1_CHAIN_HTTP > /dev/null +echo "Connected to l1 chain." + +# get the deployments file from the deployer +echo "Waiting for l1 chain deployment data." +curl \ + --fail \ + --show-error \ + --silent \ + --retry-connrefused \ + --retry-all-errors \ + --retry $RETRIES \ + --retry-delay 5 \ + $DEPLOYMENTS_HTTP \ + -o addresses.json +echo "Got l1 chain deployment data." + +# pull data from deployer dump +export FUEL_MESSAGE_PORTAL_CONTRACT_ADDRESS=$(cat "./addresses.json" | jq -r .FuelMessagePortal) +# export FUEL_MESSAGE_PORTAL_CONTRACT_ADDRESS=$(jq -r '.address' /l1chain/fuel-v2-contracts/deployments/localhost/FuelMessagePortal.json) +echo "FUEL_MESSAGE_PORTAL_CONTRACT_ADDRESS: $FUEL_MESSAGE_PORTAL_CONTRACT_ADDRESS" +echo "L1_CHAIN_HTTP: $L1_CHAIN_HTTP" + +# start the Fuel client +#--db-path ${FUEL_DB_PATH} +echo "Starting fuel node." +exec /root/fuel-core run \ + --ip $FUEL_IP \ + --port $FUEL_PORT \ + --utxo-validation \ + --vm-backtrace \ + --enable-relayer \ + --relayer $L1_CHAIN_HTTP \ + --relayer-v2-listening-contracts $FUEL_MESSAGE_PORTAL_CONTRACT_ADDRESS \ + --poa-interval-period=1s \ + --relayer-da-deploy-height=21371952 \ + --debug \ + --da-compression $DA_COMPRESSION \ + --graphql-max-complexity $GRAPHQL_COMPLEXITY \ + --min-gas-price 0 \ + --tx-ttl-check-interval 10sec \ + --tx-pool-ttl 30sec \ + --snapshot ./ diff --git a/packages/solidity-contracts/docker/fuel-core/genesis_coins.json b/packages/solidity-contracts/docker/fuel-core/genesis_coins.json new file mode 100644 index 00000000..fd78763e --- /dev/null +++ b/packages/solidity-contracts/docker/fuel-core/genesis_coins.json @@ -0,0 +1,299 @@ +[ + { + "owner": "0xc8e615a4089466174459ef19cfd257d2e17adfabff3b8f219dbb5fb4eca87c50", + "amount": 1000000000000, + "asset_id": "0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07", + "tx_id": "0x0000000000000000000000000000000000000000000000000000000000000001", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0 + }, + { + "owner": "0x92dffc873b56f219329ed03bb69bebe8c3d8b041088574882f7a6404f02e2f28", + "amount": 1000000000000, + "asset_id": "0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07", + "tx_id": "0x0000000000000000000000000000000000000000000000000000000000000002", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0 + }, + { + "owner": "0x456bdaffaf74fe03521754ac445d148033c0c6acf7d593132c43f92fdbc7324c", + "amount": 1000000000000, + "asset_id": "0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07", + "tx_id": "0x0000000000000000000000000000000000000000000000000000000000000003", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0 + }, + { + "owner": "0x639880ece7753a32e09164d14dad7436c57737e567f18b98f6ee30fec6b247ec", + "amount": 1000000000000, + "asset_id": "0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07", + "tx_id": "0x0000000000000000000000000000000000000000000000000000000000000004", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0 + }, + { + "owner": "0xfd8c520ef8caff0ad3289aa64acecd4ef86ac8f643fd9b76bf2d163a86a66716", + "amount": 1000000000000, + "asset_id": "0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07", + "tx_id": "0x0000000000000000000000000000000000000000000000000000000000000005", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0 + }, + { + "owner": "0x8247104854dd733cb475901d55047f57cb3c8cafe3a9f7233de3325b8bf56a5c", + "amount": 1000000000000, + "asset_id": "0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07", + "tx_id": "0x0000000000000000000000000000000000000000000000000000000000000006", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0 + }, + { + "owner": "0x53de37ae51fcfecb17ee3589f68904ac75bf5ec109edeb1065ccb63145287da6", + "amount": 1000000000000, + "asset_id": "0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07", + "tx_id": "0x0000000000000000000000000000000000000000000000000000000000000007", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0 + }, + { + "owner": "0x17f4bef51f63f0c28af20d4223a3bf5cf1735a3b7ec52b4fcfbdbb5f30582a6b", + "amount": 1000000000000, + "asset_id": "0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07", + "tx_id": "0x0000000000000000000000000000000000000000000000000000000000000008", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0 + }, + { + "owner": "0x95725e9083d8ed1cb52dcf6429d0cfce00cc375eeac5b620b5c36f5b1e734b31", + "amount": 1000000000000, + "asset_id": "0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07", + "tx_id": "0x0000000000000000000000000000000000000000000000000000000000000009", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0 + }, + { + "owner": "0x08792a75d5714165aa117fd75450f9efcfb7124d034ef271f2919e4cc287046c", + "amount": 1000000000000, + "asset_id": "0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07", + "tx_id": "0x000000000000000000000000000000000000000000000000000000000000000a", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0 + }, + { + "owner": "0x6a16fba49dbdf7689c52b7a22951a54dc164076d27bdc6042b5d8377d68ca10b", + "amount": 1000000000000, + "asset_id": "0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07", + "tx_id": "0x000000000000000000000000000000000000000000000000000000000000000b", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0 + }, + { + "owner": "0x6494a55c0e3da212fdd0515507d00ae99151c7966e1448079c76bc447b577254", + "amount": 1000000000000, + "asset_id": "0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07", + "tx_id": "0x000000000000000000000000000000000000000000000000000000000000000c", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0 + }, + { + "owner": "0xe0e1c94a9f9e02454772813ba6a6261b5228db1fabde3a68b23c0e9744ce22fc", + "amount": 1000000000000, + "asset_id": "0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07", + "tx_id": "0x000000000000000000000000000000000000000000000000000000000000000d", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0 + }, + { + "owner": "0x425f3e91aedff36e72ae60a8a1d328e625d66d39fcc98d5fcd1ba7df65a9f878", + "amount": 1000000000000, + "asset_id": "0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07", + "tx_id": "0x000000000000000000000000000000000000000000000000000000000000000e", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0 + }, + { + "owner": "0xe2242f2e4971c34bc6fe5e1c0043b1aba717cb6a51f31f0dc0708cca73df905a", + "amount": 1000000000000, + "asset_id": "0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07", + "tx_id": "0x000000000000000000000000000000000000000000000000000000000000000f", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0 + }, + { + "owner": "0x6aa67cb316f329111dc708bb766360f5026a614edb11882e14d4cc04f26e0a08", + "amount": 1000000000000, + "asset_id": "0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07", + "tx_id": "0x0000000000000000000000000000000000000000000000000000000000000010", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0 + }, + { + "owner": "0xef5c2b712c4f3a10a37d6371cab2b03a6afd12e4ffcc9567d45d8c4b6e217e5c", + "amount": 1000000000000, + "asset_id": "0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07", + "tx_id": "0x0000000000000000000000000000000000000000000000000000000000000011", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0 + }, + { + "owner": "0xa82f66642de54993b32036eef7914f2dbaa217aa3209707b64d0b90187456a1f", + "amount": 1000000000000, + "asset_id": "0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07", + "tx_id": "0x0000000000000000000000000000000000000000000000000000000000000012", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0 + }, + { + "owner": "0x9c9c1f3346b54fe6cb379fa27f338464592515fd865656089c4a23ac34390e6f", + "amount": 1000000000000, + "asset_id": "0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07", + "tx_id": "0x0000000000000000000000000000000000000000000000000000000000000013", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0 + }, + { + "owner": "0x8a332bc33f4c10ad36392a9ca958a5ddf56081cc764f61613ea119e42ced6ac5", + "amount": 1000000000000, + "asset_id": "0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07", + "tx_id": "0x0000000000000000000000000000000000000000000000000000000000000014", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0 + }, + { + "tx_id": "0000000000000000000000000000000000000000000000000000000000000015", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0, + "owner": "6b63804cfbf9856e68e5b6e7aef238dc8311ec55bec04df774003a2c96e0418e", + "amount": 1152921504606846976, + "asset_id": "f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07" + }, + { + "tx_id": "0000000000000000000000000000000000000000000000000000000000000016", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0, + "owner": "54944e5b8189827e470e5a8bacfc6c3667397dc4e1eef7ef3519d16d6d6c6610", + "amount": 1152921504606846976, + "asset_id": "f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07" + }, + { + "tx_id": "0000000000000000000000000000000000000000000000000000000000000017", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0, + "owner": "e10f526b192593793b7a1559a391445faba82a1d669e3eb2dcd17f9c121b24b1", + "amount": 1152921504606846976, + "asset_id": "f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07" + }, + { + "tx_id": "0000000000000000000000000000000000000000000000000000000000000018", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0, + "owner": "577e424ee53a16e6a85291feabc8443862495f74ac39a706d2dd0b9fc16955eb", + "amount": 1152921504606846976, + "asset_id": "f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07" + }, + { + "tx_id": "0000000000000000000000000000000000000000000000000000000000000019", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0, + "owner": "c36be0e14d3eaf5d8d233e0f4a40b3b4e48427d25f84c460d2b03b242a38479e", + "amount": 1152921504606846976, + "asset_id": "f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07" + }, + { + "tx_id": "000000000000000000000000000000000000000000000000000000000000002a", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0, + "owner": "a1184d77d0d08a064e03b2bd9f50863e88faddea4693a05ca1ee9b1732ea99b7", + "amount": 1152921504606846976, + "asset_id": "f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07" + }, + { + "tx_id": "000000000000000000000000000000000000000000000000000000000000002b", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0, + "owner": "b5566df884bee4e458151c2fe4082c8af38095cc442c61e0dc83a371d70d88fd", + "amount": 1152921504606846976, + "asset_id": "f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07" + }, + { + "tx_id": "000000000000000000000000000000000000000000000000000000000000002c", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0, + "owner": "9da7247e1d63d30d69f136f0f8654ee8340362c785b50f0a60513c7edbf5bb7c", + "amount": 1152921504606846976, + "asset_id": "f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07" + }, + { + "tx_id": "000000000000000000000000000000000000000000000000000000000000002d", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0, + "owner": "4b2ca966aad1a9d66994731db5138933cf61679107c3cde2a10d9594e47c084e", + "amount": 1152921504606846976, + "asset_id": "f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07" + }, + { + "tx_id": "000000000000000000000000000000000000000000000000000000000000002e", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0, + "owner": "26183fbe7375045250865947695dfc12500dcc43efb9102b4e8c4d3c20009dcb", + "amount": 1152921504606846976, + "asset_id": "f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07" + }, + { + "tx_id": "000000000000000000000000000000000000000000000000000000000000002f", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0, + "owner": "81f3a10b61828580d06cc4c7b0ed8f59b9fb618be856c55d33decd95489a1e23", + "amount": 1152921504606846976, + "asset_id": "f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07" + }, + { + "tx_id": "0000000000000000000000000000000000000000000000000000000000000030", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0, + "owner": "587aa0482482efea0234752d1ad9a9c438d1f34d2859b8bef2d56a432cb68e33", + "amount": 1152921504606846976, + "asset_id": "f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07" + }, + { + "tx_id": "0000000000000000000000000000000000000000000000000000000000000031", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0, + "owner": "53a9c6a74bee79c5e04115a007984f4bddaafed75f512f68766c6ed59d0aedec", + "amount": 1125899906842624, + "asset_id": "f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07" + } +] diff --git a/packages/solidity-contracts/docker/l1-chain/Dockerfile b/packages/solidity-contracts/docker/l1-chain/Dockerfile new file mode 100644 index 00000000..5e2f20f5 --- /dev/null +++ b/packages/solidity-contracts/docker/l1-chain/Dockerfile @@ -0,0 +1,43 @@ +FROM node:20-alpine AS BUILD_IMAGE + +# dependencies +RUN apk --no-cache add git curl +RUN npm i -g pnpm + +ARG L1_IP=0.0.0.0 +ARG L1_PORT=9545 +ARG SERVE_PORT=8081 + +WORKDIR /l1chain/fuel-v2-contracts + +# clone the contracts repo +COPY docker/l1-chain/package.json /l1chain/fuel-v2-contracts/ + +# build the ethereum contracts and environment +RUN pnpm install +COPY contracts/test/PlaceHolder.sol /l1chain/fuel-v2-contracts/contracts/test/PlaceHolder.sol +COPY hardhat.config.ts /l1chain/fuel-v2-contracts/ +COPY scripts/ /l1chain/fuel-v2-contracts/scripts/ +RUN pnpm compile + +# replace the fuel chain consts values and change contract code +COPY contracts/ /l1chain/fuel-v2-contracts/contracts/ +COPY deploy/ /l1chain/fuel-v2-contracts/deploy/ +COPY deployments/ /l1chain/fuel-v2-contracts/deployments/ +COPY protocol/ /l1chain/fuel-v2-contracts/protocol/ + + +# remove build dependencies +# RUN pnpm prune --prod +RUN pnpm compile + + +ENV L1_IP="${L1_IP}" +ENV L1_PORT="${L1_PORT}" +ENV SERVE_PORT="${SERVE_PORT}" +EXPOSE ${L1_PORT} +EXPOSE ${SERVE_PORT} + +# copy over script and run +COPY ./docker/l1-chain/l1_chain.sh /l1chain/l1_chain.sh +CMD ["sh", "/l1chain/l1_chain.sh"] diff --git a/packages/solidity-contracts/docker/l1-chain/l1_chain.sh b/packages/solidity-contracts/docker/l1-chain/l1_chain.sh new file mode 100644 index 00000000..e0df10bc --- /dev/null +++ b/packages/solidity-contracts/docker/l1-chain/l1_chain.sh @@ -0,0 +1,39 @@ +#!/bin/sh +set -euo + +RETRIES=${RETRIES:-120} +JSON='{"jsonrpc":"2.0","id":0,"method":"net_version","params":[]}' + +L1_CHAIN_HTTP="http://127.0.0.1:$L1_PORT" + +echo "Starting l1 chain." +pnpm hardhat node --network hardhat --port $L1_PORT --hostname $L1_IP & + +# wait for the base layer to be up +echo "Waiting for l1 chain." +curl \ + --fail \ + --show-error \ + --silent \ + -H "Content-Type: application/json" \ + --retry-connrefused \ + --retry $RETRIES \ + --retry-delay 1 \ + -d $JSON \ + $L1_CHAIN_HTTP > /dev/null + +echo "Connected to l1 chain." + +export LOCALHOST_HTTP=$L1_CHAIN_HTTP + +# Start auto mining +# We use a separate process to start auto mining because +# the deployment of contracts can fail if the chain is +# mining at the same time. +RPC_URL=$L1_CHAIN_HTTP pnpm run start-mining + +# serve contract deployment data +echo "Starting deployment data server." +pnpm run serve-deployments + + diff --git a/packages/solidity-contracts/docker/l1-chain/package.json b/packages/solidity-contracts/docker/l1-chain/package.json new file mode 100644 index 00000000..8d7842f8 --- /dev/null +++ b/packages/solidity-contracts/docker/l1-chain/package.json @@ -0,0 +1,84 @@ +{ + "name": "@fuel-bridge/solidity-contracts", + "version": "1.0.0", + "description": "The Fuel v2 Solidity smart contracts.", + "license": "APACHE-2.0", + "files": [ + "typechain", + "contracts", + "artifacts", + "dist" + ], + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "typings": "dist/index.d.ts", + "main": "dist/index.js", + "scripts": { + "build": "run-s clean compile build:exports", + "build:exports": "tsup", + "clean": "pnpm hardhat clean", + "compile": "pnpm hardhat compile --show-stack-traces", + "coverage": "pnpm run build && pnpm hardhat coverage --temp artifacts --network hardhat", + "check": "pnpm solhint \"contracts/**/*.sol\"", + "node": "pnpm hardhat node --network hardhat --hostname 0.0.0.0", + "start-mining": "pnpm hardhat run --no-compile --network localhost scripts/startAutoMining.ts", + "serve-deployments": "pnpm ts-node scripts/serveDeployments.ts", + "test": "pnpm hardhat test", + "integration-test": "pnpm mocha -b -r ts-node/register 'integration-tests/**/*.ts'", + "node:up": "sh ./scripts/node:up.sh", + "hardhat:test:integration": "DISABLE_GAS_REPORTER=true pnpm hardhat --network mainnetFork test fork-integration-tests/**/*.ts --bail", + "test-no-compile": "pnpm hardhat test --no-compile", + "test-parallel": "pnpx mocha 'test/**/*.ts' --recursive --parallel --require hardhat/register", + "prettier": "prettier --write --plugin=prettier-plugin-solidity 'contracts/**/*.sol'", + "lint": "prettier --list-different --plugin=prettier-plugin-solidity 'contracts/**/*.sol'" + }, + "devDependencies": { + "@fuel-ts/merkle": "^0.21.2", + "@inquirer/prompts": "^5.3.8", + "@nomicfoundation/hardhat-chai-matchers": "^2.0.4", + "@nomicfoundation/hardhat-ethers": "^3.0.5", + "@nomicfoundation/hardhat-network-helpers": "^1.0.10", + "@nomicfoundation/hardhat-verify": "1.1.1", + "@openzeppelin/contracts": "^4.8.3", + "@openzeppelin/contracts-upgradeable": "^4.8.3", + "@openzeppelin/hardhat-upgrades": "^3.0.4", + "@safe-global/api-kit": "^2.4.6", + "@safe-global/protocol-kit": "^4.1.1", + "@safe-global/safe-core-sdk-types": "^5.1.0", + "@typechain/ethers-v6": "^0.5.1", + "@typechain/hardhat": "^9.1.0", + "@types/chai": "^4.3.4", + "@types/cors": "2.8.17", + "mocha": "^10.0.0", + "@types/express": "^4.17.14", + "@types/lodash": "^4.14.202", + "@types/mocha": "^10.0.0", + "@types/node": "^18.11.9", + "@typescript-eslint/eslint-plugin": "^5.43.0", + "@typescript-eslint/parser": "^5.43.0", + "axios": "^1.7.7", + "chai": "^4.3.7", + "cors": "2.8.5", + "dotenv": "^16.0.3", + "ethers": "6.13.1", + "express": "^4.18.2", + "fuels": "0.96.1", + "hardhat": "^2.20.1", + "hardhat-deploy": "^0.12.4", + "inquirer": "^10.1.8", + "lodash": "^4.17.21", + "markdownlint": "^0.26.2", + "markdownlint-cli": "^0.32.2", + "node-fetch": "^2.6.6", + "npm-run-all": "^4.1.5", + "prettier-plugin-solidity": "^1.1.3", + "solc": "^0.8.17", + "solhint": "3.3.7", + "solidity-coverage": "^0.8.5", + "ts-generator": "^0.1.1", + "ts-node": "^10.9.1", + "tsup": "^7.2.0", + "typechain": "^8.3.2", + "typescript": "^4.9.3" + } +} diff --git a/packages/solidity-contracts/hardhat.config.ts b/packages/solidity-contracts/hardhat.config.ts index 2815dc62..887110fb 100644 --- a/packages/solidity-contracts/hardhat.config.ts +++ b/packages/solidity-contracts/hardhat.config.ts @@ -41,6 +41,10 @@ const config: HardhatUserConfig = { accounts: { count: 128, }, + forking: { + enabled: !!process.env.TENDERLY_RPC_URL, + url: process.env.TENDERLY_RPC_URL ? process.env.TENDERLY_RPC_URL : '', + }, deploy: ['deploy/hardhat'], }, localhost: { diff --git a/packages/solidity-contracts/integration-tests/bridge_erc20.ts b/packages/solidity-contracts/integration-tests/bridge_erc20.ts new file mode 100644 index 00000000..c1fe6d66 --- /dev/null +++ b/packages/solidity-contracts/integration-tests/bridge_erc20.ts @@ -0,0 +1,734 @@ +import type { BridgeFungibleToken } from '@fuel-bridge/fungible-token'; +import type { TestEnvironment } from '@fuel-bridge/test-utils'; +import { + setupEnvironment, + relayCommonMessage, + waitForMessage, + createRelayMessageParams, + getOrDeployECR20Contract, + getOrDeployL2Bridge, + FUEL_TX_PARAMS, + getMessageOutReceipt, + fuel_to_eth_address, + waitForBlockFinalization, + getTokenId, + getBlock, + FUEL_CALL_TX_PARAMS, + hardhatSkipTime, +} from '@fuel-bridge/test-utils'; +import chai from 'chai'; +import { toBeHex, parseEther } from 'ethers'; +import type { JsonRpcProvider, Signer } from 'ethers'; +import { Address, BN } from 'fuels'; +import type { + AbstractAddress, + WalletUnlocked as FuelWallet, + MessageProof, + Provider, +} from 'fuels'; + +import { RATE_LIMIT_AMOUNT, RATE_LIMIT_DURATION } from '../protocol/constants'; +import type { Token } from '../typechain'; + +const { expect } = chai; + +describe('Bridging ERC20 tokens', async function () { + // Timeout 6 minutes + const DEFAULT_TIMEOUT_MS: number = 400_000; + const FUEL_MESSAGE_TIMEOUT_MS: number = 30_000; + const DECIMAL_DIFF = 1_000_000_000n; + + let env: TestEnvironment; + let eth_testToken: Token; + let eth_testTokenAddress: string; + let eth_erc20GatewayAddress: string; + let fuel_bridge: BridgeFungibleToken; + let fuel_bridgeImpl: BridgeFungibleToken; + let fuel_bridgeContractId: string; + let fuel_testAssetId: string; + + // override the default test timeout from 2000ms + this.timeout(DEFAULT_TIMEOUT_MS); + + async function forwardFuelChain(provider: Provider, blocksToForward: string) { + await provider.produceBlocks(Number(blocksToForward)).catch(console.error); + } + + async function getBlockWithHeight(env: any, height: string): Promise { + const BLOCK_BY_HEIGHT_QUERY = `query Block($height: U64) { + block(height: $height) { + id + } + }`; + const BLOCK_BY_HEIGHT_ARGS = { + height: height, + }; + + return fetch(env.fuel.provider.url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: BLOCK_BY_HEIGHT_QUERY, + variables: BLOCK_BY_HEIGHT_ARGS, + }), + }) + .then((res: any) => res.json()) + .then(async (res) => { + if (!res.data.block) { + throw new Error(`Could not fetch block with height ${height}`); + } + + return res.data.block; + }); + } + + async function generateWithdrawalMessageProof( + fuel_bridge: BridgeFungibleToken, + fuelTokenSender: FuelWallet, + ethereumTokenReceiverAddress: string, + NUM_TOKENS: bigint, + DECIMAL_DIFF: bigint + ): Promise { + // withdraw tokens back to the base chain + fuel_bridge.account = fuelTokenSender; + const paddedAddress = + '0x' + ethereumTokenReceiverAddress.slice(2).padStart(64, '0'); + const fuelTokenSenderBalance = await fuelTokenSender.getBalance( + fuel_testAssetId + ); + const transactionRequest = await fuel_bridge.functions + .withdraw(paddedAddress) + .addContracts([fuel_bridge, fuel_bridgeImpl]) + .txParams({ + tip: 0, + maxFee: 1, + }) + .callParams({ + forward: { + amount: new BN(NUM_TOKENS.toString()).div( + new BN(DECIMAL_DIFF.toString()) + ), + assetId: fuel_testAssetId, + }, + }) + .fundWithRequiredCoins(); + + const tx = await fuelTokenSender.sendTransaction(transactionRequest); + const fWithdrawTxResult = await tx.waitForResult(); + expect(fWithdrawTxResult.status).to.equal('success'); + + // check that the sender balance has decreased by the expected amount + const newSenderBalance = await fuelTokenSender.getBalance(fuel_testAssetId); + + expect( + newSenderBalance.eq( + fuelTokenSenderBalance.sub(toBeHex(NUM_TOKENS / DECIMAL_DIFF)) + ) + ).to.be.true; + + // Wait for the commited block + const withdrawBlock = await getBlock( + env.fuel.provider.url, + fWithdrawTxResult.blockId! + ); + + const TIME_TO_FINALIZE = await env.eth.fuelChainState.TIME_TO_FINALIZE(); + + const blocksPerCommitInterval = ( + await env.eth.fuelChainState.BLOCKS_PER_COMMIT_INTERVAL() + ).toString(); + + // Add + 1 to the block height to wait the next block + // that enable to proof the message + const nextBlockHeight = new BN(withdrawBlock.header.height).add(new BN(1)); + const commitHeight = new BN(nextBlockHeight).div(blocksPerCommitInterval); + + let cooldown = await env.eth.fuelChainState.COMMIT_COOLDOWN(); + + // fast forward post the commit cooldown period + await env.eth.provider.send('evm_increaseTime', [Number(cooldown) * 10]); // Advance 1 hour + await env.eth.provider.send('evm_mine', []); // Mine a new block + + // override the commit hash in a existing block + await env.eth.fuelChainState + .connect(env.eth.signers[1]) + .commit( + '0x0000000000000000000000000000000000000000000000000000000000000000', + commitHeight.toString() + ); + + // fast forward to the block finalization time + await env.eth.provider.send('evm_increaseTime', [ + Number(TIME_TO_FINALIZE) * 2, + ]); + await env.eth.provider.send('evm_mine', []); // Mine a new block + + cooldown = await env.eth.fuelChainState.COMMIT_COOLDOWN(); + + // fast forward post the commit cooldown period + await env.eth.provider.send('evm_increaseTime', [Number(cooldown) * 10]); // Advance 1 hour + await env.eth.provider.send('evm_mine', []); // Mine a new block + + // produce more blocks to fetch the block height + await forwardFuelChain(env.fuel.provider, blocksPerCommitInterval); + + const block = await getBlockWithHeight(env, nextBlockHeight.toString()); + + // reset the commit hash in the local L2 network + await env.eth.fuelChainState + .connect(env.eth.signers[1]) + .commit(block.id, commitHeight.toString()); + + // fast forward to the block finalization time + await env.eth.provider.send('evm_increaseTime', [ + Number(TIME_TO_FINALIZE) * 2, + ]); + await env.eth.provider.send('evm_mine', []); // Mine a new block + + const messageOutReceipt = getMessageOutReceipt(fWithdrawTxResult.receipts); + return await fuelTokenSender.provider.getMessageProof( + tx.id, + messageOutReceipt.nonce, + block.id + ); + } + + async function relayMessageFromFuel( + env: TestEnvironment, + withdrawMessageProof: MessageProof + ) { + // wait for block finalization + await waitForBlockFinalization(env, withdrawMessageProof); + + // construct relay message proof data + const relayMessageParams = createRelayMessageParams(withdrawMessageProof); + + // relay message + await env.eth.fuelMessagePortal.relayMessage( + relayMessageParams.message, + relayMessageParams.rootBlockHeader, + relayMessageParams.blockHeader, + relayMessageParams.blockInHistoryProof, + relayMessageParams.messageInBlockProof + ); + } + + async function relayMessageFromEthereum( + env: TestEnvironment, + fuelTokenMessageReceiver: AbstractAddress, + fuelTokenMessageNonce: BN, + fuel_AssetId: string, + amount: bigint + ) { + // relay the message ourselves + const message = await waitForMessage( + env.fuel.provider, + fuelTokenMessageReceiver, + fuelTokenMessageNonce, + FUEL_MESSAGE_TIMEOUT_MS + ); + expect(message).to.not.be.null; + + const tx = await relayCommonMessage(env.fuel.deployer, message, { + maturity: undefined, + contractIds: [fuel_bridgeImpl.id.toHexString()], + }); + + const txResult = await tx.waitForResult(); + + expect(txResult.status).to.equal('success'); + expect(txResult.mintedAssets.length).to.equal(1); + + const [mintedAsset] = txResult.mintedAssets; + + expect(mintedAsset.assetId).to.equal(fuel_AssetId); + expect(mintedAsset.amount.toString()).to.equal( + (amount / DECIMAL_DIFF).toString() + ); + } + + before(async () => { + env = await setupEnvironment({}); + eth_erc20GatewayAddress = ( + await env.eth.fuelERC20Gateway.getAddress() + ).toLowerCase(); + + eth_testToken = await getOrDeployECR20Contract(env); + eth_testTokenAddress = (await eth_testToken.getAddress()).toLowerCase(); + + const { contract, implementation } = await getOrDeployL2Bridge( + env, + env.eth.fuelERC20Gateway + ); + + fuel_bridge = contract; + fuel_bridgeImpl = implementation; + + fuel_bridgeContractId = fuel_bridge.id.toHexString(); + + await env.eth.fuelERC20Gateway + .connect(env.eth.deployer) + .setAssetIssuerId(fuel_bridgeContractId); + fuel_testAssetId = getTokenId(fuel_bridge, eth_testTokenAddress); + + // initializing rate limit params for the token + await env.eth.fuelERC20Gateway + .connect(env.eth.deployer) + .resetRateLimitAmount( + eth_testTokenAddress, + RATE_LIMIT_AMOUNT.toString(), + RATE_LIMIT_DURATION + ); + + await env.eth.fuelERC20Gateway + .connect(env.eth.deployer) + .updateRateLimitStatus(eth_testTokenAddress, true); + + const { value: expectedGatewayContractId } = await fuel_bridge.functions + .bridged_token_gateway() + .addContracts([fuel_bridge, fuel_bridgeImpl]) + .txParams(FUEL_CALL_TX_PARAMS) + .dryRun(); + + // check that values for the test token and gateway contract match what + // was compiled into the bridge-fungible-token binaries + + expect(fuel_to_eth_address(expectedGatewayContractId)).to.equal( + eth_erc20GatewayAddress + ); + expect(await eth_testToken.decimals()).to.equal(18n); + + // mint tokens as starting balances + await eth_testToken + .mint(await env.eth.deployer.getAddress(), 10_000) + .then((tx) => tx.wait()); + + await eth_testToken + .mint(await env.eth.signers[0].getAddress(), 10_000) + .then((tx) => tx.wait()); + + await eth_testToken + .mint(await env.eth.signers[1].getAddress(), 10_000) + .then((tx) => tx.wait()); + }); + + describe('Bridge ERC20 to Fuel', async () => { + const NUM_TOKENS = 100000000000000000000n; + let ethereumTokenSender: Signer; + let ethereumTokenSenderAddress: string; + let ethereumTokenSenderBalance: bigint; + let fuelTokenReceiver: FuelWallet; + let fuelTokenReceiverAddress: string; + let fuelTokenReceiverBalance: BN; + let fuelTokenMessageNonce: BN; + let fuelTokenMessageReceiver: AbstractAddress; + + before(async () => { + ethereumTokenSender = env.eth.signers[0]; + ethereumTokenSenderAddress = await ethereumTokenSender.getAddress(); + + await eth_testToken + .mint(ethereumTokenSenderAddress, NUM_TOKENS) + .then((tx) => tx.wait()); + + ethereumTokenSenderBalance = await eth_testToken.balanceOf( + ethereumTokenSenderAddress + ); + + fuelTokenReceiver = env.fuel.signers[0]; + fuelTokenReceiverAddress = fuelTokenReceiver.address.toHexString(); + fuelTokenReceiverBalance = await fuelTokenReceiver.getBalance( + fuel_testAssetId + ); + }); + + it('Bridge ERC20 via FuelERC20Gateway', async () => { + // approve FuelERC20Gateway to spend the tokens + await eth_testToken + .connect(ethereumTokenSender) + .approve(eth_erc20GatewayAddress, NUM_TOKENS) + .then((tx) => tx.wait()); + + // use the FuelERC20Gateway to deposit test tokens and receive equivalent tokens on Fuel + const receipt = await env.eth.fuelERC20Gateway + .connect(ethereumTokenSender) + .deposit(fuelTokenReceiverAddress, eth_testTokenAddress, NUM_TOKENS) + .then((tx) => tx.wait()); + + expect(receipt!.status).to.equal(1); + // parse events from logs + const [event, ...restOfEvents] = + await env.eth.fuelMessagePortal.queryFilter( + env.eth.fuelMessagePortal.filters.MessageSent, + receipt!.blockNumber, + receipt!.blockNumber + ); + expect(restOfEvents.length).to.be.eq(0); // Should be only 1 event + + fuelTokenMessageNonce = new BN(event.args.nonce.toString()); + fuelTokenMessageReceiver = Address.fromB256(event.args.recipient); + + // check that the sender balance has decreased by the expected amount + const newSenderBalance = await eth_testToken.balanceOf( + ethereumTokenSenderAddress + ); + expect(newSenderBalance === ethereumTokenSenderBalance - NUM_TOKENS).to.be + .true; + }); + + it('Relay messages from Ethereum on Fuel', async () => { + // override the default test timeout from 2000ms + this.timeout(FUEL_MESSAGE_TIMEOUT_MS); + // relay the standard erc20 deposit + await relayMessageFromEthereum( + env, + fuelTokenMessageReceiver, + fuelTokenMessageNonce, + fuel_testAssetId, + NUM_TOKENS + ); + + // override the default test timeout from 2000ms + this.timeout(FUEL_MESSAGE_TIMEOUT_MS); + }); + + it('Check metadata was registered', async () => { + await fuel_bridge.functions + .asset_to_l1_address({ bits: fuel_testAssetId }) + .addContracts([fuel_bridge, fuel_bridgeImpl]) + .call(); + + const { value: l2_decimals } = await fuel_bridge.functions + .decimals({ bits: fuel_testAssetId }) + .addContracts([fuel_bridge, fuel_bridgeImpl]) + .get(); + + expect(l2_decimals).to.be.equal(9); + }); + + it('Check ERC20 arrived on Fuel', async () => { + // check that the recipient balance has increased by the expected amount + const newReceiverBalance = await fuelTokenReceiver.getBalance( + fuel_testAssetId + ); + + expect( + newReceiverBalance.eq( + fuelTokenReceiverBalance.add(toBeHex(NUM_TOKENS / DECIMAL_DIFF)) + ) + ).to.be.true; + }); + + it('Bridge metadata', async () => { + // use the FuelERC20Gateway to deposit test tokens and receive equivalent tokens on Fuel + const receipt = await env.eth.fuelERC20Gateway + .connect(ethereumTokenSender) + .sendMetadata(eth_testTokenAddress) + .then((tx) => tx.wait()); + + // parse events from logs + const [event, ...restOfEvents] = + await env.eth.fuelMessagePortal.queryFilter( + env.eth.fuelMessagePortal.filters.MessageSent, + receipt!.blockNumber, + receipt!.blockNumber + ); + expect(restOfEvents.length).to.be.eq(0); // Should be only 1 event + + const nonce = new BN(event.args.nonce.toString()); + const fuelReceiver = Address.fromB256(event.args.recipient); + + // relay the message ourselves + const message = await waitForMessage( + env.fuel.provider, + fuelReceiver, + nonce, + FUEL_MESSAGE_TIMEOUT_MS + ); + expect(message).to.not.be.null; + + const tx = await relayCommonMessage(env.fuel.deployer, message, { + ...FUEL_TX_PARAMS, + maturity: undefined, + contractIds: [fuel_bridgeImpl.id.toHexString()], + }); + + const txResult = await tx.waitForResult(); + expect(txResult.status).to.equal('success'); + + const fuel_name = ( + await fuel_bridge.functions.name({ bits: fuel_testAssetId }).dryRun() + ).value; + const fuel_symbol = ( + await fuel_bridge.functions.symbol({ bits: fuel_testAssetId }).dryRun() + ).value; + + const eth_name = await eth_testToken.name(); + const eth_symbol = await eth_testToken.symbol(); + + expect(fuel_name).to.equal(eth_name); + expect(fuel_symbol).to.equal(eth_symbol); + }); + }); + + describe('Bridge ERC20 from Fuel', async () => { + const NUM_TOKENS = 10000000000000000000n; + const largeRateLimit = `30`; + let fuelTokenSender: FuelWallet; + let ethereumTokenReceiver: Signer; + let ethereumTokenReceiverAddress: string; + let ethereumTokenReceiverBalance: bigint; + let withdrawMessageProof: MessageProof | null; + let tokenBalanceBeforeWithdrawingOnFuel: BN; + + before(async () => { + fuelTokenSender = env.fuel.signers[0]; + ethereumTokenReceiver = env.eth.signers[0]; + ethereumTokenReceiverAddress = await ethereumTokenReceiver.getAddress(); + ethereumTokenReceiverBalance = await eth_testToken.balanceOf( + ethereumTokenReceiverAddress + ); + + tokenBalanceBeforeWithdrawingOnFuel = await fuelTokenSender.getBalance( + fuel_testAssetId + ); + }); + + it('Bridge ERC20 via Fuel token contract', async () => { + // withdraw tokens back to the base chain + withdrawMessageProof = await generateWithdrawalMessageProof( + fuel_bridge, + fuelTokenSender, + ethereumTokenReceiverAddress, + NUM_TOKENS, + DECIMAL_DIFF + ); + }); + + it('Relay Message from Fuel on Ethereum', async () => { + const withdrawnAmountBeforeRelay = + await env.eth.fuelERC20Gateway.currentPeriodAmount( + eth_testTokenAddress + ); + + const rateLimitEndDuratioBeforeRelay = + await env.eth.fuelERC20Gateway.currentPeriodEnd(eth_testTokenAddress); + + // relay message + await relayMessageFromFuel(env, withdrawMessageProof!); + + // check rate limit params + const withdrawnAmountAfterRelay = + await env.eth.fuelERC20Gateway.currentPeriodAmount( + eth_testTokenAddress + ); + + const rateLimitEndDuratioAfterRelay = + await env.eth.fuelERC20Gateway.currentPeriodEnd(eth_testTokenAddress); + + expect(rateLimitEndDuratioAfterRelay === rateLimitEndDuratioBeforeRelay) + .to.be.true; + + expect( + withdrawnAmountAfterRelay === NUM_TOKENS + withdrawnAmountBeforeRelay + ).to.be.true; + }); + + it('Check the remaining token balance on Fuel after the first withdrawal', async () => { + // fetch the remaining token balance + const currentTokenBalance = await fuelTokenSender.getBalance( + fuel_testAssetId + ); + + // currentTokenBalance has BN type by default hence the use of BN for conversion here + const expectedRemainingTokenBalanceOnFuel = + tokenBalanceBeforeWithdrawingOnFuel.sub( + new BN((NUM_TOKENS / DECIMAL_DIFF).toString()) + ); + + expect(currentTokenBalance.eq(expectedRemainingTokenBalanceOnFuel)).to.be + .true; + }); + + it('Rate limit parameters are updated when current withdrawn amount is more than the new limit & set a new higher limit', async () => { + const deployer = await env.eth.deployer; + const newRateLimit = '5'; + + let withdrawnAmountBeforeReset = + await env.eth.fuelERC20Gateway.currentPeriodAmount( + eth_testTokenAddress + ); + + await env.eth.fuelERC20Gateway + .connect(deployer) + .resetRateLimitAmount( + eth_testTokenAddress, + parseEther(newRateLimit), + RATE_LIMIT_DURATION + ); + + let currentWithdrawnAmountAfterSettingLimit = + await env.eth.fuelERC20Gateway.currentPeriodAmount( + eth_testTokenAddress + ); + + // current withdrawn amount doesn't change when rate limit is updated + + expect( + currentWithdrawnAmountAfterSettingLimit === withdrawnAmountBeforeReset + ).to.be.true; + + withdrawnAmountBeforeReset = + await env.eth.fuelERC20Gateway.currentPeriodAmount( + eth_testTokenAddress + ); + + await env.eth.fuelERC20Gateway + .connect(deployer) + .resetRateLimitAmount( + eth_testTokenAddress, + parseEther(largeRateLimit), + RATE_LIMIT_DURATION + ); + + currentWithdrawnAmountAfterSettingLimit = + await env.eth.fuelERC20Gateway.currentPeriodAmount( + eth_testTokenAddress + ); + + expect( + currentWithdrawnAmountAfterSettingLimit === withdrawnAmountBeforeReset + ).to.be.true; + }); + + it('Rate limit parameters are updated when the initial duration is over', async () => { + const deployer = await env.eth.deployer; + + const rateLimitDuration = + await env.eth.fuelERC20Gateway.rateLimitDuration(eth_testTokenAddress); + + // fast forward time + await hardhatSkipTime( + env.eth.provider as JsonRpcProvider, + rateLimitDuration * 2n + ); + const currentPeriodEndBeforeRelay = + await env.eth.fuelERC20Gateway.currentPeriodEnd(eth_testTokenAddress); + + await env.eth.fuelERC20Gateway + .connect(deployer) + .resetRateLimitAmount( + eth_testTokenAddress, + parseEther(largeRateLimit), + RATE_LIMIT_DURATION + ); + + const currentWitdrawnAmountAfterReset = + await env.eth.fuelERC20Gateway.currentPeriodAmount( + eth_testTokenAddress + ); + + expect(currentWitdrawnAmountAfterReset == 0n).to.be.true; + + // withdraw tokens back to the base chain + withdrawMessageProof = await generateWithdrawalMessageProof( + fuel_bridge, + fuelTokenSender, + ethereumTokenReceiverAddress, + NUM_TOKENS, + DECIMAL_DIFF + ); + + // relay message + await relayMessageFromFuel(env, withdrawMessageProof!); + + const currentPeriodEndAfterRelay = + await env.eth.fuelERC20Gateway.currentPeriodEnd(eth_testTokenAddress); + + expect(currentPeriodEndAfterRelay > currentPeriodEndBeforeRelay).to.be + .true; + + const currentPeriodAmount = + await env.eth.fuelERC20Gateway.currentPeriodAmount( + eth_testTokenAddress + ); + + expect(currentPeriodAmount === NUM_TOKENS).to.be.true; + }); + + it('Check the remaining token balance on Fuel after the second withdrawal', async () => { + // fetch the remaining token balance + const currentTokenBalance = await fuelTokenSender.getBalance( + fuel_testAssetId + ); + + // currentTokenBalance has BN type by default hence the use of BN for conversion here + const expectedRemainingTokenBalanceOnFuel = + tokenBalanceBeforeWithdrawingOnFuel.sub( + new BN(((NUM_TOKENS * 2n) / DECIMAL_DIFF).toString()) + ); + + expect(currentTokenBalance.eq(expectedRemainingTokenBalanceOnFuel)).to.be + .true; + }); + + it('Rate limit parameters are updated when new limit is set after the initial duration', async () => { + const rateLimitDuration = + await env.eth.fuelERC20Gateway.rateLimitDuration(eth_testTokenAddress); + + const deployer = await env.eth.deployer; + const newRateLimit = `40`; + + // fast forward time + await hardhatSkipTime( + env.eth.provider as JsonRpcProvider, + rateLimitDuration * 2n + ); + + const currentWithdrawnAmountBeforeSettingLimit = + await env.eth.fuelERC20Gateway.currentPeriodAmount( + eth_testTokenAddress + ); + const currentPeriodEndBeforeSettingLimit = + await env.eth.fuelERC20Gateway.currentPeriodEnd(eth_testTokenAddress); + + await env.eth.fuelERC20Gateway + .connect(deployer) + .resetRateLimitAmount( + eth_testTokenAddress, + parseEther(newRateLimit), + RATE_LIMIT_DURATION + ); + + const currentPeriodEndAfterSettingLimit = + await env.eth.fuelERC20Gateway.currentPeriodEnd(eth_testTokenAddress); + const currentWithdrawnAmountAfterSettingLimit = + await env.eth.fuelERC20Gateway.currentPeriodAmount( + eth_testTokenAddress + ); + + expect( + currentPeriodEndAfterSettingLimit > currentPeriodEndBeforeSettingLimit + ).to.be.true; + + expect( + currentWithdrawnAmountBeforeSettingLimit > + currentWithdrawnAmountAfterSettingLimit + ).to.be.true; + + expect(currentWithdrawnAmountAfterSettingLimit === 0n).to.be.true; + }); + + it('Check ERC20 arrived on Ethereum', async () => { + // check that the recipient balance has increased by the expected amount + const newReceiverBalance = await eth_testToken.balanceOf( + ethereumTokenReceiverAddress + ); + expect( + newReceiverBalance === ethereumTokenReceiverBalance + NUM_TOKENS * 2n + ).to.be.true; + }); + }); +}); diff --git a/packages/solidity-contracts/integration-tests/transfer_eth.ts b/packages/solidity-contracts/integration-tests/transfer_eth.ts new file mode 100644 index 00000000..eed3f7b8 --- /dev/null +++ b/packages/solidity-contracts/integration-tests/transfer_eth.ts @@ -0,0 +1,518 @@ +import type { TestEnvironment } from '@fuel-bridge/test-utils'; +import { + setupEnvironment, + fuels_parseEther, + createRelayMessageParams, + getMessageOutReceipt, + waitForMessage, + waitForBlockFinalization, + getBlock, + FUEL_CALL_TX_PARAMS, +} from '@fuel-bridge/test-utils'; +import chai from 'chai'; +import { parseEther } from 'ethers'; +import type { Signer } from 'ethers'; +import { Address, BN, padFirst12BytesOfEvmAddress } from 'fuels'; +import type { + AbstractAddress, + WalletUnlocked as FuelWallet, + MessageProof, + Provider, +} from 'fuels'; + +const { expect } = chai; + +describe('Transferring ETH', async function () { + // Timeout 6 minutes + const DEFAULT_TIMEOUT_MS: number = 400_000; + const FUEL_MESSAGE_TIMEOUT_MS: number = 30_000; + let BASE_ASSET_ID: string; + + let env: TestEnvironment; + + // override the default test timeout of 2000ms + this.timeout(DEFAULT_TIMEOUT_MS); + + async function forwardFuelChain(provider: Provider, blocksToForward: string) { + await provider.produceBlocks(Number(blocksToForward)).catch(console.error); + } + + async function getBlockWithHeight(env: any, height: string): Promise { + const BLOCK_BY_HEIGHT_QUERY = `query Block($height: U64) { + block(height: $height) { + id + } + }`; + const BLOCK_BY_HEIGHT_ARGS = { + height: height, + }; + + return fetch(env.fuel.provider.url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: BLOCK_BY_HEIGHT_QUERY, + variables: BLOCK_BY_HEIGHT_ARGS, + }), + }) + .then((res: any) => res.json()) + .then(async (res) => { + if (!res.data.block) { + throw new Error(`Could not fetch block with height ${height}`); + } + + return res.data.block; + }); + } + + async function generateWithdrawalMessageProof( + fuelETHSender: FuelWallet, + ethereumETHReceiverAddress: string, + NUM_ETH: string + ): Promise { + // withdraw ETH back to the base chain + const fWithdrawTx = await fuelETHSender.withdrawToBaseLayer( + Address.fromString( + padFirst12BytesOfEvmAddress(ethereumETHReceiverAddress) + ), + fuels_parseEther(NUM_ETH), + FUEL_CALL_TX_PARAMS + ); + const fWithdrawTxResult = await fWithdrawTx.waitForResult(); + expect(fWithdrawTxResult.status).to.equal('success'); + + // Wait for the commited block + const withdrawBlock = await getBlock( + env.fuel.provider.url, + fWithdrawTxResult.blockId! + ); + + const TIME_TO_FINALIZE = await env.eth.fuelChainState.TIME_TO_FINALIZE(); + + const blocksPerCommitInterval = ( + await env.eth.fuelChainState.BLOCKS_PER_COMMIT_INTERVAL() + ).toString(); + + // Add + 1 to the block height to wait the next block + // that enable to proof the message + const nextBlockHeight = new BN(withdrawBlock.header.height).add(new BN(1)); + const commitHeight = new BN(nextBlockHeight).div(blocksPerCommitInterval); + + let cooldown = await env.eth.fuelChainState.COMMIT_COOLDOWN(); + + // fast forward post the commit cooldown period + await env.eth.provider.send('evm_increaseTime', [Number(cooldown) * 10]); // Advance 1 hour + await env.eth.provider.send('evm_mine', []); // Mine a new block + + // override the commit hash in a existing block + await env.eth.fuelChainState + .connect(env.eth.signers[1]) + .commit( + '0x0000000000000000000000000000000000000000000000000000000000000000', + commitHeight.toString() + ); + + // fast forward to the block finalization time + await env.eth.provider.send('evm_increaseTime', [ + Number(TIME_TO_FINALIZE) * 2, + ]); + await env.eth.provider.send('evm_mine', []); // Mine a new block + + cooldown = await env.eth.fuelChainState.COMMIT_COOLDOWN(); + + // fast forward post the commit cooldown period + await env.eth.provider.send('evm_increaseTime', [Number(cooldown) * 10]); // Advance 1 hour + await env.eth.provider.send('evm_mine', []); // Mine a new block + + // produce more blocks to fetch the block height + await forwardFuelChain(env.fuel.provider, blocksPerCommitInterval); + + const block = await getBlockWithHeight(env, nextBlockHeight.toString()); + + // reset the commit hash in the local L2 network + await env.eth.fuelChainState + .connect(env.eth.signers[1]) + .commit(block.id, commitHeight.toString()); + + // fast forward to the block finalization time + await env.eth.provider.send('evm_increaseTime', [ + Number(TIME_TO_FINALIZE) * 2, + ]); + await env.eth.provider.send('evm_mine', []); // Mine a new block + + // get message proof + const messageOutReceipt = getMessageOutReceipt(fWithdrawTxResult.receipts); + + return await fuelETHSender.provider.getMessageProof( + fWithdrawTx.id, + messageOutReceipt.nonce, + block.id + ); + } + + async function relayMessage( + env: TestEnvironment, + withdrawMessageProof: MessageProof + ) { + // wait for block finalization + await waitForBlockFinalization(env, withdrawMessageProof); + + // construct relay message proof data + const relayMessageParams = createRelayMessageParams(withdrawMessageProof); + + const TIME_TO_FINALIZE = await env.eth.fuelChainState.TIME_TO_FINALIZE(); + + // fast forward to the block finalization time + await env.eth.provider.send('evm_increaseTime', [ + Number(TIME_TO_FINALIZE) * 100, + ]); + await env.eth.provider.send('evm_mine', []); // Mine a new block + + // relay message + await env.eth.fuelMessagePortal.relayMessage( + relayMessageParams.message, + relayMessageParams.rootBlockHeader, + relayMessageParams.blockHeader, + relayMessageParams.blockInHistoryProof, + relayMessageParams.messageInBlockProof + ); + } + + before(async () => { + env = await setupEnvironment({}); + BASE_ASSET_ID = env.fuel.provider.getBaseAssetId(); + }); + + describe('Send ETH to Fuel', async () => { + const NUM_ETH = '30'; + let ethereumETHSender: Signer; + let ethereumETHSenderAddress: string; + let fuelETHReceiver: AbstractAddress; + let fuelETHReceiverAddress: string; + let fuelETHReceiverBalance: BN; + let fuelETHMessageNonce: BN; + + before(async () => { + ethereumETHSender = env.eth.signers[0]; + ethereumETHSenderAddress = await ethereumETHSender.getAddress(); + fuelETHReceiver = env.fuel.signers[0].address; + fuelETHReceiverAddress = fuelETHReceiver.toHexString(); + + fuelETHReceiverBalance = await env.fuel.provider.getBalance( + fuelETHReceiver, + BASE_ASSET_ID + ); + }); + + it('Send ETH via MessagePortal', async () => { + // use the FuelMessagePortal to directly send ETH which should be immediately spendable + const tx = await env.eth.fuelMessagePortal + .connect(ethereumETHSender) + .depositETH(fuelETHReceiverAddress, { + value: parseEther(NUM_ETH), + }); + const receipt = await tx.wait(); + expect(receipt!.status).to.equal(1); + + // parse events from logs + const filter = env.eth.fuelMessagePortal.filters.MessageSent( + undefined, // Args set to null since there should be just 1 event for MessageSent + undefined, + undefined, + undefined, + undefined + ); + + const [event, ...restOfEvents] = + await env.eth.fuelMessagePortal.queryFilter( + filter, + receipt!.blockNumber, + receipt!.blockNumber + ); + expect(restOfEvents.length).to.be.eq(0); // Should be only 1 event + + fuelETHMessageNonce = new BN(event.args.nonce.toString()); + + // check that the sender balance has decreased by the expected amount + const newSenderBalance = await env.eth.provider.getBalance( + ethereumETHSenderAddress, + receipt!.blockNumber + ); + + const txCost = receipt!.fee; + + const expectedSenderBalance = + (await env.eth.provider.getBalance( + ethereumETHSender, + receipt!.blockNumber - 1 + )) - + txCost - + parseEther(NUM_ETH); + + expect(newSenderBalance).to.be.eq(expectedSenderBalance); + }); + + it('Wait for ETH to arrive on Fuel', async function () { + // wait for message to appear in fuel client + expect( + await waitForMessage( + env.fuel.provider, + fuelETHReceiver, + fuelETHMessageNonce, + FUEL_MESSAGE_TIMEOUT_MS + ) + ).to.not.be.null; + + // check that the recipient balance has increased by the expected amount + const newReceiverBalance = await env.fuel.provider.getBalance( + fuelETHReceiver, + BASE_ASSET_ID + ); + expect( + newReceiverBalance.eq( + fuelETHReceiverBalance.add(fuels_parseEther(NUM_ETH)) + ) + ).to.be.true; + }); + }); + + describe('Send ETH from Fuel', async () => { + const NUM_ETH = '0.001'; + let fuelETHSender: FuelWallet; + let fuelETHSenderBalance: BN; + let ethereumETHReceiver: Signer; + let ethereumETHReceiverAddress: string; + let ethereumETHReceiverBalance: bigint; + let withdrawMessageProof: MessageProof | null; + + before(async () => { + fuelETHSender = env.fuel.signers[1]; + fuelETHSenderBalance = await fuelETHSender.getBalance(BASE_ASSET_ID); + ethereumETHReceiver = env.eth.signers[1]; + ethereumETHReceiverAddress = await ethereumETHReceiver.getAddress(); + ethereumETHReceiverBalance = await env.eth.provider.getBalance( + ethereumETHReceiver + ); + }); + + it('Send ETH via OutputMessage', async () => { + withdrawMessageProof = await generateWithdrawalMessageProof( + fuelETHSender, + ethereumETHReceiverAddress, + NUM_ETH + ); + + // check that the sender balance has decreased by the expected amount + const newSenderBalance = await fuelETHSender.getBalance(BASE_ASSET_ID); + + // Get just the first 3 digits of the balance to compare to the expected balance + // this is required because the payment of gas fees is not deterministic + const diffOnSenderBalance = newSenderBalance + .sub(fuelETHSenderBalance) + .formatUnits(); + expect(diffOnSenderBalance.startsWith(NUM_ETH)).to.be.true; + }); + + it('Relay Message from Fuel on Ethereum', async () => { + await relayMessage(env, withdrawMessageProof!); + }); + + it('Check ETH arrived on Ethereum', async () => { + // check that the recipient balance has increased by the expected amount + const newReceiverBalance = await env.eth.provider.getBalance( + ethereumETHReceiver + ); + + expect( + newReceiverBalance <= ethereumETHReceiverBalance + parseEther(NUM_ETH) + ).to.be.true; + }); + }); + + describe('ETH Withdrawls based on rate limit updates', async () => { + const NUM_ETH = '9'; + const largeRateLimit = `30`; + let fuelETHSender: FuelWallet; + let ethereumETHReceiver: Signer; + let ethereumETHReceiverAddress: string; + let withdrawMessageProof: MessageProof | null; + let rateLimitDuration: bigint; + + before(async () => { + fuelETHSender = env.fuel.signers[1]; + ethereumETHReceiver = env.eth.signers[1]; + ethereumETHReceiverAddress = await ethereumETHReceiver.getAddress(); + + await env.eth.fuelMessagePortal + .connect(env.eth.deployer) + .updateRateLimitStatus(true); + rateLimitDuration = await env.eth.fuelMessagePortal.RATE_LIMIT_DURATION(); + }); + + it('Checks rate limit params after relaying', async () => { + withdrawMessageProof = await generateWithdrawalMessageProof( + fuelETHSender, + ethereumETHReceiverAddress, + NUM_ETH + ); + + const withdrawnAmountBeforeRelay = + await env.eth.fuelMessagePortal.currentPeriodAmount(); + + await relayMessage(env, withdrawMessageProof!); + + const currentPeriodAmount = + await env.eth.fuelMessagePortal.currentPeriodAmount(); + + expect( + currentPeriodAmount === parseEther(NUM_ETH) + withdrawnAmountBeforeRelay + ).to.be.true; + }); + + it('Relays ETH after the rate limit is updated', async () => { + const deployer = env.eth.deployer; + const newRateLimit = `30`; + + await env.eth.fuelMessagePortal + .connect(deployer) + .resetRateLimitAmount(parseEther(newRateLimit)); + + withdrawMessageProof = await generateWithdrawalMessageProof( + fuelETHSender, + ethereumETHReceiverAddress, + NUM_ETH + ); + + const withdrawnAmountBeforeRelay = + await env.eth.fuelMessagePortal.currentPeriodAmount(); + + let currentWIthdrawnAmountReset = false; + + if (withdrawnAmountBeforeRelay > parseEther(newRateLimit)) { + currentWIthdrawnAmountReset = true; + + // fast forward time + await env.eth.provider.send('evm_increaseTime', [ + Number(rateLimitDuration) * 2, + ]); + await env.eth.provider.send('evm_mine', []); // Mine a new block + } + + await relayMessage(env, withdrawMessageProof!); + + const currentPeriodAmount = + await env.eth.fuelMessagePortal.currentPeriodAmount(); + + if (currentWIthdrawnAmountReset) + expect(currentPeriodAmount === parseEther(NUM_ETH)).to.be.true; + else { + expect( + currentPeriodAmount === + parseEther(NUM_ETH) + withdrawnAmountBeforeRelay + ).to.be.true; + } + }); + + it('Rate limit parameters are updated when current withdrawn amount is more than the new limit & set a new higher limit', async () => { + const deployer = env.eth.deployer; + const newRateLimit = `10`; + + let withdrawnAmountBeforeReset = + await env.eth.fuelMessagePortal.currentPeriodAmount(); + + await env.eth.fuelMessagePortal + .connect(deployer) + .resetRateLimitAmount(parseEther(newRateLimit)); + + let currentWithdrawnAmountAfterSettingLimit = + await env.eth.fuelMessagePortal.currentPeriodAmount(); + + // current withdrawn amount doesn't change when rate limit is updated + expect( + currentWithdrawnAmountAfterSettingLimit === withdrawnAmountBeforeReset + ).to.be.true; + + withdrawnAmountBeforeReset = + await env.eth.fuelMessagePortal.currentPeriodAmount(); + + await env.eth.fuelMessagePortal + .connect(deployer) + .resetRateLimitAmount(parseEther(largeRateLimit)); + + currentWithdrawnAmountAfterSettingLimit = + await env.eth.fuelMessagePortal.currentPeriodAmount(); + + expect( + currentWithdrawnAmountAfterSettingLimit === withdrawnAmountBeforeReset + ).to.be.true; + }); + + it('Rate limit parameters are updated when the initial duration is over', async () => { + // fast forward time + await env.eth.provider.send('evm_increaseTime', [ + Number(rateLimitDuration) * 2, + ]); + await env.eth.provider.send('evm_mine', []); // Mine a new block + + const currentPeriodEndBeforeRelay = + await env.eth.fuelMessagePortal.currentPeriodEnd(); + + withdrawMessageProof = await generateWithdrawalMessageProof( + fuelETHSender, + ethereumETHReceiverAddress, + NUM_ETH + ); + + await relayMessage(env, withdrawMessageProof!); + + const currentPeriodEndAfterRelay = + await env.eth.fuelMessagePortal.currentPeriodEnd(); + + expect(currentPeriodEndAfterRelay > currentPeriodEndBeforeRelay).to.be + .true; + + const currentPeriodAmount = + await env.eth.fuelMessagePortal.currentPeriodAmount(); + + expect(currentPeriodAmount === parseEther(NUM_ETH)).to.be.true; + }); + + it('Rate limit parameters are updated when new limit is set after the initial duration', async () => { + const deployer = await env.eth.deployer; + const newRateLimit = `40`; + + const currentWithdrawnAmountBeforeSettingLimit = + await env.eth.fuelMessagePortal.currentPeriodAmount(); + const currentPeriodEndBeforeSettingLimit = + await env.eth.fuelMessagePortal.currentPeriodEnd(); + + // fast forward time + await env.eth.provider.send('evm_increaseTime', [ + Number(rateLimitDuration) * 2, + ]); + await env.eth.provider.send('evm_mine', []); // Mine a new block + + await env.eth.fuelMessagePortal + .connect(deployer) + .resetRateLimitAmount(parseEther(newRateLimit)); + + const currentPeriodEndAfterSettingLimit = + await env.eth.fuelMessagePortal.currentPeriodEnd(); + const currentWithdrawnAmountAfterSettingLimit = + await env.eth.fuelMessagePortal.currentPeriodAmount(); + + expect( + currentPeriodEndAfterSettingLimit > currentPeriodEndBeforeSettingLimit + ).to.be.true; + + expect( + currentWithdrawnAmountBeforeSettingLimit > + currentWithdrawnAmountAfterSettingLimit + ).to.be.true; + + expect(currentWithdrawnAmountAfterSettingLimit == 0n).to.be.true; + }); + }); +}); diff --git a/packages/solidity-contracts/package.json b/packages/solidity-contracts/package.json index 70087cf7..41874449 100644 --- a/packages/solidity-contracts/package.json +++ b/packages/solidity-contracts/package.json @@ -21,14 +21,21 @@ "coverage": "pnpm run build && pnpm hardhat coverage --temp artifacts --network hardhat", "check": "pnpm solhint \"contracts/**/*.sol\"", "node": "pnpm hardhat node --network hardhat", + "test:integration": "pnpm mocha -b -r ts-node/register 'integration-tests/**/*.ts'", + "node:up": "sh ./scripts/node:up.sh", "start-mining": "pnpm hardhat run --no-compile --network localhost scripts/startAutoMining.ts", "serve-deployments": "pnpm ts-node scripts/serveDeployments.ts", "test": "pnpm hardhat test", + "upgrade:test:integration": "sh ./scripts/test.sh", "test-no-compile": "pnpm hardhat test --no-compile", "test-parallel": "pnpx mocha 'test/**/*.ts' --recursive --parallel --require hardhat/register", "prettier": "prettier --write --plugin=prettier-plugin-solidity 'contracts/**/*.sol'", "lint": "prettier --list-different --plugin=prettier-plugin-solidity 'contracts/**/*.sol'" }, + "peerDependencies": { + "@fuel-bridge/test-utils": "workspace:*", + "@fuel-bridge/fungible-token": "workspace:*" + }, "devDependencies": { "@fuel-ts/merkle": "^0.21.2", "@inquirer/prompts": "^5.3.8", @@ -54,6 +61,7 @@ "@typescript-eslint/parser": "^5.43.0", "chai": "^4.3.7", "cors": "2.8.5", + "mocha": "^10.0.0", "dotenv": "^16.0.3", "ethers": "6.13.1", "express": "^4.18.2", @@ -76,4 +84,4 @@ "typechain": "^8.3.2", "typescript": "^4.9.3" } -} +} \ No newline at end of file diff --git a/packages/solidity-contracts/scripts/node:up.sh b/packages/solidity-contracts/scripts/node:up.sh new file mode 100644 index 00000000..71620aaf --- /dev/null +++ b/packages/solidity-contracts/scripts/node:up.sh @@ -0,0 +1 @@ +docker compose -f ./docker/docker-compose.yml up -d --build \ No newline at end of file diff --git a/packages/solidity-contracts/scripts/test.sh b/packages/solidity-contracts/scripts/test.sh new file mode 100644 index 00000000..b57d53fd --- /dev/null +++ b/packages/solidity-contracts/scripts/test.sh @@ -0,0 +1,35 @@ +# Start the docker compose file with L1 and Fuel Node +echo "\n\nStarting docker..." +pnpm run node:up + +# Wait for the nodes to be ready and run the tests +HEALTH_CHECK_COUNTER=0 +HELTH_CHECK_OUTPUT="" +MAX_CHECK_ATTEMPTS=50 + +waitForNodesToBeReady() { + NODE_URL="http://localhost:4000/v1/playground"; + + printf "\rWaiting for node.${HELTH_CHECK_OUTPUT}" + + if [ $HEALTH_CHECK_COUNTER -gt $MAX_CHECK_ATTEMPTS ]; then + echo "\n\nTests failed" + exit 1 + fi + + if curl --silent --head --request GET $NODE_URL | grep "200 OK" > /dev/null; then + # If the node responds with 200, it is ready + # to run the tests. + echo "\nRun tests..." + pnpm pnpm run test:integration + else + # If the request not returns 200 the node is not ready yet + # sleep for 6 seconds before and try again. + HEALTH_CHECK_COUNTER=$((HEALTH_CHECK_COUNTER+1)) + HELTH_CHECK_OUTPUT="${HELTH_CHECK_OUTPUT}." + sleep 6 + waitForNodesToBeReady + fi +} + +waitForNodesToBeReady \ No newline at end of file diff --git a/packages/test-utils/src/utils/setup.ts b/packages/test-utils/src/utils/setup.ts index 35640936..e05d08f8 100644 --- a/packages/test-utils/src/utils/setup.ts +++ b/packages/test-utils/src/utils/setup.ts @@ -50,6 +50,7 @@ const eth_private_keys: string[] = [ const def_pk_eth_deployer: string = eth_private_keys[0]; const def_pk_eth_signer1: string = eth_private_keys[3]; const def_pk_eth_signer2: string = eth_private_keys[4]; +const def_pk_eth_signer3: string = eth_private_keys[5]; const def_pk_fuel_deployer: string = '0xde97d8624a438121b86a1956544bd72ed68cd69f2c99555b08b1e8c51ffd511c'; @@ -66,6 +67,7 @@ export interface SetupOptions { pk_eth_deployer?: string; pk_eth_signer1?: string; pk_eth_signer2?: string; + pk_eth_signer3?: string; pk_fuel_deployer?: string; pk_fuel_signer1?: string; pk_fuel_signer2?: string; @@ -109,6 +111,8 @@ export async function setupEnvironment( opts.pk_eth_signer1 || process.env.PK_ETH_SIGNER1 || def_pk_eth_signer1; const pk_eth_signer2: string = opts.pk_eth_signer2 || process.env.PK_ETH_SIGNER2 || def_pk_eth_signer2; + const pk_eth_signer3: string = + opts.pk_eth_signer3 || process.env.PK_ETH_SIGNER3 || def_pk_eth_signer3; const pk_fuel_deployer: string = opts.pk_fuel_deployer || process.env.PK_FUEL_DEPLOYER || @@ -213,6 +217,18 @@ export async function setupEnvironment( await tx.wait(); } + const eth_signer3 = new NonceManager( + new ethers.Wallet(pk_eth_signer3, eth_provider) + ); + const eth_signer3Balance = await eth_provider.getBalance(eth_signer3); + if (eth_signer3Balance < parseEther('1') && skip_deployer_balance) { + const tx = await eth_deployer.sendTransaction({ + to: eth_signer3, + value: parseEther('1'), + }); + await tx.wait(); + } + // Get contract addresses let eth_fuelChainStateAddress: string = fuel_chain_consensus_addr; let eth_fuelMessagePortalAddress: string = fuel_message_portal_addr; @@ -302,7 +318,7 @@ export async function setupEnvironment( fuelERC20Gateway: eth_fuelERC20Gateway, fuelERC721Gateway: eth_fuelERC721Gateway, deployer: eth_deployer, - signers: [eth_signer1, eth_signer2], + signers: [eth_signer1, eth_signer2, eth_signer3], }, fuel: { provider: fuel_provider, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bc55af5d..94419ef0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -127,6 +127,13 @@ importers: version: link:../solidity-contracts packages/solidity-contracts: + dependencies: + '@fuel-bridge/fungible-token': + specifier: workspace:* + version: link:../fungible-token + '@fuel-bridge/test-utils': + specifier: workspace:* + version: link:../test-utils devDependencies: '@fuel-ts/merkle': specifier: ^0.21.2 @@ -230,6 +237,9 @@ importers: markdownlint-cli: specifier: ^0.32.2 version: 0.32.2 + mocha: + specifier: ^10.0.0 + version: 10.4.0 node-fetch: specifier: ^2.6.6 version: 2.7.0 diff --git a/scripts/build.sh b/scripts/build.sh index 4e93b7d2..7fcb21b0 100644 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -1,5 +1,6 @@ #!/bin/bash forc build --release +pnpm forc fmt --path packages/message-predicates --check cargo run --bin fuel-contract-message-predicate -turbo run build +turbo run build \ No newline at end of file