diff --git a/rust/chains/hyperlane-ethereum/abis/InterchainGasPaymaster.abi.json b/rust/chains/hyperlane-ethereum/abis/InterchainGasPaymaster.abi.json index 366a422f5..c56d2aa59 100644 --- a/rust/chains/hyperlane-ethereum/abis/InterchainGasPaymaster.abi.json +++ b/rust/chains/hyperlane-ethereum/abis/InterchainGasPaymaster.abi.json @@ -16,7 +16,13 @@ { "indexed": false, "internalType": "uint256", - "name": "amount", + "name": "gasAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "payment", "type": "uint256" } ], @@ -93,9 +99,19 @@ "internalType": "uint32", "name": "_destinationDomain", "type": "uint32" + }, + { + "internalType": "uint256", + "name": "_gasAmount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_refundAddress", + "type": "address" } ], - "name": "payGasFor", + "name": "payForGas", "outputs": [], "stateMutability": "payable", "type": "function" diff --git a/rust/chains/hyperlane-ethereum/abis/Mailbox.abi.json b/rust/chains/hyperlane-ethereum/abis/Mailbox.abi.json index 120157a53..d9436364c 100644 --- a/rust/chains/hyperlane-ethereum/abis/Mailbox.abi.json +++ b/rust/chains/hyperlane-ethereum/abis/Mailbox.abi.json @@ -105,9 +105,9 @@ "name": "VERSION", "outputs": [ { - "internalType": "uint32", + "internalType": "uint8", "name": "", - "type": "uint32" + "type": "uint8" } ], "stateMutability": "view", diff --git a/rust/chains/hyperlane-ethereum/abis/MultisigIsm.abi.json b/rust/chains/hyperlane-ethereum/abis/MultisigIsm.abi.json index 3eeac90b1..c68b5bc87 100644 --- a/rust/chains/hyperlane-ethereum/abis/MultisigIsm.abi.json +++ b/rust/chains/hyperlane-ethereum/abis/MultisigIsm.abi.json @@ -1,324 +1,324 @@ [ - { - "inputs": [], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "previousOwner", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "OwnershipTransferred", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "uint32", - "name": "domain", - "type": "uint32" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "threshold", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "bytes32", - "name": "commitment", - "type": "bytes32" - } - ], - "name": "ThresholdSet", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "uint32", - "name": "domain", - "type": "uint32" - }, - { - "indexed": true, - "internalType": "address", - "name": "validator", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "validatorCount", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "bytes32", - "name": "commitment", - "type": "bytes32" - } - ], - "name": "ValidatorEnrolled", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "uint32", - "name": "domain", - "type": "uint32" - }, - { - "indexed": true, - "internalType": "address", - "name": "validator", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "validatorCount", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "bytes32", - "name": "commitment", - "type": "bytes32" - } - ], - "name": "ValidatorUnenrolled", - "type": "event" - }, - { - "inputs": [ - { - "internalType": "uint32", - "name": "", - "type": "uint32" - } - ], - "name": "commitment", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint32", - "name": "_domain", - "type": "uint32" - }, - { - "internalType": "address", - "name": "_validator", - "type": "address" - } - ], - "name": "enrollValidator", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint32", - "name": "_domain", - "type": "uint32" - }, - { - "internalType": "address", - "name": "_address", - "type": "address" - } - ], - "name": "isEnrolled", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "owner", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "renounceOwnership", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint32", - "name": "_domain", - "type": "uint32" - }, - { - "internalType": "uint256", - "name": "_threshold", - "type": "uint256" - } - ], - "name": "setThreshold", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint32", - "name": "", - "type": "uint32" - } - ], - "name": "threshold", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "transferOwnership", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint32", - "name": "_domain", - "type": "uint32" - }, - { - "internalType": "address", - "name": "_validator", - "type": "address" - } - ], - "name": "unenrollValidator", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint32", - "name": "_domain", - "type": "uint32" - } - ], - "name": "validatorCount", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint32", - "name": "_domain", - "type": "uint32" - } - ], - "name": "validators", - "outputs": [ - { - "internalType": "address[]", - "name": "", - "type": "address[]" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes", - "name": "_metadata", - "type": "bytes" - }, - { - "internalType": "bytes", - "name": "_message", - "type": "bytes" - } - ], - "name": "verify", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - } - ] + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint32", + "name": "domain", + "type": "uint32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "threshold", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "commitment", + "type": "bytes32" + } + ], + "name": "ThresholdSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint32", + "name": "domain", + "type": "uint32" + }, + { + "indexed": true, + "internalType": "address", + "name": "validator", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "validatorCount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "commitment", + "type": "bytes32" + } + ], + "name": "ValidatorEnrolled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint32", + "name": "domain", + "type": "uint32" + }, + { + "indexed": true, + "internalType": "address", + "name": "validator", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "validatorCount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "commitment", + "type": "bytes32" + } + ], + "name": "ValidatorUnenrolled", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "name": "commitment", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "_domain", + "type": "uint32" + }, + { + "internalType": "address", + "name": "_validator", + "type": "address" + } + ], + "name": "enrollValidator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "_domain", + "type": "uint32" + }, + { + "internalType": "address", + "name": "_address", + "type": "address" + } + ], + "name": "isEnrolled", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "_domain", + "type": "uint32" + }, + { + "internalType": "uint256", + "name": "_threshold", + "type": "uint256" + } + ], + "name": "setThreshold", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "name": "threshold", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "_domain", + "type": "uint32" + }, + { + "internalType": "address", + "name": "_validator", + "type": "address" + } + ], + "name": "unenrollValidator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "_domain", + "type": "uint32" + } + ], + "name": "validatorCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "_domain", + "type": "uint32" + } + ], + "name": "validators", + "outputs": [ + { + "internalType": "address[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "_metadata", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "_message", + "type": "bytes" + } + ], + "name": "verify", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/rust/chains/hyperlane-ethereum/src/interchain_gas.rs b/rust/chains/hyperlane-ethereum/src/interchain_gas.rs index 2158f93d9..e8ea1e966 100644 --- a/rust/chains/hyperlane-ethereum/src/interchain_gas.rs +++ b/rust/chains/hyperlane-ethereum/src/interchain_gas.rs @@ -119,7 +119,7 @@ where .map(|(log, log_meta)| InterchainGasPaymentWithMeta { payment: InterchainGasPayment { message_id: H256::from(log.message_id), - amount: log.amount, + payment: log.payment, }, meta: InterchainGasPaymentMeta { transaction_hash: log_meta.transaction_hash, diff --git a/rust/hyperlane-core/src/db/hyperlane_db.rs b/rust/hyperlane-core/src/db/hyperlane_db.rs index d65f9c0b3..18144222c 100644 --- a/rust/hyperlane-core/src/db/hyperlane_db.rs +++ b/rust/hyperlane-core/src/db/hyperlane_db.rs @@ -259,11 +259,14 @@ impl HyperlaneDB { &self, gas_payment: &InterchainGasPayment, ) -> Result<(), DbError> { - let InterchainGasPayment { message_id, amount } = gas_payment; + let InterchainGasPayment { + message_id, + payment, + } = gas_payment; let existing_payment = self.retrieve_gas_payment_for_message_id(*message_id)?; - let total = existing_payment + amount; + let total = existing_payment + payment; - info!(message_id=?message_id, gas_payment_amount=?amount, new_total_gas_payment=?total, "Storing gas payment"); + info!(message_id=?message_id, gas_payment_amount=?payment, new_total_gas_payment=?total, "Storing gas payment"); self.store_keyed_encodable(GAS_PAYMENT_FOR_MESSAGE_ID, &gas_payment.message_id, &total)?; Ok(()) diff --git a/rust/hyperlane-core/src/types/mod.rs b/rust/hyperlane-core/src/types/mod.rs index 3faff5636..2f9d02021 100644 --- a/rust/hyperlane-core/src/types/mod.rs +++ b/rust/hyperlane-core/src/types/mod.rs @@ -22,7 +22,7 @@ pub struct InterchainGasPayment { /// The id of the message pub message_id: H256, /// The payment amount, in origin chain native token wei - pub amount: U256, + pub payment: U256, } /// Uniquely identifying metadata for an InterchainGasPayment diff --git a/solidity/contracts/InterchainGasPaymaster.sol b/solidity/contracts/InterchainGasPaymaster.sol index 0bbe84f6a..46a56b8f7 100644 --- a/solidity/contracts/InterchainGasPaymaster.sol +++ b/solidity/contracts/InterchainGasPaymaster.sol @@ -17,9 +17,14 @@ contract InterchainGasPaymaster is IInterchainGasPaymaster, OwnableUpgradeable { /** * @notice Emitted when a payment is made for a message's gas costs. * @param messageId The ID of the message to pay for. - * @param amount The amount of native tokens paid. + * @param gasAmount The amount of destination gas paid for. + * @param payment The amount of native tokens paid. */ - event GasPayment(bytes32 indexed messageId, uint256 amount); + event GasPayment( + bytes32 indexed messageId, + uint256 gasAmount, + uint256 payment + ); // ============ Constructor ============ @@ -39,18 +44,22 @@ contract InterchainGasPaymaster is IInterchainGasPaymaster, OwnableUpgradeable { * to its destination chain. * @param _messageId The ID of the message to pay for. * @param _destinationDomain The domain of the message's destination chain. + * @param _gasAmount The amount of destination gas to pay for. Currently unused. + * @param _refundAddress The address to refund any overpayment to. Currently unused. */ - function payGasFor(bytes32 _messageId, uint32 _destinationDomain) - external - payable - override - { + function payForGas( + bytes32 _messageId, + uint32 _destinationDomain, + uint256 _gasAmount, + address _refundAddress + ) external payable override { // Silence compiler warning. The NatSpec @param requires the parameter to be named. - // While not used at the moment, future versions of the paymaster may conditionally - // forward payments depending on the destination domain. + // While not used at the moment, future versions of the paymaster have behavior specific + // to the destination domain and refund overpayments to the _refundAddress. _destinationDomain; + _refundAddress; - emit GasPayment(_messageId, msg.value); + emit GasPayment(_messageId, _gasAmount, msg.value); } /** diff --git a/solidity/contracts/Router.sol b/solidity/contracts/Router.sol index a95199f8b..42e318d7c 100644 --- a/solidity/contracts/Router.sol +++ b/solidity/contracts/Router.sol @@ -151,20 +151,26 @@ abstract contract Router is HyperlaneConnectionClient, IMessageRecipient { * @dev Reverts if there is no enrolled router for _destinationDomain. * @param _destinationDomain The domain of the chain to which to send the message. * @param _messageBody Raw bytes content of message. + * @param _gasAmount The amount of destination gas for the message that is requested via the InterchainGasPaymaster. * @param _gasPayment The amount of native tokens to pay for the message to be relayed. + * @param _gasPaymentRefundAddress The address to refund any gas overpayment to. */ function _dispatchWithGas( uint32 _destinationDomain, bytes memory _messageBody, - uint256 _gasPayment + uint256 _gasAmount, + uint256 _gasPayment, + address _gasPaymentRefundAddress ) internal { bytes32 _messageId = _dispatch(_destinationDomain, _messageBody); - if (_gasPayment > 0) { - interchainGasPaymaster.payGasFor{value: _gasPayment}( - _messageId, - _destinationDomain - ); - } + // Call the IGP even if the gas payment is zero. This is to support on-chain + // fee quoting in IGPs, which should always revert if gas payment is insufficient. + interchainGasPaymaster.payForGas{value: _gasPayment}( + _messageId, + _destinationDomain, + _gasAmount, + _gasPaymentRefundAddress + ); } /** diff --git a/solidity/contracts/middleware/liquidity-layer/LiquidityLayerRouter.sol b/solidity/contracts/middleware/liquidity-layer/LiquidityLayerRouter.sol index f61edc0d1..e29ebcbdc 100644 --- a/solidity/contracts/middleware/liquidity-layer/LiquidityLayerRouter.sol +++ b/solidity/contracts/middleware/liquidity-layer/LiquidityLayerRouter.sol @@ -70,7 +70,13 @@ contract LiquidityLayerRouter is Router { ); // Dispatch the _messageWithMetadata to the destination's LiquidityLayerRouter. - _dispatchWithGas(_destinationDomain, _messageWithMetadata, msg.value); + _dispatchWithGas( + _destinationDomain, + _messageWithMetadata, + 0, // TODO eventually accommodate gas amounts + msg.value, + msg.sender + ); } // Handles a message from an enrolled remote LiquidityLayerRouter diff --git a/solidity/contracts/test/TestRouter.sol b/solidity/contracts/test/TestRouter.sol index 7a8c806eb..db70a49a5 100644 --- a/solidity/contracts/test/TestRouter.sol +++ b/solidity/contracts/test/TestRouter.sol @@ -38,10 +38,18 @@ contract TestRouter is Router { } function dispatchWithGas( - uint32 _destination, - bytes memory _msg, - uint256 _gasPayment + uint32 _destinationDomain, + bytes memory _messageBody, + uint256 _gasAmount, + uint256 _gasPayment, + address _gasPaymentRefundAddress ) external payable { - _dispatchWithGas(_destination, _msg, _gasPayment); + _dispatchWithGas( + _destinationDomain, + _messageBody, + _gasAmount, + _gasPayment, + _gasPaymentRefundAddress + ); } } diff --git a/solidity/contracts/test/TestSendReceiver.sol b/solidity/contracts/test/TestSendReceiver.sol index fe4381c02..1f51cec0b 100644 --- a/solidity/contracts/test/TestSendReceiver.sol +++ b/solidity/contracts/test/TestSendReceiver.sol @@ -10,6 +10,8 @@ import {IMailbox} from "../../interfaces/IMailbox.sol"; contract TestSendReceiver is IMessageRecipient { using TypeCasts for address; + uint256 public constant HANDLE_GAS_AMOUNT = 50_000; + event Handled(bytes32 blockHash); function dispatchToSelf( @@ -27,15 +29,28 @@ contract TestSendReceiver is IMessageRecipient { uint256 _value = msg.value; if (_blockHashNum % 5 == 0) { // Pay in two separate calls, resulting in 2 distinct events - uint256 _half = _value / 2; - _paymaster.payGasFor{value: _half}(_messageId, _destinationDomain); - _paymaster.payGasFor{value: _value - _half}( + uint256 _halfPayment = _value / 2; + uint256 _halfGasAmount = HANDLE_GAS_AMOUNT / 2; + _paymaster.payForGas{value: _halfPayment}( + _messageId, + _destinationDomain, + _halfGasAmount, + msg.sender + ); + _paymaster.payForGas{value: _value - _halfPayment}( _messageId, - _destinationDomain + _destinationDomain, + HANDLE_GAS_AMOUNT - _halfGasAmount, + msg.sender ); } else { // Pay the entire msg.value in one call - _paymaster.payGasFor{value: _value}(_messageId, _destinationDomain); + _paymaster.payForGas{value: _value}( + _messageId, + _destinationDomain, + HANDLE_GAS_AMOUNT, + msg.sender + ); } } diff --git a/solidity/interfaces/IInterchainGasPaymaster.sol b/solidity/interfaces/IInterchainGasPaymaster.sol index c45febb7a..58d49d8f9 100644 --- a/solidity/interfaces/IInterchainGasPaymaster.sol +++ b/solidity/interfaces/IInterchainGasPaymaster.sol @@ -7,7 +7,10 @@ pragma solidity >=0.6.11; * messages to destination chains. */ interface IInterchainGasPaymaster { - function payGasFor(bytes32 _messageId, uint32 _destinationDomain) - external - payable; + function payForGas( + bytes32 _messageId, + uint32 _destinationDomain, + uint256 _gas, + address _refundAddress + ) external payable; } diff --git a/solidity/test/interchainGasPaymaster.test.ts b/solidity/test/interchainGasPaymaster.test.ts index 69141a351..3692afa9a 100644 --- a/solidity/test/interchainGasPaymaster.test.ts +++ b/solidity/test/interchainGasPaymaster.test.ts @@ -10,7 +10,9 @@ import { const MESSAGE_ID = '0x6ae9a99190641b9ed0c07143340612dde0e9cb7deaa5fe07597858ae9ba5fd7f'; const DESTINATION_DOMAIN = 1234; -const PAYMENT_AMOUNT = 123456789; +const GAS_PAYMENT_AMOUNT = 123456789; +const GAS_AMOUNT = 4444; +const REFUND_ADDRESS = '0xc0ffee0000000000000000000000000000000000'; const OWNER = '0xdeadbeef00000000000000000000000000000000'; describe('InterchainGasPaymaster', async () => { @@ -31,42 +33,60 @@ describe('InterchainGasPaymaster', async () => { }); }); - describe('#payGasFor', async () => { + describe('#payForGas', async () => { it('deposits the value into the contract', async () => { const paymasterBalanceBefore = await signer.provider!.getBalance( paymaster.address, ); - await paymaster.payGasFor(MESSAGE_ID, DESTINATION_DOMAIN, { - value: PAYMENT_AMOUNT, - }); + await paymaster.payForGas( + MESSAGE_ID, + DESTINATION_DOMAIN, + GAS_AMOUNT, + REFUND_ADDRESS, + { + value: GAS_PAYMENT_AMOUNT, + }, + ); const paymasterBalanceAfter = await signer.provider!.getBalance( paymaster.address, ); expect(paymasterBalanceAfter.sub(paymasterBalanceBefore)).equals( - PAYMENT_AMOUNT, + GAS_PAYMENT_AMOUNT, ); }); it('emits the GasPayment event', async () => { await expect( - paymaster.payGasFor(MESSAGE_ID, DESTINATION_DOMAIN, { - value: PAYMENT_AMOUNT, - }), + paymaster.payForGas( + MESSAGE_ID, + DESTINATION_DOMAIN, + GAS_AMOUNT, + REFUND_ADDRESS, + { + value: GAS_PAYMENT_AMOUNT, + }, + ), ) .to.emit(paymaster, 'GasPayment') - .withArgs(MESSAGE_ID, PAYMENT_AMOUNT); + .withArgs(MESSAGE_ID, GAS_AMOUNT, GAS_PAYMENT_AMOUNT); }); }); describe('#claim', async () => { it('sends the entire balance of the contract to the owner', async () => { // First pay some ether into the contract - await paymaster.payGasFor(MESSAGE_ID, DESTINATION_DOMAIN, { - value: PAYMENT_AMOUNT, - }); + await paymaster.payForGas( + MESSAGE_ID, + DESTINATION_DOMAIN, + GAS_AMOUNT, + REFUND_ADDRESS, + { + value: GAS_PAYMENT_AMOUNT, + }, + ); // Set the owner to a different address so we aren't paying gas with the same // address we want to observe the balance of @@ -77,12 +97,12 @@ describe('InterchainGasPaymaster', async () => { const paymasterBalanceBefore = await signer.provider!.getBalance( paymaster.address, ); - expect(paymasterBalanceBefore).equals(PAYMENT_AMOUNT); + expect(paymasterBalanceBefore).equals(GAS_PAYMENT_AMOUNT); await paymaster.claim(); const ownerBalanceAfter = await signer.provider!.getBalance(OWNER); - expect(ownerBalanceAfter).equals(PAYMENT_AMOUNT); + expect(ownerBalanceAfter).equals(GAS_PAYMENT_AMOUNT); const paymasterBalanceAfter = await signer.provider!.getBalance( paymaster.address, ); diff --git a/solidity/test/router.test.ts b/solidity/test/router.test.ts index 5fbfd00c4..d187f6fa5 100644 --- a/solidity/test/router.test.ts +++ b/solidity/test/router.test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-floating-promises */ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { expect } from 'chai'; -import { ContractTransaction } from 'ethers'; +import { BigNumberish, ContractTransaction } from 'ethers'; import { ethers } from 'hardhat'; import { utils } from '@hyperlane-xyz/utils'; @@ -24,6 +24,14 @@ const destination = 2; const destinationWithoutRouter = 3; const body = '0xdeadbeef'; +interface GasPaymentParams { + // The amount of destination gas being paid for + gasAmount: BigNumberish; + // The amount of native tokens paid + payment: BigNumberish; + refundAddress: string; +} + describe('Router', async () => { let router: TestRouter, mailbox: TestMailbox, @@ -153,7 +161,7 @@ describe('Router', async () => { const runDispatchFunctionTests = async ( dispatchFunction: ( destinationDomain: number, - interchainGasPayment?: number, + gasPaymentParams: GasPaymentParams, ) => Promise, expectGasPayment: boolean, ) => { @@ -165,17 +173,21 @@ describe('Router', async () => { return expected ? assertion : assertion.not; }; + const testGasPaymentParams: GasPaymentParams = { + gasAmount: 4321, + payment: 1234, + refundAddress: '0xc0ffee0000000000000000000000000000000000', + }; + it('dispatches a message', async () => { - await expect(dispatchFunction(destination)).to.emit( - mailbox, - 'Dispatch', - ); + await expect( + dispatchFunction(destination, testGasPaymentParams), + ).to.emit(mailbox, 'Dispatch'); }); it(`${ expectGasPayment ? 'pays' : 'does not pay' } interchain gas`, async () => { - const testInterchainGasPayment = 1234; const { id } = await inferMessageValues( mailbox, router.address, @@ -184,17 +196,21 @@ describe('Router', async () => { '', ); const assertion = expectAssertion( - expect(dispatchFunction(destination, testInterchainGasPayment)).to, + expect(dispatchFunction(destination, testGasPaymentParams)).to, expectGasPayment, ); await assertion .emit(interchainGasPaymaster, 'GasPayment') - .withArgs(id, testInterchainGasPayment); + .withArgs( + id, + testGasPaymentParams.gasAmount, + testGasPaymentParams.payment, + ); }); it('reverts when dispatching a message to an unenrolled remote router', async () => { await expect( - dispatchFunction(destinationWithoutRouter), + dispatchFunction(destinationWithoutRouter, testGasPaymentParams), ).to.be.revertedWith( `No router enrolled for domain. Did you specify the right domain ID?`, ); @@ -210,13 +226,15 @@ describe('Router', async () => { describe('#dispatchWithGas', () => { runDispatchFunctionTests( - (destinationDomain, interchainGasPayment = 0) => + (destinationDomain, gasPaymentParams) => router.dispatchWithGas( destinationDomain, '0x', - interchainGasPayment, + gasPaymentParams.gasAmount, + gasPaymentParams.payment, + gasPaymentParams.refundAddress, { - value: interchainGasPayment, + value: gasPaymentParams.payment, }, ), true, diff --git a/solidity/update_abis.sh b/solidity/update_abis.sh index 42b5b959f..c78885073 100755 --- a/solidity/update_abis.sh +++ b/solidity/update_abis.sh @@ -1,5 +1,7 @@ #!/bin/sh +# Must be ran from the `solidity` directory + copy() { # Optionally allow path to be passed in, and extract the contract name # as the string following the last instance of `/` @@ -7,4 +9,4 @@ copy() { jq .abi < artifacts/contracts/"$1".sol/"$CONTRACT_NAME".json > ../rust/chains/hyperlane-ethereum/abis/"$CONTRACT_NAME".abi.json } -copy Inbox && copy Outbox && copy validator-manager/InboxValidatorManager && copy InterchainGasPaymaster +copy Mailbox && copy isms/MultisigIsm && copy InterchainGasPaymaster diff --git a/typescript/helloworld/contracts/HelloWorld.sol b/typescript/helloworld/contracts/HelloWorld.sol index 0b433e109..aa382147b 100644 --- a/typescript/helloworld/contracts/HelloWorld.sol +++ b/typescript/helloworld/contracts/HelloWorld.sol @@ -21,6 +21,10 @@ contract HelloWorld is Router { // by this contract from the domain. mapping(uint32 => uint256) public receivedFrom; + // Keyed by domain, a generous upper bound on the amount of gas to use in the + // handle function when a message is processed. Used for paying for gas. + mapping(uint32 => uint256) public handleGasAmounts; + // ============ Events ============ event SentHelloWorld( uint32 indexed origin, @@ -33,6 +37,10 @@ contract HelloWorld is Router { bytes32 sender, string message ); + event HandleGasAmountSet( + uint32 indexed destination, + uint256 handleGasAmount + ); constructor(address _mailbox, address _interchainGasPaymaster) { // Transfer ownership of the contract to deployer @@ -49,6 +57,7 @@ contract HelloWorld is Router { * @notice Sends a message to the _destinationDomain. Any msg.value is * used as interchain gas payment. * @param _destinationDomain The destination domain to send the message to. + * @param _message The message to send. */ function sendHelloWorld(uint32 _destinationDomain, string calldata _message) external @@ -56,7 +65,13 @@ contract HelloWorld is Router { { sent += 1; sentTo[_destinationDomain] += 1; - _dispatchWithGas(_destinationDomain, bytes(_message), msg.value); + _dispatchWithGas( + _destinationDomain, + bytes(_message), + handleGasAmounts[_destinationDomain], + msg.value, + msg.sender + ); emit SentHelloWorld( mailbox.localDomain(), _destinationDomain, @@ -64,6 +79,21 @@ contract HelloWorld is Router { ); } + /** + * @notice Sets the amount of gas the recipient's handle function uses on + * the destination domain, which is used when paying for gas. + * @dev Reverts if called by a non-owner. + * @param _destinationDomain The destination domain, + * @param _handleGasAmount The handle gas amount. + */ + function setHandleGasAmount( + uint32 _destinationDomain, + uint256 _handleGasAmount + ) external onlyOwner { + handleGasAmounts[_destinationDomain] = _handleGasAmount; + emit HandleGasAmountSet(_destinationDomain, _handleGasAmount); + } + // ============ Internal functions ============ /**