I like to of it as a one way street; when you combine two it becomes a two-way street. Although a catapult would be more fun to watch!
I agree with you in that I’d like the user space “receiver” to be as simple as possible. The trick is that we want the receiver to recognize both the bridge contract (to authorize the transport layer) and the caller on the other side (to authorize the action). Passing the caller explicitly seems like the simplest 80 IQ approach…but what else are you thinking? Perhaps the GSN-style extra calldata bytes for the sender?
You know what is interesting- the CrossChainRelayer (receiver) in your diagram is basically an implementation of the CrossChainReceiver. I quite like this as it specifically defines how the Call[] batch is handled, instead of leaving it up to a user-space implementation.
Quick update for everyone else: Pierrick from PoolTogether is starting work on ERC-5164 implementations for the native Optimism, Arbitrum, and Polygon message bridges. We’re going to collaborate with @Amxx and use his prototype work. The implementations will help us refine the spec, then when we’re ready we’ll finalize the ERC and audit the bridges. Then we’ll have standardized bridges that everyone can use!
Hmmm… the trouble with the foreign chain id is that it can’t be enforced or verified; it’s just a value set by the creator of the CCReceiver contract. I think it makes more sense to use the address of the CCReceiver itself to authenticate the transport layer, because then the user contract can verify the transport layer using the basic msg.sender == receiver. This also aligns better with Nomad- because Replicas don’t actually track the sending chain id.
For the foreignSender; it feels like that would be gas-intensive wouldn’t it? It would need to do a 5k SSTORE to track the sender across calls, then each call would need do to an external call to the bridge to check (1k+).
I agree that the transport layer identification should be done using the CCReceiver address. The foreignChain() would be metadata, that helps identifying, without being trustworthy by default (like name or symbol for ERC20).
In my example above the bridges are whitelisted, so I assume checking the foreignChainId they return would be part of the whitelisting process.
And we have been working on several implementations using native bridges for Arbitrum, Optimism and Polygon. This work is available in this repository:
All this work helped us test our interfaces with various bridges, which outlined the short comings of the EIP. Bridges process messages in various ways and we have realized that our EIP should only define how messages are being relayed to other chains and traced. Execution and authentication of these messages is left to the developers to implement.
Transaction ordering is not specified but developers can use the nonce if they wish to enforce it in their application.
The ERC-5164 repository has been updated with the latest changes made to the interface.
A how to section has also been added to the README: GitHub - pooltogether/ERC5164
It should make it easier for people to use the various bridges and also contribute.
We are awaiting your feedback before the EIP enters the Last Call stage.
First of all, really excited to see this all coming along! I know I’m jumping in late to the conversation here but Hop is going to release a cross-chain messenger soon so this is top of mind for us.
I generally agree that a standard as @Amxx suggested is very much needed even if this EIP doesn’t go that direction. Both Optimism and Arbitrum built their bridges with this pattern from the beginning and we’re using this pattern for our messenger as well. I also really like your idea @Brendan to use the hidden call argument approach instead of the callback approach currently used. Either way, a standard way to validate crossChainSender and crossChainChainId after receiving a call would be very useful.
But if many projects all intended to use the same CrossChainReceiver, why should receiveCalls be standardized? I would think projects consuming this standard only care about how to validate the crossChainSender and crossChainChainId when a call is received from the CrossChainReceiver and not implementation details of how CrossChainReceiver receives the calls.
There are two goals we’d really like to see a cross-chain messaging standard achieve:
Contracts could really use a standard way to validate crossChainSender and crossChainChainId after receiving a call from a messenger. Currently, Optimism and Arbitrum have slightly varied interfaces and don’t account for crossChainChainId since they’re only dealing with a single chain-to-chain connection. This could be done the way @Amxx suggested or with the hidden call argument approach.
Off-chain messaging explorers could use a standard way to track cross-chain messages being sent and completed. This just requires standard events for message sending, relaying, and reverts.
Something like this seems relatively unopinionated and achieves both goals:
This is basically what’s the in spec. With a few notable differences:
The 5164 includes a gasLimit, but early reports from auditors suggests that it’s not needed. This may be taken out.
5164 batches messages instead of sending them individually
5164 does not catch reverts; they simply revert as normal. This was in case they needed to be retried, or what have you
Someone that you’ve added, which is interesting, is the targetChainId on the Relayer. We avoided adding this because it’s kind of out-of-band information, in the sense that it would be just arbitrarily set by the Relayer contract and not really guarantee anything. Additionally, indexers or whoever is watching the relayer would have to know what the corresponding Executor is anyway on the receiving chain. It already needs to know information that isn’t available on-chain.
PS: I did wonder whether batching was absolutely necessary for the EIP.
Ok after catching up the latest, here are some thoughts:
It’s great that validation using the hidden call parameter approach is defined!
interface CrossChainExecutor {
bytes calldata = abi.encode(Call.data, nonce, sender); // Can also use abi.encodePacked
}
What’s the reason for including nonce here if replay protection is handled by the ICrossChainReceiver though?
Would it make sense to rename nonce to messageId or something like that and use bytes32? I think there’s some expectation for nonces to be sequential like they are on Ethereum even though it’s not part of the definition of a nonce. We use a hash as the message unique identifier so it would just be a giant random uint256 which works but isn’t ideal.
I also like that a gaslimit is not included for individual messages in the batch in . Is it needed at all though?
I really like the batching! Any thoughts on including a relayCall option as well? I can get some more data on it but I think the gas savings could be non-trivial and it’s two less words of calldata.
On the Relayer side, there is a definite order to the calls. However, we don’t require strict ordering on the Executor side because that would open the ability to grief for some bridges (Arbitrum, in particular).
Instead, the Executor just uses the nonce to prevent replay attacks. However, we make the ordering available to the Receiver so that it at least has the ability to detect out-of-order messages.
Because of the above rationale, no.
Yes! It might get pulled because the auditors didn’t like it either
And then lastly, the standard is fairly opinionated that CrossChainRelayerCrossChainExecutor pairs should be between just two chains.
We’ll support 5 chains so I think we’d need to deploy 5 + 4 + 3 + 2 + 1 = 15 different CrossChainRelayerCrossChainExecutor pairs and it could get out of hand as we support more chains. How should multi-chain message bridges think about supporting this? Would it make sense to have two separate standards, one that’s one-to-one and one that’s many-to-many?
That makes a lot of sense. Our issue is more related to being a many-to-many chain message bridge. For one-to-one message bridges a sequential nonce is enough to have a unique id for the message. But for many-to-many, you’d have collisions across the different sources. That’s why we use a hash instead.