Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Epic: Relayer payments should be conditional on relaying #2456

Open
1 task
nambrot opened this issue Jun 30, 2023 · 17 comments
Open
1 task

Epic: Relayer payments should be conditional on relaying #2456

nambrot opened this issue Jun 30, 2023 · 17 comments
Labels

Comments

@nambrot
Copy link
Contributor

nambrot commented Jun 30, 2023

The current IGP scheme requires message senders to pay a specific relayer via their InterchainGasPaymaster

function payForGas(
        bytes32 _messageId,
        uint32 _destinationDomain,
        uint256 _gasAmount,
        address _refundAddress
    ) 

This function currently does a bunch of things:

  • When using the DefaultISMInterchainGasPaymaster, it will consider the overhead of using the default ISM on the destination chain + mailbox.process overhead and use the amended gas amount downstream
  • it calculates the required amount of native origin chain gas token using a IGasOracle that is configured on the IGP
  • It stores that amount that is passed via msg.value on the contract and refunds the the remainder back to the `refundAddress

There are a few shortcomings of this approach:

  • Senders have to identify a specific relayer. If there are multiple relayers, there is no way to pay whoever actually ends up relaying the message. Warp routes are specified with a specific IGP and thus relayer
  • If the relayer is faulty or malicious, there is no recourse, and the sender has lost the payment

This ticket seeks to track an improved version of a relayer payment scheme.

Alternate Approaches

IBC

IBC as the OG permissionless interoperability protocol had this problem for a while and like Hyperlane started with the simplest approach of just having some independent party running relayers for message senders for free (often paid by one/both chains to incentivize activity). It looks like the IBC folks have largely rallied around ICS29

At a high-level, it works by putting up a bounty on the origin chain. When relayers process on the destination chain, the fee middleware captures the origin chain address of the relayer (previously registered), and passes that along in the ack package back to the origin chain which unlocks the fee payment and gives it to the relayer. Interestingly, the spec allows for specific bounties for specific packages, i.e. for the original message, but also for the ack message back. (IBC also has timeout packages that can be paid for)

Unspecified is how a sender determines a large enough bounty, as well as any refund mechanism if an overpayment has happened.

Wormhole

Wormhole originally mainly was the mechanism for it's canonical token bridge Portal (which they would run dedicated relayers for or allow for self-relaying on the token bridge UI), but has since improved their relaying:

They now have generalized relayers (which according to the docs is not yet on Mainnet). It looks pretty similar to our current system as far as I can tell.

Alternatives

Preview Give feedback
  1. IGP agent bounty relayer small
@nambrot
Copy link
Contributor Author

nambrot commented Jul 3, 2023

I was originally thinking that this needed to be built into the mailbox, but I don't think that is the case. I.e. you can just have a middleware around the mailbox that the relayer calls, which itself is the source of truth of who processed a message

@tkporter
Copy link
Collaborator

tkporter commented Jul 6, 2023

It stores that amount that is passed via msg.value on the contract

Just to be clear it doesn't store the amount paid per message in a mapping, it just emits it

And yeah you're right that you can just have a contract that the relayer calls. In v3 we could consider putting the relayer address in the delivered mapping (we could still have a function delivered(...) external returns (bool) to keep the simple API), because iiuc it's the same gas cost to set an address in a mapping as it is to set a bool. I think we had previously talked about this in some bounty conversations. Note the costs may be larger for alt execution environments like Solana, where you're charged per-byte for storage like this

It'd kinda be nice to not need to deploy another contract for each chain to get this to work well & to configure it in the relayer. So I wonder if we should just consider this change in v3

@nambrot
Copy link
Contributor Author

nambrot commented Aug 4, 2023

Thinking more about this, this feels like fundamentally the same problem as settlement oracles in UniswapX. I.e. you put up the inToken, and specify desired outcome (i.e. delivery of minAmount of outToken or processing of a message) and then a settlement oracle can be specified for this request to attest that the desired outcome actually happened to release inToken.

@serejke
Copy link
Contributor

serejke commented Oct 20, 2023

Idea

Quick thought. Currently, refunding the relayer's expenses on the source chain necessitates gas/price oracles or cross-chain proofs of relaying.

What if we refunded relayers on the destination chain instead?

Relayer calls to IMessageRecipient(recipient).handle. The recipient here is a contract. DApp would pre-fund this contract with the destination chain's native tokens.

To determine the gas consumed, the Mailbox contract can utilize the gasleft() function both before and after the handle call. This gives the gas expenditure by the DApp.

The Mailbox contract invokes the IMessageRecipient(recipient).refund(this, gas * gasprice), where gasprice can also be accessed on-chain. Possibly, the process's overall overhead must be accounted here as well.

The recipient contract can then trivially implement this as target.send{value: value}. Mailbox validates that the this.value before and after the refund call has increased precisely by the expected amount, and then transfers the refund to the relayer.

Analysis

The key idea is to shift the responsibility to charge the user from the Hyperlane protocol to the DApp.

Hyperlane is a framework. It is always used via some DApp. Let DApps decide how and how much to charge the user on the source chain. And DApp will handle the relayers' refund then.

@nambrot
Copy link
Contributor Author

nambrot commented Oct 20, 2023

I like the ability by the message recipient to fund this, it is possible to somewhat grief this prefunding however, i.e. a message sender could just send a bunch of messages from a cheap chain to this one which will cause the funds to be emptied.

@serejke
Copy link
Contributor

serejke commented Oct 21, 2023

We've had a conversation in DM. I'm posting the summary of my proposal here:

DApps have full control over how they charge. Controlled by the application layer, they have several options:

  1. Use on-chain quoting between source/destination prices; IGP/oracles of the Hyperlane Core protocol will be moved out to DApps layer;
  2. Use off-chain quoting with the real market-price. When user creates a DApp transaction, the DApp will subsitute the proper payment
  3. Charge % of the bridged tokens, either on source or on destination chain
  4. Subsidize the payments or charge off-chain

The relayer's refund can be a single-chain atomic operation, where the relayer delivers the message and gets refunded in one transaction. This contrasts with the current system where the relayer relies on off-chain guarantees to be refunded.

This solves the challenge to refund any solver who delivers the message.

@avious00 avious00 moved this to Backlog in Hyperlane Tasks Nov 7, 2023
@avious00 avious00 moved this from Backlog to icebox in Hyperlane Tasks Nov 7, 2023
@nambrot nambrot added the epic label Mar 1, 2024
@nambrot nambrot changed the title Relayer payments should be conditional on relaying Epic: Relayer payments should be conditional on relaying Mar 1, 2024
@yorhodes
Copy link
Member

yorhodes commented Mar 13, 2024

And yeah you're right that you can just have a contract that the relayer calls. In v3 we could consider putting the relayer address in the delivered mapping

We added the following functions to the Mailbox in v3

function processor(bytes32 _id) external view returns (address)
function processedAt(bytes32 _id) external view returns (uint48)

The relayer can call a contract on the destination chain

function claim(uint32 origin, address hook, bytes32 id) {
    bytes memory claimMsg = abi.encode(id, mailbox.processor(id), mailbox.processedAt(id));
    mailbox.dispatch(origin, hook, claimMsg);
}

The hook would be a message recipient

function handle(uint32 origin, bytes32 sender, bytes claimMsg) {
    (bytes32 id, address relayer, uint48 timestamp) = abi.decode(claimMsg);
    // pay out id bounty to relayer (as a function of timestamp?)
}

@tkporter
Copy link
Collaborator

Have you considered if the origin chain and destination chain are different protocols? so the processor address on the destination wouldn't relate to the relayer on the origin chain

@tkporter
Copy link
Collaborator

tkporter commented Mar 13, 2024

I also feel like solutions that don't require an additional message for each message that's been processed would be much preferable. I.e. instead of claiming for a specific message, you claim for a large batch of messages

@yorhodes
Copy link
Member

yorhodes commented Mar 13, 2024

Have you considered if the origin chain and destination chain are different protocols? so the processor address on the destination wouldn't relate to the relayer on the origin chain

nope, lets add beneficiary to the claim message

I also feel like solutions that don't require an additional message for each message that's been processed would be much preferable. I.e. instead of claiming for a specific message, you claim for a large batch of messages

ofc you can implement batching as an optimization

@tkporter
Copy link
Collaborator

nope, lets add beneficiary to the claim message

And require msg.sender to equal the processor? A little unfortunate that we wouldn't allow claiming from any key (like we do rn), but this isn't so bad

ofc you can implement batching as an optimization

How would the batching work? I guess I'm hoping for something where you can claim for like hundreds or thousands of messages at a time

@yorhodes
Copy link
Member

yorhodes commented Mar 13, 2024

nope, lets add beneficiary to the claim message

And require msg.sender to equal the processor? A little unfortunate that we wouldn't allow claiming from any key (like we do rn), but this isn't so bad

If we really wanted to, we could have a mapping of processor => claimer keys

ofc you can implement batching as an optimization

How would the batching work? I guess I'm hoping for something where you can claim for like hundreds or thousands of messages at a time

honestly I think this is a premature optimization but the simplest would be a claim(bytes32[] ids) that sends many claims in a single message to amortize the hyperlane message overhead

eventually this could be some ZK or fancy cryptographic accumulator but I definitely wouldn't start with that
the benefit of hooks is we can continue iterating on this

@tkporter
Copy link
Collaborator

Idk doesn't large batching feel like a requirement for costs to make sense?

Imagine e.g. Ethereum -> inEVM messages, where (made up numbers) an Ethereum tx costs $30 and an inEVM tx costs $0.50. The message back to claim would be prohibitively expensive - claiming only makes sense if we have hundreds or thousands of processed txs to claim for. Or would we only want to offer this for cheap chains?

Also on the origin side, is the idea we keep a mapping of id => payment so that when the claim is processed, it reads that from storage?

@yorhodes
Copy link
Member

yorhodes commented Mar 13, 2024

Imagine e.g. Ethereum -> inEVM messages, where (made up numbers) an Ethereum tx costs $30 and an inEVM tx costs $0.50. The message back to claim would be prohibitively expensive - claiming only makes sense if we have hundreds or thousands of processed txs to claim for. Or would we only want to offer this for cheap chains?

Unless the message itself is high value, the originating Ethereum tx would also be prohibitively expensive
For non Ethereum origins, and especially in the long tail of abundant blockspace, I think this concern is less relevant and I would not optimize for Ethereum as an origin

Also on the origin side, is the idea we keep a mapping of id => payment so that when the claim is processed, it reads that from storage?

That's the simplest implementation yeah

@tkporter
Copy link
Collaborator

I think it's fair to not design for Ethereum as an origin. If this is something meant to replace our current IGP setup I'd like to see us do some cost estimates before going down a path without batching

In the non-batching case, even if the origin and destination chain have a $1 tx fee, you'd need to charge a $2 IGP fee on the origin for the relayer to not lose any money

@yorhodes
Copy link
Member

we could do some sort of multi-merkle proof thing against the mailbox storage root

@nambrot
Copy link
Contributor Author

nambrot commented Aug 24, 2024

+1 that batching can be done at the Hyperlane message processing level

@yorhodes yorhodes moved this to Backlog in Hyperlane Tasks Aug 28, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: Backlog
Development

No branches or pull requests

5 participants