Skip to content

Commit

Permalink
MultiOnRamp - family-agnostic messages (#1140)
Browse files Browse the repository at this point in the history
## Motivation
* Converts the `EVM2EVMMultiOnRamp` to an `EVM2Any` family-agnostic ramp
to support non-EVM use-cases from a single multi-onramp.
* Collapses `sourceTokenData` + `tokenAmounts` into one
`RampTokenAmount` array (simplification to no longer require encoding /
decoding on-chain)

## Solution
* Message format changes: convert `EVM2EVMMessage` to
`EVM2AnyRampMessage`, which contains a more generic `receiver` target
and stores the family-specific args as bytes
* Note: `hash(Any2EVMRampMessage) != hash(EVM2AnyRampMessage)` - the
messages are now asymmetric
* Note: `EVM2AnyRampMessage.messageId = hash(EVM2AnyRampMessage)`, but
`messageId != hash(Any2EVMRampMessage)` due to the message asymmetry
* Add a `familyTag` to the dest chain config, to group multiple chains
under one family to perform validations
* Convert `extraArgs` to bytes, implement conditional flows to retrieve
EVM-specific fields
* Add `RampTokenAmount` which is constructed from
`Client.EVMTokenAmount` on the OnRamp. The struct unrolls
`SourceTokenData` and `EVMTokenAmount` into one struct, no longer
requiring additional decoding
* Out-of-scope: moving family-specific fee & validation logic to
`PriceRegistry` - to be done in next PR

---------

Co-authored-by: app-token-issuer-infra-releng[bot] <120227048+app-token-issuer-infra-releng[bot]@users.noreply.github.com>
Co-authored-by: Makram <[email protected]>
  • Loading branch information
3 people authored Jul 9, 2024
1 parent 6ac3cdd commit 7d7b54a
Show file tree
Hide file tree
Showing 19 changed files with 876 additions and 664 deletions.
456 changes: 234 additions & 222 deletions contracts/gas-snapshots/ccip.gas-snapshot

Large diffs are not rendered by default.

81 changes: 73 additions & 8 deletions contracts/src/v0.8/ccip/libraries/Internal.sol
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ library Internal {
// The source pool address, abi encoded. This value is trusted as it was obtained through the onRamp. It can be
// relied upon by the destination pool to validate the source pool.
bytes sourcePoolAddress;
// The address of the destination token pool, abi encoded in the case of EVM chains
// The address of the destination token, abi encoded in the case of EVM chains
// This value is UNTRUSTED as any pool owner can return whatever value they want.
bytes destTokenAddress;
// Optional pool data to be transferred to the destination chain. Be default this is capped at
Expand Down Expand Up @@ -106,19 +106,31 @@ library Internal {
bytes32 messageId; // a hash of the message data
}

// TODO: create new const for EVM2AnyMessage
/// @dev EVM2EVMMessage struct has 13 fields, including 3 variable arrays.
/// Each variable array takes 1 more slot to store its length.
/// When abi encoded, excluding array contents,
/// EVM2EVMMessage takes up a fixed number of 16 lots, 32 bytes each.
/// For structs that contain arrays, 1 more slot is added to the front, reaching a total of 17.
uint256 public constant MESSAGE_FIXED_BYTES = 32 * 17;

// TODO: create new const for EVM2AnyMessage
/// @dev Each token transfer adds 1 EVMTokenAmount and 1 bytes.
/// When abiEncoded, each EVMTokenAmount takes 2 slots, each bytes takes 2 slots, excl bytes contents
uint256 public constant MESSAGE_FIXED_BYTES_PER_TOKEN = 32 * 4;

/// @dev Any2EVMRampMessage struct has 10 fields, including 3 variable unnested arrays (data, receiver and tokenAmounts).
/// Each variable array takes 1 more slot to store its length.
/// When abi encoded, excluding array contents,
/// Any2EVMMessage takes up a fixed number of 13 slots, 32 bytes each.
/// For structs that contain arrays, 1 more slot is added to the front, reaching a total of 14.
/// The fixed bytes does not cover struct data (this is represented by ANY_2_EVM_MESSAGE_FIXED_BYTES_PER_TOKEN)
uint256 public constant ANY_2_EVM_MESSAGE_FIXED_BYTES = 32 * 14;

/// @dev Each token transfer adds 1 RampTokenAmount
/// RampTokenAmount has 4 fields, including 3 bytes.
/// Each bytes takes 1 more slot to store its length.
/// When abi encoded, each token transfer takes up 7 slots, excl bytes contents.
uint256 public constant ANY_2_EVM_MESSAGE_FIXED_BYTES_PER_TOKEN = 32 * 7;

function _toAny2EVMMessage(
EVM2EVMMessage memory original,
Client.EVMTokenAmount[] memory destTokenAmounts
Expand Down Expand Up @@ -167,6 +179,7 @@ library Internal {
}

bytes32 internal constant ANY_2_EVM_MESSAGE_HASH = keccak256("Any2EVMMessageHashV1");
bytes32 internal constant EVM_2_ANY_MESSAGE_HASH = keccak256("EVM2AnyMessageHashV1");

/// @dev Used to hash messages for multi-lane family-agnostic OffRamps.
/// OnRamp hash(EVM2AnyMessage) != Any2EVMRampMessage.messageId
Expand Down Expand Up @@ -197,8 +210,31 @@ library Internal {
)
),
keccak256(original.data),
keccak256(abi.encode(original.tokenAmounts))
)
);
}

