Skip to content

Commit

Permalink
chore: update typeAndVersion for multi ramps
Browse files Browse the repository at this point in the history
  • Loading branch information
RayXpub committed May 3, 2024
1 parent ccaa803 commit db210c3
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 51 deletions.
67 changes: 37 additions & 30 deletions contracts/src/v0.8/ccip/offRamp/EVM2EVMMultiOffRamp.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {EnumerableMapAddresses} from "../../shared/enumerable/EnumerableMapAddre
import {AggregateRateLimiter} from "../AggregateRateLimiter.sol";
import {Client} from "../libraries/Client.sol";
import {Internal} from "../libraries/Internal.sol";
import {Pool} from "../libraries/Pool.sol";
import {RateLimiter} from "../libraries/RateLimiter.sol";
import {OCR2BaseNoChecks} from "../ocr/OCR2BaseNoChecks.sol";

Expand Down Expand Up @@ -51,7 +52,7 @@ contract EVM2EVMMultiOffRamp is IAny2EVMOffRamp, AggregateRateLimiter, ITypeAndV
error EmptyReport();
error BadARMSignal();
error InvalidMessageId();
error InvalidEVMAddress(uint256 encodedAddress);
error NotACompatiblePool(address notPool);
error InvalidDataLength(uint256 expected, uint256 got);
error InvalidNewState(uint64 sequenceNumber, Internal.MessageExecutionState newState);
error IndexOutOfRange();
Expand Down Expand Up @@ -96,9 +97,7 @@ contract EVM2EVMMultiOffRamp is IAny2EVMOffRamp, AggregateRateLimiter, ITypeAndV
}

// STATIC CONFIG
string public constant override typeAndVersion = "EVM2EVMOffRamp 1.5.0-dev";
/// @dev The expected length of the return data from a pool release or mint call.
uint256 internal constant EXPECTED_POOL_RETURN_DATA_LENGTH = 64;
string public constant override typeAndVersion = "EVM2EVMMultiOffRamp 1.6.0-dev";

/// @dev Commit store address on the destination chain
address internal immutable i_commitStore;
Expand Down Expand Up @@ -398,8 +397,8 @@ contract EVM2EVMMultiOffRamp is IAny2EVMOffRamp, AggregateRateLimiter, ITypeAndV
catch (bytes memory err) {
if (
ReceiverError.selector == bytes4(err) || TokenHandlingError.selector == bytes4(err)
|| InvalidEVMAddress.selector == bytes4(err) || InvalidDataLength.selector == bytes4(err)
|| CallWithExactGas.NoContract.selector == bytes4(err)
|| Internal.InvalidEVMAddress.selector == bytes4(err) || InvalidDataLength.selector == bytes4(err)
|| CallWithExactGas.NoContract.selector == bytes4(err) || NotACompatiblePool.selector == bytes4(err)
) {
// If CCIP receiver execution is not successful, bubble up receiver revert data,
// prepended by the 4 bytes of ReceiverError.selector, TokenHandlingError.selector or InvalidPoolAddress.selector.
Expand Down Expand Up @@ -429,8 +428,17 @@ contract EVM2EVMMultiOffRamp is IAny2EVMOffRamp, AggregateRateLimiter, ITypeAndV
message.tokenAmounts, abi.encode(message.sender), message.receiver, message.sourceTokenData, offchainTokenData
);
}
// There are three cases in which we skip calling the receiver:
// 1. If the message data is empty AND the gas limit is 0.
// This indicates a message that only transfers tokens. It is valid to only send tokens to a contract
// that supports the IAny2EVMMessageReceiver interface, but without this first check we would call the
// receiver without any gas, which would revert the transaction.
// 2. If the receiver is not a contract.
// 3. If the receiver is a contract but it does not support the IAny2EVMMessageReceiver interface.
//
// The ordering of these checks is important, as the first check is the cheapest to execute.
if (
message.receiver.code.length == 0
(message.data.length == 0 && message.gasLimit == 0) || message.receiver.code.length == 0
|| !message.receiver.supportsInterface(type(IAny2EVMMessageReceiver).interfaceId)
) return;

