diff --git a/abis/test/L1CrossDomainMessenger.json b/abis/test/L1CrossDomainMessenger.json new file mode 100644 index 0000000..b8da6f2 --- /dev/null +++ b/abis/test/L1CrossDomainMessenger.json @@ -0,0 +1 @@ +[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"msgHash","type":"bytes32"}],"name":"FailedRelayedMessage","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint8","name":"version","type":"uint8"}],"name":"Initialized","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"msgHash","type":"bytes32"}],"name":"RelayedMessage","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"target","type":"address"},{"indexed":false,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"bytes","name":"message","type":"bytes"},{"indexed":false,"internalType":"uint256","name":"messageNonce","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"gasLimit","type":"uint256"}],"name":"SentMessage","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"SentMessageExtension1","type":"event"},{"inputs":[],"name":"MESSAGE_VERSION","outputs":[{"internalType":"uint16","name":"","type":"uint16"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MIN_GAS_CALLDATA_OVERHEAD","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MIN_GAS_DYNAMIC_OVERHEAD_DENOMINATOR","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MIN_GAS_DYNAMIC_OVERHEAD_NUMERATOR","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"OTHER_MESSENGER","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"PORTAL","outputs":[{"internalType":"contract OptimismPortal","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"RELAY_CALL_OVERHEAD","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"RELAY_CONSTANT_OVERHEAD","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"RELAY_GAS_CHECK_BUFFER","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"RELAY_RESERVED_GAS","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"_message","type":"bytes"},{"internalType":"uint32","name":"_minGasLimit","type":"uint32"}],"name":"baseGas","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"failedMessages","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"contract OptimismPortal","name":"_portal","type":"address"}],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"messageNonce","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"portal","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_nonce","type":"uint256"},{"internalType":"address","name":"_sender","type":"address"},{"internalType":"address","name":"_target","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"},{"internalType":"uint256","name":"_minGasLimit","type":"uint256"},{"internalType":"bytes","name":"_message","type":"bytes"}],"name":"relayMessage","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"_target","type":"address"},{"internalType":"bytes","name":"_message","type":"bytes"},{"internalType":"uint32","name":"_minGasLimit","type":"uint32"}],"name":"sendMessage","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"successfulMessages","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"version","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"xDomainMessageSender","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"}] diff --git a/contracts/bridges/OptimismMessenger.sol b/contracts/bridges/OptimismMessenger.sol new file mode 100644 index 0000000..21102ec --- /dev/null +++ b/contracts/bridges/OptimismMessenger.sol @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +/// @dev Interface to the CrossDomainMessenger (CDM) Contract Proxy. +interface ICrossDomainMessenger { + function xDomainMessageSender() external returns (address); +} + +/// @dev Provided zero address. +error ZeroAddress(); + +/// @dev Only self contract is allowed to call the function. +/// @param sender Sender address. +/// @param instance Required contract instance address. +error SelfCallOnly(address sender, address instance); + +/// @dev Only `CDMContractProxyHome` is allowed to call the function. +/// @param sender Sender address. +/// @param CDMContractProxyHome Required CDM Contract Proxy (Home) address. +error CDMContractProxyHomeOnly(address sender, address CDMContractProxyHome); + +/// @dev Only on behalf of `foreignGovernor` the function is allowed to process the data. +/// @param sender Sender address. +/// @param foreignGovernor Required Foreign Governor address. +error ForeignGovernorOnly(address sender, address foreignGovernor); + +/// @dev Provided incorrect data length. +/// @param expected Expected minimum data length. +/// @param provided Provided data length. +error IncorrectDataLength(uint256 expected, uint256 provided); + +/// @dev Provided value is bigger than the actual balance. +/// @param value Provided value. +/// @param balance Actual balance. +error InsufficientBalance(uint256 value, uint256 balance); + +/// @dev Target execution failed. +/// @param target Target address. +/// @param value Provided value. +/// @param payload Provided payload. +error TargetExecFailed(address target, uint256 value, bytes payload); + +/// @title OptimismMessenger - Smart contract for the governor home (Optimism) bridge implementation +/// @author Aleksandr Kuperman - +/// @author Andrey Lebedev - +/// @author Mariapia Moscatiello - +contract OptimismMessenger { + event FundsReceived(address indexed sender, uint256 value); + event ForeignGovernorUpdated(address indexed foreignMessageSender); + event MessageReceived(address indexed foreignMessageSender, bytes data); + + // Default payload data length includes the number of bytes of at least one address (20 bytes or 160 bits), + // value (12 bytes or 96 bits) and the payload size (4 bytes or 32 bits) + uint256 public constant DEFAULT_DATA_LENGTH = 36; + // CDM Contract Proxy (Home) address on L2 that receives the message across the bridge from the foreign L1 network + address public immutable CDMContractProxyHome; + // Foreign governor address on L1 that is authorized to propagate the transaction execution across the bridge + address public foreignGovernor; + + /// @dev HomeMediator constructor. + /// @param _CDMContractProxyHome CDM Contract Proxy (Home) address (Optimism). + /// @param _foreignGovernor Foreign Governor address (ETH). + constructor(address _CDMContractProxyHome, address _foreignGovernor) { + // Check fo zero addresses + if (_CDMContractProxyHome == address(0) || _foreignGovernor == address(0)) { + revert ZeroAddress(); + } + + CDMContractProxyHome = _CDMContractProxyHome; + foreignGovernor = _foreignGovernor; + } + + /// @dev Receives native network token. + receive() external payable { + emit FundsReceived(msg.sender, msg.value); + } + + /// @dev Changes the Foreign Governor address (original Timelock). + /// @notice The only way to change the Foreign Governor address is by the Timelock on L1 to request that change. + /// This triggers a self-contract transaction of HomeMediator that changes the Foreign Governor address. + /// @param newForeignGovernor New Foreign Governor address. + function changeForeignGovernor(address newForeignGovernor) external { + // Check if the change is authorized by the previous governor itself + // This is possible only if all the checks in the message process function pass and the contract calls itself + if (msg.sender != address(this)) { + revert SelfCallOnly(msg.sender, address(this)); + } + + // Check for the zero address + if (newForeignGovernor == address(0)) { + revert ZeroAddress(); + } + + foreignGovernor = newForeignGovernor; + emit ForeignGovernorUpdated(newForeignGovernor); + } + + /// @dev Processes a message received from the CDM Contract Proxy (Home) contract. + /// @notice The sender must be the Foreign Governor address (Timelock). + /// @param data Bytes message sent from the CDM Contract Proxy (Home) contract. The data must be encoded as a set of + /// continuous transactions packed into a single buffer, where each transaction is composed as follows: + /// - target address of 20 bytes (160 bits); + /// - value of 12 bytes (96 bits), as a limit for all of Autonolas ecosystem contracts; + /// - payload length of 4 bytes (32 bits), as 2^32 - 1 characters is more than enough to fill a whole block; + /// - payload as bytes, with the length equal to the specified payload length. + function processMessageFromForeign(bytes memory data) external payable { + // Check for the CDM Contract Proxy (Home) address + if (msg.sender != CDMContractProxyHome) { + revert CDMContractProxyHomeOnly(msg.sender, CDMContractProxyHome); + } + + // Check for the Foreign Governor address + address governor = foreignGovernor; + address bridgeGovernor = ICrossDomainMessenger(CDMContractProxyHome).xDomainMessageSender(); + if (bridgeGovernor != governor) { + revert ForeignGovernorOnly(bridgeGovernor, governor); + } + + // Check for the correct data length + uint256 dataLength = data.length; + if (dataLength < DEFAULT_DATA_LENGTH) { + revert IncorrectDataLength(DEFAULT_DATA_LENGTH, data.length); + } + + // Unpack and process the data + for (uint256 i = 0; i < dataLength;) { + address target; + uint96 value; + uint32 payloadLength; + // solhint-disable-next-line no-inline-assembly + assembly { + // First 20 bytes is the address (160 bits) + i := add(i, 20) + target := mload(add(data, i)) + // Offset the data by 12 bytes of value (96 bits) + i := add(i, 12) + value := mload(add(data, i)) + // Offset the data by 4 bytes of payload length (32 bits) + i := add(i, 4) + payloadLength := mload(add(data, i)) + } + + // Check for the zero address + if (target == address(0)) { + revert ZeroAddress(); + } + + // Check for the value compared to the contract's balance + if (value > address(this).balance) { + revert InsufficientBalance(value, address(this).balance); + } + + // Get the payload + bytes memory payload = new bytes(payloadLength); + for (uint256 j = 0; j < payloadLength; ++j) { + payload[j] = data[i + j]; + } + // Offset the data by the payload number of bytes + i += payloadLength; + + // Call the target with the provided payload + (bool success, ) = target.call{value: value}(payload); + if (!success) { + revert TargetExecFailed(target, value, payload); + } + } + + // Emit received message + emit MessageReceived(governor, data); + } +} \ No newline at end of file diff --git a/contracts/bridges/test/MockTimelock.sol b/contracts/bridges/test/MockTimelock.sol index 67c7625..031174e 100644 --- a/contracts/bridges/test/MockTimelock.sol +++ b/contracts/bridges/test/MockTimelock.sol @@ -1,11 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -// IFxStateSender interface -interface IFxStateSender { - function sendMessageToChild(address _receiver, bytes calldata _data) external; -} - error ExecFailed(address fxRoot, bytes payload); /// @title MockTimelock - Mock of Timelock contract on the L1 side @@ -21,8 +16,8 @@ contract MockTimelock { /// @dev Executes the payload at the Fx Root address. /// @param payload Bytes of payload. - function execute(bytes memory payload) external { - (bool success, ) = fxRoot.call(payload); + function execute(bytes memory payload) external payable { + (bool success, ) = fxRoot.call{value: msg.value}(payload); if (!success) { revert ExecFailed(fxRoot, payload); } diff --git a/hardhat.config.js b/hardhat.config.js index 856da32..b949194 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -13,9 +13,8 @@ require("@nomicfoundation/hardhat-toolbox"); const ALCHEMY_API_KEY_MAINNET = process.env.ALCHEMY_API_KEY_MAINNET; const ALCHEMY_API_KEY_MATIC = process.env.ALCHEMY_API_KEY_MATIC; const ALCHEMY_API_KEY_GOERLI = process.env.ALCHEMY_API_KEY_GOERLI; +const ALCHEMY_API_KEY_SEPOLIA = process.env.ALCHEMY_API_KEY_SEPOLIA; const ALCHEMY_API_KEY_MUMBAI = process.env.ALCHEMY_API_KEY_MUMBAI; -const GNOSIS_CHAIN_API_KEY = process.env.GNOSIS_CHAIN_API_KEY; -const CHIADO_CHAIN_API_KEY = "10200"; let TESTNET_MNEMONIC = process.env.TESTNET_MNEMONIC; const accounts = { @@ -33,6 +32,11 @@ if (!TESTNET_MNEMONIC) { const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY; const POLYGONSCAN_API_KEY = process.env.POLYGONSCAN_API_KEY; +const GNOSISSCAN_API_KEY = process.env.GNOSISSCAN_API_KEY; +const ARBISCAN_API_KEY = process.env.ARBISCAN_API_KEY; +const OPSCAN_API_KEY = process.env.OPSCAN_API_KEY; +const BASESCAN_API_KEY = process.env.BASESCAN_API_KEY; +const CELOSCAN_API_KEY = process.env.CELOSCAN_API_KEY; module.exports = { networks: { @@ -54,11 +58,36 @@ module.exports = { accounts: accounts, chainId: 100, }, + arbitrumOne: { + url: "https://arb1.arbitrum.io/rpc", + accounts: accounts, + chainId: 42161, + }, + optimistic: { + url: "https://optimism.drpc.org", + accounts: accounts, + chainId: 10, + }, + base: { + url: "https://mainnet.base.org", + accounts: accounts, + chainId: 8453, + }, + celo: { + url: "https://forno.celo.org", + accounts: accounts, + chainId: 42220, + }, goerli: { url: "https://eth-goerli.g.alchemy.com/v2/" + ALCHEMY_API_KEY_GOERLI, chainId: 5, accounts: accounts, }, + sepolia: { + url: "https://eth-sepolia.g.alchemy.com/v2/" + ALCHEMY_API_KEY_SEPOLIA, + accounts: accounts, + chainId: 11155111, + }, polygonMumbai: { url: "https://polygon-mumbai.g.alchemy.com/v2/" + ALCHEMY_API_KEY_MUMBAI, accounts: accounts, @@ -67,6 +96,26 @@ module.exports = { url: "https://rpc.chiadochain.net", accounts: accounts, }, + arbitrumSepolia: { + url: "https://sepolia-rollup.arbitrum.io/rpc", + accounts: accounts, + chainId: 421614, + }, + optimisticSepolia: { + url: "https://sepolia.optimism.io", + accounts: accounts, + chainId: 11155420, + }, + baseSepolia: { + url: "https://sepolia.base.org", + accounts: accounts, + chainId: 84532, + }, + celoAlfajores: { + url: "https://alfajores-forno.celo-testnet.org", + accounts: accounts, + chainId: 44787, + }, hardhat: { allowUnlimitedContractSize: true }, @@ -77,8 +126,8 @@ module.exports = { network: "chiado", chainId: 10200, urls: { - apiURL: "https://blockscout.com/gnosis/chiado/api", - browserURL: "https://blockscout.com/gnosis/chiado", + apiURL: "https://gnosis-chiado.blockscout.com/api", + browserURL: "https://gnosis-chiado.blockscout.com/", }, }, { @@ -89,14 +138,79 @@ module.exports = { browserURL: "https://gnosisscan.io/" }, }, + { + network: "arbitrumSepolia", + chainId: 421614, + urls: { + apiURL: "https://api-sepolia.arbiscan.io/api", + browserURL: "https://sepolia.arbiscan.io" + }, + }, + { + network: "optimistic", + chainId: 10, + urls: { + apiURL: "https://api-optimistic.etherscan.io/api", + browserURL: "https://sepolia-optimistic.etherscan.io" + }, + }, + { + network: "optimisticSepolia", + chainId: 11155420, + urls: { + apiURL: "https://api-sepolia-optimism.etherscan.io/api", + browserURL: "https://sepolia-optimistic.etherscan.io" + }, + }, + { + network: "base", + chainId: 8453, + urls: { + apiURL: "https://api.basescan.org/api", + browserURL: "https://basescan.org" + }, + }, + { + network: "baseSepolia", + chainId: 84532, + urls: { + apiURL: "https://base-sepolia.blockscout.com/api", + browserURL: "https://base-sepolia.blockscout.com/" + }, + }, + { + network: "celo", + chainId: 42220, + urls: { + apiURL: "https://api.celoscan.io/api", + browserURL: "https://explorer.celo.org/" + }, + }, + { + network: "celoAlfajores", + chainId: 44787, + urls: { + apiURL: "https://api-alfajores.celoscan.io/api", + browserURL: "https://alfajores-blockscout.celo-testnet.org/" + }, + }, ], apiKey: { mainnet: ETHERSCAN_API_KEY, polygon: POLYGONSCAN_API_KEY, - gnosis: GNOSIS_CHAIN_API_KEY, + gnosis: GNOSISSCAN_API_KEY, + arbitrumOne: ARBISCAN_API_KEY, + optimistic: OPSCAN_API_KEY, + base: BASESCAN_API_KEY, + celo: CELOSCAN_API_KEY, goerli: ETHERSCAN_API_KEY, + sepolia: ETHERSCAN_API_KEY, polygonMumbai: POLYGONSCAN_API_KEY, - chiado: CHIADO_CHAIN_API_KEY, + chiado: GNOSISSCAN_API_KEY, + arbitrumSepolia: ARBISCAN_API_KEY, + optimisticSepolia: OPSCAN_API_KEY, + baseSepolia: OPSCAN_API_KEY, + celoAlfajores: CELOSCAN_API_KEY } }, solidity: { diff --git a/scripts/deployment/bridges/gnosis/deploy_01_home_mediator.js b/scripts/deployment/bridges/gnosis/deploy_01_home_mediator.js index 79c15bc..116b91c 100644 --- a/scripts/deployment/bridges/gnosis/deploy_01_home_mediator.js +++ b/scripts/deployment/bridges/gnosis/deploy_01_home_mediator.js @@ -16,8 +16,8 @@ async function main() { let networkURL; if (providerName === "gnosis") { - if (!process.env.GNOSIS_CHAIN_API_KEY) { - console.log("set GNOSIS_CHAIN_API_KEY env variable"); + if (!process.env.GNOSISSCAN_API_KEY) { + console.log("set GNOSISSCAN_API_KEY env variable"); return; } networkURL = "https://rpc.gnosischain.com"; diff --git a/scripts/deployment/bridges/gnosis/test/deploy_01_home_mediator_test.js b/scripts/deployment/bridges/gnosis/test/deploy_01_home_mediator_test.js index 3867213..cc1dfe2 100644 --- a/scripts/deployment/bridges/gnosis/test/deploy_01_home_mediator_test.js +++ b/scripts/deployment/bridges/gnosis/test/deploy_01_home_mediator_test.js @@ -16,8 +16,8 @@ async function main() { let networkURL; if (providerName === "gnosis") { - if (!process.env.GNOSIS_CHAIN_API_KEY) { - console.log("set GNOSIS_CHAIN_API_KEY env variable"); + if (!process.env.GNOSISSCAN_API_KEY) { + console.log("set GNOSISSCAN_API_KEY env variable"); return; } networkURL = "https://rpc.gnosischain.com"; diff --git a/scripts/deployment/bridges/gnosis/test/deploy_02_child_mock_erc20.js b/scripts/deployment/bridges/gnosis/test/deploy_02_child_mock_erc20.js index 6521783..48cbc30 100644 --- a/scripts/deployment/bridges/gnosis/test/deploy_02_child_mock_erc20.js +++ b/scripts/deployment/bridges/gnosis/test/deploy_02_child_mock_erc20.js @@ -16,8 +16,8 @@ async function main() { let networkURL; if (providerName === "gnosis") { - if (!process.env.GNOSIS_CHAIN_API_KEY) { - console.log("set GNOSIS_CHAIN_API_KEY env variable"); + if (!process.env.GNOSISSCAN_API_KEY) { + console.log("set GNOSISSCAN_API_KEY env variable"); return; } networkURL = "https://rpc.gnosischain.com"; diff --git a/scripts/deployment/bridges/optimistic/README.md b/scripts/deployment/bridges/optimistic/README.md new file mode 100644 index 0000000..da2ca8e --- /dev/null +++ b/scripts/deployment/bridges/optimistic/README.md @@ -0,0 +1,47 @@ +# Bridge-related deployment scripts +This process is the same as described in the original deployment procedure: [deployment](https://github.com/valory-xyz/autonolas-governance/blob/main/scripts/deployment). + +## Steps to engage +The project has submodules to get the dependencies. Make sure you run `git clone --recursive` or init the submodules yourself. +The dependency list is managed by the `package.json` file, and the setup parameters are stored in the `hardhat.config.js` file. +Simply run the following command to install the project: +``` +yarn install +``` +command and compiled with the +``` +npx hardhat compile +``` + +Create a `globals.json` file in the root folder, or copy it from the file with pre-defined parameters (i.e., `scripts/deployment/bridges/optimistic/globals_optimistic_sepolia.json` for the chiado testnet). + +Parameters of the `globals.json` file: +- `contractVerification`: flag for verifying contracts in deployment scripts (`true`) or skipping it (`false`); +- `useLedger`: flag whether to use the hardware wallet (`true`) or proceed with the seed-phrase accounts (`false`); +- `derivationPath`: string with the derivation path; +- `gasPriceInGwei`: gas price in Gwei; +- `L2CrossDomainMessengerAddress`: (Optimism) CFM Contract Proxy address serving as a system processor of inbound calls across the bridge; +- `timelockAddress`: Timelock address on the root L1 network; + +The script file name identifies the number of deployment steps taken up to the number in the file name. + +Export network-related API keys defined in `hardhat.config.js` file that correspond to the required network. + +To run the script, use the following command: +`npx hardhat run scripts/deployment/bridges/script_name --network network_type`, +where `script_number_and_name` is a script number and name, i.e. `deploy_01_home_mediator.js`, `network_type` is a network type corresponding to the `hardhat.config.js` network configuration. + +## Validity checks and contract verification +Each script controls the obtained values by checking them against the expected ones. Also, each script has a contract verification procedure. +If a contract is deployed with arguments, these arguments are taken from the corresponding `verify_number_and_name` file, where `number_and_name` corresponds to the deployment script number and name. + +## Data packing for cross-bridge transactions +In order to correctly pack the data and supply it to the Timelock such that it is correctly processed across the bridge, +use the following script: [cross-bridge data packing](https://github.com/valory-xyz/autonolas-governance/blob/main/scripts/deployment/bridges/pack-data.js). + + + + + + + diff --git a/scripts/deployment/bridges/optimistic/deploy_01_optimism_messenger.js b/scripts/deployment/bridges/optimistic/deploy_01_optimism_messenger.js new file mode 100644 index 0000000..e97f529 --- /dev/null +++ b/scripts/deployment/bridges/optimistic/deploy_01_optimism_messenger.js @@ -0,0 +1,59 @@ +/*global process*/ + +const { ethers } = require("hardhat"); +const { LedgerSigner } = require("@anders-t/ethers-ledger"); + +async function main() { + const fs = require("fs"); + const globalsFile = "globals.json"; + const dataFromJSON = fs.readFileSync(globalsFile, "utf8"); + let parsedData = JSON.parse(dataFromJSON); + const useLedger = parsedData.useLedger; + const derivationPath = parsedData.derivationPath; + const providerName = parsedData.providerName; + const gasPriceInGwei = parsedData.gasPriceInGwei; + + const networkURL = parsedData.networkURL; + const provider = new ethers.providers.JsonRpcProvider(networkURL); + const signers = await ethers.getSigners(); + + let EOA; + if (useLedger) { + EOA = new LedgerSigner(provider, derivationPath); + } else { + EOA = signers[0]; + } + // EOA address + const deployer = await EOA.getAddress(); + console.log("EOA is:", deployer); + + // Transaction signing and execution + console.log("1. EOA to deploy home mediator contract"); + const OptimismMessenger = await ethers.getContractFactory("OptimismMessenger"); + console.log("You are signing the following transaction: OptimismMessenger.connect(EOA).deploy(L2CrossDomainMessengerAddress, timelockAddress)"); + const gasPrice = ethers.utils.parseUnits(gasPriceInGwei, "gwei"); + const optimismMessenger = await OptimismMessenger.connect(EOA).deploy(parsedData.L2CrossDomainMessengerAddress, parsedData.timelockAddress, { gasPrice }); + const result = await optimismMessenger.deployed(); + + // Transaction details + console.log("Contract deployment: OptimismMessenger"); + console.log("Contract address:", optimismMessenger.address); + console.log("Transaction:", result.deployTransaction.hash); + + // Writing updated parameters back to the JSON file + parsedData.optimismMessengerAddress = optimismMessenger.address; + fs.writeFileSync(globalsFile, JSON.stringify(parsedData)); + + // Contract verification + if (parsedData.contractVerification) { + const execSync = require("child_process").execSync; + execSync("npx hardhat verify --constructor-args scripts/deployment/bridges/optimistic/verify_01_home_mediator.js --network " + providerName + " " + optimismMessenger.address, { encoding: "utf-8" }); + } +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/deployment/bridges/optimistic/globals_optimistic_mainnet.json b/scripts/deployment/bridges/optimistic/globals_optimistic_mainnet.json new file mode 100644 index 0000000..797dc2a --- /dev/null +++ b/scripts/deployment/bridges/optimistic/globals_optimistic_mainnet.json @@ -0,0 +1 @@ +{"contractVerification":true,"useLedger":true,"derivationPath":"m/44'/60'/2'/0/0","providerName":"optimistic","gasPriceInGwei":"2","AMBContractProxyHomeAddress":"0x75Df5AF045d91108662D8080fD1FEFAd6aA0bb59","timelockAddress":"0x3C1fF68f5aa342D296d4DEe4Bb1cACCA912D95fE","homeMediatorAddress":"0x15bd56669F57192a97dF41A2aa8f4403e9491776"} \ No newline at end of file diff --git a/scripts/deployment/bridges/optimistic/globals_optimistic_sepolia.json b/scripts/deployment/bridges/optimistic/globals_optimistic_sepolia.json new file mode 100644 index 0000000..cef8c5b --- /dev/null +++ b/scripts/deployment/bridges/optimistic/globals_optimistic_sepolia.json @@ -0,0 +1 @@ +{"contractVerification":true,"useLedger":false,"derivationPath":"m/44'/60'/2'/0/0","providerName":"optimisticSepolia","gasPriceInGwei":"2","L1CrossDomainMessengerProxyAddress":"0x58Cc85b8D04EA49cC6DBd3CbFFd00B4B8D6cb3ef","timelockAddress":"0x43d28764bB39936185c84906983fB57A8A905a4F","L2CrossDomainMessengerAddress":"0x4200000000000000000000000000000000000007","optimismMessengerAddress":"0x670Ac235EE13C0B2a5065282bBB0c61cfB354592"} \ No newline at end of file diff --git a/scripts/deployment/bridges/optimistic/test/deploy_00_mock_timelock.js b/scripts/deployment/bridges/optimistic/test/deploy_00_mock_timelock.js new file mode 100644 index 0000000..73515f0 --- /dev/null +++ b/scripts/deployment/bridges/optimistic/test/deploy_00_mock_timelock.js @@ -0,0 +1,57 @@ +/*global process*/ + +const { ethers } = require("hardhat"); +const { LedgerSigner } = require("@anders-t/ethers-ledger"); + +async function main() { + const fs = require("fs"); + const globalsFile = "globals.json"; + const dataFromJSON = fs.readFileSync(globalsFile, "utf8"); + let parsedData = JSON.parse(dataFromJSON); + const useLedger = parsedData.useLedger; + const derivationPath = parsedData.derivationPath; + const providerName = "sepolia"; + const gasPriceInGwei = parsedData.gasPriceInGwei; + let EOA; + + const provider = await ethers.providers.getDefaultProvider(providerName); + const signers = await ethers.getSigners(); + + if (useLedger) { + EOA = new LedgerSigner(provider, derivationPath); + } else { + EOA = signers[0]; + } + // EOA address + const deployer = await EOA.getAddress(); + console.log("EOA is:", deployer); + + // Transaction signing and execution + console.log("1. EOA to deploy mock timelock contract"); + const Timelock = await ethers.getContractFactory("MockTimelock"); + console.log("You are signing the following transaction: Timelock.connect(EOA).deploy(L1CrossDomainMessengerProxy)"); + const timelock = await Timelock.connect(EOA).deploy(parsedData.L1CrossDomainMessengerProxyAddress); + const result = await timelock.deployed(); + + // Transaction details + console.log("Contract deployment: MockTimelock"); + console.log("Contract address:", timelock.address); + console.log("Transaction:", result.deployTransaction.hash); + + // Writing updated parameters back to the JSON file + parsedData.timelockAddress = timelock.address; + fs.writeFileSync(globalsFile, JSON.stringify(parsedData)); + + // Contract verification + if (parsedData.contractVerification) { + const execSync = require("child_process").execSync; + execSync("npx hardhat verify --constructor-args scripts/deployment/bridges/optimistic/test/verify_00_mock_timelock.js --network " + providerName + " " + timelock.address, { encoding: "utf-8" }); + } +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/deployment/bridges/optimistic/test/deploy_02_child_mock_erc20.js b/scripts/deployment/bridges/optimistic/test/deploy_02_child_mock_erc20.js new file mode 100644 index 0000000..1137090 --- /dev/null +++ b/scripts/deployment/bridges/optimistic/test/deploy_02_child_mock_erc20.js @@ -0,0 +1,63 @@ +/*global process*/ + +const { ethers } = require("hardhat"); +const { LedgerSigner } = require("@anders-t/ethers-ledger"); + +async function main() { + const fs = require("fs"); + const globalsFile = "globals.json"; + const dataFromJSON = fs.readFileSync(globalsFile, "utf8"); + let parsedData = JSON.parse(dataFromJSON); + const useLedger = parsedData.useLedger; + const derivationPath = parsedData.derivationPath; + const providerName = parsedData.providerName; + const gasPriceInGwei = parsedData.gasPriceInGwei; + + const networkURL = parsedData.networkURL; + const provider = new ethers.providers.JsonRpcProvider(networkURL); + const signers = await ethers.getSigners(); + + let EOA; + if (useLedger) { + EOA = new LedgerSigner(provider, derivationPath); + } else { + EOA = signers[0]; + } + // EOA address + const deployer = await EOA.getAddress(); + console.log("EOA is:", deployer); + + // Transaction signing and execution + console.log("2. EOA to deploy child mock ERC20 contract and change its owner to the FxGovernorTunnel"); + const ChildMockERC20 = await ethers.getContractFactory("ChildMockERC20"); + console.log("You are signing the following transaction: ChildMockERC20.connect(EOA).deploy()"); + const gasPrice = ethers.utils.parseUnits(gasPriceInGwei, "gwei"); + const childMockERC20 = await ChildMockERC20.connect(EOA).deploy({ gasPrice }); + let result = await childMockERC20.deployed(); + + // Transaction details + console.log("Contract deployment: ChildMockERC20"); + console.log("Contract address:", childMockERC20.address); + console.log("Transaction:", result.deployTransaction.hash); + + // Writing updated parameters back to the JSON file + parsedData.childMockERC20Address = childMockERC20.address; + fs.writeFileSync(globalsFile, JSON.stringify(parsedData)); + + // Change the owner of the contract + result = await childMockERC20.changeOwner(parsedData.optimismMessengerAddress, { gasPrice }); + console.log("Transaction:", result.hash); + + // Contract verification + if (parsedData.contractVerification) { + const execSync = require("child_process").execSync; + execSync("npx hardhat verify --network " + providerName + " " + childMockERC20.address, { encoding: "utf-8" }); + } +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/deployment/bridges/optimistic/test/globals.json b/scripts/deployment/bridges/optimistic/test/globals.json new file mode 100644 index 0000000..b6fcc83 --- /dev/null +++ b/scripts/deployment/bridges/optimistic/test/globals.json @@ -0,0 +1 @@ +{"contractVerification":true,"useLedger":false,"derivationPath":"m/44'/60'/2'/0/0","providerName":"optimisticSepolia","gasPriceInGwei":"2","networkURL":"https://sepolia.optimism.io","L1CrossDomainMessengerProxyAddress":"0x58Cc85b8D04EA49cC6DBd3CbFFd00B4B8D6cb3ef","timelockAddress":"0x43d28764bB39936185c84906983fB57A8A905a4F","L2CrossDomainMessengerAddress":"0x4200000000000000000000000000000000000007","optimismMessengerAddress":"0x670Ac235EE13C0B2a5065282bBB0c61cfB354592","childMockERC20Address":"0x17806E2a12d5E0F48C9803cd397DB3F044DA3b77"} \ No newline at end of file diff --git a/scripts/deployment/bridges/optimistic/test/messenger_sepolia_sepolia_governor.js b/scripts/deployment/bridges/optimistic/test/messenger_sepolia_sepolia_governor.js new file mode 100644 index 0000000..b11bbbf --- /dev/null +++ b/scripts/deployment/bridges/optimistic/test/messenger_sepolia_sepolia_governor.js @@ -0,0 +1,166 @@ +/*global process*/ + +const { ethers } = require("ethers"); + +const sendFundsFromL1 = true; + +async function main() { + const ALCHEMY_API_KEY_SEPOLIA = process.env.ALCHEMY_API_KEY_SEPOLIA; + const sepoliaURL = "https://eth-sepolia.g.alchemy.com/v2/" + ALCHEMY_API_KEY_SEPOLIA; + const sepoliaProvider = new ethers.providers.JsonRpcProvider(sepoliaURL); + await sepoliaProvider.getBlockNumber().then((result) => { + console.log("Current block number sepolia: " + result); + }); + + const optimisticSepoliaURL = "https://sepolia.optimism.io"; + const optimisticSepoliaProvider = new ethers.providers.JsonRpcProvider(optimisticSepoliaURL); + await optimisticSepoliaProvider.getBlockNumber().then((result) => { + console.log("Current block number optimisticSepolia: " + result); + }); + + const fs = require("fs"); + // CDMProxy address on sepolia + const CDMProxyAddress = "0x58Cc85b8D04EA49cC6DBd3CbFFd00B4B8D6cb3ef"; + const CDMProxyJSON = "abis/test/L1CrossDomainMessenger.json"; + let contractFromJSON = fs.readFileSync(CDMProxyJSON, "utf8"); + const CDMProxyABI = JSON.parse(contractFromJSON); + const CDMProxy = new ethers.Contract(CDMProxyAddress, CDMProxyABI, sepoliaProvider); + + // Test deployed OptimismMessenger address on optimisticSepolia + const optimismMessengerAddress = "0x670Ac235EE13C0B2a5065282bBB0c61cfB354592"; + const optimismMessengerJSON = "artifacts/contracts/bridges/OptimismMessenger.sol/OptimismMessenger.json"; + contractFromJSON = fs.readFileSync(optimismMessengerJSON, "utf8"); + let parsedFile = JSON.parse(contractFromJSON); + const optimismMessengerABI = parsedFile["abi"]; + const optimismMessenger = new ethers.Contract(optimismMessengerAddress, optimismMessengerABI, optimisticSepoliaProvider); + + // Mock Timelock contract address on sepolia (has CDMProxy address in it already) + const mockTimelockAddress = "0x43d28764bB39936185c84906983fB57A8A905a4F"; + const mockTimelockJSON = "artifacts/contracts/bridges/test/MockTimelock.sol/MockTimelock.json"; + contractFromJSON = fs.readFileSync(mockTimelockJSON, "utf8"); + parsedFile = JSON.parse(contractFromJSON); + const mockTimelockABI = parsedFile["abi"]; + const mockTimelock = new ethers.Contract(mockTimelockAddress, mockTimelockABI, sepoliaProvider); + + // ChildMockERC20 address on optimisticSepolia + const mockChildERC20Address = "0x17806E2a12d5E0F48C9803cd397DB3F044DA3b77"; + const mockChildERC20JSON = "artifacts/contracts/bridges/test/ChildMockERC20.sol/ChildMockERC20.json"; + contractFromJSON = fs.readFileSync(mockChildERC20JSON, "utf8"); + parsedFile = JSON.parse(contractFromJSON); + const mockChildERC20ABI = parsedFile["abi"]; + const mockChildERC20 = new ethers.Contract(mockChildERC20Address, mockChildERC20ABI, optimisticSepoliaProvider); + + // Get the EOA + const account = ethers.utils.HDNode.fromMnemonic(process.env.TESTNET_MNEMONIC).derivePath("m/44'/60'/0'/0/0"); + const EOAsepolia = new ethers.Wallet(account, sepoliaProvider); + const EOAoptimisticSepolia = new ethers.Wallet(account, optimisticSepoliaProvider); + console.log("EOA address",EOAsepolia.address); + if (EOAoptimisticSepolia.address == EOAsepolia.address) { + console.log("Correct wallet setup"); + } + + // Amount of xETH to send + const amountToSend = ethers.utils.parseEther("0.0001"); + // Amount of ERC20 token to mint + const amountToMint = 100; + + // Send funds to the OptimismMessenger contract + let tx; + if (!sendFundsFromL1) { + // Feed the contract with funds on the L2 side + tx = await EOAoptimisticSepolia.sendTransaction({to: optimismMessenger.address, value: amountToSend}); + console.log("Send xETH hash", tx.hash); + await tx.wait(); + } + + // Pack the first part of with the zero payload + let target = EOAoptimisticSepolia.address; + let value = amountToSend; + const payloadLength = 0; + let data = ethers.utils.solidityPack( + ["address", "uint96", "uint32"], + [target, value, payloadLength] + ); + + // Mock Token contract across the bridge must mint 100 OLAS for the deployer + const rawPayload = mockChildERC20.interface.encodeFunctionData("mint", [EOAoptimisticSepolia.address, amountToMint]); + // Pack the second part of data + target = mockChildERC20Address; + value = 0; + const payload = ethers.utils.arrayify(rawPayload); + data += ethers.utils.solidityPack( + ["address", "uint96", "uint32", "bytes"], + [target, value, payload.length, payload] + ).slice(2); + + // Balance of mock tokens before the cross-bridge transaction + const balanceERC20Before = Number(await mockChildERC20.balanceOf(EOAoptimisticSepolia.address)); + // Balance of xETH of the OptimismMessenger before the cross-bridge transaction + const balanceETHBefore = await optimisticSepoliaProvider.getBalance(EOAoptimisticSepolia.address); + + // Build the final payload to be passed from the imaginary Timelock + const messengerPayload = await optimismMessenger.interface.encodeFunctionData("processMessageFromForeign", [data]); + const minGasLimit = "2000000"; + const timelockPayload = await CDMProxy.interface.encodeFunctionData("sendMessage", [optimismMessengerAddress, + messengerPayload, minGasLimit]); + + // Send the message to optimisticSepolia receiver + if (!sendFundsFromL1) { + // Funds are not sent from the L1 side, so if the value in payload is non-zero - make sure the L2 contract is fed + const gasLimit = "500000"; + tx = await mockTimelock.connect(EOAsepolia).execute(timelockPayload, {gasLimit }); + console.log("Timelock data execution hash", tx.hash); + await tx.wait(); + } else { + // If one wants to sent ETH along with the tx, they need to provide much more start up gas limit + // along with the transferred value, as there are more value specific calculations required pre-transfer + const gasLimit = "5000000"; + tx = await mockTimelock.connect(EOAsepolia).execute(timelockPayload, { value: amountToSend, gasLimit }); + console.log("Timelock data execution hash", tx.hash); + await tx.wait(); + } + + // Wait for the event of a processed data on optimisticSepolia + // catch NewFxMessage event from mockChildERC20 and MessageReceived event from optimismMessenger + // Compare the data sent and the data from the NewFxMessage event that must match + // MessageReceived(uint256 indexed stateId, address indexed sender, bytes message) + let waitForEvent = true; + while (waitForEvent) { + // Check for the last 100 blocks in order to catch the event + const events = await optimismMessenger.queryFilter("MessageReceived", -200); + events.forEach((item) => { + const msg = item["args"]["data"]; + if (msg == data) { + console.log("Event MessageReceived. Message in optimisticSepolia:", msg); + waitForEvent = false; + } + }); + // Continue waiting for the event if none was received + if (waitForEvent) { + console.log("Waiting for the receive event, next update in 5 minutes ..."); + // Sleep for a minute + await new Promise(r => setTimeout(r, 300000)); + } + } + + // Balance of ERC20 token after the cross-bridge transaction + const balanceERC20After = Number(await mockChildERC20.balanceOf(EOAoptimisticSepolia.address)); + const balanceERC20Diff = balanceERC20After - balanceERC20Before; + if (balanceERC20Diff == amountToMint) { + console.log("Successfully minted MockChildERC20"); + } + + // Balance of xETH of the OptimismMessenger after the cross-bridge transaction + const balanceETHAfter = await optimisticSepoliaProvider.getBalance(EOAoptimisticSepolia.address); + const balanceETHDiff = balanceETHAfter - balanceETHBefore; + if (balanceETHDiff == amountToSend) { + console.log("Successfully sent xETH"); + } +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/deployment/bridges/optimistic/test/verify_00_mock_timelock.js b/scripts/deployment/bridges/optimistic/test/verify_00_mock_timelock.js new file mode 100644 index 0000000..77cb69c --- /dev/null +++ b/scripts/deployment/bridges/optimistic/test/verify_00_mock_timelock.js @@ -0,0 +1,8 @@ +const fs = require("fs"); +const globalsFile = "globals.json"; +const dataFromJSON = fs.readFileSync(globalsFile, "utf8"); +const parsedData = JSON.parse(dataFromJSON); + +module.exports = [ + parsedData.L1CrossDomainMessengerProxyAddress +]; \ No newline at end of file diff --git a/scripts/deployment/bridges/optimistic/verify_01_home_mediator.js b/scripts/deployment/bridges/optimistic/verify_01_home_mediator.js new file mode 100644 index 0000000..e42543b --- /dev/null +++ b/scripts/deployment/bridges/optimistic/verify_01_home_mediator.js @@ -0,0 +1,9 @@ +const fs = require("fs"); +const globalsFile = "globals.json"; +const dataFromJSON = fs.readFileSync(globalsFile, "utf8"); +const parsedData = JSON.parse(dataFromJSON); + +module.exports = [ + parsedData.L2CrossDomainMessengerAddress, + parsedData.timelockAddress +]; \ No newline at end of file