Skip to content

Commit

Permalink
Merge branch 'ccip-develop' into CCIP-1039-Publish-test-image-on-each…
Browse files Browse the repository at this point in the history
…-release-with-release-tag
  • Loading branch information
jasonmci authored Oct 12, 2023
2 parents d47b37c + e2fd8cb commit 25a5afb
Show file tree
Hide file tree
Showing 36 changed files with 5,742 additions and 270 deletions.
1 change: 0 additions & 1 deletion contracts/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ solc_version = '0.8.15'
src = 'src/v0.8/metatx'
test = 'src/v0.8/metatx/test'
optimizer_runs = 26_000
deny_warnings = true

[profile.functions]
solc_version = '0.8.19'
Expand Down
161 changes: 83 additions & 78 deletions contracts/gas-snapshots/ccip.gas-snapshot

Large diffs are not rendered by default.

59 changes: 11 additions & 48 deletions contracts/src/v0.8/ccip/Router.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {IWrappedNative} from "./interfaces/IWrappedNative.sol";
import {IAny2EVMMessageReceiver} from "./interfaces/IAny2EVMMessageReceiver.sol";

import {Client} from "./libraries/Client.sol";
import {Internal} from "./libraries/Internal.sol";
import {CallWithExactGas} from "./libraries/CallWithExactGas.sol";
import {OwnerIsCreator} from "./../shared/access/OwnerIsCreator.sol";

