diff --git a/contracts/crosschain/README.adoc b/contracts/crosschain/README.adoc new file mode 100644 index 0000000..1a5707f --- /dev/null +++ b/contracts/crosschain/README.adoc @@ -0,0 +1,42 @@ += Crosschain + +[.readme-notice] +NOTE: This document is better viewed at https://docs.openzeppelin.com/community-contracts/api/crosschain + +Gateways are contracts that enable cross-chain communication. These can either be a message source or a destination according to ERC-7786, which defines the following interfaces: + + * {IERC7786GatewaySource}: A contract interface to send a message to another contract on a different chain. + * {IERC7786Receiver}: An interface that allows an smart contract to receive a crosschain message provided by a trusted destination gateway. + +The library provides an implementation of an ERC-7786 receiver contract as a building block: + + * {ERC7786Receiver}: ERC-7786 cross-chain message receiver. + +Given ERC-7786 could be enabled natively or via adapters. Developers can access interoperability protocols through gateway adapters. The library includes the following gateway adapters: + + * {AxelarGatewayBase}: Core gateway logic for the https://www.axelar.network/[Axelar] adapter. + * {AxelarGatewaySource}: ERC-7786 source gateway adapter (sending side) for Axelar. + * {AxelarGatewayDestination}: ERC-7786 destination gateway adapter (receiving side) for Axelar. + * {AxelarGatewayDuplex}: ERC-7786 gateway adapter that operates in both directions (i.e. send and receive messages) using the Axelar network. + +== Gateways + +{{IERC7786GatewaySource}} + +== Clients + +{{IERC7786Receiver}} + +{{ERC7786Receiver}} + +== Adapters + +=== Axelar + +{{AxelarGatewayBase}} + +{{AxelarGatewaySource}} + +{{AxelarGatewayDestination}} + +{{AxelarGatewayDuplex}} diff --git a/contracts/crosschain/axelar/AxelarGatewayDestination.sol b/contracts/crosschain/axelar/AxelarGatewayDestination.sol index 96bd841..f21c548 100644 --- a/contracts/crosschain/axelar/AxelarGatewayDestination.sol +++ b/contracts/crosschain/axelar/AxelarGatewayDestination.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.27; import {AxelarExecutable} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/executable/AxelarExecutable.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; -import {IERC7786Receiver} from "../../interfaces/IERC7786.sol"; +import {IERC7786Receiver} from "../utils/IERC7786.sol"; import {AxelarGatewayBase} from "./AxelarGatewayBase.sol"; /** diff --git a/contracts/crosschain/utils/ERC7786Receiver.sol b/contracts/crosschain/utils/ERC7786Receiver.sol index a889bf0..ba8ea40 100644 --- a/contracts/crosschain/utils/ERC7786Receiver.sol +++ b/contracts/crosschain/utils/ERC7786Receiver.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.27; -import {IERC7786Receiver} from "../../interfaces/IERC7786.sol"; +import {IERC7786Receiver} from "./IERC7786.sol"; /** * @dev Base implementation of an ERC-7786 compliant cross-chain message receiver. @@ -33,7 +33,7 @@ abstract contract ERC7786Receiver is IERC7786Receiver { } /// @dev Virtual getter that returns whether an address is a valid ERC-7786 gateway. - function _isKnownGateway(address instance) internal view virtual returns (bool); + function _isKnownGateway(address instance) internal virtual returns (bool); /// @dev Virtual function that should contain the logic to execute when a cross-chain message is received. function _processMessage( diff --git a/contracts/crosschain/utils/IERC7786.sol b/contracts/crosschain/utils/IERC7786.sol new file mode 100644 index 0000000..3195cde --- /dev/null +++ b/contracts/crosschain/utils/IERC7786.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/** + * @dev Interface for ERC-7786 source gateways. + * + * See ERC-7786 for more details + */ +interface IERC7786GatewaySource { + /** + * @dev Event emitted when a message is created. If `outboxId` is zero, no further processing is necessary. If + * `outboxId` is not zero, then further (gateway specific, and non-standardized) action is required. + */ + event MessagePosted( + bytes32 indexed outboxId, + string sender, // CAIP-10 account identifier (chain identifier + ":" + account address) + string receiver, // CAIP-10 account identifier (chain identifier + ":" + account address) + bytes payload, + bytes[] attributes + ); + + /// @dev This error is thrown when a message creation fails because of an unsupported attribute being specified. + error UnsupportedAttribute(bytes4 selector); + + /// @dev Getter to check whether an attribute is supported or not. + function supportsAttribute(bytes4 selector) external view returns (bool); + + /** + * @dev Endpoint for creating a new message. If the message requires further (gateway specific) processing before + * it can be sent to the destination chain, then a non-zero `outboxId` must be returned. Otherwise, the + * message MUST be sent and this function must return 0. + * @param destinationChain {CAIP2} chain identifier + * @param receiver {CAIP10} account address (does not include the chain identifier) + * + * * MUST emit a {MessagePosted} event. + * + * If any of the `attributes` is not supported, this function SHOULD revert with an {UnsupportedAttribute} error. + * Other errors SHOULD revert with errors not specified in ERC-7786. + */ + function sendMessage( + string calldata destinationChain, + string calldata receiver, + bytes calldata payload, + bytes[] calldata attributes + ) external payable returns (bytes32 outboxId); +} + +/** + * @dev Interface for the ERC-7786 client contract (receiver). + * + * See ERC-7786 for more details + */ +interface IERC7786Receiver { + /** + * @dev Endpoint for receiving cross-chain message. + * @param sourceChain {CAIP2} chain identifier + * @param sender {CAIP10} account address (does not include the chain identifier) + * + * This function may be called directly by the gateway. + */ + function executeMessage( + string calldata sourceChain, // CAIP-2 chain identifier + string calldata sender, // CAIP-10 account address (does not include the chain identifier) + bytes calldata payload, + bytes[] calldata attributes + ) external payable returns (bytes4); +} diff --git a/contracts/interfaces/IERC7786.sol b/contracts/interfaces/IERC7786.sol index 3195cde..06112fe 100644 --- a/contracts/interfaces/IERC7786.sol +++ b/contracts/interfaces/IERC7786.sol @@ -2,67 +2,4 @@ pragma solidity ^0.8.0; -/** - * @dev Interface for ERC-7786 source gateways. - * - * See ERC-7786 for more details - */ -interface IERC7786GatewaySource { - /** - * @dev Event emitted when a message is created. If `outboxId` is zero, no further processing is necessary. If - * `outboxId` is not zero, then further (gateway specific, and non-standardized) action is required. - */ - event MessagePosted( - bytes32 indexed outboxId, - string sender, // CAIP-10 account identifier (chain identifier + ":" + account address) - string receiver, // CAIP-10 account identifier (chain identifier + ":" + account address) - bytes payload, - bytes[] attributes - ); - - /// @dev This error is thrown when a message creation fails because of an unsupported attribute being specified. - error UnsupportedAttribute(bytes4 selector); - - /// @dev Getter to check whether an attribute is supported or not. - function supportsAttribute(bytes4 selector) external view returns (bool); - - /** - * @dev Endpoint for creating a new message. If the message requires further (gateway specific) processing before - * it can be sent to the destination chain, then a non-zero `outboxId` must be returned. Otherwise, the - * message MUST be sent and this function must return 0. - * @param destinationChain {CAIP2} chain identifier - * @param receiver {CAIP10} account address (does not include the chain identifier) - * - * * MUST emit a {MessagePosted} event. - * - * If any of the `attributes` is not supported, this function SHOULD revert with an {UnsupportedAttribute} error. - * Other errors SHOULD revert with errors not specified in ERC-7786. - */ - function sendMessage( - string calldata destinationChain, - string calldata receiver, - bytes calldata payload, - bytes[] calldata attributes - ) external payable returns (bytes32 outboxId); -} - -/** - * @dev Interface for the ERC-7786 client contract (receiver). - * - * See ERC-7786 for more details - */ -interface IERC7786Receiver { - /** - * @dev Endpoint for receiving cross-chain message. - * @param sourceChain {CAIP2} chain identifier - * @param sender {CAIP10} account address (does not include the chain identifier) - * - * This function may be called directly by the gateway. - */ - function executeMessage( - string calldata sourceChain, // CAIP-2 chain identifier - string calldata sender, // CAIP-10 account address (does not include the chain identifier) - bytes calldata payload, - bytes[] calldata attributes - ) external payable returns (bytes4); -} +import {IERC7786GatewaySource, IERC7786Receiver} from "../crosschain/utils/IERC7786.sol"; diff --git a/contracts/mocks/docs/crosschain/MyCustomAxelarGatewayDestination.sol b/contracts/mocks/docs/crosschain/MyCustomAxelarGatewayDestination.sol new file mode 100644 index 0000000..c972440 --- /dev/null +++ b/contracts/mocks/docs/crosschain/MyCustomAxelarGatewayDestination.sol @@ -0,0 +1,11 @@ +// contracts/MyCustomAxelarGatewayDestination.sol +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {AxelarGatewayDestination, AxelarExecutable} from "../../../crosschain/axelar/AxelarGatewayDestination.sol"; +import {IAxelarGateway} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol"; + +abstract contract MyCustomAxelarGatewayDestination is AxelarGatewayDestination { + /// @dev Initializes the contract with the Axelar gateway and the initial owner. + constructor(IAxelarGateway gateway, address initialOwner) AxelarExecutable(address(gateway)) {} +} diff --git a/contracts/mocks/docs/crosschain/MyCustomAxelarGatewayDuplex.sol b/contracts/mocks/docs/crosschain/MyCustomAxelarGatewayDuplex.sol new file mode 100644 index 0000000..fa2afe6 --- /dev/null +++ b/contracts/mocks/docs/crosschain/MyCustomAxelarGatewayDuplex.sol @@ -0,0 +1,11 @@ +// contracts/MyCustomAxelarGatewayDuplex.sol +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {AxelarGatewayDuplex, AxelarExecutable} from "../../../crosschain/axelar/AxelarGatewayDuplex.sol"; +import {IAxelarGateway} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol"; + +abstract contract MyCustomAxelarGatewayDuplex is AxelarGatewayDuplex { + /// @dev Initializes the contract with the Axelar gateway and the initial owner. + constructor(IAxelarGateway gateway, address initialOwner) AxelarGatewayDuplex(gateway, initialOwner) {} +} diff --git a/contracts/mocks/docs/crosschain/MyCustomAxelarGatewaySource.sol b/contracts/mocks/docs/crosschain/MyCustomAxelarGatewaySource.sol new file mode 100644 index 0000000..dbeb5ad --- /dev/null +++ b/contracts/mocks/docs/crosschain/MyCustomAxelarGatewaySource.sol @@ -0,0 +1,13 @@ +// contracts/MyERC7786ReceiverContract.sol +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {AxelarGatewaySource} from "../../../crosschain/axelar/AxelarGatewaySource.sol"; +import {AxelarGatewayBase} from "../../../crosschain/axelar/AxelarGatewayBase.sol"; +import {IAxelarGateway} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol"; + +abstract contract MyCustomAxelarGatewaySource is AxelarGatewaySource { + /// @dev Initializes the contract with the Axelar gateway and the initial owner. + constructor(IAxelarGateway gateway, address initialOwner) Ownable(initialOwner) AxelarGatewayBase(gateway) {} +} diff --git a/contracts/mocks/docs/crosschain/MyERC7786GatewaySource.sol b/contracts/mocks/docs/crosschain/MyERC7786GatewaySource.sol new file mode 100644 index 0000000..767ba75 --- /dev/null +++ b/contracts/mocks/docs/crosschain/MyERC7786GatewaySource.sol @@ -0,0 +1,47 @@ +// contracts/MyERC7786GatewaySource.sol +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {CAIP2} from "@openzeppelin/contracts/utils/CAIP2.sol"; +import {CAIP10} from "@openzeppelin/contracts/utils/CAIP10.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {IERC7786GatewaySource} from "../../../interfaces/IERC7786.sol"; + +abstract contract MyERC7786GatewaySource is IERC7786GatewaySource { + using Strings for address; + + error UnsupportedNativeTransfer(); + + /// @inheritdoc IERC7786GatewaySource + function supportsAttribute(bytes4 /*selector*/) public pure returns (bool) { + return false; + } + + /// @inheritdoc IERC7786GatewaySource + function sendMessage( + string calldata destinationChain, // CAIP-2 chain identifier + string calldata receiver, // CAIP-10 account address (does not include the chain identifier) + bytes calldata payload, + bytes[] calldata attributes + ) external payable returns (bytes32 outboxId) { + require(msg.value == 0, UnsupportedNativeTransfer()); + // Use of `if () revert` syntax to avoid accessing attributes[0] if it's empty + if (attributes.length > 0) + revert UnsupportedAttribute(attributes[0].length < 0x04 ? bytes4(0) : bytes4(attributes[0][0:4])); + + // Emit event + outboxId = bytes32(0); // Explicitly set to 0. Can be used for post-processing + emit MessagePosted( + outboxId, + CAIP10.format(CAIP2.local(), msg.sender.toChecksumHexString()), + CAIP10.format(destinationChain, receiver), + payload, + attributes + ); + + // Optionally: If this is an adapter, send the message to a protocol gateway for processing + // This may require the logic for tracking destination gateway addresses and chain identifiers + + return outboxId; + } +} diff --git a/contracts/mocks/docs/crosschain/MyERC7786ReceiverContract.sol b/contracts/mocks/docs/crosschain/MyERC7786ReceiverContract.sol new file mode 100644 index 0000000..70adcb4 --- /dev/null +++ b/contracts/mocks/docs/crosschain/MyERC7786ReceiverContract.sol @@ -0,0 +1,29 @@ +// contracts/MyERC7786ReceiverContract.sol +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {AccessManaged} from "@openzeppelin/contracts/access/manager/AccessManaged.sol"; +import {ERC7786Receiver} from "../../../crosschain/utils/ERC7786Receiver.sol"; + +contract MyERC7786ReceiverContract is ERC7786Receiver, AccessManaged { + constructor(address initialAuthority) AccessManaged(initialAuthority) {} + + /// @dev Check if the given instance is a known gateway. + function _isKnownGateway(address /* instance */) internal virtual override restricted returns (bool) { + // The restricted modifier ensures that this function is only called by a known authority. + return true; + } + + /// @dev Internal endpoint for receiving cross-chain message. + /// @param sourceChain {CAIP2} chain identifier + /// @param sender {CAIP10} account address (does not include the chain identifier) + function _processMessage( + address gateway, + string calldata sourceChain, + string calldata sender, + bytes calldata payload, + bytes[] calldata attributes + ) internal virtual override restricted { + // Process the message here + } +} diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index f46339c..daecb27 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -1,3 +1,4 @@ * xref:index.adoc[Overview] * xref:account-abstraction.adoc[Account Abstraction] +* xref:crosschain.adoc[Crosschain] * xref:utilities.adoc[Utilities] diff --git a/docs/modules/ROOT/pages/crosschain.adoc b/docs/modules/ROOT/pages/crosschain.adoc new file mode 100644 index 0000000..3b1a6be --- /dev/null +++ b/docs/modules/ROOT/pages/crosschain.adoc @@ -0,0 +1,85 @@ += Cross-chain messaging + +Developers building contracts may require cross-chain functionality. To accomplish it, multiple protocols have implemented their own ways to process operations across chains. + +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. + +This guide will teach you how to follow 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). + +== ERC-7786 gateway + +To address the lack of composability in a simple and unopinionated way, ERC-7786 proposes a standard to implement a gateway that relays 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. + +=== Message passing overview + +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. + +* On the **source chain**, the contract implements a standard xref:api:crosschain.adoc#IERC7786GatewaySource-sendMessage-string-string-bytes-bytes---[`sendMessage`] function and emits a xref:api:crosschain.adoc#IERC7786GatewaySource-MessagePosted-bytes32-string-string-bytes-bytes---[`MessagePosted`] event to signal that the message should be relayed by the underlying protocol. + +* On the **destination chain**, the gateway that receives the message and passes it to the contract receiver by calling the xref:api:crosschain.adoc#IERC7786Receiver-executeMessage-string-string-bytes-bytes---[`executeMessage`] function. + +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. + +=== Getting started with Axelar Network + +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 a receive cross-chain messages from other applications or users leveraging automated execution by Axelar relayers on the destination chain. + +[source,solidity] +---- +include::api:example$crosschain/MyCustomAxelarGatewayDuplex.sol[] +---- + +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 + +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 + +== Cross-chain communication + +=== Sending a message + +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. + +[source,solidity] +---- +include::api:example$crosschain/MyERC7786GatewaySource.sol[] +---- + +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. + +=== Receiving a message + +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. + +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. + +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. + +[source,solidity] +---- +include::api:example$crosschain/MyERC7786ReceiverContract.sol[] +---- + +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. + +=== Axelar Network + +Aside from the xref:api:crosschain.adoc#AxelarGatewayDuple[AxelarGatewayDuple], 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 + +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: + +* Keep track of equivalences between Axelar chain names and CAIP-2 identifiers +* Record a destination gateway per network using their CAIP-2 identifier + +The xref:api:crosschain.adoc#AxelarGatewaySource[AxelarGatewaySource] implementation can be used out of the box + +[source,solidity] +---- +include::api:example$crosschain/MyCustomAxelarGatewaySource.sol[] +---- + +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]. + +[source,solidity] +---- +include::api:example$crosschain/MyCustomAxelarGatewayDestination.sol[] +----