From 3be877ece29c0f18a2e1836915c9ddb7e8347efd Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Tue, 12 Nov 2024 20:16:33 -0300 Subject: [PATCH 01/35] Update events --- src/Gateway.sol | 64 +++++++++++------ src/Primitives.sol | 14 ++++ src/storage/Routes.sol | 63 ++++++++++------- src/storage/Shards.sol | 155 +++++++++++++++++++---------------------- src/utils/GasUtils.sol | 4 +- 5 files changed, 169 insertions(+), 131 deletions(-) diff --git a/src/Gateway.sol b/src/Gateway.sol index df1b724..475c0fd 100644 --- a/src/Gateway.sol +++ b/src/Gateway.sol @@ -61,9 +61,6 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { using UFloatMath for UFloat9x56; using ShardStore for ShardStore.MainStorage; - uint8 internal constant SHARD_ACTIVE = (1 << 0); // Shard active bitflag - uint8 internal constant SHARD_Y_PARITY = (1 << 1); // Pubkey y parity bitflag - /** * @dev Maximum size of the GMP payload */ @@ -156,7 +153,7 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { return _messages[id]; } - function keyInfo(bytes32 id) external view returns (ShardStore.KeyInfo memory) { + function keyInfo(bytes32 id) external view returns (ShardStore.ShardInfo memory) { ShardStore.MainStorage storage store = ShardStore.getMainStorage(); return store.get(ShardStore.ShardID.wrap(id)); } @@ -178,19 +175,11 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { */ function _verifySignature(Signature calldata signature, bytes32 message) private view { // Load shard from storage - ShardStore.KeyInfo storage signer; - { - ShardStore.MainStorage storage store = ShardStore.getMainStorage(); - signer = store.get(signature); - } - - // Verify if shard is active - uint8 status = signer.status; - require((status & SHARD_ACTIVE) > 0, "shard key revoked or not exists"); + 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((status & SHARD_Y_PARITY) > 0, 28, 27); + uint8 yParity = BranchlessMath.ternaryU8(signer.yParity > 0, 28, 27); // Verify Signature require( @@ -416,7 +405,44 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { // emit GmpCreated(prevHash, source, destinationAddress, destinationNetwork, executionGasLimit, salt, data); // return prevHash; // ``` - bytes32 eventSelector = GmpCreated.selector; + // bytes32 eventSelector = GmpCreated.selector; + _emitGmpCreated(prevHash, source, destinationAddress, destinationNetwork, executionGasLimit, salt, payload); + // assembly { + // let ptr := sub(payload, 0x80) + // mstore(ptr, destinationNetwork) // dest network + // mstore(add(ptr, 0x20), executionGasLimit) // gas limit + // mstore(add(ptr, 0x40), salt) // salt + // mstore(add(ptr, 0x60), 0x80) // data offset + // let size := and(add(mload(payload), 31), 0xffffffe0) + // size := add(size, 160) + // log4(ptr, size, eventSelector, prevHash, source, destinationAddress) + // mstore(0, prevHash) + // return(0, 32) + // } + } + + /** + * @dev Selector of `GmpCreated` event. + * keccak256("GmpCreated(bytes32,bytes32,address,uint16,uint256,uint256,bytes)"); + */ + bytes32 private constant GMP_CREATED_EVENT_SELECTOR = + 0x0114885f90b5168242aa31b7afb9c2e9f88e90ce329c893d3e6c56021c4c03a5; + + function _emitGmpCreated( + bytes32 prevHash, + GmpSender source, + address destinationAddress, + uint16 destinationNetwork, + uint256 executionGasLimit, + uint256 salt, + 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, salt, data); + // return prevHash; + // ``` assembly { let ptr := sub(payload, 0x80) mstore(ptr, destinationNetwork) // dest network @@ -425,7 +451,7 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { mstore(add(ptr, 0x60), 0x80) // data offset let size := and(add(mload(payload), 31), 0xffffffe0) size := add(size, 160) - log4(ptr, size, eventSelector, prevHash, source, destinationAddress) + log4(ptr, size, GMP_CREATED_EVENT_SELECTOR, prevHash, source, destinationAddress) mstore(0, prevHash) return(0, 32) } @@ -589,15 +615,13 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { // OBS: remove != revoke (when revoked, you cannot register again) function sudoRemoveShards(TssKey[] memory revokedKeys) external payable { require(msg.sender == _getAdmin(), "unauthorized"); - ShardStore.MainStorage storage shards = ShardStore.getMainStorage(); - shards.revokeKeys(revokedKeys); + ShardStore.getMainStorage().revokeKeys(revokedKeys); emit KeySetChanged(bytes32(0), revokedKeys, new TssKey[](0)); } function sudoAddShards(TssKey[] memory newKeys) external payable { require(msg.sender == _getAdmin(), "unauthorized"); - ShardStore.MainStorage storage shards = ShardStore.getMainStorage(); - shards.registerTssKeys(newKeys); + ShardStore.getMainStorage().registerTssKeys(newKeys); emit KeySetChanged(bytes32(0), new TssKey[](0), newKeys); } diff --git a/src/Primitives.sol b/src/Primitives.sol index 97ed938..bc9e4f6 100644 --- a/src/Primitives.sol +++ b/src/Primitives.sol @@ -104,6 +104,20 @@ enum GmpStatus { PENDING } +struct GmpCallback { + GmpSender sender; + address dest; + uint16 destNetwork; + uint256 gasLimit; + uint256 salt; + bytes data; +} + +/** + * @dev EIP-712 utility for `GmpCallback` + */ +library GmpCallbackUtils {} + /** * @dev EIP-712 utility functions for primitives */ diff --git a/src/storage/Routes.sol b/src/storage/Routes.sol index cd48497..5c1ad80 100644 --- a/src/storage/Routes.sol +++ b/src/storage/Routes.sol @@ -17,6 +17,7 @@ library RouteStore { using Pointer for uint256; using EnumerableSet for EnumerableSet.Map; using NetworkIDHelpers for NetworkID; + using UFloatMath for UFloat9x56; /** * @dev Namespace of the routes storage `analog.one.gateway.routes`. @@ -24,9 +25,6 @@ library RouteStore { */ bytes32 internal constant _EIP7201_NAMESPACE = 0xb184f2aad520cf7f1f1270909517c75ae33cdf2bd7d32b997a96577f11a48800; - uint8 internal constant SHARD_ACTIVE = (1 << 0); // Shard active bitflag - uint8 internal constant SHARD_Y_PARITY = (1 << 1); // Pubkey y parity bitflag - /** * @dev Network info stored in the Gateway Contract * @param domainSeparator Domain EIP-712 - Replay Protection Mechanism. @@ -41,6 +39,23 @@ library RouteStore { uint128 baseFee; } + /** + * @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; + uint256 relativeGasPriceNumerator; + uint256 relativeGasPriceDenominator; + } + /** * @dev Network info stored in the Gateway Contract * @param id Message unique id. @@ -81,25 +96,15 @@ library RouteStore { } } - function asPtr(NetworkInfo storage keyInfo) internal pure returns (StoragePtr ptr) { - assembly { - ptr := keyInfo.slot - } - } - - function _ptrToRoute(StoragePtr ptr) private pure returns (NetworkInfo storage route) { + /** + * @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 contains(MainStorage storage store, NetworkInfo storage keyInfo) internal view returns (bool) { - return store.routes.contains(asPtr(keyInfo)); - } - /** * @dev Returns true if the value is in the set. O(1). */ @@ -115,7 +120,7 @@ library RouteStore { */ 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, _ptrToRoute(ptr)); + return (success, pointerToRoute(ptr)); } /** @@ -154,7 +159,7 @@ library RouteStore { if (ptr.isNull()) { revert IndexOutOfBounds(index); } - return _ptrToRoute(ptr); + return pointerToRoute(ptr); } /** @@ -168,7 +173,7 @@ library RouteStore { if (ptr.isNull()) { revert ShardNotExists(id); } - return _ptrToRoute(ptr); + return pointerToRoute(ptr); } /** @@ -176,7 +181,7 @@ library RouteStore { */ 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, _ptrToRoute(ptr)); + return (exists, pointerToRoute(ptr)); } function createOrUpdateNetworkInfo(MainStorage storage store, bytes32 messageHash, UpdateNetworkInfo calldata info) @@ -250,11 +255,21 @@ library RouteStore { * 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 (NetworkInfo[] memory) { + function listRoutes(MainStorage storage store) internal view returns (Route[] memory) { bytes32[] memory idx = store.routes.keys; - NetworkInfo[] memory routes = new NetworkInfo[](idx.length); + Route[] memory routes = new Route[](idx.length); for (uint256 i = 0; i < idx.length; i++) { - routes[i] = _ptrToRoute(store.routes.values[idx[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: route.domainSeparator, + relativeGasPriceNumerator: numerator, + relativeGasPriceDenominator: denominator + }); } return routes; } diff --git a/src/storage/Shards.sol b/src/storage/Shards.sol index 30dddcc..11778dd 100644 --- a/src/storage/Shards.sol +++ b/src/storage/Shards.sol @@ -7,6 +7,30 @@ 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 */ @@ -14,6 +38,9 @@ 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`. @@ -29,15 +56,6 @@ library ShardStore { */ type ShardID is bytes32; - /** - * @dev Current status of the shard - */ - enum ShardStatus { - Unregistered, - Active, - Revoked - } - /** * @dev Shard info stored in the Gateway Contract * OBS: the order of the attributes matters! ethereum storage is 256bit aligned, try to keep @@ -46,9 +64,10 @@ library ShardStore { * * @custom:storage-location erc7201:analog.one.gateway.shards */ - struct KeyInfo { - uint8 status; + struct ShardInfo { + uint8 yParity; uint32 nonce; + uint64 createdAtBlock; } /** @@ -73,25 +92,6 @@ library ShardStore { } } - function asPtr(KeyInfo storage keyInfo) internal pure returns (StoragePtr ptr) { - assembly { - ptr := keyInfo.slot - } - } - - function _getKeyInfo(StoragePtr ptr) private pure returns (KeyInfo storage keyInfo) { - assembly { - keyInfo.slot := ptr - } - } - - /** - * @dev Returns true if the value is in the set. O(1). - */ - function contains(MainStorage storage store, KeyInfo storage keyInfo) internal view returns (bool) { - return store.shards.contains(asPtr(keyInfo)); - } - /** * @dev Returns true if the value is in the set. O(1). */ @@ -105,25 +105,21 @@ library ShardStore { * 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, KeyInfo storage) { + function getOrAdd(MainStorage storage store, ShardID xCoord) private returns (bool, ShardInfo storage) { (bool success, StoragePtr ptr) = store.shards.tryAdd(ShardID.unwrap(xCoord)); - return (success, _getKeyInfo(ptr)); + return (success, ptr.asShardInfo()); } /** * @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. + * Reverts if the value does not exist in the set. */ - function remove(MainStorage storage store, ShardID id) internal returns (bool) { + function remove(MainStorage storage store, ShardID id) internal { StoragePtr ptr = store.shards.remove(ShardID.unwrap(id)); if (ptr.isNull()) { - return false; + revert ShardNotExists(id); } - KeyInfo storage keyInfo = _getKeyInfo(ptr); - keyInfo.status &= ~SHARD_ACTIVE; - return true; } /** @@ -143,12 +139,12 @@ library ShardStore { * * - `index` must be strictly less than {length}. */ - function at(MainStorage storage store, uint256 index) internal view returns (KeyInfo storage) { + function at(MainStorage storage store, uint256 index) internal view returns (ShardInfo storage) { StoragePtr ptr = store.shards.at(index); if (ptr.isNull()) { revert IndexOutOfBounds(index); } - return _getKeyInfo(ptr); + return ptr.asShardInfo(); } /** @@ -157,12 +153,12 @@ library ShardStore { * Requirements: * - `key` must be in the map. */ - function get(MainStorage storage store, ShardID key) internal view returns (KeyInfo storage) { + 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 _getKeyInfo(ptr); + return ptr.asShardInfo(); } /** @@ -171,7 +167,7 @@ library ShardStore { * Requirements: * - `key.xCoord` must be in the map. */ - function get(MainStorage storage store, TssKey calldata key) internal view returns (KeyInfo storage) { + function get(MainStorage storage store, TssKey calldata key) internal view returns (ShardInfo storage) { return get(store, ShardID.wrap(bytes32(key.xCoord))); } @@ -181,16 +177,16 @@ library ShardStore { * Requirements: * - `signature.xCoord` must be in the map. */ - function get(MainStorage storage store, Signature calldata signature) internal view returns (KeyInfo storage) { + 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) internal view returns (bool, KeyInfo storage) { + 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, _getKeyInfo(ptr)); + return (exists, ptr.asShardInfo()); } /** @@ -204,36 +200,35 @@ library ShardStore { // Register or activate tss key (revoked keys keep the previous nonce) for (uint256 i = 0; i < keys.length; i++) { TssKey memory newKey = keys[i]; - uint8 yParity = newKey.yParity; - require(yParity == (yParity & 1), "y parity bit must be 0 or 1, cannot register shard"); + + // Check y-parity + require(newKey.yParity == (newKey.yParity & 1), "y parity bit must be 0 or 1, cannot register shard"); // Read shard from storage ShardID id = ShardID.wrap(bytes32(newKey.xCoord)); - (bool success, KeyInfo storage shard) = getOrAdd(store, id); + (bool success, ShardInfo storage stored) = getOrAdd(store, id); // Check if the shard is already registered if (!success) { revert ShardAlreadyRegistered(id); } - uint32 nonce = shard.nonce; - uint8 status = shard.status; - { - uint8 actualYParity = uint8(BranchlessMath.toUint((status & SHARD_Y_PARITY) > 0)); - require( - nonce == 0 || actualYParity == yParity, - "the provided y-parity doesn't match the existing y-parity, cannot register shard" - ); - nonce += uint32(BranchlessMath.toUint(nonce == 0)); - } + // Get the current status and nonce + ShardInfo memory shard = stored; - // enable/disable the y-parity flag - status = BranchlessMath.ternaryU8(yParity > 0, status | SHARD_Y_PARITY, status & ~SHARD_Y_PARITY); - status |= SHARD_ACTIVE; + require( + shard.createdAtBlock == 0 || shard.yParity == 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 - shard.status = status; - shard.nonce = nonce; + stored.createdAtBlock = + BranchlessMath.ternaryU64(shard.createdAtBlock > 0, shard.createdAtBlock, uint64(block.number)); + stored.nonce = shard.nonce; + stored.yParity = newKey.yParity; } } } @@ -252,24 +247,10 @@ library ShardStore { // Read shard from storage ShardID id = ShardID.wrap(bytes32(revokedKey.xCoord)); - KeyInfo storage shard; - { - bool shardExists; - (shardExists, shard) = tryGet(store, id); - - if (!shardExists || shard.nonce == 0) { - revert ShardNotExists(id); - } - } + ShardInfo memory shard = get(store, id); // Check y-parity - { - uint8 yParity = (shard.status & SHARD_Y_PARITY) > 0 ? 1 : 0; - require(yParity == revokedKey.yParity, "y parity bit mismatch, cannot revoke key"); - } - - // Disable SHARD_ACTIVE bitflag - shard.status = shard.status & (~SHARD_ACTIVE); // Disable active flag + require(shard.yParity == revokedKey.yParity, "y parity mismatch, cannot revoke key"); // Remove from the set store.shards.remove(ShardID.unwrap(id)); @@ -287,11 +268,15 @@ library ShardStore { */ function listShards(MainStorage storage store) internal view returns (TssKey[] memory) { bytes32[] memory idx = store.shards.keys; - TssKey[] memory keys = new TssKey[](idx.length); + TssKey[] memory shards = new TssKey[](idx.length); for (uint256 i = 0; i < idx.length; i++) { - KeyInfo storage keyInfo = _getKeyInfo(store.shards.values[idx[i]]); - keys[i] = TssKey(keyInfo.status & SHARD_Y_PARITY, uint256(idx[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))); } - return keys; + return shards; } } diff --git a/src/utils/GasUtils.sol b/src/utils/GasUtils.sol index 0d5f12c..750e2cc 100644 --- a/src/utils/GasUtils.sol +++ b/src/utils/GasUtils.sol @@ -13,12 +13,12 @@ library GasUtils { /** * @dev Base cost of the `IExecutor.execute` method. */ - uint256 internal constant EXECUTION_BASE_COST = 44469 + 2245 - 11; + uint256 internal constant EXECUTION_BASE_COST = 46667; /** * @dev Base cost of the `IGateway.submitMessage` method. */ - uint256 internal constant SUBMIT_BASE_COST = 23181; + uint256 internal constant SUBMIT_BASE_COST = 23206; using BranchlessMath for uint256; From 558e0f243850fde0502c0112a75b921408eccb35 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Wed, 13 Nov 2024 02:00:28 -0300 Subject: [PATCH 02/35] Fix unit tests --- src/Primitives.sol | 65 ++++++++++++++++++++++++------------------ src/utils/GasUtils.sol | 4 +-- test/GasUtils.t.sol | 24 ++++++++-------- test/Gateway.t.sol | 2 +- 4 files changed, 53 insertions(+), 42 deletions(-) diff --git a/src/Primitives.sol b/src/Primitives.sol index bc9e4f6..b4f1a76 100644 --- a/src/Primitives.sol +++ b/src/Primitives.sol @@ -234,47 +234,58 @@ library PrimitiveUtils { returns (bytes32 messageHash, bytes memory r) { bytes calldata data = message.data; - /// @solidity memory-safe-assembly + // /// @solidity memory-safe-assembly assembly { r := mload(0x40) // GmpMessage Type Hash - mstore(add(r, 0x0004), GMP_MESSAGE_TYPE_HASH) - mstore(add(r, 0x0024), calldataload(add(message, 0x00))) // message.source - mstore(add(r, 0x0044), calldataload(add(message, 0x20))) // message.srcNetwork - mstore(add(r, 0x0064), calldataload(add(message, 0x40))) // message.dest - mstore(add(r, 0x0084), calldataload(add(message, 0x60))) // message.destNetwork - mstore(add(r, 0x00a4), calldataload(add(message, 0x80))) // message.gasLimit - mstore(add(r, 0x00c4), calldataload(add(message, 0xa0))) // message.salt + mstore(add(r, 0x0000), GMP_MESSAGE_TYPE_HASH) + mstore(add(r, 0x0020), calldataload(add(message, 0x00))) // message.source + mstore(add(r, 0x0040), calldataload(add(message, 0x20))) // message.srcNetwork + mstore(add(r, 0x0060), calldataload(add(message, 0x40))) // message.dest + mstore(add(r, 0x0080), calldataload(add(message, 0x60))) // message.destNetwork + mstore(add(r, 0x00a0), calldataload(add(message, 0x80))) // message.gasLimit + mstore(add(r, 0x00c0), calldataload(add(message, 0xa0))) // message.salt + // mstore(add(r, 0x00e0), add(r, 0x01e0)) // message.data + + // --- test // Copy message.data to memory let size := data.length - mstore(add(r, 0x0104), size) // message.data length - calldatacopy(add(r, 0x0124), data.offset, size) // message.data + mstore(add(r, 0x01c0), size) // message.data.length + calldatacopy(add(r, 0x01e0), data.offset, size) // message.data // Computed GMP Typed Hash - messageHash := keccak256(add(r, 0x0124), size) // keccak(message.data) - mstore(add(r, 0x00e4), messageHash) - messageHash := keccak256(add(r, 0x04), 0x0100) // GMP eip712 hash + messageHash := keccak256(add(r, 0x01e0), size) // keccak(message.data) + mstore(add(r, 0x00e0), messageHash) + messageHash := keccak256(r, 0x0100) // GMP eip712 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 - // onGmpReceived - size := and(add(size, 31), 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0) - size := add(size, 0xa4) - mstore(add(r, 0x0064), 0x01900937) // selector - mstore(add(r, 0x0060), size) // length - mstore(add(r, 0x0084), messageHash) // GMP Typed Hash - mstore(add(r, 0x00a4), calldataload(add(message, 0x20))) // msg.network - mstore(add(r, 0x00c4), calldataload(add(message, 0x00))) // msg.source - mstore(add(r, 0x00e4), 0x80) // msg.data offset - - size := and(add(size, 31), 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0) - size := add(size, 0x60) - mstore(0x40, add(add(r, size), 0x40)) - r := add(r, 0x60) + // Retore message.data.offset + mstore(add(r, 0x00e0), add(r, 0x01e0)) + + // 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(r, 0x0120), 0x01900937) // selector + mstore(add(r, 0x011c), size) // length + mstore(add(r, 0x0140), messageHash) // id + mstore(add(r, 0x0160), calldataload(add(message, 0x20))) // network + mstore(add(r, 0x0180), calldataload(add(message, 0x00))) // source + mstore(add(r, 0x01a0), 0x80) // message.data.offset + // mstore(add(r, 0x01c0), data.length) // message.data.length + // calldatacopy(add(r, 0x01e0), data.offset, size) // message.data + + // update free memory pointer + size := add(and(add(size, 31), 0xffffffe0), 0x011c) + size := and(add(add(r, size), 31), 0xffffffe0) + mstore(0x40, add(size, 0x40)) + + r := add(r, 0x011c) } } diff --git a/src/utils/GasUtils.sol b/src/utils/GasUtils.sol index 750e2cc..85b6b14 100644 --- a/src/utils/GasUtils.sol +++ b/src/utils/GasUtils.sol @@ -13,7 +13,7 @@ library GasUtils { /** * @dev Base cost of the `IExecutor.execute` method. */ - uint256 internal constant EXECUTION_BASE_COST = 46667; + uint256 internal constant EXECUTION_BASE_COST = 46667 + 17; /** * @dev Base cost of the `IGateway.submitMessage` method. @@ -186,7 +186,7 @@ library GasUtils { // Memory expansion cost words = 0xa4 + (words << 5); // onGmpReceived encoded call size words = (words + 31) & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0; - words += 0x0200; // Memory size + words += 0x02bc; // Memory size words = (words + 31) >> 5; // to words gas = gas.saturatingAdd(((words * words) >> 9) + (words * 3)); return gas; diff --git a/test/GasUtils.t.sol b/test/GasUtils.t.sol index 5c75d7d..af3f2f6 100644 --- a/test/GasUtils.t.sol +++ b/test/GasUtils.t.sol @@ -229,22 +229,22 @@ contract GasUtilsBase is Test { function test_gasUtils() external pure { uint256 baseCost = GasUtils.EXECUTION_BASE_COST; - assertEq(GasUtils.estimateGas(0, 0, 0), 31739 + baseCost); - assertEq(GasUtils.estimateGas(0, 33, 0), 32112 + baseCost); - assertEq(GasUtils.estimateGas(33, 0, 0), 32772 + baseCost); - assertEq(GasUtils.estimateGas(20, 13, 0), 32512 + baseCost); + assertEq(GasUtils.estimateGas(0, 0, 0), 31758 + baseCost); + assertEq(GasUtils.estimateGas(0, 33, 0), 32130 + baseCost); + assertEq(GasUtils.estimateGas(33, 0, 0), 32790 + baseCost); + assertEq(GasUtils.estimateGas(20, 13, 0), 32530 + baseCost); UFloat9x56 one = UFloatMath.ONE; - assertEq(GasUtils.estimateWeiCost(one, 0, 0, 0, 0), 31739 + baseCost); - assertEq(GasUtils.estimateWeiCost(one, 0, 0, 33, 0), 32112 + baseCost); - assertEq(GasUtils.estimateWeiCost(one, 0, 33, 0, 0), 32772 + baseCost); - assertEq(GasUtils.estimateWeiCost(one, 0, 20, 13, 0), 32512 + baseCost); + assertEq(GasUtils.estimateWeiCost(one, 0, 0, 0, 0), 31758 + baseCost); + assertEq(GasUtils.estimateWeiCost(one, 0, 0, 33, 0), 32130 + baseCost); + assertEq(GasUtils.estimateWeiCost(one, 0, 33, 0, 0), 32790 + baseCost); + assertEq(GasUtils.estimateWeiCost(one, 0, 20, 13, 0), 32530 + baseCost); UFloat9x56 two = UFloat9x56.wrap(0x8080000000000000); - assertEq(GasUtils.estimateWeiCost(two, 0, 0, 0, 0), (31739 + baseCost) * 2); - assertEq(GasUtils.estimateWeiCost(two, 0, 0, 33, 0), (32112 + baseCost) * 2); - assertEq(GasUtils.estimateWeiCost(two, 0, 33, 0, 0), (32772 + baseCost) * 2); - assertEq(GasUtils.estimateWeiCost(two, 0, 20, 13, 0), (32512 + baseCost) * 2); + assertEq(GasUtils.estimateWeiCost(two, 0, 0, 0, 0), (31758 + baseCost) * 2); + assertEq(GasUtils.estimateWeiCost(two, 0, 0, 33, 0), (32130 + baseCost) * 2); + assertEq(GasUtils.estimateWeiCost(two, 0, 33, 0, 0), (32790 + baseCost) * 2); + assertEq(GasUtils.estimateWeiCost(two, 0, 20, 13, 0), (32530 + baseCost) * 2); } } diff --git a/test/Gateway.t.sol b/test/Gateway.t.sol index 79eb0e5..afbf84f 100644 --- a/test/Gateway.t.sol +++ b/test/Gateway.t.sol @@ -216,7 +216,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 + 134032); + assertEq(cost, GasUtils.EXECUTION_BASE_COST + 134050); } function test_checkPayloadSize() external { From 603413f32a1191e9d108cdf455988511491888c2 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Wed, 13 Nov 2024 05:25:50 -0300 Subject: [PATCH 03/35] Create GmpCallback --- src/Gateway.sol | 33 ++++++++-------- src/Primitives.sol | 88 +++++++++++++++++++++--------------------- src/utils/GasUtils.sol | 32 +++++++++++---- test/GasUtils.t.sol | 26 +++++++------ test/Gateway.t.sol | 2 +- 5 files changed, 98 insertions(+), 83 deletions(-) diff --git a/src/Gateway.sol b/src/Gateway.sol index 475c0fd..60d5b13 100644 --- a/src/Gateway.sol +++ b/src/Gateway.sol @@ -22,6 +22,7 @@ import { Network, GmpStatus, GmpSender, + GmpCallback, PrimitiveUtils } from "./Primitives.sol"; @@ -239,12 +240,9 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { } // Execute GMP message - function _execute(bytes32 payloadHash, GmpMessage calldata message, bytes memory data) - private - returns (GmpStatus status, bytes32 result) - { + function _execute(GmpCallback memory message) private returns (GmpStatus status, bytes32 result) { // Verify if this GMP message was already executed - GmpInfo storage gmp = _messages[payloadHash]; + GmpInfo storage gmp = _messages[message.id]; require(gmp.status == GmpStatus.NOT_FOUND, "message already executed"); // Update status to `pending` to prevent reentrancy attacks. @@ -268,13 +266,14 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { bool success; address dest = message.dest; + bytes memory callback = message.callback; /// @solidity memory-safe-assembly assembly { // Using low-level assembly because the GMP is considered executed // regardless if the call reverts or not. - let ptr := add(data, 32) - let size := mload(data) - mstore(data, 0) + let ptr := add(callback, 32) + let size := mload(callback) + mstore(callback, 0) // returns 1 if the call succeed, and 0 if it reverted success := @@ -284,13 +283,13 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { 0, // value in wei to transfer (always zero for GMP) ptr, // input memory pointer size, // input size - data, // output memory pointer + callback, // output memory pointer 32 // output size (fixed 32 bytes) ) // Get Result, reuse data to keep a predictable memory expansion - result := mload(data) - mstore(data, size) + result := mload(callback) + mstore(callback, size) } // Update GMP status @@ -300,7 +299,7 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { gmp.status = status; // Emit event - emit GmpExecuted(payloadHash, message.source, message.dest, status, result); + emit GmpExecuted(message.id, message.source, message.dest, status, result); } /** @@ -315,7 +314,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(453); + initialGas = initialGas.saturatingAdd(454); // Theoretically we could remove the destination network field // and fill it up with the network id of the contract, then the signature will fail. @@ -325,16 +324,16 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { require(message.data.length <= MAX_PAYLOAD_SIZE, "msg data too large"); // Verify the signature - (bytes32 messageHash, bytes memory data) = message.encodeCallback(DOMAIN_SEPARATOR); - _verifySignature(signature, messageHash); + (GmpCallback memory callback) = message.encodeCallback(DOMAIN_SEPARATOR); + _verifySignature(signature, callback.id); // Execute GMP message - (status, result) = _execute(messageHash, message, data); + (status, result) = _execute(callback); // Refund the chronicle gas unchecked { // Compute GMP gas used - uint256 gasUsed = 7214; + uint256 gasUsed = 7211; gasUsed = gasUsed.saturatingAdd(GasUtils.txBaseCost()); gasUsed = gasUsed.saturatingAdd(GasUtils.proxyOverheadGasCost(uint16(msg.data.length), 64)); gasUsed = gasUsed.saturatingAdd(initialGas - gasleft()); diff --git a/src/Primitives.sol b/src/Primitives.sol index b4f1a76..c75f9a2 100644 --- a/src/Primitives.sol +++ b/src/Primitives.sol @@ -105,18 +105,20 @@ enum GmpStatus { } struct GmpCallback { - GmpSender sender; + bytes32 id; + GmpSender source; + uint16 srcNetwork; address dest; uint16 destNetwork; uint256 gasLimit; uint256 salt; - bytes data; + bytes callback; } -/** - * @dev EIP-712 utility for `GmpCallback` - */ -library GmpCallbackUtils {} +// /** +// * @dev EIP-712 utility for `GmpCallback` +// */ +// library GmpCallbackUtils {} /** * @dev EIP-712 utility functions for primitives @@ -231,7 +233,7 @@ library PrimitiveUtils { function encodeCallback(GmpMessage calldata message, bytes32 domainSeparator) internal pure - returns (bytes32 messageHash, bytes memory r) + returns (GmpCallback memory r) { bytes calldata data = message.data; // /// @solidity memory-safe-assembly @@ -246,46 +248,42 @@ library PrimitiveUtils { mstore(add(r, 0x0080), calldataload(add(message, 0x60))) // message.destNetwork mstore(add(r, 0x00a0), calldataload(add(message, 0x80))) // message.gasLimit mstore(add(r, 0x00c0), calldataload(add(message, 0xa0))) // message.salt - // mstore(add(r, 0x00e0), add(r, 0x01e0)) // message.data - - // --- test // Copy message.data to memory - let size := data.length - mstore(add(r, 0x01c0), size) // message.data.length - calldatacopy(add(r, 0x01e0), data.offset, size) // message.data - - // Computed GMP Typed Hash - messageHash := keccak256(add(r, 0x01e0), size) // keccak(message.data) - mstore(add(r, 0x00e0), messageHash) - messageHash := keccak256(r, 0x0100) // GMP eip712 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(r, 0x00e0), add(r, 0x01e0)) - - // 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(r, 0x0120), 0x01900937) // selector - mstore(add(r, 0x011c), size) // length - mstore(add(r, 0x0140), messageHash) // id - mstore(add(r, 0x0160), calldataload(add(message, 0x20))) // network - mstore(add(r, 0x0180), calldataload(add(message, 0x00))) // source - mstore(add(r, 0x01a0), 0x80) // message.data.offset - // mstore(add(r, 0x01c0), data.length) // message.data.length - // calldatacopy(add(r, 0x01e0), data.offset, size) // message.data - - // update free memory pointer - size := add(and(add(size, 31), 0xffffffe0), 0x011c) - size := and(add(add(r, size), 31), 0xffffffe0) - mstore(0x40, add(size, 0x40)) - - r := add(r, 0x011c) + { + let size := data.length + mstore(add(r, 0x01c4), size) // message.data.length + calldatacopy(add(r, 0x01e4), data.offset, size) // message.data + + // Computed GMP Typed Hash + let messageHash := keccak256(add(r, 0x01e4), size) // keccak(message.data) + mstore(add(r, 0x00e0), messageHash) + messageHash := keccak256(r, 0x0100) // GMP eip712 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(r, 0x00e0), add(r, 0x0120)) + mstore(r, 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(r, 0x0124), 0x01900937) // selector + mstore(add(r, 0x0120), size) // length + mstore(add(r, 0x0144), messageHash) // id + mstore(add(r, 0x0164), calldataload(add(message, 0x20))) // network + mstore(add(r, 0x0184), calldataload(add(message, 0x00))) // source + mstore(add(r, 0x01a4), 0x80) // payload.offset + + // update free memory pointer + size := add(and(add(size, 31), 0xffffffe0), 0x0120) + size := and(add(add(r, size), 31), 0xffffffe0) + mstore(0x40, add(size, 0x40)) + } } } diff --git a/src/utils/GasUtils.sol b/src/utils/GasUtils.sol index 85b6b14..0207df1 100644 --- a/src/utils/GasUtils.sol +++ b/src/utils/GasUtils.sol @@ -13,15 +13,30 @@ library GasUtils { /** * @dev Base cost of the `IExecutor.execute` method. */ - uint256 internal constant EXECUTION_BASE_COST = 46667 + 17; + uint256 internal constant EXECUTION_BASE_COST = 46685; + + /** + * @dev Base amount of memory used by `IExecutor.execute` method. + */ + uint256 internal constant MEMORY_OFFSET = 0x3c0; /** * @dev Base cost of the `IGateway.submitMessage` method. */ - uint256 internal constant SUBMIT_BASE_COST = 23206; + uint256 internal constant SUBMIT_BASE_COST = 23313; using BranchlessMath for uint256; + /** + * @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 @@ -44,7 +59,7 @@ library GasUtils { // MEMORY EXPANSION uint256 words = BranchlessMath.max(calldataLen, returnLen); - gasCost = gasCost.saturatingAdd((words.saturatingMul(words) >> 9).saturatingAdd(words * 3)); + gasCost = gasCost.saturatingAdd(memoryExpansionGasCost(words)); return gasCost; } } @@ -186,9 +201,10 @@ library GasUtils { // Memory expansion cost words = 0xa4 + (words << 5); // onGmpReceived encoded call size words = (words + 31) & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0; - words += 0x02bc; // Memory size + words += MEMORY_OFFSET; // Memory size words = (words + 31) >> 5; // to words gas = gas.saturatingAdd(((words * words) >> 9) + (words * 3)); + return gas; } } @@ -197,7 +213,7 @@ library GasUtils { * @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) private pure returns (uint256 inverse) { + function inverseOfAllButOne64th(uint256 x) internal pure returns (uint256 inverse) { unchecked { // inverse = (x * 64) / 63 inverse = x.saturatingShl(6).saturatingDiv(63); @@ -213,10 +229,10 @@ library GasUtils { */ function executionGasNeeded(uint256 messageSize, uint256 gasLimit) internal pure returns (uint256 gasNeeded) { unchecked { - gasNeeded = _inverseOfAllButOne64th(gasLimit); + gasNeeded = inverseOfAllButOne64th(gasLimit); gasNeeded = gasNeeded.saturatingAdd(_executionGasCost(messageSize, gasLimit)); - gasNeeded = gasNeeded.saturatingAdd(2114); - gasNeeded = _inverseOfAllButOne64th(gasNeeded); + gasNeeded = gasNeeded.saturatingAdd(2114 + 2); + gasNeeded = inverseOfAllButOne64th(gasNeeded); messageSize = (uint256(messageSize).saturatingAdd(31) >> 5) << 5; messageSize = messageSize.saturatingAdd(388); gasNeeded = gasNeeded.saturatingAdd(proxyOverheadGasCost(messageSize, 64)); diff --git a/test/GasUtils.t.sol b/test/GasUtils.t.sol index af3f2f6..abba481 100644 --- a/test/GasUtils.t.sol +++ b/test/GasUtils.t.sol @@ -196,6 +196,8 @@ contract GasUtilsBase is Test { vm.assume(gasLimit >= 5000); vm.assume(messageSize <= (0x6000 - 32)); messageSize += 32; + console.log("messageSize:", messageSize); + console.log(" gasLimit:", gasLimit); vm.txGasPrice(1); address sender = TestUtils.createTestAccount(100 ether); @@ -229,22 +231,22 @@ contract GasUtilsBase is Test { function test_gasUtils() external pure { uint256 baseCost = GasUtils.EXECUTION_BASE_COST; - assertEq(GasUtils.estimateGas(0, 0, 0), 31758 + baseCost); - assertEq(GasUtils.estimateGas(0, 33, 0), 32130 + baseCost); - assertEq(GasUtils.estimateGas(33, 0, 0), 32790 + baseCost); - assertEq(GasUtils.estimateGas(20, 13, 0), 32530 + baseCost); + 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); UFloat9x56 one = UFloatMath.ONE; - assertEq(GasUtils.estimateWeiCost(one, 0, 0, 0, 0), 31758 + baseCost); - assertEq(GasUtils.estimateWeiCost(one, 0, 0, 33, 0), 32130 + baseCost); - assertEq(GasUtils.estimateWeiCost(one, 0, 33, 0, 0), 32790 + baseCost); - assertEq(GasUtils.estimateWeiCost(one, 0, 20, 13, 0), 32530 + baseCost); + 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); UFloat9x56 two = UFloat9x56.wrap(0x8080000000000000); - assertEq(GasUtils.estimateWeiCost(two, 0, 0, 0, 0), (31758 + baseCost) * 2); - assertEq(GasUtils.estimateWeiCost(two, 0, 0, 33, 0), (32130 + baseCost) * 2); - assertEq(GasUtils.estimateWeiCost(two, 0, 33, 0, 0), (32790 + baseCost) * 2); - assertEq(GasUtils.estimateWeiCost(two, 0, 20, 13, 0), (32530 + baseCost) * 2); + 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); } } diff --git a/test/Gateway.t.sol b/test/Gateway.t.sol index afbf84f..a19f4f7 100644 --- a/test/Gateway.t.sol +++ b/test/Gateway.t.sol @@ -216,7 +216,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 + 134050); + assertEq(cost, GasUtils.EXECUTION_BASE_COST + 134075); } function test_checkPayloadSize() external { From 40a81ecd5742bbc450a4381b90b31f786ffbfd39 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Wed, 13 Nov 2024 07:29:59 -0300 Subject: [PATCH 04/35] Update gas values --- src/Gateway.sol | 267 +++++++++++++---------------------- src/Primitives.sol | 84 ++++++----- src/interfaces/IExecutor.sol | 33 +++-- src/storage/Routes.sol | 92 ++++-------- src/storage/Shards.sol | 75 +++++----- src/utils/GasUtils.sol | 4 +- test/GasUtils.t.sol | 2 - test/Gateway.t.sol | 2 +- 8 files changed, 246 insertions(+), 313 deletions(-) diff --git a/src/Gateway.sol b/src/Gateway.sol index 60d5b13..700f085 100644 --- a/src/Gateway.sol +++ b/src/Gateway.sol @@ -8,6 +8,7 @@ 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"; @@ -20,13 +21,17 @@ import { UpdateNetworkInfo, Signature, Network, + Route, GmpStatus, GmpSender, GmpCallback, PrimitiveUtils } 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; @@ -36,17 +41,17 @@ abstract contract GatewayEIP712 { constructor(uint16 networkId, address gateway) { NETWORK_ID = networkId; PROXY_ADDRESS = gateway; - DOMAIN_SEPARATOR = computeDomainSeparator(networkId, gateway); + DOMAIN_SEPARATOR = computeDomainSeparator(NetworkID.wrap(networkId), gateway); } // Computes the EIP-712 domain separador - function computeDomainSeparator(uint256 networkId, address addr) internal pure returns (bytes32) { + function computeDomainSeparator(NetworkID 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), + uint256(networkId.asUint()), address(addr) ) ); @@ -61,15 +66,26 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { using BranchlessMath for uint256; using UFloatMath for UFloat9x56; using ShardStore for ShardStore.MainStorage; + using RouteStore for RouteStore.MainStorage; + using NetworkIDHelpers for NetworkID; /** * @dev Maximum size of the GMP payload */ uint256 internal constant MAX_PAYLOAD_SIZE = 0x6000; - // Non-zero value used to initialize the `prevMessageHash` storage + /** + * @dev Non-zero value used to initialize the `prevMessageHash` storage + */ bytes32 internal constant FIRST_MESSAGE_PLACEHOLDER = bytes32(uint256(2 ** 256 - 1)); + /** + * @dev Selector of `GmpCreated` event. + * keccak256("GmpCreated(bytes32,bytes32,address,uint16,uint256,uint256,bytes)"); + */ + bytes32 private constant GMP_CREATED_EVENT_SELECTOR = + 0x0114885f90b5168242aa31b7afb9c2e9f88e90ce329c893d3e6c56021c4c03a5; + // GMP message status mapping(bytes32 => GmpInfo) private _messages; @@ -80,9 +96,6 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { // messageHash => shardId mapping(bytes32 => bytes32) private _executedMessages; - // Network ID => Source network - mapping(uint16 => NetworkInfo) private _networkInfo; - /** * @dev GMP info stored in the Gateway Contract * OBS: the order of the attributes matters! ethereum storage is 256bit aligned, try to keep @@ -129,7 +142,7 @@ 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[] memory keys, Network[] calldata networks) external { + function initialize(address admin, TssKey[] calldata keys, Network[] calldata networks) external { require(PROXY_ADDRESS == address(this), "only proxy can be initialize"); require(prevMessageHash == 0, "already initialized"); ERC1967.setAdmin(admin); @@ -139,11 +152,11 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { prevMessageHash = FIRST_MESSAGE_PLACEHOLDER; // Register networks - _updateNetworks(networks); + RouteStore.getMainStorage().initialize(networks, NetworkID.wrap(NETWORK_ID), computeDomainSeparator); + // _updateNetworks(networks); // Register keys - ShardStore.MainStorage storage shards = ShardStore.getMainStorage(); - shards.registerTssKeys(keys); + ShardStore.getMainStorage().registerTssKeys(keys); // emit event TssKey[] memory revoked = new TssKey[](0); @@ -163,12 +176,8 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { return NETWORK_ID; } - function networkInfo(uint16 id) external view returns (NetworkInfo memory) { - return _networkInfo[id]; - } - - function listShards() external view returns (TssKey[] memory) { - return ShardStore.getMainStorage().listShards(); + function networkInfo(uint16 id) external view returns (RouteStore.NetworkInfo memory) { + return RouteStore.getMainStorage().get(NetworkID.wrap(id)); } /** @@ -189,43 +198,8 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { ); } - // Converts a `TssKey` into an `ShardStore.ShardID` unique identifier - function _tssKeyToShardId(TssKey memory tssKey) private pure returns (ShardStore.ShardID) { - // The tssKey coord x is already collision resistant - // if we are unsure about it, we can hash the coord and parity bit - return ShardStore.ShardID.wrap(bytes32(tssKey.xCoord)); - } - - // Initialize networks - function _updateNetworks(Network[] calldata networks) private { - for (uint256 i = 0; i < networks.length; i++) { - Network calldata network = networks[i]; - NetworkInfo storage info = _networkInfo[network.id]; - require(info.domainSeparator == bytes32(0), "network already initialized"); - require(network.id != NETWORK_ID || network.gateway == address(this), "wrong gateway address"); - info.domainSeparator = computeDomainSeparator(network.id, network.gateway); - info.gasLimit = 15_000_000; // Default to 15M gas - info.relativeGasPrice = UFloatMath.ONE; - info.baseFee = 0; - } - } - - // Register/Revoke TSS keys and emits [`KeySetChanged`] event - function _updateKeys(bytes32 messageHash, TssKey[] memory keysToRevoke, TssKey[] memory newKeys) private { - ShardStore.MainStorage storage shards = ShardStore.getMainStorage(); - - // Revoke tss keys (revoked keys can be registred again keeping the previous nonce) - shards.revokeKeys(keysToRevoke); - - // Register or activate revoked keys - shards.registerTssKeys(newKeys); - - // Emit event - emit KeySetChanged(messageHash, keysToRevoke, newKeys); - } - // Register/Revoke TSS keys using shard TSS signature - function updateKeys(Signature calldata signature, UpdateKeysMessage memory message) public { + function updateKeys(Signature calldata signature, UpdateKeysMessage calldata message) external { bytes32 messageHash = message.eip712TypedHash(DOMAIN_SEPARATOR); // Verify signature and if the message was already executed @@ -236,7 +210,16 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { _executedMessages[messageHash] = bytes32(signature.xCoord); // Register/Revoke shards pubkeys - _updateKeys(messageHash, message.revoke, message.register); + 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 + emit KeySetChanged(messageHash, message.revoke, message.register); } // Execute GMP message @@ -314,7 +297,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(454); + initialGas = initialGas.saturatingAdd(453); // Theoretically we could remove the destination network field // and fill it up with the network id of the contract, then the signature will fail. @@ -365,8 +348,9 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { require(data.length <= MAX_PAYLOAD_SIZE, "msg data too large"); // Check if the destination network is supported - NetworkInfo storage info = _networkInfo[destinationNetwork]; - bytes32 domainSeparator = info.domainSeparator; + // NetworkInfo storage info = _networkInfo[destinationNetwork]; + RouteStore.NetworkInfo storage route = RouteStore.getMainStorage().get(NetworkID.wrap(destinationNetwork)); + bytes32 domainSeparator = route.domainSeparator; require(domainSeparator != bytes32(0), "unsupported network"); // Check if the sender has deposited enougth funds to execute the GMP message @@ -374,7 +358,7 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { uint256 nonZeros = GasUtils.countNonZerosCalldata(data); uint256 zeros = data.length - nonZeros; uint256 msgPrice = GasUtils.estimateWeiCost( - info.relativeGasPrice, info.baseFee, uint16(nonZeros), uint16(zeros), executionGasLimit + route.relativeGasPrice, route.baseFee, uint16(nonZeros), uint16(zeros), executionGasLimit ); require(msg.value >= msgPrice, "insufficient tx value"); } @@ -399,34 +383,12 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { } // 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, salt, data); - // return prevHash; - // ``` - // bytes32 eventSelector = GmpCreated.selector; _emitGmpCreated(prevHash, source, destinationAddress, destinationNetwork, executionGasLimit, salt, payload); - // assembly { - // let ptr := sub(payload, 0x80) - // mstore(ptr, destinationNetwork) // dest network - // mstore(add(ptr, 0x20), executionGasLimit) // gas limit - // mstore(add(ptr, 0x40), salt) // salt - // mstore(add(ptr, 0x60), 0x80) // data offset - // let size := and(add(mload(payload), 31), 0xffffffe0) - // size := add(size, 160) - // log4(ptr, size, eventSelector, prevHash, source, destinationAddress) - // mstore(0, prevHash) - // return(0, 32) - // } } /** - * @dev Selector of `GmpCreated` event. - * keccak256("GmpCreated(bytes32,bytes32,address,uint16,uint256,uint256,bytes)"); + * @dev Emit `GmpCreated` event without copy the data, to simplify the gas estimation. */ - bytes32 private constant GMP_CREATED_EVENT_SELECTOR = - 0x0114885f90b5168242aa31b7afb9c2e9f88e90ce329c893d3e6c56021c4c03a5; - function _emitGmpCreated( bytes32 prevHash, GmpSender source, @@ -472,7 +434,8 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { view returns (uint256) { - NetworkInfo storage network = _networkInfo[networkid]; + // NetworkInfo storage network = _networkInfo[networkid]; + RouteStore.NetworkInfo memory network = RouteStore.getMainStorage().get(NetworkID.wrap(networkid)); uint256 baseFee = uint256(network.baseFee); UFloat9x56 relativeGasPrice = network.relativeGasPrice; @@ -486,89 +449,9 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { return GasUtils.estimateWeiCost(relativeGasPrice, baseFee, uint16(messageSize), 0, gasLimit); } - function _setNetworkInfo(bytes32 executor, bytes32 messageHash, UpdateNetworkInfo calldata info) private { - require(info.mortality >= block.number, "message expired"); - require(executor != bytes32(0), "executor cannot be zero"); - - // Verify signature and if the message was already executed - require(_executedMessages[messageHash] == bytes32(0), "message already executed"); - - // Update network info - NetworkInfo memory stored = _networkInfo[info.networkId]; - - // Verify and update domain separator if it's not zero - stored.domainSeparator = - BranchlessMath.ternary(info.domainSeparator != bytes32(0), info.domainSeparator, stored.domainSeparator); - require(stored.domainSeparator != bytes32(0), "domain separator cannot be zero"); - - // Update gas limit if it's not zero - stored.gasLimit = BranchlessMath.ternaryU64(info.gasLimit > 0, info.gasLimit, stored.gasLimit); - - // Update relative gas price and base fee if any of them are greater than zero - { - bool shouldUpdate = UFloat9x56.unwrap(info.relativeGasPrice) > 0 || info.baseFee > 0; - stored.relativeGasPrice = UFloat9x56.wrap( - BranchlessMath.ternaryU64( - shouldUpdate, UFloat9x56.unwrap(info.relativeGasPrice), UFloat9x56.unwrap(stored.relativeGasPrice) - ) - ); - stored.baseFee = BranchlessMath.ternaryU128(shouldUpdate, info.baseFee, stored.baseFee); - } - - // Save the message hash to prevent replay attacks - _executedMessages[messageHash] = executor; - - // Update network info - _networkInfo[info.networkId] = stored; - - emit NetworkUpdated( - messageHash, - info.networkId, - stored.domainSeparator, - stored.relativeGasPrice, - stored.baseFee, - stored.gasLimit - ); - } - - /** - * @dev set network info using admin account - */ - function setNetworkInfo(UpdateNetworkInfo calldata info) external { - require(msg.sender == _getAdmin(), "unauthorized"); - bytes32 messageHash = info.eip712TypedHash(DOMAIN_SEPARATOR); - _setNetworkInfo(bytes32(uint256(uint160(_getAdmin()))), messageHash, info); - } - - /** - * @dev set network info using admin account - */ - function updateNetworks(UpdateNetworkInfo[] calldata networks) external { - require(networks.length > 0, "networks cannot be empty"); - require(msg.sender == _getAdmin(), "unauthorized"); - for (uint256 i = 0; i < networks.length; i++) { - UpdateNetworkInfo calldata info = networks[i]; - bytes32 messageHash = info.eip712TypedHash(DOMAIN_SEPARATOR); - _setNetworkInfo(bytes32(uint256(uint160(_getAdmin()))), messageHash, info); - } - } - - /** - * @dev Update network information - * @param signature Schnorr signature - * @param info new network info - */ - function setNetworkInfo(Signature calldata signature, UpdateNetworkInfo calldata info) external { - // Verify signature and check if the message was already executed - bytes32 messageHash = info.eip712TypedHash(DOMAIN_SEPARATOR); - _verifySignature(signature, messageHash); - - // Update network info - _setNetworkInfo(bytes32(signature.xCoord), messageHash, info); - } - /** * Deposit funds to the gateway contract + * IMPORTANT: this function must be called only by the administrator!!!! */ function deposit() external payable {} @@ -600,6 +483,60 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { ADMIN LOGIC //////////////////////////////////////////////////////////////*/ + /** + * @dev List all shards. + */ + function shards() external view returns (TssKey[] memory) { + return ShardStore.getMainStorage().listShards(); + } + + /** + * @dev Register a single Shards with provided TSS public key. + */ + function setShard(TssKey calldata publicKey) external { + require(msg.sender == _getAdmin(), "unauthorized"); + ShardStore.getMainStorage().register(publicKey); + } + + /** + * @dev Register a single Shards with provided TSS public key. + */ + function setShard(TssKey[] calldata publicKeys) external { + require(msg.sender == _getAdmin(), "unauthorized"); + ShardStore.getMainStorage().registerTssKeys(publicKeys); + } + + /** + * @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 == _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"); + 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 _getAdmin() private view returns (address admin) { admin = ERC1967.getAdmin(); // If the admin slot is empty, then the 0xd4833be6144AF48d4B09E5Ce41f826eEcb7706D6 is the admin @@ -612,13 +549,13 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { } // OBS: remove != revoke (when revoked, you cannot register again) - function sudoRemoveShards(TssKey[] memory revokedKeys) external payable { + function sudoRemoveShards(TssKey[] calldata revokedKeys) external payable { require(msg.sender == _getAdmin(), "unauthorized"); ShardStore.getMainStorage().revokeKeys(revokedKeys); emit KeySetChanged(bytes32(0), revokedKeys, new TssKey[](0)); } - function sudoAddShards(TssKey[] memory newKeys) external payable { + function sudoAddShards(TssKey[] calldata newKeys) external payable { require(msg.sender == _getAdmin(), "unauthorized"); ShardStore.getMainStorage().registerTssKeys(newKeys); emit KeySetChanged(bytes32(0), new TssKey[](0), newKeys); diff --git a/src/Primitives.sol b/src/Primitives.sol index c75f9a2..07e3ecf 100644 --- a/src/Primitives.sol +++ b/src/Primitives.sol @@ -5,6 +5,7 @@ pragma solidity >=0.8.0; import {BranchlessMath} from "./utils/BranchlessMath.sol"; import {UFloatMath, UFloat9x56} from "./utils/Float9x56.sol"; +import {NetworkID} from "./NetworkID.sol"; /** * @dev GmpSender is the sender of a GMP message @@ -83,6 +84,23 @@ struct UpdateNetworkInfo { uint64 mortality; } +/** + * @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; + uint256 relativeGasPriceNumerator; + uint256 relativeGasPriceDenominator; +} + /** * @dev Message payload used to revoke or/and register new shards * @param revoke Shard's keys to revoke @@ -250,40 +268,38 @@ library PrimitiveUtils { mstore(add(r, 0x00c0), calldataload(add(message, 0xa0))) // message.salt // Copy message.data to memory - { - let size := data.length - mstore(add(r, 0x01c4), size) // message.data.length - calldatacopy(add(r, 0x01e4), data.offset, size) // message.data - - // Computed GMP Typed Hash - let messageHash := keccak256(add(r, 0x01e4), size) // keccak(message.data) - mstore(add(r, 0x00e0), messageHash) - messageHash := keccak256(r, 0x0100) // GMP eip712 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(r, 0x00e0), add(r, 0x0120)) - mstore(r, 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(r, 0x0124), 0x01900937) // selector - mstore(add(r, 0x0120), size) // length - mstore(add(r, 0x0144), messageHash) // id - mstore(add(r, 0x0164), calldataload(add(message, 0x20))) // network - mstore(add(r, 0x0184), calldataload(add(message, 0x00))) // source - mstore(add(r, 0x01a4), 0x80) // payload.offset - - // update free memory pointer - size := add(and(add(size, 31), 0xffffffe0), 0x0120) - size := and(add(add(r, size), 31), 0xffffffe0) - mstore(0x40, add(size, 0x40)) - } + let size := data.length + mstore(add(r, 0x01c4), size) // message.data.length + calldatacopy(add(r, 0x01e4), data.offset, size) // message.data + + // Computed GMP Typed Hash + let messageHash := keccak256(add(r, 0x01e4), size) // keccak(message.data) + mstore(add(r, 0x00e0), messageHash) + messageHash := keccak256(r, 0x0100) // GMP eip712 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(r, 0x00e0), add(r, 0x0120)) + mstore(r, 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(r, 0x0124), 0x01900937) // selector + mstore(add(r, 0x0120), size) // length + mstore(add(r, 0x0144), messageHash) // id + mstore(add(r, 0x0164), calldataload(add(message, 0x20))) // network + mstore(add(r, 0x0184), calldataload(add(message, 0x00))) // source + mstore(add(r, 0x01a4), 0x80) // payload.offset + + // update free memory pointer + size := add(and(add(size, 31), 0xffffffe0), 0x0120) + size := and(add(add(r, size), 31), 0xffffffe0) + mstore(0x40, add(size, 0x40)) } } diff --git a/src/interfaces/IExecutor.sol b/src/interfaces/IExecutor.sol index fe4d7c9..4a33d52 100644 --- a/src/interfaces/IExecutor.sol +++ b/src/interfaces/IExecutor.sol @@ -12,7 +12,7 @@ import { UpdateKeysMessage, UpdateNetworkInfo, GmpSender, - TssKey + Route } from "../Primitives.sol"; /** @@ -42,7 +42,12 @@ interface IExecutor { /** * @dev List all shards currently registered in the gateway. */ - function listShards() external returns (TssKey[] memory); + function shards() external returns (TssKey[] memory); + + /** + * @dev List all shards currently registered in the gateway. + */ + function routes() external returns (Route[] memory); /** * Execute GMP message @@ -53,19 +58,19 @@ interface IExecutor { external returns (GmpStatus status, bytes32 result); - /** - * Update TSS key set - * @param signature Schnorr signature - * @param message Shard's keys to register and revoke - */ - function updateKeys(Signature memory signature, UpdateKeysMessage memory message) external; + // /** + // * Update TSS key set + // * @param signature Schnorr signature + // * @param message Shard's keys to register and revoke + // */ + // function updateKeys(Signature memory signature, UpdateKeysMessage memory message) external; - /** - * Update or insert a new network info - * @param signature Schnorr signature - * @param info Network info - */ - function setNetworkInfo(Signature memory signature, UpdateNetworkInfo memory info) external; + // /** + // * Update or insert a new network info + // * @param signature Schnorr signature + // * @param info Network info + // */ + // function setNetworkInfo(Signature memory signature, UpdateNetworkInfo memory info) external; /** * Deposit funds to the gateway contract diff --git a/src/storage/Routes.sol b/src/storage/Routes.sol index 5c1ad80..78b8e86 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} from "../Primitives.sol"; +import {UpdateNetworkInfo, Signature, Network, Route} from "../Primitives.sol"; import {NetworkIDHelpers, NetworkID} from "../NetworkID.sol"; import {EnumerableSet, Pointer} from "../utils/EnumerableSet.sol"; import {BranchlessMath} from "../utils/BranchlessMath.sol"; @@ -40,33 +40,14 @@ library RouteStore { } /** - * @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; - uint256 relativeGasPriceNumerator; - uint256 relativeGasPriceDenominator; - } - - /** - * @dev Network info stored in the Gateway Contract - * @param id Message unique id. + * @dev Emitted when a route is updated. * @param networkId Network identifier. * @param domainSeparator Domain EIP-712 - Replay Protection Mechanism. * @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 NetworkUpdated( - bytes32 indexed id, + event RouteUpdated( uint16 indexed networkId, bytes32 indexed domainSeparator, UFloat9x56 relativeGasPrice, @@ -86,8 +67,7 @@ library RouteStore { EnumerableSet.Map routes; } - error ShardAlreadyRegistered(NetworkID id); - error ShardNotExists(NetworkID id); + error RouteNotExists(NetworkID id); error IndexOutOfBounds(uint256 index); function getMainStorage() internal pure returns (MainStorage storage $) { @@ -171,7 +151,7 @@ library RouteStore { 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 ShardNotExists(id); + revert RouteNotExists(id); } return pointerToRoute(ptr); } @@ -184,63 +164,53 @@ library RouteStore { return (exists, pointerToRoute(ptr)); } - function createOrUpdateNetworkInfo(MainStorage storage store, bytes32 messageHash, UpdateNetworkInfo calldata info) - private - { - require(info.mortality >= block.number, "message expired"); - - // Verify signature and if the message was already executed - // require(_executedMessages[messageHash] == bytes32(0), "message already executed"); - + function createOrUpdateRoute(MainStorage storage store, Route calldata route) internal { // Update network info - (bool created, NetworkInfo storage stored) = getOrAdd(store, NetworkID.wrap(info.networkId)); - require(!created || info.domainSeparator != bytes32(0), "domain separator cannot be zero"); + (bool created, NetworkInfo storage stored) = getOrAdd(store, route.networkId); + require(!created || stored.domainSeparator != bytes32(0), "domain separator cannot be zero"); // Verify and update domain separator if it's not zero - if (info.domainSeparator != bytes32(0)) { - stored.domainSeparator = info.domainSeparator; + if (route.gateway != bytes32(0)) { + stored.domainSeparator = route.gateway; } // Update gas limit if it's not zero - if (info.gasLimit > 0) { - stored.gasLimit = info.gasLimit; + if (route.gasLimit > 0) { + stored.gasLimit = route.gasLimit; } // Update relative gas price and base fee if any of them are greater than zero - if (UFloat9x56.unwrap(info.relativeGasPrice) > 0 || info.baseFee > 0) { - stored.relativeGasPrice = info.relativeGasPrice; - stored.baseFee = info.baseFee; + if (route.relativeGasPriceDenominator > 0) { + UFloat9x56 relativeGasPrice = + UFloatMath.fromRational(route.relativeGasPriceNumerator, route.relativeGasPriceDenominator); + stored.relativeGasPrice = relativeGasPrice; + stored.baseFee = route.baseFee; } - // Save the message hash to prevent replay attacks - // _executedMessages[messageHash] = executor; - - // Update network info - // _networkInfo[info.networkId] = stored; - - emit NetworkUpdated( - messageHash, - info.networkId, - stored.domainSeparator, - stored.relativeGasPrice, - stored.baseFee, - stored.gasLimit + emit RouteUpdated( + route.networkId.asUint(), stored.domainSeparator, stored.relativeGasPrice, stored.baseFee, stored.gasLimit ); } - // Initialize networks + /** + * @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. + * @param computeDomainSeparator Function to compute the domain separator. + */ function initialize( MainStorage storage store, Network[] calldata networks, NetworkID networkdID, - function(Network calldata) internal pure returns(bytes32) computeDomainSeparator - ) private { + function(NetworkID, address) internal pure returns (bytes32) computeDomainSeparator + ) internal { for (uint256 i = 0; i < networks.length; i++) { Network calldata network = networks[i]; - (bool exists, NetworkInfo storage info) = tryGet(store, NetworkID.wrap(network.id)); - require(!exists && info.domainSeparator == bytes32(0), "network already initialized"); + (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.domainSeparator = computeDomainSeparator(network); + info.domainSeparator = computeDomainSeparator(NetworkID.wrap(network.id), network.gateway); info.gasLimit = 15_000_000; // Default to 15M gas info.relativeGasPrice = UFloatMath.ONE; info.baseFee = 0; diff --git a/src/storage/Shards.sol b/src/storage/Shards.sol index 11778dd..43f192d 100644 --- a/src/storage/Shards.sol +++ b/src/storage/Shards.sol @@ -190,45 +190,52 @@ library ShardStore { } /** - * @dev Register TSS keys. + * @dev Register a single TSS key. + * Requirements: + * - The `newKey` should not be already registered. + */ + function register(MainStorage storage store, TssKey calldata newKey) internal { + // Check y-parity + require(newKey.yParity == (newKey.yParity & 1), "y parity bit must be 0 or 1, 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) { + revert ShardAlreadyRegistered(id); + } + + // Get the current status and nonce + ShardInfo memory shard = stored; + + require( + shard.createdAtBlock == 0 || shard.yParity == 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; + } + + /** + * @dev Register TSS keys in batch. * Requirements: * - The `keys` should not be already registered. */ - function registerTssKeys(MainStorage storage store, TssKey[] memory keys) internal { + 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++) { - TssKey memory newKey = keys[i]; - - // Check y-parity - require(newKey.yParity == (newKey.yParity & 1), "y parity bit must be 0 or 1, cannot register shard"); - - // Read shard from storage - ShardID id = ShardID.wrap(bytes32(newKey.xCoord)); - (bool success, ShardInfo storage stored) = getOrAdd(store, id); - - // Check if the shard is already registered - if (!success) { - revert ShardAlreadyRegistered(id); - } - - // Get the current status and nonce - ShardInfo memory shard = stored; - - require( - shard.createdAtBlock == 0 || shard.yParity == 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; + register(store, keys[i]); } } } @@ -238,12 +245,12 @@ library ShardStore { * Requirements: * - The `keys` must be registered. */ - function revokeKeys(MainStorage storage store, TssKey[] memory keys) internal { + function revokeKeys(MainStorage storage store, TssKey[] calldata keys) internal { // We don't perform any arithmetic operation, except iterate a loop unchecked { // Revoke tss keys for (uint256 i = 0; i < keys.length; i++) { - TssKey memory revokedKey = keys[i]; + TssKey calldata revokedKey = keys[i]; // Read shard from storage ShardID id = ShardID.wrap(bytes32(revokedKey.xCoord)); diff --git a/src/utils/GasUtils.sol b/src/utils/GasUtils.sol index 0207df1..af1d6e1 100644 --- a/src/utils/GasUtils.sol +++ b/src/utils/GasUtils.sol @@ -13,7 +13,7 @@ library GasUtils { /** * @dev Base cost of the `IExecutor.execute` method. */ - uint256 internal constant EXECUTION_BASE_COST = 46685; + uint256 internal constant EXECUTION_BASE_COST = 46674; /** * @dev Base amount of memory used by `IExecutor.execute` method. @@ -23,7 +23,7 @@ library GasUtils { /** * @dev Base cost of the `IGateway.submitMessage` method. */ - uint256 internal constant SUBMIT_BASE_COST = 23313; + uint256 internal constant SUBMIT_BASE_COST = 25473; using BranchlessMath for uint256; diff --git a/test/GasUtils.t.sol b/test/GasUtils.t.sol index abba481..08fdcd0 100644 --- a/test/GasUtils.t.sol +++ b/test/GasUtils.t.sol @@ -196,8 +196,6 @@ contract GasUtilsBase is Test { vm.assume(gasLimit >= 5000); vm.assume(messageSize <= (0x6000 - 32)); messageSize += 32; - console.log("messageSize:", messageSize); - console.log(" gasLimit:", gasLimit); vm.txGasPrice(1); address sender = TestUtils.createTestAccount(100 ether); diff --git a/test/Gateway.t.sol b/test/Gateway.t.sol index a19f4f7..5dcf0fe 100644 --- a/test/Gateway.t.sol +++ b/test/Gateway.t.sol @@ -627,7 +627,7 @@ contract GatewayBase is Test { id, GmpSender.unwrap(gmp.source), gmp.dest, gmp.destNetwork, gmp.gasLimit, gmp.salt, gmp.data ); assertEq(ctx.submitMessage(gmp), id, "unexpected GMP id"); - assertEq(ctx.executionCost, expectedCost - 6800, "unexpected execution gas cost"); + assertEq(ctx.executionCost, expectedCost - 8800, "unexpected execution gas cost"); } } From 74088e7e44b90de0e0422e4736ed610e46dea61d Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Wed, 13 Nov 2024 07:50:57 -0300 Subject: [PATCH 05/35] Check for target chain block gas limit before accept GMP message --- src/Gateway.sol | 31 +++++++++++++++---------------- src/utils/GasUtils.sol | 4 ++-- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/Gateway.sol b/src/Gateway.sol index 700f085..3a38781 100644 --- a/src/Gateway.sol +++ b/src/Gateway.sol @@ -334,26 +334,25 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { /** * @dev Send message from this chain to another chain. * @param destinationAddress the target address on the destination chain - * @param destinationNetwork the target chain where the contract call will be made + * @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 destinationNetwork, - uint256 executionGasLimit, - bytes calldata data - ) external payable returns (bytes32) { + 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 too large"); - // Check if the destination network is supported - // NetworkInfo storage info = _networkInfo[destinationNetwork]; - RouteStore.NetworkInfo storage route = RouteStore.getMainStorage().get(NetworkID.wrap(destinationNetwork)); - bytes32 domainSeparator = route.domainSeparator; - require(domainSeparator != bytes32(0), "unsupported network"); + // Check if the provided parameters are valid + RouteStore.NetworkInfo memory route = RouteStore.getMainStorage().get(NetworkID.wrap(routeId)); + require(route.domainSeparator != bytes32(0), "unsupported route"); + require(route.baseFee > 0 || UFloat9x56.unwrap(route.relativeGasPrice) > 0, "route temporarily disabled"); + require(executionGasLimit <= route.gasLimit, "message exceeds maximum gas limit"); - // Check if the sender has deposited enougth funds to execute the GMP message + // Check if the sender has deposited sufficient funds to execute the GMP message { uint256 nonZeros = GasUtils.countNonZerosCalldata(data); uint256 zeros = data.length - nonZeros; @@ -376,14 +375,14 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { bytes memory payload; { GmpMessage memory message = - GmpMessage(source, NETWORK_ID, destinationAddress, destinationNetwork, executionGasLimit, salt, data); - prevHash = message.eip712TypedHash(domainSeparator); + GmpMessage(source, NETWORK_ID, destinationAddress, routeId, executionGasLimit, salt, data); + prevHash = message.eip712TypedHash(route.domainSeparator); prevMessageHash = prevHash; payload = message.data; } // Emit `GmpCreated` event without copy the data, to simplify the gas estimation. - _emitGmpCreated(prevHash, source, destinationAddress, destinationNetwork, executionGasLimit, salt, payload); + _emitGmpCreated(prevHash, source, destinationAddress, routeId, executionGasLimit, salt, payload); } /** diff --git a/src/utils/GasUtils.sol b/src/utils/GasUtils.sol index af1d6e1..cbd70f1 100644 --- a/src/utils/GasUtils.sol +++ b/src/utils/GasUtils.sol @@ -23,7 +23,7 @@ library GasUtils { /** * @dev Base cost of the `IGateway.submitMessage` method. */ - uint256 internal constant SUBMIT_BASE_COST = 25473; + uint256 internal constant SUBMIT_BASE_COST = 25710; using BranchlessMath for uint256; @@ -86,7 +86,7 @@ library GasUtils { gasCost += words << 8; // Memory expansion cost - words += 13; + words += 13 + 4; gasCost += ((words * words) >> 9) + (words * 3); return gasCost; From e350659658cab2f9831239fe6f08f70a1097a72a Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Wed, 13 Nov 2024 08:25:45 -0300 Subject: [PATCH 06/35] Remove unused code from gateway --- src/Gateway.sol | 44 +++++++++++------------------------------- src/Primitives.sol | 5 +++++ src/storage/Routes.sol | 43 ++++++++++++++++++++++++++++++++++++++++- src/utils/GasUtils.sol | 4 ++-- 4 files changed, 60 insertions(+), 36 deletions(-) diff --git a/src/Gateway.sol b/src/Gateway.sol index 3a38781..ce48272 100644 --- a/src/Gateway.sol +++ b/src/Gateway.sol @@ -25,7 +25,8 @@ import { GmpStatus, GmpSender, GmpCallback, - PrimitiveUtils + PrimitiveUtils, + MAX_PAYLOAD_SIZE } from "./Primitives.sol"; import {NetworkID, NetworkIDHelpers} from "./NetworkID.sol"; @@ -67,13 +68,9 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { using UFloatMath for UFloat9x56; using ShardStore for ShardStore.MainStorage; using RouteStore for RouteStore.MainStorage; + using RouteStore for RouteStore.NetworkInfo; using NetworkIDHelpers for NetworkID; - /** - * @dev Maximum size of the GMP payload - */ - uint256 internal constant MAX_PAYLOAD_SIZE = 0x6000; - /** * @dev Non-zero value used to initialize the `prevMessageHash` storage */ @@ -200,13 +197,12 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { // 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.eip712TypedHash(DOMAIN_SEPARATOR); - - // Verify signature and if the message was already executed require(_executedMessages[messageHash] == bytes32(0), "message already executed"); - _verifySignature(signature, messageHash); - // Store the message hash to prevent replay attacks + // Verify the signature and store the message hash + _verifySignature(signature, messageHash); _executedMessages[messageHash] = bytes32(signature.xCoord); // Register/Revoke shards pubkeys @@ -326,6 +322,7 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { /// @solidity memory-safe-assembly assembly { + // Refund the gas used pop(call(gas(), caller(), refund, 0, 0, 0, 0)) } } @@ -347,20 +344,9 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { require(data.length <= MAX_PAYLOAD_SIZE, "msg data too large"); // 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)); - require(route.domainSeparator != bytes32(0), "unsupported route"); - require(route.baseFee > 0 || UFloat9x56.unwrap(route.relativeGasPrice) > 0, "route temporarily disabled"); - require(executionGasLimit <= route.gasLimit, "message exceeds maximum gas limit"); - - // Check if the sender has deposited sufficient funds to execute the GMP message - { - uint256 nonZeros = GasUtils.countNonZerosCalldata(data); - uint256 zeros = data.length - nonZeros; - uint256 msgPrice = GasUtils.estimateWeiCost( - route.relativeGasPrice, route.baseFee, uint16(nonZeros), uint16(zeros), executionGasLimit - ); - require(msg.value >= msgPrice, "insufficient tx value"); - } + 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); @@ -434,18 +420,10 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { returns (uint256) { // NetworkInfo storage network = _networkInfo[networkid]; - RouteStore.NetworkInfo memory network = RouteStore.getMainStorage().get(NetworkID.wrap(networkid)); - uint256 baseFee = uint256(network.baseFee); - UFloat9x56 relativeGasPrice = network.relativeGasPrice; - - // Verify if the network exists - require(baseFee > 0 || UFloat9x56.unwrap(relativeGasPrice) > 0, "unsupported network"); - - // if the message data is too large, we use the maximum base fee. - baseFee = BranchlessMath.ternary(messageSize > MAX_PAYLOAD_SIZE, 2 ** 256 - 1, baseFee); + RouteStore.NetworkInfo memory route = RouteStore.getMainStorage().get(NetworkID.wrap(networkid)); // Estimate the cost - return GasUtils.estimateWeiCost(relativeGasPrice, baseFee, uint16(messageSize), 0, gasLimit); + return route.estimateWeiCost(uint16(messageSize), gasLimit); } /** diff --git a/src/Primitives.sol b/src/Primitives.sol index 07e3ecf..75a61d1 100644 --- a/src/Primitives.sol +++ b/src/Primitives.sol @@ -7,6 +7,11 @@ import {BranchlessMath} from "./utils/BranchlessMath.sol"; import {UFloatMath, UFloat9x56} from "./utils/Float9x56.sol"; import {NetworkID} from "./NetworkID.sol"; +/** + * @dev Maximum size of the GMP payload + */ +uint256 constant MAX_PAYLOAD_SIZE = 0x6000; + /** * @dev GmpSender is the sender of a GMP message */ diff --git a/src/storage/Routes.sol b/src/storage/Routes.sol index 78b8e86..38611ce 100644 --- a/src/storage/Routes.sol +++ b/src/storage/Routes.sol @@ -2,12 +2,13 @@ // Analog's Contracts (last updated v0.1.0) (src/storage/Routes.sol) pragma solidity ^0.8.20; -import {UpdateNetworkInfo, Signature, Network, Route} from "../Primitives.sol"; +import {UpdateNetworkInfo, 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 @@ -243,4 +244,44 @@ library RouteStore { } 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.domainSeparator != bytes32(0), "unsupported route"); + 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 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/src/utils/GasUtils.sol b/src/utils/GasUtils.sol index cbd70f1..a054bc4 100644 --- a/src/utils/GasUtils.sol +++ b/src/utils/GasUtils.sol @@ -16,14 +16,14 @@ library GasUtils { uint256 internal constant EXECUTION_BASE_COST = 46674; /** - * @dev Base amount of memory used by `IExecutor.execute` method. + * @dev Initial amount of memory used by `IExecutor.execute` method. */ uint256 internal constant MEMORY_OFFSET = 0x3c0; /** * @dev Base cost of the `IGateway.submitMessage` method. */ - uint256 internal constant SUBMIT_BASE_COST = 25710; + uint256 internal constant SUBMIT_BASE_COST = 25814; using BranchlessMath for uint256; From ae16b6fd473f5538fa7301cf067168fe9ad34e29 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Wed, 13 Nov 2024 09:10:30 -0300 Subject: [PATCH 07/35] Update gas meter again.. --- src/Gateway.sol | 44 +++++++++++++++++++++++++++++++++---- src/storage/Routes.sol | 8 +++---- src/storage/Shards.sol | 38 ++++++++++++++++++-------------- src/utils/EnumerableSet.sol | 7 +++--- src/utils/GasUtils.sol | 4 ++-- test/EnumerableSet.t.sol | 7 ++++-- test/Gateway.t.sol | 3 +-- 7 files changed, 76 insertions(+), 35 deletions(-) diff --git a/src/Gateway.sol b/src/Gateway.sol index ce48272..3ece7f3 100644 --- a/src/Gateway.sol +++ b/src/Gateway.sol @@ -293,7 +293,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(453); + initialGas = initialGas.saturatingAdd(437); // Theoretically we could remove the destination network field // and fill it up with the network id of the contract, then the signature will fail. @@ -312,7 +312,7 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { // Refund the chronicle gas unchecked { // Compute GMP gas used - uint256 gasUsed = 7211; + uint256 gasUsed = 7223; gasUsed = gasUsed.saturatingAdd(GasUtils.txBaseCost()); gasUsed = gasUsed.saturatingAdd(GasUtils.proxyOverheadGasCost(uint16(msg.data.length), 64)); gasUsed = gasUsed.saturatingAdd(initialGas - gasleft()); @@ -457,7 +457,7 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { } /*////////////////////////////////////////////////////////////// - ADMIN LOGIC + SHARDS MANAGEMENT METHODS //////////////////////////////////////////////////////////////*/ /** @@ -467,6 +467,22 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { 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}); + } + /** * @dev Register a single Shards with provided TSS public key. */ @@ -476,13 +492,33 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { } /** - * @dev Register a single Shards with provided TSS public key. + * @dev Register Shards in batch. */ function setShard(TssKey[] calldata publicKeys) external { require(msg.sender == _getAdmin(), "unauthorized"); ShardStore.getMainStorage().registerTssKeys(publicKeys); } + /** + * @dev Revoke a single shard TSS Key. + */ + function revokeShard(TssKey calldata publicKey) external { + require(msg.sender == _getAdmin(), "unauthorized"); + ShardStore.getMainStorage().revoke(publicKey); + } + + /** + * @dev Revoke Shards in batch. + */ + function revokeShard(TssKey[] calldata publicKeys) external { + require(msg.sender == _getAdmin(), "unauthorized"); + ShardStore.getMainStorage().revokeKeys(publicKeys); + } + + /*////////////////////////////////////////////////////////////// + LISTING ROUTES AND SHARDS + //////////////////////////////////////////////////////////////*/ + /** * @dev List all routes. */ diff --git a/src/storage/Routes.sol b/src/storage/Routes.sol index 38611ce..7d8decf 100644 --- a/src/storage/Routes.sol +++ b/src/storage/Routes.sol @@ -135,12 +135,12 @@ library RouteStore { * * - `index` must be strictly less than {length}. */ - function at(MainStorage storage store, uint256 index) internal view returns (NetworkInfo storage) { - StoragePtr ptr = store.routes.at(index); - if (ptr.isNull()) { + 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 pointerToRoute(ptr); + return (NetworkID.wrap(uint16(uint256(key))), pointerToRoute(value)); } /** diff --git a/src/storage/Shards.sol b/src/storage/Shards.sol index 43f192d..aa890a1 100644 --- a/src/storage/Shards.sol +++ b/src/storage/Shards.sol @@ -139,12 +139,12 @@ library ShardStore { * * - `index` must be strictly less than {length}. */ - function at(MainStorage storage store, uint256 index) internal view returns (ShardInfo storage) { - StoragePtr ptr = store.shards.at(index); + 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 ptr.asShardInfo(); + return (ShardID.wrap(xCoord), ptr.asShardInfo()); } /** @@ -245,23 +245,27 @@ library ShardStore { * Requirements: * - The `keys` must be registered. */ - function revokeKeys(MainStorage storage store, TssKey[] calldata keys) internal { - // We don't perform any arithmetic operation, except iterate a loop - unchecked { - // Revoke tss keys - for (uint256 i = 0; i < keys.length; i++) { - TssKey calldata revokedKey = keys[i]; + function revoke(MainStorage storage store, TssKey calldata publicKey) internal { + // Read shard from storage + ShardID id = ShardID.wrap(bytes32(publicKey.xCoord)); + ShardInfo memory shard = get(store, id); - // Read shard from storage - ShardID id = ShardID.wrap(bytes32(revokedKey.xCoord)); - ShardInfo memory shard = get(store, id); + // Check y-parity + require(shard.yParity == shard.yParity, "y parity mismatch, cannot revoke key"); - // Check y-parity - require(shard.yParity == revokedKey.yParity, "y parity mismatch, cannot revoke key"); + // Remove from the set + store.shards.remove(ShardID.unwrap(id)); + } - // Remove from the set - store.shards.remove(ShardID.unwrap(id)); - } + /** + * @dev Revoke TSS keys im batch. + * Requirements: + * - The `publicKeys` must be registered. + */ + function revokeKeys(MainStorage storage store, TssKey[] calldata publicKeys) internal { + // Revoke tss keys + for (uint256 i = 0; i < publicKeys.length; i++) { + revoke(store, publicKeys[i]); } } diff --git a/src/utils/EnumerableSet.sol b/src/utils/EnumerableSet.sol index cff2a74..3055eed 100644 --- a/src/utils/EnumerableSet.sol +++ b/src/utils/EnumerableSet.sol @@ -167,15 +167,14 @@ library EnumerableSet { * * - `index` must be strictly less than {length}. */ - function at(Map storage map, uint256 index) internal view returns (StoragePtr r) { + function at(Map storage map, uint256 index) internal view returns (bytes32 key, StoragePtr r) { assembly ("memory-safe") { mstore(0x00, map.slot) - let key := sload(add(keccak256(0x00, 0x20), index)) + key := sload(add(keccak256(0x00, 0x20), index)) mstore(0x00, key) mstore(0x20, add(map.slot, 1)) r := keccak256(0x00, 0x40) - key := not(sload(sub(r, 1))) - r := mul(r, and(lt(index, sload(map.slot)), eq(index, key))) + r := mul(r, and(lt(index, sload(map.slot)), eq(index, not(sload(sub(r, 1)))))) } } diff --git a/src/utils/GasUtils.sol b/src/utils/GasUtils.sol index a054bc4..ac618f8 100644 --- a/src/utils/GasUtils.sol +++ b/src/utils/GasUtils.sol @@ -13,7 +13,7 @@ library GasUtils { /** * @dev Base cost of the `IExecutor.execute` method. */ - uint256 internal constant EXECUTION_BASE_COST = 46674; + uint256 internal constant EXECUTION_BASE_COST = 46658; /** * @dev Initial amount of memory used by `IExecutor.execute` method. @@ -23,7 +23,7 @@ library GasUtils { /** * @dev Base cost of the `IGateway.submitMessage` method. */ - uint256 internal constant SUBMIT_BASE_COST = 25814; + uint256 internal constant SUBMIT_BASE_COST = 25802; using BranchlessMath for uint256; diff --git a/test/EnumerableSet.t.sol b/test/EnumerableSet.t.sol index 3ad112f..ebdeb7d 100644 --- a/test/EnumerableSet.t.sol +++ b/test/EnumerableSet.t.sol @@ -45,7 +45,8 @@ contract EnumerableSetTest is Test { } function _at(uint256 index, bool success) private view returns (MyStruct storage r) { - bytes32 ptr = map.at(index).asBytes32(); + (, StoragePtr raw) = map.at(index); + bytes32 ptr = raw.asBytes32(); if (success) { assertNotEq(ptr, bytes32(0), "map.at failed"); assembly { @@ -189,8 +190,10 @@ contract EnumerableSetTest is Test { assertEq(index, 0, "unexpected index"); // Map.at works - store = map.at(0).getUint256Slot(); + (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); diff --git a/test/Gateway.t.sol b/test/Gateway.t.sol index 5dcf0fe..3c645e3 100644 --- a/test/Gateway.t.sol +++ b/test/Gateway.t.sol @@ -137,10 +137,9 @@ contract GatewayBase is Test { bytes32 private _srcDomainSeparator; bytes32 private _dstDomainSeparator; - uint256 private constant SUBMIT_GAS_COST = 15034; + // Netowrk ids uint16 private constant SRC_NETWORK_ID = 1234; uint16 internal constant DEST_NETWORK_ID = 1337; - uint8 private constant GMP_STATUS_SUCCESS = 1; constructor() { signer = new Signer(secret); From 9932eb574d7aa93cc6010d25218361c6350d0feb Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Wed, 13 Nov 2024 09:12:29 -0300 Subject: [PATCH 08/35] Remove unused code --- src/interfaces/IExecutor.sol | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/interfaces/IExecutor.sol b/src/interfaces/IExecutor.sol index 4a33d52..71b18d9 100644 --- a/src/interfaces/IExecutor.sol +++ b/src/interfaces/IExecutor.sol @@ -58,20 +58,6 @@ interface IExecutor { external returns (GmpStatus status, bytes32 result); - // /** - // * Update TSS key set - // * @param signature Schnorr signature - // * @param message Shard's keys to register and revoke - // */ - // function updateKeys(Signature memory signature, UpdateKeysMessage memory message) external; - - // /** - // * Update or insert a new network info - // * @param signature Schnorr signature - // * @param info Network info - // */ - // function setNetworkInfo(Signature memory signature, UpdateNetworkInfo memory info) external; - /** * Deposit funds to the gateway contract */ From c68ce2522f77a5a37c9cd86bdea990ec808cc3d6 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Wed, 13 Nov 2024 09:37:19 -0300 Subject: [PATCH 09/35] Add docs to 'GmpCallback' struct --- src/Gateway.sol | 13 ++++++++----- src/Primitives.sol | 37 +++++++++++++++++++++++++++++-------- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/Gateway.sol b/src/Gateway.sol index 3ece7f3..fd60cf6 100644 --- a/src/Gateway.sol +++ b/src/Gateway.sol @@ -221,7 +221,7 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { // Execute GMP message function _execute(GmpCallback memory message) private returns (GmpStatus status, bytes32 result) { // Verify if this GMP message was already executed - GmpInfo storage gmp = _messages[message.id]; + GmpInfo storage gmp = _messages[message.eip712hash]; require(gmp.status == GmpStatus.NOT_FOUND, "message already executed"); // Update status to `pending` to prevent reentrancy attacks. @@ -278,7 +278,7 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { gmp.status = status; // Emit event - emit GmpExecuted(message.id, message.source, message.dest, status, result); + emit GmpExecuted(message.eip712hash, message.source, message.dest, status, result); } /** @@ -302,9 +302,12 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { // Check if the message data is too large require(message.data.length <= MAX_PAYLOAD_SIZE, "msg data too large"); - // Verify the signature - (GmpCallback memory callback) = message.encodeCallback(DOMAIN_SEPARATOR); - _verifySignature(signature, callback.id); + // Convert the `GmpMessage` into `GmpCallback`, which is a more efficient representation. + // see `src/Primitives.sol` for more details. + GmpCallback memory callback = message.intoCallback(DOMAIN_SEPARATOR); + + // Verify the TSS Schnorr Signature + _verifySignature(signature, callback.eip712hash); // Execute GMP message (status, result) = _execute(callback); diff --git a/src/Primitives.sol b/src/Primitives.sol index 75a61d1..ac51f0e 100644 --- a/src/Primitives.sol +++ b/src/Primitives.sol @@ -127,8 +127,19 @@ enum GmpStatus { 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 salt Message salt, useful for sending two messages with same content + * @param callback encoded callback of `IGmpRecipient` interface, see `IGateway.sol` for more details. + */ struct GmpCallback { - bytes32 id; + bytes32 eip712hash; GmpSender source; uint16 srcNetwork; address dest; @@ -138,11 +149,6 @@ struct GmpCallback { bytes callback; } -// /** -// * @dev EIP-712 utility for `GmpCallback` -// */ -// library GmpCallbackUtils {} - /** * @dev EIP-712 utility functions for primitives */ @@ -253,13 +259,28 @@ library PrimitiveUtils { } } - function encodeCallback(GmpMessage calldata message, bytes32 domainSeparator) + /** + * @dev Converts the `GmpMessage` into a `GmpCallback` struct, which contains all fields from + * `GmpMessage`, plus the EIP-712 id and `IGmpReceiver.onGmpReceived` callback encoded. + * + * This method also prevents copying the `message.data` to memory twice, which is expensive if + * the `message.data` is large, using traditional solidity does the following: + * 1. Copy the 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. + * + * @param message GmpMessage from calldata to be encoded + * @param domainSeparator EIP-712 domain separator + * @return r `GmpCallback` struct + */ + function intoCallback(GmpMessage calldata message, bytes32 domainSeparator) internal pure returns (GmpCallback memory r) { bytes calldata data = message.data; - // /// @solidity memory-safe-assembly + /// @solidity memory-safe-assembly assembly { r := mload(0x40) From bfb187f55d6f61d2c4980d2e90d5a6a250be7764 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Wed, 13 Nov 2024 09:40:53 -0300 Subject: [PATCH 10/35] Refactor variable names --- src/Primitives.sol | 52 +++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/Primitives.sol b/src/Primitives.sol index ac51f0e..3db6bf8 100644 --- a/src/Primitives.sol +++ b/src/Primitives.sol @@ -264,67 +264,67 @@ library PrimitiveUtils { * `GmpMessage`, plus the EIP-712 id and `IGmpReceiver.onGmpReceived` callback encoded. * * This method also prevents copying the `message.data` to memory twice, which is expensive if - * the `message.data` is large, using traditional solidity does the following: - * 1. Copy the data to memory to compute the `GmpMessage` EIP-712 hash. + * the data is large, using solidity `abi.encode` 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. * * @param message GmpMessage from calldata to be encoded * @param domainSeparator EIP-712 domain separator - * @return r `GmpCallback` struct + * @return callback `GmpCallback` struct */ function intoCallback(GmpMessage calldata message, bytes32 domainSeparator) internal pure - returns (GmpCallback memory r) + returns (GmpCallback memory callback) { bytes calldata data = message.data; /// @solidity memory-safe-assembly assembly { - r := mload(0x40) + callback := mload(0x40) // GmpMessage Type Hash - mstore(add(r, 0x0000), GMP_MESSAGE_TYPE_HASH) - mstore(add(r, 0x0020), calldataload(add(message, 0x00))) // message.source - mstore(add(r, 0x0040), calldataload(add(message, 0x20))) // message.srcNetwork - mstore(add(r, 0x0060), calldataload(add(message, 0x40))) // message.dest - mstore(add(r, 0x0080), calldataload(add(message, 0x60))) // message.destNetwork - mstore(add(r, 0x00a0), calldataload(add(message, 0x80))) // message.gasLimit - mstore(add(r, 0x00c0), calldataload(add(message, 0xa0))) // message.salt + 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(r, 0x01c4), size) // message.data.length - calldatacopy(add(r, 0x01e4), data.offset, size) // message.data + 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(r, 0x01e4), size) // keccak(message.data) - mstore(add(r, 0x00e0), messageHash) - messageHash := keccak256(r, 0x0100) // GMP eip712 hash + let messageHash := keccak256(add(callback, 0x01e4), size) // keccak(message.data) + mstore(add(callback, 0x00e0), messageHash) + messageHash := keccak256(callback, 0x0100) // GMP eip712 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(r, 0x00e0), add(r, 0x0120)) - mstore(r, messageHash) + mstore(add(callback, 0x00e0), add(callback, 0x0120)) + 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(r, 0x0124), 0x01900937) // selector - mstore(add(r, 0x0120), size) // length - mstore(add(r, 0x0144), messageHash) // id - mstore(add(r, 0x0164), calldataload(add(message, 0x20))) // network - mstore(add(r, 0x0184), calldataload(add(message, 0x00))) // source - mstore(add(r, 0x01a4), 0x80) // payload.offset + 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(r, size), 31), 0xffffffe0) + size := and(add(add(callback, size), 31), 0xffffffe0) mstore(0x40, add(size, 0x40)) } } From a4e4f22e8b3184a47710426ba7d293017cacbdd5 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Wed, 13 Nov 2024 10:42:29 -0300 Subject: [PATCH 11/35] Improve documentation --- src/Primitives.sol | 106 ++++++++++++++++++++++++++++++++------------- 1 file changed, 75 insertions(+), 31 deletions(-) diff --git a/src/Primitives.sol b/src/Primitives.sol index 3db6bf8..7ac91e3 100644 --- a/src/Primitives.sol +++ b/src/Primitives.sol @@ -261,14 +261,16 @@ library PrimitiveUtils { /** * @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 @@ -282,47 +284,89 @@ library PrimitiveUtils { bytes calldata data = message.data; /// @solidity memory-safe-assembly assembly { + // Allocate memory for the callback 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 + ////////////////////////////////////////////////////////// + // 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. + mstore(add(callback, 0x0020), calldataload(add(message, 0x00))) // callback.source + mstore(add(callback, 0x0040), calldataload(add(message, 0x20))) // callback.srcNetwork + mstore(add(callback, 0x0060), calldataload(add(message, 0x40))) // callback.dest + mstore(add(callback, 0x0080), calldataload(add(message, 0x60))) // callback.destNetwork + mstore(add(callback, 0x00a0), calldataload(add(message, 0x80))) // callback.gasLimit + mstore(add(callback, 0x00c0), calldataload(add(message, 0xa0))) // callback.salt + + // mstore(add(callback, 0x00e0), calldataload(add(message, 0xa0))) // callback.data <- pointer + // + // Following `Solidity memory layout`, here we must store a pointer to the `bytes` field. + // ref: https://docs.soliditylang.org/en/latest/internals/layout_in_memory.html + // + // But we will skip this for now, once we need to store the `keccak256(message.data)` here + // for computing the EIP-712 hash. + + // MEMORY OFFSET | RESERVED FIELD | + // 0x00e0..0x0100 <- reserved for the `data` pointer + // 0x0100..0x0120 <- reserved for the `callback.length` field. + // 0x0120..0x0124 <- reserved for the `onGmpReceived.selector` field (4 bytes). + // 0x0124..0x0144 <- reserved for the `onGmpReceived.id` param. + // 0x0144..0x0164 <- reserved for the `onGmpReceived.network` param. + // 0x0164..0x0184 <- reserved for the `onGmpReceived.source` param. + // 0x0184..0x01a4 <- reserved for the `onGmpReceived.data.offset` param (calldata pointer). + // 0x01a4..0x01c4 <- reserved for the `onGmpReceived.data.length` param. + // 0x01c4..?????? <- reserved for the `onGmpReceived.data` bytes. + + // Store `onGmpReceived.data.length` at `0x01a4..0x01c4`. let size := data.length - mstore(add(callback, 0x01c4), size) // message.data.length + mstore(add(callback, 0x01c4), size) + + // Copy `message.data` to memory at `0x01c4`, as described above. calldatacopy(add(callback, 0x01e4), data.offset, size) // message.data - // Computed GMP Typed Hash - let messageHash := keccak256(add(callback, 0x01e4), size) // keccak(message.data) + // Compute `keccak256(message.data)` + let messageHash := keccak256(add(callback, 0x01e4), 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 + // Replace the `eip712hash` by the `callback.data.offset`. + mstore(add(callback, 0x00e0), add(callback, 0x0120)) - // update free memory pointer + // 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, 0x0124), 0x01900937) // selector (4 bytes) + mstore(add(callback, 0x0120), size) // length (32 bytes) + mstore(add(callback, 0x0144), messageHash) // eip-712 hash (32 bytes) + mstore(add(callback, 0x0164), calldataload(add(message, 0x20))) // network (32 bytes) + mstore(add(callback, 0x0184), calldataload(add(message, 0x00))) // source (32 bytes) + mstore(add(callback, 0x01a4), 0x80) // payload.offset (32 bytes) + + // Update free memory pointer and align it to 32 bytes size := add(and(add(size, 31), 0xffffffe0), 0x0120) size := and(add(add(callback, size), 31), 0xffffffe0) mstore(0x40, add(size, 0x40)) From 0e600bb155daa65f9c86d9697ca48b942b8ef5e1 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Fri, 29 Nov 2024 11:00:46 -0300 Subject: [PATCH 12/35] Fix unit tests --- foundry.toml | 43 ++++++- src/Gateway.sol | 22 ++-- src/Primitives.sol | 152 +++++++++++++++------- src/utils/BranchlessMath.sol | 9 ++ src/utils/GasUtils.sol | 148 ++++++++++++++++------ src/utils/Schnorr.sol | 62 +++++++++ test/Example.t.sol | 4 +- test/GasUtils.t.sol | 45 ++++--- test/Gateway.t.sol | 179 ++++++++++++++++++++++++-- test/TestUtils.sol | 236 ++++++++++++++++++++++++++++++----- 10 files changed, 742 insertions(+), 158 deletions(-) create mode 100644 src/utils/Schnorr.sol diff --git a/foundry.toml b/foundry.toml index c03dcb5..5ba672a 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,28 +4,61 @@ out = "out" libs = ["lib"] match_contract = ".+Test" -# Lint +######## +# Lint # +######## deny_warnings = true -# Solc options -solc = '0.8.25' +################ +# Solc options # +################ +solc = '0.8.28' 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. runs = 2500 # When debuging fuzz tests, uncomment this seed to make tests reproducible. -# seed = "0xdeadbeefdeadbeefdeadbeefdeadbeef" +seed = "0xdeadbeefdeadbeefdeadbeefdeadbeef" # RPC endpoints [rpc_endpoints] diff --git a/src/Gateway.sol b/src/Gateway.sol index fd60cf6..4ae1bd1 100644 --- a/src/Gateway.sol +++ b/src/Gateway.sol @@ -3,7 +3,8 @@ pragma solidity >=0.8.0; -import {Schnorr} from "@frost-evm/Schnorr.sol"; +// 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"; @@ -250,25 +251,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 +289,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(451); // 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 +311,7 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { // Refund the chronicle gas unchecked { // Compute GMP gas used - uint256 gasUsed = 7223; + uint256 gasUsed = 7223 - 16 - 55; gasUsed = gasUsed.saturatingAdd(GasUtils.txBaseCost()); gasUsed = gasUsed.saturatingAdd(GasUtils.proxyOverheadGasCost(uint16(msg.data.length), 64)); gasUsed = gasUsed.saturatingAdd(initialGas - gasleft()); diff --git a/src/Primitives.sol b/src/Primitives.sol index 7ac91e3..2d36813 100644 --- a/src/Primitives.sol +++ b/src/Primitives.sol @@ -188,7 +188,6 @@ library PrimitiveUtils { mstore(ptr, hash) } } - return keccak256(keysHashed); } @@ -259,6 +258,32 @@ 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 hash and `IGmpReceiver.onGmpReceived` callback. @@ -274,18 +299,15 @@ library PrimitiveUtils { * * @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 { + assembly ("memory-safe") { // Allocate memory for the callback - callback := mload(0x40) + // callback := mload(0x40) ////////////////////////////////////////////////////////// // First need compute to `GmpMessage` EIP-712 Type Hash // @@ -295,12 +317,43 @@ library PrimitiveUtils { mstore(add(callback, 0x0000), GMP_MESSAGE_TYPE_HASH) // callback.eip712hash // Then we copy all `GmpMessage` fields to memory, except the `data` field. - mstore(add(callback, 0x0020), calldataload(add(message, 0x00))) // callback.source - mstore(add(callback, 0x0040), calldataload(add(message, 0x20))) // callback.srcNetwork - mstore(add(callback, 0x0060), calldataload(add(message, 0x40))) // callback.dest - mstore(add(callback, 0x0080), calldataload(add(message, 0x60))) // callback.destNetwork - mstore(add(callback, 0x00a0), calldataload(add(message, 0x80))) // callback.gasLimit - mstore(add(callback, 0x00c0), calldataload(add(message, 0xa0))) // callback.salt + 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) + } + } // mstore(add(callback, 0x00e0), calldataload(add(message, 0xa0))) // callback.data <- pointer // @@ -310,26 +363,19 @@ library PrimitiveUtils { // But we will skip this for now, once we need to store the `keccak256(message.data)` here // for computing the EIP-712 hash. - // MEMORY OFFSET | RESERVED FIELD | - // 0x00e0..0x0100 <- reserved for the `data` pointer - // 0x0100..0x0120 <- reserved for the `callback.length` field. - // 0x0120..0x0124 <- reserved for the `onGmpReceived.selector` field (4 bytes). - // 0x0124..0x0144 <- reserved for the `onGmpReceived.id` param. - // 0x0144..0x0164 <- reserved for the `onGmpReceived.network` param. - // 0x0164..0x0184 <- reserved for the `onGmpReceived.source` param. - // 0x0184..0x01a4 <- reserved for the `onGmpReceived.data.offset` param (calldata pointer). - // 0x01a4..0x01c4 <- reserved for the `onGmpReceived.data.length` param. - // 0x01c4..?????? <- reserved for the `onGmpReceived.data` bytes. - - // Store `onGmpReceived.data.length` at `0x01a4..0x01c4`. - let size := data.length - mstore(add(callback, 0x01c4), size) - - // Copy `message.data` to memory at `0x01c4`, as described above. - calldatacopy(add(callback, 0x01e4), data.offset, size) // message.data + // | MEMORY OFFSET | RESERVED FIELD | + // | 0x00e0..0x0100 <- reserved for the `callback.data` pointer + // | 0x0100..0x0120 <- reserved for the `callback.data.length` field. + // | 0x0120..0x0124 <- reserved for the `onGmpReceived.selector` field (4 bytes). + // | 0x0124..0x0144 <- reserved for the `onGmpReceived.id` param. + // | 0x0144..0x0164 <- reserved for the `onGmpReceived.network` param. + // | 0x0164..0x0184 <- reserved for the `onGmpReceived.source` param. + // | 0x0184..0x01a4 <- reserved for the `onGmpReceived.data.offset` param (calldata pointer). + // | 0x01a4..0x01c4 <- reserved for the `onGmpReceived.data.length` param. + // | 0x01c4..?????? <- reserved for the `onGmpReceived.data` bytes. // Compute `keccak256(message.data)` - let messageHash := keccak256(add(callback, 0x01e4), size) + let messageHash := keccak256(add(callback, 0x01c4), size) // temporarily store the result at `0x00e0..0x0100`, which is the end of // the `GmpMessage` struct. @@ -348,28 +394,40 @@ library PrimitiveUtils { mstore(callback, messageHash) // Replace the `eip712hash` by the `callback.data.offset`. - mstore(add(callback, 0x00e0), add(callback, 0x0120)) + mstore(add(callback, 0x00e0), add(callback, 0x0100)) + + // Update free memory pointer to the end of the callback (0x01c4 + data.length) + // size := and(add(add(add(callback, 0x01c4), size), 31), 0xffffffe0) + size := and(add(size, 31), 0xffffffe0) + { + let end := add(add(callback, 0x01c4), size) + mstore(0x40, and(add(end, 63), 0xffffffe0)) + } // 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) + size := add(size, 164) // Add the missing fields between `0x0100..0x01a4` to the callback. // The fields between `0x01a4..0x01c4` are already set. - mstore(add(callback, 0x0124), 0x01900937) // selector (4 bytes) - mstore(add(callback, 0x0120), size) // length (32 bytes) - mstore(add(callback, 0x0144), messageHash) // eip-712 hash (32 bytes) - mstore(add(callback, 0x0164), calldataload(add(message, 0x20))) // network (32 bytes) - mstore(add(callback, 0x0184), calldataload(add(message, 0x00))) // source (32 bytes) - mstore(add(callback, 0x01a4), 0x80) // payload.offset (32 bytes) - - // Update free memory pointer and align it to 32 bytes - size := add(and(add(size, 31), 0xffffffe0), 0x0120) - size := and(add(add(callback, size), 31), 0xffffffe0) - mstore(0x40, add(size, 0x40)) + 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) } } 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..0c8d86a 100644 --- a/src/utils/GasUtils.sol +++ b/src/utils/GasUtils.sol @@ -10,22 +10,26 @@ import {BranchlessMath} from "./BranchlessMath.sol"; * @dev Utilities for compute the GMP gas price, gas cost and gas needed. */ library GasUtils { - /** - * @dev Base cost of the `IExecutor.execute` method. - */ - uint256 internal constant EXECUTION_BASE_COST = 46658; + using BranchlessMath for uint256; + + // /** + // * @dev GatewayProxy overhead + // * SLOAD 2100 (implementation_slot) + // * CALLDATACOPY 3 + (words * 3) + (words * words) >> 9 + (words * 3) + // * DELEGATECALL 2100 (implementation) + // * OTHER OPCODES 21 + // */ + // uint256 internal constant GATEWAY_PROXY_OVERHEAD = 2100 + 3 + 2100 + 21; /** - * @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 = 46149; //46578; /** * @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 = 25838; /** * @dev Compute the gas cost of memory expansion. @@ -176,35 +180,107 @@ 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)) + } + } + 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 { - // 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 + // uint256 baseCost = 442; + uint256 memoryExpansion = 0x60; + + // all opcodes until message.intoCallback(DOMAIN_SEPARATOR) + // baseCost += 449; + + // -- message.intoCallback() -- + // baseCost += 438; + memoryExpansion = 0x80 + 0x01c4; + + // CALLDATACOPY 3 + (3 * words) + memory_expansion + // baseCost += 3; + uint256 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 += 448; + // -- 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 += 4; + memoryExpansion = memoryExpansion.align32(); + // ------ CALL ------ + // baseCost += 67; + // baseCost += 100; // SLOAD + // baseCost += 69; + // baseCost += 100; // SSTORE + + // -- emit GmpExecuted -- + // baseCost += 207; + // memoryExpansion += 0x40; + // baseCost += 2387; // LOG4 + // -- emit GmpExecuted -- + // baseCost += 26; + // 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 += 560; + // ----- GAS ------- - // keccak256 (6 gas per word) - gas = gas.saturatingAdd(words * 6); + // baseCost += 168; + // baseCost += 6800; + // REFUND CALL - // 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)); + // baseCost += 184; + // RETURN + // gas = gas.saturatingAdd(baseCost); + gas = gas.saturatingAdd(EXECUTION_BASE_COST); + gas = gas.saturatingAdd(memoryExpansionGasCost(_toWord(memoryExpansion))); return gas; } } @@ -229,12 +305,12 @@ 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 = inverseOfAllButOne64th(gasLimit); + // gasNeeded = gasNeeded.saturatingAdd(_executionGasCost(messageSize, gasLimit)); + 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); } 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..e6ff099 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,7 +28,7 @@ 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; diff --git a/test/GasUtils.t.sol b/test/GasUtils.t.sol index 08fdcd0..f92fd4a 100644 --- a/test/GasUtils.t.sol +++ b/test/GasUtils.t.sol @@ -193,6 +193,9 @@ contract GasUtilsBase is Test { * @dev Compare the estimated gas cost VS the actual gas cost of the `execute` method. */ function test_baseExecutionCost(uint16 messageSize, uint16 gasLimit) external { + // uint16 messageSize, uint16 gasLimit + // uint16 messageSize = 7944; + // uint16 gasLimit = 19072; vm.assume(gasLimit >= 5000); vm.assume(messageSize <= (0x6000 - 32)); messageSize += 32; @@ -220,31 +223,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), 31519 + baseCost); + assertEq(GasUtils.estimateGas(0, 33, 0), 31888 + baseCost); + assertEq(GasUtils.estimateGas(33, 0, 0), 32548 + baseCost); + assertEq(GasUtils.estimateGas(20, 13, 0), 32288 + 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), 31519 + baseCost); + assertEq(GasUtils.estimateWeiCost(one, 0, 0, 33, 0), 31888 + baseCost); + assertEq(GasUtils.estimateWeiCost(one, 0, 33, 0, 0), 32548 + baseCost); + assertEq(GasUtils.estimateWeiCost(one, 0, 20, 13, 0), 32288 + 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), (31519 + baseCost) * 2); + assertEq(GasUtils.estimateWeiCost(two, 0, 0, 33, 0), (31888 + baseCost) * 2); + assertEq(GasUtils.estimateWeiCost(two, 0, 33, 0, 0), (32548 + baseCost) * 2); + assertEq(GasUtils.estimateWeiCost(two, 0, 20, 13, 0), (32288 + baseCost) * 2); } } diff --git a/test/Gateway.t.sol b/test/Gateway.t.sol index 3c645e3..a611c39 100644 --- a/test/Gateway.t.sol +++ b/test/Gateway.t.sol @@ -3,10 +3,10 @@ pragma solidity >=0.8.0; -import {Signer} from "frost-evm/sol/Signer.sol"; +// 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 +26,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 +59,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 +145,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; @@ -142,7 +165,7 @@ contract GatewayBase is Test { uint16 internal constant DEST_NETWORK_ID = 1337; constructor() { - signer = new Signer(secret); + SigningKey memory signer = TestUtils.createSigner(SECRET); address deployer = TestUtils.createTestAccount(100 ether); vm.startPrank(deployer, deployer); @@ -152,6 +175,7 @@ contract GatewayBase is Test { // 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 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 @@ -189,8 +213,9 @@ 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}); } @@ -261,8 +286,11 @@ contract GatewayBase is Test { /** * @dev Test the gas metering for the `execute` function. */ - function test_gasMeter(uint16 messageSize) external { - vm.assume(messageSize <= 0x6000); + // function test_gasMeter(uint16 messageSize) external { + // vm.assume(messageSize <= 0x6000); + function test_gasMeter() external { + // uint16 messageSize = 7992; + uint16 messageSize = 800; vm.txGasPrice(1); address sender = TestUtils.createTestAccount(100 ether); @@ -270,16 +298,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,8 +341,18 @@ contract GatewayBase is Test { // Give sufficient gas ctx.gasLimit += 1; + ctx.executionCost = 0; + ctx.baseCost = 0; (status, returned) = ctx.execute(sig, gmp); + // if (ctx.gasLimit > 20000) { + // console.log(" gas needed:", GasUtils.executionGasNeeded(uint16(gmp.data.length), gmp.gasLimit)); + // console.log(" gas provided:", ctx.gasLimit - baseCost); + // console.log(" execution refund:", GasUtils.computeExecutionRefund(uint16(gmp.data.length), gmp.gasLimit)); + // console.log(" actual gas used:", ctx.executionCost); + // revert("successo"); + // } + 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"); @@ -350,6 +395,114 @@ contract GatewayBase is Test { ); } + function _binarySearch(uint256 lower, uint256 upper, function (uint256) internal returns (bool) cb) + private + returns (uint256, uint256) + { + unchecked { + uint256 snapshotId = vm.snapshotState(); + require(lower < upper, "'lower' must be less than 'upper'"); + uint256 freeMemory; + assembly { + freeMemory := mload(0x40) + } + bool target = cb(lower); + console.log("setup lower"); + vm.revertToState(snapshotId); + console.log("setup upper"); + require(cb(upper) != target, "cb(lower) == cb(upper)"); + console.log("setup done"); + // assembly { + // // Clear the memory + // let len := sub(mload(0x40), freeMemory) + // calldatacopy(freeMemory, calldatasize(), len) + // } + uint256 prev = 0; + uint256 mid = type(uint256).max; + while (prev != mid) { + prev = mid; + mid = (upper + lower) >> 1; + vm.revertToState(snapshotId); + if (cb(mid) == target) { + console.log("lower: ", mid, lower, upper); + lower = mid; + } else { + console.log("upper: ", mid, lower, upper); + upper = mid; + } + // assembly { + // // Clear the memory + // let len := sub(mload(0x40), freeMemory) + // calldatacopy(freeMemory, calldatasize(), len) + // mstore(0x40, freeMemory) + // } + } + vm.revertToState(snapshotId); + console.log("will return", lower, upper); + return (lower, upper); + } + } + + function _testMaxMemory(uint256 maxMemory) internal returns (bool) { + try vm.stopExpectSafeMemory() {} + catch { + console.log("vm.stopExpectSafeMemory ERROR"); + } + + uint16 messageSize = 800; + 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, + salt: 0, + data: new bytes(messageSize) + }); + Signature memory sig = sign(gmp); + + // Transaction Parameters + CallOptions memory ctx = CallOptions({ + from: sender, + to: address(gateway), + value: 0, + gasLimit: gasleft() / 2, + executionCost: 0, + baseCost: 0 + }); + + // Expect a revert + try vm.expectSafeMemoryCall(0, uint64(maxMemory)) {} + catch { + console.log("vm.expectSafeMemoryCall ERROR"); + } + (bool success,,) = ctx.tryExecute(sig, gmp); + return success; + } + + // function _testMaxMemoryCB(uint256 maxMemory) private returns (bool) { + // try GatewayBase(address(this))._testMaxMemory(maxMemory) { + // console.log(" OK -> ", maxMemory); + // return true; + // } catch (bytes memory reason) { + // console.log("ERR -> ", maxMemory, string(reason)); + // // console.log(string(reason)); + // return false; + // } + // } + + function test_jose() external { + uint256 lower = 1; + uint256 upper = 2000; + // require(_testMaxMemory(upper), "deu ruim"); + (lower, upper) = _binarySearch(lower, upper, _testMaxMemory); + console.log("success!", lower, upper); + } + function test_submitMessageMeter(uint16 messageSize) external { vm.assume(messageSize <= 0x6000); vm.txGasPrice(1); diff --git a/test/TestUtils.sol b/test/TestUtils.sol index 621a64c..b8c69f6 100644 --- a/test/TestUtils.sol +++ b/test/TestUtils.sol @@ -4,6 +4,7 @@ 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"; @@ -228,6 +229,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 +262,6 @@ library TestUtils { } // Execute - bool success; { (VmSafe.CallerMode callerMode, address msgSender, address txOrigin) = setCallerMode(VmSafe.CallerMode.RecurrentPrank, sender, sender); @@ -261,11 +274,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 +315,193 @@ library TestUtils { f(); setCallerMode(callerMode, msgSender, txOrigin); } + + /** + * @dev Call a function, then clear and restore the free memory pointer after the call + */ + function restoreMemory(uint256 data, function (uint256) internal returns (bool) callback) internal { + uint256 freeMemPtr; + assembly { + freeMemPtr := mload(0x40) + } + callback(data); + assembly { + // Clear the memory + let len := sub(mload(0x40), freeMemPtr) + calldatacopy(freeMemPtr, calldatasize(), len) + + // Restore the memory pointer + mstore(0x40, freeMemPtr) + } + } + + function binarySearch(uint256 lower, uint256 upper, function (uint256) internal returns (bool) cb) + private + returns (uint256, uint256) + { + unchecked { + uint256 snapshotId = vm.snapshotState(); + require(lower < upper, "'lower' must be less than 'upper'"); + uint256 freeMemory; + assembly { + freeMemory := mload(0x40) + } + bool target = cb(lower); + console.log("setup lower"); + vm.revertToState(snapshotId); + console.log("setup upper"); + require(cb(upper) != target, "cb(lower) == cb(upper)"); + console.log("setup done"); + assembly { + // Clear the memory + let len := sub(mload(0x40), freeMemory) + calldatacopy(freeMemory, calldatasize(), len) + } + uint256 prev = 0; + uint256 mid = type(uint256).max; + while (prev != mid) { + prev = mid; + mid = (upper + lower) >> 1; + vm.revertToState(snapshotId); + if (cb(mid) == target) { + console.log("lower: ", mid, lower, upper); + lower = mid; + } else { + console.log("upper: ", mid, lower, upper); + upper = mid; + } + // assembly { + // // Clear the memory + // let len := sub(mload(0x40), freeMemory) + // calldatacopy(freeMemory, calldatasize(), len) + // mstore(0x40, freeMemory) + // } + } + vm.revertToState(snapshotId); + console.log("will return", lower, upper); + return (lower, upper); + } + } } -library SigningUtils { +type Fn is uint256; + +type FnMut is uint256; + +library FnUtils { + function intoFn(function(uint256) internal pure returns (uint256) callback) internal pure returns (Fn fn) { + assembly { + fn := callback + } + } + + function intoFn(function(uint256) internal returns (uint256) callback) internal pure returns (FnMut fn) { + assembly { + fn := callback + } + } + + function callU256(FnMut fn, uint256 param) internal returns (uint256) { + function(uint256) internal returns (uint256) callback; + assembly { + callback := fn + } + return callback(param); + } + + function callU256(FnMut fn, bytes32 param) internal returns (uint256) { + function(bytes32) internal returns (uint256) callback; + assembly { + callback := fn + } + return callback(param); + } + + function callU256(FnMut fn, bytes memory param) internal returns (uint256) { + function(bytes memory) internal returns (uint256) callback; + assembly { + callback := fn + } + return callback(param); + } + + function callBool(FnMut fn, uint256 param) internal returns (bool) { + function(uint256) internal returns (bool) callback; + assembly { + callback := fn + } + return callback(param); + } + + function callBool(FnMut fn, bytes32 param) internal returns (bool) { + function(bytes32) internal returns (bool) callback; + assembly { + callback := fn + } + return callback(param); + } + + function callBool(FnMut fn, bytes memory param) internal returns (bool) { + function(bytes memory) internal returns (bool) callback; + assembly { + callback := fn + } + return callback(param); + } + + function asUint256Bytes(Fn fn) internal pure returns (function(uint256) internal pure returns (uint256) callback) { + assembly { + fn := callback + } + } +} + +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(VerifyingKey memory pubkey) internal pure returns (uint8) { + // return uint8(pubkey.py % 2) + 27; + // } + 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(VerifyingKey memory pubkey, bytes32 hash, address r) internal pure returns (uint256) { + // return uint256(keccak256(abi.encodePacked(r, yParity(pubkey), pubkey.px, uint256(hash)))); + // } + 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)))); + // return challenge(signer.pubkey, hash, r); } function signPrehashed(SigningKey memory signer, bytes32 hash, uint256 nonce) @@ -346,28 +524,29 @@ 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(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); + // return verifyPrehash(signer.pubkey, prehash, c, z); } function verify(SigningKey memory signer, bytes memory message, uint256 c, uint256 z) @@ -375,6 +554,7 @@ library SigningUtils { pure returns (bool) { - return verifyPrehash(signer.pubkey, keccak256(message), c, z); + return verifyPrehash(signer, keccak256(message), c, z); + // return verifyPrehash(signer.pubkey, keccak256(message), c, z); } } From 7ec87bbf8f33918d33fd5ad53f9f1c5385125564 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Fri, 29 Nov 2024 11:59:20 -0300 Subject: [PATCH 13/35] GatewayProxy --- src/GatewayProxy.sol | 47 ++++++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/src/GatewayProxy.sol b/src/GatewayProxy.sol index c9e0f11..f24d421 100644 --- a/src/GatewayProxy.sol +++ b/src/GatewayProxy.sol @@ -4,33 +4,54 @@ 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. + */ + IUniversalFactory internal constant FACTORY = IUniversalFactory(0x0000000000001C4Bf962dF86e38F0c10c7972C6E); + + /** + * @dev Minimal ERC-1967 proxy bytecode. */ bytes private constant PROXY_BYTECODE = hex"363d3d373d3d3d363d7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d82803e903d91603857fd5bf3"; - constructor(address implementation, bytes memory initializer) payable { - // Copy Proxy bytecode to memory - bytes memory bytecode = PROXY_BYTECODE; + 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); - } + // Copy Proxy bytecode to memory + bytes memory bytecode = PROXY_BYTECODE; - // Verify initialization result + // Return the `PROXY_BYTECODE`. /// @solidity memory-safe-assembly assembly { - if success { return(add(bytecode, 32), mload(bytecode)) } - revert(add(returndata, 32), mload(returndata)) + return(add(bytecode, 32), mload(bytecode)) } } } From f69c65e19733d17318a61a1a12d43fee8211b2b7 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Fri, 29 Nov 2024 12:11:52 -0300 Subject: [PATCH 14/35] allow build src --- foundry.toml | 3 ++- src/Gateway.sol | 4 ++-- src/Primitives.sol | 4 ++-- src/interfaces/IExecutor.sol | 19 +++++++++++++++++++ src/storage/Routes.sol | 4 ++-- 5 files changed, 27 insertions(+), 7 deletions(-) diff --git a/foundry.toml b/foundry.toml index 5ba672a..e4ed221 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,5 +1,6 @@ [profile.default] src = "src" +test = "test2" # TODO: change back to test before merge out = "out" libs = ["lib"] match_contract = ".+Test" @@ -7,7 +8,7 @@ match_contract = ".+Test" ######## # Lint # ######## -deny_warnings = true +# deny_warnings = true ################ # Solc options # diff --git a/src/Gateway.sol b/src/Gateway.sol index 4ae1bd1..cb01c90 100644 --- a/src/Gateway.sol +++ b/src/Gateway.sol @@ -493,7 +493,7 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { /** * @dev Register Shards in batch. */ - function setShard(TssKey[] calldata publicKeys) external { + function setShards(TssKey[] calldata publicKeys) external { require(msg.sender == _getAdmin(), "unauthorized"); ShardStore.getMainStorage().registerTssKeys(publicKeys); } @@ -536,7 +536,7 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { /** * @dev Create or update an array of routes */ - function setRoute(Route[] calldata values) external { + function setRoutes(Route[] calldata values) external { require(msg.sender == _getAdmin(), "unauthorized"); require(values.length > 0, "routes cannot be empty"); RouteStore.MainStorage storage store = RouteStore.getMainStorage(); diff --git a/src/Primitives.sol b/src/Primitives.sol index 2d36813..1c0e84c 100644 --- a/src/Primitives.sol +++ b/src/Primitives.sol @@ -102,8 +102,8 @@ struct Route { uint64 gasLimit; uint128 baseFee; bytes32 gateway; - uint256 relativeGasPriceNumerator; - uint256 relativeGasPriceDenominator; + uint128 relativeGasPriceNumerator; + uint128 relativeGasPriceDenominator; } /** diff --git a/src/interfaces/IExecutor.sol b/src/interfaces/IExecutor.sol index 71b18d9..df882ea 100644 --- a/src/interfaces/IExecutor.sol +++ b/src/interfaces/IExecutor.sol @@ -44,11 +44,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..a9724f2 100644 --- a/src/storage/Routes.sol +++ b/src/storage/Routes.sol @@ -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; From 3a941f50a16d95204cd14c46d832643df3d959e8 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Fri, 29 Nov 2024 13:47:19 -0300 Subject: [PATCH 15/35] Fixed unit tests --- src/Gateway.sol | 2 +- src/Primitives.sol | 14 +++++--------- src/utils/GasUtils.sol | 33 ++++++++++++++++++--------------- test/GasUtils.t.sol | 24 ++++++++++++------------ test/Gateway.t.sol | 2 +- 5 files changed, 37 insertions(+), 38 deletions(-) diff --git a/src/Gateway.sol b/src/Gateway.sol index cb01c90..9b4844e 100644 --- a/src/Gateway.sol +++ b/src/Gateway.sol @@ -289,7 +289,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(451); + initialGas = initialGas.saturatingAdd(429); // Theoretically we could remove the destination network field // and fill it up with the network id of the contract, then the signature will fail. diff --git a/src/Primitives.sol b/src/Primitives.sol index 1c0e84c..04d7391 100644 --- a/src/Primitives.sol +++ b/src/Primitives.sol @@ -396,20 +396,12 @@ library PrimitiveUtils { // Replace the `eip712hash` by the `callback.data.offset`. mstore(add(callback, 0x00e0), add(callback, 0x0100)) - // Update free memory pointer to the end of the callback (0x01c4 + data.length) - // size := and(add(add(add(callback, 0x01c4), size), 31), 0xffffffe0) - size := and(add(size, 31), 0xffffffe0) - { - let end := add(add(callback, 0x01c4), size) - mstore(0x40, and(add(end, 63), 0xffffffe0)) - } - // 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(size, 164) + 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. @@ -428,6 +420,10 @@ library PrimitiveUtils { } 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/utils/GasUtils.sol b/src/utils/GasUtils.sol index 0c8d86a..84bc253 100644 --- a/src/utils/GasUtils.sol +++ b/src/utils/GasUtils.sol @@ -24,12 +24,12 @@ library GasUtils { /** * @dev Base cost of the `IExecutor.execute` method. */ - uint256 internal constant EXECUTION_BASE_COST = 46149; //46578; + uint256 internal constant EXECUTION_BASE_COST = 46100; /** * @dev Base cost of the `IGateway.submitMessage` method. */ - uint256 internal constant SUBMIT_BASE_COST = 25838; + uint256 internal constant SUBMIT_BASE_COST = 25845; /** * @dev Compute the gas cost of memory expansion. @@ -193,7 +193,7 @@ library GasUtils { // Safety: The operations below can't overflow because the message size can't be greater than 2**16 unchecked { // Selector overhead - // uint256 baseCost = 442; + // uint256 baseCost = 420; uint256 memoryExpansion = 0x60; // all opcodes until message.intoCallback(DOMAIN_SEPARATOR) @@ -215,9 +215,10 @@ library GasUtils { // keccak256 30 + 6 gas per word // baseCost += 30; gas = gas.saturatingAdd(_toWord(messageSize) * 6); + // baseCost += 424; - // baseCost += 448; // -- message.intoCallback() -- + // baseCost += 34; // -- _verifySignature -- @@ -229,12 +230,14 @@ library GasUtils { // _execute // baseCost += 22551; // baseCost += 2; // GAS + // baseCost += 97; // ------ CALL ------ + // baseCost += 2600; gas = gas.saturatingAdd(gasUsed); - memoryExpansion += 4; - memoryExpansion = memoryExpansion.align32(); + memoryExpansion = (messageSize.align32() + 0x80 + 0x0120 + 164).align32(); + // ------ CALL ------ // baseCost += 67; // baseCost += 100; // SLOAD @@ -242,11 +245,14 @@ library GasUtils { // baseCost += 100; // SSTORE // -- emit GmpExecuted -- - // baseCost += 207; - // memoryExpansion += 0x40; + // baseCost += 141; + memoryExpansion += 0x20; // MSTORE + // baseCost += 24; + memoryExpansion += 0x20; // MSTORE + // baseCost += 39; // baseCost += 2387; // LOG4 - // -- emit GmpExecuted -- // baseCost += 26; + // -- emit GmpExecuted -- // end _execute // baseCost += 34; @@ -271,12 +277,9 @@ library GasUtils { // baseCost += 560; // ----- GAS ------- - // baseCost += 168; - // baseCost += 6800; - // REFUND CALL - - // baseCost += 184; - // RETURN + // baseCost += 168; // GAS + // baseCost += 6800; // REFUND CALL + // baseCost += 184; // RETURN // gas = gas.saturatingAdd(baseCost); gas = gas.saturatingAdd(EXECUTION_BASE_COST); diff --git a/test/GasUtils.t.sol b/test/GasUtils.t.sol index f92fd4a..f0271e1 100644 --- a/test/GasUtils.t.sol +++ b/test/GasUtils.t.sol @@ -246,22 +246,22 @@ contract GasUtilsBase is Test { function test_gasUtils() external pure { uint256 baseCost = GasUtils.EXECUTION_BASE_COST; - assertEq(GasUtils.estimateGas(0, 0, 0), 31519 + baseCost); - assertEq(GasUtils.estimateGas(0, 33, 0), 31888 + baseCost); - assertEq(GasUtils.estimateGas(33, 0, 0), 32548 + baseCost); - assertEq(GasUtils.estimateGas(20, 13, 0), 32288 + baseCost); + assertEq(GasUtils.estimateGas(0, 0, 0), 31522 + baseCost); + assertEq(GasUtils.estimateGas(0, 33, 0), 31895 + baseCost); + assertEq(GasUtils.estimateGas(33, 0, 0), 32555 + baseCost); + assertEq(GasUtils.estimateGas(20, 13, 0), 32295 + baseCost); UFloat9x56 one = UFloatMath.ONE; - assertEq(GasUtils.estimateWeiCost(one, 0, 0, 0, 0), 31519 + baseCost); - assertEq(GasUtils.estimateWeiCost(one, 0, 0, 33, 0), 31888 + baseCost); - assertEq(GasUtils.estimateWeiCost(one, 0, 33, 0, 0), 32548 + baseCost); - assertEq(GasUtils.estimateWeiCost(one, 0, 20, 13, 0), 32288 + baseCost); + assertEq(GasUtils.estimateWeiCost(one, 0, 0, 0, 0), 31522 + baseCost); + assertEq(GasUtils.estimateWeiCost(one, 0, 0, 33, 0), 31895 + baseCost); + assertEq(GasUtils.estimateWeiCost(one, 0, 33, 0, 0), 32555 + baseCost); + assertEq(GasUtils.estimateWeiCost(one, 0, 20, 13, 0), 32295 + baseCost); UFloat9x56 two = UFloat9x56.wrap(0x8080000000000000); - assertEq(GasUtils.estimateWeiCost(two, 0, 0, 0, 0), (31519 + baseCost) * 2); - assertEq(GasUtils.estimateWeiCost(two, 0, 0, 33, 0), (31888 + baseCost) * 2); - assertEq(GasUtils.estimateWeiCost(two, 0, 33, 0, 0), (32548 + baseCost) * 2); - assertEq(GasUtils.estimateWeiCost(two, 0, 20, 13, 0), (32288 + baseCost) * 2); + assertEq(GasUtils.estimateWeiCost(two, 0, 0, 0, 0), (31522 + baseCost) * 2); + assertEq(GasUtils.estimateWeiCost(two, 0, 0, 33, 0), (31895 + baseCost) * 2); + assertEq(GasUtils.estimateWeiCost(two, 0, 33, 0, 0), (32555 + baseCost) * 2); + assertEq(GasUtils.estimateWeiCost(two, 0, 20, 13, 0), (32295 + baseCost) * 2); } } diff --git a/test/Gateway.t.sol b/test/Gateway.t.sol index a611c39..6ba4a89 100644 --- a/test/Gateway.t.sol +++ b/test/Gateway.t.sol @@ -240,7 +240,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 + 133815); } function test_checkPayloadSize() external { From f9e6eae0f8b6c1835790d2864f4bed780e0c884b Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Fri, 29 Nov 2024 14:01:55 -0300 Subject: [PATCH 16/35] Remove unused code --- src/Primitives.sol | 33 ++++------- src/utils/GasUtils.sol | 13 +---- test/GasUtils.t.sol | 3 - test/Gateway.t.sol | 125 +---------------------------------------- 4 files changed, 14 insertions(+), 160 deletions(-) diff --git a/src/Primitives.sol b/src/Primitives.sol index 04d7391..f7d38fb 100644 --- a/src/Primitives.sol +++ b/src/Primitives.sol @@ -306,9 +306,17 @@ library PrimitiveUtils { view { assembly ("memory-safe") { - // Allocate memory for the callback - // callback := mload(0x40) - + // | 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 // ////////////////////////////////////////////////////////// @@ -355,25 +363,6 @@ library PrimitiveUtils { } } - // mstore(add(callback, 0x00e0), calldataload(add(message, 0xa0))) // callback.data <- pointer - // - // Following `Solidity memory layout`, here we must store a pointer to the `bytes` field. - // ref: https://docs.soliditylang.org/en/latest/internals/layout_in_memory.html - // - // But we will skip this for now, once we need to store the `keccak256(message.data)` here - // for computing the EIP-712 hash. - - // | MEMORY OFFSET | RESERVED FIELD | - // | 0x00e0..0x0100 <- reserved for the `callback.data` pointer - // | 0x0100..0x0120 <- reserved for the `callback.data.length` field. - // | 0x0120..0x0124 <- reserved for the `onGmpReceived.selector` field (4 bytes). - // | 0x0124..0x0144 <- reserved for the `onGmpReceived.id` param. - // | 0x0144..0x0164 <- reserved for the `onGmpReceived.network` param. - // | 0x0164..0x0184 <- reserved for the `onGmpReceived.source` param. - // | 0x0184..0x01a4 <- reserved for the `onGmpReceived.data.offset` param (calldata pointer). - // | 0x01a4..0x01c4 <- reserved for the `onGmpReceived.data.length` param. - // | 0x01c4..?????? <- reserved for the `onGmpReceived.data` bytes. - // Compute `keccak256(message.data)` let messageHash := keccak256(add(callback, 0x01c4), size) diff --git a/src/utils/GasUtils.sol b/src/utils/GasUtils.sol index 84bc253..158bf83 100644 --- a/src/utils/GasUtils.sol +++ b/src/utils/GasUtils.sol @@ -12,15 +12,6 @@ import {BranchlessMath} from "./BranchlessMath.sol"; library GasUtils { using BranchlessMath for uint256; - // /** - // * @dev GatewayProxy overhead - // * SLOAD 2100 (implementation_slot) - // * CALLDATACOPY 3 + (words * 3) + (words * words) >> 9 + (words * 3) - // * DELEGATECALL 2100 (implementation) - // * OTHER OPCODES 21 - // */ - // uint256 internal constant GATEWAY_PROXY_OVERHEAD = 2100 + 3 + 2100 + 21; - /** * @dev Base cost of the `IExecutor.execute` method. */ @@ -90,7 +81,7 @@ library GasUtils { gasCost += words << 8; // Memory expansion cost - words += 13 + 4; + words += 17; gasCost += ((words * words) >> 9) + (words * 3); return gasCost; @@ -308,8 +299,6 @@ library GasUtils { */ function executionGasNeeded(uint256 messageSize, uint256 gasLimit) internal pure returns (uint256 gasNeeded) { unchecked { - // gasNeeded = inverseOfAllButOne64th(gasLimit); - // gasNeeded = gasNeeded.saturatingAdd(_executionGasCost(messageSize, gasLimit)); gasNeeded = _executionGasCost(messageSize, gasLimit); gasNeeded = gasNeeded.saturatingAdd(2300 - 184); gasNeeded = inverseOfAllButOne64th(gasNeeded); diff --git a/test/GasUtils.t.sol b/test/GasUtils.t.sol index f0271e1..5b1fec6 100644 --- a/test/GasUtils.t.sol +++ b/test/GasUtils.t.sol @@ -193,9 +193,6 @@ contract GasUtilsBase is Test { * @dev Compare the estimated gas cost VS the actual gas cost of the `execute` method. */ function test_baseExecutionCost(uint16 messageSize, uint16 gasLimit) external { - // uint16 messageSize, uint16 gasLimit - // uint16 messageSize = 7944; - // uint16 gasLimit = 19072; vm.assume(gasLimit >= 5000); vm.assume(messageSize <= (0x6000 - 32)); messageSize += 32; diff --git a/test/Gateway.t.sol b/test/Gateway.t.sol index 6ba4a89..871881b 100644 --- a/test/Gateway.t.sol +++ b/test/Gateway.t.sol @@ -3,7 +3,6 @@ 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, SigningKey, SigningUtils} from "./TestUtils.sol"; @@ -175,7 +174,6 @@ contract GatewayBase is Test { // 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 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 @@ -286,11 +284,8 @@ contract GatewayBase is Test { /** * @dev Test the gas metering for the `execute` function. */ - // function test_gasMeter(uint16 messageSize) external { - // vm.assume(messageSize <= 0x6000); - function test_gasMeter() external { - // uint16 messageSize = 7992; - uint16 messageSize = 800; + function test_gasMeter(uint16 messageSize) external { + vm.assume(messageSize <= 0x6000 && messageSize >= 32); vm.txGasPrice(1); address sender = TestUtils.createTestAccount(100 ether); @@ -345,14 +340,6 @@ contract GatewayBase is Test { ctx.baseCost = 0; (status, returned) = ctx.execute(sig, gmp); - // if (ctx.gasLimit > 20000) { - // console.log(" gas needed:", GasUtils.executionGasNeeded(uint16(gmp.data.length), gmp.gasLimit)); - // console.log(" gas provided:", ctx.gasLimit - baseCost); - // console.log(" execution refund:", GasUtils.computeExecutionRefund(uint16(gmp.data.length), gmp.gasLimit)); - // console.log(" actual gas used:", ctx.executionCost); - // revert("successo"); - // } - 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"); @@ -395,114 +382,6 @@ contract GatewayBase is Test { ); } - function _binarySearch(uint256 lower, uint256 upper, function (uint256) internal returns (bool) cb) - private - returns (uint256, uint256) - { - unchecked { - uint256 snapshotId = vm.snapshotState(); - require(lower < upper, "'lower' must be less than 'upper'"); - uint256 freeMemory; - assembly { - freeMemory := mload(0x40) - } - bool target = cb(lower); - console.log("setup lower"); - vm.revertToState(snapshotId); - console.log("setup upper"); - require(cb(upper) != target, "cb(lower) == cb(upper)"); - console.log("setup done"); - // assembly { - // // Clear the memory - // let len := sub(mload(0x40), freeMemory) - // calldatacopy(freeMemory, calldatasize(), len) - // } - uint256 prev = 0; - uint256 mid = type(uint256).max; - while (prev != mid) { - prev = mid; - mid = (upper + lower) >> 1; - vm.revertToState(snapshotId); - if (cb(mid) == target) { - console.log("lower: ", mid, lower, upper); - lower = mid; - } else { - console.log("upper: ", mid, lower, upper); - upper = mid; - } - // assembly { - // // Clear the memory - // let len := sub(mload(0x40), freeMemory) - // calldatacopy(freeMemory, calldatasize(), len) - // mstore(0x40, freeMemory) - // } - } - vm.revertToState(snapshotId); - console.log("will return", lower, upper); - return (lower, upper); - } - } - - function _testMaxMemory(uint256 maxMemory) internal returns (bool) { - try vm.stopExpectSafeMemory() {} - catch { - console.log("vm.stopExpectSafeMemory ERROR"); - } - - uint16 messageSize = 800; - 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, - salt: 0, - data: new bytes(messageSize) - }); - Signature memory sig = sign(gmp); - - // Transaction Parameters - CallOptions memory ctx = CallOptions({ - from: sender, - to: address(gateway), - value: 0, - gasLimit: gasleft() / 2, - executionCost: 0, - baseCost: 0 - }); - - // Expect a revert - try vm.expectSafeMemoryCall(0, uint64(maxMemory)) {} - catch { - console.log("vm.expectSafeMemoryCall ERROR"); - } - (bool success,,) = ctx.tryExecute(sig, gmp); - return success; - } - - // function _testMaxMemoryCB(uint256 maxMemory) private returns (bool) { - // try GatewayBase(address(this))._testMaxMemory(maxMemory) { - // console.log(" OK -> ", maxMemory); - // return true; - // } catch (bytes memory reason) { - // console.log("ERR -> ", maxMemory, string(reason)); - // // console.log(string(reason)); - // return false; - // } - // } - - function test_jose() external { - uint256 lower = 1; - uint256 upper = 2000; - // require(_testMaxMemory(upper), "deu ruim"); - (lower, upper) = _binarySearch(lower, upper, _testMaxMemory); - console.log("success!", lower, upper); - } - function test_submitMessageMeter(uint16 messageSize) external { vm.assume(messageSize <= 0x6000); vm.txGasPrice(1); From 8f604eb6fe1e18462aebb4fca37d2f6e7c52cadb Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Fri, 29 Nov 2024 14:02:29 -0300 Subject: [PATCH 17/35] Cleanup TestUtils.sol --- test/TestUtils.sol | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/test/TestUtils.sol b/test/TestUtils.sol index b8c69f6..4c0192b 100644 --- a/test/TestUtils.sol +++ b/test/TestUtils.sol @@ -370,12 +370,6 @@ library TestUtils { console.log("upper: ", mid, lower, upper); upper = mid; } - // assembly { - // // Clear the memory - // let len := sub(mload(0x40), freeMemory) - // calldatacopy(freeMemory, calldatasize(), len) - // mstore(0x40, freeMemory) - // } } vm.revertToState(snapshotId); console.log("will return", lower, upper); @@ -483,10 +477,6 @@ library VerifyingUtils { } library SigningUtils { - // function yParity(VerifyingKey memory pubkey) internal pure returns (uint8) { - // return uint8(pubkey.py % 2) + 27; - // } - function yParity(SigningKey memory signer) internal pure returns (uint8) { return uint8(signer.pubkey.py % 2) + 27; } @@ -495,13 +485,8 @@ library SigningUtils { return signer.pubkey.px; } - // 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 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)))); - // return challenge(signer.pubkey, hash, r); } function signPrehashed(SigningKey memory signer, bytes32 hash, uint256 nonce) @@ -524,29 +509,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 Schnorr.verify(yParity(signer), signer.pubkey.px, uint256(prehash), c, z); - // return verifyPrehash(signer.pubkey, prehash, c, z); } function verify(SigningKey memory signer, bytes memory message, uint256 c, uint256 z) @@ -555,6 +523,5 @@ library SigningUtils { returns (bool) { return verifyPrehash(signer, keccak256(message), c, z); - // return verifyPrehash(signer.pubkey, keccak256(message), c, z); } } From dba2c1bbbd8306a0e4e0ac9a014253d8d51ae2c9 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Fri, 29 Nov 2024 14:05:45 -0300 Subject: [PATCH 18/35] Remove unused import --- src/Gateway.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Gateway.sol b/src/Gateway.sol index 9b4844e..54609c9 100644 --- a/src/Gateway.sol +++ b/src/Gateway.sol @@ -3,7 +3,6 @@ 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"; From c34bd5a33233158472fb0743aaf4eabc4df6a7d0 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Fri, 29 Nov 2024 14:12:51 -0300 Subject: [PATCH 19/35] update refund constants --- src/Gateway.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gateway.sol b/src/Gateway.sol index 54609c9..d36645c 100644 --- a/src/Gateway.sol +++ b/src/Gateway.sol @@ -310,7 +310,7 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { // Refund the chronicle gas unchecked { // Compute GMP gas used - uint256 gasUsed = 7223 - 16 - 55; + uint256 gasUsed = 7152; gasUsed = gasUsed.saturatingAdd(GasUtils.txBaseCost()); gasUsed = gasUsed.saturatingAdd(GasUtils.proxyOverheadGasCost(uint16(msg.data.length), 64)); gasUsed = gasUsed.saturatingAdd(initialGas - gasleft()); From 7e34a3b8d8963b2a99e9e47fdff8e9cf4cfdd27c Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Fri, 29 Nov 2024 14:15:21 -0300 Subject: [PATCH 20/35] Remove more commented code --- src/Gateway.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Gateway.sol b/src/Gateway.sol index d36645c..89dc35b 100644 --- a/src/Gateway.sol +++ b/src/Gateway.sol @@ -150,7 +150,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); @@ -417,7 +416,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 From d371876d8e2c957b73c26328e50561abf94a5531 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Tue, 3 Dec 2024 07:07:57 -0300 Subject: [PATCH 21/35] Replace shards --- src/Gateway.sol | 2 +- test/Gateway.t.sol | 68 +++++++++++++++++++++++++++++++++++++++++++--- test/TestUtils.sol | 11 ++++++-- 3 files changed, 74 insertions(+), 7 deletions(-) diff --git a/src/Gateway.sol b/src/Gateway.sol index 89dc35b..db22b33 100644 --- a/src/Gateway.sol +++ b/src/Gateway.sol @@ -492,7 +492,7 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { */ function setShards(TssKey[] calldata publicKeys) external { require(msg.sender == _getAdmin(), "unauthorized"); - ShardStore.getMainStorage().registerTssKeys(publicKeys); + ShardStore.getMainStorage().replaceTssKeys(publicKeys); } /** diff --git a/test/Gateway.t.sol b/test/Gateway.t.sol index 871881b..c63ec70 100644 --- a/test/Gateway.t.sol +++ b/test/Gateway.t.sol @@ -163,13 +163,15 @@ contract GatewayBase is Test { uint16 private constant SRC_NETWORK_ID = 1234; uint16 internal constant DEST_NETWORK_ID = 1337; + address internal constant ADMIN = address(uint160(uint256(keccak256("proxy.admin")))); + constructor() { SigningKey memory signer = TestUtils.createSigner(SECRET); - address deployer = TestUtils.createTestAccount(100 ether); - vm.startPrank(deployer, deployer); + vm.deal(ADMIN, 100 ether); + vm.startPrank(ADMIN, ADMIN); // 1 - Deploy the implementation contract - address proxyAddr = vm.computeCreateAddress(deployer, vm.getNonce(deployer) + 1); + address proxyAddr = vm.computeCreateAddress(ADMIN, vm.getNonce(ADMIN) + 1); Gateway implementation = new Gateway(DEST_NETWORK_ID, proxyAddr); // 2 - Deploy the Proxy Contract @@ -180,7 +182,7 @@ contract GatewayBase is Test { 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)); + bytes memory initializer = abi.encodeCall(Gateway.initialize, (ADMIN, keys, networks)); gateway = Gateway(address(new GatewayProxy(address(implementation), initializer))); vm.deal(address(gateway), 100 ether); @@ -217,6 +219,64 @@ contract GatewayBase is Test { 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[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 ? 1 : 0, 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 ? 1 : 0; + _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, diff --git a/test/TestUtils.sol b/test/TestUtils.sol index 4c0192b..175eef7 100644 --- a/test/TestUtils.sol +++ b/test/TestUtils.sol @@ -180,9 +180,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); } From 0ec53a573fc7306a690decc82a5d641efaf69e6e Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Tue, 3 Dec 2024 08:22:05 -0300 Subject: [PATCH 22/35] update shards --- src/storage/Shards.sol | 80 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 73 insertions(+), 7 deletions(-) diff --git a/src/storage/Shards.sol b/src/storage/Shards.sol index aa890a1..60f8e20 100644 --- a/src/storage/Shards.sol +++ b/src/storage/Shards.sol @@ -194,7 +194,7 @@ 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"); @@ -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 @@ -223,6 +224,7 @@ library ShardStore { BranchlessMath.ternaryU64(shard.createdAtBlock > 0, shard.createdAtBlock, uint64(block.number)); stored.nonce = shard.nonce; stored.yParity = newKey.yParity; + return true; } /** @@ -240,14 +242,73 @@ library ShardStore { } } + /** + * @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 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 Date: Tue, 3 Dec 2024 08:53:23 -0300 Subject: [PATCH 23/35] add admin method --- src/Gateway.sol | 38 ++++++++++++++++++-------------------- src/utils/GasUtils.sol | 4 ++-- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/Gateway.sol b/src/Gateway.sol index db22b33..e01ef48 100644 --- a/src/Gateway.sol +++ b/src/Gateway.sol @@ -139,10 +139,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 { + function initialize(address proxyAdmin, TssKey[] calldata keys, Network[] calldata networks) external { require(PROXY_ADDRESS == address(this), "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. @@ -287,7 +287,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(429); + initialGas = initialGas.saturatingAdd(496); // Theoretically we could remove the destination network field // and fill it up with the network id of the contract, then the signature will fail. @@ -435,7 +435,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,7 +483,7 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { * @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); } @@ -491,7 +491,7 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { * @dev Register Shards in batch. */ function setShards(TssKey[] calldata publicKeys) external { - require(msg.sender == _getAdmin(), "unauthorized"); + require(msg.sender == ERC1967.getAdmin(), "unauthorized"); ShardStore.getMainStorage().replaceTssKeys(publicKeys); } @@ -499,7 +499,7 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { * @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); } @@ -507,7 +507,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); } @@ -526,7 +526,7 @@ 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); } @@ -534,7 +534,7 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { * @dev Create or update an array of routes */ function setRoutes(Route[] calldata values) external { - require(msg.sender == _getAdmin(), "unauthorized"); + 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++) { @@ -546,33 +546,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; @@ -597,7 +595,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); @@ -608,7 +606,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/utils/GasUtils.sol b/src/utils/GasUtils.sol index 158bf83..b4b7928 100644 --- a/src/utils/GasUtils.sol +++ b/src/utils/GasUtils.sol @@ -15,12 +15,12 @@ library GasUtils { /** * @dev Base cost of the `IExecutor.execute` method. */ - uint256 internal constant EXECUTION_BASE_COST = 46100; + uint256 internal constant EXECUTION_BASE_COST = 46167; /** * @dev Base cost of the `IGateway.submitMessage` method. */ - uint256 internal constant SUBMIT_BASE_COST = 25845; + uint256 internal constant SUBMIT_BASE_COST = 25845 - 22; /** * @dev Compute the gas cost of memory expansion. From af3baf3f9c95498011435b20346be6e3bbb07dc3 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Tue, 3 Dec 2024 10:13:26 -0300 Subject: [PATCH 24/35] accept 3 or 2 as parity bit --- src/Gateway.sol | 10 ++++++++-- src/storage/Shards.sol | 8 ++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Gateway.sol b/src/Gateway.sol index e01ef48..656ee73 100644 --- a/src/Gateway.sol +++ b/src/Gateway.sol @@ -83,6 +83,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; @@ -92,6 +97,7 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { // 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 @@ -140,7 +146,7 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { // EIP-712 typed hash function initialize(address proxyAdmin, TssKey[] calldata keys, Network[] calldata networks) external { - require(PROXY_ADDRESS == address(this), "only proxy can be initialize"); + require(PROXY_ADDRESS == address(this) || msg.sender == FACTORY, "only proxy can be initialize"); require(prevMessageHash == 0, "already initialized"); ERC1967.setAdmin(proxyAdmin); @@ -476,7 +482,7 @@ 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}); } /** diff --git a/src/storage/Shards.sol b/src/storage/Shards.sol index 60f8e20..2033663 100644 --- a/src/storage/Shards.sol +++ b/src/storage/Shards.sol @@ -196,7 +196,7 @@ library ShardStore { */ 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)); @@ -223,7 +223,7 @@ 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; } @@ -265,7 +265,7 @@ library ShardStore { if (register(store, key)) { // Shard registered created[createdCount++] = TssKey({ - yParity: key.yParity, + yParity: key.yParity + 2, xCoord: key.xCoord }); } else { @@ -352,7 +352,7 @@ library ShardStore { 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; } From 1f9e31bc198421aad8aa9210c0c88f2b36692204 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Tue, 3 Dec 2024 11:49:47 -0300 Subject: [PATCH 25/35] Fix update route method --- scripts/Deploy.sol | 19 +++++++++++++++- src/Gateway.sol | 16 -------------- src/Primitives.sol | 42 +----------------------------------- src/interfaces/IExecutor.sol | 1 - src/storage/Routes.sol | 4 ++-- src/storage/Shards.sol | 19 +++++++++------- test/Gateway.t.sol | 6 +++--- 7 files changed, 35 insertions(+), 72 deletions(-) diff --git a/scripts/Deploy.sol b/scripts/Deploy.sol index c960606..ea26e16 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; diff --git a/src/Gateway.sol b/src/Gateway.sol index 656ee73..f25b52b 100644 --- a/src/Gateway.sol +++ b/src/Gateway.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; @@ -110,20 +108,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. diff --git a/src/Primitives.sol b/src/Primitives.sol index f7d38fb..9ce9a80 100644 --- a/src/Primitives.sol +++ b/src/Primitives.sol @@ -71,23 +71,7 @@ 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. @@ -202,30 +186,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 diff --git a/src/interfaces/IExecutor.sol b/src/interfaces/IExecutor.sol index df882ea..ab0393f 100644 --- a/src/interfaces/IExecutor.sol +++ b/src/interfaces/IExecutor.sol @@ -10,7 +10,6 @@ import { GmpStatus, GmpStatus, UpdateKeysMessage, - UpdateNetworkInfo, GmpSender, Route } from "../Primitives.sol"; diff --git a/src/storage/Routes.sol b/src/storage/Routes.sol index a9724f2..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)) { diff --git a/src/storage/Shards.sol b/src/storage/Shards.sol index 2033663..5841258 100644 --- a/src/storage/Shards.sol +++ b/src/storage/Shards.sol @@ -298,22 +298,25 @@ library ShardStore { } } - function revoke(MainStorage storage store, TssKey calldata id) internal { - _revoke(store, ShardID.wrap(bytes32(id.xCoord))); - } - /** - * @dev Register TSS keys. + * @dev Revoke Shards keys. * Requirements: * - The `keys` must be registered. */ - function _revoke(MainStorage storage store, ShardID id) private { + function revoke(MainStorage storage store, TssKey calldata key) internal { // Read shard from storage - 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)); } diff --git a/test/Gateway.t.sol b/test/Gateway.t.sol index c63ec70..90c5eb5 100644 --- a/test/Gateway.t.sol +++ b/test/Gateway.t.sol @@ -239,7 +239,7 @@ contract GatewayBase is Test { SigningKey memory signer; for (uint256 i = 0; i < keys.length; i++) { signer = TestUtils.signerFromEntropy(bytes32(i)); - keys[i] = TssKey({yParity: signer.yParity() == 28 ? 1 : 0, xCoord: signer.xCoord()}); + keys[i] = TssKey({yParity: signer.yParity() == 28 ? 3 : 2, xCoord: signer.xCoord()}); } _shortTssKeys(keys); @@ -260,10 +260,10 @@ contract GatewayBase is Test { assertEq(shards[i].yParity, keys[i].yParity); } - // Replace one shard key + // // Replace one shard key signer = TestUtils.signerFromEntropy(bytes32(uint256(12345))); keys[0].xCoord = signer.xCoord(); - keys[0].yParity = signer.yParity() == 28 ? 1 : 0; + keys[0].yParity = signer.yParity() == 28 ? 3 : 2; _shortTssKeys(keys); vm.prank(ADMIN, ADMIN); gateway.setShards(keys); From 9234bc012925db4566bdc7caac7ecd93fe7cd816 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Tue, 3 Dec 2024 13:00:04 -0300 Subject: [PATCH 26/35] created GmpProxy --- test/utils/GmpProxy.sol | 63 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 test/utils/GmpProxy.sol diff --git a/test/utils/GmpProxy.sol b/test/utils/GmpProxy.sol new file mode 100644 index 0000000..9222183 --- /dev/null +++ b/test/utils/GmpProxy.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (src/GatewayProxy.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"; +// import {GmpMessage as Message} from "../../src/Primitives.sol"; +// import {Context, CreateKind, IUniversalFactory} from "@universal-factory/IUniversalFactory.sol"; + + +contract GatewayProxy is IGmpReceiver { + using BranchlessMath for uint256; + + event MessageReceived(bytes32 indexed id, GmpMessage msg); + + struct GmpMessage { + bytes32 foreign; + uint16 foreign_network; + address local; + uint128 gasLimit; + uint128 gasCost; + 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 { + require(message.foreign == bytes32(uint256(uint160(address(this)))), "Invalid foreign address"); + require(message.foreign_network == NETWORK_ID, "Invalid foreign network"); + require(message.local == address(this), "Invalid local address"); + uint256 value = address(this).balance.min(msg.value); + address destination = address(uint160(uint256(message.foreign))); + GATEWAY.submitMessage{value: value}(destination, message.foreign_network, message.gasLimit, message.data); + } + + function onGmpReceived(bytes32 id, uint128 network, bytes32 source, bytes calldata payload) + external + payable + returns (bytes32) + { + GmpMessage memory message = GmpMessage({ + foreign: source, + foreign_network: uint16(network), + local: address(this), + gasLimit: uint128(gasleft()), + gasCost: uint128(tx.gasprice), + nonce: 0, + data: payload + }); + emit MessageReceived(id, message); + return id; + } +} From 074bad74abbdd6486be355646057463485fc07f5 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Tue, 3 Dec 2024 13:02:49 -0300 Subject: [PATCH 27/35] Remove unused code --- test/utils/GmpProxy.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/utils/GmpProxy.sol b/test/utils/GmpProxy.sol index 9222183..fc13b9b 100644 --- a/test/utils/GmpProxy.sol +++ b/test/utils/GmpProxy.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Analog's Contracts (last updated v0.1.0) (src/GatewayProxy.sol) +// Analog's Contracts (last updated v0.1.0) (test/utils/GmpProxy.sol) pragma solidity >=0.8.0; @@ -7,8 +7,6 @@ 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"; -// import {GmpMessage as Message} from "../../src/Primitives.sol"; -// import {Context, CreateKind, IUniversalFactory} from "@universal-factory/IUniversalFactory.sol"; contract GatewayProxy is IGmpReceiver { From fca744fb8f98661c415b360d1a75c79aa8c768ee Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Tue, 10 Dec 2024 07:08:11 -0300 Subject: [PATCH 28/35] Fix unit tests --- src/Gateway.sol | 3 +- src/Primitives.sol | 2 - src/interfaces/IExecutor.sol | 9 +-- src/storage/Shards.sol | 28 ++++---- src/utils/GasUtils.sol | 120 +++++++++++++++++++++-------------- test/GasUtils.t.sol | 24 +++---- test/Gateway.t.sol | 7 +- test/utils/GmpProxy.sol | 19 +++--- 8 files changed, 113 insertions(+), 99 deletions(-) diff --git a/src/Gateway.sol b/src/Gateway.sol index f25b52b..d783859 100644 --- a/src/Gateway.sol +++ b/src/Gateway.sol @@ -95,7 +95,6 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { // 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 @@ -277,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(496); + 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. diff --git a/src/Primitives.sol b/src/Primitives.sol index 9ce9a80..13d00aa 100644 --- a/src/Primitives.sol +++ b/src/Primitives.sol @@ -71,8 +71,6 @@ struct UpdateKeysMessage { TssKey[] register; } - - /** * @dev A Route represents a communication channel between two networks. * @param networkId The id of the provided network. diff --git a/src/interfaces/IExecutor.sol b/src/interfaces/IExecutor.sol index ab0393f..c96c444 100644 --- a/src/interfaces/IExecutor.sol +++ b/src/interfaces/IExecutor.sol @@ -4,14 +4,7 @@ pragma solidity >=0.8.0; import { - Signature, - GmpMessage, - TssKey, - GmpStatus, - GmpStatus, - UpdateKeysMessage, - GmpSender, - Route + Signature, GmpMessage, TssKey, GmpStatus, GmpStatus, UpdateKeysMessage, GmpSender, Route } from "../Primitives.sol"; /** diff --git a/src/storage/Shards.sol b/src/storage/Shards.sol index 5841258..f7176de 100644 --- a/src/storage/Shards.sol +++ b/src/storage/Shards.sol @@ -247,7 +247,10 @@ library ShardStore { * Requirements: * - The `keys` should not be already registered. */ - function replaceTssKeys(MainStorage storage store, TssKey[] calldata keys) internal returns(TssKey[] memory created, TssKey[] memory revoked) { + 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); @@ -255,23 +258,22 @@ library ShardStore { // 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 0) { - TssKey calldata previousKey = keys[i-1]; - require(previousKey.xCoord < key.xCoord, "tss keys must be orderd by 'key.xCoord' in asceding order"); + 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 - }); + 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> 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); @@ -52,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; } @@ -180,99 +186,118 @@ library GasUtils { } } - 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 + function _debugExecutionGasCost(uint256 messageSize, uint256 gasUsed) internal pure returns (uint256) { unchecked { // Selector overhead - // uint256 baseCost = 420; + // -- First GAS opcode + uint256 baseCost = EXECUTION_SELECTOR_OVERHEAD - 9; uint256 memoryExpansion = 0x60; + // -- First GAS opcode // all opcodes until message.intoCallback(DOMAIN_SEPARATOR) - // baseCost += 449; + baseCost += 449; // -- message.intoCallback() -- - // baseCost += 438; + baseCost += 438; memoryExpansion = 0x80 + 0x01c4; - + uint256 gas = 0; // CALLDATACOPY 3 + (3 * words) + memory_expansion - // baseCost += 3; - uint256 gas = _toWord(messageSize) * 3; + baseCost += 3; + gas = _toWord(messageSize) * 3; memoryExpansion += messageSize; memoryExpansion = memoryExpansion.align32(); // opcodes until keccak256 - // baseCost += 31; + baseCost += 31; // keccak256 30 + 6 gas per word - // baseCost += 30; + baseCost += 30; gas = gas.saturatingAdd(_toWord(messageSize) * 6); - // baseCost += 424; - + // + baseCost += 424; // -- message.intoCallback() -- - // baseCost += 34; + baseCost += 34; // -- _verifySignature -- - // baseCost += 7933; + baseCost += 7933; // -- _verifySignature -- - // baseCost += 18; + baseCost += 18; // _execute - // baseCost += 22551; - // baseCost += 2; // GAS + baseCost += 22551; + baseCost += 2; // GAS - // baseCost += 97; + baseCost += 97; // ------ CALL ------ - // baseCost += 2600; + baseCost += 2600; gas = gas.saturatingAdd(gasUsed); memoryExpansion = (messageSize.align32() + 0x80 + 0x0120 + 164).align32(); // ------ CALL ------ - // baseCost += 67; - // baseCost += 100; // SLOAD - // baseCost += 69; - // baseCost += 100; // SSTORE + baseCost += 67; + baseCost += 100; // SLOAD + baseCost += 69; + baseCost += 100; // SSTORE // -- emit GmpExecuted -- - // baseCost += 141; + baseCost += 141; memoryExpansion += 0x20; // MSTORE - // baseCost += 24; + baseCost += 24; memoryExpansion += 0x20; // MSTORE - // baseCost += 39; - // baseCost += 2387; // LOG4 - // baseCost += 26; + baseCost += 39; + baseCost += 2387; // LOG4 + baseCost += 26; // -- emit GmpExecuted -- // end _execute - // baseCost += 34; + baseCost += 34; // GasUtils.txBaseCost() { - // baseCost += 64; // base cost + baseCost += 64; // base cost // chunk start cost - // baseCost += 66; + 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 + baseCost += 171; // End countNonZeros + baseCost += 70; // End txBaseCost } // end GasUtils.txBaseCost() - // baseCost += 560; + baseCost += 482; // ----- GAS ------- - // baseCost += 168; // GAS - // baseCost += 6800; // REFUND CALL - // baseCost += 184; // RETURN + baseCost += 168; // GAS + baseCost += 6800; // REFUND CALL + baseCost += 184; // RETURN + + gas = gas.saturatingAdd(baseCost); + gas = gas.saturatingAdd(memoryExpansionGasCost(_toWord(memoryExpansion))); + return gas; + } + } - // gas = gas.saturatingAdd(baseCost); + 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; @@ -304,7 +329,8 @@ library GasUtils { gasNeeded = inverseOfAllButOne64th(gasNeeded); 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/test/GasUtils.t.sol b/test/GasUtils.t.sol index 5b1fec6..02b642a 100644 --- a/test/GasUtils.t.sol +++ b/test/GasUtils.t.sol @@ -243,22 +243,22 @@ contract GasUtilsBase is Test { function test_gasUtils() external pure { uint256 baseCost = GasUtils.EXECUTION_BASE_COST; - assertEq(GasUtils.estimateGas(0, 0, 0), 31522 + baseCost); - assertEq(GasUtils.estimateGas(0, 33, 0), 31895 + baseCost); - assertEq(GasUtils.estimateGas(33, 0, 0), 32555 + baseCost); - assertEq(GasUtils.estimateGas(20, 13, 0), 32295 + 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), 31522 + baseCost); - assertEq(GasUtils.estimateWeiCost(one, 0, 0, 33, 0), 31895 + baseCost); - assertEq(GasUtils.estimateWeiCost(one, 0, 33, 0, 0), 32555 + baseCost); - assertEq(GasUtils.estimateWeiCost(one, 0, 20, 13, 0), 32295 + 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), (31522 + baseCost) * 2); - assertEq(GasUtils.estimateWeiCost(two, 0, 0, 33, 0), (31895 + baseCost) * 2); - assertEq(GasUtils.estimateWeiCost(two, 0, 33, 0, 0), (32555 + baseCost) * 2); - assertEq(GasUtils.estimateWeiCost(two, 0, 20, 13, 0), (32295 + 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 90c5eb5..fbf9d69 100644 --- a/test/Gateway.t.sol +++ b/test/Gateway.t.sol @@ -221,8 +221,8 @@ contract GatewayBase is Test { function _shortTssKeys(TssKey[] memory keys) private pure { // sort keys by xCoord - for (uint256 i=0; i keys[j].xCoord) { TssKey memory temp = keys[i]; keys[i] = keys[j]; @@ -242,7 +242,6 @@ contract GatewayBase is Test { keys[i] = TssKey({yParity: signer.yParity() == 28 ? 3 : 2, xCoord: signer.xCoord()}); } _shortTssKeys(keys); - // Only admin can set shards keys vm.expectRevert("unauthorized"); @@ -298,7 +297,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 + 133815); + assertEq(cost, GasUtils.EXECUTION_BASE_COST + 133821); } function test_checkPayloadSize() external { diff --git a/test/utils/GmpProxy.sol b/test/utils/GmpProxy.sol index fc13b9b..2afc51b 100644 --- a/test/utils/GmpProxy.sol +++ b/test/utils/GmpProxy.sol @@ -8,21 +8,20 @@ import {IGmpReceiver} from "../../src/interfaces/IGmpReceiver.sol"; import {IGateway} from "../../src/interfaces/IGateway.sol"; import {BranchlessMath} from "../../src/utils/BranchlessMath.sol"; - contract GatewayProxy is IGmpReceiver { using BranchlessMath for uint256; event MessageReceived(bytes32 indexed id, GmpMessage msg); - struct GmpMessage { - bytes32 foreign; - uint16 foreign_network; - address local; - uint128 gasLimit; - uint128 gasCost; - uint64 nonce; - bytes data; - } + struct GmpMessage { + bytes32 foreign; + uint16 foreign_network; + address local; + uint128 gasLimit; + uint128 gasCost; + uint64 nonce; + bytes data; + } IGateway public immutable GATEWAY; uint16 public immutable NETWORK_ID; From cd8069c3137316c2ebe8cf8fd7e633769cceca98 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Tue, 10 Dec 2024 10:20:16 -0300 Subject: [PATCH 29/35] Update remaining tests --- foundry.toml | 4 +- scripts/Deploy.sol | 3 +- src/GatewayProxy.sol | 29 +++++-- test/Example.t.sol | 21 +++-- test/GasUtils.t.sol | 23 ++--- test/Gateway.t.sol | 27 +----- test/GatewayProxy.t.sol | 128 ++++++++++++++++++++++++++++ test/GmpTestTools.sol | 15 +++- test/TestUtils.sol | 181 +++++++++++++++++++++++++++++++++++++++- 9 files changed, 368 insertions(+), 63 deletions(-) create mode 100644 test/GatewayProxy.t.sol diff --git a/foundry.toml b/foundry.toml index e4ed221..09817cd 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,9 +1,11 @@ [profile.default] src = "src" -test = "test2" # TODO: change back to test before merge +test = "test" # TODO: change back to test before merge out = "out" libs = ["lib"] match_contract = ".+Test" +# Permissions +fs_permissions = [{ access = "read", path = "./lib/universal-factory/abi" }] ######## # Lint # diff --git a/scripts/Deploy.sol b/scripts/Deploy.sol index ea26e16..94d6c3e 100644 --- a/scripts/Deploy.sol +++ b/scripts/Deploy.sol @@ -494,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/GatewayProxy.sol b/src/GatewayProxy.sol index f24d421..e6bf128 100644 --- a/src/GatewayProxy.sol +++ b/src/GatewayProxy.sol @@ -13,10 +13,11 @@ contract GatewayProxy { IUniversalFactory internal constant FACTORY = IUniversalFactory(0x0000000000001C4Bf962dF86e38F0c10c7972C6E); /** - * @dev Minimal ERC-1967 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 */ - bytes private constant PROXY_BYTECODE = - hex"363d3d373d3d3d363d7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d82803e903d91603857fd5bf3"; + bytes32 private constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; constructor(address admin) payable { // This contract must be deployed by the `UniversalFactory` @@ -44,14 +45,24 @@ contract GatewayProxy { // 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()) - // Copy Proxy bytecode to memory - bytes memory bytecode = PROXY_BYTECODE; + // Return if the call succeeded + if success { return(0, returndatasize()) } - // Return the `PROXY_BYTECODE`. - /// @solidity memory-safe-assembly - assembly { - return(add(bytecode, 32), mload(bytecode)) + // Revert if the call failed + revert(0, returndatasize()) } } } diff --git a/test/Example.t.sol b/test/Example.t.sol index e6ff099..10a6f8f 100644 --- a/test/Example.t.sol +++ b/test/Example.t.sol @@ -34,6 +34,7 @@ contract ExampleTest is Test { 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,23 @@ 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 +76,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,12 +88,13 @@ 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); diff --git a/test/GasUtils.t.sol b/test/GasUtils.t.sol index 02b642a..50bae4a 100644 --- a/test/GasUtils.t.sol +++ b/test/GasUtils.t.sol @@ -65,27 +65,16 @@ 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 +87,6 @@ contract GasUtilsBase is Test { hex"603c80600a5f395ff3fe5a600201803d523d60209160643560240135146018575bfd5b60365a116018575a604903565b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5bf3"; receiver = IGmpReceiver(TestUtils.deployContract(bytecode)); } - - vm.stopPrank(); } function sign(GmpMessage memory gmp) internal view returns (Signature memory) { diff --git a/test/Gateway.t.sol b/test/Gateway.t.sol index fbf9d69..5ccab79 100644 --- a/test/Gateway.t.sol +++ b/test/Gateway.t.sol @@ -163,33 +163,14 @@ contract GatewayBase is Test { uint16 private constant SRC_NETWORK_ID = 1234; uint16 internal constant DEST_NETWORK_ID = 1337; - address internal constant ADMIN = address(uint160(uint256(keccak256("proxy.admin")))); + address internal constant ADMIN = 0x6f4c950442e1Af093BcfF730381E63Ae9171b87a; constructor() { - SigningKey memory signer = TestUtils.createSigner(SECRET); - vm.deal(ADMIN, 100 ether); - vm.startPrank(ADMIN, ADMIN); - - // 1 - Deploy the implementation contract - address proxyAddr = vm.computeCreateAddress(ADMIN, vm.getNonce(ADMIN) + 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, (ADMIN, keys, networks)); - gateway = Gateway(address(new GatewayProxy(address(implementation), initializer))); - vm.deal(address(gateway), 100 ether); - + 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 { diff --git a/test/GatewayProxy.t.sol b/test/GatewayProxy.t.sol new file mode 100644 index 0000000..0340250 --- /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 175eef7..e81bb40 100644 --- a/test/TestUtils.sol +++ b/test/TestUtils.sol @@ -8,6 +8,21 @@ 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; @@ -24,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 */ @@ -383,6 +413,155 @@ library TestUtils { return (lower, upper); } } + + 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); + /* + /////////////////////////////////////////// + // 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(dstRoute, proxyAddr)); + address implementation = FACTORY.create2(salt, implementationCreationCode, abi.encode(dstRoute)); + + //////////////////////////////////////////////////////// + // 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: SigningUtils.yParity(signer) == 28 ? 1 : 0, xCoord: SigningUtils.xCoord(signer)}); // Shard key + Network[] memory networks = new Network[](2); + 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 + + // 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); + */ + } } type Fn is uint256; From f63bee72b8faed134fe810e681a22a1a6be2a004 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Tue, 10 Dec 2024 10:20:58 -0300 Subject: [PATCH 30/35] forge fmt --- test/Example.t.sol | 3 ++- test/GasUtils.t.sol | 3 ++- test/Gateway.t.sol | 3 ++- test/GatewayProxy.t.sol | 4 ++-- test/TestUtils.sol | 16 +++++++++++++--- 5 files changed, 21 insertions(+), 8 deletions(-) diff --git a/test/Example.t.sol b/test/Example.t.sol index 10a6f8f..fa2863f 100644 --- a/test/Example.t.sol +++ b/test/Example.t.sol @@ -61,7 +61,8 @@ contract ExampleTest is Test { // 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)); + 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); } diff --git a/test/GasUtils.t.sol b/test/GasUtils.t.sol index 50bae4a..bf70ca3 100644 --- a/test/GasUtils.t.sol +++ b/test/GasUtils.t.sol @@ -74,7 +74,8 @@ contract GasUtilsBase is Test { mock = new GasUtilsMock(); // Deploy the GatewayProxy - gateway = Gateway(address(TestUtils.setupGateway(deployer, bytes32(uint256(0)), SRC_NETWORK_ID, DEST_NETWORK_ID))); + 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)); diff --git a/test/Gateway.t.sol b/test/Gateway.t.sol index 5ccab79..5ce9f6a 100644 --- a/test/Gateway.t.sol +++ b/test/Gateway.t.sol @@ -168,7 +168,8 @@ contract GatewayBase is Test { 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))); + 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)); } diff --git a/test/GatewayProxy.t.sol b/test/GatewayProxy.t.sol index 0340250..ccac9ad 100644 --- a/test/GatewayProxy.t.sol +++ b/test/GatewayProxy.t.sol @@ -77,7 +77,6 @@ contract GatewayProxyTest is Test { } function _setup(VmSafe.Wallet memory admin, bytes32 salt, uint16 routeID) private returns (Gateway gateway) { - /////////////////////////////////////////// // 1. Deploy the implementation contract // /////////////////////////////////////////// @@ -86,7 +85,8 @@ contract GatewayProxyTest is Test { 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)); + 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); diff --git a/test/TestUtils.sol b/test/TestUtils.sol index e81bb40..d15835a 100644 --- a/test/TestUtils.sol +++ b/test/TestUtils.sol @@ -468,7 +468,13 @@ library TestUtils { /** * @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) { + 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"); /////////////////////////////////////////// @@ -478,7 +484,8 @@ library TestUtils { 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)); + bytes memory implementationCreationCode = + abi.encodePacked(type(Gateway).creationCode, abi.encode(routeId, proxyAddr)); address implementation = FACTORY.create2(salt, implementationCreationCode, abi.encode(routeId)); //////////////////////////////////////////////////////// @@ -507,7 +514,10 @@ library TestUtils { /** * @dev Deploy a new Gateway and GatewayProxy contracts. */ - function setupGateway(VmSafe.Wallet memory admin, bytes32 salt, uint16 srcRoute, uint16 dstRoute) internal returns (IGateway gateway) { + 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); From 4a23994e592686bf0e375cc259f984c508781e0b Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Tue, 10 Dec 2024 10:47:07 -0300 Subject: [PATCH 31/35] Remove source contract bitflag --- src/Gateway.sol | 2 +- src/utils/GasUtils.sol | 2 +- test/Example.t.sol | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Gateway.sol b/src/Gateway.sol index d783859..efba5c8 100644 --- a/src/Gateway.sol +++ b/src/Gateway.sol @@ -335,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; diff --git a/src/utils/GasUtils.sol b/src/utils/GasUtils.sol index 6e6a84f..817126e 100644 --- a/src/utils/GasUtils.sol +++ b/src/utils/GasUtils.sol @@ -25,7 +25,7 @@ library GasUtils { /** * @dev Base cost of the `IGateway.submitMessage` method. */ - uint256 internal constant SUBMIT_BASE_COST = 25818; + uint256 internal constant SUBMIT_BASE_COST = 25818 - 20; /** * @dev Compute the gas cost of memory expansion. diff --git a/test/Example.t.sol b/test/Example.t.sol index fa2863f..f58234c 100644 --- a/test/Example.t.sol +++ b/test/Example.t.sol @@ -102,7 +102,7 @@ contract ExampleTest is Test { 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, From 0944826c9721f17bb37f3e96e930f5a10f110f84 Mon Sep 17 00:00:00 2001 From: Haider Ali <70846121+Haider-Ali-DS@users.noreply.github.com> Date: Thu, 12 Dec 2024 19:47:06 +0500 Subject: [PATCH 32/35] Updates GmpProxy.sol (#29) --- test/utils/GmpProxy.sol | 55 +++++++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/test/utils/GmpProxy.sol b/test/utils/GmpProxy.sol index 2afc51b..7d97164 100644 --- a/test/utils/GmpProxy.sol +++ b/test/utils/GmpProxy.sol @@ -8,18 +8,19 @@ import {IGmpReceiver} from "../../src/interfaces/IGmpReceiver.sol"; import {IGateway} from "../../src/interfaces/IGateway.sol"; import {BranchlessMath} from "../../src/utils/BranchlessMath.sol"; -contract GatewayProxy is IGmpReceiver { +contract GmpProxy is IGmpReceiver { using BranchlessMath for uint256; event MessageReceived(bytes32 indexed id, GmpMessage msg); struct GmpMessage { - bytes32 foreign; - uint16 foreign_network; - address local; + uint16 srcNetwork; + uint16 destNetwork; + bytes32 src; + bytes32 dest; + uint64 nonce; uint128 gasLimit; uint128 gasCost; - uint64 nonce; bytes data; } @@ -32,28 +33,46 @@ contract GatewayProxy is IGmpReceiver { } function sendMessage(GmpMessage calldata message) external payable { - require(message.foreign == bytes32(uint256(uint160(address(this)))), "Invalid foreign address"); - require(message.foreign_network == NETWORK_ID, "Invalid foreign network"); - require(message.local == address(this), "Invalid local address"); uint256 value = address(this).balance.min(msg.value); - address destination = address(uint160(uint256(message.foreign))); - GATEWAY.submitMessage{value: value}(destination, message.foreign_network, message.gasLimit, message.data); + address destination = address(uint160(uint256(message.dest))); + GATEWAY.submitMessage{value: value}(destination, message.destNetwork, message.gasLimit, message.data); } - function onGmpReceived(bytes32 id, uint128 network, bytes32 source, bytes calldata payload) + + 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({ - foreign: source, - foreign_network: uint16(network), - local: address(this), - gasLimit: uint128(gasleft()), - gasCost: uint128(tx.gasprice), - nonce: 0, - data: payload + 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; } From 4c5991075d772cf90a14f5773a71d7c093790097 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Mon, 16 Dec 2024 13:36:59 -0300 Subject: [PATCH 33/35] Remove duplicated --- foundry.toml | 6 +++--- src/Primitives.sol | 17 ----------------- src/utils/GasUtils.sol | 10 ---------- 3 files changed, 3 insertions(+), 30 deletions(-) diff --git a/foundry.toml b/foundry.toml index 09817cd..17fbcab 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,6 +1,6 @@ [profile.default] src = "src" -test = "test" # TODO: change back to test before merge +test = "test" out = "out" libs = ["lib"] match_contract = ".+Test" @@ -10,7 +10,7 @@ fs_permissions = [{ access = "read", path = "./lib/universal-factory/abi" }] ######## # Lint # ######## -# deny_warnings = true +deny_warnings = true ################ # Solc options # @@ -61,7 +61,7 @@ simple_counter_for_loop_unchecked_increment = true runs = 2500 # When debuging fuzz tests, uncomment this seed to make tests reproducible. -seed = "0xdeadbeefdeadbeefdeadbeefdeadbeef" +# seed = "0xdeadbeefdeadbeefdeadbeefdeadbeef" # RPC endpoints [rpc_endpoints] diff --git a/src/Primitives.sol b/src/Primitives.sol index d853fa2..13d00aa 100644 --- a/src/Primitives.sol +++ b/src/Primitives.sol @@ -88,23 +88,6 @@ struct Route { uint128 relativeGasPriceDenominator; } -/** - * @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; - uint256 relativeGasPriceNumerator; - uint256 relativeGasPriceDenominator; -} - /** * @dev Message payload used to revoke or/and register new shards * @param revoke Shard's keys to revoke diff --git a/src/utils/GasUtils.sol b/src/utils/GasUtils.sol index 473db4e..817126e 100644 --- a/src/utils/GasUtils.sol +++ b/src/utils/GasUtils.sol @@ -37,16 +37,6 @@ library GasUtils { } } - /** - * @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 From 1465b4645132a1b830c2c92bc6e59a2732566660 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Mon, 16 Dec 2024 13:41:16 -0300 Subject: [PATCH 34/35] forge fmt --- test/TestUtils.sol | 175 ---------------------------------------- test/utils/GmpProxy.sol | 11 +-- 2 files changed, 3 insertions(+), 183 deletions(-) diff --git a/test/TestUtils.sol b/test/TestUtils.sol index d15835a..1e7e1cf 100644 --- a/test/TestUtils.sol +++ b/test/TestUtils.sol @@ -353,67 +353,6 @@ library TestUtils { setCallerMode(callerMode, msgSender, txOrigin); } - /** - * @dev Call a function, then clear and restore the free memory pointer after the call - */ - function restoreMemory(uint256 data, function (uint256) internal returns (bool) callback) internal { - uint256 freeMemPtr; - assembly { - freeMemPtr := mload(0x40) - } - callback(data); - assembly { - // Clear the memory - let len := sub(mload(0x40), freeMemPtr) - calldatacopy(freeMemPtr, calldatasize(), len) - - // Restore the memory pointer - mstore(0x40, freeMemPtr) - } - } - - function binarySearch(uint256 lower, uint256 upper, function (uint256) internal returns (bool) cb) - private - returns (uint256, uint256) - { - unchecked { - uint256 snapshotId = vm.snapshotState(); - require(lower < upper, "'lower' must be less than 'upper'"); - uint256 freeMemory; - assembly { - freeMemory := mload(0x40) - } - bool target = cb(lower); - console.log("setup lower"); - vm.revertToState(snapshotId); - console.log("setup upper"); - require(cb(upper) != target, "cb(lower) == cb(upper)"); - console.log("setup done"); - assembly { - // Clear the memory - let len := sub(mload(0x40), freeMemory) - calldatacopy(freeMemory, calldatasize(), len) - } - uint256 prev = 0; - uint256 mid = type(uint256).max; - while (prev != mid) { - prev = mid; - mid = (upper + lower) >> 1; - vm.revertToState(snapshotId); - if (cb(mid) == target) { - console.log("lower: ", mid, lower, upper); - lower = mid; - } else { - console.log("upper: ", mid, lower, upper); - upper = mid; - } - } - vm.revertToState(snapshotId); - console.log("will return", lower, upper); - return (lower, upper); - } - } - function deployFactory() internal returns (IUniversalFactory) { // Check if the factory is already deployed if (address(FACTORY).code.length > 0) { @@ -529,120 +468,6 @@ library TestUtils { networks[1].id = dstRoute; // shibuya network id networks[1].gateway = proxyAddr; // shibuya proxy address return setupGateway(admin, salt, dstRoute, keys, networks); - /* - /////////////////////////////////////////// - // 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(dstRoute, proxyAddr)); - address implementation = FACTORY.create2(salt, implementationCreationCode, abi.encode(dstRoute)); - - //////////////////////////////////////////////////////// - // 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: SigningUtils.yParity(signer) == 28 ? 1 : 0, xCoord: SigningUtils.xCoord(signer)}); // Shard key - Network[] memory networks = new Network[](2); - 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 - - // 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); - */ - } -} - -type Fn is uint256; - -type FnMut is uint256; - -library FnUtils { - function intoFn(function(uint256) internal pure returns (uint256) callback) internal pure returns (Fn fn) { - assembly { - fn := callback - } - } - - function intoFn(function(uint256) internal returns (uint256) callback) internal pure returns (FnMut fn) { - assembly { - fn := callback - } - } - - function callU256(FnMut fn, uint256 param) internal returns (uint256) { - function(uint256) internal returns (uint256) callback; - assembly { - callback := fn - } - return callback(param); - } - - function callU256(FnMut fn, bytes32 param) internal returns (uint256) { - function(bytes32) internal returns (uint256) callback; - assembly { - callback := fn - } - return callback(param); - } - - function callU256(FnMut fn, bytes memory param) internal returns (uint256) { - function(bytes memory) internal returns (uint256) callback; - assembly { - callback := fn - } - return callback(param); - } - - function callBool(FnMut fn, uint256 param) internal returns (bool) { - function(uint256) internal returns (bool) callback; - assembly { - callback := fn - } - return callback(param); - } - - function callBool(FnMut fn, bytes32 param) internal returns (bool) { - function(bytes32) internal returns (bool) callback; - assembly { - callback := fn - } - return callback(param); - } - - function callBool(FnMut fn, bytes memory param) internal returns (bool) { - function(bytes memory) internal returns (bool) callback; - assembly { - callback := fn - } - return callback(param); - } - - function asUint256Bytes(Fn fn) internal pure returns (function(uint256) internal pure returns (uint256) callback) { - assembly { - fn := callback - } } } diff --git a/test/utils/GmpProxy.sol b/test/utils/GmpProxy.sol index 7d97164..0f52d14 100644 --- a/test/utils/GmpProxy.sol +++ b/test/utils/GmpProxy.sol @@ -38,17 +38,12 @@ contract GmpProxy is IGmpReceiver { GATEWAY.submitMessage{value: value}(destination, message.destNetwork, message.gasLimit, message.data); } - - function estimateMessageCost(uint256 messageSize, uint256 gasLimit) external view returns (uint256){ + 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 + 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, From 5156fbf7511a6757c9e71e3dba789dbe72c79774 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Mon, 16 Dec 2024 14:12:58 -0300 Subject: [PATCH 35/35] Improve foundry.toml docs --- foundry.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/foundry.toml b/foundry.toml index 17fbcab..d422c72 100644 --- a/foundry.toml +++ b/foundry.toml @@ -16,6 +16,10 @@ deny_warnings = true # 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