Skip to content

Commit 12dd1d5

Browse files
authored
Standard cross-chain gateways and Axelar adapters (#9)
2 parents b8b9edc + 27fbb93 commit 12dd1d5

21 files changed

+1908
-1818
lines changed

.github/workflows/checks.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ jobs:
3333
GAS: true
3434
steps:
3535
- uses: actions/checkout@v4
36+
with:
37+
submodules: recursive
3638
- name: Set up environment
3739
uses: ./.github/actions/setup
3840
- name: Run tests and generate gas report
@@ -42,6 +44,8 @@ jobs:
4244
runs-on: ubuntu-latest
4345
steps:
4446
- uses: actions/checkout@v4
47+
with:
48+
submodules: recursive
4549
- name: Set up environment
4650
uses: ./.github/actions/setup
4751
- name: Run coverage
@@ -62,6 +66,8 @@ jobs:
6266
runs-on: ubuntu-latest
6367
steps:
6468
- uses: actions/checkout@v4
69+
with:
70+
submodules: recursive
6571
- name: Set up environment
6672
uses: ./.github/actions/setup
6773
- run: rm foundry.toml
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.0;
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+
/// @dev Axelar's official gateway for the current chain.
26+
IAxelarGateway public immutable localGateway;
27+
28+
mapping(string caip2 => string remoteGateway) private _remoteGateways;
29+
mapping(string caip2OrAxelar => string axelarOrCaip2) private _chainEquivalence;
30+
31+
/// @dev Sets the local gateway address (i.e. Axelar's official gateway for the current chain).
32+
constructor(IAxelarGateway _gateway) {
33+
localGateway = _gateway;
34+
}
35+
36+
/// @dev Returns the equivalent chain given an id that can be either CAIP-2 or an Axelar network identifier.
37+
function getEquivalentChain(string memory input) public view virtual returns (string memory output) {
38+
output = _chainEquivalence[input];
39+
require(bytes(output).length > 0, UnsupportedChain(input));
40+
}
41+
42+
/// @dev Returns the remote gateway address for a given chain.
43+
function getRemoteGateway(string memory caip2) public view virtual returns (string memory remoteGateway) {
44+
remoteGateway = _remoteGateways[caip2];
45+
require(bytes(remoteGateway).length > 0, UnsupportedChain(caip2));
46+
}
47+
48+
/// @dev Registers a chain equivalence between a CAIP-2 chain identifier and an Axelar network identifier.
49+
function registerChainEquivalence(string calldata caip2, string calldata axelarSupported) public virtual onlyOwner {
50+
require(bytes(_chainEquivalence[caip2]).length == 0);
51+
_chainEquivalence[caip2] = axelarSupported;
52+
_chainEquivalence[axelarSupported] = caip2;
53+
emit RegisteredChainEquivalence(caip2, axelarSupported);
54+
}
55+
56+
/// @dev Registers a remote gateway address for a given CAIP-2 chain identifier.
57+
function registerRemoteGateway(string calldata caip2, string calldata remoteGateway) public virtual onlyOwner {
58+
require(bytes(_remoteGateways[caip2]).length == 0);
59+
_remoteGateways[caip2] = remoteGateway;
60+
emit RegisteredRemoteGateway(caip2, remoteGateway);
61+
}
62+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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 {CAIP2} from "@openzeppelin/contracts@master/utils/CAIP2.sol";
7+
import {CAIP10} from "@openzeppelin/contracts@master/utils/CAIP10.sol";
8+
import {Strings} from "@openzeppelin/contracts@master/utils/Strings.sol";
9+
import {IERC7786GatewayDestinationPassive, IERC7786Receiver} from "../interfaces/draft-IERC7786.sol";
10+
import {AxelarGatewayBase} from "./AxelarGatewayBase.sol";
11+
12+
/**
13+
* @dev Implementation of an ERC7786 gateway destination adapter for the Axelar Network in dual mode.
14+
*
15+
* The contract implements implements AxelarExecutable's {_execute} function to execute the message,
16+
* converting Axelar's native workflow into the standard ERC7786 active mode.
17+
* Alternatively, it provides a way to set a message as executed by calling the {setExecutedMessage}
18+
* function (passive mode).
19+
*/
20+
abstract contract AxelarGatewayDestination is IERC7786GatewayDestinationPassive, AxelarGatewayBase, AxelarExecutable {
21+
using Strings for address;
22+
using Strings for string;
23+
24+
/// @dev Sets a message as executed so it can't be executed again. Should be called by the receiver contract.
25+
function setExecutedMessage(
26+
bytes calldata messageKey,
27+
string calldata source, // CAIP-2
28+
string calldata sender, // CAIP-10
29+
bytes calldata payload,
30+
bytes[] calldata attributes
31+
) external {
32+
// Extract Axelar commandId
33+
bytes32 commandId = abi.decode(messageKey, (bytes32));
34+
35+
// Rebuild expected package
36+
bytes memory adapterPayload = abi.encode(
37+
sender,
38+
msg.sender.toChecksumHexString(), // receiver
39+
payload,
40+
attributes
41+
);
42+
43+
// Check package was received from remote gateway on src chain
44+
require(
45+
gateway.validateContractCall(
46+
commandId,
47+
getEquivalentChain(source),
48+
getRemoteGateway(source),
49+
keccak256(adapterPayload)
50+
),
51+
NotApprovedByGateway()
52+
);
53+
}
54+
55+
/**
56+
* @dev Active mode execution of a cross-chain message.
57+
*
58+
* In this function:
59+
*
60+
* - `remoteChain` is in the Axelar format. It should not be expected to be a proper CAIP-2 format
61+
* - `remoteAccount` is the sender of the crosschain message. That should be the remote gateway on the chain which
62+
* the message originates from. It is NOT the sender of the crosschain message
63+
*
64+
* Proper CAIP-10 encoding of the message sender (including the CAIP-2 name of the origin chain can be found in
65+
* the message)
66+
*/
67+
function _execute(
68+
string calldata remoteChain, // chain of the remote gateway - axelar format
69+
string calldata remoteAccount, // address of the remote gateway
70+
bytes calldata adapterPayload
71+
) internal override {
72+
// Parse the package
73+
(string memory sender, string memory receiver, bytes memory payload, bytes[] memory attributes) = abi.decode(
74+
adapterPayload,
75+
(string, string, bytes, bytes[])
76+
);
77+
string memory source = getEquivalentChain(remoteChain);
78+
79+
// check message validity
80+
// - `remoteAccount` is the remote gateway on the origin chain.
81+
require(getRemoteGateway(source).equal(remoteAccount), "Invalid origin gateway");
82+
83+
// Active mode
84+
IERC7786Receiver(receiver.parseAddress()).receiveMessage(
85+
address(0), // not needed in active mode
86+
new bytes(0), // not needed in active mode
87+
source,
88+
sender,
89+
payload,
90+
attributes
91+
);
92+
}
93+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.27;
4+
5+
import {CAIP2} from "@openzeppelin/contracts@master/utils/CAIP2.sol";
6+
import {CAIP10} from "@openzeppelin/contracts@master/utils/CAIP10.sol";
7+
import {Strings} from "@openzeppelin/contracts@master/utils/Strings.sol";
8+
import {AxelarGatewayBase} from "./AxelarGatewayBase.sol";
9+
import {IERC7786GatewaySource} from "../interfaces/draft-IERC7786.sol";
10+
11+
/**
12+
* @dev Implementation of an ERC7786 gateway source adapter for the Axelar Network.
13+
*
14+
* The contract provides a way to send messages to a remote chain using the Axelar Network
15+
* using the {sendMessage} function.
16+
*/
17+
abstract contract AxelarGatewaySource is IERC7786GatewaySource, AxelarGatewayBase {
18+
using Strings for address;
19+
20+
/// @inheritdoc IERC7786GatewaySource
21+
function supportsAttribute(bytes4 /*selector*/) public pure returns (bool) {
22+
return false;
23+
}
24+
25+
/// @inheritdoc IERC7786GatewaySource
26+
function sendMessage(
27+
string calldata destination, // CAIP-2 chain ID
28+
string calldata receiver, // i.e. address
29+
bytes calldata payload,
30+
bytes[] calldata attributes
31+
) external payable returns (bytes32) {
32+
require(msg.value == 0, "Value not supported");
33+
if (attributes.length > 0) revert UnsupportedAttribute(bytes4(attributes[0][0:4]));
34+
35+
// Create the package
36+
string memory sender = msg.sender.toChecksumHexString();
37+
bytes memory adapterPayload = abi.encode(sender, receiver, payload, attributes);
38+
39+
// Emit event
40+
emit MessageCreated(
41+
0,
42+
CAIP10.format(CAIP2.local(), sender),
43+
CAIP10.format(destination, receiver),
44+
payload,
45+
attributes
46+
);
47+
48+
// Send the message
49+
string memory axelarDestination = getEquivalentChain(destination);
50+
string memory remoteGateway = getRemoteGateway(destination);
51+
localGateway.callContract(axelarDestination, remoteGateway, adapterPayload);
52+
53+
return 0;
54+
}
55+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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@master/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+
bool private activeMode;
16+
BitMaps.BitMap private pendingCommandIds;
17+
18+
event CommandIdPending(
19+
bytes32 indexed commandId,
20+
string destinationChain,
21+
string destinationContractAddress,
22+
bytes payload
23+
);
24+
25+
function setActive(bool enabled) public {
26+
activeMode = enabled;
27+
}
28+
29+
function callContract(
30+
string calldata destinationChain,
31+
string calldata destinationContractAddress,
32+
bytes calldata payload
33+
) external {
34+
// TODO: check that destination chain is local
35+
36+
emit IAxelarGateway.ContractCall(
37+
msg.sender,
38+
destinationChain,
39+
destinationContractAddress,
40+
keccak256(payload),
41+
payload
42+
);
43+
44+
bytes32 commandId = keccak256(
45+
abi.encode(
46+
destinationChain,
47+
msg.sender.toChecksumHexString(),
48+
destinationContractAddress,
49+
keccak256(payload)
50+
)
51+
);
52+
53+
require(!pendingCommandIds.get(uint256(commandId)));
54+
pendingCommandIds.set(uint256(commandId));
55+
56+
emit CommandIdPending(commandId, destinationChain, destinationContractAddress, payload);
57+
58+
if (activeMode) {
59+
// NOTE:
60+
// - source chain and destination chain are the same in this mock
61+
address target = destinationContractAddress.parseAddress();
62+
IAxelarExecutable(target).execute(commandId, destinationChain, msg.sender.toChecksumHexString(), payload);
63+
}
64+
}
65+
66+
function validateContractCall(
67+
bytes32 commandId,
68+
string calldata sourceChain,
69+
string calldata sourceAddress,
70+
bytes32 payloadHash
71+
) external returns (bool) {
72+
if (pendingCommandIds.get(uint256(commandId))) {
73+
pendingCommandIds.unset(uint256(commandId));
74+
75+
emit IAxelarGateway.ContractCallExecuted(commandId);
76+
77+
return
78+
commandId ==
79+
keccak256(abi.encode(sourceChain, sourceAddress, msg.sender.toChecksumHexString(), payloadHash));
80+
} else return false;
81+
}
82+
}

0 commit comments

Comments
 (0)