diff --git a/.github/workflows/auto-deploy-contracts.yaml b/.github/workflows/auto-deploy-contracts.yaml index 5bcdea09fd..2403137bcf 100644 --- a/.github/workflows/auto-deploy-contracts.yaml +++ b/.github/workflows/auto-deploy-contracts.yaml @@ -66,7 +66,7 @@ jobs: run: | cd contracts npm install --save hardhat - output=$(make deploy-ipc NETWORK=calibrationnet) + output=$(make deploy-stack NETWORK=calibrationnet) echo "deploy_output<> $GITHUB_OUTPUT echo "$output" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT diff --git a/.github/workflows/contracts-deployment-test.yaml b/.github/workflows/contracts-deployment-test.yaml index 0e22bbb42e..ae4c8a2c11 100644 --- a/.github/workflows/contracts-deployment-test.yaml +++ b/.github/workflows/contracts-deployment-test.yaml @@ -38,4 +38,4 @@ jobs: export PATH="$PATH:/home/runner/.config/.foundry/bin" pnpm exec ganache-cli -g0 -p1337 --account 0x$PRIVATE_KEY,1001901919191919191 & sleep 5 - make deploy-ipc + make deploy-stack diff --git a/.github/workflows/contracts-pnpm-audit.yaml b/.github/workflows/contracts-pnpm-audit.yaml index 52de0f9f14..4f9913a014 100644 --- a/.github/workflows/contracts-pnpm-audit.yaml +++ b/.github/workflows/contracts-pnpm-audit.yaml @@ -21,4 +21,4 @@ jobs: - name: NPM Audit run: | cd contracts - pnpm audit + pnpm audit --audit-level=moderate diff --git a/.github/workflows/contracts-sast.yaml b/.github/workflows/contracts-sast.yaml index 17c77e9976..3141eda453 100644 --- a/.github/workflows/contracts-sast.yaml +++ b/.github/workflows/contracts-sast.yaml @@ -52,8 +52,11 @@ jobs: - name: Force an ordinary npm install run: cd contracts && rm -rf node_modules && npm install + - name: Print Aderyn version + run: aderyn --version + - name: Run aderyn - run: aderyn ./ -o report.json + run: cd contracts && aderyn ./ -o report.json - name: Check results run: cd contracts && ./tools/check_aderyn.sh @@ -78,17 +81,3 @@ jobs: run: cd contracts && pnpm exec prettier --check 'contracts/**/*.sol' 'test/*.sol' - name: Solhint check run: cd contracts && pnpm exec solhint 'contracts/**/*.sol' - - codespell: - name: Codespell check - runs-on: ubuntu-latest - if: ${{ !github.event.pull_request.draft }} - steps: - - uses: actions/checkout@v3 - - name: CodeSpell check - uses: codespell-project/actions-codespell@v2.0 - with: - check_hidden: true - check_filenames: true - path: contracts/contracts/*,contracts/script/*,contracts/scripts/*,contracts/test/* - ignore_words_file: contracts/.codespellignore diff --git a/.github/workflows/lint-pr.yaml b/.github/workflows/lint-pr.yaml index ab05793dfd..d0199bc69e 100644 --- a/.github/workflows/lint-pr.yaml +++ b/.github/workflows/lint-pr.yaml @@ -1,7 +1,7 @@ name: "Lint PR" on: - pull_request_target: + pull_request: types: - opened - edited @@ -29,6 +29,7 @@ jobs: cli contracts core + ci deps docker ethapi diff --git a/contracts/.codespellignore b/contracts/.codespellignore deleted file mode 100644 index 89df1e25f4..0000000000 --- a/contracts/.codespellignore +++ /dev/null @@ -1 +0,0 @@ -fils diff --git a/contracts/.gitignore b/contracts/.gitignore index 9dce217b9b..05b7ae9569 100644 --- a/contracts/.gitignore +++ b/contracts/.gitignore @@ -17,6 +17,7 @@ solidity-files-cache.json typechain # Deploment assets +deployments/ deployments.json scripts/*.out scripts/deploy-registry.ts diff --git a/contracts/Makefile b/contracts/Makefile index 3236575bcc..bb76e795c4 100644 --- a/contracts/Makefile +++ b/contracts/Makefile @@ -1,6 +1,5 @@ # Targets that are commands. -COMMANDS := deploy-ipc deploy-subnet-registry deploy-subnet upgrade-gw-diamond upgrade-sa-diamond \ - upgrade-sr-diamond gen compile-abi rust-binding slither lint fmt deps build \ +COMMANDS := deploy-stack gen compile-abi rust-binding slither lint fmt deps build \ test install-dev install-npm-package install-eth-abi storage clean coverage \ prepare build-selector-library forge @@ -32,23 +31,8 @@ NETWORK ?= auto # It is required by docker builds, but shouldn't be checked into git. OUTPUT ?= out -deploy-ipc: - ./ops/deploy.sh $(NETWORK) - -deploy-subnet-registry: - ./ops/deploy-subnet-registry.sh $(NETWORK) - -deploy-subnet: - ./ops/deploy-subnet.sh $(NETWORK) - -upgrade-gw-diamond: - ./ops/upgrade-gw-diamond.sh $(NETWORK) - -upgrade-sa-diamond: - ./ops/upgrade-sa-diamond.sh $(NETWORK) $(SUBNET_ACTOR_ADDRESS) - -upgrade-sr-diamond: - ./ops/upgrade-sr-diamond.sh $(NETWORK) +deploy-stack: + pnpm exec hardhat deploy-stack --network $(NETWORK) # ============================================================================== # Code generation diff --git a/contracts/extensions.ts b/contracts/extensions.ts new file mode 100644 index 0000000000..6061e6fe95 --- /dev/null +++ b/contracts/extensions.ts @@ -0,0 +1,48 @@ +import { extendProvider } from 'hardhat/config' +import '@nomiclabs/hardhat-ethers' + +const emptyBlock = { + number: '0x0', + hash: '0x0000000000000000000000000000000000000000000000000000000000000000', + parentHash: '0x0000000000000000000000000000000000000000000000000000000000000000', + mixHash: '0x0000000000000000000000000000000000000000000000000000000000000000', + nonce: '0x0000000000000000', + sha3Uncles: '0x0000000000000000000000000000000000000000000000000000000000000000', + logsBloom: '0x' + '00'.repeat(256), + transactionsRoot: '0x0000000000000000000000000000000000000000000000000000000000000000', + stateRoot: '0x0000000000000000000000000000000000000000000000000000000000000000', + receiptsRoot: '0x0000000000000000000000000000000000000000000000000000000000000000', + miner: '0x0000000000000000000000000000000000000000', + difficulty: '0x0', + totalDifficulty: '0x0', + extraData: '0x', + size: '0x0', + gasLimit: '0x0', + gasUsed: '0x0', + timestamp: '0x0', + transactions: [], + uncles: [], +} + +extendProvider((provider) => { + const interceptedRpcMethods = ['eth_getBlockByNumber', 'eth_getBlockByHash'] + + const originalProvider = provider.request.bind(provider) + provider.request = async (args) => { + try { + return await originalProvider(args) + } catch (err) { + if ( + interceptedRpcMethods.includes(args.method) && + err.message.includes('requested epoch was a null round') + ) { + console.warn(`[${args.method}] null round hit, returning empty block`) + return emptyBlock + } + console.log(`Rethrowing error ${err}`) + throw err + } + } + + return provider +}) diff --git a/contracts/hardhat.config.ts b/contracts/hardhat.config.ts index f96adacd33..1de63b39ad 100644 --- a/contracts/hardhat.config.ts +++ b/contracts/hardhat.config.ts @@ -1,280 +1,44 @@ import '@nomicfoundation/hardhat-foundry' import '@nomiclabs/hardhat-ethers' import '@typechain/hardhat' -import * as fs from 'fs' import 'hardhat-contract-sizer' -import 'hardhat-deploy' import 'hardhat-storage-layout-changes' -import { HardhatUserConfig, task } from 'hardhat/config' -import { HardhatRuntimeEnvironment } from 'hardhat/types' - -const lazyImport = async (module: any) => { - return await import(module) -} - -async function saveDeployments(env: string, deploymentData: { [key in string]: string }, branch?: string) { - const deploymentsJsonPath = `${process.cwd()}/deployments.json` - - let deploymentsJson = { [env]: {} } - if (fs.existsSync(deploymentsJsonPath)) { - deploymentsJson = JSON.parse(fs.readFileSync(deploymentsJsonPath).toString()) - } - - if (branch) { - deploymentsJson[env] = { - ...deploymentsJson[env], - [branch]: deploymentData, - } - } else { - deploymentsJson[env] = { ...deploymentsJson[env], ...deploymentData } - } - - fs.writeFileSync(deploymentsJsonPath, JSON.stringify(deploymentsJson)) -} - -async function saveDeploymentsFacets( - filename: string, - env: string, - updatedFacets: { [key: string]: string }, - branch?: string, -) { - const deploymentsJsonPath = `${process.cwd()}/${filename}` - let deploymentsJson = { [env]: {} } - if (fs.existsSync(deploymentsJsonPath)) { - deploymentsJson = JSON.parse(fs.readFileSync(deploymentsJsonPath).toString()) - } - const facets = deploymentsJson[env]['Facets'] - for (const facetIndex in facets) { - const facetName = facets[facetIndex].name - if (updatedFacets[facetName]) { - facets[facetIndex].address = updatedFacets[facetName] - } - } - fs.writeFileSync(deploymentsJsonPath, JSON.stringify(deploymentsJson)) -} -async function saveSubnetRegistry(env: string, subnetRegistryData: { [key in string]: string }) { - const subnetRegistryJsonPath = `${process.cwd()}/subnet.registry.json` - - let subnetRegistryJson = { [env]: {} } - if (fs.existsSync(subnetRegistryJsonPath)) { - subnetRegistryJson = JSON.parse(fs.readFileSync(subnetRegistryJsonPath).toString()) - } - - subnetRegistryJson[env] = { - ...subnetRegistryJson[env], - ...subnetRegistryData, - } - - fs.writeFileSync(subnetRegistryJsonPath, JSON.stringify(subnetRegistryJson)) -} - -async function readSubnetActor(subnetActorAddress, network) { - const subnetActorJsonPath = `${process.cwd()}/subnet.actor-${subnetActorAddress}.json` - if (fs.existsSync(subnetActorJsonPath)) { - const subnetActor = JSON.parse(fs.readFileSync(subnetActorJsonPath).toString()) - return subnetActor - } - const subnetRegistry = await getSubnetRegistry(network) - const deployments = { - SubnetActorDiamond: subnetActorAddress, - Facets: subnetRegistry.SubnetActorFacets, - } - return deployments -} - -async function saveSubnetActor(deployments, updatedFacets: { [key in string]: string }) { - const subnetActorJsonPath = `${process.cwd()}/subnet.actor-${deployments.SubnetActorDiamond}.json` - for (const facetIndex in deployments.Facets) { - const facetName = deployments.Facets[facetIndex].name - if (updatedFacets[facetName]) { - deployments.Facets[facetIndex].address = updatedFacets[facetName] - } - } - fs.writeFileSync(subnetActorJsonPath, JSON.stringify(deployments)) -} - -async function getSubnetRegistry(env: string): Promise<{ [key in string]: string }> { - const subnetRegistryJsonPath = `${process.cwd()}/subnet.registry.json` - - let subnetRegistry = {} - if (fs.existsSync(subnetRegistryJsonPath)) { - subnetRegistry = JSON.parse(fs.readFileSync(subnetRegistryJsonPath).toString())[env] - } - - return subnetRegistry -} - -async function getSubnetActor(env: string): Promise<{ [key in string]: string }> { - const subnetRegistryJsonPath = `${process.cwd()}/subnet.actor.json` - - let subnetRegistry = {} - if (fs.existsSync(subnetRegistryJsonPath)) { - subnetRegistry = JSON.parse(fs.readFileSync(subnetRegistryJsonPath).toString())[env] - } - - return subnetRegistry -} - -async function getDeployments(env: string): Promise<{ [key in string]: string }> { - const deploymentsJsonPath = `${process.cwd()}/deployments.json` - - let deployments = {} - if (fs.existsSync(deploymentsJsonPath)) { - deployments = JSON.parse(fs.readFileSync(deploymentsJsonPath).toString())[env] - } +import { HardhatUserConfig } from 'hardhat/config' - return deployments -} - -task( - 'deploy-libraries', - 'Build and deploys all libraries on the selected network', - async (args, hre: HardhatRuntimeEnvironment) => { - const { deploy } = await lazyImport('./scripts/deploy-libraries') - const libsDeployment = await deploy() - console.log(libsDeployment) - await saveDeployments(hre.network.name, libsDeployment, 'libs') - }, -) - -task( - 'deploy-gateway', - 'Builds and deploys the Gateway contract on the selected network', - async (args, hre: HardhatRuntimeEnvironment) => { - const network = hre.network.name - - const deployments = await getDeployments(network) - const { deploy } = await lazyImport('./scripts/deploy-gateway') - const gatewayDeployment = await deploy(deployments.libs) - - console.log(JSON.stringify(gatewayDeployment, null, 2)) - - await saveDeployments(network, gatewayDeployment) - }, -) - -task( - 'deploy-subnet-registry', - 'Builds and deploys the Registry contract on the selected network', - async (args, hre: HardhatRuntimeEnvironment) => { - const network = hre.network.name - const { deploy } = await lazyImport('./scripts/deploy-registry') - const subnetRegistryDeployment = await deploy() - - console.log(JSON.stringify(subnetRegistryDeployment, null, 2)) - - await saveSubnetRegistry(network, subnetRegistryDeployment) - }, -) - -task( - 'deploy-gw-diamond-and-facets', - 'Builds and deploys Gateway Actor diamond and its facets', - async (args, hre: HardhatRuntimeEnvironment) => { - const network = hre.network.name - const deployments = await getDeployments(network) - const { deployDiamond } = await lazyImport('./scripts/deploy-gw-diamond') - const gatewayActorDiamond = await deployDiamond(deployments.libs) - await saveDeployments(network, gatewayActorDiamond) - }, -) - -task( - 'deploy', - 'Builds and deploys all contracts on the selected network', - async (args, hre: HardhatRuntimeEnvironment) => { - await hre.run('compile') - await hre.run('deploy-libraries') - await hre.run('deploy-gateway') - }, -) - -task('deploy-gw-diamond', 'Builds and deploys Gateway Actor diamond', async (args, hre: HardhatRuntimeEnvironment) => { - await hre.run('compile') - await hre.run('deploy-libraries') - await hre.run('deploy-gw-diamond-and-facets') -}) - -task('deploy-sa-diamond', 'Builds and deploys Subnet Actor diamond', async (args, hre: HardhatRuntimeEnvironment) => { - await hre.run('compile') - await hre.run('deploy-libraries') - await hre.run('deploy-sa-diamond-and-facets') +// Hardhat deploy stuff. +import 'hardhat-deploy' +import 'hardhat-deploy-ethers' + +// Import our extensions. +import './extensions' + +// Load environment variables from .env file. +import { config as dotenvConfig } from 'dotenv' +dotenvConfig({ path: './.env' }) + +// Import our tasks. +import './tasks' + +// Define network configurations. +const networkDefinition = (chainId: number, url: string) => ({ + chainId, + url: url, + accounts: [process.env.PRIVATE_KEY!], + // timeout to support also slow networks (like calibration/mainnet) + timeout: 1000000, + saveDeployments: true, }) -task( - 'upgrade-gw-diamond', - 'Upgrades IPC Gateway Actor Diamond Facets on an EVM-compatible subnet using hardhat', - async (args, hre: HardhatRuntimeEnvironment) => { - await hre.run('compile') - const network = hre.network.name - const deployments = await getDeployments(network) - const { upgradeDiamond } = await lazyImport('./scripts/upgrade-gw-diamond') - const updatedFacets = await upgradeDiamond(deployments) - await saveDeploymentsFacets('deployments.json', network, updatedFacets) - }, -) - -task( - 'upgrade-sr-diamond', - 'Upgrades IPC Subnet Registry Diamond Facets on an EVM-compatible subnet using hardhat', - async (args, hre: HardhatRuntimeEnvironment) => { - await hre.run('compile') - const network = hre.network.name - const subnetRegistry = await getSubnetRegistry(network) - const { upgradeDiamond } = await lazyImport('./scripts/upgrade-sr-diamond') - const updatedFacets = await upgradeDiamond(subnetRegistry) - await saveDeploymentsFacets('subnet.registry.json', network, updatedFacets) - }, -) - -task( - 'upgrade-sa-diamond', - 'Upgrades IPC Subnet Actor Diamond Facets on an EVM-compatible subnet using hardhat', - async (args, hre: HardhatRuntimeEnvironment) => { - await hre.run('compile') - const network = hre.network.name - if (!args.address) { - console.error('No address provided. Usage: pnpm exec hardhat upgrade-sa-diamond --address 0x80afa...') - process.exit(1) - } - - const deployments = await readSubnetActor(args.address, network) - - const { upgradeDiamond } = await lazyImport('./scripts/upgrade-sa-diamond') - const updatedFacets = await upgradeDiamond(deployments) - await saveSubnetActor(deployments, updatedFacets) - }, -).addParam('address', 'The address to upgrade', undefined, types.string, false) - -/** @type import('hardhat/config').HardhatUserConfig */ const config: HardhatUserConfig = { defaultNetwork: 'calibrationnet', networks: { - mainnet: { - chainId: 314, - url: process.env.RPC_URL!, - accounts: [process.env.PRIVATE_KEY!], - timeout: 1000000, - }, - calibrationnet: { - chainId: 314159, - url: process.env.RPC_URL!, - accounts: [process.env.PRIVATE_KEY!], - timeout: 1000000, - }, - localnet: { - chainId: 31415926, - url: process.env.RPC_URL!, - accounts: [process.env.PRIVATE_KEY!], - }, - // automatically fetch chainID for network - auto: { - chainId: parseInt(process.env.CHAIN_ID!, 16), - url: process.env.RPC_URL!, - accounts: [process.env.PRIVATE_KEY!], - // timeout to support also slow networks (like calibration/mainnet) - timeout: 1000000, - }, + // Static networks. + mainnet: networkDefinition(314, 'https://api.node.glif.io/rpc/v1'), + calibrationnet: networkDefinition(314159, 'https://api.calibration.node.glif.io/rpc/v1'), + localnet: networkDefinition(31415926, 'http://localhost:8545'), + // Auto uses RPC_URL provided by the user, and an optional CHAIN_ID. + // If provided, Hardhat will assert that the chain ID matches the one returned by the RPC. + auto: networkDefinition(parseInt(process.env.CHAIN_ID, 10), process.env.RPC_URL!), }, solidity: { compilers: [ diff --git a/contracts/ops/deploy-subnet-registry.sh b/contracts/ops/deploy-subnet-registry.sh deleted file mode 100755 index 4e74e6d63d..0000000000 --- a/contracts/ops/deploy-subnet-registry.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash -# Upgrades IPC Gateway Diamond Facets on an EVM-compatible subnet using hardhat -set -eu -set -o pipefail - -if [ $# -ne 1 ] -then - echo "Expected a single argument with the name of the network to deploy (localnet, calibrationnet, mainnet)" - exit 1 -fi - -NETWORK=$1 - -if [ "$NETWORK" = "auto" ]; then - echo "[*] Automatically getting chainID for network" - source ops/chain-id.sh -fi - - -pnpm exec hardhat deploy-subnet-registry --network ${NETWORK} diff --git a/contracts/ops/deploy-subnet.sh b/contracts/ops/deploy-subnet.sh deleted file mode 100755 index c727b02212..0000000000 --- a/contracts/ops/deploy-subnet.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash -# Upgrades IPC Gateway Diamond Facets on an EVM-compatible subnet using hardhat -set -eu -set -o pipefail - -if [ $# -ne 1 ] -then - echo "Expected a single argument with the name of the network to deploy (localnet, calibrationnet, mainnet)" - exit 1 -fi - -NETWORK=$1 - -if [ "$NETWORK" = "auto" ]; then - echo "[*] Automatically getting chainID for network" - source ops/chain-id.sh -fi - - -pnpm exec hardhat deploy-sa-diamond-and-facets --network ${NETWORK} - diff --git a/contracts/ops/deploy.sh b/contracts/ops/deploy.sh deleted file mode 100755 index 66e95afec3..0000000000 --- a/contracts/ops/deploy.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/bash -# Deploys IPC on an EVM-compatible subnet using hardhat -set -eu -set -o pipefail - -if [ $# -ne 1 ] -then - echo "Expected a single argument with the name of the network to deploy (localnet, calibrationnet, mainnet)" - exit 1 -fi - -if [ -f .env ]; then - source .env -fi - - -LIB_OUTPUT="libraries.out" -GATEWAY_OUTPUT="gateway.out" -NETWORK=$1 - -if [ "$NETWORK" = "auto" ]; then - echo "[*] Automatically getting chainID for network" - source ops/chain-id.sh -fi - -echo "[*] Deploying libraries" -(pnpm exec hardhat deploy-libraries --network ${NETWORK} | sed -n '/{/,/}/p') > scripts/${LIB_OUTPUT} -echo "const LIBMAP =" | cat - scripts/${LIB_OUTPUT} > temp && mv temp scripts/${LIB_OUTPUT} -echo "[*] Output libraries available in $PWD/scripts/${LIB_OUTPUT}" - -echo "[*] Populating deploy-gateway script" -cat scripts/${LIB_OUTPUT} | cat - scripts/deploy-gateway.template.ts > temp && mv temp scripts/deploy-gateway.ts -echo "[*] Gateway script in $PWD/scripts/deploy-gateway.ts" -(pnpm exec hardhat deploy-gateway --network ${NETWORK} | sed '/^[a-zA-Z]/d' ) > scripts/${GATEWAY_OUTPUT} -echo "[*] Gateway deployed: " | cat - scripts/${GATEWAY_OUTPUT} -echo "const GATEWAY =" | cat - scripts/${GATEWAY_OUTPUT} > temp && mv temp scripts/${GATEWAY_OUTPUT} -echo "[*] Output gateway address in $PWD/scripts/${GATEWAY_OUTPUT}" - -echo "[*] Populating deploy-registry script" -cat scripts/${LIB_OUTPUT} | sed '/IpcMsgHelper/d' | cat - scripts/deploy-registry.template.ts > temp && mv temp scripts/deploy-registry.ts -cat scripts/${GATEWAY_OUTPUT} | cat - scripts/deploy-registry.ts > temp && mv temp scripts/deploy-registry.ts -echo "[*] Registry script in $PWD/scripts/deploy-registry.ts" -pnpm exec hardhat deploy-subnet-registry --network ${NETWORK} -echo "[*] IPC actors successfully deployed" diff --git a/contracts/ops/upgrade-gw-diamond.sh b/contracts/ops/upgrade-gw-diamond.sh deleted file mode 100755 index 5f37363a8b..0000000000 --- a/contracts/ops/upgrade-gw-diamond.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash -# Upgrades IPC Gateway Diamond Facets on an EVM-compatible subnet using hardhat -set -eu -set -o pipefail - -if [ $# -ne 1 ] -then - echo "Expected a single argument with the name of the network to deploy (localnet, calibrationnet, mainnet)" - exit 1 -fi - -NETWORK="$1" - -if [ "$NETWORK" = "auto" ]; then - echo "[*] Automatically getting chainID for network" - source ops/chain-id.sh -fi - - -pnpm exec hardhat upgrade-gw-diamond --network "${NETWORK}" diff --git a/contracts/ops/upgrade-sa-diamond.sh b/contracts/ops/upgrade-sa-diamond.sh deleted file mode 100755 index 9fad578db0..0000000000 --- a/contracts/ops/upgrade-sa-diamond.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash -# Upgrades IPC Subnet Actor Diamond Facets on an EVM-compatible subnet using hardhat -set -eu -set -o pipefail - -if [ $# -ne 2 ] -then - echo "Expected an argument with the name of the network to deploy (localnet, calibrationnet, mainnet) followed by an argument for the Subnet actor address to upgrade" - exit 1 -fi - -NETWORK="$1" -SUBNET_ACTOR_ADDRESS="$2" - -if [ "$NETWORK" = "auto" ]; then - echo "[*] Automatically getting chainID for network" - source ops/chain-id.sh -fi - - -pnpm exec hardhat upgrade-sa-diamond --network "${NETWORK}" --address "$SUBNET_ACTOR_ADDRESS" diff --git a/contracts/ops/upgrade-sr-diamond.sh b/contracts/ops/upgrade-sr-diamond.sh deleted file mode 100755 index e855f59d70..0000000000 --- a/contracts/ops/upgrade-sr-diamond.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash -# Upgrades IPC Subnet Registry Diamond Facets on an EVM-compatible subnet using hardhat -set -eu -set -o pipefail - -if [ $# -ne 1 ] -then - echo "Expected a single argument with the name of the network to deploy (localnet, calibrationnet, mainnet)" - exit 1 -fi - -NETWORK="$1" - -if [ "$NETWORK" = "auto" ]; then - echo "[*] Automatically getting chainID for network" - source ops/chain-id.sh -fi - - -pnpm exec hardhat upgrade-sr-diamond --network "${NETWORK}" diff --git a/contracts/package.json b/contracts/package.json index 9271eb86cf..882de07980 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -36,6 +36,7 @@ "@nomiclabs/hardhat-ethers": "^2.2.3", "@typechain/ethers-v5": "^11.1.2", "@typechain/hardhat": "^7.0.0", + "@types/lodash": "^4.17.7", "dotenv": "^16.0.1", "ethers": "^5.7.0", "fs-extra": "^11.2.0", @@ -62,7 +63,8 @@ "@solidity-parser/parser": "^0.18.0", "diamond-util": "^1.1.1", "elliptic-curve-solidity": "github:witnet/elliptic-curve-solidity#3475478", - "fevmate": "github:wadealexc/fevmate#6a80e98" + "fevmate": "github:wadealexc/fevmate#6a80e98", + "lodash": "^4.17.21" }, "lint-staged": { "*.{js,jsx,ts,tsx,json,css,md,sol}": "prettier --write" diff --git a/contracts/scripts/README.md b/contracts/scripts/README.md deleted file mode 100644 index c1e9c3eaef..0000000000 --- a/contracts/scripts/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# Before deploying: - -- Copy `.env.template` to `.env`. -- In `.env`, fill in your own values for private key and RPC URL for the target network (e.g. for Calibrationnet). -- Install `pnpm`: `npm install -g pnpm`. - -# To deploy everything run: - -```bash -pnpm exec hardhat deploy -``` - -## To deploy only the libraries: - -```bash -pnpm exec hardhat deploy-libraries -``` - -## To deploy only the Gateway: - -```bash -pnpm exec hardhat deploy-gateway -``` - -## To deploy only the Gateway Actor: - -```bash -pnpm exec hardhat deploy-gateway -``` - -## To deploy only the Registry: - -```bash -pnpm exec hardhat run scripts/deploy-registry.ts -``` diff --git a/contracts/scripts/deploy-gateway.template.ts b/contracts/scripts/deploy-gateway.template.ts deleted file mode 100644 index 169c7d63b4..0000000000 --- a/contracts/scripts/deploy-gateway.template.ts +++ /dev/null @@ -1,131 +0,0 @@ -/* global ethers */ - -/* eslint prefer-const: "off" */ -import { deployContractWithDeployer, getTransactionFees } from './util' -import { ethers } from 'hardhat' - -const { getSelectors, FacetCutAction } = require('./js/diamond.js') - -function getGitCommitSha(): string { - const commitSha = require('child_process').execSync('git rev-parse --short HEAD').toString().trim() - return commitSha -} -export async function deploy(libs: { [key in string]: string }) { - if (!libs || Object.keys(libs).length === 0) throw new Error(`Libraries are missing`) - - // choose chain ID according to the network in - // environmental variable - let chainId = 31415926 - if (process.env.NETWORK == 'calibrationnet') { - chainId = 314159 - } else if (process.env.NETWORK == 'mainnet') { - chainId = 314 - } else if (process.env.NETWORK == 'auto') { - chainId = parseInt(process.env.CHAIN_ID!, 16) - } - - await hre.run('compile') - - const [deployer] = await ethers.getSigners() - const balance = await ethers.provider.getBalance(deployer.address) - console.log('Deploying gateway with the account:', deployer.address, ' balance:', ethers.utils.formatEther(balance)) - - const txArgs = await getTransactionFees() - - const facetCuts = [] - - type Libraries = { - [libraryName: string]: string - } - - const getterFacetLibs: Libraries = { - SubnetIDHelper: libs['SubnetIDHelper'], - LibQuorum: libs['LibQuorum'], - } - - const managerFacetLibs: Libraries = { - CrossMsgHelper: libs['CrossMsgHelper'], - SubnetIDHelper: libs['SubnetIDHelper'], - } - const messengerFacetLibs: Libraries = { - SubnetIDHelper: libs['SubnetIDHelper'], - CrossMsgHelper: libs['CrossMsgHelper'], - } - - const checkpointingFacetLibs: Libraries = { - AccountHelper: libs['AccountHelper'], - SubnetIDHelper: libs['SubnetIDHelper'], - CrossMsgHelper: libs['CrossMsgHelper'], - } - - const xnetMessagingFacetLibs: Libraries = { - AccountHelper: libs['AccountHelper'], - CrossMsgHelper: libs['CrossMsgHelper'], - SubnetIDHelper: libs['SubnetIDHelper'], - } - - const topDownFinalityFacetLibs: Libraries = { - AccountHelper: libs['AccountHelper'], - } - - const facets = [ - { name: 'GatewayGetterFacet', libs: getterFacetLibs }, - { name: 'DiamondLoupeFacet', libs: {} }, - { name: 'DiamondCutFacet', libs: {} }, - { name: 'GatewayManagerFacet', libs: managerFacetLibs }, - { name: 'GatewayMessengerFacet', libs: messengerFacetLibs }, - { - name: 'CheckpointingFacet', - libs: checkpointingFacetLibs, - }, - { - name: 'XnetMessagingFacet', - libs: xnetMessagingFacetLibs, - }, - { name: 'TopDownFinalityFacet', libs: topDownFinalityFacetLibs }, - { name: 'OwnershipFacet', libs: {} }, - ] - - for (const facet of facets) { - const facetInstance = await deployContractWithDeployer(deployer, facet.name, facet.libs, txArgs) - await facetInstance.deployed() - - facet.address = facetInstance.address - - facetCuts.push({ - facetAddress: facetInstance.address, - action: FacetCutAction.Add, - functionSelectors: getSelectors(facetInstance), - }) - } - - const gatewayConstructorParams = { - bottomUpCheckPeriod: 10, - activeValidatorsLimit: 100, - majorityPercentage: 66, - networkName: { - root: chainId, - route: [], - }, - genesisValidators: [], - commitSha: ethers.utils.formatBytes32String(getGitCommitSha()), - } - - const diamondLibs: Libraries = {} - // deploy Diamond - const { address: gatewayAddress } = await deployContractWithDeployer( - deployer, - 'GatewayDiamond', - diamondLibs, - facetCuts, - gatewayConstructorParams, - txArgs, - ) - - // returning the address of the diamond - return { - ChainID: chainId, - Gateway: gatewayAddress, - Facets: facets, - } -} diff --git a/contracts/scripts/deploy-libraries.ts b/contracts/scripts/deploy-libraries.ts deleted file mode 100644 index 946c179bac..0000000000 --- a/contracts/scripts/deploy-libraries.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* global ethers */ - -/* eslint prefer-const: "off" */ -import { deployContractWithDeployer, getTransactionFees } from './util' -import { ethers } from 'hardhat' - -export async function deploy() { - await hre.run('compile') - - const [deployer] = await ethers.getSigners() - const balance = await ethers.provider.getBalance(deployer.address) - console.log( - 'Deploying libraries with the account:', - deployer.address, - ' balance:', - ethers.utils.formatEther(balance), - ) - - const txArgs = await getTransactionFees() - - const { address: accountHelperAddress } = await deployContractWithDeployer(deployer, 'AccountHelper', {}, txArgs) - const { address: libStakingAddress } = await deployContractWithDeployer(deployer, 'LibStaking', {}, txArgs) - - const { address: subnetIDHelperAddress } = await deployContractWithDeployer(deployer, 'SubnetIDHelper', {}, txArgs) - - const { address: libQuorumAddress } = await deployContractWithDeployer(deployer, 'LibQuorum', {}, txArgs) - - // nested libs - const { address: crossMsgHelperAddress } = await deployContractWithDeployer( - deployer, - 'CrossMsgHelper', - { SubnetIDHelper: subnetIDHelperAddress }, - txArgs, - ) - - return { - AccountHelper: accountHelperAddress, - SubnetIDHelper: subnetIDHelperAddress, - CrossMsgHelper: crossMsgHelperAddress, - LibStaking: libStakingAddress, - LibQuorum: libQuorumAddress, - } -} - -// deploy(); -// We recommend this pattern to be able to use async/await everywhere -// and properly handle errors. -if (require.main === module) { - deploy() - .then(() => process.exit(0)) - .catch((error: Error) => { - console.error(error) - process.exit(1) - }) -} diff --git a/contracts/scripts/deploy-registry.template.ts b/contracts/scripts/deploy-registry.template.ts deleted file mode 100644 index 883ec5fa0e..0000000000 --- a/contracts/scripts/deploy-registry.template.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { deployContractWithDeployer, getTransactionFees, subnetCreationPrivileges } from './util' -import { ethers } from 'hardhat' - -const { getSelectors, FacetCutAction } = require('./js/diamond.js') - -export async function deploy() { - const [deployer] = await ethers.getSigners() - const balance = await deployer.getBalance() - - console.log(`Deploying contracts with account: ${deployer.address} and balance: ${balance.toString()}`) - - const mode = subnetCreationPrivileges() - console.log( - ` - *************************************************************** - ** ** - ** Subnet creation privileges: ${mode} ** - ** ** - *************************************************************** - `, - ) - - const gatewayAddress = GATEWAY.Gateway - const txArgs = await getTransactionFees() - - const subnetActorDeployFacets = [] - - // deploy - const getterFacet = await deployContractWithDeployer( - deployer, - 'SubnetActorGetterFacet', - { - SubnetIDHelper: LIBMAP['SubnetIDHelper'], - }, - txArgs, - ) - const getterSelectors = getSelectors(getterFacet) - - subnetActorDeployFacets.push({ - name: 'SubnetActorGetterFacet', - libs: { - SubnetIDHelper: LIBMAP['SubnetIDHelper'], - }, - address: getterFacet.address, - }) - - const managerFacet = await deployContractWithDeployer(deployer, 'SubnetActorManagerFacet', {}, txArgs) - const managerSelectors = getSelectors(managerFacet) - - subnetActorDeployFacets.push({ - name: 'SubnetActorManagerFacet', - libs: {}, - address: managerFacet.address, - }) - - const pauserFacet = await deployContractWithDeployer(deployer, 'SubnetActorPauseFacet', {}, txArgs) - const pauserSelectors = getSelectors(pauserFacet) - - subnetActorDeployFacets.push({ - name: 'SubnetActorPauseFacet', - libs: {}, - address: pauserFacet.address, - }) - - const rewarderFacet = await deployContractWithDeployer(deployer, 'SubnetActorRewardFacet', {}, txArgs) - const rewarderSelectors = getSelectors(rewarderFacet) - subnetActorDeployFacets.push({ - name: 'SubnetActorRewardFacet', - libs: {}, - address: rewarderFacet.address, - }) - - const checkpointerFacet = await deployContractWithDeployer(deployer, 'SubnetActorCheckpointingFacet', {}, txArgs) - const checkpointerSelectors = getSelectors(checkpointerFacet) - subnetActorDeployFacets.push({ - name: 'SubnetActorCheckpointingFacet', - libs: {}, - address: checkpointerFacet.address, - }) - - const diamondCutFacet = await deployContractWithDeployer(deployer, 'DiamondCutFacet', {}, txArgs) - const diamondCutSelectors = getSelectors(diamondCutFacet) - subnetActorDeployFacets.push({ - name: 'DiamondCutFacet', - libs: {}, - address: diamondCutFacet.address, - }) - - const diamondLoupeFacet = await deployContractWithDeployer(deployer, 'DiamondLoupeFacet', {}, txArgs) - const diamondLoupeSelectors = getSelectors(diamondLoupeFacet) - subnetActorDeployFacets.push({ - name: 'DiamondLoupeFacet', - libs: {}, - address: diamondLoupeFacet.address, - }) - - const ownershipFacet = await deployContractWithDeployer(deployer, 'OwnershipFacet', {}, txArgs) - const ownershipSelectors = getSelectors(ownershipFacet) - subnetActorDeployFacets.push({ - name: 'OwnershipFacet', - libs: {}, - address: ownershipFacet.address, - }) - - //deploy subnet registry diamond - const registry = await ethers.getContractFactory('SubnetRegistryDiamond', { - signer: deployer, - }) - - const registryConstructorParams = { - gateway: gatewayAddress, - getterFacet: getterFacet.address, - managerFacet: managerFacet.address, - rewarderFacet: rewarderFacet.address, - checkpointerFacet: checkpointerFacet.address, - pauserFacet: pauserFacet.address, - diamondCutFacet: diamondCutFacet.address, - diamondLoupeFacet: diamondLoupeFacet.address, - ownershipFacet: ownershipFacet.address, - subnetActorGetterSelectors: getterSelectors, - subnetActorManagerSelectors: managerSelectors, - subnetActorRewarderSelectors: rewarderSelectors, - subnetActorCheckpointerSelectors: checkpointerSelectors, - subnetActorPauserSelectors: pauserSelectors, - subnetActorDiamondCutSelectors: diamondCutSelectors, - subnetActorDiamondLoupeSelectors: diamondLoupeSelectors, - subnetActorOwnershipSelectors: ownershipSelectors, - creationPrivileges: Number(mode), - } - - const facetCuts = [] //TODO - - const facets = [ - { - name: 'RegisterSubnetFacet', - libs: { - SubnetIDHelper: LIBMAP['SubnetIDHelper'], - }, - }, - { name: 'SubnetGetterFacet', libs: {} }, - { name: 'DiamondLoupeFacet', libs: {} }, - { name: 'DiamondCutFacet', libs: {} }, - { name: 'OwnershipFacet', libs: {} }, - ] - - for (const facet of facets) { - const facetInstance = await deployContractWithDeployer(deployer, facet.name, facet.libs, txArgs) - await facetInstance.deployed() - - facet.address = facetInstance.address - - facetCuts.push({ - facetAddress: facetInstance.address, - action: FacetCutAction.Add, - functionSelectors: getSelectors(facetInstance), - }) - } - - const diamondLibs = { - SubnetIDHelper: LIBMAP['SubnetIDHelper'], - } - // deploy Diamond - const { address: subnetRegistryAddress } = await deployContractWithDeployer( - deployer, - 'SubnetRegistryDiamond', - {}, - facetCuts, - registryConstructorParams, - txArgs, - ) - - return { - SubnetRegistry: subnetRegistryAddress, - Facets: facets, - SubnetActorFacets: subnetActorDeployFacets, - } -} diff --git a/contracts/scripts/deploy-sa-diamond.ts b/contracts/scripts/deploy-sa-diamond.ts deleted file mode 100644 index 19d5f639fc..0000000000 --- a/contracts/scripts/deploy-sa-diamond.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { deployContractWithDeployer, getTransactionFees } from './util' -import hre, { ethers } from 'hardhat' - -const { getSelectors, FacetCutAction } = require('./js/diamond.js') - -async function deploySubnetActorDiamond(gatewayDiamondAddress: string, libs: { [key in string]: string }) { - if (!gatewayDiamondAddress) throw new Error(`Gateway is missing`) - if (!libs || Object.keys(libs).length === 0) throw new Error(`Libraries are missing`) - - console.log('Deploying Subnet Actor diamond with libraries:', libs) - - await hre.run('compile') - - const [deployer] = await ethers.getSigners() - const txArgs = await getTransactionFees() - - type Libraries = { - [libraryName: string]: string - } - - const getterFacetLibs: Libraries = { - SubnetIDHelper: libs['SubnetIDHelper'], - } - - const managerFacetLibs: Libraries = {} - - const rewarderFacetLibs: Libraries = {} - - const pauserFacetLibs: Libraries = {} - - const checkpointerFacetLibs: Libraries = {} - - const facets = [ - { name: 'DiamondLoupeFacet', libs: {} }, - { name: 'DiamondCutFacet', libs: {} }, - { name: 'SubnetActorGetterFacet', libs: getterFacetLibs }, - { name: 'SubnetActorManagerFacet', libs: managerFacetLibs }, - { name: 'SubnetActorRewardFacet', libs: rewarderFacetLibs }, - { name: 'SubnetActorCheckpointingFacet', libs: checkpointerFacetLibs }, - { name: 'SubnetActorPauseFacet', libs: pauserFacetLibs }, - { name: 'OwnershipFacet', libs: {} }, - ] - // The `facetCuts` variable is the FacetCut[] that contains the functions to add during diamond deployment - const facetCuts = [] - - for (const facet of facets) { - const facetInstance = await deployContractWithDeployer(deployer, facet.name, facet.libs, txArgs) - await facetInstance.deployed() - - facet.address = facetInstance.address - - facetCuts.push({ - facetAddress: facetInstance.address, - action: FacetCutAction.Add, - functionSelectors: getSelectors(facetInstance), - }) - } - - const gatewayGetterFacet = await ethers.getContractAt('GatewayGetterFacet', gatewayDiamondAddress) - const parentId = await gatewayGetterFacet.getNetworkName() - console.log('parentId', parentId[0]) - console.log('parentId', parentId[1]) - - const constructorParams = { - parentId, - ipcGatewayAddr: gatewayDiamondAddress, - consensus: 0, - minActivationCollateral: ethers.utils.parseEther('1'), - minValidators: 3, - bottomUpCheckPeriod: 10, - majorityPercentage: 66, - activeValidatorsLimit: 100, - minCrossMsgFee: 1, - powerScale: 1, - } - - console.log('constructorParams', constructorParams) - - const diamondLibs: Libraries = { - SubnetIDHelper: libs['SubnetIDHelper'], - } - - // deploy Diamond - const { address: diamondAddress } = await deployContractWithDeployer( - deployer, - 'SubnetActorDiamond', - diamondLibs, - facetCuts, - constructorParams, - txArgs, - ) - - console.log('Subnet Actor Diamond address:', diamondAddress) - - // returning the address of the diamond - return { - SubnetActorDiamond: diamondAddress, - Facets: facets, - } -} - -// We recommend this pattern to be able to use async/await everywhere -// and properly handle errors. -if (require.main === module) { - deploySubnetActorDiamond() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error) - process.exit(1) - }) -} - -exports.deployDiamond = deploySubnetActorDiamond diff --git a/contracts/scripts/js/diamond.js b/contracts/scripts/js/diamond.js deleted file mode 100644 index df1c979677..0000000000 --- a/contracts/scripts/js/diamond.js +++ /dev/null @@ -1,82 +0,0 @@ -/* global ethers */ - -const FacetCutAction = { Add: 0, Replace: 1, Remove: 2 } - -// get function selectors from ABI -function getSelectors(contract) { - const signatures = Object.keys(contract.interface.functions) - const selectors = signatures.reduce((acc, val) => { - if (val !== 'init(bytes)') { - acc.push(contract.interface.getSighash(val)) - } - return acc - }, []) - selectors.contract = contract - selectors.remove = remove - selectors.get = get - return selectors -} - -// get function selector from function signature -function getSelector(func) { - const abiInterface = new ethers.utils.Interface([func]) - return abiInterface.getSighash(ethers.utils.Fragment.from(func)) -} - -// used with getSelectors to remove selectors from an array of selectors -// functionNames argument is an array of function signatures -function remove(functionNames) { - const selectors = this.filter((v) => { - for (const functionName of functionNames) { - if (v === this.contract.interface.getSighash(functionName)) { - return false - } - } - return true - }) - selectors.contract = this.contract - selectors.remove = this.remove - selectors.get = this.get - return selectors -} - -// used with getSelectors to get selectors from an array of selectors -// functionNames argument is an array of function signatures -function get(functionNames) { - const selectors = this.filter((v) => { - for (const functionName of functionNames) { - if (v === this.contract.interface.getSighash(functionName)) { - return true - } - } - return false - }) - selectors.contract = this.contract - selectors.remove = this.remove - selectors.get = this.get - return selectors -} - -// remove selectors using an array of signatures -function removeSelectors(selectors, signatures) { - const iface = new ethers.utils.Interface(signatures.map((v) => 'function ' + v)) - const removeSelectors = signatures.map((v) => iface.getSighash(v)) - selectors = selectors.filter((v) => !removeSelectors.includes(v)) - return selectors -} - -// find a particular address position in the return value of diamondLoupeFacet.facets() -function findAddressPositionInFacets(facetAddress, facets) { - for (let i = 0; i < facets.length; i++) { - if (facets[i].facetAddress === facetAddress) { - return i - } - } -} - -exports.getSelectors = getSelectors -exports.getSelector = getSelector -exports.FacetCutAction = FacetCutAction -exports.remove = remove -exports.removeSelectors = removeSelectors -exports.findAddressPositionInFacets = findAddressPositionInFacets diff --git a/contracts/scripts/upgrade-gw-diamond.ts b/contracts/scripts/upgrade-gw-diamond.ts deleted file mode 100644 index 771c58da20..0000000000 --- a/contracts/scripts/upgrade-gw-diamond.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { ethers } from 'hardhat' -import { - getFacets, - getBytecodeFromFacet, - getOnChainBytecodeFromFacets, - upgradeFacetOnChain, - upgradeFacet, - logMissingFacetInfo, -} from './util' - -/** - * Upgrade the Gateway Actor Diamond. - * @param deployments - The deployment data. - * @returns An object of updated facets. - */ -async function upgradeGatewayActorDiamond(deployments) { - const gatewayDiamondAddress = deployments.Gateway - - const onChainFacets = await getFacets(gatewayDiamondAddress) - const updatedFacets = {} - const onChainFacetBytecodes = await getOnChainBytecodeFromFacets(onChainFacets) - - for (const facet of deployments.Facets) { - await upgradeFacet( - facet, - onChainFacets, - gatewayDiamondAddress, - updatedFacets, - onChainFacetBytecodes, - deployments, - ) - } - - return updatedFacets -} - -export { upgradeGatewayActorDiamond as upgradeDiamond } diff --git a/contracts/scripts/upgrade-sa-diamond.ts b/contracts/scripts/upgrade-sa-diamond.ts deleted file mode 100644 index 221a8afd98..0000000000 --- a/contracts/scripts/upgrade-sa-diamond.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { ethers } from 'hardhat' -import { - getFacets, - getBytecodeFromFacet, - getOnChainBytecodeFromFacets, - upgradeFacetOnChain, - upgradeFacet, - logMissingFacetInfo, -} from './util' - -/** - * Upgrade the Subnet Actor Diamond. - * @param deployments - The deployment data. - * @returns An object of updated facets. - */ -async function upgradeSubnetActorDiamond(deployments) { - const subnetActorDiamondAddress = deployments.SubnetActorDiamond - - const onChainFacets = await getFacets(subnetActorDiamondAddress) - - const updatedFacets = {} - const onChainFacetBytecodes = await getOnChainBytecodeFromFacets(onChainFacets) - - for (const facet of deployments.Facets) { - await upgradeFacet( - facet, - onChainFacets, - subnetActorDiamondAddress, - updatedFacets, - onChainFacetBytecodes, - deployments, - ) - } - - return updatedFacets -} - -export { upgradeSubnetActorDiamond as upgradeDiamond } diff --git a/contracts/scripts/upgrade-sr-diamond.ts b/contracts/scripts/upgrade-sr-diamond.ts deleted file mode 100644 index 2ded86d34a..0000000000 --- a/contracts/scripts/upgrade-sr-diamond.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { ethers } from 'hardhat' -import { - getFacets, - getBytecodeFromFacet, - getOnChainBytecodeFromFacets, - upgradeFacetOnChain, - upgradeFacet, - logMissingFacetInfo, -} from './util' - -/** - * Upgrade the Subnet Registry Diamond. - * @param deployments - The deployment data. - * @returns An object of updated facets. - */ -async function upgradeSubnetRegistryDiamond(deployments) { - const subnetRegistryDiamondAddress = deployments.SubnetRegistry - - const onChainFacets = await getFacets(subnetRegistryDiamondAddress) - const updatedFacets = {} - const onChainFacetBytecodes = await getOnChainBytecodeFromFacets(onChainFacets) - - for (const facet of deployments.Facets) { - await upgradeFacet( - facet, - onChainFacets, - subnetRegistryDiamondAddress, - updatedFacets, - onChainFacetBytecodes, - deployments, - ) - } - - return updatedFacets -} - -export { upgradeSubnetRegistryDiamond as upgradeDiamond } diff --git a/contracts/scripts/util.ts b/contracts/scripts/util.ts deleted file mode 100644 index 78f0b9dfad..0000000000 --- a/contracts/scripts/util.ts +++ /dev/null @@ -1,397 +0,0 @@ -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { providers, Wallet, ContractFactory, Contract } from 'ethers' -import ganache from 'ganache' -import { ethers } from 'hardhat' -import * as linker from 'solc/linker' - -const { getSelectors, FacetCutAction } = require('./js/diamond.js') -const fs = require('fs') -const path = require('path') - -function findFileInDir(filename, folder) { - // This function checks each entry in the directory: if it's a file matching the filename, it returns its path. - // If it's a directory, the function recurses into it. - function searchDirectory(currentPath) { - const entries = fs.readdirSync(currentPath, { withFileTypes: true }) - - for (let entry of entries) { - const entryPath = path.join(currentPath, entry.name) - - if (entry.isDirectory()) { - const result = searchDirectory(entryPath) - if (result) return result - } else if (entry.isFile() && entry.name === filename) { - return entryPath - } - } - - // If no file is found, return null. - return null - } - - return searchDirectory(folder) -} - -export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' - -const isolatedPort = 18678 - -export enum SubnetCreationPrivileges { - Unrestricted = 0, - Owner = 1, -} - -export async function deployContractWithDeployer( - deployer: SignerWithAddress, - contractName: string, - libs: { [key in string]: string }, - ...args: any[] -): Promise { - const contractFactory = await ethers.getContractFactory(contractName, { - signer: deployer, - libraries: libs, - }) - return contractFactory.deploy(...args) -} - -export function subnetCreationPrivileges(): SubnetCreationPrivileges { - const value = process.env.REGISTRY_CREATION_PRIVILEGES || 'unrestricted' - return value === 'owner' ? SubnetCreationPrivileges.Owner : SubnetCreationPrivileges.Unrestricted -} - -export async function getTransactionFees() { - const feeData = await ethers.provider.getFeeData() - - return { - maxFeePerGas: feeData.maxFeePerGas, - maxPriorityFeePerGas: feeData.maxPriorityFeePerGas, - type: 2, - } -} - -interface Facet { - facetAddress: string - functionSelectors: string[] -} -type FacetMap = { [key: string]: string[] } - -export async function getFacets(diamondAddress: string): Promise { - // Ensure you have the ABI for the diamond loupe functions - const diamondLoupeABI = [ - { - inputs: [], - name: 'facets', - outputs: [ - { - components: [ - { - internaltype: 'address', - name: 'facetaddress', - type: 'address', - }, - { - internaltype: 'bytes4[]', - name: 'functionselectors', - type: 'bytes4[]', - }, - ], - name: 'facets_', - type: 'tuple[]', - }, - ], - statemutability: 'view', - constant: true, - type: 'function', - }, - ] - - const provider = ethers.provider - const diamond = new Contract(diamondAddress, diamondLoupeABI, provider) - const facetsData = await diamond.facets() - - // Convert facetsData to the Facet[] type. - const facets: Facet[] = facetsData.map((facetData) => ({ - facetAddress: facetData[0], - functionSelectors: facetData[1], - })) - - const facetMap = facets.reduce((acc, facet) => { - acc[facet.facetAddress] = facet.functionSelectors - return acc - }, {}) - - return facetMap -} - -async function startGanache() { - return new Promise((resolve, reject) => { - const server = ganache.server({ - miner: { defaultGasPrice: '0x0' }, - chain: { hardfork: 'berlin' }, - logging: { quiet: true }, - }) - server.listen(isolatedPort, (err) => { - if (err) reject(err) - else resolve(server) - }) - }) -} - -async function stopGanache(server) { - return new Promise((resolve, reject) => { - server.close((err) => { - if (err) reject(err) - else resolve() - }) - }) -} - -export async function getRuntimeBytecode(bytecode) { - // Check if bytecode is provided - if (!bytecode) { - throw new Error('No bytecode provided') - } - const ganacheServer = await startGanache() - - const provider = new providers.JsonRpcProvider(`http://127.0.0.1:${isolatedPort}`) - const wallet = new Wallet(process.env.PRIVATE_KEY, provider) - const contractFactory = new ContractFactory([], bytecode, wallet) - const contract = await contractFactory.deploy({ gasPrice: 0 }) - await contract.deployed() - - const runtimeBytecode = await provider.getCode(contract.address) - stopGanache(ganacheServer) - return runtimeBytecode -} - -export async function getBytecodeFromFacet(facet) { - const facetName = facet.name - const libs = facet.libs - const factoryFileName = findFileInDir(`${facetName}__factory.ts`, `./typechain/factories/`) - if (factoryFileName === null) { - throw new Error('Typescript bindings for Facet not found') - } - const bytecodeNeedsLink = getBytecodeFromFacetTypeChainFilename(factoryFileName) - let libs2 = {} - // Loop through each key in the libs - for (let key in libs) { - let newKey = `src/lib/${key}.sol:${key}` - libs2[newKey] = libs[key] - } - - // Link the bytecode with the libraries - const bytecode = linker.linkBytecode(bytecodeNeedsLink, libs2) - return await getRuntimeBytecode(bytecode) -} - -function getBytecodeFromFacetTypeChainFilename(fileName) { - try { - // Read the file synchronously - const fileContent = fs.readFileSync(fileName, 'utf8') - - // Split the file content into lines - const lines = fileContent.split('\n') - - // Initialize a flag to identify when the target line is found - let found = false - - for (const line of lines) { - // If the previous line was the target line, return the current line - if (found) { - // Trim semicolons and quotes from the beginning and end of the string - return line.trim().replace(/^[";]+|[";]+$/g, '') - } - - // Check if the current line is the target line - if (line.includes('const _bytecode =')) { - found = true - } - } - - // If the loop completes without returning, the target line was not found - throw new Error('Target line "const _bytecode =" not found in the file') - } catch (error) { - console.error('Error reading file:', error.message) - } -} - -// Loop through each contract address in the facets -// query web3 api to get deployed bytecode -export async function getOnChainBytecodeFromFacets(facets) { - const deployedBytecode = {} - for (let contractAddress in facets) { - try { - // Fetch the bytecode of the contract - const bytecode = await ethers.provider.getCode(contractAddress) - deployedBytecode[bytecode] = contractAddress - // Log the bytecode to the console - } catch (error) { - // Print any errors to stderr - console.error(`Error fetching bytecode for ${contractAddress}:`, error.message) - } - } - return deployedBytecode -} - -/** - * Filters the input array to only return strings that start with '0x'. - * - * @param {Object} input - The object containing the functionSelectors array. - * @returns {Array} - An array of strings from functionSelectors that start with '0x'. - */ -function filterSelectors(input) { - return input.filter((item) => { - return typeof item === 'string' && item.startsWith('0x') - }) -} - -function compareArrays(onChain, newArr) { - const result = { - removedSelectors: [], - matchingSelectors: [], - addedSelectors: [], - } - - // Create a Map for easier lookup - const onChainMap = new Map() - onChain.forEach((selector) => onChainMap.set(selector, true)) - - const newArrMap = new Map() - newArr.forEach((selector) => newArrMap.set(selector, true)) - - // Find matching and removed selectors - onChain.forEach((selector) => { - if (newArrMap.has(selector)) { - result.matchingSelectors.push(selector) - } else { - result.removedSelectors.push(selector) - } - }) - - // Find added selectors - newArr.forEach((selector) => { - if (!onChainMap.has(selector)) { - result.addedSelectors.push(selector) - } - }) - - return result -} -async function cutFacetOnChain(diamondAddress: string, replacementFacet: any, action, functionSelectors) { - const [deployer] = await ethers.getSigners() - const txArgs = await getTransactionFees() - - const facetCuts = [ - { - facetAddress: action === FacetCutAction.Remove ? ethers.constants.AddressZero : replacementFacet.address, - action: action, - functionSelectors: functionSelectors, - }, - ] - const diamondCutter = await ethers.getContractAt('DiamondCutFacet', diamondAddress, deployer) - const tx = await diamondCutter.diamondCut( - facetCuts, - ethers.constants.AddressZero, - ethers.utils.formatBytes32String(''), - txArgs, - ) - await tx.wait() -} - -// given a facet address and a diamond address, -// upgrade the diamond to use the new facet -export async function upgradeFacetOnChain(diamondAddress: string, facet, onChainFunctionSelectors) { - const replacementFacetName = facet.name - const facetLibs = facet.libs - console.info(` -Diamond Facet Upgrade: ------------------------------------ -Diamond Address: ${diamondAddress} -Replacement Facet Name: ${replacementFacetName} -`) - - if (!diamondAddress) throw new Error(`Gateway is missing`) - - const [deployer] = await ethers.getSigners() - const txArgs = await getTransactionFees() - let replacementFacet = await deployContractWithDeployer(deployer, replacementFacetName, facetLibs, txArgs) - await replacementFacet.deployed() - - const result = compareArrays(onChainFunctionSelectors, filterSelectors(getSelectors(replacementFacet))) - - async function cutSelectorsOnChain(action, selectors) { - if (selectors.length > 0) { - await cutFacetOnChain(diamondAddress, replacementFacet, action, selectors) - } - } - - // cut changes for each facet cut action - remove replace and add - await cutSelectorsOnChain(FacetCutAction.Remove, result['removedSelectors']) - await cutSelectorsOnChain(FacetCutAction.Replace, result['matchingSelectors']) - await cutSelectorsOnChain(FacetCutAction.Add, result['addedSelectors']) - - //end move facet - return { [replacementFacetName]: replacementFacet.address } -} - -/** - * Log information about a missing facet. - * @param facet - The facet to display. - */ -export function logMissingFacetInfo(facet) { - const formattedLibs = Object.entries(facet.libs) - .map(([key, value]) => ` - ${key}: ${value}`) - .join('\n') - - console.info(` -Facet Bytecode Not Found: ---------------------------------- -Facet Name: ${facet.name} -Libraries: -${formattedLibs} -Address: ${facet.address} -`) -} - -function getDeployedFacetAddressFromName(facetName, deployments) { - for (let facet of deployments.Facets) { - if (facet.name === facetName) { - return facet.address - } - } -} - -/** - * Handle facet upgrades on chain. - * @param facet - The facet to process. - * @param onChainFacets - the on chain facets and their functions as returned by DiamondLoupe - * @param gatewayDiamondAddress - The address of the Gateway Diamond. - * @param updatedFacets - A collection of updated facets. - * @param onChainFacetBytecodes - The bytecodes from the on-chain facets. - */ -export async function upgradeFacet( - facet, - onChainFacets, - gatewayDiamondAddress, - updatedFacets, - onChainFacetBytecodes, - deployments, -) { - const facetBytecode = await getBytecodeFromFacet(facet) - - if (!onChainFacetBytecodes[facetBytecode]) { - logMissingFacetInfo(facet) - - const onChainFunctionSelectors = onChainFacets[getDeployedFacetAddressFromName(facet.name, deployments)] - - const newFacet = await upgradeFacetOnChain(gatewayDiamondAddress, facet, onChainFunctionSelectors) - for (let key in newFacet) updatedFacets[key] = newFacet[key] - - const DEPLOYMENT_STATUS_MESSAGE = ` -Deployment Status: -------------------------- -New replacement facet (${facet.name}) deployed. -` - console.info(DEPLOYMENT_STATUS_MESSAGE) - } -} diff --git a/contracts/tasks/deploy-gateway.ts b/contracts/tasks/deploy-gateway.ts new file mode 100644 index 0000000000..d8971e82d4 --- /dev/null +++ b/contracts/tasks/deploy-gateway.ts @@ -0,0 +1,92 @@ +import { task, types } from 'hardhat/config' +import { HardhatRuntimeEnvironment, TaskArguments } from 'hardhat/types' +import { Deployments } from './lib' + +// Customize your gateway parameters here. +const gatewayConstructorParams = { + bottomUpCheckPeriod: 10, + activeValidatorsLimit: 100, + majorityPercentage: 66, + networkName: { + root: undefined, // Will be set later. + route: [], + }, + genesisValidators: [], + commitSha: undefined, // Will be set later. +} + +task('deploy-gateway') + .setDescription('Builds and deploys the gateway contract') + .addOptionalPositionalParam( + 'upgrade', + 'Must be set to true if upgrading to skip the diamond deployment', + false, + types.boolean, + ) + .setAction(async (args: TaskArguments, hre: HardhatRuntimeEnvironment): Promise => { + await hre.run('compile') + + const [deployer] = await hre.getUnnamedAccounts() + + // Deploy the facets. + const facets = await deployFacets(hre, deployer) + + if (args.upgrade) { + console.log('Running as part of an upgrade; skipping deployment of GatewayDiamond') + return facets + } + + // Deploy the diamond. + const diamond = await deployGatewayDiamond(hre, deployer, facets) + return facets.join(diamond) + }) + +async function deployFacets(hre: HardhatRuntimeEnvironment, deployer: string): Promise { + const facets = [ + { + name: 'GatewayGetterFacet', + libraries: ['SubnetIDHelper', 'LibQuorum'], + }, + { name: 'DiamondLoupeFacet' }, + { name: 'DiamondCutFacet' }, + { + name: 'GatewayManagerFacet', + libraries: ['CrossMsgHelper', 'SubnetIDHelper'], + }, + { + name: 'GatewayMessengerFacet', + libraries: ['CrossMsgHelper', 'SubnetIDHelper'], + }, + { + name: 'CheckpointingFacet', + libraries: ['AccountHelper', 'SubnetIDHelper', 'CrossMsgHelper'], + }, + { + name: 'XnetMessagingFacet', + libraries: ['AccountHelper', 'CrossMsgHelper', 'SubnetIDHelper'], + }, + { name: 'TopDownFinalityFacet', libraries: ['AccountHelper'] }, + { name: 'OwnershipFacet' }, + ] + + return await Deployments.deploy(hre, deployer, ...facets) +} + +async function deployGatewayDiamond( + hre: HardhatRuntimeEnvironment, + deployer: string, + facets: Deployments, +): Promise { + gatewayConstructorParams.networkName.root = await hre.getChainId() + gatewayConstructorParams.commitSha = hre.ethers.utils.formatBytes32String(gitCommitSha()) + + const deployments = await Deployments.deploy(hre, deployer, { + name: 'GatewayDiamond', + args: [facets.asFacetCuts(), gatewayConstructorParams], + }) + return deployments +} + +function gitCommitSha(): string { + return require('child_process').execSync('git rev-parse --short HEAD').toString().trim() +} diff --git a/contracts/tasks/deploy-libraries.ts b/contracts/tasks/deploy-libraries.ts new file mode 100644 index 0000000000..5f52b1a432 --- /dev/null +++ b/contracts/tasks/deploy-libraries.ts @@ -0,0 +1,23 @@ +import { task } from 'hardhat/config' +import { HardhatRuntimeEnvironment } from 'hardhat/types' +import { Deployments } from './lib' + +task( + 'deploy-libraries', + 'Build and deploys all libraries on the selected network', + async (_args, hre: HardhatRuntimeEnvironment): Promise => { + await hre.run('compile') + + const [deployer] = await hre.getUnnamedAccounts() + + const contracts = [ + { name: 'AccountHelper' }, + { name: 'SubnetIDHelper' }, + { name: 'LibStaking' }, + { name: 'LibQuorum' }, + { name: 'CrossMsgHelper', libraries: ['SubnetIDHelper'] }, + ] + + return await Deployments.deploy(hre, deployer, ...contracts) + }, +) diff --git a/contracts/tasks/deploy-registry.ts b/contracts/tasks/deploy-registry.ts new file mode 100644 index 0000000000..246db888f4 --- /dev/null +++ b/contracts/tasks/deploy-registry.ts @@ -0,0 +1,105 @@ +import { task, types } from 'hardhat/config' +import { HardhatRuntimeEnvironment, TaskArguments } from 'hardhat/types' +import { Deployments, selectors } from './lib' + +export enum SubnetCreationPrivileges { + Unrestricted = 0, + Owner = 1, +} + +task('deploy-registry') + .setDescription('Builds and deploys the registry contract') + .addOptionalPositionalParam( + 'upgrade', + 'Must be set to true if upgrading to skip the diamond deployment', + false, + types.boolean, + ) + .setAction(async (args: TaskArguments, hre: HardhatRuntimeEnvironment) => { + await hre.run('compile') + + const [deployer] = await hre.getUnnamedAccounts() + const balance = await hre.ethers.provider.getBalance(deployer) + console.log( + `Deploying registry contracts with account: ${deployer} and balance: ${hre.ethers.utils.formatEther(balance.toString())}`, + ) + + const subnetActorFacets = await Deployments.deploy( + hre, + deployer, + { + name: 'SubnetActorGetterFacet', + libraries: ['SubnetIDHelper'], + }, + { name: 'SubnetActorManagerFacet' }, + { name: 'SubnetActorPauseFacet' }, + { name: 'SubnetActorRewardFacet' }, + { name: 'SubnetActorCheckpointingFacet' }, + { name: 'DiamondCutFacet' }, + { name: 'DiamondLoupeFacet' }, + { name: 'OwnershipFacet' }, + ) + + const registryFacets = await Deployments.deploy( + hre, + deployer, + { + name: 'RegisterSubnetFacet', + libraries: ['SubnetIDHelper'], + }, + { name: 'SubnetGetterFacet' }, + { name: 'DiamondLoupeFacet' }, + { name: 'DiamondCutFacet' }, + { name: 'OwnershipFacet' }, + ) + + if (args.upgrade) { + console.log('Running as part of an upgrade; skipping deployment of SubnetRegistryDiamond') + return + } + + // TODO(raulk): document changed default to owner from unrestricted + const mode = + process.env.REGISTRY_CREATION_PRIVILEGES === 'unrestricted' + ? SubnetCreationPrivileges.Unrestricted + : SubnetCreationPrivileges.Owner + + console.log(` + *************************************************************** + ** ** + ** Subnet creation privileges: ${mode} ** + ** ** + *************************************************************** + `) + + const registryConstructorParams = { + gateway: (await hre.deployments.get('GatewayDiamond')).address, + getterFacet: subnetActorFacets.addresses['SubnetActorGetterFacet'], + managerFacet: subnetActorFacets.addresses['SubnetActorManagerFacet'], + rewarderFacet: subnetActorFacets.addresses['SubnetActorRewardFacet'], + checkpointerFacet: subnetActorFacets.addresses['SubnetActorCheckpointingFacet'], + pauserFacet: subnetActorFacets.addresses['SubnetActorPauseFacet'], + diamondCutFacet: subnetActorFacets.addresses['DiamondCutFacet'], + diamondLoupeFacet: subnetActorFacets.addresses['DiamondLoupeFacet'], + ownershipFacet: subnetActorFacets.addresses['OwnershipFacet'], + + subnetActorGetterSelectors: selectors(subnetActorFacets.contracts['SubnetActorGetterFacet']), + subnetActorManagerSelectors: selectors(subnetActorFacets.contracts['SubnetActorManagerFacet']), + subnetActorRewarderSelectors: selectors(subnetActorFacets.contracts['SubnetActorRewardFacet']), + subnetActorCheckpointerSelectors: selectors(subnetActorFacets.contracts['SubnetActorCheckpointingFacet']), + subnetActorPauserSelectors: selectors(subnetActorFacets.contracts['SubnetActorPauseFacet']), + subnetActorDiamondCutSelectors: selectors(subnetActorFacets.contracts['DiamondCutFacet']), + subnetActorDiamondLoupeSelectors: selectors(subnetActorFacets.contracts['DiamondLoupeFacet']), + subnetActorOwnershipSelectors: selectors(subnetActorFacets.contracts['OwnershipFacet']), + creationPrivileges: Number(mode), + } + + console.log(`Deploying SubnetRegistryDiamond...`) + const registry = await hre.deployments.deploy('SubnetRegistryDiamond', { + from: deployer, + args: [registryFacets.asFacetCuts(), registryConstructorParams], + log: true, + waitConfirmations: 1, + }) + console.log(`SubnetRegistryDiamond deployed at ${registry.address}`) + }) diff --git a/contracts/tasks/deploy.ts b/contracts/tasks/deploy.ts new file mode 100644 index 0000000000..ee102c043e --- /dev/null +++ b/contracts/tasks/deploy.ts @@ -0,0 +1,35 @@ +import { task } from 'hardhat/config' +import { HardhatRuntimeEnvironment } from 'hardhat/types' + +task( + 'deploy-stack', + 'Builds and deploys the IPC stack on the selected network', + async (args, hre: HardhatRuntimeEnvironment) => { + await hre.run('compile') + + console.log() + console.log( + '==== LIBRARY DEPLOYMENT ===========================================================================', + ) + await hre.run('deploy-libraries') + console.log() + + console.log( + '==== GATEWAY DEPLOYMENT ===========================================================================', + ) + await hre.run('deploy-gateway') + console.log() + + console.log( + '==== REGISTRY DEPLOYMENT ==========================================================================', + ) + await hre.run('deploy-registry') + console.log() + + console.log('████████████████████████████████████████████████████████████') + console.log('█ █') + console.log('█ IPC STACK DEPLOYED! 🚀 █') + console.log('█ █') + console.log('████████████████████████████████████████████████████████████') + }, +) diff --git a/contracts/tasks/index.ts b/contracts/tasks/index.ts new file mode 100644 index 0000000000..07f1ab39f8 --- /dev/null +++ b/contracts/tasks/index.ts @@ -0,0 +1,6 @@ +// Create index.ts file to export all tasks +import './deploy-libraries' +import './deploy-gateway' +import './deploy-registry' +import './deploy' +import './upgrade' diff --git a/contracts/tasks/lib/deployments.ts b/contracts/tasks/lib/deployments.ts new file mode 100644 index 0000000000..dc15c970fe --- /dev/null +++ b/contracts/tasks/lib/deployments.ts @@ -0,0 +1,116 @@ +import * as fs from 'fs' +import {HardhatRuntimeEnvironment} from 'hardhat/types' +import {Contract} from 'ethers' +import {DeployResult} from 'hardhat-deploy/types' +import {selectors} from "./selectors"; +import _ = require('lodash'); + +export enum FacetCutAction { + Add, + Replace, + Remove, +} + +export type FacetCut = Facet & { + action: FacetCutAction +} + +export type Facet = { + facetAddress: string + functionSelectors: string[] +} + +export class Deployments { + private readonly _contracts: { [key: string]: Contract } + private readonly _deployResults?: { [key: string]: DeployResult } + + private constructor( + contracts: { [key: string]: Contract }, + deployResults?: { [key: string]: DeployResult }, + ) { + this._contracts = contracts + this._deployResults = deployResults + } + + private static async resolveContracts( + hre: HardhatRuntimeEnvironment, + contractNames: string[], + ): Promise<{ [key: string]: Contract }> { + return Object.fromEntries( + await Promise.all( + contractNames.map(async (name) => { + const {address} = await hre.deployments.get(name) + return [name, await hre.ethers.getContractAt(name, address)] + }), + ), + ) + } + + public static async resolve( + hre: HardhatRuntimeEnvironment, + ...contractNames: string[] + ): Promise { + return new Deployments( + await Deployments.resolveContracts(hre, contractNames), + ) + } + + public static async deploy( + hre: HardhatRuntimeEnvironment, + deployer: string, + ...contracts: { name: string; args?: any; libraries?: string[] }[] + ): Promise { + const results = {} + for (const contract of contracts) { + console.log(`Deploying ${contract.name}...`) + const libraries = await Deployments.resolve( + hre, + ...(contract.libraries || []), + ) + + const result = await hre.deployments.deploy(contract.name, { + from: deployer, + log: true, + args: contract.args, + libraries: libraries.addresses, + waitConfirmations: 1, + }) + results[contract.name] = result + console.log(`${contract.name} deployed at ${result.address}`) + } + return new Deployments( + await Deployments.resolveContracts(hre, Object.keys(results)), + results, + ) + } + + public asFacetCuts(): FacetCut[] { + return Object.values(this._contracts).map((contract) => ({ + facetAddress: contract.address, + action: FacetCutAction.Add, + functionSelectors: selectors(contract), + })) + } + + public join(other: Deployments): Deployments { + return new Deployments( + Object.assign({}, this._contracts, other._contracts), + Object.assign({}, this._deployResults, other._deployResults), + ) + } + + get addresses(): { [key: string]: string } { + return _.mapValues( + this.contracts, + (contract: Contract) => contract.address, + ) + } + + get contracts(): { [key: string]: Contract } { + return this._contracts + } + + get results(): { [key: string]: DeployResult } { + return this._deployResults + } +} diff --git a/contracts/tasks/lib/diamond.ts b/contracts/tasks/lib/diamond.ts new file mode 100644 index 0000000000..975e8fba64 --- /dev/null +++ b/contracts/tasks/lib/diamond.ts @@ -0,0 +1,113 @@ +import {Contract} from 'ethers' +import {HardhatRuntimeEnvironment} from 'hardhat/types' +import {IDiamondLoupe} from '../../typechain' +import {Deployments, FacetCut} from './deployments' +import {selectors} from "./selectors"; +import _ = require('lodash'); + +enum FacetCutAction { + Add = 0, + Replace = 1, + Remove = 2, +} + +type Selectors = { [selector: string]: string } + +export class Diamond { + public readonly contract: Contract + private hre: HardhatRuntimeEnvironment + + constructor(hre: HardhatRuntimeEnvironment, contract: Contract) { + this.hre = hre + this.contract = contract + } + + /** + * Computes the facet cuts needed to upgrade the diamond to the target. + * @param target The end state we desire. + */ + public async computeFacetCuts(target: Deployments): Promise { + // These objects will hold selector => facet address mappings. + const loupe = (await this.hre.ethers.getContractAt( + 'DiamondLoupeFacet', + this.contract.address, + )) as IDiamondLoupe + + // Current selectors (selector => address). + const current: Selectors = {} + for (const facet of await loupe.facets()) { + for (const selector of facet.functionSelectors) { + current[selector] = facet.facetAddress + } + } + + // Needed selectors (selector => address). + const needed: Selectors = {} + for (const contract of Object.values(target.contracts)) { + for (const selector of selectors(contract)) { + needed[selector] = contract.address + } + } + + // Generate the facet cuts. + const facetCuts: FacetCut[] = [] + facetCuts.push(...this.computeRemoved(current, needed)) + facetCuts.push(...this.computeAdded(current, needed)) + facetCuts.push(...this.computeReplaced(current, needed)) + + return facetCuts + } + + /** + * Computes the removed facet cuts. + */ + private computeRemoved(current: Selectors, needed: Selectors): FacetCut[] { + const selectorsToRemove = Object.keys(current).filter( + (selector) => !needed[selector], + ) + if (selectorsToRemove.length === 0) { + return [] + } + return [ + { + facetAddress: '0x0000000000000000000000000000000000000000', + action: FacetCutAction.Remove, + functionSelectors: selectorsToRemove, + }, + ] + } + + /** + * Computes the added facet cuts. + */ + private computeAdded(current: Selectors, needed: Selectors): FacetCut[] { + const selectorsToAdd = Object.keys(needed).filter( + (selector) => !current[selector], + ) + + const addressToSelectors = _.groupBy(selectorsToAdd, (selector) => needed[selector]) + + return Object.entries(addressToSelectors).map(([facetAddress, functionSelectors]) => ({ + facetAddress, + action: FacetCutAction.Add, + functionSelectors, + })) + } + + /** + * Computes the replaced facet cuts. + */ + private computeReplaced(current: Selectors, needed: Selectors): FacetCut[] { + const selectorsToReplace = Object.keys(needed).filter( + (selector) => + current[selector] && current[selector] !== needed[selector], + ) + const addressToSelectors = _.groupBy(selectorsToReplace, (selector) => needed[selector]) + + return Object.entries(addressToSelectors).map(([facetAddress, functionSelectors]) => ({ + facetAddress, + action: FacetCutAction.Replace, + functionSelectors, + })) + } +} diff --git a/contracts/tasks/lib/index.ts b/contracts/tasks/lib/index.ts new file mode 100644 index 0000000000..9824f91364 --- /dev/null +++ b/contracts/tasks/lib/index.ts @@ -0,0 +1,3 @@ +export * from './diamond' +export * from './deployments' +export * from './selectors' \ No newline at end of file diff --git a/contracts/tasks/lib/selectors.ts b/contracts/tasks/lib/selectors.ts new file mode 100644 index 0000000000..78a4f8a45b --- /dev/null +++ b/contracts/tasks/lib/selectors.ts @@ -0,0 +1,7 @@ +import {Contract} from "ethers"; + +export function selectors(contract: Contract) { + return Object.keys(contract.interface.functions) + .filter((sig) => sig !== 'init(bytes)') + .map((sig) => contract.interface.getSighash(sig)) +} \ No newline at end of file diff --git a/contracts/tasks/upgrade.ts b/contracts/tasks/upgrade.ts new file mode 100644 index 0000000000..8a0c8c8fb7 --- /dev/null +++ b/contracts/tasks/upgrade.ts @@ -0,0 +1,77 @@ +import { task, types } from 'hardhat/config' +import { HardhatRuntimeEnvironment, TaskArguments } from 'hardhat/types' +import { Deployments, Diamond } from './lib' +import { IDiamond, IDiamondCut } from '../typechain' +import _ = require('lodash') + +task('upgrade', 'Upgrades a diamond contract') + .addParam('contract', 'The contract to upgrade. One of: gateway, registry', null, types.string) + .addOptionalParam( + 'initAddress', + 'The address of the contract to call init on (optional)', + '0x0000000000000000000000000000000000000000', + types.string, + ) + .addOptionalParam('initCalldata', 'The calldata to pass to init in hex(optional)', '0x', types.string) + .setAction(async (args: TaskArguments, hre: HardhatRuntimeEnvironment) => { + await hre.run('compile') + + const { contract, initAddress, initCalldata } = args + if (contract !== 'gateway' && contract !== 'registry') { + throw new Error('Invalid contract specified') + } + + const libraries: Deployments = await hre.run('deploy-libraries') + logNewDeployments('library', libraries) + + // Run the corresponding deploy task passing in upgrade=true to skip the diamond deployment. + // This will only deploy the facets that have changed, but will return the full set of facets. + // One can know which facets were newly deployed by checking the newlyDeployed property of the result. + const updatedFacets: Deployments = await hre.run('deploy-' + contract, { + upgrade: true, + }) + logNewDeployments('facet', updatedFacets) + + const diamond = await (async (diamondName) => { + const deployments = await Deployments.resolve(hre, diamondName) + const contract = deployments.contracts[diamondName] + return new Diamond(hre, contract) + })(contract === 'gateway' ? 'GatewayDiamond' : 'SubnetRegistryDiamond') + + const facetCuts = await diamond.computeFacetCuts(updatedFacets) + + if (facetCuts.length === 0) { + console.log('No facet cuts needed') + return + } + + const [deployer] = await hre.getUnnamedAccounts() + const balance = await hre.ethers.provider.getBalance(deployer) + + console.log( + `Performing facet cuts with account: ${deployer} and balance: ${hre.ethers.utils.formatEther(balance.toString())}`, + ) + + console.log(`Facet cuts:\n\n${JSON.stringify(facetCuts, null, 2)}`) + + const diamondCut = (await hre.ethers.getContractAt('IDiamondCut', diamond.contract.address)) as IDiamondCut + const result = await diamondCut.diamondCut(facetCuts, initAddress, initCalldata) + const receipt = await result.wait(1) + console.log(`Diamond cut transaction: ${receipt.transactionHash}`) + console.log(`Included in block: ${receipt.blockNumber}`) + console.log(`Gas used: ${receipt.gasUsed.toString()}`) + console.log(`Status: ${receipt.status}`) + console.log(`Events:\n\n${JSON.stringify(receipt.events, null, 2)}`) + }) + +function logNewDeployments(kind: string, deployments: Deployments) { + const newlyDeployed = _.pickBy(deployments.results, ({ newlyDeployed }) => newlyDeployed) + if (Object.keys(newlyDeployed).length === 0) { + console.log(`No ${kind} contracts were newly deployed`) + return + } + console.log(`Newly deployed ${kind} contracts:`) + for (const [name, { address }] of Object.entries(newlyDeployed)) { + console.log(` ${name} at ${address}`) + } +} diff --git a/package.json b/package.json index 360e2217b3..2e6832dda7 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "pnpm": { "overrides": { "axios@>=0.8.1 <0.28.0": ">=0.28.0", - "ws": ">=8.17.1" + "ws": ">=8.17.1", + "axios@>=1.3.2 <=1.7.3": ">=1.7.4" } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 700d8656f9..9a36458ad2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,7 @@ settings: overrides: axios@>=0.8.1 <0.28.0: '>=0.28.0' + axios@>=1.3.2 <=1.7.3: '>=1.7.4' ws: '>=8.17.1' importers: @@ -32,6 +33,9 @@ importers: fevmate: specifier: github:wadealexc/fevmate#6a80e98 version: https://codeload.github.com/wadealexc/fevmate/tar.gz/6a80e98 + lodash: + specifier: ^4.17.21 + version: 4.17.21 devDependencies: '@nomicfoundation/hardhat-foundry': specifier: ^1.0.1 @@ -45,6 +49,9 @@ importers: '@typechain/hardhat': specifier: ^7.0.0 version: 7.0.0(@ethersproject/abi@5.7.0)(@ethersproject/providers@5.7.2(bufferutil@4.0.7)(utf-8-validate@5.0.10))(@typechain/ethers-v5@11.1.2(@ethersproject/abi@5.7.0)(@ethersproject/providers@5.7.2(bufferutil@4.0.7)(utf-8-validate@5.0.10))(ethers@5.7.2(bufferutil@4.0.7)(utf-8-validate@5.0.10))(typechain@8.3.2(typescript@5.4.5))(typescript@5.4.5))(ethers@5.7.2(bufferutil@4.0.7)(utf-8-validate@5.0.10))(hardhat@2.22.5(bufferutil@4.0.7)(ts-node@10.9.2(@types/node@20.14.2)(typescript@5.4.5))(typescript@5.4.5)(utf-8-validate@5.0.10))(typechain@8.3.2(typescript@5.4.5)) + '@types/lodash': + specifier: ^4.17.7 + version: 4.17.7 dotenv: specifier: ^16.0.1 version: 16.4.5 @@ -516,6 +523,9 @@ packages: '@types/bn.js@5.1.5': resolution: {integrity: sha512-V46N0zwKRF5Q00AZ6hWtN0T8gGmDUaUzLWQvHFo5yThtVwK/VCenFY3wXVbOvNfajEpsTfQM4IN9k/d6gUVX3A==} + '@types/lodash@4.17.7': + resolution: {integrity: sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==} + '@types/lru-cache@5.1.1': resolution: {integrity: sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw==} @@ -659,8 +669,8 @@ packages: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} engines: {node: '>= 4.0.0'} - axios@1.7.2: - resolution: {integrity: sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==} + axios@1.7.4: + resolution: {integrity: sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==} balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -2581,6 +2591,8 @@ snapshots: dependencies: '@types/node': 20.14.2 + '@types/lodash@4.17.7': {} + '@types/lru-cache@5.1.1': {} '@types/node@20.14.2': @@ -2714,7 +2726,7 @@ snapshots: at-least-node@1.0.0: {} - axios@1.7.2(debug@4.3.5): + axios@1.7.4(debug@4.3.5): dependencies: follow-redirects: 1.15.6(debug@4.3.5) form-data: 4.0.0 @@ -3329,7 +3341,7 @@ snapshots: '@ethersproject/transactions': 5.7.0 '@ethersproject/wallet': 5.7.0 '@types/qs': 6.9.15 - axios: 1.7.2(debug@4.3.5) + axios: 1.7.4(debug@4.3.5) chalk: 4.1.2 chokidar: 3.6.0 debug: 4.3.5