diff --git a/contracts/ccip/GovernanceCCIPRelay.sol b/contracts/ccip/GovernanceCCIPRelay.sol index 049152d..b79ef02 100644 --- a/contracts/ccip/GovernanceCCIPRelay.sol +++ b/contracts/ccip/GovernanceCCIPRelay.sol @@ -21,6 +21,8 @@ contract GovernanceCCIPRelay is IGovernanceCCIPRelay { mapping(uint64 => address) public destinationReceivers; uint64 private CCIPMainnetChainSelector = 5009297550715157269; + uint256 constant MIN_GAS_LIMIT = 50_000; + uint256 constant MAX_GAS_LIMIT = 10_000_000; /// @dev Modifier to restrict access to the Timelock contract. modifier onlyTimeLock() { @@ -103,11 +105,17 @@ contract GovernanceCCIPRelay is IGovernanceCCIPRelay { /// @inheritdoc IGovernanceCCIPRelay function relayMessage( uint64 destinationChainSelector, + uint256 gasLimit, address target, bytes calldata payload ) external payable onlyTimeLock returns (bytes32 messageId) { require(target != address(0), AddressCannotBeZero()); require(payload.length != 0, PayloadCannotBeEmpty()); + require(gasLimit >= MIN_GAS_LIMIT, GasLimitTooLow(gasLimit, MIN_GAS_LIMIT)); + require( + gasLimit <= MAX_GAS_LIMIT, + GasLimitTooHigh(gasLimit, MAX_GAS_LIMIT) + ); address destinationReceiver = destinationReceivers[ destinationChainSelector @@ -121,7 +129,12 @@ contract GovernanceCCIPRelay is IGovernanceCCIPRelay { receiver: abi.encode(destinationReceiver), data: abi.encode(target, payload), tokenAmounts: new Client.EVMTokenAmount[](0), - extraArgs: "", + extraArgs: Client._argsToBytes( + Client.EVMExtraArgsV2({ + gasLimit: gasLimit, + allowOutOfOrderExecution: true + }) + ), feeToken: address(0) }); diff --git a/contracts/ccip/interfaces/IGovernanceCCIPRelay.sol b/contracts/ccip/interfaces/IGovernanceCCIPRelay.sol index 62e33fd..191c6e7 100644 --- a/contracts/ccip/interfaces/IGovernanceCCIPRelay.sol +++ b/contracts/ccip/interfaces/IGovernanceCCIPRelay.sol @@ -25,6 +25,16 @@ interface IGovernanceCCIPRelay { /// @dev Error thrown when a provided address is the zero address. error AddressCannotBeZero(); + /// @dev Thrown when the specified gas limit is too low to execute the transaction. + /// @param gasLimit The provided gas limit. + /// @param minGasLimit The minimum required gas limit. + error GasLimitTooLow(uint256 gasLimit, uint256 minGasLimit); + + /// @dev Thrown when the specified gas limit exceeds the maximum allowed threshold. + /// @param gasLimit The provided gas limit. + /// @param maxGasLimit The maximum allowed gas limit. + error GasLimitTooHigh(uint256 gasLimit, uint256 maxGasLimit); + /// @dev Error thrown when payload is empty. error PayloadCannotBeEmpty(); @@ -86,11 +96,13 @@ interface IGovernanceCCIPRelay { /// @notice Relays a governance message to the destination chain. /// @param destinationChainSelector The ccip chain selector of the destination chain for the message. + /// @param gasLimit the maximum amount of gas CCIP can consume to execute ccipReceive() /// @param target The target address to execute the message on. /// @param payload The calldata payload to execute. /// @return messageId The unique identifier of the sent message. function relayMessage( uint64 destinationChainSelector, + uint256 gasLimit, address target, bytes calldata payload ) external payable returns (bytes32 messageId); diff --git a/test/ccip/GovernanceCCIPIntegrationTest.t.sol b/test/ccip/GovernanceCCIPIntegrationTest.t.sol index 23c1306..4bb3c70 100644 --- a/test/ccip/GovernanceCCIPIntegrationTest.t.sol +++ b/test/ccip/GovernanceCCIPIntegrationTest.t.sol @@ -230,6 +230,7 @@ contract GovernanceCCIPIntegrationTest is Test { vm.prank(address(timelock)); bytes32 messageId = governanceRelay.relayMessage{value: fee}( polygonMainnetChainSelector, + 200_000, target, payload ); @@ -276,6 +277,7 @@ contract GovernanceCCIPIntegrationTest is Test { vm.prank(address(timelock)); bytes32 messageId = governanceRelay.relayMessage{value: fee}( polygonMainnetChainSelector, + 200_000, target, payload ); @@ -303,8 +305,13 @@ contract GovernanceCCIPIntegrationTest is Test { targets[0] = address(governanceRelay); values[0] = 1 ether; - signatures[0] = "relayMessage(uint64,address,bytes)"; - calldatas[0] = abi.encode(polygonMainnetChainSelector, target, payload); + signatures[0] = "relayMessage(uint64,uint256,address,bytes)"; + calldatas[0] = abi.encode( + polygonMainnetChainSelector, + 200_000, + target, + payload + ); assertEq(numberUpdater.number(), 0, "Initial number should be 0"); uint256 fee = 0.1 ether; diff --git a/test/ccip/GovernanceCCIPRelayTest.t.sol b/test/ccip/GovernanceCCIPRelayTest.t.sol index 0301d21..b4416f4 100644 --- a/test/ccip/GovernanceCCIPRelayTest.t.sol +++ b/test/ccip/GovernanceCCIPRelayTest.t.sol @@ -108,7 +108,12 @@ contract GovernanceCCIPRelayTest is Test { vm.deal(address(timelock), fee); // Fund this contract vm.prank(timelock); - relay.relayMessage{value: fee}(destinationChainSelector, target, payload); + relay.relayMessage{value: fee}( + destinationChainSelector, + 200_000, + target, + payload + ); } /// @notice Test relayMessage reverts when called by non-timelock @@ -123,7 +128,7 @@ contract GovernanceCCIPRelayTest is Test { attacker ) ); - relay.relayMessage(destinationChainSelector, target, payload); + relay.relayMessage(destinationChainSelector, 200_000, target, payload); } /// @notice Test relayMessage returns excess ether @@ -144,6 +149,7 @@ contract GovernanceCCIPRelayTest is Test { vm.prank(timelock); relay.relayMessage{value: fee + excess}( destinationChainSelector, + 200_000, target, payload ); @@ -173,7 +179,12 @@ contract GovernanceCCIPRelayTest is Test { vm.prank(timelock); vm.expectEmit(true, true, true, true); emit IGovernanceCCIPRelay.MessageRelayed(bytes32(""), target, payload); - relay.relayMessage{value: fee}(destinationChainSelector, target, payload); + relay.relayMessage{value: fee}( + destinationChainSelector, + 200_000, + target, + payload + ); } /// @notice Test relayMessage raises InsufficientFee error when value is less than fee @@ -200,6 +211,7 @@ contract GovernanceCCIPRelayTest is Test { ); relay.relayMessage{value: fee - 0.1 ether}( destinationChainSelector, + 200_000, target, payload ); @@ -227,7 +239,12 @@ contract GovernanceCCIPRelayTest is Test { unsupportedChainSelector ) ); - relay.relayMessage{value: fee}(unsupportedChainSelector, target, payload); + relay.relayMessage{value: fee}( + unsupportedChainSelector, + 200_000, + target, + payload + ); } function testAddDestinationChainsByTimelock() public { @@ -373,7 +390,7 @@ contract GovernanceCCIPRelayTest is Test { vm.deal(address(timelock), 1 ether); vm.startPrank(timelock); vm.expectRevert(IGovernanceCCIPRelay.AddressCannotBeZero.selector); - relay.relayMessage{value: 1}(1, address(0), "0x1234"); + relay.relayMessage{value: 1}(1, 200_000, address(0), "0x1234"); vm.stopPrank(); } @@ -381,7 +398,37 @@ contract GovernanceCCIPRelayTest is Test { vm.deal(address(timelock), 1 ether); vm.startPrank(timelock); vm.expectRevert(IGovernanceCCIPRelay.PayloadCannotBeEmpty.selector); - relay.relayMessage{value: 1}(1, address(0xabc), ""); + relay.relayMessage{value: 1}(1, 200_000, address(0xabc), ""); vm.stopPrank(); } + + function testRelayMessage_GasLimitTooLow() public { + vm.deal(address(timelock), 1 ether); + vm.prank(timelock); + vm.expectRevert( + abi.encodeWithSelector( + IGovernanceCCIPRelay.GasLimitTooLow.selector, + 1, + 50_000 + ) + ); + relay.relayMessage(destinationChainSelector, 1, address(0x4), "payload"); + } + + function testRelayMessage_GasLimitTooHigh() public { + vm.prank(timelock); // Simulate timelock calling + vm.expectRevert( + abi.encodeWithSelector( + IGovernanceCCIPRelay.GasLimitTooHigh.selector, + 10_000_000 + 1, + 10_000_000 + ) + ); + relay.relayMessage( + destinationChainSelector, + 10_000_000 + 1, + address(0x4), + "payload" + ); + } }