function _hash(EVM2AnyRampMessage memory original, bytes32 metadataHash) internal pure returns (bytes32) {
// Fixed-size message fields are included in nested hash to reduce stack pressure.
// This hashing scheme is also used by RMN. If changing it, please notify the RMN maintainers.
return keccak256(
abi.encode(
MerkleMultiProof.LEAF_DOMAIN_SEPARATOR,
metadataHash,
keccak256(
abi.encode(
original.sender,
original.receiver,
original.header.sequenceNumber,
original.header.nonce,
original.feeToken,
original.feeTokenAmount
)
),
keccak256(original.data),
keccak256(abi.encode(original.tokenAmounts)),
keccak256(abi.encode(original.sourceTokenData))
keccak256(original.extraArgs)
)
);
}
Expand Down Expand Up @@ -246,9 +282,23 @@ library Internal {
Execution
}

/// @notice Family-agnostic token amounts used for both OnRamp & OffRamp messages
struct RampTokenAmount {
// The source pool address, abi encoded. This value is trusted as it was obtained through the onRamp. It can be
// relied upon by the destination pool to validate the source pool.
bytes sourcePoolAddress;
// The address of the destination token, abi encoded in the case of EVM chains
// This value is UNTRUSTED as any pool owner can return whatever value they want.
bytes destTokenAddress;
// Optional pool data to be transferred to the destination chain. Be default this is capped at
// CCIP_LOCK_OR_BURN_V1_RET_BYTES bytes. If more data is required, the TokenTransferFeeConfig.destBytesOverhead
// has to be set for the specific token.
bytes extraData;
uint256 amount; // Amount of tokens.
}

