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.
and fromChainId was appended to the validation data
interface CrossChainExecutor {
bytes calldata = abi.encode(Call.data, nonce, sender, fromChainId); // Can also use abi.encodePacked
}
it would be compatible with many-to-many chain message bridges. And weâd love to adopt it and could have a second reference implementation for you really soon!
I wasnât following this comment though so maybe Iâm missing something.
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.
Contracts receiving messages can verify the source chain id in our system and that data is transported the same way the original sender address is.