Skip to content

Commit ddaed9c

Browse files
luiz-lvjernestognw
andauthored
Add documentation for crosschain message passing (#138)
Co-authored-by: Ernesto García <[email protected]>
1 parent d8e9d13 commit ddaed9c

File tree

7 files changed

+288
-0
lines changed

7 files changed

+288
-0
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// contracts/MyCustomAxelarGatewayDestination.sol
2+
// SPDX-License-Identifier: MIT
3+
pragma solidity ^0.8.27;
4+
5+
import {AxelarGatewayDestination, AxelarExecutable} from "../../../crosschain/axelar/AxelarGatewayDestination.sol";
6+
import {IAxelarGateway} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol";
7+
8+
abstract contract MyCustomAxelarGatewayDestination is AxelarGatewayDestination {
9+
/// @dev Initializes the contract with the Axelar gateway and the initial owner.
10+
constructor(IAxelarGateway gateway, address initialOwner) AxelarExecutable(address(gateway)) {}
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// contracts/MyCustomAxelarGatewayDuplex.sol
2+
// SPDX-License-Identifier: MIT
3+
pragma solidity ^0.8.27;
4+
5+
import {AxelarGatewayDuplex, AxelarExecutable} from "../../../crosschain/axelar/AxelarGatewayDuplex.sol";
6+
import {IAxelarGateway} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol";
7+
8+
abstract contract MyCustomAxelarGatewayDuplex is AxelarGatewayDuplex {
9+
/// @dev Initializes the contract with the Axelar gateway and the initial owner.
10+
constructor(IAxelarGateway gateway, address initialOwner) AxelarGatewayDuplex(gateway, initialOwner) {}
11+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// contracts/MyERC7786ReceiverContract.sol
2+
// SPDX-License-Identifier: MIT
3+
pragma solidity ^0.8.27;
4+
5+
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
6+
import {AxelarGatewaySource} from "../../../crosschain/axelar/AxelarGatewaySource.sol";
7+
import {AxelarGatewayBase} from "../../../crosschain/axelar/AxelarGatewayBase.sol";
8+
import {IAxelarGateway} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol";
9+
10+
abstract contract MyCustomAxelarGatewaySource is AxelarGatewaySource {
11+
/// @dev Initializes the contract with the Axelar gateway and the initial owner.
12+
constructor(IAxelarGateway gateway, address initialOwner) Ownable(initialOwner) AxelarGatewayBase(gateway) {}
13+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// contracts/MyERC7786GatewaySource.sol
2+
// SPDX-License-Identifier: MIT
3+
pragma solidity ^0.8.24;
4+
5+
import {CAIP2} from "@openzeppelin/contracts/utils/CAIP2.sol";
6+
import {CAIP10} from "@openzeppelin/contracts/utils/CAIP10.sol";
7+
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
8+
import {IERC7786GatewaySource} from "../../../interfaces/IERC7786.sol";
9+
10+
abstract contract MyERC7786GatewaySource is IERC7786GatewaySource {
11+
using Strings for address;
12+
13+
error UnsupportedNativeTransfer();
14+
15+
/// @inheritdoc IERC7786GatewaySource
16+
function supportsAttribute(bytes4 /*selector*/) public pure returns (bool) {
17+
return false;
18+
}
19+
20+
/// @inheritdoc IERC7786GatewaySource
21+
function sendMessage(
22+
string calldata destinationChain, // CAIP-2 chain identifier
23+
string calldata receiver, // CAIP-10 account address (does not include the chain identifier)
24+
bytes calldata payload,
25+
bytes[] calldata attributes
26+
) external payable returns (bytes32 outboxId) {
27+
require(msg.value == 0, UnsupportedNativeTransfer());
28+
// Use of `if () revert` syntax to avoid accessing attributes[0] if it's empty
29+
if (attributes.length > 0)
30+
revert UnsupportedAttribute(attributes[0].length < 0x04 ? bytes4(0) : bytes4(attributes[0][0:4]));
31+
32+
// Emit event
33+
outboxId = bytes32(0); // Explicitly set to 0. Can be used for post-processing
34+
emit MessagePosted(
35+
outboxId,
36+
CAIP10.format(CAIP2.local(), msg.sender.toChecksumHexString()),
37+
CAIP10.format(destinationChain, receiver),
38+
payload,
39+
attributes
40+
);
41+
42+
// Optionally: If this is an adapter, send the message to a protocol gateway for processing
43+
// This may require the logic for tracking destination gateway addresses and chain identifiers
44+
45+
return outboxId;
46+
}
47+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// contracts/MyERC7786ReceiverContract.sol
2+
// SPDX-License-Identifier: MIT
3+
pragma solidity ^0.8.27;
4+
5+
import {AccessManaged} from "@openzeppelin/contracts/access/manager/AccessManaged.sol";
6+
import {ERC7786Receiver} from "../../../crosschain/utils/ERC7786Receiver.sol";
7+
8+
contract MyERC7786ReceiverContract is ERC7786Receiver, AccessManaged {
9+
constructor(address initialAuthority) AccessManaged(initialAuthority) {}
10+
11+
/// @dev Check if the given instance is a known gateway.
12+
function _isKnownGateway(address /* instance */) internal view virtual override returns (bool) {
13+
return true;
14+
}
15+
16+
/// @dev Internal endpoint for receiving cross-chain message.
17+
/// @param sourceChain {CAIP2} chain identifier
18+
/// @param sender {CAIP10} account address (does not include the chain identifier)
19+
function _processMessage(
20+
address gateway,
21+
string calldata messageId,
22+
string calldata sourceChain,
23+
string calldata sender,
24+
bytes calldata payload,
25+
bytes[] calldata attributes
26+
) internal virtual override restricted {
27+
// Process the message here
28+
}
29+
}

docs/modules/ROOT/nav.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44
*** xref:eoa-delegation.adoc[EOA Delegation]
55
*** xref:multisig.adoc[Multisig]
66
** xref:paymasters.adoc[Paymasters]
7+
* xref:crosschain.adoc[Crosschain]
78
* xref:utilities.adoc[Utilities]
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
= Cross-chain messaging
2+
3+
Developers building contracts may require cross-chain functionality. To accomplish this, multiple protocols have implemented their own ways to process operations across chains.
4+
5+
The variety of these bridges is outlined in https://x.com/norswap[@norswap]'s https://github.com/0xFableOrg/xchain/blob/master/README.md[Cross-Chain Interoperability Report] that proposes https://github.com/0xFableOrg/xchain/blob/master/README.md#bridge-taxonomy[a taxonomy of 7 bridge categories]. This diversity makes it difficult for developers to design cross-chain applications given the lack of portability.
6+
7+
This guide will teach you how to follow https://eips.ethereum.org/EIPS/eip-7786[ERC-7786] to establish messaging gateways across chains regardless of the underlying bridge. Developers can implement gateway contracts that process cross-chain messages and connect any crosschain protocol they want (or implement themselves).
8+
9+
== ERC-7786 Gateway
10+
11+
To address the lack of composability in a simple and unopinionated way, ERC-7786 proposes a standard for implementing gateways that relay messages to other chains. This generalized approach is expressive enough to enable new types of applications and can be adapted to any bridge taxonomy or specific bridge interface with standardized attributes.
12+
13+
=== Message passing overview
14+
15+
The ERC defines a source and a destination gateway. Both are contracts that implement a protocol to send a message and process its reception respectively. These two processes are identified explicitly by the ERC-7786 specification since they define the minimal requirements for both gateways.
16+
17+
* On the **source chain**, the contract implements a standard xref:api:crosschain.adoc#AxelarGatewaySource-sendMessage-string-string-bytes-bytes---[`sendMessage`] function and emits a xref:api:crosschain.adoc#AxelarGatewaySource-MessagePosted-bytes32-string-string-bytes-bytes---[`MessagePosted`] event to signal that the message should be relayed by the underlying protocol.
18+
19+
* On the **destination chain**, the gateway receives the message and passes it to the receiver contract by calling the xref:api:crosschain.adoc#ERC7786Receiver-executeMessage-string-string-string-bytes-bytes---[`executeMessage`] function.
20+
21+
Smart contract developers only need to worry about implementing the xref:api:crosschain.adoc#IERC7786GatewaySource[IERC7786GatewaySource] interface to send a message on the source chain and the xref:api:crosschain.adoc#IERC7786GatewaySource[IERC7786GatewaySource] and xref:api:crosschain.adoc#IERC7786Receiver[IERC7786Receiver] interface to receive such message on the destination chain.
22+
23+
=== Getting started with Axelar Network
24+
25+
To start sending cross-chain messages, developers can get started with a duplex gateway powered by Axelar Network. This will allow a contract to send or receive cross-chain messages leveraging automated execution by Axelar relayers on the destination chain.
26+
27+
[source,solidity]
28+
----
29+
include::api:example$crosschain/MyCustomAxelarGatewayDuplex.sol[]
30+
----
31+
32+
For more details of how the duplex gateway works, see xref:crosschain.adoc#axelar_network[how to send and receive messages with the Axelar Network] below
33+
34+
NOTE: Developers can register supported chains and destination gateways using the xref:api:crosschain.adoc#AxelarGatewayBase-registerChainEquivalence-string-string-[`registerChainEquivalence`] and xref:api:crosschain.adoc#AxelarGatewayBase-registerRemoteGateway-string-string-[`registerRemoteGateway`] functions
35+
36+
== Cross-chain communication
37+
38+
=== Sending a message
39+
40+
The interface for a source gateway is general enough that it allows wrapping a custom protocol to authenticate messages. Depending on the use case, developers can implement any offchain mechanism to read the standard xref:api:crosschain.adoc#IERC7786GatewaySource-MessagePosted-bytes32-string-string-bytes-bytes---[`MessagePosted`] event and deliver it to the receiver on the destination chain.
41+
42+
[source,solidity]
43+
----
44+
include::api:example$crosschain/MyERC7786GatewaySource.sol[]
45+
----
46+
47+
NOTE: The standard represents chains using https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md[CAIP-2] identifiers and accounts using https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md[CAIP-10] identifiers for increased interoperability with non-EVM chains. Consider using the Strings library in the contracts library to process these identifiers.
48+
49+
=== Receiving a message
50+
51+
To successfully process a message on the destination chain, a destination gateway is required. Although ERC-7786 doesn't define a standard interface for the destination gateway, it requires that it calls the `executeMessage` upon message reception.
52+
53+
Every cross-chain message protocol already offers a way to receive the message either through a canonical bridge or an intermediate contract. Developers can easily wrap the receiving contract into a gateway that calls the `executeMessage` function as mandated by the ERC.
54+
55+
To receive a message on a custom smart contract, OpenZeppelin Community Contracts provide an xref:api:crosschain.adoc#ERC7786Receiver[ERC7786Receiver] implementation for developers to inherit. This way your contracts can receive a cross-chain message relayed through a known destination gateway gateway.
56+
57+
[source,solidity]
58+
----
59+
include::api:example$crosschain/MyERC7786ReceiverContract.sol[]
60+
----
61+
62+
The standard receiving interface abstracts away the underlying protocol. This way, it is possible for a contract to send a message through an ERC-7786 compliant gateway (or through an adapter) and get it received on the destination chain without worrying about the protocol implementation details.
63+
64+
=== Axelar Network
65+
66+
Aside from the xref:api:crosschain.adoc#AxelarGatewayDuplex[AxelarGatewayDuplex], the library offers an implementation of the xref:api:crosschain.adoc#IERC7786GatewaySource[IERC7786GatewaySource] interface called xref:api:crosschain.adoc#AxelarGatewaySource[AxelarGatewaySource] that works as an adapter for sending messages in compliance with ERC-7786
67+
68+
The implementation takes a local gateway address that MUST correspond to https://axelarscan.io/resources/chains?type=evm[Axelar's native gateways] and has mechanisms to:
69+
70+
* Keep track of equivalences between Axelar chain names and CAIP-2 identifiers
71+
* Record a destination gateway per network using their CAIP-2 identifier
72+
73+
The xref:api:crosschain.adoc#AxelarGatewaySource[AxelarGatewaySource] implementation can be used out of the box
74+
75+
[source,solidity]
76+
----
77+
include::api:example$crosschain/MyCustomAxelarGatewaySource.sol[]
78+
----
79+
80+
For a destination gateway, the library provides an adapter of the `AxelarExecutable` interface to receive messages and relay them to an xref:api:crosschain.adoc#IERC7786Receiver[IERC7786Receiver].
81+
82+
[source,solidity]
83+
----
84+
include::api:example$crosschain/MyCustomAxelarGatewayDestination.sol[]
85+
----
86+
87+
=== Multi Bridge Aggregator
88+
89+
The xref:api:crosschain.adoc#ERC7786Aggregator[ERC7786Aggregator] is a special gateway that implements both xref:api:crosschain.adoc#IERC7786GatewaySource[IERC7786GatewaySource] and xref:api:crosschain.adoc#IERC7786Receiver[IERC7786Receiver] interfaces. It provides a way to send messages across multiple bridges simultaneously and ensures message delivery through a threshold-based confirmation system.
90+
91+
The aggregator maintains a list of known gateways and a confirmation threshold. When sending a message, it broadcasts to all registered gateways, and when receiving, it requires a minimum number of confirmations before executing the message. This approach increases reliability by ensuring messages are properly delivered and validated across multiple bridges.
92+
93+
When sending a message, the aggregator tracks the message IDs from each gateway to maintain a record of the message's journey across different bridges:
94+
95+
```solidity
96+
function sendMessage(
97+
string calldata destinationChain,
98+
string memory receiver,
99+
bytes memory payload,
100+
bytes[] memory attributes
101+
) public payable virtual whenNotPaused returns (bytes32 outboxId) {
102+
103+
// ... Initialize variables and prepare payload ...
104+
105+
// Post on all gateways
106+
Outbox[] memory outbox = new Outbox[](_gateways.length());
107+
bool needsId = false;
108+
for (uint256 i = 0; i < outbox.length; ++i) {
109+
address gateway = _gateways.at(i);
110+
// send message
111+
bytes32 id = IERC7786GatewaySource(gateway).sendMessage(
112+
destinationChain,
113+
aggregator,
114+
wrappedPayload,
115+
attributes
116+
);
117+
// if ID, track it
118+
if (id != bytes32(0)) {
119+
outbox[i] = Outbox(gateway, id);
120+
needsId = true;
121+
}
122+
}
123+
124+
// ... Handle message tracking and return value ...
125+
}
126+
```
127+
128+
On the receiving end, the aggregator implements a threshold-based confirmation system. Messages are only executed after receiving enough confirmations from the gateways, ensuring message validity and preventing double execution. The xref:api:crosschain.adoc#ERC7786Aggregator-executeMessage-string-string-string-bytes-bytes---[`executeMessage`] function handles this process:
129+
130+
```solidity
131+
function executeMessage(
132+
string calldata /*messageId*/, // gateway specific, empty or unique
133+
string calldata sourceChain, // CAIP-2 chain identifier
134+
string calldata sender, // CAIP-10 account address (does not include the chain identifier)
135+
bytes calldata payload,
136+
bytes[] calldata attributes
137+
) public payable virtual whenNotPaused returns (bytes4) {
138+
139+
// ... Validate message format and extract message ID ...
140+
141+
// If call is first from a trusted gateway
142+
if (_gateways.contains(msg.sender) && !tracker.receivedBy[msg.sender]) {
143+
// Count number of time received
144+
tracker.receivedBy[msg.sender] = true;
145+
++tracker.countReceived;
146+
emit Received(id, msg.sender);
147+
148+
// if already executed, leave gracefully
149+
if (tracker.executed) return IERC7786Receiver.executeMessage.selector;
150+
} else if (tracker.executed) {
151+
revert ERC7786AggregatorAlreadyExecuted();
152+
}
153+
154+
// ... Validate sender and prepare payload for execution ...
155+
156+
// If ready to execute, and not yet executed
157+
if (tracker.countReceived >= getThreshold()) {
158+
// prevent re-entry
159+
tracker.executed = true;
160+
161+
// ... Prepare execution context and validate state ...
162+
bytes memory call = abi.encodeCall(
163+
IERC7786Receiver.executeMessage,
164+
(uint256(id).toHexString(32), sourceChain, originalSender, unwrappedPayload, attributes)
165+
);
166+
167+
(bool success, bytes memory returndata) = receiver.parseAddress().call(call);
168+
169+
// ... Handle the result ...
170+
}
171+
172+
return IERC7786Receiver.executeMessage.selector;
173+
}
174+
```
175+
176+
The aggregator is designed to be configurable. As an `Ownable` contract, it allows the owner to manage the list of trusted gateways and adjust the confirmation threshold. The `_gateways` list and threshold are initially set during contract deployment using the xref:api:crosschain.adoc#ERC7786Aggregator-_addGateway-address-[`++_addGateway++`] and xref:api:crosschain.adoc#ERC7786Aggregator-_setThreshold-uint8-[`++_setThreshold++`] functions. The owner can update these settings as needed to adapt to changing requirements or add new gateways.

0 commit comments

Comments
 (0)