Skip to content
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
44 changes: 19 additions & 25 deletions solidity/contracts/token/TokenBridgeCctpBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {TypeCasts} from "../libs/TypeCasts.sol";
import {MovableCollateralRouter, MovableCollateralRouterStorage} from "./libs/MovableCollateralRouter.sol";
import {TokenRouter} from "./libs/TokenRouter.sol";
import {AbstractPostDispatchHook} from "../hooks/libs/AbstractPostDispatchHook.sol";
import {AbstractMessageIdAuthorizedIsm} from "../isms/hook/AbstractMessageIdAuthorizedIsm.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

Expand All @@ -41,22 +40,11 @@ struct Domain {
uint32 circle;
}

// need intermediate contract to insert slots between TokenBridgeCctpBase and AbstractMessageIdAuthorizedIsm
abstract contract TokenBridgeCctpIntermediateStorage is
// see ./CCTP.md for sequence diagrams of the destination chain control flow
abstract contract TokenBridgeCctpBase is
TokenBridgeCctpBaseStorage,
AbstractCcipReadIsm,
AbstractPostDispatchHook
{
/// @notice Hyperlane domain => Domain struct.
/// We use a struct to avoid ambiguity with domain 0 being unknown.
mapping(uint32 hypDomain => Domain circleDomain)
internal _hyperlaneDomainMap;
}

