v2 IGP interface change (#1308)

* Solidity / ts changes

* Update ABIs

* Agent changes

* cargo fmt

* TestSendReceiver changes

* Change param order

* Rm gas payment > 0 check, add handleGasAmounts mapping to HelloWorld

* Some renames

* Update abi

* Fix router test

* Note on why we call the IGP always in _dispatchWithGas
pull/1378/head
Trevor Porter 2 years ago committed by GitHub
parent 0f49c66c1c
commit 6046ce68ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 20
      rust/chains/hyperlane-ethereum/abis/InterchainGasPaymaster.abi.json
  2. 4
      rust/chains/hyperlane-ethereum/abis/Mailbox.abi.json
  3. 2
      rust/chains/hyperlane-ethereum/src/interchain_gas.rs
  4. 9
      rust/hyperlane-core/src/db/hyperlane_db.rs
  5. 2
      rust/hyperlane-core/src/types/mod.rs
  6. 29
      solidity/contracts/InterchainGasPaymaster.sol
  7. 16
      solidity/contracts/Router.sol
  8. 8
      solidity/contracts/middleware/liquidity-layer/LiquidityLayerRouter.sol
  9. 16
      solidity/contracts/test/TestRouter.sol
  10. 25
      solidity/contracts/test/TestSendReceiver.sol
  11. 9
      solidity/interfaces/IInterchainGasPaymaster.sol
  12. 50
      solidity/test/interchainGasPaymaster.test.ts
  13. 44
      solidity/test/router.test.ts
  14. 4
      solidity/update_abis.sh
  15. 32
      typescript/helloworld/contracts/HelloWorld.sol

@ -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"

@ -105,9 +105,9 @@
"name": "VERSION",
"outputs": [
{
"internalType": "uint32",
"internalType": "uint8",
"name": "",
"type": "uint32"
"type": "uint8"
}
],
"stateMutability": "view",

@ -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,

@ -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(())

@ -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

@ -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);
}
/**

@ -151,21 +151,27 @@ 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}(
// 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
_destinationDomain,
_gasAmount,
_gasPaymentRefundAddress
);
}
}
/**
* @notice Dispatches a message to an enrolled router via the provided Mailbox.

@ -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

@ -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
);
}
}

@ -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
);
}
}

@ -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;
}

@ -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,
);

@ -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<ContractTransaction>,
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,

@ -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

@ -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 ============
/**

Loading…
Cancel
Save