Transaction Hash:
Block:
20614625 at Aug-26-2024 06:41:35 PM +UTC
Transaction Fee:
0.000328693344438084 ETH
$0.83
Gas Used:
119,436 Gas / 2.752045819 Gwei
Emitted Events:
190 |
PriceRegistry.UsdPerUnitGasUpdated( destChain=7264351850409363825, value=520207845799274239367403183286949792935721434, timestamp=1724697695 )
|
191 |
CommitStore.Transmitted( configDigest=00012DA522C4EB868A6396E5A3F26B12E29D3A68935341A418F613954FB6B34E, epoch=1793 )
|
Account State Difference:
Address | Before | After | State Difference | ||
---|---|---|---|---|---|
0x3e37F3D1...C36C88f62
Miner
| 2.627749251080206201 Eth | 2.627861628412606201 Eth | 0.0001123773324 | ||
0x76264869...67c7F190e | |||||
0x8c9b2Efb...763b958Ad | |||||
0xfc038715...B21434157 |
4.566943104748398273 Eth
Nonce: 23111
|
4.566614411403960189 Eth
Nonce: 23112
| 0.000328693344438084 |
Execution Trace
CommitStore.transmit( )
ARMProxy.STATICCALL( )
-
ARM.CALL( )
-
-
PriceRegistry.updatePrices( priceUpdates=[{name:tokenPriceUpdates, type:tuple[], order:1, indexed:false}, {name:gasPriceUpdates, type:tuple[], order:2, indexed:false}] )
-
Null: 0x000...001.44d0bd53( )
-
Null: 0x000...001.44d0bd53( )
-
Null: 0x000...001.44d0bd53( )
-
Null: 0x000...001.44d0bd53( )
-
Null: 0x000...001.44d0bd53( )
-
Null: 0x000...001.44d0bd53( )
File 1 of 4: CommitStore
File 2 of 4: PriceRegistry
File 3 of 4: ARMProxy
File 4 of 4: ARM
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.19; import {ITypeAndVersion} from "../shared/interfaces/ITypeAndVersion.sol"; import {ICommitStore} from "./interfaces/ICommitStore.sol"; import {IARM} from "./interfaces/IARM.sol"; import {IPriceRegistry} from "./interfaces/IPriceRegistry.sol"; import {OCR2Base} from "./ocr/OCR2Base.sol"; import {Internal} from "./libraries/Internal.sol"; import {MerkleMultiProof} from "./libraries/MerkleMultiProof.sol"; contract CommitStore is ICommitStore, ITypeAndVersion, OCR2Base { error StaleReport(); error PausedError(); error InvalidInterval(Interval interval); error InvalidRoot(); error InvalidCommitStoreConfig(); error BadARMSignal(); error RootAlreadyCommitted(); event Paused(address account); event Unpaused(address account); /// @dev RMN depends on this event, if changing, please notify the RMN maintainers. event ReportAccepted(CommitReport report); event ConfigSet(StaticConfig staticConfig, DynamicConfig dynamicConfig); event RootRemoved(bytes32 root); /// @notice Static commit store config /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. struct StaticConfig { uint64 chainSelector; // ───────╮ Destination chainSelector uint64 sourceChainSelector; // ─╯ Source chainSelector address onRamp; // OnRamp address on the source chain address armProxy; // ARM proxy address } /// @notice Dynamic commit store config struct DynamicConfig { address priceRegistry; // Price registry address on the destination chain } /// @notice a sequenceNumber interval /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. struct Interval { uint64 min; // ───╮ Minimum sequence number, inclusive uint64 max; // ───╯ Maximum sequence number, inclusive } /// @notice Report that is committed by the observing DON at the committing phase /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. struct CommitReport { Internal.PriceUpdates priceUpdates; Interval interval; bytes32 merkleRoot; } // STATIC CONFIG // solhint-disable-next-line chainlink-solidity/all-caps-constant-storage-variables string public constant override typeAndVersion = "CommitStore 1.2.0"; // Chain ID of this chain uint64 internal immutable i_chainSelector; // Chain ID of the source chain uint64 internal immutable i_sourceChainSelector; // The onRamp address on the source chain address internal immutable i_onRamp; // The address of the arm proxy address internal immutable i_armProxy; // DYNAMIC CONFIG // The dynamic commitStore config DynamicConfig internal s_dynamicConfig; // STATE // The min sequence number expected for future messages uint64 private s_minSeqNr = 1; /// @dev The epoch and round of the last report uint40 private s_latestPriceEpochAndRound; /// @dev Whether this OnRamp is paused or not bool private s_paused = false; // merkleRoot => timestamp when received mapping(bytes32 merkleRoot => uint256 timestamp) private s_roots; /// @param staticConfig Containing the static part of the commitStore config /// @dev When instantiating OCR2Base we set UNIQUE_REPORTS to false, which means /// that we do not require 2f+1 signatures on a report, only f+1 to save gas. 2f+1 is required /// only if one must strictly ensure that for a given round there is only one valid report ever generated by /// the DON. In our case additional valid reports (i.e. approved by >= f+1 oracles) are not a problem, as they will /// will either be ignored (reverted as an invalid interval) or will be accepted as an additional valid price update. constructor(StaticConfig memory staticConfig) OCR2Base(false) { if ( staticConfig.onRamp == address(0) || staticConfig.chainSelector == 0 || staticConfig.sourceChainSelector == 0 || staticConfig.armProxy == address(0) ) revert InvalidCommitStoreConfig(); i_chainSelector = staticConfig.chainSelector; i_sourceChainSelector = staticConfig.sourceChainSelector; i_onRamp = staticConfig.onRamp; i_armProxy = staticConfig.armProxy; } // ================================================================ // │ Verification │ // ================================================================ /// @notice Returns the next expected sequence number. /// @return the next expected sequenceNumber. function getExpectedNextSequenceNumber() external view returns (uint64) { return s_minSeqNr; } /// @notice Sets the minimum sequence number. /// @param minSeqNr The new minimum sequence number. function setMinSeqNr(uint64 minSeqNr) external onlyOwner { s_minSeqNr = minSeqNr; } /// @notice Returns the epoch and round of the last price update. /// @return the latest price epoch and round. function getLatestPriceEpochAndRound() public view returns (uint64) { return s_latestPriceEpochAndRound; } /// @notice Sets the latest epoch and round for price update. /// @param latestPriceEpochAndRound The new epoch and round for prices. function setLatestPriceEpochAndRound(uint40 latestPriceEpochAndRound) external onlyOwner { s_latestPriceEpochAndRound = latestPriceEpochAndRound; } /// @notice Returns the timestamp of a potentially previously committed merkle root. /// If the root was never committed 0 will be returned. /// @param root The merkle root to check the commit status for. /// @return the timestamp of the committed root or zero in the case that it was never /// committed. function getMerkleRoot(bytes32 root) external view returns (uint256) { return s_roots[root]; } /// @notice Returns if a root is blessed or not. /// @param root The merkle root to check the blessing status for. /// @return whether the root is blessed or not. function isBlessed(bytes32 root) public view returns (bool) { return IARM(i_armProxy).isBlessed(IARM.TaggedRoot({commitStore: address(this), root: root})); } /// @notice Used by the owner in case an invalid sequence of roots has been /// posted and needs to be removed. The interval in the report is trusted. /// @param rootToReset The roots that will be reset. This function will only /// reset roots that are not blessed. function resetUnblessedRoots(bytes32[] calldata rootToReset) external onlyOwner { for (uint256 i = 0; i < rootToReset.length; ++i) { bytes32 root = rootToReset[i]; if (!isBlessed(root)) { delete s_roots[root]; emit RootRemoved(root); } } } /// @inheritdoc ICommitStore function verify( bytes32[] calldata hashedLeaves, bytes32[] calldata proofs, uint256 proofFlagBits ) external view override whenNotPaused returns (uint256 timestamp) { bytes32 root = MerkleMultiProof.merkleRoot(hashedLeaves, proofs, proofFlagBits); // Only return non-zero if present and blessed. if (!isBlessed(root)) { return 0; } return s_roots[root]; } /// @inheritdoc OCR2Base /// @dev A commitReport can have two distinct parts (batched together to amortize the cost of checking sigs): /// 1. Price updates /// 2. A merkle root and sequence number interval /// 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 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 updates in later rounds. function _report(bytes calldata encodedReport, uint40 epochAndRound) internal override whenNotPaused whenHealthy { CommitReport memory report = abi.decode(encodedReport, (CommitReport)); // Check if the report contains price updates if (report.priceUpdates.tokenPriceUpdates.length > 0 || report.priceUpdates.gasPriceUpdates.length > 0) { // Check for price staleness based on the epoch and round if (s_latestPriceEpochAndRound < epochAndRound) { // If prices are not stale, update the latest epoch and round s_latestPriceEpochAndRound = epochAndRound; // And update the prices in the price registry IPriceRegistry(s_dynamicConfig.priceRegistry).updatePrices(report.priceUpdates); // If there is no root, the report only contained fee updated and // we return to not revert on the empty root check below. if (report.merkleRoot == bytes32(0)) return; } 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 (report.merkleRoot == bytes32(0)) revert StaleReport(); } } // If we reached this section, the report should contain a valid root if (s_minSeqNr != report.interval.min || report.interval.min > report.interval.max) revert InvalidInterval(report.interval); if (report.merkleRoot == bytes32(0)) revert InvalidRoot(); // Disallow duplicate roots as that would reset the timestamp and // delay potential manual execution. if (s_roots[report.merkleRoot] != 0) revert RootAlreadyCommitted(); s_minSeqNr = report.interval.max + 1; s_roots[report.merkleRoot] = block.timestamp; emit ReportAccepted(report); } // ================================================================ // │ Config │ // ================================================================ /// @notice Returns the static commit store config. /// @dev RMN depends on this function, if changing, please notify the RMN maintainers. /// @return the configuration. function getStaticConfig() external view returns (StaticConfig memory) { return StaticConfig({ chainSelector: i_chainSelector, sourceChainSelector: i_sourceChainSelector, onRamp: i_onRamp, armProxy: i_armProxy }); } /// @notice Returns the dynamic commit store config. /// @return the configuration. function getDynamicConfig() external view returns (DynamicConfig memory) { return s_dynamicConfig; } /// @notice Sets the dynamic config. This function is called during `setOCR2Config` flow function _beforeSetConfig(bytes memory onchainConfig) internal override { DynamicConfig memory dynamicConfig = abi.decode(onchainConfig, (DynamicConfig)); if (dynamicConfig.priceRegistry == address(0)) revert InvalidCommitStoreConfig(); s_dynamicConfig = dynamicConfig; // When the OCR config changes, we reset the price epoch and round // since epoch and rounds are 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_latestPriceEpochAndRound = 0; emit ConfigSet( StaticConfig({ chainSelector: i_chainSelector, sourceChainSelector: i_sourceChainSelector, onRamp: i_onRamp, armProxy: i_armProxy }), dynamicConfig ); } // ================================================================ // │ Access and ARM │ // ================================================================ /// @notice Single function to check the status of the commitStore. function isUnpausedAndARMHealthy() external view returns (bool) { return !IARM(i_armProxy).isCursed() && !s_paused; } /// @notice Support querying whether health checker is healthy. function isARMHealthy() external view returns (bool) { return !IARM(i_armProxy).isCursed(); } /// @notice Ensure that the ARM has not emitted a bad signal, and that the latest heartbeat is not stale. modifier whenHealthy() { if (IARM(i_armProxy).isCursed()) revert BadARMSignal(); _; } /// @notice Modifier to make a function callable only when the contract is not paused. modifier whenNotPaused() { if (paused()) revert PausedError(); _; } /// @notice Returns true if the contract is paused, and false otherwise. function paused() public view returns (bool) { return s_paused; } /// @notice Pause the contract /// @dev only callable by the owner function pause() external onlyOwner { s_paused = true; emit Paused(msg.sender); } /// @notice Unpause the contract /// @dev only callable by the owner function unpause() external onlyOwner { s_paused = false; emit Unpaused(msg.sender); } } // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface ITypeAndVersion { function typeAndVersion() external pure returns (string memory); } // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface ICommitStore { /// @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. function verify( bytes32[] calldata hashedLeaves, bytes32[] calldata proofs, uint256 proofFlagBits ) external view returns (uint256 timestamp); /// @notice Returns the expected next sequence number function getExpectedNextSequenceNumber() external view returns (uint64 sequenceNumber); } // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; /// @notice This interface contains the only ARM-related functions that might be used on-chain by other CCIP contracts. interface IARM { /// @notice A Merkle root tagged with the address of the commit store contract it is destined for. struct TaggedRoot { address commitStore; bytes32 root; } /// @notice Callers MUST NOT cache the return value as a blessed tagged root could become unblessed. function isBlessed(TaggedRoot calldata taggedRoot) external view returns (bool); /// @notice When the ARM is "cursed", CCIP pauses until the curse is lifted. function isCursed() external view returns (bool); } // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {Internal} from "../libraries/Internal.sol"; interface IPriceRegistry { /// @notice Update the price for given tokens and gas prices for given chains. /// @param priceUpdates The price updates to apply. function updatePrices(Internal.PriceUpdates memory priceUpdates) external; /// @notice Get the `tokenPrice` for a given token. /// @param token The token to get the price for. /// @return tokenPrice The tokenPrice for the given token. function getTokenPrice(address token) external view returns (Internal.TimestampedPackedUint224 memory); /// @notice Get the `tokenPrice` for a given token, checks if the price is valid. /// @param token The token to get the price for. /// @return tokenPrice The tokenPrice for the given token if it exists and is valid. function getValidatedTokenPrice(address token) external view returns (uint224); /// @notice Get the `tokenPrice` for an array of tokens. /// @param tokens The tokens to get prices for. /// @return tokenPrices The tokenPrices for the given tokens. function getTokenPrices(address[] calldata tokens) external view returns (Internal.TimestampedPackedUint224[] memory); /// @notice Get an encoded `gasPrice` for a given destination chain ID. /// The 224-bit result encodes necessary gas price components. /// On L1 chains like Ethereum or Avax, the only component is the gas price. /// On Optimistic Rollups, there are two components - the L2 gas price, and L1 base fee for data availability. /// On future chains, there could be more or differing price components. /// PriceRegistry does not contain chain-specific logic to parse destination chain price components. /// @param destChainSelector The destination chain to get the price for. /// @return gasPrice The encoded gasPrice for the given destination chain ID. function getDestinationChainGasPrice( uint64 destChainSelector ) external view returns (Internal.TimestampedPackedUint224 memory); /// @notice Gets the fee token price and the gas price, both denominated in dollars. /// @param token The source token to get the price for. /// @param destChainSelector The destination chain to get the gas price for. /// @return tokenPrice The price of the feeToken in 1e18 dollars per base unit. /// @return gasPrice The price of gas in 1e18 dollars per base unit. function getTokenAndGasPrices( address token, uint64 destChainSelector ) external view returns (uint224 tokenPrice, uint224 gasPrice); /// @notice Convert a given token amount to target token amount. /// @param fromToken The given token address. /// @param fromTokenAmount The given token amount. /// @param toToken The target token address. /// @return toTokenAmount The target token amount. function convertTokenAmount( address fromToken, uint256 fromTokenAmount, address toToken ) external view returns (uint256 toTokenAmount); } // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.0; import {OwnerIsCreator} from "../../shared/access/OwnerIsCreator.sol"; import {OCR2Abstract} from "./OCR2Abstract.sol"; /// @notice Onchain verification of reports from the offchain reporting protocol /// @dev For details on its operation, see the offchain reporting protocol design /// doc, which refers to this contract as simply the "contract". abstract contract OCR2Base is OwnerIsCreator, OCR2Abstract { error InvalidConfig(string message); error WrongMessageLength(uint256 expected, uint256 actual); error ConfigDigestMismatch(bytes32 expected, bytes32 actual); error ForkedChain(uint256 expected, uint256 actual); error WrongNumberOfSignatures(); error SignaturesOutOfRegistration(); error UnauthorizedTransmitter(); error UnauthorizedSigner(); error NonUniqueSignatures(); error OracleCannotBeZeroAddress(); // Packing these fields used on the hot path in a ConfigInfo variable reduces the // retrieval of all of them to a minimum number of SLOADs. struct ConfigInfo { bytes32 latestConfigDigest; uint8 f; uint8 n; } // Used for s_oracles[a].role, where a is an address, to track the purpose // of the address, or to indicate that the address is unset. enum Role { // No oracle role has been set for address a Unset, // Signing address for the s_oracles[a].index'th oracle. I.e., report // signatures from this oracle should ecrecover back to address a. Signer, // Transmission address for the s_oracles[a].index'th oracle. I.e., if a // report is received by OCR2Aggregator.transmit in which msg.sender is // a, it is attributed to the s_oracles[a].index'th oracle. Transmitter } struct Oracle { uint8 index; // Index of oracle in s_signers/s_transmitters Role role; // Role of the address which mapped to this struct } // The current config ConfigInfo internal s_configInfo; // incremented each time a new config is posted. This count is incorporated // into the config digest, to prevent replay attacks. uint32 internal s_configCount; // makes it easier for offchain systems to extract config from logs. uint32 internal s_latestConfigBlockNumber; // signer OR transmitter address mapping(address signerOrTransmitter => Oracle oracle) internal s_oracles; // s_signers contains the signing address of each oracle address[] internal s_signers; // s_transmitters contains the transmission address of each oracle, // i.e. the address the oracle actually sends transactions to the contract from address[] internal s_transmitters; // The constant-length components of the msg.data sent to transmit. // See the "If we wanted to call sam" example on for example reasoning // https://solidity.readthedocs.io/en/v0.7.2/abi-spec.html uint16 private constant TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT = 4 + // function selector 32 * 3 + // 3 words containing reportContext 32 + // word containing start location of abiencoded report value 32 + // word containing location start of abiencoded rs value 32 + // word containing start location of abiencoded ss value 32 + // rawVs value 32 + // word containing length of report 32 + // word containing length rs 32; // word containing length of ss bool internal immutable i_uniqueReports; uint256 internal immutable i_chainID; constructor(bool uniqueReports) { i_uniqueReports = uniqueReports; i_chainID = block.chainid; } // Reverts transaction if config args are invalid modifier checkConfigValid( uint256 numSigners, uint256 numTransmitters, uint256 f ) { if (numSigners > MAX_NUM_ORACLES) revert InvalidConfig("too many signers"); if (f == 0) revert InvalidConfig("f must be positive"); if (numSigners != numTransmitters) revert InvalidConfig("oracle addresses out of registration"); if (numSigners <= 3 * f) revert InvalidConfig("faulty-oracle f too high"); _; } /// @notice sets offchain reporting protocol configuration incl. participating oracles /// @param signers addresses with which oracles sign the reports /// @param transmitters addresses oracles use to transmit the reports /// @param f number of faulty oracles the system can tolerate /// @param onchainConfig encoded on-chain contract configuration /// @param offchainConfigVersion version number for offchainEncoding schema /// @param offchainConfig encoded off-chain oracle configuration function setOCR2Config( address[] memory signers, address[] memory transmitters, uint8 f, bytes memory onchainConfig, uint64 offchainConfigVersion, bytes memory offchainConfig ) external override checkConfigValid(signers.length, transmitters.length, f) onlyOwner { _beforeSetConfig(onchainConfig); uint256 oldSignerLength = s_signers.length; for (uint256 i = 0; i < oldSignerLength; ++i) { delete s_oracles[s_signers[i]]; delete s_oracles[s_transmitters[i]]; } uint256 newSignersLength = signers.length; for (uint256 i = 0; i < newSignersLength; ++i) { // add new signer/transmitter addresses address signer = signers[i]; if (s_oracles[signer].role != Role.Unset) revert InvalidConfig("repeated signer address"); if (signer == address(0)) revert OracleCannotBeZeroAddress(); s_oracles[signer] = Oracle(uint8(i), Role.Signer); address transmitter = transmitters[i]; if (s_oracles[transmitter].role != Role.Unset) revert InvalidConfig("repeated transmitter address"); if (transmitter == address(0)) revert OracleCannotBeZeroAddress(); s_oracles[transmitter] = Oracle(uint8(i), Role.Transmitter); } s_signers = signers; s_transmitters = transmitters; s_configInfo.f = f; s_configInfo.n = uint8(newSignersLength); s_configInfo.latestConfigDigest = _configDigestFromConfigData( block.chainid, address(this), ++s_configCount, signers, transmitters, f, onchainConfig, offchainConfigVersion, offchainConfig ); uint32 previousConfigBlockNumber = s_latestConfigBlockNumber; s_latestConfigBlockNumber = uint32(block.number); emit ConfigSet( previousConfigBlockNumber, s_configInfo.latestConfigDigest, s_configCount, signers, transmitters, f, onchainConfig, offchainConfigVersion, offchainConfig ); } /// @dev Hook that is run from setOCR2Config() right after validating configuration. /// Empty by default, please provide an implementation in a child contract if you need additional configuration processing function _beforeSetConfig(bytes memory _onchainConfig) internal virtual {} /// @return list of addresses permitted to transmit reports to this contract /// @dev The list will match the order used to specify the transmitter during setConfig function getTransmitters() external view returns (address[] memory) { return s_transmitters; } /// @notice transmit is called to post a new report to the contract /// @param report serialized report, which the signatures are signing. /// @param rs ith element is the R components of the ith signature on report. Must have at most MAX_NUM_ORACLES entries /// @param ss ith element is the S components of the ith signature on report. Must have at most MAX_NUM_ORACLES entries /// @param rawVs ith element is the the V component of the ith signature function transmit( // NOTE: If these parameters are changed, expectedMsgDataLength and/or // TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT need to be changed accordingly bytes32[3] calldata reportContext, bytes calldata report, bytes32[] calldata rs, bytes32[] calldata ss, bytes32 rawVs // signatures ) external override { // Scoping this reduces stack pressure and gas usage { // report and epochAndRound _report(report, uint40(uint256(reportContext[1]))); } // reportContext consists of: // reportContext[0]: ConfigDigest // reportContext[1]: 27 byte padding, 4-byte epoch and 1-byte round // reportContext[2]: ExtraHash bytes32 configDigest = reportContext[0]; ConfigInfo memory configInfo = s_configInfo; if (configInfo.latestConfigDigest != configDigest) revert ConfigDigestMismatch(configInfo.latestConfigDigest, configDigest); // If the cached chainID at time of deployment doesn't match the current chainID, we reject all signed reports. // This avoids a (rare) scenario where chain A forks into chain A and A', A' still has configDigest // calculated from chain A and so OCR reports will be valid on both forks. if (i_chainID != block.chainid) revert ForkedChain(i_chainID, block.chainid); emit Transmitted(configDigest, uint32(uint256(reportContext[1]) >> 8)); uint256 expectedNumSignatures; if (i_uniqueReports) { expectedNumSignatures = (configInfo.n + configInfo.f) / 2 + 1; } else { expectedNumSignatures = configInfo.f + 1; } if (rs.length != expectedNumSignatures) revert WrongNumberOfSignatures(); if (rs.length != ss.length) revert SignaturesOutOfRegistration(); // Scoping this reduces stack pressure and gas usage { Oracle memory transmitter = s_oracles[msg.sender]; // Check that sender is authorized to report if (!(transmitter.role == Role.Transmitter && msg.sender == s_transmitters[transmitter.index])) revert UnauthorizedTransmitter(); } // Scoping this reduces stack pressure and gas usage { uint256 expectedDataLength = uint256(TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT) + report.length + // one byte pure entry in _report rs.length * 32 + // 32 bytes per entry in _rs ss.length * 32; // 32 bytes per entry in _ss) if (msg.data.length != expectedDataLength) revert WrongMessageLength(expectedDataLength, msg.data.length); } // Verify signatures attached to report bytes32 h = keccak256(abi.encodePacked(keccak256(report), reportContext)); bool[MAX_NUM_ORACLES] memory signed; uint256 numberOfSignatures = rs.length; for (uint256 i = 0; i < numberOfSignatures; ++i) { // Safe from ECDSA malleability here since we check for duplicate signers. address signer = ecrecover(h, uint8(rawVs[i]) + 27, rs[i], ss[i]); // Since we disallow address(0) as a valid signer address, it can // never have a signer role. Oracle memory oracle = s_oracles[signer]; if (oracle.role != Role.Signer) revert UnauthorizedSigner(); if (signed[oracle.index]) revert NonUniqueSignatures(); signed[oracle.index] = true; } } /// @notice information about current offchain reporting protocol configuration /// @return configCount ordinal number of current config, out of all configs applied to this contract so far /// @return blockNumber block at which this config was set /// @return configDigest domain-separation tag for current config (see _configDigestFromConfigData) function latestConfigDetails() external view override returns (uint32 configCount, uint32 blockNumber, bytes32 configDigest) { return (s_configCount, s_latestConfigBlockNumber, s_configInfo.latestConfigDigest); } /// @inheritdoc OCR2Abstract function latestConfigDigestAndEpoch() external view virtual override returns (bool scanLogs, bytes32 configDigest, uint32 epoch) { return (true, bytes32(0), uint32(0)); } function _report(bytes calldata report, uint40 epochAndRound) internal virtual; } // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {Client} from "./Client.sol"; 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; /// @notice A collection of token price and gas price updates. /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. struct PriceUpdates { TokenPriceUpdate[] tokenPriceUpdates; GasPriceUpdate[] gasPriceUpdates; } /// @notice Token price in USD. /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. struct TokenPriceUpdate { address sourceToken; // Source token uint224 usdPerToken; // 1e18 USD per smallest unit of token } /// @notice Gas price for a given chain in USD, its value may contain tightly packed fields. /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. struct GasPriceUpdate { uint64 destChainSelector; // Destination chain selector uint224 usdPerUnitGas; // 1e18 USD per smallest unit (e.g. wei) of destination chain gas } /// @notice A timestamped uint224 value that can contain several tightly packed fields. struct TimestampedPackedUint224 { uint224 value; // ───────╮ Value in uint224, packed. uint32 timestamp; // ────╯ Timestamp of the most recent price update. } /// @dev Gas price is stored in 112-bit unsigned int. uint224 can pack 2 prices. /// When packing L1 and L2 gas prices, L1 gas price is left-shifted to the higher-order bits. /// Using uint8 type, which cannot be higher than other bit shift operands, to avoid shift operand type warning. uint8 public constant GAS_PRICE_BITS = 112; struct PoolUpdate { address token; // The IERC20 token address address pool; // The token pool address } /// @notice Report that is submitted by the execution DON at the execution phase. /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. struct ExecutionReport { EVM2EVMMessage[] messages; // Contains a bytes array for each message, each inner bytes array contains bytes per transferred token bytes[][] offchainTokenData; bytes32[] proofs; uint256 proofFlagBits; } /// @notice The cross chain message that gets committed to EVM chains. /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. struct EVM2EVMMessage { uint64 sourceChainSelector; // ─────────╮ the chain selector of the source chain, note: not chainId address sender; // ─────────────────────╯ sender address on the source chain address receiver; // ───────────────────╮ receiver address on the destination chain uint64 sequenceNumber; // ──────────────╯ sequence number, not unique across lanes uint256 gasLimit; // user supplied maximum gas amount available for dest chain execution bool strict; // ────────────────────────╮ DEPRECATED uint64 nonce; // │ nonce for this lane for this sender, not unique across senders/lanes address feeToken; // ───────────────────╯ fee token uint256 feeTokenAmount; // fee token amount bytes data; // arbitrary data payload supplied by the message sender Client.EVMTokenAmount[] tokenAmounts; // array of tokens and amounts to transfer bytes[] sourceTokenData; // array of token pool return values, one per token bytes32 messageId; // a hash of the message data } /// @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; /// @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; function _toAny2EVMMessage( EVM2EVMMessage memory original, Client.EVMTokenAmount[] memory destTokenAmounts ) internal pure returns (Client.Any2EVMMessage memory message) { message = Client.Any2EVMMessage({ messageId: original.messageId, sourceChainSelector: original.sourceChainSelector, sender: abi.encode(original.sender), data: original.data, destTokenAmounts: destTokenAmounts }); } bytes32 internal constant EVM_2_EVM_MESSAGE_HASH = keccak256("EVM2EVMMessageHashV2"); function _hash(EVM2EVMMessage 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.sequenceNumber, original.gasLimit, original.strict, original.nonce, original.feeToken, original.feeTokenAmount ) ), keccak256(original.data), keccak256(abi.encode(original.tokenAmounts)), keccak256(abi.encode(original.sourceTokenData)) ) ); } /// @notice Enum listing the possible message execution states within /// the offRamp contract. /// UNTOUCHED never executed /// IN_PROGRESS currently being executed, used a replay protection /// SUCCESS successfully executed. End state /// FAILURE unsuccessfully executed, manual execution is now enabled. /// @dev RMN depends on this enum, if changing, please notify the RMN maintainers. enum MessageExecutionState { UNTOUCHED, IN_PROGRESS, SUCCESS, FAILURE } } // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.0; library MerkleMultiProof { /// @notice Leaf domain separator, should be used as the first 32 bytes of a leaf's preimage. bytes32 internal constant LEAF_DOMAIN_SEPARATOR = 0x0000000000000000000000000000000000000000000000000000000000000000; /// @notice Internal domain separator, should be used as the first 32 bytes of an internal node's preiimage. bytes32 internal constant INTERNAL_DOMAIN_SEPARATOR = 0x0000000000000000000000000000000000000000000000000000000000000001; uint256 internal constant MAX_NUM_HASHES = 256; error InvalidProof(); error LeavesCannotBeEmpty(); /// @notice Computes the root based on provided pre-hashed leaf nodes in /// leaves, internal nodes in proofs, and using proofFlagBits' i-th bit to /// determine if an element of proofs or one of the previously computed leafs /// or internal nodes will be used for the i-th hash. /// @param leaves Should be pre-hashed and the first 32 bytes of a leaf's /// preimage should match LEAF_DOMAIN_SEPARATOR. /// @param proofs The hashes to be used instead of a leaf hash when the proofFlagBits /// indicates a proof should be used. /// @param proofFlagBits A single uint256 of which each bit indicates whether a leaf or /// a proof needs to be used in a hash operation. /// @dev the maximum number of hash operations it set to 256. Any input that would require /// more than 256 hashes to get to a root will revert. /// @dev For given input `leaves` = [a,b,c] `proofs` = [D] and `proofFlagBits` = 5 /// totalHashes = 3 + 1 - 1 = 3 /// ** round 1 ** /// proofFlagBits = (5 >> 0) & 1 = true /// hashes[0] = hashPair(a, b) /// (leafPos, hashPos, proofPos) = (2, 0, 0); /// /// ** round 2 ** /// proofFlagBits = (5 >> 1) & 1 = false /// hashes[1] = hashPair(D, c) /// (leafPos, hashPos, proofPos) = (3, 0, 1); /// /// ** round 3 ** /// proofFlagBits = (5 >> 2) & 1 = true /// hashes[2] = hashPair(hashes[0], hashes[1]) /// (leafPos, hashPos, proofPos) = (3, 2, 1); /// /// i = 3 and no longer < totalHashes. The algorithm is done /// return hashes[totalHashes - 1] = hashes[2]; the last hash we computed. // We mark this function as internal to force it to be inlined in contracts // that use it, but semantically it is public. // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore function merkleRoot( bytes32[] memory leaves, bytes32[] memory proofs, uint256 proofFlagBits ) internal pure returns (bytes32) { unchecked { uint256 leavesLen = leaves.length; uint256 proofsLen = proofs.length; if (leavesLen == 0) revert LeavesCannotBeEmpty(); if (!(leavesLen <= MAX_NUM_HASHES + 1 && proofsLen <= MAX_NUM_HASHES + 1)) revert InvalidProof(); uint256 totalHashes = leavesLen + proofsLen - 1; if (!(totalHashes <= MAX_NUM_HASHES)) revert InvalidProof(); if (totalHashes == 0) { return leaves[0]; } bytes32[] memory hashes = new bytes32[](totalHashes); (uint256 leafPos, uint256 hashPos, uint256 proofPos) = (0, 0, 0); for (uint256 i = 0; i < totalHashes; ++i) { // Checks if the bit flag signals the use of a supplied proof or a leaf/previous hash. bytes32 a; if (proofFlagBits & (1 << i) == (1 << i)) { // Use a leaf or a previously computed hash. if (leafPos < leavesLen) { a = leaves[leafPos++]; } else { a = hashes[hashPos++]; } } else { // Use a supplied proof. a = proofs[proofPos++]; } // The second part of the hashed pair is never a proof as hashing two proofs would result in a // hash that can already be computed offchain. bytes32 b; if (leafPos < leavesLen) { b = leaves[leafPos++]; } else { b = hashes[hashPos++]; } if (!(hashPos <= i)) revert InvalidProof(); hashes[i] = _hashPair(a, b); } if (!(hashPos == totalHashes - 1 && leafPos == leavesLen && proofPos == proofsLen)) revert InvalidProof(); // Return the last hash. return hashes[totalHashes - 1]; } } /// @notice Hashes two bytes32 objects in their given order, prepended by the /// INTERNAL_DOMAIN_SEPARATOR. function _hashInternalNode(bytes32 left, bytes32 right) private pure returns (bytes32 hash) { return keccak256(abi.encode(INTERNAL_DOMAIN_SEPARATOR, left, right)); } /// @notice Hashes two bytes32 objects. The order is taken into account, /// using the lower value first. function _hashPair(bytes32 a, bytes32 b) private pure returns (bytes32) { return a < b ? _hashInternalNode(a, b) : _hashInternalNode(b, a); } } // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {ConfirmedOwner} from "./ConfirmedOwner.sol"; /// @title The OwnerIsCreator contract /// @notice A contract with helpers for basic contract ownership. contract OwnerIsCreator is ConfirmedOwner { constructor() ConfirmedOwner(msg.sender) {} } // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.0; import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; abstract contract OCR2Abstract is ITypeAndVersion { // Maximum number of oracles the offchain reporting protocol is designed for uint256 internal constant MAX_NUM_ORACLES = 31; /// @notice triggers a new run of the offchain reporting protocol /// @param previousConfigBlockNumber block in which the previous config was set, to simplify historic analysis /// @param configDigest configDigest of this configuration /// @param configCount ordinal number of this config setting among all config settings over the life of this contract /// @param signers ith element is address ith oracle uses to sign a report /// @param transmitters ith element is address ith oracle uses to transmit a report via the transmit method /// @param f maximum number of faulty/dishonest oracles the protocol can tolerate while still working correctly /// @param onchainConfig serialized configuration used by the contract (and possibly oracles) /// @param offchainConfigVersion version of the serialization format used for "offchainConfig" parameter /// @param offchainConfig serialized configuration used by the oracles exclusively and only passed through the contract event ConfigSet( uint32 previousConfigBlockNumber, bytes32 configDigest, uint64 configCount, address[] signers, address[] transmitters, uint8 f, bytes onchainConfig, uint64 offchainConfigVersion, bytes offchainConfig ); /// @notice sets offchain reporting protocol configuration incl. participating oracles /// @param signers addresses with which oracles sign the reports /// @param transmitters addresses oracles use to transmit the reports /// @param f number of faulty oracles the system can tolerate /// @param onchainConfig serialized configuration used by the contract (and possibly oracles) /// @param offchainConfigVersion version number for offchainEncoding schema /// @param offchainConfig serialized configuration used by the oracles exclusively and only passed through the contract function setOCR2Config( address[] memory signers, address[] memory transmitters, uint8 f, bytes memory onchainConfig, uint64 offchainConfigVersion, bytes memory offchainConfig ) external virtual; /// @notice information about current offchain reporting protocol configuration /// @return configCount ordinal number of current config, out of all configs applied to this contract so far /// @return blockNumber block at which this config was set /// @return configDigest domain-separation tag for current config (see _configDigestFromConfigData) function latestConfigDetails() external view virtual returns (uint32 configCount, uint32 blockNumber, bytes32 configDigest); function _configDigestFromConfigData( uint256 chainId, address contractAddress, uint64 configCount, address[] memory signers, address[] memory transmitters, uint8 f, bytes memory onchainConfig, uint64 offchainConfigVersion, bytes memory offchainConfig ) internal pure returns (bytes32) { uint256 h = uint256( keccak256( abi.encode( chainId, contractAddress, configCount, signers, transmitters, f, onchainConfig, offchainConfigVersion, offchainConfig ) ) ); uint256 prefixMask = type(uint256).max << (256 - 16); // 0xFFFF00..00 uint256 prefix = 0x0001 << (256 - 16); // 0x000100..00 return bytes32((prefix & prefixMask) | (h & ~prefixMask)); } /// @notice optionally emitted to indicate the latest configDigest and epoch for /// which a report was successfully transmitted. Alternatively, the contract may /// use latestConfigDigestAndEpoch with scanLogs set to false. event Transmitted(bytes32 configDigest, uint32 epoch); /// @notice optionally returns the latest configDigest and epoch for which a /// report was successfully transmitted. Alternatively, the contract may return /// scanLogs set to true and use Transmitted events to provide this information /// to offchain watchers. /// @return scanLogs indicates whether to rely on the configDigest and epoch /// returned or whether to scan logs for the Transmitted event instead. /// @return configDigest /// @return epoch function latestConfigDigestAndEpoch() external view virtual returns (bool scanLogs, bytes32 configDigest, uint32 epoch); /// @notice transmit is called to post a new report to the contract /// @param report serialized report, which the signatures are signing. /// @param rs ith element is the R components of the ith signature on report. Must have at most MAX_NUM_ORACLES entries /// @param ss ith element is the S components of the ith signature on report. Must have at most MAX_NUM_ORACLES entries /// @param rawVs ith element is the the V component of the ith signature function transmit( // NOTE: If these parameters are changed, expectedMsgDataLength and/or // TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT need to be changed accordingly bytes32[3] calldata reportContext, bytes calldata report, bytes32[] calldata rs, bytes32[] calldata ss, bytes32 rawVs // signatures ) external virtual; } // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; // End consumer library. library Client { /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. struct EVMTokenAmount { address token; // token address on the local chain. uint256 amount; // Amount of tokens. } struct Any2EVMMessage { bytes32 messageId; // MessageId corresponding to ccipSend on source. uint64 sourceChainSelector; // Source chain selector. bytes sender; // abi.decode(sender) if coming from an EVM chain. bytes data; // payload sent in original message. EVMTokenAmount[] destTokenAmounts; // Tokens and their amounts in their destination chain representation. } // If extraArgs is empty bytes, the default is 200k gas limit. struct EVM2AnyMessage { bytes receiver; // abi.encode(receiver address) for dest EVM chains bytes data; // Data payload EVMTokenAmount[] tokenAmounts; // Token transfers address feeToken; // Address of feeToken. address(0) means you will send msg.value. bytes extraArgs; // Populate this with _argsToBytes(EVMExtraArgsV1) } // bytes4(keccak256("CCIP EVMExtraArgsV1")); bytes4 public constant EVM_EXTRA_ARGS_V1_TAG = 0x97a657c9; struct EVMExtraArgsV1 { uint256 gasLimit; } function _argsToBytes(EVMExtraArgsV1 memory extraArgs) internal pure returns (bytes memory bts) { return abi.encodeWithSelector(EVM_EXTRA_ARGS_V1_TAG, extraArgs); } } // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {ConfirmedOwnerWithProposal} from "./ConfirmedOwnerWithProposal.sol"; /// @title The ConfirmedOwner contract /// @notice A contract with helpers for basic contract ownership. contract ConfirmedOwner is ConfirmedOwnerWithProposal { constructor(address newOwner) ConfirmedOwnerWithProposal(newOwner, address(0)) {} } // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {IOwnable} from "../interfaces/IOwnable.sol"; /// @title The ConfirmedOwner contract /// @notice A contract with helpers for basic contract ownership. contract ConfirmedOwnerWithProposal is IOwnable { address private s_owner; address private s_pendingOwner; event OwnershipTransferRequested(address indexed from, address indexed to); event OwnershipTransferred(address indexed from, address indexed to); constructor(address newOwner, address pendingOwner) { // solhint-disable-next-line custom-errors require(newOwner != address(0), "Cannot set owner to zero"); s_owner = newOwner; if (pendingOwner != address(0)) { _transferOwnership(pendingOwner); } } /// @notice Allows an owner to begin transferring ownership to a new address. function transferOwnership(address to) public override onlyOwner { _transferOwnership(to); } /// @notice Allows an ownership transfer to be completed by the recipient. function acceptOwnership() external override { // solhint-disable-next-line custom-errors require(msg.sender == s_pendingOwner, "Must be proposed owner"); address oldOwner = s_owner; s_owner = msg.sender; s_pendingOwner = address(0); emit OwnershipTransferred(oldOwner, msg.sender); } /// @notice Get the current owner function owner() public view override returns (address) { return s_owner; } /// @notice validate, transfer ownership, and emit relevant events function _transferOwnership(address to) private { // solhint-disable-next-line custom-errors require(to != msg.sender, "Cannot transfer to self"); s_pendingOwner = to; emit OwnershipTransferRequested(s_owner, to); } /// @notice validate access function _validateOwnership() internal view { // solhint-disable-next-line custom-errors require(msg.sender == s_owner, "Only callable by owner"); } /// @notice Reverts if called by anyone other than the contract owner. modifier onlyOwner() { _validateOwnership(); _; } } // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface IOwnable { function owner() external returns (address); function transferOwnership(address recipient) external; function acceptOwnership() external; }
File 2 of 4: PriceRegistry
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.19; import {ITypeAndVersion} from "../shared/interfaces/ITypeAndVersion.sol"; import {IPriceRegistry} from "./interfaces/IPriceRegistry.sol"; import {OwnerIsCreator} from "./../shared/access/OwnerIsCreator.sol"; import {Internal} from "./libraries/Internal.sol"; import {USDPriceWith18Decimals} from "./libraries/USDPriceWith18Decimals.sol"; import {EnumerableSet} from "../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/structs/EnumerableSet.sol"; /// @notice The PriceRegistry contract responsibility is to store the current gas price in USD for a given destination chain, /// and the price of a token in USD allowing the owner or priceUpdater to update this value. contract PriceRegistry is IPriceRegistry, OwnerIsCreator, ITypeAndVersion { using EnumerableSet for EnumerableSet.AddressSet; using USDPriceWith18Decimals for uint224; error TokenNotSupported(address token); error ChainNotSupported(uint64 chain); error OnlyCallableByUpdaterOrOwner(); error StaleGasPrice(uint64 destChainSelector, uint256 threshold, uint256 timePassed); error StaleTokenPrice(address token, uint256 threshold, uint256 timePassed); error InvalidStalenessThreshold(); event PriceUpdaterSet(address indexed priceUpdater); event PriceUpdaterRemoved(address indexed priceUpdater); event FeeTokenAdded(address indexed feeToken); event FeeTokenRemoved(address indexed feeToken); event UsdPerUnitGasUpdated(uint64 indexed destChain, uint256 value, uint256 timestamp); event UsdPerTokenUpdated(address indexed token, uint256 value, uint256 timestamp); // solhint-disable-next-line chainlink-solidity/all-caps-constant-storage-variables string public constant override typeAndVersion = "PriceRegistry 1.2.0"; /// @dev The gas price per unit of gas for a given destination chain, in USD with 18 decimals. /// Multiple gas prices can be encoded into the same value. Each price takes {Internal.GAS_PRICE_BITS} bits. /// For example, if Optimism is the destination chain, gas price can include L1 base fee and L2 gas price. /// Logic to parse the price components is chain-specific, and should live in OnRamp. /// @dev Price of 1e18 is 1 USD. Examples: /// Very Expensive: 1 unit of gas costs 1 USD -> 1e18 /// Expensive: 1 unit of gas costs 0.1 USD -> 1e17 /// Cheap: 1 unit of gas costs 0.000001 USD -> 1e12 mapping(uint64 destChainSelector => Internal.TimestampedPackedUint224 price) private s_usdPerUnitGasByDestChainSelector; /// @dev The price, in USD with 18 decimals, per 1e18 of the smallest token denomination. /// @dev Price of 1e18 represents 1 USD per 1e18 token amount. /// 1 USDC = 1.00 USD per full token, each full token is 1e6 units -> 1 * 1e18 * 1e18 / 1e6 = 1e30 /// 1 ETH = 2,000 USD per full token, each full token is 1e18 units -> 2000 * 1e18 * 1e18 / 1e18 = 2_000e18 /// 1 LINK = 5.00 USD per full token, each full token is 1e18 units -> 5 * 1e18 * 1e18 / 1e18 = 5e18 mapping(address token => Internal.TimestampedPackedUint224 price) private s_usdPerToken; // Price updaters are allowed to update the prices. EnumerableSet.AddressSet private s_priceUpdaters; // Subset of tokens which prices tracked by this registry which are fee tokens. EnumerableSet.AddressSet private s_feeTokens; // The amount of time a price can be stale before it is considered invalid. uint32 private immutable i_stalenessThreshold; constructor(address[] memory priceUpdaters, address[] memory feeTokens, uint32 stalenessThreshold) { _applyPriceUpdatersUpdates(priceUpdaters, new address[](0)); _applyFeeTokensUpdates(feeTokens, new address[](0)); if (stalenessThreshold == 0) revert InvalidStalenessThreshold(); i_stalenessThreshold = stalenessThreshold; } // ================================================================ // │ Price calculations │ // ================================================================ // @inheritdoc IPriceRegistry function getTokenPrice(address token) public view override returns (Internal.TimestampedPackedUint224 memory) { return s_usdPerToken[token]; } // @inheritdoc IPriceRegistry function getValidatedTokenPrice(address token) external view override returns (uint224) { return _getValidatedTokenPrice(token); } // @inheritdoc IPriceRegistry function getTokenPrices( address[] calldata tokens ) external view override returns (Internal.TimestampedPackedUint224[] memory) { uint256 length = tokens.length; Internal.TimestampedPackedUint224[] memory tokenPrices = new Internal.TimestampedPackedUint224[](length); for (uint256 i = 0; i < length; ++i) { tokenPrices[i] = getTokenPrice(tokens[i]); } return tokenPrices; } /// @notice Get the staleness threshold. /// @return stalenessThreshold The staleness threshold. function getStalenessThreshold() external view returns (uint128) { return i_stalenessThreshold; } // @inheritdoc IPriceRegistry function getDestinationChainGasPrice( uint64 destChainSelector ) external view override returns (Internal.TimestampedPackedUint224 memory) { return s_usdPerUnitGasByDestChainSelector[destChainSelector]; } function getTokenAndGasPrices( address token, uint64 destChainSelector ) external view override returns (uint224 tokenPrice, uint224 gasPriceValue) { Internal.TimestampedPackedUint224 memory gasPrice = s_usdPerUnitGasByDestChainSelector[destChainSelector]; // We do allow a gas price of 0, but no stale or unset gas prices if (gasPrice.timestamp == 0) revert ChainNotSupported(destChainSelector); uint256 timePassed = block.timestamp - gasPrice.timestamp; if (timePassed > i_stalenessThreshold) revert StaleGasPrice(destChainSelector, i_stalenessThreshold, timePassed); return (_getValidatedTokenPrice(token), gasPrice.value); } /// @inheritdoc IPriceRegistry /// @dev this function assumes that no more than 1e59 dollars are sent as payment. /// If more is sent, the multiplication of feeTokenAmount and feeTokenValue will overflow. /// Since there isn't even close to 1e59 dollars in the world economy this is safe. function convertTokenAmount( address fromToken, uint256 fromTokenAmount, address toToken ) external view override returns (uint256) { /// Example: /// fromTokenAmount: 1e18 // 1 ETH /// ETH: 2_000e18 /// LINK: 5e18 /// return: 1e18 * 2_000e18 / 5e18 = 400e18 (400 LINK) return (fromTokenAmount * _getValidatedTokenPrice(fromToken)) / _getValidatedTokenPrice(toToken); } /// @notice Gets the token price for a given token and revert if the token is either /// not supported or the price is stale. /// @param token The address of the token to get the price for /// @return the token price function _getValidatedTokenPrice(address token) internal view returns (uint224) { Internal.TimestampedPackedUint224 memory tokenPrice = s_usdPerToken[token]; if (tokenPrice.timestamp == 0 || tokenPrice.value == 0) revert TokenNotSupported(token); uint256 timePassed = block.timestamp - tokenPrice.timestamp; if (timePassed > i_stalenessThreshold) revert StaleTokenPrice(token, i_stalenessThreshold, timePassed); return tokenPrice.value; } // ================================================================ // │ Fee tokens │ // ================================================================ /// @notice Get the list of fee tokens. /// @return The tokens set as fee tokens. function getFeeTokens() external view returns (address[] memory) { return s_feeTokens.values(); } /// @notice Add and remove tokens from feeTokens set. /// @param feeTokensToAdd The addresses of the tokens which are now considered fee tokens /// and can be used to calculate fees. /// @param feeTokensToRemove The addresses of the tokens which are no longer considered feeTokens. function applyFeeTokensUpdates( address[] memory feeTokensToAdd, address[] memory feeTokensToRemove ) external onlyOwner { _applyFeeTokensUpdates(feeTokensToAdd, feeTokensToRemove); } /// @notice Add and remove tokens from feeTokens set. /// @param feeTokensToAdd The addresses of the tokens which are now considered fee tokens /// and can be used to calculate fees. /// @param feeTokensToRemove The addresses of the tokens which are no longer considered feeTokens. function _applyFeeTokensUpdates(address[] memory feeTokensToAdd, address[] memory feeTokensToRemove) private { for (uint256 i = 0; i < feeTokensToAdd.length; ++i) { if (s_feeTokens.add(feeTokensToAdd[i])) { emit FeeTokenAdded(feeTokensToAdd[i]); } } for (uint256 i = 0; i < feeTokensToRemove.length; ++i) { if (s_feeTokens.remove(feeTokensToRemove[i])) { emit FeeTokenRemoved(feeTokensToRemove[i]); } } } // ================================================================ // │ Price updates │ // ================================================================ // @inheritdoc IPriceRegistry function updatePrices(Internal.PriceUpdates calldata priceUpdates) external override requireUpdaterOrOwner { uint256 tokenUpdatesLength = priceUpdates.tokenPriceUpdates.length; for (uint256 i = 0; i < tokenUpdatesLength; ++i) { Internal.TokenPriceUpdate memory update = priceUpdates.tokenPriceUpdates[i]; s_usdPerToken[update.sourceToken] = Internal.TimestampedPackedUint224({ value: update.usdPerToken, timestamp: uint32(block.timestamp) }); emit UsdPerTokenUpdated(update.sourceToken, update.usdPerToken, block.timestamp); } uint256 gasUpdatesLength = priceUpdates.gasPriceUpdates.length; for (uint256 i = 0; i < gasUpdatesLength; ++i) { Internal.GasPriceUpdate memory update = priceUpdates.gasPriceUpdates[i]; s_usdPerUnitGasByDestChainSelector[update.destChainSelector] = Internal.TimestampedPackedUint224({ value: update.usdPerUnitGas, timestamp: uint32(block.timestamp) }); emit UsdPerUnitGasUpdated(update.destChainSelector, update.usdPerUnitGas, block.timestamp); } } // ================================================================ // │ Access │ // ================================================================ /// @notice Get the list of price updaters. /// @return The price updaters. function getPriceUpdaters() external view returns (address[] memory) { return s_priceUpdaters.values(); } /// @notice Adds new priceUpdaters and remove existing ones. /// @param priceUpdatersToAdd The addresses of the priceUpdaters that are now allowed /// to send fee updates. /// @param priceUpdatersToRemove The addresses of the priceUpdaters that are no longer allowed /// to send fee updates. function applyPriceUpdatersUpdates( address[] memory priceUpdatersToAdd, address[] memory priceUpdatersToRemove ) external onlyOwner { _applyPriceUpdatersUpdates(priceUpdatersToAdd, priceUpdatersToRemove); } /// @notice Adds new priceUpdaters and remove existing ones. /// @param priceUpdatersToAdd The addresses of the priceUpdaters that are now allowed /// to send fee updates. /// @param priceUpdatersToRemove The addresses of the priceUpdaters that are no longer allowed /// to send fee updates. function _applyPriceUpdatersUpdates( address[] memory priceUpdatersToAdd, address[] memory priceUpdatersToRemove ) private { for (uint256 i = 0; i < priceUpdatersToAdd.length; ++i) { if (s_priceUpdaters.add(priceUpdatersToAdd[i])) { emit PriceUpdaterSet(priceUpdatersToAdd[i]); } } for (uint256 i = 0; i < priceUpdatersToRemove.length; ++i) { if (s_priceUpdaters.remove(priceUpdatersToRemove[i])) { emit PriceUpdaterRemoved(priceUpdatersToRemove[i]); } } } /// @notice Require that the caller is the owner or a fee updater. modifier requireUpdaterOrOwner() { if (msg.sender != owner() && !s_priceUpdaters.contains(msg.sender)) revert OnlyCallableByUpdaterOrOwner(); _; } } // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface ITypeAndVersion { function typeAndVersion() external pure returns (string memory); } // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {Internal} from "../libraries/Internal.sol"; interface IPriceRegistry { /// @notice Update the price for given tokens and gas prices for given chains. /// @param priceUpdates The price updates to apply. function updatePrices(Internal.PriceUpdates memory priceUpdates) external; /// @notice Get the `tokenPrice` for a given token. /// @param token The token to get the price for. /// @return tokenPrice The tokenPrice for the given token. function getTokenPrice(address token) external view returns (Internal.TimestampedPackedUint224 memory); /// @notice Get the `tokenPrice` for a given token, checks if the price is valid. /// @param token The token to get the price for. /// @return tokenPrice The tokenPrice for the given token if it exists and is valid. function getValidatedTokenPrice(address token) external view returns (uint224); /// @notice Get the `tokenPrice` for an array of tokens. /// @param tokens The tokens to get prices for. /// @return tokenPrices The tokenPrices for the given tokens. function getTokenPrices(address[] calldata tokens) external view returns (Internal.TimestampedPackedUint224[] memory); /// @notice Get an encoded `gasPrice` for a given destination chain ID. /// The 224-bit result encodes necessary gas price components. /// On L1 chains like Ethereum or Avax, the only component is the gas price. /// On Optimistic Rollups, there are two components - the L2 gas price, and L1 base fee for data availability. /// On future chains, there could be more or differing price components. /// PriceRegistry does not contain chain-specific logic to parse destination chain price components. /// @param destChainSelector The destination chain to get the price for. /// @return gasPrice The encoded gasPrice for the given destination chain ID. function getDestinationChainGasPrice( uint64 destChainSelector ) external view returns (Internal.TimestampedPackedUint224 memory); /// @notice Gets the fee token price and the gas price, both denominated in dollars. /// @param token The source token to get the price for. /// @param destChainSelector The destination chain to get the gas price for. /// @return tokenPrice The price of the feeToken in 1e18 dollars per base unit. /// @return gasPrice The price of gas in 1e18 dollars per base unit. function getTokenAndGasPrices( address token, uint64 destChainSelector ) external view returns (uint224 tokenPrice, uint224 gasPrice); /// @notice Convert a given token amount to target token amount. /// @param fromToken The given token address. /// @param fromTokenAmount The given token amount. /// @param toToken The target token address. /// @return toTokenAmount The target token amount. function convertTokenAmount( address fromToken, uint256 fromTokenAmount, address toToken ) external view returns (uint256 toTokenAmount); } // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {ConfirmedOwner} from "./ConfirmedOwner.sol"; /// @title The OwnerIsCreator contract /// @notice A contract with helpers for basic contract ownership. contract OwnerIsCreator is ConfirmedOwner { constructor() ConfirmedOwner(msg.sender) {} } // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {Client} from "./Client.sol"; 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; /// @notice A collection of token price and gas price updates. /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. struct PriceUpdates { TokenPriceUpdate[] tokenPriceUpdates; GasPriceUpdate[] gasPriceUpdates; } /// @notice Token price in USD. /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. struct TokenPriceUpdate { address sourceToken; // Source token uint224 usdPerToken; // 1e18 USD per smallest unit of token } /// @notice Gas price for a given chain in USD, its value may contain tightly packed fields. /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. struct GasPriceUpdate { uint64 destChainSelector; // Destination chain selector uint224 usdPerUnitGas; // 1e18 USD per smallest unit (e.g. wei) of destination chain gas } /// @notice A timestamped uint224 value that can contain several tightly packed fields. struct TimestampedPackedUint224 { uint224 value; // ───────╮ Value in uint224, packed. uint32 timestamp; // ────╯ Timestamp of the most recent price update. } /// @dev Gas price is stored in 112-bit unsigned int. uint224 can pack 2 prices. /// When packing L1 and L2 gas prices, L1 gas price is left-shifted to the higher-order bits. /// Using uint8 type, which cannot be higher than other bit shift operands, to avoid shift operand type warning. uint8 public constant GAS_PRICE_BITS = 112; struct PoolUpdate { address token; // The IERC20 token address address pool; // The token pool address } /// @notice Report that is submitted by the execution DON at the execution phase. /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. struct ExecutionReport { EVM2EVMMessage[] messages; // Contains a bytes array for each message, each inner bytes array contains bytes per transferred token bytes[][] offchainTokenData; bytes32[] proofs; uint256 proofFlagBits; } /// @notice The cross chain message that gets committed to EVM chains. /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. struct EVM2EVMMessage { uint64 sourceChainSelector; // ─────────╮ the chain selector of the source chain, note: not chainId address sender; // ─────────────────────╯ sender address on the source chain address receiver; // ───────────────────╮ receiver address on the destination chain uint64 sequenceNumber; // ──────────────╯ sequence number, not unique across lanes uint256 gasLimit; // user supplied maximum gas amount available for dest chain execution bool strict; // ────────────────────────╮ DEPRECATED uint64 nonce; // │ nonce for this lane for this sender, not unique across senders/lanes address feeToken; // ───────────────────╯ fee token uint256 feeTokenAmount; // fee token amount bytes data; // arbitrary data payload supplied by the message sender Client.EVMTokenAmount[] tokenAmounts; // array of tokens and amounts to transfer bytes[] sourceTokenData; // array of token pool return values, one per token bytes32 messageId; // a hash of the message data } /// @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; /// @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; function _toAny2EVMMessage( EVM2EVMMessage memory original, Client.EVMTokenAmount[] memory destTokenAmounts ) internal pure returns (Client.Any2EVMMessage memory message) { message = Client.Any2EVMMessage({ messageId: original.messageId, sourceChainSelector: original.sourceChainSelector, sender: abi.encode(original.sender), data: original.data, destTokenAmounts: destTokenAmounts }); } bytes32 internal constant EVM_2_EVM_MESSAGE_HASH = keccak256("EVM2EVMMessageHashV2"); function _hash(EVM2EVMMessage 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.sequenceNumber, original.gasLimit, original.strict, original.nonce, original.feeToken, original.feeTokenAmount ) ), keccak256(original.data), keccak256(abi.encode(original.tokenAmounts)), keccak256(abi.encode(original.sourceTokenData)) ) ); } /// @notice Enum listing the possible message execution states within /// the offRamp contract. /// UNTOUCHED never executed /// IN_PROGRESS currently being executed, used a replay protection /// SUCCESS successfully executed. End state /// FAILURE unsuccessfully executed, manual execution is now enabled. /// @dev RMN depends on this enum, if changing, please notify the RMN maintainers. enum MessageExecutionState { UNTOUCHED, IN_PROGRESS, SUCCESS, FAILURE } } // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.0; library USDPriceWith18Decimals { /// @notice Takes a price in USD, with 18 decimals per 1e18 token amount, /// and amount of the smallest token denomination, /// calculates the value in USD with 18 decimals. /// @param tokenPrice The USD price of the token. /// @param tokenAmount Amount of the smallest token denomination. /// @return USD value with 18 decimals. /// @dev this function assumes that no more than 1e59 US dollar worth of token is passed in. /// If more is sent, this function will overflow and revert. /// Since there isn't even close to 1e59 dollars, this is ok for all legit tokens. function _calcUSDValueFromTokenAmount(uint224 tokenPrice, uint256 tokenAmount) internal pure returns (uint256) { /// LINK Example: /// tokenPrice: 8e18 -> $8/LINK, as 1e18 token amount is 1 LINK, worth 8 USD, or 8e18 with 18 decimals /// tokenAmount: 2e18 -> 2 LINK /// result: 8e18 * 2e18 / 1e18 -> 16e18 with 18 decimals = $16 /// USDC Example: /// tokenPrice: 1e30 -> $1/USDC, as 1e18 token amount is 1e12 USDC, worth 1e12 USD, or 1e30 with 18 decimals /// tokenAmount: 5e6 -> 5 USDC /// result: 1e30 * 5e6 / 1e18 -> 5e18 with 18 decimals = $5 return (tokenPrice * tokenAmount) / 1e18; } /// @notice Takes a price in USD, with 18 decimals per 1e18 token amount, /// and USD value with 18 decimals, /// calculates amount of the smallest token denomination. /// @param tokenPrice The USD price of the token. /// @param usdValue USD value with 18 decimals. /// @return Amount of the smallest token denomination. function _calcTokenAmountFromUSDValue(uint224 tokenPrice, uint256 usdValue) internal pure returns (uint256) { /// LINK Example: /// tokenPrice: 8e18 -> $8/LINK, as 1e18 token amount is 1 LINK, worth 8 USD, or 8e18 with 18 decimals /// usdValue: 16e18 -> $16 /// result: 16e18 * 1e18 / 8e18 -> 2e18 = 2 LINK /// USDC Example: /// tokenPrice: 1e30 -> $1/USDC, as 1e18 token amount is 1e12 USDC, worth 1e12 USD, or 1e30 with 18 decimals /// usdValue: 5e18 -> $5 /// result: 5e18 * 1e18 / 1e30 -> 5e6 = 5 USDC return (usdValue * 1e18) / tokenPrice; } } // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.8.0) (utils/structs/EnumerableSet.sol) // This file was procedurally generated from scripts/generate/templates/EnumerableSet.js. pragma solidity ^0.8.0; /** * @dev Library for managing * https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive * types. * * Sets have the following properties: * * - Elements are added, removed, and checked for existence in constant time * (O(1)). * - Elements are enumerated in O(n). No guarantees are made on the ordering. * * ``` * contract Example { * // Add the library methods * using EnumerableSet for EnumerableSet.AddressSet; * * // Declare a set state variable * EnumerableSet.AddressSet private mySet; * } * ``` * * As of v3.3.0, sets of type `bytes32` (`Bytes32Set`), `address` (`AddressSet`) * and `uint256` (`UintSet`) are supported. * * [WARNING] * ==== * Trying to delete such a structure from storage will likely result in data corruption, rendering the structure * unusable. * See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info. * * In order to clean an EnumerableSet, you can either remove all elements one by one or create a fresh instance using an * array of EnumerableSet. * ==== */ library EnumerableSet { // To implement this library for multiple types with as little code // repetition as possible, we write it in terms of a generic Set type with // bytes32 values. // The Set implementation uses private functions, and user-facing // implementations (such as AddressSet) are just wrappers around the // underlying Set. // This means that we can only create new EnumerableSets for types that fit // in bytes32. struct Set { // Storage of set values bytes32[] _values; // Position of the value in the `values` array, plus 1 because index 0 // means a value is not in the set. mapping(bytes32 => uint256) _indexes; } /** * @dev Add a value to a set. O(1). * * Returns true if the value was added to the set, that is if it was not * already present. */ function _add(Set storage set, bytes32 value) private returns (bool) { if (!_contains(set, value)) { set._values.push(value); // The value is stored at length-1, but we add 1 to all indexes // and use 0 as a sentinel value set._indexes[value] = set._values.length; return true; } else { return false; } } /** * @dev Removes a value from a set. O(1). * * Returns true if the value was removed from the set, that is if it was * present. */ function _remove(Set storage set, bytes32 value) private returns (bool) { // We read and store the value's index to prevent multiple reads from the same storage slot uint256 valueIndex = set._indexes[value]; if (valueIndex != 0) { // Equivalent to contains(set, value) // To delete an element from the _values array in O(1), we swap the element to delete with the last one in // the array, and then remove the last element (sometimes called as 'swap and pop'). // This modifies the order of the array, as noted in {at}. uint256 toDeleteIndex = valueIndex - 1; uint256 lastIndex = set._values.length - 1; if (lastIndex != toDeleteIndex) { bytes32 lastValue = set._values[lastIndex]; // Move the last value to the index where the value to delete is set._values[toDeleteIndex] = lastValue; // Update the index for the moved value set._indexes[lastValue] = valueIndex; // Replace lastValue's index to valueIndex } // Delete the slot where the moved value was stored set._values.pop(); // Delete the index for the deleted slot delete set._indexes[value]; return true; } else { return false; } } /** * @dev Returns true if the value is in the set. O(1). */ function _contains(Set storage set, bytes32 value) private view returns (bool) { return set._indexes[value] != 0; } /** * @dev Returns the number of values on the set. O(1). */ function _length(Set storage set) private view returns (uint256) { return set._values.length; } /** * @dev Returns the value stored at position `index` in the set. O(1). * * Note that there are no guarantees on the ordering of values inside the * array, and it may change when more values are added or removed. * * Requirements: * * - `index` must be strictly less than {length}. */ function _at(Set storage set, uint256 index) private view returns (bytes32) { return set._values[index]; } /** * @dev Return the entire set in an array * * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that * this function has an unbounded cost, and using it as part of a state-changing function may render the function * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. */ function _values(Set storage set) private view returns (bytes32[] memory) { return set._values; } // Bytes32Set struct Bytes32Set { Set _inner; } /** * @dev Add a value to a set. O(1). * * Returns true if the value was added to the set, that is if it was not * already present. */ function add(Bytes32Set storage set, bytes32 value) internal returns (bool) { return _add(set._inner, value); } /** * @dev Removes a value from a set. O(1). * * Returns true if the value was removed from the set, that is if it was * present. */ function remove(Bytes32Set storage set, bytes32 value) internal returns (bool) { return _remove(set._inner, value); } /** * @dev Returns true if the value is in the set. O(1). */ function contains(Bytes32Set storage set, bytes32 value) internal view returns (bool) { return _contains(set._inner, value); } /** * @dev Returns the number of values in the set. O(1). */ function length(Bytes32Set storage set) internal view returns (uint256) { return _length(set._inner); } /** * @dev Returns the value stored at position `index` in the set. O(1). * * Note that there are no guarantees on the ordering of values inside the * array, and it may change when more values are added or removed. * * Requirements: * * - `index` must be strictly less than {length}. */ function at(Bytes32Set storage set, uint256 index) internal view returns (bytes32) { return _at(set._inner, index); } /** * @dev Return the entire set in an array * * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that * this function has an unbounded cost, and using it as part of a state-changing function may render the function * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. */ function values(Bytes32Set storage set) internal view returns (bytes32[] memory) { bytes32[] memory store = _values(set._inner); bytes32[] memory result; /// @solidity memory-safe-assembly assembly { result := store } return result; } // AddressSet struct AddressSet { Set _inner; } /** * @dev Add a value to a set. O(1). * * Returns true if the value was added to the set, that is if it was not * already present. */ function add(AddressSet storage set, address value) internal returns (bool) { return _add(set._inner, bytes32(uint256(uint160(value)))); } /** * @dev Removes a value from a set. O(1). * * Returns true if the value was removed from the set, that is if it was * present. */ function remove(AddressSet storage set, address value) internal returns (bool) { return _remove(set._inner, bytes32(uint256(uint160(value)))); } /** * @dev Returns true if the value is in the set. O(1). */ function contains(AddressSet storage set, address value) internal view returns (bool) { return _contains(set._inner, bytes32(uint256(uint160(value)))); } /** * @dev Returns the number of values in the set. O(1). */ function length(AddressSet storage set) internal view returns (uint256) { return _length(set._inner); } /** * @dev Returns the value stored at position `index` in the set. O(1). * * Note that there are no guarantees on the ordering of values inside the * array, and it may change when more values are added or removed. * * Requirements: * * - `index` must be strictly less than {length}. */ function at(AddressSet storage set, uint256 index) internal view returns (address) { return address(uint160(uint256(_at(set._inner, index)))); } /** * @dev Return the entire set in an array * * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that * this function has an unbounded cost, and using it as part of a state-changing function may render the function * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. */ function values(AddressSet storage set) internal view returns (address[] memory) { bytes32[] memory store = _values(set._inner); address[] memory result; /// @solidity memory-safe-assembly assembly { result := store } return result; } // UintSet struct UintSet { Set _inner; } /** * @dev Add a value to a set. O(1). * * Returns true if the value was added to the set, that is if it was not * already present. */ function add(UintSet storage set, uint256 value) internal returns (bool) { return _add(set._inner, bytes32(value)); } /** * @dev Removes a value from a set. O(1). * * Returns true if the value was removed from the set, that is if it was * present. */ function remove(UintSet storage set, uint256 value) internal returns (bool) { return _remove(set._inner, bytes32(value)); } /** * @dev Returns true if the value is in the set. O(1). */ function contains(UintSet storage set, uint256 value) internal view returns (bool) { return _contains(set._inner, bytes32(value)); } /** * @dev Returns the number of values in the set. O(1). */ function length(UintSet storage set) internal view returns (uint256) { return _length(set._inner); } /** * @dev Returns the value stored at position `index` in the set. O(1). * * Note that there are no guarantees on the ordering of values inside the * array, and it may change when more values are added or removed. * * Requirements: * * - `index` must be strictly less than {length}. */ function at(UintSet storage set, uint256 index) internal view returns (uint256) { return uint256(_at(set._inner, index)); } /** * @dev Return the entire set in an array * * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that * this function has an unbounded cost, and using it as part of a state-changing function may render the function * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. */ function values(UintSet storage set) internal view returns (uint256[] memory) { bytes32[] memory store = _values(set._inner); uint256[] memory result; /// @solidity memory-safe-assembly assembly { result := store } return result; } } // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {ConfirmedOwnerWithProposal} from "./ConfirmedOwnerWithProposal.sol"; /// @title The ConfirmedOwner contract /// @notice A contract with helpers for basic contract ownership. contract ConfirmedOwner is ConfirmedOwnerWithProposal { constructor(address newOwner) ConfirmedOwnerWithProposal(newOwner, address(0)) {} } // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; // End consumer library. library Client { /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. struct EVMTokenAmount { address token; // token address on the local chain. uint256 amount; // Amount of tokens. } struct Any2EVMMessage { bytes32 messageId; // MessageId corresponding to ccipSend on source. uint64 sourceChainSelector; // Source chain selector. bytes sender; // abi.decode(sender) if coming from an EVM chain. bytes data; // payload sent in original message. EVMTokenAmount[] destTokenAmounts; // Tokens and their amounts in their destination chain representation. } // If extraArgs is empty bytes, the default is 200k gas limit. struct EVM2AnyMessage { bytes receiver; // abi.encode(receiver address) for dest EVM chains bytes data; // Data payload EVMTokenAmount[] tokenAmounts; // Token transfers address feeToken; // Address of feeToken. address(0) means you will send msg.value. bytes extraArgs; // Populate this with _argsToBytes(EVMExtraArgsV1) } // bytes4(keccak256("CCIP EVMExtraArgsV1")); bytes4 public constant EVM_EXTRA_ARGS_V1_TAG = 0x97a657c9; struct EVMExtraArgsV1 { uint256 gasLimit; } function _argsToBytes(EVMExtraArgsV1 memory extraArgs) internal pure returns (bytes memory bts) { return abi.encodeWithSelector(EVM_EXTRA_ARGS_V1_TAG, extraArgs); } } // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.0; library MerkleMultiProof { /// @notice Leaf domain separator, should be used as the first 32 bytes of a leaf's preimage. bytes32 internal constant LEAF_DOMAIN_SEPARATOR = 0x0000000000000000000000000000000000000000000000000000000000000000; /// @notice Internal domain separator, should be used as the first 32 bytes of an internal node's preiimage. bytes32 internal constant INTERNAL_DOMAIN_SEPARATOR = 0x0000000000000000000000000000000000000000000000000000000000000001; uint256 internal constant MAX_NUM_HASHES = 256; error InvalidProof(); error LeavesCannotBeEmpty(); /// @notice Computes the root based on provided pre-hashed leaf nodes in /// leaves, internal nodes in proofs, and using proofFlagBits' i-th bit to /// determine if an element of proofs or one of the previously computed leafs /// or internal nodes will be used for the i-th hash. /// @param leaves Should be pre-hashed and the first 32 bytes of a leaf's /// preimage should match LEAF_DOMAIN_SEPARATOR. /// @param proofs The hashes to be used instead of a leaf hash when the proofFlagBits /// indicates a proof should be used. /// @param proofFlagBits A single uint256 of which each bit indicates whether a leaf or /// a proof needs to be used in a hash operation. /// @dev the maximum number of hash operations it set to 256. Any input that would require /// more than 256 hashes to get to a root will revert. /// @dev For given input `leaves` = [a,b,c] `proofs` = [D] and `proofFlagBits` = 5 /// totalHashes = 3 + 1 - 1 = 3 /// ** round 1 ** /// proofFlagBits = (5 >> 0) & 1 = true /// hashes[0] = hashPair(a, b) /// (leafPos, hashPos, proofPos) = (2, 0, 0); /// /// ** round 2 ** /// proofFlagBits = (5 >> 1) & 1 = false /// hashes[1] = hashPair(D, c) /// (leafPos, hashPos, proofPos) = (3, 0, 1); /// /// ** round 3 ** /// proofFlagBits = (5 >> 2) & 1 = true /// hashes[2] = hashPair(hashes[0], hashes[1]) /// (leafPos, hashPos, proofPos) = (3, 2, 1); /// /// i = 3 and no longer < totalHashes. The algorithm is done /// return hashes[totalHashes - 1] = hashes[2]; the last hash we computed. // We mark this function as internal to force it to be inlined in contracts // that use it, but semantically it is public. // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore function merkleRoot( bytes32[] memory leaves, bytes32[] memory proofs, uint256 proofFlagBits ) internal pure returns (bytes32) { unchecked { uint256 leavesLen = leaves.length; uint256 proofsLen = proofs.length; if (leavesLen == 0) revert LeavesCannotBeEmpty(); if (!(leavesLen <= MAX_NUM_HASHES + 1 && proofsLen <= MAX_NUM_HASHES + 1)) revert InvalidProof(); uint256 totalHashes = leavesLen + proofsLen - 1; if (!(totalHashes <= MAX_NUM_HASHES)) revert InvalidProof(); if (totalHashes == 0) { return leaves[0]; } bytes32[] memory hashes = new bytes32[](totalHashes); (uint256 leafPos, uint256 hashPos, uint256 proofPos) = (0, 0, 0); for (uint256 i = 0; i < totalHashes; ++i) { // Checks if the bit flag signals the use of a supplied proof or a leaf/previous hash. bytes32 a; if (proofFlagBits & (1 << i) == (1 << i)) { // Use a leaf or a previously computed hash. if (leafPos < leavesLen) { a = leaves[leafPos++]; } else { a = hashes[hashPos++]; } } else { // Use a supplied proof. a = proofs[proofPos++]; } // The second part of the hashed pair is never a proof as hashing two proofs would result in a // hash that can already be computed offchain. bytes32 b; if (leafPos < leavesLen) { b = leaves[leafPos++]; } else { b = hashes[hashPos++]; } if (!(hashPos <= i)) revert InvalidProof(); hashes[i] = _hashPair(a, b); } if (!(hashPos == totalHashes - 1 && leafPos == leavesLen && proofPos == proofsLen)) revert InvalidProof(); // Return the last hash. return hashes[totalHashes - 1]; } } /// @notice Hashes two bytes32 objects in their given order, prepended by the /// INTERNAL_DOMAIN_SEPARATOR. function _hashInternalNode(bytes32 left, bytes32 right) private pure returns (bytes32 hash) { return keccak256(abi.encode(INTERNAL_DOMAIN_SEPARATOR, left, right)); } /// @notice Hashes two bytes32 objects. The order is taken into account, /// using the lower value first. function _hashPair(bytes32 a, bytes32 b) private pure returns (bytes32) { return a < b ? _hashInternalNode(a, b) : _hashInternalNode(b, a); } } // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {IOwnable} from "../interfaces/IOwnable.sol"; /// @title The ConfirmedOwner contract /// @notice A contract with helpers for basic contract ownership. contract ConfirmedOwnerWithProposal is IOwnable { address private s_owner; address private s_pendingOwner; event OwnershipTransferRequested(address indexed from, address indexed to); event OwnershipTransferred(address indexed from, address indexed to); constructor(address newOwner, address pendingOwner) { // solhint-disable-next-line custom-errors require(newOwner != address(0), "Cannot set owner to zero"); s_owner = newOwner; if (pendingOwner != address(0)) { _transferOwnership(pendingOwner); } } /// @notice Allows an owner to begin transferring ownership to a new address. function transferOwnership(address to) public override onlyOwner { _transferOwnership(to); } /// @notice Allows an ownership transfer to be completed by the recipient. function acceptOwnership() external override { // solhint-disable-next-line custom-errors require(msg.sender == s_pendingOwner, "Must be proposed owner"); address oldOwner = s_owner; s_owner = msg.sender; s_pendingOwner = address(0); emit OwnershipTransferred(oldOwner, msg.sender); } /// @notice Get the current owner function owner() public view override returns (address) { return s_owner; } /// @notice validate, transfer ownership, and emit relevant events function _transferOwnership(address to) private { // solhint-disable-next-line custom-errors require(to != msg.sender, "Cannot transfer to self"); s_pendingOwner = to; emit OwnershipTransferRequested(s_owner, to); } /// @notice validate access function _validateOwnership() internal view { // solhint-disable-next-line custom-errors require(msg.sender == s_owner, "Only callable by owner"); } /// @notice Reverts if called by anyone other than the contract owner. modifier onlyOwner() { _validateOwnership(); _; } } // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface IOwnable { function owner() external returns (address); function transferOwnership(address recipient) external; function acceptOwnership() external; }
File 3 of 4: ARMProxy
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.19; import {TypeAndVersionInterface} from "../interfaces/TypeAndVersionInterface.sol"; import {IARM} from "./interfaces/IARM.sol"; import {OwnerIsCreator} from "./../shared/access/OwnerIsCreator.sol"; contract ARMProxy is OwnerIsCreator, TypeAndVersionInterface { error ZeroAddressNotAllowed(); event ARMSet(address arm); // STATIC CONFIG // solhint-disable-next-line chainlink-solidity/all-caps-constant-storage-variables string public constant override typeAndVersion = "ARMProxy 1.0.0"; // DYNAMIC CONFIG address private s_arm; constructor(address arm) { setARM(arm); } /// @notice SetARM sets the ARM implementation contract address. /// @param arm The address of the arm implementation contract. function setARM(address arm) public onlyOwner { if (arm == address(0)) revert ZeroAddressNotAllowed(); s_arm = arm; emit ARMSet(arm); } /// @notice getARM gets the ARM implementation contract address. /// @return arm The address of the arm implementation contract. function getARM() external view returns (address) { return s_arm; } // We use a fallback function instead of explicit implementations of the functions // defined in IARM.sol to preserve compatibility with future additions to the IARM // interface. Calling IARM interface methods in ARMProxy should be transparent, i.e. // their input/output behaviour should be identical to calling the proxied s_arm // contract directly. (If s_arm doesn't point to a contract, we always revert.) fallback() external { address arm = s_arm; assembly { // Revert if no contract present at destination address, otherwise call // might succeed unintentionally. if iszero(extcodesize(arm)) { revert(0, 0) } // We use memory starting at zero, overwriting anything that might already // be stored there. This messes with Solidity's expectations around memory // layout, but it's fine because we always exit execution of this contract // inside this assembly block, i.e. we don't cede control to code generated // by the Solidity compiler that might have expectations around memory // layout. // Copy calldatasize() bytes from calldata offset 0 to memory offset 0. calldatacopy(0, 0, calldatasize()) // Call the underlying ARM implementation. out and outsize are 0 because // we don't know the size yet. We hardcode value to zero. let success := call(gas(), arm, 0, 0, calldatasize(), 0, 0) // Copy the returned data. returndatacopy(0, 0, returndatasize()) // Pass through successful return or revert and associated data. if success { return(0, returndatasize()) } revert(0, returndatasize()) } } } // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; abstract contract TypeAndVersionInterface { function typeAndVersion() external pure virtual returns (string memory); } // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.0; /// @notice This interface contains the only ARM-related functions that might be used on-chain by other CCIP contracts. interface IARM { /// @notice A Merkle root tagged with the address of the commit store contract it is destined for. struct TaggedRoot { address commitStore; bytes32 root; } /// @notice Callers MUST NOT cache the return value as a blessed tagged root could become unblessed. function isBlessed(TaggedRoot calldata taggedRoot) external view returns (bool); /// @notice When the ARM is "cursed", CCIP pauses until the curse is lifted. function isCursed() external view returns (bool); } // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {ConfirmedOwner} from "../../ConfirmedOwner.sol"; /// @title The OwnerIsCreator contract /// @notice A contract with helpers for basic contract ownership. contract OwnerIsCreator is ConfirmedOwner { constructor() ConfirmedOwner(msg.sender) {} } // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "./ConfirmedOwnerWithProposal.sol"; /** * @title The ConfirmedOwner contract * @notice A contract with helpers for basic contract ownership. */ contract ConfirmedOwner is ConfirmedOwnerWithProposal { constructor(address newOwner) ConfirmedOwnerWithProposal(newOwner, address(0)) {} } // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "./interfaces/OwnableInterface.sol"; /** * @title The ConfirmedOwner contract * @notice A contract with helpers for basic contract ownership. */ contract ConfirmedOwnerWithProposal is OwnableInterface { address private s_owner; address private s_pendingOwner; event OwnershipTransferRequested(address indexed from, address indexed to); event OwnershipTransferred(address indexed from, address indexed to); constructor(address newOwner, address pendingOwner) { require(newOwner != address(0), "Cannot set owner to zero"); s_owner = newOwner; if (pendingOwner != address(0)) { _transferOwnership(pendingOwner); } } /** * @notice Allows an owner to begin transferring ownership to a new address, * pending. */ function transferOwnership(address to) public override onlyOwner { _transferOwnership(to); } /** * @notice Allows an ownership transfer to be completed by the recipient. */ function acceptOwnership() external override { require(msg.sender == s_pendingOwner, "Must be proposed owner"); address oldOwner = s_owner; s_owner = msg.sender; s_pendingOwner = address(0); emit OwnershipTransferred(oldOwner, msg.sender); } /** * @notice Get the current owner */ function owner() public view override returns (address) { return s_owner; } /** * @notice validate, transfer ownership, and emit relevant events */ function _transferOwnership(address to) private { require(to != msg.sender, "Cannot transfer to self"); s_pendingOwner = to; emit OwnershipTransferRequested(s_owner, to); } /** * @notice validate access */ function _validateOwnership() internal view { require(msg.sender == s_owner, "Only callable by owner"); } /** * @notice Reverts if called by anyone other than the contract owner. */ modifier onlyOwner() { _validateOwnership(); _; } } // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface OwnableInterface { function owner() external returns (address); function transferOwnership(address recipient) external; function acceptOwnership() external; }
File 4 of 4: ARM
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.19; import {TypeAndVersionInterface} from "../interfaces/TypeAndVersionInterface.sol"; import {IARM} from "./interfaces/IARM.sol"; import {OwnerIsCreator} from "./../shared/access/OwnerIsCreator.sol"; contract ARM is IARM, OwnerIsCreator, TypeAndVersionInterface { // STATIC CONFIG // solhint-disable-next-line chainlink-solidity/all-caps-constant-storage-variables string public constant override typeAndVersion = "ARM 1.0.0"; uint256 private constant MAX_NUM_VOTERS = 128; // DYNAMIC CONFIG struct Voter { // This is the address the voter should use to call voteToBless. address blessVoteAddr; // This is the address the voter should use to call voteToCurse. address curseVoteAddr; // This is the address the voter should use to call unvoteToCurse. address curseUnvoteAddr; // The weight of this voter's vote for blessing. uint8 blessWeight; // The weight of this voter's vote for cursing. uint8 curseWeight; } struct Config { Voter[] voters; // When the total weight of voters that have voted to bless a tagged root reaches // or exceeds blessWeightThreshold, the tagged root becomes blessed. uint16 blessWeightThreshold; // When the total weight of voters that have voted to curse reaches or // exceeds curseWeightThreshold, the ARM enters the cursed state. uint16 curseWeightThreshold; } struct VersionedConfig { Config config; // The version is incremented every time the config changes. // The initial configuration on the contract will have configVersion == 1. uint32 configVersion; // The block number at which the config was last set. Helps the offchain // code check that the config was set in a stable block or double-check // that it has the correct config by querying logs at that block number. uint32 blockNumber; } VersionedConfig private s_versionedConfig; // STATE struct BlesserRecord { // The config version at which this BlesserRecord was last set. A blesser // is considered active iff this configVersion equals // s_versionedConfig.configVersion. uint32 configVersion; uint8 weight; uint8 index; } mapping(address blessVoteAddr => BlesserRecord blesserRecord) private s_blesserRecords; struct BlessVoteProgress { // A BlessVoteProgress is considered invalid if weightThresholdMet is false when // s_versionedConfig.configVersion changes. we don't want old in-progress // votes to continue when we set a new config! // The config version at which the bless vote for a tagged root was initiated. uint32 configVersion; uint16 accumulatedWeight; // Care must be taken that the bitmap has as many bits as MAX_NUM_VOTERS. uint128 voterBitmap; bool weightThresholdMet; } mapping(bytes32 taggedRootHash => BlessVoteProgress blessVoteProgress) private s_blessVoteProgressByTaggedRootHash; // voteCount and cursesHash can be reset through unvoteToCurse, and ownerUnvoteToCurse, and may be reset through // setConfig if the curser is not part of the new config. struct CurserRecord { bool active; uint8 weight; uint32 voteCount; address curseUnvoteAddr; bytes32 cursesHash; } mapping(address curseVoteAddr => CurserRecord curserRecord) private s_curserRecords; // Maintains a per-curser set of curseIds. Entries from this mapping are // never cleared. Once a curseId is used it can never be reused, even after // an unvoteToCurse or ownerUnvoteToCurse. This is to prevent accidental // re-votes to curse, e.g. caused by TOCTOU issues. mapping(address curseVoteAddr => mapping(bytes32 curseId => bool voted)) private s_curseVotes; struct CurseVoteProgress { uint16 curseWeightThreshold; uint16 accumulatedWeight; // A curse becomes active after: // - accumulatedWeight becomes greater or equal than curseWeightThreshold; or // - the owner curses. // Once a curse is active, only the owner can lift it. bool curseActive; } CurseVoteProgress private s_curseVoteProgress; // AUXILLARY STRUCTS struct UnvoteToCurseRecord { address curseVoteAddr; bytes32 cursesHash; bool forceUnvote; } // EVENTS, ERRORS event ConfigSet(uint32 indexed configVersion, Config config); error InvalidConfig(); event TaggedRootBlessed(uint32 indexed configVersion, IARM.TaggedRoot taggedRoot, uint16 accumulatedWeight); event TaggedRootBlessVotesReset(uint32 indexed configVersion, IARM.TaggedRoot taggedRoot, bool wasBlessed); event VotedToBless(uint32 indexed configVersion, address indexed voter, IARM.TaggedRoot taggedRoot, uint8 weight); event VotedToCurse( uint32 indexed configVersion, address indexed voter, uint8 weight, uint32 voteCount, bytes32 curseId, bytes32 cursesHash, uint16 accumulatedWeight ); event ReusedVotesToCurse( uint32 indexed configVersion, address indexed voter, uint8 weight, uint32 voteCount, bytes32 cursesHash, uint16 accumulatedWeight ); event UnvotedToCurse( uint32 indexed configVersion, address indexed voter, uint8 weight, uint32 voteCount, bytes32 cursesHash ); event SkippedUnvoteToCurse(address indexed voter, bytes32 expectedCursesHash, bytes32 actualCursesHash); event OwnerCursed(uint256 timestamp); event Cursed(uint32 indexed configVersion, uint256 timestamp); // These events make it easier for offchain logic to discover that it performs // the same actions multiple times. event AlreadyVotedToBless(uint32 indexed configVersion, address indexed voter, IARM.TaggedRoot taggedRoot); event AlreadyBlessed(uint32 indexed configVersion, address indexed voter, IARM.TaggedRoot taggedRoot); event RecoveredFromCurse(); error AlreadyVotedToCurse(address voter, bytes32 curseId); error InvalidVoter(address voter); error InvalidCurseState(); error InvalidCursesHash(bytes32 expectedCursesHash, bytes32 actualCursesHash); error MustRecoverFromCurse(); constructor(Config memory config) { { // Ensure that the bitmap is large enough to hold MAX_NUM_VOTERS. // We do this in the constructor because MAX_NUM_VOTERS is constant. BlessVoteProgress memory vp; vp.voterBitmap = ~uint128(0); assert(vp.voterBitmap >> (MAX_NUM_VOTERS - 1) >= 1); } _setConfig(config); } function _bitmapGet(uint128 bitmap, uint8 index) internal pure returns (bool) { assert(index < MAX_NUM_VOTERS); return bitmap & (uint128(1) << index) != 0; } function _bitmapSet(uint128 bitmap, uint8 index) internal pure returns (uint128) { assert(index < MAX_NUM_VOTERS); return bitmap | (uint128(1) << index); } function _bitmapCount(uint128 bitmap) internal pure returns (uint8 oneBits) { // https://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetKernighan for (; bitmap != 0; ++oneBits) { bitmap &= bitmap - 1; } } function _taggedRootHash(IARM.TaggedRoot memory taggedRoot) internal pure returns (bytes32) { return keccak256(abi.encode(taggedRoot.commitStore, taggedRoot.root)); } /// @param taggedRoots A tagged root is hashed as `keccak256(abi.encode(taggedRoot.commitStore /// /* address */, taggedRoot.root /* bytes32 */))`. function voteToBless(IARM.TaggedRoot[] calldata taggedRoots) external { // If we have an active curse, something is really wrong. Let's err on the // side of caution and not accept further blessings during this time of // uncertainty. if (isCursed()) revert MustRecoverFromCurse(); uint32 configVersion = s_versionedConfig.configVersion; BlesserRecord memory blesserRecord = s_blesserRecords[msg.sender]; if (blesserRecord.configVersion != configVersion) revert InvalidVoter(msg.sender); for (uint256 i = 0; i < taggedRoots.length; ++i) { IARM.TaggedRoot memory taggedRoot = taggedRoots[i]; bytes32 taggedRootHash = _taggedRootHash(taggedRoot); BlessVoteProgress memory voteProgress = s_blessVoteProgressByTaggedRootHash[taggedRootHash]; if (voteProgress.weightThresholdMet) { // We don't revert here because it's unreasonable to expect from the // voter to know exactly when to stop voting. Most likely when they // voted they didn't realize the threshold would be reached by the time // their vote was counted. // Additionally, there might be other tagged roots for which votes might // count, and we want to allow that to happen. emit AlreadyBlessed(configVersion, msg.sender, taggedRoot); continue; } if (voteProgress.configVersion != configVersion) { // Note that voteProgress.weightThresholdMet must be false at this point // If votes were received while an older config was in effect, // invalidate them and start from scratch. // If votes were never received, set the current config version. voteProgress = BlessVoteProgress({ configVersion: configVersion, voterBitmap: 0, accumulatedWeight: 0, weightThresholdMet: false }); } if (_bitmapGet(voteProgress.voterBitmap, blesserRecord.index)) { // We don't revert here because there might be other tagged roots for // which votes might count, and we want to allow that to happen. emit AlreadyVotedToBless(configVersion, msg.sender, taggedRoot); continue; } voteProgress.voterBitmap = _bitmapSet(voteProgress.voterBitmap, blesserRecord.index); voteProgress.accumulatedWeight += blesserRecord.weight; emit VotedToBless(configVersion, msg.sender, taggedRoot, blesserRecord.weight); if (voteProgress.accumulatedWeight >= s_versionedConfig.config.blessWeightThreshold) { voteProgress.weightThresholdMet = true; emit TaggedRootBlessed(configVersion, taggedRoot, voteProgress.accumulatedWeight); } s_blessVoteProgressByTaggedRootHash[taggedRootHash] = voteProgress; } } /// @notice Can be called by the owner to remove unintentionally voted or even blessed tagged roots in a recovery /// scenario. The owner must ensure that there are no in-flight transactions by ARM nodes voting for any of the /// taggedRoots before calling this function, as such in-flight transactions could lead to the roots becoming /// re-blessed shortly after the call to this function, contrary to the original intention. function ownerResetBlessVotes(IARM.TaggedRoot[] calldata taggedRoots) external onlyOwner { uint32 configVersion = s_versionedConfig.configVersion; for (uint256 i = 0; i < taggedRoots.length; ++i) { IARM.TaggedRoot memory taggedRoot = taggedRoots[i]; bytes32 taggedRootHash = _taggedRootHash(taggedRoot); BlessVoteProgress memory voteProgress = s_blessVoteProgressByTaggedRootHash[taggedRootHash]; delete s_blessVoteProgressByTaggedRootHash[taggedRootHash]; bool wasBlessed = voteProgress.weightThresholdMet; if (voteProgress.configVersion == configVersion || wasBlessed) { emit TaggedRootBlessVotesReset(configVersion, taggedRoot, wasBlessed); } } } /// @notice Can be called by a curser to remove unintentional votes to curse. /// We expect this to be called very rarely, e.g. in case of a bug in the /// offchain code causing false voteToCurse calls. /// @notice Should be called from curser's corresponding curseUnvoteAddr. function unvoteToCurse(address curseVoteAddr, bytes32 cursesHash) external { CurserRecord memory curserRecord = s_curserRecords[curseVoteAddr]; // If a curse is active, only the owner is allowed to lift it. if (isCursed()) revert MustRecoverFromCurse(); if (msg.sender != curserRecord.curseUnvoteAddr) revert InvalidVoter(msg.sender); if (!curserRecord.active || curserRecord.voteCount == 0) revert InvalidCurseState(); if (curserRecord.cursesHash != cursesHash) revert InvalidCursesHash(curserRecord.cursesHash, cursesHash); emit UnvotedToCurse( s_versionedConfig.configVersion, curseVoteAddr, curserRecord.weight, curserRecord.voteCount, cursesHash ); curserRecord.voteCount = 0; curserRecord.cursesHash = 0; s_curserRecords[curseVoteAddr] = curserRecord; s_curseVoteProgress.accumulatedWeight -= curserRecord.weight; } /// @notice A vote to curse is appropriate during unhealthy blockchain conditions /// (eg. finality violations). function voteToCurse(bytes32 curseId) external { CurserRecord memory curserRecord = s_curserRecords[msg.sender]; if (!curserRecord.active) revert InvalidVoter(msg.sender); if (s_curseVotes[msg.sender][curseId]) revert AlreadyVotedToCurse(msg.sender, curseId); s_curseVotes[msg.sender][curseId] = true; ++curserRecord.voteCount; curserRecord.cursesHash = keccak256(abi.encode(curserRecord.cursesHash, curseId)); s_curserRecords[msg.sender] = curserRecord; CurseVoteProgress memory curseVoteProgress = s_curseVoteProgress; if (curserRecord.voteCount == 1) { curseVoteProgress.accumulatedWeight += curserRecord.weight; } // NOTE: We could pack configVersion into CurserRecord that we already load in the beginning of this function to // avoid the following extra storage read for it, but since voteToCurse is not on the hot path we'd rather keep // things simple. uint32 configVersion = s_versionedConfig.configVersion; emit VotedToCurse( configVersion, msg.sender, curserRecord.weight, curserRecord.voteCount, curseId, curserRecord.cursesHash, curseVoteProgress.accumulatedWeight ); if ( !curseVoteProgress.curseActive && curseVoteProgress.accumulatedWeight >= curseVoteProgress.curseWeightThreshold ) { curseVoteProgress.curseActive = true; emit Cursed(configVersion, block.timestamp); } s_curseVoteProgress = curseVoteProgress; } /// @notice Enables the owner to immediately have the system enter the cursed state. function ownerCurse() external onlyOwner { emit OwnerCursed(block.timestamp); if (!s_curseVoteProgress.curseActive) { s_curseVoteProgress.curseActive = true; emit Cursed(s_versionedConfig.configVersion, block.timestamp); } } /// @notice Enables the owner to remove curse votes. After the curse votes are removed, /// this function will check whether the curse is still valid and restore the uncursed state if possible. /// This function also enables the owner to lift a curse created through ownerCurse. function ownerUnvoteToCurse(UnvoteToCurseRecord[] calldata unvoteRecords) external onlyOwner { for (uint256 i = 0; i < unvoteRecords.length; ++i) { UnvoteToCurseRecord memory unvoteRecord = unvoteRecords[i]; CurserRecord memory curserRecord = s_curserRecords[unvoteRecord.curseVoteAddr]; // Owner can avoid the curses hash check by setting forceUnvote to true, in case // a malicious curser is flooding the system with votes to curse with the // intention to disallow the owner to clear their curse. if (!unvoteRecord.forceUnvote && curserRecord.cursesHash != unvoteRecord.cursesHash) { emit SkippedUnvoteToCurse(unvoteRecord.curseVoteAddr, curserRecord.cursesHash, unvoteRecord.cursesHash); continue; } if (!curserRecord.active || curserRecord.voteCount == 0) continue; emit UnvotedToCurse( s_versionedConfig.configVersion, unvoteRecord.curseVoteAddr, curserRecord.weight, curserRecord.voteCount, curserRecord.cursesHash ); curserRecord.voteCount = 0; curserRecord.cursesHash = 0; s_curserRecords[unvoteRecord.curseVoteAddr] = curserRecord; s_curseVoteProgress.accumulatedWeight -= curserRecord.weight; } if ( s_curseVoteProgress.curseActive && s_curseVoteProgress.accumulatedWeight < s_curseVoteProgress.curseWeightThreshold ) { s_curseVoteProgress.curseActive = false; emit RecoveredFromCurse(); // Invalidate all in-progress votes to bless by bumping the config version. // They might have been based on false information about the source chain // (e.g. in case of a finality violation). _setConfig(s_versionedConfig.config); } } /// @notice Will revert in case a curse is active. To avoid accidentally invalidating an in-progress curse vote, it /// may be advisable to remove voters one-by-one over time, rather than many at once. /// @dev The gas use of this function varies depending on the number of curse votes that are active. When calling this /// function, be sure to include a gas cushion to account for curse votes that may occur between your transaction /// being sent and mined. function setConfig(Config memory config) external onlyOwner { _setConfig(config); } /// @inheritdoc IARM function isBlessed(IARM.TaggedRoot calldata taggedRoot) external view override returns (bool) { return s_blessVoteProgressByTaggedRootHash[_taggedRootHash(taggedRoot)].weightThresholdMet; } /// @inheritdoc IARM function isCursed() public view override returns (bool) { return s_curseVoteProgress.curseActive; } /// @notice Config version might be incremented for many reasons, including /// recovery from a curse and a regular config change. function getConfigDetails() external view returns (uint32 version, uint32 blockNumber, Config memory config) { version = s_versionedConfig.configVersion; blockNumber = s_versionedConfig.blockNumber; config = s_versionedConfig.config; } /// @return blessVoteAddrs addresses of voters, will be empty if voting took place with an older config version /// @return accumulatedWeight sum of weights of voters, will be zero if voting took place with an older config version /// @return blessed will be accurate regardless of when voting took place /// @dev This is a helper method for offchain code so efficiency is not really a concern. function getBlessProgress( IARM.TaggedRoot calldata taggedRoot ) external view returns (address[] memory blessVoteAddrs, uint16 accumulatedWeight, bool blessed) { bytes32 taggedRootHash = _taggedRootHash(taggedRoot); BlessVoteProgress memory progress = s_blessVoteProgressByTaggedRootHash[taggedRootHash]; blessed = progress.weightThresholdMet; if (progress.configVersion == s_versionedConfig.configVersion) { accumulatedWeight = progress.accumulatedWeight; uint128 bitmap = progress.voterBitmap; blessVoteAddrs = new address[](_bitmapCount(bitmap)); Voter[] memory voters = s_versionedConfig.config.voters; uint256 j = 0; for (uint256 i = 0; i < voters.length; ++i) { if (_bitmapGet(bitmap, s_blesserRecords[voters[i].blessVoteAddr].index)) { blessVoteAddrs[j] = voters[i].blessVoteAddr; ++j; } } } } /// @dev This is a helper method for offchain code so efficiency is not really a concern. function getCurseProgress() external view returns ( address[] memory curseVoteAddrs, uint32[] memory voteCounts, bytes32[] memory cursesHashes, uint16 accumulatedWeight, bool cursed ) { accumulatedWeight = s_curseVoteProgress.accumulatedWeight; cursed = s_curseVoteProgress.curseActive; uint256 numCursers; Voter[] memory voters = s_versionedConfig.config.voters; for (uint256 i = 0; i < voters.length; ++i) { CurserRecord memory curserRecord = s_curserRecords[voters[i].curseVoteAddr]; if (curserRecord.voteCount > 0) { ++numCursers; } } curseVoteAddrs = new address[](numCursers); voteCounts = new uint32[](numCursers); cursesHashes = new bytes32[](numCursers); uint256 j = 0; for (uint256 i = 0; i < voters.length; ++i) { address curseVoteAddr = voters[i].curseVoteAddr; CurserRecord memory curserRecord = s_curserRecords[curseVoteAddr]; if (curserRecord.voteCount > 0) { curseVoteAddrs[j] = curseVoteAddr; voteCounts[j] = curserRecord.voteCount; cursesHashes[j] = curserRecord.cursesHash; ++j; } } } function _validateConfig(Config memory config) internal pure returns (bool) { if ( config.voters.length == 0 || config.voters.length > MAX_NUM_VOTERS || config.blessWeightThreshold == 0 || config.curseWeightThreshold == 0 ) { return false; } uint256 totalBlessWeight = 0; uint256 totalCurseWeight = 0; address[] memory allAddrs = new address[](3 * config.voters.length); for (uint256 i = 0; i < config.voters.length; ++i) { Voter memory voter = config.voters[i]; if ( voter.blessVoteAddr == address(0) || voter.curseVoteAddr == address(0) || voter.curseUnvoteAddr == address(0) || (voter.blessWeight == 0 && voter.curseWeight == 0) ) { return false; } allAddrs[3 * i + 0] = voter.blessVoteAddr; allAddrs[3 * i + 1] = voter.curseVoteAddr; allAddrs[3 * i + 2] = voter.curseUnvoteAddr; totalBlessWeight += voter.blessWeight; totalCurseWeight += voter.curseWeight; } for (uint256 i = 0; i < allAddrs.length; ++i) { address allAddrs_i = allAddrs[i]; for (uint256 j = i + 1; j < allAddrs.length; ++j) { if (allAddrs_i == allAddrs[j]) { return false; } } } return totalBlessWeight >= config.blessWeightThreshold && totalCurseWeight >= config.curseWeightThreshold; } function _setConfig(Config memory config) private { if (isCursed()) revert MustRecoverFromCurse(); if (!_validateConfig(config)) revert InvalidConfig(); Config memory oldConfig = s_versionedConfig.config; // We can't directly assign s_versionedConfig.config to config // because copying a memory array into storage is not supported. { s_versionedConfig.config.blessWeightThreshold = config.blessWeightThreshold; s_versionedConfig.config.curseWeightThreshold = config.curseWeightThreshold; while (s_versionedConfig.config.voters.length != 0) { Voter memory voter = s_versionedConfig.config.voters[s_versionedConfig.config.voters.length - 1]; delete s_blesserRecords[voter.blessVoteAddr]; s_curserRecords[voter.curseVoteAddr].active = false; s_versionedConfig.config.voters.pop(); } for (uint256 i = 0; i < config.voters.length; ++i) { s_versionedConfig.config.voters.push(config.voters[i]); } } ++s_versionedConfig.configVersion; uint32 configVersion = s_versionedConfig.configVersion; for (uint8 i = 0; i < config.voters.length; ++i) { Voter memory voter = config.voters[i]; s_blesserRecords[voter.blessVoteAddr] = BlesserRecord({ configVersion: configVersion, index: i, weight: voter.blessWeight }); s_curserRecords[voter.curseVoteAddr] = CurserRecord({ active: true, weight: voter.curseWeight, curseUnvoteAddr: voter.curseUnvoteAddr, voteCount: s_curserRecords[voter.curseVoteAddr].voteCount, cursesHash: s_curserRecords[voter.curseVoteAddr].cursesHash }); } s_versionedConfig.blockNumber = uint32(block.number); emit ConfigSet(configVersion, config); CurseVoteProgress memory newCurseVoteProgress = CurseVoteProgress({ curseWeightThreshold: config.curseWeightThreshold, accumulatedWeight: 0, curseActive: false }); // Retain votes for the cursers who are still part of the new config and delete records for the cursers who are not. for (uint8 i = 0; i < oldConfig.voters.length; ++i) { // We could be more efficient with this but since this is only for // setConfig it will do for now. address curseVoteAddr = oldConfig.voters[i].curseVoteAddr; CurserRecord memory curserRecord = s_curserRecords[curseVoteAddr]; if (!curserRecord.active) { delete s_curserRecords[curseVoteAddr]; } else if (curserRecord.active && curserRecord.voteCount > 0) { newCurseVoteProgress.accumulatedWeight += curserRecord.weight; emit ReusedVotesToCurse( configVersion, curseVoteAddr, curserRecord.weight, curserRecord.voteCount, curserRecord.cursesHash, newCurseVoteProgress.accumulatedWeight ); } } newCurseVoteProgress.curseActive = newCurseVoteProgress.accumulatedWeight >= newCurseVoteProgress.curseWeightThreshold; if (newCurseVoteProgress.curseActive) { emit Cursed(configVersion, block.timestamp); } s_curseVoteProgress = newCurseVoteProgress; } } // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; abstract contract TypeAndVersionInterface { function typeAndVersion() external pure virtual returns (string memory); } // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.0; /// @notice This interface contains the only ARM-related functions that might be used on-chain by other CCIP contracts. interface IARM { /// @notice A Merkle root tagged with the address of the commit store contract it is destined for. struct TaggedRoot { address commitStore; bytes32 root; } /// @notice Callers MUST NOT cache the return value as a blessed tagged root could become unblessed. function isBlessed(TaggedRoot calldata taggedRoot) external view returns (bool); /// @notice When the ARM is "cursed", CCIP pauses until the curse is lifted. function isCursed() external view returns (bool); } // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {ConfirmedOwner} from "../../ConfirmedOwner.sol"; /// @title The OwnerIsCreator contract /// @notice A contract with helpers for basic contract ownership. contract OwnerIsCreator is ConfirmedOwner { constructor() ConfirmedOwner(msg.sender) {} } // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "./ConfirmedOwnerWithProposal.sol"; /** * @title The ConfirmedOwner contract * @notice A contract with helpers for basic contract ownership. */ contract ConfirmedOwner is ConfirmedOwnerWithProposal { constructor(address newOwner) ConfirmedOwnerWithProposal(newOwner, address(0)) {} } // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "./interfaces/OwnableInterface.sol"; /** * @title The ConfirmedOwner contract * @notice A contract with helpers for basic contract ownership. */ contract ConfirmedOwnerWithProposal is OwnableInterface { address private s_owner; address private s_pendingOwner; event OwnershipTransferRequested(address indexed from, address indexed to); event OwnershipTransferred(address indexed from, address indexed to); constructor(address newOwner, address pendingOwner) { require(newOwner != address(0), "Cannot set owner to zero"); s_owner = newOwner; if (pendingOwner != address(0)) { _transferOwnership(pendingOwner); } } /** * @notice Allows an owner to begin transferring ownership to a new address, * pending. */ function transferOwnership(address to) public override onlyOwner { _transferOwnership(to); } /** * @notice Allows an ownership transfer to be completed by the recipient. */ function acceptOwnership() external override { require(msg.sender == s_pendingOwner, "Must be proposed owner"); address oldOwner = s_owner; s_owner = msg.sender; s_pendingOwner = address(0); emit OwnershipTransferred(oldOwner, msg.sender); } /** * @notice Get the current owner */ function owner() public view override returns (address) { return s_owner; } /** * @notice validate, transfer ownership, and emit relevant events */ function _transferOwnership(address to) private { require(to != msg.sender, "Cannot transfer to self"); s_pendingOwner = to; emit OwnershipTransferRequested(s_owner, to); } /** * @notice validate access */ function _validateOwnership() internal view { require(msg.sender == s_owner, "Only callable by owner"); } /** * @notice Reverts if called by anyone other than the contract owner. */ modifier onlyOwner() { _validateOwnership(); _; } } // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface OwnableInterface { function owner() external returns (address); function transferOwnership(address recipient) external; function acceptOwnership() external; }