diff --git a/.github/workflows/pr-test-forge.yaml b/.github/workflows/pr-test-forge.yaml new file mode 100644 index 000000000..1660ccd0b --- /dev/null +++ b/.github/workflows/pr-test-forge.yaml @@ -0,0 +1,46 @@ +name: forge + +on: + push: + branches: + - master + - main + - staging + - release-v* + pull_request: + paths: + - '.github/workflows/pr-test-forge.yaml' + - 'analog-gmp/**' + - 'lib/**' +env: + FOUNDRY_PROFILE: ci + +jobs: + check: + strategy: + fail-fast: true + + name: Foundry project + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Run Forge build + run: | + cd analog-gmp + forge --version + forge build --sizes + id: build + + - name: Run Forge tests + run: | + cd analog-gmp + forge test -vvv + id: test diff --git a/.gitmodules b/.gitmodules index e61165e00..70a5f7c26 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,15 @@ -[submodule "lib/forge-std"] - path = lib/forge-std +[submodule "analog-gmp/lib/forge-std"] + path = analog-gmp/lib/forge-std url = https://github.com/foundry-rs/forge-std -[submodule "lib/frost-evm"] - path = lib/frost-evm - url = https://github.com/analog-labs/frost-evm -[submodule "analog-gmp"] - path = analog-gmp - url = git@github.com:Analog-Labs/analog-gmp.git +[submodule "analog-gmp/lib/frost-evm"] + path = analog-gmp/lib/frost-evm + url = https://github.com/Analog-Labs/frost-evm +[submodule "analog-gmp/lib/universal-factory"] + path = analog-gmp/lib/universal-factory + url = https://github.com/Analog-Labs/universal-factory +[submodule "analog-gmp/lib/solmate"] + path = analog-gmp/lib/solmate + url = https://github.com/transmissions11/solmate +[submodule "analog-gmp/lib/evm-interpreter"] + path = analog-gmp/lib/evm-interpreter + url = https://github.com/Analog-Labs/evm-interpreter diff --git a/analog-gmp b/analog-gmp deleted file mode 160000 index 8db5a430f..000000000 --- a/analog-gmp +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8db5a430f298ddfa8aa58aaf3c2294609c91b84a diff --git a/analog-gmp/.env.example b/analog-gmp/.env.example new file mode 100644 index 000000000..c6ec7128d --- /dev/null +++ b/analog-gmp/.env.example @@ -0,0 +1,7 @@ +PRIVATE_KEY= +PROXY_ADDRESS=0x000000033763b9d6d94efd3209dc255686aa8fba +PROXY_DEPLOYER=0x9020e86C34Da64C78fd290eBee2E171C402F6890 +#PROXY_ADDRESS=0x000000007f56768de3133034fa730a909003a165 +SEPOLIA_RPC_URL=https://ethereum-sepolia-rpc.publicnode.com +SHIBUYA_RPC_URL=https://evm.shibuya.astar.network +POLYGON_AMOY_RPC_URL=https://rpc-amoy.polygon.technology diff --git a/analog-gmp/.gitignore b/analog-gmp/.gitignore new file mode 100644 index 000000000..ded856a92 --- /dev/null +++ b/analog-gmp/.gitignore @@ -0,0 +1,20 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env + +# MacOS +**/.DS_Store + +# built artifacts +src/artifacts/ diff --git a/analog-gmp/LICENSE b/analog-gmp/LICENSE new file mode 100644 index 000000000..93d9a107a --- /dev/null +++ b/analog-gmp/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2016-2024 Zeppelin Group Ltd and contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/analog-gmp/README.md b/analog-gmp/README.md new file mode 100644 index 000000000..caf02622f --- /dev/null +++ b/analog-gmp/README.md @@ -0,0 +1,53 @@ +## Analog's Contracts + +**This repository contains all the necessary ingredients for successful cross-chain development utilizing the Analog General Message Passing protocol.** + +## Dependencies + +This project uses **Forge** Ethereum testing framework (like Truffle, Hardhat and DappTools). +Install instructions: https://book.getfoundry.sh/ + +## Usage + +### Build + +```sh +forge build +``` + +### Test + +```sh +forge test +``` + +### Format + +```sh +forge fmt +``` + +### Gas Snapshots + +```sh +forge snapshot +``` + +## Documentation + +Use the following command to generate project documentation: + +```sh +forge doc -b +``` + +You can now read the docs from the generated [mdbook](https://github.com/rust-lang/mdBook) as follows: + +``` sh +cd docs +mdbook serve --open +``` + +## License + +Analog's Contracts is released under the [MIT License](LICENSE). diff --git a/analog-gmp/foundry.toml b/analog-gmp/foundry.toml new file mode 100644 index 000000000..82cb75a8d --- /dev/null +++ b/analog-gmp/foundry.toml @@ -0,0 +1,75 @@ +[profile.default] +src = "src" +test = "test" +out = "out" +libs = ["lib"] +# Permissions +fs_permissions = [{ access = "read", path = "./lib/universal-factory/abi" }] + +######## +# Lint # +######## +deny_warnings = true + +################ +# Solc options # +################ +solc = '0.8.28' +# Using `shanghai` once other EVM chains such as `Astar/Shibuya` and +# `Ethereum Classic` doesn't support `cancun` yet. +# - https://github.com/rust-ethereum/evm/issues/290 +# - https://ethereumclassic.org/knowledge/history +evm_version = 'shanghai' +optimizer = true +optimizer_runs = 200000 + +############### +# EVM options # +############### +gas_limit = 30000000 +gas_price = 1 +block_base_fee_per_gas = 0 +block_gas_limit = 30000000 + +##################### +# optimizer details # +##################### +[profile.default.optimizer_details] +yul = true +# The peephole optimizer is always on if no details are given, +# use details to switch it off. +peephole = true +# The inliner is always off if no details are given, +# use details to switch it on. +inliner = true +# The unused jumpdest remover is always on if no details are given, +# use details to switch it off. +jumpdest_remover = true +# Sometimes re-orders literals in commutative operations. +order_literals = true +# Removes duplicate code blocks +deduplicate = true +# Common subexpression elimination, this is the most complicated step but +# can also provide the largest gain. +cse = true +# Optimize representation of literal numbers and strings in code. +constant_optimizer = true +# Use unchecked arithmetic when incrementing the counter of for loops +# under certain circumstances. It is always on if no details are given. +simple_counter_for_loop_unchecked_increment = true + +# Fuzz tests options +[fuzz] +# Reduce the numbers of runs if fuzz tests takes too long in your machine. +runs = 2500 + +# When debuging fuzz tests, uncomment this seed to make tests reproducible. +# seed = "0xdeadbeefdeadbeefdeadbeefdeadbeef" + +# RPC endpoints +[rpc_endpoints] +sepolia = "https://ethereum-sepolia-rpc.publicnode.com" +shibuya = "https://evm.shibuya.astar.network" +amoy = "https://rpc-amoy.polygon.technology" +arbitrum_sepolia = "https://arbitrum-sepolia.gateway.tenderly.co" +bnb_testnet = "https://bsc-testnet-rpc.publicnode.com" diff --git a/analog-gmp/lib/evm-interpreter b/analog-gmp/lib/evm-interpreter new file mode 160000 index 000000000..75df08b2e --- /dev/null +++ b/analog-gmp/lib/evm-interpreter @@ -0,0 +1 @@ +Subproject commit 75df08b2e92510e2812c67fe6df79d4dd5e57806 diff --git a/analog-gmp/lib/forge-std b/analog-gmp/lib/forge-std new file mode 160000 index 000000000..bf909b22f --- /dev/null +++ b/analog-gmp/lib/forge-std @@ -0,0 +1 @@ +Subproject commit bf909b22fa55e244796dfa920c9639fdffa1c545 diff --git a/analog-gmp/lib/frost-evm b/analog-gmp/lib/frost-evm new file mode 160000 index 000000000..8f10a6e3b --- /dev/null +++ b/analog-gmp/lib/frost-evm @@ -0,0 +1 @@ +Subproject commit 8f10a6e3b9afaaf9523fdea2aeb1918919a7716e diff --git a/analog-gmp/lib/solmate b/analog-gmp/lib/solmate new file mode 160000 index 000000000..c93f7716c --- /dev/null +++ b/analog-gmp/lib/solmate @@ -0,0 +1 @@ +Subproject commit c93f7716c9909175d45f6ef80a34a650e2d24e56 diff --git a/analog-gmp/lib/universal-factory b/analog-gmp/lib/universal-factory new file mode 160000 index 000000000..f19228149 --- /dev/null +++ b/analog-gmp/lib/universal-factory @@ -0,0 +1 @@ +Subproject commit f19228149ab806f5c8b859c4d65d3625130fb850 diff --git a/analog-gmp/remappings.txt b/analog-gmp/remappings.txt new file mode 100644 index 000000000..bf85bb85a --- /dev/null +++ b/analog-gmp/remappings.txt @@ -0,0 +1,8 @@ +forge-std/=lib/forge-std/src/ +@frost-evm/=lib/frost-evm/sol/ +@solmate/=lib/solmate/src/ +ds-test/=lib/solmate/lib/ds-test/src/ +@universal-factory/=lib/universal-factory/src/ +@evm-interpreter/=lib/evm-interpreter/src/ +@analog-gmp/=src/ +@analog-gmp-testing/=test/ diff --git a/analog-gmp/run-migration.sh b/analog-gmp/run-migration.sh new file mode 100755 index 000000000..0a6256f04 --- /dev/null +++ b/analog-gmp/run-migration.sh @@ -0,0 +1,93 @@ +#!/bin/bash +set -e + +# By default, doesn't broadcast any transactions. +DRY_RUN=1 + +# Setup console colors +if test -t 1 && command -v tput >/dev/null 2>&1; then + ncolors=$(tput colors) + if test -n "${ncolors}" && test "${ncolors}" -ge 8; then + bold_color=$(tput bold) + green_color=$(tput setaf 2) + warn_color=$(tput setaf 3) + error_color=$(tput setaf 1) + reset_color=$(tput sgr0) + fi + # 72 used instead of 80 since that's the default of pr + ncols=$(tput cols) +fi +: "${ncols:=72}" + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + --migrate) + unset DRY_RUN + shift 1 + ;; + --proxy=*) + PROXY_ADDRESS="${i#*=}" + shift 1 + ;; + -pk=*|--private-key=*) + PRIVATE_KEY="${i#*=}" + shift 1 + ;; + --sepolia-rpc=*) + SEPOLIA_RPC_URL="${i#*=}" + shift 1 + ;; + --shibuya-rpc=*) + SHIBUYA_RPC_URL="${i#*=}" + shift 1 + ;; + --amoy-rpc=*) + POLYGON_AMOY_RPC_URL="${i#*=}" + shift 1 + ;; + *) + warn "Unknown argument: $1" + echo "Usage: $0 --pk= --proxy=PROXY_ADDRESS [--migrate] [--sepolia-rpc=] [--shibuya-rpc=] [--amoy-rpc=]" + ;; + esac +done + +# Load .env file +if [ -f .env ]; then + echo "Load .env file" + source .env +else + echo ".env file not found, run 'cp .env.example .env' and fill the values" +fi + +# Check if PRIVATE_KEY is set +if [ -z "${PRIVATE_KEY}" ]; then + echo "PRIVATE_KEY is not set" + exit 1 +fi + +# Check if PROXY_ADDRESS is set +if [ -z "${PROXY_ADDRESS}" ]; then + echo "PROXY_ADDRESS is not set" + exit 1 +fi + +# Set fork-url +PARAMS=(-vvvv) + +# Verify if the migration is going to be broadcasted +if [ -z "${DRY_RUN}" ]; then + read -r -p "running in broadcast mode, the transaction will be broadcasted, are you sure you want to continue? [y/n] " response + case "$response" in + [yY][eE][sS]|[yY]) + PARAMS+=(--broadcast) + ;; + *) + echo "running in dry-mode..." + ;; + esac +fi + +forge script ./scripts/Migrate.sol "${PARAMS[@]}" diff --git a/analog-gmp/scripts/Deploy.sol b/analog-gmp/scripts/Deploy.sol new file mode 100644 index 000000000..94d6c3e8f --- /dev/null +++ b/analog-gmp/scripts/Deploy.sol @@ -0,0 +1,505 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (scripts/Deploy.sol) + +pragma solidity ^0.8.0; + +import {FactoryUtils} from "@universal-factory/FactoryUtils.sol"; +import {IUniversalFactory} from "@universal-factory/IUniversalFactory.sol"; +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {IGateway} from "../src/interfaces/IGateway.sol"; +import {NetworkID, NetworkIDHelpers} from "../src/NetworkID.sol"; +import {ERC1967} from "../src/utils/ERC1967.sol"; +import {BranchlessMath} from "../src/utils/BranchlessMath.sol"; +import {UFloat9x56, UFloatMath} from "../src/utils/Float9x56.sol"; +import {GatewayProxy} from "../src/GatewayProxy.sol"; +import {Gateway} from "../src/Gateway.sol"; +import { + TssKey, + GmpMessage, + UpdateKeysMessage, + Signature, + Network, + GmpStatus, + GmpSender, + PrimitiveUtils +} from "../src/Primitives.sol"; + +/** + * @dev Message payload used to update the network info. + * @param networkId Domain EIP-712 - Replay Protection Mechanism. + * @param domainSeparator Domain EIP-712 - Replay Protection Mechanism. + * @param gasLimit The maximum amount of gas we allow on this particular network. + * @param relativeGasPrice Gas price of destination chain, in terms of the source chain token. + * @param baseFee Base fee for cross-chain message approval on destination, in terms of source native gas token. + * @param mortality maximum block in which this message is valid. + */ +struct UpdateNetworkInfo { + uint16 networkId; + bytes32 domainSeparator; + uint64 gasLimit; + UFloat9x56 relativeGasPrice; + uint128 baseFee; + uint64 mortality; +} + +contract MigrateGateway is Script { + using NetworkIDHelpers for NetworkID; + using FactoryUtils for IUniversalFactory; + + /** + * @dev The codehash of the proxy contract + */ + bytes32 internal constant PROXY_CODEHASH = 0x54afeb06256bce71659256132ac18f1515de3011aaec4fbd6fc7b0c00c7263d8; + + /** + * @dev The minimal balance required to deploy the proxy contract + */ + uint256 internal constant MINIMAL_DEPLOYER_BALANCE = 0.5 ether; + + /** + * @dev Universal Factory used to deploy the implementation contract + * see https://github.com/Analog-Labs/universal-factory/tree/main for mode details. + */ + IUniversalFactory internal constant FACTORY = IUniversalFactory(0x0000000000001C4Bf962dF86e38F0c10c7972C6E); + + /** + * @dev Bytecode hash of the Universal Factory, used to verify if the contract is deployed. + */ + bytes32 internal constant FACTORY_CODEHASH = 0x0dac89b851eaa2369ef725788f1aa9e2094bc7819f5951e3eeaa28420f202b50; + + /** + * @dev Hash of the implementation contract creation code. + */ + bytes32 internal constant IMPLEMENTATION_CODEHASH = keccak256(type(Gateway).creationCode); + + /** + * @dev Salt of the implementation. + */ + bytes32 internal constant IMPLEMENTATION_SALT = bytes32(uint256(0x010000000000)); + + /** + * @dev Default Proxy Admin, if none is provided, use this one. + */ + address internal constant DEFAULT_ADMIN_ACCOUNT = 0xB41440FF80e1083350c91B21DE1061e0920A75AD; + + /** + * Information about the current state of the migration + * @param forkId The network fork id, see: https://book.getfoundry.sh/forge/fork-testing#forking-cheatcodes + * @param mortality The maximum block number where the migration can be executed. + * @param proxyAddress The address of the proxy contract + */ + struct State { + uint256 forkId; + uint64 mortality; + address proxyAddress; + } + + struct NetworkConfiguration { + string name; + uint256 forkID; + uint256 chainID; + bool hasProxy; + address adminAddress; + address implementationContract; + UpdateNetworkInfo info; + } + + /** + * Information about the current state of the migration + * @param proxy The address of the proxy contract + * @param proxyAdmin The address of the proxy admin + * @param proxyDeployer The address of the proxy deployer + * @param proxyDeployerNonce The nonce that must be used by the deployer to deploy the proxy contract + * @param implementationDeployer Account used to deploy the implementation contract. + */ + struct Configuration { + address proxy; + address proxyAdmin; + address proxyDeployer; + uint256 proxyDeployerNonce; + address implementationDeployer; + NetworkConfiguration[] networks; + } + + /** + * @dev Maps the network id to its migration state + */ + mapping(uint16 => State) public states; + + // Computes the EIP-712 domain separador + function _computeDomainSeparator(uint256 networkId, address addr) private pure returns (bytes32) { + return keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256("Analog Gateway Contract"), + keccak256("0.1.0"), + uint256(networkId), + address(addr) + ) + ); + } + + function _toString(string memory a, uint256 b, string memory c, uint256 d) private pure returns (string memory) { + return string(bytes.concat( + bytes(a), + bytes(vm.toString(b)), + bytes(c), + bytes(vm.toString(d)) + )); + } + + function _toString(string memory a, address b, string memory c, address d) private pure returns (string memory) { + return string(bytes.concat( + bytes(a), + bytes(vm.toString(b)), + bytes(c), + bytes(vm.toString(d)) + )); + } + + /** + * @dev Convert an address to string. + */ + function _toString(string memory a, address b, string memory c) private pure returns (string memory) { + return string(bytes.concat(bytes(a), bytes(vm.toString(b)), bytes(c))); + } + + /** + * @dev Find what nonce the deployer must use to deploy the contract at the specified address. + */ + function _findDeployerNonce(address deployer, address contractAddress) private pure returns (uint64) { + for (uint256 nonce = 0; nonce < 1000; nonce++) { + // Obs: using this instead of `vm.computeCreateAddress(deployer, nonce);` to avoid `OutOfMemory` error. + address addr = FACTORY.computeCreateAddress(deployer, nonce); + if (contractAddress == addr) { + return uint64(nonce); + } + } + return type(uint64).max; + } + + /** + * @dev Retrieve network info and verify if the deployer is the admin of the proxy contract. + */ + function _setupNetwork(NetworkConfiguration memory network, address proxyAddress, address proxyDeployer, address proxyAdmin) + private + view + { + network.chainID = block.chainid; + + // Check if the chain has a network id + NetworkID networkId; + { + bool exists; + (exists,networkId) = NetworkIDHelpers.tryFromChainID(block.chainid); + require(exists, string(bytes.concat(bytes("network id not found for chain "), bytes(vm.toString(block.chainid))))); + } + + ////////////////////////////////////////////////// + // Verify if the proxy is deployed and is valid // + ////////////////////////////////////////////////// + network.hasProxy = proxyAddress.code.length > 0; + if (network.hasProxy) { + require(proxyAddress.codehash == PROXY_CODEHASH, "invalid proxy codehash"); + } else { + console.log("PROXY NOT DEPLOYED"); + } + + //////////////////////////////////////////////// + // Verify if the UNIVERSAL FACORY is deployed // + //////////////////////////////////////////////// + require(address(FACTORY).code.length > 0, "universal factory not deployed"); + require(address(FACTORY).codehash == FACTORY_CODEHASH, "invalid universal factory codehash"); + + ///////////////////////// + // Retrieve Chain Info // + ///////////////////////// + + // Allocate the network information + UpdateNetworkInfo memory info = network.info; + info.networkId = networkId.asUint(); + info.domainSeparator = bytes32(0); + info.gasLimit = 0; + info.relativeGasPrice = UFloatMath.ONE; + info.baseFee = 0; + info.mortality = 0; + console.log(" NETWORK ID", info.networkId); + + // Print information about the proxy contract + if (network.hasProxy) { + // Check if the gateway.networkId == info.networkId + uint16 gatewayNetworkId = IGateway(proxyAddress).networkId(); + vm.assertEq(gatewayNetworkId, info.networkId, "network id mismatch"); + + // Retrieve the proxy admin + network.adminAddress = address(uint160(uint256(vm.load(proxyAddress, ERC1967.ADMIN_SLOT)))); + console.log(" PROXY_ADMIN", network.adminAddress); + + // Retrieve the current implementation + network.implementationContract = address(uint160(uint256(vm.load(proxyAddress, ERC1967.IMPLEMENTATION_SLOT)))); + console.log(" IMPLEMENATTION", network.implementationContract); + } else { + console.log(" PROXY_ADMIN", "N/A"); + console.log(" IMPLEMENATTION", "N/A"); + network.adminAddress = address(0); + network.implementationContract = address(0); + } + + // Print information about the current network + uint256 nonce = vm.getNonce(proxyDeployer); + console.log(" GATEWAY BALANCE", proxyAddress.balance); + console.log(" DEPLOYER BALANCE", proxyDeployer.balance); + console.log(" DEPLOYER NONCE", nonce); + console.log(" ADMIN BALANCE", proxyAdmin.balance); + console.log(" ADMIN NONCE", vm.getNonce(proxyAdmin)); + console.log(" LATEST BLOCK", block.number); + console.log(" BLOCK GAS LIMIT", block.gaslimit); + console.log(" CHAIN ID", block.chainid); + console.log(" GAS PRICE", tx.gasprice); + console.log(" BASE FEE", block.basefee, "\n"); + + if (network.hasProxy && network.adminAddress != proxyAdmin) { + revert(_toString("proxy admin mismatch, got ", network.adminAddress, " but expected ", proxyAdmin)); + } + require(block.gaslimit < uint64(type(int64).max), "block gas limit exceeds the limit of int64"); + require(block.gaslimit > 1_000_000, "block gas limit is too low"); + require(block.number < uint64(type(int64).max), "block number limit exceeds the limit of int64"); + require(block.number > 1_000_000, "block number is low, is this a local testnet?"); + + // If this chain needs proxy, the deployer nonce must match the expected nonce. + if (network.hasProxy == false) { + address addr = vm.computeCreateAddress(proxyDeployer, nonce); + if (addr != proxyAddress) { + uint256 expected = _findDeployerNonce(proxyDeployer, proxyAddress); + revert(_toString("Deployer nonce mismatch, got ", nonce, " but expected ", expected)); + } + } + + // Update network information + info.domainSeparator = _computeDomainSeparator(info.networkId, proxyAddress); + info.gasLimit = uint64(block.gaslimit >> 1); + info.relativeGasPrice = UFloatMath.ONE; + info.baseFee = 0; + info.mortality = uint64(block.number + 128); + } + + function _setupNetworks(address proxyAddress, address proxyDeployer, address proxyAdmin) private returns (NetworkConfiguration[] memory networks) { + string[2][] memory urls = vm.rpcUrls(); + require(urls.length > 0, "no rpc urls found, check the `foundry.toml` file"); + networks = new NetworkConfiguration[](urls.length); + + // Initialize and check the network information + for (uint256 i = 0; i < urls.length; i++) { + networks[i] = NetworkConfiguration({ + name: urls[i][0], + forkID: 0, + chainID: 0, + hasProxy: false, + adminAddress: address(0), + implementationContract: address(0), + info: UpdateNetworkInfo({ + networkId: 0, + domainSeparator: bytes32(0), + gasLimit: 0, + relativeGasPrice: UFloatMath.ONE, + baseFee: 0, + mortality: 0 + }) + }); + NetworkConfiguration memory network = networks[i]; + string[2] memory entry = urls[i]; + console.log(" BLOCKCHAIN", entry[0]); + console.log(" RPC URL", entry[1]); + network.forkID = vm.createSelectFork(entry[1]); + _setupNetwork(network, proxyAddress, proxyDeployer, proxyAdmin); + console.log(""); + } + } + + /** + * @dev Script entry point, the following core will upgrade the gateway contract of all networks. + */ + function run() external { + Configuration memory config; + { + // Retrieve the `GatewayProxy` address + address proxy = vm.envOr("PROXY_ADDRESS", address(0)); + if (proxy == address(0)) { + proxy = vm.promptAddress("Enter the address of the proxy contract"); + } + + // Retrieve the account that must be used to deploy the proxy contract. + address proxyDeployer = vm.envOr("PROXY_DEPLOYER", address(0)); + if (proxyDeployer == address(0)) { + proxyDeployer = vm.promptAddress("Enter the address of the account that must be used to deploy the proxy contract"); + } + require(msg.sender != proxyDeployer, "The account used to deploy the implementation and the proxy cannot be the same"); + + // Find the nonce that must be used by the deployer to deploy the proxy contract + uint64 proxyDeployerNonce = _findDeployerNonce(proxyDeployer, proxy); + if (proxyDeployerNonce == type(uint64).max) { + revert(_toString("The provided deployer ", proxyDeployer, " cannot deploy the proxy contract")); + } + + // Retrieve the proxy admin account + address proxyAdmin = vm.envOr("PROXY_ADMIN", DEFAULT_ADMIN_ACCOUNT); + + // Initialize Networks + config = Configuration({ + proxy: proxy, + proxyAdmin: proxyAdmin, + proxyDeployer: proxyDeployer, + proxyDeployerNonce: proxyDeployerNonce, + implementationDeployer: msg.sender, + networks: _setupNetworks(proxy, proxyDeployer, proxyAdmin) + }); + } + + console.log(" FUNDING ACCOUNT", config.implementationDeployer); + console.log(" PROXY_ADDRESS", config.proxy); + console.log(" PROXY DEPLOYER", config.proxyDeployer); + console.log(" ADMIN ACCOUNT", config.proxyAdmin, "\n"); + + // Iterate over all the RPC URLs, defined in the `foundry.toml` file + NetworkConfiguration[] memory allNetworks = config.networks; + + // Filter the networks that need a proxy + NetworkConfiguration[] memory needsProxy = new NetworkConfiguration[](allNetworks.length); + { + uint256 count = 0; + for (uint256 i=0; i MINIMAL_DEPLOYER_BALANCE, "insufficient funds"); + + vm.startBroadcast(msg.sender); + payable(deployer).transfer(MINIMAL_DEPLOYER_BALANCE - balance); + vm.stopBroadcast(); + } + console.log(""); + } + + console.log(" --------------- DEPLOYING IMPLEMENTATION --------------- "); + { + bytes memory bytecode = type(Gateway).creationCode; + console.log(" IMPL HASH", vm.toString(bytes32(keccak256(bytecode))), "\n"); + for (uint256 i = 0; i < needsProxy.length; i++) { + NetworkConfiguration memory network = needsProxy[i]; + + // Switch to the selected network + vm.selectFork(network.forkID); + require(network.chainID == block.chainid, "chain id mismatch"); + + // Print information about the current network + console.log(" -- BLOCKCHAIN", network.name); + console.log(" CHAIN ID", block.chainid); + console.log(" NETWORK ID", network.info.networkId); + console.log(" BLOCK NUMBER", block.number); + + // Check if the the implementation is already deployed. + bytes memory initCode = bytes.concat( + bytecode, + abi.encode(uint16(network.info.networkId), address(config.proxy)) + ); + address deployer = config.implementationDeployer; + network.implementationContract = FACTORY.computeCreate2Address(IMPLEMENTATION_SALT, initCode); + console.log(" DEPLOYER", deployer); + console.log(" DEPLOYER BALANCE", deployer.balance); + console.log(" CONTRACT ADDRESS", network.implementationContract); + + if (network.implementationContract.code.length == 0) { + // Deploy the implementation contract + vm.startBroadcast(deployer); + address implementation = FACTORY.create2(IMPLEMENTATION_SALT, initCode); + vm.stopBroadcast(); + vm.assertEq(network.implementationContract, implementation, "implementation address mismatch"); + console.log(" DEPLOYMENT STATUS", "Deployed"); + } else { + console.log(" DEPLOYMENT STATUS", "Skipped, already deployed"); + } + console.log(); + } + } + + console.log(" -------------------- DEPLOYING PROXY ------------------- "); + for (uint256 i = 0; i < needsProxy.length; i++) { + NetworkConfiguration memory network = needsProxy[i]; + + // Switch to the selected network + vm.selectFork(network.forkID); + require(network.chainID == block.chainid, "chain id mismatch"); + + console.log(" -- BLOCKCHAIN", network.name); + console.log(" CHAIN ID", block.chainid); + console.log(" NETWORK ID", network.info.networkId); + console.log(" BLOCK NUMBER", block.number); + + address deployer = config.proxyDeployer; + address implementation = network.implementationContract; + uint256 nonce = vm.getNonce(deployer); + console.log(" DEPLOYER ACCOUNT", deployer); + console.log(" DEPLOYER BALANCE", deployer.balance); + console.log(" DEPLOYER NONCE", nonce); + console.log(" IMPLEMENTATION", implementation); + console.log(" PROXY ADDRESS", config.proxy); + require(config.proxy.code.length == 0, "proxy already deployed"); + require(deployer.balance >= MINIMAL_DEPLOYER_BALANCE, "deployer insufficient funds"); + require(nonce == config.proxyDeployerNonce, "wrong deployer nonce"); + require(implementation.code.length > 0, "implementation not found"); + + // TODO: Load the shards from the network, currently only the admin can add the shards. + TssKey[] memory emptyShards = new TssKey[](0); + Network[] memory emptyNetworks = new Network[](0); + bytes memory initializer = abi.encodeCall(Gateway.initialize, (config.proxyAdmin, emptyShards, emptyNetworks)); + + vm.startBroadcast(deployer); + // address deployed = address(new GatewayProxy(implementation, initializer)); + address deployed = address(new GatewayProxy(config.proxyAdmin)); // TODO: fix me + vm.stopBroadcast(); + console.log(" PROXY ADDRESS", deployed); + console.log(" DEPLOYMENT STATUS", deployed == config.proxy ? "Success" : "Address Mismatch"); + console.log(""); + } + } +} diff --git a/analog-gmp/scripts/Migrate.sol b/analog-gmp/scripts/Migrate.sol new file mode 100644 index 000000000..4c0716c98 --- /dev/null +++ b/analog-gmp/scripts/Migrate.sol @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (scripts/Upgrade.sol) + +pragma solidity ^0.8.0; + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {IGateway} from "../src/interfaces/IGateway.sol"; +import {ERC1967} from "../src/utils/ERC1967.sol"; +import {UFloat9x56, UFloatMath} from "../src/utils/Float9x56.sol"; +import {Gateway} from "../src/Gateway.sol"; +import { + TssKey, + GmpMessage, + UpdateKeysMessage, + UpdateNetworkInfo, + Signature, + Network, + GmpStatus, + GmpSender, + PrimitiveUtils +} from "../src/Primitives.sol"; + +contract MigrateGateway is Script { + bytes32 internal constant PROXY_CODEHASH = 0x54afeb06256bce71659256132ac18f1515de3011aaec4fbd6fc7b0c00c7263d8; + + /** + * Information about the current state of the migration + * @param forkId The network fork id, see: https://book.getfoundry.sh/forge/fork-testing#forking-cheatcodes + * @param mortality The maximum block number where the migration can be executed. + * @param proxyAddress The address of the proxy contract + */ + struct State { + uint256 forkId; + uint64 mortality; + address proxyAddress; + } + + /** + * @dev Maps the network id to its migration state + */ + mapping(uint16 => State) public states; + + // Computes the EIP-712 domain separador + function _computeDomainSeparator(uint256 networkId, address addr) private pure returns (bytes32) { + return keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256("Analog Gateway Contract"), + keccak256("0.1.0"), + uint256(networkId), + address(addr) + ) + ); + } + + /** + * @dev Retrieve network info and verify if the deployer is the admin of the proxy contract. + */ + function _setupNetwork(string memory name, address proxyAddress, address deployer) + private + returns (UpdateNetworkInfo memory info) + { + console.log(string(bytes.concat(" -- CHECKING ", bytes(name)))); + + // Retrieve the RPC URL + string memory rpcUrl = vm.envOr(name, string("")); + require(bytes(rpcUrl).length > 0, "rpc url not found"); + console.log(" RPC", rpcUrl); + + // Create a new fork + uint256 forkId = vm.createSelectFork(rpcUrl); + + // Verify if the provided proxy address is valid + { + require(proxyAddress.code.length > 0, "UpgradeGateway: proxy doesn't exists"); + bytes32 codehash; + assembly { + codehash := extcodehash(proxyAddress) + } + require(codehash == PROXY_CODEHASH, "UpgradeGateway: invalid proxy codehash"); + } + + // Allocate the network information + info = UpdateNetworkInfo({ + networkId: 0, + domainSeparator: bytes32(0), + gasLimit: 0, + relativeGasPrice: UFloatMath.ONE, + baseFee: 0, + mortality: 0 + }); + + // Retrieve the network id + info.networkId = IGateway(proxyAddress).networkId(); + console.log(" NETWORK ID", info.networkId); + + // Retrieve the proxy admin + address admin = address(uint160(uint256(vm.load(proxyAddress, ERC1967.ADMIN_SLOT)))); + console.log(" PROXY_ADMIN", admin); + + // Retrieve the current implementation + address implementation = address(uint160(uint256(vm.load(proxyAddress, ERC1967.IMPLEMENTATION_SLOT)))); + console.log(" IMPLEMENATTION", implementation); + + // Print information about the current network + console.log(" GATEWAY BALANCE", proxyAddress.balance); + console.log(" DEPLOYER BALANCE", deployer.balance); + console.log(" LATEST BLOCK", block.number); + console.log(" BLOCK GAS LIMIT", block.gaslimit); + console.log(" CHAIN ID", block.chainid); + console.log(" GAS PRICE", tx.gasprice); + console.log(" BASE FEE", block.basefee, "\n"); + + require(admin == deployer, "deployer is not the admin if this contract"); + require(block.gaslimit < uint64(type(int64).max), "block gas limit exceeds the limit of int64"); + require(block.gaslimit > 1_000_000, "block gas limit is too low"); + require(block.number < uint64(type(int64).max), "block number limit exceeds the limit of int64"); + require(block.number > 1_000_000, "block number is low, is this a local testnet?"); + + // Update network information + info.domainSeparator = _computeDomainSeparator(info.networkId, proxyAddress); + info.gasLimit = uint64(block.gaslimit >> 1); + info.relativeGasPrice = UFloatMath.ONE; + info.baseFee = 0; + info.mortality = uint64(block.number + 128); + + // Save migration state information + states[info.networkId] = State({forkId: forkId, mortality: info.mortality, proxyAddress: proxyAddress}); + } + + /** + * @dev Verify the networks and check if the deployer is the admin of the proxy contract + */ + function _setupNetworks(address proxyAddress, address deployer) + private + returns (UpdateNetworkInfo[] memory networks) + { + networks = new UpdateNetworkInfo[](3); + networks[0] = _setupNetwork("SEPOLIA_RPC_URL", proxyAddress, deployer); + + networks[1] = _setupNetwork("SHIBUYA_RPC_URL", proxyAddress, deployer); + require(networks[0].networkId != networks[1].networkId, "SEPOLIA and SHIBUYA have the same network id"); + + networks[2] = _setupNetwork("POLYGON_AMOY_RPC_URL", proxyAddress, deployer); + require(networks[2].networkId != networks[0].networkId, "AMOY and SEPOLIA have the same network id"); + require(networks[2].networkId != networks[0].networkId, "AMOY and SHIBUYA have the same network id"); + } + + /** + * @dev Deploy the new Gateway implementation and upgrade the proxy contract + */ + function _upgradeNetwork(uint16 networkId, uint256 deployerPrivateKey, UpdateNetworkInfo[] memory networks) + private + { + State memory state = states[networkId]; + + // Switch to the fork + vm.selectFork(state.forkId); + + // Check if the implementation expected address + { + // Retrieve the deployer nonce + address deployer = vm.addr(deployerPrivateKey); + uint256 nonce = vm.getNonce(deployer); + address implementation = vm.computeCreateAddress(deployer, nonce); + console.log(" NEW IMPLEMENATTION", implementation); + } + + // Update message mortality + for (uint256 i = 0; i < networks.length; i++) { + networks[i].mortality = state.mortality; + } + + // Deploy the new implementation contract + vm.startBroadcast(deployerPrivateKey); + Gateway newImplementation = new Gateway(networkId, state.proxyAddress); + console.log(" DEPLOYED", address(newImplementation)); + + bytes memory initializer = abi.encodeCall(Gateway.updateNetworks, (networks)); + console.log(" INITIALIZER:"); + console.logBytes(initializer); + Gateway(state.proxyAddress).upgradeAndCall(address(newImplementation), initializer); + console.log(" GATEWAY UPGRADED"); + vm.stopBroadcast(); + } + + /** + * @dev Script entry point, the following core will upgrade the gateway contract of all networks. + */ + function run() external { + // Retrieve the gateway proxy address + address proxyAddress = vm.envAddress("PROXY_ADDRESS"); + console.log(" PROXY_ADDRESS", proxyAddress); + + // Retrieve deployer private key + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address deployer = vm.addr(deployerPrivateKey); + console.log(" DEPLOYER", deployer, "\n"); + + // Setup the networks + UpdateNetworkInfo[] memory networkInfos = _setupNetworks(proxyAddress, deployer); + + // Extract the network ids + uint16[] memory networkByID = new uint16[](networkInfos.length); + for (uint256 i = 0; i < networkInfos.length; i++) { + networkByID[i] = networkInfos[i].networkId; + } + + // Upgrade the networks + for (uint256 i = 0; i < networkByID.length; i++) { + uint16 networkID = networkByID[i]; + console.log(" -- UPGRADING NETWORK", networkID); + _upgradeNetwork(networkID, deployerPrivateKey, networkInfos); + } + } +} diff --git a/analog-gmp/src/Gateway.sol b/analog-gmp/src/Gateway.sol new file mode 100644 index 000000000..9f0c2c944 --- /dev/null +++ b/analog-gmp/src/Gateway.sol @@ -0,0 +1,814 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (src/Gateway.sol) + +pragma solidity >=0.8.0; + +import {Hashing} from "./utils/Hashing.sol"; +import {Schnorr} from "./utils/Schnorr.sol"; +import {BranchlessMath} from "./utils/BranchlessMath.sol"; +import {GasUtils} from "./utils/GasUtils.sol"; +import {ERC1967} from "./utils/ERC1967.sol"; +import {UFloat9x56, UFloatMath} from "./utils/Float9x56.sol"; +import {RouteStore} from "./storage/Routes.sol"; +import {ShardStore} from "./storage/Shards.sol"; +import {IGateway} from "./interfaces/IGateway.sol"; +import {IUpgradable} from "./interfaces/IUpgradable.sol"; +import {IGmpReceiver} from "./interfaces/IGmpReceiver.sol"; +import {IExecutor} from "./interfaces/IExecutor.sol"; +import { + Command, + InboundMessage, + GatewayOp, + GmpCallback, + GmpMessage, + GmpStatus, + GmpSender, + Network, + Route, + PrimitiveUtils, + UpdateKeysMessage, + Signature, + TssKey, + MAX_PAYLOAD_SIZE +} from "./Primitives.sol"; +import {NetworkID, NetworkIDHelpers} from "./NetworkID.sol"; + +abstract contract GatewayEIP712 { + using NetworkIDHelpers for NetworkID; + + // EIP-712: Typed structured data hashing and signing + // https://eips.ethereum.org/EIPS/eip-712 + uint16 internal immutable NETWORK_ID; + address internal immutable PROXY_ADDRESS; + + constructor(uint16 networkId, address gateway) { + NETWORK_ID = networkId; + PROXY_ADDRESS = gateway; + } +} + +contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { + using PrimitiveUtils for UpdateKeysMessage; + using PrimitiveUtils for GmpMessage; + using PrimitiveUtils for address; + using BranchlessMath for uint256; + using UFloatMath for UFloat9x56; + using ShardStore for ShardStore.MainStorage; + using RouteStore for RouteStore.MainStorage; + using RouteStore for RouteStore.NetworkInfo; + using NetworkIDHelpers for NetworkID; + + /** + * @dev Selector of `GmpCreated` event. + * keccak256("GmpCreated(bytes32,bytes32,address,uint16,uint64,uint64,uint64,bytes)"); + */ + bytes32 private constant GMP_CREATED_EVENT_SELECTOR = + 0x081a0b65828c1720ce022ffb992d4a5ec86e2abc4c383acd4029ba8486e41b4f; + + /** + * @dev The address of the `UniversalFactory` contract, must be the same on all networks. + */ + address internal constant FACTORY = 0x0000000000001C4Bf962dF86e38F0c10c7972C6E; + + // GMP message status + mapping(bytes32 => GmpInfo) private _messages; + + // Hash of the previous GMP message submitted. + mapping(address => uint256) private _nonces; + + // Replay protection mechanism, stores the hash of the executed messages + // messageHash => shardId + mapping(bytes32 => bytes32) private _executedMessages; + + /** + * @dev GMP info stored in the Gateway Contract + * OBS: the order of the attributes matters! ethereum storage is 256bit aligned, try to keep + * the attributes 256 bit aligned, ex: nonce, block and status can be read in one storage access. + * reference: https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html + */ + struct GmpInfo { + GmpStatus status; + uint64 blockNumber; // block in which the message was processed + } + + constructor(uint16 network, address proxy) payable GatewayEIP712(network, proxy) {} + + // EIP-712 typed hash + function initialize(address proxyAdmin, TssKey[] calldata keys, Network[] calldata networks) external { + require(PROXY_ADDRESS == address(this) || msg.sender == FACTORY, "only proxy can be initialize"); + ERC1967.setAdmin(proxyAdmin); + + // Register networks + RouteStore.getMainStorage().initialize(networks, NetworkID.wrap(NETWORK_ID)); + + // Register keys + ShardStore.getMainStorage().registerTssKeys(keys); + + // emit event + emit ShardsRegistered(keys); + } + + function nonceOf(address account) external view returns (uint64) { + return uint64(_nonces[account]); + } + + function gmpInfo(bytes32 id) external view returns (GmpInfo memory) { + return _messages[id]; + } + + function keyInfo(bytes32 id) external view returns (ShardStore.ShardInfo memory) { + ShardStore.MainStorage storage store = ShardStore.getMainStorage(); + return store.get(ShardStore.ShardID.wrap(id)); + } + + function networkId() external view returns (uint16) { + return NETWORK_ID; + } + + function networkInfo(uint16 id) external view returns (RouteStore.NetworkInfo memory) { + return RouteStore.getMainStorage().get(NetworkID.wrap(id)); + } + + /** + * @dev Verify if shard exists, if the TSS signature is valid then increment shard's nonce. + */ + function _verifySignature(Signature calldata signature, bytes32 message) private view { + // Load shard from storage + ShardStore.ShardInfo storage signer = ShardStore.getMainStorage().get(signature); + + // Load y parity bit, it must be 27 (even), or 28 (odd) + // ref: https://ethereum.github.io/yellowpaper/paper.pdf + uint8 yParity = BranchlessMath.ternaryU8(signer.yParity > 0, 28, 27); + + // Verify Signature + require( + Schnorr.verify(yParity, signature.xCoord, uint256(message), signature.e, signature.s), + "invalid tss signature" + ); + } + + // Register/Revoke TSS keys using shard TSS signature + function updateKeys(Signature calldata signature, UpdateKeysMessage calldata message) external { + // Check if the message was already executed to prevent replay attacks + bytes32 messageHash = message.eip712hash(); + require(_executedMessages[messageHash] == bytes32(0), "message already executed"); + + // Verify the signature and store the message hash + _verifySignature(signature, messageHash); + _executedMessages[messageHash] = bytes32(signature.xCoord); + + // Register/Revoke shards pubkeys + ShardStore.MainStorage storage store = ShardStore.getMainStorage(); + + // Revoke tss keys (revoked keys can be registred again keeping the previous nonce) + store.revokeKeys(message.revoke); + + // Register or activate revoked keys + store.registerTssKeys(message.register); + + // Emit event + if (message.revoke.length > 0) { + emit ShardsUnregistered(message.revoke); + } + + if (message.register.length > 0) { + emit ShardsRegistered(message.register); + } + } + + /*////////////////////////////////////////////////////////////// + GATEWAY OPERATIONS AND COMMANDS + //////////////////////////////////////////////////////////////*/ + + /** + * @dev Lookup table to find the function pointer for a given command. + * Different than nested if-else, this has constant gas cost regardless the number of commands. + * + * Obs: supports up to 16 commands. + * See: `_buildCommandsLUT` and `_cmdTableLookup` methods for more details. + */ + type CommandsLookUpTable is uint256; + + /** + * @dev Dispatch a single GMP message. + */ + function _gmpCommand(bytes calldata params) private returns (bytes32 operationHash) { + require(params.length >= 256, "invalid GmpMessage"); + GmpMessage calldata gmp; + assembly { + gmp := add(params.offset, 0x20) + } + _checkGmpMessage(gmp); + // Convert the `GmpMessage` into `GmpCallback`, which is a more efficient representation. + // see `src/Primitives.sol` for more details. + GmpCallback memory callback = gmp.intoCallback(); + operationHash = callback.eip712hash; + _execute(callback); + } + + /** + * @dev Register a single shard and returns the GatewayOp hash. + */ + function _registerShardCommand(bytes calldata params) private returns (bytes32 operationHash) { + require(params.length == 64, "invalid TssKey"); + TssKey calldata newShard; + assembly { + newShard := params.offset + } + operationHash = Hashing.hash(newShard.yParity, newShard.xCoord); + _setShard(newShard); + } + + /** + * @dev Removes a single shard from the set. + */ + function _unregisterShardCommand(bytes calldata params) private returns (bytes32 operationHash) { + require(params.length == 64, "invalid TssKey"); + TssKey calldata shard; + assembly { + shard := params.offset + } + operationHash = Hashing.hash(shard.yParity, shard.xCoord); + _revokeShard(shard); + } + + /** + * Cast the command function into a uint256. + */ + function fnToPtr(function(bytes calldata) internal returns (bytes32) fn) private pure returns (uint256 ptr) { + assembly { + ptr := fn + } + } + + /** + * @dev Creates a lookup table to find the function pointer for a given command. + * + * Motivation: More efficient than nested if-else, and also guarantees a constant gas overhead for any command, which + * makes easier to estimate the gas cost necessary to execute the whole batch. + */ + function _buildCommandsLUT() private pure returns (CommandsLookUpTable) { + uint256 lookupTable; + // GMP + lookupTable = fnToPtr(_gmpCommand) << (uint256(Command.GMP) << 4); + // RegisterShard + lookupTable |= fnToPtr(_registerShardCommand) << (uint256(Command.RegisterShard) << 4); + // UnregisterShard + lookupTable |= fnToPtr(_unregisterShardCommand) << (uint256(Command.UnregisterShard) << 4); + return CommandsLookUpTable.wrap(lookupTable); + } + + /** + * @dev Get in constant gas the function pointer for the provided command. + * See `_buildCommandsLUT` for more details. + */ + function _cmdTableLookup(CommandsLookUpTable lut, Command command) + private + pure + returns (function(bytes calldata) internal returns (bytes32) fn) + { + unchecked { + // Extract the function pointer from the table using the `Command` as index. + uint256 ptr = CommandsLookUpTable.unwrap(lut) >> (uint256(command) << 4); + ptr &= 0xffff; + + // Make sure the function pointer is within the code bounds. + uint256 codeSize; + assembly { + codeSize := codesize() + } + require(ptr > 0 && ptr < codeSize, "invalid command"); + + // Converts the `uint256` back to `function(bytes calldata) internal returns (bytes32) fn` + assembly { + fn := ptr + } + } + } + + /** + * @dev Execute a batch of `GatewayOp` and returns the maximum amount of memory used in bytes and the operations root hash. + * + * This method also reuses the same memory space for each command, to prevent the memory to grow and + * increase the cost exponentially. + * @return (uint256, bytes32) Returns a tuple containing the maximum amount of memory used in bytes and the operations root hash. + */ + function _executeCommands(GatewayOp[] calldata operations) private returns (uint256, bytes32) { + // Track the free memory pointer, to reset the memory after each command executed. + uint256 freeMemPointer = GasUtils.readAllocatedMemory(); + uint256 maxAllocatedMemory = freeMemPointer; + + // Create the Command LookUp Table + CommandsLookUpTable lut = _buildCommandsLUT(); + + bytes32 operationsRootHash = bytes32(0); + for (uint256 i = 0; i < operations.length; i++) { + GatewayOp calldata operation = operations[i]; + + // Lookup the command function pointer + function(bytes calldata) internal returns (bytes32) commandFN = _cmdTableLookup(lut, operation.command); + + // Execute the command + bytes32 operationHash = commandFN(operation.params); + + // Update the operations root hash + operationsRootHash = + Hashing.hash(uint256(operationsRootHash), uint256(operation.command), uint256(operationHash)); + + // Restore the memory, to prevent the memory expansion costs to increase exponentially. + uint256 newFreeMemPointer = GasUtils.unsafeReplaceAllocatedMemory(freeMemPointer); + + // Update the Max Allocated Memory + maxAllocatedMemory = maxAllocatedMemory.max(newFreeMemPointer); + } + + // Compute what was the maximum amount of memory used in bytes + maxAllocatedMemory = maxAllocatedMemory - freeMemPointer; + + return (maxAllocatedMemory, operationsRootHash); + } + + /** + * @dev Verify and dispatch messages from the Timechain. + */ + function batchExecute(Signature calldata signature, InboundMessage calldata message) external { + uint256 initialGas = gasleft(); + // Add the solidity selector overhead to the initial gas, this way we guarantee that + // the `initialGas` represents the actual gas that was available to this contract. + initialGas = initialGas.saturatingAdd(GasUtils.BATCH_SELECTOR_OVERHEAD); + + // Execute the commands and compute the operations root hash + (, bytes32 rootHash) = _executeCommands(message.ops); + emit BatchExecuted(message.batchID); + + // Compute the Batch signing hash + rootHash = Hashing.hash(message.version, message.batchID, uint256(rootHash)); + bytes32 signingHash = + keccak256(abi.encodePacked("Analog GMP v2", NETWORK_ID, bytes32(uint256(uint160(address(this)))), rootHash)); + + // Verify the signature + _verifySignature(signature, signingHash); + + // Refund the chronicle gas + unchecked { + // Extra gas overhead used to execute the refund logic. + uint256 gasUsed = 7188; + + // Compute the gas used + base cost + proxy overhead + gasUsed = gasUsed.saturatingAdd(GasUtils.txBaseCost()); + gasUsed = gasUsed.saturatingAdd(GasUtils.proxyOverheadGasCost(uint16(msg.data.length), 0)); + gasUsed = gasUsed.saturatingAdd(initialGas - gasleft()); + + // Compute refund amount + uint256 refund = BranchlessMath.min(gasUsed.saturatingMul(tx.gasprice), address(this).balance); + + // Refund the gas used + assembly ("memory-safe") { + pop(call(gas(), caller(), refund, 0, 0, 0, 0)) + } + } + } + + function _execute(GmpCallback memory callback) private returns (GmpStatus, bytes32) { + // Verify if this GMP message was already executed + GmpInfo storage gmp = _messages[callback.eip712hash]; + require(gmp.status == GmpStatus.NOT_FOUND, "message already executed"); + + // Update status to `pending` to prevent reentrancy attacks. + gmp.status = GmpStatus.PENDING; + gmp.blockNumber = uint64(block.number); + + // Cap the GMP gas limit to 50% of the block gas limit + // OBS: we assume the remaining 50% is enough for the Gateway execution, which is a safe assumption + // once most EVM blockchains have gas limits above 10M and don't need more than 60k gas for the Gateway execution. + uint256 gasLimit = BranchlessMath.min(callback.gasLimit, block.gaslimit >> 1); + unchecked { + // Add `all but one 64th` to the gas needed, as the defined by EIP-150 + // https://eips.ethereum.org/EIPS/eip-150 + uint256 gasNeeded = gasLimit.saturatingMul(64).saturatingDiv(63); + // to guarantee it was provided enough gas to execute the GMP message + gasNeeded = gasNeeded.saturatingAdd(10000); + require(gasleft() >= gasNeeded, "insufficient gas to execute GMP message"); + } + + // Execute GMP call + bool success; + bytes32 result; + { + address dest = callback.dest; + bytes memory onGmpReceivedCallback = callback.callback; + assembly ("memory-safe") { + // Using low-level assembly because the GMP is considered executed + // regardless if the call reverts or not. + mstore(0, 0) + success := + call( + gasLimit, // call gas limit defined in the GMP message or 50% of the block gas limit + dest, // dest address + 0, // value in wei to transfer (always zero for GMP) + add(onGmpReceivedCallback, 32), // input memory pointer + mload(onGmpReceivedCallback), // input size + 0, // output memory pointer + 32 // output size (fixed 32 bytes) + ) + + // Get Result, reuse data to keep a predictable memory expansion + result := mload(0) + } + } + + // Update GMP status + GmpStatus status = + GmpStatus(BranchlessMath.ternary(success, uint256(GmpStatus.SUCCESS), uint256(GmpStatus.REVERT))); + + // Persist gmp execution status on storage + gmp.status = status; + + // Emit event + emit GmpExecuted(callback.eip712hash, callback.source, callback.dest, status, result); + + return (status, result); + } + + /** + * @dev Check if the GmpMessage network is correct and if the data is within the maximum size. + */ + function _checkGmpMessage(GmpMessage calldata message) private view { + // Theoretically we could remove the destination network field + // and fill it up with the network id of the contract, then the signature will fail. + require(message.destNetwork == NETWORK_ID, "invalid gmp network"); + + // Check if the message data is too large + require(message.data.length <= MAX_PAYLOAD_SIZE, "msg data too large"); + } + + /** + * Execute GMP message + * @param signature Schnorr signature + * @param message GMP message + */ + function execute(Signature calldata signature, GmpMessage calldata message) + external + returns (GmpStatus status, bytes32 result) + { + uint256 initialGas = gasleft(); + // Add the solidity selector overhead to the initial gas, this way we guarantee that + // the `initialGas` represents the actual gas that was available to this contract. + initialGas = initialGas.saturatingAdd(GasUtils.EXECUTION_SELECTOR_OVERHEAD); + + // Check GMP Message + _checkGmpMessage(message); + + // Convert the `GmpMessage` into `GmpCallback`, which is a more efficient representation. + // see `src/Primitives.sol` for more details. + GmpCallback memory callback = message.intoCallback(); + + // Verify the TSS Schnorr Signature + _verifySignature(signature, callback.eip712hash); + + // Execute GMP message + (status, result) = _execute(callback); + + // Refund the chronicle gas + unchecked { + // Compute GMP gas used + uint256 gasUsed = 7188 - 11; + gasUsed = gasUsed.saturatingAdd(GasUtils.txBaseCost()); + gasUsed = gasUsed.saturatingAdd(GasUtils.proxyOverheadGasCost(uint16(msg.data.length), 64)); + gasUsed = gasUsed.saturatingAdd(initialGas - gasleft()); + + // Compute refund amount + uint256 refund = BranchlessMath.min(gasUsed.saturatingMul(tx.gasprice), address(this).balance); + + assembly ("memory-safe") { + // Refund the gas used + pop(call(gas(), caller(), refund, 0, 0, 0, 0)) + } + } + } + + /** + * @dev Send message from this chain to another chain. + * @param destinationAddress the target address on the destination chain + * @param routeId the target chain where the contract call will be made + * @param executionGasLimit the gas limit available for the contract call + * @param data message data with no specified format + */ + function submitMessage(address destinationAddress, uint16 routeId, uint256 executionGasLimit, bytes calldata data) + external + payable + returns (bytes32) + { + // Check if the message data is too large + require(data.length <= MAX_PAYLOAD_SIZE, "msg data is too big"); + + // Check if the provided parameters are valid + // See `RouteStorage.estimateWeiCost` at `storage/Routes.sol` for more details. + RouteStore.NetworkInfo memory route = RouteStore.getMainStorage().get(NetworkID.wrap(routeId)); + (uint256 gasCost, uint256 fee) = route.estimateCost(data, executionGasLimit); + require(msg.value >= fee, "insufficient tx value"); + + // We use 20 bytes for represent the address and 1 bit for the contract flag + GmpSender source = msg.sender.toSender(false); + + unchecked { + // Nonce is per sender, it's incremented for every message sent. + uint64 nextNonce = uint64(_nonces[msg.sender]++); + + // Create GMP message and update nonce + GmpMessage memory message = + GmpMessage(source, NETWORK_ID, destinationAddress, routeId, uint64(executionGasLimit), nextNonce, data); + + // Emit `GmpCreated` event without copy the data, to simplify the gas estimation. + _emitGmpCreated( + message.eip712hash(), + source, + destinationAddress, + routeId, + executionGasLimit, + gasCost, + nextNonce, + message.data + ); + } + } + + /** + * @dev Emit `GmpCreated` event without copy the data, to simplify the gas estimation. + */ + function _emitGmpCreated( + bytes32 messageID, + GmpSender source, + address destinationAddress, + uint16 destinationNetwork, + uint256 executionGasLimit, + uint256 gasCost, + uint256 nonce, + bytes memory payload + ) private { + // Emit `GmpCreated` event without copy the data, to simplify the gas estimation. + // the assembly code below is equivalent to: + // ```solidity + // emit GmpCreated(prevHash, source, destinationAddress, destinationNetwork, executionGasLimit, gasCost, nonce, data); + // return prevHash; + // ``` + assembly { + let ptr := sub(payload, 0xa0) + mstore(add(ptr, 0x00), destinationNetwork) // dest network + mstore(add(ptr, 0x20), executionGasLimit) // gas limit + mstore(add(ptr, 0x40), gasCost) // gasCost + mstore(add(ptr, 0x60), nonce) // nonce + mstore(add(ptr, 0x80), 0xa0) // data offset + let size := and(add(mload(payload), 31), 0xffffffe0) + size := add(size, 192) + log4(ptr, size, GMP_CREATED_EVENT_SELECTOR, messageID, source, destinationAddress) + mstore(0, messageID) + return(0, 32) + } + } + + /*////////////////////////////////////////////////////////////// + FEE AND PAYMENT LOGIC + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Estimate the gas cost of execute a GMP message. + * @dev This function is called on the destination chain before calling the gateway to execute a source contract. + * @param networkid The target chain where the contract call will be made + * @param messageSize Message size + * @param messageSize Message gas limit + */ + function estimateMessageCost(uint16 networkid, uint256 messageSize, uint256 gasLimit) + external + view + returns (uint256) + { + RouteStore.NetworkInfo memory route = RouteStore.getMainStorage().get(NetworkID.wrap(networkid)); + + // Estimate the cost + return route.estimateWeiCost(uint16(messageSize), gasLimit); + } + + /** + * Deposit funds to the gateway contract + * IMPORTANT: this function must be called only by the administrator!!!! + */ + function deposit() external payable {} + receive() external payable {} + + /** + * Withdraw funds from the gateway contract + * @param amount The amount to withdraw + * @param recipient The recipient address + * @param data The data to send to the recipient (in case it is a contract) + */ + function withdraw(uint256 amount, address recipient, bytes calldata data) external returns (bytes memory output) { + require(msg.sender == ERC1967.getAdmin(), "unauthorized"); + // Check if the recipient is a contract + if (recipient.code.length > 0) { + bool success; + (success, output) = recipient.call{value: amount, gas: gasleft()}(data); + if (!success) { + assembly ("memory-safe") { + revert(add(output, 32), mload(output)) + } + } + } else { + payable(recipient).transfer(amount); + output = ""; + } + } + + /*////////////////////////////////////////////////////////////// + SHARDS MANAGEMENT METHODS + //////////////////////////////////////////////////////////////*/ + + /** + * @dev Register a single Shards with provided TSS public key. + */ + function _setShard(TssKey calldata publicKey) private { + bool isSuccess = ShardStore.getMainStorage().register(publicKey); + if (isSuccess) { + TssKey[] memory keys = new TssKey[](1); + keys[0] = publicKey; + emit ShardsRegistered(keys); + } + } + + /** + * @dev Revoke a single shard TSS Key. + */ + function _revokeShard(TssKey calldata publicKey) private { + bool isSuccess = ShardStore.getMainStorage().revoke(publicKey); + if (isSuccess) { + TssKey[] memory keys = new TssKey[](1); + keys[0] = publicKey; + emit ShardsUnregistered(keys); + } + } + + /** + * @dev List all shards. + */ + function shards() external view returns (TssKey[] memory) { + return ShardStore.getMainStorage().listShards(); + } + + /** + * @dev Returns the number of active shards. + */ + function shardCount() external view returns (uint256) { + return ShardStore.getMainStorage().length(); + } + + /** + * @dev Returns a shard by index. + * - Reverts with `IndexOutOfBounds` if the index is out of bounds. + */ + function shardAt(uint256 index) external view returns (TssKey memory) { + (ShardStore.ShardID xCoord, ShardStore.ShardInfo storage shard) = ShardStore.getMainStorage().at(index); + return TssKey({xCoord: uint256(ShardStore.ShardID.unwrap(xCoord)), yParity: shard.yParity + 2}); + } + + /** + * @dev Register a single Shards with provided TSS public key. + */ + function setShard(TssKey calldata publicKey) external { + require(msg.sender == ERC1967.getAdmin(), "unauthorized"); + _setShard(publicKey); + } + + /** + * @dev Register Shards in batch. + */ + function setShards(TssKey[] calldata publicKeys) external { + require(msg.sender == ERC1967.getAdmin(), "unauthorized"); + (TssKey[] memory created, TssKey[] memory revoked) = ShardStore.getMainStorage().replaceTssKeys(publicKeys); + + if (created.length > 0) { + emit ShardsRegistered(created); + } + + if (revoked.length > 0) { + emit ShardsUnregistered(revoked); + } + } + + /** + * @dev Revoke a single shard TSS Key. + */ + function revokeShard(TssKey calldata publicKey) external { + require(msg.sender == ERC1967.getAdmin(), "unauthorized"); + _revokeShard(publicKey); + } + + /** + * @dev Revoke Shards in batch. + */ + function revokeShards(TssKey[] calldata publicKeys) external { + require(msg.sender == ERC1967.getAdmin(), "unauthorized"); + TssKey[] memory revokedKeys = ShardStore.getMainStorage().revokeKeys(publicKeys); + if (revokedKeys.length > 0) { + emit ShardsUnregistered(revokedKeys); + } + } + + /*////////////////////////////////////////////////////////////// + LISTING ROUTES AND SHARDS + //////////////////////////////////////////////////////////////*/ + + /** + * @dev List all routes. + */ + function routes() external view returns (Route[] memory) { + return RouteStore.getMainStorage().listRoutes(); + } + + /** + * @dev Create or update a single route + */ + function setRoute(Route calldata info) external { + require(msg.sender == ERC1967.getAdmin(), "unauthorized"); + RouteStore.getMainStorage().createOrUpdateRoute(info); + } + + /** + * @dev Create or update an array of routes + */ + function setRoutes(Route[] calldata values) external { + require(msg.sender == ERC1967.getAdmin(), "unauthorized"); + require(values.length > 0, "routes cannot be empty"); + RouteStore.MainStorage storage store = RouteStore.getMainStorage(); + for (uint256 i = 0; i < values.length; i++) { + store.createOrUpdateRoute(values[i]); + } + } + + /*////////////////////////////////////////////////////////////// + ADMIN LOGIC + //////////////////////////////////////////////////////////////*/ + + function admin() external view returns (address) { + return ERC1967.getAdmin(); + } + + function setAdmin(address newAdmin) external payable { + require(msg.sender == ERC1967.getAdmin(), "unauthorized"); + ERC1967.setAdmin(newAdmin); + } + + // DANGER: This function is for migration purposes only, it allows the admin to set any storage slot. + function sudoSetStorage(uint256[2][] calldata values) external payable { + require(msg.sender == ERC1967.getAdmin(), "unauthorized"); + require(values.length > 0, "invalid values"); + + uint256 prev = 0; + for (uint256 i = 0; i < values.length; i++) { + uint256[2] memory entry = values[i]; + // Guarantee that the storage slot is in ascending order + // and that there are no repeated storage slots + uint256 key = entry[0]; + require(i == 0 || key > prev, "repeated storage slot"); + + // Protect admin and implementation slots + require(key != uint256(ERC1967.ADMIN_SLOT), "use setAdmin instead"); + require(key != uint256(ERC1967.IMPLEMENTATION_SLOT), "use upgrade instead"); + + // Set storage slot + uint256 value = entry[1]; + assembly { + sstore(key, value) + } + prev = key; + } + } + + function upgrade(address newImplementation) external payable { + require(msg.sender == ERC1967.getAdmin(), "unauthorized"); + + // Store the address of the implementation contract + ERC1967.setImplementation(newImplementation); + } + + function upgradeAndCall(address newImplementation, bytes memory initializer) + external + payable + returns (bytes memory returndata) + { + require(msg.sender == ERC1967.getAdmin(), "unauthorized"); + + // Store the address of the implementation contract + ERC1967.setImplementation(newImplementation); + + // Initialize storage by calling the implementation's using `delegatecall`. + bool success; + (success, returndata) = newImplementation.delegatecall(initializer); + + // Revert if the initialization failed + if (!success) { + assembly ("memory-safe") { + revert(add(returndata, 32), mload(returndata)) + } + } + } +} diff --git a/analog-gmp/src/GatewayProxy.sol b/analog-gmp/src/GatewayProxy.sol new file mode 100644 index 000000000..e6bf128b0 --- /dev/null +++ b/analog-gmp/src/GatewayProxy.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (src/GatewayProxy.sol) + +pragma solidity >=0.8.0; + +import {ERC1967} from "./utils/ERC1967.sol"; +import {Context, CreateKind, IUniversalFactory} from "@universal-factory/IUniversalFactory.sol"; + +contract GatewayProxy { + /** + * @dev The address of the `UniversalFactory` contract, must be the same on all networks. + */ + IUniversalFactory internal constant FACTORY = IUniversalFactory(0x0000000000001C4Bf962dF86e38F0c10c7972C6E); + + /** + * @dev EIP-1967 storage slot with the address of the current implementation. + * This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1. + * Ref: https://eips.ethereum.org/EIPS/eip-1967 + */ + bytes32 private constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + + constructor(address admin) payable { + // This contract must be deployed by the `UniversalFactory` + Context memory ctx = FACTORY.context(); + require(ctx.contractAddress == address(this), "Only the UniversalFactory can deploy this contract"); + require(ctx.kind == CreateKind.CREATE2, "Only CREATE2 is allowed"); + + require(ctx.data.length > 0, "ctx.data cannot be empty length"); + require(ctx.data.length >= 128, "unexpected ctx.data format, expected 128 bytes"); + + // Store the address of the implementation contract + // DeploymentAuthorization memory authorization; + uint8 v; + bytes32 r; + bytes32 s; + address implementation; + (v, r, s, implementation) = abi.decode(ctx.data, (uint8, bytes32, bytes32, address)); + + // Verify the signature + bytes32 digest = keccak256(abi.encode(address(this), implementation)); + require(admin == ecrecover(digest, v, r, s), "invalid signature"); + + // Set the ERC1967 admin. + ERC1967.setAdmin(admin); + + // Set the ERC1967 implementation. + ERC1967.setImplementation(implementation); + } + + fallback() external payable { + assembly ("memory-safe") { + // Copy the calldata to memory + calldatacopy(0, 0, calldatasize()) + + // Delegate call to the implementation contract + let success := delegatecall(gas(), sload(IMPLEMENTATION_SLOT), 0, calldatasize(), 0, 0) + + // Copy the return data to memory + returndatacopy(0, 0, returndatasize()) + + // Return if the call succeeded + if success { return(0, returndatasize()) } + + // Revert if the call failed + revert(0, returndatasize()) + } + } +} diff --git a/analog-gmp/src/NetworkID.sol b/analog-gmp/src/NetworkID.sol new file mode 100644 index 000000000..51e217cbb --- /dev/null +++ b/analog-gmp/src/NetworkID.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (src/NetworkID.sol) + +pragma solidity ^0.8.0; + +import {BranchlessMath} from "./utils/BranchlessMath.sol"; + +type NetworkID is uint16; + +library NetworkIDHelpers { + NetworkID internal constant MAINNET = NetworkID.wrap(0); + NetworkID internal constant ASTAR = NetworkID.wrap(1); + NetworkID internal constant POLYGON_POS = NetworkID.wrap(2); + NetworkID internal constant ETHEREUM_LOCAL_DEV = NetworkID.wrap(3); + NetworkID internal constant GOERLI = NetworkID.wrap(4); + NetworkID internal constant SEPOLIA = NetworkID.wrap(5); + NetworkID internal constant ASTAR_LOCAL_DEV = NetworkID.wrap(6); + NetworkID internal constant SHIBUYA = NetworkID.wrap(7); + NetworkID internal constant POLYGON_AMOY = NetworkID.wrap(8); + NetworkID internal constant BINANCE_SMART_CHAIN_TESTNET = NetworkID.wrap(9); + NetworkID internal constant ARBITRUM_SEPOLIA = NetworkID.wrap(10); + + /** + * @dev Converts a `NetworkID` into a `uint16`. + */ + function asUint(NetworkID networkId) internal pure returns (uint16) { + return NetworkID.unwrap(networkId); + } + + /** + * @dev Get the EIP-150 chain id from the network id. + */ + function chainId(NetworkID networkId) internal pure returns (uint64 chainID) { + assembly { + switch networkId + case 0 { + // Ethereum Mainnet + chainID := 0 + } + case 1 { + // Astar + chainID := 592 + } + case 2 { + // Polygon PoS + chainID := 137 + } + case 3 { + // Ethereum local testnet + chainID := 1337 + } + case 4 { + // Goerli + chainID := 5 + } + case 5 { + // Sepolia + chainID := 11155111 + } + case 6 { + // Astar local testnet + chainID := 592 + } + case 7 { + // Shibuya + chainID := 81 + } + case 8 { + // Polygon Amoy + chainID := 80002 + } + case 9 { + // Binance Smart Chain + chainID := 97 + } + case 10 { + // Arbitrum Sepolia + chainID := 421614 + } + default { + // Unknown network id + chainID := 0xffffffffffffffff + } + } + require(chainID > 2 ** 24, "the provided network id doesn't exists"); + return uint64(chainID); + } + + /** + * @dev Try to get the network id from the chain id. + */ + function tryFromChainID(uint256 chainid) internal pure returns (bool, NetworkID) { + uint256 networkId = type(uint256).max; + + // Ethereum Mainnet + networkId = BranchlessMath.ternary(chainid == 0, asUint(MAINNET), networkId); + // Astar + networkId = BranchlessMath.ternary(chainid == 592, asUint(ASTAR), networkId); + // Polygon PoS + networkId = BranchlessMath.ternary(chainid == 137, asUint(POLYGON_POS), networkId); + // Ethereum local testnet + networkId = BranchlessMath.ternary(chainid == 1337, asUint(ETHEREUM_LOCAL_DEV), networkId); + // Goerli + networkId = BranchlessMath.ternary(chainid == 5, asUint(GOERLI), networkId); + // Sepolia + networkId = BranchlessMath.ternary(chainid == 11155111, asUint(SEPOLIA), networkId); + // Astar local testnet + networkId = BranchlessMath.ternary(chainid == 592, asUint(ASTAR_LOCAL_DEV), networkId); + // Shibuya + networkId = BranchlessMath.ternary(chainid == 81, asUint(SHIBUYA), networkId); + // Polygon Amoy + networkId = BranchlessMath.ternary(chainid == 80002, asUint(POLYGON_AMOY), networkId); + // Binance Smart Chain + networkId = BranchlessMath.ternary(chainid == 97, asUint(BINANCE_SMART_CHAIN_TESTNET), networkId); + // Arbitrum Sepolia + networkId = BranchlessMath.ternary(chainid == 421614, asUint(ARBITRUM_SEPOLIA), networkId); + + bool exists = networkId != type(uint256).max; + return (exists, NetworkID.wrap(uint16(networkId))); + } + + /** + * @dev Converts a EIP-155 chain id into a `NetworkID`, reverts if the network id doesn't exists. + */ + function fromChainID(uint256 chainid) internal pure returns (NetworkID) { + (bool exists, NetworkID networkId) = tryFromChainID(chainid); + require(exists, "network id doesn't exists for the given chain id"); + return networkId; + } +} diff --git a/analog-gmp/src/Primitives.sol b/analog-gmp/src/Primitives.sol new file mode 100644 index 000000000..b279c95fa --- /dev/null +++ b/analog-gmp/src/Primitives.sol @@ -0,0 +1,373 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (src/Primitives.sol) + +pragma solidity >=0.8.0; + +import {BranchlessMath} from "./utils/BranchlessMath.sol"; +import {UFloatMath, UFloat9x56} from "./utils/Float9x56.sol"; +import {NetworkID} from "./NetworkID.sol"; + +/** + * @dev GMP message EIP-712 Type Hash. + * Declared as raw value to enable it to be used in inline assembly + * keccak256("GmpMessage(bytes32 source,uint16 srcNetwork,address dest,uint16 destNetwork,uint64 gasLimit,uint64 gasCost,uint32 nonce,bytes data)") + */ +uint256 constant GMP_VERSION = 0; + +/** + * @dev Maximum size of the GMP payload + */ +uint256 constant MAX_PAYLOAD_SIZE = 0x6000; + +/** + * @dev GmpSender is the sender of a GMP message + */ +type GmpSender is bytes32; + +/** + * @dev Tss public key + * @param yParity public key y-coord parity, the contract converts it to 27/28 + * @param xCoord affine x-coordinate + */ +struct TssKey { + uint8 yParity; + uint256 xCoord; +} + +/** + * @dev Schnorr signature. + * OBS: what is actually signed is: keccak256(abi.encodePacked(R, parity, px, nonce, message)) + * Where `parity` is the public key y coordinate stored in the contract, and `R` is computed from `e` and `s` parameters. + * @param xCoord public key x coordinates, y-parity is stored in the contract + * @param e Schnorr signature e component + * @param s Schnorr signature s component + */ +struct Signature { + uint256 xCoord; + uint256 e; + uint256 s; +} + +/** + * @dev GMP payload, this is what the timechain creates as task payload + * @param source Pubkey/Address of who send the GMP message + * @param srcNetwork Source chain identifier (for ethereum networks it is the EIP-155 chain id) + * @param dest Destination/Recipient contract address + * @param destNetwork Destination chain identifier (it's the EIP-155 chain_id for ethereum networks) + * @param gasLimit gas limit of the GMP call + * @param nonce Sequence nonce per sender, allows sending two messages with same content + * @param data message data with no specified format + */ +struct GmpMessage { + GmpSender source; + uint16 srcNetwork; + address dest; + uint16 destNetwork; + uint64 gasLimit; + uint64 nonce; + bytes data; +} + +/** + * @dev Message payload used to revoke or/and register new shards + * @param revoke Shard's keys to revoke + * @param register Shard's keys to register + */ +struct UpdateKeysMessage { + TssKey[] revoke; + TssKey[] register; +} + +/** + * @dev Messages from Timechain take the form of these commands. + */ +enum Command { + Invalid, + GMP, + RegisterShard, + UnregisterShard, + SetRoute +} + +/** + * @dev Inbound message from a Timechain + * @param command Command identifier. + * @param params Encoded command. + */ +struct GatewayOp { + /// @dev The command to execute + Command command; + /// @dev The Parameters for the command + bytes params; +} + +/** + * @dev Inbound message from a Timechain + * @param version Message version, will change if the message format changes. + * @param batchID Sequence number representing the batch order. + * @param ops List of operations to execute. + */ +struct InboundMessage { + uint8 version; + /// @dev The batch ID + uint64 batchID; + /// @dev + GatewayOp[] ops; +} + +/** + * @dev A Route represents a communication channel between two networks. + * @param networkId The id of the provided network. + * @param gasLimit The maximum amount of gas we allow on this particular network. + * @param gateway Destination chain gateway address. + * @param relativeGasPriceNumerator Gas price numerator in terms of the source chain token. + * @param relativeGasPriceDenominator Gas price denominator in terms of the source chain token. + */ +struct Route { + NetworkID networkId; + uint64 gasLimit; + uint128 baseFee; + bytes32 gateway; + uint128 relativeGasPriceNumerator; + uint128 relativeGasPriceDenominator; +} + +/** + * @dev Message payload used to revoke or/and register new shards + * @param revoke Shard's keys to revoke + * @param register Shard's keys to register + */ +struct Network { + uint16 id; + address gateway; +} + +/** + * @dev Status of a GMP message + */ +enum GmpStatus { + NOT_FOUND, + SUCCESS, + REVERT, + INSUFFICIENT_FUNDS, + PENDING +} + +/** + * @dev GmpMessage with EIP-712 GMP ID and callback function encoded. + * @param eip712hash EIP-712 hash of the `GmpMessage`, which is it's unique identifier + * @param source Pubkey/Address of who send the GMP message + * @param srcNetwork Source chain identifier (for ethereum networks it is the EIP-155 chain id) + * @param dest Destination/Recipient contract address + * @param destNetwork Destination chain identifier (it's the EIP-155 chain_id for ethereum networks) + * @param gasLimit gas limit of the GMP call + * @param nonce Sequence nonce per sender, allows sending two messages with same content + * @param callback encoded callback of `IGmpRecipient` interface, see `IGateway.sol` for more details. + */ +struct GmpCallback { + bytes32 eip712hash; + GmpSender source; + uint16 srcNetwork; + address dest; + uint16 destNetwork; + uint64 gasLimit; + uint64 nonce; + bytes callback; +} + +/** + * @dev EIP-712 utility functions for primitives + */ +library PrimitiveUtils { + function toAddress(GmpSender sender) internal pure returns (address) { + return address(uint160(uint256(GmpSender.unwrap(sender)))); + } + + function toSender(address addr, bool isContract) internal pure returns (GmpSender) { + uint256 sender = BranchlessMath.toUint(isContract) << 160 | uint256(uint160(addr)); + return GmpSender.wrap(bytes32(sender)); + } + + // computes the hash of an array of tss keys + function eip712hash(TssKey memory tssKey) internal pure returns (bytes32) { + return keccak256(abi.encode(keccak256("TssKey(uint8 yParity,uint256 xCoord)"), tssKey.yParity, tssKey.xCoord)); + } + + // computes the hash of an array of tss keys + function eip712hash(TssKey[] memory tssKeys) internal pure returns (bytes32) { + bytes memory keysHashed = new bytes(tssKeys.length * 32); + uint256 ptr; + assembly { + ptr := keysHashed + } + for (uint256 i = 0; i < tssKeys.length; i++) { + bytes32 hash = eip712hash(tssKeys[i]); + assembly { + ptr := add(ptr, 32) + mstore(ptr, hash) + } + } + return keccak256(keysHashed); + } + + // computes the hash of the fully encoded EIP-712 message for the domain, which can be used to recover the signer + function eip712hash(UpdateKeysMessage memory message) internal pure returns (bytes32) { + return keccak256( + abi.encode( + keccak256("UpdateKeysMessage(TssKey[] revoke,TssKey[] register)TssKey(uint8 yParity,uint256 xCoord)"), + eip712hash(message.revoke), + eip712hash(message.register) + ) + ); + } + + function eip712hash(GmpMessage memory message) internal pure returns (bytes32 id) { + bytes memory data = message.data; + assembly ("memory-safe") { + // keccak256(message.data) + id := keccak256(add(data, 32), mload(data)) + + // now compute the GmpMessage Type Hash without memory copying + let offset := sub(message, 32) + let backup := mload(offset) + { + mstore(offset, GMP_VERSION) + { + let offset2 := add(offset, 0xe0) + let backup2 := mload(offset2) + mstore(offset2, id) + id := keccak256(offset, 0x100) + mstore(offset2, backup2) + } + } + mstore(offset, backup) + } + } + + type MessagePtr is uint256; + + function _intoMemoryPointer(MessagePtr ptr) private pure returns (GmpMessage memory r) { + assembly { + r := ptr + } + } + + function _intoCalldataPointer(MessagePtr ptr) private pure returns (GmpMessage calldata r) { + assembly { + r := ptr + } + } + + function memToCallback(GmpMessage memory message) internal pure returns (GmpCallback memory callback) { + MessagePtr ptr; + assembly { + ptr := message + } + _intoCallback(ptr, false, callback); + } + + function intoCallback(GmpMessage calldata message) internal pure returns (GmpCallback memory callback) { + MessagePtr ptr; + assembly { + ptr := message + } + _intoCallback(ptr, true, callback); + } + + /** + * @dev Computes the message ID from the provided `GmpCallback` struct. + */ + function _computeMessageID(GmpCallback memory callback) private pure { + bytes memory onGmpReceived = callback.callback; + bytes32 dataHash; + assembly ("memory-safe") { + let offset := add(onGmpReceived, 0xa4) + dataHash := keccak256(add(offset, 0x20), mload(offset)) + } + callback.eip712hash = bytes32(GMP_VERSION); + assembly ("memory-safe") { + // temporarily store the result at `0x00e0..0x0100`, which is the `GmpCallback.callback.offset` field. + mstore(add(callback, 0xe0), dataHash) + + // Compute `keccak256(abi.encode(GMP_VERSION, message.source, ..., keccak256(message.data)))` + dataHash := keccak256(callback, 0x0100) + + // Replace the `eip712hash` by the `callback.data.offset`. + mstore(add(callback, 0xe0), onGmpReceived) + + // Replace the `id` in `onGmpReceived(uint256 id,...)` in the callback. + mstore(add(onGmpReceived, 0x24), dataHash) + } + callback.eip712hash = dataHash; + } + + /** + * @dev Converts the `GmpMessage` into a `GmpCallback` struct, which contains all fields from + * `GmpMessage`, plus the EIP-712 hash and `IGmpReceiver.onGmpReceived` callback. + * + * This method also prevents copying the `message.data` to memory twice, which is expensive if + * the data is large. + * Example: using solidity high-level `abi.encode` method does the following. + * 1. Copy the `message.data` to memory to compute the `GmpMessage` EIP-712 hash. + * 2. Copy again to encode the `IGmpReceiver.onGmpReceived` callback. + * + * Instead we copy it once and use the same memory location to compute the EIP-712 hash and + * create he `IGmpReceiver.onGmpReceived` callback, unfortunately this requires inline assembly. + * + * @param message GmpMessage from calldata to be encoded + * @param callback `GmpCallback` struct + */ + function _intoCallback(MessagePtr message, bool isCalldata, GmpCallback memory callback) private pure { + // | MEMORY OFFSET | RESERVED FIELD | + // | 0x0000..0x0020 <- GmpCallback.eip712hash + // | 0x0020..0x0040 <- GmpCallback.source + // | 0x0040..0x0060 <- GmpCallback.srcNetwork + // | 0x0060..0x0080 <- GmpCallback.dest + // | 0x0080..0x00a0 <- GmpCallback.destNetwork + // | 0x00a0..0x00c0 <- GmpCallback.gasLimit + // | 0x00c0..0x00e0 <- GmpCallback.nonce + // | 0x00e0..0x0100 <- GmpCallback.callback.offset + // | 0x0100..0x0120 <- GmpCallback.callback.length + // | 0x0120..0x0124 <- onGmpReceived.selector (4 bytes) + // | 0x0124..0x0144 <- onGmpReceived.id + // | 0x0144..0x0164 <- onGmpReceived.network + // | 0x0164..0x0184 <- onGmpReceived.source + // | 0x0184..0x01a4 <- onGmpReceived.data.offset + // | 0x01a4..0x01c4 <- onGmpReceived.data.length + // | 0x01c4........ <- onGmpReceived.data + if (isCalldata) { + GmpMessage calldata m = _intoCalldataPointer(message); + callback.source = m.source; + callback.srcNetwork = m.srcNetwork; + callback.dest = m.dest; + callback.destNetwork = m.destNetwork; + callback.gasLimit = m.gasLimit; + callback.nonce = m.nonce; + bytes calldata data = m.data; + callback.callback = abi.encodeWithSignature( + "onGmpReceived(bytes32,uint128,bytes32,bytes)", + callback.eip712hash, + callback.srcNetwork, + callback.source, + data + ); + } else { + GmpMessage memory m = _intoMemoryPointer(message); + callback.source = m.source; + callback.srcNetwork = m.srcNetwork; + callback.dest = m.dest; + callback.destNetwork = m.destNetwork; + callback.gasLimit = m.gasLimit; + callback.nonce = m.nonce; + callback.callback = abi.encodeWithSignature( + "onGmpReceived(bytes32,uint128,bytes32,bytes)", + callback.eip712hash, + callback.srcNetwork, + callback.source, + m.data + ); + } + // Compute the message ID + _computeMessageID(callback); + } +} diff --git a/analog-gmp/src/interfaces/IExecutor.sol b/analog-gmp/src/interfaces/IExecutor.sol new file mode 100644 index 000000000..6272dd654 --- /dev/null +++ b/analog-gmp/src/interfaces/IExecutor.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (src/interfaces/IExecutor.sol) + +pragma solidity >=0.8.0; + +import { + InboundMessage, + Signature, + GmpMessage, + TssKey, + GmpStatus, + GmpStatus, + UpdateKeysMessage, + GmpSender, + Route +} from "../Primitives.sol"; + +/** + * @dev Required interface of an Gateway compliant contract + */ +interface IExecutor { + /** + * @dev Emitted when `GmpMessage` is executed. + * @param id EIP-712 hash of the `GmpPayload`, which is it's unique identifier + * @param source sender pubkey/address (the format depends on src chain) + * @param dest recipient address + * @param status GMP message execution status + * @param result GMP result + */ + event GmpExecuted( + bytes32 indexed id, GmpSender indexed source, address indexed dest, GmpStatus status, bytes32 result + ); + + /** + * @dev Emitted when a Batch is executed. + * @param batch batch_id which is executed + */ + event BatchExecuted(uint64 batch); + + /** + * @dev Emitted when shards are registered. + * @param keys registered shard's keys + */ + event ShardsRegistered(TssKey[] keys); + + /** + * @dev Emitted when shards are unregistered. + * @param keys unregistered shard's keys + */ + event ShardsUnregistered(TssKey[] keys); + + /** + * @dev List all shards currently registered in the gateway. + */ + function shards() external returns (TssKey[] memory); + + function setShard(TssKey calldata publicKey) external; + + /** + * @dev Register Shards in batch. + */ + function setShards(TssKey[] calldata publicKeys) external; + + /** + * @dev Revoke a single shard TSS Key. + */ + function revokeShard(TssKey calldata publicKey) external; + + /** + * @dev Revoke a single shard TSS Key. + */ + function revokeShards(TssKey[] calldata publicKey) external; + + /** + * @dev List all shards currently registered in the gateway. + */ + function routes() external returns (Route[] memory); + + function setRoute(Route calldata info) external; + + /** + * @dev Create or update an array of routes + */ + function setRoutes(Route[] calldata values) external; + + /** + * Execute operatins in batch + * @param signature Schnorr signature + * @param message GMP message + */ + function batchExecute(Signature calldata signature, InboundMessage calldata message) external; + + /** + * Execute GMP message + * @param signature Schnorr signature + * @param message GMP message + */ + function execute(Signature calldata signature, GmpMessage calldata message) + external + returns (GmpStatus status, bytes32 result); + + /** + * Deposit funds to the gateway contract + */ + function deposit() external payable; +} diff --git a/analog-gmp/src/interfaces/IGateway.sol b/analog-gmp/src/interfaces/IGateway.sol new file mode 100644 index 000000000..e61d83c40 --- /dev/null +++ b/analog-gmp/src/interfaces/IGateway.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (src/interfaces/IGateway.sol) + +pragma solidity >=0.8.0; + +import {GmpSender} from "../Primitives.sol"; + +/** + * @dev Required interface of an Gateway compliant contract + */ +interface IGateway { + /** + * @dev New GMP submitted by calling the `submitMessage` method. + * @param id EIP-712 hash of the `GmpPayload`, which is it's unique identifier + * @param source sender account, with an extra flag indicating if it is a contract or an EOA + * @param destinationAddress the target address on the destination chain. + * @param destinationNetwork the target chain where the contract call will be made. + * @param executionGasLimit the gas limit available for the contract call + * @param gasCost the gas limit available for the contract call + * @param nonce Sequence number per sender, used to guarantee each message is unique. + * @param data message data with no specified format + */ + event GmpCreated( + bytes32 indexed id, + bytes32 indexed source, + address indexed destinationAddress, + uint16 destinationNetwork, + uint64 executionGasLimit, + uint64 gasCost, + uint64 nonce, + bytes data + ); + + function networkId() external view returns (uint16); + + /** + * @notice Estimate the gas cost of execute a GMP message. + * @dev This function is called on the destination chain before calling the gateway to execute a source contract. + * @param networkid The target chain where the contract call will be made + * @param messageSize Message size + * @param messageSize Message gas limit + */ + function estimateMessageCost(uint16 networkid, uint256 messageSize, uint256 gasLimit) + external + view + returns (uint256); + + /** + * @dev Send message from chain A to chain B + * @param destinationAddress the target address on the destination chain + * @param destinationNetwork the target chain where the contract call will be made + * @param executionGasLimit the gas limit available for the contract call + * @param data message data with no specified format + */ + function submitMessage( + address destinationAddress, + uint16 destinationNetwork, + uint256 executionGasLimit, + bytes calldata data + ) external payable returns (bytes32); +} diff --git a/analog-gmp/src/interfaces/IGmpReceiver.sol b/analog-gmp/src/interfaces/IGmpReceiver.sol new file mode 100644 index 000000000..64fccd4a3 --- /dev/null +++ b/analog-gmp/src/interfaces/IGmpReceiver.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (src/interfaces/IGmpReceiver.sol) + +pragma solidity >=0.8.0; + +/** + * @dev Required interface of an GMP compliant contract + */ +interface IGmpReceiver { + /** + * @dev Handles the receipt of a single GMP message. + * The contract must verify the msg.sender, it must be the Gateway Contract address. + * + * @param id The EIP-712 hash of the message payload, used as GMP unique identifier + * @param network The chain_id of the source chain who send the message + * @param source The pubkey/address which sent the GMP message + * @param payload The message payload with no specified format + * @return 32 byte result which will be stored together with GMP message + */ + function onGmpReceived(bytes32 id, uint128 network, bytes32 source, bytes calldata payload) + external + payable + returns (bytes32); +} diff --git a/analog-gmp/src/interfaces/IUpgradable.sol b/analog-gmp/src/interfaces/IUpgradable.sol new file mode 100644 index 000000000..e6d694be5 --- /dev/null +++ b/analog-gmp/src/interfaces/IUpgradable.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (src/interfaces/IUpgradable.sol) + +pragma solidity >=0.8.0; + +interface IUpgradable { + // The new implementation address is a not a contract + error InvalidContract(); + // The supplied codehash does not match the new implementation codehash + error InvalidCodeHash(); + + // The implementation contract was upgraded + event Upgraded(address indexed implementation); +} diff --git a/analog-gmp/src/storage/Routes.sol b/analog-gmp/src/storage/Routes.sol new file mode 100644 index 000000000..0c7f7984a --- /dev/null +++ b/analog-gmp/src/storage/Routes.sol @@ -0,0 +1,286 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (src/storage/Routes.sol) +pragma solidity ^0.8.20; + +import {Signature, Network, Route, MAX_PAYLOAD_SIZE} from "../Primitives.sol"; +import {NetworkIDHelpers, NetworkID} from "../NetworkID.sol"; +import {EnumerableSet, Pointer} from "../utils/EnumerableSet.sol"; +import {BranchlessMath} from "../utils/BranchlessMath.sol"; +import {UFloat9x56, UFloatMath} from "../utils/Float9x56.sol"; +import {StoragePtr} from "../utils/Pointer.sol"; +import {GasUtils} from "../utils/GasUtils.sol"; + +/** + * @dev EIP-7201 Route's Storage + */ +library RouteStore { + using Pointer for StoragePtr; + using Pointer for uint256; + using EnumerableSet for EnumerableSet.Map; + using NetworkIDHelpers for NetworkID; + using UFloatMath for UFloat9x56; + using BranchlessMath for uint256; + + /** + * @dev Namespace of the routes storage `analog.one.gateway.routes`. + * keccak256(abi.encode(uint256(keccak256("analog.one.gateway.routes")) - 1)) & ~bytes32(uint256(0xff)); + */ + bytes32 internal constant _EIP7201_NAMESPACE = 0xb184f2aad520cf7f1f1270909517c75ae33cdf2bd7d32b997a96577f11a48800; + + /** + * @dev Network info stored in the Gateway Contract + * @param gasLimit The maximum amount of gas we allow on this particular network. + * @param relativeGasPrice Gas price of destination chain, in terms of the source chain token. + * @param baseFee Base fee for cross-chain message approval on destination, in terms of source native gas token. + */ + struct NetworkInfo { + uint64 gasLimit; + UFloat9x56 relativeGasPrice; + uint128 baseFee; + } + + /** + * @dev Emitted when a route is updated. + * @param networkId Network identifier. + * @param relativeGasPrice Gas price of destination chain, in terms of the source chain token. + * @param baseFee Base fee for cross-chain message approval on destination, in terms of source native gas token. + * @param gasLimit The maximum amount of gas we allow on this particular network. + */ + event RouteUpdated(uint16 indexed networkId, UFloat9x56 relativeGasPrice, uint128 baseFee, uint64 gasLimit); + + /** + * @dev Shard info stored in the Gateway Contract + * OBS: the order of the attributes matters! ethereum storage is 256bit aligned, try to keep + * the shard info below 256 bit, so it can be stored in one single storage slot. + * reference: https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html + * + * @custom:storage-location erc7201:analog.one.gateway.routes + */ + struct MainStorage { + EnumerableSet.Map routes; + } + + error RouteNotExists(NetworkID id); + error IndexOutOfBounds(uint256 index); + + function getMainStorage() internal pure returns (MainStorage storage $) { + assembly { + $.slot := _EIP7201_NAMESPACE + } + } + + /** + * @dev Converts a `StoragePtr` into an `NetworkInfo`. + */ + function pointerToRoute(StoragePtr ptr) private pure returns (NetworkInfo storage route) { + assembly { + route.slot := ptr + } + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function has(MainStorage storage store, NetworkID id) internal view returns (bool) { + return store.routes.has(bytes32(uint256(id.asUint()))); + } + + /** + * @dev Get or create a value. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function getOrAdd(MainStorage storage store, NetworkID id) private returns (bool, NetworkInfo storage) { + (bool success, StoragePtr ptr) = store.routes.tryAdd(bytes32(uint256(id.asUint()))); + return (success, pointerToRoute(ptr)); + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function remove(MainStorage storage store, NetworkID id) internal returns (bool) { + StoragePtr ptr = store.routes.remove(bytes32(uint256(id.asUint()))); + if (ptr.isNull()) { + return false; + } + return true; + } + + /** + * @dev Returns the number of values on the set. O(1). + */ + function length(MainStorage storage store) internal view returns (uint256) { + return store.routes.length(); + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(MainStorage storage store, uint256 index) internal view returns (NetworkID, NetworkInfo storage) { + (bytes32 key, StoragePtr value) = store.routes.at(index); + if (value.isNull()) { + revert IndexOutOfBounds(index); + } + return (NetworkID.wrap(uint16(uint256(key))), pointerToRoute(value)); + } + + /** + * @dev Returns the value associated with `NetworkInfo`. O(1). + * + * Requirements: + * - `NetworkInfo` must be in the map. + */ + function get(MainStorage storage store, NetworkID id) internal view returns (NetworkInfo storage) { + StoragePtr ptr = store.routes.get(bytes32(uint256(id.asUint()))); + if (ptr.isNull()) { + revert RouteNotExists(id); + } + return pointerToRoute(ptr); + } + + /** + * @dev Returns the value associated with `NetworkInfo`. O(1). + */ + function tryGet(MainStorage storage store, NetworkID id) internal view returns (bool, NetworkInfo storage) { + (bool exists, StoragePtr ptr) = store.routes.tryGet(bytes32(uint256(id.asUint()))); + return (exists, pointerToRoute(ptr)); + } + + function createOrUpdateRoute(MainStorage storage store, Route calldata route) internal { + // Update network info + (bool created, NetworkInfo storage stored) = getOrAdd(store, route.networkId); + require((created && route.gateway != bytes32(0)) || !created, "domain separator cannot be zero"); + + // Update gas limit if it's not zero + if (route.gasLimit > 0) { + stored.gasLimit = route.gasLimit; + } + + // Update relative gas price and base fee if any of them are greater than zero + if (route.relativeGasPriceDenominator > 0) { + UFloat9x56 relativeGasPrice = + UFloatMath.fromRational(route.relativeGasPriceNumerator, route.relativeGasPriceDenominator); + stored.relativeGasPrice = relativeGasPrice; + stored.baseFee = route.baseFee; + } + + emit RouteUpdated(route.networkId.asUint(), stored.relativeGasPrice, stored.baseFee, stored.gasLimit); + } + + /** + * @dev Storage initializer function, used to set up the initial storage of the contract. + * @param store Storage location. + * @param networks List of networks to initialize. + * @param networkdID The network id of this chain. + */ + function initialize(MainStorage storage store, Network[] calldata networks, NetworkID networkdID) internal { + for (uint256 i = 0; i < networks.length; i++) { + Network calldata network = networks[i]; + (bool created, NetworkInfo storage info) = getOrAdd(store, NetworkID.wrap(network.id)); + require(created, "network already initialized"); + require(network.id != networkdID.asUint() || network.gateway == address(this), "wrong gateway address"); + info.gasLimit = 15_000_000; // Default to 15M gas + info.relativeGasPrice = UFloatMath.ONE; + info.baseFee = 0; + } + } + + /** + * @dev Return all routes registered currently registered. + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function listRoutes(MainStorage storage store) internal view returns (Route[] memory) { + bytes32[] memory idx = store.routes.keys; + Route[] memory routes = new Route[](idx.length); + for (uint256 i = 0; i < idx.length; i++) { + (bool success, NetworkInfo storage route) = tryGet(store, NetworkID.wrap(uint16(uint256(idx[i])))); + require(success, "route not found"); + (uint256 numerator, uint256 denominator) = route.relativeGasPrice.toRational(); + routes[i] = Route({ + networkId: NetworkID.wrap(uint16(uint256(idx[i]))), + gasLimit: route.gasLimit, + baseFee: route.baseFee, + gateway: bytes32(uint256(uint160(address(this)))), + relativeGasPriceNumerator: uint128(numerator), + relativeGasPriceDenominator: uint128(denominator) + }); + } + return routes; + } + + /** + * @dev Check a few preconditions before estimate the GMP wei cost. + */ + function _checkPreconditions(NetworkInfo memory route, uint256 messageSize, uint256 gasLimit) private pure { + // Verify if the network exists + require(route.baseFee > 0 || UFloat9x56.unwrap(route.relativeGasPrice) > 0, "route is temporarily disabled"); + + // Verify if the gas limit and message size are within the limits + require(gasLimit <= route.gasLimit, "gas limit exceeded"); + require(messageSize <= MAX_PAYLOAD_SIZE, "maximum payload size exceeded"); + } + + /** + * @dev Utility function for measure the wei cost of a GMP message. + */ + function estimateCost(NetworkInfo memory route, bytes calldata data, uint256 gasLimit) + internal + pure + returns (uint256 gasCost, uint256 fee) + { + // Guarantee the networks exists and `data` is less than `MAX_PAYLOAD_SIZE` + _checkPreconditions(route, data.length, gasLimit); + + // Compute base cost + uint256 nonZeros = GasUtils.countNonZerosCalldata(data); + uint256 zeros = data.length - nonZeros; + + // Compute execution cost + gasCost = GasUtils.estimateGas(uint16(nonZeros), uint16(zeros), gasLimit); + + // Calculate the gas cost: gasPrice * gasCost + baseFee + fee = UFloatMath.saturatingMul(route.relativeGasPrice, gasCost).saturatingAdd(route.baseFee); + } + + /** + * @dev Utility function for measure the wei cost of a GMP message. + */ + function estimateWeiCost(NetworkInfo memory route, bytes calldata data, uint256 gasLimit) + internal + pure + returns (uint256) + { + _checkPreconditions(route, data.length, gasLimit); + uint256 nonZeros = GasUtils.countNonZerosCalldata(data); + uint256 zeros = data.length - nonZeros; + return + GasUtils.estimateWeiCost(route.relativeGasPrice, route.baseFee, uint16(nonZeros), uint16(zeros), gasLimit); + } + + /** + * @dev Utility function for measure the wei cost of a GMP message. + */ + function estimateWeiCost(NetworkInfo memory route, uint256 messageSize, uint256 gasLimit) + internal + pure + returns (uint256) + { + _checkPreconditions(route, messageSize, gasLimit); + return GasUtils.estimateWeiCost(route.relativeGasPrice, route.baseFee, uint16(messageSize), 0, gasLimit); + } +} diff --git a/analog-gmp/src/storage/Shards.sol b/analog-gmp/src/storage/Shards.sol new file mode 100644 index 000000000..26aed8e94 --- /dev/null +++ b/analog-gmp/src/storage/Shards.sol @@ -0,0 +1,382 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (src/storage/Shards.sol) +pragma solidity ^0.8.20; + +import {TssKey, Signature} from "../Primitives.sol"; +import {EnumerableSet, Pointer} from "../utils/EnumerableSet.sol"; +import {BranchlessMath} from "../utils/BranchlessMath.sol"; +import {StoragePtr} from "../utils/Pointer.sol"; + +library _ShardStore { + function from(uint256 xCoord) internal pure returns (ShardStore.ShardID) { + return ShardStore.ShardID.wrap(bytes32(xCoord)); + } + + /** + * @dev Converts a `StoragePtr` into a `ShardInfo`. + */ + function asShardInfo(StoragePtr ptr) internal pure returns (ShardStore.ShardInfo storage info) { + assembly { + info.slot := ptr + } + } + + /** + * @dev Converts a `ShardInfo` into a `StoragePtr`. + */ + function asPtr(ShardStore.ShardInfo storage info) internal pure returns (StoragePtr ptr) { + assembly { + ptr := info.slot + } + } +} + +/** + * @dev EIP-7201 Shard's Storage + */ +library ShardStore { + using Pointer for StoragePtr; + using Pointer for uint256; + using EnumerableSet for EnumerableSet.Map; + using _ShardStore for uint256; + using _ShardStore for StoragePtr; + using _ShardStore for ShardInfo; + + /** + * @dev Namespace of the shards storage `analog.one.gateway.shards`. + * keccak256(abi.encode(uint256(keccak256("analog.one.gateway.shards")) - 1)) & ~bytes32(uint256(0xff)); + */ + bytes32 internal constant _EIP7201_NAMESPACE = 0x582bcdebbeef4fb96dde802cfe96e9942657f4bedb5cfe94e8786bb683eb1f00; + + uint8 internal constant SHARD_ACTIVE = (1 << 0); // Shard active bitflag + uint8 internal constant SHARD_Y_PARITY = (1 << 1); // Pubkey y parity bitflag + + /** + * @dev Shard ID, this is the xCoord of the TssKey + */ + type ShardID is bytes32; + + /** + * @dev Shard info stored in the Gateway Contract + * OBS: the order of the attributes matters! ethereum storage is 256bit aligned, try to keep + * the shard info below 256 bit, so it can be stored in one single storage slot. + * reference: https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html + * + * @custom:storage-location erc7201:analog.one.gateway.shards + */ + struct ShardInfo { + uint8 yParity; + uint32 nonce; + uint64 createdAtBlock; + } + + /** + * @dev Shard info stored in the Gateway Contract + * OBS: the order of the attributes matters! ethereum storage is 256bit aligned, try to keep + * the shard info below 256 bit, so it can be stored in one single storage slot. + * reference: https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html + * + * @custom:storage-location erc7201:analog.one.gateway.shards + */ + struct MainStorage { + EnumerableSet.Map shards; + } + + error ShardAlreadyRegistered(ShardID id); + error ShardNotExists(ShardID id); + error IndexOutOfBounds(uint256 index); + + function getMainStorage() internal pure returns (MainStorage storage $) { + assembly { + $.slot := _EIP7201_NAMESPACE + } + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function has(MainStorage storage store, ShardID id) internal view returns (bool) { + return store.shards.has(ShardID.unwrap(id)); + } + + /** + * @dev Get or create a value. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function getOrAdd(MainStorage storage store, ShardID xCoord) private returns (bool, ShardInfo storage) { + (bool success, StoragePtr ptr) = store.shards.tryAdd(ShardID.unwrap(xCoord)); + return (success, ptr.asShardInfo()); + } + + /** + * @dev Removes a value from a set. O(1). + * + * Reverts if the value does not exist in the set. + */ + function remove(MainStorage storage store, ShardID id) internal { + StoragePtr ptr = store.shards.remove(ShardID.unwrap(id)); + if (ptr.isNull()) { + revert ShardNotExists(id); + } + } + + /** + * @dev Returns the number of values on the set. O(1). + */ + function length(MainStorage storage store) internal view returns (uint256) { + return store.shards.length(); + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(MainStorage storage store, uint256 index) internal view returns (ShardID, ShardInfo storage) { + (bytes32 xCoord, StoragePtr ptr) = store.shards.at(index); + if (ptr.isNull()) { + revert IndexOutOfBounds(index); + } + return (ShardID.wrap(xCoord), ptr.asShardInfo()); + } + + /** + * @dev Returns the value associated with `key`. O(1). + * + * Requirements: + * - `key` must be in the map. + */ + function get(MainStorage storage store, ShardID key) internal view returns (ShardInfo storage) { + StoragePtr ptr = store.shards.get(ShardID.unwrap(key)); + if (ptr.isNull()) { + revert ShardNotExists(key); + } + return ptr.asShardInfo(); + } + + /** + * @dev Returns the `KeyInfo` associated with `TssKey`. O(1). + * + * Requirements: + * - `key.xCoord` must be in the map. + */ + function get(MainStorage storage store, TssKey calldata key) internal view returns (ShardInfo storage) { + return get(store, ShardID.wrap(bytes32(key.xCoord))); + } + + /** + * @dev Returns the `KeyInfo` associated with `Signature`. O(1). + * + * Requirements: + * - `signature.xCoord` must be in the map. + */ + function get(MainStorage storage store, Signature calldata signature) internal view returns (ShardInfo storage) { + return get(store, ShardID.wrap(bytes32(signature.xCoord))); + } + + /** + * @dev Returns the value associated with `key`. O(1). + */ + function tryGet(MainStorage storage store, ShardID key) private view returns (bool, ShardInfo storage) { + (bool exists, StoragePtr ptr) = store.shards.tryGet(ShardID.unwrap(key)); + return (exists, ptr.asShardInfo()); + } + + /** + * @dev Register a single TSS key. + * Requirements: + * - The `newKey` should not be already registered. + */ + function register(MainStorage storage store, TssKey calldata newKey) internal returns (bool) { + // Check y-parity + require((newKey.yParity == 2 || newKey.yParity == 3), "y parity bit must be 2 or 3, cannot register shard"); + + // Read shard from storage + ShardID id = ShardID.wrap(bytes32(newKey.xCoord)); + (bool created, ShardInfo storage stored) = getOrAdd(store, id); + + // Check if the shard is already registered + if (!created) { + require(stored.nonce == 1 || newKey.yParity == (stored.yParity | 2), "tsskey.yParity mismatch"); + return false; + } + + // Get the current status and nonce + ShardInfo memory shard = stored; + + require( + shard.createdAtBlock == 0 || (shard.yParity | 2) == newKey.yParity, + "the provided y-parity doesn't match the existing y-parity, cannot register shard" + ); + + // Update nonce + shard.nonce |= uint32(BranchlessMath.toUint(shard.nonce == 0)); + + // Save new status and nonce in the storage + stored.createdAtBlock = + BranchlessMath.ternaryU64(shard.createdAtBlock > 0, shard.createdAtBlock, uint64(block.number)); + stored.nonce = shard.nonce; + stored.yParity = newKey.yParity & 1; + return true; + } + + /** + * @dev Register TSS keys in batch. + * Requirements: + * - The `keys` should not be already registered. + */ + function registerTssKeys(MainStorage storage store, TssKey[] calldata keys) internal { + // We don't perform any arithmetic operation, except iterate a loop + unchecked { + // Register or activate tss key (revoked keys keep the previous nonce) + for (uint256 i = 0; i < keys.length; i++) { + register(store, keys[i]); + } + } + } + + /** + * @dev Replace TSS keys in batch. + * Requirements: + * - The `keys` may or may not be registered. + */ + function replaceTssKeys(MainStorage storage store, TssKey[] calldata keys) + internal + returns (TssKey[] memory created, TssKey[] memory revoked) + { + unchecked { + revoked = listShards(store); + created = new TssKey[](keys.length); + + // Make sure the tss keys are correctly ordered, this makes easier to prevent repeated keys, and + // allows binary search. + uint256 createdCount = 0; + for (uint256 i = 0; i < keys.length; i++) { + TssKey calldata key = keys[i]; + if (i > 0) { + TssKey calldata previousKey = keys[i - 1]; + require( + previousKey.xCoord < key.xCoord, "tss keys must be orderd by 'key.xCoord' in asceding order" + ); + } + + if (register(store, key)) { + // Shard registered + created[createdCount++] = TssKey({yParity: key.yParity, xCoord: key.xCoord}); + } else { + // Shard already registered, remove it from the revoke list. + uint256 len = revoked.length; + for (uint256 j = 0; j < len; j++) { + TssKey memory current = revoked[j]; + if (current.xCoord == key.xCoord) { + revoked[j] = revoked[len - 1]; + len--; + assembly { + // decrement list, equivalent to `revoked.length--` + mstore(revoked, len) + } + } + } + } + } + + // Update `created` list length + assembly { + mstore(created, createdCount) + } + + // Revoke Shards + for (uint256 i = 0; i < revoked.length; i++) { + TssKey memory key = revoked[i]; + _revoke(store, ShardID.wrap(bytes32(key.xCoord))); + } + } + } + + /** + * @dev Revoke Shards keys. + * Requirements: + * - The `keys` must be registered. + */ + function revoke(MainStorage storage store, TssKey calldata key) internal returns (bool) { + // Read shard from storage + ShardID id = ShardID.wrap(bytes32(key.xCoord)); + (bool exists, ShardInfo memory stored) = tryGet(store, id); + + if (exists) { + // Check y-parity + require(stored.yParity == (key.yParity & 1), "y parity mismatch, cannot revoke key"); + return _revoke(store, id); + } + return false; + } + + /** + * @dev Revoke Shards keys. + */ + function _revoke(MainStorage storage store, ShardID id) private returns (bool) { + // Remove from the set + StoragePtr ptr = store.shards.remove(ShardID.unwrap(id)); + return !ptr.isNull(); + } + + /** + * @dev Revoke TSS keys im batch. + * Requirements: + * - The `publicKeys` must be registered. + */ + function revokeKeys(MainStorage storage store, TssKey[] calldata publicKeys) + internal + returns (TssKey[] memory revokedKeys) + { + // Revoke tss keys + uint256 keysLength = publicKeys.length; + revokedKeys = new TssKey[](keysLength); + uint256 revokedCount = 0; + + for (uint256 i = 0; i < publicKeys.length; i++) { + if (revoke(store, publicKeys[i])) { + revokedKeys[revokedCount++] = publicKeys[i]; + } + } + + if (revokedKeys.length != keysLength) { + assembly { + mstore(revokedKeys, revokedCount) + } + } + return revokedKeys; + } + + function _t(MainStorage storage store) internal view returns (TssKey[] memory) {} + + /** + * @dev Return all shards registered currently registered. + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function listShards(MainStorage storage store) internal view returns (TssKey[] memory) { + bytes32[] storage idx = store.shards.keys; + uint256 len = idx.length; + TssKey[] memory shards = new TssKey[](len); + for (uint256 i = 0; i < len; i++) { + ShardID id = ShardID.wrap(idx[i]); + (bool success, ShardInfo storage shard) = tryGet(store, id); + if (!success) { + revert ShardNotExists(id); + } + shards[i] = TssKey(shard.yParity + 2, uint256(ShardID.unwrap(id))); + } + return shards; + } +} diff --git a/analog-gmp/src/utils/BranchlessMath.sol b/analog-gmp/src/utils/BranchlessMath.sol new file mode 100644 index 000000000..988a1d470 --- /dev/null +++ b/analog-gmp/src/utils/BranchlessMath.sol @@ -0,0 +1,456 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (src/utils/BranchlessMath.sol) + +pragma solidity >=0.8.20; + +/** + * Rounding mode used when divide an integer. + */ +enum Rounding { + // Rounds towards zero + Floor, + // Rounds to the nearest value; if the number falls midway, + // it is rounded to the value above. + Nearest, + // Rounds towards positive infinite + Ceil +} + +/** + * @dev Utilities for branchless operations, useful when a constant gas cost is required. + */ +library BranchlessMath { + /** + * @dev Returns the smallest of two numbers. + */ + function min(uint256 x, uint256 y) internal pure returns (uint256) { + return ternary(x < y, x, y); + } + + /** + * @dev Returns the largest of two numbers. + */ + function max(uint256 x, uint256 y) internal pure returns (uint256) { + return ternary(x > y, x, y); + } + + /** + * @dev If `condition` is true returns `a`, otherwise returns `b`. + */ + function ternary(bool condition, uint256 a, uint256 b) internal pure returns (uint256) { + unchecked { + // branchless select, works because: + // b ^ (a ^ b) == a + // b ^ 0 == b + // + // This is better than doing `condition ? a : b` because: + // - Consumes less gas + // - Constant gas cost regardless the inputs + // - Reduces the final bytecode size + return b ^ ((a ^ b) * toUint(condition)); + } + } + + /** + * @dev If `condition` is true returns `a`, otherwise returns `b`. + * see `BranchlessMath.ternary` + */ + function ternary(bool condition, int256 a, int256 b) internal pure returns (int256 r) { + assembly { + r := xor(b, mul(xor(a, b), condition)) + } + } + + /** + * @dev If `condition` is true returns `a`, otherwise returns `b`. + * see `BranchlessMath.ternary` + */ + function ternary(bool condition, address a, address b) internal pure returns (address r) { + assembly { + r := xor(b, mul(xor(a, b), condition)) + } + } + + /** + * @dev If `condition` is true returns `a`, otherwise returns `b`. + * see `BranchlessMath.ternary` + */ + function ternary(bool condition, bytes32 a, bytes32 b) internal pure returns (bytes32 r) { + assembly { + r := xor(b, mul(xor(a, b), condition)) + } + } + + /** + * @dev If `condition` is true returns `a`, otherwise returns `b`. + * see `BranchlessMath.ternary` + */ + function ternaryU128(bool condition, uint128 a, uint128 b) internal pure returns (uint128 r) { + assembly { + r := xor(b, mul(xor(a, b), condition)) + } + } + + /** + * @dev If `condition` is true returns `a`, otherwise returns `b`. + * see `BranchlessMath.ternary` + */ + function ternaryU64(bool condition, uint64 a, uint64 b) internal pure returns (uint64 r) { + assembly { + r := xor(b, mul(xor(a, b), condition)) + } + } + + /** + * @dev If `condition` is true returns `a`, otherwise returns `b`. + * see `BranchlessMath.ternary` + */ + function ternaryU32(bool condition, uint32 a, uint32 b) internal pure returns (uint32 r) { + assembly { + r := xor(b, mul(xor(a, b), condition)) + } + } + + /** + * @dev If `condition` is true returns `a`, otherwise returns `b`. + * see `BranchlessMath.ternary` + */ + function ternaryU8(bool condition, uint8 a, uint8 b) internal pure returns (uint8 r) { + assembly { + r := xor(b, mul(xor(a, b), condition)) + } + } + + /** + * @dev If `condition` is true return `value`, otherwise return zero. + * see `BranchlessMath.ternary` + */ + function selectIf(bool condition, uint256 value) internal pure returns (uint256) { + unchecked { + return value * toUint(condition); + } + } + + /** + * @dev Unsigned saturating addition, bounds to UINT256 MAX instead of overflowing. + * equivalent to: + * uint256 r = x + y; + * return r >= x ? r : UINT256_MAX; + */ + function saturatingAdd(uint256 x, uint256 y) internal pure returns (uint256) { + unchecked { + x = x + y; + y = 0 - toUint(x < y); + return x | y; + } + } + + /** + * @dev Unsigned saturating subtraction, bounds to zero instead of overflowing. + * equivalent to: x > y ? x - y : 0 + */ + function saturatingSub(uint256 a, uint256 b) internal pure returns (uint256) { + unchecked { + // equivalent to: a > b ? a - b : 0 + return (a - b) * toUint(a > b); + } + } + + /** + * @dev Unsigned saturating multiplication, bounds to `2 ** 256 - 1` instead of overflowing. + */ + function saturatingMul(uint256 a, uint256 b) internal pure returns (uint256) { + unchecked { + uint256 c = a * b; + bool success; + assembly { + // Only true when the multiplication doesn't overflow + // (c / a == b) || (a == 0) + success := or(eq(div(c, a), b), iszero(a)) + } + return c | (toUint(success) - 1); + } + } + + /** + * @dev Unsigned saturating division, bounds to UINT256 MAX instead of overflowing. + */ + function saturatingDiv(uint256 x, uint256 y) internal pure returns (uint256 r) { + assembly { + // Solidity reverts with a division by zero error, while using inline assembly division does + // not revert, it returns zero. + // Reference: https://github.com/ethereum/solidity/issues/15200 + r := div(x, y) + } + } + + /** + * @dev Returns the ceiling of the division of two numbers. + * + * This differs from standard division with `/` in that it rounds towards infinity instead + * of rounding towards zero. + */ + function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) { + unchecked { + // The following calculation ensures accurate ceiling division without overflow. + // Since a is non-zero, (a - 1) / b will not overflow. + // The largest possible result occurs when (a - 1) / b is type(uint256).max, + // but the largest value we can obtain is type(uint256).max - 1, which happens + // when a = type(uint256).max and b = 1. + return selectIf(a > 0, ((a - 1) / b + 1)); + } + } + + /** + * @dev Unsigned saturating left shift, bounds to `2 ** 256 - 1` instead of overflowing. + */ + function saturatingShl(uint256 x, uint8 shift) internal pure returns (uint256 r) { + assembly { + // Detect overflow by checking if (x >> (256 - shift)) > 0 + r := gt(shr(sub(256, shift), x), 0) + + // Bounds to `type(uint256).max` if an overflow happened + r := or(shl(shift, x), sub(0, r)) + } + } + + /** + * @dev Returns the absolute unsigned value of a signed value. + * + * Ref: https://graphics.stanford.edu/~seander/bithacks.html#IntegerAbs + */ + function abs(int256 a) internal pure returns (uint256 r) { + assembly { + // Formula from the "Bit Twiddling Hacks" by Sean Eron Anderson. + // Since `n` is a signed integer, the generated bytecode will use the SAR opcode to perform the right shift, + // taking advantage of the most significant (or "sign" bit) in two's complement representation. + // This opcode adds new most significant bits set to the value of the previous most significant bit. As a result, + // the mask will either be `bytes(0)` (if n is positive) or `~bytes32(0)` (if n is negative). + let mask := sar(255, a) + + // A `bytes(0)` mask leaves the input unchanged, while a `~bytes32(0)` mask complements it. + r := xor(add(a, mask), mask) + } + } + + /** + * @dev Computes the absolute difference between x and y. + */ + function absDiff(uint256 x, uint256 y) internal pure returns (uint256) { + return abs(int256(x) - int256(y)); + } + + /** + * @dev Computes the absolute difference between x and y. + */ + function absDiff(int256 x, int256 y) internal pure returns (uint256) { + return abs(x - y); + } + + /** + * @dev Cast a boolean (false or true) to a uint256 (0 or 1) with no jump. + */ + function toUint(bool b) internal pure returns (uint256 u) { + assembly ("memory-safe") { + u := iszero(iszero(b)) + } + } + + /** + * @dev Cast a boolean (false or true) to a int256 (0 or 1) with no jump. + */ + function toInt(bool b) internal pure returns (int256 i) { + assembly ("memory-safe") { + i := iszero(iszero(b)) + } + } + + /** + * @dev Cast an address to uint256 + */ + function toUint(address addr) internal pure returns (uint256) { + return uint256(uint160(addr)); + } + + /** + * @dev Count the consecutive zero bits (trailing) on the right. + */ + function trailingZeros(uint256 x) internal pure returns (uint256 r) { + assembly { + // Compute largest power of two divisor of `x`. + x := and(x, sub(0, x)) + + // Use De Bruijn lookups to convert the power of 2 to log2(x). + // Reference: https://graphics.stanford.edu/~seander/bithacks.html#IntegerLogDeBruijn + r := byte(and(div(80, mod(x, 255)), 31), 0x0706050000040000010003000000000000000000020000000000000000000000) + r := add(byte(31, div(0xf8f0e8e0d8d0c8c0b8b0a8a09890888078706860585048403830282018100800, shr(r, x))), r) + } + } + + /** + * @dev Count the consecutive zero bits (trailing) on the right. + */ + function leadingZeros(uint256 x) internal pure returns (uint256 r) { + return 255 - log2(x) + toUint(x == 0); + } + + /** + * @dev Return the log in base 2 of a positive value rounded towards zero. + * Returns 0 if given 0. + */ + function log2(uint256 x) internal pure returns (uint256 r) { + unchecked { + // Round down to the closest power of 2 + // Reference: https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 + x |= x >> 1; + x |= x >> 2; + x |= x >> 4; + x |= x >> 8; + x |= x >> 16; + x |= x >> 32; + x |= x >> 64; + x |= x >> 128; + x = (x >> 1) + 1; + + // Use De Bruijn lookups to convert the power of 2 to floor(log2(x)). + // Reference: https://graphics.stanford.edu/~seander/bithacks.html#IntegerLogDeBruijn + assembly { + r := + byte(and(div(80, mod(x, 255)), 31), 0x0706050000040000010003000000000000000000020000000000000000000000) + r := + add(byte(31, div(0xf8f0e8e0d8d0c8c0b8b0a8a09890888078706860585048403830282018100800, shr(r, x))), r) + } + } + } + + /** + * @dev Aligns `x` to 32 bytes. + */ + function align32(uint256 x) internal pure returns (uint256 r) { + unchecked { + r = saturatingAdd(x, 31) & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0; + } + } + + /** + * @dev Computes `x * 2**exponent`, essentially shifting the value to the left when + * `exp` is positive, or shift to the right when `exp` is negative. + */ + function mul2pow(uint256 x, int256 exponent) internal pure returns (uint256) { + unchecked { + // Rationale: + // - When the exponent is negative, then `x << exp` is zero. + // - When the exponent is positive, then `x >> -exp` is zero. + // Then we can use the `or` operation to get the correct result. + // result = (x << exp) | (x >> -exp) + return (x << uint256(exponent)) | (x >> uint256(-exponent)); + } + } + + /** + * @dev Computes `x * 2**exponent`, bounds to `2 ** 256 - 1` instead overflowing. + */ + function saturatingMul2pow(uint256 x, int256 exponent) internal pure returns (uint256 result) { + unchecked { + result = mul2pow(x, exponent); + // An overflow happens when exponent is positive and (x << exp) >> exp != x. + bool success = (result >> uint256(exponent)) == (x * toUint(exponent > 0)); + // Bounds to `type(uint256).max` if `success` is false. + return result | (toUint(success) - 1); + } + } + + /** + * @notice Calculates x * y / denominator with full precision, following the selected rounding direction. + * Throws if result overflows a uint256 or denominator == 0. + * + * @dev This this an modified version of the original implementation by OpenZeppelin SDK, which is released under MIT. + * original: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.0.2/contracts/utils/math/Math.sol#L117-L202 + */ + function mulDiv(uint256 x, uint256 y, uint256 denominator, Rounding rounding) internal pure returns (uint256) { + unchecked { + // Compute remainder. + // - Rounding.Floor then remainder is 0 + // - Rounding.Nearest then remainder is denominator / 2 + // - Rounding.Ceil then remainder is denominator - 1 + uint256 remainder = denominator; + remainder *= toUint(rounding != Rounding.Floor); + remainder >>= toUint(rounding == Rounding.Nearest); + remainder -= toUint(rounding == Rounding.Ceil); + + // 512-bit multiply [prod1 prod0] = x * y + remainder. + // Compute the product mod 2²⁵⁶ and mod 2²⁵⁶ - 1, then use the Chinese Remainder Theorem to reconstruct + // the 512 bit result. The result is stored in two 256 variables such that product = prod1 * 2²⁵⁶ + prod0. + uint256 prod0 = x * y; // Least significant 256 bits of the product + uint256 prod1; // Most significant 256 bits of the product + assembly { + let mm := mulmod(x, y, not(0)) + prod1 := sub(sub(mm, prod0), lt(mm, prod0)) + + // Only round up if the final result is less than 2²⁵⁶. + remainder := mul(remainder, lt(prod1, denominator)) + + // Add 256 bit remainder to 512 bit number. + // Cannot overflow once (2²⁵⁶ - 1)² + 2²⁵⁶ - 1 < 2⁵¹². + mm := add(prod0, remainder) + prod1 := add(prod1, lt(mm, prod0)) + prod0 := mm + } + + // Make sure the result is less than 2**256. Also prevents denominator == 0. + require(prod1 < denominator, "muldiv overflow"); + + /////////////////////////////////////////////// + // 512 by 256 division. + /////////////////////////////////////////////// + + // Make division exact by subtracting the remainder from [prod1 prod0]. + assembly { + // Compute remainder using addmod and mulmod. + remainder := addmod(remainder, mulmod(x, y, denominator), denominator) + + // Subtract 256 bit number from 512 bit number. + prod1 := sub(prod1, gt(remainder, prod0)) + prod0 := sub(prod0, remainder) + } + + // Factor powers of two out of denominator and compute largest power of two divisor of denominator. + // Always >= 1. See https://cs.stackexchange.com/q/138556/92363. + + uint256 twos = denominator & (0 - denominator); + assembly { + // Divide denominator by twos. + denominator := div(denominator, twos) + + // Divide [prod1 prod0] by twos. + prod0 := div(prod0, twos) + + // Flip twos such that it is 2²⁵⁶ / twos. If twos is zero, then it becomes one. + twos := add(div(sub(0, twos), twos), 1) + } + + // Shift in bits from prod1 into prod0. + prod0 |= prod1 * twos; + + // Invert denominator mod 2²⁵⁶. Now that denominator is an odd number, it has an inverse modulo 2²⁵⁶ such + // that denominator * inv ≡ 1 mod 2²⁵⁶. Compute the inverse by starting with a seed that is correct for + // four bits. That is, denominator * inv ≡ 1 mod 2⁴. + uint256 inverse = (3 * denominator) ^ 2; + + // Use the Newton-Raphson iteration to improve the precision. Thanks to Hensel's lifting lemma, this also + // works in modular arithmetic, doubling the correct bits in each step. + inverse *= 2 - denominator * inverse; // inverse mod 2⁸ + inverse *= 2 - denominator * inverse; // inverse mod 2¹⁶ + inverse *= 2 - denominator * inverse; // inverse mod 2³² + inverse *= 2 - denominator * inverse; // inverse mod 2⁶⁴ + inverse *= 2 - denominator * inverse; // inverse mod 2¹²⁸ + inverse *= 2 - denominator * inverse; // inverse mod 2²⁵⁶ + + // Because the division is now exact we can divide by multiplying with the modular inverse of denominator. + // This will give us the correct result modulo 2²⁵⁶. Since the preconditions guarantee that the outcome is + // less than 2²⁵⁶, this is the final result. We don't need to compute the high bits of the result and prod1 + // is no longer required. + return prod0 * inverse; + } + } +} diff --git a/analog-gmp/src/utils/ERC1967.sol b/analog-gmp/src/utils/ERC1967.sol new file mode 100644 index 000000000..420ef1350 --- /dev/null +++ b/analog-gmp/src/utils/ERC1967.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (src/utils/ERC1967.sol) + +pragma solidity >=0.8.0; + +/// @title Minimal implementation of ERC1967 storage slot +library ERC1967 { + /** + * @dev Storage slot with the address of the current implementation. + * This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1. + */ + // solhint-disable-next-line private-vars-leading-underscore + bytes32 internal constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + + /** + * @dev Storage slot with the admin of the contract. + * This is the keccak-256 hash of "eip1967.proxy.admin" subtracted by 1. + */ + // solhint-disable-next-line private-vars-leading-underscore + bytes32 internal constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + + /** + * @dev Emitted when the implementation is upgraded. + */ + event Upgraded(address indexed implementation); + + /** + * @dev Emitted when the admin account has changed. + */ + event AdminChanged(address previousAdmin, address newAdmin); + + /** + * @dev The `implementation` of the proxy is invalid. + */ + error ERC1967InvalidImplementation(address implementation); + + /** + * @dev The `admin` of the proxy is invalid. + */ + error ERC1967InvalidAdmin(address admin); + + /** + * @dev Returns the current admin. + * + * TIP: To get this value clients can read directly from the storage slot shown below (specified by ERC-1967) using + * the https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. + * `0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103` + */ + function getAdmin() internal view returns (address) { + return _getAddressSlot(ADMIN_SLOT); + } + + /** + * @dev Stores a new address in the ERC-1967 admin slot. + */ + function setAdmin(address newAdmin) internal { + if (newAdmin == address(0)) { + revert ERC1967InvalidAdmin(address(0)); + } + emit AdminChanged(getAdmin(), newAdmin); + _setAddressSlot(ADMIN_SLOT, newAdmin); + } + + /** + * @dev Returns the current implementation address. + */ + function getImplementation() internal view returns (address) { + return _getAddressSlot(IMPLEMENTATION_SLOT); + } + + /** + * @dev Stores a new address in the ERC-1967 implementation slot. + */ + function setImplementation(address newImplementation) internal { + if (newImplementation.code.length == 0) { + revert ERC1967InvalidImplementation(newImplementation); + } + emit Upgraded(newImplementation); + _setAddressSlot(IMPLEMENTATION_SLOT, newImplementation); + } + + function _getAddressSlot(bytes32 slot) private view returns (address addr) { + assembly { + addr := sload(slot) + } + } + + function _setAddressSlot(bytes32 slot, address addr) private { + assembly { + sstore(slot, addr) + } + } +} diff --git a/analog-gmp/src/utils/EnumerableSet.sol b/analog-gmp/src/utils/EnumerableSet.sol new file mode 100644 index 000000000..3055eed88 --- /dev/null +++ b/analog-gmp/src/utils/EnumerableSet.sol @@ -0,0 +1,236 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (src/utils/EnumerableMap.sol) +pragma solidity ^0.8.20; + +import {StoragePtr, Pointer} from "./Pointer.sol"; + +/** + * @dev Library for managing an enumerable variant of Solidity's + * https://solidity.readthedocs.io/en/latest/types.html#mapping-types[`mapping`] + * type. + */ +library EnumerableSet { + using Pointer for StoragePtr; + + error ValueAlreadyPresent(bytes32); + + /** + * @dev Shard info stored in the Gateway Contract + * OBS: the order of the attributes matters! ethereum storage is 256bit aligned, try to keep + * the shard info below 256 bit, so it can be stored in one single storage slot. + * reference: https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html + * + * @custom:storage-location erc7201:analog.one.gateway.shards + */ + struct Map { + bytes32[] keys; + mapping(bytes32 => StoragePtr) values; + } + + /** + * @dev Returns index of a given value in the set. O(1). + * + * Returns -1 if the value is not in the set. + */ + function indexOf(Map storage map, StoragePtr ptr) internal view returns (int256 index) { + assembly ("memory-safe") { + index := not(sload(sub(ptr, 1))) + mstore(0x00, map.slot) + mstore(0x00, sload(add(keccak256(0x00, 0x20), index))) + mstore(0x20, add(map.slot, 1)) + index := or(index, sub(and(eq(ptr, keccak256(0x00, 0x40)), lt(index, sload(map.slot))), 1)) + } + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function contains(Map storage map, StoragePtr value) internal view returns (bool r) { + return indexOf(map, value) >= 0; + } + + /** + * @dev Returns true if the key is in the set. O(1). + */ + function has(Map storage map, bytes32 key) internal view returns (bool r) { + assembly ("memory-safe") { + mstore(0x00, key) + mstore(0x20, add(map.slot, 1)) + r := keccak256(0x00, 0x40) + r := gt(sload(sub(r, 1)), 0) + } + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function add(Map storage map, bytes32 key) internal returns (StoragePtr r) { + bool success; + (success, r) = tryAdd(map, key); + if (!success) { + revert ValueAlreadyPresent(key); + } + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function tryAdd(Map storage map, bytes32 key) internal returns (bool success, StoragePtr r) { + assembly ("memory-safe") { + mstore(0x00, key) + mstore(0x20, add(map.slot, 1)) + r := keccak256(0x00, 0x40) + success := iszero(sload(sub(r, 1))) + if success { + // Load the array size + let size := sload(map.slot) + + // Store the value + mstore(0x00, map.slot) + sstore(add(keccak256(0x00, 0x20), size), key) + + // Update the value's index + sstore(sub(r, 1), not(size)) + + // Update array size + size := add(size, 1) + sstore(map.slot, size) + } + } + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns the removed value storage pointer, if it was present, or null if it was not. + */ + function remove(Map storage map, bytes32 key) internal returns (StoragePtr r) { + assembly ("memory-safe") { + // Find the value's index + mstore(0x00, key) + mstore(0x20, add(map.slot, 1)) + r := keccak256(0x00, 0x40) + let index := not(sload(sub(r, 1))) + + // First element storage index + let keys_count := sload(map.slot) + mstore(0x00, map.slot) + let keys_start := keccak256(0x00, 0x20) + let val_key_ptr := add(keys_start, index) + + // (index < map.keys.length) && key == map.keys[map.values[key].index] + r := mul(r, and(lt(index, keys_count), eq(key, sload(val_key_ptr)))) + + if r { + // (index + 1) < map.keys.length + if lt(add(index, 1), keys_count) { + // Move the last element to the removed element's position + let last_index := sub(keys_count, 1) + let last_key := sload(add(keys_start, last_index)) + sstore(val_key_ptr, last_key) + + // Update the last element's index + mstore(0x00, last_key) + mstore(0x20, add(map.slot, 1)) + sstore(sub(keccak256(0x00, 0x40), 1), not(index)) + } + + // Update array size + sstore(map.slot, sub(keys_count, 1)) + + // Remove index + sstore(sub(r, 1), 0) + } + } + } + + /** + * @dev Returns the number of values on the set. O(1). + */ + function length(Map storage map) internal view returns (uint256) { + return map.keys.length; + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(Map storage map, uint256 index) internal view returns (bytes32 key, StoragePtr r) { + assembly ("memory-safe") { + mstore(0x00, map.slot) + key := sload(add(keccak256(0x00, 0x20), index)) + mstore(0x00, key) + mstore(0x20, add(map.slot, 1)) + r := keccak256(0x00, 0x40) + r := mul(r, and(lt(index, sload(map.slot)), eq(index, not(sload(sub(r, 1)))))) + } + } + + /** + * @dev Returns the value associated with `key`. O(1). + * + * Requirements: + * - `key` must be in the map. + */ + function get(Map storage map, bytes32 key) internal view returns (StoragePtr r) { + assembly ("memory-safe") { + mstore(0x00, key) + mstore(0x20, add(map.slot, 1)) + r := keccak256(0x00, 0x40) + r := mul(r, gt(sload(sub(r, 1)), 0)) + } + } + + /** + * @dev Tries to returns the value associated with `key`. O(1). + * Does not revert if `key` is not in the map. + */ + function tryGet(Map storage map, bytes32 key) internal view returns (bool exists, StoragePtr r) { + assembly ("memory-safe") { + mstore(0x00, key) + mstore(0x20, add(map.slot, 1)) + r := keccak256(0x00, 0x40) + exists := gt(sload(sub(r, 1)), 0) + } + } + + /** + * @dev Returns the value associated with `key`. O(1). + */ + function getUnchecked(Map storage map, bytes32 key) internal pure returns (StoragePtr r) { + assembly ("memory-safe") { + mstore(0x00, key) + mstore(0x20, add(map.slot, 1)) + r := keccak256(0x00, 0x40) + } + } + + // /** + // * @dev Return the entire set in an array + // * + // * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + // * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + // * this function has an unbounded cost, and using it as part of a state-changing function may render the function + // * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + // */ + // function _values(EnumerableMap storage m) private view returns (KeyInfo[] memory) { + // ShardID[] memory keys = s.keys; + // KeyInfo[] memory values = new KeyInfo[](keys.length); + // for (uint256 i = 0; i < keys.length; i++) { + // values[i] = s.shards[keys[i]]; + // } + // return values; + // } +} diff --git a/analog-gmp/src/utils/Float9x56.sol b/analog-gmp/src/utils/Float9x56.sol new file mode 100644 index 000000000..85cea4368 --- /dev/null +++ b/analog-gmp/src/utils/Float9x56.sol @@ -0,0 +1,453 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (src/utils/Float9x56.sol) + +pragma solidity >=0.8.0; + +import {BranchlessMath, Rounding} from "./BranchlessMath.sol"; + +/** + * @dev Unsigned Float with 9-bit exponent and 56-bit significand precision (55 explicitly stored). + * + * UFloat9x56 values are described by `2**exponent * (1 + fraction)`, where the `exponent` is a signed + * integer between -255 and 255, and the `fraction` is the next 55 binary digits, which translates to + * 15~17 decimal digits. Zero and values below 2**-255 have a special encoding format. + * + * # Exponent Encoding + * The exponent is encoded using offset-binary representation, with the zero offset being 256, example: + * - 2**-255 is encoded as -255 + 256 == 1. (smallest exponent for normal numbers) + * - 2**0 is encoded as 0 + 256 == 256 (zero offset) + * - 2**6 is encoded as 6 + 256 == 262 + * - 2**255 is encoded as 255 + 256 == 511 (highest exponent) + * + * # Subnormal Numbers + * The smallest possible exponent -256 have a special meaning, it represents subnormal numbers, where the + * exponent is -255 and the +1 is removed, this is useful to represent zero and values below 2**-255. + * + * Assume `e` is an 9-bit encoded exponent between 0~511: + * - When `e > 0`, the number is described by: 2**(e - 256) * (1 + fraction) + * - When `e == 0`, the number is described by: 2**-255 * fraction + */ +type UFloat9x56 is uint64; + +library UFloatMath { + using BranchlessMath for uint256; + + /** + * @dev Constant representing 0.0 in UFloat9x56. + */ + UFloat9x56 internal constant ZERO = UFloat9x56.wrap(0x0000000000000000); + + /** + * @dev Constant representing 1.0 in UFloat9x56. + */ + UFloat9x56 internal constant ONE = UFloat9x56.wrap(0x8000000000000000); + + /** + * @dev Maximum value representable in UFloat9x56, i.e., 2**200 * (2**56 - 1). + */ + UFloat9x56 internal constant MAX = UFloat9x56.wrap(0xffffffffffffffff); + + /** + * @dev Default rounding mode for conversion functions. + */ + Rounding internal constant DEFAULT_ROUNDING = Rounding.Nearest; + + /** + * @dev Number of bits used to represent the mantissa. + */ + uint256 internal constant MANTISSA_DIGITS = 56; + + /** + * @dev Maximum value the mantissa can assume. + */ + uint256 internal constant MANTISSA_MAX = (2 ** MANTISSA_DIGITS) - 1; + + /** + * @dev Minimum value the mantissa can assume, lower than this value is considered a subnormal number. + */ + uint256 internal constant MANTISSA_MIN = 1 << (MANTISSA_DIGITS - 1); + + /** + * @dev The maximum value that can be represented by `UFloat9x56`. + */ + uint256 internal constant MAX_VALUE = MANTISSA_MAX << (256 - MANTISSA_DIGITS); + + /** + * @dev Mask to extract the mantissa from raw `UFloat9x56`. + */ + uint256 private constant MANTISSA_MASK = MANTISSA_MAX >> 1; + + /** + * @dev Bit offset used to extract the exponent from raw `UFloat9x56`. + * This value also represents the number of signficand bits explicitly stored. + */ + uint256 private constant EXPONENT_OFFSET = MANTISSA_DIGITS - 1; + + /** + * @dev Position of the carry bit when converting uint256 to `UFloat9x56`. + */ + uint256 private constant CARRY_BIT = 2 ** (256 - MANTISSA_DIGITS); + + /** + * @dev multiply an UFloat9x56 by an uint256 in constant gas. + */ + function mul(UFloat9x56 x, uint256 y) internal pure returns (uint256 result) { + assembly { + // Extract exponent and fraction + let exponent := shr(55, x) + let fraction := or(and(x, 0x007fffffffffffff), shl(55, gt(exponent, 0))) + exponent := sub(exponent, sub(311, iszero(exponent))) + fraction := shl(mul(exponent, sgt(exponent, 0)), fraction) + + // 512-bit multiply [high low] = x * y. Compute the product mod 2²⁵⁶ and mod 2²⁵⁶ - 1 + let mm := mulmod(y, fraction, not(0)) + let low := mul(y, fraction) + let high := sub(sub(mm, low), lt(mm, low)) + + // Shift low and high if exponent >= 256 + let shift := mul(sub(0, exponent), slt(exponent, 0)) + mm := gt(shift, 255) + low := xor(low, mul(xor(low, high), mm)) + high := mul(high, iszero(mm)) + + // make sure shift is between 0 and 255 + shift := mod(shift, 256) + + // Combine high and low + high := shl(sub(256, shift), high) + low := shr(shift, low) + result := or(high, low) + } + } + + /** + * @dev Saturating multiplication, bounds to `2 ** 256 - 1` instead of overflowing. + */ + function saturatingMul(UFloat9x56 x, uint256 y) internal pure returns (uint256 result) { + assembly { + // Extract exponent and fraction + let exponent := shr(55, x) + let fraction := or(and(x, 0x007fffffffffffff), shl(55, gt(exponent, 0))) + exponent := sub(exponent, sub(311, iszero(exponent))) + fraction := shl(mul(exponent, sgt(exponent, 0)), fraction) + + // 512-bit multiply [high low] = x * y. Compute the product mod 2²⁵⁶ and mod 2²⁵⁶ - 1 + let mm := mulmod(y, fraction, not(0)) + let low := mul(y, fraction) + let high := sub(sub(mm, low), lt(mm, low)) + + // Shift low and high if exponent >= 256 + let shift := mul(sub(0, exponent), slt(exponent, 0)) + mm := gt(shift, 255) + low := xor(low, mul(xor(low, high), mm)) + high := mul(high, iszero(mm)) + + // make sure shift is between 0 and 255 + shift := mod(shift, 256) + + // Combine high and low + mm := iszero(shr(shift, high)) // detect overflow + high := shl(sub(256, shift), high) + low := shr(shift, low) + result := or(high, low) + result := or(result, sub(mm, 1)) // saturate if overflow + } + } + + /** + * @dev Returns the mantissa and base 2 exponent as integers, respectively. + * The original number can be recovered by `mantissa * 2 ** exponent`. + * Returns (0, -311) if the value is zero. + */ + function decode(UFloat9x56 value) internal pure returns (uint56, int16) { + unchecked { + // Extract the exponent + int256 exponent = int256(uint256(UFloat9x56.unwrap(value)) >> EXPONENT_OFFSET); + + // Extract the mantissa + uint256 mantissa = uint56(UFloat9x56.unwrap(value) & MANTISSA_MASK); + + // If the value is subnormal, then the exponent is -310 and mantissa msb is not set. + bool isSubnormal = exponent == 0; + mantissa |= BranchlessMath.toUint(!isSubnormal) << EXPONENT_OFFSET; + exponent += BranchlessMath.toInt(isSubnormal && mantissa > 0); + + // Exponent bias + mantissa shift + exponent -= 256 + int256(EXPONENT_OFFSET); + + return (uint56(mantissa), int16(exponent)); + } + } + + /** + * @dev Encode the provided `mantissa` and `exponent` into `UFloat9x56`, this method assumes the + * mantissa msb is set when the number is normal, and unset when the number is subnormal. + */ + function encode(uint256 mantissa, int256 exponent) internal pure returns (UFloat9x56) { + unchecked { + // Minimum exponent is -310 when the mantissa is greater than zero. + int256 minExponent = -311 + BranchlessMath.toInt(mantissa > 0); + require(exponent >= minExponent && exponent <= 200, "UFloat9x56: exponent out of bounds"); + + // If the mantissa is zero, then the exponent must be -311. + exponent = BranchlessMath.ternary(mantissa == 0, -311, exponent); + + // For subnormal numbers the mantissa msb is not set. + bool isSubnormal = mantissa < MANTISSA_MIN && exponent == minExponent; + require( + isSubnormal || (mantissa >= MANTISSA_MIN && mantissa <= MANTISSA_MAX), "UFloat9x56: invalid mantissa" + ); + isSubnormal = isSubnormal && mantissa > 0; + + // Remove mantissa most significant bit. + mantissa &= MANTISSA_MASK; + + // Encode the exponent as an 9-bit unsigned integer. + exponent += 311 - BranchlessMath.toInt(isSubnormal); + + // Shift the exponent to the correct position + exponent <<= EXPONENT_OFFSET; + + // Encode the exponent and mantissa into `UFloat9x56` + return UFloat9x56.wrap(uint64(mantissa) | uint64(uint256(exponent))); + } + } + + /** + * @dev Compare if `UFloat9x56` is equal to another integer, considering only the mantissa bits. + */ + function _integerMask(UFloat9x56 x) private pure returns (uint256, uint256) { + (uint256 mantissa, int256 exponent) = decode(x); + unchecked { + // Shift y if the exponent is negative + mantissa >>= BranchlessMath.abs(exponent) * BranchlessMath.toUint(exponent < 0); + uint256 shift = uint256(exponent) * BranchlessMath.toUint(exponent > 0); + return (type(uint256).max << shift, mantissa << shift); + } + } + + /** + * @dev Compare if `UFloat9x56` is equal to another integer, considering only the mantissa bits. + */ + function eq(UFloat9x56 x, uint256 y) internal pure returns (bool r) { + (uint256 mask, uint256 integer) = _integerMask(x); + return (y & mask) == integer; + } + + /** + * @dev Compare if `UFloat9x56` is equal to another integer, considering only the mantissa bits. + */ + function eq(UFloat9x56 a, UFloat9x56 b) internal pure returns (bool) { + return UFloat9x56.unwrap(a) == UFloat9x56.unwrap(b); + } + + /** + * @dev Compare if `UFloat9x56` is equal to another integer, considering only the mantissa bits. + */ + function gt(UFloat9x56 a, UFloat9x56 b) internal pure returns (bool) { + return UFloat9x56.unwrap(a) > UFloat9x56.unwrap(b); + } + + /** + * @dev Compare if `UFloat9x56` is equal to another integer, considering only the mantissa bits. + */ + function gt(UFloat9x56 x, uint256 y) internal pure returns (bool r) { + return truncate(x) > y; + } + + /** + * @dev Compare if `UFloat9x56` is equal to another integer, considering only the mantissa bits. + */ + function ge(UFloat9x56 a, UFloat9x56 b) internal pure returns (bool) { + return UFloat9x56.unwrap(a) >= UFloat9x56.unwrap(b); + } + + /** + * @dev Compare if `UFloat9x56` is equal to another integer, considering only the mantissa bits. + */ + function lt(UFloat9x56 a, UFloat9x56 b) internal pure returns (bool) { + return UFloat9x56.unwrap(a) < UFloat9x56.unwrap(b); + } + + /** + * @dev Compare if `UFloat9x56` is equal to another integer, considering only the mantissa bits. + */ + function le(UFloat9x56 a, UFloat9x56 b) internal pure returns (bool) { + return UFloat9x56.unwrap(a) <= UFloat9x56.unwrap(b); + } + + /** + * @dev Returns the integer part. This means that non-integer numbers are always truncated towards zero + * This function always returns the precise result. + */ + function truncate(UFloat9x56 value) internal pure returns (uint256) { + (uint256 mantissa, int256 exponent) = decode(value); + return mantissa.mul2pow(exponent); + } + + /** + * @dev Converts uint256 to `UFloat9x56`, following the selected rounding direction. + * By default, it rounds to the nearest value. + */ + function fromUint(uint256 value) internal pure returns (UFloat9x56) { + return fromUint(value, DEFAULT_ROUNDING); + } + + /** + * @dev Converts uint256 to `UFloat9x56`, following the selected rounding direction. + * IMPORTANT: Always round down if the value is greater than `MAX_VALUE`. + */ + function fromUint(uint256 value, Rounding rounding) internal pure returns (UFloat9x56) { + unchecked { + // Compute the exponent, if `value > 0` then the exponent cannot be less than 2**-55. + uint256 exponent = BranchlessMath.log2(value) + 256; + + // Normalize mantissa by removing leading zeros, this step make sure the `CARRY_BIT` + // is always in the same position for any given value. + uint256 mantissa = value << (511 - exponent); + + // Set carry bit based on selected rouding direction. + uint256 carry = CARRY_BIT; + carry *= BranchlessMath.toUint(rounding != Rounding.Floor && value < MAX_VALUE); + carry -= BranchlessMath.toUint(rounding == Rounding.Ceil && carry > 0); + carry >>= BranchlessMath.toUint(rounding == Rounding.Nearest); + carry += mantissa & (CARRY_BIT - 1); + carry = BranchlessMath.toUint(carry >= CARRY_BIT); + + // Shift mantissa to a 56 bit integer then add the carry bit. + mantissa >>= 256 - MANTISSA_DIGITS; + mantissa += carry; + + // Increment the exponent if mantissa overflow after adding the carry bit. + carry = BranchlessMath.toUint(mantissa > MANTISSA_MAX); + mantissa >>= carry; + exponent += carry; + + // If the value is zero, then the exponent must be -311. + exponent *= BranchlessMath.toUint(value > 0); + + // Encode mantissa and exponent into `UFloat9x56` + mantissa &= MANTISSA_MASK; + exponent <<= EXPONENT_OFFSET; + return UFloat9x56.wrap(uint64(mantissa) | uint64(exponent)); + } + } + + /** + * @dev Converts numerator / denominator to `UFloat9x56`, following the selected rounding direction. + */ + function fromRational(uint256 numerator, uint256 denominator) internal pure returns (UFloat9x56) { + return fromRational(numerator, denominator, DEFAULT_ROUNDING); + } + + /** + * @dev Converts numerator / denominator to `UFloat9x56`, following the selected rounding direction. + */ + function fromRational(uint256 numerator, uint256 denominator, Rounding rounding) + internal + pure + returns (UFloat9x56) + { + unchecked { + int256 exponent; + { + // Remove leading zeros from numerator and denominator + uint256 numbits = BranchlessMath.log2(numerator); + uint256 denbits = BranchlessMath.log2(denominator); + numerator <<= 255 - numbits; + denominator <<= 255 - denbits; + + // Compute exponent + exponent = int256(numbits) - int256(denbits); + } + + // If `(numerator / denominator) <= 2**-255` then it is subnormal number + bool isSubnormal = numerator > 0 && exponent <= -255; + + // Adjust the exponent to guarantee the mantissa most significant bit is set + uint256 shift = MANTISSA_DIGITS; + shift -= BranchlessMath.toUint(numerator >= denominator || isSubnormal); + exponent -= int256(shift); + + // Compute (numerator * 2**exponent) / denominator + uint256 mantissa = BranchlessMath.mulDiv(numerator, 1 << shift, denominator, rounding); + + // Adjust mantissa and exponent when it exceeds 56 bits, this is only possible when + // all mantissa bits are set and the value is rounded up, as described below. + // ```solidity + // UFloatMath.fromRational(0x2ffffffffffffff, 3, Rounding.Floor) == (2**0 * 0xffffffffffffff) + // UFloatMath.fromRational(0x2ffffffffffffff, 3, Rounding.Ceil) == (2**1 * 0x80000000000000) + // ``` + shift = BranchlessMath.toUint(mantissa > MANTISSA_MAX); + mantissa >>= shift; + exponent += int256(shift); + + // If the mantissa is zero, then the exponent is the minimum value. + exponent = BranchlessMath.ternary(mantissa == 0, -311, exponent); + + // Adjust exponent to fit in 9 bits + exponent += 311 - int256(BranchlessMath.toUint(isSubnormal)); + exponent <<= EXPONENT_OFFSET; + + // Remove mantissa most significant bit + mantissa &= MANTISSA_MASK; + + return UFloat9x56.wrap(uint64(mantissa) | uint64(uint256(exponent))); + } + } + + /** + * @dev Convert `UFloat9x56` to a rational number, expressed as numerator / denominator. + * Obs: Values above 2**-256 are represented precisely, values below are approximated or round down to zero. + */ + function toRational(UFloat9x56 value) internal pure returns (uint256 numerator, uint256 denominator) { + unchecked { + if (UFloat9x56.unwrap(value) == 0) { + return (0, 1); + } + + int256 exponent; + (numerator, exponent) = decode(value); + + // Remove trailing zeros from mantissa. + { + uint256 trailingZeros = BranchlessMath.trailingZeros(numerator); + trailingZeros *= BranchlessMath.toUint(exponent < 0); + exponent += int256(trailingZeros); + numerator >>= trailingZeros; + } + + if (exponent > 0) { + // The exponent is positive, cannot overflow once the maximum exponent is 200. + // Calculates: (mantissa * 2**exponent) / 1 + numerator <<= uint256(exponent); + denominator = 1; + } else if (exponent > -256) { + // The exponent is negative, so we shift the denominator and keep the numerator. + // Calculates: mantissa / 2**-exponent + denominator = 1 << uint256(-exponent); + } else { + // Is not possible to represent such tiny values accurately given the denominator has more than 256 bit, + // but is still possible to get a good aproximation if we set the numerator to one: + // Calculates: 1 / (2**-exponent / mantissa) + // + // The final exponent is computed as a product of two exponents: + // 2**-exponent == 2**exp0 * 2*exp1 + uint256 exp0 = 255; + uint256 exp1 = BranchlessMath.abs(exponent) - exp0; + + // If numerator is less or equal to `2**exp1`, then the denominator has more than 256bit, so return zero. + if (exp1 >= numerator) { + return (0, 1); + } + + // Compute full 512 bit multiplication and division as (2**exp0 * 2**exp1) / mantissa. + denominator = BranchlessMath.mulDiv(1 << exp0, 1 << exp1, numerator, Rounding.Nearest); + + // Handle the case where the denominator is round towards zero. + numerator = BranchlessMath.toUint(denominator > 0); + denominator |= BranchlessMath.toUint(denominator == 0); + } + } + } +} diff --git a/analog-gmp/src/utils/GasUtils.sol b/analog-gmp/src/utils/GasUtils.sol new file mode 100644 index 000000000..4a9de9b2e --- /dev/null +++ b/analog-gmp/src/utils/GasUtils.sol @@ -0,0 +1,512 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (src/utils/GasUtils.sol) + +pragma solidity >=0.8.20; + +import {UFloat9x56, UFloatMath} from "./Float9x56.sol"; +import {BranchlessMath} from "./BranchlessMath.sol"; + +/** + * @dev Utilities for compute the GMP gas price, gas cost and gas needed. + */ +library GasUtils { + using BranchlessMath for uint256; + + /** + * @dev How much gas is used until the first `gasleft()` instruction is executed in the `Gateway.batchExecute` method. + * + * HOW TO UPDATE THIS VALUE: + * 1. Run `forge test --match-test=test_gasMeter --fuzz-runs=1 --debug` + * 2. Move the cursor until you enter the `src/Gateway.sol` file. + * 3. Execute the opcodes until you reach the first `GAS` opcode. + * 4. Execute the GAS opcode then copy the `Gas used in call` value to the constant below. + * + * Obs: To guarantee the overhead is constant regardless the input size, always use `calldata` instead of `memory` + * for external functions. + */ + uint256 internal constant BATCH_SELECTOR_OVERHEAD = 465; + + /** + * @dev How much gas is used until the first `gasleft()` instruction is executed. + * + * HOW TO UPDATE THIS VALUE: + * 1. Run `forge test --match-test=test_submitMessageMeter --fuzz-runs=1 --debug` + * 2. Move the cursor until you enter the `src/Gateway.sol` file. + * 3. Execute the opcodes until you reach the first `GAS` opcode. + * 4. Execute the GAS opcode then copy the `Gas used in call` value to the constant below. + * + * Obs: To guarantee the overhead is constant regardless the input size, always use `calldata` instead of `memory` + * for external functions. + */ + uint256 internal constant EXECUTION_SELECTOR_OVERHEAD = 474; + + /** + * @dev Base cost of the `IExecutor.execute` method. + */ + uint256 internal constant EXECUTION_BASE_COST = EXECUTION_SELECTOR_OVERHEAD + 46960; + + /** + * @dev Base cost of the `IGateway.submitMessage` method. + */ + uint256 internal constant SUBMIT_BASE_COST = 24138; + + /** + * @dev Extra gas cost that any account `Contract or EOA` must pay when calling `IGateway.submitMessage` method. + * This cost is necessary for initialize the account's `nonce` storage slot. + */ + uint256 internal constant FIRST_MESSAGE_EXTRA_COST = 17100; + + /** + * @dev Solidity's reserved location for the free memory pointer. + * Reference: https://docs.soliditylang.org/en/v0.8.28/internals/layout_in_memory.html + */ + uint256 internal constant ALLOCATED_MEMORY = 0x40; + + /** + * @dev Read the current allocated size (a.k.a free memory pointer). + */ + function readAllocatedMemory() internal pure returns (uint256 pointer) { + assembly ("memory-safe") { + pointer := mload(ALLOCATED_MEMORY) + } + } + + /** + * @dev Replace the current allocated size by the `newPointer`, and returns the old value stored. + * CAUTION: Only use this method if you know what you are doing. Make sure you don't overwrite any + * memory location that is still in use by the current call context. + */ + function unsafeReplaceAllocatedMemory(uint256 newPointer) internal pure returns (uint256 oldPointer) { + assembly ("memory-safe") { + oldPointer := mload(ALLOCATED_MEMORY) + mstore(ALLOCATED_MEMORY, newPointer) + } + } + + /** + * @dev Compute the gas cost of memory expansion. + * @param words number of words, where a word is 32 bytes + */ + function memoryExpansionGasCost(uint256 words) internal pure returns (uint256) { + unchecked { + return (words.saturatingMul(words) >> 9).saturatingAdd(words.saturatingMul(3)); + } + } + + /** + * @dev Compute the amount of gas used by the `GatewayProxy`. + * @param calldataLen The length of the calldata in bytes + * @param returnLen The length of the return data in bytes + */ + function proxyOverheadGasCost(uint256 calldataLen, uint256 returnLen) internal pure returns (uint256) { + unchecked { + // Convert the calldata and return data length to words + calldataLen = _toWord(calldataLen); + returnLen = _toWord(returnLen); + + // Base cost: OPCODES + COLD SLOAD + COLD DELEGATECALL + RETURNDATACOPY + // uint256 gasCost = 57 + 2100 + 2600; + uint256 gasCost = 31 + 2100 + 2600 + 32; + + // CALLDATACOPY + gasCost = gasCost.saturatingAdd(calldataLen * 3); + + // RETURNDATACOPY + gasCost = gasCost.saturatingAdd(returnLen * 3); + + // MEMORY EXPANSION (minimal 3 due mstore(0x40, 0x80)) + uint256 words = calldataLen.max(returnLen).max(3); + gasCost = gasCost.saturatingAdd(memoryExpansionGasCost(words)); + return gasCost; + } + } + + /** + * @dev Compute the gas cost of the `IGateway.submitMessage` method, without the `GatewayProxy` overhead. + * @param messageSize The size of the message in bytes. This is the `gmp.data.length`. + */ + function _submitMessageGasCost(uint16 messageSize) private pure returns (uint256 gasCost) { + unchecked { + gasCost = SUBMIT_BASE_COST; + + // `countNonZeros` gas cost + uint256 words = (messageSize + 31) >> 5; + gasCost += (words * 106) + (((words + 254) / 255) * 214); + + // CALLDATACOPY + gasCost += words * 3; + + // keccak256 (6 gas per word) + gasCost += words * 6; + + // emit GmpCreated() gas cost (8 gas per byte) + gasCost += words << 8; + + // Memory expansion cost + words += 17 - 1; + gasCost += ((words * words) >> 9) + (words * 3); + + return gasCost; + } + } + + /** + * @dev Compute the gas cost of the `IGateway.submitMessage` method including the `GatewayProxy` overhead. + * @param messageSize The size of the message in bytes. This is the `gmp.data.length`. + */ + function submitMessageGasCost(uint16 messageSize) internal pure returns (uint256 gasCost) { + unchecked { + // Compute the gas cost of the `IGateway.submitMessage` method. + gasCost = _submitMessageGasCost(messageSize); + + // Convert `gmp.data.length` to `abi.encodeCall(IGateway.submitMessage, (...)).length`. + uint256 calldataSize = ((messageSize + 31) & 0xffe0) + 164; + + // Compute the `GatewayProxy` gas overhead. + gasCost += proxyOverheadGasCost(uint16(calldataSize), 32); + + return gasCost; + } + } + + /** + * @dev Compute the minimal gas needed to execute the `IGateway.submitMessage` method. + * @param messageSize The size of the message in bytes. This is the `gmp.data.length`. + */ + function submitMessageGasNeeded(uint16 messageSize) internal pure returns (uint256 gasNeeded) { + unchecked { + // Compute the gas cost of the `IGateway.submitMessage` method. + gasNeeded = _submitMessageGasCost(messageSize); + // gasNeeded = gasNeeded.saturatingSub(2114); + // gasNeeded = _inverseOfAllButOne64th(); + uint256 calldataSize = ((messageSize + 31) & 0xffe0) + 164; + gasNeeded = gasNeeded.saturatingAdd(proxyOverheadGasCost(calldataSize, 32)); + gasNeeded = gasNeeded.saturatingSub(36); + } + } + + /** + * @dev Estimate the price in wei for send an GMP message. + * @param gasPrice The gas price in UFloat9x56 format. + * @param baseFee The base fee in wei. + * @param nonZeros The number of non-zero bytes in the gmp data. + * @param zeros The number of zero bytes in the gmp data. + * @param gasLimit The message gas limit. + */ + function estimateWeiCost(UFloat9x56 gasPrice, uint256 baseFee, uint16 nonZeros, uint16 zeros, uint256 gasLimit) + internal + pure + returns (uint256) + { + // Add execution cost + uint256 gasCost = estimateGas(nonZeros, zeros, gasLimit); + + // Calculate the gas cost: gasPrice * gasCost + baseFee + return UFloatMath.saturatingMul(gasPrice, gasCost).saturatingAdd(baseFee); + } + + /** + * @dev Estimate the gas cost of a GMP message. + * @param dataNonZeros The number of non-zero bytes in the gmp data. + * @param dataZeros The number of zero bytes in the gmp data. + * @param gasLimit The message gas limit. + */ + function estimateGas(uint16 dataNonZeros, uint16 dataZeros, uint256 gasLimit) internal pure returns (uint256) { + uint256 messageSize = uint256(dataNonZeros) + uint256(dataZeros); + unchecked { + // add execution cost + uint256 gasCost = + computeExecutionRefund(uint16(BranchlessMath.min(messageSize, type(uint16).max)), gasLimit); + // add base cost + gasCost = gasCost.saturatingAdd(21000); + + // calldata zero bytes + uint256 zeros = 31 + 30 + 12 + 30 + 31 + 30; + zeros = zeros.saturatingAdd((messageSize.saturatingAdd(31) & 0xffffe0) - uint256(dataZeros)); + gasCost = gasCost.saturatingAdd(zeros.saturatingMul(4)); + + // calldata non-zero bytes + uint256 nonZeros = uint256(dataNonZeros).saturatingAdd(4 + 96 + 1 + 32 + 2 + 20 + 2 + 32 + 32 + 1 + 2); + gasCost = gasCost.saturatingAdd(nonZeros.saturatingMul(16)); + + return gasCost; + } + } + + /** + * @dev Convert byte count to 256bit word count, rounded up. + */ + function _toWord(uint256 byteCount) private pure returns (uint256 words) { + assembly { + words := add(shr(5, byteCount), gt(and(byteCount, 0x1f), 0)) + } + } + + function _debugExecutionGasCost(uint256 messageSize, uint256 gasUsed) internal pure returns (uint256) { + unchecked { + // Selector overhead + // -- First GAS opcode + uint256 baseCost = EXECUTION_SELECTOR_OVERHEAD - 9; + uint256 memoryExpansion = 0x60; + // -- First GAS opcode + + // all opcodes until message.intoCallback() + baseCost += 449; + + // -- message.intoCallback() -- + baseCost += 438; + memoryExpansion = 0x80 + 0x01c4; + uint256 gas = 0; + // CALLDATACOPY 3 + (3 * words) + memory_expansion + baseCost += 3; + gas = _toWord(messageSize) * 3; + memoryExpansion += messageSize; + memoryExpansion = memoryExpansion.align32(); + + // opcodes until keccak256 + baseCost += 31; + + // keccak256 30 + 6 gas per word + baseCost += 30; + gas = gas.saturatingAdd(_toWord(messageSize) * 6); + // + baseCost += 424; + // -- message.intoCallback() -- + + baseCost += 34; + + // -- _verifySignature -- + baseCost += 7933; + // -- _verifySignature -- + + baseCost += 18; + + // _execute + baseCost += 22551; + baseCost += 2; // GAS + + baseCost += 97; + // ------ CALL ------ + + baseCost += 2600; + gas = gas.saturatingAdd(gasUsed); + memoryExpansion = (messageSize.align32() + 0x80 + 0x0120 + 164).align32(); + + // ------ CALL ------ + baseCost += 67; + baseCost += 100; // SLOAD + baseCost += 69; + baseCost += 100; // SSTORE + + // -- emit GmpExecuted -- + baseCost += 141; + memoryExpansion += 0x20; // MSTORE + baseCost += 24; + memoryExpansion += 0x20; // MSTORE + baseCost += 39; + baseCost += 2387; // LOG4 + baseCost += 26; + // -- emit GmpExecuted -- + // end _execute + + baseCost += 34; + + // GasUtils.txBaseCost() + { + baseCost += 64; // base cost + + // chunk start cost + baseCost += 66; + + // Selector + Signature + GmpMessage + uint256 words = messageSize.align32().saturatingAdd(388 + 31) >> 5; + words = (words * 106) + (((words.saturatingSub(255) + 254) / 255) * 214); + gas = gas.saturatingAdd(words); + + baseCost += 171; // End countNonZeros + baseCost += 70; // End txBaseCost + } + // end GasUtils.txBaseCost() + + baseCost += 482; + // ----- GAS ------- + + baseCost += 168; // GAS + baseCost += 6800; // REFUND CALL + baseCost += 184; // RETURN + + gas = gas.saturatingAdd(baseCost); + gas = gas.saturatingAdd(memoryExpansionGasCost(_toWord(memoryExpansion))); + return gas; + } + } + + function _executionGasCost(uint256 messageSize, uint256 gasUsed) internal pure returns (uint256) { + // Safety: The operations below can't overflow because the message size can't be greater than 2**16 + unchecked { + uint256 gas = _toWord(messageSize) * 3; + gas = gas.saturatingAdd(_toWord(messageSize) * 6); + gas = gas.saturatingAdd(gasUsed); + uint256 memoryExpansion = messageSize.align32() + 0x80 + 0x0120 + 164 + 0x40; + { + // Selector + Signature + GmpMessage + uint256 words = messageSize.align32().saturatingAdd(388 + 31) >> 5; + words = (words * 106) + (((words.saturatingSub(255) + 254) / 255) * 214); + gas = gas.saturatingAdd(words); + } + gas = gas.saturatingAdd(EXECUTION_BASE_COST); + gas = gas.saturatingAdd(memoryExpansionGasCost(_toWord(memoryExpansion))); + return gas; + } + } + + /** + * @dev Compute the inverse of `N - floor(N / 64)` defined by EIP-150, used to + * compute the gas needed for a transaction. + */ + function inverseOfAllButOne64th(uint256 x) internal pure returns (uint256 inverse) { + unchecked { + // inverse = (x * 64) / 63 + inverse = x.saturatingShl(6).saturatingDiv(63); + + // Subtract 1 if `inverse` is a multiple of 64 and greater than 0 + inverse -= BranchlessMath.toUint(inverse > 0 && (inverse % 64) == 0); + } + } + + /** + * @dev Compute the gas needed for the transaction, this is different from the gas used. + * `gas needed > gas used` because of EIP-150 + */ + function executionGasNeeded(uint256 messageSize, uint256 gasLimit) internal pure returns (uint256 gasNeeded) { + unchecked { + gasNeeded = _executionGasCost(messageSize, gasLimit); + gasNeeded = gasNeeded.saturatingAdd(2300 - 184); + gasNeeded = inverseOfAllButOne64th(gasNeeded); + messageSize = messageSize.align32().saturatingAdd(388); + gasNeeded = gasNeeded.saturatingAdd(proxyOverheadGasCost(messageSize, 64)); + // Remove the proxy final overhead, once the message requires (2300 - 184) extra gas. + gasNeeded = gasNeeded.saturatingSub(38); + } + } + + /** + * @dev Compute the gas that should be refunded to the executor for the execution. + * @param messageSize The size of the message. + * @param gasUsed The gas used by the gmp message. + */ + function computeExecutionRefund(uint16 messageSize, uint256 gasUsed) + internal + pure + returns (uint256 executionCost) + { + // Add the base `IExecutor.execute` gas cost. + executionCost = _executionGasCost(messageSize, gasUsed); + + // Add `GatewayProxy` gas overhead + unchecked { + // Safety: The operations below can't overflow because the message size can't be greater than 2**16 + uint256 calldataSize = ((uint256(messageSize) + 31) & 0xffffe0) + 388; // selector + Signature + GmpMessage + executionCost = executionCost.saturatingAdd(proxyOverheadGasCost(calldataSize, 64)); + } + } + + /** + * @dev Count the number of non-zero bytes in a byte sequence from memory. + * gas cost = 217 + (words * 112) + ((words - 1) * 193) + */ + function countNonZeros(bytes memory data) internal pure returns (uint256 nonZeros) { + assembly ("memory-safe") { + // Efficient algorithm for counting non-zero bytes in parallel + let size := mload(data) + + // Temporary set the length of the data to zero + mstore(data, 0) + + nonZeros := 0 + for { + // 32 byte aligned pointer, ex: if data.length is 54, then `ptr = data + 32` + let ptr := add(data, and(add(size, 31), 0xffffffe0)) + let end := xor(data, mul(xor(sub(ptr, 480), data), gt(sub(ptr, data), 480))) + } true { end := xor(data, mul(xor(sub(ptr, 480), data), gt(sub(ptr, data), 480))) } { + // Normalize and count non-zero bytes in parallel + let v := 0 + for {} gt(ptr, end) { ptr := sub(ptr, 32) } { + let r := mload(ptr) + r := or(r, shr(4, r)) + r := or(r, shr(2, r)) + r := or(r, shr(1, r)) + r := and(r, 0x0101010101010101010101010101010101010101010101010101010101010101) + v := add(v, r) + } + + // Sum bytes in parallel + v := add(v, shr(128, v)) + v := add(v, shr(64, v)) + v := add(v, shr(32, v)) + v := add(v, shr(16, v)) + v := and(v, 0xffff) + v := add(and(v, 0xff), shr(8, v)) + nonZeros := add(nonZeros, v) + + if eq(ptr, data) { break } + } + + // Restore the original length of the data + mstore(data, size) + } + } + + /** + * @dev Count the number of non-zero bytes from calldata. + * gas cost = 224 + (words * 106) + (((words + 254) / 255) * 214) + */ + function countNonZerosCalldata(bytes calldata data) internal pure returns (uint256 nonZeros) { + assembly ("memory-safe") { + nonZeros := 0 + for { + let ptr := data.offset + let end := add(ptr, data.length) + } lt(ptr, end) {} { + // calculate min(ptr + data.length, ptr + 8160) + let range := add(ptr, 8160) + range := xor(end, mul(xor(range, end), lt(range, end))) + + // Normalize and count non-zero bytes in parallel + let v := 0 + for {} lt(ptr, range) { ptr := add(ptr, 32) } { + let r := calldataload(ptr) + r := or(r, shr(4, r)) + r := or(r, shr(2, r)) + r := or(r, shr(1, r)) + r := and(r, 0x0101010101010101010101010101010101010101010101010101010101010101) + v := add(v, r) + } + + // Sum bytes in parallel + { + let l := and(v, 0x00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff) + v := shr(8, xor(v, l)) + v := add(v, l) + } + v := add(v, shr(128, v)) + v := add(v, shr(64, v)) + v := add(v, shr(32, v)) + v := add(v, shr(16, v)) + v := and(v, 0xffff) + nonZeros := add(nonZeros, v) + } + } + } + + /** + * @dev Compute the transaction base cost. + */ + function txBaseCost() internal pure returns (uint256) { + unchecked { + uint256 nonZeros = countNonZerosCalldata(msg.data); + uint256 zeros = msg.data.length - nonZeros; + return 21000 + (nonZeros * 16) + (zeros * 4); + } + } +} diff --git a/analog-gmp/src/utils/Hashing.sol b/analog-gmp/src/utils/Hashing.sol new file mode 100644 index 000000000..408757104 --- /dev/null +++ b/analog-gmp/src/utils/Hashing.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (src/utils/Hashing.sol) + +pragma solidity >=0.8.20; + +library Hashing { + /** + * @dev Solidity's reserved location for the free memory pointer. + * Reference: https://docs.soliditylang.org/en/v0.8.28/internals/layout_in_memory.html + */ + uint256 internal constant ALLOCATED_MEMORY = 0x40; + + /** + * @dev Solidity's reserved location for the scratch memory. + * Reference: https://docs.soliditylang.org/en/v0.8.28/internals/layout_in_memory.html + */ + uint256 internal constant SCRATCH_MEMORY = 0x60; + + /** + * @dev Hashes a single 256-bit integer without memory allocation, uses the memory between 0x00~0x20. + */ + function hash(uint256 a) internal pure returns (bytes32 h) { + assembly ("memory-safe") { + mstore(0x00, a) + h := keccak256(0x00, 0x20) + } + } + + /** + * @dev Hashes two 256-bit words without memory allocation, uses the memory between 0x00~0x40. + */ + function hash(uint256 a, uint256 b) internal pure returns (bytes32 h) { + assembly ("memory-safe") { + mstore(0x00, a) + mstore(0x20, b) + h := keccak256(0x00, 0x40) + } + } + + /** + * @dev Hashes three 256-bit words without memory allocation, uses the memory between 0x00~0x60. + * + * The reserverd memory region `0x40~0x60` is restored to its previous state after execution. + * See https://docs.soliditylang.org/en/v0.8.28/internals/layout_in_memory.html for more details. + */ + function hash(uint256 a, uint256 b, uint256 c) internal pure returns (bytes32 h) { + assembly ("memory-safe") { + mstore(0x00, a) + mstore(0x20, b) + + // Backup the free memory pointer + let freeMemBackup := mload(ALLOCATED_MEMORY) + + mstore(ALLOCATED_MEMORY, c) + h := keccak256(0x00, 0x60) + + // Restore the free memory pointer + mstore(ALLOCATED_MEMORY, freeMemBackup) + } + } + + /** + * @dev Hashes four 256-bit words without memory allocation, uses the memory between 0x00~0x80. + * + * The reserverd memory regions `0x40` and `0x60` are saved and restored after the hash is computed. + * See https://docs.soliditylang.org/en/v0.8.28/internals/layout_in_memory.html for more details. + */ + function hash(bytes32 a, bytes32 b, bytes32 c, bytes32 d) internal pure returns (bytes32) { + return hash(uint256(a), uint256(b), uint256(c), uint256(d)); + } + + /** + * @dev Hashes four 256-bit words without memory allocation, uses the memory between 0x00~0x80. + * + * The reserverd memory region `0x40~0x80` is restored to its previous state after execution. + * See https://docs.soliditylang.org/en/v0.8.28/internals/layout_in_memory.html for more details. + */ + function hash(uint256 a, uint256 b, uint256 c, uint256 d) internal pure returns (bytes32 h) { + assembly ("memory-safe") { + mstore(0x00, a) + mstore(0x20, b) + + // Backup the free memory pointer + let freeMemBackup := mload(ALLOCATED_MEMORY) + mstore(ALLOCATED_MEMORY, c) + { + // Backup the scratch space 0x60 + let backup := mload(0x60) + + // Compute the hash + mstore(SCRATCH_MEMORY, d) + h := keccak256(0x00, 0x80) + + // Restore the scratch space 0x60 + mstore(SCRATCH_MEMORY, backup) + } + // Restore the free memory pointer + mstore(ALLOCATED_MEMORY, freeMemBackup) + } + } +} diff --git a/analog-gmp/src/utils/Pointer.sol b/analog-gmp/src/utils/Pointer.sol new file mode 100644 index 000000000..ef92f9a3f --- /dev/null +++ b/analog-gmp/src/utils/Pointer.sol @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (src/utils/StoragePtr.sol) +pragma solidity ^0.8.20; + +/** + * @dev Represents a raw pointer to a value in storage. + */ +type StoragePtr is uint256; + +/** + * @dev Library for reading and writing primitive types to specific storage slots. + * + * Storage slots are often used to avoid storage conflict when dealing with upgradeable contracts. + * This library helps with reading and writing to such slots without the need for inline assembly. + * + * The functions in this library return Slot structs that contain a `value` member that can be used to read or write. + * + * Example usage to set ERC-1967 implementation slot: + * ```solidity + * contract ERC1967 { + * // Define the slot. Alternatively, use the SlotDerivation library to derive the slot. + * bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + * + * function _getImplementation() internal view returns (address) { + * return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; + * } + * + * function _setImplementation(address newImplementation) internal { + * require(newImplementation.code.length > 0); + * StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; + * } + * } + * ``` + * + * TIP: Consider using this library along with {SlotDerivation}. + */ +library Pointer { + struct AddressSlot { + address value; + } + + struct BooleanSlot { + bool value; + } + + struct Bytes32Slot { + bytes32 value; + } + + struct Uint256Slot { + uint256 value; + } + + struct Int256Slot { + int256 value; + } + + struct StringSlot { + string value; + } + + struct BytesSlot { + bytes value; + } + + /** + * @dev Converts `uint256[] storage` to `StoragePtr`. + */ + function asPtr(uint256[] storage store) internal pure returns (StoragePtr ptr) { + assembly ("memory-safe") { + ptr := store.slot + } + } + + /** + * @dev Converts `bytes32[] storage` to `StoragePtr`. + */ + function asPtr(bytes32[] storage store) internal pure returns (StoragePtr ptr) { + assembly ("memory-safe") { + ptr := store.slot + } + } + + /** + * @dev Converts `bytes storage` to `StoragePtr`. + */ + function asPtr(bytes storage store) internal pure returns (StoragePtr ptr) { + assembly ("memory-safe") { + ptr := store.slot + } + } + + /** + * @dev Wraps a value in a `StoragePtr`. + */ + function asPtr(uint256 value) internal pure returns (StoragePtr) { + return StoragePtr.wrap(value); + } + + /** + * @dev Unwraps a `StoragePtr` to a value. + */ + function asPtr(bytes32 value) internal pure returns (StoragePtr) { + return StoragePtr.wrap(uint256(value)); + } + + /** + * @dev Convert a `StoragePtr` to `uint256`. + */ + function asUint(StoragePtr ptr) internal pure returns (uint256) { + return StoragePtr.unwrap(ptr); + } + + /** + * @dev Convert a `StoragePtr` to `int256`. + */ + function asInt(StoragePtr ptr) internal pure returns (int256) { + return int256(StoragePtr.unwrap(ptr)); + } + + /** + * @dev Convert a `StoragePtr` to `bytes32`. + */ + function asBytes32(StoragePtr ptr) internal pure returns (bytes32) { + return bytes32(StoragePtr.unwrap(ptr)); + } + + /** + * @dev Whether the `StoragePtr` is zero or not. + */ + function isNull(StoragePtr ptr) internal pure returns (bool r) { + assembly ("memory-safe") { + r := iszero(ptr) + } + } + + /** + * @dev Returns an `AddressSlot` with member `value` located at `slot`. + */ + function getAddressSlot(StoragePtr slot) internal pure returns (AddressSlot storage r) { + assembly ("memory-safe") { + r.slot := slot + } + } + + /** + * @dev Converts a `AddressSlot` into an `StoragePtr`. + */ + function asPtr(AddressSlot storage store) internal pure returns (StoragePtr ptr) { + assembly ("memory-safe") { + ptr := store.slot + } + } + + /** + * @dev Returns a `BooleanSlot` with member `value` located at `slot`. + */ + function getBooleanSlot(StoragePtr slot) internal pure returns (BooleanSlot storage r) { + assembly ("memory-safe") { + r.slot := slot + } + } + + /** + * @dev Converts a `BooleanSlot` into an `StoragePtr`. + */ + function asPtr(BooleanSlot storage store) internal pure returns (StoragePtr ptr) { + assembly ("memory-safe") { + ptr := store.slot + } + } + + /** + * @dev Returns a `Bytes32Slot` with member `value` located at `slot`. + */ + function getBytes32Slot(StoragePtr slot) internal pure returns (Bytes32Slot storage r) { + assembly ("memory-safe") { + r.slot := slot + } + } + + /** + * @dev Converts a `Bytes32Slot` into an `StoragePtr`. + */ + function asPtr(Bytes32Slot storage store) internal pure returns (StoragePtr ptr) { + assembly ("memory-safe") { + ptr := store.slot + } + } + + /** + * @dev Returns a `Uint256Slot` with member `value` located at `slot`. + */ + function getUint256Slot(StoragePtr slot) internal pure returns (Uint256Slot storage r) { + assembly ("memory-safe") { + r.slot := slot + } + } + + /** + * @dev Converts a `Uint256Slot` into an `StoragePtr`. + */ + function asPtr(Uint256Slot storage store) internal pure returns (StoragePtr ptr) { + assembly ("memory-safe") { + ptr := store.slot + } + } + + /** + * @dev Returns a `Int256Slot` with member `value` located at `slot`. + */ + function getInt256Slot(StoragePtr slot) internal pure returns (Int256Slot storage r) { + assembly ("memory-safe") { + r.slot := slot + } + } + + /** + * @dev Converts a `Int256Slot` into an `StoragePtr`. + */ + function asPtr(Int256Slot storage store) internal pure returns (StoragePtr ptr) { + assembly ("memory-safe") { + ptr := store.slot + } + } + + /** + * @dev Returns an `StringSlot` representation of the string storage pointer `store`. + */ + function getStringSlot(StoragePtr slot) internal pure returns (StringSlot storage r) { + assembly ("memory-safe") { + r.slot := slot + } + } + + /** + * @dev Returns a `BytesSlot` with member `value` located at `slot`. + */ + function getBytesSlot(StoragePtr slot) internal pure returns (BytesSlot storage r) { + assembly ("memory-safe") { + r.slot := slot + } + } + + /** + * @dev Converts a `BytesSlot` into an `StoragePtr`. + */ + function asPtr(BytesSlot storage store) internal pure returns (StoragePtr ptr) { + assembly ("memory-safe") { + ptr := store.slot + } + } +} diff --git a/analog-gmp/src/utils/Schnorr.sol b/analog-gmp/src/utils/Schnorr.sol new file mode 100644 index 000000000..b72a02c3c --- /dev/null +++ b/analog-gmp/src/utils/Schnorr.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (src/utils/Schnorr.sol) + +pragma solidity >=0.8.20; + +library Schnorr { + // secp256k1 group order + uint256 internal constant Q = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141; + + /** + * Verify Schnorr signature (secp256k1) without memory allocation, Solidity's `ecrecover` + * allocates memory, which complicates the gas estimation. + * + * @param parity public key y-coord parity (27 or 28) + * @param px public key x-coord + * @param message 32-byte message hash + * @param e schnorr signature challenge + * @param s schnorr signature + */ + function verify(uint8 parity, uint256 px, uint256 message, uint256 e, uint256 s) + internal + view + returns (bool valid) + { + // the ecrecover precompile implementation checks that the `r` and `s` + // inputs are non-zero (in this case, `px` and `ep`), thus we don't need to + // check if they're zero. + assembly ("memory-safe") { + // backup the memory values for restore later + let b0 := mload(0x40) + let b1 := mload(0x60) + { + // sp = Q - mulmod(s, px, Q) + let sp := sub(Q, mulmod(s, px, Q)) + + // ep = Q - mulmod(e, px, Q) + let ep := sub(Q, mulmod(e, px, Q)) + + // R = ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) + mstore(0x00, sp) + mstore(0x20, parity) + mstore(0x40, px) + mstore(0x60, ep) + pop(staticcall(gas(), 1, 0x00, 0x80, 0x00, 0x20)) + let R := mload(0x00) + + // Compute keccak256(abi.encodePacked(R, parity, px, message)) + mstore(0x20, shl(248, parity)) + mstore(0x21, px) + mstore(0x41, message) + + // sp != 0 && R != 0 + valid := and(gt(sp, 0), gt(R, 0)) + // R == keccak256(abi.encodePacked(R, parity, px, message) + valid := and(valid, eq(e, keccak256(0x0c, 85))) + } + // restore the original memory values + mstore(0x40, b0) + mstore(0x60, b1) + } + } +} diff --git a/analog-gmp/test/Batching.t.sol b/analog-gmp/test/Batching.t.sol new file mode 100644 index 000000000..573103f3a --- /dev/null +++ b/analog-gmp/test/Batching.t.sol @@ -0,0 +1,504 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (test/Batch.t.sol) + +pragma solidity >=0.8.0; + +import {Test, console, Vm} from "forge-std/Test.sol"; +import {VmSafe} from "forge-std/Vm.sol"; +import {FactoryUtils} from "@universal-factory/FactoryUtils.sol"; +import {IUniversalFactory} from "@universal-factory/IUniversalFactory.sol"; +import {TestUtils, SigningKey, SigningUtils} from "./TestUtils.sol"; +import {GasSpender} from "./utils/GasSpender.sol"; +import {BaseTest} from "./utils/BaseTest.sol"; +import {Gateway, GatewayEIP712} from "../src/Gateway.sol"; +import {GatewayProxy} from "../src/GatewayProxy.sol"; +import {Hashing} from "../src/utils/Hashing.sol"; +import {GasUtils} from "../src/utils/GasUtils.sol"; +import {BranchlessMath} from "../src/utils/BranchlessMath.sol"; +import {UFloat9x56, UFloatMath} from "../src/utils/Float9x56.sol"; +import {IGateway} from "../src/interfaces/IGateway.sol"; +import {IGmpReceiver} from "../src/interfaces/IGmpReceiver.sol"; +import {IExecutor} from "../src/interfaces/IExecutor.sol"; +import { + InboundMessage, + GatewayOp, + Command, + GmpMessage, + GmpCallback, + UpdateKeysMessage, + Signature, + TssKey, + Network, + GmpStatus, + PrimitiveUtils, + GmpSender, + GMP_VERSION +} from "../src/Primitives.sol"; + +contract Batching is BaseTest { + using PrimitiveUtils for UpdateKeysMessage; + using PrimitiveUtils for GmpMessage; + using PrimitiveUtils for GmpSender; + using PrimitiveUtils for address; + using BranchlessMath for uint256; + using SigningUtils for SigningKey; + using FactoryUtils for IUniversalFactory; + + // Chronicle TSS Secret + uint256 private constant SECRET = 0x42; + uint256 private constant SIGNING_NONCE = 0x69; + + // Netowrk ids + uint16 private constant SRC_NETWORK_ID = 1234; + uint16 internal constant DEST_NETWORK_ID = 1337; + + address private constant RECEIVER_CONTRACT = 0x9888d4d78827bE0C2aDeb578019D35b5a36E80a4; + address private constant RECEIVER_CONTRACT_02 = address( + uint160( + uint256( + keccak256( + abi.encodePacked( + uint8(0xff), address(FACTORY), uint256(1), keccak256(type(GasSpender).creationCode) + ) + ) + ) + ) + ); + + // Domain Separators + bytes32 private constant DOMAIN_TYPED_HASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + + bytes32 private constant SRC_DOMAIN_SEPARATOR = keccak256( + abi.encode( + DOMAIN_TYPED_HASH, + keccak256("Analog Gateway Contract"), + keccak256("0.1.0"), + uint256(SRC_NETWORK_ID), + GATEWAY_PROXY + ) + ); + bytes32 private DST_DOMAIN_SEPARATOR = keccak256( + abi.encode( + DOMAIN_TYPED_HASH, + keccak256("Analog Gateway Contract"), + keccak256("0.1.0"), + uint256(DEST_NETWORK_ID), + GATEWAY_PROXY + ) + ); + + address private constant DEPLOYER = 0xbe862AD9AbFe6f22BCb087716c7D89a26051f74C; + uint256 private constant ADMIN_SECRET = 0x955acb49dbb669143455ffbf98e30ae5b2d95343c8b46ce10bf1975d722e8001; + address private constant ADMIN = 0xBf3C099fAAC29F7AF0d883374A790f9b3B06c93A; + + // Gateway Proxy + bytes private constant PROXY_BYTECODE = abi.encodePacked(type(GatewayProxy).creationCode, abi.encode(ADMIN)); + bytes32 private constant PROXY_BYTECODE_HASH = keccak256(PROXY_BYTECODE); + address payable private constant GATEWAY_PROXY = payable( + address( + uint160( + uint256( + keccak256( + abi.encodePacked( + uint8(0xff), + address(FACTORY), + uint256(0), + keccak256(abi.encodePacked(type(GatewayProxy).creationCode, abi.encode(ADMIN))) + ) + ) + ) + ) + ) + ); + + // Gateway Implementation + bytes private constant IMPLEMENTATION_BYTECODE = + abi.encodePacked(type(Gateway).creationCode, abi.encode(DEST_NETWORK_ID, GATEWAY_PROXY)); + bytes32 private constant IMPLEMENTATION_BYTECODE_HASH = keccak256(IMPLEMENTATION_BYTECODE); + address private constant GATEWAY_IMPLEMENTATION = address( + uint160( + uint256( + keccak256(abi.encodePacked(uint8(0xff), address(FACTORY), uint256(0), IMPLEMENTATION_BYTECODE_HASH)) + ) + ) + ); + + bytes32 private constant SIGNING_HASH = keccak256(abi.encode(GATEWAY_PROXY, GATEWAY_IMPLEMENTATION)); + + constructor() { + // Create the Admin account + SigningKey memory signer = TestUtils.createSigner(ADMIN_SECRET); + console.logBytes(abi.encodePacked(signer.pubkey.px, signer.pubkey.py)); + assertEq(ADMIN, signer.addr(), "admin address missmatch"); + + // Deploy `GasSpender` contract + vm.prank(ADMIN, ADMIN); + assertEq( + RECEIVER_CONTRACT, + FACTORY.create3(bytes32(0), type(GasSpender).creationCode), + "GasSpender address missmatch" + ); + + assertEq( + RECEIVER_CONTRACT_02, + FACTORY.create2(bytes32(uint256(1)), type(GasSpender).creationCode), + "GasSpender address missmatch" + ); + + // Deposit funds to the ADMIN account + vm.deal(ADMIN, 100 ether); + } + + function setUp() external { + console.log("-- STEP 01"); + { + // Encode the `IGmpReceiver.onGmpReceived` call + uint256 gasToWaste = 1000; + bytes memory encodedCall = abi.encodeCall( + IGmpReceiver.onGmpReceived, + ( + 0x0000000000000000000000000000000000000000000000000000000000000000, + 1, + 0x0000000000000000000000000000000000000000000000000000000000000000, + abi.encode(gasToWaste) + ) + ); + uint256 gasLimit = TestUtils.calculateBaseCost(encodedCall) + gasToWaste; + TestUtils.executeCall(ADMIN, address(RECEIVER_CONTRACT), gasLimit, 0, encodedCall); + } + console.log("-- STEP 02"); + + SigningKey memory signer = TestUtils.createSigner(SECRET); + vm.deal(DEPLOYER, 100 ether); + vm.startPrank(DEPLOYER, DEPLOYER); + assertEq( + GATEWAY_PROXY, + FACTORY.computeCreate2Address(bytes32(0), PROXY_BYTECODE_HASH), + "GatewayProxy address missmatch" + ); + // bytes memory bytecode = abi.encodePacked(type(Gateway).creationCode, abi.encode(DEST_NETWORK_ID, GATEWAY_PROXY)); + console.log("implementation:", GATEWAY_IMPLEMENTATION); + console.log(" proxy:", GATEWAY_PROXY); + assertEq( + GATEWAY_IMPLEMENTATION, + FACTORY.create2(bytes32(0), IMPLEMENTATION_BYTECODE), + "Gateway implementation address missmatch" + ); + + // 2 - Deploy the Proxy Contract + console.log("-- Deploying GatewayProxy"); + TssKey[] memory keys = new TssKey[](1); + // keys[0] = TssKey({yParity: signer.yParity() == 28 ? 1 : 0, xCoord: signer.xCoord()}); // Shard key + keys[0] = TssKey({yParity: signer.yParity() == 28 ? 3 : 2, xCoord: signer.xCoord()}); // Shard key + Network[] memory networks = new Network[](2); + networks[0].id = SRC_NETWORK_ID; // sepolia network id + networks[0].gateway = GATEWAY_PROXY; // sepolia proxy address + networks[1].id = DEST_NETWORK_ID; // shibuya network id + networks[1].gateway = GATEWAY_PROXY; // shibuya proxy address + bytes memory initializer = abi.encodeCall(Gateway.initialize, (ADMIN, keys, networks)); + // console.logBytes(initializer); + + // vm.startStateDiffRecording(); + { + VmSafe.Wallet memory adminSigner = vm.createWallet(ADMIN_SECRET); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(adminSigner, SIGNING_HASH); + bytes memory authorization = abi.encode(v, r, s, GATEWAY_IMPLEMENTATION); + assertEq( + GATEWAY_PROXY, + FACTORY.create2(bytes32(0), PROXY_BYTECODE, authorization, initializer), + "GatewayProxy address missmatch" + ); + } + + // Deposit funds to the gateway contract + Gateway(GATEWAY_PROXY).deposit{value: 10 ether}(); + } + + function sign(SigningKey memory signer, GmpMessage memory gmp) private pure returns (Signature memory) { + GmpCallback memory callback = gmp.memToCallback(); + (uint256 e, uint256 s) = signer.signPrehashed(callback.eip712hash, SIGNING_NONCE); + return Signature({xCoord: signer.xCoord(), e: e, s: s}); + } + + function computeGmpMessageID(GmpMessage calldata message) external pure returns (bytes32) { + console.logBytes(msg.data); + return message.intoCallback().eip712hash; + } + + function sign(SigningKey memory signer, InboundMessage memory message) + private + view + returns (Signature memory sig) + { + signAt(signer, message, sig); + } + + function computeInboundMessageSigningHash(InboundMessage calldata message) external pure returns (bytes32) { + bytes32 rootHash = bytes32(0); + + GatewayOp[] calldata ops = message.ops; + for (uint256 i = 0; i < ops.length; i++) { + GatewayOp calldata op = ops[i]; + bytes calldata params = op.params; + + bytes32 operationHash; + if (op.command == Command.GMP) { + GmpMessage calldata gmp; + assembly { + gmp := add(params.offset, 0x20) + } + operationHash = gmp.intoCallback().eip712hash; + } else { + TssKey calldata tssKey; + assembly { + tssKey := params.offset + } + operationHash = Hashing.hash(tssKey.yParity, tssKey.xCoord); + } + rootHash = Hashing.hash(uint256(rootHash), uint256(op.command), uint256(operationHash)); + } + rootHash = Hashing.hash(message.version, message.batchID, uint256(rootHash)); + return keccak256( + abi.encodePacked( + "Analog GMP v2", DEST_NETWORK_ID, bytes32(uint256(uint160(address(GATEWAY_PROXY)))), rootHash + ) + ); + } + + function signAt(SigningKey memory signer, InboundMessage memory message, Signature memory sig) private view { + bytes32 signingHash = this.computeInboundMessageSigningHash(message); + (uint256 e, uint256 s) = signer.signPrehashed(signingHash, SIGNING_NONCE); + sig.xCoord = signer.xCoord(); + sig.e = e; + sig.s = s; + } + + function test_gmp_debug() external { + vm.txGasPrice(1); + SigningKey memory signer = TestUtils.createSigner(SECRET); + + // Build and sign GMP message + // bytes memory data = new bytes(3070 + 32); + GmpMessage memory gmp = GmpMessage({ + source: ADMIN.toSender(false), + srcNetwork: SRC_NETWORK_ID, + dest: RECEIVER_CONTRACT, + destNetwork: DEST_NETWORK_ID, + gasLimit: 7845, + nonce: 0, + data: new bytes(800) + }); + { + bytes memory data = gmp.data; + assembly { + mstore(add(data, 0x20), 7845) + } + } + + Signature memory sig = sign(signer, gmp); + + vm.deal(DEPLOYER, 100 ether); + vm.startPrank(DEPLOYER, DEPLOYER); + uint256 gasNeeded = GasUtils.executionGasNeeded(uint16(gmp.data.length), gmp.gasLimit); + uint256 executionCost = GasUtils._executionGasCost(uint16(gmp.data.length), gmp.gasLimit); + emit log_named_uint(" gas needed", gasNeeded); + + require(GATEWAY_PROXY.code.length > 0, "gateway proxy not found"); + Gateway(GATEWAY_PROXY).execute{gas: gasNeeded}(sig, gmp); + + emit log_named_uint(" total cost", GasUtils.computeExecutionRefund(uint16(gmp.data.length), gmp.gasLimit)); + emit log_named_uint("execution cost", executionCost); + } + + function batchExecute(Signature calldata, InboundMessage calldata message) external pure { + console.log("batchExecute:"); + console.logBytes(msg.data); + for (uint256 i = 0; i < message.ops.length; i++) { + console.log("\nop:", i); + GatewayOp calldata op = message.ops[i]; + bytes calldata data = op.params; + console.logBytes(data); + + uint256 offset; + GmpMessage calldata gmp; + assembly { + offset := data.offset + gmp := add(offset, 0x20) + } + data = gmp.data; + + bytes32 value; + assembly { + offset := data.offset + value := calldataload(value) + } + + console.log(offset, vm.toString(value)); + console.logBytes(data); + } + } + + function test_buildBatch() external view { + GatewayOp[] memory ops = new GatewayOp[](2); + ops[0] = GatewayOp({ + command: Command.GMP, + params: abi.encode( + GmpMessage({ + source: GmpSender.wrap(0x7777777777777777777777777777777777777777777777777777777777777777), + srcNetwork: 0x8888, + dest: 0x9999999999999999999999999999999999999999, + destNetwork: 0xAAAA, + gasLimit: 0xBBBBBBBBBBBBBBBB, + nonce: 0xCCCCCCCCCCCCCCCC, + data: abi.encode(uint256(0xDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD)) + }) + ) + }); + // data: hex"DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD" + + ops[1] = GatewayOp({ + command: Command.GMP, + params: abi.encode( + GmpMessage({ + source: GmpSender.wrap(0x7070707070707070707070707070707070707070707070707070707070707070), + srcNetwork: 0x8080, + dest: 0x9090909090909090909090909090909090909090, + destNetwork: 0xA0A0, + gasLimit: 0xB0B0B0B0B0B0B0B0, + nonce: 0xC0C0C0C0C0C0C0C0, + data: abi.encode(uint256(0xD0D0D0D0D0D0D0D0D0D0D0D0D0D0D0)) + }) + ) + }); + // data: hex"D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0" + + bytes memory b = abi.encodeCall( + Gateway.batchExecute, + ( + Signature({ + xCoord: 0x1111111111111111111111111111111111111111111111111111111111111111, + e: 0x2222222222222222222222222222222222222222222222222222222222222222, + s: 0x3333333333333333333333333333333333333333333333333333333333333333 + }), + InboundMessage({version: 0x44, batchID: 0x5555555555555555, ops: ops}) + ) + ); + console.logBytes(b); + (bool success,) = address(this).staticcall(b); + require(success, "call failed"); + // assertEq(b.length, 1234); + } + + function test_buildBatch2() external view { + SigningKey memory signer = TestUtils.createSigner(SECRET); + + // Build and sign GMP + uint64 gasLimit = 7845; + GatewayOp[] memory ops = new GatewayOp[](1); + ops[0] = GatewayOp({ + command: Command.GMP, + params: abi.encode( + GmpMessage({ + source: DEPLOYER.toSender(false), + srcNetwork: SRC_NETWORK_ID, + dest: RECEIVER_CONTRACT, + destNetwork: DEST_NETWORK_ID, + gasLimit: gasLimit, + nonce: 0, + data: abi.encode(gasLimit) + }) + ) + }); + Signature memory sig = Signature({xCoord: 0, e: 0, s: 0}); + InboundMessage memory inbound = + InboundMessage({version: 1, batchID: uint64(uint256(keccak256("some batch"))), ops: ops}); + + // console.log("will sign..."); + signAt(signer, inbound, sig); + bytes memory b = abi.encodeCall(Gateway.batchExecute, (sig, inbound)); + console.logBytes(b); + (bool success,) = address(this).staticcall(b); + require(success, "call failed"); + // assertEq(b.length, 1234); + } + + function test_batch_debug() external { + vm.txGasPrice(1); + SigningKey memory signer = TestUtils.createSigner(SECRET); + vm.deal(signer.addr(), 100 ether); + uint64 gasLimit = 7845; + + ///////////////////// + // Build the batch // + ///////////////////// + GatewayOp[] memory ops = new GatewayOp[](2); + ops[0] = GatewayOp({ + command: Command.GMP, + params: abi.encode( + GmpMessage({ + source: DEPLOYER.toSender(false), + srcNetwork: SRC_NETWORK_ID, + dest: RECEIVER_CONTRACT, + destNetwork: DEST_NETWORK_ID, + gasLimit: gasLimit, + nonce: 0, + data: abi.encode(gasLimit) + }) + ) + }); + ops[1] = GatewayOp({ + command: Command.GMP, + params: abi.encode( + GmpMessage({ + source: DEPLOYER.toSender(false), + srcNetwork: SRC_NETWORK_ID, + dest: RECEIVER_CONTRACT_02, + destNetwork: DEST_NETWORK_ID, + gasLimit: gasLimit, + nonce: 0, + data: abi.encode(gasLimit) + }) + ) + }); + InboundMessage memory inbound = + InboundMessage({version: 1, batchID: uint64(uint256(keccak256("some batch"))), ops: ops}); + + //////////////////// + // Sign the batch // + //////////////////// + Signature memory sig = sign(signer, inbound); + bytes memory encodedCall = abi.encodeCall(Gateway.batchExecute, (sig, inbound)); + + console.log("encoded call:"); + console.logBytes(encodedCall); + + // vm.deal(DEPLOYER, 100 ether); + // vm.startPrank(DEPLOYER, DEPLOYER); + uint256 gasNeeded = GasUtils.executionGasNeeded(uint16(32), gasLimit); + uint256 executionCost = GasUtils._executionGasCost(uint16(32), gasLimit); + emit log_named_uint(" gas needed", gasNeeded); + emit log_named_uint("execution cost", executionCost); + require(GATEWAY_PROXY.code.length > 0, "gateway proxy not found"); + + uint256 baseCost; + bool success; + bytes memory result; + // (executionCost, baseCost, success, result) = address(GATEWAY_PROXY).call(encodedCall); + console.log("will execute.."); + (executionCost, baseCost, success, result) = + TestUtils.tryExecuteCall(signer.addr(), GATEWAY_PROXY, 500_000, 0, encodedCall); + emit log_named_uint("execution cost", executionCost); + emit log_named_uint(" base cost", baseCost); + if (!success) { + console.log("reverted:"); + console.logBytes(result); + assembly { + revert(add(result, 0x20), mload(result)) + } + } + + emit log_named_uint(" total cost", GasUtils.computeExecutionRefund(uint16(32), gasLimit)); + emit log_named_uint("execution cost", executionCost); + } +} diff --git a/analog-gmp/test/EnumerableSet.t.sol b/analog-gmp/test/EnumerableSet.t.sol new file mode 100644 index 000000000..ebdeb7d28 --- /dev/null +++ b/analog-gmp/test/EnumerableSet.t.sol @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (test/EnumerableSet.t.sol) + +pragma solidity >=0.8.0; + +import {Test, console} from "forge-std/Test.sol"; +import {TestUtils} from "./TestUtils.sol"; +import {EnumerableSet, StoragePtr} from "../src/utils/EnumerableSet.sol"; +import {StoragePtr, Pointer} from "../src/utils/Pointer.sol"; +import {BranchlessMath} from "../src/utils/BranchlessMath.sol"; + +contract EnumerableSetTest is Test { + using BranchlessMath for uint256; + using EnumerableSet for EnumerableSet.Map; + using Pointer for StoragePtr; + using Pointer for uint256; + using Pointer for Pointer.Uint256Slot; + + uint256 private constant ITERATIONS = 10; + + EnumerableSet.Map private map; + + struct MyStruct { + uint256 a; + uint256 b; + uint256 c; + } + + function _add(uint256 key, uint256 value, bool success) private returns (MyStruct storage r) { + bytes32 ptr = map.add(bytes32(key)).asBytes32(); + if (success) { + assertNotEq(ptr, bytes32(0), "map.add failed"); + assembly { + r.slot := ptr + } + r.a = value; + r.b = value + 1; + r.c = value + 2; + } else { + assertEq(ptr, bytes32(0), "expect map.add to fail"); + assembly { + r.slot := 0 + } + } + } + + function _at(uint256 index, bool success) private view returns (MyStruct storage r) { + (, StoragePtr raw) = map.at(index); + bytes32 ptr = raw.asBytes32(); + if (success) { + assertNotEq(ptr, bytes32(0), "map.at failed"); + assembly { + r.slot := ptr + } + } else { + assertEq(ptr, bytes32(0), "expect map.at to fail"); + assembly { + r.slot := 0 + } + } + } + + function _get(uint256 key, bool success) private view returns (MyStruct storage r) { + bytes32 ptr = map.get(bytes32(key)).asBytes32(); + if (success) { + assertNotEq(ptr, bytes32(0), "map.at failed"); + assembly { + r.slot := ptr + } + } else { + assertEq(ptr, bytes32(0), "expect map.at to fail"); + assembly { + r.slot := 0 + } + } + } + + function _removeByKey(uint256 key, bool success) private returns (MyStruct storage r) { + bytes32 ptr = map.remove(bytes32(key)).asBytes32(); + if (success) { + assertNotEq(ptr, bytes32(0), "map.at failed"); + assembly { + r.slot := ptr + } + } else { + assertEq(ptr, bytes32(0), "expect map.at to fail"); + assembly { + r.slot := 0 + } + } + } + + /** + * Test if `Map.add` and `Map.at` work as expected. + */ + function test_add() external { + assertEq(map.length(), 0, "Map should be empty"); + + MyStruct storage s; + for (uint256 i = 0; i < ITERATIONS; i++) { + s = _add(0x1234 + i, i + 1, true); + assertEq(map.length(), i + 1); + for (uint256 j = 0; j < ITERATIONS; j++) { + s = _at(j, j <= i); + if (j <= i) { + assertEq(s.a, j + 1, "MyStruct.a mismatch"); + assertEq(s.b, j + 2, "MyStruct.b mismatch"); + assertEq(s.c, j + 3, "MyStruct.c mismatch"); + } + } + } + } + + /** + * Test if `Map.add` and `Map.at` work as expected. + */ + function test_remove() external { + assertEq(map.length(), 0, "Map should be empty"); + + MyStruct storage s; + for (uint256 i = 0; i < ITERATIONS; i++) { + s = _add(0xdeadbeef + i, i + 1, true); + } + + assertEq(map.length(), ITERATIONS, "unexpected map length"); + uint256 count = ITERATIONS - 1; + _removeByKey(0xdeadbeef + count, true); + assertEq(map.length(), count, "element not removed"); + + // Cannot remove the same key twice + _removeByKey(0xdeadbeef + count, false); + + // Cannot remove an unknown key + _removeByKey(0xdeadbeef + ITERATIONS * 2, false); + + for (uint256 i = 0; i < ITERATIONS; i++) { + s = _at(i, i < count); + if (i < count) { + assertEq(s.a, i + 1, "MyStruct.a mismatch"); + assertEq(s.b, i + 2, "MyStruct.b mismatch"); + assertEq(s.c, i + 3, "MyStruct.c mismatch"); + } + } + + uint256 removeIndex = count - 3; + _removeByKey(0xdeadbeef + removeIndex, true); + count -= 1; + assertEq(map.length(), count, "element not removed"); + s = _at(removeIndex, true); + assertEq(s.a, count + 1, "MyStruct.a mismatch"); + assertEq(s.b, count + 2, "MyStruct.b mismatch"); + assertEq(s.c, count + 3, "MyStruct.c mismatch"); + + s = _at(removeIndex + 1, true); + assertEq(s.a, removeIndex + 2, "MyStruct.a mismatch"); + assertEq(s.b, removeIndex + 3, "MyStruct.b mismatch"); + assertEq(s.c, removeIndex + 4, "MyStruct.c mismatch"); + + for (uint256 i = 0; i < map.keys.length; i++) { + uint256 key = uint256(map.keys[i]); + assertEq(map.values[bytes32(key)].asUint(), key - 0xdeadbeef + 1, "MyStruct.a mismatch"); + s = _get(key, true); + assertEq(s.a, key - 0xdeadbeef + 1, "MyStruct.a mismatch"); + assertEq(s.b, key - 0xdeadbeef + 2, "MyStruct.b mismatch"); + assertEq(s.c, key - 0xdeadbeef + 3, "MyStruct.c mismatch"); + } + } + + /** + * Test if `Map.add` and `Map.at` work as expected. + */ + function test_fuzz(bytes32 key, uint256 value) external { + assertEq(map.length(), 0, "Map should be empty"); + + // Map.length works + Pointer.Uint256Slot storage store; + store = map.add(key).getUint256Slot(); + assertFalse(store.asPtr().isNull(), "invalid pointer"); + store.value = value; + + // Map.length works + assertEq(map.length(), 1, "unexpected map length"); + + // Map.get works + store = map.get(key).getUint256Slot(); + assertEq(store.value, value, "unexpected value when retrieving by key"); + + // Map.indexOf works + int256 index = map.indexOf(store.asPtr()); + assertEq(index, 0, "unexpected index"); + + // Map.at works + (bytes32 atKey, StoragePtr raw) = map.at(0); + store = raw.getUint256Slot(); + assertEq(store.value, value, "unexpected value when retrieving by index"); + assertEq(atKey, key, "unexpected key when retrieving by index"); + + // Map.contains works + StoragePtr ptr = map.get(key); + assertTrue(map.contains(ptr), "the key should be in the map"); + + // Map.contains returns false for invalid pointers + ptr = (ptr.asUint() + 1).asPtr(); + assertFalse(map.contains(ptr), "invalid pointer"); + ptr = (ptr.asUint() - 2).asPtr(); + assertFalse(map.contains(ptr), "invalid pointer"); + } +} diff --git a/analog-gmp/test/Example.t.sol b/analog-gmp/test/Example.t.sol new file mode 100644 index 000000000..0b196058e --- /dev/null +++ b/analog-gmp/test/Example.t.sol @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (test/Example.t.sol) + +pragma solidity >=0.8.0; + +import {Test} from "forge-std/Test.sol"; +import {VmSafe} from "forge-std/Vm.sol"; +import {console} from "forge-std/console.sol"; +import {Random} from "./Random.sol"; +import {MockERC20} from "./MockERC20.sol"; +import {GmpTestTools} from "./GmpTestTools.sol"; +import {TestUtils, SigningKey, VerifyingKey, SigningUtils, VerifyingUtils} from "./TestUtils.sol"; +import {Gateway, GatewayEIP712} from "../src/Gateway.sol"; +import {GatewayProxy} from "../src/GatewayProxy.sol"; +import {IGateway} from "../src/interfaces/IGateway.sol"; +import {IGmpReceiver} from "../src/interfaces/IGmpReceiver.sol"; +import {IExecutor} from "../src/interfaces/IExecutor.sol"; +import {GasUtils} from "../src/utils/GasUtils.sol"; +import { + GmpMessage, + UpdateKeysMessage, + Signature, + TssKey, + Network, + GmpStatus, + GmpSender, + PrimitiveUtils +} from "../src/Primitives.sol"; + +contract ExampleTest is Test { + using SigningUtils for SigningKey; + using VerifyingUtils for VerifyingKey; + using PrimitiveUtils for GmpMessage; + using PrimitiveUtils for address; + + uint16 private constant SRC_NETWORK_ID = 1234; + uint16 private constant DEST_NETWORK_ID = 1337; + uint256 private constant SENDER_SECRET = uint248(uint256(keccak256("secret"))); + address private _sender; + + address private constant ALICE = address(bytes20(keccak256("Alice"))); + address private constant BOB = address(bytes20(keccak256("Bob"))); + + function setUp() external { + vm.deal(ALICE, 100 ether); + vm.deal(BOB, 100 ether); + } + + function deployGateway(VmSafe.Wallet memory admin, SigningKey memory signer, uint16[] memory networkIds) + private + returns (Network[] memory networks) + { + TssKey[] memory keys = new TssKey[](1); + keys[0] = TssKey({yParity: signer.pubkey.yParity() == 28 ? 3 : 2, xCoord: signer.pubkey.px}); + + networks = new Network[](networkIds.length); + for (uint256 i = 0; i < networks.length; i++) { + networks[i].id = networkIds[i]; + networks[i].gateway = TestUtils.computeGatewayProxyAddress(admin.addr, bytes32(uint256(networks[i].id))); + vm.deal(networks[i].gateway, 100 ether); + } + + // bytes memory initializer = abi.encodeCall(Gateway.initialize, (msg.sender, keys, networks)); + for (uint256 i = 0; i < networks.length; i++) { + address proxy = + address(TestUtils.setupGateway(admin, bytes32(uint256(networks[i].id)), networks[i].id, keys, networks)); + assertEq(proxy, networks[i].gateway, "GatewayProxy address mismatch"); + vm.deal(proxy, 100 ether); + } + } + + function testSignature() external pure { + SigningKey memory sk = TestUtils.createSigner(); + VerifyingKey memory vk = sk.pubkey; + (uint256 c, uint256 z) = sk.sign("hello world!", Random.nextUint()); + assertTrue(vk.verify("hello world!", c, z), "invalid signature"); + } + + function testTeleportTokens() external { + vm.txGasPrice(1); + VmSafe.Wallet memory senderWallet = vm.createWallet(SENDER_SECRET); + _sender = senderWallet.addr; + vm.deal(_sender, 100 ether); + + // Step 1: Deploy the Gateway contract + SigningKey memory signer = TestUtils.createSigner(); + Gateway srcGateway; + Gateway dstGateway; + { + uint16[] memory networkIds = new uint16[](2); + networkIds[0] = SRC_NETWORK_ID; + networkIds[1] = DEST_NETWORK_ID; + Network[] memory networks = deployGateway(senderWallet, signer, networkIds); + srcGateway = Gateway(payable(networks[0].gateway)); + dstGateway = Gateway(payable(networks[1].gateway)); + } + + // Step 2: Deploy the sender and recipient contracts + vm.startPrank(_sender, _sender); + MockERC20 srcToken = MockERC20(vm.computeCreateAddress(_sender, vm.getNonce(_sender) + 1)); + MockERC20 dstToken = + new MockERC20("Destination Token", "B", dstGateway, srcToken, srcGateway.networkId(), ALICE, 0); + srcToken = new MockERC20("Source Token", "A", srcGateway, dstToken, dstGateway.networkId(), ALICE, 1000); + + // Step 3: Send GMP message + GmpSender source = address(srcToken).toSender(false); + GmpMessage memory gmp = GmpMessage({ + source: source, + srcNetwork: SRC_NETWORK_ID, + dest: address(dstToken), + destNetwork: DEST_NETWORK_ID, + gasLimit: 100_000, + nonce: 0, + data: abi.encode(MockERC20.CrossChainTransfer({from: ALICE, to: BOB, amount: 100})) + }); + + // Calculate the expect GMP gas cost + uint256 gasCost; + { + uint256 nonZeros = GasUtils.countNonZeros(gmp.data); + uint256 zeros = gmp.data.length - nonZeros; + gasCost = GasUtils.estimateGas(uint16(nonZeros), uint16(zeros), gmp.gasLimit); + } + + // Expect `GmpCreated` to be emitted + bytes32 messageID = gmp.eip712hash(); + vm.expectEmit(true, true, true, true, address(srcGateway)); + emit IGateway.GmpCreated( + messageID, + GmpSender.unwrap(gmp.source), + gmp.dest, + gmp.destNetwork, + gmp.gasLimit, + uint64(gasCost), + gmp.nonce, + gmp.data + ); + + { + // Estimate the cost of teleporting 100 tokens + uint256 gmpCost = srcToken.teleportCost(); + + // Submit the GMP message from `sender` contract + vm.stopPrank(); + vm.prank(ALICE, ALICE); + srcToken.teleport{value: gmpCost}(BOB, 100); + } + + vm.startPrank(_sender, _sender); + (uint256 c, uint256 z) = signer.signPrehashed(messageID, Random.nextUint()); + Signature memory sig = Signature({xCoord: signer.pubkey.px, e: c, s: z}); + assertTrue(dstGateway.gmpInfo(messageID).status == GmpStatus.NOT_FOUND, "GMP message already executed"); + + // Expect `GmpExecuted` to be emitted + vm.expectEmit(true, true, true, true, address(dstGateway)); + emit IExecutor.GmpExecuted(messageID, gmp.source, gmp.dest, GmpStatus.SUCCESS, messageID); + + // Execute the GMP message + dstGateway.execute(sig, gmp); + assertTrue(dstGateway.gmpInfo(messageID).status == GmpStatus.SUCCESS, "failed to execute GMP message"); + + // Check balance + assertEq(srcToken.balanceOf(ALICE), 900, "sender balance mismatch"); + assertEq(dstToken.balanceOf(ALICE), 0, "recipient balance mismatch"); + assertEq(srcToken.balanceOf(BOB), 0, "sender balance mismatch"); + assertEq(dstToken.balanceOf(BOB), 100, "recipient balance mismatch"); + } +} diff --git a/analog-gmp/test/Float9x56.t.sol b/analog-gmp/test/Float9x56.t.sol new file mode 100644 index 000000000..e1586b167 --- /dev/null +++ b/analog-gmp/test/Float9x56.t.sol @@ -0,0 +1,367 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (test/Float9x56.t.sol) + +pragma solidity >=0.8.0; + +import {Test, console} from "forge-std/Test.sol"; +import {BranchlessMath, Rounding} from "../src/utils/BranchlessMath.sol"; +import {UFloat9x56, UFloatMath} from "../src/utils/Float9x56.sol"; + +contract UFloatMathMock { + function encode(uint256 mantissa, int256 exponent) external pure returns (UFloat9x56) { + return UFloatMath.encode(mantissa, exponent); + } +} + +contract UFloatMathTest is Test { + using UFloatMath for UFloat9x56; + using BranchlessMath for uint256; + + UFloatMathMock mock; + + // Fuzz test fixtures for mantissa, see: + // - https://book.getfoundry.sh/forge/fuzz-testing#fuzz-test-fixtures + uint56[] public fixtureMantissa = [0, 1, uint56(UFloatMath.MANTISSA_MIN), uint56(UFloatMath.MANTISSA_MAX)]; + + constructor() { + mock = new UFloatMathMock(); + } + + /** + * @dev multiply an UFloat9x56 by an uint256 in constant gas. + */ + function test_fuzzMul(uint256 x) external pure { + // Any value multiplied by 0 should be 0 + uint256 result = UFloatMath.ZERO.mul(x); + assertEq(result, 0); + + // Any value times 1 should be the same value + result = UFloatMath.ONE.mul(x); + assertEq(result, x); + + // Any value times 2 should be the double + result = UFloatMath.fromUint(2).mul(x); + unchecked { + assertEq(result, x << 1); + } + } + + function test_mul() external pure { + unchecked { + // Any value multiplied by 0 should be 0 + UFloat9x56 value = UFloatMath.ZERO; + assertEq(value.mul(0), 0); + assertEq(value.mul(1), 0); + assertEq(value.mul(type(uint256).max), 0); + + // Any value multiplied 1 should be the same value + value = UFloatMath.ONE; + assertEq(value.mul(0), 0); + assertEq(value.mul(1), 1); + assertEq(value.mul(type(uint256).max), type(uint256).max); + + // Any value times 2 should be the double + value = UFloatMath.fromUint(2); + assertEq(value.mul(0), 0); + assertEq(value.mul(1), 2); + assertEq(value.mul(type(uint256).max), type(uint256).max * 2); + + // Test fractions + value = UFloatMath.fromRational(1, 2, Rounding.Floor); + assertEq(value.mul(0), 0); + assertEq(value.mul(1), 0); + assertEq(value.mul(2), 1); + assertEq(value.mul(type(uint256).max), type(uint256).max / 2); + } + } + + function test_eq() external pure { + unchecked { + assertTrue(UFloatMath.ZERO.eq(0)); + assertFalse(UFloatMath.ZERO.eq(1)); + assertFalse(UFloatMath.ZERO.eq(type(uint256).max)); + + assertFalse(UFloatMath.ONE.eq(0)); + assertTrue(UFloatMath.ONE.eq(1)); + assertFalse(UFloatMath.ONE.eq(type(uint256).max)); + + UFloat9x56 value = UFloatMath.fromUint(type(uint256).max); + assertFalse(value.eq(0)); + assertFalse(value.eq(1)); + assertFalse(value.eq(type(uint256).max / 2)); + assertTrue(value.eq(type(uint256).max)); + + value = UFloatMath.fromRational(123456789, 1111); + assertFalse(value.eq(0)); + assertFalse(value.eq(1)); + assertFalse(value.eq(type(uint256).max)); + assertTrue(value.eq(uint256(123456789) / uint256(1111))); + } + } + + /** + * @dev Saturating multiplication, bounds to `2 ** 256 - 1` instead of overflowing. + */ + function test_saturatingMul() external pure { + unchecked { + // Any value multiplied by 0 should be 0 + UFloat9x56 value = UFloatMath.ZERO; + assertEq(value.saturatingMul(0), 0); + assertEq(value.saturatingMul(1), 0); + assertEq(value.saturatingMul(type(uint256).max), 0); + + // Any value multiplied 1 should be the same value + value = UFloatMath.ONE; + assertEq(value.saturatingMul(0), 0); + assertEq(value.saturatingMul(1), 1); + assertEq(value.saturatingMul(type(uint256).max), type(uint256).max); + + // Any value times 2 should be the double or bounded to 2 ** 256 - 1 + value = UFloatMath.fromUint(2); + assertEq(value.saturatingMul(0), 0); + assertEq(value.saturatingMul(1), 2); + assertEq(value.saturatingMul(type(uint256).max - 1), type(uint256).max); + assertEq(value.saturatingMul(type(uint256).max), type(uint256).max); + + // Test fractions + value = UFloatMath.fromRational(1, 2, Rounding.Floor); + assertEq(value.saturatingMul(0), 0); + assertEq(value.saturatingMul(1), 0); + assertEq(value.saturatingMul(2), 1); + assertEq(value.saturatingMul(type(uint256).max), type(uint256).max / 2); + } + } + + /** + * @dev Returns the mantissa and base 2 exponent as integers, respectively. + * The original number can be recovered by `mantissa * 2 ** exponent`. + * Returns (0, -311) if the value is zero. + */ + function test_decode() external pure { + (uint256 mantissa, int256 exponent) = UFloatMath.ZERO.decode(); + assertEq(mantissa, 0); + assertEq(exponent, -311); + + (mantissa, exponent) = UFloatMath.ONE.decode(); + assertEq(mantissa, UFloatMath.MANTISSA_MIN); + assertEq(exponent, -55); + + (mantissa, exponent) = UFloatMath.MAX.decode(); + assertEq(mantissa, 0xffffffffffffff); + assertEq(exponent, 200); + + (mantissa, exponent) = UFloatMath.fromRational(1, 3).decode(); + assertEq(mantissa, 0xaaaaaaaaaaaaab); + assertEq(exponent, -57); + + (mantissa, exponent) = UFloatMath.fromRational(123456789123456789, 1000000000).decode(); + assertEq(mantissa, 0xeb79a2a3f35ba7); + assertEq(exponent, -29); + } + + /** + * @dev Test encoding `mantissa` and `exponent` into `UFloat9x56` + */ + function test_encode() external { + // Encode zero + assertEq(UFloatMath.encode(0, 0), UFloatMath.ZERO); + assertEq(UFloatMath.encode(0, -311), UFloatMath.ZERO); + assertEq(UFloatMath.encode(0, -310), UFloatMath.ZERO); + assertEq(UFloatMath.encode(0, 200), UFloatMath.ZERO); + + // Normal numbers + assertEq(UFloatMath.encode(0x80000000000000, -55), UFloatMath.ONE); + assertEq(UFloatMath.encode(UFloatMath.MANTISSA_MAX, 200), UFloatMath.MAX); + assertEq(UFloatMath.encode(0xaaaaaaaaaaaaab, -57), UFloatMath.fromRational(1, 3)); + assertEq(UFloatMath.encode(0xeb79a2a3f35ba7, -29), UFloatMath.fromRational(123456789123456789, 1000000000)); + assertEq(UFloatMath.encode(UFloatMath.MANTISSA_MIN, -310), UFloat9x56.wrap(0x0080000000000000)); + + // Subnormal numbers + assertEq(UFloatMath.encode(0x00000000000000, -311), UFloat9x56.wrap(0x0000000000000000)); + assertEq(UFloatMath.encode(0x00000000000001, -310), UFloat9x56.wrap(0x0000000000000001)); + assertEq(UFloatMath.encode(0x7fffffffffffff, -310), UFloat9x56.wrap(0x007fffffffffffff)); + + // Revert if the exponent is out of bounds + vm.expectRevert("UFloat9x56: exponent out of bounds"); + mock.encode(0, -312); + vm.expectRevert("UFloat9x56: exponent out of bounds"); + mock.encode(0, 201); + + // Revert if the mantissa is invalid + vm.expectRevert("UFloat9x56: invalid mantissa"); + mock.encode(1, 0); + vm.expectRevert("UFloat9x56: invalid mantissa"); + mock.encode(UFloatMath.MANTISSA_MAX + 1, 200); + vm.expectRevert("UFloat9x56: invalid mantissa"); + mock.encode(UFloatMath.MANTISSA_MIN - 1, -309); + } + + /** + * @dev Fuzz test converting between `uint256` and `UFloat9x56`. + * The conversion must be exact given x is 56 bit long. + */ + function test_fuzzConvertUint(uint56 mantissa, uint8 exponent) external pure { + unchecked { + uint256 value = uint256(mantissa) << uint256(exponent); + UFloat9x56 float = UFloatMath.fromUint(value); + assertEq(float.truncate(), value); + } + } + + /** + * @dev Fuzz test `UFloatMath.mul` and `UFloatMath.saturatingMul`. + */ + function test_fuzzMultiplication(uint56 mantissa, uint8 exponent, uint256 multiplier) external pure { + unchecked { + uint256 value = uint256(mantissa) << uint256(exponent); + UFloat9x56 float = UFloatMath.fromUint(value); + assertEq(float.mul(multiplier), value * multiplier); + assertEq(float.saturatingMul(multiplier), value.saturatingMul(multiplier)); + } + } + + /** + * @dev Compare two `UFloat`. + */ + function assertEq(UFloat9x56 left, UFloat9x56 right) internal pure { + assertEq(UFloat9x56.unwrap(left), UFloat9x56.unwrap(right)); + } + + function assertEq(UFloat9x56 left, UFloat9x56 right, string memory message) internal pure { + assertEq(UFloat9x56.unwrap(left), UFloat9x56.unwrap(right), message); + } + + /** + * @dev Computes `num * 2**exp / den` with the given rounding direction. + */ + function muldiv(uint256 num, int256 exp, uint256 den, Rounding rounding) internal pure returns (uint256) { + uint256 numzeros = num.leadingZeros(); + uint256 denzeros = den.leadingZeros(); + assertLe(exp, int256(denzeros)); + assertGe(exp, -int256(numzeros + 255)); + + if (exp < -255) { + num <<= uint256(-exp) - 255; + exp = 255; + } else if (exp <= 0) { + exp = -exp; + } else { + den <<= uint256(exp); + exp = 0; + } + + return BranchlessMath.mulDiv(num, 1 << uint256(exp), den, rounding); + } + + /** + * @dev When `numerator` and `denominator` are exact, the conversion must be exact. + */ + function checkfromRationalRouding(uint256 num, uint256 den, UFloat9x56 expected) internal pure { + (uint256 mantissa, int256 exponent) = expected.decode(); + assertGt(mantissa + BranchlessMath.toUint(expected.eq(UFloatMath.ZERO)), 0, "mantissa is zero"); + uint256 value = muldiv(num, exponent, den, Rounding.Floor); + assertEq(mantissa, value, "Rounding.Floor failed"); + + bool roundUp = muldiv(num, exponent, den, Rounding.Nearest) > value && expected.lt(UFloatMath.MAX); + expected = UFloat9x56.wrap(uint64(UFloat9x56.unwrap(expected) + BranchlessMath.toUint(roundUp))); + (mantissa, exponent) = expected.decode(); + value = muldiv(num, exponent, den, Rounding.Nearest); + assertEq(mantissa, value, "Rounding.Nearest failed"); + + roundUp = muldiv(num, exponent, den, Rounding.Ceil) > value && expected.lt(UFloatMath.MAX); + expected = UFloat9x56.wrap(uint64(UFloat9x56.unwrap(expected) + BranchlessMath.toUint(roundUp))); + (mantissa, exponent) = expected.decode(); + value = muldiv(num, exponent, den, Rounding.Ceil); + assertEq(mantissa, value, "Rounding.Ceil failed"); + } + + /** + * @dev Converts numerator / denominator to `UFloat9x56`, following the selected rounding direction. + */ + function test_fromRational() external pure { + // Normal numbers + checkfromRationalRouding(0, 1, UFloatMath.ZERO); + checkfromRationalRouding(0, 2, UFloatMath.ZERO); + checkfromRationalRouding(0, type(uint256).max, UFloatMath.ZERO); + checkfromRationalRouding(1, 1, UFloatMath.ONE); + checkfromRationalRouding(2, 2, UFloatMath.ONE); + checkfromRationalRouding(UFloatMath.MANTISSA_MAX, UFloatMath.MANTISSA_MAX, UFloatMath.ONE); + checkfromRationalRouding(type(uint256).max, type(uint256).max, UFloatMath.ONE); + checkfromRationalRouding(0x2ffffffffffffff, 3, UFloat9x56.wrap(0x9bffffffffffffff)); + + // Test rounding + assertEq(UFloatMath.fromRational(0x2ffffffffffffff, 3, Rounding.Floor), UFloat9x56.wrap(0x9bffffffffffffff)); + assertEq(UFloatMath.fromRational(0x2ffffffffffffff, 3, Rounding.Nearest), UFloat9x56.wrap(0x9c00000000000000)); + assertEq(UFloatMath.fromRational(0x2ffffffffffffff, 3, Rounding.Ceil), UFloat9x56.wrap(0x9c00000000000000)); + } + + /** + * @dev Converts numerator / denominator to `UFloat9x56`, following the selected rounding direction. + */ + function test_fuzzFromRational(uint248 numerator, uint248 denominator) external pure { + vm.assume(denominator > 0 && numerator > 0); + uint256 numbits = BranchlessMath.log2(numerator); + uint256 denbits = BranchlessMath.log2(denominator); + vm.assume(numbits.absDiff(denbits) <= 200); + unchecked { + UFloat9x56 float = UFloatMath.fromRational(numerator, denominator, Rounding.Floor); + checkfromRationalRouding(numerator, denominator, float); + uint256 integer = uint256(numerator) / uint256(denominator); + assertTrue(float.eq(integer), "float is not equal to integer"); + + // Find a multiplier such that (multipler / numerator) * numerator > 0 + uint256 multiplier = denbits.saturatingSub(numbits) + UFloatMath.MANTISSA_DIGITS - 1; + multiplier = multiplier.min(255 - numbits); + multiplier = 2 ** multiplier; + + // Calculate numerator * multipler / numerator + integer = BranchlessMath.mulDiv(numerator, multiplier, denominator, Rounding.Floor); + { + // Keep only the `MANTISSA_DIGITS` most significant bits. + uint256 shift = integer.log2().saturatingSub(UFloatMath.MANTISSA_DIGITS - 1); + uint256 mask = type(uint256).max << shift; + integer &= mask; + } + assertGt(integer, 0, "integer is zero"); + assertEq(float.mul(multiplier), integer); + } + } + + /** + * @dev Convert `UFloat9x56` to a rational number, returns the numerator and denominator, respectively. + * Obs: Values above 2**-256 are represented precisely, values below are approximated or round down to zero. + */ + function test_toRational(uint56 mantissa, uint16 exponent) external pure { + // Make sure the exponent is within 9 bit bounds. + exponent %= 2 ** 9; + + // Doesn't allow exponent == 0, once subnormal numbers cannot be converted to rational precisely. + vm.assume(exponent > 0); + unchecked { + UFloat9x56 float = + UFloat9x56.wrap(uint64(mantissa) | uint64(exponent) << uint64(UFloatMath.MANTISSA_DIGITS - 1)); + (uint256 numerator, uint256 denominator) = float.toRational(); + uint256 numbits = numerator.log2(); + uint256 denbits = denominator.log2(); + + uint256 integer = uint256(numerator) / uint256(denominator); + assertTrue(float.eq(integer), "float is not equal to integer"); + + // Find a multiplier such that (multipler / numerator) * numerator > 0 + uint256 multiplier = denbits.saturatingSub(numbits) + UFloatMath.MANTISSA_DIGITS - 1; + multiplier = multiplier.min(255 - numbits); + multiplier = 2 ** multiplier; + + // Calculate numerator * multipler / numerator + integer = BranchlessMath.mulDiv(numerator, multiplier, denominator, Rounding.Floor); + { + // Keep only the `MANTISSA_DIGITS` most significant bits. + uint256 shift = integer.log2().saturatingSub(UFloatMath.MANTISSA_DIGITS - 1); + uint256 mask = type(uint256).max << shift; + integer &= mask; + } + assertGt(integer, 0, "integer is zero"); + assertEq(float.mul(multiplier), integer); + } + } +} diff --git a/analog-gmp/test/GasUtils.t.sol b/analog-gmp/test/GasUtils.t.sol new file mode 100644 index 000000000..86cd2df14 --- /dev/null +++ b/analog-gmp/test/GasUtils.t.sol @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (test/GasUtils.t.sol) + +pragma solidity >=0.8.0; + +import {Signer} from "frost-evm/sol/Signer.sol"; +import {Test, console} from "forge-std/Test.sol"; +import {VmSafe} from "forge-std/Vm.sol"; +import {TestUtils} from "./TestUtils.sol"; +import {BaseTest} from "./utils/BaseTest.sol"; +import {GasSpender} from "./utils/GasSpender.sol"; +import {Gateway, GatewayEIP712} from "../src/Gateway.sol"; +import {GatewayProxy} from "../src/GatewayProxy.sol"; +import {GasUtils} from "../src/utils/GasUtils.sol"; +import {BranchlessMath} from "../src/utils/BranchlessMath.sol"; +import {UFloat9x56, UFloatMath} from "../src/utils/Float9x56.sol"; +import {IGateway} from "../src/interfaces/IGateway.sol"; +import {IGmpReceiver} from "../src/interfaces/IGmpReceiver.sol"; +import {IExecutor} from "../src/interfaces/IExecutor.sol"; +import {CallOptions, GatewayUtils} from "./Gateway.t.sol"; +import { + GmpMessage, + UpdateKeysMessage, + Signature, + TssKey, + Network, + GmpStatus, + PrimitiveUtils, + GmpSender +} from "../src/Primitives.sol"; + +uint256 constant secret = 0x42; +uint256 constant nonce = 0x69; + +contract GasUtilsMock { + function execute(Signature calldata, GmpMessage calldata) + external + pure + returns (uint256 baseCost, uint256 nonZeros, uint256 zeros) + { + baseCost = GasUtils.txBaseCost(); + nonZeros = GasUtils.countNonZerosCalldata(msg.data); + zeros = msg.data.length - nonZeros; + } +} + +contract GasUtilsTest is BaseTest { + using PrimitiveUtils for UpdateKeysMessage; + using PrimitiveUtils for GmpMessage; + using PrimitiveUtils for GmpSender; + using PrimitiveUtils for address; + using GatewayUtils for CallOptions; + using BranchlessMath for uint256; + + GasUtilsMock internal mock; + Gateway internal gateway; + Signer internal signer; + + // Receiver Contract, the will waste the exact amount of gas you sent to it in the data field + IGmpReceiver internal receiver; + + uint16 private constant SRC_NETWORK_ID = 1234; + uint16 internal constant DEST_NETWORK_ID = 1337; + + constructor() { + TestUtils.deployFactory(); + + // Create the Shard and Admin accounts + signer = new Signer(secret); + VmSafe.Wallet memory deployer = vm.createWallet(secret); + vm.deal(deployer.addr, 100 ether); + + // Deploy the GasUtilsMock contract + mock = new GasUtilsMock(); + + // Deploy the GatewayProxy + gateway = Gateway( + payable(address(TestUtils.setupGateway(deployer, bytes32(uint256(0)), SRC_NETWORK_ID, DEST_NETWORK_ID))) + ); + vm.deal(address(gateway), 100 ether); + + // Deploy the GasSpender contract, which implements the IGmpReceiver interface. + receiver = IGmpReceiver(new GasSpender()); + } + + function sign(GmpMessage memory gmp) internal view returns (Signature memory) { + uint256 hash = uint256(gmp.eip712hash()); + (uint256 e, uint256 s) = signer.signPrehashed(hash, nonce); + return Signature({xCoord: signer.xCoord(), e: e, s: s}); + } + + /** + * @dev Create a GMP message with the provided parameters. + */ + function _buildGmpMessage(address sender, uint64 gasLimit, uint64 gasUsed, uint256 messageSize) + private + view + returns (GmpMessage memory message, Signature memory signature, CallOptions memory context) + { + require(gasUsed == 0 || messageSize >= 32, "If gasUsed > 0, then messageSize must be >= 32"); + require(messageSize <= 0x6000, "message is too big"); + + // Setup data and receiver addresses. + bytes memory data = new bytes(messageSize); + address gmpReceiver; + if (gasUsed > 0) { + gmpReceiver = address(receiver); + assembly { + mstore(add(data, 32), gasUsed) + } + } else { + // Create a new unique receiver address for each message, otherwise the gas refund will not work. + gmpReceiver = address(bytes20(keccak256(abi.encode(sender, gasLimit, messageSize)))); + } + + // Build the GMP message + message = GmpMessage({ + source: sender.toSender(false), + srcNetwork: SRC_NETWORK_ID, + dest: gmpReceiver, + destNetwork: DEST_NETWORK_ID, + gasLimit: gasLimit, + nonce: 0, + data: data + }); + + // Sign the message + signature = sign(message); + + // Calculate memory expansion cost and base cost + (uint256 baseCost, uint256 executionCost) = GatewayUtils.computeGmpGasCost(signature, message); + + // Set Transaction Parameters + context = CallOptions({ + from: sender, + to: address(gateway), + value: 0, + gasLimit: GasUtils.executionGasNeeded(message.data.length, message.gasLimit).saturatingAdd(baseCost), + executionCost: executionCost, + baseCost: baseCost + }); + } + + /** + * Test the `GasUtils.txBaseCost` method. + */ + function test_txBaseCost() external view { + // Build and sign GMP message + GmpMessage memory gmp = GmpMessage({ + source: address(0x1111111111111111111111111111111111111111).toSender(false), + srcNetwork: 1234, + dest: address(0x2222222222222222222222222222222222222222), + destNetwork: 1337, + gasLimit: 0, + nonce: 0, + data: hex"00" + }); + Signature memory sig = sign(gmp); + sig.xCoord = type(uint256).max; + sig.e = type(uint256).max; + sig.s = type(uint256).max; + + // Check if `IExecutor.execute` match the expected base cost + (uint256 baseCost, uint256 nonZeros, uint256 zeros) = mock.execute(sig, gmp); + assertEq(baseCost, 24444, "Wrong calldata gas cost"); + assertEq(nonZeros, 147, "wrong number of non-zeros"); + assertEq(zeros, 273, "wrong number of zeros"); + } + + /** + * @dev Compare the estimated gas cost VS the actual gas cost of the `execute` method. + */ + function test_baseExecutionCost(uint16 messageSize, uint16 gasLimit) external { + vm.assume(gasLimit >= 5000); + vm.assume(messageSize <= (0x6000 - 32)); + messageSize += 32; + vm.txGasPrice(1); + address sender = TestUtils.createTestAccount(100 ether); + + // Build the GMP message + GmpMessage memory gmp; + Signature memory sig; + CallOptions memory ctx; + (gmp, sig, ctx) = _buildGmpMessage(sender, gasLimit, gasLimit, messageSize); + + // Increase the gas limit to avoid out-of-gas errors + ctx.gasLimit = ctx.gasLimit.saturatingAdd(10_000_000); + + // Execute the GMP message + { + bytes32 gmpId = gmp.eip712hash(); + vm.expectEmit(true, true, true, true); + emit IExecutor.GmpExecuted(gmpId, gmp.source, gmp.dest, GmpStatus.SUCCESS, bytes32(uint256(gasLimit))); + uint256 balanceBefore = ctx.from.balance; + (GmpStatus status, bytes32 result) = ctx.execute(sig, gmp); + assertEq(uint256(status), uint256(GmpStatus.SUCCESS), "GMP execution failed"); + assertEq(result, bytes32(uint256(gasLimit)), "unexpected result"); + assertEq(balanceBefore, ctx.from.balance, "Balance should not change"); + } + + emit log_named_uint("execution cost", GasUtils._executionGasCost(gmp.data.length, gmp.gasLimit)); + uint256 executionCost = GasUtils.computeExecutionRefund(uint16(gmp.data.length), gmp.gasLimit); + assertEq(ctx.executionCost, executionCost, "execution cost mismatch"); + + // Calculate the expected base cost + uint256 dynamicCost = executionCost - GasUtils.EXECUTION_BASE_COST; + uint256 expectedBaseCost = ctx.executionCost - dynamicCost; + { + console.log("proxy: ", ctx.to); + console.logBytes(ctx.to.code); + address implementationAddr = address( + uint160(uint256(vm.load(ctx.to, 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc))) + ); + console.log("implementation: ", implementationAddr); + console.logBytes(implementationAddr.code); + console.log("calldata:"); + console.logBytes(abi.encodeCall(IExecutor.execute, (sig, gmp))); + } + assertEq(expectedBaseCost, GasUtils.EXECUTION_BASE_COST, "Wrong EXECUTION_BASE_COST"); + } + + function test_gasUtils() external pure { + uint256 baseCost = GasUtils.EXECUTION_BASE_COST; + assertEq(GasUtils.estimateGas(0, 0, 0), 31528 + baseCost); + assertEq(GasUtils.estimateGas(0, 33, 0), 31901 + baseCost); + assertEq(GasUtils.estimateGas(33, 0, 0), 32561 + baseCost); + assertEq(GasUtils.estimateGas(20, 13, 0), 32301 + baseCost); + + UFloat9x56 one = UFloatMath.ONE; + assertEq(GasUtils.estimateWeiCost(one, 0, 0, 0, 0), 31528 + baseCost); + assertEq(GasUtils.estimateWeiCost(one, 0, 0, 33, 0), 31901 + baseCost); + assertEq(GasUtils.estimateWeiCost(one, 0, 33, 0, 0), 32561 + baseCost); + assertEq(GasUtils.estimateWeiCost(one, 0, 20, 13, 0), 32301 + baseCost); + + UFloat9x56 two = UFloat9x56.wrap(0x8080000000000000); + assertEq(GasUtils.estimateWeiCost(two, 0, 0, 0, 0), (31528 + baseCost) * 2); + assertEq(GasUtils.estimateWeiCost(two, 0, 0, 33, 0), (31901 + baseCost) * 2); + assertEq(GasUtils.estimateWeiCost(two, 0, 33, 0, 0), (32561 + baseCost) * 2); + assertEq(GasUtils.estimateWeiCost(two, 0, 20, 13, 0), (32301 + baseCost) * 2); + } +} diff --git a/analog-gmp/test/Gateway.t.sol b/analog-gmp/test/Gateway.t.sol new file mode 100644 index 000000000..4c94fb507 --- /dev/null +++ b/analog-gmp/test/Gateway.t.sol @@ -0,0 +1,787 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (test/Gateway.t.sol) + +pragma solidity >=0.8.0; + +import {Test, console, Vm} from "forge-std/Test.sol"; +import {VmSafe} from "forge-std/Vm.sol"; +import {TestUtils, SigningKey, SigningUtils} from "./TestUtils.sol"; +import {GasSpender} from "./utils/GasSpender.sol"; +import {BaseTest} from "./utils/BaseTest.sol"; +import {Gateway, GatewayEIP712} from "../src/Gateway.sol"; +import {GatewayProxy} from "../src/GatewayProxy.sol"; +import {GasUtils} from "../src/utils/GasUtils.sol"; +import {BranchlessMath} from "../src/utils/BranchlessMath.sol"; +import {UFloat9x56, UFloatMath} from "../src/utils/Float9x56.sol"; +import {IGateway} from "../src/interfaces/IGateway.sol"; +import {IGmpReceiver} from "../src/interfaces/IGmpReceiver.sol"; +import {IExecutor} from "../src/interfaces/IExecutor.sol"; +import { + GmpMessage, + UpdateKeysMessage, + Signature, + TssKey, + Network, + GmpStatus, + PrimitiveUtils, + GmpSender, + GMP_VERSION +} from "../src/Primitives.sol"; + +contract SigUtilsTest is GatewayEIP712, Test { + using PrimitiveUtils for GmpMessage; + + constructor() GatewayEIP712(69, address(0)) {} + + function testPayload() public pure { + GmpMessage memory gmp = GmpMessage({ + source: GmpSender.wrap(0x0), + srcNetwork: 42, + dest: address(0x0), + destNetwork: 69, + gasLimit: 0, + nonce: 0, + data: "" + }); + bytes32 typedHash = gmp.eip712hash(); + bytes32 expected = keccak256( + abi.encode( + GMP_VERSION, + gmp.source, + gmp.srcNetwork, + gmp.dest, + gmp.destNetwork, + gmp.gasLimit, + gmp.nonce, + keccak256(gmp.data) + ) + ); + assertEq(typedHash, expected); + } +} + +struct CallOptions { + address from; + address to; + uint256 value; + uint256 gasLimit; + uint256 executionCost; + uint256 baseCost; +} + +library GatewayUtils { + function tryExecute(CallOptions memory ctx, Signature memory signature, GmpMessage memory message) + internal + returns (bool success, GmpStatus status, bytes32 result) + { + bytes memory encodedCall = abi.encodeCall(IExecutor.execute, (signature, message)); + bytes memory output; + (ctx.executionCost, ctx.baseCost, success, output) = + TestUtils.tryExecuteCall(ctx.from, ctx.to, ctx.gasLimit, ctx.value, encodedCall); + + if (success) { + require(output.length == 64, "unexpected output length for IExecutor.execute method"); + assembly { + let ptr := add(output, 32) + status := mload(ptr) + result := mload(add(ptr, 32)) + } + } else { + status = GmpStatus.NOT_FOUND; + result = bytes32(0); + } + } + + function execute(CallOptions memory ctx, Signature memory signature, GmpMessage memory message) + internal + returns (GmpStatus status, bytes32 result) + { + bytes memory encodedCall = abi.encodeCall(IExecutor.execute, (signature, message)); + (uint256 executionCost, uint256 baseCost, bytes memory output) = + TestUtils.executeCall(ctx.from, ctx.to, ctx.gasLimit, ctx.value, encodedCall); + + ctx.executionCost = executionCost; + ctx.baseCost = baseCost; + if (output.length == 64) { + assembly { + let ptr := add(output, 32) + status := mload(ptr) + result := mload(add(ptr, 32)) + } + } + } + + function submitMessage(CallOptions memory ctx, GmpMessage memory gmp) internal returns (bytes32 result) { + bytes memory encodedCall = + abi.encodeCall(IGateway.submitMessage, (gmp.dest, gmp.destNetwork, gmp.gasLimit, gmp.data)); + (uint256 executionCost, uint256 baseCost, bytes memory output) = + TestUtils.executeCall(ctx.from, ctx.to, ctx.gasLimit, ctx.value, encodedCall); + ctx.executionCost = executionCost; + ctx.baseCost = baseCost; + if (output.length == 32) { + assembly { + result := mload(add(output, 32)) + } + } + } + + function computeGmpGasCost(Signature memory signature, GmpMessage memory message) + internal + pure + returns (uint256 baseCost, uint256 executionCost) + { + executionCost = GasUtils.computeExecutionRefund(uint16(message.data.length), 0); + bytes memory encodedCall = abi.encodeCall(IExecutor.execute, (signature, message)); + baseCost = TestUtils.calculateBaseCost(encodedCall); + } +} + +// contract GatewayBase is Test { +contract GatewayTest is BaseTest { + using PrimitiveUtils for UpdateKeysMessage; + using PrimitiveUtils for GmpMessage; + using PrimitiveUtils for GmpSender; + using PrimitiveUtils for address; + using GatewayUtils for CallOptions; + using BranchlessMath for uint256; + using SigningUtils for SigningKey; + + Gateway internal gateway; + + // Chronicle TSS Secret + uint256 private constant SECRET = 0x42; + uint256 private constant SIGNING_NONCE = 0x69; + + // Receiver Contract, the will waste the exact amount of gas you sent to it in the data field + IGmpReceiver internal receiver; + + // Netowrk ids + uint16 private constant SRC_NETWORK_ID = 1234; + uint16 internal constant DEST_NETWORK_ID = 1337; + + address internal constant ADMIN = 0x6f4c950442e1Af093BcfF730381E63Ae9171b87a; + + constructor() { + VmSafe.Wallet memory admin = vm.createWallet(SECRET); + assertEq(ADMIN, admin.addr, "admin address mismatch"); + gateway = Gateway( + payable(address(TestUtils.setupGateway(admin, bytes32(uint256(1234)), SRC_NETWORK_ID, DEST_NETWORK_ID))) + ); + receiver = IGmpReceiver(new GasSpender()); + } + + function setUp() external view { + // check block gas limit as gas left + assertEq(block.gaslimit, 30_000_000); + assertTrue(gasleft() >= 10_000_000); + } + + function sign(GmpMessage memory gmp) internal pure returns (Signature memory) { + bytes32 hash = gmp.eip712hash(); + SigningKey memory signer = TestUtils.createSigner(SECRET); + (uint256 e, uint256 s) = signer.signPrehashed(hash, SIGNING_NONCE); + return Signature({xCoord: signer.xCoord(), e: e, s: s}); + } + + function _sortTssKeys(TssKey[] memory keys) private pure { + // sort keys by xCoord + for (uint256 i = 0; i < keys.length; i++) { + for (uint256 j = i + 1; j < keys.length; j++) { + if (keys[i].xCoord > keys[j].xCoord) { + TssKey memory temp = keys[i]; + keys[i] = keys[j]; + keys[j] = temp; + } + } + } + } + + function test_withinSizeLimit() external { + bytes memory implementationCreationCode = + abi.encodePacked(type(Gateway).creationCode, abi.encode(DEST_NETWORK_ID, address(gateway))); + address implementation = + FACTORY.create2(bytes32(uint256(1337)), implementationCreationCode, abi.encode(DEST_NETWORK_ID)); + assertLt(implementation.code.length, 0x6000, "implementation code length is too large"); + } + + function test_setShards() external { + TssKey[] memory keys = new TssKey[](10); + + // create random shard keys + SigningKey memory signer; + for (uint256 i = 0; i < keys.length; i++) { + signer = TestUtils.signerFromEntropy(bytes32(i)); + keys[i] = TssKey({yParity: signer.yParity() == 28 ? 3 : 2, xCoord: signer.xCoord()}); + } + _sortTssKeys(keys); + + // Only admin can set shards keys + vm.expectRevert("unauthorized"); + gateway.setShards(keys); + + // Set shards keys must work + vm.prank(ADMIN, ADMIN); + gateway.setShards(keys); + + // Check shards keys + TssKey[] memory shards = gateway.shards(); + _sortTssKeys(shards); + for (uint256 i = 0; i < shards.length; i++) { + assertEq(shards[i].xCoord, keys[i].xCoord); + assertEq(shards[i].yParity, keys[i].yParity); + } + + // // Replace one shard key + signer = TestUtils.signerFromEntropy(bytes32(uint256(12345))); + keys[0].xCoord = signer.xCoord(); + keys[0].yParity = signer.yParity() == 28 ? 3 : 2; + _sortTssKeys(keys); + vm.prank(ADMIN, ADMIN); + gateway.setShards(keys); + + // Check shards keys + shards = gateway.shards(); + _sortTssKeys(shards); + for (uint256 i = 0; i < shards.length; i++) { + assertEq(shards[i].xCoord, keys[i].xCoord); + assertEq(shards[i].yParity, keys[i].yParity); + } + } + + function test_shardEvents() external { + TssKey[] memory keys = new TssKey[](10); + + // create random shard keys + SigningKey memory signer; + for (uint256 i = 0; i < keys.length; i++) { + signer = TestUtils.signerFromEntropy(bytes32(i)); + keys[i] = TssKey({yParity: signer.yParity() == 28 ? 3 : 2, xCoord: signer.xCoord()}); + } + _sortTssKeys(keys); + + // set shards + vm.prank(ADMIN, ADMIN); + vm.expectEmit(false, false, false, true); + emit IExecutor.ShardsRegistered(keys); + gateway.setShards(keys); + + // set a shard which is already registered and verify that is does not emit a event. + vm.prank(ADMIN, ADMIN); + vm.recordLogs(); + gateway.setShard(keys[0]); + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 0); + + // Revoke a registered shard thats not registered. + uint256 unregisteredSignerKey = 11; + signer = TestUtils.signerFromEntropy(bytes32(unregisteredSignerKey)); + TssKey memory nonRegisteredKey = TssKey({yParity: signer.yParity() == 28 ? 3 : 2, xCoord: signer.xCoord()}); + vm.prank(ADMIN, ADMIN); + vm.recordLogs(); + gateway.revokeShard(nonRegisteredKey); + Vm.Log[] memory entries1 = vm.getRecordedLogs(); + assertEq(entries1.length, 0); + + // Revoke a registered shard + vm.prank(ADMIN, ADMIN); + TssKey[] memory unregisteredShardKey = new TssKey[](1); + unregisteredShardKey[0] = keys[0]; + vm.expectEmit(false, false, false, true); + emit IExecutor.ShardsUnregistered(unregisteredShardKey); + gateway.revokeShard(keys[0]); + + // Register a revoked shard + vm.prank(ADMIN, ADMIN); + vm.expectEmit(false, false, false, true); + emit IExecutor.ShardsRegistered(unregisteredShardKey); + gateway.setShard(unregisteredShardKey[0]); + + // Revoke half of the keys and verify event length + vm.prank(ADMIN, ADMIN); + uint256 halfKeysLength = keys.length / 2; + uint256 secondHalfLength = keys.length - halfKeysLength; + TssKey[] memory firstHalf = new TssKey[](halfKeysLength); + TssKey[] memory secondHalf = new TssKey[](secondHalfLength); + for (uint256 i = 0; i < keys.length; i++) { + if (i < halfKeysLength) { + firstHalf[i] = keys[i]; + } else { + secondHalf[i - halfKeysLength] = keys[i]; + } + } + vm.expectEmit(false, false, false, true); + emit IExecutor.ShardsUnregistered(firstHalf); + gateway.revokeShards(firstHalf); + + // register first half keys and check if the other half is unregistered + vm.prank(ADMIN, ADMIN); + vm.expectEmit(false, false, false, true); + emit IExecutor.ShardsRegistered(firstHalf); + emit IExecutor.ShardsUnregistered(secondHalf); + gateway.setShards(firstHalf); + } + + function test_Receiver() external { + bytes memory testEncodedCall = abi.encodeCall( + IGmpReceiver.onGmpReceived, + ( + 0x0000000000000000000000000000000000000000000000000000000000000000, + 1, + 0x0000000000000000000000000000000000000000000000000000000000000000, + abi.encode(uint256(1234)) + ) + ); + // Calling the receiver contract directly to make the address warm + address sender = TestUtils.createTestAccount(10 ether); + (uint256 gasUsed,, bytes memory output) = + TestUtils.executeCall(sender, address(receiver), 23_318, 0, testEncodedCall); + assertEq(gasUsed, 1234); + assertEq(output.length, 32); + } + + function test_estimateMessageCost() external { + vm.txGasPrice(1); + uint256 cost = gateway.estimateMessageCost(DEST_NETWORK_ID, 96, 100000); + assertEq(cost, GasUtils.EXECUTION_BASE_COST + 133821); + } + + function test_checkPayloadSize() external { + vm.txGasPrice(1); + address sender = TestUtils.createTestAccount(100 ether); + + // Build and sign GMP message + GmpMessage memory gmp = GmpMessage({ + source: sender.toSender(false), + srcNetwork: SRC_NETWORK_ID, + dest: address(bytes20(keccak256("dummy_address"))), + destNetwork: DEST_NETWORK_ID, + gasLimit: 0, + nonce: 0, + data: new bytes(24576 + 1) + }); + + Signature memory sig = sign(gmp); + + // Calculate memory expansion cost and base cost + (uint256 baseCost, uint256 executionCost) = GatewayUtils.computeGmpGasCost(sig, gmp); + + // Transaction Parameters + CallOptions memory ctx = CallOptions({ + from: sender, + to: address(gateway), + value: 0, + gasLimit: GasUtils.executionGasNeeded(gmp.data.length, gmp.gasLimit) + baseCost + 1_000_000, + executionCost: 0, + baseCost: 0 + }); + + GmpStatus status; + bytes32 returned; + + // Expect a revert + vm.expectRevert("msg data too large"); + (status, returned) = ctx.execute(sig, gmp); + assertLt(ctx.executionCost, executionCost, "revert should use less gas!!"); + assertEq(ctx.baseCost, baseCost, "unexpected base cost"); + } + + /** + * @dev Test the gas metering for the `execute` function. + */ + function test_gasMeter(uint16 messageSize) external { + vm.assume(messageSize <= 0x6000 && messageSize >= 32); + vm.txGasPrice(1); + address sender = TestUtils.createTestAccount(100 ether); + + // Build and sign GMP message + GmpMessage memory gmp = GmpMessage({ + source: sender.toSender(false), + srcNetwork: SRC_NETWORK_ID, + dest: address(receiver), + destNetwork: DEST_NETWORK_ID, + gasLimit: 1000, + nonce: 0, + data: new bytes(messageSize) + }); + { + bytes memory gmpData = gmp.data; + assembly { + mstore(add(gmpData, 0x20), 1000) + } + } + Signature memory sig = sign(gmp); + + // Calculate memory expansion cost and base cost + (uint256 baseCost, uint256 executionCost) = GatewayUtils.computeGmpGasCost(sig, gmp); + executionCost += gmp.gasLimit; + + // Transaction Parameters + CallOptions memory ctx = CallOptions({ + from: sender, + to: address(gateway), + value: 0, + gasLimit: GasUtils.executionGasNeeded(gmp.data.length, gmp.gasLimit) + baseCost - 1, + executionCost: 0, + baseCost: 0 + }); + + GmpStatus status; + bytes32 returned; + + // Expect a revert + vm.expectRevert(); + (status, returned) = ctx.execute(sig, gmp); + + // Check if the gateway has enough balance to refund the gas + uint256 gatewayBalance = address(gateway).balance; + uint256 senderBalance = address(sender).balance; + assertGe(gatewayBalance, executionCost + baseCost); + assertGe(senderBalance, ctx.gasLimit + ctx.value); + + // Give sufficient gas + ctx.gasLimit += 1; + ctx.executionCost = 0; + ctx.baseCost = 0; + (status, returned) = ctx.execute(sig, gmp); + + assertEq(uint256(status), uint256(GmpStatus.SUCCESS), "gmp execution failed"); + assertEq(uint256(returned), gmp.gasLimit, "wrong gmp return value"); + assertEq(ctx.baseCost, baseCost, "ctx.baseCost != baseCost"); + assertEq(ctx.executionCost, executionCost, "ctx.executionCost != executionCost"); + assertEq(gatewayBalance - address(gateway).balance, executionCost + baseCost, "wrong refund amount"); + assertEq(senderBalance, address(sender).balance, "sender balance should not change"); + assertEq( + ctx.gasLimit - baseCost, GasUtils.executionGasNeeded(gmp.data.length, gmp.gasLimit), "gas needed mismatch" + ); + + // Submit GMP message + { + // Calculate the minimal gmp value minus one + uint256 nonZeros = GasUtils.countNonZeros(gmp.data); + uint256 zeros = gmp.data.length - nonZeros; + ctx.value = GasUtils.estimateGas(uint16(nonZeros), uint16(zeros), gmp.gasLimit) - 1; + + // Add sufficient gas + ctx.gasLimit += gmp.data.length * 8; + } + + // Must revert if fund are insufficient + vm.expectRevert("insufficient tx value"); + ctx.submitMessage(gmp); + + { + bytes memory submitEncoded = + abi.encodeCall(IGateway.submitMessage, (gmp.dest, gmp.destNetwork, gmp.gasLimit, gmp.data)); + assertEq(submitEncoded.length, ((gmp.data.length + 31) & 0xffe0) + 164, "wrong encoded length"); + } + + // Must work if the funds are sufficient + ctx.value += 1; + ctx.submitMessage(gmp); + + assertEq( + ctx.executionCost, + GasUtils.submitMessageGasCost(uint16(gmp.data.length)) - 4500 + 17100, + "unexpected submit message gas cost" + ); + } + + function test_submitMessageMeter(uint16 messageSize) external { + vm.assume(messageSize <= 0x6000); + vm.txGasPrice(1); + address sender = TestUtils.createTestAccount(1000 ether); + + // Build and sign GMP message + GmpMessage memory gmp = GmpMessage({ + source: sender.toSender(false), + srcNetwork: DEST_NETWORK_ID, + dest: address(bytes20(keccak256("dummy_address"))), + destNetwork: DEST_NETWORK_ID, + gasLimit: 0, + nonce: 0, + data: new bytes(messageSize) + }); + + // Calculate memory expansion cost and base cost + uint256 baseCost; + { + bytes memory encoded = + abi.encodeCall(IGateway.submitMessage, (gmp.dest, gmp.destNetwork, gmp.gasLimit, gmp.data)); + assertEq(encoded.length, ((gmp.data.length + 31) & 0xffe0) + 164, "wrong encoded length"); + baseCost = TestUtils.calculateBaseCost(encoded); + } + + // Transaction Parameters + CallOptions memory ctx = CallOptions({ + from: sender, + to: address(gateway), + value: 0, + gasLimit: GasUtils.submitMessageGasNeeded(uint16(gmp.data.length)) + baseCost, + executionCost: 0, + baseCost: 0 + }); + + // Submit the transaction + { + uint256 nonZeros = GasUtils.countNonZeros(gmp.data); + uint256 zeros = gmp.data.length - nonZeros; + ctx.value = GasUtils.estimateGas(uint16(nonZeros), uint16(zeros), gmp.gasLimit); + } + + uint256 snapshot = vm.snapshotState(); + // Must work if the funds and gas limit are sufficient + bytes32 id = gmp.eip712hash(); + vm.expectEmit(true, true, true, true); + emit IGateway.GmpCreated( + id, + GmpSender.unwrap(gmp.source), + gmp.dest, + gmp.destNetwork, + uint64(gmp.gasLimit), + uint64(ctx.value), + gmp.nonce, + gmp.data + ); + console.log("expect: ", ctx.value); + ctx.gasLimit += 17100; + assertEq(ctx.submitMessage(gmp), id, "unexpected GMP id"); + + // Verify the execution cost + assertEq( + ctx.executionCost, + GasUtils.submitMessageGasCost(uint16(gmp.data.length)) + 17100, + "unexpected submit message gas cost" + ); + + // Must revert if fund are insufficient + vm.revertToState(snapshot); + ctx.value -= 1; + vm.expectRevert("insufficient tx value"); + ctx.submitMessage(gmp); + } + + function test_refund() external { + vm.txGasPrice(1); + GmpSender sender = TestUtils.createTestAccount(100 ether).toSender(false); + + // GMP message gas used + uint64 gmpGasUsed = 2_000; + + // Build and sign GMP message + GmpMessage memory gmp = GmpMessage({ + source: sender, + srcNetwork: SRC_NETWORK_ID, + dest: address(receiver), + destNetwork: DEST_NETWORK_ID, + gasLimit: gmpGasUsed, + nonce: 1, + data: abi.encodePacked(uint256(gmpGasUsed)) + }); + Signature memory sig = sign(gmp); + + // Estimate execution cost + (uint256 baseCost, uint256 executionCost) = GatewayUtils.computeGmpGasCost(sig, gmp); + uint256 expectGasUsed = baseCost + executionCost + gmp.gasLimit; + + // Execute GMP message + uint256 beforeBalance = sender.toAddress().balance; + { + CallOptions memory ctx = CallOptions({ + from: sender.toAddress(), + to: address(gateway), + value: 0, + gasLimit: GasUtils.executionGasNeeded(gmp.data.length, gmp.gasLimit) + baseCost, + executionCost: 0, + baseCost: 0 + }); + (GmpStatus status, bytes32 returned) = ctx.execute(sig, gmp); + { + // Verify the gas cost + VmSafe.Gas memory gas = vm.lastCallGas(); + assertEq(gas.gasTotalUsed, executionCost + 2000, "unexpected gas used"); + } + + // Verify the GMP message status + assertEq(uint256(status), uint256(GmpStatus.SUCCESS), "Unexpected GMP status"); + Gateway.GmpInfo memory info = gateway.gmpInfo(gmp.eip712hash()); + assertEq( + uint256(info.status), uint256(GmpStatus.SUCCESS), "GMP status stored doesn't match the returned status" + ); + assertEq(returned, bytes32(uint256(gmp.gasLimit)), "unexpected GMP result"); + + // Verify the gas cost + assertEq(ctx.executionCost + ctx.baseCost, expectGasUsed, "unexpected gas used"); + assertEq(ctx.executionCost, executionCost + gmp.gasLimit, "unexpected execution cost"); + } + + // Verify the gas refund + uint256 afterBalance = sender.toAddress().balance; + assertEq(beforeBalance, afterBalance, "wrong refund amount"); + } + + function test_ExecuteRevertsWrongNetwork() external { + vm.txGasPrice(1); + uint256 amount = 10 ether; + address sender = TestUtils.createTestAccount(amount * 2); + + GmpMessage memory wrongNetwork = GmpMessage({ + source: sender.toSender(false), + srcNetwork: SRC_NETWORK_ID, + dest: address(0x0), + destNetwork: SRC_NETWORK_ID, + gasLimit: 1000, + nonce: 1, + data: "" + }); + Signature memory wrongNetworkSig = sign(wrongNetwork); + CallOptions memory ctx = CallOptions({ + from: sender, + to: address(gateway), + value: 0, + gasLimit: 1_000_000, + executionCost: 0, + baseCost: 0 + }); + vm.expectRevert("invalid gmp network"); + ctx.execute(wrongNetworkSig, wrongNetwork); + } + + function test_ExecuteRevertsBelowGasLimit() external { + vm.txGasPrice(1); + GmpSender sender = TestUtils.createTestAccount(100 ether).toSender(false); + GmpMessage memory gmp = GmpMessage({ + source: sender, + srcNetwork: SRC_NETWORK_ID, + dest: address(receiver), + destNetwork: DEST_NETWORK_ID, + gasLimit: 100_000, + nonce: 1, + data: abi.encode(uint256(100_000)) + }); + Signature memory sig = sign(gmp); + + // Deposit funds + (uint256 baseCost, uint256 executionCost) = GatewayUtils.computeGmpGasCost(sig, gmp); + + // Execute GMP message + CallOptions memory ctx = CallOptions({ + from: sender.toAddress(), + to: address(gateway), + value: 0, + gasLimit: baseCost + executionCost, + executionCost: 0, + baseCost: 0 + }); + vm.expectRevert("insufficient gas to execute GMP message"); + ctx.execute(sig, gmp); + } + + function test_executeRevertsAlreadyExecuted() external { + vm.txGasPrice(1); + GmpSender sender = TestUtils.createTestAccount(1000 ether).toSender(false); + GmpMessage memory gmp = GmpMessage({ + source: sender, + srcNetwork: SRC_NETWORK_ID, + dest: address(receiver), + destNetwork: DEST_NETWORK_ID, + gasLimit: 1000, + nonce: 1, + data: abi.encode(uint256(1000)) + }); + Signature memory sig = sign(gmp); + + // Execute GMP message first time + CallOptions memory ctx = CallOptions({ + from: sender.toAddress(), + to: address(gateway), + value: 0, + gasLimit: 1_000_000, + executionCost: 0, + baseCost: 0 + }); + (GmpStatus status, bytes32 result) = ctx.execute(sig, gmp); + assertEq(uint256(status), uint256(GmpStatus.SUCCESS), "unexpected GMP status"); + assertEq(gmp.gasLimit, uint256(result), "unexpected GMP result"); + + // Execute GMP message second time + vm.expectRevert("message already executed"); + ctx.execute(sig, gmp); + } + + function test_submitGmpMessage() external { + vm.txGasPrice(1); + GmpSender gmpSender = TestUtils.createTestAccount(1000 ether).toSender(false); + GmpMessage memory gmp = GmpMessage({ + source: gmpSender, + srcNetwork: DEST_NETWORK_ID, + dest: address(receiver), + destNetwork: DEST_NETWORK_ID, + gasLimit: 100_000, + nonce: 0, + data: abi.encodePacked(uint256(100_000)) + }); + bytes32 id = gmp.eip712hash(); + + // Check the previous message hash + assertEq(gateway.nonceOf(gmp.source.toAddress()), 0, "wrong previous message hash"); + + CallOptions memory ctx = CallOptions({ + from: gmpSender.toAddress(), + to: address(gateway), + value: 0, + gasLimit: 1_000_000, + executionCost: 0, + baseCost: 0 + }); + + // Compute GMP message price + { + uint16 nonZeros = uint16(GasUtils.countNonZeros(gmp.data)); + uint16 zeros = uint16(gmp.data.length) - nonZeros; + ctx.value = GasUtils.estimateWeiCost(UFloatMath.ONE, 0, nonZeros, zeros, gmp.gasLimit); + } + + // Submit message with insufficient funds + ctx.value -= 1; + vm.expectRevert("insufficient tx value"); + ctx.submitMessage(gmp); + + // Submit message with sufficient funds + ctx.value += 1; + vm.expectEmit(true, true, true, true); + emit IGateway.GmpCreated( + id, + GmpSender.unwrap(gmp.source), + gmp.dest, + gmp.destNetwork, + uint64(gmp.gasLimit), + uint64(ctx.value), + gmp.nonce, + gmp.data + ); + assertEq(ctx.submitMessage(gmp), id, "unexpected GMP id"); + + // Verify the gas cost + uint256 expectedCost = GasUtils.submitMessageGasCost(uint16(gmp.data.length)) - 6500; + assertEq(ctx.executionCost, expectedCost + 17100, "unexpected execution gas cost in first call"); + + // Now the second GMP message nonce must be equals to previous message nonce + 1. + gmp.nonce = gateway.nonceOf(gmp.source.toAddress()); + id = gmp.eip712hash(); + + // Expect event + vm.expectEmit(true, true, true, true); + emit IGateway.GmpCreated( + id, + GmpSender.unwrap(gmp.source), + gmp.dest, + gmp.destNetwork, + uint64(gmp.gasLimit), + uint64(ctx.value), + gmp.nonce, + gmp.data + ); + assertEq(ctx.submitMessage(gmp), id, "unexpected GMP id"); + assertEq(ctx.executionCost, expectedCost - 6800, "unexpected execution gas cost in second call"); + } +} diff --git a/analog-gmp/test/GatewayProxy.t.sol b/analog-gmp/test/GatewayProxy.t.sol new file mode 100644 index 000000000..66e904997 --- /dev/null +++ b/analog-gmp/test/GatewayProxy.t.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (test/Gateway.t.sol) + +pragma solidity >=0.8.0; + +import {IUniversalFactory} from "@universal-factory/IUniversalFactory.sol"; +import {FactoryUtils} from "@universal-factory/FactoryUtils.sol"; +import {Test, console} from "forge-std/Test.sol"; +import {VmSafe} from "forge-std/Vm.sol"; +import {TestUtils, SigningKey, SigningUtils} from "./TestUtils.sol"; +import {GasSpender} from "./utils/GasSpender.sol"; +import {Gateway, GatewayEIP712} from "../src/Gateway.sol"; +import {GatewayProxy} from "../src/GatewayProxy.sol"; +import {GasUtils} from "../src/utils/GasUtils.sol"; +import {BranchlessMath} from "../src/utils/BranchlessMath.sol"; +import {UFloat9x56, UFloatMath} from "../src/utils/Float9x56.sol"; +import {IGateway} from "../src/interfaces/IGateway.sol"; +import {IGmpReceiver} from "../src/interfaces/IGmpReceiver.sol"; +import {IExecutor} from "../src/interfaces/IExecutor.sol"; +import { + GmpMessage, + UpdateKeysMessage, + Signature, + TssKey, + Network, + GmpStatus, + PrimitiveUtils, + GmpSender +} from "../src/Primitives.sol"; + +contract GatewayProxyTest is Test { + using PrimitiveUtils for UpdateKeysMessage; + using PrimitiveUtils for GmpMessage; + using PrimitiveUtils for GmpSender; + using PrimitiveUtils for address; + using BranchlessMath for uint256; + using SigningUtils for SigningKey; + using FactoryUtils for IUniversalFactory; + + /** + * @dev The address of the `UniversalFactory` contract, must be the same on all networks. + */ + IUniversalFactory private constant FACTORY = IUniversalFactory(0x0000000000001C4Bf962dF86e38F0c10c7972C6E); + // VmSafe.Wallet private proxyAdmin; + // Gateway private gateway; + + // Chronicle TSS Secret + // uint256 private constant ADMIN_SECRET = 0x42; + uint256 private constant SIGNING_NONCE = 0x69; + + // Route IDS + uint16 private constant SRC_NETWORK_ID = 1234; + uint16 private constant DEST_NETWORK_ID = 1337; + + /** + * @dev his is a special contract that implements the IGmpReceiver interface and wastes an exact amount of gas you send to it, helpful + * for testing GMP refunds and gas limits. + * See the file `GasSpender` contract for more details. + */ + IGmpReceiver internal receiver; + + constructor() { + require(FACTORY == TestUtils.deployFactory(), "factory address mismatch"); + receiver = IGmpReceiver(new GasSpender()); + } + + function setUp() external view { + // check block gas limit as gas left + assertEq(block.gaslimit, 30_000_000); + assertTrue(gasleft() >= 10_000_000); + } + + function _setup(VmSafe.Wallet memory admin, bytes32 salt, uint16 routeID) private returns (Gateway gateway) { + /////////////////////////////////////////// + // 1. Deploy the implementation contract // + /////////////////////////////////////////// + // 1.1 Compute the `GatewayProxy` address + bytes memory proxyCreationCode = abi.encodePacked(type(GatewayProxy).creationCode, abi.encode(admin.addr)); + address proxyAddr = FACTORY.computeCreate2Address(salt, proxyCreationCode); + + // 1.2 Deploy the `Gateway` implementation contract + bytes memory implementationCreationCode = + abi.encodePacked(type(Gateway).creationCode, abi.encode(routeID, proxyAddr)); + address payable implementation = payable(FACTORY.create2(salt, implementationCreationCode, abi.encode(routeID))); + assertEq(Gateway(implementation).networkId(), routeID); + + //////////////////////////////////////////////////////// + // 2. ProxyAdmin approves the implementation contract // + //////////////////////////////////////////////////////// + bytes memory authorization; + { + // This allows anyone to deploy the Proxy. + bytes32 digest = keccak256(abi.encode(proxyAddr, address(implementation))); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(admin.privateKey, digest); + authorization = abi.encode(v, r, s, address(implementation)); + } + + //////////////////////////////////////////////////////////////// + // 3 - Deploy the `GatewayProxy` using the `UniversalFactory` // + //////////////////////////////////////////////////////////////// + SigningKey memory signer = TestUtils.createSigner(admin.privateKey); + TssKey[] memory keys = new TssKey[](1); + keys[0] = TssKey({yParity: signer.yParity() == 28 ? 1 : 0, xCoord: signer.xCoord()}); // Shard key + Network[] memory networks = new Network[](2); + networks[0].id = SRC_NETWORK_ID; // sepolia network id + networks[0].gateway = proxyAddr; // sepolia proxy address + networks[1].id = DEST_NETWORK_ID; // shibuya network id + networks[1].gateway = proxyAddr; // shibuya proxy address + + // Initializer, used to initialize the Gateway contract + bytes memory initializer = abi.encodeCall(Gateway.initialize, (admin.addr, keys, networks)); + address payable gatewayAddr = payable(FACTORY.create2(salt, proxyCreationCode, authorization, initializer)); + gateway = Gateway(gatewayAddr); + + // Send funds to the gateway contract + vm.deal(address(gateway), 100 ether); + } + + function test_deployProxy() external { + VmSafe.Wallet memory admin = vm.createWallet(vm.randomUint()); + TestUtils.setupGateway(admin, bytes32(uint256(1234)), SRC_NETWORK_ID, DEST_NETWORK_ID); + } +} diff --git a/analog-gmp/test/GmpTestTools.sol b/analog-gmp/test/GmpTestTools.sol new file mode 100644 index 000000000..eded8415e --- /dev/null +++ b/analog-gmp/test/GmpTestTools.sol @@ -0,0 +1,324 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (test/GmpTestTools.sol) + +pragma solidity >=0.8.0; + +import {VmSafe, Vm} from "forge-std/Vm.sol"; +import {TestUtils, SigningKey, SigningUtils} from "./TestUtils.sol"; +import {Random} from "./Random.sol"; +import {Gateway} from "../src/Gateway.sol"; +import {GatewayProxy} from "../src/GatewayProxy.sol"; +import {IGateway} from "../src/interfaces/IGateway.sol"; +import {BranchlessMath} from "../src/utils/BranchlessMath.sol"; +import {GmpMessage, TssKey, Network, Signature, GmpSender, PrimitiveUtils} from "../src/Primitives.sol"; +import {IUniversalFactory} from "@universal-factory/IUniversalFactory.sol"; + +library GmpTestTools { + /** + * @dev Forge Cheat Code VM address, 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D. + */ + address private constant VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code")))); + Vm private constant vm = Vm(VM_ADDRESS); + + // Sepolia Properties + Gateway internal constant SEPOLIA_GATEWAY = Gateway(payable(0x000000007f56768De3133034fa730a909003A166)); + uint16 internal constant SEPOLIA_NETWORK_ID = 5; + bytes32 internal constant SEPOLIA_SHARD_SECRET = keccak256("analog.sepolia.shard.secret"); + bytes32 internal constant SEPOLIA_DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256("Analog Gateway Contract"), + keccak256("0.1.0"), + uint256(SEPOLIA_NETWORK_ID), + address(SEPOLIA_GATEWAY) + ) + ); + + // Shibuya Properties + Gateway internal constant SHIBUYA_GATEWAY = Gateway(payable(0x000000007f56768DE3133034fA730A909003a167)); + uint16 internal constant SHIBUYA_NETWORK_ID = 7; + bytes32 internal constant SHIBUYA_SHARD_SECRET = keccak256("analog.shibuya.shard.secret"); + bytes32 internal constant SHIBUYA_DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256("Analog Gateway Contract"), + keccak256("0.1.0"), + uint256(SHIBUYA_NETWORK_ID), + address(SHIBUYA_GATEWAY) + ) + ); + + /** + * @dev Minimal Eip1667 proxy bytecode. + */ + bytes private constant _PROXY_BYTECODE = + hex"363d3d373d3d3d363d7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d82803e903d91603857fd5bf3"; + + /** + * @dev Storage slot with the address of the current implementation. + * This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1. + */ + bytes32 private constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + + /** + * @dev Log index storage slot. + * This prevents a given message from being execute more than once. + */ + bytes32 private constant _LOG_INDEX_SLOT = bytes32(uint256(keccak256("analog.GmpTestTools.logIndex")) - 1); + + /** + * @dev Mapping of network ID to fork ID. + */ + bytes32 private constant _FORKS_SLOT = bytes32(uint256(keccak256("analog.GmpTestTools.forks")) - 1); + + /** + * @dev Storage slot. + */ + function setup() internal { + require(vm.isPersistent(address(this)), "GmpTestTools must only be called from Test contract"); + // Create forks + uint256 sepoliaForkID = vm.createFork("https://sepolia.infura.io/v3/b9794ad1ddf84dfb8c34d6bb5dca2001", 5714300); + uint256 shibuyaForkID = vm.createFork("https://evm.shibuya.astar.network", 6102790); + + // Save the fork IDs + _storeForkID(SEPOLIA_NETWORK_ID, sepoliaForkID); + _storeForkID(SHIBUYA_NETWORK_ID, shibuyaForkID); + + // Deploy the gateways + Network[] memory networks = new Network[](2); + networks[0] = Network({id: SEPOLIA_NETWORK_ID, gateway: address(SEPOLIA_GATEWAY)}); + networks[1] = Network({id: SHIBUYA_NETWORK_ID, gateway: address(SHIBUYA_GATEWAY)}); + + // Setup the networks + require(switchNetwork(SEPOLIA_NETWORK_ID) == sepoliaForkID, "unexpected sepolia fork id"); + setupNetwork(SEPOLIA_NETWORK_ID, address(SEPOLIA_GATEWAY), SEPOLIA_SHARD_SECRET, networks); + + require(switchNetwork(SHIBUYA_NETWORK_ID) == shibuyaForkID, "unexpected shibuya fork id"); + setupNetwork(SHIBUYA_NETWORK_ID, address(SHIBUYA_GATEWAY), SHIBUYA_SHARD_SECRET, networks); + + // Record logs must be enabled to allow this tool to retrieve the GMP messages + vm.recordLogs(); + } + + function setupNetwork(uint16 networkId, address gateway, bytes32 secret, Network[] memory networks) internal { + // Deploy `Universal Factory` contract + IUniversalFactory factory = TestUtils.deployFactory(); + + SigningKey memory signer = TestUtils.signerFromEntropy(secret); + TssKey[] memory keys = new TssKey[](1); + keys[0] = TssKey({yParity: uint8(signer.pubkey.py % 2), xCoord: signer.pubkey.px}); + + // Check if the gateway is already deployed + bool exists = gateway.code.length > 0; + + // Deploy the gateway proxy + bytes memory implementationCreationCode = abi.encodePacked(type(Gateway).creationCode, abi.encode(gateway)); + + // address implementation = address(new Gateway(networkId, gateway)); + address implementation; + bytes32 salt = bytes32(0); + if (exists) { + implementation = factory.create2(salt, implementationCreationCode, ""); + } else { + implementation = factory.create2(salt, implementationCreationCode, abi.encode(networkId, address(gateway))); + } + vm.etch(gateway, _PROXY_BYTECODE); + vm.store(gateway, _IMPLEMENTATION_SLOT, bytes32(uint256(uint160(implementation)))); + + // If the gateway is already deployed, just register the shard + // This is useful when using forked networks + if (exists) { + bytes32 prevMessageHash = vm.load(gateway, bytes32(uint256(4))); + if (prevMessageHash != bytes32(0)) { + registerShard(gateway, signer); + revert("ALREADY INITIALIZED"); + } + } + + // Change caller mode because only the gateway can initialize itself + (VmSafe.CallerMode callerMode, address msgSender, address txOrigin) = + TestUtils.setCallerMode(VmSafe.CallerMode.Prank, gateway, gateway); + + // Initialize the gateway + Gateway(payable(gateway)).initialize(msgSender, keys, networks); + + // Restore previous caller mode + TestUtils.setCallerMode(callerMode, msgSender, txOrigin); + } + + function deal(address account, uint256 newBalance) internal { + // If the account is persistent, just need to deal once + if (vm.isPersistent(account)) { + vm.deal(account, newBalance); + return; + } + // Select sepolia and execute callback + switchNetwork(SEPOLIA_NETWORK_ID); + vm.deal(account, newBalance); + + // Select shibuya and execute callback + switchNetwork(SHIBUYA_NETWORK_ID); + vm.deal(account, newBalance); + } + + /** + * @dev Execute all pending GMP messages. + */ + function relayMessages() internal { + uint256 activeFork = vm.activeFork(); + GmpMessage[] memory allMessages = messages(); + _executeMessages(SHIBUYA_GATEWAY, SHIBUYA_NETWORK_ID, SHIBUYA_SHARD_SECRET, allMessages); + _executeMessages(SEPOLIA_GATEWAY, SEPOLIA_NETWORK_ID, SEPOLIA_SHARD_SECRET, allMessages); + vm.selectFork(activeFork); + } + + /** + * @dev Switch to `network` fork id. + */ + function switchNetwork(uint16 network) internal returns (uint256 forkId) { + forkId = _loadForkID(network); + require(forkId != uint256(_FORKS_SLOT), "GmpTestTools: network not found"); + vm.selectFork(forkId); + } + + /** + * @dev Switch to the `network` fork id and sets all subsequent calls' `msg.sender` to `msgSender`. + */ + function switchNetwork(uint16 network, address msgSender) internal returns (uint256 forkId) { + forkId = switchNetwork(network); + vm.stopPrank(); + vm.startPrank(msgSender, msgSender); + } + + /** + * @dev Stores the network fork id. + */ + function _storeForkID(uint16 network, uint256 forkId) private { + bytes32 slot = _deriveMapping(_FORKS_SLOT, uint256(network)); + // Once zero is a valid fork id, we XOR before storing to prevent + // an invalid network from returning a valid fork id + _sstoreUint256(slot, forkId ^ uint256(_FORKS_SLOT)); + } + + /** + * @dev Load the fork id of a given `network` + */ + function _loadForkID(uint16 network) private view returns (uint256) { + bytes32 slot = _deriveMapping(_FORKS_SLOT, uint256(network)); + // Once zero is a valid fork id, we XOR the returned result to prevent + // an invalid network from returning a valid fork id + return _sloadUint256(slot) ^ uint256(_FORKS_SLOT); + } + + /** + * @dev Returns the `uint256` located at `slot`. + */ + function _sloadUint256(bytes32 slot) private view returns (uint256 r) { + assembly { + r := sload(slot) + } + } + + /** + * @dev Store `value` at `slot`. + */ + function _sstoreUint256(bytes32 slot, uint256 value) private { + assembly { + sstore(slot, value) + } + } + + /** + * @dev Force register a new shard in the gateway. + */ + function registerShard(address gateway, SigningKey memory shard) internal { + // uint256 shardInfo = 1 | (shard.pubkey.py % 2); + bytes32 slot = _deriveMapping(bytes32(0), shard.pubkey.px); + uint256 shardInfo = uint256(vm.load(gateway, slot)); + uint256 nonce = shardInfo >> 224; + nonce = BranchlessMath.ternary(nonce > 0, nonce, 1); + shardInfo = (nonce << 224) | (1 << 216) | ((shard.pubkey.py % 2) << 217); + vm.store(gateway, slot, bytes32(shardInfo)); + } + + /** + * @dev Derive the location of a mapping element from the key. + */ + function _deriveMapping(bytes32 slot, uint256 key) private pure returns (bytes32 result) { + assembly ("memory-safe") { + mstore(0x00, key) + mstore(0x20, slot) + result := keccak256(0x00, 0x40) + } + } + + /** + * @dev Retrieve all pending messages from the recorded logs + */ + function messages() internal returns (GmpMessage[] memory gmpMessages) { + bytes32[] memory topics = new bytes32[](1); + topics[0] = IGateway.GmpCreated.selector; + Vm.Log[] memory logs = vm.getRecordedLogs(); + uint256 logIndex = _sloadUint256(_LOG_INDEX_SLOT); + gmpMessages = new GmpMessage[](logs.length - logIndex); + uint256 pos = 0; + for (uint256 i = logIndex; i < logs.length; i++) { + Vm.Log memory log = logs[i]; + + // Filter emitters + uint16 srcNetwork; + if (log.emitter == address(SEPOLIA_GATEWAY)) { + srcNetwork = SEPOLIA_NETWORK_ID; + } else if (log.emitter == address(SHIBUYA_GATEWAY)) { + srcNetwork = SHIBUYA_NETWORK_ID; + } else { + continue; + } + + // Filter topics + if (log.topics.length != 4 || log.topics[0] != IGateway.GmpCreated.selector) { + continue; + } + + // Decode the GMP message + (uint16 destNetwork, uint64 gasLimit, uint64 nonce, bytes memory data) = + abi.decode(log.data, (uint16, uint64, uint64, bytes)); + gmpMessages[pos++] = GmpMessage({ + source: GmpSender.wrap(log.topics[2]), + srcNetwork: srcNetwork, + dest: address(uint160(uint256(log.topics[3]))), + destNetwork: destNetwork, + gasLimit: gasLimit, + nonce: nonce, + data: data + }); + } + _sstoreUint256(_LOG_INDEX_SLOT, logs.length); + } + + function _executeMessages(Gateway gateway, uint16 network, bytes32 secret, GmpMessage[] memory gmpMessages) + private + { + switchNetwork(network); + SigningKey memory signer = TestUtils.signerFromEntropy(secret); + + for (uint256 i = 0; i < gmpMessages.length; i++) { + GmpMessage memory message = gmpMessages[i]; + + // Compute the message ID + bytes32 messageID = PrimitiveUtils.eip712hash(message); + + // Skip if the message is not intended for this network + if (message.destNetwork != network) { + continue; + } + + // Sign the message + (uint256 c, uint256 z) = SigningUtils.signPrehashed(signer, messageID, Random.nextUint()); + Signature memory signature = Signature({xCoord: signer.pubkey.px, e: c, s: z}); + + // Execute the message + gateway.execute(signature, message); + } + } +} diff --git a/analog-gmp/test/GmpTestTools.t.sol b/analog-gmp/test/GmpTestTools.t.sol new file mode 100644 index 000000000..7cd96cc0c --- /dev/null +++ b/analog-gmp/test/GmpTestTools.t.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (test/GmpTestTools.t.sol) + +pragma solidity >=0.8.0; + +import {Test} from "forge-std/Test.sol"; +import {MockERC20} from "./MockERC20.sol"; +import {TestUtils} from "./TestUtils.sol"; +import {GmpTestTools} from "./GmpTestTools.sol"; +import {Gateway, GatewayEIP712} from "../src/Gateway.sol"; +import {GatewayProxy} from "../src/GatewayProxy.sol"; +import {IGateway} from "../src/interfaces/IGateway.sol"; +import {IExecutor} from "../src/interfaces/IExecutor.sol"; +import {GmpMessage, GmpStatus, GmpSender, PrimitiveUtils} from "../src/Primitives.sol"; + +contract GmpTestToolsTest is Test { + using PrimitiveUtils for GmpMessage; + using PrimitiveUtils for GmpSender; + using PrimitiveUtils for address; + + address private constant ALICE = address(bytes20(keccak256("Alice"))); + address private constant BOB = address(bytes20(keccak256("Bob"))); + + Gateway private constant SEPOLIA_GATEWAY = Gateway(GmpTestTools.SEPOLIA_GATEWAY); + uint16 private constant SEPOLIA_NETWORK = GmpTestTools.SEPOLIA_NETWORK_ID; + + Gateway private constant SHIBUYA_GATEWAY = Gateway(GmpTestTools.SHIBUYA_GATEWAY); + uint16 private constant SHIBUYA_NETWORK = GmpTestTools.SHIBUYA_NETWORK_ID; + + /// @dev Test the teleport of tokens from Alice's account in Shibuya to Bob's account in Sepolia + function testTeleportAliceTokens() external { + //////////////////////////////////// + // Step 1: Setup test environment // + //////////////////////////////////// + + if (msg.data.length > 0) { + return; + } + + // Deploy the gateway contracts at pre-defined addresses + // Also creates one fork for each supported network + GmpTestTools.setup(); + + // Add funds to Alice and Bob in all networks + GmpTestTools.deal(ALICE, 100 ether); + GmpTestTools.deal(BOB, 100 ether); + + /////////////////////////////////////////////////////// + // Step 2: Deploy the sender and recipient contracts // + /////////////////////////////////////////////////////// + + // Pre-compute the contract addresses, because the contracts must know each other addresses. + MockERC20 shibuyaErc20 = MockERC20(vm.computeCreateAddress(ALICE, vm.getNonce(ALICE))); + MockERC20 sepoliaErc20 = MockERC20(vm.computeCreateAddress(BOB, vm.getNonce(BOB))); + + // Switch to Shibuya network and deploy the ERC20 using Alice account + GmpTestTools.switchNetwork(SHIBUYA_NETWORK, ALICE); + shibuyaErc20 = new MockERC20("Shibuya ", "A", SHIBUYA_GATEWAY, sepoliaErc20, SEPOLIA_NETWORK, ALICE, 1000); + assertEq(shibuyaErc20.balanceOf(ALICE), 1000, "unexpected alice balance in shibuya"); + assertEq(shibuyaErc20.balanceOf(BOB), 0, "unexpected bob balance in shibuya"); + + // Switch to Sepolia network and deploy the ERC20 using Bob account + GmpTestTools.switchNetwork(SEPOLIA_NETWORK, BOB); + sepoliaErc20 = new MockERC20("Sepolia", "B", SEPOLIA_GATEWAY, shibuyaErc20, SHIBUYA_NETWORK, BOB, 0); + assertEq(sepoliaErc20.balanceOf(ALICE), 0, "unexpected alice balance in sepolia"); + assertEq(sepoliaErc20.balanceOf(BOB), 0, "unexpected bob balance in sepolia"); + + // Check if the computed addresses matches + assertEq(address(shibuyaErc20), vm.computeCreateAddress(ALICE, 0), "unexpected shibuyaErc20 address"); + assertEq(address(sepoliaErc20), vm.computeCreateAddress(BOB, 0), "unexpected sepoliaErc20 address"); + + /////////////////////////////////////////////////////////// + // Step 3: Deposit funds to destination Gateway Contract // + /////////////////////////////////////////////////////////// + + // Switch to Sepolia network and Alice account + GmpTestTools.switchNetwork(SEPOLIA_NETWORK, ALICE); + + ////////////////////////////// + // Step 4: Send GMP message // + ////////////////////////////// + + // Switch to Shibuya network and Alice account + GmpTestTools.switchNetwork(SHIBUYA_NETWORK, ALICE); + + // Teleport 100 tokens from Alice to to Bob's account in sepolia + // Obs: The `teleport` method internally calls `gateway.submitMessage(...)` + bytes32 messageID; + { + // Estimate the cost of teleporting 100 tokens + uint256 gmpCost = shibuyaErc20.teleportCost(); + messageID = shibuyaErc20.teleport{value: gmpCost}(BOB, 100); + } + + // Now with the `messageID`, Alice can check the message status in the destination gateway contract + // status 0: means the message is pending + // status 1: means the message was executed successfully + // status 2: means the message was executed but reverted + GmpTestTools.switchNetwork(SEPOLIA_NETWORK, ALICE); + assertTrue( + SEPOLIA_GATEWAY.gmpInfo(messageID).status == GmpStatus.NOT_FOUND, + "unexpected message status, expect 'pending'" + ); + + /////////////////////////////////////////////////// + // Step 5: Wait Chronicles Relay the GMP message // + /////////////////////////////////////////////////// + + // The GMP hasn't been executed yet... + assertEq(sepoliaErc20.balanceOf(ALICE), 0, "unexpected alice balance in shibuya"); + + // Note: In a live network, the GMP message will be relayed by Chronicle Nodes after a minimum number of confirmations. + // here we can simulate this behavior by calling `GmpTestTools.relayMessages()`, this will relay all pending messages. + GmpTestTools.relayMessages(); + + // Success! The GMP message was executed!!! + assertTrue(SEPOLIA_GATEWAY.gmpInfo(messageID).status == GmpStatus.SUCCESS, "failed to execute GMP"); + + // Check ALICE and BOB balance in shibuya + GmpTestTools.switchNetwork(SHIBUYA_NETWORK); + assertEq(shibuyaErc20.balanceOf(ALICE), 900, "unexpected alice's balance in shibuya"); + assertEq(shibuyaErc20.balanceOf(BOB), 0, "unexpected bob's balance in shibuya"); + + // Check ALICE and BOB balance in sepolia + GmpTestTools.switchNetwork(SEPOLIA_NETWORK); + assertEq(sepoliaErc20.balanceOf(ALICE), 0, "unexpected alice's balance in sepolia"); + assertEq(sepoliaErc20.balanceOf(BOB), 100, "unexpected bob's balance in sepolia"); + } +} diff --git a/analog-gmp/test/MockERC20.sol b/analog-gmp/test/MockERC20.sol new file mode 100644 index 000000000..d7c39b246 --- /dev/null +++ b/analog-gmp/test/MockERC20.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (test/MockERC20.sol) + +pragma solidity >=0.8.0; + +import {ERC20} from "@solmate/tokens/ERC20.sol"; +import {IGmpReceiver} from "../src/interfaces/IGmpReceiver.sol"; +import {IGateway} from "../src/interfaces/IGateway.sol"; + +contract MockERC20 is ERC20, IGmpReceiver { + IGateway private immutable _gateway; + MockERC20 private immutable _recipientErc20; + uint16 private immutable _recipientNetwork; + + // Gas limit used to execute `onGmpReceived` method. + uint256 private constant MSG_GAS_LIMIT = 100_000; + + /** + * @dev Struct to represent a cross-chain transfer message. + * @param from The sender address. + * @param to The recipient address. + * @param amount The amount of tokens to teleport. + */ + struct CrossChainTransfer { + address from; + address to; + uint256 amount; + } + + constructor( + string memory name, + string memory symbol, + IGateway gatewayAddress, + MockERC20 recipient, + uint16 recipientNetwork, + address holder, + uint256 initialSupply + ) ERC20(name, symbol, 10) { + _gateway = gatewayAddress; + _recipientErc20 = recipient; + _recipientNetwork = recipientNetwork; + if (initialSupply > 0) { + _mint(holder, initialSupply); + } + } + + /** + * @dev Estimate the cost of teleporting tokens to another network. + */ + function teleportCost() external view returns (uint256) { + // Estimate the cost + return _gateway.estimateMessageCost(_recipientNetwork, 96, MSG_GAS_LIMIT); + } + + /** + * @dev Teleport tokens to from this contract to another contract on a different network. + * IMPORTANT: the caller is responsible to compute the teleport cost and send the required amount of ETH. + * The teleport cost can be computed using the `teleportCost` method. + * + * @param to The recipient address on the destination network. + * @param amount The amount of tokens to teleport. + */ + function teleport(address to, uint256 amount) external payable returns (bytes32) { + // Encode the message + bytes memory message = abi.encode(CrossChainTransfer({from: msg.sender, to: to, amount: amount})); + + // Burn the tokens + _burn(msg.sender, amount); + + // Submit the GMP message + return _gateway.submitMessage{value: msg.value}( + address(_recipientErc20), _recipientNetwork, MSG_GAS_LIMIT, message + ); + } + + function onGmpReceived(bytes32 id, uint128 network, bytes32 sender, bytes calldata data) + external + payable + returns (bytes32) + { + require(msg.sender == address(_gateway), "Unauthorized: only the gateway can call this method"); + require(network == _recipientNetwork, "Unauthorized network"); + require(address(uint160(uint256(sender))) == address(_recipientErc20), "Unauthorized sender"); + CrossChainTransfer memory message = abi.decode(data, (CrossChainTransfer)); + _mint(message.to, message.amount); + return id; + } +} diff --git a/analog-gmp/test/Random.sol b/analog-gmp/test/Random.sol new file mode 100644 index 000000000..6b1403c6a --- /dev/null +++ b/analog-gmp/test/Random.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (test/Random.sol) + +pragma solidity >=0.8.0; + +/** + * @dev Utilities for generating pseudo-random values + */ +library Random { + function _next() private pure returns (uint256 rand) { + assembly ("memory-safe") { + rand := keccak256(0x00, 0x60) + let ptr := mload(0x40) + mstore(0x00, xor(rand, calldataload(0))) + mstore(0x20, xor(rand, calldatasize())) + mstore(0x40, xor(rand, mload(ptr))) + rand := keccak256(0x00, 0x60) + mstore(0x00, rand) + mstore(0x20, rand) + mstore(0x40, ptr) + mstore(ptr, rand) + } + } + + function nextUint() internal pure returns (uint256 rand) { + rand = _next(); + } + + function nextInt() internal pure returns (int256 rand) { + rand = int256(_next()); + } +} diff --git a/analog-gmp/test/TestUtils.sol b/analog-gmp/test/TestUtils.sol new file mode 100644 index 000000000..bbeaf7a07 --- /dev/null +++ b/analog-gmp/test/TestUtils.sol @@ -0,0 +1,542 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (test/TestUtils.sol) + +pragma solidity >=0.8.0; + +import {VmSafe, Vm} from "forge-std/Vm.sol"; +import {console} from "forge-std/console.sol"; +import {Schnorr} from "@frost-evm/Schnorr.sol"; +import {SECP256K1} from "@frost-evm/SECP256K1.sol"; +import {BranchlessMath} from "../src/utils/BranchlessMath.sol"; +import {IUniversalFactory} from "@universal-factory/IUniversalFactory.sol"; +import {FactoryUtils} from "@universal-factory/FactoryUtils.sol"; +import {IGateway} from "../src/interfaces/IGateway.sol"; +import {Gateway, GatewayEIP712} from "../src/Gateway.sol"; +import {GatewayProxy} from "../src/GatewayProxy.sol"; +import { + GmpMessage, + UpdateKeysMessage, + Signature, + TssKey, + Network, + GmpStatus, + PrimitiveUtils, + GmpSender +} from "../src/Primitives.sol"; + +struct VerifyingKey { + uint256 px; + uint256 py; +} + +struct SigningKey { + uint256 secret; + VerifyingKey pubkey; +} + +/** + * @dev Utilities for testing purposes + */ +library TestUtils { + using BranchlessMath for uint256; + using FactoryUtils for IUniversalFactory; + + // Cheat code address, 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D. + address internal constant VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code")))); + Vm internal constant vm = Vm(VM_ADDRESS); + + /** + * @dev The address of the `UniversalFactory` contract, must be the same on all networks. + */ + address internal constant FACTORY_DEPLOYER = 0x908064dE91a32edaC91393FEc3308E6624b85941; + + /** + * @dev The codehash of the `UniversalFactory` contract, must be the same on all networks. + */ + bytes32 internal constant FACTORY_CODEHASH = 0x0dac89b851eaa2369ef725788f1aa9e2094bc7819f5951e3eeaa28420f202b50; + + /** + * @dev The address of the `UniversalFactory` contract, must be the same on all networks. + */ + IUniversalFactory internal constant FACTORY = IUniversalFactory(0x0000000000001C4Bf962dF86e38F0c10c7972C6E); + + /** + * @dev Deploys a contract with the given bytecode + */ + function deployContract(bytes memory bytecode) internal returns (address addr) { + require(bytecode.length > 0, "Error: deploy code is empty"); + assembly ("memory-safe") { + let ptr := add(bytecode, 32) + let size := mload(bytecode) + addr := create(0, ptr, size) + } + require(addr != address(0), "Error: failed to deploy contract"); + } + + /** + * @dev Delegate call to another contract bytecode + * This execute the code of another contract in the context of the current contract + */ + function delegateCall(address contractAddr, bytes memory data) + internal + returns (bool success, bytes memory output) + { + require(contractAddr.code.length > 0, "Error: provided address is not a contract"); + assembly ("memory-safe") { + success := + delegatecall( + gas(), // call gas limit + contractAddr, // dest address + add(32, data), // input memory pointer + mload(data), // input size + 0, // output memory pointer + 0 // output size + ) + + // Alloc memory for the output + output := mload(0x40) + let ptr := add(output, 32) + let size := returndatasize() + mstore(0x40, add(ptr, size)) // Increment free memory pointer + + // Store return data size + mstore(output, size) + + // Copy delegatecall output to memory + returndatacopy(ptr, 0, size) + } + } + + /** + * @dev Count the number of non-zero bytes in a byte sequence. + * Reference: https://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetParallel + */ + function countNonZeros(bytes memory data) internal pure returns (uint256 nonZeros) { + assembly ("memory-safe") { + // Efficient algorithm for counting non-zero bytes in parallel + nonZeros := 0 + for { + // 32 byte aligned pointer, ex: if data.length is 54, `ptr` starts at 32 + let ptr := add(data, and(add(mload(data), 31), 0xffffffe0)) + } gt(ptr, data) { ptr := sub(ptr, 32) } { + // Normalize + let v := mload(ptr) + v := or(v, shr(4, v)) + v := or(v, shr(2, v)) + v := or(v, shr(1, v)) + v := and(v, 0x0101010101010101010101010101010101010101010101010101010101010101) + + // Count bytes in parallel + v := add(v, shr(128, v)) + v := add(v, shr(64, v)) + v := add(v, shr(32, v)) + v := add(v, shr(16, v)) + v := add(v, shr(8, v)) + v := and(v, 0xff) + nonZeros := add(nonZeros, v) + } + } + } + + /** + * @dev Calculate the tx base cost. + * formula: 21000 + zeros * 4 + nonZeros * 16 + * Reference: https://eips.ethereum.org/EIPS/eip-2028 + */ + function calculateBaseCost(bytes memory txData) internal pure returns (uint256 baseCost) { + uint256 nonZeros = countNonZeros(txData); + uint256 zeros = txData.length - nonZeros; + baseCost = 21_000 + (nonZeros * 16) + (zeros * 4); + } + + /** + * @dev Calculate the tx base cost. + * formula: 21000 + zeros * 4 + nonZeros * 16 + * Reference: https://eips.ethereum.org/EIPS/eip-2028 + */ + function memExpansionCost(uint256 size) internal pure returns (uint256) { + uint256 words = (size + 31) / 32; + return ((words ** 2) / 512) + (words * 3); + } + + /** + * @dev Generate a new account account from the calldata + * This will generate a unique deterministic address for each test case + */ + function createTestAccount(uint256 initialBalance) internal returns (address account) { + // Generate a new account address from the calldata + // This will generate a unique deterministic address for each test case + account = address(uint160(uint256(keccak256(msg.data)))); + vm.deal(account, initialBalance); + } + + /** + * @dev Generate a new account account from the calldata + */ + function createTestAccount() internal returns (address account) { + // Create an account with 100 ether + account = createTestAccount(100 ether); + } + + /** + * @dev Convert an address to GMP bytes32 identifier + */ + function source(address account, bool isContract) internal pure returns (bytes32) { + uint256 contractFlag = isContract ? 1 << 160 : 0; + return bytes32(contractFlag | uint256(uint160(account))); + } + + /** + * @dev Convert an address to GMP bytes32 identifier + */ + function source(address account) internal view returns (bytes32) { + return source(account, account.code.length > 0); + } + + /** + * @dev Creates a new TSS signer + */ + function createSigner(uint256 secret) internal pure returns (SigningKey memory) { + require(secret != 0, "secret must be greater than 0"); + require(secret < Schnorr.Q, "secret must be less than secp256k1 group order"); + (uint256 px, uint256 py) = SECP256K1.publicKey(secret); + return SigningKey({secret: secret, pubkey: VerifyingKey({px: px, py: py})}); + } + + /** + * @dev Creates a new TSS signer + */ + function signerFromEntropy(bytes32 entropy) internal pure returns (SigningKey memory) { + uint256 secret; + assembly { + mstore(0, entropy) + secret := keccak256(0x00, 0x20) + } + while (secret >= Schnorr.Q) { + assembly { + mstore(0, secret) + secret := keccak256(0x00, 0x20) + } + } + return createSigner(secret); + } + + /** + * @dev Creates an unique TSS signer per test case + */ + function createSigner() internal pure returns (SigningKey memory) { + return signerFromEntropy(keccak256(msg.data)); + } + + // Workaround for set the tx.gasLimit, currently is not possible to define the gaslimit in foundry + // Reference: https://github.com/foundry-rs/foundry/issues/2224 + function _call(address addr, uint256 gasLimit, uint256 value, bytes memory data) + private + returns (uint256 gasUsed, bool success, bytes memory out) + { + require(gasleft() > gasLimit.saturatingAdd(5000), "insufficient gas"); + require(addr.code.length > 0, "Not a contract address"); + assembly ("memory-safe") { + success := + call( + 0x7E7E7E7E7E7E7E7E7E7E7E7E7E7E7E7E7E7E7E7E7E7E7E7E7E7E7E7E7E7E7E, // Code Injection TAG + gasLimit, // gas limit + addr, // addr + value, // value + add(data, 32), // arg offset + mload(data), // arg size + 0 // return offset + ) + gasUsed := mload(0) + + out := mload(0x40) + let size := returndatasize() + mstore(out, size) + let ptr := add(out, 32) + returndatacopy(ptr, 0, size) + mstore(0x40, add(ptr, size)) + } + } + + // Execute a contract call and calculate the acurrate execution gas cost + function executeCall(address sender, address dest, uint256 gasLimit, uint256 value, bytes memory data) + internal + returns (uint256 executionCost, uint256 baseCost, bytes memory out) + { + bool success; + (executionCost, baseCost, success, out) = tryExecuteCall(sender, dest, gasLimit, value, data); + // Revert if the execution failed + assembly { + if iszero(success) { revert(add(out, 32), mload(out)) } + } + } + + // Execute a contract call and calculate the acurrate execution gas cost + function tryExecuteCall(address sender, address dest, uint256 gasLimit, uint256 value, bytes memory data) + internal + returns (uint256 executionCost, uint256 baseCost, bool success, bytes memory out) + { + // Guarantee there's enough gas to execute the call + { + uint256 gasRequired = (gasLimit * 64) / 63; + gasRequired += 50_000; + require(gasleft() > gasRequired, "insufficient gas left to execute call"); + } + + // Compute the base tx cost (21k + 4 * zeros + 16 * nonZeros) + baseCost = calculateBaseCost(data); + + // Decrement sender base cost and value + { + uint256 txFees = gasLimit.saturatingMul(tx.gasprice); + require(sender.balance >= txFees.saturatingAdd(value), "account has no sufficient funds"); + vm.deal(sender, sender.balance - txFees); + gasLimit = gasLimit.saturatingSub(baseCost); + } + + // Execute + { + (VmSafe.CallerMode callerMode, address msgSender, address txOrigin) = + setCallerMode(VmSafe.CallerMode.RecurrentPrank, sender, sender); + (executionCost, success, out) = _call(dest, gasLimit, value, data); + setCallerMode(callerMode, msgSender, txOrigin); + } + + // Refund unused gas + uint256 refund = gasLimit.saturatingSub(executionCost).saturatingMul(tx.gasprice); + if (refund > 0) { + vm.deal(sender, sender.balance + refund); + } + } + + function setCallerMode(VmSafe.CallerMode callerMode, address msgSender, address txOrigin) + internal + returns (VmSafe.CallerMode prevCallerMode, address prevMsgSender, address prevTxOrigin) + { + (prevCallerMode, prevMsgSender, prevTxOrigin) = vm.readCallers(); + + // Stop previous caller mode + if (prevCallerMode == VmSafe.CallerMode.RecurrentBroadcast) { + vm.stopBroadcast(); + } else if (prevCallerMode == VmSafe.CallerMode.RecurrentPrank) { + vm.stopPrank(); + } + + // Set new caller mode + if (callerMode == VmSafe.CallerMode.Broadcast) { + vm.broadcast(msgSender); + } else if (callerMode == VmSafe.CallerMode.RecurrentBroadcast) { + vm.startBroadcast(msgSender); + } else if (callerMode == VmSafe.CallerMode.Prank) { + vm.prank(msgSender, txOrigin); + } else if (callerMode == VmSafe.CallerMode.RecurrentPrank) { + vm.startPrank(msgSender, txOrigin); + } + } + + function prank(address msgSender, address txOrigin, function() f) internal { + VmSafe.CallerMode callerMode = VmSafe.CallerMode.RecurrentPrank; + (callerMode, msgSender, txOrigin) = setCallerMode(VmSafe.CallerMode.RecurrentPrank, msgSender, txOrigin); + f(); + setCallerMode(callerMode, msgSender, txOrigin); + } + + function prank(address msgSender, function() f) internal { + VmSafe.CallerMode callerMode = VmSafe.CallerMode.RecurrentPrank; + address txOrigin = msgSender; + (callerMode, msgSender, txOrigin) = setCallerMode(VmSafe.CallerMode.RecurrentPrank, msgSender, txOrigin); + f(); + setCallerMode(callerMode, msgSender, txOrigin); + } + + function deployFactory() internal returns (IUniversalFactory) { + // Check if the factory is already deployed + if (address(FACTORY).code.length > 0) { + bytes32 codehash; + address addr = address(FACTORY); + assembly { + codehash := extcodehash(addr) + } + require(codehash == FACTORY_CODEHASH, "Invalid factory codehash"); + return FACTORY; + } + + uint256 nonce = vm.getNonce(FACTORY_DEPLOYER); + require(nonce == 0, "Factory deployer account has already been used"); + + bytes memory creationCode = vm.getCode("./lib/universal-factory/abi/UniversalFactory.json"); + vm.deal(FACTORY_DEPLOYER, 100 ether); + vm.prank(FACTORY_DEPLOYER, FACTORY_DEPLOYER); + address factory; + assembly { + factory := create(0, add(creationCode, 32), mload(creationCode)) + } + require(factory == address(FACTORY), "Factory address mismatch"); + require(keccak256(factory.code) == FACTORY_CODEHASH, "Factory codehash mismatch"); + return FACTORY; + } + + /** + * @dev Deploy a new Gateway and GatewayProxy contracts. + */ + function computeGatewayProxyAddress(address admin, bytes32 salt) internal pure returns (address) { + // 1.1 Compute the `GatewayProxy` address + bytes memory proxyCreationCode = abi.encodePacked(type(GatewayProxy).creationCode, abi.encode(admin)); + return FACTORY.computeCreate2Address(salt, proxyCreationCode); + } + + /** + * @dev Deploy a new Gateway and GatewayProxy contracts. + */ + function setupGateway( + VmSafe.Wallet memory admin, + bytes32 salt, + uint16 routeId, + TssKey[] memory keys, + Network[] memory networks + ) internal returns (IGateway gateway) { + require(FACTORY == TestUtils.deployFactory(), "UniversalFactory not deployed"); + + /////////////////////////////////////////// + // 1. Deploy the implementation contract // + /////////////////////////////////////////// + // 1.1 Compute the `GatewayProxy` address + address proxyAddr = computeGatewayProxyAddress(admin.addr, salt); + + // 1.2 Deploy the `Gateway` implementation contract + bytes memory implementationCreationCode = + abi.encodePacked(type(Gateway).creationCode, abi.encode(routeId, proxyAddr)); + address implementation = FACTORY.create2(salt, implementationCreationCode, abi.encode(routeId)); + + //////////////////////////////////////////////////////// + // 2. ProxyAdmin approves the implementation contract // + //////////////////////////////////////////////////////// + bytes memory authorization; + { + // This allows anyone to deploy the Proxy. + bytes32 digest = keccak256(abi.encode(proxyAddr, address(implementation))); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(admin.privateKey, digest); + authorization = abi.encode(v, r, s, address(implementation)); + } + + //////////////////////////////////////////////////////////////// + // 3 - Deploy the `GatewayProxy` using the `UniversalFactory` // + //////////////////////////////////////////////////////////////// + // Initializer, used to initialize the Gateway contract + bytes memory initializer = abi.encodeCall(Gateway.initialize, (admin.addr, keys, networks)); + bytes memory proxyCreationCode = abi.encodePacked(type(GatewayProxy).creationCode, abi.encode(admin.addr)); + address payable gatewayAddr = payable(FACTORY.create2(salt, proxyCreationCode, authorization, initializer)); + gateway = Gateway(gatewayAddr); + + // Send funds to the gateway contract + vm.deal(address(gateway), 100 ether); + } + + /** + * @dev Deploy a new Gateway and GatewayProxy contracts. + */ + function setupGateway(VmSafe.Wallet memory admin, bytes32 salt, uint16 srcRoute, uint16 dstRoute) + internal + returns (IGateway gateway) + { + require(FACTORY == TestUtils.deployFactory(), "UniversalFactory not deployed"); + SigningKey memory signer = TestUtils.createSigner(admin.privateKey); + TssKey[] memory keys = new TssKey[](1); + keys[0] = TssKey({yParity: SigningUtils.yParity(signer) == 28 ? 3 : 2, xCoord: SigningUtils.xCoord(signer)}); // Shard key + Network[] memory networks = new Network[](2); + address proxyAddr = computeGatewayProxyAddress(admin.addr, salt); + networks[0].id = srcRoute; // sepolia network id + networks[0].gateway = proxyAddr; // sepolia proxy address + networks[1].id = dstRoute; // shibuya network id + networks[1].gateway = proxyAddr; // shibuya proxy address + return setupGateway(admin, salt, dstRoute, keys, networks); + } +} + +library VerifyingUtils { + function addr(VerifyingKey memory pubkey) internal pure returns (address) { + uint256 hash; + assembly { + hash := keccak256(pubkey, 0x40) + } + return address(uint160(hash)); + } + + function yParity(VerifyingKey memory pubkey) internal pure returns (uint8) { + return uint8(pubkey.py % 2) + 27; + } + + function challenge(VerifyingKey memory pubkey, bytes32 hash, address r) internal pure returns (uint256) { + return uint256(keccak256(abi.encodePacked(r, yParity(pubkey), pubkey.px, uint256(hash)))); + } + + function verifyPrehash(VerifyingKey memory pubkey, bytes32 prehash, uint256 c, uint256 z) + internal + pure + returns (bool) + { + return Schnorr.verify(yParity(pubkey), pubkey.px, uint256(prehash), c, z); + } + + function verify(VerifyingKey memory pubkey, bytes memory message, uint256 c, uint256 z) + internal + pure + returns (bool) + { + return verifyPrehash(pubkey, keccak256(message), c, z); + } +} + +library SigningUtils { + function addr(SigningKey memory signer) internal pure returns (address) { + return VerifyingUtils.addr(signer.pubkey); + } + + function yParity(SigningKey memory signer) internal pure returns (uint8) { + return uint8(signer.pubkey.py % 2) + 27; + } + + function xCoord(SigningKey memory signer) internal pure returns (uint256) { + return signer.pubkey.px; + } + + function challenge(SigningKey memory signer, bytes32 hash, address r) internal pure returns (uint256) { + return uint256(keccak256(abi.encodePacked(r, yParity(signer), signer.pubkey.px, uint256(hash)))); + } + + function signPrehashed(SigningKey memory signer, bytes32 hash, uint256 nonce) + internal + pure + returns (uint256, uint256) + { + (uint256 rx, uint256 ry) = SECP256K1.publicKey(nonce); + address r = SECP256K1.point_hash(rx, ry); + uint256 c = challenge(signer, hash, r); + uint256 z = addmod(nonce, mulmod(c, signer.secret, Schnorr.Q), Schnorr.Q); + return (c, z); + } + + function sign(SigningKey memory signer, bytes memory message, uint256 nonce) + internal + pure + returns (uint256, uint256) + { + return signPrehashed(signer, keccak256(message), nonce); + } + + function verifyPrehash(SigningKey memory signer, bytes32 prehash, uint256 c, uint256 z) + internal + pure + returns (bool) + { + return Schnorr.verify(yParity(signer), signer.pubkey.px, uint256(prehash), c, z); + } + + function verify(SigningKey memory signer, bytes memory message, uint256 c, uint256 z) + internal + pure + returns (bool) + { + return verifyPrehash(signer, keccak256(message), c, z); + } +} diff --git a/analog-gmp/test/utils/BaseTest.sol b/analog-gmp/test/utils/BaseTest.sol new file mode 100644 index 000000000..9559308ec --- /dev/null +++ b/analog-gmp/test/utils/BaseTest.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (test/utils/BaseTest.sol) + +pragma solidity >=0.8.0; + +import {IUniversalFactory} from "@universal-factory/IUniversalFactory.sol"; +import {FactoryUtils} from "@universal-factory/FactoryUtils.sol"; +import {Interpreter} from "@evm-interpreter/Interpreter.sol"; +import {Test, console, Vm} from "forge-std/Test.sol"; +import {VmSafe} from "forge-std/Vm.sol"; + +abstract contract BaseTest is Test { + using FactoryUtils for IUniversalFactory; + + /** + * @dev Universal Factory used to deploy contracts at deterministic addresses. + * see: https://github.com/Analog-Labs/Universal-factory + */ + IUniversalFactory internal constant FACTORY = IUniversalFactory(0x0000000000001C4Bf962dF86e38F0c10c7972C6E); + + /** + * @dev EVM Interpreter, used to extract the `type(Gateway).runtimeCode` from the `type(Gateway).creationCode`. + * see: https://github.com/analog-Labs/evm-interpreter + */ + address internal constant EVM_INTERPRETER = 0x0000000000001e3F4F615cd5e20c681Cf7d85e8D; + + /** + * @dev CREATE2 Salt used to deploy the `findBytes` contracts at deterministic addresses. + */ + bytes32 private constant CREATE2_SALT = bytes32(uint256(1234)); + + /** + * @dev Address who must deploy the `UniversalFactory` contract, to guarantee the same address on all networks. + */ + address internal constant FACTORY_DEPLOYER = 0x908064dE91a32edaC91393FEc3308E6624b85941; + + /** + * @dev The `findBytes` contract bytecode, used to find byte sequences in a given bytecode. + */ + bytes private constant FIND_BYTES = + hex"602e80600a5f395ff3fe60403610602a573d35601f5b6001018035821881361102600b576020813614602a5790033d5260203df35b3d3dfd"; + + /** + * @dev CODEHASH from the `findBytes` contract bytecode, used to compute the final CREATE2 address.. + */ + bytes32 private constant FIND_BYTES_CODEHASH = keccak256(FIND_BYTES); + + /** + * @dev All byte32 sequences in the form `0x7E7E7E7E7E7E...` will be replaced by the `INLINE_BYTECODE`. + */ + bytes32 private constant INLINE_BYTECODE = 0x6000823f505a96949290959391f15a607b019091036800000000000000000052; + + constructor() { + // Initialize the Universal Factory + if (address(FACTORY).code.length == 0) { + bytes memory creationCode = vm.getCode("./lib/universal-factory/abi/UniversalFactory.json"); + vm.deal(FACTORY_DEPLOYER, 100 ether); + vm.prank(FACTORY_DEPLOYER, FACTORY_DEPLOYER); + address factory; + assembly { + factory := create(0, add(creationCode, 32), mload(creationCode)) + } + require(factory == address(FACTORY), "Factory address mismatch"); + } + + // Initialize the EVM Interpreter + if (EVM_INTERPRETER.code.length == 0) { + bytes memory creationCode = vm.getCode("Interpreter.sol"); + address interpreter; + assembly { + interpreter := create(0, add(creationCode, 32), mload(creationCode)) + } + assertTrue(interpreter != address(0), "interpreter creation failed"); + assertGt(interpreter.code.length, 0, "interpreter code length mismatch"); + vm.etch(EVM_INTERPRETER, interpreter.code); + } + + // Deploy the find bytes contract + address findBytes = FACTORY.computeCreate2Address(CREATE2_SALT, FIND_BYTES_CODEHASH); + + // Initialize using the EVM interpreter + if (findBytes.code.length == 0) { + assertEq(FACTORY.create2(CREATE2_SALT, FIND_BYTES), findBytes, "find bytes creation failed"); + assembly { + // Copy code to memory + codecopy(0, 0, codesize()) + + // Execute constructor using the EVM interpreter + let success := delegatecall(gas(), EVM_INTERPRETER, 0, add(codesize(), 0x20), 0, 0) + + // Copy result to memory + returndatacopy(0x20, 0, returndatasize()) + if iszero(success) { revert(0x20, returndatasize()) } + + // COPY TAG to memory + let tag := mul(0x7E, 0x0101010101010101010101010101010101010101010101010101010101010101) + mstore(0x00, tag) + + // Find the `0x7E7E7E...` tag in the bytecode + let size := returndatasize() + if iszero(staticcall(gas(), findBytes, 0x00, add(size, 0x20), 0x00, 0x20)) { revert(0, 0) } + + // Replace the `0x7E7E7E...` by the `INLINE_BYTECODE` + let offset := add(mload(0x00), 0x20) + mstore(add(offset, 1), 0x5B) + mstore(offset, INLINE_BYTECODE) + + // Replace remaining occurences of `0x7E7E7E...` by the `INLINE_BYTECODE` + + for { let end := add(size, 0x20) } lt(offset, end) {} { + let backup := mload(offset) + mstore(offset, tag) + success := staticcall(gas(), findBytes, offset, sub(add(size, 0x20), offset), 0x00, 0x20) + mstore(offset, backup) + if iszero(success) { break } + offset := add(mload(0x00), 0x20) + mstore(add(offset, 1), 0x5B) + mstore(offset, INLINE_BYTECODE) + } + + return(0x20, size) + } + } + } +} diff --git a/analog-gmp/test/utils/GasSpender.sol b/analog-gmp/test/utils/GasSpender.sol new file mode 100644 index 000000000..799d179fa --- /dev/null +++ b/analog-gmp/test/utils/GasSpender.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (test/utils/GasSpender.sol) + +pragma solidity >=0.8.0; + +import {IGmpReceiver} from "../../src/interfaces/IGmpReceiver.sol"; + +contract GasSpender is IGmpReceiver { + // Following a contract implements the IGmpReceiver interface and wastes the exact amount of gas you send to it in the payload field. + // OFFSET OPCODE + // 0x00 0x5a GAS + // 0x01 0x6002 PUSH1 0x02 + // 0x03 0x01 ADD + // 0x04 0x80 DUP1 + // 0x05 0x3d RETURNDATASIZE + // 0x06 0x52 MSTORE + // 0x07 0x3d RETURNDATASIZE + // 0x08 0x6020 PUSH1 0x20 + // 0x0a 0x91 SWAP2 + // 0x0b 0x6064 PUSH1 0x64 + // 0x0d 0x35 CALLDATALOAD -- Load the payload offset from the calldata + // 0x0e 0x6024 PUSH1 0x24 + // 0x10 0x01 ADD + // 0x11 0x35 CALLDATALOAD -- Load the gasToWaste from the payload + // 0x12 0x14 EQ + // 0x13 0x6018 PUSH1 0x18 + // ,=<0x15 0x57 JUMPI + // | 0x16 0x5b JUMPDEST + // | 0x17 0xfd REVERT -- Reverts if the gas left is less than the gas to waste. + // |=>0x18 0x5b JUMPDEST -- Waste 22 gas each on each iteration + // | 0x19 0x6036 PUSH1 0x36 + // | 0x1b 0x5a GAS + // | 0x1c 0x11 GT + // | 0x1d 0x6018 PUSH1 0x18 + // `=<0x1f 0x57 JUMPI + // 0x20 0x5a GAS + // 0x21 0x6049 PUSH1 0x49 + // 0x23 0x03 SUB + // ,=<0x24 0x56 JUMP -- Jumps depending on how much gas is left + // |=>0x25 0x5b JUMPDEST + // |=>0x26 0x5b JUMPDEST + // |=>0x27 0x5b JUMPDEST + // |=>0x28 0x5b JUMPDEST + // |=>0x29 0x5b JUMPDEST + // |=>0x2a 0x5b JUMPDEST + // |=>0x2b 0x5b JUMPDEST + // |=>0x2c 0x5b JUMPDEST + // |=>0x2d 0x5b JUMPDEST + // |=>0x2e 0x5b JUMPDEST + // |=>0x2f 0x5b JUMPDEST + // |=>0x30 0x5b JUMPDEST + // |=>0x31 0x5b JUMPDEST + // |=>0x32 0x5b JUMPDEST + // |=>0x33 0x5b JUMPDEST + // |=>0x34 0x5b JUMPDEST + // |=>0x35 0x5b JUMPDEST + // |=>0x36 0x5b JUMPDEST + // |=>0x37 0x5b JUMPDEST + // |=>0x38 0x5b JUMPDEST + // |=>0x39 0x5b JUMPDEST + // `=>0x3a 0x5b JUMPDEST + // 0x3b 0xf3 RETURN + bytes private constant BYTECODE = + hex"5a600201803d523d60209160643560240135146018575bfd5b60365a116018575a604903565b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5bf3"; + + constructor() payable { + bytes memory bytecode = BYTECODE; + assembly { + return(add(bytecode, 0x20), mload(bytecode)) + } + } + + function onGmpReceived(bytes32, uint128, bytes32, bytes calldata payload) external payable returns (bytes32) { + unchecked { + // OBS: This is just an example on how this contract works, the actual code is implemented directly in + // low level EVM, as defined in the `BYTECODE` constant. + uint256 initialGas = gasleft() + 2; + uint256 gasToWaste = abi.decode(payload, (uint256)); + require(initialGas > gasToWaste); + uint256 finalGas = initialGas - gasToWaste; + while (gasleft() > finalGas) {} + return bytes32(initialGas); + } + } +} diff --git a/analog-gmp/test/utils/GasSpender.t.sol b/analog-gmp/test/utils/GasSpender.t.sol new file mode 100644 index 000000000..4f284ffcb --- /dev/null +++ b/analog-gmp/test/utils/GasSpender.t.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (test/Gateway.t.sol) + +pragma solidity >=0.8.0; + +import {Test, console, Vm} from "forge-std/Test.sol"; +import {VmSafe} from "forge-std/Vm.sol"; +import {TestUtils} from "../TestUtils.sol"; +import {BaseTest} from "./BaseTest.sol"; +import {GasSpender} from "./GasSpender.sol"; +import {GasUtils} from "../../src/utils/GasUtils.sol"; +import {IGmpReceiver} from "../../src/interfaces/IGmpReceiver.sol"; + +contract GasSpenderTest is BaseTest { + function buildCall(uint256 gasToWaste) private pure returns (uint256 gasLimit, bytes memory encodedCall) { + // Encode the `IGmpReceiver.onGmpReceived` call + encodedCall = abi.encodeCall( + IGmpReceiver.onGmpReceived, + ( + 0x0000000000000000000000000000000000000000000000000000000000000000, + 1, + 0x0000000000000000000000000000000000000000000000000000000000000000, + abi.encode(gasToWaste) + ) + ); + gasLimit = TestUtils.calculateBaseCost(encodedCall) + gasToWaste; + } + + function test_onGmpReceivedWorks(uint16 delta) external { + // Guarantee the gas limit is not less than 1000 + uint256 gasToWaste = 1000 + uint256(delta); + vm.txGasPrice(1); + + // Create the Sender account + address sender = TestUtils.createTestAccount(100 ether); + + // Deploy the GasSpender contract + GasSpender spender = new GasSpender(); + + // Encode the `IGmpReceiver.onGmpReceived` call + (uint256 gasLimit, bytes memory encodedCall) = buildCall(gasToWaste); + + (uint256 gasUsed,, bytes memory output) = + TestUtils.executeCall(sender, address(spender), gasLimit, 0, encodedCall); + assertEq(gasUsed, gasToWaste); + assertEq(output.length, 32); + } + + function test_revertsMoreGas(uint16 delta) external { + // Guarantee the gas limit is not less than 1000 + uint256 gasToWaste = 1000 + uint256(delta); + vm.txGasPrice(1); + + // Create the Sender account + address sender = TestUtils.createTestAccount(100 ether); + + // Deploy the GasSpender contract + GasSpender spender = new GasSpender(); + + // Encode the `IGmpReceiver.onGmpReceived` call + (uint256 gasLimit, bytes memory encodedCall) = buildCall(gasToWaste); + + vm.expectRevert(); + TestUtils.executeCall(sender, address(spender), gasLimit + 1, 0, encodedCall); + } + + function test_revertsLessGas(uint16 delta) external { + // Guarantee the gas limit is not less than 1000 + uint256 gasToWaste = 1000 + uint256(delta); + vm.txGasPrice(1); + + // Create the Sender account + address sender = TestUtils.createTestAccount(100 ether); + + // Deploy the GasSpender contract + GasSpender spender = new GasSpender(); + + // Encode the `IGmpReceiver.onGmpReceived` call + (uint256 gasLimit, bytes memory encodedCall) = buildCall(gasToWaste); + + vm.expectRevert(); + TestUtils.executeCall(sender, address(spender), gasLimit - 1, 0, encodedCall); + } +} diff --git a/analog-gmp/test/utils/GmpProxy.sol b/analog-gmp/test/utils/GmpProxy.sol new file mode 100644 index 000000000..176a1dd8d --- /dev/null +++ b/analog-gmp/test/utils/GmpProxy.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (test/utils/GmpProxy.sol) + +pragma solidity >=0.8.0; + +import {ERC1967} from "../../src/utils/ERC1967.sol"; +import {IGmpReceiver} from "../../src/interfaces/IGmpReceiver.sol"; +import {IGateway} from "../../src/interfaces/IGateway.sol"; +import {BranchlessMath} from "../../src/utils/BranchlessMath.sol"; + +contract GmpProxy is IGmpReceiver { + using BranchlessMath for uint256; + + event MessageReceived(GmpMessage msg); + + struct GmpMessage { + bytes32 source; + uint16 srcNetwork; + address dest; + uint16 destNetwork; + uint64 gasLimit; + uint64 nonce; + bytes data; + } + + IGateway public immutable GATEWAY; + uint16 public immutable NETWORK_ID; + + constructor(address gateway) payable { + GATEWAY = IGateway(gateway); + NETWORK_ID = GATEWAY.networkId(); + } + + function sendMessage(GmpMessage calldata message) external payable { + uint256 value = address(this).balance.min(msg.value); + GATEWAY.submitMessage{value: value}(message.dest, message.destNetwork, message.gasLimit, message.data); + } + + function estimateMessageCost(uint256 messageSize, uint256 gasLimit) external view returns (uint256) { + return GATEWAY.estimateMessageCost(NETWORK_ID, messageSize, gasLimit); + } + + function onGmpReceived(bytes32 id, uint128, bytes32, bytes calldata payload) external payable returns (bytes32) { + // For testing purpose + // we keep the original struct in payload so we dont depend on OnGmpReceived call since it doesnt provide everything. + (GmpMessage memory message) = abi.decode(payload, (GmpMessage)); + message.data = payload; + + emit MessageReceived(message); + return id; + } +} diff --git a/lib/forge-std b/lib/forge-std deleted file mode 160000 index ae570fec0..000000000 --- a/lib/forge-std +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ae570fec082bfe1c1f45b0acca4a2b4f84d345ce diff --git a/lib/frost-evm b/lib/frost-evm deleted file mode 160000 index 935934a74..000000000 --- a/lib/frost-evm +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 935934a74d0978a539267edee30a9255399d0e37