import {EnumerableMap} from "../vendor/openzeppelin-solidity/v4.8.0/contracts/utils/structs/EnumerableMap.sol";
Expand Down Expand Up @@ -149,13 +151,7 @@ contract Router is IRouter, IRouterClient, ITypeAndVersion, OwnerIsCreator {
// ================================================================

/// @inheritdoc IRouter
/// @dev Handles the edge case where we want to pass a specific amount of gas,
/// @dev but EIP-150 sends all but 1/64 of the remaining gas instead so the user gets
/// @dev less gas than they paid for. The other 2 parts of EIP-150 do not apply since
/// @dev a) we hard code value=0 and b) we ensure code already exists.
/// @dev If we revert instead, then that will never happen.
/// @dev Separately we capture the return data up to a maximum size to avoid return bombs,
/// @dev borrowed from https://github.com/nomad-xyz/ExcessivelySafeCall/blob/main/src/ExcessivelySafeCall.sol.
/// @dev _callWithExactGas protects against return data bombs by capping the return data size at MAX_RET_BYTES.
function routeMessage(
Client.Any2EVMMessage calldata message,
uint16 gasForCallExactCheck,
Expand All @@ -171,48 +167,15 @@ contract Router is IRouter, IRouterClient, ITypeAndVersion, OwnerIsCreator {
// We encode here instead of the offRamps to constrain specifically what functions
// can be called from the router.
bytes memory data = abi.encodeWithSelector(IAny2EVMMessageReceiver.ccipReceive.selector, message);
// allocate retData memory ahead of time
retData = new bytes(MAX_RET_BYTES);

// solhint-disable-next-line no-inline-assembly
assembly {
// solidity calls check that a contract actually exists at the destination, so we do the same
// Note we do this check prior to measuring gas so gasForCallExactCheck (our "cushion")
// doesn't need to account for it.
if iszero(extcodesize(receiver)) {
revert(0, 0)
}

let g := gas()
// Compute g -= gasForCallExactCheck and check for underflow
// The gas actually passed to the callee is _min(gasAmount, 63//64*gas available).
// We want to ensure that we revert if gasAmount > 63//64*gas available
// as we do not want to provide them with less, however that check itself costs
// gas. gasForCallExactCheck ensures we have at least enough gas to be able
// to revert if gasAmount > 63//64*gas available.
if lt(g, gasForCallExactCheck) {
revert(0, 0)
}
g := sub(g, gasForCallExactCheck)
// if g - g//64 <= gasAmount, revert
// (we subtract g//64 because of EIP-150)
if iszero(gt(sub(g, div(g, 64)), gasLimit)) {
revert(0, 0)
}
// call and return whether we succeeded. ignore return data
// call(gas,addr,value,argsOffset,argsLength,retOffset,retLength)
success := call(gasLimit, receiver, 0, add(data, 0x20), mload(data), 0, 0)

// limit our copy to MAX_RET_BYTES bytes
let toCopy := returndatasize()
if gt(toCopy, MAX_RET_BYTES) {
toCopy := MAX_RET_BYTES
}
// Store the length of the copied bytes
mstore(retData, toCopy)
// copy the bytes from retData[0:_toCopy]
returndatacopy(add(retData, 0x20), 0, toCopy)
}
(success, retData) = CallWithExactGas._callWithExactGas(
data,
receiver,
gasLimit,
Internal.MAX_RET_BYTES,
gasForCallExactCheck
);

emit MessageExecuted(message.messageId, message.sourceChainSelector, msg.sender, keccak256(data));
return (success, retData);
}
Expand Down
74 changes: 74 additions & 0 deletions contracts/src/v0.8/ccip/libraries/CallWithExactGas.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

/// @dev Handles the edge case where we want to pass a specific amount of gas,
/// @dev but EIP-150 sends all but 1/64 of the remaining gas instead so the user gets
/// @dev less gas than they paid for. The other 2 parts of EIP-150 do not apply since
/// @dev a) we hard code value=0 and b) we ensure code already exists.
/// @dev If we revert instead, then that will never happen.
/// @dev Separately we capture the return data up to a maximum size to avoid return bombs,
/// @dev borrowed from https://github.com/nomad-xyz/ExcessivelySafeCall/blob/main/src/ExcessivelySafeCall.sol.
library CallWithExactGas {
error NoContract();
error NoGasForCallExactCheck();
error NotEnoughGasForCall();

function _callWithExactGas(
bytes memory payload,
address target,
uint256 gasLimit,
uint16 maxReturnBytes,
uint16 gasForCallExactCheck
) internal returns (bool success, bytes memory retData) {
// allocate retData memory ahead of time
retData = new bytes(maxReturnBytes);

bytes4 noContract = NoContract.selector;
bytes4 noGasForCallExactCheck = NoGasForCallExactCheck.selector;
bytes4 notEnoughGasForCall = NotEnoughGasForCall.selector;

// solhint-disable-next-line no-inline-assembly
assembly {
// solidity calls check that a contract actually exists at the destination, so we do the same
// Note we do this check prior to measuring gas so gasForCallExactCheck (our "cushion")
// doesn't need to account for it.
if iszero(extcodesize(target)) {
mstore(0, noContract)
revert(0, 0x4)
}

let g := gas()
// Compute g -= gasForCallExactCheck and check for underflow
// The gas actually passed to the callee is _min(gasAmount, 63//64*gas available).
// We want to ensure that we revert if gasAmount > 63//64*gas available
// as we do not want to provide them with less, however that check itself costs
// gas. gasForCallExactCheck ensures we have at least enough gas to be able
// to revert if gasAmount > 63//64*gas available.
if lt(g, gasForCallExactCheck) {
mstore(0, noGasForCallExactCheck)
revert(0, 0x4)
}
g := sub(g, gasForCallExactCheck)
// if g - g//64 <= gasAmount, revert
// (we subtract g//64 because of EIP-150)
if iszero(gt(sub(g, div(g, 64)), gasLimit)) {
mstore(0, notEnoughGasForCall)
revert(0, 0x4)
}
// call and return whether we succeeded. ignore return data
// call(gas,addr,value,argsOffset,argsLength,retOffset,retLength)
success := call(gasLimit, target, 0, add(payload, 0x20), mload(payload), 0, 0)

// limit our copy to maxReturnBytes bytes
let toCopy := returndatasize()
if gt(toCopy, maxReturnBytes) {
toCopy := maxReturnBytes
}
// Store the length of the copied bytes
mstore(retData, toCopy)
// copy the bytes from retData[0:_toCopy]
returndatacopy(add(retData, 0x20), 0, toCopy)
}
return (success, retData);
}
}
9 changes: 9 additions & 0 deletions contracts/src/v0.8/ccip/libraries/Internal.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ import {MerkleMultiProof} from "../libraries/MerkleMultiProof.sol";

// Library for CCIP internal definitions common to multiple contracts.
library Internal {
/// @dev The minimum amount of gas to perform the call with exact gas.
/// We include this in the offramp so that we can redeploy to adjust it
/// should a hardfork change the gas costs of relevant opcodes in callWithExactGas.
uint16 internal constant GAS_FOR_CALL_EXACT_CHECK = 5_000;
// @dev We limit return data to a selector plus 4 words. This is to avoid
// malicious contracts from returning large amounts of data and causing
// repeated out-of-gas scenarios.
uint16 internal constant MAX_RET_BYTES = 4 + 4 * 32;

struct PriceUpdates {
TokenPriceUpdate[] tokenPriceUpdates;
GasPriceUpdate[] gasPriceUpdates;
Expand Down
84 changes: 53 additions & 31 deletions contracts/src/v0.8/ccip/offRamp/EVM2EVMOffRamp.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {IAny2EVMOffRamp} from "../interfaces/IAny2EVMOffRamp.sol";
import {Client} from "../libraries/Client.sol";
import {Internal} from "../libraries/Internal.sol";
import {RateLimiter} from "../libraries/RateLimiter.sol";
import {CallWithExactGas} from "../libraries/CallWithExactGas.sol";
import {OCR2BaseNoChecks} from "../ocr/OCR2BaseNoChecks.sol";
import {AggregateRateLimiter} from "../AggregateRateLimiter.sol";
import {EnumerableMapAddresses} from "../../shared/enumerable/EnumerableMapAddresses.sol";
Expand Down Expand Up @@ -51,7 +52,6 @@ contract EVM2EVMOffRamp is IAny2EVMOffRamp, AggregateRateLimiter, ITypeAndVersio
error CanOnlySelfCall();
error ReceiverError(bytes error);
error TokenHandlingError(bytes error);
error TokenRateLimitError(bytes error);
error EmptyReport();
error BadARMSignal();
error InvalidMessageId();
Expand Down Expand Up @@ -91,16 +91,13 @@ contract EVM2EVMOffRamp is IAny2EVMOffRamp, AggregateRateLimiter, ITypeAndVersio
address router; // ─────────────────────────────────╯ Router address
address priceRegistry; // ─────╮ Price registry address
uint16 maxTokensLength; // │ Maximum number of ERC20 token transfers that can be included per message
uint32 maxDataSize; // ────────╯ Maximum payload data size
uint32 maxDataSize; // │ Maximum payload data size
uint32 maxPoolGas; // ─────────╯ Maximum amount of gas passed on to token pool when calling releaseOrMint
}

// STATIC CONFIG
// solhint-disable-next-line chainlink-solidity/all-caps-constant-storage-variables
string public constant override typeAndVersion = "EVM2EVMOffRamp 1.2.0";
/// @dev The minimum amount of gas to perform the call with exact gas.
/// We include this in the offramp so that we can redeploy to adjust it
/// should a hardfork change the gas costs of relevant opcodes in callWithExactGas.
uint16 private constant GAS_FOR_CALL_EXACT_CHECK = 5_000;
/// @dev Commit store address on the destination chain
address internal immutable i_commitStore;
/// @dev ChainSelector of the source chain
Expand Down Expand Up @@ -332,7 +329,13 @@ contract EVM2EVMOffRamp is IAny2EVMOffRamp, AggregateRateLimiter, ITypeAndVersio
// Although we expect only valid messages will be committed, we check again
// when executing as a defense in depth measure.
bytes[] memory offchainTokenData = report.offchainTokenData[i];
_isWellFormed(message, offchainTokenData.length);
_isWellFormed(
message.sequenceNumber,
message.sourceChainSelector,
message.tokenAmounts.length,
message.data.length,
offchainTokenData.length
);

_setExecutionState(message.sequenceNumber, Internal.MessageExecutionState.IN_PROGRESS);
(Internal.MessageExecutionState newState, bytes memory returnData) = _trialExecute(message, offchainTokenData);
Expand All @@ -357,19 +360,28 @@ contract EVM2EVMOffRamp is IAny2EVMOffRamp, AggregateRateLimiter, ITypeAndVersio
}

/// @notice Does basic message validation. Should never fail.
/// @param message The message to be validated.
/// @param sequenceNumber Sequence number of the message.
/// @param sourceChainSelector SourceChainSelector of the message.
/// @param numberOfTokens Length of tokenAmounts array in the message.
/// @param dataLength Length of data field in the message.
/// @param offchainTokenDataLength Length of offchainTokenData array.
/// @dev reverts on validation failures.
function _isWellFormed(Internal.EVM2EVMMessage memory message, uint256 offchainTokenDataLength) private view {
if (message.sourceChainSelector != i_sourceChainSelector) revert InvalidSourceChain(message.sourceChainSelector);
if (message.tokenAmounts.length > uint256(s_dynamicConfig.maxTokensLength))
revert UnsupportedNumberOfTokens(message.sequenceNumber);
if (message.tokenAmounts.length != offchainTokenDataLength) revert TokenDataMismatch(message.sequenceNumber);
if (message.data.length > uint256(s_dynamicConfig.maxDataSize))
revert MessageTooLarge(uint256(s_dynamicConfig.maxDataSize), message.data.length);
function _isWellFormed(
uint64 sequenceNumber,
uint64 sourceChainSelector,
uint256 numberOfTokens,
uint256 dataLength,
uint256 offchainTokenDataLength
) private view {
if (sourceChainSelector != i_sourceChainSelector) revert InvalidSourceChain(sourceChainSelector);
if (numberOfTokens > uint256(s_dynamicConfig.maxTokensLength)) revert UnsupportedNumberOfTokens(sequenceNumber);
if (numberOfTokens != offchainTokenDataLength) revert TokenDataMismatch(sequenceNumber);
if (dataLength > uint256(s_dynamicConfig.maxDataSize))
revert MessageTooLarge(uint256(s_dynamicConfig.maxDataSize), dataLength);
}

/// @notice Try executing a message.
/// @param message Client.Any2EVMMessage memory message.
/// @param message Internal.EVM2EVMMessage memory message.
/// @param offchainTokenData Data provided by the DON for token transfers.
/// @return the new state of the message, being either SUCCESS or FAILURE.
/// @return revert data in bytes if CCIP receiver reverted during execution.
Expand Down Expand Up @@ -397,7 +409,7 @@ contract EVM2EVMOffRamp is IAny2EVMOffRamp, AggregateRateLimiter, ITypeAndVersio
/// @param offchainTokenData Token transfer data to be passed to TokenPool.
/// @dev We make this external and callable by the contract itself, in order to try/catch
/// its execution and enforce atomicity among successful message processing and token transfer.
/// @dev We use 165 to check for the ccipReceive interface to permit sending tokens to contracts
/// @dev We use ERC-165 to check for the ccipReceive interface to permit sending tokens to contracts
/// (for example smart contract wallets) without an associated message.
function executeSingleMessage(Internal.EVM2EVMMessage memory message, bytes[] memory offchainTokenData) external {
if (msg.sender != address(this)) revert CanOnlySelfCall();
Expand All @@ -417,7 +429,7 @@ contract EVM2EVMOffRamp is IAny2EVMOffRamp, AggregateRateLimiter, ITypeAndVersio

(bool success, bytes memory returnData) = IRouter(s_dynamicConfig.router).routeMessage(
Internal._toAny2EVMMessage(message, destTokenAmounts),
GAS_FOR_CALL_EXACT_CHECK,
Internal.GAS_FOR_CALL_EXACT_CHECK,
message.gasLimit,
message.receiver
);
Expand Down Expand Up @@ -502,9 +514,7 @@ contract EVM2EVMOffRamp is IAny2EVMOffRamp, AggregateRateLimiter, ITypeAndVersio
/// @param sourceToken The source token
/// @return the destination token
function getDestinationToken(IERC20 sourceToken) external view returns (IERC20) {
(bool success, address pool) = s_poolsBySourceToken.tryGet(address(sourceToken));
if (!success) revert UnsupportedToken(sourceToken);
return IPool(pool).getToken();
return getPoolBySourceToken(sourceToken).getToken();
}

/// @notice Get a token pool by its dest token
Expand Down Expand Up @@ -566,7 +576,10 @@ contract EVM2EVMOffRamp is IAny2EVMOffRamp, AggregateRateLimiter, ITypeAndVersio

/// @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 originalSender The message sender.
/// @param receiver The address that will receive the tokens.
/// @param sourceTokenData Array of token data returned by token pools on the source chain.
/// @param offchainTokenData Array of token data fetched offchain by the DON.
/// @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.
Expand All @@ -580,22 +593,31 @@ contract EVM2EVMOffRamp is IAny2EVMOffRamp, AggregateRateLimiter, ITypeAndVersio
Client.EVMTokenAmount[] memory destTokenAmounts = new Client.EVMTokenAmount[](sourceTokenAmounts.length);
for (uint256 i = 0; i < sourceTokenAmounts.length; ++i) {
IPool pool = getPoolBySourceToken(IERC20(sourceTokenAmounts[i].token));

try
pool.releaseOrMint(
uint256 sourceTokenAmount = sourceTokenAmounts[i].amount;

// Call the pool with exact gas to increase resistance against malicious tokens or token pools.
// _callWithExactGas also protects against return data bombs by capping the return data size
// at MAX_RET_BYTES.
(bool success, bytes memory returnData) = CallWithExactGas._callWithExactGas(
abi.encodeWithSelector(
pool.releaseOrMint.selector,
originalSender,
receiver,
sourceTokenAmounts[i].amount,
sourceTokenAmount,
i_sourceChainSelector,
abi.encode(sourceTokenData[i], offchainTokenData[i])
)
{} catch (bytes memory err) {
/// @dev wrap and rethrow the error so we can catch it lower in the stack
revert TokenHandlingError(err);
}
),
address(pool),
s_dynamicConfig.maxPoolGas,
Internal.MAX_RET_BYTES,
Internal.GAS_FOR_CALL_EXACT_CHECK
);

// wrap and rethrow the error so we can catch it lower in the stack
if (!success) revert TokenHandlingError(returnData);

destTokenAmounts[i].token = address(pool.getToken());
destTokenAmounts[i].amount = sourceTokenAmounts[i].amount;
destTokenAmounts[i].amount = sourceTokenAmount;
}
_rateLimitValue(destTokenAmounts, IPriceRegistry(s_dynamicConfig.priceRegistry));
return destTokenAmounts;
Expand Down
Loading

0 comments on commit 25a5afb

Please sign in to comment.