Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Standard cross-chain gateways and Axelar adapters #9

Merged
merged 52 commits into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from 49 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
eee0853
cross-chain prototype v1
Amxx Jun 21, 2024
50ced4a
split common <> axelar
Amxx Jun 21, 2024
28e88cd
add relay observability
Amxx Jun 23, 2024
948dab4
Update oz to master
ernestognw Jul 30, 2024
e350890
Iterate
Aug 13, 2024
de9da70
Remove salt
Aug 13, 2024
d760742
Iterate
Aug 14, 2024
348ad8d
Add GatewayAxelar specialization
Aug 14, 2024
edfd3ed
Iterate
ernestognw Aug 14, 2024
645794b
Fix GatewayAxelarSource
ernestognw Aug 20, 2024
270c5bd
Remove unnecessary contract
ernestognw Aug 26, 2024
4605a57
Iteration
ernestognw Aug 26, 2024
597bfc2
Remove interfaces
ernestognw Aug 26, 2024
2951fa5
Checkpoint
ernestognw Aug 26, 2024
a7bd130
Add incoming dual mode
ernestognw Aug 26, 2024
2f4588e
Fix compilation
ernestognw Aug 26, 2024
fd010bd
Apply review suggestions
ernestognw Aug 26, 2024
aa731cc
Install axelar contracts
ernestognw Sep 3, 2024
e39ff3a
Apply review sugggestion
ernestognw Sep 3, 2024
b54dda1
Resolve conflcits
ernestognw Sep 3, 2024
0e7d040
Apply suggestions
ernestognw Sep 3, 2024
2402926
wip fixes
frangio Sep 3, 2024
27dcd99
trying to get crosschain to compile
Amxx Sep 5, 2024
01d98fd
fix compilation
Amxx Sep 5, 2024
85ee12a
minor update
Amxx Sep 5, 2024
cdc3c94
make attributes a bytes[]
Amxx Sep 5, 2024
d7ce229
Address comments and add some tests
ernestognw Sep 10, 2024
6dc1c44
refactor and test caip utils
Amxx Sep 10, 2024
4cdf242
up
Amxx Sep 11, 2024
40b33a2
using unmerged version of Strings with parsing
Amxx Sep 16, 2024
a8ee58b
up
Amxx Sep 18, 2024
91cb0ac
workflow testing (active and passive)
Amxx Sep 18, 2024
80b0a59
update
Amxx Sep 19, 2024
b2ac19d
up
Amxx Sep 20, 2024
4f0f1f0
address PR comments
Amxx Sep 30, 2024
5a5b764
renovate
Amxx Sep 30, 2024
46db15d
rename
Amxx Sep 30, 2024
70855f7
fix foundry
Amxx Sep 30, 2024
3f3d6b6
codespell
Amxx Sep 30, 2024
336a56b
use checksumed addresses
Amxx Oct 1, 2024
86caeeb
use @openzepplin/contracts@master
Amxx Oct 22, 2024
426c3fc
add/move files that are no longer planned to be in the main repo
Amxx Oct 22, 2024
12236dd
get submodules when running tests
Amxx Oct 22, 2024
aad8745
Apply suggestions from code review
Amxx Oct 22, 2024
2aa5c15
Apply suggestions from code review
Amxx Oct 22, 2024
98dede1
simplify
Amxx Oct 22, 2024
9bd6a3b
update
Amxx Oct 22, 2024
c926b36
validateReceivedMessage -> setExecutedMessage
ernestognw Oct 29, 2024
a5f1368
Add docs
ernestognw Oct 29, 2024
6089900
Update AxelarGatewayDestination.sol
ernestognw Oct 30, 2024
a38ec99
Update
ernestognw Oct 30, 2024
27fbb93
Update
ernestognw Oct 30, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ jobs:
GAS: true
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up environment
uses: ./.github/actions/setup
- name: Run tests and generate gas report
Expand All @@ -42,6 +44,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up environment
uses: ./.github/actions/setup
- name: Run coverage
Expand All @@ -62,6 +66,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up environment
uses: ./.github/actions/setup
- run: rm foundry.toml
Expand Down
62 changes: 62 additions & 0 deletions contracts/crosschain/axelar/AxelarGatewayBase.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IAxelarGateway} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol";

