From 7fa8dac889720e51d26d357512bdc73c021b7aae Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Mon, 16 Dec 2024 17:13:25 +0000 Subject: [PATCH] GMP V2 (#28) Co-authored-by: Haider Ali <70846121+Haider-Ali-DS@users.noreply.github.com> --- foundry.toml | 48 +++++++- scripts/Deploy.sol | 22 +++- src/Gateway.sol | 92 ++++++-------- src/GatewayProxy.sol | 66 +++++++--- src/Primitives.sol | 231 ++++++++++++++++++++-------------- src/interfaces/IExecutor.sol | 29 +++-- src/storage/Routes.sol | 8 +- src/storage/Shards.sol | 95 ++++++++++++-- src/utils/BranchlessMath.sol | 9 ++ src/utils/GasUtils.sol | 180 ++++++++++++++++++++------- src/utils/Schnorr.sol | 62 ++++++++++ test/Example.t.sol | 28 +++-- test/GasUtils.t.sol | 66 +++++----- test/Gateway.t.sol | 141 ++++++++++++++++----- test/GatewayProxy.t.sol | 128 +++++++++++++++++++ test/GmpTestTools.sol | 15 ++- test/TestUtils.sol | 232 ++++++++++++++++++++++++++++++----- test/utils/GmpProxy.sol | 74 +++++++++++ 18 files changed, 1174 insertions(+), 352 deletions(-) create mode 100644 src/utils/Schnorr.sol create mode 100644 test/GatewayProxy.t.sol create mode 100644 test/utils/GmpProxy.sol diff --git a/foundry.toml b/foundry.toml index c03dcb5..d422c72 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,24 +1,64 @@ [profile.default] src = "src" +test = "test" out = "out" libs = ["lib"] match_contract = ".+Test" +# Permissions +fs_permissions = [{ access = "read", path = "./lib/universal-factory/abi" }] -# Lint +######## +# Lint # +######## deny_warnings = true -# Solc options -solc = '0.8.25' +################ +# Solc options # +################ +solc = '0.8.28' +# Keep `shanghai` once blockchains 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 +############### +# 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 = false +# 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. diff --git a/scripts/Deploy.sol b/scripts/Deploy.sol index c960606..94d6c3e 100644 --- a/scripts/Deploy.sol +++ b/scripts/Deploy.sol @@ -18,7 +18,6 @@ import { TssKey, GmpMessage, UpdateKeysMessage, - UpdateNetworkInfo, Signature, Network, GmpStatus, @@ -26,6 +25,24 @@ import { 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; @@ -477,7 +494,8 @@ contract MigrateGateway is Script { 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(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"); diff --git a/src/Gateway.sol b/src/Gateway.sol index fd60cf6..efba5c8 100644 --- a/src/Gateway.sol +++ b/src/Gateway.sol @@ -3,7 +3,7 @@ pragma solidity >=0.8.0; -import {Schnorr} from "@frost-evm/Schnorr.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"; @@ -18,7 +18,6 @@ import { TssKey, GmpMessage, UpdateKeysMessage, - UpdateNetworkInfo, Signature, Network, Route, @@ -61,7 +60,6 @@ abstract contract GatewayEIP712 { contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { using PrimitiveUtils for UpdateKeysMessage; - using PrimitiveUtils for UpdateNetworkInfo; using PrimitiveUtils for GmpMessage; using PrimitiveUtils for address; using BranchlessMath for uint256; @@ -83,6 +81,11 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { bytes32 private constant GMP_CREATED_EVENT_SELECTOR = 0x0114885f90b5168242aa31b7afb9c2e9f88e90ce329c893d3e6c56021c4c03a5; + /** + * @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; @@ -104,20 +107,6 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { uint64 blockNumber; // block in which the message was processed } - /** - * @dev Network info stored in the Gateway Contract - * @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. - */ - struct NetworkInfo { - bytes32 domainSeparator; - uint64 gasLimit; - UFloat9x56 relativeGasPrice; - uint128 baseFee; - } - /** * @dev Network info stored in the Gateway Contract * @param id Message unique id. @@ -139,10 +128,10 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { constructor(uint16 network, address proxy) payable GatewayEIP712(network, proxy) {} // EIP-712 typed hash - function initialize(address admin, TssKey[] calldata keys, Network[] calldata networks) external { - require(PROXY_ADDRESS == address(this), "only proxy can be initialize"); + function initialize(address proxyAdmin, TssKey[] calldata keys, Network[] calldata networks) external { + require(PROXY_ADDRESS == address(this) || msg.sender == FACTORY, "only proxy can be initialize"); require(prevMessageHash == 0, "already initialized"); - ERC1967.setAdmin(admin); + ERC1967.setAdmin(proxyAdmin); // Initialize the prevMessageHash with a non-zero value to avoid the first GMP to spent more gas, // once initialize the storage cost 21k gas, while alter it cost just 2800 gas. @@ -150,7 +139,6 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { // Register networks RouteStore.getMainStorage().initialize(networks, NetworkID.wrap(NETWORK_ID), computeDomainSeparator); - // _updateNetworks(networks); // Register keys ShardStore.getMainStorage().registerTssKeys(keys); @@ -250,25 +238,20 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { assembly { // Using low-level assembly because the GMP is considered executed // regardless if the call reverts or not. - let ptr := add(callback, 32) - let size := mload(callback) - mstore(callback, 0) - - // returns 1 if the call succeed, and 0 if it reverted + 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) - ptr, // input memory pointer - size, // input size - callback, // output memory pointer + add(callback, 32), // input memory pointer + mload(callback), // input size + 0, // output memory pointer 32 // output size (fixed 32 bytes) ) // Get Result, reuse data to keep a predictable memory expansion - result := mload(callback) - mstore(callback, size) + result := mload(0) } // Update GMP status @@ -293,7 +276,7 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { 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(437); + initialGas = initialGas.saturatingAdd(GasUtils.EXECUTION_SELECTOR_OVERHEAD); // Theoretically we could remove the destination network field // and fill it up with the network id of the contract, then the signature will fail. @@ -315,7 +298,7 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { // Refund the chronicle gas unchecked { // Compute GMP gas used - uint256 gasUsed = 7223; + uint256 gasUsed = 7152; gasUsed = gasUsed.saturatingAdd(GasUtils.txBaseCost()); gasUsed = gasUsed.saturatingAdd(GasUtils.proxyOverheadGasCost(uint16(msg.data.length), 64)); gasUsed = gasUsed.saturatingAdd(initialGas - gasleft()); @@ -352,7 +335,7 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { require(msg.value >= route.estimateWeiCost(data, executionGasLimit), "insufficient tx value"); // We use 20 bytes for represent the address and 1 bit for the contract flag - GmpSender source = msg.sender.toSender(tx.origin != msg.sender); + GmpSender source = msg.sender.toSender(false); // Salt is equal to the previous message id (EIP-712 hash), this allows us to establish a sequence and eaily query the message history. bytes32 prevHash = prevMessageHash; @@ -422,7 +405,6 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { view returns (uint256) { - // NetworkInfo storage network = _networkInfo[networkid]; RouteStore.NetworkInfo memory route = RouteStore.getMainStorage().get(NetworkID.wrap(networkid)); // Estimate the cost @@ -442,7 +424,7 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { * @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 == _getAdmin(), "unauthorized"); + require(msg.sender == ERC1967.getAdmin(), "unauthorized"); // Check if the recipient is a contract if (recipient.code.length > 0) { bool success; @@ -483,30 +465,30 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { */ 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}); + 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 == _getAdmin(), "unauthorized"); + require(msg.sender == ERC1967.getAdmin(), "unauthorized"); ShardStore.getMainStorage().register(publicKey); } /** * @dev Register Shards in batch. */ - function setShard(TssKey[] calldata publicKeys) external { - require(msg.sender == _getAdmin(), "unauthorized"); - ShardStore.getMainStorage().registerTssKeys(publicKeys); + function setShards(TssKey[] calldata publicKeys) external { + require(msg.sender == ERC1967.getAdmin(), "unauthorized"); + ShardStore.getMainStorage().replaceTssKeys(publicKeys); } /** * @dev Revoke a single shard TSS Key. */ function revokeShard(TssKey calldata publicKey) external { - require(msg.sender == _getAdmin(), "unauthorized"); + require(msg.sender == ERC1967.getAdmin(), "unauthorized"); ShardStore.getMainStorage().revoke(publicKey); } @@ -514,7 +496,7 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { * @dev Revoke Shards in batch. */ function revokeShard(TssKey[] calldata publicKeys) external { - require(msg.sender == _getAdmin(), "unauthorized"); + require(msg.sender == ERC1967.getAdmin(), "unauthorized"); ShardStore.getMainStorage().revokeKeys(publicKeys); } @@ -533,15 +515,15 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { * @dev Create or update a single route */ function setRoute(Route calldata info) external { - require(msg.sender == _getAdmin(), "unauthorized"); + require(msg.sender == ERC1967.getAdmin(), "unauthorized"); RouteStore.getMainStorage().createOrUpdateRoute(info); } /** * @dev Create or update an array of routes */ - function setRoute(Route[] calldata values) external { - require(msg.sender == _getAdmin(), "unauthorized"); + 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++) { @@ -553,33 +535,31 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { ADMIN LOGIC //////////////////////////////////////////////////////////////*/ - function _getAdmin() private view returns (address admin) { - admin = ERC1967.getAdmin(); - // If the admin slot is empty, then the 0xd4833be6144AF48d4B09E5Ce41f826eEcb7706D6 is the admin - admin = BranchlessMath.ternary(admin == address(0x0), 0xd4833be6144AF48d4B09E5Ce41f826eEcb7706D6, admin); + function admin() external view returns (address) { + return ERC1967.getAdmin(); } function setAdmin(address newAdmin) external payable { - require(msg.sender == _getAdmin(), "unauthorized"); + require(msg.sender == ERC1967.getAdmin(), "unauthorized"); ERC1967.setAdmin(newAdmin); } // OBS: remove != revoke (when revoked, you cannot register again) function sudoRemoveShards(TssKey[] calldata revokedKeys) external payable { - require(msg.sender == _getAdmin(), "unauthorized"); + require(msg.sender == ERC1967.getAdmin(), "unauthorized"); ShardStore.getMainStorage().revokeKeys(revokedKeys); emit KeySetChanged(bytes32(0), revokedKeys, new TssKey[](0)); } function sudoAddShards(TssKey[] calldata newKeys) external payable { - require(msg.sender == _getAdmin(), "unauthorized"); + require(msg.sender == ERC1967.getAdmin(), "unauthorized"); ShardStore.getMainStorage().registerTssKeys(newKeys); emit KeySetChanged(bytes32(0), new TssKey[](0), newKeys); } // 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 == _getAdmin(), "unauthorized"); + require(msg.sender == ERC1967.getAdmin(), "unauthorized"); require(values.length > 0, "invalid values"); uint256 prev = 0; @@ -604,7 +584,7 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { } function upgrade(address newImplementation) external payable { - require(msg.sender == _getAdmin(), "unauthorized"); + require(msg.sender == ERC1967.getAdmin(), "unauthorized"); // Store the address of the implementation contract ERC1967.setImplementation(newImplementation); @@ -615,7 +595,7 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { payable returns (bytes memory returndata) { - require(msg.sender == _getAdmin(), "unauthorized"); + require(msg.sender == ERC1967.getAdmin(), "unauthorized"); // Store the address of the implementation contract ERC1967.setImplementation(newImplementation); diff --git a/src/GatewayProxy.sol b/src/GatewayProxy.sol index c9e0f11..e6bf128 100644 --- a/src/GatewayProxy.sol +++ b/src/GatewayProxy.sol @@ -4,33 +4,65 @@ pragma solidity >=0.8.0; import {ERC1967} from "./utils/ERC1967.sol"; +import {Context, CreateKind, IUniversalFactory} from "@universal-factory/IUniversalFactory.sol"; contract GatewayProxy { /** - * @dev Minimal EIP-1967 proxy bytecode. + * @dev The address of the `UniversalFactory` contract, must be the same on all networks. */ - bytes private constant PROXY_BYTECODE = - hex"363d3d373d3d3d363d7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d82803e903d91603857fd5bf3"; + IUniversalFactory internal constant FACTORY = IUniversalFactory(0x0000000000001C4Bf962dF86e38F0c10c7972C6E); - constructor(address implementation, bytes memory initializer) payable { - // Copy Proxy bytecode to memory - bytes memory bytecode = PROXY_BYTECODE; + /** + * @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); + } - // Initialize storage by calling the implementation's using `delegatecall`. - bool success = true; - bytes memory returndata = ""; - if (initializer.length > 0) { - (success, returndata) = implementation.delegatecall(initializer); - } + 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()) } - // Verify initialization result - /// @solidity memory-safe-assembly - assembly { - if success { return(add(bytecode, 32), mload(bytecode)) } - revert(add(returndata, 32), mload(returndata)) + // Revert if the call failed + revert(0, returndatasize()) } } } diff --git a/src/Primitives.sol b/src/Primitives.sol index 3db6bf8..13d00aa 100644 --- a/src/Primitives.sol +++ b/src/Primitives.sol @@ -71,24 +71,6 @@ struct UpdateKeysMessage { TssKey[] register; } -/** - * @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; -} - /** * @dev A Route represents a communication channel between two networks. * @param networkId The id of the provided network. @@ -102,8 +84,8 @@ struct Route { uint64 gasLimit; uint128 baseFee; bytes32 gateway; - uint256 relativeGasPriceNumerator; - uint256 relativeGasPriceDenominator; + uint128 relativeGasPriceNumerator; + uint128 relativeGasPriceDenominator; } /** @@ -188,7 +170,6 @@ library PrimitiveUtils { mstore(ptr, hash) } } - return keccak256(keysHashed); } @@ -203,30 +184,6 @@ library PrimitiveUtils { ); } - // computes the hash of the fully encoded EIP-712 message for the domain, which can be used to recover the signer - function eip712hash(UpdateNetworkInfo calldata message) internal pure returns (bytes32) { - return keccak256( - abi.encode( - keccak256( - "UpdateNetworkInfo(uint16 networkId,bytes32 domainSeparator,uint64 gasLimit,UFloat9x56 relativeGasPrice,uint128 baseFee)" - ), - message.networkId, - message.domainSeparator, - message.gasLimit, - message.relativeGasPrice, - message.baseFee - ) - ); - } - - function eip712TypedHash(UpdateNetworkInfo calldata message, bytes32 domainSeparator) - internal - pure - returns (bytes32) - { - return _computeTypedHash(domainSeparator, eip712hash(message)); - } - function eip712TypedHash(UpdateKeysMessage memory message, bytes32 domainSeparator) internal pure @@ -259,73 +216,161 @@ library PrimitiveUtils { } } + type MessagePtr is uint256; + + function memToCallback(GmpMessage memory message, bytes32 domainSeparator) + internal + view + returns (GmpCallback memory callback) + { + MessagePtr ptr; + assembly { + ptr := message + } + _intoCallback(ptr, domainSeparator, false, callback); + } + + function intoCallback(GmpMessage calldata message, bytes32 domainSeparator) + internal + view + returns (GmpCallback memory callback) + { + MessagePtr ptr; + assembly { + ptr := message + } + _intoCallback(ptr, domainSeparator, true, callback); + } + /** * @dev Converts the `GmpMessage` into a `GmpCallback` struct, which contains all fields from - * `GmpMessage`, plus the EIP-712 id and `IGmpReceiver.onGmpReceived` callback encoded. + * `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, using solidity `abi.encode` does the following: + * 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 compute the EIP-712 hash and create - * the `IGmpReceiver.onGmpReceived` callback, unfortunately this requires inline assembly. + * + * 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 domainSeparator EIP-712 domain separator - * @return callback `GmpCallback` struct + * @param callback `GmpCallback` struct */ - function intoCallback(GmpMessage calldata message, bytes32 domainSeparator) - internal - pure - returns (GmpCallback memory callback) + function _intoCallback(MessagePtr message, bytes32 domainSeparator, bool isCalldata, GmpCallback memory callback) + private + view { - bytes calldata data = message.data; - /// @solidity memory-safe-assembly - assembly { - callback := mload(0x40) - - // GmpMessage Type Hash - mstore(add(callback, 0x0000), GMP_MESSAGE_TYPE_HASH) - mstore(add(callback, 0x0020), calldataload(add(message, 0x00))) // message.source - mstore(add(callback, 0x0040), calldataload(add(message, 0x20))) // message.srcNetwork - mstore(add(callback, 0x0060), calldataload(add(message, 0x40))) // message.dest - mstore(add(callback, 0x0080), calldataload(add(message, 0x60))) // message.destNetwork - mstore(add(callback, 0x00a0), calldataload(add(message, 0x80))) // message.gasLimit - mstore(add(callback, 0x00c0), calldataload(add(message, 0xa0))) // message.salt - - // Copy message.data to memory - let size := data.length - mstore(add(callback, 0x01c4), size) // message.data.length - calldatacopy(add(callback, 0x01e4), data.offset, size) // message.data - - // Computed GMP Typed Hash - let messageHash := keccak256(add(callback, 0x01e4), size) // keccak(message.data) + assembly ("memory-safe") { + // | MEMORY OFFSET | RESERVED FIELD | + // | 0x00e0..0x0100 <- `callback.data` pointer + // | 0x0100..0x0120 <- `callback.data.length` field. + // | 0x0120..0x0124 <- `onGmpReceived.selector` field (4 bytes). + // | 0x0124..0x0144 <- `onGmpReceived.id` param. + // | 0x0144..0x0164 <- `onGmpReceived.network` param. + // | 0x0164..0x0184 <- `onGmpReceived.source` param. + // | 0x0184..0x01a4 <- `onGmpReceived.data.offset` param (calldata pointer). + // | 0x01a4..0x01c4 <- `onGmpReceived.data.length` param. + // | 0x01c4..?????? <- `onGmpReceived.data` bytes. + // + ////////////////////////////////////////////////////////// + // First need compute to `GmpMessage` EIP-712 Type Hash // + ////////////////////////////////////////////////////////// + + // Store the `GMP_MESSAGE_TYPE_HASH` in the first 32 bytes of the callback. + mstore(add(callback, 0x0000), GMP_MESSAGE_TYPE_HASH) // callback.eip712hash + + // Then we copy all `GmpMessage` fields to memory, except the `data` field. + let size + { + switch isCalldata + case 0 { + mstore(add(callback, 0x20), mload(add(message, 0x00))) // callback.source + mstore(add(callback, 0x40), mload(add(message, 0x20))) // callback.srcNetwork + mstore(add(callback, 0x60), mload(add(message, 0x40))) // callback.dest + mstore(add(callback, 0x80), mload(add(message, 0x60))) // callback.destNetwork + mstore(add(callback, 0xa0), mload(add(message, 0x80))) // callback.gasLimit + mstore(add(callback, 0xc0), mload(add(message, 0xa0))) // callback.salt + + // Store `onGmpReceived.data.length` at `0x01a4..0x01c4`. + let offset := mload(add(message, 0xc0)) + size := mload(offset) + mstore(add(callback, 0x01a4), size) + + if iszero(staticcall(gas(), 0x04, add(offset, 0x20), size, add(callback, 0x01c4), size)) { + revert(0, 0) + } + } + default { + mstore(add(callback, 0x20), calldataload(add(message, 0x00))) // callback.source + mstore(add(callback, 0x40), calldataload(add(message, 0x20))) // callback.srcNetwork + mstore(add(callback, 0x60), calldataload(add(message, 0x40))) // callback.dest + mstore(add(callback, 0x80), calldataload(add(message, 0x60))) // callback.destNetwork + mstore(add(callback, 0xa0), calldataload(add(message, 0x80))) // callback.gasLimit + mstore(add(callback, 0xc0), calldataload(add(message, 0xa0))) // callback.salt + + // Store `onGmpReceived.data.length` at `0x01a4..0x01c4`. + let offset := add(calldataload(add(message, 0xc0)), message) + size := calldataload(offset) + mstore(add(callback, 0x01a4), size) + + // Copy `message.data` to memory at `0x01c4`, as described above. + calldatacopy(add(callback, 0x01c4), add(offset, 0x20), size) + } + } + + // Compute `keccak256(message.data)` + let messageHash := keccak256(add(callback, 0x01c4), size) + + // temporarily store the result at `0x00e0..0x0100`, which is the end of + // the `GmpMessage` struct. mstore(add(callback, 0x00e0), messageHash) - messageHash := keccak256(callback, 0x0100) // GMP eip712 hash + + // Compute `keccak256(abi.encode(GMP_MESSAGE_TYPE_HASH, message.source, ..., keccak256(message.data)))` + messageHash := keccak256(callback, 0x0100) + + // Compute the final EIP-712 Signature Hash mstore(0, 0x1901) mstore(0x20, domainSeparator) mstore(0x40, messageHash) // this will be restored at the end of this function messageHash := keccak256(0x1e, 0x42) // GMP Typed Hash - // Retore message.data.offset - mstore(add(callback, 0x00e0), add(callback, 0x0120)) + // Replace the `GMP_MESSAGE_TYPE_HASH` by the `eip712hash`. mstore(callback, messageHash) - // selector + GMP_ID + network + source + data.offset + data.length - size := add(and(add(size, 31), 0xffffffe0), 0xa4) - - // onGmpReceived(bytes32 id, uint128 network, bytes32 source, bytes calldata payload) - mstore(add(callback, 0x0124), 0x01900937) // selector - mstore(add(callback, 0x0120), size) // length - mstore(add(callback, 0x0144), messageHash) // id - mstore(add(callback, 0x0164), calldataload(add(message, 0x20))) // network - mstore(add(callback, 0x0184), calldataload(add(message, 0x00))) // source - mstore(add(callback, 0x01a4), 0x80) // payload.offset - - // update free memory pointer - size := add(and(add(size, 31), 0xffffffe0), 0x0120) - size := and(add(add(callback, size), 31), 0xffffffe0) - mstore(0x40, add(size, 0x40)) + // Replace the `eip712hash` by the `callback.data.offset`. + mstore(add(callback, 0x00e0), add(callback, 0x0100)) + + // Compute the callback size, which is equivalent to compute the following: + // ```solidity + // abi.encodeCall(IGmpReceiver.onGmpReceived, (eip712hash, network, sender, data)).length; + // ``` + // So essentially is the size of `message.data` 32 byte aligned + 164 bytes for the other fields. + size := add(and(add(size, 31), 0xffffffe0), 164) + + // Add the missing fields between `0x0100..0x01a4` to the callback. + // The fields between `0x01a4..0x01c4` are already set. + mstore(add(callback, 0x0104), 0x01900937) // selector (4 bytes) + mstore(add(callback, 0x0124), messageHash) // eip712hash (32 bytes) + { + switch isCalldata + case 0 { + mstore(add(callback, 0x0144), mload(add(message, 0x20))) // network (32 bytes) + mstore(add(callback, 0x0164), mload(add(message, 0x00))) // source (32 bytes) + } + default { + mstore(add(callback, 0x0144), calldataload(add(message, 0x20))) // network (32 bytes) + mstore(add(callback, 0x0164), calldataload(add(message, 0x00))) // source (32 bytes) + } + } + mstore(add(callback, 0x0184), 0x80) // payload.offset (32 bytes) + mstore(add(callback, 0x0100), size) // callback.data.length (32 bytes) + { + // Update free memory pointer to the end of the callback (0x0120 + data.length) + mstore(0x40, and(add(add(callback, 0x013f), size), 0xffffffe0)) + } } } diff --git a/src/interfaces/IExecutor.sol b/src/interfaces/IExecutor.sol index 71b18d9..c96c444 100644 --- a/src/interfaces/IExecutor.sol +++ b/src/interfaces/IExecutor.sol @@ -4,15 +4,7 @@ pragma solidity >=0.8.0; import { - Signature, - GmpMessage, - TssKey, - GmpStatus, - GmpStatus, - UpdateKeysMessage, - UpdateNetworkInfo, - GmpSender, - Route + Signature, GmpMessage, TssKey, GmpStatus, GmpStatus, UpdateKeysMessage, GmpSender, Route } from "../Primitives.sol"; /** @@ -44,11 +36,30 @@ interface IExecutor { */ 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 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 GMP message * @param signature Schnorr signature diff --git a/src/storage/Routes.sol b/src/storage/Routes.sol index 7d8decf..32f08b0 100644 --- a/src/storage/Routes.sol +++ b/src/storage/Routes.sol @@ -2,7 +2,7 @@ // Analog's Contracts (last updated v0.1.0) (src/storage/Routes.sol) pragma solidity ^0.8.20; -import {UpdateNetworkInfo, Signature, Network, Route, MAX_PAYLOAD_SIZE} from "../Primitives.sol"; +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"; @@ -168,7 +168,7 @@ library RouteStore { function createOrUpdateRoute(MainStorage storage store, Route calldata route) internal { // Update network info (bool created, NetworkInfo storage stored) = getOrAdd(store, route.networkId); - require(!created || stored.domainSeparator != bytes32(0), "domain separator cannot be zero"); + require((created && route.gateway != bytes32(0)) || !created, "domain separator cannot be zero"); // Verify and update domain separator if it's not zero if (route.gateway != bytes32(0)) { @@ -238,8 +238,8 @@ library RouteStore { gasLimit: route.gasLimit, baseFee: route.baseFee, gateway: route.domainSeparator, - relativeGasPriceNumerator: numerator, - relativeGasPriceDenominator: denominator + relativeGasPriceNumerator: uint128(numerator), + relativeGasPriceDenominator: uint128(denominator) }); } return routes; diff --git a/src/storage/Shards.sol b/src/storage/Shards.sol index aa890a1..f7176de 100644 --- a/src/storage/Shards.sol +++ b/src/storage/Shards.sol @@ -194,9 +194,9 @@ library ShardStore { * Requirements: * - The `newKey` should not be already registered. */ - function register(MainStorage storage store, TssKey calldata newKey) internal { + function register(MainStorage storage store, TssKey calldata newKey) internal returns (bool) { // Check y-parity - require(newKey.yParity == (newKey.yParity & 1), "y parity bit must be 0 or 1, cannot register shard"); + require(newKey.yParity == (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)); @@ -204,7 +204,8 @@ library ShardStore { // Check if the shard is already registered if (!created) { - revert ShardAlreadyRegistered(id); + require(stored.nonce == 1 || newKey.yParity == stored.yParity, "tsskey.yParity mismatch"); + return false; } // Get the current status and nonce @@ -222,7 +223,8 @@ library ShardStore { stored.createdAtBlock = BranchlessMath.ternaryU64(shard.createdAtBlock > 0, shard.createdAtBlock, uint64(block.number)); stored.nonce = shard.nonce; - stored.yParity = newKey.yParity; + stored.yParity = newKey.yParity & 1; + return true; } /** @@ -241,18 +243,82 @@ library ShardStore { } /** - * @dev Register TSS keys. + * @dev Replace TSS keys in batch. + * Requirements: + * - The `keys` should not be already 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 + 2, 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 publicKey) internal { + function revoke(MainStorage storage store, TssKey calldata key) internal { // Read shard from storage - ShardID id = ShardID.wrap(bytes32(publicKey.xCoord)); - ShardInfo memory shard = get(store, id); + ShardID id = ShardID.wrap(bytes32(key.xCoord)); + ShardInfo memory stored = get(store, id); // Check y-parity - require(shard.yParity == shard.yParity, "y parity mismatch, cannot revoke key"); + require(stored.yParity == (key.yParity & 1), "y parity mismatch, cannot revoke key"); + _revoke(store, id); + } + /** + * @dev Revoke Shards keys. + */ + function _revoke(MainStorage storage store, ShardID id) private { // Remove from the set store.shards.remove(ShardID.unwrap(id)); } @@ -269,6 +335,8 @@ library ShardStore { } } + function _t(MainStorage storage store) internal view returns (TssKey[] memory) {} + /** * @dev Return all shards registered currently registered. * @@ -278,15 +346,16 @@ library ShardStore { * 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[] memory idx = store.shards.keys; - TssKey[] memory shards = new TssKey[](idx.length); - for (uint256 i = 0; i < idx.length; i++) { + 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, uint256(ShardID.unwrap(id))); + shards[i] = TssKey(shard.yParity + 2, uint256(ShardID.unwrap(id))); } return shards; } diff --git a/src/utils/BranchlessMath.sol b/src/utils/BranchlessMath.sol index 7c2bc1c..4022873 100644 --- a/src/utils/BranchlessMath.sol +++ b/src/utils/BranchlessMath.sol @@ -325,6 +325,15 @@ library BranchlessMath { } } + /** + * @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. diff --git a/src/utils/GasUtils.sol b/src/utils/GasUtils.sol index ac618f8..817126e 100644 --- a/src/utils/GasUtils.sol +++ b/src/utils/GasUtils.sol @@ -10,22 +10,22 @@ 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 Base cost of the `IExecutor.execute` method. + * @dev How much gas is used until the first `gasleft()` instruction is executed. */ - uint256 internal constant EXECUTION_BASE_COST = 46658; + uint256 internal constant EXECUTION_SELECTOR_OVERHEAD = 496; /** - * @dev Initial amount of memory used by `IExecutor.execute` method. + * @dev Base cost of the `IExecutor.execute` method. */ - uint256 internal constant MEMORY_OFFSET = 0x3c0; + uint256 internal constant EXECUTION_BASE_COST = EXECUTION_SELECTOR_OVERHEAD + 45593; /** * @dev Base cost of the `IGateway.submitMessage` method. */ - uint256 internal constant SUBMIT_BASE_COST = 25802; - - using BranchlessMath for uint256; + uint256 internal constant SUBMIT_BASE_COST = 25818 - 20; /** * @dev Compute the gas cost of memory expansion. @@ -45,11 +45,12 @@ library GasUtils { function proxyOverheadGasCost(uint256 calldataLen, uint256 returnLen) internal pure returns (uint256) { unchecked { // Convert the calldata and return data length to words - calldataLen = calldataLen.saturatingAdd(31) >> 5; - returnLen = returnLen.saturatingAdd(31) >> 5; + calldataLen = _toWord(calldataLen); + returnLen = _toWord(returnLen); - // Base cost: OPCODES + COLD SLOAD + COLD DELEGATECALL - uint256 gasCost = 57 + 2100 + 2600; + // 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); @@ -57,8 +58,8 @@ library GasUtils { // RETURNDATACOPY gasCost = gasCost.saturatingAdd(returnLen * 3); - // MEMORY EXPANSION - uint256 words = BranchlessMath.max(calldataLen, returnLen); + // MEMORY EXPANSION (minimal 3 due mstore(0x40, 0x80)) + uint256 words = calldataLen.max(returnLen).max(3); gasCost = gasCost.saturatingAdd(memoryExpansionGasCost(words)); return gasCost; } @@ -86,7 +87,7 @@ library GasUtils { gasCost += words << 8; // Memory expansion cost - words += 13 + 4; + words += 17; gasCost += ((words * words) >> 9) + (words * 3); return gasCost; @@ -176,35 +177,129 @@ library GasUtils { } } - function _executionGasCost(uint256 messageSize, uint256 gasUsed) private pure returns (uint256) { - // Add the base execution gas cost - uint256 gas = EXECUTION_BASE_COST.saturatingAdd(gasUsed); + /** + * @dev Compute the number of words. + */ + function _toWord(uint256 x) private pure returns (uint256 r) { + assembly { + r := add(shr(5, x), gt(and(x, 0x1f), 0)) + } + } - // Safety: The operations below can't overflow because the message size can't be greater than 2**16 + function _debugExecutionGasCost(uint256 messageSize, uint256 gasUsed) internal pure returns (uint256) { unchecked { - // Add padding to the message size, making it a multiple of 32 - messageSize = (uint256(messageSize) + 31) & 0xffffe0; - - // selector + Signature + GmpMessage - uint256 words = messageSize.saturatingAdd(388 + 31) >> 5; - - // Add `countZeros` gas cost - gas = gas.saturatingAdd((words * 106) + (((words + 254) / 255) * 214)); + // Selector overhead + // -- First GAS opcode + uint256 baseCost = EXECUTION_SELECTOR_OVERHEAD - 9; + uint256 memoryExpansion = 0x60; + // -- First GAS opcode + + // all opcodes until message.intoCallback(DOMAIN_SEPARATOR) + 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() - // calldatacopy (3 gas per word) - words = messageSize >> 5; - gas = gas.saturatingAdd(words * 3); + baseCost += 482; + // ----- GAS ------- - // keccak256 (6 gas per word) - gas = gas.saturatingAdd(words * 6); + baseCost += 168; // GAS + baseCost += 6800; // REFUND CALL + baseCost += 184; // RETURN - // Memory expansion cost - words = 0xa4 + (words << 5); // onGmpReceived encoded call size - words = (words + 31) & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0; - words += MEMORY_OFFSET; // Memory size - words = (words + 31) >> 5; // to words - gas = gas.saturatingAdd(((words * words) >> 9) + (words * 3)); + 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).align32() + 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; } } @@ -229,14 +324,13 @@ library GasUtils { */ function executionGasNeeded(uint256 messageSize, uint256 gasLimit) internal pure returns (uint256 gasNeeded) { unchecked { - gasNeeded = inverseOfAllButOne64th(gasLimit); - gasNeeded = gasNeeded.saturatingAdd(_executionGasCost(messageSize, gasLimit)); - gasNeeded = gasNeeded.saturatingAdd(2114 + 2); + gasNeeded = _executionGasCost(messageSize, gasLimit); + gasNeeded = gasNeeded.saturatingAdd(2300 - 184); gasNeeded = inverseOfAllButOne64th(gasNeeded); - messageSize = (uint256(messageSize).saturatingAdd(31) >> 5) << 5; - messageSize = messageSize.saturatingAdd(388); + messageSize = messageSize.align32().saturatingAdd(388); gasNeeded = gasNeeded.saturatingAdd(proxyOverheadGasCost(messageSize, 64)); - gasNeeded = gasNeeded.saturatingSub(39); + // Remove the proxy final overhead, once the message requires (2300 - 184) extra gas. + gasNeeded = gasNeeded.saturatingSub(38); } } diff --git a/src/utils/Schnorr.sol b/src/utils/Schnorr.sol new file mode 100644 index 0000000..b72a02c --- /dev/null +++ b/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/test/Example.t.sol b/test/Example.t.sol index b5a82b2..f58234c 100644 --- a/test/Example.t.sol +++ b/test/Example.t.sol @@ -9,7 +9,7 @@ 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} from "./TestUtils.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"; @@ -28,12 +28,13 @@ import { contract ExampleTest is Test { using SigningUtils for SigningKey; - using SigningUtils for VerifyingKey; + 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"))); @@ -44,23 +45,24 @@ contract ExampleTest is Test { vm.deal(BOB, 100 ether); } - function deployGateway(VerifyingKey memory pubkey, uint16[] memory networkIds) + 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: pubkey.yParity() == 28 ? 1 : 0, xCoord: pubkey.px}); + keys[0] = TssKey({yParity: signer.pubkey.yParity() == 28 ? 1 : 0, 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 = vm.computeCreateAddress(_sender, vm.getNonce(_sender) + 1 + (i * 2)); + 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)); + // bytes memory initializer = abi.encodeCall(Gateway.initialize, (msg.sender, keys, networks)); for (uint256 i = 0; i < networks.length; i++) { - address implementation = address(new Gateway(networks[i].id, networks[i].gateway)); - address proxy = address(new GatewayProxy(implementation, initializer)); + 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); } @@ -75,8 +77,9 @@ contract ExampleTest is Test { function testTeleportTokens() external { vm.txGasPrice(1); - _sender = TestUtils.createTestAccount(100 ether); - vm.startPrank(_sender, _sender); + 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(); @@ -86,19 +89,20 @@ contract ExampleTest is Test { uint16[] memory networkIds = new uint16[](2); networkIds[0] = SRC_NETWORK_ID; networkIds[1] = DEST_NETWORK_ID; - Network[] memory networks = deployGateway(signer.pubkey, networkIds); + Network[] memory networks = deployGateway(senderWallet, signer, networkIds); srcGateway = Gateway(networks[0].gateway); dstGateway = Gateway(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(true); + GmpSender source = address(srcToken).toSender(false); GmpMessage memory gmp = GmpMessage({ source: source, srcNetwork: SRC_NETWORK_ID, diff --git a/test/GasUtils.t.sol b/test/GasUtils.t.sol index 08fdcd0..bf70ca3 100644 --- a/test/GasUtils.t.sol +++ b/test/GasUtils.t.sol @@ -65,27 +65,17 @@ contract GasUtilsBase is Test { uint16 internal constant DEST_NETWORK_ID = 1337; constructor() { + // Create the Shard and Admin accounts signer = new Signer(secret); - address deployer = TestUtils.createTestAccount(100 ether); - vm.startPrank(deployer, deployer); + VmSafe.Wallet memory deployer = vm.createWallet(secret); + vm.deal(deployer.addr, 100 ether); // Deploy the GasUtilsMock contract mock = new GasUtilsMock(); - // 1 - Deploy the implementation contract - address proxyAddr = vm.computeCreateAddress(deployer, vm.getNonce(deployer) + 1); - Gateway implementation = new Gateway(DEST_NETWORK_ID, proxyAddr); - - // 2 - Deploy the Proxy Contract - 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 - bytes memory initializer = abi.encodeCall(Gateway.initialize, (msg.sender, keys, networks)); - gateway = Gateway(address(new GatewayProxy(address(implementation), initializer))); + // Deploy the GatewayProxy + gateway = + Gateway(address(TestUtils.setupGateway(deployer, bytes32(uint256(0)), SRC_NETWORK_ID, DEST_NETWORK_ID))); vm.deal(address(gateway), 100 ether); _srcDomainSeparator = GatewayUtils.computeDomainSeparator(SRC_NETWORK_ID, address(gateway)); @@ -98,8 +88,6 @@ contract GasUtilsBase is Test { hex"603c80600a5f395ff3fe5a600201803d523d60209160643560240135146018575bfd5b60365a116018575a604903565b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5bf3"; receiver = IGmpReceiver(TestUtils.deployContract(bytecode)); } - - vm.stopPrank(); } function sign(GmpMessage memory gmp) internal view returns (Signature memory) { @@ -220,31 +208,45 @@ contract GasUtilsBase is Test { 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 = - GasUtils.computeExecutionRefund(uint16(gmp.data.length), gmp.gasLimit) - GasUtils.EXECUTION_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), 31783 + baseCost); - assertEq(GasUtils.estimateGas(0, 33, 0), 32155 + baseCost); - assertEq(GasUtils.estimateGas(33, 0, 0), 32815 + baseCost); - assertEq(GasUtils.estimateGas(20, 13, 0), 32555 + baseCost); + 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), 31783 + baseCost); - assertEq(GasUtils.estimateWeiCost(one, 0, 0, 33, 0), 32155 + baseCost); - assertEq(GasUtils.estimateWeiCost(one, 0, 33, 0, 0), 32815 + baseCost); - assertEq(GasUtils.estimateWeiCost(one, 0, 20, 13, 0), 32555 + baseCost); + 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), (31783 + baseCost) * 2); - assertEq(GasUtils.estimateWeiCost(two, 0, 0, 33, 0), (32155 + baseCost) * 2); - assertEq(GasUtils.estimateWeiCost(two, 0, 33, 0, 0), (32815 + baseCost) * 2); - assertEq(GasUtils.estimateWeiCost(two, 0, 20, 13, 0), (32555 + baseCost) * 2); + 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/test/Gateway.t.sol b/test/Gateway.t.sol index 3c645e3..5ce9f6a 100644 --- a/test/Gateway.t.sol +++ b/test/Gateway.t.sol @@ -3,10 +3,9 @@ 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 {TestUtils, SigningKey, SigningUtils} from "./TestUtils.sol"; import {Gateway, GatewayEIP712} from "../src/Gateway.sol"; import {GatewayProxy} from "../src/GatewayProxy.sol"; import {GasUtils} from "../src/utils/GasUtils.sol"; @@ -26,9 +25,6 @@ import { GmpSender } from "../src/Primitives.sol"; -uint256 constant secret = 0x42; -uint256 constant nonce = 0x69; - contract SigUtilsTest is GatewayEIP712, Test { using PrimitiveUtils for GmpMessage; @@ -62,6 +58,28 @@ struct CallOptions { } 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) @@ -126,9 +144,13 @@ contract GatewayBase is Test { using PrimitiveUtils for address; using GatewayUtils for CallOptions; using BranchlessMath for uint256; + using SigningUtils for SigningKey; Gateway internal gateway; - Signer internal signer; + + // 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; @@ -141,31 +163,15 @@ contract GatewayBase is Test { uint16 private constant SRC_NETWORK_ID = 1234; uint16 internal constant DEST_NETWORK_ID = 1337; - constructor() { - signer = new Signer(secret); - address deployer = TestUtils.createTestAccount(100 ether); - vm.startPrank(deployer, deployer); - - // 1 - Deploy the implementation contract - address proxyAddr = vm.computeCreateAddress(deployer, vm.getNonce(deployer) + 1); - Gateway implementation = new Gateway(DEST_NETWORK_ID, proxyAddr); - - // 2 - Deploy the Proxy Contract - 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 - bytes memory initializer = abi.encodeCall(Gateway.initialize, (msg.sender, keys, networks)); - gateway = Gateway(address(new GatewayProxy(address(implementation), initializer))); - vm.deal(address(gateway), 100 ether); + address internal constant ADMIN = 0x6f4c950442e1Af093BcfF730381E63Ae9171b87a; + constructor() { + VmSafe.Wallet memory admin = vm.createWallet(SECRET); + assertEq(ADMIN, admin.addr, "admin address mismatch"); + gateway = + Gateway(address(TestUtils.setupGateway(admin, bytes32(uint256(1234)), SRC_NETWORK_ID, DEST_NETWORK_ID))); _srcDomainSeparator = GatewayUtils.computeDomainSeparator(SRC_NETWORK_ID, address(gateway)); _dstDomainSeparator = GatewayUtils.computeDomainSeparator(DEST_NETWORK_ID, address(gateway)); - - vm.stopPrank(); } function setUp() public { @@ -189,11 +195,69 @@ contract GatewayBase is Test { } else { domainSeparator = _dstDomainSeparator; } - uint256 hash = uint256(gmp.eip712TypedHash(domainSeparator)); - (uint256 e, uint256 s) = signer.signPrehashed(hash, nonce); + bytes32 hash = gmp.eip712TypedHash(domainSeparator); + 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 _shortTssKeys(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_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()}); + } + _shortTssKeys(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(); + _shortTssKeys(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; + _shortTssKeys(keys); + vm.prank(ADMIN, ADMIN); + gateway.setShards(keys); + + // Check shards keys + shards = gateway.shards(); + _shortTssKeys(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_Receiver() external { bytes memory testEncodedCall = abi.encodeCall( IGmpReceiver.onGmpReceived, @@ -215,7 +279,7 @@ contract GatewayBase is Test { function test_estimateMessageCost() external { vm.txGasPrice(1); uint256 cost = gateway.estimateMessageCost(DEST_NETWORK_ID, 96, 100000); - assertEq(cost, GasUtils.EXECUTION_BASE_COST + 134075); + assertEq(cost, GasUtils.EXECUTION_BASE_COST + 133821); } function test_checkPayloadSize() external { @@ -262,7 +326,7 @@ contract GatewayBase is Test { * @dev Test the gas metering for the `execute` function. */ function test_gasMeter(uint16 messageSize) external { - vm.assume(messageSize <= 0x6000); + vm.assume(messageSize <= 0x6000 && messageSize >= 32); vm.txGasPrice(1); address sender = TestUtils.createTestAccount(100 ether); @@ -270,16 +334,23 @@ contract GatewayBase is Test { GmpMessage memory gmp = GmpMessage({ source: sender.toSender(false), srcNetwork: SRC_NETWORK_ID, - dest: address(bytes20(keccak256("dummy_address"))), + dest: address(receiver), destNetwork: DEST_NETWORK_ID, - gasLimit: 0, + gasLimit: 1000, salt: 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({ @@ -306,6 +377,8 @@ contract GatewayBase is Test { // 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"); diff --git a/test/GatewayProxy.t.sol b/test/GatewayProxy.t.sol new file mode 100644 index 0000000..ccac9ad --- /dev/null +++ b/test/GatewayProxy.t.sol @@ -0,0 +1,128 @@ +// 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 {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 The `GatewayProxy` contract admin. + // */ + // address private constant ADMIN = 0x6f4c950442e1Af093BcfF730381E63Ae9171b87a; + + /** + * @dev his is a special contract that wastes an exact amount of gas you send to it, helpful for testing GMP refunds and gas limits. + * See the file `HelperContract.opcode` for more details. + */ + bytes private constant RECEIVER_BYTECODE = + hex"603c80600a5f395ff3fe5a600201803d523d60209160643560240135146018575bfd5b60365a116018575a604903565b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5bf3"; + + // Receiver Contract, the will waste the exact amount of gas you sent to it in the data field + IGmpReceiver internal receiver; + + constructor() { + require(FACTORY == TestUtils.deployFactory(), "factory address mismatch"); + } + + 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 implementation = 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)); + gateway = Gateway(FACTORY.create2(salt, proxyCreationCode, authorization, initializer)); + + // 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/test/GmpTestTools.sol b/test/GmpTestTools.sol index f13c214..0387568 100644 --- a/test/GmpTestTools.sol +++ b/test/GmpTestTools.sol @@ -11,6 +11,7 @@ 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 { /** @@ -100,6 +101,9 @@ library GmpTestTools { } 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}); @@ -108,7 +112,16 @@ library GmpTestTools { bool exists = gateway.code.length > 0; // Deploy the gateway proxy - address implementation = address(new Gateway(networkId, gateway)); + 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)))); diff --git a/test/TestUtils.sol b/test/TestUtils.sol index 621a64c..1e7e1cf 100644 --- a/test/TestUtils.sol +++ b/test/TestUtils.sol @@ -4,9 +4,25 @@ 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; @@ -23,12 +39,27 @@ struct SigningKey { */ 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 */ @@ -179,9 +210,16 @@ library TestUtils { * @dev Creates a new TSS signer */ function signerFromEntropy(bytes32 entropy) internal pure returns (SigningKey memory) { - uint256 secret = uint256(entropy); + uint256 secret; + assembly { + mstore(0, entropy) + secret := keccak256(0x00, 0x20) + } while (secret >= Schnorr.Q) { - secret = uint256(keccak256(abi.encodePacked(secret))); + assembly { + mstore(0, secret) + secret := keccak256(0x00, 0x20) + } } return createSigner(secret); } @@ -228,6 +266,19 @@ library TestUtils { 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 { @@ -248,7 +299,6 @@ library TestUtils { } // Execute - bool success; { (VmSafe.CallerMode callerMode, address msgSender, address txOrigin) = setCallerMode(VmSafe.CallerMode.RecurrentPrank, sender, sender); @@ -261,11 +311,6 @@ library TestUtils { if (refund > 0) { vm.deal(sender, sender.balance + refund); } - - // Revert if the execution failed - assembly { - if iszero(success) { revert(add(out, 32), mload(out)) } - } } function setCallerMode(VmSafe.CallerMode callerMode, address msgSender, address txOrigin) @@ -307,23 +352,162 @@ library TestUtils { 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 Computes the EIP-712 domain separador + */ + function computeDomainSeparator(uint256 networkId, address addr) internal 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 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)); + gateway = Gateway(FACTORY.create2(salt, proxyCreationCode, authorization, initializer)); + + // 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 ? 1 : 0, 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 SigningUtils { +library VerifyingUtils { 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 yParity(SigningKey memory signer) internal pure returns (uint8) { - return yParity(signer.pubkey); + return uint8(signer.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 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 challenge(signer.pubkey, hash, r); + return uint256(keccak256(abi.encodePacked(r, yParity(signer), signer.pubkey.px, uint256(hash)))); } function signPrehashed(SigningKey memory signer, bytes32 hash, uint256 nonce) @@ -346,28 +530,12 @@ library SigningUtils { return signPrehashed(signer, keccak256(message), nonce); } - 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); - } - function verifyPrehash(SigningKey memory signer, bytes32 prehash, uint256 c, uint256 z) internal pure returns (bool) { - return verifyPrehash(signer.pubkey, prehash, c, z); + return Schnorr.verify(yParity(signer), signer.pubkey.px, uint256(prehash), c, z); } function verify(SigningKey memory signer, bytes memory message, uint256 c, uint256 z) @@ -375,6 +543,6 @@ library SigningUtils { pure returns (bool) { - return verifyPrehash(signer.pubkey, keccak256(message), c, z); + return verifyPrehash(signer, keccak256(message), c, z); } } diff --git a/test/utils/GmpProxy.sol b/test/utils/GmpProxy.sol new file mode 100644 index 0000000..0f52d14 --- /dev/null +++ b/test/utils/GmpProxy.sol @@ -0,0 +1,74 @@ +// 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(bytes32 indexed id, GmpMessage msg); + + struct GmpMessage { + uint16 srcNetwork; + uint16 destNetwork; + bytes32 src; + bytes32 dest; + uint64 nonce; + uint128 gasLimit; + uint128 gasCost; + 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); + address destination = address(uint160(uint256(message.dest))); + GATEWAY.submitMessage{value: value}(destination, 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. + ( + uint16 srcNetwork, + uint16 destNetwork, + bytes32 src, + bytes32 dest, + uint64 nonce, + uint128 gasLimit, + uint128 gasCost, + bytes memory data + ) = abi.decode(payload, (uint16, uint16, bytes32, bytes32, uint64, uint128, uint128, bytes)); + + GmpMessage memory message = GmpMessage({ + srcNetwork: srcNetwork, + destNetwork: destNetwork, + src: src, + dest: dest, + nonce: nonce, + gasLimit: gasLimit, + gasCost: gasCost, + data: data + }); + message.data = payload; + + emit MessageReceived(id, message); + return id; + } +}