Expand Down Expand Up @@ -554,11 +562,17 @@ contract EVM2EVMMultiOffRamp is IAny2EVMOffRamp, AggregateRateLimiter, ITypeAndV
for (uint256 i = 0; i < sourceTokenAmounts.length; ++i) {
// This should never revert as the onRamp creates the sourceTokenData. Only the inner components from
// this struct come from untrusted sources.
IPool.SourceTokenData memory sourceTokenData = abi.decode(encodedSourceTokenData[i], (IPool.SourceTokenData));
Internal.SourceTokenData memory sourceTokenData =
abi.decode(encodedSourceTokenData[i], (Internal.SourceTokenData));
// We need to safely decode the pool address from the sourceTokenData, as it could be wrong,
// in which case it doesn't have to be a valid EVM address.
if (sourceTokenData.destPoolAddress.length != 32) {
revert InvalidDataLength(32, sourceTokenData.destPoolAddress.length);
address localPoolAddress = Internal._validateEVMAddress(sourceTokenData.destPoolAddress);
// This will call the supportsInterface through the ERC165Checker, and not directly on the pool address.
// This is done to prevent a pool from reverting the entire transaction if it doesn't support the interface.
// The call gets a max or 30k gas per instance, of which there are three. This means gas estimations should
// account for 90k gas overhead due to the interface check.
if (!localPoolAddress.supportsInterface(Pool.CCIP_POOL_V1)) {
revert NotACompatiblePool(localPoolAddress);
}

// We determined that the pool address is a valid EVM address, but that does not mean the code at this
Expand All @@ -569,14 +583,17 @@ contract EVM2EVMMultiOffRamp is IAny2EVMOffRamp, AggregateRateLimiter, ITypeAndV
(bool success, bytes memory returnData,) = CallWithExactGas._callWithExactGasSafeReturnData(
abi.encodeWithSelector(
IPool.releaseOrMint.selector,
originalSender,
receiver,
sourceTokenAmounts[i].amount,
i_sourceChainSelector,
sourceTokenData,
offchainTokenData[i]
Pool.ReleaseOrMintInV1({
originalSender: originalSender,
receiver: receiver,
amount: sourceTokenAmounts[i].amount,
remoteChainSelector: i_sourceChainSelector,
sourcePoolAddress: sourceTokenData.sourcePoolAddress,
sourcePoolData: sourceTokenData.extraData,
offchainTokenData: offchainTokenData[i]
})
),
_validateEVMAddress(abi.decode(sourceTokenData.destPoolAddress, (uint256))),
localPoolAddress,
s_dynamicConfig.maxPoolReleaseOrMintGas,
Internal.GAS_FOR_CALL_EXACT_CHECK,
Internal.MAX_RET_BYTES
Expand All @@ -586,11 +603,11 @@ contract EVM2EVMMultiOffRamp is IAny2EVMOffRamp, AggregateRateLimiter, ITypeAndV
if (!success) revert TokenHandlingError(returnData);

// If the call was successful, the returnData should be the local token address.
if (returnData.length != EXPECTED_POOL_RETURN_DATA_LENGTH) {
revert InvalidDataLength(EXPECTED_POOL_RETURN_DATA_LENGTH, returnData.length);
if (returnData.length != Pool.CCIP_POOL_V1_RET_BYTES) {
revert InvalidDataLength(Pool.CCIP_POOL_V1_RET_BYTES, returnData.length);
}
(uint256 decodedAddress, uint256 amount) = abi.decode(returnData, (uint256, uint256));
destTokenAmounts[i].token = _validateEVMAddress(decodedAddress);
destTokenAmounts[i].token = Internal._validateEVMAddressFromUint256(decodedAddress);
destTokenAmounts[i].amount = amount;

if (s_rateLimitedTokensDestToSource.contains(destTokenAmounts[i].token)) {
Expand All @@ -603,16 +620,6 @@ contract EVM2EVMMultiOffRamp is IAny2EVMOffRamp, AggregateRateLimiter, ITypeAndV
return destTokenAmounts;
}

/// @notice This methods provides validation for parsing abi encoded addresses by ensuring the
/// address is within the EVM address space. If it isn't it will revert with an InvalidEVMAddress error, which
/// we can catch and handle more gracefully than a revert from abi.decode.
/// @param decodedAddress The address to validate, decoded into a uint256.
/// @return The address if it is valid, the function will revert otherwise.
function _validateEVMAddress(uint256 decodedAddress) internal pure returns (address) {
if (decodedAddress > type(uint160).max || decodedAddress < 10) revert InvalidEVMAddress(decodedAddress);
return address(uint160(decodedAddress));
}

// ================================================================
// │ Access and ARM │
// ================================================================
Expand Down
44 changes: 23 additions & 21 deletions contracts/src/v0.8/ccip/onRamp/EVM2EVMMultiOnRamp.sol
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ contract EVM2EVMMultiOnRamp is IEVM2AnyOnRamp, ILinkAvailable, AggregateRateLimi
error MessageTooLarge(uint256 maxSize, uint256 actualSize);
error MessageGasLimitTooHigh();
error UnsupportedNumberOfTokens();
error UnsupportedToken(IERC20 token);
error UnsupportedToken(address token);
error MustBeCalledByRouter();
error RouterMustSetOriginalSender();
error InvalidConfig();
Expand Down Expand Up @@ -145,7 +145,7 @@ contract EVM2EVMMultiOnRamp is IEVM2AnyOnRamp, ILinkAvailable, AggregateRateLimi
}

// STATIC CONFIG
string public constant override typeAndVersion = "EVM2EVMOnRamp 1.5.0-dev";
string public constant override typeAndVersion = "EVM2EVMMultiOnRamp 1.6.0-dev";
/// @dev metadataHash is a lane-specific prefix for a message hash preimage which ensures global uniqueness
/// Ensures that 2 identical messages sent to 2 different lanes will have a distinct hash.
/// Must match the metadataHash used in computing leaf hashes offchain for the root committed in
Expand Down Expand Up @@ -262,13 +262,6 @@ contract EVM2EVMMultiOnRamp is IEVM2AnyOnRamp, ILinkAvailable, AggregateRateLimi
if (msg.sender != s_dynamicConfig.router) revert MustBeCalledByRouter();
if (destChainSelector != i_destChainSelector) revert InvalidChainSelector(destChainSelector);

// EVM destination addresses should be abi encoded and therefore always 32 bytes long
// Not duplicately validated in `getFee`. Invalid address is uncommon, gas cost outweighs UX gain.
if (message.receiver.length != 32) revert InvalidAddress(message.receiver);
uint256 decodedReceiver = abi.decode(message.receiver, (uint256));
// We want to disallow sending to address(0) and to precompiles, which exist on address(1) through address(9).
if (decodedReceiver > type(uint160).max || decodedReceiver < 10) revert InvalidAddress(message.receiver);

uint256 gasLimit = _fromBytes(message.extraArgs).gasLimit;
// Validate the message with various checks
uint256 numberOfTokens = message.tokenAmounts.length;
Expand Down Expand Up @@ -309,7 +302,9 @@ contract EVM2EVMMultiOnRamp is IEVM2AnyOnRamp, ILinkAvailable, AggregateRateLimi
Internal.EVM2EVMMessage memory newMessage = Internal.EVM2EVMMessage({
sourceChainSelector: i_chainSelector,
sender: originalSender,
receiver: address(uint160(decodedReceiver)),
// EVM destination addresses should be abi encoded and therefore always 32 bytes long
// Not duplicately validated in `getFee`. Invalid address is uncommon, gas cost outweighs UX gain.
receiver: Internal._validateEVMAddress(message.receiver),
sequenceNumber: ++s_sequenceNumber,
gasLimit: gasLimit,
strict: false,
Expand All @@ -327,27 +322,34 @@ contract EVM2EVMMultiOnRamp is IEVM2AnyOnRamp, ILinkAvailable, AggregateRateLimi
for (uint256 i = 0; i < numberOfTokens; ++i) {
Client.EVMTokenAmount memory tokenAndAmount = message.tokenAmounts[i];
IPool sourcePool = getPoolBySourceToken(destChainSelector, IERC20(tokenAndAmount.token));
bytes memory encodedReturnData = sourcePool.lockOrBurn(
originalSender,
message.receiver,
tokenAndAmount.amount,
i_destChainSelector,
bytes("") // any future extraArgs component would be added here
// We don't have to check if it supports the pool version in a non-reverting way here because
// if we revert here, there is no effect on CCIP. Therefore we directly call the supportsInterface
// function and not through the ERC165Checker.
if (address(sourcePool) == address(0) || !sourcePool.supportsInterface(Pool.CCIP_POOL_V1)) {
revert UnsupportedToken(tokenAndAmount.token);
}

Pool.LockOrBurnOutV1 memory poolReturnData = sourcePool.lockOrBurn(
Pool.LockOrBurnInV1({
originalSender: originalSender,
receiver: message.receiver,
amount: tokenAndAmount.amount,
remoteChainSelector: i_destChainSelector
})
);

Pool.PoolReturnDataV1 memory poolReturnData = Pool._decodePoolReturnDataV1(encodedReturnData);
// Since the DON has to pay for the extraData to be included on the destination chain, we cap the length of the extraData.
// This prevents gas bomb attacks on the NOPs. We use destBytesOverhead as a proxy to cap the number of bytes we accept.
// As destBytesOverhead accounts for extraData + offchainData, this caps the worst case abuse to the number of bytes reserved for offchainData.
// It therefore fully mitigates gas bombs for most tokens, as most tokens don't use offchainData.
if (poolReturnData.destPoolData.length > s_tokenTransferFeeConfig[tokenAndAmount.token].destBytesOverhead) {
revert SourceTokenDataTooLarge(tokenAndAmount.token);
}
// Since this is an EVM2EVM message, the pool address should be exactly 32 bytes
if (poolReturnData.destPoolAddress.length != 32) revert InvalidAddress(poolReturnData.destPoolAddress);
// We validate the pool address to ensure it is a valid EVM address
Internal._validateEVMAddress(poolReturnData.destPoolAddress);

newMessage.sourceTokenData[i] = abi.encode(
IPool.SourceTokenData({
Internal.SourceTokenData({
sourcePoolAddress: abi.encode(sourcePool),
destPoolAddress: poolReturnData.destPoolAddress,
extraData: poolReturnData.destPoolData
Expand Down Expand Up @@ -583,7 +585,7 @@ contract EVM2EVMMultiOnRamp is IEVM2AnyOnRamp, ILinkAvailable, AggregateRateLimi

// Validate if the token is supported, do not calculate fee for unsupported tokens.
if (address(getPoolBySourceToken(i_destChainSelector, IERC20(tokenAmount.token))) == address(0)) {
revert UnsupportedToken(IERC20(tokenAmount.token));
revert UnsupportedToken(tokenAmount.token);
}

TokenTransferFeeConfig memory transferFeeConfig = s_tokenTransferFeeConfig[tokenAmount.token];
Expand Down

0 comments on commit db210c3

Please sign in to comment.