From ba3060214226e42892a541db77ef7d4cc9ab54e4 Mon Sep 17 00:00:00 2001 From: Sean Casey Date: Thu, 21 Mar 2024 19:20:28 -0400 Subject: [PATCH] refactor: reduce gsn paymaster validation and allow additional callers --- .../gas-relayer/GasRelayPaymasterLib.sol | 133 +++++----- .../gas-relayer/IGasRelayPaymaster.sol | 6 + tests/interfaces/external/IGSNForwarder.sol | 16 ++ tests/interfaces/external/IGSNPaymaster.sol | 6 + tests/interfaces/external/IGSNRelayHub.sol | 14 + tests/interfaces/external/IGSNTypes.sol | 22 ++ tests/tests/infrastracture/GasRelayer.t.sol | 241 ++++++++++++++++++ tests/utils/CommonUtils.sol | 11 +- tests/utils/common/SignatureUtils.sol | 12 + tests/utils/infrastructure/GSNUtils.sol | 160 ++++++++++++ 10 files changed, 558 insertions(+), 63 deletions(-) create mode 100644 tests/interfaces/external/IGSNForwarder.sol create mode 100644 tests/interfaces/external/IGSNPaymaster.sol create mode 100644 tests/interfaces/external/IGSNRelayHub.sol create mode 100644 tests/interfaces/external/IGSNTypes.sol create mode 100644 tests/tests/infrastracture/GasRelayer.t.sol create mode 100644 tests/utils/common/SignatureUtils.sol create mode 100644 tests/utils/infrastructure/GSNUtils.sol diff --git a/contracts/release/infrastructure/gas-relayer/GasRelayPaymasterLib.sol b/contracts/release/infrastructure/gas-relayer/GasRelayPaymasterLib.sol index ed980cf9b..e7be0f218 100644 --- a/contracts/release/infrastructure/gas-relayer/GasRelayPaymasterLib.sol +++ b/contracts/release/infrastructure/gas-relayer/GasRelayPaymasterLib.sol @@ -29,9 +29,17 @@ import {IGasRelayPaymasterDepositor} from "./IGasRelayPaymasterDepositor.sol"; /// @title GasRelayPaymasterLib Contract /// @author Enzyme Council /// @notice The core logic library for the "paymaster" contract which refunds GSN relayers +/// @dev Allows any permissioned user of the fund to relay any call, +/// without validation of the target of the call itself. +/// Funds with untrusted permissioned users should monitor for abuse (i.e., relaying personal calls). +/// The extent of abuse is throttled by `DEPOSIT_COOLDOWN` and `DEPOSIT_MAX_TOTAL`. contract GasRelayPaymasterLib is IGasRelayPaymaster, GasRelayPaymasterLibBase2 { using SafeMath for uint256; + event AdditionalRelayUserAdded(address indexed account); + + event AdditionalRelayUserRemoved(address indexed account); + // Immutable and constants // Sane defaults, subject to change after gas profiling uint256 private constant CALLDATA_SIZE_LIMIT = 10500; @@ -50,11 +58,18 @@ contract GasRelayPaymasterLib is IGasRelayPaymaster, GasRelayPaymasterLibBase2 { address private immutable TRUSTED_FORWARDER; address private immutable WETH_TOKEN; + mapping(address => bool) private accountToIsAdditionalRelayUser; + modifier onlyComptroller() { require(msg.sender == getParentComptroller(), "Can only be called by the parent comptroller"); _; } + modifier onlyFundOwner() { + require(__msgSender() == IVault(getParentVault()).getOwner(), "Only the fund owner can call this function"); + _; + } + modifier relayHubOnly() { require(msg.sender == getHubAddr(), "Can only be called by RelayHub"); _; @@ -110,15 +125,19 @@ contract GasRelayPaymasterLib is IGasRelayPaymaster, GasRelayPaymasterLibBase2 { require(_relayRequest.relayData.baseRelayFee <= RELAY_FEE_MAX_BASE, "preRelayedCall: High baseRelayFee"); require(_relayRequest.relayData.pctRelayFee <= RELAY_FEE_MAX_PERCENT, "preRelayedCall: High pctRelayFee"); - address vaultProxy = getParentVault(); - require(IVault(vaultProxy).canRelayCalls(_relayRequest.request.from), "preRelayedCall: Unauthorized caller"); + // No Enzyme txs require msg.value + require(_relayRequest.request.value == 0, "preRelayedCall: Non-zero value"); - bytes4 selector = __parseTxDataFunctionSelector(_relayRequest.request.data); + // Allow any transaction, as long as it's from a permissioned account for the fund + address vaultProxy = getParentVault(); require( - __isAllowedCall(vaultProxy, _relayRequest.request.to, selector, _relayRequest.request.data), - "preRelayedCall: Function call not permitted" + IVault(vaultProxy).canRelayCalls(_relayRequest.request.from) + || isAdditionalRelayUser(_relayRequest.request.from), + "preRelayedCall: Unauthorized caller" ); + bytes4 selector = __parseTxDataFunctionSelector(_relayRequest.request.data); + return (abi.encode(_relayRequest.request.from, selector), false); } @@ -143,8 +162,9 @@ contract GasRelayPaymasterLib is IGasRelayPaymaster, GasRelayPaymasterLibBase2 { /// @notice Send any deposited ETH back to the vault function withdrawBalance() external override { address vaultProxy = getParentVault(); + address canonicalSender = __msgSender(); require( - msg.sender == IVault(vaultProxy).getOwner() || msg.sender == __getComptrollerForVault(vaultProxy), + canonicalSender == IVault(vaultProxy).getOwner() || canonicalSender == __getComptrollerForVault(vaultProxy), "withdrawBalance: Only owner or comptroller is authorized" ); @@ -197,65 +217,18 @@ contract GasRelayPaymasterLib is IGasRelayPaymaster, GasRelayPaymasterLibBase2 { return IVault(_vaultProxy).getAccessor(); } - /// @dev Helper to check if a contract call is allowed to be relayed using this paymaster - /// Allowed contracts are: - /// - VaultProxy - /// - ComptrollerProxy - /// - PolicyManager - /// - FundDeployer - function __isAllowedCall(address _vaultProxy, address _contract, bytes4 _selector, bytes calldata _txData) - private - view - returns (bool allowed_) - { - if (_contract == _vaultProxy) { - // All calls to the VaultProxy are allowed - return true; - } - - address parentComptroller = __getComptrollerForVault(_vaultProxy); - if (_contract == parentComptroller) { - if ( - _selector == IComptroller.callOnExtension.selector - || _selector == IComptroller.vaultCallOnContract.selector - || _selector == IComptroller.buyBackProtocolFeeShares.selector - || _selector == IComptroller.depositToGasRelayPaymaster.selector - || _selector == IComptroller.setAutoProtocolFeeSharesBuyback.selector - ) { - return true; - } - } else if (_contract == IComptroller(parentComptroller).getPolicyManager()) { - if ( - _selector == IPolicyManager.updatePolicySettingsForFund.selector - || _selector == IPolicyManager.enablePolicyForFund.selector - || _selector == IPolicyManager.disablePolicyForFund.selector - ) { - return __parseTxDataFirstParameterAsAddress(_txData) == getParentComptroller(); + /// @dev Helper to parse the canonical msg sender from trusted forwarder relayed calls + /// See https://github.com/opengsn/gsn/blob/da4222b76e3ae1968608dc5c5d80074dcac7c4be/packages/contracts/src/ERC2771Recipient.sol#L41-L53 + function __msgSender() internal view returns (address canonicalSender_) { + if (msg.data.length >= 20 && msg.sender == TRUSTED_FORWARDER) { + assembly { + canonicalSender_ := shr(96, calldataload(sub(calldatasize(), 20))) } - } else if (_contract == IComptroller(parentComptroller).getFundDeployer()) { - if ( - _selector == IFundDeployer.createReconfigurationRequest.selector - || _selector == IFundDeployer.executeReconfiguration.selector - || _selector == IFundDeployer.cancelReconfiguration.selector - ) { - return __parseTxDataFirstParameterAsAddress(_txData) == getParentVault(); - } - } - return false; - } - - /// @notice Parses the first parameter of tx data as an address - /// @param _txData The tx data to retrieve the address from - /// @return retrievedAddress_ The extracted address - function __parseTxDataFirstParameterAsAddress(bytes calldata _txData) - private - pure - returns (address retrievedAddress_) - { - require(_txData.length >= 36, "__parseTxDataFirstParameterAsAddress: _txData is not a valid length"); + return canonicalSender_; + } - return abi.decode(_txData[4:36], (address)); + return msg.sender; } /// @notice Parses the function selector from tx data @@ -271,6 +244,36 @@ contract GasRelayPaymasterLib is IGasRelayPaymaster, GasRelayPaymasterLibBase2 { return functionSelector_; } + ////////////////////////////////////// + // REGISTRY: ADDITIONAL RELAY USERS // + ////////////////////////////////////// + + /// @notice Adds additional relay users + /// @param _usersToAdd The users to add + function addAdditionalRelayUsers(address[] calldata _usersToAdd) external override onlyFundOwner { + for (uint256 i; i < _usersToAdd.length; i++) { + address user = _usersToAdd[i]; + require(!isAdditionalRelayUser(user), "addAdditionalRelayUsers: User registered"); + + accountToIsAdditionalRelayUser[user] = true; + + emit AdditionalRelayUserAdded(user); + } + } + + /// @notice Removes additional relay users + /// @param _usersToRemove The users to remove + function removeAdditionalRelayUsers(address[] calldata _usersToRemove) external override onlyFundOwner { + for (uint256 i; i < _usersToRemove.length; i++) { + address user = _usersToRemove[i]; + require(isAdditionalRelayUser(user), "removeAdditionalRelayUsers: User not registered"); + + accountToIsAdditionalRelayUser[user] = false; + + emit AdditionalRelayUserRemoved(user); + } + } + /////////////////// // STATE GETTERS // /////////////////// @@ -313,6 +316,12 @@ contract GasRelayPaymasterLib is IGasRelayPaymaster, GasRelayPaymasterLibBase2 { return WETH_TOKEN; } + /// @notice Checks whether an account is an approved additional relayer user + /// @return isAdditionalRelayUser_ True if the account is an additional relayer user + function isAdditionalRelayUser(address _who) public view override returns (bool isAdditionalRelayUser_) { + return accountToIsAdditionalRelayUser[_who]; + } + /// @notice Gets the `TRUSTED_FORWARDER` variable value /// @return trustedForwarder_ The forwarder contract which is trusted to validated the relayed tx signature function trustedForwarder() external view override returns (address trustedForwarder_) { diff --git a/contracts/release/infrastructure/gas-relayer/IGasRelayPaymaster.sol b/contracts/release/infrastructure/gas-relayer/IGasRelayPaymaster.sol index aee895ea1..fbb9f0d92 100644 --- a/contracts/release/infrastructure/gas-relayer/IGasRelayPaymaster.sol +++ b/contracts/release/infrastructure/gas-relayer/IGasRelayPaymaster.sol @@ -17,6 +17,8 @@ import {IGsnPaymaster} from "../../../external-interfaces/IGsnPaymaster.sol"; /// @title IGasRelayPaymaster Interface /// @author Enzyme Council interface IGasRelayPaymaster is IGsnPaymaster { + function addAdditionalRelayUsers(address[] calldata _usersToAdd) external; + function deposit() external; function getLastDepositTimestamp() external view returns (uint256 lastDepositTimestamp_); @@ -29,5 +31,9 @@ interface IGasRelayPaymaster is IGsnPaymaster { function init(address _vault) external; + function isAdditionalRelayUser(address _who) external view returns (bool isAdditionalRelayUser_); + + function removeAdditionalRelayUsers(address[] calldata _usersToRemove) external; + function withdrawBalance() external; } diff --git a/tests/interfaces/external/IGSNForwarder.sol b/tests/interfaces/external/IGSNForwarder.sol new file mode 100644 index 000000000..8497f9e28 --- /dev/null +++ b/tests/interfaces/external/IGSNForwarder.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.6.0 <0.9.0; + +interface IGSNForwarder { + struct ForwardRequest { + address from; + address to; + uint256 value; + uint256 gas; + uint256 nonce; + bytes data; + uint256 validUntil; + } + + function getNonce(address _from) external view returns (uint256 nonce_); +} diff --git a/tests/interfaces/external/IGSNPaymaster.sol b/tests/interfaces/external/IGSNPaymaster.sol new file mode 100644 index 000000000..2431051a7 --- /dev/null +++ b/tests/interfaces/external/IGSNPaymaster.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.6.0 <0.9.0; + +interface IGSNPaymaster { + function trustedForwarder() external view returns (address trustedForwarder_); +} diff --git a/tests/interfaces/external/IGSNRelayHub.sol b/tests/interfaces/external/IGSNRelayHub.sol new file mode 100644 index 000000000..f73bd7d01 --- /dev/null +++ b/tests/interfaces/external/IGSNRelayHub.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.6.0 <0.9.0; + +import {IGSNTypes} from "./IGSNTypes.sol"; + +interface IGSNRelayHub { + function relayCall( + uint256 _maxAcceptanceBudget, + IGSNTypes.RelayRequest calldata _relayRequest, + bytes calldata _signature, + bytes calldata _approvalData, + uint256 _externalGasLimit + ) external returns (bool paymasterAccepted_, bytes memory returnValue_); +} diff --git a/tests/interfaces/external/IGSNTypes.sol b/tests/interfaces/external/IGSNTypes.sol new file mode 100644 index 000000000..722ea3a03 --- /dev/null +++ b/tests/interfaces/external/IGSNTypes.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.6.0 <0.9.0; + +import {IGSNForwarder} from "./IGSNForwarder.sol"; + +interface IGSNTypes { + struct RelayData { + uint256 gasPrice; + uint256 pctRelayFee; + uint256 baseRelayFee; + address relayWorker; + address paymaster; + address forwarder; + bytes paymasterData; + uint256 clientId; + } + + struct RelayRequest { + IGSNForwarder.ForwardRequest request; + RelayData relayData; + } +} diff --git a/tests/tests/infrastracture/GasRelayer.t.sol b/tests/tests/infrastracture/GasRelayer.t.sol new file mode 100644 index 000000000..6015fcab3 --- /dev/null +++ b/tests/tests/infrastracture/GasRelayer.t.sol @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.19; + +import {IntegrationTest} from "tests/bases/IntegrationTest.sol"; +import {IGSNTypes} from "tests/interfaces/external/IGSNTypes.sol"; +import {IComptrollerLib} from "tests/interfaces/internal/IComptrollerLib.sol"; +import {IGasRelayPaymasterLib} from "tests/interfaces/internal/IGasRelayPaymasterLib.sol"; +import {IVaultLib} from "tests/interfaces/internal/IVaultLib.sol"; +import {GSNUtils} from "tests/utils/infrastructure/GSNUtils.sol"; + +abstract contract GasRelayerTestBase is IntegrationTest, GSNUtils { + event AdditionalRelayUserAdded(address indexed account); + event AdditionalRelayUserRemoved(address indexed account); + + address fundOwner; + uint256 fundOwnerPrivateKey; + IComptrollerLib comptrollerProxy; + IVaultLib vaultProxy; + IGasRelayPaymasterLib paymaster; + address hubAddress; + + function __initialize() internal { + (fundOwner, fundOwnerPrivateKey) = makeAddrAndKey("FundOwner"); + + // Deploy fund + (comptrollerProxy, vaultProxy) = createVault({ + _fundDeployer: core.release.fundDeployer, + _vaultOwner: fundOwner, + _denominationAsset: address(wethToken) + }); + + // Seed with plenty of wrapped native asset to use gas relaying + increaseTokenBalance({ + _token: wrappedNativeToken, + _to: address(vaultProxy), + _amount: assetUnit(wrappedNativeToken) * 100 + }); + + // Deploy paymaster for fund + vm.prank(fundOwner); + comptrollerProxy.deployGasRelayPaymaster(); + paymaster = IGasRelayPaymasterLib(comptrollerProxy.getGasRelayPaymaster()); + + // Get hub from paymaster + hubAddress = paymaster.getHubAddr(); + + // Define simple call to be made + } + + // HELPERS + + // low-level paymaster.preRelayedCall (avoids need to convert struct type) + function __preRelayedCall(IGSNTypes.RelayRequest memory _relayRequest) internal { + vm.prank(hubAddress); + (bool success,) = + address(paymaster).call(abi.encodeWithSelector(paymaster.preRelayedCall.selector, _relayRequest, "", "", 0)); + require(success, "preRelayedCall failed"); + } + + function __simpleValidRelayRequest(bool _topUp) internal returns (IGSNTypes.RelayRequest memory relayRequest_) { + // Call to relay: VaultProxy.addAssetManagers + address to = address(vaultProxy); + bytes memory txData = + abi.encodeWithSelector(IVaultLib.addAssetManagers.selector, toArray(makeAddr("NewAssetManager"))); + + // Do not set _from, to force the test to set it + return gsnConstructRelayRequest({ + _from: makeAddr("DummyFrom"), + _to: to, + _txData: txData, + _paymasterAddress: address(paymaster), + _topUp: _topUp, + _relayWorker: makeAddr("RelayWorker") + }); + } + + // TESTS: PRE-RELAYED CALL + + // TODO: other preRelayedCall validations + + function test_preRelayedCall_failsWithNonZeroValue() public { + IGSNTypes.RelayRequest memory relayRequest = __simpleValidRelayRequest({_topUp: false}); + + relayRequest.request.from = fundOwner; + relayRequest.request.value = 1; + + vm.expectRevert("preRelayedCall: Non-zero value"); + __preRelayedCall(relayRequest); + } + + function test_preRelayedCall_failsWithUnauthorizedCaller() public { + IGSNTypes.RelayRequest memory relayRequest = __simpleValidRelayRequest({_topUp: false}); + address randomUser = makeAddr("RandomUser"); + + relayRequest.request.from = randomUser; + + vm.expectRevert("preRelayedCall: Unauthorized caller"); + __preRelayedCall(relayRequest); + } + + function test_preRelayedCall_successWithVaultPermissionedRole() public { + IGSNTypes.RelayRequest memory relayRequest = __simpleValidRelayRequest({_topUp: false}); + + relayRequest.request.from = fundOwner; + + __preRelayedCall(relayRequest); + } + + function test_preRelayedCall_successWithAdditionalRelayUser() public { + address relayUser = makeAddr("RelayUser"); + IGSNTypes.RelayRequest memory relayRequest = __simpleValidRelayRequest({_topUp: false}); + + relayRequest.request.from = relayUser; + + // Add relay user + vm.prank(fundOwner); + paymaster.addAdditionalRelayUsers(toArray(relayUser)); + + // Call should not revert + __preRelayedCall(relayRequest); + } + + // TESTS: ADDITIONAL RELAY USERS + + function test_addAdditionalRelayUsers_failsWithUnauthorized() public { + address randomCaller = makeAddr("RandomCaller"); + address relayUser = makeAddr("RelayUser"); + + vm.expectRevert("Only the fund owner can call this function"); + vm.prank(randomCaller); + paymaster.addAdditionalRelayUsers(toArray(relayUser)); + } + + function test_addAdditionalRelayUsers_failsWithAlreadyRegistered() public { + address relayUser = makeAddr("RelayUser"); + + // Add relay user + vm.prank(fundOwner); + paymaster.addAdditionalRelayUsers(toArray(relayUser)); + + vm.expectRevert("addAdditionalRelayUsers: User registered"); + vm.prank(fundOwner); + paymaster.addAdditionalRelayUsers(toArray(relayUser)); + } + + function test_addAdditionalRelayUsers_success() public { + address[] memory relayUsers = toArray(makeAddr("RelayUser"), makeAddr("RelayUser2")); + + for (uint256 i; i < relayUsers.length; i++) { + assertFalse(paymaster.isAdditionalRelayUser(relayUsers[i])); + } + + // Pre-assert events + for (uint256 i; i < relayUsers.length; i++) { + expectEmit(address(paymaster)); + emit AdditionalRelayUserAdded(relayUsers[i]); + } + + // Add relay users + vm.prank(fundOwner); + paymaster.addAdditionalRelayUsers(relayUsers); + + // Assert that the users were added + for (uint256 i; i < relayUsers.length; i++) { + assertTrue(paymaster.isAdditionalRelayUser(relayUsers[i])); + } + } + + function test_removeAdditionalRelayUsers_failsWithUnauthorized() public { + address randomCaller = makeAddr("RandomCaller"); + address relayUser = makeAddr("RelayUser"); + + // Add relay user + vm.prank(fundOwner); + paymaster.addAdditionalRelayUsers(toArray(relayUser)); + + vm.expectRevert("Only the fund owner can call this function"); + vm.prank(randomCaller); + paymaster.removeAdditionalRelayUsers(toArray(relayUser)); + } + + function test_removeAdditionalRelayUsers_failsWithNotRegistered() public { + address relayUser = makeAddr("RelayUser"); + + vm.expectRevert("removeAdditionalRelayUsers: User not registered"); + vm.prank(fundOwner); + paymaster.removeAdditionalRelayUsers(toArray(relayUser)); + } + + function test_removeAdditionalRelayUsers_success() public { + address[] memory relayUsers = toArray(makeAddr("RelayUser"), makeAddr("RelayUser2")); + + // Add relay users + vm.prank(fundOwner); + paymaster.addAdditionalRelayUsers(relayUsers); + + // Pre-assert events + for (uint256 i; i < relayUsers.length; i++) { + expectEmit(address(paymaster)); + emit AdditionalRelayUserRemoved(relayUsers[i]); + } + + // Remove relay users + vm.prank(fundOwner); + paymaster.removeAdditionalRelayUsers(relayUsers); + + // Assert that the users were removed + for (uint256 i; i < relayUsers.length; i++) { + assertFalse(paymaster.isAdditionalRelayUser(relayUsers[i])); + } + } + + function test_e2e_successWithoutTopUp() public { + // Simple call to test: add an asset manager to the vault + address newAssetManager = makeAddr("NewAssetManager"); + bytes memory txData = abi.encodeWithSelector(IVaultLib.addAssetManagers.selector, toArray(newAssetManager)); + + // Not an asset manager prior to the relayed call + assertFalse(vaultProxy.isAssetManager(newAssetManager)); + + gsnRelayCall({ + _from: fundOwner, + _to: address(vaultProxy), + _txData: txData, + _paymasterAddress: address(paymaster), + _topUp: false, + _privateKey: fundOwnerPrivateKey + }); + + // Should now be an asset manager + assertTrue(vaultProxy.isAssetManager(newAssetManager)); + } +} + +contract EthereumGasRelayerTest is GasRelayerTestBase { + function setUp() public override { + setUpMainnetEnvironment(); + + __initialize(); + } +} diff --git a/tests/utils/CommonUtils.sol b/tests/utils/CommonUtils.sol index 6d3883d02..22dd59690 100644 --- a/tests/utils/CommonUtils.sol +++ b/tests/utils/CommonUtils.sol @@ -4,8 +4,17 @@ pragma solidity 0.8.19; import {AssetBalanceUtils} from "tests/utils/common/AssetBalanceUtils.sol"; import {ErrorUtils} from "tests/utils/common/ErrorUtils.sol"; import {EventUtils} from "tests/utils/common/EventUtils.sol"; +import {SignatureUtils} from "tests/utils/common/SignatureUtils.sol"; import {StorageUtils} from "tests/utils/common/StorageUtils.sol"; import {TokenUtils} from "tests/utils/common/TokenUtils.sol"; import {TypeUtils} from "tests/utils/common/TypeUtils.sol"; -abstract contract CommonUtils is AssetBalanceUtils, ErrorUtils, EventUtils, StorageUtils, TokenUtils, TypeUtils {} +abstract contract CommonUtils is + AssetBalanceUtils, + ErrorUtils, + EventUtils, + SignatureUtils, + StorageUtils, + TokenUtils, + TypeUtils +{} diff --git a/tests/utils/common/SignatureUtils.sol b/tests/utils/common/SignatureUtils.sol new file mode 100644 index 000000000..5e19c4ed0 --- /dev/null +++ b/tests/utils/common/SignatureUtils.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.19; + +import {CommonUtilsBase} from "tests/utils/bases/CommonUtilsBase.sol"; + +abstract contract SignatureUtils is CommonUtilsBase { + function createSignature(uint256 _privateKey, bytes32 _digest) internal pure returns (bytes memory signature_) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign({privateKey: _privateKey, digest: _digest}); + + return abi.encodePacked(r, s, v); + } +} diff --git a/tests/utils/infrastructure/GSNUtils.sol b/tests/utils/infrastructure/GSNUtils.sol new file mode 100644 index 000000000..ce343ca86 --- /dev/null +++ b/tests/utils/infrastructure/GSNUtils.sol @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.19; + +import {AddOnUtilsBase} from "tests/utils/bases/AddOnUtilsBase.sol"; + +import {IGSNForwarder} from "tests/interfaces/external/IGSNForwarder.sol"; +import {IGSNPaymaster} from "tests/interfaces/external/IGSNPaymaster.sol"; +import {IGSNRelayHub} from "tests/interfaces/external/IGSNRelayHub.sol"; +import {IGSNTypes} from "tests/interfaces/external/IGSNTypes.sol"; + +bytes32 constant ETHEREUM_DOMAIN_SEPARATOR = 0x2faf2522ab3d28e3d6391818d17f33f3cd87ed88d7f51fe14cf24a08ce656414; +address constant ETHEREUM_RELAY_HUB = 0x9e59Ea5333cD4f402dAc320a04fafA023fe3810D; +address constant ETHEREUM_RELAY_WORKER = 0x1FD0C666094d8c5daE247aA6C3C4c33Fd21bdC91; +address constant ETHEREUM_TRUSTED_FORWARDER = 0xca57e5D6218AeB093D76372B51Ba355CfB3C6Cd0; + +// Copied from GsnEip712Library +string constant GENERIC_PARAMS = + "address from,address to,uint256 value,uint256 gas,uint256 nonce,bytes data,uint256 validUntil"; +bytes constant RELAY_DATA_TYPE = + "RelayData(uint256 gasPrice,uint256 pctRelayFee,uint256 baseRelayFee,address relayWorker,address paymaster,address forwarder,bytes paymasterData,uint256 clientId)"; +string constant RELAY_REQUEST_NAME = "RelayRequest"; +string constant RELAY_REQUEST_SUFFIX = string(abi.encodePacked("RelayData relayData)", RELAY_DATA_TYPE)); +bytes constant RELAY_REQUEST_TYPE = abi.encodePacked(RELAY_REQUEST_NAME, "(", GENERIC_PARAMS, ",", RELAY_REQUEST_SUFFIX); +bytes32 constant RELAY_DATA_TYPE_HASH = keccak256(RELAY_DATA_TYPE); +bytes32 constant RELAY_REQUEST_TYPE_HASH = keccak256(RELAY_REQUEST_TYPE); + +abstract contract GSNUtils is AddOnUtilsBase { + function gsnConstructRelayRequest( + address _from, + address _to, + bytes memory _txData, + address _paymasterAddress, + bool _topUp, + address _relayWorker + ) internal view returns (IGSNTypes.RelayRequest memory relayRequest_) { + address trustedForwarderAddress = IGSNPaymaster(_paymasterAddress).trustedForwarder(); + uint256 nonce = IGSNForwarder(trustedForwarderAddress).getNonce(_from); + + return IGSNTypes.RelayRequest({ + request: IGSNForwarder.ForwardRequest({ + from: _from, + to: _to, + value: 0, // Always 0 in enzyme + gas: 10_000_000, // High, safe amount of gas + nonce: nonce, + data: _txData, + validUntil: block.timestamp + }), + relayData: IGSNTypes.RelayData({ + gasPrice: 10e9, // 10 gwei + pctRelayFee: 10, // value on eth mainnet + baseRelayFee: 0, // value on eth mainnet + relayWorker: _relayWorker, + paymaster: _paymasterAddress, + forwarder: trustedForwarderAddress, + paymasterData: gsnEncodePaymasterData({_shouldTopUpDeposit: _topUp}), + clientId: 1 // dummy value + }) + }); + } + + function gsnEncodePaymasterData(bool _shouldTopUpDeposit) internal pure returns (bytes memory paymasterData_) { + return abi.encode(_shouldTopUpDeposit); + } + + function gsnRelayCall( + address _from, + address _to, + bytes memory _txData, + address _paymasterAddress, + bool _topUp, + uint256 _privateKey + ) internal { + IGSNRelayHub relayHub; + address relayWorker; + if (block.chainid == ETHEREUM_CHAIN_ID) { + relayHub = IGSNRelayHub(ETHEREUM_RELAY_HUB); + relayWorker = ETHEREUM_RELAY_WORKER; + } else { + revert("gsnRelayCall: Unsupported network"); + } + + // Construct request + IGSNTypes.RelayRequest memory relayRequest = gsnConstructRelayRequest({ + _from: _from, + _to: _to, + _txData: _txData, + _paymasterAddress: _paymasterAddress, + _topUp: _topUp, + _relayWorker: relayWorker + }); + + // Sign request + bytes memory signature = gsnSignRelayCall({_privateKey: _privateKey, _relayRequest: relayRequest}); + + // Relay call + uint256 externalCallDataCostOverhead = 22_414; + uint256 msgGas = relayRequest.request.gas + 1_000_000; // Add a buffer above the actual request action cost + vm.prank(relayWorker, relayWorker); + vm.txGasPrice(relayRequest.relayData.gasPrice); + relayHub.relayCall{gas: msgGas}({ + _maxAcceptanceBudget: 285252, // Hardcoded in the Enzyme relayer logic + _relayRequest: relayRequest, + _signature: signature, + _approvalData: "", // Can be empty + _externalGasLimit: msgGas + externalCallDataCostOverhead + }); + } + + function gsnSignRelayCall(uint256 _privateKey, IGSNTypes.RelayRequest memory _relayRequest) + internal + view + returns (bytes memory signature_) + { + bytes32 domainSeparator; + if (block.chainid == ETHEREUM_CHAIN_ID) { + domainSeparator = ETHEREUM_DOMAIN_SEPARATOR; + } else { + revert("gsnSignRelayCall: Unsupported network"); + } + + bytes memory suffixData = abi.encode( + keccak256( + abi.encode( + RELAY_DATA_TYPE_HASH, + _relayRequest.relayData.gasPrice, + _relayRequest.relayData.pctRelayFee, + _relayRequest.relayData.baseRelayFee, + _relayRequest.relayData.relayWorker, + _relayRequest.relayData.paymaster, + _relayRequest.relayData.forwarder, + keccak256(_relayRequest.relayData.paymasterData), + _relayRequest.relayData.clientId + ) + ) + ); + + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + domainSeparator, + keccak256( + abi.encodePacked( + RELAY_REQUEST_TYPE_HASH, + uint256(uint160(_relayRequest.request.from)), + uint256(uint160(_relayRequest.request.to)), + _relayRequest.request.value, + _relayRequest.request.gas, + _relayRequest.request.nonce, + keccak256(_relayRequest.request.data), + _relayRequest.request.validUntil, + suffixData + ) + ) + ) + ); + + return createSignature({_privateKey: _privateKey, _digest: digest}); + } +}