Contract detail
OffRamp
OffRamp is a verified Robinhood Chain smart contract at 0xcdca5d37…96ec35c3 with 36994 indexed transactions.
- Address type
- Smart contract
- Transactions
- 36994
- First indexed block
- #4775
- Latest indexed block
- #90476983
- Native balance
- 0 ETH
- Balance observed at
- Block #34305421
- ERC-20 transfers
- 4
- Emitted events
- 57538
- Verification
- Verified
- Compiler
- v0.8.26+commit.8a97fa7a
- Verification match
- partial
- Creation transaction
- 0x4e1862a8…d736bba6
- Created at block
- #4775
- Language
- solidity
- EVM version
- paris
- License
- mit
- Compiler optimization
- Enabled · 800 runs
Related activity
Transactions
Verified contract
Contract source code
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.24;
import {IAny2EVMMessageReceiver} from "../interfaces/IAny2EVMMessageReceiver.sol";
import {IFeeQuoter} from "../interfaces/IFeeQuoter.sol";
import {IMessageInterceptor} from "../interfaces/IMessageInterceptor.sol";
import {INonceManager} from "../interfaces/INonceManager.sol";
import {IPoolV1} from "../interfaces/IPool.sol";
import {IRMNRemote} from "../interfaces/IRMNRemote.sol";
import {IRouter} from "../interfaces/IRouter.sol";
import {ITokenAdminRegistry} from "../interfaces/ITokenAdminRegistry.sol";
import {ITypeAndVersion} from "@chainlink/shared/interfaces/ITypeAndVersion.sol";
import {Client} from "../libraries/Client.sol";
import {ERC165CheckerReverting} from "../libraries/ERC165CheckerReverting.sol";
import {Internal} from "../libraries/Internal.sol";
import {MerkleMultiProof} from "../libraries/MerkleMultiProof.sol";
import {Pool} from "../libraries/Pool.sol";
import {MultiOCR3Base} from "../ocr/MultiOCR3Base.sol";
import {CallWithExactGas} from "@chainlink/shared/call/CallWithExactGas.sol";
import {IERC20} from "@chainlink/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/IERC20.sol";
import {EnumerableSet} from "@chainlink/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/structs/EnumerableSet.sol";
/// @notice OffRamp enables OCR networks to execute multiple messages in an OffRamp in a single transaction.
/// @dev The OnRamp and OffRamp form a cross chain upgradeable unit. Any change to one of them results an
/// onchain upgrade of both contracts.
/// @dev MultiOCR3Base is used to store multiple OCR configs for the OffRamp. The execution plugin type has to be
/// configured without signature verification, and the commit plugin type with verification.
contract OffRamp is ITypeAndVersion, MultiOCR3Base {
using ERC165CheckerReverting for address;
using EnumerableSet for EnumerableSet.UintSet;
error ZeroChainSelectorNotAllowed();
error ExecutionError(bytes32 messageId, bytes err);
error SourceChainNotEnabled(uint64 sourceChainSelector);
error TokenDataMismatch(uint64 sourceChainSelector, uint64 sequenceNumber);
error UnexpectedTokenData();
error ManualExecutionNotYetEnabled(uint64 sourceChainSelector);
error ManualExecutionGasLimitMismatch();
error InvalidManualExecutionGasLimit(uint64 sourceChainSelector, bytes32 messageId, uint256 newLimit);
error InvalidManualExecutionTokenGasOverride(
bytes32 messageId, uint256 tokenIndex, uint256 oldLimit, uint256 tokenGasOverride
);
error ManualExecutionGasAmountCountMismatch(bytes32 messageId, uint64 sequenceNumber);
error RootNotCommitted(uint64 sourceChainSelector);
error RootAlreadyCommitted(uint64 sourceChainSelector, bytes32 merkleRoot);
error InvalidRoot();
error CanOnlySelfCall();
error ReceiverError(bytes err);
error TokenHandlingError(address target, bytes err);
error ReleaseOrMintBalanceMismatch(uint256 amountReleased, uint256 balancePre, uint256 balancePost);
error EmptyReport(uint64 sourceChainSelector);
error EmptyBatch();
error CursedByRMN(uint64 sourceChainSelector);
error NotACompatiblePool(address notPool);
error InvalidDataLength(uint256 expected, uint256 got);
error InvalidNewState(uint64 sourceChainSelector, uint64 sequenceNumber, Internal.MessageExecutionState newState);
error StaleCommitReport();
error InvalidInterval(uint64 sourceChainSelector, uint64 min, uint64 max);
error ZeroAddressNotAllowed();
error InvalidMessageDestChainSelector(uint64 messageDestChainSelector);
error SourceChainSelectorMismatch(uint64 reportSourceChainSelector, uint64 messageSourceChainSelector);
error SignatureVerificationRequiredInCommitPlugin();
error SignatureVerificationNotAllowedInExecutionPlugin();
error CommitOnRampMismatch(bytes reportOnRamp, bytes configOnRamp);
error InvalidOnRampUpdate(uint64 sourceChainSelector);
error RootBlessingMismatch(uint64 sourceChainSelector, bytes32 merkleRoot, bool isBlessed);
/// @dev Atlas depends on various events, if changing, please notify Atlas.
event StaticConfigSet(StaticConfig staticConfig);
event DynamicConfigSet(DynamicConfig dynamicConfig);
event ExecutionStateChanged(
uint64 indexed sourceChainSelector,
uint64 indexed sequenceNumber,
bytes32 indexed messageId,
bytes32 messageHash,
Internal.MessageExecutionState state,
bytes returnData,
uint256 gasUsed
);
event SourceChainSelectorAdded(uint64 sourceChainSelector);
event SourceChainConfigSet(uint64 indexed sourceChainSelector, SourceChainConfig sourceConfig);
event SkippedAlreadyExecutedMessage(uint64 sourceChainSelector, uint64 sequenceNumber);
event AlreadyAttempted(uint64 sourceChainSelector, uint64 sequenceNumber);
event CommitReportAccepted(
Internal.MerkleRoot[] blessedMerkleRoots,
Internal.MerkleRoot[] unblessedMerkleRoots,
Internal.PriceUpdates priceUpdates
);
event RootRemoved(bytes32 root);
event SkippedReportExecution(uint64 sourceChainSelector);
/// @dev Struct that contains the static configuration. The individual components are stored as immutable variables.
// solhint-disable-next-line gas-struct-packing
struct StaticConfig {
uint64 chainSelector; // ───────╮ Destination chainSelector
uint16 gasForCallExactCheck; // | Gas for call exact check
IRMNRemote rmnRemote; // ───────╯ RMN Verification Contract
address tokenAdminRegistry; // Token admin registry address
address nonceManager; // Nonce manager address
}
/// @dev Per-chain source config (defining a lane from a Source Chain -> Dest OffRamp).
struct SourceChainConfig {
IRouter router; // ─────────────────╮ Local router to use for messages coming from this source chain.
bool isEnabled; // │ Flag whether the source chain is enabled or not.
uint64 minSeqNr; // │ The min sequence number expected for future messages.
bool isRMNVerificationDisabled; // ─╯ Flag whether the RMN verification is disabled or not.
bytes onRamp; // OnRamp address on the source chain.
}
/// @dev Same as SourceChainConfig but with source chain selector so that an array of these
/// can be passed in the constructor and the applySourceChainConfigUpdates function.
struct SourceChainConfigArgs {
IRouter router; // ─────────────────╮ Local router to use for messages coming from this source chain.
uint64 sourceChainSelector; // │ Source chain selector of the config to update.
bool isEnabled; // │ Flag whether the source chain is enabled or not.
bool isRMNVerificationDisabled; // ─╯ Flag whether the RMN verification is disabled or not.
bytes onRamp; // OnRamp address on the source chain.
}
/// @dev Dynamic offRamp config.
/// @dev Since DynamicConfig is part of DynamicConfigSet event, if changing it, we should update the ABI on Atlas.
struct DynamicConfig {
address feeQuoter; // ──────────────────────────────╮ FeeQuoter address on the local chain.
uint32 permissionLessExecutionThresholdSeconds; // ─╯ Waiting time before manual execution is enabled.
address messageInterceptor; // Optional, validates incoming messages (zero address = no interceptor).
}
/// @dev Report that is committed by the observing DON at the committing phase.
struct CommitReport {
Internal.PriceUpdates priceUpdates; // List of gas and price updates to commit.
Internal.MerkleRoot[] blessedMerkleRoots; // List of merkle roots from source chains for which RMN is enabled.
Internal.MerkleRoot[] unblessedMerkleRoots; // List of merkle roots from source chains for which RMN is disabled.
IRMNRemote.Signature[] rmnSignatures; // RMN signatures on the merkle roots.
}
/// @dev Both receiverExecutionGasLimit and tokenGasOverrides are optional. To indicate no override, set the value
/// to 0. The length of tokenGasOverrides must match the length of tokenAmounts, even if it only contains zeros.
struct GasLimitOverride {
uint256 receiverExecutionGasLimit; // Overrides EVM2EVMMessage.gasLimit.
uint32[] tokenGasOverrides; // Overrides EVM2EVMMessage.sourceTokenData.destGasAmount, length must be same as tokenAmounts.
}
// STATIC CONFIG
string public constant override typeAndVersion = "OffRamp 1.6.0";
/// @dev Hash of encoded address(0) used for empty address checks.
bytes32 internal constant EMPTY_ENCODED_ADDRESS_HASH = keccak256(abi.encode(address(0)));
/// @dev ChainSelector of this chain.
uint64 internal immutable i_chainSelector;
/// @dev The RMN verification contract.
IRMNRemote internal immutable i_rmnRemote;
/// @dev The address of the token admin registry.
address internal immutable i_tokenAdminRegistry;
/// @dev The address of the nonce manager.
address internal immutable i_nonceManager;
/// @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 immutable i_gasForCallExactCheck;
// DYNAMIC CONFIG
DynamicConfig internal s_dynamicConfig;
/// @notice Set of source chain selectors.
EnumerableSet.UintSet internal s_sourceChainSelectors;
/// @notice SourceChainConfig per source chain selector.
mapping(uint64 sourceChainSelector => SourceChainConfig sourceChainConfig) private s_sourceChainConfigs;
// STATE
/// @dev A mapping of sequence numbers (per source chain) to execution state using a bitmap with each execution
/// state only taking up 2 bits of the uint256, packing 128 states into a single slot.
/// Message state is tracked to ensure message can only be executed successfully once.
mapping(uint64 sourceChainSelector => mapping(uint64 seqNum => uint256 executionStateBitmap)) internal
s_executionStates;
/// @notice Commit timestamp of merkle roots per source chain.
mapping(uint64 sourceChainSelector => mapping(bytes32 merkleRoot => uint256 timestamp)) internal s_roots;
/// @dev The sequence number of the last price update.
uint64 private s_latestPriceSequenceNumber;
constructor(
StaticConfig memory staticConfig,
DynamicConfig memory dynamicConfig,
SourceChainConfigArgs[] memory sourceChainConfigs
) MultiOCR3Base() {
if (
address(staticConfig.rmnRemote) == address(0) || staticConfig.tokenAdminRegistry == address(0)
|| staticConfig.nonceManager == address(0)
) {
revert ZeroAddressNotAllowed();
}
if (staticConfig.chainSelector == 0) {
revert ZeroChainSelectorNotAllowed();
}
i_chainSelector = staticConfig.chainSelector;
i_rmnRemote = staticConfig.rmnRemote;
i_tokenAdminRegistry = staticConfig.tokenAdminRegistry;
i_nonceManager = staticConfig.nonceManager;
i_gasForCallExactCheck = staticConfig.gasForCallExactCheck;
emit StaticConfigSet(staticConfig);
_setDynamicConfig(dynamicConfig);
_applySourceChainConfigUpdates(sourceChainConfigs);
}
// ================================================================
// │ Execution │
// ================================================================
// The size of the execution state in bits.
uint256 private constant MESSAGE_EXECUTION_STATE_BIT_WIDTH = 2;
// The mask for the execution state bits.
uint256 private constant MESSAGE_EXECUTION_STATE_MASK = (1 << MESSAGE_EXECUTION_STATE_BIT_WIDTH) - 1;
/// @notice Returns the current execution state of a message based on its sequenceNumber.
/// @param sourceChainSelector The source chain to get the execution state for.
/// @param sequenceNumber The sequence number of the message to get the execution state for.
/// @return executionState The current execution state of the message.
/// @dev We use the literal number 128 because using a constant increased gas usage.
function getExecutionState(
uint64 sourceChainSelector,
uint64 sequenceNumber
) public view returns (Internal.MessageExecutionState) {
return Internal.MessageExecutionState(
(
_getSequenceNumberBitmap(sourceChainSelector, sequenceNumber)
>> ((sequenceNumber % 128) * MESSAGE_EXECUTION_STATE_BIT_WIDTH)
) & MESSAGE_EXECUTION_STATE_MASK
);
}
/// @notice Sets a new execution state for a given sequence number. It will overwrite any existing state.
/// @param sourceChainSelector The source chain to set the execution state for.
/// @param sequenceNumber The sequence number for which the state will be saved.
/// @param newState The new value the state will be in after this function is called.
/// @dev We use the literal number 128 because using a constant increased gas usage.
function _setExecutionState(
uint64 sourceChainSelector,
uint64 sequenceNumber,
Internal.MessageExecutionState newState
) internal {
uint256 offset = (sequenceNumber % 128) * MESSAGE_EXECUTION_STATE_BIT_WIDTH;
uint256 bitmap = _getSequenceNumberBitmap(sourceChainSelector, sequenceNumber);
// To unset any potential existing state we zero the bits of the section the state occupies,
// then we do an AND operation to blank out any existing state for the section.
bitmap &= ~(MESSAGE_EXECUTION_STATE_MASK << offset);
// Set the new state.
bitmap |= uint256(newState) << offset;
s_executionStates[sourceChainSelector][sequenceNumber / 128] = bitmap;
}
/// @param sourceChainSelector remote source chain selector to get sequence number bitmap for.
/// @param sequenceNumber sequence number to get bitmap for.
/// @return bitmap Bitmap of the given sequence number for the provided source chain selector. One bitmap represents
/// 128 sequence numbers.
function _getSequenceNumberBitmap(
uint64 sourceChainSelector,
uint64 sequenceNumber
) internal view returns (uint256 bitmap) {
return s_executionStates[sourceChainSelector][sequenceNumber / 128];
}
/// @notice Manually executes a set of reports.
/// @param reports Internal.ExecutionReportSingleChain[] - list of reports to execute.
/// @param gasLimitOverrides New gasLimit for each message per report. The outer array represents each report, the
// inner array represents each message in the report.
// i.e. gasLimitOverrides[report1][report1Message1] -> access message1 from report1
/// @dev We permit gas limit overrides so that users may manually execute messages which failed due to insufficient
/// gas provided. The reports do not have to contain all the messages (they can be omitted). Multiple reports can be
/// passed in simultaneously.
function manuallyExecute(
Internal.ExecutionReport[] memory reports,
GasLimitOverride[][] memory gasLimitOverrides
) external {
// We do this here because the other _execute path is already covered by MultiOCR3Base.
_whenChainNotForked();
uint256 numReports = reports.length;
if (numReports != gasLimitOverrides.length) revert ManualExecutionGasLimitMismatch();
for (uint256 reportIndex = 0; reportIndex < numReports; ++reportIndex) {
Internal.ExecutionReport memory report = reports[reportIndex];
uint256 numMsgs = report.messages.length;
GasLimitOverride[] memory msgGasLimitOverrides = gasLimitOverrides[reportIndex];
// Gas override values need to be provided, even when no override is desired. We expect an array of the correct
// size with all `0` values if no override is desired.
if (numMsgs != msgGasLimitOverrides.length) revert ManualExecutionGasLimitMismatch();
for (uint256 msgIndex = 0; msgIndex < numMsgs; ++msgIndex) {
uint256 newLimit = msgGasLimitOverrides[msgIndex].receiverExecutionGasLimit;
Internal.Any2EVMRampMessage memory message = report.messages[msgIndex];
if (newLimit != 0) {
// Checks to ensure messages will not be executed with less gas than specified.
if (newLimit < message.gasLimit) {
revert InvalidManualExecutionGasLimit(report.sourceChainSelector, message.header.messageId, newLimit);
}
}
if (message.tokenAmounts.length != msgGasLimitOverrides[msgIndex].tokenGasOverrides.length) {
revert ManualExecutionGasAmountCountMismatch(message.header.messageId, message.header.sequenceNumber);
}
// The gas limit can not be lowered as that could cause the message to fail. If manual execution is done
// from an UNTOUCHED state and we would allow lower gas limit, anyone could grief by executing the message with
// lower gas limit than the DON would have used. This results in the message being marked FAILURE and the DON
// would not attempt it with the correct gas limit.
for (uint256 tokenIndex = 0; tokenIndex < message.tokenAmounts.length; ++tokenIndex) {
uint256 tokenGasOverride = msgGasLimitOverrides[msgIndex].tokenGasOverrides[tokenIndex];
if (tokenGasOverride != 0) {
uint256 destGasAmount = message.tokenAmounts[tokenIndex].destGasAmount;
if (tokenGasOverride < destGasAmount) {
revert InvalidManualExecutionTokenGasOverride(
message.header.messageId, tokenIndex, destGasAmount, tokenGasOverride
);
}
}
}
}
}
_batchExecute(reports, gasLimitOverrides);
}
/// @notice Transmit function for execution reports. The function takes no signatures, and expects the exec plugin
/// type to be configured with no signatures.
/// @param report serialized execution report.
function execute(bytes32[2] calldata reportContext, bytes calldata report) external {
_batchExecute(abi.decode(report, (Internal.ExecutionReport[])), new GasLimitOverride[][](0));
bytes32[] memory emptySigs = new bytes32[](0);
_transmit(uint8(Internal.OCRPluginType.Execution), reportContext, report, emptySigs, emptySigs, bytes32(""));
}
/// @notice Batch executes a set of reports, each report matching one single source chain.
/// @param reports Set of execution reports (one per chain) containing the messages and proofs.
/// @param manualExecGasLimits An array of gas limits to use for manual execution The outer array represents each
// report, the inner array represents each message in the report.
// i.e. gasLimitOverrides[report1][report1Message1] -> access message1 from report1.
/// @dev The manualExecGasLimits array should either be empty, or match the length of the reports array.
/// @dev If called from manual execution, each inner array's length has to match the number of messages.
function _batchExecute(
Internal.ExecutionReport[] memory reports,
GasLimitOverride[][] memory manualExecGasOverrides
) internal {
if (reports.length == 0) revert EmptyBatch();
bool areManualGasLimitsEmpty = manualExecGasOverrides.length == 0;
// Cache array for gas savings in the loop's condition.
GasLimitOverride[] memory emptyGasLimits = new GasLimitOverride[](0);
for (uint256 i = 0; i < reports.length; ++i) {
_executeSingleReport(reports[i], areManualGasLimitsEmpty ? emptyGasLimits : manualExecGasOverrides[i]);
}
}
/// @notice Executes a report, executing each message in order.
/// @param report The execution report containing the messages and proofs.
/// @param manualExecGasExecOverrides An array of gas limits to use for manual execution.
/// @dev If called from the DON, this array is always empty.
/// @dev If called from manual execution, this array is always same length as messages.
/// @dev This function can fully revert in some cases, reverting potentially valid other reports with it. The reasons
/// for these reverts are so severe that we prefer to revert the entire batch instead of silently failing.
function _executeSingleReport(
Internal.ExecutionReport memory report,
GasLimitOverride[] memory manualExecGasExecOverrides
) internal {
uint64 sourceChainSelector = report.sourceChainSelector;
bool manualExecution = manualExecGasExecOverrides.length != 0;
if (i_rmnRemote.isCursed(bytes16(uint128(sourceChainSelector)))) {
if (manualExecution) {
// For manual execution we don't want to silently fail so we revert.
revert CursedByRMN(sourceChainSelector);
}
// For DON execution we do not revert as a single lane curse can revert the entire batch.
emit SkippedReportExecution(sourceChainSelector);
return;
}
uint256 numMsgs = report.messages.length;
if (numMsgs == 0) revert EmptyReport(report.sourceChainSelector);
if (numMsgs != report.offchainTokenData.length) revert UnexpectedTokenData();
bytes32[] memory hashedLeaves = new bytes32[](numMsgs);
{
// We do this hash here instead of in _verify to avoid two separate loops over the same data. Hashing all of the
// message fields ensures that the message being executed is correct and not tampered with. Including the known
// OnRamp ensures that the message originates from the correct on ramp version. We know the sourceChainSelector
// and i_destChainSelector are correct because we revert below when they are not.
bytes32 metaDataHash = keccak256(
abi.encode(
Internal.ANY_2_EVM_MESSAGE_HASH,
sourceChainSelector,
i_chainSelector,
keccak256(_getEnabledSourceChainConfig(sourceChainSelector).onRamp)
)
);
for (uint256 i = 0; i < numMsgs; ++i) {
Internal.Any2EVMRampMessage memory message = report.messages[i];
// Commits do not verify the destChainSelector in the message since only the root is committed, so we
// have to check it explicitly. This check is also important as we have assumed the metaDataHash above uses
// the i_chainSelector as the destChainSelector.
if (message.header.destChainSelector != i_chainSelector) {
revert InvalidMessageDestChainSelector(message.header.destChainSelector);
}
// If the message source chain selector does not match the report's source chain selector and the root has not
// been committed for the report source chain selector this will be caught by the root verification.
// This acts as an extra check to ensure the message source chain selector matches the report's source chain.
if (message.header.sourceChainSelector != sourceChainSelector) {
revert SourceChainSelectorMismatch(sourceChainSelector, message.header.sourceChainSelector);
}
hashedLeaves[i] = Internal._hash(message, metaDataHash);
}
}
// SECURITY CRITICAL CHECK.
uint256 timestampCommitted = _verify(sourceChainSelector, hashedLeaves, report.proofs, report.proofFlagBits);
if (timestampCommitted == 0) revert RootNotCommitted(sourceChainSelector);
// Execute messages.
for (uint256 i = 0; i < numMsgs; ++i) {
uint256 gasStart = gasleft();
Internal.Any2EVMRampMessage memory message = report.messages[i];
message = _beforeExecuteSingleMessage(message);
Internal.MessageExecutionState originalState =
getExecutionState(sourceChainSelector, message.header.sequenceNumber);
// Two valid cases here, we either have never touched this message before, or we tried to execute and failed. This
// check protects against reentry and re-execution because the other state is IN_PROGRESS which should not be
// allowed to execute.
if (
!(
originalState == Internal.MessageExecutionState.UNTOUCHED
|| originalState == Internal.MessageExecutionState.FAILURE
)
) {
// If the message has already been executed, we skip it. We want to not revert on race conditions between
// executing parties. This will allow us to open up manual exec while also attempting with the DON, without
// reverting an entire DON batch when a user manually executes while the tx is inflight.
emit SkippedAlreadyExecutedMessage(sourceChainSelector, message.header.sequenceNumber);
continue;
}
uint32[] memory tokenGasOverrides;
if (manualExecution) {
tokenGasOverrides = manualExecGasExecOverrides[i].tokenGasOverrides;
bool isOldCommitReport =
(block.timestamp - timestampCommitted) > s_dynamicConfig.permissionLessExecutionThresholdSeconds;
// Manually execution is fine if we previously failed or if the commit report is just too old.
// Acceptable state transitions: UNTOUCHED->SUCCESS, UNTOUCHED->FAILURE, FAILURE->SUCCESS.
if (!(isOldCommitReport || originalState == Internal.MessageExecutionState.FAILURE)) {
revert ManualExecutionNotYetEnabled(sourceChainSelector);
}
// Manual execution gas limit can override gas limit specified in the message. Value of 0 indicates no override.
if (manualExecGasExecOverrides[i].receiverExecutionGasLimit != 0) {
message.gasLimit = manualExecGasExecOverrides[i].receiverExecutionGasLimit;
}
} else {
// DON can only execute a message once.
// Acceptable state transitions: UNTOUCHED->SUCCESS, UNTOUCHED->FAILURE.
if (originalState != Internal.MessageExecutionState.UNTOUCHED) {
emit AlreadyAttempted(sourceChainSelector, message.header.sequenceNumber);
continue;
}
}
// Nonce changes per state transition (these only apply for ordered messages):
// UNTOUCHED -> FAILURE nonce bump.
// UNTOUCHED -> SUCCESS nonce bump.
// FAILURE -> SUCCESS no nonce bump.
// UNTOUCHED messages MUST be executed in order always.
// If nonce == 0 then out of order execution is allowed.
if (message.header.nonce != 0) {
if (originalState == Internal.MessageExecutionState.UNTOUCHED) {
// If a nonce is not incremented, that means it was skipped, and we can ignore the message.
if (
!INonceManager(i_nonceManager).incrementInboundNonce(
sourceChainSelector, message.header.nonce, message.sender
)
) continue;
}
}
// We expect only valid messages will be committed but we check when executing as a defense in depth measure.
bytes[] memory offchainTokenData = report.offchainTokenData[i];
if (message.tokenAmounts.length != offchainTokenData.length) {
revert TokenDataMismatch(sourceChainSelector, message.header.sequenceNumber);
}
_setExecutionState(sourceChainSelector, message.header.sequenceNumber, Internal.MessageExecutionState.IN_PROGRESS);
(Internal.MessageExecutionState newState, bytes memory returnData) =
_trialExecute(message, offchainTokenData, tokenGasOverrides);
_setExecutionState(sourceChainSelector, message.header.sequenceNumber, newState);
// Since it's hard to estimate whether manual execution will succeed, we revert the entire transaction if it
// fails. This will show the user if their manual exec will fail before they submit it.
if (manualExecution) {
if (newState == Internal.MessageExecutionState.FAILURE) {
if (originalState != Internal.MessageExecutionState.UNTOUCHED) {
// If manual execution fails, we revert the entire transaction, unless the originalState is UNTOUCHED as we
// would still be making progress by changing the state from UNTOUCHED to FAILURE.
revert ExecutionError(message.header.messageId, returnData);
}
}
}
// The only valid prior states are UNTOUCHED and FAILURE (checked above).
// The only valid post states are FAILURE and SUCCESS (checked below).
if (newState != Internal.MessageExecutionState.SUCCESS) {
if (newState != Internal.MessageExecutionState.FAILURE) {
revert InvalidNewState(sourceChainSelector, message.header.sequenceNumber, newState);
}
}
emit ExecutionStateChanged(
sourceChainSelector,
message.header.sequenceNumber,
message.header.messageId,
hashedLeaves[i],
newState,
returnData,
// This emit covers not only the execution through the router, but also all of the overhead in executing the
// message. This gives the most accurate representation of the gas used in the execution.
gasStart - gasleft()
);
}
}
/// @notice Try executing a message.
/// @param message Internal.Any2EVMRampMessage memory message.
/// @param offchainTokenData Data provided by the DON for token transfers.
/// @return executionState The new state of the message, being either SUCCESS or FAILURE.
/// @return errData Revert data in bytes if CCIP receiver reverted during execution.
function _trialExecute(
Internal.Any2EVMRampMessage memory message,
bytes[] memory offchainTokenData,
uint32[] memory tokenGasOverrides
) internal returns (Internal.MessageExecutionState executionState, bytes memory) {
try this.executeSingleMessage(message, offchainTokenData, tokenGasOverrides) {}
catch (bytes memory err) {
if (msg.sender == Internal.GAS_ESTIMATION_SENDER) {
if (
CallWithExactGas.NOT_ENOUGH_GAS_FOR_CALL_SIG == bytes4(err)
|| CallWithExactGas.NO_GAS_FOR_CALL_EXACT_CHECK_SIG == bytes4(err)
|| ERC165CheckerReverting.InsufficientGasForStaticCall.selector == bytes4(err)
) {
revert InsufficientGasToCompleteTx(bytes4(err));
}
}
// return the message execution state as FAILURE and the revert data.
// Max length of revert data is Router.MAX_RET_BYTES, max length of err is 4 + Router.MAX_RET_BYTES.
return (Internal.MessageExecutionState.FAILURE, err);
}
// If message execution succeeded, no CCIP receiver return data is expected, return with empty bytes.
return (Internal.MessageExecutionState.SUCCESS, "");
}
/// @notice hook for applying custom logic to the input message before executeSingleMessage()
/// @param message initial message
/// @return transformedMessage modified message
function _beforeExecuteSingleMessage(
Internal.Any2EVMRampMessage memory message
) internal virtual returns (Internal.Any2EVMRampMessage memory transformedMessage) {
return message;
}
/// @notice Executes a single message.
/// @param message The message that will be executed.
/// @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 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.Any2EVMRampMessage memory message,
bytes[] calldata offchainTokenData,
uint32[] calldata tokenGasOverrides
) external {
if (msg.sender != address(this)) revert CanOnlySelfCall();
Client.EVMTokenAmount[] memory destTokenAmounts = new Client.EVMTokenAmount[](0);
if (message.tokenAmounts.length > 0) {
destTokenAmounts = _releaseOrMintTokens(
message.tokenAmounts,
message.sender,
message.receiver,
message.header.sourceChainSelector,
offchainTokenData,
tokenGasOverrides
);
}
Client.Any2EVMMessage memory any2EvmMessage = Client.Any2EVMMessage({
messageId: message.header.messageId,
sourceChainSelector: message.header.sourceChainSelector,
sender: message.sender,
data: message.data,
destTokenAmounts: destTokenAmounts
});
// The main message interceptor is the aggregate rate limiter, but we also allow for a custom interceptor. This is
// why we always have to call into the contract when it's enabled, even when there are no tokens in the message.
address messageInterceptor = s_dynamicConfig.messageInterceptor;
if (messageInterceptor != address(0)) {
try IMessageInterceptor(messageInterceptor).onInboundMessage(any2EvmMessage) {}
catch (bytes memory err) {
revert IMessageInterceptor.MessageValidationError(err);
}
}
// 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.
//
// To prevent message delivery bypass issues, a modified version of the ERC165Checker is used
// which checks for sufficient gas before making the external call.
if (
(message.data.length == 0 && message.gasLimit == 0) || message.receiver.code.length == 0
|| !message.receiver._supportsInterfaceReverting(type(IAny2EVMMessageReceiver).interfaceId)
) return;
(bool success, bytes memory returnData,) = s_sourceChainConfigs[message.header.sourceChainSelector]
.router
.routeMessage(any2EvmMessage, i_gasForCallExactCheck, message.gasLimit, message.receiver);
// If CCIP receiver execution is not successful, revert the call including token transfers.
if (!success) revert ReceiverError(returnData);
}
// ================================================================
// │ Tokens and pools │
// ================================================================
/// @notice Uses a pool to release or mint a token to a receiver address, with balance checks before and after the
/// transfer. This is done to ensure the exact number of tokens the pool claims to release are actually transferred.
/// @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 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 offchainTokenData Data fetched offchain by the DON.
/// @return destTokenAmount local token address with amount.
function _releaseOrMintSingleToken(
Internal.Any2EVMTokenTransfer memory sourceTokenAmount,
bytes memory originalSender,
address receiver,
uint64 sourceChainSelector,
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 = 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.
// 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 offchain gas estimations should
// account for 90k gas overhead due to the interface check.
if (localPoolAddress == address(0) || !localPoolAddress._supportsInterfaceReverting(Pool.CCIP_POOL_V1)) {
revert NotACompatiblePool(localPoolAddress);
}
// We retrieve the local token balance of the receiver before the pool call.
(uint256 balancePre, uint256 gasLeft) = _getBalanceOfReceiver(receiver, localToken, sourceTokenAmount.destGasAmount);
// We determined that the pool address is a valid EVM address, but that does not mean the code at this address is a
// (compatible) pool contract. _callWithExactGasSafeReturnData will check if the location contains a contract. If it
// doesn't it reverts with a known error. We call the pool with exact gas to increase resistance against malicious
// tokens or token pools. We protect against return data bombs by capping the return data size at MAX_RET_BYTES.
(bool success, bytes memory returnData, uint256 gasUsedReleaseOrMint) = CallWithExactGas
._callWithExactGasSafeReturnData(
abi.encodeCall(
IPoolV1.releaseOrMint,
Pool.ReleaseOrMintInV1({
originalSender: originalSender,
receiver: receiver,
amount: sourceTokenAmount.amount,
localToken: localToken,
remoteChainSelector: sourceChainSelector,
sourcePoolAddress: sourceTokenAmount.sourcePoolAddress,
sourcePoolData: sourceTokenAmount.extraData,
offchainTokenData: offchainTokenData
})
),
localPoolAddress,
gasLeft,
i_gasForCallExactCheck,
Internal.MAX_RET_BYTES
);
// Wrap and rethrow the error so we can catch it lower in the stack.
if (!success) revert TokenHandlingError(localPoolAddress, returnData);
// If the call was successful, the returnData should be the amount released or minted denominated in the local
// token's decimals.
if (returnData.length != Pool.CCIP_POOL_V1_RET_BYTES) {
revert InvalidDataLength(Pool.CCIP_POOL_V1_RET_BYTES, returnData.length);
}
uint256 localAmount = abi.decode(returnData, (uint256));
// We don't need to do balance checks if the pool is the receiver, as they would always fail in the case
// of a lockRelease pool.
if (receiver != localPoolAddress) {
(uint256 balancePost,) = _getBalanceOfReceiver(receiver, localToken, gasLeft - gasUsedReleaseOrMint);
// First we check if the subtraction would result in an underflow to ensure we revert with a clear error.
if (balancePost < balancePre || balancePost - balancePre != localAmount) {
revert ReleaseOrMintBalanceMismatch(localAmount, balancePre, balancePost);
}
}
return Client.EVMTokenAmount({token: localToken, amount: localAmount});
}
/// @notice Retrieves the balance of a receiver address for a given token.
/// @param receiver The address to check the balance of.
/// @param token The token address.
/// @param gasLimit The gas limit to use for the call.
/// @return balance The balance of the receiver.
/// @return gasLeft The gas left after the call.
function _getBalanceOfReceiver(
address receiver,
address token,
uint256 gasLimit
) internal returns (uint256 balance, uint256 gasLeft) {
(bool success, bytes memory returnData, uint256 gasUsed) = CallWithExactGas._callWithExactGasSafeReturnData(
abi.encodeCall(IERC20.balanceOf, (receiver)), token, gasLimit, i_gasForCallExactCheck, Internal.MAX_RET_BYTES
);
if (!success) revert TokenHandlingError(token, returnData);
// If the call was successful, the returnData should contain only the balance.
if (returnData.length != Internal.MAX_BALANCE_OF_RET_BYTES) {
revert InvalidDataLength(Internal.MAX_BALANCE_OF_RET_BYTES, returnData.length);
}
// Return the decoded balance, which cannot fail as we checked the length, and the gas that is left
// after this call.
return (abi.decode(returnData, (uint256)), gasLimit - gasUsed);
}
/// @notice Uses pools to release or mint a number of different tokens to a receiver address.
/// @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 offchainTokenData Array of token data fetched offchain by the DON.
/// @param tokenGasOverrides Array of override gas limits to use for token transfers. If empty, the normal gas limit
/// as defined on the source chain is used.
/// @return destTokenAmounts local token addresses with amounts.
function _releaseOrMintTokens(
Internal.Any2EVMTokenTransfer[] memory sourceTokenAmounts,
bytes memory originalSender,
address receiver,
uint64 sourceChainSelector,
bytes[] calldata offchainTokenData,
uint32[] calldata tokenGasOverrides
) internal returns (Client.EVMTokenAmount[] memory destTokenAmounts) {
destTokenAmounts = new Client.EVMTokenAmount[](sourceTokenAmounts.length);
bool isTokenGasOverridesEmpty = tokenGasOverrides.length == 0;
for (uint256 i = 0; i < sourceTokenAmounts.length; ++i) {
if (!isTokenGasOverridesEmpty) {
if (tokenGasOverrides[i] != 0) {
sourceTokenAmounts[i].destGasAmount = tokenGasOverrides[i];
}
}
destTokenAmounts[i] = _releaseOrMintSingleToken(
sourceTokenAmounts[i], originalSender, receiver, sourceChainSelector, offchainTokenData[i]
);
}
return destTokenAmounts;
}
// ================================================================
// │ Commit │
// ================================================================
/// @notice Transmit function for commit reports. The function requires signatures,
/// and expects the commit plugin type to be configured with signatures.
/// @param report serialized commit report.
/// @dev A commitReport can have two distinct parts (batched together to amortize the cost of checking sigs):
/// 1. Price updates
/// 2. A batch of merkle root and sequence number intervals (per-source)
/// Both have their own, separate, staleness checks, with price updates using the epoch and round number of the latest
/// price update. The merkle root checks for staleness are based on the seqNums. They need to be separate because
/// a price report for round t+2 might be included before a report containing a merkle root for round t+1. This merkle
/// root report for round t+1 is still valid and should not be rejected. When a report with a stale root but valid
/// price updates is submitted, we are OK to revert to preserve the invariant that we always revert on invalid
/// sequence number ranges. If that happens, prices will be updated in later rounds.
function commit(
bytes32[2] calldata reportContext,
bytes calldata report,
bytes32[] calldata rs,
bytes32[] calldata ss,
bytes32 rawVs
) external {
CommitReport memory commitReport = abi.decode(report, (CommitReport));
DynamicConfig storage dynamicConfig = s_dynamicConfig;
// Verify RMN signatures
if (commitReport.blessedMerkleRoots.length > 0) {
i_rmnRemote.verify(address(this), commitReport.blessedMerkleRoots, commitReport.rmnSignatures);
}
// Check if the report contains price updates.
if (commitReport.priceUpdates.tokenPriceUpdates.length > 0 || commitReport.priceUpdates.gasPriceUpdates.length > 0)
{
uint64 ocrSequenceNumber = uint64(uint256(reportContext[1]));
// Check for price staleness based on the epoch and round.
if (s_latestPriceSequenceNumber < ocrSequenceNumber) {
// If prices are not stale, update the latest epoch and round.
s_latestPriceSequenceNumber = ocrSequenceNumber;
// And update the prices in the fee quoter.
IFeeQuoter(dynamicConfig.feeQuoter).updatePrices(commitReport.priceUpdates);
} else {
// If prices are stale and the report doesn't contain a root, this report does not have any valid information
// and we revert. If it does contain a merkle root, continue to the root checking section.
if (commitReport.blessedMerkleRoots.length + commitReport.unblessedMerkleRoots.length == 0) {
revert StaleCommitReport();
}
}
}
for (uint256 i = 0; i < commitReport.blessedMerkleRoots.length; ++i) {
_commitRoot(commitReport.blessedMerkleRoots[i], true);
}
for (uint256 i = 0; i < commitReport.unblessedMerkleRoots.length; ++i) {
_commitRoot(commitReport.unblessedMerkleRoots[i], false);
}
emit CommitReportAccepted(
commitReport.blessedMerkleRoots, commitReport.unblessedMerkleRoots, commitReport.priceUpdates
);
_transmit(uint8(Internal.OCRPluginType.Commit), reportContext, report, rs, ss, rawVs);
}
/// @notice Commits a single merkle root. The blessing status has to match the source chain config.
/// @dev An unblessed root means that RMN verification is disabled for the source chain. It does not mean there is
/// some future point where the root will be blessed.
/// @param root The merkle root to commit.
/// @param isBlessed The blessing status of the root.
function _commitRoot(Internal.MerkleRoot memory root, bool isBlessed) internal {
uint64 sourceChainSelector = root.sourceChainSelector;
if (i_rmnRemote.isCursed(bytes16(uint128(sourceChainSelector)))) {
revert CursedByRMN(sourceChainSelector);
}
SourceChainConfig storage sourceChainConfig = _getEnabledSourceChainConfig(sourceChainSelector);
// If the root is blessed but RMN blessing is disabled for the source chain, or if the root is not blessed but RMN
// blessing is enabled, we revert.
if (isBlessed == sourceChainConfig.isRMNVerificationDisabled) {
revert RootBlessingMismatch(sourceChainSelector, root.merkleRoot, isBlessed);
}
if (keccak256(root.onRampAddress) != keccak256(sourceChainConfig.onRamp)) {
revert CommitOnRampMismatch(root.onRampAddress, sourceChainConfig.onRamp);
}
if (sourceChainConfig.minSeqNr != root.minSeqNr || root.minSeqNr > root.maxSeqNr) {
revert InvalidInterval(sourceChainSelector, root.minSeqNr, root.maxSeqNr);
}
bytes32 merkleRoot = root.merkleRoot;
if (merkleRoot == bytes32(0)) revert InvalidRoot();
// If we reached this section, the report should contain a valid root.
// We disallow duplicate roots as that would reset the timestamp and delay potential manual execution.
if (s_roots[sourceChainSelector][merkleRoot] != 0) {
revert RootAlreadyCommitted(sourceChainSelector, merkleRoot);
}
sourceChainConfig.minSeqNr = root.maxSeqNr + 1;
s_roots[sourceChainSelector][merkleRoot] = block.timestamp;
}
/// @notice Returns the sequence number of the last price update.
/// @return sequenceNumber The latest price update sequence number.
function getLatestPriceSequenceNumber() external view returns (uint64) {
return s_latestPriceSequenceNumber;
}
/// @notice Returns the timestamp of a potentially previously committed merkle root.
/// If the root was never committed 0 will be returned.
/// @param sourceChainSelector The source chain selector.
/// @param root The merkle root to check the commit status for.
/// @return timestamp The timestamp of the committed root or zero in the case that it was never committed.
function getMerkleRoot(uint64 sourceChainSelector, bytes32 root) external view returns (uint256) {
return s_roots[sourceChainSelector][root];
}
/// @notice Returns timestamp of when root was accepted or 0 if verification fails.
/// @dev This method uses a merkle tree within a merkle tree, with the hashedLeaves,
/// proofs and proofFlagBits being used to get the root of the inner tree.
/// This root is then used as the singular leaf of the outer tree.
/// @return timestamp The commit timestamp of the root.
function _verify(
uint64 sourceChainSelector,
bytes32[] memory hashedLeaves,
bytes32[] memory proofs,
uint256 proofFlagBits
) internal view virtual returns (uint256 timestamp) {
bytes32 root = MerkleMultiProof._merkleRoot(hashedLeaves, proofs, proofFlagBits);
return s_roots[sourceChainSelector][root];
}
/// @inheritdoc MultiOCR3Base
function _afterOCR3ConfigSet(
uint8 ocrPluginType
) internal override {
bool isSignatureVerificationEnabled = s_ocrConfigs[ocrPluginType].configInfo.isSignatureVerificationEnabled;
if (ocrPluginType == uint8(Internal.OCRPluginType.Commit)) {
// Signature verification must be enabled for commit plugin.
if (!isSignatureVerificationEnabled) {
revert SignatureVerificationRequiredInCommitPlugin();
}
// When the OCR config changes, we reset the sequence number since it is scoped per config digest.
// Note that s_minSeqNr/roots do not need to be reset as the roots persist across reconfigurations
// and are de-duplicated separately.
s_latestPriceSequenceNumber = 0;
} else if (ocrPluginType == uint8(Internal.OCRPluginType.Execution)) {
// Signature verification must be disabled for execution plugin.
if (isSignatureVerificationEnabled) {
revert SignatureVerificationNotAllowedInExecutionPlugin();
}
}
}
// ================================================================
// │ Config │
// ================================================================
/// @notice Returns the static config.
/// @dev This function will always return the same struct as the contents is static and can never change.
/// @return staticConfig The static config.
function getStaticConfig() external view returns (StaticConfig memory) {
return StaticConfig({
chainSelector: i_chainSelector,
gasForCallExactCheck: i_gasForCallExactCheck,
rmnRemote: i_rmnRemote,
tokenAdminRegistry: i_tokenAdminRegistry,
nonceManager: i_nonceManager
});
}
/// @notice Returns the current dynamic config.
/// @return dynamicConfig The current dynamic config.
function getDynamicConfig() external view returns (DynamicConfig memory) {
return s_dynamicConfig;
}
/// @notice Returns the source chain config for the provided source chain selector.
/// @param sourceChainSelector chain to retrieve configuration for.
/// @return sourceChainConfig The config for the source chain.
function getSourceChainConfig(
uint64 sourceChainSelector
) external view returns (SourceChainConfig memory) {
return s_sourceChainConfigs[sourceChainSelector];
}
/// @notice Returns all source chain configs.
/// @return sourceChainConfigs The source chain configs corresponding to all the supported chain selectors.
function getAllSourceChainConfigs() external view returns (uint64[] memory, SourceChainConfig[] memory) {
SourceChainConfig[] memory sourceChainConfigs = new SourceChainConfig[](s_sourceChainSelectors.length());
uint64[] memory sourceChainSelectors = new uint64[](s_sourceChainSelectors.length());
for (uint256 i = 0; i < s_sourceChainSelectors.length(); ++i) {
sourceChainSelectors[i] = uint64(s_sourceChainSelectors.at(i));
sourceChainConfigs[i] = s_sourceChainConfigs[sourceChainSelectors[i]];
}
return (sourceChainSelectors, sourceChainConfigs);
}
/// @notice Updates source configs.
/// @param sourceChainConfigUpdates Source chain configs.
function applySourceChainConfigUpdates(
SourceChainConfigArgs[] memory sourceChainConfigUpdates
) external onlyOwner {
_applySourceChainConfigUpdates(sourceChainConfigUpdates);
}
/// @notice Updates source configs.
/// @param sourceChainConfigUpdates Source chain configs.
function _applySourceChainConfigUpdates(
SourceChainConfigArgs[] memory sourceChainConfigUpdates
) internal {
for (uint256 i = 0; i < sourceChainConfigUpdates.length; ++i) {
SourceChainConfigArgs memory sourceConfigUpdate = sourceChainConfigUpdates[i];
uint64 sourceChainSelector = sourceConfigUpdate.sourceChainSelector;
if (sourceChainSelector == 0) {
revert ZeroChainSelectorNotAllowed();
}
if (address(sourceConfigUpdate.router) == address(0)) {
revert ZeroAddressNotAllowed();
}
SourceChainConfig storage currentConfig = s_sourceChainConfigs[sourceChainSelector];
bytes memory newOnRamp = sourceConfigUpdate.onRamp;
if (currentConfig.onRamp.length == 0) {
currentConfig.minSeqNr = 1;
emit SourceChainSelectorAdded(sourceChainSelector);
} else {
if (currentConfig.minSeqNr != 1 && keccak256(currentConfig.onRamp) != keccak256(newOnRamp)) {
// OnRamp updates should only happens due to a misconfiguration.
// If an OnRamp is misconfigured, no reports should have been committed and no messages should have been
// executed. This is enforced by the onRamp address check in the commit function.
revert InvalidOnRampUpdate(sourceChainSelector);
}
}
// OnRamp can never be zero - if it is, then the source chain has been added for the first time.
if (newOnRamp.length == 0 || keccak256(newOnRamp) == EMPTY_ENCODED_ADDRESS_HASH) {
revert ZeroAddressNotAllowed();
}
currentConfig.onRamp = newOnRamp;
currentConfig.isEnabled = sourceConfigUpdate.isEnabled;
currentConfig.router = sourceConfigUpdate.router;
currentConfig.isRMNVerificationDisabled = sourceConfigUpdate.isRMNVerificationDisabled;
// We don't need to check the return value, as inserting the item twice has no effect.
s_sourceChainSelectors.add(sourceChainSelector);
emit SourceChainConfigSet(sourceChainSelector, currentConfig);
}
}
/// @notice Sets the dynamic config.
/// @param dynamicConfig The new dynamic config.
function setDynamicConfig(
DynamicConfig memory dynamicConfig
) external onlyOwner {
_setDynamicConfig(dynamicConfig);
}
/// @notice Sets the dynamic config.
/// @param dynamicConfig The dynamic config.
function _setDynamicConfig(
DynamicConfig memory dynamicConfig
) internal {
if (dynamicConfig.feeQuoter == address(0)) {
revert ZeroAddressNotAllowed();
}
s_dynamicConfig = dynamicConfig;
emit DynamicConfigSet(dynamicConfig);
}
/// @notice Returns a source chain config with a check that the config is enabled.
/// @param sourceChainSelector Source chain selector to check for cursing.
/// @return sourceChainConfig The source chain config storage pointer.
function _getEnabledSourceChainConfig(
uint64 sourceChainSelector
) internal view returns (SourceChainConfig storage) {
SourceChainConfig storage sourceChainConfig = s_sourceChainConfigs[sourceChainSelector];
if (!sourceChainConfig.isEnabled) {
revert SourceChainNotEnabled(sourceChainSelector);
}
return sourceChainConfig;
}
// ================================================================
// │ Access │
// ================================================================
/// @notice Reverts as this contract should not be able to receive CCIP messages.
function ccipReceive(
Client.Any2EVMMessage calldata
) external pure {
// solhint-disable-next-line
revert();
}
}