// see ./CCTP.md for sequence diagrams of the destination chain control flow
abstract contract TokenBridgeCctpBase is
TokenBridgeCctpIntermediateStorage,
AbstractMessageIdAuthorizedIsm
{
using Message for bytes;
using TypeCasts for bytes32;
Expand All @@ -72,11 +60,20 @@ abstract contract TokenBridgeCctpBase is
// @notice CCTP token messenger contract
ITokenMessenger public immutable tokenMessenger;

/// @notice Hyperlane domain => Domain struct.
/// We use a struct to avoid ambiguity with domain 0 being unknown.
mapping(uint32 hypDomain => Domain circleDomain)
internal _hyperlaneDomainMap;

/// @notice Circle domain => Domain struct.
// We use a struct to avoid ambiguity with domain 0 being unknown.
mapping(uint32 circleDomain => Domain hyperlaneDomain)
internal _circleDomainMap;

/// @notice Maps messageId to whether or not the message has been verified
/// by the CCTP message transmitter
mapping(bytes32 messageId => bool) public isVerified;

/**
* @notice Emitted when the Hyperlane domain to Circle domain mapping is updated.
* @param hyperlaneDomain The Hyperlane domain.
Expand Down Expand Up @@ -252,13 +249,9 @@ abstract contract TokenBridgeCctpBase is
function verify(
bytes calldata _metadata,
bytes calldata _hyperlaneMessage
)
external
override(AbstractMessageIdAuthorizedIsm, IInterchainSecurityModule)
returns (bool)
{
) external returns (bool) {
// check if hyperlane message has already been verified by CCTP
if (isVerified(_hyperlaneMessage)) {
if (isVerified[_hyperlaneMessage.id()]) {
return true;
}

Expand Down Expand Up @@ -299,22 +292,23 @@ abstract contract TokenBridgeCctpBase is
bytes32 circleSender,
bytes32 messageId
) internal returns (bool) {
require(
msg.sender == address(messageTransmitter),
"Not message transmitter"
);

// ensure that the message was sent from the hook on the origin chain
uint32 origin = circleDomainToHyperlaneDomain(circleSource);
require(
_mustHaveRemoteRouter(origin) == circleSender,
"Unauthorized circle sender"
);

preVerifyMessage(messageId, 0);
isVerified[messageId] = true;

return true;
}

function _isAuthorized() internal view override returns (bool) {
return msg.sender == address(messageTransmitter);
}

function _offchainLookupCalldata(
bytes calldata _message
) internal pure override returns (bytes memory) {
Expand Down
73 changes: 9 additions & 64 deletions solidity/test/token/TokenBridgeCctp.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -928,7 +928,7 @@ contract TokenBridgeCctpV1Test is Test {

assertTrue(result);
assertTrue(
TokenBridgeCctpBase(address(tbDestination)).isVerified(message)
TokenBridgeCctpBase(address(tbDestination)).isVerified(messageId)
);
}

Expand All @@ -950,9 +950,7 @@ contract TokenBridgeCctpV1Test is Test {
) public virtual {
// Try to call from a non-message-transmitter address
vm.prank(evil);
vm.expectRevert(
bytes("AbstractMessageIdAuthorizedIsm: sender is not the hook")
);
vm.expectRevert(bytes("Not message transmitter"));
TokenBridgeCctpV1(address(tbDestination)).handleReceiveMessage(
cctpOrigin,
address(tbOrigin).addressToBytes32(),
Expand Down Expand Up @@ -993,29 +991,6 @@ contract TokenBridgeCctpV1Test is Test {
);
}

function test_handleReceiveMessage_revertsWhen_messageAlreadyDelivered(
bytes32 messageId
) public virtual {
// First delivery succeeds
vm.prank(address(messageTransmitterDestination));
TokenBridgeCctpV1(address(tbDestination)).handleReceiveMessage(
cctpOrigin,
address(tbOrigin).addressToBytes32(),
abi.encode(messageId)
);

// Second delivery of the same message should revert
vm.prank(address(messageTransmitterDestination));
vm.expectRevert(
bytes("AbstractMessageIdAuthorizedIsm: message already verified")
);
TokenBridgeCctpV1(address(tbDestination)).handleReceiveMessage(
cctpOrigin,
address(tbOrigin).addressToBytes32(),
abi.encode(messageId)
);
}

function test_verify_returnsTrue_afterDirectDelivery(
bytes calldata message
) public virtual {
Expand All @@ -1033,7 +1008,7 @@ contract TokenBridgeCctpV1Test is Test {

// Verify the message is marked as verified
assertTrue(
TokenBridgeCctpBase(address(tbDestination)).isVerified(message)
TokenBridgeCctpBase(address(tbDestination)).isVerified(messageId)
);

// Now call verify with empty metadata - should return true without attestation
Expand Down Expand Up @@ -1645,7 +1620,7 @@ contract TokenBridgeCctpV2Test is TokenBridgeCctpV1Test {

assertTrue(result);
assertTrue(
TokenBridgeCctpBase(address(tbDestination)).isVerified(message)
TokenBridgeCctpBase(address(tbDestination)).isVerified(messageId)
);
}

Expand All @@ -1668,9 +1643,7 @@ contract TokenBridgeCctpV2Test is TokenBridgeCctpV1Test {
uint32 finalityThreshold
) public {
vm.prank(evil);
vm.expectRevert(
bytes("AbstractMessageIdAuthorizedIsm: sender is not the hook")
);
vm.expectRevert(bytes("Not message transmitter"));
TokenBridgeCctpV2(address(tbDestination)).handleReceiveFinalizedMessage(
cctpOrigin,
address(tbOrigin).addressToBytes32(),
Expand Down Expand Up @@ -1733,7 +1706,7 @@ contract TokenBridgeCctpV2Test is TokenBridgeCctpV1Test {

assertTrue(result);
assertTrue(
TokenBridgeCctpBase(address(tbDestination)).isVerified(message)
TokenBridgeCctpBase(address(tbDestination)).isVerified(messageId)
);
}

Expand All @@ -1757,9 +1730,7 @@ contract TokenBridgeCctpV2Test is TokenBridgeCctpV1Test {
uint32 finalityThreshold
) public {
vm.prank(evil);
vm.expectRevert(
bytes("AbstractMessageIdAuthorizedIsm: sender is not the hook")
);
vm.expectRevert(bytes("Not message transmitter"));
TokenBridgeCctpV2(address(tbDestination))
.handleReceiveUnfinalizedMessage(
cctpOrigin,
Expand Down Expand Up @@ -1806,32 +1777,6 @@ contract TokenBridgeCctpV2Test is TokenBridgeCctpV1Test {
);
}

function test_handleReceiveMessage_revertsWhen_messageAlreadyDelivered(
bytes32 messageId
) public override {
uint32 finalityThreshold = 2000;
// First delivery succeeds
vm.prank(address(messageTransmitterDestination));
TokenBridgeCctpV2(address(tbDestination)).handleReceiveFinalizedMessage(
cctpOrigin,
address(tbOrigin).addressToBytes32(),
finalityThreshold,
abi.encode(messageId)
);

// Second delivery of the same message should revert
vm.prank(address(messageTransmitterDestination));
vm.expectRevert(
bytes("AbstractMessageIdAuthorizedIsm: message already verified")
);
TokenBridgeCctpV2(address(tbDestination)).handleReceiveFinalizedMessage(
cctpOrigin,
address(tbOrigin).addressToBytes32(),
finalityThreshold,
abi.encode(messageId)
);
}

function test_verify_returnsTrue_afterDirectDelivery(
bytes calldata message
) public override {
Expand All @@ -1852,7 +1797,7 @@ contract TokenBridgeCctpV2Test is TokenBridgeCctpV1Test {

// Verify the message is marked as verified
assertTrue(
TokenBridgeCctpBase(address(tbDestination)).isVerified(message)
TokenBridgeCctpBase(address(tbDestination)).isVerified(messageId)
);

// Now call verify with empty metadata - should return true without attestation
Expand Down Expand Up @@ -1880,7 +1825,7 @@ contract TokenBridgeCctpV2Test is TokenBridgeCctpV1Test {

// Verify the message is marked as verified
assertTrue(
TokenBridgeCctpBase(address(tbDestination)).isVerified(message)
TokenBridgeCctpBase(address(tbDestination)).isVerified(messageId)
);

// Now call verify with empty metadata - should return true without attestation
Expand Down
Loading