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

[SC-425] Adding Circle CCTP support #15

Merged
merged 8 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ jobs:
ARBITRUM_NOVA_RPC_URL: ${{secrets.ARBITRUM_NOVA_RPC_URL}}
GNOSIS_CHAIN_RPC_URL: ${{secrets.GNOSIS_CHAIN_RPC_URL}}
BASE_RPC_URL: ${{secrets.BASE_RPC_URL}}
POLYGON_RPC_URL: ${{secrets.POLYGON_RPC_URL}}
run: FOUNDRY_PROFILE=ci forge test

coverage:
Expand All @@ -58,6 +59,7 @@ jobs:
ARBITRUM_NOVA_RPC_URL: ${{secrets.ARBITRUM_NOVA_RPC_URL}}
GNOSIS_CHAIN_RPC_URL: ${{secrets.GNOSIS_CHAIN_RPC_URL}}
BASE_RPC_URL: ${{secrets.BASE_RPC_URL}}
POLYGON_RPC_URL: ${{secrets.POLYGON_RPC_URL}}
run: forge coverage --report summary --report lcov

# To ignore coverage for certain directories modify the paths in this step as needed. The
Expand Down
52 changes: 52 additions & 0 deletions src/CCTPReceiver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity ^0.8.0;

/**
* @title CCTPReceiver
* @notice Receive messages from CCTP-style bridge.
*/
abstract contract CCTPReceiver {

address public immutable l2CrossDomain;
uint32 public immutable sourceDomain;
address public immutable l1Authority;
barrutko marked this conversation as resolved.
Show resolved Hide resolved

constructor(
address _l2CrossDomain,
lucas-manuel marked this conversation as resolved.
Show resolved Hide resolved
uint32 _sourceDomain,
address _l1Authority
) {
l2CrossDomain = _l2CrossDomain;
sourceDomain = _sourceDomain;
l1Authority = _l1Authority;
}

function _onlyCrossChainMessage() internal view {
require(msg.sender == address(this), "Receiver/invalid-sender");
}

modifier onlyCrossChainMessage() {
_onlyCrossChainMessage();
_;
}

function handleReceiveMessage(
uint32 _sourceDomain,
bytes32 sender,
bytes calldata messageBody
) external returns (bool) {
require(msg.sender == l2CrossDomain, "Receiver/invalid-sender");
require(_sourceDomain == sourceDomain, "Receiver/invalid-sourceDomain");
require(sender == bytes32(uint256(uint160(l1Authority))), "Receiver/invalid-l1Authority");

(bool success, bytes memory ret) = address(this).call(messageBody);
if (!success) {
assembly {
revert(add(ret, 0x20), mload(ret))
}
}

return true;
}

}
63 changes: 63 additions & 0 deletions src/XChainForwarders.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ interface ICrossDomainZkEVM {
) external payable;
}

interface ICrossDomainCircleCCTP {
function sendMessage(
uint32 destinationDomain,
bytes32 recipient,
bytes calldata messageBody
) external;
}

