Skip to content

Commit 3496da2

Browse files
ernestognwAmxxErnesto Garcíafrangio
authored
Cross-chain standardisation: ERC-7786 interfaces, helpers and Axelar adapter (#28)
* cross-chain prototype v1 * split common <> axelar * add relay observability * Update oz to master * Iterate * Remove salt * Iterate * Add GatewayAxelar specialization * Iterate * Fix GatewayAxelarSource * Remove unnecessary contract * Iteration * Remove interfaces * Checkpoint * Add incoming dual mode * Fix compilation * Apply review suggestions * Install axelar contracts * Apply review sugggestion * Apply suggestions * wip fixes * trying to get crosschain to compile * fix compilation * minor update * make attributes a bytes[] * Address comments and add some tests * refactor and test caip utils * up * using unmerged version of Strings with parsing * up * workflow testing (active and passive) * update * up * address PR comments * renovate * rename * fix foundry * codespell * use checksumed addresses * use @openzepplin/contracts@master * add/move files that are no longer planned to be in the main repo * get submodules when running tests * Apply suggestions from code review * Apply suggestions from code review Co-authored-by: Francisco Giordano <[email protected]> * simplify * update * validateReceivedMessage -> setExecutedMessage * Add docs * Update AxelarGatewayDestination.sol Co-authored-by: Francisco Giordano <[email protected]> * Update * Update * Pick CI changes * Run `forge update` * Sync prettier version with vanilla contracts * Point .gitmodules to master * fix imports * Implement audit recommendations for crosschain gateway implementations (#22) * Updated ERC specs * executeMessage returns bytes4 * Add AxelarGatewayDuplex * remove npm contracts dependency in favor of the master submodule * slither remappings * clarify CAIP-10 format * document reverts * unused imports * unexpected attributes too short * document remoteGateway format * clarify AxelarGatewayDestination._execute * rewrite require(..., error); as if (...) revert error; * typography * custom errors * replace require with string with custom error * Apply audit review suggestions * Adjust checks.yml * Point gitmodules to master on OZ contracts * lint * Fix CI * Fix CI 2 * Fix slither * up * up * Disable locked-ether slither rule for duplex * Make prettier version consistent * Remove passive mode * Apply suggestions from code review Co-authored-by: Ernesto García <[email protected]> * Update contracts/crosschain/axelar/AxelarGatewayDuplex.sol --------- Co-authored-by: Ernesto García <[email protected]> * cleanup --------- Co-authored-by: Hadrien Croubois <[email protected]> Co-authored-by: Ernesto García <[email protected]> Co-authored-by: Francisco Giordano <[email protected]>
1 parent e6609a9 commit 3496da2

24 files changed

+1872
-1835
lines changed

.github/workflows/checks.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,6 @@ jobs:
5151
runs-on: ubuntu-latest
5252
steps:
5353
- uses: actions/checkout@v4
54-
with:
55-
submodules: recursive
5654
- name: Set up environment
5755
uses: ./.github/actions/setup
5856
- name: Run tests

.gitmodules

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
[submodule "lib/@openzeppelin-contracts"]
22
path = lib/@openzeppelin-contracts
33
url = https://github.com/OpenZeppelin/openzeppelin-contracts.git
4+
branch = master
45
[submodule "lib/@openzeppelin-contracts-upgradeable"]
56
path = lib/@openzeppelin-contracts-upgradeable
67
url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable.git
8+
branch = master
79
[submodule "lib/forge-std"]
810
path = lib/forge-std
911
url = https://github.com/foundry-rs/forge-std.git
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.27;
4+
5+
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
6+
import {IAxelarGateway} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol";
7+
8+
/**
9+
* @dev Base implementation of a cross-chain gateway adapter for the Axelar Network.
10+
*
11+
* This contract allows developers to register equivalence between chains (i.e. CAIP-2 chain identifiers
12+
* to Axelar chain identifiers) and remote gateways (i.e. gateways on other chains) to
13+
* facilitate cross-chain communication.
14+
*/
15+
abstract contract AxelarGatewayBase is Ownable {
16+
/// @dev A remote gateway has been registered for a chain.
17+
event RegisteredRemoteGateway(string caip2, string gatewayAddress);
18+
19+
/// @dev A chain equivalence has been registered.
20+
event RegisteredChainEquivalence(string caip2, string destinationChain);
21+
22+
/// @dev Error emitted when an unsupported chain is queried.
23+
error UnsupportedChain(string caip2);
24+
25+
error ChainEquivalenceAlreadyRegistered(string caip2);
26+
error RemoteGatewayAlreadyRegistered(string caip2);
27+
28+
/// @dev Axelar's official gateway for the current chain.
29+
IAxelarGateway public immutable localGateway;
30+
31+
mapping(string caip2 => string remoteGateway) private _remoteGateways;
32+
mapping(string caip2OrAxelar => string axelarOrCaip2) private _chainEquivalence;
33+
34+
/// @dev Sets the local gateway address (i.e. Axelar's official gateway for the current chain).
35+
constructor(IAxelarGateway _gateway) {
36+
localGateway = _gateway;
37+
}
38+
39+
/// @dev Returns the equivalent chain given an id that can be either CAIP-2 or an Axelar network identifier.
40+
function getEquivalentChain(string memory input) public view virtual returns (string memory output) {
41+
output = _chainEquivalence[input];
42+
require(bytes(output).length > 0, UnsupportedChain(input));
43+
}
44+
45+
/// @dev Returns the address string of the remote gateway for a given CAIP-2 chain identifier.
46+
function getRemoteGateway(string memory caip2) public view virtual returns (string memory remoteGateway) {
47+
remoteGateway = _remoteGateways[caip2];
48+
require(bytes(remoteGateway).length > 0, UnsupportedChain(caip2));
49+
}
50+
51+
/// @dev Registers a chain equivalence between a CAIP-2 chain identifier and an Axelar network identifier.
52+
function registerChainEquivalence(string calldata caip2, string calldata axelarSupported) public virtual onlyOwner {
53+
require(bytes(_chainEquivalence[caip2]).length == 0, ChainEquivalenceAlreadyRegistered(caip2));
54+
_chainEquivalence[caip2] = axelarSupported;
55+
_chainEquivalence[axelarSupported] = caip2;
56+
emit RegisteredChainEquivalence(caip2, axelarSupported);
57+
}
58+
59+
/// @dev Registers the address string of the remote gateway for a given CAIP-2 chain identifier.
60+
function registerRemoteGateway(string calldata caip2, string calldata remoteGateway) public virtual onlyOwner {
61+
require(bytes(_remoteGateways[caip2]).length == 0, RemoteGatewayAlreadyRegistered(caip2));
62+
_remoteGateways[caip2] = remoteGateway;
63+
emit RegisteredRemoteGateway(caip2, remoteGateway);
64+
}
65+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.27;
4+
5+
import {AxelarExecutable} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/executable/AxelarExecutable.sol";
6+
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
7+
import {IERC7786Receiver} from "../interfaces/draft-IERC7786.sol";
8+
import {AxelarGatewayBase} from "./AxelarGatewayBase.sol";
9+
10+
/**
11+
* @dev Implementation of an ERC-7786 gateway destination adapter for the Axelar Network in dual mode.
12+
*
13+
* The contract implements AxelarExecutable's {_execute} function to execute the message, converting Axelar's native
14+
* workflow into the standard ERC-7786.
15+
*/
16+
abstract contract AxelarGatewayDestination is AxelarGatewayBase, AxelarExecutable {
17+
using Strings for address;
18+
using Strings for string;
19+
20+
error InvalidOriginGateway(string sourceChain, string axelarSourceAddress);
21+
error ReceiverExecutionFailed();
22+
23+
/**
24+
* @dev Active mode execution of a cross-chain message.
25+
*
26+
* In this function:
27+
*
28+
* - `axelarSourceChain` is in the Axelar format. It should not be expected to be a proper CAIP-2 format
29+
* - `axelarSourceAddress` is the sender of the Axelar message. That should be the remote gateway on the chain
30+
* which the message originates from. It is NOT the sender of the ERC-7786 crosschain message.
31+
*
32+
* Proper CAIP-10 encoding of the message sender (including the CAIP-2 name of the origin chain can be found in
33+
* the message)
34+
*/
35+
function _execute(
36+
string calldata axelarSourceChain, // chain of the remote gateway - axelar format
37+
string calldata axelarSourceAddress, // address of the remote gateway
38+
bytes calldata adapterPayload
39+
) internal override {
40+
// Parse the package
41+
(string memory sender, string memory receiver, bytes memory payload, bytes[] memory attributes) = abi.decode(
42+
adapterPayload,
43+
(string, string, bytes, bytes[])
44+
);
45+
// Axelar to CAIP-2 translation
46+
string memory sourceChain = getEquivalentChain(axelarSourceChain);
47+
48+
// check message validity
49+
// - `axelarSourceAddress` is the remote gateway on the origin chain.
50+
require(
51+
getRemoteGateway(sourceChain).equal(axelarSourceAddress),
52+
InvalidOriginGateway(sourceChain, axelarSourceAddress)
53+
);
54+
55+
// Active mode
56+
bytes4 result = IERC7786Receiver(receiver.parseAddress()).executeMessage(
57+
sourceChain,
58+
sender,
59+
payload,
60+
attributes
61+
);
62+
require(result == IERC7786Receiver.executeMessage.selector, ReceiverExecutionFailed());
63+
}
64+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.27;
4+
5+
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
6+
import {AxelarGatewayBase, IAxelarGateway} from "./AxelarGatewayBase.sol";
7+
import {AxelarGatewayDestination, AxelarExecutable} from "./AxelarGatewayDestination.sol";
8+
import {AxelarGatewaySource} from "./AxelarGatewaySource.sol";
9+
10+
/**
11+
* @dev A contract that combines the functionality of both the source and destination gateway
12+
* adapters for the Axelar Network. Allowing to either send or receive messages across chains.
13+
*/
14+
// slither-disable-next-line locked-ether
15+
contract AxelarGatewayDuplex is AxelarGatewaySource, AxelarGatewayDestination {
16+
/// @dev Initializes the contract with the Axelar gateway and the initial owner.
17+
constructor(
18+
IAxelarGateway gateway,
19+
address initialOwner
20+
) Ownable(initialOwner) AxelarGatewayBase(gateway) AxelarExecutable(address(gateway)) {}
21+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.27;
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 {AxelarGatewayBase} from "./AxelarGatewayBase.sol";
9+
import {IERC7786GatewaySource} from "../interfaces/draft-IERC7786.sol";
10+
11+
/**
12+
* @dev Implementation of an ERC-7786 gateway source adapter for the Axelar Network.
13+
*
14+
* The contract provides a way to send messages to a remote chain via the Axelar Network
15+
* using the {sendMessage} function.
16+
*/
17+
abstract contract AxelarGatewaySource is IERC7786GatewaySource, AxelarGatewayBase {
18+
using Strings for address;
19+
20+
error UnsupportedNativeTransfer();
21+
22+
/// @inheritdoc IERC7786GatewaySource
23+
function supportsAttribute(bytes4 /*selector*/) public pure returns (bool) {
24+
return false;
25+
}
26+
27+
/// @inheritdoc IERC7786GatewaySource
28+
function sendMessage(
29+
string calldata destinationChain, // CAIP-2 chain identifier
30+
string calldata receiver, // CAIP-10 account address (does not include the chain identifier)
31+
bytes calldata payload,
32+
bytes[] calldata attributes
33+
) external payable returns (bytes32 outboxId) {
34+
require(msg.value == 0, UnsupportedNativeTransfer());
35+
// Use of `if () revert` syntax to avoid accessing attributes[0] if it's empty
36+
if (attributes.length > 0)
37+
revert UnsupportedAttribute(attributes[0].length < 0x04 ? bytes4(0) : bytes4(attributes[0][0:4]));
38+
39+
// Create the package
40+
string memory sender = msg.sender.toChecksumHexString();
41+
bytes memory adapterPayload = abi.encode(sender, receiver, payload, attributes);
42+
43+
// Emit event
44+
outboxId = bytes32(0); // Explicitly set to 0
45+
emit MessagePosted(
46+
outboxId,
47+
CAIP10.format(CAIP2.local(), sender),
48+
CAIP10.format(destinationChain, receiver),
49+
payload,
50+
attributes
51+
);
52+
53+
// Send the message
54+
string memory axelarDestination = getEquivalentChain(destinationChain);
55+
string memory remoteGateway = getRemoteGateway(destinationChain);
56+
localGateway.callContract(axelarDestination, remoteGateway, adapterPayload);
57+
58+
return outboxId;
59+
}
60+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.27;
4+
5+
import {IAxelarGateway} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol";
6+
import {IAxelarExecutable} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarExecutable.sol";
7+
import {BitMaps} from "@openzeppelin/contracts/utils/structs/BitMaps.sol";
8+
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
9+
10+
contract AxelarGatewayMock {
11+
using Strings for address;
12+
using Strings for string;
13+
using BitMaps for BitMaps.BitMap;
14+
15+
BitMaps.BitMap private pendingCommandIds;
16+
17+
event CommandIdPending(
18+
bytes32 indexed commandId,
19+
string destinationChain,
20+
string destinationContractAddress,
21+
bytes payload
22+
);
23+
24+
function callContract(
25+
string calldata destinationChain,
26+
string calldata destinationContractAddress,
27+
bytes calldata payload
28+
) external {
29+
// TODO: check that destination chain is local
30+
31+
emit IAxelarGateway.ContractCall(
32+
msg.sender,
33+
destinationChain,
34+
destinationContractAddress,
35+
keccak256(payload),
36+
payload
37+
);
38+
39+
bytes32 commandId = keccak256(
40+
abi.encode(
41+
destinationChain,
42+
msg.sender.toChecksumHexString(),
43+
destinationContractAddress,
44+
keccak256(payload)
45+
)
46+
);
47+
48+
require(!pendingCommandIds.get(uint256(commandId)));
49+
pendingCommandIds.set(uint256(commandId));
50+
51+
emit CommandIdPending(commandId, destinationChain, destinationContractAddress, payload);
52+
53+
// NOTE: source chain and destination chain are the same in this mock
54+
address target = destinationContractAddress.parseAddress();
55+
IAxelarExecutable(target).execute(commandId, destinationChain, msg.sender.toChecksumHexString(), payload);
56+
}
57+
58+
function validateContractCall(
59+
bytes32 commandId,
60+
string calldata sourceChain,
61+
string calldata sourceAddress,
62+
bytes32 payloadHash
63+
) external returns (bool) {
64+
if (pendingCommandIds.get(uint256(commandId))) {
65+
pendingCommandIds.unset(uint256(commandId));
66+
67+
emit IAxelarGateway.ContractCallExecuted(commandId);
68+
69+
return
70+
commandId ==
71+
keccak256(abi.encode(sourceChain, sourceAddress, msg.sender.toChecksumHexString(), payloadHash));
72+
} else return false;
73+
}
74+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.0;
4+
5+
/**
6+
* @dev Interface for ERC-7786 source gateways.
7+
*
8+
* See ERC-7786 for more details
9+
*/
10+
interface IERC7786GatewaySource {
11+
/**
12+
* @dev Event emitted when a message is created. If `outboxId` is zero, no further processing is necessary. If
13+
* `outboxId` is not zero, then further (gateway specific, and non-standardized) action is required.
14+
*/
15+
event MessagePosted(
16+
bytes32 indexed outboxId,
17+
string sender, // CAIP-10 account identifier (chain identifier + ":" + account address)
18+
string receiver, // CAIP-10 account identifier (chain identifier + ":" + account address)
19+
bytes payload,
20+
bytes[] attributes
21+
);
22+
23+
/// @dev This error is thrown when a message creation fails because of an unsupported attribute being specified.
24+
error UnsupportedAttribute(bytes4 selector);
25+
26+
/// @dev Getter to check whether an attribute is supported or not.
27+
function supportsAttribute(bytes4 selector) external view returns (bool);
28+
29+
/**
30+
* @dev Endpoint for creating a new message. If the message requires further (gateway specific) processing before
31+
* it can be sent to the destination chain, then a non-zero `outboxId` must be returned. Otherwise, the
32+
* message MUST be sent and this function must return 0.
33+
* @param destinationChain {CAIP2} chain identifier
34+
* @param receiver {CAIP10} account address (does not include the chain identifier)
35+
*
36+
* * MUST emit a {MessagePosted} event.
37+
*
38+
* If any of the `attributes` is not supported, this function SHOULD revert with an {UnsupportedAttribute} error.
39+
* Other errors SHOULD revert with errors not specified in ERC-7786.
40+
*/
41+
function sendMessage(
42+
string calldata destinationChain,
43+
string calldata receiver,
44+
bytes calldata payload,
45+
bytes[] calldata attributes
46+
) external payable returns (bytes32 outboxId);
47+
}
48+
49+
/**
50+
* @dev Interface for the ERC-7786 client contract (receiver).
51+
*
52+
* See ERC-7786 for more details
53+
*/
54+
interface IERC7786Receiver {
55+
/**
56+
* @dev Endpoint for receiving cross-chain message.
57+
* @param sourceChain {CAIP2} chain identifier
58+
* @param sender {CAIP10} account address (does not include the chain identifier)
59+
*
60+
* This function may be called directly by the gateway.
61+
*/
62+
function executeMessage(
63+
string calldata sourceChain, // CAIP-2 chain identifier
64+
string calldata sender, // CAIP-10 account address (does not include the chain identifier)
65+
bytes calldata payload,
66+
bytes[] calldata attributes
67+
) external payable returns (bytes4);
68+
}

0 commit comments

Comments
 (0)