/**
* @dev Base implementation of a cross-chain gateway adapter for the Axelar Network.
*
* This contract allows developers to register equivalence between chains (i.e. CAIP-2 chain identifiers
* to Axelar chain identifiers) and remote gateways (i.e. gateways on other chains) to
* facilitate cross-chain communication.
*/
abstract contract AxelarGatewayBase is Ownable {
/// @dev A remote gateway has been registered for a chain.
event RegisteredRemoteGateway(string caip2, string gatewayAddress);

/// @dev A chain equivalence has been registered.
event RegisteredChainEquivalence(string caip2, string destinationChain);

/// @dev Error emitted when an unsupported chain is queried.
error UnsupportedChain(string caip2);

/// @dev Axelar's official gateway for the current chain.
IAxelarGateway public immutable localGateway;

mapping(string caip2 => string remoteGateway) private _remoteGateways;
mapping(string caip2OrAxelar => string axelarOrCaip2) private _chainEquivalence;

/// @dev Sets the local gateway address (i.e. Axelar's official gateway for the current chain).
constructor(IAxelarGateway _gateway) {
localGateway = _gateway;
}

/// @dev Returns the equivalent chain given an id that can be either CAIP-2 or an Axelar network identifier.
function getEquivalentChain(string memory input) public view virtual returns (string memory output) {
output = _chainEquivalence[input];
require(bytes(output).length > 0, UnsupportedChain(input));
}

/// @dev Returns the remote gateway address for a given chain.
function getRemoteGateway(string memory caip2) public view virtual returns (string memory remoteGateway) {
remoteGateway = _remoteGateways[caip2];
require(bytes(remoteGateway).length > 0, UnsupportedChain(caip2));
}

/// @dev Registers a chain equivalence between a CAIP-2 chain identifier and an Axelar network identifier.
function registerChainEquivalence(string calldata caip2, string calldata axelarSupported) public virtual onlyOwner {
require(bytes(_chainEquivalence[caip2]).length == 0);
_chainEquivalence[caip2] = axelarSupported;
_chainEquivalence[axelarSupported] = caip2;
emit RegisteredChainEquivalence(caip2, axelarSupported);
}

/// @dev Registers a remote gateway address for a given CAIP-2 chain identifier.
function registerRemoteGateway(string calldata caip2, string calldata remoteGateway) public virtual onlyOwner {
require(bytes(_remoteGateways[caip2]).length == 0);
_remoteGateways[caip2] = remoteGateway;
emit RegisteredRemoteGateway(caip2, remoteGateway);
}
}
92 changes: 92 additions & 0 deletions contracts/crosschain/axelar/AxelarGatewayDestination.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.27;

import {AxelarExecutable} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/executable/AxelarExecutable.sol";
import {CAIP2} from "@openzeppelin/contracts@master/utils/CAIP2.sol";
import {CAIP10} from "@openzeppelin/contracts@master/utils/CAIP10.sol";
import {Strings} from "@openzeppelin/contracts@master/utils/Strings.sol";
import {IERC7786GatewayDestinationPassive, IERC7786Receiver} from "../interfaces/draft-IERC7786.sol";
import {AxelarGatewayBase} from "./AxelarGatewayBase.sol";

/**
* @dev Implementation of an ERC7786 gateway destination adapter for the Axelar Network in dual mode.
*
* The contract provides an internal {_execute} function to be used by a child contract (active mode),
* Alternatively, it provides a way to set a message as executed by calling the {setExecutedMessage}
* function (passive mode).
ernestognw marked this conversation as resolved.
Show resolved Hide resolved
*/
abstract contract AxelarGatewayDestination is IERC7786GatewayDestinationPassive, AxelarGatewayBase, AxelarExecutable {
using Strings for address;
using Strings for string;

/// @dev Sets a message as executed so it can't be executed again. Should be called by the receiver contract.
function setExecutedMessage(
bytes calldata messageKey,
string calldata source, // CAIP-2
string calldata sender, // CAIP-10
bytes calldata payload,
bytes[] calldata attributes
) external {
// Extract Axelar commandId
bytes32 commandId = abi.decode(messageKey, (bytes32));

// Rebuild expected package
bytes memory adapterPayload = abi.encode(
sender,
msg.sender.toChecksumHexString(), // receiver
payload,
attributes
);

// Check package was received from remote gateway on src chain
require(
gateway.validateContractCall(
commandId,
getEquivalentChain(source),
getRemoteGateway(source),
keccak256(adapterPayload)
),
NotApprovedByGateway()
);
}

/**
* @dev Active mode execution of a cross-chain message.
*
* In this function:
*
* - `remoteChain` is in the Axelar format. It should not be expected to be a proper CAIP-2 format
* - `remoteAccount` is the sender of the crosschain message. That should be the remote gateway on the chain which
* the message originates from. It is NOT the sender of the crosschain message
*
* Proper CAIP-10 encoding of the message sender (including the CAIP-2 name of the origin chain can be found in
* the message)
ernestognw marked this conversation as resolved.
Show resolved Hide resolved
*/
function _execute(
string calldata remoteChain, // chain of the remote gateway - axelar format
string calldata remoteAccount, // address of the remote gateway
bytes calldata adapterPayload
) internal override {
// Parse the package
(string memory sender, string memory receiver, bytes memory payload, bytes[] memory attributes) = abi.decode(
adapterPayload,
(string, string, bytes, bytes[])
);
string memory source = getEquivalentChain(remoteChain);

// check message validity
// - `remoteAccount` is the remote gateway on the origin chain.
require(getRemoteGateway(source).equal(remoteAccount), "Invalid origin gateway");

// Active mode
IERC7786Receiver(receiver.parseAddress()).receiveMessage(
address(0), // not needed in active mode
new bytes(0), // not needed in active mode
source,
sender,
payload,
attributes
);
}
}
55 changes: 55 additions & 0 deletions contracts/crosschain/axelar/AxelarGatewaySource.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.27;