/**
* @title XChainForwarders
* @notice Helper functions to abstract over L1 -> L2 message passing.
Expand Down Expand Up @@ -196,4 +204,59 @@ library XChainForwarders {
);
}

/// ================================ CCTP ================================

function sendMessageCCTP(
address l1CrossDomain,
lucas-manuel marked this conversation as resolved.
Show resolved Hide resolved
uint32 destinationDomain,
bytes32 recipient,
bytes memory messageBody
) internal {
ICrossDomainCircleCCTP(l1CrossDomain).sendMessage(
destinationDomain,
recipient,
messageBody
);
}

function sendMessageCCTP(
address l1CrossDomain,
uint32 destinationDomain,
address recipient,
bytes memory messageBody
) internal {
sendMessageCCTP(
l1CrossDomain,
destinationDomain,
bytes32(uint256(uint160(recipient))),
messageBody
);
}

function sendMessageCircleCCTP(
uint32 destinationDomain,
bytes32 recipient,
bytes memory messageBody
) internal {
sendMessageCCTP(
0x0a992d191DEeC32aFe36203Ad87D7d289a738F81,
destinationDomain,
recipient,
messageBody
);
}

function sendMessageCircleCCTP(
uint32 destinationDomain,
address recipient,
bytes memory messageBody
) internal {
sendMessageCCTP(
0x0a992d191DEeC32aFe36203Ad87D7d289a738F81,
destinationDomain,
bytes32(uint256(uint160(recipient))),
messageBody
);
}

}
99 changes: 99 additions & 0 deletions src/testing/CircleCCTPDomain.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity >=0.8.0;

import { StdChains } from "forge-std/StdChains.sol";
import { Vm } from "forge-std/Vm.sol";

import { Domain, BridgedDomain } from "./BridgedDomain.sol";
import { RecordedLogs } from "./RecordedLogs.sol";

interface MessengerLike {
function receiveMessage(bytes calldata message, bytes calldata attestation) external returns (bool success);
}

contract CircleCCTPDomain is BridgedDomain {

bytes32 private constant SENT_MESSAGE_TOPIC = keccak256("MessageSent(bytes)");

MessengerLike public constant L1_MESSENGER = MessengerLike(0x0a992d191DEeC32aFe36203Ad87D7d289a738F81);
lucas-manuel marked this conversation as resolved.
Show resolved Hide resolved
MessengerLike public L2_MESSENGER;

uint256 internal lastFromHostLogIndex;
uint256 internal lastToHostLogIndex;
barrutko marked this conversation as resolved.
Show resolved Hide resolved

constructor(StdChains.Chain memory _chain, Domain _hostDomain) Domain(_chain) BridgedDomain(_hostDomain) {
barrutko marked this conversation as resolved.
Show resolved Hide resolved
bytes32 name = keccak256(bytes(_chain.chainAlias));
if (name == keccak256("avalanche")) {
L2_MESSENGER = MessengerLike(0x8186359aF5F57FbB40c6b14A588d2A59C0C29880);
lucas-manuel marked this conversation as resolved.
Show resolved Hide resolved
} else if (name == keccak256("optimism")) {
L2_MESSENGER = MessengerLike(0x4D41f22c5a0e5c74090899E5a8Fb597a8842b3e8);
} else if (name == keccak256("arbitrum_one")) {
L2_MESSENGER = MessengerLike(0xC30362313FBBA5cf9163F0bb16a0e01f01A896ca);
} else if (name == keccak256("base")) {
L2_MESSENGER = MessengerLike(0xAD09780d193884d503182aD4588450C416D6F9D4);
} else if (name == keccak256("polygon")) {
L2_MESSENGER = MessengerLike(0xF3be9355363857F3e001be68856A2f96b4C39Ba9);
} else {
revert("Unsupported chain");
}

// Set minimum required signatures to zero for both domains
selectFork();
vm.store(
address(L2_MESSENGER),
bytes32(uint256(4)),
0
);
hostDomain.selectFork();
vm.store(
address(L1_MESSENGER),
bytes32(uint256(4)),
0
);

vm.recordLogs();
}

function relayFromHost(bool switchToGuest) external override {
selectFork();

// Read all L1 -> L2 messages and relay them under CCTP fork
Vm.Log[] memory logs = RecordedLogs.getLogs();
for (; lastFromHostLogIndex < logs.length; lastFromHostLogIndex++) {
Vm.Log memory log = logs[lastFromHostLogIndex];
if (log.topics[0] == SENT_MESSAGE_TOPIC && log.emitter == address(L1_MESSENGER)) {
L2_MESSENGER.receiveMessage(removeFirst64Bytes(log.data), "");
}
}

if (!switchToGuest) {
hostDomain.selectFork();
}
}

function relayToHost(bool switchToHost) external override {
hostDomain.selectFork();

// Read all L2 -> L1 messages and relay them under host fork
Vm.Log[] memory logs = RecordedLogs.getLogs();
for (; lastToHostLogIndex < logs.length; lastToHostLogIndex++) {
Vm.Log memory log = logs[lastToHostLogIndex];
if (log.topics[0] == SENT_MESSAGE_TOPIC && log.emitter == address(L2_MESSENGER)) {
L1_MESSENGER.receiveMessage(removeFirst64Bytes(log.data), "");
}
}

if (!switchToHost) {
selectFork();
}
}

function removeFirst64Bytes(bytes memory inputData) public pure returns (bytes memory) {
bytes memory returnValue = new bytes(inputData.length - 64);
for (uint256 i = 0; i < inputData.length - 64; i++) {
returnValue[i] = inputData[i + 64];
}
return returnValue;
}

}
149 changes: 149 additions & 0 deletions test/CCTPIntegration.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity >=0.8.0;

import "./IntegrationBase.t.sol";

import { CircleCCTPDomain } from "../src/testing/CircleCCTPDomain.sol";

import { CCTPReceiver } from "../src/CCTPReceiver.sol";

contract MessageOrderingCCTP is MessageOrdering, CCTPReceiver {

constructor(
address _l2CrossDomain,
barrutko marked this conversation as resolved.
Show resolved Hide resolved
uint32 _sourceDomain,
address _l1Authority
) CCTPReceiver(
_l2CrossDomain,
_sourceDomain,
_l1Authority
) {}

function push(uint256 messageId) public override onlyCrossChainMessage {
super.push(messageId);
}

}

contract CircleCCTPIntegrationTest is IntegrationBaseTest {

function test_avalanche() public {
CircleCCTPDomain cctp = new CircleCCTPDomain(getChain("avalanche"), mainnet);
checkCircleCCTPStyle(cctp, 1);
}

function test_optimism() public {
CircleCCTPDomain cctp = new CircleCCTPDomain(getChain("optimism"), mainnet);
checkCircleCCTPStyle(cctp, 2);
}

function test_arbitrum_one() public {
CircleCCTPDomain cctp = new CircleCCTPDomain(getChain("arbitrum_one"), mainnet);
checkCircleCCTPStyle(cctp, 3);
}

function test_base() public {
CircleCCTPDomain cctp = new CircleCCTPDomain(getChain("base"), mainnet);
checkCircleCCTPStyle(cctp, 6);
}
barrutko marked this conversation as resolved.
Show resolved Hide resolved

function test_polygon() public {
CircleCCTPDomain cctp = new CircleCCTPDomain(getChain("polygon"), mainnet);
checkCircleCCTPStyle(cctp, 7);
}

function checkCircleCCTPStyle(CircleCCTPDomain cctp, uint32 guestDomain) public {
Domain host = cctp.hostDomain();
uint32 hostDomain = 0; // Ethereum
lucas-manuel marked this conversation as resolved.
Show resolved Hide resolved

host.selectFork();

MessageOrderingCCTP moHost = new MessageOrderingCCTP(
address(cctp.L1_MESSENGER()),
guestDomain,
l1Authority
);

cctp.selectFork();

MessageOrderingCCTP moCCTP = new MessageOrderingCCTP(
address(cctp.L2_MESSENGER()),
hostDomain,
l1Authority
);

// Queue up some L2 -> L1 messages
barrutko marked this conversation as resolved.
Show resolved Hide resolved
vm.startPrank(l1Authority);
lucas-manuel marked this conversation as resolved.
Show resolved Hide resolved
XChainForwarders.sendMessageCCTP(
address(cctp.L2_MESSENGER()),
hostDomain,
address(moHost),
abi.encodeWithSelector(MessageOrdering.push.selector, 3)
);
XChainForwarders.sendMessageCCTP(
address(cctp.L2_MESSENGER()),
hostDomain,
address(moHost),
abi.encodeWithSelector(MessageOrdering.push.selector, 4)
);
vm.stopPrank();

assertEq(moCCTP.length(), 0);

// Do not relay right away
host.selectFork();

// Queue up two more L1 -> L2 messages
vm.startPrank(l1Authority);
XChainForwarders.sendMessageCircleCCTP(
guestDomain,
address(moCCTP),
abi.encodeWithSelector(MessageOrdering.push.selector, 1)
);
XChainForwarders.sendMessageCircleCCTP(
guestDomain,
address(moCCTP),
abi.encodeWithSelector(MessageOrdering.push.selector, 2)
);
vm.stopPrank();

assertEq(moHost.length(), 0);

cctp.relayFromHost(true);

assertEq(moCCTP.length(), 2);
assertEq(moCCTP.messages(0), 1);
assertEq(moCCTP.messages(1), 2);

cctp.relayToHost(true);

assertEq(moHost.length(), 2);
assertEq(moHost.messages(0), 3);
assertEq(moHost.messages(1), 4);

// Validate the message receiver failure modes
vm.startPrank(notL1Authority);
XChainForwarders.sendMessageCircleCCTP(
guestDomain,
address(moCCTP),
abi.encodeWithSelector(MessageOrdering.push.selector, 999)
);
vm.stopPrank();

vm.expectRevert("Receiver/invalid-l1Authority");
cctp.relayFromHost(true);

cctp.selectFork();
vm.expectRevert("Receiver/invalid-sender");
moCCTP.push(999);

vm.expectRevert("Receiver/invalid-sender");
moCCTP.handleReceiveMessage(0, bytes32(uint256(uint160(l1Authority))), abi.encodeWithSelector(MessageOrdering.push.selector, 999));

assertEq(moCCTP.sourceDomain(), 0);
vm.prank(address(cctp.L2_MESSENGER()));
vm.expectRevert("Receiver/invalid-sourceDomain");
moCCTP.handleReceiveMessage(1, bytes32(uint256(uint160(l1Authority))), abi.encodeWithSelector(MessageOrdering.push.selector, 999));
}

}
Loading