/// @notice Family-agnostic header for OnRamp & OffRamp messages.
/// The messageId is not expected to match hash(message), since it may originate from another ramp family
// TODO: revisit if destChainSelector is required (likely sufficient to have it implicitly in the commit roots)
struct RampMessageHeader {
bytes32 messageId; // Unique identifier for the message, generated with the source chain's encoding scheme (i.e. not necessarily abi.encoded)
uint64 sourceChainSelector; // ───────╮ the chain selector of the source chain, note: not chainId
Expand All @@ -266,8 +316,23 @@ library Internal {
bytes data; // arbitrary data payload supplied by the message sender
address receiver; // receiver address on the destination chain
uint256 gasLimit; // user supplied maximum gas amount available for dest chain execution
// TODO: revisit collapsing tokenAmounts + sourceTokenData into one struct array
Client.EVMTokenAmount[] tokenAmounts; // array of tokens and amounts to transfer
bytes[] sourceTokenData; // array of token data, one per token
RampTokenAmount[] tokenAmounts; // array of tokens and amounts to transfer
}

/// @notice Family-agnostic message emitted from the OnRamp
/// Note: hash(Any2EVMRampMessage) != hash(EVM2AnyRampMessage) due to encoding & parameter differences
/// messageId = hash(EVM2AnyRampMessage) using the source EVM chain's encoding format
struct EVM2AnyRampMessage {
RampMessageHeader header; // Message header
address sender; // sender address on the source chain
bytes data; // arbitrary data payload supplied by the message sender
bytes receiver; // receiver address on the destination chain
bytes extraArgs; // destination-chain specific extra args, such as the gasLimit for EVM chains
address feeToken; // fee token
uint256 feeTokenAmount; // fee token amount
RampTokenAmount[] tokenAmounts; // array of tokens and amounts to transfer
}

// bytes4(keccak256("CCIP ChainFamilySelector EVM"))
bytes4 public constant CHAIN_FAMILY_SELECTOR_EVM = 0x2812d52c;
}
38 changes: 11 additions & 27 deletions contracts/src/v0.8/ccip/offRamp/EVM2EVMMultiOffRamp.sol
Original file line number Diff line number Diff line change
Expand Up @@ -497,12 +497,7 @@ contract EVM2EVMMultiOffRamp is ITypeAndVersion, MultiOCR3Base {
Client.EVMTokenAmount[] memory destTokenAmounts = new Client.EVMTokenAmount[](0);
if (message.tokenAmounts.length > 0) {
destTokenAmounts = _releaseOrMintTokens(
message.tokenAmounts,
message.sender,
message.receiver,
message.header.sourceChainSelector,
message.sourceTokenData,
offchainTokenData
message.tokenAmounts, message.sender, message.receiver, message.header.sourceChainSelector, offchainTokenData
);
}

Expand Down Expand Up @@ -787,25 +782,22 @@ contract EVM2EVMMultiOffRamp is ITypeAndVersion, MultiOCR3Base {
/// @dev The local token address is validated through the TokenAdminRegistry. If, due to some misconfiguration, the
/// token is unknown to the registry, the offRamp will revert. The tx, and the tokens, can be retrieved by
/// registering the token on this chain, and re-trying the msg.
/// @param sourceAmount The amount of tokens to be released/minted.
/// @param sourceTokenAmount Amount and source data of the token to be released/minted.
/// @param originalSender The message sender on the source chain.
/// @param receiver The address that will receive the tokens.
/// @param sourceChainSelector The remote source chain selector
/// @param sourceTokenData A struct containing the local token address, the source pool address and optional data
/// returned from the source pool.
/// @param offchainTokenData Data fetched offchain by the DON.
/// @return destTokenAmount local token address with amount
function _releaseOrMintSingleToken(
uint256 sourceAmount,
Internal.RampTokenAmount memory sourceTokenAmount,
bytes memory originalSender,
address receiver,
uint64 sourceChainSelector,
Internal.SourceTokenData memory sourceTokenData,
bytes memory offchainTokenData
) internal returns (Client.EVMTokenAmount memory destTokenAmount) {
// We need to safely decode the token address from the sourceTokenData, as it could be wrong,
// in which case it doesn't have to be a valid EVM address.
address localToken = Internal._validateEVMAddress(sourceTokenData.destTokenAddress);
address localToken = Internal._validateEVMAddress(sourceTokenAmount.destTokenAddress);
// We check with the token admin registry if the token has a pool on this chain.
address localPoolAddress = ITokenAdminRegistry(i_tokenAdminRegistry).getPool(localToken);
// This will call the supportsInterface through the ERC165Checker, and not directly on the pool address.
Expand All @@ -827,11 +819,11 @@ contract EVM2EVMMultiOffRamp is ITypeAndVersion, MultiOCR3Base {
Pool.ReleaseOrMintInV1({
originalSender: originalSender,
receiver: receiver,
amount: sourceAmount,
amount: sourceTokenAmount.amount,
localToken: localToken,
remoteChainSelector: sourceChainSelector,
sourcePoolAddress: sourceTokenData.sourcePoolAddress,
sourcePoolData: sourceTokenData.extraData,
sourcePoolAddress: sourceTokenAmount.sourcePoolAddress,
sourcePoolData: sourceTokenAmount.extraData,
offchainTokenData: offchainTokenData
})
),
Expand Down Expand Up @@ -866,34 +858,26 @@ contract EVM2EVMMultiOffRamp is ITypeAndVersion, MultiOCR3Base {
}

/// @notice Uses pools to release or mint a number of different tokens to a receiver address.
/// @param sourceTokenAmounts List of tokens and amount values to be released/minted.
/// @param sourceTokenAmounts List of token amounts with source data of the tokens to be released/minted.
/// @param originalSender The message sender on the source chain.
/// @param receiver The address that will receive the tokens.
/// @param sourceChainSelector The remote source chain selector
/// @param encodedSourceTokenData Encoded source token data, decoding to Internal.SourceTokenData
/// @param offchainTokenData Array of token data fetched offchain by the DON.
/// @return destTokenAmounts local token addresses with amounts
/// @dev This function wrappes the token pool call in a try catch block to gracefully handle
/// any non-rate limiting errors that may occur. If we encounter a rate limiting related error
/// we bubble it up. If we encounter a non-rate limiting error we wrap it in a TokenHandlingError.
function _releaseOrMintTokens(
Client.EVMTokenAmount[] memory sourceTokenAmounts,
Internal.RampTokenAmount[] memory sourceTokenAmounts,
bytes memory originalSender,
address receiver,
uint64 sourceChainSelector,
bytes[] memory encodedSourceTokenData,
bytes[] memory offchainTokenData
) internal returns (Client.EVMTokenAmount[] memory destTokenAmounts) {
// Creating a copy is more gas efficient than initializing a new array.
destTokenAmounts = sourceTokenAmounts;
destTokenAmounts = new Client.EVMTokenAmount[](sourceTokenAmounts.length);
for (uint256 i = 0; i < sourceTokenAmounts.length; ++i) {
destTokenAmounts[i] = _releaseOrMintSingleToken(
sourceTokenAmounts[i].amount,
originalSender,
receiver,
sourceChainSelector,
abi.decode(encodedSourceTokenData[i], (Internal.SourceTokenData)),
offchainTokenData[i]
sourceTokenAmounts[i], originalSender, receiver, sourceChainSelector, offchainTokenData[i]
);
}

Expand Down
Loading

0 comments on commit 7d7b54a

Please sign in to comment.