import {CAIP2} from "@openzeppelin/contracts@master/utils/CAIP2.sol";
import {CAIP10} from "@openzeppelin/contracts@master/utils/CAIP10.sol";
import {Strings} from "@openzeppelin/contracts@master/utils/Strings.sol";
import {AxelarGatewayBase} from "./AxelarGatewayBase.sol";
import {IERC7786GatewaySource} from "../interfaces/draft-IERC7786.sol";

/**
* @dev Implementation of an ERC7786 gateway source adapter for the Axelar Network.
*
* The contract provides a way to send messages to a remote chain using the Axelar Network
* using the {sendMessage} function.
*/
abstract contract AxelarGatewaySource is IERC7786GatewaySource, AxelarGatewayBase {
using Strings for address;

/// @inheritdoc IERC7786GatewaySource
function supportsAttribute(bytes4 /*selector*/) public pure returns (bool) {
return false;
}

/// @inheritdoc IERC7786GatewaySource
function sendMessage(
string calldata destination, // CAIP-2 chain ID
string calldata receiver, // i.e. address
bytes calldata payload,
bytes[] calldata attributes
) external payable returns (bytes32) {
require(msg.value == 0, "Value not supported");
if (attributes.length > 0) revert UnsupportedAttribute(bytes4(attributes[0][0:4]));

// Create the package
string memory sender = msg.sender.toChecksumHexString();
bytes memory adapterPayload = abi.encode(sender, receiver, payload, attributes);

// Emit event
emit MessageCreated(
0,
CAIP10.format(CAIP2.local(), sender),
CAIP10.format(destination, receiver),
payload,
attributes
);

// Send the message
string memory axelarDestination = getEquivalentChain(destination);
string memory remoteGateway = getRemoteGateway(destination);
localGateway.callContract(axelarDestination, remoteGateway, adapterPayload);

return 0;
}
}
82 changes: 82 additions & 0 deletions contracts/crosschain/axelar/mocks/AxelarGatewayMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.27;

import {IAxelarGateway} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol";
import {IAxelarExecutable} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarExecutable.sol";
import {BitMaps} from "@openzeppelin/contracts/utils/structs/BitMaps.sol";
import {Strings} from "@openzeppelin/contracts@master/utils/Strings.sol";

contract AxelarGatewayMock {
using Strings for address;
using Strings for string;
using BitMaps for BitMaps.BitMap;

bool private activeMode;
BitMaps.BitMap private pendingCommandIds;

event CommandIdPending(
bytes32 indexed commandId,
string destinationChain,
string destinationContractAddress,
bytes payload
);

function setActive(bool enabled) public {
activeMode = enabled;
}

function callContract(
string calldata destinationChain,
string calldata destinationContractAddress,
bytes calldata payload
) external {
// TODO: check that destination chain is local

emit IAxelarGateway.ContractCall(
msg.sender,
destinationChain,
destinationContractAddress,
keccak256(payload),
payload
);

bytes32 commandId = keccak256(
abi.encode(
destinationChain,
msg.sender.toChecksumHexString(),
destinationContractAddress,
keccak256(payload)
)
);

require(!pendingCommandIds.get(uint256(commandId)));
pendingCommandIds.set(uint256(commandId));

emit CommandIdPending(commandId, destinationChain, destinationContractAddress, payload);

if (activeMode) {
// NOTE:
// - source chain and destination chain are the same in this mock
address target = destinationContractAddress.parseAddress();
IAxelarExecutable(target).execute(commandId, destinationChain, msg.sender.toChecksumHexString(), payload);
}
}

function validateContractCall(
bytes32 commandId,
string calldata sourceChain,
string calldata sourceAddress,
bytes32 payloadHash
) external returns (bool) {
if (pendingCommandIds.get(uint256(commandId))) {
pendingCommandIds.unset(uint256(commandId));

emit IAxelarGateway.ContractCallExecuted(commandId);

return
commandId ==
keccak256(abi.encode(sourceChain, sourceAddress, msg.sender.toChecksumHexString(), payloadHash));
} else return false;
}
}
Loading