Skip to content

Update Crosschain contract: transition from CAIP-10 to ERC-7930 #171

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

Merged
merged 20 commits into from
Jun 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
120 changes: 61 additions & 59 deletions contracts/crosschain/ERC7786Aggregator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,17 @@ pragma solidity ^0.8.27;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {CAIP2} from "@openzeppelin/contracts/utils/CAIP2.sol";
import {CAIP10} from "@openzeppelin/contracts/utils/CAIP10.sol";
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
import {IERC7786GatewaySource, IERC7786Receiver} from "../interfaces/IERC7786.sol";
import {InteroperableAddress} from "@openzeppelin/contracts/utils/draft-InteroperableAddress.sol";

/**
* @dev N of M gateway: Sends your message through M independent gateways. It will be delivered to the receiver by an
* equivalent aggregator on the destination chain if N of the M gateways agree.
*/
contract ERC7786Aggregator is IERC7786GatewaySource, IERC7786Receiver, Ownable, Pausable {
using EnumerableSet for *;
using Strings for *;
using EnumerableSet for EnumerableSet.AddressSet;
using InteroperableAddress for bytes;

struct Outbox {
address gateway;
Expand All @@ -30,18 +28,18 @@ contract ERC7786Aggregator is IERC7786GatewaySource, IERC7786Receiver, Ownable,
bool executed;
}

event OutboxDetails(bytes32 indexed outboxId, Outbox[] outbox);
event OutboxDetails(bytes32 indexed sendId, Outbox[] outbox);
event Received(bytes32 indexed receiveId, address gateway);
event ExecutionSuccess(bytes32 indexed receiveId);
event ExecutionFailed(bytes32 indexed receiveId);
event GatewayAdded(address indexed gateway);
event GatewayRemoved(address indexed gateway);
event ThresholdUpdated(uint8 threshold);

error ERC7786AggregatorValueNotSupported();
error UnsupportedNativeTransfer();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this error will throw an "Already declared identifier" if used along with AxelarGatewaySource (i.e. contract A is ERC7786Aggregator, AxelarGatewaySource, ... {}).

We may want to create a library for 7786-related errors

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can rename that error, but the aggregator is an independant contract that does calls to gateways. The aggregator should NOT inherit from another gateway implementation.

I think the analogy is ERC-4626 <> ERC-20.
ERC-4626 implements the ERC-20 interface, and all the corresponding logic, but you would not do contract A is ERC4626, WETH, ... {}

Copy link
Member

@ernestognw ernestognw Jun 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, I just don't think this is an aggregator then..., it's more like a redundant bridge

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its going to be renamed anyway. The current marking name is "ERC7786OpenBridge". Makes little sens in my opinion.

I think "multibridge" might be a good option.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More in favor of MultiBridge after past discussions but happy with OpenBridge since marketing has gone with that one.

error ERC7786AggregatorInvalidCrosschainSender();
error ERC7786AggregatorAlreadyExecuted();
error ERC7786AggregatorRemoteNotRegistered(string caip2);
error ERC7786AggregatorRemoteNotRegistered(bytes2 chainType, bytes chainReference);
error ERC7786AggregatorGatewayAlreadyRegistered(address gateway);
error ERC7786AggregatorGatewayNotRegistered(address gateway);
error ERC7786AggregatorThresholdViolation();
Expand All @@ -52,7 +50,7 @@ contract ERC7786Aggregator is IERC7786GatewaySource, IERC7786Receiver, Ownable,
****************************************************************************************************************/

/// @dev address of the matching aggregator for a given CAIP2 chain
mapping(string caip2 => string) private _remotes;
mapping(bytes2 chainType => mapping(bytes chainReference => bytes addr)) private _remotes;

/// @dev Tracking of the received message pending final delivery
mapping(bytes32 id => Tracker) private _trackers;
Expand All @@ -69,8 +67,8 @@ contract ERC7786Aggregator is IERC7786GatewaySource, IERC7786Receiver, Ownable,
/****************************************************************************************************************
* E V E N T S & E R R O R S *
****************************************************************************************************************/
event RemoteRegistered(string chainId, string aggregator);
error RemoteAlreadyRegistered(string chainId);
event RemoteRegistered(bytes remote);
error RemoteAlreadyRegistered(bytes remote);

/****************************************************************************************************************
* F U N C T I O N S *
Expand All @@ -92,31 +90,29 @@ contract ERC7786Aggregator is IERC7786GatewaySource, IERC7786Receiver, Ownable,
/// @inheritdoc IERC7786GatewaySource
/// @dev Using memory instead of calldata avoids stack too deep errors
function sendMessage(
string calldata destinationChain,
string memory receiver,
bytes memory payload,
bytes[] memory attributes
) public payable virtual whenNotPaused returns (bytes32 outboxId) {
if (attributes.length > 0) revert UnsupportedAttribute(bytes4(attributes[0]));
if (msg.value > 0) revert ERC7786AggregatorValueNotSupported();
bytes calldata recipient, // Binary Interoperable Address
bytes calldata payload,
bytes[] calldata attributes
) public payable virtual whenNotPaused returns (bytes32 sendId) {
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]));

// address of the remote aggregator, revert if not registered
string memory aggregator = getRemoteAggregator(destinationChain);
bytes memory aggregator = getRemoteAggregator(recipient);
bytes memory sender = InteroperableAddress.formatEvmV1(block.chainid, msg.sender);

// wrapping the payload
bytes memory wrappedPayload = abi.encode(++_nonce, msg.sender.toChecksumHexString(), receiver, payload);
bytes memory wrappedPayload = abi.encode(++_nonce, sender, recipient, payload);

// Post on all gateways
Outbox[] memory outbox = new Outbox[](_gateways.length());
bool needsId = false;
for (uint256 i = 0; i < outbox.length; ++i) {
address gateway = _gateways.at(i);
// send message
bytes32 id = IERC7786GatewaySource(gateway).sendMessage(
destinationChain,
aggregator,
wrappedPayload,
attributes
);
bytes32 id = IERC7786GatewaySource(gateway).sendMessage(aggregator, wrappedPayload, attributes);
// if ID, track it
if (id != bytes32(0)) {
outbox[i] = Outbox(gateway, id);
Expand All @@ -125,17 +121,11 @@ contract ERC7786Aggregator is IERC7786GatewaySource, IERC7786Receiver, Ownable,
}

if (needsId) {
outboxId = keccak256(abi.encode(outbox));
emit OutboxDetails(outboxId, outbox);
sendId = keccak256(abi.encode(outbox));
emit OutboxDetails(sendId, outbox);
}

emit MessagePosted(
outboxId,
CAIP10.local(msg.sender),
CAIP10.format(destinationChain, receiver),
payload,
attributes
);
emit MessageSent(sendId, sender, recipient, payload, 0, attributes);
}

// ============================================== IERC7786Receiver ===============================================
Expand Down Expand Up @@ -182,18 +172,21 @@ contract ERC7786Aggregator is IERC7786GatewaySource, IERC7786Receiver, Ownable,
* some value for unknown reason. In that case we want to register this gateway having delivered the message and
* not revert. Any value accrued that way can be recovered by the admin using the {sweep} function.
*/
// slither-disable-next-line reentrancy-no-eth
function executeMessage(
string calldata /*messageId*/, // gateway specific, empty or unique
string calldata sourceChain, // CAIP-2 chain identifier
string calldata sender, // CAIP-10 account address (does not include the chain identifier)
bytes32 /*receiveId*/,
bytes calldata sender, // Binary Interoperable Address
bytes calldata payload,
bytes[] calldata attributes
) public payable virtual whenNotPaused returns (bytes4) {
// Check sender is a trusted remote aggregator
if (!_remotes[sourceChain].equal(sender)) revert ERC7786AggregatorInvalidCrosschainSender();
// Check sender is a trusted aggregator
require(
keccak256(getRemoteAggregator(sender)) == keccak256(sender),
ERC7786AggregatorInvalidCrosschainSender()
);

// Message reception tracker
bytes32 id = keccak256(abi.encode(sourceChain, sender, payload, attributes));
bytes32 id = keccak256(abi.encode(sender, payload, attributes));
Tracker storage tracker = _trackers[id];

// If call is first from a trusted gateway
Expand All @@ -210,9 +203,9 @@ contract ERC7786Aggregator is IERC7786GatewaySource, IERC7786Receiver, Ownable,
}

// Parse payload
(, string memory originalSender, string memory receiver, bytes memory unwrappedPayload) = abi.decode(
(, bytes memory originalSender, bytes memory recipient, bytes memory unwrappedPayload) = abi.decode(
payload,
(uint256, string, string, bytes)
(uint256, bytes, bytes, bytes)
);

// If ready to execute, and not yet executed
Expand All @@ -222,10 +215,11 @@ contract ERC7786Aggregator is IERC7786GatewaySource, IERC7786Receiver, Ownable,

bytes memory call = abi.encodeCall(
IERC7786Receiver.executeMessage,
(uint256(id).toHexString(32), sourceChain, originalSender, unwrappedPayload, attributes)
(id, originalSender, unwrappedPayload, attributes)
);
// slither-disable-next-line reentrancy-no-eth
(bool success, bytes memory returndata) = receiver.parseAddress().call(call);
(, address target) = recipient.parseEvmV1();
(bool success, bytes memory returndata) = target.call(call);

if (!success) {
// rollback to enable retry
Expand Down Expand Up @@ -253,10 +247,18 @@ contract ERC7786Aggregator is IERC7786GatewaySource, IERC7786Receiver, Ownable,
return _threshold;
}

function getRemoteAggregator(string calldata caip2) public view virtual returns (string memory) {
string memory aggregator = _remotes[caip2];
if (bytes(aggregator).length == 0) revert ERC7786AggregatorRemoteNotRegistered(caip2);
return aggregator;
function getRemoteAggregator(bytes memory chain) public view virtual returns (bytes memory) {
(bytes2 chainType, bytes memory chainReference, ) = chain.parseV1();
return getRemoteAggregator(chainType, chainReference);
}

function getRemoteAggregator(
bytes2 chainType,
bytes memory chainReference
) public view virtual returns (bytes memory) {
bytes memory addr = _remotes[chainType][chainReference];
require(bytes(addr).length != 0, ERC7786AggregatorRemoteNotRegistered(chainType, chainReference));
return InteroperableAddress.formatV1(chainType, chainReference, addr);
}

// =================================================== Setters ===================================================
Expand All @@ -273,8 +275,8 @@ contract ERC7786Aggregator is IERC7786GatewaySource, IERC7786Receiver, Ownable,
_setThreshold(newThreshold);
}

function registerRemoteAggregator(string memory caip2, string memory aggregator) public virtual onlyOwner {
_registerRemoteAggregator(caip2, aggregator);
function registerRemoteAggregator(bytes calldata aggregator) public virtual onlyOwner {
_registerRemoteAggregator(aggregator);
}

function pause() public virtual onlyOwner {
Expand All @@ -293,25 +295,25 @@ contract ERC7786Aggregator is IERC7786GatewaySource, IERC7786Receiver, Ownable,
// ================================================== Internal ===================================================

function _addGateway(address gateway) internal virtual {
if (!_gateways.add(gateway)) revert ERC7786AggregatorGatewayAlreadyRegistered(gateway);
require(_gateways.add(gateway), ERC7786AggregatorGatewayAlreadyRegistered(gateway));
emit GatewayAdded(gateway);
}

function _removeGateway(address gateway) internal virtual {
if (!_gateways.remove(gateway)) revert ERC7786AggregatorGatewayNotRegistered(gateway);
if (_threshold > _gateways.length()) revert ERC7786AggregatorThresholdViolation();
require(_gateways.remove(gateway), ERC7786AggregatorGatewayNotRegistered(gateway));
require(_threshold <= _gateways.length(), ERC7786AggregatorThresholdViolation());
emit GatewayRemoved(gateway);
}

function _setThreshold(uint8 newThreshold) internal virtual {
if (newThreshold == 0 || _threshold > _gateways.length()) revert ERC7786AggregatorThresholdViolation();
require(newThreshold > 0 && newThreshold <= _gateways.length(), ERC7786AggregatorThresholdViolation());
_threshold = newThreshold;
emit ThresholdUpdated(newThreshold);
}

function _registerRemoteAggregator(string memory caip2, string memory aggregator) internal virtual {
_remotes[caip2] = aggregator;

emit RemoteRegistered(caip2, aggregator);
function _registerRemoteAggregator(bytes calldata aggregator) internal virtual {
(bytes2 chainType, bytes calldata chainReference, bytes calldata addr) = aggregator.parseV1Calldata();
_remotes[chainType][chainReference] = addr;
emit RemoteRegistered(aggregator);
}
}
Loading