From e500c1a471c4e9bb66a89ff27a763a83824f767c Mon Sep 17 00:00:00 2001 From: frank zhu Date: Wed, 7 Aug 2024 10:02:24 -0500 Subject: [PATCH 1/9] chore: update dependabot config gomod (#14063) --- .github/dependabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 19e008c8ce4..cea4f07b90d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,7 +4,7 @@ updates: directory: "/" schedule: interval: monthly - open-pull-requests-limit: 10 + open-pull-requests-limit: 0 ignore: # Old versions are pinned for libocr. - dependency-name: github.com/libp2p/go-libp2p-core From 215277f9e041d18dc5686c697e6959d5edaaf346 Mon Sep 17 00:00:00 2001 From: FelixFan1992 Date: Wed, 7 Aug 2024 11:21:31 -0400 Subject: [PATCH 2/9] auto-10161: replicate v2_3 to v2_3_zksync (#14035) * auto-10161: replicate v2_3 to v2_3_zksync * update * small fixes * add an zksync automation forwarder * fix linter * update * update * lint --- contracts/.changeset/loud-lobsters-guess.md | 5 + contracts/.solhintignore | 1 + .../native_solc_compile_all_automation | 2 +- .../automation/ZKSyncAutomationForwarder.sol | 92 ++ .../v0.8/automation/test/{v2_3 => }/WETH9.sol | 0 .../v0.8/automation/test/v2_3/BaseTest.t.sol | 4 +- .../ZKSyncAutomationRegistry2_3.sol | 391 ++++++ .../ZKSyncAutomationRegistryBase2_3.sol | 1216 +++++++++++++++++ .../ZKSyncAutomationRegistryLogicA2_3.sol | 283 ++++ .../ZKSyncAutomationRegistryLogicB2_3.sol | 449 ++++++ .../ZKSyncAutomationRegistryLogicC2_3.sol | 638 +++++++++ .../automation/AutomationRegistry2_3.test.ts | 1 - contracts/test/v0.8/automation/helpers.ts | 4 +- 13 files changed, 3080 insertions(+), 6 deletions(-) create mode 100644 contracts/.changeset/loud-lobsters-guess.md create mode 100644 contracts/src/v0.8/automation/ZKSyncAutomationForwarder.sol rename contracts/src/v0.8/automation/test/{v2_3 => }/WETH9.sol (100%) create mode 100644 contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistry2_3.sol create mode 100644 contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryBase2_3.sol create mode 100644 contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryLogicA2_3.sol create mode 100644 contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryLogicB2_3.sol create mode 100644 contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryLogicC2_3.sol diff --git a/contracts/.changeset/loud-lobsters-guess.md b/contracts/.changeset/loud-lobsters-guess.md new file mode 100644 index 00000000000..e470267e4e4 --- /dev/null +++ b/contracts/.changeset/loud-lobsters-guess.md @@ -0,0 +1,5 @@ +--- +'@chainlink/contracts': patch +--- + +auto: create a replication from v2_3 to v2_3_zksync diff --git a/contracts/.solhintignore b/contracts/.solhintignore index bad1935442b..55d195c3059 100644 --- a/contracts/.solhintignore +++ b/contracts/.solhintignore @@ -18,6 +18,7 @@ ./src/v0.8/automation/libraries/internal/Cron.sol ./src/v0.8/automation/AutomationForwarder.sol ./src/v0.8/automation/AutomationForwarderLogic.sol +./src/v0.8/automation/ZKSyncAutomationForwarder.sol ./src/v0.8/automation/interfaces/v2_2/IAutomationRegistryMaster.sol ./src/v0.8/automation/interfaces/v2_3/IAutomationRegistryMaster2_3.sol diff --git a/contracts/scripts/native_solc_compile_all_automation b/contracts/scripts/native_solc_compile_all_automation index f144e4f7dc8..29326a15c05 100755 --- a/contracts/scripts/native_solc_compile_all_automation +++ b/contracts/scripts/native_solc_compile_all_automation @@ -108,4 +108,4 @@ compileContract automation/v2_3/AutomationUtils2_3.sol compileContract automation/interfaces/v2_3/IAutomationRegistryMaster2_3.sol compileContract automation/testhelpers/MockETHUSDAggregator.sol -compileContract automation/test/v2_3/WETH9.sol +compileContract automation/test/WETH9.sol diff --git a/contracts/src/v0.8/automation/ZKSyncAutomationForwarder.sol b/contracts/src/v0.8/automation/ZKSyncAutomationForwarder.sol new file mode 100644 index 00000000000..cfbff1365e1 --- /dev/null +++ b/contracts/src/v0.8/automation/ZKSyncAutomationForwarder.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.16; + +import {IAutomationRegistryConsumer} from "./interfaces/IAutomationRegistryConsumer.sol"; + +uint256 constant PERFORM_GAS_CUSHION = 5_000; + +/** + * @title AutomationForwarder is a relayer that sits between the registry and the customer's target contract + * @dev The purpose of the forwarder is to give customers a consistent address to authorize against, + * which stays consistent between migrations. The Forwarder also exposes the registry address, so that users who + * want to programmatically interact with the registry (ie top up funds) can do so. + */ +contract ZKSyncAutomationForwarder { + /// @notice the user's target contract address + address private immutable i_target; + + /// @notice the shared logic address + address private immutable i_logic; + + IAutomationRegistryConsumer private s_registry; + + constructor(address target, address registry, address logic) { + s_registry = IAutomationRegistryConsumer(registry); + i_target = target; + i_logic = logic; + } + + /** + * @notice forward is called by the registry and forwards the call to the target + * @param gasAmount is the amount of gas to use in the call + * @param data is the 4 bytes function selector + arbitrary function data + * @return success indicating whether the target call succeeded or failed + */ + function forward(uint256 gasAmount, bytes memory data) external returns (bool success, uint256 gasUsed) { + if (msg.sender != address(s_registry)) revert(); + address target = i_target; + gasUsed = gasleft(); + assembly { + let g := gas() + // Compute g -= PERFORM_GAS_CUSHION and check for underflow + if lt(g, PERFORM_GAS_CUSHION) { + revert(0, 0) + } + g := sub(g, PERFORM_GAS_CUSHION) + // if g - g//64 <= gasAmount, revert + // (we subtract g//64 because of EIP-150) + if iszero(gt(sub(g, div(g, 64)), gasAmount)) { + revert(0, 0) + } + // solidity calls check that a contract actually exists at the destination, so we do the same + if iszero(extcodesize(target)) { + revert(0, 0) + } + // call with exact gas + success := call(gasAmount, target, 0, add(data, 0x20), mload(data), 0, 0) + } + gasUsed = gasUsed - gasleft(); + return (success, gasUsed); + } + + function getTarget() external view returns (address) { + return i_target; + } + + fallback() external { + // copy to memory for assembly access + address logic = i_logic; + // copied directly from OZ's Proxy contract + assembly { + // Copy msg.data. We take full control of memory in this inline assembly + // block because it will not return to Solidity code. We overwrite the + // Solidity scratch pad at memory position 0. + calldatacopy(0, 0, calldatasize()) + + // out and outsize are 0 because we don't know the size yet. + let result := delegatecall(gas(), logic, 0, calldatasize(), 0, 0) + + // Copy the returned data. + returndatacopy(0, 0, returndatasize()) + + switch result + // delegatecall returns 0 on error. + case 0 { + revert(0, returndatasize()) + } + default { + return(0, returndatasize()) + } + } + } +} diff --git a/contracts/src/v0.8/automation/test/v2_3/WETH9.sol b/contracts/src/v0.8/automation/test/WETH9.sol similarity index 100% rename from contracts/src/v0.8/automation/test/v2_3/WETH9.sol rename to contracts/src/v0.8/automation/test/WETH9.sol diff --git a/contracts/src/v0.8/automation/test/v2_3/BaseTest.t.sol b/contracts/src/v0.8/automation/test/v2_3/BaseTest.t.sol index 9016f52c55d..9e46e7bb40d 100644 --- a/contracts/src/v0.8/automation/test/v2_3/BaseTest.t.sol +++ b/contracts/src/v0.8/automation/test/v2_3/BaseTest.t.sol @@ -20,14 +20,14 @@ import {ChainModuleBase} from "../../chains/ChainModuleBase.sol"; import {IERC20Metadata as IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import {MockUpkeep} from "../../mocks/MockUpkeep.sol"; import {IWrappedNative} from "../../interfaces/v2_3/IWrappedNative.sol"; -import {WETH9} from "./WETH9.sol"; +import {WETH9} from "../WETH9.sol"; /** * @title BaseTest provides basic test setup procedures and dependencies for use by other * unit tests */ contract BaseTest is Test { - // test state (not exposed to derrived tests) + // test state (not exposed to derived tests) uint256 private nonce; // constants diff --git a/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistry2_3.sol b/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistry2_3.sol new file mode 100644 index 00000000000..027fe59aca7 --- /dev/null +++ b/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistry2_3.sol @@ -0,0 +1,391 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.19; + +import {EnumerableSet} from "../../vendor/openzeppelin-solidity/v4.7.3/contracts/utils/structs/EnumerableSet.sol"; +import {Address} from "../../vendor/openzeppelin-solidity/v4.7.3/contracts/utils/Address.sol"; +import {ZKSyncAutomationRegistryBase2_3} from "./ZKSyncAutomationRegistryBase2_3.sol"; +import {ZKSyncAutomationRegistryLogicA2_3} from "./ZKSyncAutomationRegistryLogicA2_3.sol"; +import {ZKSyncAutomationRegistryLogicC2_3} from "./ZKSyncAutomationRegistryLogicC2_3.sol"; +import {Chainable} from "../Chainable.sol"; +import {OCR2Abstract} from "../../shared/ocr2/OCR2Abstract.sol"; +import {IERC20Metadata as IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +/** + * @notice Registry for adding work for Chainlink nodes to perform on client + * contracts. Clients must support the AutomationCompatibleInterface interface. + */ +contract ZKSyncAutomationRegistry2_3 is ZKSyncAutomationRegistryBase2_3, OCR2Abstract, Chainable { + using Address for address; + using EnumerableSet for EnumerableSet.UintSet; + using EnumerableSet for EnumerableSet.AddressSet; + + /** + * @notice versions: + * AutomationRegistry 2.3.0: supports native and ERC20 billing + * changes flat fee to USD-denominated + * adds support for custom billing overrides + * AutomationRegistry 2.2.0: moves chain-specific integration code into a separate module + * KeeperRegistry 2.1.0: introduces support for log triggers + * removes the need for "wrapped perform data" + * KeeperRegistry 2.0.2: pass revert bytes as performData when target contract reverts + * fixes issue with arbitrum block number + * does an early return in case of stale report instead of revert + * KeeperRegistry 2.0.1: implements workaround for buggy migrate function in 1.X + * KeeperRegistry 2.0.0: implement OCR interface + * KeeperRegistry 1.3.0: split contract into Proxy and Logic + * account for Arbitrum and Optimism L1 gas fee + * allow users to configure upkeeps + * KeeperRegistry 1.2.0: allow funding within performUpkeep + * allow configurable registry maxPerformGas + * add function to let admin change upkeep gas limit + * add minUpkeepSpend requirement + * upgrade to solidity v0.8 + * KeeperRegistry 1.1.0: added flatFeeMicroLink + * KeeperRegistry 1.0.0: initial release + */ + string public constant override typeAndVersion = "AutomationRegistry 2.3.0"; + + /** + * @param logicA the address of the first logic contract + * @dev we cast the contract to logicC in order to call logicC functions (via fallback) + */ + constructor( + ZKSyncAutomationRegistryLogicA2_3 logicA + ) + ZKSyncAutomationRegistryBase2_3( + ZKSyncAutomationRegistryLogicC2_3(address(logicA)).getLinkAddress(), + ZKSyncAutomationRegistryLogicC2_3(address(logicA)).getLinkUSDFeedAddress(), + ZKSyncAutomationRegistryLogicC2_3(address(logicA)).getNativeUSDFeedAddress(), + ZKSyncAutomationRegistryLogicC2_3(address(logicA)).getFastGasFeedAddress(), + ZKSyncAutomationRegistryLogicC2_3(address(logicA)).getAutomationForwarderLogic(), + ZKSyncAutomationRegistryLogicC2_3(address(logicA)).getAllowedReadOnlyAddress(), + ZKSyncAutomationRegistryLogicC2_3(address(logicA)).getPayoutMode(), + ZKSyncAutomationRegistryLogicC2_3(address(logicA)).getWrappedNativeTokenAddress() + ) + Chainable(address(logicA)) + {} + + /** + * @notice holds the variables used in the transmit function, necessary to avoid stack too deep errors + */ + struct TransmitVars { + uint16 numUpkeepsPassedChecks; + uint96 totalReimbursement; + uint96 totalPremium; + uint256 totalCalldataWeight; + } + + // ================================================================ + // | HOT PATH ACTIONS | + // ================================================================ + + /** + * @inheritdoc OCR2Abstract + */ + function transmit( + bytes32[3] calldata reportContext, + bytes calldata rawReport, + bytes32[] calldata rs, + bytes32[] calldata ss, + bytes32 rawVs + ) external override { + uint256 gasOverhead = gasleft(); + // use this msg.data length check to ensure no extra data is included in the call + // 4 is first 4 bytes of the keccak-256 hash of the function signature. ss.length == rs.length so use one of them + // 4 + (32 * 3) + (rawReport.length + 32 + 32) + (32 * rs.length + 32 + 32) + (32 * ss.length + 32 + 32) + 32 + uint256 requiredLength = 324 + rawReport.length + 64 * rs.length; + if (msg.data.length != requiredLength) revert InvalidDataLength(); + HotVars memory hotVars = s_hotVars; + + if (hotVars.paused) revert RegistryPaused(); + if (!s_transmitters[msg.sender].active) revert OnlyActiveTransmitters(); + + // Verify signatures + if (s_latestConfigDigest != reportContext[0]) revert ConfigDigestMismatch(); + if (rs.length != hotVars.f + 1 || rs.length != ss.length) revert IncorrectNumberOfSignatures(); + _verifyReportSignature(reportContext, rawReport, rs, ss, rawVs); + + Report memory report = _decodeReport(rawReport); + + uint40 epochAndRound = uint40(uint256(reportContext[1])); + uint32 epoch = uint32(epochAndRound >> 8); + + _handleReport(hotVars, report, gasOverhead); + + if (epoch > hotVars.latestEpoch) { + s_hotVars.latestEpoch = epoch; + } + } + + /** + * @notice handles the report by performing the upkeeps and updating the state + * @param hotVars the hot variables of the registry + * @param report the report to be handled (already verified and decoded) + * @param gasOverhead the running tally of gas overhead to be split across the upkeeps + * @dev had to split this function from transmit() to avoid stack too deep errors + * @dev all other internal / private functions are generally defined in the Base contract + * we leave this here because it is essentially a continuation of the transmit() function, + */ + function _handleReport(HotVars memory hotVars, Report memory report, uint256 gasOverhead) private { + UpkeepTransmitInfo[] memory upkeepTransmitInfo = new UpkeepTransmitInfo[](report.upkeepIds.length); + TransmitVars memory transmitVars = TransmitVars({ + numUpkeepsPassedChecks: 0, + totalCalldataWeight: 0, + totalReimbursement: 0, + totalPremium: 0 + }); + + uint256 blocknumber = hotVars.chainModule.blockNumber(); + uint256 l1Fee = hotVars.chainModule.getCurrentL1Fee(); + + for (uint256 i = 0; i < report.upkeepIds.length; i++) { + upkeepTransmitInfo[i].upkeep = s_upkeep[report.upkeepIds[i]]; + upkeepTransmitInfo[i].triggerType = _getTriggerType(report.upkeepIds[i]); + + (upkeepTransmitInfo[i].earlyChecksPassed, upkeepTransmitInfo[i].dedupID) = _prePerformChecks( + report.upkeepIds[i], + blocknumber, + report.triggers[i], + upkeepTransmitInfo[i], + hotVars + ); + + if (upkeepTransmitInfo[i].earlyChecksPassed) { + transmitVars.numUpkeepsPassedChecks += 1; + } else { + continue; + } + + // Actually perform the target upkeep + (upkeepTransmitInfo[i].performSuccess, upkeepTransmitInfo[i].gasUsed) = _performUpkeep( + upkeepTransmitInfo[i].upkeep.forwarder, + report.gasLimits[i], + report.performDatas[i] + ); + + // To split L1 fee across the upkeeps, assign a weight to this upkeep based on the length + // of the perform data and calldata overhead + upkeepTransmitInfo[i].calldataWeight = + report.performDatas[i].length + + TRANSMIT_CALLDATA_FIXED_BYTES_OVERHEAD + + (TRANSMIT_CALLDATA_PER_SIGNER_BYTES_OVERHEAD * (hotVars.f + 1)); + transmitVars.totalCalldataWeight += upkeepTransmitInfo[i].calldataWeight; + + // Deduct the gasUsed by upkeep from the overhead tally - upkeeps pay for their own gas individually + gasOverhead -= upkeepTransmitInfo[i].gasUsed; + + // Store last perform block number / deduping key for upkeep + _updateTriggerMarker(report.upkeepIds[i], blocknumber, upkeepTransmitInfo[i]); + } + // No upkeeps to be performed in this report + if (transmitVars.numUpkeepsPassedChecks == 0) { + return; + } + + // This is the overall gas overhead that will be split across performed upkeeps + // Take upper bound of 16 gas per callData bytes + gasOverhead = (gasOverhead - gasleft()) + (16 * msg.data.length) + ACCOUNTING_FIXED_GAS_OVERHEAD; + gasOverhead = gasOverhead / transmitVars.numUpkeepsPassedChecks + ACCOUNTING_PER_UPKEEP_GAS_OVERHEAD; + + { + BillingTokenPaymentParams memory billingTokenParams; + uint256 nativeUSD = _getNativeUSD(hotVars); + for (uint256 i = 0; i < report.upkeepIds.length; i++) { + if (upkeepTransmitInfo[i].earlyChecksPassed) { + if (i == 0 || upkeepTransmitInfo[i].upkeep.billingToken != upkeepTransmitInfo[i - 1].upkeep.billingToken) { + billingTokenParams = _getBillingTokenPaymentParams(hotVars, upkeepTransmitInfo[i].upkeep.billingToken); + } + PaymentReceipt memory receipt = _handlePayment( + hotVars, + PaymentParams({ + gasLimit: upkeepTransmitInfo[i].gasUsed, + gasOverhead: gasOverhead, + l1CostWei: (l1Fee * upkeepTransmitInfo[i].calldataWeight) / transmitVars.totalCalldataWeight, + fastGasWei: report.fastGasWei, + linkUSD: report.linkUSD, + nativeUSD: nativeUSD, + billingToken: upkeepTransmitInfo[i].upkeep.billingToken, + billingTokenParams: billingTokenParams, + isTransaction: true + }), + report.upkeepIds[i], + upkeepTransmitInfo[i].upkeep + ); + transmitVars.totalPremium += receipt.premiumInJuels; + transmitVars.totalReimbursement += receipt.gasReimbursementInJuels; + + emit UpkeepPerformed( + report.upkeepIds[i], + upkeepTransmitInfo[i].performSuccess, + receipt.gasChargeInBillingToken + receipt.premiumInBillingToken, + upkeepTransmitInfo[i].gasUsed, + gasOverhead, + report.triggers[i] + ); + } + } + } + // record payments to NOPs, all in LINK + s_transmitters[msg.sender].balance += transmitVars.totalReimbursement; + s_hotVars.totalPremium += transmitVars.totalPremium; + s_reserveAmounts[IERC20(address(i_link))] += transmitVars.totalReimbursement + transmitVars.totalPremium; + } + + // ================================================================ + // | OCR2ABSTRACT | + // ================================================================ + + /** + * @inheritdoc OCR2Abstract + * @dev prefer the type-safe version of setConfig (below) whenever possible. The OnchainConfig could differ between registry versions + * @dev this function takes up precious space on the root contract, but must be implemented to conform to the OCR2Abstract interface + */ + function setConfig( + address[] memory signers, + address[] memory transmitters, + uint8 f, + bytes memory onchainConfigBytes, + uint64 offchainConfigVersion, + bytes memory offchainConfig + ) external override { + (OnchainConfig memory config, IERC20[] memory billingTokens, BillingConfig[] memory billingConfigs) = abi.decode( + onchainConfigBytes, + (OnchainConfig, IERC20[], BillingConfig[]) + ); + + setConfigTypeSafe( + signers, + transmitters, + f, + config, + offchainConfigVersion, + offchainConfig, + billingTokens, + billingConfigs + ); + } + + /** + * @notice sets the configuration for the registry + * @param signers the list of permitted signers + * @param transmitters the list of permitted transmitters + * @param f the maximum tolerance for faulty nodes + * @param onchainConfig configuration values that are used on-chain + * @param offchainConfigVersion the version of the offchainConfig + * @param offchainConfig configuration values that are used off-chain + * @param billingTokens the list of valid billing tokens + * @param billingConfigs the configurations for each billing token + */ + function setConfigTypeSafe( + address[] memory signers, + address[] memory transmitters, + uint8 f, + OnchainConfig memory onchainConfig, + uint64 offchainConfigVersion, + bytes memory offchainConfig, + IERC20[] memory billingTokens, + BillingConfig[] memory billingConfigs + ) public onlyOwner { + if (signers.length > MAX_NUM_ORACLES) revert TooManyOracles(); + if (f == 0) revert IncorrectNumberOfFaultyOracles(); + if (signers.length != transmitters.length || signers.length <= 3 * f) revert IncorrectNumberOfSigners(); + if (billingTokens.length != billingConfigs.length) revert ParameterLengthError(); + // set billing config for tokens + _setBillingConfig(billingTokens, billingConfigs); + + _updateTransmitters(signers, transmitters); + + s_hotVars = HotVars({ + f: f, + stalenessSeconds: onchainConfig.stalenessSeconds, + gasCeilingMultiplier: onchainConfig.gasCeilingMultiplier, + paused: s_hotVars.paused, + reentrancyGuard: s_hotVars.reentrancyGuard, + totalPremium: s_hotVars.totalPremium, + latestEpoch: 0, // DON restarts epoch + reorgProtectionEnabled: onchainConfig.reorgProtectionEnabled, + chainModule: onchainConfig.chainModule + }); + + uint32 previousConfigBlockNumber = s_storage.latestConfigBlockNumber; + uint32 newLatestConfigBlockNumber = uint32(onchainConfig.chainModule.blockNumber()); + uint32 newConfigCount = s_storage.configCount + 1; + + s_storage = Storage({ + checkGasLimit: onchainConfig.checkGasLimit, + maxPerformGas: onchainConfig.maxPerformGas, + transcoder: onchainConfig.transcoder, + maxCheckDataSize: onchainConfig.maxCheckDataSize, + maxPerformDataSize: onchainConfig.maxPerformDataSize, + maxRevertDataSize: onchainConfig.maxRevertDataSize, + upkeepPrivilegeManager: onchainConfig.upkeepPrivilegeManager, + financeAdmin: onchainConfig.financeAdmin, + nonce: s_storage.nonce, + configCount: newConfigCount, + latestConfigBlockNumber: newLatestConfigBlockNumber + }); + s_fallbackGasPrice = onchainConfig.fallbackGasPrice; + s_fallbackLinkPrice = onchainConfig.fallbackLinkPrice; + s_fallbackNativePrice = onchainConfig.fallbackNativePrice; + + bytes memory onchainConfigBytes = abi.encode(onchainConfig); + + s_latestConfigDigest = _configDigestFromConfigData( + block.chainid, + address(this), + s_storage.configCount, + signers, + transmitters, + f, + onchainConfigBytes, + offchainConfigVersion, + offchainConfig + ); + + for (uint256 idx = s_registrars.length(); idx > 0; idx--) { + s_registrars.remove(s_registrars.at(idx - 1)); + } + + for (uint256 idx = 0; idx < onchainConfig.registrars.length; idx++) { + s_registrars.add(onchainConfig.registrars[idx]); + } + + emit ConfigSet( + previousConfigBlockNumber, + s_latestConfigDigest, + s_storage.configCount, + signers, + transmitters, + f, + onchainConfigBytes, + offchainConfigVersion, + offchainConfig + ); + } + + /** + * @inheritdoc OCR2Abstract + * @dev this function takes up precious space on the root contract, but must be implemented to conform to the OCR2Abstract interface + */ + function latestConfigDetails() + external + view + override + returns (uint32 configCount, uint32 blockNumber, bytes32 configDigest) + { + return (s_storage.configCount, s_storage.latestConfigBlockNumber, s_latestConfigDigest); + } + + /** + * @inheritdoc OCR2Abstract + * @dev this function takes up precious space on the root contract, but must be implemented to conform to the OCR2Abstract interface + */ + function latestConfigDigestAndEpoch() + external + view + override + returns (bool scanLogs, bytes32 configDigest, uint32 epoch) + { + return (false, s_latestConfigDigest, s_hotVars.latestEpoch); + } +} diff --git a/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryBase2_3.sol b/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryBase2_3.sol new file mode 100644 index 00000000000..524ecacc826 --- /dev/null +++ b/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryBase2_3.sol @@ -0,0 +1,1216 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.19; + +import {EnumerableSet} from "../../vendor/openzeppelin-solidity/v4.7.3/contracts/utils/structs/EnumerableSet.sol"; +import {Address} from "../../vendor/openzeppelin-solidity/v4.7.3/contracts/utils/Address.sol"; +import {StreamsLookupCompatibleInterface} from "../interfaces/StreamsLookupCompatibleInterface.sol"; +import {ILogAutomation, Log} from "../interfaces/ILogAutomation.sol"; +import {IAutomationForwarder} from "../interfaces/IAutomationForwarder.sol"; +import {ConfirmedOwner} from "../../shared/access/ConfirmedOwner.sol"; +import {AggregatorV3Interface} from "../../shared/interfaces/AggregatorV3Interface.sol"; +import {LinkTokenInterface} from "../../shared/interfaces/LinkTokenInterface.sol"; +import {KeeperCompatibleInterface} from "../interfaces/KeeperCompatibleInterface.sol"; +import {IChainModule} from "../interfaces/IChainModule.sol"; +import {IERC20Metadata as IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {SafeCast} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/math/SafeCast.sol"; +import {IWrappedNative} from "../interfaces/v2_3/IWrappedNative.sol"; + +/** + * @notice Base Keeper Registry contract, contains shared logic between + * AutomationRegistry and AutomationRegistryLogic + * @dev all errors, events, and internal functions should live here + */ +// solhint-disable-next-line max-states-count +abstract contract ZKSyncAutomationRegistryBase2_3 is ConfirmedOwner { + using Address for address; + using EnumerableSet for EnumerableSet.UintSet; + using EnumerableSet for EnumerableSet.AddressSet; + + address internal constant ZERO_ADDRESS = address(0); + address internal constant IGNORE_ADDRESS = 0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF; + bytes4 internal constant CHECK_SELECTOR = KeeperCompatibleInterface.checkUpkeep.selector; + bytes4 internal constant PERFORM_SELECTOR = KeeperCompatibleInterface.performUpkeep.selector; + bytes4 internal constant CHECK_CALLBACK_SELECTOR = StreamsLookupCompatibleInterface.checkCallback.selector; + bytes4 internal constant CHECK_LOG_SELECTOR = ILogAutomation.checkLog.selector; + uint256 internal constant PERFORM_GAS_MIN = 2_300; + uint256 internal constant CANCELLATION_DELAY = 50; + uint256 internal constant PERFORM_GAS_CUSHION = 5_000; + uint256 internal constant PPB_BASE = 1_000_000_000; + uint32 internal constant UINT32_MAX = type(uint32).max; + // The first byte of the mask can be 0, because we only ever have 31 oracles + uint256 internal constant ORACLE_MASK = 0x0001010101010101010101010101010101010101010101010101010101010101; + uint8 internal constant UPKEEP_VERSION_BASE = 4; + + // Next block of constants are only used in maxPayment estimation during checkUpkeep simulation + // These values are calibrated using hardhat tests which simulate various cases and verify that + // the variables result in accurate estimation + uint256 internal constant REGISTRY_CONDITIONAL_OVERHEAD = 98_200; // Fixed gas overhead for conditional upkeeps + uint256 internal constant REGISTRY_LOG_OVERHEAD = 122_500; // Fixed gas overhead for log upkeeps + uint256 internal constant REGISTRY_PER_SIGNER_GAS_OVERHEAD = 5_600; // Value scales with f + uint256 internal constant REGISTRY_PER_PERFORM_BYTE_GAS_OVERHEAD = 24; // Per perform data byte overhead + + // The overhead (in bytes) in addition to perform data for upkeep sent in calldata + // This includes overhead for all struct encoding as well as report signatures + // There is a fixed component and a per signer component. This is calculated exactly by doing abi encoding + uint256 internal constant TRANSMIT_CALLDATA_FIXED_BYTES_OVERHEAD = 932; + uint256 internal constant TRANSMIT_CALLDATA_PER_SIGNER_BYTES_OVERHEAD = 64; + + // Next block of constants are used in actual payment calculation. We calculate the exact gas used within the + // tx itself, but since payment processing itself takes gas, and it needs the overhead as input, we use fixed constants + // to account for gas used in payment processing. These values are calibrated using hardhat tests which simulates various cases and verifies that + // the variables result in accurate estimation + uint256 internal constant ACCOUNTING_FIXED_GAS_OVERHEAD = 51_200; // Fixed overhead per tx + uint256 internal constant ACCOUNTING_PER_UPKEEP_GAS_OVERHEAD = 14_200; // Overhead per upkeep performed in batch + + LinkTokenInterface internal immutable i_link; + AggregatorV3Interface internal immutable i_linkUSDFeed; + AggregatorV3Interface internal immutable i_nativeUSDFeed; + AggregatorV3Interface internal immutable i_fastGasFeed; + address internal immutable i_automationForwarderLogic; + address internal immutable i_allowedReadOnlyAddress; + IWrappedNative internal immutable i_wrappedNativeToken; + + /** + * @dev - The storage is gas optimised for one and only one function - transmit. All the storage accessed in transmit + * is stored compactly. Rest of the storage layout is not of much concern as transmit is the only hot path + */ + + // Upkeep storage + EnumerableSet.UintSet internal s_upkeepIDs; + mapping(uint256 => Upkeep) internal s_upkeep; // accessed during transmit + mapping(uint256 => address) internal s_upkeepAdmin; + mapping(uint256 => address) internal s_proposedAdmin; + mapping(uint256 => bytes) internal s_checkData; + mapping(bytes32 => bool) internal s_dedupKeys; + // Registry config and state + EnumerableSet.AddressSet internal s_registrars; + mapping(address => Transmitter) internal s_transmitters; + mapping(address => Signer) internal s_signers; + address[] internal s_signersList; // s_signersList contains the signing address of each oracle + address[] internal s_transmittersList; // s_transmittersList contains the transmission address of each oracle + EnumerableSet.AddressSet internal s_deactivatedTransmitters; + mapping(address => address) internal s_transmitterPayees; // s_payees contains the mapping from transmitter to payee. + mapping(address => address) internal s_proposedPayee; // proposed payee for a transmitter + bytes32 internal s_latestConfigDigest; // Read on transmit path in case of signature verification + HotVars internal s_hotVars; // Mixture of config and state, used in transmit + Storage internal s_storage; // Mixture of config and state, not used in transmit + uint256 internal s_fallbackGasPrice; + uint256 internal s_fallbackLinkPrice; + uint256 internal s_fallbackNativePrice; + mapping(address => MigrationPermission) internal s_peerRegistryMigrationPermission; // Permissions for migration to and fro + mapping(uint256 => bytes) internal s_upkeepTriggerConfig; // upkeep triggers + mapping(uint256 => bytes) internal s_upkeepOffchainConfig; // general config set by users for each upkeep + mapping(uint256 => bytes) internal s_upkeepPrivilegeConfig; // general config set by an administrative role for an upkeep + mapping(address => bytes) internal s_adminPrivilegeConfig; // general config set by an administrative role for an admin + // billing + mapping(IERC20 billingToken => uint256 reserveAmount) internal s_reserveAmounts; // unspent user deposits + unwithdrawn NOP payments + mapping(IERC20 billingToken => BillingConfig billingConfig) internal s_billingConfigs; // billing configurations for different tokens + mapping(uint256 upkeepID => BillingOverrides billingOverrides) internal s_billingOverrides; // billing overrides for specific upkeeps + IERC20[] internal s_billingTokens; // list of billing tokens + PayoutMode internal s_payoutMode; + + error ArrayHasNoEntries(); + error CannotCancel(); + error CheckDataExceedsLimit(); + error ConfigDigestMismatch(); + error DuplicateEntry(); + error DuplicateSigners(); + error GasLimitCanOnlyIncrease(); + error GasLimitOutsideRange(); + error IncorrectNumberOfFaultyOracles(); + error IncorrectNumberOfSignatures(); + error IncorrectNumberOfSigners(); + error IndexOutOfRange(); + error InsufficientBalance(uint256 available, uint256 requested); + error InsufficientLinkLiquidity(); + error InvalidDataLength(); + error InvalidFeed(); + error InvalidTrigger(); + error InvalidPayee(); + error InvalidRecipient(); + error InvalidReport(); + error InvalidSigner(); + error InvalidToken(); + error InvalidTransmitter(); + error InvalidTriggerType(); + error MigrationNotPermitted(); + error MustSettleOffchain(); + error MustSettleOnchain(); + error NotAContract(); + error OnlyActiveSigners(); + error OnlyActiveTransmitters(); + error OnlyCallableByAdmin(); + error OnlyCallableByLINKToken(); + error OnlyCallableByOwnerOrAdmin(); + error OnlyCallableByOwnerOrRegistrar(); + error OnlyCallableByPayee(); + error OnlyCallableByProposedAdmin(); + error OnlyCallableByProposedPayee(); + error OnlyCallableByUpkeepPrivilegeManager(); + error OnlyFinanceAdmin(); + error OnlyPausedUpkeep(); + error OnlySimulatedBackend(); + error OnlyUnpausedUpkeep(); + error ParameterLengthError(); + error ReentrantCall(); + error RegistryPaused(); + error RepeatedSigner(); + error RepeatedTransmitter(); + error TargetCheckReverted(bytes reason); + error TooManyOracles(); + error TranscoderNotSet(); + error TransferFailed(); + error UpkeepAlreadyExists(); + error UpkeepCancelled(); + error UpkeepNotCanceled(); + error UpkeepNotNeeded(); + error ValueNotChanged(); + error ZeroAddressNotAllowed(); + + enum MigrationPermission { + NONE, + OUTGOING, + INCOMING, + BIDIRECTIONAL + } + + enum Trigger { + CONDITION, + LOG + } + + enum UpkeepFailureReason { + NONE, + UPKEEP_CANCELLED, + UPKEEP_PAUSED, + TARGET_CHECK_REVERTED, + UPKEEP_NOT_NEEDED, + PERFORM_DATA_EXCEEDS_LIMIT, + INSUFFICIENT_BALANCE, + CALLBACK_REVERTED, + REVERT_DATA_EXCEEDS_LIMIT, + REGISTRY_PAUSED + } + + enum PayoutMode { + ON_CHAIN, + OFF_CHAIN + } + + /** + * @notice OnchainConfig of the registry + * @dev used only in setConfig() + * @member checkGasLimit gas limit when checking for upkeep + * @member stalenessSeconds number of seconds that is allowed for feed data to + * be stale before switching to the fallback pricing + * @member gasCeilingMultiplier multiplier to apply to the fast gas feed price + * when calculating the payment ceiling for keepers + * @member maxPerformGas max performGas allowed for an upkeep on this registry + * @member maxCheckDataSize max length of checkData bytes + * @member maxPerformDataSize max length of performData bytes + * @member maxRevertDataSize max length of revertData bytes + * @member fallbackGasPrice gas price used if the gas price feed is stale + * @member fallbackLinkPrice LINK price used if the LINK price feed is stale + * @member transcoder address of the transcoder contract + * @member registrars addresses of the registrar contracts + * @member upkeepPrivilegeManager address which can set privilege for upkeeps + * @member reorgProtectionEnabled if this registry enables re-org protection checks + * @member chainModule the chain specific module + */ + struct OnchainConfig { + uint32 checkGasLimit; + uint32 maxPerformGas; + uint32 maxCheckDataSize; + address transcoder; + // 1 word full + bool reorgProtectionEnabled; + uint24 stalenessSeconds; + uint32 maxPerformDataSize; + uint32 maxRevertDataSize; + address upkeepPrivilegeManager; + // 2 words full + uint16 gasCeilingMultiplier; + address financeAdmin; + // 3 words + uint256 fallbackGasPrice; + uint256 fallbackLinkPrice; + uint256 fallbackNativePrice; + address[] registrars; + IChainModule chainModule; + } + + /** + * @notice relevant state of an upkeep which is used in transmit function + * @member paused if this upkeep has been paused + * @member overridesEnabled if this upkeep has overrides enabled + * @member performGas the gas limit of upkeep execution + * @member maxValidBlocknumber until which block this upkeep is valid + * @member forwarder the forwarder contract to use for this upkeep + * @member amountSpent the amount this upkeep has spent, in the upkeep's billing token + * @member balance the balance of this upkeep + * @member lastPerformedBlockNumber the last block number when this upkeep was performed + */ + struct Upkeep { + bool paused; + bool overridesEnabled; + uint32 performGas; + uint32 maxValidBlocknumber; + IAutomationForwarder forwarder; + // 2 bytes left in 1st EVM word - read in transmit path + uint128 amountSpent; + uint96 balance; + uint32 lastPerformedBlockNumber; + // 0 bytes left in 2nd EVM word - written in transmit path + IERC20 billingToken; + // 12 bytes left in 3rd EVM word - read in transmit path + } + + /// @dev Config + State storage struct which is on hot transmit path + struct HotVars { + uint96 totalPremium; // ─────────╮ total historical payment to oracles for premium + uint32 latestEpoch; // │ latest epoch for which a report was transmitted + uint24 stalenessSeconds; // │ Staleness tolerance for feeds + uint16 gasCeilingMultiplier; // │ multiplier on top of fast gas feed for upper bound + uint8 f; // │ maximum number of faulty oracles + bool paused; // │ pause switch for all upkeeps in the registry + bool reentrancyGuard; // | guard against reentrancy + bool reorgProtectionEnabled; // ─╯ if this registry should enable the re-org protection mechanism + IChainModule chainModule; // the interface of chain specific module + } + + /// @dev Config + State storage struct which is not on hot transmit path + struct Storage { + address transcoder; // Address of transcoder contract used in migrations + uint32 checkGasLimit; // Gas limit allowed in checkUpkeep + uint32 maxPerformGas; // Max gas an upkeep can use on this registry + uint32 nonce; // Nonce for each upkeep created + // 1 EVM word full + address upkeepPrivilegeManager; // address which can set privilege for upkeeps + uint32 configCount; // incremented each time a new config is posted, The count is incorporated into the config digest to prevent replay attacks. + uint32 latestConfigBlockNumber; // makes it easier for offchain systems to extract config from logs + uint32 maxCheckDataSize; // max length of checkData bytes + // 2 EVM word full + address financeAdmin; // address which can withdraw funds from the contract + uint32 maxPerformDataSize; // max length of performData bytes + uint32 maxRevertDataSize; // max length of revertData bytes + // 4 bytes left in 3rd EVM word + } + + /// @dev Report transmitted by OCR to transmit function + struct Report { + uint256 fastGasWei; + uint256 linkUSD; + uint256[] upkeepIds; + uint256[] gasLimits; + bytes[] triggers; + bytes[] performDatas; + } + + /** + * @dev This struct is used to maintain run time information about an upkeep in transmit function + * @member upkeep the upkeep struct + * @member earlyChecksPassed whether the upkeep passed early checks before perform + * @member performSuccess whether the perform was successful + * @member triggerType the type of trigger + * @member gasUsed gasUsed by this upkeep in perform + * @member calldataWeight weight assigned to this upkeep for its contribution to calldata. It is used to split L1 fee + * @member dedupID unique ID used to dedup an upkeep/trigger combo + */ + struct UpkeepTransmitInfo { + Upkeep upkeep; + bool earlyChecksPassed; + bool performSuccess; + Trigger triggerType; + uint256 gasUsed; + uint256 calldataWeight; + bytes32 dedupID; + } + + /** + * @notice holds information about a transmiter / node in the DON + * @member active can this transmitter submit reports + * @member index of oracle in s_signersList/s_transmittersList + * @member balance a node's balance in LINK + * @member lastCollected the total balance at which the node last withdrew + * @dev uint96 is safe for balance / last collected because transmitters are only ever paid in LINK + */ + struct Transmitter { + bool active; + uint8 index; + uint96 balance; + uint96 lastCollected; + } + + struct TransmitterPayeeInfo { + address transmitterAddress; + address payeeAddress; + } + + struct Signer { + bool active; + // Index of oracle in s_signersList/s_transmittersList + uint8 index; + } + + /** + * @notice the trigger structure conditional trigger type + */ + struct ConditionalTrigger { + uint32 blockNum; + bytes32 blockHash; + } + + /** + * @notice the trigger structure of log upkeeps + * @dev NOTE that blockNum / blockHash describe the block used for the callback, + * not necessarily the block number that the log was emitted in!!!! + */ + struct LogTrigger { + bytes32 logBlockHash; + bytes32 txHash; + uint32 logIndex; + uint32 blockNum; + bytes32 blockHash; + } + + /** + * @notice the billing config of a token + * @dev this is a storage struct + */ + // solhint-disable-next-line gas-struct-packing + struct BillingConfig { + uint32 gasFeePPB; + uint24 flatFeeMilliCents; // min fee is $0.00001, max fee is $167 + AggregatorV3Interface priceFeed; + uint8 decimals; + // 1st word, read in calculating BillingTokenPaymentParams + uint256 fallbackPrice; + // 2nd word only read if stale + uint96 minSpend; + // 3rd word only read during cancellation + } + + /** + * @notice override-able billing params of a billing token + */ + struct BillingOverrides { + uint32 gasFeePPB; + uint24 flatFeeMilliCents; + } + + /** + * @notice pricing params for a billing token + * @dev this is a memory-only struct, so struct packing is less important + */ + struct BillingTokenPaymentParams { + uint8 decimals; + uint32 gasFeePPB; + uint24 flatFeeMilliCents; + uint256 priceUSD; + } + + /** + * @notice struct containing price & payment information used in calculating payment amount + * @member gasLimit the amount of gas used + * @member gasOverhead the amount of gas overhead + * @member l1CostWei the amount to be charged for L1 fee in wei + * @member fastGasWei the fast gas price + * @member linkUSD the exchange ratio between LINK and USD + * @member nativeUSD the exchange ratio between the chain's native token and USD + * @member billingToken the billing token + * @member billingTokenParams the payment params specific to a particular payment token + * @member isTransaction is this an eth_call or a transaction + */ + struct PaymentParams { + uint256 gasLimit; + uint256 gasOverhead; + uint256 l1CostWei; + uint256 fastGasWei; + uint256 linkUSD; + uint256 nativeUSD; + IERC20 billingToken; + BillingTokenPaymentParams billingTokenParams; + bool isTransaction; + } + + /** + * @notice struct containing receipt information about a payment or cost estimation + * @member gasChargeInBillingToken the amount to charge a user for gas spent using the billing token's native decimals + * @member premiumInBillingToken the premium charged to the user, shared between all nodes, using the billing token's native decimals + * @member gasReimbursementInJuels the amount to reimburse a node for gas spent + * @member premiumInJuels the premium paid to NOPs, shared between all nodes + */ + // solhint-disable-next-line gas-struct-packing + struct PaymentReceipt { + uint96 gasChargeInBillingToken; + uint96 premiumInBillingToken; + // one word ends + uint96 gasReimbursementInJuels; + uint96 premiumInJuels; + // second word ends + IERC20 billingToken; + uint96 linkUSD; + // third word ends + uint96 nativeUSD; + uint96 billingUSD; + // fourth word ends + } + + event AdminPrivilegeConfigSet(address indexed admin, bytes privilegeConfig); + event BillingConfigOverridden(uint256 indexed id, BillingOverrides overrides); + event BillingConfigOverrideRemoved(uint256 indexed id); + event BillingConfigSet(IERC20 indexed token, BillingConfig config); + event CancelledUpkeepReport(uint256 indexed id, bytes trigger); + event ChainSpecificModuleUpdated(address newModule); + event DedupKeyAdded(bytes32 indexed dedupKey); + event FeesWithdrawn(address indexed assetAddress, address indexed recipient, uint256 amount); + event FundsAdded(uint256 indexed id, address indexed from, uint96 amount); + event FundsWithdrawn(uint256 indexed id, uint256 amount, address to); + event InsufficientFundsUpkeepReport(uint256 indexed id, bytes trigger); + event NOPsSettledOffchain(address[] payees, uint256[] payments); + event Paused(address account); + event PayeesUpdated(address[] transmitters, address[] payees); + event PayeeshipTransferRequested(address indexed transmitter, address indexed from, address indexed to); + event PayeeshipTransferred(address indexed transmitter, address indexed from, address indexed to); + event PaymentWithdrawn(address indexed transmitter, uint256 indexed amount, address indexed to, address payee); + event ReorgedUpkeepReport(uint256 indexed id, bytes trigger); + event StaleUpkeepReport(uint256 indexed id, bytes trigger); + event UpkeepAdminTransferred(uint256 indexed id, address indexed from, address indexed to); + event UpkeepAdminTransferRequested(uint256 indexed id, address indexed from, address indexed to); + event UpkeepCanceled(uint256 indexed id, uint64 indexed atBlockHeight); + event UpkeepCheckDataSet(uint256 indexed id, bytes newCheckData); + event UpkeepGasLimitSet(uint256 indexed id, uint96 gasLimit); + event UpkeepMigrated(uint256 indexed id, uint256 remainingBalance, address destination); + event UpkeepOffchainConfigSet(uint256 indexed id, bytes offchainConfig); + event UpkeepPaused(uint256 indexed id); + event UpkeepPerformed( + uint256 indexed id, + bool indexed success, + uint96 totalPayment, + uint256 gasUsed, + uint256 gasOverhead, + bytes trigger + ); + event UpkeepCharged(uint256 indexed id, PaymentReceipt receipt); + event UpkeepPrivilegeConfigSet(uint256 indexed id, bytes privilegeConfig); + event UpkeepReceived(uint256 indexed id, uint256 startingBalance, address importedFrom); + event UpkeepRegistered(uint256 indexed id, uint32 performGas, address admin); + event UpkeepTriggerConfigSet(uint256 indexed id, bytes triggerConfig); + event UpkeepUnpaused(uint256 indexed id); + event Unpaused(address account); + + /** + * @param link address of the LINK Token + * @param linkUSDFeed address of the LINK/USD price feed + * @param nativeUSDFeed address of the Native/USD price feed + * @param fastGasFeed address of the Fast Gas price feed + * @param automationForwarderLogic the address of automation forwarder logic + * @param allowedReadOnlyAddress the address of the allowed read only address + * @param payoutMode the payout mode + */ + constructor( + address link, + address linkUSDFeed, + address nativeUSDFeed, + address fastGasFeed, + address automationForwarderLogic, + address allowedReadOnlyAddress, + PayoutMode payoutMode, + address wrappedNativeTokenAddress + ) ConfirmedOwner(msg.sender) { + i_link = LinkTokenInterface(link); + i_linkUSDFeed = AggregatorV3Interface(linkUSDFeed); + i_nativeUSDFeed = AggregatorV3Interface(nativeUSDFeed); + i_fastGasFeed = AggregatorV3Interface(fastGasFeed); + i_automationForwarderLogic = automationForwarderLogic; + i_allowedReadOnlyAddress = allowedReadOnlyAddress; + s_payoutMode = payoutMode; + i_wrappedNativeToken = IWrappedNative(wrappedNativeTokenAddress); + if (i_linkUSDFeed.decimals() != i_nativeUSDFeed.decimals()) { + revert InvalidFeed(); + } + } + + // ================================================================ + // | INTERNAL FUNCTIONS ONLY | + // ================================================================ + + /** + * @dev creates a new upkeep with the given fields + * @param id the id of the upkeep + * @param upkeep the upkeep to create + * @param admin address to cancel upkeep and withdraw remaining funds + * @param checkData data which is passed to user's checkUpkeep + * @param triggerConfig the trigger config for this upkeep + * @param offchainConfig the off-chain config of this upkeep + */ + function _createUpkeep( + uint256 id, + Upkeep memory upkeep, + address admin, + bytes memory checkData, + bytes memory triggerConfig, + bytes memory offchainConfig + ) internal { + if (s_hotVars.paused) revert RegistryPaused(); + if (checkData.length > s_storage.maxCheckDataSize) revert CheckDataExceedsLimit(); + if (upkeep.performGas < PERFORM_GAS_MIN || upkeep.performGas > s_storage.maxPerformGas) + revert GasLimitOutsideRange(); + if (address(s_upkeep[id].forwarder) != address(0)) revert UpkeepAlreadyExists(); + if (address(s_billingConfigs[upkeep.billingToken].priceFeed) == address(0)) revert InvalidToken(); + s_upkeep[id] = upkeep; + s_upkeepAdmin[id] = admin; + s_checkData[id] = checkData; + s_reserveAmounts[upkeep.billingToken] = s_reserveAmounts[upkeep.billingToken] + upkeep.balance; + s_upkeepTriggerConfig[id] = triggerConfig; + s_upkeepOffchainConfig[id] = offchainConfig; + s_upkeepIDs.add(id); + } + + /** + * @dev creates an ID for the upkeep based on the upkeep's type + * @dev the format of the ID looks like this: + * ****00000000000X**************** + * 4 bytes of entropy + * 11 bytes of zeros + * 1 identifying byte for the trigger type + * 16 bytes of entropy + * @dev this maintains the same level of entropy as eth addresses, so IDs will still be unique + * @dev we add the "identifying" part in the middle so that it is mostly hidden from users who usually only + * see the first 4 and last 4 hex values ex 0x1234...ABCD + */ + function _createID(Trigger triggerType) internal view returns (uint256) { + bytes1 empty; + IChainModule chainModule = s_hotVars.chainModule; + bytes memory idBytes = abi.encodePacked( + keccak256(abi.encode(chainModule.blockHash((chainModule.blockNumber() - 1)), address(this), s_storage.nonce)) + ); + for (uint256 idx = 4; idx < 15; idx++) { + idBytes[idx] = empty; + } + idBytes[15] = bytes1(uint8(triggerType)); + return uint256(bytes32(idBytes)); + } + + /** + * @dev retrieves feed data for fast gas/native and link/native prices. if the feed + * data is stale it uses the configured fallback price. Once a price is picked + * for gas it takes the min of gas price in the transaction or the fast gas + * price in order to reduce costs for the upkeep clients. + */ + function _getFeedData( + HotVars memory hotVars + ) internal view returns (uint256 gasWei, uint256 linkUSD, uint256 nativeUSD) { + uint32 stalenessSeconds = hotVars.stalenessSeconds; + bool staleFallback = stalenessSeconds > 0; + uint256 timestamp; + int256 feedValue; + (, feedValue, , timestamp, ) = i_fastGasFeed.latestRoundData(); + if ( + feedValue <= 0 || block.timestamp < timestamp || (staleFallback && stalenessSeconds < block.timestamp - timestamp) + ) { + gasWei = s_fallbackGasPrice; + } else { + gasWei = uint256(feedValue); + } + (, feedValue, , timestamp, ) = i_linkUSDFeed.latestRoundData(); + if ( + feedValue <= 0 || block.timestamp < timestamp || (staleFallback && stalenessSeconds < block.timestamp - timestamp) + ) { + linkUSD = s_fallbackLinkPrice; + } else { + linkUSD = uint256(feedValue); + } + return (gasWei, linkUSD, _getNativeUSD(hotVars)); + } + + /** + * @dev this price has it's own getter for use in the transmit() hot path + * in the future, all price data should be included in the report instead of + * getting read during execution + */ + function _getNativeUSD(HotVars memory hotVars) internal view returns (uint256) { + (, int256 feedValue, , uint256 timestamp, ) = i_nativeUSDFeed.latestRoundData(); + if ( + feedValue <= 0 || + block.timestamp < timestamp || + (hotVars.stalenessSeconds > 0 && hotVars.stalenessSeconds < block.timestamp - timestamp) + ) { + return s_fallbackNativePrice; + } else { + return uint256(feedValue); + } + } + + /** + * @dev gets the price and billing params for a specific billing token + */ + function _getBillingTokenPaymentParams( + HotVars memory hotVars, + IERC20 billingToken + ) internal view returns (BillingTokenPaymentParams memory paymentParams) { + BillingConfig storage config = s_billingConfigs[billingToken]; + paymentParams.flatFeeMilliCents = config.flatFeeMilliCents; + paymentParams.gasFeePPB = config.gasFeePPB; + paymentParams.decimals = config.decimals; + (, int256 feedValue, , uint256 timestamp, ) = config.priceFeed.latestRoundData(); + if ( + feedValue <= 0 || + block.timestamp < timestamp || + (hotVars.stalenessSeconds > 0 && hotVars.stalenessSeconds < block.timestamp - timestamp) + ) { + paymentParams.priceUSD = config.fallbackPrice; + } else { + paymentParams.priceUSD = uint256(feedValue); + } + return paymentParams; + } + + /** + * @param hotVars the hot path variables + * @param paymentParams the pricing data and gas usage data + * @return receipt the receipt of payment with pricing breakdown + * @dev use of PaymentParams struct is necessary to avoid stack too deep errors + * @dev calculates LINK paid for gas spent plus a configure premium percentage + * @dev 1 USD = 1e18 attoUSD + * @dev 1 USD = 1e26 hexaicosaUSD (had to borrow this prefix from geometry because there is no metric prefix for 1e-26) + * @dev 1 millicent = 1e-5 USD = 1e13 attoUSD + */ + function _calculatePaymentAmount( + HotVars memory hotVars, + PaymentParams memory paymentParams + ) internal view returns (PaymentReceipt memory receipt) { + uint256 decimals = paymentParams.billingTokenParams.decimals; + uint256 gasWei = paymentParams.fastGasWei * hotVars.gasCeilingMultiplier; + // in case it's actual execution use actual gas price, capped by fastGasWei * gasCeilingMultiplier + if (paymentParams.isTransaction && tx.gasprice < gasWei) { + gasWei = tx.gasprice; + } + + // scaling factor is based on decimals of billing token, and applies to premium and gasCharge + uint256 numeratorScalingFactor = decimals > 18 ? 10 ** (decimals - 18) : 1; + uint256 denominatorScalingFactor = decimals < 18 ? 10 ** (18 - decimals) : 1; + + // gas calculation + uint256 gasPaymentHexaicosaUSD = (gasWei * + (paymentParams.gasLimit + paymentParams.gasOverhead) + + paymentParams.l1CostWei) * paymentParams.nativeUSD; // gasPaymentHexaicosaUSD has an extra 8 zeros because of decimals on nativeUSD feed + // gasChargeInBillingToken is scaled by the billing token's decimals. Round up to ensure a minimum billing token is charged for gas + receipt.gasChargeInBillingToken = SafeCast.toUint96( + ((gasPaymentHexaicosaUSD * numeratorScalingFactor) + + (paymentParams.billingTokenParams.priceUSD * denominatorScalingFactor - 1)) / + (paymentParams.billingTokenParams.priceUSD * denominatorScalingFactor) + ); + // 18 decimals: 26 decimals / 8 decimals + receipt.gasReimbursementInJuels = SafeCast.toUint96(gasPaymentHexaicosaUSD / paymentParams.linkUSD); + + // premium calculation + uint256 flatFeeHexaicosaUSD = uint256(paymentParams.billingTokenParams.flatFeeMilliCents) * 1e21; // 1e13 for milliCents to attoUSD and 1e8 for attoUSD to hexaicosaUSD + uint256 premiumHexaicosaUSD = ((((gasWei * paymentParams.gasLimit) + paymentParams.l1CostWei) * + paymentParams.billingTokenParams.gasFeePPB * + paymentParams.nativeUSD) / 1e9) + flatFeeHexaicosaUSD; + // premium is scaled by the billing token's decimals. Round up to ensure at least minimum charge + receipt.premiumInBillingToken = SafeCast.toUint96( + ((premiumHexaicosaUSD * numeratorScalingFactor) + + (paymentParams.billingTokenParams.priceUSD * denominatorScalingFactor - 1)) / + (paymentParams.billingTokenParams.priceUSD * denominatorScalingFactor) + ); + receipt.premiumInJuels = SafeCast.toUint96(premiumHexaicosaUSD / paymentParams.linkUSD); + + receipt.billingToken = paymentParams.billingToken; + receipt.linkUSD = SafeCast.toUint96(paymentParams.linkUSD); + receipt.nativeUSD = SafeCast.toUint96(paymentParams.nativeUSD); + receipt.billingUSD = SafeCast.toUint96(paymentParams.billingTokenParams.priceUSD); + + return receipt; + } + + /** + * @dev calculates the max payment for an upkeep. Called during checkUpkeep simulation and assumes + * maximum gas overhead, L1 fee + */ + function _getMaxPayment( + uint256 upkeepId, + HotVars memory hotVars, + Trigger triggerType, + uint32 performGas, + uint256 fastGasWei, + uint256 linkUSD, + uint256 nativeUSD, + IERC20 billingToken + ) internal view returns (uint96) { + uint256 maxL1Fee; + uint256 maxGasOverhead; + + { + if (triggerType == Trigger.CONDITION) { + maxGasOverhead = REGISTRY_CONDITIONAL_OVERHEAD; + } else if (triggerType == Trigger.LOG) { + maxGasOverhead = REGISTRY_LOG_OVERHEAD; + } else { + revert InvalidTriggerType(); + } + uint256 maxCalldataSize = s_storage.maxPerformDataSize + + TRANSMIT_CALLDATA_FIXED_BYTES_OVERHEAD + + (TRANSMIT_CALLDATA_PER_SIGNER_BYTES_OVERHEAD * (hotVars.f + 1)); + (uint256 chainModuleFixedOverhead, uint256 chainModulePerByteOverhead) = s_hotVars.chainModule.getGasOverhead(); + maxGasOverhead += + (REGISTRY_PER_SIGNER_GAS_OVERHEAD * (hotVars.f + 1)) + + ((REGISTRY_PER_PERFORM_BYTE_GAS_OVERHEAD + chainModulePerByteOverhead) * maxCalldataSize) + + chainModuleFixedOverhead; + maxL1Fee = hotVars.gasCeilingMultiplier * hotVars.chainModule.getMaxL1Fee(maxCalldataSize); + } + + BillingTokenPaymentParams memory paymentParams = _getBillingTokenPaymentParams(hotVars, billingToken); + if (s_upkeep[upkeepId].overridesEnabled) { + BillingOverrides memory billingOverrides = s_billingOverrides[upkeepId]; + // use the overridden configs + paymentParams.gasFeePPB = billingOverrides.gasFeePPB; + paymentParams.flatFeeMilliCents = billingOverrides.flatFeeMilliCents; + } + + PaymentReceipt memory receipt = _calculatePaymentAmount( + hotVars, + PaymentParams({ + gasLimit: performGas, + gasOverhead: maxGasOverhead, + l1CostWei: maxL1Fee, + fastGasWei: fastGasWei, + linkUSD: linkUSD, + nativeUSD: nativeUSD, + billingToken: billingToken, + billingTokenParams: paymentParams, + isTransaction: false + }) + ); + + return receipt.gasChargeInBillingToken + receipt.premiumInBillingToken; + } + + /** + * @dev move a transmitter's balance from total pool to withdrawable balance + */ + function _updateTransmitterBalanceFromPool( + address transmitterAddress, + uint96 totalPremium, + uint96 payeeCount + ) internal returns (uint96) { + Transmitter memory transmitter = s_transmitters[transmitterAddress]; + + if (transmitter.active) { + uint96 uncollected = totalPremium - transmitter.lastCollected; + uint96 due = uncollected / payeeCount; + transmitter.balance += due; + transmitter.lastCollected += due * payeeCount; + s_transmitters[transmitterAddress] = transmitter; + } + + return transmitter.balance; + } + + /** + * @dev gets the trigger type from an upkeepID (trigger type is encoded in the middle of the ID) + */ + function _getTriggerType(uint256 upkeepId) internal pure returns (Trigger) { + bytes32 rawID = bytes32(upkeepId); + bytes1 empty = bytes1(0); + for (uint256 idx = 4; idx < 15; idx++) { + if (rawID[idx] != empty) { + // old IDs that were created before this standard and migrated to this registry + return Trigger.CONDITION; + } + } + return Trigger(uint8(rawID[15])); + } + + function _checkPayload( + uint256 upkeepId, + Trigger triggerType, + bytes memory triggerData + ) internal view returns (bytes memory) { + if (triggerType == Trigger.CONDITION) { + return abi.encodeWithSelector(CHECK_SELECTOR, s_checkData[upkeepId]); + } else if (triggerType == Trigger.LOG) { + Log memory log = abi.decode(triggerData, (Log)); + return abi.encodeWithSelector(CHECK_LOG_SELECTOR, log, s_checkData[upkeepId]); + } + revert InvalidTriggerType(); + } + + /** + * @dev _decodeReport decodes a serialized report into a Report struct + */ + function _decodeReport(bytes calldata rawReport) internal pure returns (Report memory) { + Report memory report = abi.decode(rawReport, (Report)); + uint256 expectedLength = report.upkeepIds.length; + if ( + report.gasLimits.length != expectedLength || + report.triggers.length != expectedLength || + report.performDatas.length != expectedLength + ) { + revert InvalidReport(); + } + return report; + } + + /** + * @dev Does some early sanity checks before actually performing an upkeep + * @return bool whether the upkeep should be performed + * @return bytes32 dedupID for preventing duplicate performances of this trigger + */ + function _prePerformChecks( + uint256 upkeepId, + uint256 blocknumber, + bytes memory rawTrigger, + UpkeepTransmitInfo memory transmitInfo, + HotVars memory hotVars + ) internal returns (bool, bytes32) { + bytes32 dedupID; + if (transmitInfo.triggerType == Trigger.CONDITION) { + if (!_validateConditionalTrigger(upkeepId, blocknumber, rawTrigger, transmitInfo, hotVars)) + return (false, dedupID); + } else if (transmitInfo.triggerType == Trigger.LOG) { + bool valid; + (valid, dedupID) = _validateLogTrigger(upkeepId, blocknumber, rawTrigger, hotVars); + if (!valid) return (false, dedupID); + } else { + revert InvalidTriggerType(); + } + if (transmitInfo.upkeep.maxValidBlocknumber <= blocknumber) { + // Can happen when an upkeep got cancelled after report was generated. + // However we have a CANCELLATION_DELAY of 50 blocks so shouldn't happen in practice + emit CancelledUpkeepReport(upkeepId, rawTrigger); + return (false, dedupID); + } + return (true, dedupID); + } + + /** + * @dev Does some early sanity checks before actually performing an upkeep + */ + function _validateConditionalTrigger( + uint256 upkeepId, + uint256 blocknumber, + bytes memory rawTrigger, + UpkeepTransmitInfo memory transmitInfo, + HotVars memory hotVars + ) internal returns (bool) { + ConditionalTrigger memory trigger = abi.decode(rawTrigger, (ConditionalTrigger)); + if (trigger.blockNum < transmitInfo.upkeep.lastPerformedBlockNumber) { + // Can happen when another report performed this upkeep after this report was generated + emit StaleUpkeepReport(upkeepId, rawTrigger); + return false; + } + if ( + (hotVars.reorgProtectionEnabled && + (trigger.blockHash != bytes32("") && hotVars.chainModule.blockHash(trigger.blockNum) != trigger.blockHash)) || + trigger.blockNum >= blocknumber + ) { + // There are two cases of reorged report + // 1. trigger block number is in future: this is an edge case during extreme deep reorgs of chain + // which is always protected against + // 2. blockHash at trigger block number was same as trigger time. This is an optional check which is + // applied if DON sends non empty trigger.blockHash. Note: It only works for last 256 blocks on chain + // when it is sent + emit ReorgedUpkeepReport(upkeepId, rawTrigger); + return false; + } + return true; + } + + function _validateLogTrigger( + uint256 upkeepId, + uint256 blocknumber, + bytes memory rawTrigger, + HotVars memory hotVars + ) internal returns (bool, bytes32) { + LogTrigger memory trigger = abi.decode(rawTrigger, (LogTrigger)); + bytes32 dedupID = keccak256(abi.encodePacked(upkeepId, trigger.logBlockHash, trigger.txHash, trigger.logIndex)); + if ( + (hotVars.reorgProtectionEnabled && + (trigger.blockHash != bytes32("") && hotVars.chainModule.blockHash(trigger.blockNum) != trigger.blockHash)) || + trigger.blockNum >= blocknumber + ) { + // Reorg protection is same as conditional trigger upkeeps + emit ReorgedUpkeepReport(upkeepId, rawTrigger); + return (false, dedupID); + } + if (s_dedupKeys[dedupID]) { + emit StaleUpkeepReport(upkeepId, rawTrigger); + return (false, dedupID); + } + return (true, dedupID); + } + + /** + * @dev Verify signatures attached to report + */ + function _verifyReportSignature( + bytes32[3] calldata reportContext, + bytes calldata report, + bytes32[] calldata rs, + bytes32[] calldata ss, + bytes32 rawVs + ) internal view { + bytes32 h = keccak256(abi.encode(keccak256(report), reportContext)); + // i-th byte counts number of sigs made by i-th signer + uint256 signedCount = 0; + + Signer memory signer; + address signerAddress; + for (uint256 i = 0; i < rs.length; i++) { + signerAddress = ecrecover(h, uint8(rawVs[i]) + 27, rs[i], ss[i]); + signer = s_signers[signerAddress]; + if (!signer.active) revert OnlyActiveSigners(); + unchecked { + signedCount += 1 << (8 * signer.index); + } + } + + if (signedCount & ORACLE_MASK != signedCount) revert DuplicateSigners(); + } + + /** + * @dev updates a storage marker for this upkeep to prevent duplicate and out of order performances + * @dev for conditional triggers we set the latest block number, for log triggers we store a dedupID + */ + function _updateTriggerMarker( + uint256 upkeepID, + uint256 blocknumber, + UpkeepTransmitInfo memory upkeepTransmitInfo + ) internal { + if (upkeepTransmitInfo.triggerType == Trigger.CONDITION) { + s_upkeep[upkeepID].lastPerformedBlockNumber = uint32(blocknumber); + } else if (upkeepTransmitInfo.triggerType == Trigger.LOG) { + s_dedupKeys[upkeepTransmitInfo.dedupID] = true; + emit DedupKeyAdded(upkeepTransmitInfo.dedupID); + } + } + + /** + * @dev calls the Upkeep target with the performData param passed in by the + * transmitter and the exact gas required by the Upkeep + */ + function _performUpkeep( + IAutomationForwarder forwarder, + uint256 performGas, + bytes memory performData + ) internal nonReentrant returns (bool success, uint256 gasUsed) { + performData = abi.encodeWithSelector(PERFORM_SELECTOR, performData); + return forwarder.forward(performGas, performData); + } + + /** + * @dev handles the payment processing after an upkeep has been performed. + * Deducts an upkeep's balance and increases the amount spent. + */ + function _handlePayment( + HotVars memory hotVars, + PaymentParams memory paymentParams, + uint256 upkeepId, + Upkeep memory upkeep + ) internal returns (PaymentReceipt memory) { + if (upkeep.overridesEnabled) { + BillingOverrides memory billingOverrides = s_billingOverrides[upkeepId]; + // use the overridden configs + paymentParams.billingTokenParams.gasFeePPB = billingOverrides.gasFeePPB; + paymentParams.billingTokenParams.flatFeeMilliCents = billingOverrides.flatFeeMilliCents; + } + + PaymentReceipt memory receipt = _calculatePaymentAmount(hotVars, paymentParams); + + // balance is in the token's native decimals + uint96 balance = upkeep.balance; + // payment is in the token's native decimals + uint96 payment = receipt.gasChargeInBillingToken + receipt.premiumInBillingToken; + + // scaling factors to adjust decimals between billing token and LINK + uint256 decimals = paymentParams.billingTokenParams.decimals; + uint256 scalingFactor1 = decimals < 18 ? 10 ** (18 - decimals) : 1; + uint256 scalingFactor2 = decimals > 18 ? 10 ** (decimals - 18) : 1; + + // this shouldn't happen, but in rare edge cases, we charge the full balance in case the user + // can't cover the amount owed + if (balance < receipt.gasChargeInBillingToken) { + // if the user can't cover the gas fee, then direct all of the payment to the transmitter and distribute no premium to the DON + payment = balance; + receipt.gasReimbursementInJuels = SafeCast.toUint96( + (balance * paymentParams.billingTokenParams.priceUSD * scalingFactor1) / + (paymentParams.linkUSD * scalingFactor2) + ); + receipt.premiumInJuels = 0; + receipt.premiumInBillingToken = 0; + receipt.gasChargeInBillingToken = balance; + } else if (balance < payment) { + // if the user can cover the gas fee, but not the premium, then reduce the premium + payment = balance; + receipt.premiumInJuels = SafeCast.toUint96( + ((balance * paymentParams.billingTokenParams.priceUSD * scalingFactor1) / + (paymentParams.linkUSD * scalingFactor2)) - receipt.gasReimbursementInJuels + ); + // round up + receipt.premiumInBillingToken = SafeCast.toUint96( + ((receipt.premiumInJuels * paymentParams.linkUSD * scalingFactor2) + + (paymentParams.billingTokenParams.priceUSD * scalingFactor1 - 1)) / + (paymentParams.billingTokenParams.priceUSD * scalingFactor1) + ); + } + + s_upkeep[upkeepId].balance -= payment; + s_upkeep[upkeepId].amountSpent += payment; + s_reserveAmounts[paymentParams.billingToken] -= payment; + + emit UpkeepCharged(upkeepId, receipt); + return receipt; + } + + /** + * @dev ensures the upkeep is not cancelled and the caller is the upkeep admin + */ + function _requireAdminAndNotCancelled(uint256 upkeepId) internal view { + if (msg.sender != s_upkeepAdmin[upkeepId]) revert OnlyCallableByAdmin(); + if (s_upkeep[upkeepId].maxValidBlocknumber != UINT32_MAX) revert UpkeepCancelled(); + } + + /** + * @dev replicates Open Zeppelin's ReentrancyGuard but optimized to fit our storage + */ + modifier nonReentrant() { + if (s_hotVars.reentrancyGuard) revert ReentrantCall(); + s_hotVars.reentrancyGuard = true; + _; + s_hotVars.reentrancyGuard = false; + } + + /** + * @notice only allows a pre-configured address to initiate offchain read + */ + function _preventExecution() internal view { + // solhint-disable-next-line avoid-tx-origin + if (tx.origin != i_allowedReadOnlyAddress) { + revert OnlySimulatedBackend(); + } + } + + /** + * @notice only allows finance admin to call the function + */ + function _onlyFinanceAdminAllowed() internal view { + if (msg.sender != s_storage.financeAdmin) { + revert OnlyFinanceAdmin(); + } + } + + /** + * @notice only allows privilege manager to call the function + */ + function _onlyPrivilegeManagerAllowed() internal view { + if (msg.sender != s_storage.upkeepPrivilegeManager) { + revert OnlyCallableByUpkeepPrivilegeManager(); + } + } + + /** + * @notice sets billing configuration for a token + * @param billingTokens the addresses of tokens + * @param billingConfigs the configs for tokens + */ + function _setBillingConfig(IERC20[] memory billingTokens, BillingConfig[] memory billingConfigs) internal { + // Clear existing data + for (uint256 i = 0; i < s_billingTokens.length; i++) { + delete s_billingConfigs[s_billingTokens[i]]; + } + delete s_billingTokens; + + PayoutMode mode = s_payoutMode; + for (uint256 i = 0; i < billingTokens.length; i++) { + IERC20 token = billingTokens[i]; + BillingConfig memory config = billingConfigs[i]; + + // most ERC20 tokens are 18 decimals, priceFeed must be 8 decimals + if (config.decimals != token.decimals() || config.priceFeed.decimals() != 8) { + revert InvalidToken(); + } + + // if LINK is a billing option, payout mode must be ON_CHAIN + if (address(token) == address(i_link) && mode == PayoutMode.OFF_CHAIN) { + revert InvalidToken(); + } + if (address(token) == ZERO_ADDRESS || address(config.priceFeed) == ZERO_ADDRESS) { + revert ZeroAddressNotAllowed(); + } + + // if this is a new token, add it to tokens list. Otherwise revert + if (address(s_billingConfigs[token].priceFeed) != ZERO_ADDRESS) { + revert DuplicateEntry(); + } + s_billingTokens.push(token); + + // update the billing config for an existing token or add a new one + s_billingConfigs[token] = config; + + emit BillingConfigSet(token, config); + } + } + + /** + * @notice updates the signers and transmitters lists + */ + function _updateTransmitters(address[] memory signers, address[] memory transmitters) internal { + uint96 transmittersListLength = uint96(s_transmittersList.length); + uint96 totalPremium = s_hotVars.totalPremium; + + // move all pooled payments out of the pool to each transmitter's balance + for (uint256 i = 0; i < s_transmittersList.length; i++) { + _updateTransmitterBalanceFromPool(s_transmittersList[i], totalPremium, transmittersListLength); + } + + // remove any old signer/transmitter addresses + address transmitterAddress; + PayoutMode mode = s_payoutMode; + for (uint256 i = 0; i < s_transmittersList.length; i++) { + transmitterAddress = s_transmittersList[i]; + delete s_signers[s_signersList[i]]; + // Do not delete the whole transmitter struct as it has balance information stored + s_transmitters[transmitterAddress].active = false; + if (mode == PayoutMode.OFF_CHAIN && s_transmitters[transmitterAddress].balance > 0) { + s_deactivatedTransmitters.add(transmitterAddress); + } + } + delete s_signersList; + delete s_transmittersList; + + // add new signer/transmitter addresses + Transmitter memory transmitter; + for (uint256 i = 0; i < signers.length; i++) { + if (s_signers[signers[i]].active) revert RepeatedSigner(); + if (signers[i] == ZERO_ADDRESS) revert InvalidSigner(); + s_signers[signers[i]] = Signer({active: true, index: uint8(i)}); + + transmitterAddress = transmitters[i]; + if (transmitterAddress == ZERO_ADDRESS) revert InvalidTransmitter(); + transmitter = s_transmitters[transmitterAddress]; + if (transmitter.active) revert RepeatedTransmitter(); + transmitter.active = true; + transmitter.index = uint8(i); + // new transmitters start afresh from current totalPremium + // some spare change of premium from previous pool will be forfeited + transmitter.lastCollected = s_hotVars.totalPremium; + s_transmitters[transmitterAddress] = transmitter; + if (mode == PayoutMode.OFF_CHAIN) { + s_deactivatedTransmitters.remove(transmitterAddress); + } + } + + s_signersList = signers; + s_transmittersList = transmitters; + } + + /** + * @notice returns the size of the LINK liquidity pool + # @dev LINK max supply < 2^96, so casting to int256 is safe + */ + function _linkAvailableForPayment() internal view returns (int256) { + return int256(i_link.balanceOf(address(this))) - int256(s_reserveAmounts[IERC20(address(i_link))]); + } +} diff --git a/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryLogicA2_3.sol b/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryLogicA2_3.sol new file mode 100644 index 00000000000..64d697c70f9 --- /dev/null +++ b/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryLogicA2_3.sol @@ -0,0 +1,283 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.19; + +import {EnumerableSet} from "../../vendor/openzeppelin-solidity/v4.7.3/contracts/utils/structs/EnumerableSet.sol"; +import {Address} from "../../vendor/openzeppelin-solidity/v4.7.3/contracts/utils/Address.sol"; +import {ZKSyncAutomationRegistryBase2_3} from "./ZKSyncAutomationRegistryBase2_3.sol"; +import {ZKSyncAutomationRegistryLogicC2_3} from "./ZKSyncAutomationRegistryLogicC2_3.sol"; +import {ZKSyncAutomationRegistryLogicB2_3} from "./ZKSyncAutomationRegistryLogicB2_3.sol"; +import {Chainable} from "../Chainable.sol"; +import {ZKSyncAutomationForwarder} from "../ZKSyncAutomationForwarder.sol"; +import {IAutomationForwarder} from "../interfaces/IAutomationForwarder.sol"; +import {UpkeepTranscoderInterfaceV2} from "../interfaces/UpkeepTranscoderInterfaceV2.sol"; +import {MigratableKeeperRegistryInterfaceV2} from "../interfaces/MigratableKeeperRegistryInterfaceV2.sol"; +import {IERC20Metadata as IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {SafeERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC677Receiver} from "../../shared/interfaces/IERC677Receiver.sol"; + +/** + * @notice Logic contract, works in tandem with AutomationRegistry as a proxy + */ +contract ZKSyncAutomationRegistryLogicA2_3 is ZKSyncAutomationRegistryBase2_3, Chainable, IERC677Receiver { + using Address for address; + using EnumerableSet for EnumerableSet.UintSet; + using EnumerableSet for EnumerableSet.AddressSet; + using SafeERC20 for IERC20; + + /** + * @param logicB the address of the second logic contract + * @dev we cast the contract to logicC in order to call logicC functions (via fallback) + */ + constructor( + ZKSyncAutomationRegistryLogicB2_3 logicB + ) + ZKSyncAutomationRegistryBase2_3( + ZKSyncAutomationRegistryLogicC2_3(address(logicB)).getLinkAddress(), + ZKSyncAutomationRegistryLogicC2_3(address(logicB)).getLinkUSDFeedAddress(), + ZKSyncAutomationRegistryLogicC2_3(address(logicB)).getNativeUSDFeedAddress(), + ZKSyncAutomationRegistryLogicC2_3(address(logicB)).getFastGasFeedAddress(), + ZKSyncAutomationRegistryLogicC2_3(address(logicB)).getAutomationForwarderLogic(), + ZKSyncAutomationRegistryLogicC2_3(address(logicB)).getAllowedReadOnlyAddress(), + ZKSyncAutomationRegistryLogicC2_3(address(logicB)).getPayoutMode(), + ZKSyncAutomationRegistryLogicC2_3(address(logicB)).getWrappedNativeTokenAddress() + ) + Chainable(address(logicB)) + {} + + /** + * @notice uses LINK's transferAndCall to LINK and add funding to an upkeep + * @dev safe to cast uint256 to uint96 as total LINK supply is under UINT96MAX + * @param sender the account which transferred the funds + * @param amount number of LINK transfer + */ + function onTokenTransfer(address sender, uint256 amount, bytes calldata data) external override { + if (msg.sender != address(i_link)) revert OnlyCallableByLINKToken(); + if (data.length != 32) revert InvalidDataLength(); + uint256 id = abi.decode(data, (uint256)); + if (s_upkeep[id].maxValidBlocknumber != UINT32_MAX) revert UpkeepCancelled(); + if (address(s_upkeep[id].billingToken) != address(i_link)) revert InvalidToken(); + s_upkeep[id].balance = s_upkeep[id].balance + uint96(amount); + s_reserveAmounts[IERC20(address(i_link))] = s_reserveAmounts[IERC20(address(i_link))] + amount; + emit FundsAdded(id, sender, uint96(amount)); + } + + // ================================================================ + // | UPKEEP MANAGEMENT | + // ================================================================ + + /** + * @notice adds a new upkeep + * @param target address to perform upkeep on + * @param gasLimit amount of gas to provide the target contract when + * performing upkeep + * @param admin address to cancel upkeep and withdraw remaining funds + * @param triggerType the trigger for the upkeep + * @param billingToken the billing token for the upkeep + * @param checkData data passed to the contract when checking for upkeep + * @param triggerConfig the config for the trigger + * @param offchainConfig arbitrary offchain config for the upkeep + */ + function registerUpkeep( + address target, + uint32 gasLimit, + address admin, + Trigger triggerType, + IERC20 billingToken, + bytes calldata checkData, + bytes memory triggerConfig, + bytes memory offchainConfig + ) public returns (uint256 id) { + if (msg.sender != owner() && !s_registrars.contains(msg.sender)) revert OnlyCallableByOwnerOrRegistrar(); + if (!target.isContract()) revert NotAContract(); + id = _createID(triggerType); + IAutomationForwarder forwarder = IAutomationForwarder( + address(new ZKSyncAutomationForwarder(target, address(this), i_automationForwarderLogic)) + ); + _createUpkeep( + id, + Upkeep({ + overridesEnabled: false, + performGas: gasLimit, + balance: 0, + maxValidBlocknumber: UINT32_MAX, + lastPerformedBlockNumber: 0, + amountSpent: 0, + paused: false, + forwarder: forwarder, + billingToken: billingToken + }), + admin, + checkData, + triggerConfig, + offchainConfig + ); + s_storage.nonce++; + emit UpkeepRegistered(id, gasLimit, admin); + emit UpkeepCheckDataSet(id, checkData); + emit UpkeepTriggerConfigSet(id, triggerConfig); + emit UpkeepOffchainConfigSet(id, offchainConfig); + return (id); + } + + /** + * @notice cancels an upkeep + * @param id the upkeepID to cancel + * @dev if a user cancels an upkeep, their funds are locked for CANCELLATION_DELAY blocks to + * allow any pending performUpkeep txs time to get confirmed + */ + function cancelUpkeep(uint256 id) external { + Upkeep memory upkeep = s_upkeep[id]; + bool isOwner = msg.sender == owner(); + uint96 minSpend = s_billingConfigs[upkeep.billingToken].minSpend; + + uint256 height = s_hotVars.chainModule.blockNumber(); + if (upkeep.maxValidBlocknumber == 0) revert CannotCancel(); + if (upkeep.maxValidBlocknumber != UINT32_MAX) revert UpkeepCancelled(); + if (!isOwner && msg.sender != s_upkeepAdmin[id]) revert OnlyCallableByOwnerOrAdmin(); + + if (!isOwner) { + height = height + CANCELLATION_DELAY; + } + s_upkeep[id].maxValidBlocknumber = uint32(height); + s_upkeepIDs.remove(id); + + // charge the cancellation fee if the minSpend is not met + uint96 cancellationFee = 0; + // cancellationFee is min(max(minSpend - amountSpent, 0), amountLeft) + if (upkeep.amountSpent < minSpend) { + cancellationFee = minSpend - uint96(upkeep.amountSpent); + if (cancellationFee > upkeep.balance) { + cancellationFee = upkeep.balance; + } + } + s_upkeep[id].balance = upkeep.balance - cancellationFee; + s_reserveAmounts[upkeep.billingToken] = s_reserveAmounts[upkeep.billingToken] - cancellationFee; + + emit UpkeepCanceled(id, uint64(height)); + } + + /** + * @notice migrates upkeeps from one registry to another. + * @param ids the upkeepIDs to migrate + * @param destination the destination registry address + * @dev a transcoder must be set in order to enable migration + * @dev migration permissions must be set on *both* sending and receiving registries + * @dev only an upkeep admin can migrate their upkeeps + * @dev this function is most gas-efficient if upkeepIDs are sorted by billing token + * @dev s_billingOverrides and s_upkeepPrivilegeConfig are not migrated in this function + */ + function migrateUpkeeps(uint256[] calldata ids, address destination) external { + if ( + s_peerRegistryMigrationPermission[destination] != MigrationPermission.OUTGOING && + s_peerRegistryMigrationPermission[destination] != MigrationPermission.BIDIRECTIONAL + ) revert MigrationNotPermitted(); + if (s_storage.transcoder == ZERO_ADDRESS) revert TranscoderNotSet(); + if (ids.length == 0) revert ArrayHasNoEntries(); + + IERC20 billingToken; + uint256 balanceToTransfer; + uint256 id; + Upkeep memory upkeep; + address[] memory admins = new address[](ids.length); + Upkeep[] memory upkeeps = new Upkeep[](ids.length); + bytes[] memory checkDatas = new bytes[](ids.length); + bytes[] memory triggerConfigs = new bytes[](ids.length); + bytes[] memory offchainConfigs = new bytes[](ids.length); + + for (uint256 idx = 0; idx < ids.length; idx++) { + id = ids[idx]; + upkeep = s_upkeep[id]; + + if (idx == 0) { + billingToken = upkeep.billingToken; + balanceToTransfer = upkeep.balance; + } + + // if we encounter a new billing token, send the sum from the last billing token to the destination registry + if (upkeep.billingToken != billingToken) { + s_reserveAmounts[billingToken] = s_reserveAmounts[billingToken] - balanceToTransfer; + billingToken.safeTransfer(destination, balanceToTransfer); + billingToken = upkeep.billingToken; + balanceToTransfer = upkeep.balance; + } else if (idx != 0) { + balanceToTransfer += upkeep.balance; + } + + _requireAdminAndNotCancelled(id); + upkeep.forwarder.updateRegistry(destination); + + upkeeps[idx] = upkeep; + admins[idx] = s_upkeepAdmin[id]; + checkDatas[idx] = s_checkData[id]; + triggerConfigs[idx] = s_upkeepTriggerConfig[id]; + offchainConfigs[idx] = s_upkeepOffchainConfig[id]; + delete s_upkeep[id]; + delete s_checkData[id]; + delete s_upkeepTriggerConfig[id]; + delete s_upkeepOffchainConfig[id]; + // nullify existing proposed admin change if an upkeep is being migrated + delete s_proposedAdmin[id]; + delete s_upkeepAdmin[id]; + s_upkeepIDs.remove(id); + emit UpkeepMigrated(id, upkeep.balance, destination); + } + // always transfer the rolling sum in the end + s_reserveAmounts[billingToken] = s_reserveAmounts[billingToken] - balanceToTransfer; + billingToken.safeTransfer(destination, balanceToTransfer); + + bytes memory encodedUpkeeps = abi.encode( + ids, + upkeeps, + new address[](ids.length), + admins, + checkDatas, + triggerConfigs, + offchainConfigs + ); + MigratableKeeperRegistryInterfaceV2(destination).receiveUpkeeps( + UpkeepTranscoderInterfaceV2(s_storage.transcoder).transcodeUpkeeps( + UPKEEP_VERSION_BASE, + MigratableKeeperRegistryInterfaceV2(destination).upkeepVersion(), + encodedUpkeeps + ) + ); + } + + /** + * @notice received upkeeps migrated from another registry + * @param encodedUpkeeps the raw upkeep data to import + * @dev this function is never called directly, it is only called by another registry's migrate function + * @dev s_billingOverrides and s_upkeepPrivilegeConfig are not handled in this function + */ + function receiveUpkeeps(bytes calldata encodedUpkeeps) external { + if ( + s_peerRegistryMigrationPermission[msg.sender] != MigrationPermission.INCOMING && + s_peerRegistryMigrationPermission[msg.sender] != MigrationPermission.BIDIRECTIONAL + ) revert MigrationNotPermitted(); + ( + uint256[] memory ids, + Upkeep[] memory upkeeps, + address[] memory targets, + address[] memory upkeepAdmins, + bytes[] memory checkDatas, + bytes[] memory triggerConfigs, + bytes[] memory offchainConfigs + ) = abi.decode(encodedUpkeeps, (uint256[], Upkeep[], address[], address[], bytes[], bytes[], bytes[])); + for (uint256 idx = 0; idx < ids.length; idx++) { + if (address(upkeeps[idx].forwarder) == ZERO_ADDRESS) { + upkeeps[idx].forwarder = IAutomationForwarder( + address(new ZKSyncAutomationForwarder(targets[idx], address(this), i_automationForwarderLogic)) + ); + } + _createUpkeep( + ids[idx], + upkeeps[idx], + upkeepAdmins[idx], + checkDatas[idx], + triggerConfigs[idx], + offchainConfigs[idx] + ); + emit UpkeepReceived(ids[idx], upkeeps[idx].balance, msg.sender); + } + } +} diff --git a/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryLogicB2_3.sol b/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryLogicB2_3.sol new file mode 100644 index 00000000000..55af99fde87 --- /dev/null +++ b/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryLogicB2_3.sol @@ -0,0 +1,449 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.19; + +import {ZKSyncAutomationRegistryBase2_3} from "./ZKSyncAutomationRegistryBase2_3.sol"; +import {EnumerableSet} from "../../vendor/openzeppelin-solidity/v4.7.3/contracts/utils/structs/EnumerableSet.sol"; +import {Address} from "../../vendor/openzeppelin-solidity/v4.7.3/contracts/utils/Address.sol"; +import {ZKSyncAutomationRegistryLogicC2_3} from "./ZKSyncAutomationRegistryLogicC2_3.sol"; +import {Chainable} from "../Chainable.sol"; +import {IERC20Metadata as IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {SafeERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; +import {SafeCast} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/math/SafeCast.sol"; + +contract ZKSyncAutomationRegistryLogicB2_3 is ZKSyncAutomationRegistryBase2_3, Chainable { + using Address for address; + using EnumerableSet for EnumerableSet.UintSet; + using EnumerableSet for EnumerableSet.AddressSet; + using SafeERC20 for IERC20; + + /** + * @param logicC the address of the third logic contract + */ + constructor( + ZKSyncAutomationRegistryLogicC2_3 logicC + ) + ZKSyncAutomationRegistryBase2_3( + logicC.getLinkAddress(), + logicC.getLinkUSDFeedAddress(), + logicC.getNativeUSDFeedAddress(), + logicC.getFastGasFeedAddress(), + logicC.getAutomationForwarderLogic(), + logicC.getAllowedReadOnlyAddress(), + logicC.getPayoutMode(), + logicC.getWrappedNativeTokenAddress() + ) + Chainable(address(logicC)) + {} + + // ================================================================ + // | PIPELINE FUNCTIONS | + // ================================================================ + + /** + * @notice called by the automation DON to check if work is needed + * @param id the upkeep ID to check for work needed + * @param triggerData extra contextual data about the trigger (not used in all code paths) + * @dev this one of the core functions called in the hot path + * @dev there is a 2nd checkUpkeep function (below) that is being maintained for backwards compatibility + * @dev there is an incongruency on what gets returned during failure modes + * ex sometimes we include price data, sometimes we omit it depending on the failure + */ + function checkUpkeep( + uint256 id, + bytes memory triggerData + ) + public + returns ( + bool upkeepNeeded, + bytes memory performData, + UpkeepFailureReason upkeepFailureReason, + uint256 gasUsed, + uint256 gasLimit, + uint256 fastGasWei, + uint256 linkUSD + ) + { + _preventExecution(); + + Trigger triggerType = _getTriggerType(id); + HotVars memory hotVars = s_hotVars; + Upkeep memory upkeep = s_upkeep[id]; + + { + uint256 nativeUSD; + uint96 maxPayment; + if (hotVars.paused) return (false, bytes(""), UpkeepFailureReason.REGISTRY_PAUSED, 0, upkeep.performGas, 0, 0); + if (upkeep.maxValidBlocknumber != UINT32_MAX) + return (false, bytes(""), UpkeepFailureReason.UPKEEP_CANCELLED, 0, upkeep.performGas, 0, 0); + if (upkeep.paused) return (false, bytes(""), UpkeepFailureReason.UPKEEP_PAUSED, 0, upkeep.performGas, 0, 0); + (fastGasWei, linkUSD, nativeUSD) = _getFeedData(hotVars); + maxPayment = _getMaxPayment( + id, + hotVars, + triggerType, + upkeep.performGas, + fastGasWei, + linkUSD, + nativeUSD, + upkeep.billingToken + ); + if (upkeep.balance < maxPayment) { + return (false, bytes(""), UpkeepFailureReason.INSUFFICIENT_BALANCE, 0, upkeep.performGas, 0, 0); + } + } + + bytes memory callData = _checkPayload(id, triggerType, triggerData); + + gasUsed = gasleft(); + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory result) = upkeep.forwarder.getTarget().call{gas: s_storage.checkGasLimit}(callData); + gasUsed = gasUsed - gasleft(); + + if (!success) { + // User's target check reverted. We capture the revert data here and pass it within performData + if (result.length > s_storage.maxRevertDataSize) { + return ( + false, + bytes(""), + UpkeepFailureReason.REVERT_DATA_EXCEEDS_LIMIT, + gasUsed, + upkeep.performGas, + fastGasWei, + linkUSD + ); + } + return ( + upkeepNeeded, + result, + UpkeepFailureReason.TARGET_CHECK_REVERTED, + gasUsed, + upkeep.performGas, + fastGasWei, + linkUSD + ); + } + + (upkeepNeeded, performData) = abi.decode(result, (bool, bytes)); + if (!upkeepNeeded) + return (false, bytes(""), UpkeepFailureReason.UPKEEP_NOT_NEEDED, gasUsed, upkeep.performGas, fastGasWei, linkUSD); + + if (performData.length > s_storage.maxPerformDataSize) + return ( + false, + bytes(""), + UpkeepFailureReason.PERFORM_DATA_EXCEEDS_LIMIT, + gasUsed, + upkeep.performGas, + fastGasWei, + linkUSD + ); + + return (upkeepNeeded, performData, upkeepFailureReason, gasUsed, upkeep.performGas, fastGasWei, linkUSD); + } + + /** + * @notice see other checkUpkeep function for description + * @dev this function may be deprecated in a future version of chainlink automation + */ + function checkUpkeep( + uint256 id + ) + external + returns ( + bool upkeepNeeded, + bytes memory performData, + UpkeepFailureReason upkeepFailureReason, + uint256 gasUsed, + uint256 gasLimit, + uint256 fastGasWei, + uint256 linkUSD + ) + { + return checkUpkeep(id, bytes("")); + } + + /** + * @dev checkCallback is used specifically for automation data streams lookups (see StreamsLookupCompatibleInterface.sol) + * @param id the upkeepID to execute a callback for + * @param values the values returned from the data streams lookup + * @param extraData the user-provided extra context data + */ + function checkCallback( + uint256 id, + bytes[] memory values, + bytes calldata extraData + ) + external + returns (bool upkeepNeeded, bytes memory performData, UpkeepFailureReason upkeepFailureReason, uint256 gasUsed) + { + bytes memory payload = abi.encodeWithSelector(CHECK_CALLBACK_SELECTOR, values, extraData); + return executeCallback(id, payload); + } + + /** + * @notice this is a generic callback executor that forwards a call to a user's contract with the configured + * gas limit + * @param id the upkeepID to execute a callback for + * @param payload the data (including function selector) to call on the upkeep target contract + */ + function executeCallback( + uint256 id, + bytes memory payload + ) + public + returns (bool upkeepNeeded, bytes memory performData, UpkeepFailureReason upkeepFailureReason, uint256 gasUsed) + { + _preventExecution(); + + Upkeep memory upkeep = s_upkeep[id]; + gasUsed = gasleft(); + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory result) = upkeep.forwarder.getTarget().call{gas: s_storage.checkGasLimit}(payload); + gasUsed = gasUsed - gasleft(); + if (!success) { + return (false, bytes(""), UpkeepFailureReason.CALLBACK_REVERTED, gasUsed); + } + (upkeepNeeded, performData) = abi.decode(result, (bool, bytes)); + if (!upkeepNeeded) { + return (false, bytes(""), UpkeepFailureReason.UPKEEP_NOT_NEEDED, gasUsed); + } + if (performData.length > s_storage.maxPerformDataSize) { + return (false, bytes(""), UpkeepFailureReason.PERFORM_DATA_EXCEEDS_LIMIT, gasUsed); + } + return (upkeepNeeded, performData, upkeepFailureReason, gasUsed); + } + + /** + * @notice simulates the upkeep with the perform data returned from checkUpkeep + * @param id identifier of the upkeep to execute the data with. + * @param performData calldata parameter to be passed to the target upkeep. + * @return success whether the call reverted or not + * @return gasUsed the amount of gas the target contract consumed + */ + function simulatePerformUpkeep( + uint256 id, + bytes calldata performData + ) external returns (bool success, uint256 gasUsed) { + _preventExecution(); + + if (s_hotVars.paused) revert RegistryPaused(); + Upkeep memory upkeep = s_upkeep[id]; + (success, gasUsed) = _performUpkeep(upkeep.forwarder, upkeep.performGas, performData); + return (success, gasUsed); + } + + // ================================================================ + // | UPKEEP MANAGEMENT | + // ================================================================ + + /** + * @notice adds fund to an upkeep + * @param id the upkeepID + * @param amount the amount of funds to add, in the upkeep's billing token + */ + function addFunds(uint256 id, uint96 amount) external payable { + Upkeep memory upkeep = s_upkeep[id]; + if (upkeep.maxValidBlocknumber != UINT32_MAX) revert UpkeepCancelled(); + + if (msg.value != 0) { + if (upkeep.billingToken != IERC20(i_wrappedNativeToken)) { + revert InvalidToken(); + } + amount = SafeCast.toUint96(msg.value); + } + + s_upkeep[id].balance = upkeep.balance + amount; + s_reserveAmounts[upkeep.billingToken] = s_reserveAmounts[upkeep.billingToken] + amount; + + if (msg.value == 0) { + // ERC20 payment + upkeep.billingToken.safeTransferFrom(msg.sender, address(this), amount); + } else { + // native payment + i_wrappedNativeToken.deposit{value: amount}(); + } + + emit FundsAdded(id, msg.sender, amount); + } + + /** + * @notice overrides the billing config for an upkeep + * @param id the upkeepID + * @param billingOverrides the override-able billing config + */ + function setBillingOverrides(uint256 id, BillingOverrides calldata billingOverrides) external { + _onlyPrivilegeManagerAllowed(); + if (s_upkeep[id].maxValidBlocknumber != UINT32_MAX) revert UpkeepCancelled(); + + s_upkeep[id].overridesEnabled = true; + s_billingOverrides[id] = billingOverrides; + emit BillingConfigOverridden(id, billingOverrides); + } + + /** + * @notice remove the overridden billing config for an upkeep + * @param id the upkeepID + */ + function removeBillingOverrides(uint256 id) external { + _onlyPrivilegeManagerAllowed(); + + s_upkeep[id].overridesEnabled = false; + delete s_billingOverrides[id]; + emit BillingConfigOverrideRemoved(id); + } + + /** + * @notice transfers the address of an admin for an upkeep + */ + function transferUpkeepAdmin(uint256 id, address proposed) external { + _requireAdminAndNotCancelled(id); + if (proposed == msg.sender) revert ValueNotChanged(); + + if (s_proposedAdmin[id] != proposed) { + s_proposedAdmin[id] = proposed; + emit UpkeepAdminTransferRequested(id, msg.sender, proposed); + } + } + + /** + * @notice accepts the transfer of an upkeep admin + */ + function acceptUpkeepAdmin(uint256 id) external { + Upkeep memory upkeep = s_upkeep[id]; + if (upkeep.maxValidBlocknumber != UINT32_MAX) revert UpkeepCancelled(); + if (s_proposedAdmin[id] != msg.sender) revert OnlyCallableByProposedAdmin(); + address past = s_upkeepAdmin[id]; + s_upkeepAdmin[id] = msg.sender; + s_proposedAdmin[id] = ZERO_ADDRESS; + + emit UpkeepAdminTransferred(id, past, msg.sender); + } + + /** + * @notice pauses an upkeep - an upkeep will be neither checked nor performed while paused + */ + function pauseUpkeep(uint256 id) external { + _requireAdminAndNotCancelled(id); + Upkeep memory upkeep = s_upkeep[id]; + if (upkeep.paused) revert OnlyUnpausedUpkeep(); + s_upkeep[id].paused = true; + s_upkeepIDs.remove(id); + emit UpkeepPaused(id); + } + + /** + * @notice unpauses an upkeep + */ + function unpauseUpkeep(uint256 id) external { + _requireAdminAndNotCancelled(id); + Upkeep memory upkeep = s_upkeep[id]; + if (!upkeep.paused) revert OnlyPausedUpkeep(); + s_upkeep[id].paused = false; + s_upkeepIDs.add(id); + emit UpkeepUnpaused(id); + } + + /** + * @notice updates the checkData for an upkeep + */ + function setUpkeepCheckData(uint256 id, bytes calldata newCheckData) external { + _requireAdminAndNotCancelled(id); + if (newCheckData.length > s_storage.maxCheckDataSize) revert CheckDataExceedsLimit(); + s_checkData[id] = newCheckData; + emit UpkeepCheckDataSet(id, newCheckData); + } + + /** + * @notice updates the gas limit for an upkeep + */ + function setUpkeepGasLimit(uint256 id, uint32 gasLimit) external { + if (gasLimit < PERFORM_GAS_MIN || gasLimit > s_storage.maxPerformGas) revert GasLimitOutsideRange(); + _requireAdminAndNotCancelled(id); + s_upkeep[id].performGas = gasLimit; + + emit UpkeepGasLimitSet(id, gasLimit); + } + + /** + * @notice updates the offchain config for an upkeep + */ + function setUpkeepOffchainConfig(uint256 id, bytes calldata config) external { + _requireAdminAndNotCancelled(id); + s_upkeepOffchainConfig[id] = config; + emit UpkeepOffchainConfigSet(id, config); + } + + /** + * @notice sets the upkeep trigger config + * @param id the upkeepID to change the trigger for + * @param triggerConfig the new trigger config + */ + function setUpkeepTriggerConfig(uint256 id, bytes calldata triggerConfig) external { + _requireAdminAndNotCancelled(id); + s_upkeepTriggerConfig[id] = triggerConfig; + emit UpkeepTriggerConfigSet(id, triggerConfig); + } + + /** + * @notice withdraws an upkeep's funds from an upkeep + * @dev note that an upkeep must be cancelled first!! + */ + function withdrawFunds(uint256 id, address to) external nonReentrant { + if (to == ZERO_ADDRESS) revert InvalidRecipient(); + Upkeep memory upkeep = s_upkeep[id]; + if (s_upkeepAdmin[id] != msg.sender) revert OnlyCallableByAdmin(); + if (upkeep.maxValidBlocknumber > s_hotVars.chainModule.blockNumber()) revert UpkeepNotCanceled(); + uint96 amountToWithdraw = s_upkeep[id].balance; + s_reserveAmounts[upkeep.billingToken] = s_reserveAmounts[upkeep.billingToken] - amountToWithdraw; + s_upkeep[id].balance = 0; + upkeep.billingToken.safeTransfer(to, amountToWithdraw); + emit FundsWithdrawn(id, amountToWithdraw, to); + } + + // ================================================================ + // | FINANCE ACTIONS | + // ================================================================ + + /** + * @notice withdraws excess LINK from the liquidity pool + * @param to the address to send the fees to + * @param amount the amount to withdraw + */ + function withdrawLink(address to, uint256 amount) external { + _onlyFinanceAdminAllowed(); + if (to == ZERO_ADDRESS) revert InvalidRecipient(); + + int256 available = _linkAvailableForPayment(); + if (available < 0) { + revert InsufficientBalance(0, amount); + } else if (amount > uint256(available)) { + revert InsufficientBalance(uint256(available), amount); + } + + bool transferStatus = i_link.transfer(to, amount); + if (!transferStatus) { + revert TransferFailed(); + } + emit FeesWithdrawn(address(i_link), to, amount); + } + + /** + * @notice withdraws non-LINK fees earned by the contract + * @param asset the asset to withdraw + * @param to the address to send the fees to + * @param amount the amount to withdraw + * @dev in ON_CHAIN mode, we prevent withdrawing non-LINK fees unless there is sufficient LINK liquidity + * to cover all outstanding debts on the registry + */ + function withdrawERC20Fees(IERC20 asset, address to, uint256 amount) external { + _onlyFinanceAdminAllowed(); + if (to == ZERO_ADDRESS) revert InvalidRecipient(); + if (address(asset) == address(i_link)) revert InvalidToken(); + if (_linkAvailableForPayment() < 0 && s_payoutMode == PayoutMode.ON_CHAIN) revert InsufficientLinkLiquidity(); + uint256 available = asset.balanceOf(address(this)) - s_reserveAmounts[asset]; + if (amount > available) revert InsufficientBalance(available, amount); + + asset.safeTransfer(to, amount); + emit FeesWithdrawn(address(asset), to, amount); + } +} diff --git a/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryLogicC2_3.sol b/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryLogicC2_3.sol new file mode 100644 index 00000000000..61d0eecfbaf --- /dev/null +++ b/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryLogicC2_3.sol @@ -0,0 +1,638 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.19; + +import {ZKSyncAutomationRegistryBase2_3} from "./ZKSyncAutomationRegistryBase2_3.sol"; +import {EnumerableSet} from "../../vendor/openzeppelin-solidity/v4.7.3/contracts/utils/structs/EnumerableSet.sol"; +import {Address} from "../../vendor/openzeppelin-solidity/v4.7.3/contracts/utils/Address.sol"; +import {IAutomationForwarder} from "../interfaces/IAutomationForwarder.sol"; +import {IChainModule} from "../interfaces/IChainModule.sol"; +import {IERC20Metadata as IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {IAutomationV21PlusCommon} from "../interfaces/IAutomationV21PlusCommon.sol"; + +contract ZKSyncAutomationRegistryLogicC2_3 is ZKSyncAutomationRegistryBase2_3 { + using Address for address; + using EnumerableSet for EnumerableSet.UintSet; + using EnumerableSet for EnumerableSet.AddressSet; + + /** + * @dev see AutomationRegistry master contract for constructor description + */ + constructor( + address link, + address linkUSDFeed, + address nativeUSDFeed, + address fastGasFeed, + address automationForwarderLogic, + address allowedReadOnlyAddress, + PayoutMode payoutMode, + address wrappedNativeTokenAddress + ) + ZKSyncAutomationRegistryBase2_3( + link, + linkUSDFeed, + nativeUSDFeed, + fastGasFeed, + automationForwarderLogic, + allowedReadOnlyAddress, + payoutMode, + wrappedNativeTokenAddress + ) + {} + + // ================================================================ + // | NODE ACTIONS | + // ================================================================ + + /** + * @notice transfers the address of payee for a transmitter + */ + function transferPayeeship(address transmitter, address proposed) external { + if (s_transmitterPayees[transmitter] != msg.sender) revert OnlyCallableByPayee(); + if (proposed == msg.sender) revert ValueNotChanged(); + + if (s_proposedPayee[transmitter] != proposed) { + s_proposedPayee[transmitter] = proposed; + emit PayeeshipTransferRequested(transmitter, msg.sender, proposed); + } + } + + /** + * @notice accepts the transfer of the payee + */ + function acceptPayeeship(address transmitter) external { + if (s_proposedPayee[transmitter] != msg.sender) revert OnlyCallableByProposedPayee(); + address past = s_transmitterPayees[transmitter]; + s_transmitterPayees[transmitter] = msg.sender; + s_proposedPayee[transmitter] = ZERO_ADDRESS; + + emit PayeeshipTransferred(transmitter, past, msg.sender); + } + + /** + * @notice this is for NOPs to withdraw LINK received as payment for work performed + */ + function withdrawPayment(address from, address to) external { + if (to == ZERO_ADDRESS) revert InvalidRecipient(); + if (s_payoutMode == PayoutMode.OFF_CHAIN) revert MustSettleOffchain(); + if (s_transmitterPayees[from] != msg.sender) revert OnlyCallableByPayee(); + uint96 balance = _updateTransmitterBalanceFromPool(from, s_hotVars.totalPremium, uint96(s_transmittersList.length)); + s_transmitters[from].balance = 0; + s_reserveAmounts[IERC20(address(i_link))] = s_reserveAmounts[IERC20(address(i_link))] - balance; + bool transferStatus = i_link.transfer(to, balance); + if (!transferStatus) { + revert TransferFailed(); + } + emit PaymentWithdrawn(from, balance, to, msg.sender); + } + + // ================================================================ + // | OWNER / MANAGER ACTIONS | + // ================================================================ + + /** + * @notice sets the privilege config for an upkeep + */ + function setUpkeepPrivilegeConfig(uint256 upkeepId, bytes calldata newPrivilegeConfig) external { + _onlyPrivilegeManagerAllowed(); + s_upkeepPrivilegeConfig[upkeepId] = newPrivilegeConfig; + emit UpkeepPrivilegeConfigSet(upkeepId, newPrivilegeConfig); + } + + /** + * @notice this is used by the owner to set the initial payees for newly added transmitters. The owner is not allowed to change payees for existing transmitters. + * @dev the IGNORE_ADDRESS is a "helper" that makes it easier to construct a list of payees when you only care about setting the payee for a small number of transmitters. + */ + function setPayees(address[] calldata payees) external onlyOwner { + if (s_transmittersList.length != payees.length) revert ParameterLengthError(); + for (uint256 i = 0; i < s_transmittersList.length; i++) { + address transmitter = s_transmittersList[i]; + address oldPayee = s_transmitterPayees[transmitter]; + address newPayee = payees[i]; + + if ( + (newPayee == ZERO_ADDRESS) || (oldPayee != ZERO_ADDRESS && oldPayee != newPayee && newPayee != IGNORE_ADDRESS) + ) { + revert InvalidPayee(); + } + + if (newPayee != IGNORE_ADDRESS) { + s_transmitterPayees[transmitter] = newPayee; + } + } + emit PayeesUpdated(s_transmittersList, payees); + } + + /** + * @notice sets the migration permission for a peer registry + * @dev this must be done before upkeeps can be migrated to/from another registry + */ + function setPeerRegistryMigrationPermission(address peer, MigrationPermission permission) external onlyOwner { + s_peerRegistryMigrationPermission[peer] = permission; + } + + /** + * @notice pauses the entire registry + */ + function pause() external onlyOwner { + s_hotVars.paused = true; + emit Paused(msg.sender); + } + + /** + * @notice unpauses the entire registry + */ + function unpause() external onlyOwner { + s_hotVars.paused = false; + emit Unpaused(msg.sender); + } + + /** + * @notice sets a generic bytes field used to indicate the privilege that this admin address had + * @param admin the address to set privilege for + * @param newPrivilegeConfig the privileges that this admin has + */ + function setAdminPrivilegeConfig(address admin, bytes calldata newPrivilegeConfig) external { + _onlyPrivilegeManagerAllowed(); + s_adminPrivilegeConfig[admin] = newPrivilegeConfig; + emit AdminPrivilegeConfigSet(admin, newPrivilegeConfig); + } + + /** + * @notice settles NOPs' LINK payment offchain + */ + function settleNOPsOffchain() external { + _onlyFinanceAdminAllowed(); + if (s_payoutMode == PayoutMode.ON_CHAIN) revert MustSettleOnchain(); + + uint96 totalPremium = s_hotVars.totalPremium; + uint256 activeTransmittersLength = s_transmittersList.length; + uint256 deactivatedTransmittersLength = s_deactivatedTransmitters.length(); + uint256 length = activeTransmittersLength + deactivatedTransmittersLength; + uint256[] memory payments = new uint256[](length); + address[] memory payees = new address[](length); + + for (uint256 i = 0; i < activeTransmittersLength; i++) { + address transmitterAddr = s_transmittersList[i]; + uint96 balance = _updateTransmitterBalanceFromPool( + transmitterAddr, + totalPremium, + uint96(activeTransmittersLength) + ); + + payments[i] = balance; + payees[i] = s_transmitterPayees[transmitterAddr]; + s_transmitters[transmitterAddr].balance = 0; + } + + for (uint256 i = 0; i < deactivatedTransmittersLength; i++) { + address deactivatedAddr = s_deactivatedTransmitters.at(i); + Transmitter memory transmitter = s_transmitters[deactivatedAddr]; + + payees[i + activeTransmittersLength] = s_transmitterPayees[deactivatedAddr]; + payments[i + activeTransmittersLength] = transmitter.balance; + s_transmitters[deactivatedAddr].balance = 0; + } + + // reserve amount of LINK is reset to 0 since no user deposits of LINK are expected in offchain mode + s_reserveAmounts[IERC20(address(i_link))] = 0; + + for (uint256 idx = s_deactivatedTransmitters.length(); idx > 0; idx--) { + s_deactivatedTransmitters.remove(s_deactivatedTransmitters.at(idx - 1)); + } + + emit NOPsSettledOffchain(payees, payments); + } + + /** + * @notice disables offchain payment for NOPs + */ + function disableOffchainPayments() external onlyOwner { + s_payoutMode = PayoutMode.ON_CHAIN; + } + + // ================================================================ + // | GETTERS | + // ================================================================ + + function getConditionalGasOverhead() external pure returns (uint256) { + return REGISTRY_CONDITIONAL_OVERHEAD; + } + + function getLogGasOverhead() external pure returns (uint256) { + return REGISTRY_LOG_OVERHEAD; + } + + function getPerPerformByteGasOverhead() external pure returns (uint256) { + return REGISTRY_PER_PERFORM_BYTE_GAS_OVERHEAD; + } + + function getPerSignerGasOverhead() external pure returns (uint256) { + return REGISTRY_PER_SIGNER_GAS_OVERHEAD; + } + + function getTransmitCalldataFixedBytesOverhead() external pure returns (uint256) { + return TRANSMIT_CALLDATA_FIXED_BYTES_OVERHEAD; + } + + function getTransmitCalldataPerSignerBytesOverhead() external pure returns (uint256) { + return TRANSMIT_CALLDATA_PER_SIGNER_BYTES_OVERHEAD; + } + + function getCancellationDelay() external pure returns (uint256) { + return CANCELLATION_DELAY; + } + + function getLinkAddress() external view returns (address) { + return address(i_link); + } + + function getLinkUSDFeedAddress() external view returns (address) { + return address(i_linkUSDFeed); + } + + function getNativeUSDFeedAddress() external view returns (address) { + return address(i_nativeUSDFeed); + } + + function getFastGasFeedAddress() external view returns (address) { + return address(i_fastGasFeed); + } + + function getAutomationForwarderLogic() external view returns (address) { + return i_automationForwarderLogic; + } + + function getAllowedReadOnlyAddress() external view returns (address) { + return i_allowedReadOnlyAddress; + } + + function getWrappedNativeTokenAddress() external view returns (address) { + return address(i_wrappedNativeToken); + } + + function getBillingToken(uint256 upkeepID) external view returns (IERC20) { + return s_upkeep[upkeepID].billingToken; + } + + function getBillingTokens() external view returns (IERC20[] memory) { + return s_billingTokens; + } + + function supportsBillingToken(IERC20 token) external view returns (bool) { + return address(s_billingConfigs[token].priceFeed) != address(0); + } + + function getBillingTokenConfig(IERC20 token) external view returns (BillingConfig memory) { + return s_billingConfigs[token]; + } + + function getBillingOverridesEnabled(uint256 upkeepID) external view returns (bool) { + return s_upkeep[upkeepID].overridesEnabled; + } + + function getPayoutMode() external view returns (PayoutMode) { + return s_payoutMode; + } + + function upkeepVersion() public pure returns (uint8) { + return UPKEEP_VERSION_BASE; + } + + /** + * @notice gets the number of upkeeps on the registry + */ + function getNumUpkeeps() external view returns (uint256) { + return s_upkeepIDs.length(); + } + + /** + * @notice read all of the details about an upkeep + * @dev this function may be deprecated in a future version of automation in favor of individual + * getters for each field + */ + function getUpkeep(uint256 id) external view returns (IAutomationV21PlusCommon.UpkeepInfoLegacy memory upkeepInfo) { + Upkeep memory reg = s_upkeep[id]; + address target = address(reg.forwarder) == address(0) ? address(0) : reg.forwarder.getTarget(); + upkeepInfo = IAutomationV21PlusCommon.UpkeepInfoLegacy({ + target: target, + performGas: reg.performGas, + checkData: s_checkData[id], + balance: reg.balance, + admin: s_upkeepAdmin[id], + maxValidBlocknumber: reg.maxValidBlocknumber, + lastPerformedBlockNumber: reg.lastPerformedBlockNumber, + amountSpent: uint96(reg.amountSpent), // force casting to uint96 for backwards compatibility. Not an issue if it overflows. + paused: reg.paused, + offchainConfig: s_upkeepOffchainConfig[id] + }); + return upkeepInfo; + } + + /** + * @notice retrieve active upkeep IDs. Active upkeep is defined as an upkeep which is not paused and not canceled. + * @param startIndex starting index in list + * @param maxCount max count to retrieve (0 = unlimited) + * @dev the order of IDs in the list is **not guaranteed**, therefore, if making successive calls, one + * should consider keeping the blockheight constant to ensure a holistic picture of the contract state + */ + function getActiveUpkeepIDs(uint256 startIndex, uint256 maxCount) external view returns (uint256[] memory) { + uint256 numUpkeeps = s_upkeepIDs.length(); + if (startIndex >= numUpkeeps) revert IndexOutOfRange(); + uint256 endIndex = startIndex + maxCount; + endIndex = endIndex > numUpkeeps || maxCount == 0 ? numUpkeeps : endIndex; + uint256[] memory ids = new uint256[](endIndex - startIndex); + for (uint256 idx = 0; idx < ids.length; idx++) { + ids[idx] = s_upkeepIDs.at(idx + startIndex); + } + return ids; + } + + /** + * @notice returns the upkeep's trigger type + */ + function getTriggerType(uint256 upkeepId) external pure returns (Trigger) { + return _getTriggerType(upkeepId); + } + + /** + * @notice returns the trigger config for an upkeeep + */ + function getUpkeepTriggerConfig(uint256 upkeepId) public view returns (bytes memory) { + return s_upkeepTriggerConfig[upkeepId]; + } + + /** + * @notice read the current info about any transmitter address + */ + function getTransmitterInfo( + address query + ) external view returns (bool active, uint8 index, uint96 balance, uint96 lastCollected, address payee) { + Transmitter memory transmitter = s_transmitters[query]; + + uint96 pooledShare = 0; + if (transmitter.active) { + uint96 totalDifference = s_hotVars.totalPremium - transmitter.lastCollected; + pooledShare = totalDifference / uint96(s_transmittersList.length); + } + + return ( + transmitter.active, + transmitter.index, + (transmitter.balance + pooledShare), + transmitter.lastCollected, + s_transmitterPayees[query] + ); + } + + /** + * @notice read the current info about any signer address + */ + function getSignerInfo(address query) external view returns (bool active, uint8 index) { + Signer memory signer = s_signers[query]; + return (signer.active, signer.index); + } + + /** + * @notice read the current on-chain config of the registry + * @dev this function will change between versions, it should never be used where + * backwards compatibility matters! + */ + function getConfig() external view returns (OnchainConfig memory) { + return + OnchainConfig({ + checkGasLimit: s_storage.checkGasLimit, + stalenessSeconds: s_hotVars.stalenessSeconds, + gasCeilingMultiplier: s_hotVars.gasCeilingMultiplier, + maxPerformGas: s_storage.maxPerformGas, + maxCheckDataSize: s_storage.maxCheckDataSize, + maxPerformDataSize: s_storage.maxPerformDataSize, + maxRevertDataSize: s_storage.maxRevertDataSize, + fallbackGasPrice: s_fallbackGasPrice, + fallbackLinkPrice: s_fallbackLinkPrice, + fallbackNativePrice: s_fallbackNativePrice, + transcoder: s_storage.transcoder, + registrars: s_registrars.values(), + upkeepPrivilegeManager: s_storage.upkeepPrivilegeManager, + chainModule: s_hotVars.chainModule, + reorgProtectionEnabled: s_hotVars.reorgProtectionEnabled, + financeAdmin: s_storage.financeAdmin + }); + } + + /** + * @notice read the current state of the registry + * @dev this function is deprecated + */ + function getState() + external + view + returns ( + IAutomationV21PlusCommon.StateLegacy memory state, + IAutomationV21PlusCommon.OnchainConfigLegacy memory config, + address[] memory signers, + address[] memory transmitters, + uint8 f + ) + { + state = IAutomationV21PlusCommon.StateLegacy({ + nonce: s_storage.nonce, + ownerLinkBalance: 0, // deprecated + expectedLinkBalance: 0, // deprecated + totalPremium: s_hotVars.totalPremium, + numUpkeeps: s_upkeepIDs.length(), + configCount: s_storage.configCount, + latestConfigBlockNumber: s_storage.latestConfigBlockNumber, + latestConfigDigest: s_latestConfigDigest, + latestEpoch: s_hotVars.latestEpoch, + paused: s_hotVars.paused + }); + + config = IAutomationV21PlusCommon.OnchainConfigLegacy({ + paymentPremiumPPB: 0, // deprecated + flatFeeMicroLink: 0, // deprecated + checkGasLimit: s_storage.checkGasLimit, + stalenessSeconds: s_hotVars.stalenessSeconds, + gasCeilingMultiplier: s_hotVars.gasCeilingMultiplier, + minUpkeepSpend: 0, // deprecated + maxPerformGas: s_storage.maxPerformGas, + maxCheckDataSize: s_storage.maxCheckDataSize, + maxPerformDataSize: s_storage.maxPerformDataSize, + maxRevertDataSize: s_storage.maxRevertDataSize, + fallbackGasPrice: s_fallbackGasPrice, + fallbackLinkPrice: s_fallbackLinkPrice, + transcoder: s_storage.transcoder, + registrars: s_registrars.values(), + upkeepPrivilegeManager: s_storage.upkeepPrivilegeManager + }); + + return (state, config, s_signersList, s_transmittersList, s_hotVars.f); + } + + /** + * @notice read the Storage data + * @dev this function signature will change with each version of automation + * this should not be treated as a stable function + */ + function getStorage() external view returns (Storage memory) { + return s_storage; + } + + /** + * @notice read the HotVars data + * @dev this function signature will change with each version of automation + * this should not be treated as a stable function + */ + function getHotVars() external view returns (HotVars memory) { + return s_hotVars; + } + + /** + * @notice get the chain module + */ + function getChainModule() external view returns (IChainModule chainModule) { + return s_hotVars.chainModule; + } + + /** + * @notice if this registry has reorg protection enabled + */ + function getReorgProtectionEnabled() external view returns (bool reorgProtectionEnabled) { + return s_hotVars.reorgProtectionEnabled; + } + + /** + * @notice calculates the minimum balance required for an upkeep to remain eligible + * @param id the upkeep id to calculate minimum balance for + */ + function getBalance(uint256 id) external view returns (uint96 balance) { + return s_upkeep[id].balance; + } + + /** + * @notice calculates the minimum balance required for an upkeep to remain eligible + * @param id the upkeep id to calculate minimum balance for + */ + function getMinBalance(uint256 id) external view returns (uint96) { + return getMinBalanceForUpkeep(id); + } + + /** + * @notice calculates the minimum balance required for an upkeep to remain eligible + * @param id the upkeep id to calculate minimum balance for + * @dev this will be deprecated in a future version in favor of getMinBalance + */ + function getMinBalanceForUpkeep(uint256 id) public view returns (uint96 minBalance) { + Upkeep memory upkeep = s_upkeep[id]; + return getMaxPaymentForGas(id, _getTriggerType(id), upkeep.performGas, upkeep.billingToken); + } + + /** + * @notice calculates the maximum payment for a given gas limit + * @param gasLimit the gas to calculate payment for + */ + function getMaxPaymentForGas( + uint256 id, + Trigger triggerType, + uint32 gasLimit, + IERC20 billingToken + ) public view returns (uint96 maxPayment) { + HotVars memory hotVars = s_hotVars; + (uint256 fastGasWei, uint256 linkUSD, uint256 nativeUSD) = _getFeedData(hotVars); + return _getMaxPayment(id, hotVars, triggerType, gasLimit, fastGasWei, linkUSD, nativeUSD, billingToken); + } + + /** + * @notice retrieves the migration permission for a peer registry + */ + function getPeerRegistryMigrationPermission(address peer) external view returns (MigrationPermission) { + return s_peerRegistryMigrationPermission[peer]; + } + + /** + * @notice returns the upkeep privilege config + */ + function getUpkeepPrivilegeConfig(uint256 upkeepId) external view returns (bytes memory) { + return s_upkeepPrivilegeConfig[upkeepId]; + } + + /** + * @notice returns the admin's privilege config + */ + function getAdminPrivilegeConfig(address admin) external view returns (bytes memory) { + return s_adminPrivilegeConfig[admin]; + } + + /** + * @notice returns the upkeep's forwarder contract + */ + function getForwarder(uint256 upkeepID) external view returns (IAutomationForwarder) { + return s_upkeep[upkeepID].forwarder; + } + + /** + * @notice returns if the dedupKey exists or not + */ + function hasDedupKey(bytes32 dedupKey) external view returns (bool) { + return s_dedupKeys[dedupKey]; + } + + /** + * @notice returns the fallback native price + */ + function getFallbackNativePrice() external view returns (uint256) { + return s_fallbackNativePrice; + } + + /** + * @notice returns the amount of a particular token that is reserved as + * user deposits / NOP payments + */ + function getReserveAmount(IERC20 billingToken) external view returns (uint256) { + return s_reserveAmounts[billingToken]; + } + + /** + * @notice returns the amount of a particular token that is withdraw-able by finance admin + */ + function getAvailableERC20ForPayment(IERC20 billingToken) external view returns (uint256) { + return billingToken.balanceOf(address(this)) - s_reserveAmounts[IERC20(address(billingToken))]; + } + + /** + * @notice returns the size of the LINK liquidity pool + */ + function linkAvailableForPayment() public view returns (int256) { + return _linkAvailableForPayment(); + } + + /** + * @notice returns the BillingOverrides config for a given upkeep + */ + function getBillingOverrides(uint256 upkeepID) external view returns (BillingOverrides memory) { + return s_billingOverrides[upkeepID]; + } + + /** + * @notice returns the BillingConfig for a given billing token, this includes decimals and price feed etc + */ + function getBillingConfig(IERC20 billingToken) external view returns (BillingConfig memory) { + return s_billingConfigs[billingToken]; + } + + /** + * @notice returns all active transmitters with their associated payees + */ + function getTransmittersWithPayees() external view returns (TransmitterPayeeInfo[] memory) { + uint256 transmitterCount = s_transmittersList.length; + TransmitterPayeeInfo[] memory transmitters = new TransmitterPayeeInfo[](transmitterCount); + + for (uint256 i = 0; i < transmitterCount; i++) { + address transmitterAddress = s_transmittersList[i]; + address payeeAddress = s_transmitterPayees[transmitterAddress]; + + transmitters[i] = TransmitterPayeeInfo(transmitterAddress, payeeAddress); + } + + return transmitters; + } +} diff --git a/contracts/test/v0.8/automation/AutomationRegistry2_3.test.ts b/contracts/test/v0.8/automation/AutomationRegistry2_3.test.ts index 9a572269695..f993271fbbc 100644 --- a/contracts/test/v0.8/automation/AutomationRegistry2_3.test.ts +++ b/contracts/test/v0.8/automation/AutomationRegistry2_3.test.ts @@ -25,7 +25,6 @@ import { ChainModuleBase__factory as ChainModuleBaseFactory } from '../../../typ import { ArbitrumModule__factory as ArbitrumModuleFactory } from '../../../typechain/factories/ArbitrumModule__factory' import { OptimismModule__factory as OptimismModuleFactory } from '../../../typechain/factories/OptimismModule__factory' import { ILogAutomation__factory as ILogAutomationactory } from '../../../typechain/factories/ILogAutomation__factory' -import { IAutomationForwarder__factory as IAutomationForwarderFactory } from '../../../typechain/factories/IAutomationForwarder__factory' import { MockArbSys__factory as MockArbSysFactory } from '../../../typechain/factories/MockArbSys__factory' import { AutomationCompatibleUtils } from '../../../typechain/AutomationCompatibleUtils' import { MockArbGasInfo } from '../../../typechain/MockArbGasInfo' diff --git a/contracts/test/v0.8/automation/helpers.ts b/contracts/test/v0.8/automation/helpers.ts index 5a95fb482cd..b2cdfb4efd9 100644 --- a/contracts/test/v0.8/automation/helpers.ts +++ b/contracts/test/v0.8/automation/helpers.ts @@ -170,10 +170,10 @@ export const deployRegistry23 = async ( link: Parameters[0], linkUSD: Parameters[1], nativeUSD: Parameters[2], - fastgas: Parameters[2], + fastgas: Parameters[3], allowedReadOnlyAddress: Parameters< AutomationRegistryLogicC2_3Factory['deploy'] - >[3], + >[5], payoutMode: Parameters[6], wrappedNativeTokenAddress: Parameters< AutomationRegistryLogicC2_3Factory['deploy'] From 477c8ce4b5b0a33a1645c15027bce3d23cff8c44 Mon Sep 17 00:00:00 2001 From: Jordan Krage Date: Wed, 7 Aug 2024 17:38:04 +0200 Subject: [PATCH 3/9] enable gomods (#14042) --- GNUmakefile | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index 3cba1738d5d..3b781a665d2 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -27,12 +27,8 @@ gomod: ## Ensure chainlink's go dependencies are installed. go mod download .PHONY: gomodtidy -gomodtidy: ## Run go mod tidy on all modules. - go mod tidy - cd ./core/scripts && go mod tidy - cd ./integration-tests && go mod tidy - cd ./integration-tests/load && go mod tidy - cd ./dashboard-lib && go mod tidy +gomodtidy: gomods ## Run go mod tidy on all modules. + gomods tidy .PHONY: docs docs: ## Install and run pkgsite to view Go docs @@ -89,12 +85,8 @@ abigen: ## Build & install abigen. ./tools/bin/build_abigen .PHONY: generate -generate: abigen codecgen mockery protoc ## Execute all go:generate commands. - go generate -x ./... - cd ./core/scripts && go generate -x ./... - cd ./integration-tests && go generate -x ./... - cd ./integration-tests/load && go generate -x ./... - cd ./dashboard-lib && go generate -x ./... +generate: abigen codecgen mockery protoc gomods ## Execute all go:generate commands. + gomods -w go generate -x ./... mockery .PHONY: rm-mocked @@ -136,7 +128,7 @@ presubmit: ## Format go files and imports. .PHONY: gomods gomods: ## Install gomods - go install github.com/jmank88/gomods@v0.1.1 + go install github.com/jmank88/gomods@v0.1.3 .PHONY: mockery mockery: $(mockery) ## Install mockery. From 499a67705ac7ea525685c4a064ff4aa52b08fa44 Mon Sep 17 00:00:00 2001 From: Ryan Hall Date: Wed, 7 Aug 2024 12:51:02 -0400 Subject: [PATCH 4/9] add OZ 5.0.2 contracts (#14065) --- contracts/.changeset/mean-zoos-fly.md | 5 + .../v5.0.2/contracts/access/AccessControl.sol | 209 +++ .../contracts/access/IAccessControl.sol | 98 ++ .../v5.0.2/contracts/interfaces/IERC165.sol | 6 + .../v5.0.2/contracts/interfaces/IERC20.sol | 6 + .../v5.0.2/contracts/interfaces/IERC5267.sol | 28 + .../contracts/interfaces/draft-IERC6093.sol | 161 +++ .../v5.0.2/contracts/token/ERC20/ERC20.sol | 316 +++++ .../v5.0.2/contracts/token/ERC20/IERC20.sol | 79 ++ .../token/ERC20/extensions/ERC20Burnable.sol | 39 + .../token/ERC20/extensions/IERC20Metadata.sol | 26 + .../token/ERC20/extensions/IERC20Permit.sol | 90 ++ .../contracts/token/ERC20/utils/SafeERC20.sol | 118 ++ .../v5.0.2/contracts/utils/Address.sol | 159 +++ .../v5.0.2/contracts/utils/Context.sol | 28 + .../v5.0.2/contracts/utils/Pausable.sol | 119 ++ .../v5.0.2/contracts/utils/ShortStrings.sol | 123 ++ .../v5.0.2/contracts/utils/StorageSlot.sol | 135 ++ .../v5.0.2/contracts/utils/Strings.sol | 94 ++ .../contracts/utils/cryptography/ECDSA.sol | 174 +++ .../contracts/utils/cryptography/EIP712.sol | 160 +++ .../utils/cryptography/MessageHashUtils.sol | 86 ++ .../contracts/utils/introspection/ERC165.sol | 27 + .../utils/introspection/ERC165Checker.sol | 124 ++ .../contracts/utils/introspection/IERC165.sol | 25 + .../v5.0.2/contracts/utils/math/Math.sol | 415 ++++++ .../v5.0.2/contracts/utils/math/SafeCast.sol | 1153 +++++++++++++++++ .../contracts/utils/math/SignedMath.sol | 43 + .../contracts/utils/structs/EnumerableMap.sol | 533 ++++++++ .../contracts/utils/structs/EnumerableSet.sol | 378 ++++++ 30 files changed, 4957 insertions(+) create mode 100644 contracts/.changeset/mean-zoos-fly.md create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/access/AccessControl.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/access/IAccessControl.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/interfaces/IERC165.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/interfaces/IERC20.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/interfaces/IERC5267.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/interfaces/draft-IERC6093.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/ERC20.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/IERC20.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/extensions/ERC20Burnable.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/extensions/IERC20Metadata.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/extensions/IERC20Permit.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/utils/SafeERC20.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Address.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Context.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Pausable.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/ShortStrings.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/StorageSlot.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Strings.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/cryptography/ECDSA.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/cryptography/EIP712.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/cryptography/MessageHashUtils.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/introspection/ERC165.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/introspection/ERC165Checker.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/introspection/IERC165.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/math/Math.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/math/SafeCast.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/math/SignedMath.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/structs/EnumerableMap.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/structs/EnumerableSet.sol diff --git a/contracts/.changeset/mean-zoos-fly.md b/contracts/.changeset/mean-zoos-fly.md new file mode 100644 index 00000000000..72eb98198d0 --- /dev/null +++ b/contracts/.changeset/mean-zoos-fly.md @@ -0,0 +1,5 @@ +--- +'@chainlink/contracts': patch +--- + +add OZ v0.5 contracts diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/access/AccessControl.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/access/AccessControl.sol new file mode 100644 index 00000000000..3e3341e9cfd --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/access/AccessControl.sol @@ -0,0 +1,209 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (access/AccessControl.sol) + +pragma solidity ^0.8.20; + +import {IAccessControl} from "./IAccessControl.sol"; +import {Context} from "../utils/Context.sol"; +import {ERC165} from "../utils/introspection/ERC165.sol"; + +/** + * @dev Contract module that allows children to implement role-based access + * control mechanisms. This is a lightweight version that doesn't allow enumerating role + * members except through off-chain means by accessing the contract event logs. Some + * applications may benefit from on-chain enumerability, for those cases see + * {AccessControlEnumerable}. + * + * Roles are referred to by their `bytes32` identifier. These should be exposed + * in the external API and be unique. The best way to achieve this is by + * using `public constant` hash digests: + * + * ```solidity + * bytes32 public constant MY_ROLE = keccak256("MY_ROLE"); + * ``` + * + * Roles can be used to represent a set of permissions. To restrict access to a + * function call, use {hasRole}: + * + * ```solidity + * function foo() public { + * require(hasRole(MY_ROLE, msg.sender)); + * ... + * } + * ``` + * + * Roles can be granted and revoked dynamically via the {grantRole} and + * {revokeRole} functions. Each role has an associated admin role, and only + * accounts that have a role's admin role can call {grantRole} and {revokeRole}. + * + * By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means + * that only accounts with this role will be able to grant or revoke other + * roles. More complex role relationships can be created by using + * {_setRoleAdmin}. + * + * WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to + * grant and revoke this role. Extra precautions should be taken to secure + * accounts that have been granted it. We recommend using {AccessControlDefaultAdminRules} + * to enforce additional security measures for this role. + */ +abstract contract AccessControl is Context, IAccessControl, ERC165 { + struct RoleData { + mapping(address account => bool) hasRole; + bytes32 adminRole; + } + + mapping(bytes32 role => RoleData) private _roles; + + bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00; + + /** + * @dev Modifier that checks that an account has a specific role. Reverts + * with an {AccessControlUnauthorizedAccount} error including the required role. + */ + modifier onlyRole(bytes32 role) { + _checkRole(role); + _; + } + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return interfaceId == type(IAccessControl).interfaceId || super.supportsInterface(interfaceId); + } + + /** + * @dev Returns `true` if `account` has been granted `role`. + */ + function hasRole(bytes32 role, address account) public view virtual returns (bool) { + return _roles[role].hasRole[account]; + } + + /** + * @dev Reverts with an {AccessControlUnauthorizedAccount} error if `_msgSender()` + * is missing `role`. Overriding this function changes the behavior of the {onlyRole} modifier. + */ + function _checkRole(bytes32 role) internal view virtual { + _checkRole(role, _msgSender()); + } + + /** + * @dev Reverts with an {AccessControlUnauthorizedAccount} error if `account` + * is missing `role`. + */ + function _checkRole(bytes32 role, address account) internal view virtual { + if (!hasRole(role, account)) { + revert AccessControlUnauthorizedAccount(account, role); + } + } + + /** + * @dev Returns the admin role that controls `role`. See {grantRole} and + * {revokeRole}. + * + * To change a role's admin, use {_setRoleAdmin}. + */ + function getRoleAdmin(bytes32 role) public view virtual returns (bytes32) { + return _roles[role].adminRole; + } + + /** + * @dev Grants `role` to `account`. + * + * If `account` had not been already granted `role`, emits a {RoleGranted} + * event. + * + * Requirements: + * + * - the caller must have ``role``'s admin role. + * + * May emit a {RoleGranted} event. + */ + function grantRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) { + _grantRole(role, account); + } + + /** + * @dev Revokes `role` from `account`. + * + * If `account` had been granted `role`, emits a {RoleRevoked} event. + * + * Requirements: + * + * - the caller must have ``role``'s admin role. + * + * May emit a {RoleRevoked} event. + */ + function revokeRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) { + _revokeRole(role, account); + } + + /** + * @dev Revokes `role` from the calling account. + * + * Roles are often managed via {grantRole} and {revokeRole}: this function's + * purpose is to provide a mechanism for accounts to lose their privileges + * if they are compromised (such as when a trusted device is misplaced). + * + * If the calling account had been revoked `role`, emits a {RoleRevoked} + * event. + * + * Requirements: + * + * - the caller must be `callerConfirmation`. + * + * May emit a {RoleRevoked} event. + */ + function renounceRole(bytes32 role, address callerConfirmation) public virtual { + if (callerConfirmation != _msgSender()) { + revert AccessControlBadConfirmation(); + } + + _revokeRole(role, callerConfirmation); + } + + /** + * @dev Sets `adminRole` as ``role``'s admin role. + * + * Emits a {RoleAdminChanged} event. + */ + function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual { + bytes32 previousAdminRole = getRoleAdmin(role); + _roles[role].adminRole = adminRole; + emit RoleAdminChanged(role, previousAdminRole, adminRole); + } + + /** + * @dev Attempts to grant `role` to `account` and returns a boolean indicating if `role` was granted. + * + * Internal function without access restriction. + * + * May emit a {RoleGranted} event. + */ + function _grantRole(bytes32 role, address account) internal virtual returns (bool) { + if (!hasRole(role, account)) { + _roles[role].hasRole[account] = true; + emit RoleGranted(role, account, _msgSender()); + return true; + } else { + return false; + } + } + + /** + * @dev Attempts to revoke `role` to `account` and returns a boolean indicating if `role` was revoked. + * + * Internal function without access restriction. + * + * May emit a {RoleRevoked} event. + */ + function _revokeRole(bytes32 role, address account) internal virtual returns (bool) { + if (hasRole(role, account)) { + _roles[role].hasRole[account] = false; + emit RoleRevoked(role, account, _msgSender()); + return true; + } else { + return false; + } + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/access/IAccessControl.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/access/IAccessControl.sol new file mode 100644 index 00000000000..2ac89ca7356 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/access/IAccessControl.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (access/IAccessControl.sol) + +pragma solidity ^0.8.20; + +/** + * @dev External interface of AccessControl declared to support ERC165 detection. + */ +interface IAccessControl { + /** + * @dev The `account` is missing a role. + */ + error AccessControlUnauthorizedAccount(address account, bytes32 neededRole); + + /** + * @dev The caller of a function is not the expected one. + * + * NOTE: Don't confuse with {AccessControlUnauthorizedAccount}. + */ + error AccessControlBadConfirmation(); + + /** + * @dev Emitted when `newAdminRole` is set as ``role``'s admin role, replacing `previousAdminRole` + * + * `DEFAULT_ADMIN_ROLE` is the starting admin for all roles, despite + * {RoleAdminChanged} not being emitted signaling this. + */ + event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole); + + /** + * @dev Emitted when `account` is granted `role`. + * + * `sender` is the account that originated the contract call, an admin role + * bearer except when using {AccessControl-_setupRole}. + */ + event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); + + /** + * @dev Emitted when `account` is revoked `role`. + * + * `sender` is the account that originated the contract call: + * - if using `revokeRole`, it is the admin role bearer + * - if using `renounceRole`, it is the role bearer (i.e. `account`) + */ + event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender); + + /** + * @dev Returns `true` if `account` has been granted `role`. + */ + function hasRole(bytes32 role, address account) external view returns (bool); + + /** + * @dev Returns the admin role that controls `role`. See {grantRole} and + * {revokeRole}. + * + * To change a role's admin, use {AccessControl-_setRoleAdmin}. + */ + function getRoleAdmin(bytes32 role) external view returns (bytes32); + + /** + * @dev Grants `role` to `account`. + * + * If `account` had not been already granted `role`, emits a {RoleGranted} + * event. + * + * Requirements: + * + * - the caller must have ``role``'s admin role. + */ + function grantRole(bytes32 role, address account) external; + + /** + * @dev Revokes `role` from `account`. + * + * If `account` had been granted `role`, emits a {RoleRevoked} event. + * + * Requirements: + * + * - the caller must have ``role``'s admin role. + */ + function revokeRole(bytes32 role, address account) external; + + /** + * @dev Revokes `role` from the calling account. + * + * Roles are often managed via {grantRole} and {revokeRole}: this function's + * purpose is to provide a mechanism for accounts to lose their privileges + * if they are compromised (such as when a trusted device is misplaced). + * + * If the calling account had been granted `role`, emits a {RoleRevoked} + * event. + * + * Requirements: + * + * - the caller must be `callerConfirmation`. + */ + function renounceRole(bytes32 role, address callerConfirmation) external; +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/interfaces/IERC165.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/interfaces/IERC165.sol new file mode 100644 index 00000000000..944dd0d5912 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/interfaces/IERC165.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC165.sol) + +pragma solidity ^0.8.20; + +import {IERC165} from "../utils/introspection/IERC165.sol"; diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/interfaces/IERC20.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/interfaces/IERC20.sol new file mode 100644 index 00000000000..21d5a413275 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/interfaces/IERC20.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC20.sol) + +pragma solidity ^0.8.20; + +import {IERC20} from "../token/ERC20/IERC20.sol"; diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/interfaces/IERC5267.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/interfaces/IERC5267.sol new file mode 100644 index 00000000000..47a9fd58855 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/interfaces/IERC5267.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC5267.sol) + +pragma solidity ^0.8.20; + +interface IERC5267 { + /** + * @dev MAY be emitted to signal that the domain could have changed. + */ + event EIP712DomainChanged(); + + /** + * @dev returns the fields and values that describe the domain separator used by this contract for EIP-712 + * signature. + */ + function eip712Domain() + external + view + returns ( + bytes1 fields, + string memory name, + string memory version, + uint256 chainId, + address verifyingContract, + bytes32 salt, + uint256[] memory extensions + ); +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/interfaces/draft-IERC6093.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/interfaces/draft-IERC6093.sol new file mode 100644 index 00000000000..f6990e607c9 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/interfaces/draft-IERC6093.sol @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/draft-IERC6093.sol) +pragma solidity ^0.8.20; + +/** + * @dev Standard ERC20 Errors + * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC20 tokens. + */ +interface IERC20Errors { + /** + * @dev Indicates an error related to the current `balance` of a `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + * @param balance Current balance for the interacting account. + * @param needed Minimum amount required to perform a transfer. + */ + error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed); + + /** + * @dev Indicates a failure with the token `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + */ + error ERC20InvalidSender(address sender); + + /** + * @dev Indicates a failure with the token `receiver`. Used in transfers. + * @param receiver Address to which tokens are being transferred. + */ + error ERC20InvalidReceiver(address receiver); + + /** + * @dev Indicates a failure with the `spender`’s `allowance`. Used in transfers. + * @param spender Address that may be allowed to operate on tokens without being their owner. + * @param allowance Amount of tokens a `spender` is allowed to operate with. + * @param needed Minimum amount required to perform a transfer. + */ + error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed); + + /** + * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals. + * @param approver Address initiating an approval operation. + */ + error ERC20InvalidApprover(address approver); + + /** + * @dev Indicates a failure with the `spender` to be approved. Used in approvals. + * @param spender Address that may be allowed to operate on tokens without being their owner. + */ + error ERC20InvalidSpender(address spender); +} + +/** + * @dev Standard ERC721 Errors + * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC721 tokens. + */ +interface IERC721Errors { + /** + * @dev Indicates that an address can't be an owner. For example, `address(0)` is a forbidden owner in EIP-20. + * Used in balance queries. + * @param owner Address of the current owner of a token. + */ + error ERC721InvalidOwner(address owner); + + /** + * @dev Indicates a `tokenId` whose `owner` is the zero address. + * @param tokenId Identifier number of a token. + */ + error ERC721NonexistentToken(uint256 tokenId); + + /** + * @dev Indicates an error related to the ownership over a particular token. Used in transfers. + * @param sender Address whose tokens are being transferred. + * @param tokenId Identifier number of a token. + * @param owner Address of the current owner of a token. + */ + error ERC721IncorrectOwner(address sender, uint256 tokenId, address owner); + + /** + * @dev Indicates a failure with the token `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + */ + error ERC721InvalidSender(address sender); + + /** + * @dev Indicates a failure with the token `receiver`. Used in transfers. + * @param receiver Address to which tokens are being transferred. + */ + error ERC721InvalidReceiver(address receiver); + + /** + * @dev Indicates a failure with the `operator`’s approval. Used in transfers. + * @param operator Address that may be allowed to operate on tokens without being their owner. + * @param tokenId Identifier number of a token. + */ + error ERC721InsufficientApproval(address operator, uint256 tokenId); + + /** + * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals. + * @param approver Address initiating an approval operation. + */ + error ERC721InvalidApprover(address approver); + + /** + * @dev Indicates a failure with the `operator` to be approved. Used in approvals. + * @param operator Address that may be allowed to operate on tokens without being their owner. + */ + error ERC721InvalidOperator(address operator); +} + +/** + * @dev Standard ERC1155 Errors + * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC1155 tokens. + */ +interface IERC1155Errors { + /** + * @dev Indicates an error related to the current `balance` of a `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + * @param balance Current balance for the interacting account. + * @param needed Minimum amount required to perform a transfer. + * @param tokenId Identifier number of a token. + */ + error ERC1155InsufficientBalance(address sender, uint256 balance, uint256 needed, uint256 tokenId); + + /** + * @dev Indicates a failure with the token `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + */ + error ERC1155InvalidSender(address sender); + + /** + * @dev Indicates a failure with the token `receiver`. Used in transfers. + * @param receiver Address to which tokens are being transferred. + */ + error ERC1155InvalidReceiver(address receiver); + + /** + * @dev Indicates a failure with the `operator`’s approval. Used in transfers. + * @param operator Address that may be allowed to operate on tokens without being their owner. + * @param owner Address of the current owner of a token. + */ + error ERC1155MissingApprovalForAll(address operator, address owner); + + /** + * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals. + * @param approver Address initiating an approval operation. + */ + error ERC1155InvalidApprover(address approver); + + /** + * @dev Indicates a failure with the `operator` to be approved. Used in approvals. + * @param operator Address that may be allowed to operate on tokens without being their owner. + */ + error ERC1155InvalidOperator(address operator); + + /** + * @dev Indicates an array length mismatch between ids and values in a safeBatchTransferFrom operation. + * Used in batch transfers. + * @param idsLength Length of the array of token identifiers + * @param valuesLength Length of the array of token amounts + */ + error ERC1155InvalidArrayLength(uint256 idsLength, uint256 valuesLength); +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/ERC20.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/ERC20.sol new file mode 100644 index 00000000000..1fde5279d00 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/ERC20.sol @@ -0,0 +1,316 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/ERC20.sol) + +pragma solidity ^0.8.20; + +import {IERC20} from "./IERC20.sol"; +import {IERC20Metadata} from "./extensions/IERC20Metadata.sol"; +import {Context} from "../../utils/Context.sol"; +import {IERC20Errors} from "../../interfaces/draft-IERC6093.sol"; + +/** + * @dev Implementation of the {IERC20} interface. + * + * This implementation is agnostic to the way tokens are created. This means + * that a supply mechanism has to be added in a derived contract using {_mint}. + * + * TIP: For a detailed writeup see our guide + * https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How + * to implement supply mechanisms]. + * + * The default value of {decimals} is 18. To change this, you should override + * this function so it returns a different value. + * + * We have followed general OpenZeppelin Contracts guidelines: functions revert + * instead returning `false` on failure. This behavior is nonetheless + * conventional and does not conflict with the expectations of ERC20 + * applications. + * + * Additionally, an {Approval} event is emitted on calls to {transferFrom}. + * This allows applications to reconstruct the allowance for all accounts just + * by listening to said events. Other implementations of the EIP may not emit + * these events, as it isn't required by the specification. + */ +abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors { + mapping(address account => uint256) private _balances; + + mapping(address account => mapping(address spender => uint256)) private _allowances; + + uint256 private _totalSupply; + + string private _name; + string private _symbol; + + /** + * @dev Sets the values for {name} and {symbol}. + * + * All two of these values are immutable: they can only be set once during + * construction. + */ + constructor(string memory name_, string memory symbol_) { + _name = name_; + _symbol = symbol_; + } + + /** + * @dev Returns the name of the token. + */ + function name() public view virtual returns (string memory) { + return _name; + } + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() public view virtual returns (string memory) { + return _symbol; + } + + /** + * @dev Returns the number of decimals used to get its user representation. + * For example, if `decimals` equals `2`, a balance of `505` tokens should + * be displayed to a user as `5.05` (`505 / 10 ** 2`). + * + * Tokens usually opt for a value of 18, imitating the relationship between + * Ether and Wei. This is the default value returned by this function, unless + * it's overridden. + * + * NOTE: This information is only used for _display_ purposes: it in + * no way affects any of the arithmetic of the contract, including + * {IERC20-balanceOf} and {IERC20-transfer}. + */ + function decimals() public view virtual returns (uint8) { + return 18; + } + + /** + * @dev See {IERC20-totalSupply}. + */ + function totalSupply() public view virtual returns (uint256) { + return _totalSupply; + } + + /** + * @dev See {IERC20-balanceOf}. + */ + function balanceOf(address account) public view virtual returns (uint256) { + return _balances[account]; + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - the caller must have a balance of at least `value`. + */ + function transfer(address to, uint256 value) public virtual returns (bool) { + address owner = _msgSender(); + _transfer(owner, to, value); + return true; + } + + /** + * @dev See {IERC20-allowance}. + */ + function allowance(address owner, address spender) public view virtual returns (uint256) { + return _allowances[owner][spender]; + } + + /** + * @dev See {IERC20-approve}. + * + * NOTE: If `value` is the maximum `uint256`, the allowance is not updated on + * `transferFrom`. This is semantically equivalent to an infinite approval. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(address spender, uint256 value) public virtual returns (bool) { + address owner = _msgSender(); + _approve(owner, spender, value); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Emits an {Approval} event indicating the updated allowance. This is not + * required by the EIP. See the note at the beginning of {ERC20}. + * + * NOTE: Does not update the allowance if the current allowance + * is the maximum `uint256`. + * + * Requirements: + * + * - `from` and `to` cannot be the zero address. + * - `from` must have a balance of at least `value`. + * - the caller must have allowance for ``from``'s tokens of at least + * `value`. + */ + function transferFrom(address from, address to, uint256 value) public virtual returns (bool) { + address spender = _msgSender(); + _spendAllowance(from, spender, value); + _transfer(from, to, value); + return true; + } + + /** + * @dev Moves a `value` amount of tokens from `from` to `to`. + * + * This internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * NOTE: This function is not virtual, {_update} should be overridden instead. + */ + function _transfer(address from, address to, uint256 value) internal { + if (from == address(0)) { + revert ERC20InvalidSender(address(0)); + } + if (to == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + _update(from, to, value); + } + + /** + * @dev Transfers a `value` amount of tokens from `from` to `to`, or alternatively mints (or burns) if `from` + * (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding + * this function. + * + * Emits a {Transfer} event. + */ + function _update(address from, address to, uint256 value) internal virtual { + if (from == address(0)) { + // Overflow check required: The rest of the code assumes that totalSupply never overflows + _totalSupply += value; + } else { + uint256 fromBalance = _balances[from]; + if (fromBalance < value) { + revert ERC20InsufficientBalance(from, fromBalance, value); + } + unchecked { + // Overflow not possible: value <= fromBalance <= totalSupply. + _balances[from] = fromBalance - value; + } + } + + if (to == address(0)) { + unchecked { + // Overflow not possible: value <= totalSupply or value <= fromBalance <= totalSupply. + _totalSupply -= value; + } + } else { + unchecked { + // Overflow not possible: balance + value is at most totalSupply, which we know fits into a uint256. + _balances[to] += value; + } + } + + emit Transfer(from, to, value); + } + + /** + * @dev Creates a `value` amount of tokens and assigns them to `account`, by transferring it from address(0). + * Relies on the `_update` mechanism + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * NOTE: This function is not virtual, {_update} should be overridden instead. + */ + function _mint(address account, uint256 value) internal { + if (account == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + _update(address(0), account, value); + } + + /** + * @dev Destroys a `value` amount of tokens from `account`, lowering the total supply. + * Relies on the `_update` mechanism. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * NOTE: This function is not virtual, {_update} should be overridden instead + */ + function _burn(address account, uint256 value) internal { + if (account == address(0)) { + revert ERC20InvalidSender(address(0)); + } + _update(account, address(0), value); + } + + /** + * @dev Sets `value` as the allowance of `spender` over the `owner` s tokens. + * + * This internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + * + * Overrides to this logic should be done to the variant with an additional `bool emitEvent` argument. + */ + function _approve(address owner, address spender, uint256 value) internal { + _approve(owner, spender, value, true); + } + + /** + * @dev Variant of {_approve} with an optional flag to enable or disable the {Approval} event. + * + * By default (when calling {_approve}) the flag is set to true. On the other hand, approval changes made by + * `_spendAllowance` during the `transferFrom` operation set the flag to false. This saves gas by not emitting any + * `Approval` event during `transferFrom` operations. + * + * Anyone who wishes to continue emitting `Approval` events on the`transferFrom` operation can force the flag to + * true using the following override: + * ``` + * function _approve(address owner, address spender, uint256 value, bool) internal virtual override { + * super._approve(owner, spender, value, true); + * } + * ``` + * + * Requirements are the same as {_approve}. + */ + function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual { + if (owner == address(0)) { + revert ERC20InvalidApprover(address(0)); + } + if (spender == address(0)) { + revert ERC20InvalidSpender(address(0)); + } + _allowances[owner][spender] = value; + if (emitEvent) { + emit Approval(owner, spender, value); + } + } + + /** + * @dev Updates `owner` s allowance for `spender` based on spent `value`. + * + * Does not update the allowance value in case of infinite allowance. + * Revert if not enough allowance is available. + * + * Does not emit an {Approval} event. + */ + function _spendAllowance(address owner, address spender, uint256 value) internal virtual { + uint256 currentAllowance = allowance(owner, spender); + if (currentAllowance != type(uint256).max) { + if (currentAllowance < value) { + revert ERC20InsufficientAllowance(spender, currentAllowance, value); + } + unchecked { + _approve(owner, spender, currentAllowance - value, false); + } + } + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/IERC20.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/IERC20.sol new file mode 100644 index 00000000000..db01cf4c751 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/IERC20.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/IERC20.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. + */ +interface IERC20 { + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); + + /** + * @dev Returns the value of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the value of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves a `value` amount of tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 value) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets a `value` amount of tokens as the allowance of `spender` over the + * caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 value) external returns (bool); + + /** + * @dev Moves a `value` amount of tokens from `from` to `to` using the + * allowance mechanism. `value` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom(address from, address to, uint256 value) external returns (bool); +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/extensions/ERC20Burnable.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/extensions/ERC20Burnable.sol new file mode 100644 index 00000000000..4d482d8ec83 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/extensions/ERC20Burnable.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/extensions/ERC20Burnable.sol) + +pragma solidity ^0.8.20; + +import {ERC20} from "../ERC20.sol"; +import {Context} from "../../../utils/Context.sol"; + +/** + * @dev Extension of {ERC20} that allows token holders to destroy both their own + * tokens and those that they have an allowance for, in a way that can be + * recognized off-chain (via event analysis). + */ +abstract contract ERC20Burnable is Context, ERC20 { + /** + * @dev Destroys a `value` amount of tokens from the caller. + * + * See {ERC20-_burn}. + */ + function burn(uint256 value) public virtual { + _burn(_msgSender(), value); + } + + /** + * @dev Destroys a `value` amount of tokens from `account`, deducting from + * the caller's allowance. + * + * See {ERC20-_burn} and {ERC20-allowance}. + * + * Requirements: + * + * - the caller must have allowance for ``accounts``'s tokens of at least + * `value`. + */ + function burnFrom(address account, uint256 value) public virtual { + _spendAllowance(account, _msgSender(), value); + _burn(account, value); + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/extensions/IERC20Metadata.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/extensions/IERC20Metadata.sol new file mode 100644 index 00000000000..1a38cba3e06 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/extensions/IERC20Metadata.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/extensions/IERC20Metadata.sol) + +pragma solidity ^0.8.20; + +import {IERC20} from "../IERC20.sol"; + +/** + * @dev Interface for the optional metadata functions from the ERC20 standard. + */ +interface IERC20Metadata is IERC20 { + /** + * @dev Returns the name of the token. + */ + function name() external view returns (string memory); + + /** + * @dev Returns the symbol of the token. + */ + function symbol() external view returns (string memory); + + /** + * @dev Returns the decimals places of the token. + */ + function decimals() external view returns (uint8); +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/extensions/IERC20Permit.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/extensions/IERC20Permit.sol new file mode 100644 index 00000000000..5af48101ab8 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/extensions/IERC20Permit.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/extensions/IERC20Permit.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Interface of the ERC20 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC20 allowance (see {IERC20-allowance}) by + * presenting a message signed by the account. By not relying on {IERC20-approve}, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + * + * ==== Security Considerations + * + * There are two important considerations concerning the use of `permit`. The first is that a valid permit signature + * expresses an allowance, and it should not be assumed to convey additional meaning. In particular, it should not be + * considered as an intention to spend the allowance in any specific way. The second is that because permits have + * built-in replay protection and can be submitted by anyone, they can be frontrun. A protocol that uses permits should + * take this into consideration and allow a `permit` call to fail. Combining these two aspects, a pattern that may be + * generally recommended is: + * + * ```solidity + * function doThingWithPermit(..., uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) public { + * try token.permit(msg.sender, address(this), value, deadline, v, r, s) {} catch {} + * doThing(..., value); + * } + * + * function doThing(..., uint256 value) public { + * token.safeTransferFrom(msg.sender, address(this), value); + * ... + * } + * ``` + * + * Observe that: 1) `msg.sender` is used as the owner, leaving no ambiguity as to the signer intent, and 2) the use of + * `try/catch` allows the permit to fail and makes the code tolerant to frontrunning. (See also + * {SafeERC20-safeTransferFrom}). + * + * Additionally, note that smart contract wallets (such as Argent or Safe) are not able to produce permit signatures, so + * contracts should have entry points that don't rely on permit. + */ +interface IERC20Permit { + /** + * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, + * given ``owner``'s signed approval. + * + * IMPORTANT: The same issues {IERC20-approve} has related to transaction + * ordering also apply here. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `deadline` must be a timestamp in the future. + * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` + * over the EIP712-formatted function arguments. + * - the signature must use ``owner``'s current nonce (see {nonces}). + * + * For more information on the signature format, see the + * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP + * section]. + * + * CAUTION: See Security Considerations above. + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + /** + * @dev Returns the current nonce for `owner`. This value must be + * included whenever a signature is generated for {permit}. + * + * Every successful call to {permit} increases ``owner``'s nonce by one. This + * prevents a signature from being used multiple times. + */ + function nonces(address owner) external view returns (uint256); + + /** + * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32); +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/utils/SafeERC20.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/utils/SafeERC20.sol new file mode 100644 index 00000000000..bb65709b46b --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/utils/SafeERC20.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/utils/SafeERC20.sol) + +pragma solidity ^0.8.20; + +import {IERC20} from "../IERC20.sol"; +import {IERC20Permit} from "../extensions/IERC20Permit.sol"; +import {Address} from "../../../utils/Address.sol"; + +/** + * @title SafeERC20 + * @dev Wrappers around ERC20 operations that throw on failure (when the token + * contract returns false). Tokens that return no value (and instead revert or + * throw on failure) are also supported, non-reverting calls are assumed to be + * successful. + * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract, + * which allows you to call the safe operations as `token.safeTransfer(...)`, etc. + */ +library SafeERC20 { + using Address for address; + + /** + * @dev An operation with an ERC20 token failed. + */ + error SafeERC20FailedOperation(address token); + + /** + * @dev Indicates a failed `decreaseAllowance` request. + */ + error SafeERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease); + + /** + * @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value, + * non-reverting calls are assumed to be successful. + */ + function safeTransfer(IERC20 token, address to, uint256 value) internal { + _callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value))); + } + + /** + * @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the + * calling contract. If `token` returns no value, non-reverting calls are assumed to be successful. + */ + function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal { + _callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value))); + } + + /** + * @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value, + * non-reverting calls are assumed to be successful. + */ + function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal { + uint256 oldAllowance = token.allowance(address(this), spender); + forceApprove(token, spender, oldAllowance + value); + } + + /** + * @dev Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no + * value, non-reverting calls are assumed to be successful. + */ + function safeDecreaseAllowance(IERC20 token, address spender, uint256 requestedDecrease) internal { + unchecked { + uint256 currentAllowance = token.allowance(address(this), spender); + if (currentAllowance < requestedDecrease) { + revert SafeERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease); + } + forceApprove(token, spender, currentAllowance - requestedDecrease); + } + } + + /** + * @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value, + * non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval + * to be set to zero before setting it to a non-zero value, such as USDT. + */ + function forceApprove(IERC20 token, address spender, uint256 value) internal { + bytes memory approvalCall = abi.encodeCall(token.approve, (spender, value)); + + if (!_callOptionalReturnBool(token, approvalCall)) { + _callOptionalReturn(token, abi.encodeCall(token.approve, (spender, 0))); + _callOptionalReturn(token, approvalCall); + } + } + + /** + * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement + * on the return value: the return value is optional (but if data is returned, it must not be false). + * @param token The token targeted by the call. + * @param data The call data (encoded using abi.encode or one of its variants). + */ + function _callOptionalReturn(IERC20 token, bytes memory data) private { + // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since + // we're implementing it ourselves. We use {Address-functionCall} to perform this call, which verifies that + // the target address contains contract code and also asserts for success in the low-level call. + + bytes memory returndata = address(token).functionCall(data); + if (returndata.length != 0 && !abi.decode(returndata, (bool))) { + revert SafeERC20FailedOperation(address(token)); + } + } + + /** + * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement + * on the return value: the return value is optional (but if data is returned, it must not be false). + * @param token The token targeted by the call. + * @param data The call data (encoded using abi.encode or one of its variants). + * + * This is a variant of {_callOptionalReturn} that silents catches all reverts and returns a bool instead. + */ + function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) { + // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since + // we're implementing it ourselves. We cannot use {Address-functionCall} here since this should return false + // and not revert is the subcall reverts. + + (bool success, bytes memory returndata) = address(token).call(data); + return success && (returndata.length == 0 || abi.decode(returndata, (bool))) && address(token).code.length > 0; + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Address.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Address.sol new file mode 100644 index 00000000000..b7e3059529a --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Address.sol @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/Address.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Collection of functions related to the address type + */ +library Address { + /** + * @dev The ETH balance of the account is not enough to perform the operation. + */ + error AddressInsufficientBalance(address account); + + /** + * @dev There's no code at `target` (it is not a contract). + */ + error AddressEmptyCode(address target); + + /** + * @dev A call to an address target failed. The target may have reverted. + */ + error FailedInnerCall(); + + /** + * @dev Replacement for Solidity's `transfer`: sends `amount` wei to + * `recipient`, forwarding all available gas and reverting on errors. + * + * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost + * of certain opcodes, possibly making contracts go over the 2300 gas limit + * imposed by `transfer`, making them unable to receive funds via + * `transfer`. {sendValue} removes this limitation. + * + * https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/[Learn more]. + * + * IMPORTANT: because control is transferred to `recipient`, care must be + * taken to not create reentrancy vulnerabilities. Consider using + * {ReentrancyGuard} or the + * https://solidity.readthedocs.io/en/v0.8.20/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. + */ + function sendValue(address payable recipient, uint256 amount) internal { + if (address(this).balance < amount) { + revert AddressInsufficientBalance(address(this)); + } + + (bool success, ) = recipient.call{value: amount}(""); + if (!success) { + revert FailedInnerCall(); + } + } + + /** + * @dev Performs a Solidity function call using a low level `call`. A + * plain `call` is an unsafe replacement for a function call: use this + * function instead. + * + * If `target` reverts with a revert reason or custom error, it is bubbled + * up by this function (like regular Solidity function calls). However, if + * the call reverted with no returned reason, this function reverts with a + * {FailedInnerCall} error. + * + * Returns the raw returned data. To convert to the expected return value, + * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. + * + * Requirements: + * + * - `target` must be a contract. + * - calling `target` with `data` must not revert. + */ + function functionCall(address target, bytes memory data) internal returns (bytes memory) { + return functionCallWithValue(target, data, 0); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but also transferring `value` wei to `target`. + * + * Requirements: + * + * - the calling contract must have an ETH balance of at least `value`. + * - the called Solidity function must be `payable`. + */ + function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) { + if (address(this).balance < value) { + revert AddressInsufficientBalance(address(this)); + } + (bool success, bytes memory returndata) = target.call{value: value}(data); + return verifyCallResultFromTarget(target, success, returndata); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a static call. + */ + function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) { + (bool success, bytes memory returndata) = target.staticcall(data); + return verifyCallResultFromTarget(target, success, returndata); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a delegate call. + */ + function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) { + (bool success, bytes memory returndata) = target.delegatecall(data); + return verifyCallResultFromTarget(target, success, returndata); + } + + /** + * @dev Tool to verify that a low level call to smart-contract was successful, and reverts if the target + * was not a contract or bubbling up the revert reason (falling back to {FailedInnerCall}) in case of an + * unsuccessful call. + */ + function verifyCallResultFromTarget( + address target, + bool success, + bytes memory returndata + ) internal view returns (bytes memory) { + if (!success) { + _revert(returndata); + } else { + // only check if target is a contract if the call was successful and the return data is empty + // otherwise we already know that it was a contract + if (returndata.length == 0 && target.code.length == 0) { + revert AddressEmptyCode(target); + } + return returndata; + } + } + + /** + * @dev Tool to verify that a low level call was successful, and reverts if it wasn't, either by bubbling the + * revert reason or with a default {FailedInnerCall} error. + */ + function verifyCallResult(bool success, bytes memory returndata) internal pure returns (bytes memory) { + if (!success) { + _revert(returndata); + } else { + return returndata; + } + } + + /** + * @dev Reverts with returndata if present. Otherwise reverts with {FailedInnerCall}. + */ + function _revert(bytes memory returndata) private pure { + // Look for revert reason and bubble it up if present + if (returndata.length > 0) { + // The easiest way to bubble the revert reason is using memory via assembly + /// @solidity memory-safe-assembly + assembly { + let returndata_size := mload(returndata) + revert(add(32, returndata), returndata_size) + } + } else { + revert FailedInnerCall(); + } + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Context.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Context.sol new file mode 100644 index 00000000000..4e535fe03c2 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Context.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Provides information about the current execution context, including the + * sender of the transaction and its data. While these are generally available + * via msg.sender and msg.data, they should not be accessed in such a direct + * manner, since when dealing with meta-transactions the account sending and + * paying for execution may not be the actual sender (as far as an application + * is concerned). + * + * This contract is only required for intermediate, library-like contracts. + */ +abstract contract Context { + function _msgSender() internal view virtual returns (address) { + return msg.sender; + } + + function _msgData() internal view virtual returns (bytes calldata) { + return msg.data; + } + + function _contextSuffixLength() internal view virtual returns (uint256) { + return 0; + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Pausable.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Pausable.sol new file mode 100644 index 00000000000..312f1cb90fe --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Pausable.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/Pausable.sol) + +pragma solidity ^0.8.20; + +import {Context} from "../utils/Context.sol"; + +/** + * @dev Contract module which allows children to implement an emergency stop + * mechanism that can be triggered by an authorized account. + * + * This module is used through inheritance. It will make available the + * modifiers `whenNotPaused` and `whenPaused`, which can be applied to + * the functions of your contract. Note that they will not be pausable by + * simply including this module, only once the modifiers are put in place. + */ +abstract contract Pausable is Context { + bool private _paused; + + /** + * @dev Emitted when the pause is triggered by `account`. + */ + event Paused(address account); + + /** + * @dev Emitted when the pause is lifted by `account`. + */ + event Unpaused(address account); + + /** + * @dev The operation failed because the contract is paused. + */ + error EnforcedPause(); + + /** + * @dev The operation failed because the contract is not paused. + */ + error ExpectedPause(); + + /** + * @dev Initializes the contract in unpaused state. + */ + constructor() { + _paused = false; + } + + /** + * @dev Modifier to make a function callable only when the contract is not paused. + * + * Requirements: + * + * - The contract must not be paused. + */ + modifier whenNotPaused() { + _requireNotPaused(); + _; + } + + /** + * @dev Modifier to make a function callable only when the contract is paused. + * + * Requirements: + * + * - The contract must be paused. + */ + modifier whenPaused() { + _requirePaused(); + _; + } + + /** + * @dev Returns true if the contract is paused, and false otherwise. + */ + function paused() public view virtual returns (bool) { + return _paused; + } + + /** + * @dev Throws if the contract is paused. + */ + function _requireNotPaused() internal view virtual { + if (paused()) { + revert EnforcedPause(); + } + } + + /** + * @dev Throws if the contract is not paused. + */ + function _requirePaused() internal view virtual { + if (!paused()) { + revert ExpectedPause(); + } + } + + /** + * @dev Triggers stopped state. + * + * Requirements: + * + * - The contract must not be paused. + */ + function _pause() internal virtual whenNotPaused { + _paused = true; + emit Paused(_msgSender()); + } + + /** + * @dev Returns to normal state. + * + * Requirements: + * + * - The contract must be paused. + */ + function _unpause() internal virtual whenPaused { + _paused = false; + emit Unpaused(_msgSender()); + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/ShortStrings.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/ShortStrings.sol new file mode 100644 index 00000000000..fdfe774d635 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/ShortStrings.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/ShortStrings.sol) + +pragma solidity ^0.8.20; + +import {StorageSlot} from "./StorageSlot.sol"; + +// | string | 0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA | +// | length | 0x BB | +type ShortString is bytes32; + +/** + * @dev This library provides functions to convert short memory strings + * into a `ShortString` type that can be used as an immutable variable. + * + * Strings of arbitrary length can be optimized using this library if + * they are short enough (up to 31 bytes) by packing them with their + * length (1 byte) in a single EVM word (32 bytes). Additionally, a + * fallback mechanism can be used for every other case. + * + * Usage example: + * + * ```solidity + * contract Named { + * using ShortStrings for *; + * + * ShortString private immutable _name; + * string private _nameFallback; + * + * constructor(string memory contractName) { + * _name = contractName.toShortStringWithFallback(_nameFallback); + * } + * + * function name() external view returns (string memory) { + * return _name.toStringWithFallback(_nameFallback); + * } + * } + * ``` + */ +library ShortStrings { + // Used as an identifier for strings longer than 31 bytes. + bytes32 private constant FALLBACK_SENTINEL = 0x00000000000000000000000000000000000000000000000000000000000000FF; + + error StringTooLong(string str); + error InvalidShortString(); + + /** + * @dev Encode a string of at most 31 chars into a `ShortString`. + * + * This will trigger a `StringTooLong` error is the input string is too long. + */ + function toShortString(string memory str) internal pure returns (ShortString) { + bytes memory bstr = bytes(str); + if (bstr.length > 31) { + revert StringTooLong(str); + } + return ShortString.wrap(bytes32(uint256(bytes32(bstr)) | bstr.length)); + } + + /** + * @dev Decode a `ShortString` back to a "normal" string. + */ + function toString(ShortString sstr) internal pure returns (string memory) { + uint256 len = byteLength(sstr); + // using `new string(len)` would work locally but is not memory safe. + string memory str = new string(32); + /// @solidity memory-safe-assembly + assembly { + mstore(str, len) + mstore(add(str, 0x20), sstr) + } + return str; + } + + /** + * @dev Return the length of a `ShortString`. + */ + function byteLength(ShortString sstr) internal pure returns (uint256) { + uint256 result = uint256(ShortString.unwrap(sstr)) & 0xFF; + if (result > 31) { + revert InvalidShortString(); + } + return result; + } + + /** + * @dev Encode a string into a `ShortString`, or write it to storage if it is too long. + */ + function toShortStringWithFallback(string memory value, string storage store) internal returns (ShortString) { + if (bytes(value).length < 32) { + return toShortString(value); + } else { + StorageSlot.getStringSlot(store).value = value; + return ShortString.wrap(FALLBACK_SENTINEL); + } + } + + /** + * @dev Decode a string that was encoded to `ShortString` or written to storage using {setWithFallback}. + */ + function toStringWithFallback(ShortString value, string storage store) internal pure returns (string memory) { + if (ShortString.unwrap(value) != FALLBACK_SENTINEL) { + return toString(value); + } else { + return store; + } + } + + /** + * @dev Return the length of a string that was encoded to `ShortString` or written to storage using + * {setWithFallback}. + * + * WARNING: This will return the "byte length" of the string. This may not reflect the actual length in terms of + * actual characters as the UTF-8 encoding of a single character can span over multiple bytes. + */ + function byteLengthWithFallback(ShortString value, string storage store) internal view returns (uint256) { + if (ShortString.unwrap(value) != FALLBACK_SENTINEL) { + return byteLength(value); + } else { + return bytes(store).length; + } + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/StorageSlot.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/StorageSlot.sol new file mode 100644 index 00000000000..08418327a59 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/StorageSlot.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/StorageSlot.sol) +// This file was procedurally generated from scripts/generate/templates/StorageSlot.js. + +pragma solidity ^0.8.20; + +/** + * @dev Library for reading and writing primitive types to specific storage slots. + * + * Storage slots are often used to avoid storage conflict when dealing with upgradeable contracts. + * This library helps with reading and writing to such slots without the need for inline assembly. + * + * The functions in this library return Slot structs that contain a `value` member that can be used to read or write. + * + * Example usage to set ERC1967 implementation slot: + * ```solidity + * contract ERC1967 { + * bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + * + * function _getImplementation() internal view returns (address) { + * return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; + * } + * + * function _setImplementation(address newImplementation) internal { + * require(newImplementation.code.length > 0); + * StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; + * } + * } + * ``` + */ +library StorageSlot { + struct AddressSlot { + address value; + } + + struct BooleanSlot { + bool value; + } + + struct Bytes32Slot { + bytes32 value; + } + + struct Uint256Slot { + uint256 value; + } + + struct StringSlot { + string value; + } + + struct BytesSlot { + bytes value; + } + + /** + * @dev Returns an `AddressSlot` with member `value` located at `slot`. + */ + function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `BooleanSlot` with member `value` located at `slot`. + */ + function getBooleanSlot(bytes32 slot) internal pure returns (BooleanSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `Bytes32Slot` with member `value` located at `slot`. + */ + function getBytes32Slot(bytes32 slot) internal pure returns (Bytes32Slot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `Uint256Slot` with member `value` located at `slot`. + */ + function getUint256Slot(bytes32 slot) internal pure returns (Uint256Slot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `StringSlot` with member `value` located at `slot`. + */ + function getStringSlot(bytes32 slot) internal pure returns (StringSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `StringSlot` representation of the string storage pointer `store`. + */ + function getStringSlot(string storage store) internal pure returns (StringSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := store.slot + } + } + + /** + * @dev Returns an `BytesSlot` with member `value` located at `slot`. + */ + function getBytesSlot(bytes32 slot) internal pure returns (BytesSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `BytesSlot` representation of the bytes storage pointer `store`. + */ + function getBytesSlot(bytes storage store) internal pure returns (BytesSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := store.slot + } + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Strings.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Strings.sol new file mode 100644 index 00000000000..b2c0a40fb2a --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Strings.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/Strings.sol) + +pragma solidity ^0.8.20; + +import {Math} from "./math/Math.sol"; +import {SignedMath} from "./math/SignedMath.sol"; + +/** + * @dev String operations. + */ +library Strings { + bytes16 private constant HEX_DIGITS = "0123456789abcdef"; + uint8 private constant ADDRESS_LENGTH = 20; + + /** + * @dev The `value` string doesn't fit in the specified `length`. + */ + error StringsInsufficientHexLength(uint256 value, uint256 length); + + /** + * @dev Converts a `uint256` to its ASCII `string` decimal representation. + */ + function toString(uint256 value) internal pure returns (string memory) { + unchecked { + uint256 length = Math.log10(value) + 1; + string memory buffer = new string(length); + uint256 ptr; + /// @solidity memory-safe-assembly + assembly { + ptr := add(buffer, add(32, length)) + } + while (true) { + ptr--; + /// @solidity memory-safe-assembly + assembly { + mstore8(ptr, byte(mod(value, 10), HEX_DIGITS)) + } + value /= 10; + if (value == 0) break; + } + return buffer; + } + } + + /** + * @dev Converts a `int256` to its ASCII `string` decimal representation. + */ + function toStringSigned(int256 value) internal pure returns (string memory) { + return string.concat(value < 0 ? "-" : "", toString(SignedMath.abs(value))); + } + + /** + * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation. + */ + function toHexString(uint256 value) internal pure returns (string memory) { + unchecked { + return toHexString(value, Math.log256(value) + 1); + } + } + + /** + * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length. + */ + function toHexString(uint256 value, uint256 length) internal pure returns (string memory) { + uint256 localValue = value; + bytes memory buffer = new bytes(2 * length + 2); + buffer[0] = "0"; + buffer[1] = "x"; + for (uint256 i = 2 * length + 1; i > 1; --i) { + buffer[i] = HEX_DIGITS[localValue & 0xf]; + localValue >>= 4; + } + if (localValue != 0) { + revert StringsInsufficientHexLength(value, length); + } + return string(buffer); + } + + /** + * @dev Converts an `address` with fixed length of 20 bytes to its not checksummed ASCII `string` hexadecimal + * representation. + */ + function toHexString(address addr) internal pure returns (string memory) { + return toHexString(uint256(uint160(addr)), ADDRESS_LENGTH); + } + + /** + * @dev Returns true if the two strings are equal. + */ + function equal(string memory a, string memory b) internal pure returns (bool) { + return bytes(a).length == bytes(b).length && keccak256(bytes(a)) == keccak256(bytes(b)); + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/cryptography/ECDSA.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/cryptography/ECDSA.sol new file mode 100644 index 00000000000..04b3e5e0646 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/cryptography/ECDSA.sol @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/cryptography/ECDSA.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations. + * + * These functions can be used to verify that a message was signed by the holder + * of the private keys of a given address. + */ +library ECDSA { + enum RecoverError { + NoError, + InvalidSignature, + InvalidSignatureLength, + InvalidSignatureS + } + + /** + * @dev The signature derives the `address(0)`. + */ + error ECDSAInvalidSignature(); + + /** + * @dev The signature has an invalid length. + */ + error ECDSAInvalidSignatureLength(uint256 length); + + /** + * @dev The signature has an S value that is in the upper half order. + */ + error ECDSAInvalidSignatureS(bytes32 s); + + /** + * @dev Returns the address that signed a hashed message (`hash`) with `signature` or an error. This will not + * return address(0) without also returning an error description. Errors are documented using an enum (error type) + * and a bytes32 providing additional information about the error. + * + * If no error is returned, then the address can be used for verification purposes. + * + * The `ecrecover` EVM precompile allows for malleable (non-unique) signatures: + * this function rejects them by requiring the `s` value to be in the lower + * half order, and the `v` value to be either 27 or 28. + * + * IMPORTANT: `hash` _must_ be the result of a hash operation for the + * verification to be secure: it is possible to craft signatures that + * recover to arbitrary addresses for non-hashed data. A safe way to ensure + * this is by receiving a hash of the original message (which may otherwise + * be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it. + * + * Documentation for signature generation: + * - with https://web3js.readthedocs.io/en/v1.3.4/web3-eth-accounts.html#sign[Web3.js] + * - with https://docs.ethers.io/v5/api/signer/#Signer-signMessage[ethers] + */ + function tryRecover(bytes32 hash, bytes memory signature) internal pure returns (address, RecoverError, bytes32) { + if (signature.length == 65) { + bytes32 r; + bytes32 s; + uint8 v; + // ecrecover takes the signature parameters, and the only way to get them + // currently is to use assembly. + /// @solidity memory-safe-assembly + assembly { + r := mload(add(signature, 0x20)) + s := mload(add(signature, 0x40)) + v := byte(0, mload(add(signature, 0x60))) + } + return tryRecover(hash, v, r, s); + } else { + return (address(0), RecoverError.InvalidSignatureLength, bytes32(signature.length)); + } + } + + /** + * @dev Returns the address that signed a hashed message (`hash`) with + * `signature`. This address can then be used for verification purposes. + * + * The `ecrecover` EVM precompile allows for malleable (non-unique) signatures: + * this function rejects them by requiring the `s` value to be in the lower + * half order, and the `v` value to be either 27 or 28. + * + * IMPORTANT: `hash` _must_ be the result of a hash operation for the + * verification to be secure: it is possible to craft signatures that + * recover to arbitrary addresses for non-hashed data. A safe way to ensure + * this is by receiving a hash of the original message (which may otherwise + * be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it. + */ + function recover(bytes32 hash, bytes memory signature) internal pure returns (address) { + (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, signature); + _throwError(error, errorArg); + return recovered; + } + + /** + * @dev Overload of {ECDSA-tryRecover} that receives the `r` and `vs` short-signature fields separately. + * + * See https://eips.ethereum.org/EIPS/eip-2098[EIP-2098 short signatures] + */ + function tryRecover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address, RecoverError, bytes32) { + unchecked { + bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff); + // We do not check for an overflow here since the shift operation results in 0 or 1. + uint8 v = uint8((uint256(vs) >> 255) + 27); + return tryRecover(hash, v, r, s); + } + } + + /** + * @dev Overload of {ECDSA-recover} that receives the `r and `vs` short-signature fields separately. + */ + function recover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address) { + (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, r, vs); + _throwError(error, errorArg); + return recovered; + } + + /** + * @dev Overload of {ECDSA-tryRecover} that receives the `v`, + * `r` and `s` signature fields separately. + */ + function tryRecover( + bytes32 hash, + uint8 v, + bytes32 r, + bytes32 s + ) internal pure returns (address, RecoverError, bytes32) { + // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature + // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines + // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most + // signatures from current libraries generate a unique signature with an s-value in the lower half order. + // + // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value + // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or + // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept + // these malleable signatures as well. + if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { + return (address(0), RecoverError.InvalidSignatureS, s); + } + + // If the signature is valid (and not malleable), return the signer address + address signer = ecrecover(hash, v, r, s); + if (signer == address(0)) { + return (address(0), RecoverError.InvalidSignature, bytes32(0)); + } + + return (signer, RecoverError.NoError, bytes32(0)); + } + + /** + * @dev Overload of {ECDSA-recover} that receives the `v`, + * `r` and `s` signature fields separately. + */ + function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) { + (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, v, r, s); + _throwError(error, errorArg); + return recovered; + } + + /** + * @dev Optionally reverts with the corresponding custom error according to the `error` argument provided. + */ + function _throwError(RecoverError error, bytes32 errorArg) private pure { + if (error == RecoverError.NoError) { + return; // no error: do nothing + } else if (error == RecoverError.InvalidSignature) { + revert ECDSAInvalidSignature(); + } else if (error == RecoverError.InvalidSignatureLength) { + revert ECDSAInvalidSignatureLength(uint256(errorArg)); + } else if (error == RecoverError.InvalidSignatureS) { + revert ECDSAInvalidSignatureS(errorArg); + } + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/cryptography/EIP712.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/cryptography/EIP712.sol new file mode 100644 index 00000000000..8e548cdd8f0 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/cryptography/EIP712.sol @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/cryptography/EIP712.sol) + +pragma solidity ^0.8.20; + +import {MessageHashUtils} from "./MessageHashUtils.sol"; +import {ShortStrings, ShortString} from "../ShortStrings.sol"; +import {IERC5267} from "../../interfaces/IERC5267.sol"; + +/** + * @dev https://eips.ethereum.org/EIPS/eip-712[EIP 712] is a standard for hashing and signing of typed structured data. + * + * The encoding scheme specified in the EIP requires a domain separator and a hash of the typed structured data, whose + * encoding is very generic and therefore its implementation in Solidity is not feasible, thus this contract + * does not implement the encoding itself. Protocols need to implement the type-specific encoding they need in order to + * produce the hash of their typed data using a combination of `abi.encode` and `keccak256`. + * + * This contract implements the EIP 712 domain separator ({_domainSeparatorV4}) that is used as part of the encoding + * scheme, and the final step of the encoding to obtain the message digest that is then signed via ECDSA + * ({_hashTypedDataV4}). + * + * The implementation of the domain separator was designed to be as efficient as possible while still properly updating + * the chain id to protect against replay attacks on an eventual fork of the chain. + * + * NOTE: This contract implements the version of the encoding known as "v4", as implemented by the JSON RPC method + * https://docs.metamask.io/guide/signing-data.html[`eth_signTypedDataV4` in MetaMask]. + * + * NOTE: In the upgradeable version of this contract, the cached values will correspond to the address, and the domain + * separator of the implementation contract. This will cause the {_domainSeparatorV4} function to always rebuild the + * separator from the immutable values, which is cheaper than accessing a cached version in cold storage. + * + * @custom:oz-upgrades-unsafe-allow state-variable-immutable + */ +abstract contract EIP712 is IERC5267 { + using ShortStrings for *; + + bytes32 private constant TYPE_HASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + + // Cache the domain separator as an immutable value, but also store the chain id that it corresponds to, in order to + // invalidate the cached domain separator if the chain id changes. + bytes32 private immutable _cachedDomainSeparator; + uint256 private immutable _cachedChainId; + address private immutable _cachedThis; + + bytes32 private immutable _hashedName; + bytes32 private immutable _hashedVersion; + + ShortString private immutable _name; + ShortString private immutable _version; + string private _nameFallback; + string private _versionFallback; + + /** + * @dev Initializes the domain separator and parameter caches. + * + * The meaning of `name` and `version` is specified in + * https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator[EIP 712]: + * + * - `name`: the user readable name of the signing domain, i.e. the name of the DApp or the protocol. + * - `version`: the current major version of the signing domain. + * + * NOTE: These parameters cannot be changed except through a xref:learn::upgrading-smart-contracts.adoc[smart + * contract upgrade]. + */ + constructor(string memory name, string memory version) { + _name = name.toShortStringWithFallback(_nameFallback); + _version = version.toShortStringWithFallback(_versionFallback); + _hashedName = keccak256(bytes(name)); + _hashedVersion = keccak256(bytes(version)); + + _cachedChainId = block.chainid; + _cachedDomainSeparator = _buildDomainSeparator(); + _cachedThis = address(this); + } + + /** + * @dev Returns the domain separator for the current chain. + */ + function _domainSeparatorV4() internal view returns (bytes32) { + if (address(this) == _cachedThis && block.chainid == _cachedChainId) { + return _cachedDomainSeparator; + } else { + return _buildDomainSeparator(); + } + } + + function _buildDomainSeparator() private view returns (bytes32) { + return keccak256(abi.encode(TYPE_HASH, _hashedName, _hashedVersion, block.chainid, address(this))); + } + + /** + * @dev Given an already https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct[hashed struct], this + * function returns the hash of the fully encoded EIP712 message for this domain. + * + * This hash can be used together with {ECDSA-recover} to obtain the signer of a message. For example: + * + * ```solidity + * bytes32 digest = _hashTypedDataV4(keccak256(abi.encode( + * keccak256("Mail(address to,string contents)"), + * mailTo, + * keccak256(bytes(mailContents)) + * ))); + * address signer = ECDSA.recover(digest, signature); + * ``` + */ + function _hashTypedDataV4(bytes32 structHash) internal view virtual returns (bytes32) { + return MessageHashUtils.toTypedDataHash(_domainSeparatorV4(), structHash); + } + + /** + * @dev See {IERC-5267}. + */ + function eip712Domain() + public + view + virtual + returns ( + bytes1 fields, + string memory name, + string memory version, + uint256 chainId, + address verifyingContract, + bytes32 salt, + uint256[] memory extensions + ) + { + return ( + hex"0f", // 01111 + _EIP712Name(), + _EIP712Version(), + block.chainid, + address(this), + bytes32(0), + new uint256[](0) + ); + } + + /** + * @dev The name parameter for the EIP712 domain. + * + * NOTE: By default this function reads _name which is an immutable value. + * It only reads from storage if necessary (in case the value is too large to fit in a ShortString). + */ + // solhint-disable-next-line func-name-mixedcase + function _EIP712Name() internal view returns (string memory) { + return _name.toStringWithFallback(_nameFallback); + } + + /** + * @dev The version parameter for the EIP712 domain. + * + * NOTE: By default this function reads _version which is an immutable value. + * It only reads from storage if necessary (in case the value is too large to fit in a ShortString). + */ + // solhint-disable-next-line func-name-mixedcase + function _EIP712Version() internal view returns (string memory) { + return _version.toStringWithFallback(_versionFallback); + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/cryptography/MessageHashUtils.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/cryptography/MessageHashUtils.sol new file mode 100644 index 00000000000..8836693e79b --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/cryptography/MessageHashUtils.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/cryptography/MessageHashUtils.sol) + +pragma solidity ^0.8.20; + +import {Strings} from "../Strings.sol"; + +/** + * @dev Signature message hash utilities for producing digests to be consumed by {ECDSA} recovery or signing. + * + * The library provides methods for generating a hash of a message that conforms to the + * https://eips.ethereum.org/EIPS/eip-191[EIP 191] and https://eips.ethereum.org/EIPS/eip-712[EIP 712] + * specifications. + */ +library MessageHashUtils { + /** + * @dev Returns the keccak256 digest of an EIP-191 signed data with version + * `0x45` (`personal_sign` messages). + * + * The digest is calculated by prefixing a bytes32 `messageHash` with + * `"\x19Ethereum Signed Message:\n32"` and hashing the result. It corresponds with the + * hash signed when using the https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`] JSON-RPC method. + * + * NOTE: The `messageHash` parameter is intended to be the result of hashing a raw message with + * keccak256, although any bytes32 value can be safely used because the final digest will + * be re-hashed. + * + * See {ECDSA-recover}. + */ + function toEthSignedMessageHash(bytes32 messageHash) internal pure returns (bytes32 digest) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, "\x19Ethereum Signed Message:\n32") // 32 is the bytes-length of messageHash + mstore(0x1c, messageHash) // 0x1c (28) is the length of the prefix + digest := keccak256(0x00, 0x3c) // 0x3c is the length of the prefix (0x1c) + messageHash (0x20) + } + } + + /** + * @dev Returns the keccak256 digest of an EIP-191 signed data with version + * `0x45` (`personal_sign` messages). + * + * The digest is calculated by prefixing an arbitrary `message` with + * `"\x19Ethereum Signed Message:\n" + len(message)` and hashing the result. It corresponds with the + * hash signed when using the https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`] JSON-RPC method. + * + * See {ECDSA-recover}. + */ + function toEthSignedMessageHash(bytes memory message) internal pure returns (bytes32) { + return + keccak256(bytes.concat("\x19Ethereum Signed Message:\n", bytes(Strings.toString(message.length)), message)); + } + + /** + * @dev Returns the keccak256 digest of an EIP-191 signed data with version + * `0x00` (data with intended validator). + * + * The digest is calculated by prefixing an arbitrary `data` with `"\x19\x00"` and the intended + * `validator` address. Then hashing the result. + * + * See {ECDSA-recover}. + */ + function toDataWithIntendedValidatorHash(address validator, bytes memory data) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(hex"19_00", validator, data)); + } + + /** + * @dev Returns the keccak256 digest of an EIP-712 typed data (EIP-191 version `0x01`). + * + * The digest is calculated from a `domainSeparator` and a `structHash`, by prefixing them with + * `\x19\x01` and hashing the result. It corresponds to the hash signed by the + * https://eips.ethereum.org/EIPS/eip-712[`eth_signTypedData`] JSON-RPC method as part of EIP-712. + * + * See {ECDSA-recover}. + */ + function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internal pure returns (bytes32 digest) { + /// @solidity memory-safe-assembly + assembly { + let ptr := mload(0x40) + mstore(ptr, hex"19_01") + mstore(add(ptr, 0x02), domainSeparator) + mstore(add(ptr, 0x22), structHash) + digest := keccak256(ptr, 0x42) + } + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/introspection/ERC165.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/introspection/ERC165.sol new file mode 100644 index 00000000000..1e77b60d739 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/introspection/ERC165.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/introspection/ERC165.sol) + +pragma solidity ^0.8.20; + +import {IERC165} from "./IERC165.sol"; + +/** + * @dev Implementation of the {IERC165} interface. + * + * Contracts that want to implement ERC165 should inherit from this contract and override {supportsInterface} to check + * for the additional interface id that will be supported. For example: + * + * ```solidity + * function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + * return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId); + * } + * ``` + */ +abstract contract ERC165 is IERC165 { + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { + return interfaceId == type(IERC165).interfaceId; + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/introspection/ERC165Checker.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/introspection/ERC165Checker.sol new file mode 100644 index 00000000000..7b52241446d --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/introspection/ERC165Checker.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/introspection/ERC165Checker.sol) + +pragma solidity ^0.8.20; + +import {IERC165} from "./IERC165.sol"; + +/** + * @dev Library used to query support of an interface declared via {IERC165}. + * + * Note that these functions return the actual result of the query: they do not + * `revert` if an interface is not supported. It is up to the caller to decide + * what to do in these cases. + */ +library ERC165Checker { + // As per the EIP-165 spec, no interface should ever match 0xffffffff + bytes4 private constant INTERFACE_ID_INVALID = 0xffffffff; + + /** + * @dev Returns true if `account` supports the {IERC165} interface. + */ + function supportsERC165(address account) internal view returns (bool) { + // Any contract that implements ERC165 must explicitly indicate support of + // InterfaceId_ERC165 and explicitly indicate non-support of InterfaceId_Invalid + return + supportsERC165InterfaceUnchecked(account, type(IERC165).interfaceId) && + !supportsERC165InterfaceUnchecked(account, INTERFACE_ID_INVALID); + } + + /** + * @dev Returns true if `account` supports the interface defined by + * `interfaceId`. Support for {IERC165} itself is queried automatically. + * + * See {IERC165-supportsInterface}. + */ + function supportsInterface(address account, bytes4 interfaceId) internal view returns (bool) { + // query support of both ERC165 as per the spec and support of _interfaceId + return supportsERC165(account) && supportsERC165InterfaceUnchecked(account, interfaceId); + } + + /** + * @dev Returns a boolean array where each value corresponds to the + * interfaces passed in and whether they're supported or not. This allows + * you to batch check interfaces for a contract where your expectation + * is that some interfaces may not be supported. + * + * See {IERC165-supportsInterface}. + */ + function getSupportedInterfaces( + address account, + bytes4[] memory interfaceIds + ) internal view returns (bool[] memory) { + // an array of booleans corresponding to interfaceIds and whether they're supported or not + bool[] memory interfaceIdsSupported = new bool[](interfaceIds.length); + + // query support of ERC165 itself + if (supportsERC165(account)) { + // query support of each interface in interfaceIds + for (uint256 i = 0; i < interfaceIds.length; i++) { + interfaceIdsSupported[i] = supportsERC165InterfaceUnchecked(account, interfaceIds[i]); + } + } + + return interfaceIdsSupported; + } + + /** + * @dev Returns true if `account` supports all the interfaces defined in + * `interfaceIds`. Support for {IERC165} itself is queried automatically. + * + * Batch-querying can lead to gas savings by skipping repeated checks for + * {IERC165} support. + * + * See {IERC165-supportsInterface}. + */ + function supportsAllInterfaces(address account, bytes4[] memory interfaceIds) internal view returns (bool) { + // query support of ERC165 itself + if (!supportsERC165(account)) { + return false; + } + + // query support of each interface in interfaceIds + for (uint256 i = 0; i < interfaceIds.length; i++) { + if (!supportsERC165InterfaceUnchecked(account, interfaceIds[i])) { + return false; + } + } + + // all interfaces supported + return true; + } + + /** + * @notice Query if a contract implements an interface, does not check ERC165 support + * @param account The address of the contract to query for support of an interface + * @param interfaceId The interface identifier, as specified in ERC-165 + * @return true if the contract at account indicates support of the interface with + * identifier interfaceId, false otherwise + * @dev Assumes that account contains a contract that supports ERC165, otherwise + * the behavior of this method is undefined. This precondition can be checked + * with {supportsERC165}. + * + * Some precompiled contracts will falsely indicate support for a given interface, so caution + * should be exercised when using this function. + * + * Interface identification is specified in ERC-165. + */ + function supportsERC165InterfaceUnchecked(address account, bytes4 interfaceId) internal view returns (bool) { + // prepare call + bytes memory encodedParams = abi.encodeCall(IERC165.supportsInterface, (interfaceId)); + + // perform static call + bool success; + uint256 returnSize; + uint256 returnValue; + assembly { + success := staticcall(30000, account, add(encodedParams, 0x20), mload(encodedParams), 0x00, 0x20) + returnSize := returndatasize() + returnValue := mload(0x00) + } + + return success && returnSize >= 0x20 && returnValue > 0; + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/introspection/IERC165.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/introspection/IERC165.sol new file mode 100644 index 00000000000..c09f31fe128 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/introspection/IERC165.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/introspection/IERC165.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Interface of the ERC165 standard, as defined in the + * https://eips.ethereum.org/EIPS/eip-165[EIP]. + * + * Implementers can declare support of contract interfaces, which can then be + * queried by others ({ERC165Checker}). + * + * For an implementation, see {ERC165}. + */ +interface IERC165 { + /** + * @dev Returns true if this contract implements the interface defined by + * `interfaceId`. See the corresponding + * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] + * to learn more about how these ids are created. + * + * This function call must use less than 30 000 gas. + */ + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/math/Math.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/math/Math.sol new file mode 100644 index 00000000000..9681524529b --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/math/Math.sol @@ -0,0 +1,415 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/math/Math.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Standard math utilities missing in the Solidity language. + */ +library Math { + /** + * @dev Muldiv operation overflow. + */ + error MathOverflowedMulDiv(); + + enum Rounding { + Floor, // Toward negative infinity + Ceil, // Toward positive infinity + Trunc, // Toward zero + Expand // Away from zero + } + + /** + * @dev Returns the addition of two unsigned integers, with an overflow flag. + */ + function tryAdd(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + uint256 c = a + b; + if (c < a) return (false, 0); + return (true, c); + } + } + + /** + * @dev Returns the subtraction of two unsigned integers, with an overflow flag. + */ + function trySub(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + if (b > a) return (false, 0); + return (true, a - b); + } + } + + /** + * @dev Returns the multiplication of two unsigned integers, with an overflow flag. + */ + function tryMul(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + // Gas optimization: this is cheaper than requiring 'a' not being zero, but the + // benefit is lost if 'b' is also tested. + // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 + if (a == 0) return (true, 0); + uint256 c = a * b; + if (c / a != b) return (false, 0); + return (true, c); + } + } + + /** + * @dev Returns the division of two unsigned integers, with a division by zero flag. + */ + function tryDiv(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + if (b == 0) return (false, 0); + return (true, a / b); + } + } + + /** + * @dev Returns the remainder of dividing two unsigned integers, with a division by zero flag. + */ + function tryMod(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + if (b == 0) return (false, 0); + return (true, a % b); + } + } + + /** + * @dev Returns the largest of two numbers. + */ + function max(uint256 a, uint256 b) internal pure returns (uint256) { + return a > b ? a : b; + } + + /** + * @dev Returns the smallest of two numbers. + */ + function min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } + + /** + * @dev Returns the average of two numbers. The result is rounded towards + * zero. + */ + function average(uint256 a, uint256 b) internal pure returns (uint256) { + // (a + b) / 2 can overflow. + return (a & b) + (a ^ b) / 2; + } + + /** + * @dev Returns the ceiling of the division of two numbers. + * + * This differs from standard division with `/` in that it rounds towards infinity instead + * of rounding towards zero. + */ + function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) { + if (b == 0) { + // Guarantee the same behavior as in a regular Solidity division. + return a / b; + } + + // (a + b - 1) / b can overflow on addition, so we distribute. + return a == 0 ? 0 : (a - 1) / b + 1; + } + + /** + * @notice Calculates floor(x * y / denominator) with full precision. Throws if result overflows a uint256 or + * denominator == 0. + * @dev Original credit to Remco Bloemen under MIT license (https://xn--2-umb.com/21/muldiv) with further edits by + * Uniswap Labs also under MIT license. + */ + function mulDiv(uint256 x, uint256 y, uint256 denominator) internal pure returns (uint256 result) { + unchecked { + // 512-bit multiply [prod1 prod0] = x * y. Compute the product mod 2^256 and mod 2^256 - 1, then use + // use the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256 + // variables such that product = prod1 * 2^256 + prod0. + uint256 prod0 = x * y; // Least significant 256 bits of the product + uint256 prod1; // Most significant 256 bits of the product + assembly { + let mm := mulmod(x, y, not(0)) + prod1 := sub(sub(mm, prod0), lt(mm, prod0)) + } + + // Handle non-overflow cases, 256 by 256 division. + if (prod1 == 0) { + // Solidity will revert if denominator == 0, unlike the div opcode on its own. + // The surrounding unchecked block does not change this fact. + // See https://docs.soliditylang.org/en/latest/control-structures.html#checked-or-unchecked-arithmetic. + return prod0 / denominator; + } + + // Make sure the result is less than 2^256. Also prevents denominator == 0. + if (denominator <= prod1) { + revert MathOverflowedMulDiv(); + } + + /////////////////////////////////////////////// + // 512 by 256 division. + /////////////////////////////////////////////// + + // Make division exact by subtracting the remainder from [prod1 prod0]. + uint256 remainder; + assembly { + // Compute remainder using mulmod. + remainder := mulmod(x, y, denominator) + + // Subtract 256 bit number from 512 bit number. + prod1 := sub(prod1, gt(remainder, prod0)) + prod0 := sub(prod0, remainder) + } + + // Factor powers of two out of denominator and compute largest power of two divisor of denominator. + // Always >= 1. See https://cs.stackexchange.com/q/138556/92363. + + uint256 twos = denominator & (0 - denominator); + assembly { + // Divide denominator by twos. + denominator := div(denominator, twos) + + // Divide [prod1 prod0] by twos. + prod0 := div(prod0, twos) + + // Flip twos such that it is 2^256 / twos. If twos is zero, then it becomes one. + twos := add(div(sub(0, twos), twos), 1) + } + + // Shift in bits from prod1 into prod0. + prod0 |= prod1 * twos; + + // Invert denominator mod 2^256. Now that denominator is an odd number, it has an inverse modulo 2^256 such + // that denominator * inv = 1 mod 2^256. Compute the inverse by starting with a seed that is correct for + // four bits. That is, denominator * inv = 1 mod 2^4. + uint256 inverse = (3 * denominator) ^ 2; + + // Use the Newton-Raphson iteration to improve the precision. Thanks to Hensel's lifting lemma, this also + // works in modular arithmetic, doubling the correct bits in each step. + inverse *= 2 - denominator * inverse; // inverse mod 2^8 + inverse *= 2 - denominator * inverse; // inverse mod 2^16 + inverse *= 2 - denominator * inverse; // inverse mod 2^32 + inverse *= 2 - denominator * inverse; // inverse mod 2^64 + inverse *= 2 - denominator * inverse; // inverse mod 2^128 + inverse *= 2 - denominator * inverse; // inverse mod 2^256 + + // Because the division is now exact we can divide by multiplying with the modular inverse of denominator. + // This will give us the correct result modulo 2^256. Since the preconditions guarantee that the outcome is + // less than 2^256, this is the final result. We don't need to compute the high bits of the result and prod1 + // is no longer required. + result = prod0 * inverse; + return result; + } + } + + /** + * @notice Calculates x * y / denominator with full precision, following the selected rounding direction. + */ + function mulDiv(uint256 x, uint256 y, uint256 denominator, Rounding rounding) internal pure returns (uint256) { + uint256 result = mulDiv(x, y, denominator); + if (unsignedRoundsUp(rounding) && mulmod(x, y, denominator) > 0) { + result += 1; + } + return result; + } + + /** + * @dev Returns the square root of a number. If the number is not a perfect square, the value is rounded + * towards zero. + * + * Inspired by Henry S. Warren, Jr.'s "Hacker's Delight" (Chapter 11). + */ + function sqrt(uint256 a) internal pure returns (uint256) { + if (a == 0) { + return 0; + } + + // For our first guess, we get the biggest power of 2 which is smaller than the square root of the target. + // + // We know that the "msb" (most significant bit) of our target number `a` is a power of 2 such that we have + // `msb(a) <= a < 2*msb(a)`. This value can be written `msb(a)=2**k` with `k=log2(a)`. + // + // This can be rewritten `2**log2(a) <= a < 2**(log2(a) + 1)` + // → `sqrt(2**k) <= sqrt(a) < sqrt(2**(k+1))` + // → `2**(k/2) <= sqrt(a) < 2**((k+1)/2) <= 2**(k/2 + 1)` + // + // Consequently, `2**(log2(a) / 2)` is a good first approximation of `sqrt(a)` with at least 1 correct bit. + uint256 result = 1 << (log2(a) >> 1); + + // At this point `result` is an estimation with one bit of precision. We know the true value is a uint128, + // since it is the square root of a uint256. Newton's method converges quadratically (precision doubles at + // every iteration). We thus need at most 7 iteration to turn our partial result with one bit of precision + // into the expected uint128 result. + unchecked { + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + return min(result, a / result); + } + } + + /** + * @notice Calculates sqrt(a), following the selected rounding direction. + */ + function sqrt(uint256 a, Rounding rounding) internal pure returns (uint256) { + unchecked { + uint256 result = sqrt(a); + return result + (unsignedRoundsUp(rounding) && result * result < a ? 1 : 0); + } + } + + /** + * @dev Return the log in base 2 of a positive value rounded towards zero. + * Returns 0 if given 0. + */ + function log2(uint256 value) internal pure returns (uint256) { + uint256 result = 0; + unchecked { + if (value >> 128 > 0) { + value >>= 128; + result += 128; + } + if (value >> 64 > 0) { + value >>= 64; + result += 64; + } + if (value >> 32 > 0) { + value >>= 32; + result += 32; + } + if (value >> 16 > 0) { + value >>= 16; + result += 16; + } + if (value >> 8 > 0) { + value >>= 8; + result += 8; + } + if (value >> 4 > 0) { + value >>= 4; + result += 4; + } + if (value >> 2 > 0) { + value >>= 2; + result += 2; + } + if (value >> 1 > 0) { + result += 1; + } + } + return result; + } + + /** + * @dev Return the log in base 2, following the selected rounding direction, of a positive value. + * Returns 0 if given 0. + */ + function log2(uint256 value, Rounding rounding) internal pure returns (uint256) { + unchecked { + uint256 result = log2(value); + return result + (unsignedRoundsUp(rounding) && 1 << result < value ? 1 : 0); + } + } + + /** + * @dev Return the log in base 10 of a positive value rounded towards zero. + * Returns 0 if given 0. + */ + function log10(uint256 value) internal pure returns (uint256) { + uint256 result = 0; + unchecked { + if (value >= 10 ** 64) { + value /= 10 ** 64; + result += 64; + } + if (value >= 10 ** 32) { + value /= 10 ** 32; + result += 32; + } + if (value >= 10 ** 16) { + value /= 10 ** 16; + result += 16; + } + if (value >= 10 ** 8) { + value /= 10 ** 8; + result += 8; + } + if (value >= 10 ** 4) { + value /= 10 ** 4; + result += 4; + } + if (value >= 10 ** 2) { + value /= 10 ** 2; + result += 2; + } + if (value >= 10 ** 1) { + result += 1; + } + } + return result; + } + + /** + * @dev Return the log in base 10, following the selected rounding direction, of a positive value. + * Returns 0 if given 0. + */ + function log10(uint256 value, Rounding rounding) internal pure returns (uint256) { + unchecked { + uint256 result = log10(value); + return result + (unsignedRoundsUp(rounding) && 10 ** result < value ? 1 : 0); + } + } + + /** + * @dev Return the log in base 256 of a positive value rounded towards zero. + * Returns 0 if given 0. + * + * Adding one to the result gives the number of pairs of hex symbols needed to represent `value` as a hex string. + */ + function log256(uint256 value) internal pure returns (uint256) { + uint256 result = 0; + unchecked { + if (value >> 128 > 0) { + value >>= 128; + result += 16; + } + if (value >> 64 > 0) { + value >>= 64; + result += 8; + } + if (value >> 32 > 0) { + value >>= 32; + result += 4; + } + if (value >> 16 > 0) { + value >>= 16; + result += 2; + } + if (value >> 8 > 0) { + result += 1; + } + } + return result; + } + + /** + * @dev Return the log in base 256, following the selected rounding direction, of a positive value. + * Returns 0 if given 0. + */ + function log256(uint256 value, Rounding rounding) internal pure returns (uint256) { + unchecked { + uint256 result = log256(value); + return result + (unsignedRoundsUp(rounding) && 1 << (result << 3) < value ? 1 : 0); + } + } + + /** + * @dev Returns whether a provided rounding mode is considered rounding up for unsigned integers. + */ + function unsignedRoundsUp(Rounding rounding) internal pure returns (bool) { + return uint8(rounding) % 2 == 1; + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/math/SafeCast.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/math/SafeCast.sol new file mode 100644 index 00000000000..0ed458b43c2 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/math/SafeCast.sol @@ -0,0 +1,1153 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/math/SafeCast.sol) +// This file was procedurally generated from scripts/generate/templates/SafeCast.js. + +pragma solidity ^0.8.20; + +/** + * @dev Wrappers over Solidity's uintXX/intXX casting operators with added overflow + * checks. + * + * Downcasting from uint256/int256 in Solidity does not revert on overflow. This can + * easily result in undesired exploitation or bugs, since developers usually + * assume that overflows raise errors. `SafeCast` restores this intuition by + * reverting the transaction when such an operation overflows. + * + * Using this library instead of the unchecked operations eliminates an entire + * class of bugs, so it's recommended to use it always. + */ +library SafeCast { + /** + * @dev Value doesn't fit in an uint of `bits` size. + */ + error SafeCastOverflowedUintDowncast(uint8 bits, uint256 value); + + /** + * @dev An int value doesn't fit in an uint of `bits` size. + */ + error SafeCastOverflowedIntToUint(int256 value); + + /** + * @dev Value doesn't fit in an int of `bits` size. + */ + error SafeCastOverflowedIntDowncast(uint8 bits, int256 value); + + /** + * @dev An uint value doesn't fit in an int of `bits` size. + */ + error SafeCastOverflowedUintToInt(uint256 value); + + /** + * @dev Returns the downcasted uint248 from uint256, reverting on + * overflow (when the input is greater than largest uint248). + * + * Counterpart to Solidity's `uint248` operator. + * + * Requirements: + * + * - input must fit into 248 bits + */ + function toUint248(uint256 value) internal pure returns (uint248) { + if (value > type(uint248).max) { + revert SafeCastOverflowedUintDowncast(248, value); + } + return uint248(value); + } + + /** + * @dev Returns the downcasted uint240 from uint256, reverting on + * overflow (when the input is greater than largest uint240). + * + * Counterpart to Solidity's `uint240` operator. + * + * Requirements: + * + * - input must fit into 240 bits + */ + function toUint240(uint256 value) internal pure returns (uint240) { + if (value > type(uint240).max) { + revert SafeCastOverflowedUintDowncast(240, value); + } + return uint240(value); + } + + /** + * @dev Returns the downcasted uint232 from uint256, reverting on + * overflow (when the input is greater than largest uint232). + * + * Counterpart to Solidity's `uint232` operator. + * + * Requirements: + * + * - input must fit into 232 bits + */ + function toUint232(uint256 value) internal pure returns (uint232) { + if (value > type(uint232).max) { + revert SafeCastOverflowedUintDowncast(232, value); + } + return uint232(value); + } + + /** + * @dev Returns the downcasted uint224 from uint256, reverting on + * overflow (when the input is greater than largest uint224). + * + * Counterpart to Solidity's `uint224` operator. + * + * Requirements: + * + * - input must fit into 224 bits + */ + function toUint224(uint256 value) internal pure returns (uint224) { + if (value > type(uint224).max) { + revert SafeCastOverflowedUintDowncast(224, value); + } + return uint224(value); + } + + /** + * @dev Returns the downcasted uint216 from uint256, reverting on + * overflow (when the input is greater than largest uint216). + * + * Counterpart to Solidity's `uint216` operator. + * + * Requirements: + * + * - input must fit into 216 bits + */ + function toUint216(uint256 value) internal pure returns (uint216) { + if (value > type(uint216).max) { + revert SafeCastOverflowedUintDowncast(216, value); + } + return uint216(value); + } + + /** + * @dev Returns the downcasted uint208 from uint256, reverting on + * overflow (when the input is greater than largest uint208). + * + * Counterpart to Solidity's `uint208` operator. + * + * Requirements: + * + * - input must fit into 208 bits + */ + function toUint208(uint256 value) internal pure returns (uint208) { + if (value > type(uint208).max) { + revert SafeCastOverflowedUintDowncast(208, value); + } + return uint208(value); + } + + /** + * @dev Returns the downcasted uint200 from uint256, reverting on + * overflow (when the input is greater than largest uint200). + * + * Counterpart to Solidity's `uint200` operator. + * + * Requirements: + * + * - input must fit into 200 bits + */ + function toUint200(uint256 value) internal pure returns (uint200) { + if (value > type(uint200).max) { + revert SafeCastOverflowedUintDowncast(200, value); + } + return uint200(value); + } + + /** + * @dev Returns the downcasted uint192 from uint256, reverting on + * overflow (when the input is greater than largest uint192). + * + * Counterpart to Solidity's `uint192` operator. + * + * Requirements: + * + * - input must fit into 192 bits + */ + function toUint192(uint256 value) internal pure returns (uint192) { + if (value > type(uint192).max) { + revert SafeCastOverflowedUintDowncast(192, value); + } + return uint192(value); + } + + /** + * @dev Returns the downcasted uint184 from uint256, reverting on + * overflow (when the input is greater than largest uint184). + * + * Counterpart to Solidity's `uint184` operator. + * + * Requirements: + * + * - input must fit into 184 bits + */ + function toUint184(uint256 value) internal pure returns (uint184) { + if (value > type(uint184).max) { + revert SafeCastOverflowedUintDowncast(184, value); + } + return uint184(value); + } + + /** + * @dev Returns the downcasted uint176 from uint256, reverting on + * overflow (when the input is greater than largest uint176). + * + * Counterpart to Solidity's `uint176` operator. + * + * Requirements: + * + * - input must fit into 176 bits + */ + function toUint176(uint256 value) internal pure returns (uint176) { + if (value > type(uint176).max) { + revert SafeCastOverflowedUintDowncast(176, value); + } + return uint176(value); + } + + /** + * @dev Returns the downcasted uint168 from uint256, reverting on + * overflow (when the input is greater than largest uint168). + * + * Counterpart to Solidity's `uint168` operator. + * + * Requirements: + * + * - input must fit into 168 bits + */ + function toUint168(uint256 value) internal pure returns (uint168) { + if (value > type(uint168).max) { + revert SafeCastOverflowedUintDowncast(168, value); + } + return uint168(value); + } + + /** + * @dev Returns the downcasted uint160 from uint256, reverting on + * overflow (when the input is greater than largest uint160). + * + * Counterpart to Solidity's `uint160` operator. + * + * Requirements: + * + * - input must fit into 160 bits + */ + function toUint160(uint256 value) internal pure returns (uint160) { + if (value > type(uint160).max) { + revert SafeCastOverflowedUintDowncast(160, value); + } + return uint160(value); + } + + /** + * @dev Returns the downcasted uint152 from uint256, reverting on + * overflow (when the input is greater than largest uint152). + * + * Counterpart to Solidity's `uint152` operator. + * + * Requirements: + * + * - input must fit into 152 bits + */ + function toUint152(uint256 value) internal pure returns (uint152) { + if (value > type(uint152).max) { + revert SafeCastOverflowedUintDowncast(152, value); + } + return uint152(value); + } + + /** + * @dev Returns the downcasted uint144 from uint256, reverting on + * overflow (when the input is greater than largest uint144). + * + * Counterpart to Solidity's `uint144` operator. + * + * Requirements: + * + * - input must fit into 144 bits + */ + function toUint144(uint256 value) internal pure returns (uint144) { + if (value > type(uint144).max) { + revert SafeCastOverflowedUintDowncast(144, value); + } + return uint144(value); + } + + /** + * @dev Returns the downcasted uint136 from uint256, reverting on + * overflow (when the input is greater than largest uint136). + * + * Counterpart to Solidity's `uint136` operator. + * + * Requirements: + * + * - input must fit into 136 bits + */ + function toUint136(uint256 value) internal pure returns (uint136) { + if (value > type(uint136).max) { + revert SafeCastOverflowedUintDowncast(136, value); + } + return uint136(value); + } + + /** + * @dev Returns the downcasted uint128 from uint256, reverting on + * overflow (when the input is greater than largest uint128). + * + * Counterpart to Solidity's `uint128` operator. + * + * Requirements: + * + * - input must fit into 128 bits + */ + function toUint128(uint256 value) internal pure returns (uint128) { + if (value > type(uint128).max) { + revert SafeCastOverflowedUintDowncast(128, value); + } + return uint128(value); + } + + /** + * @dev Returns the downcasted uint120 from uint256, reverting on + * overflow (when the input is greater than largest uint120). + * + * Counterpart to Solidity's `uint120` operator. + * + * Requirements: + * + * - input must fit into 120 bits + */ + function toUint120(uint256 value) internal pure returns (uint120) { + if (value > type(uint120).max) { + revert SafeCastOverflowedUintDowncast(120, value); + } + return uint120(value); + } + + /** + * @dev Returns the downcasted uint112 from uint256, reverting on + * overflow (when the input is greater than largest uint112). + * + * Counterpart to Solidity's `uint112` operator. + * + * Requirements: + * + * - input must fit into 112 bits + */ + function toUint112(uint256 value) internal pure returns (uint112) { + if (value > type(uint112).max) { + revert SafeCastOverflowedUintDowncast(112, value); + } + return uint112(value); + } + + /** + * @dev Returns the downcasted uint104 from uint256, reverting on + * overflow (when the input is greater than largest uint104). + * + * Counterpart to Solidity's `uint104` operator. + * + * Requirements: + * + * - input must fit into 104 bits + */ + function toUint104(uint256 value) internal pure returns (uint104) { + if (value > type(uint104).max) { + revert SafeCastOverflowedUintDowncast(104, value); + } + return uint104(value); + } + + /** + * @dev Returns the downcasted uint96 from uint256, reverting on + * overflow (when the input is greater than largest uint96). + * + * Counterpart to Solidity's `uint96` operator. + * + * Requirements: + * + * - input must fit into 96 bits + */ + function toUint96(uint256 value) internal pure returns (uint96) { + if (value > type(uint96).max) { + revert SafeCastOverflowedUintDowncast(96, value); + } + return uint96(value); + } + + /** + * @dev Returns the downcasted uint88 from uint256, reverting on + * overflow (when the input is greater than largest uint88). + * + * Counterpart to Solidity's `uint88` operator. + * + * Requirements: + * + * - input must fit into 88 bits + */ + function toUint88(uint256 value) internal pure returns (uint88) { + if (value > type(uint88).max) { + revert SafeCastOverflowedUintDowncast(88, value); + } + return uint88(value); + } + + /** + * @dev Returns the downcasted uint80 from uint256, reverting on + * overflow (when the input is greater than largest uint80). + * + * Counterpart to Solidity's `uint80` operator. + * + * Requirements: + * + * - input must fit into 80 bits + */ + function toUint80(uint256 value) internal pure returns (uint80) { + if (value > type(uint80).max) { + revert SafeCastOverflowedUintDowncast(80, value); + } + return uint80(value); + } + + /** + * @dev Returns the downcasted uint72 from uint256, reverting on + * overflow (when the input is greater than largest uint72). + * + * Counterpart to Solidity's `uint72` operator. + * + * Requirements: + * + * - input must fit into 72 bits + */ + function toUint72(uint256 value) internal pure returns (uint72) { + if (value > type(uint72).max) { + revert SafeCastOverflowedUintDowncast(72, value); + } + return uint72(value); + } + + /** + * @dev Returns the downcasted uint64 from uint256, reverting on + * overflow (when the input is greater than largest uint64). + * + * Counterpart to Solidity's `uint64` operator. + * + * Requirements: + * + * - input must fit into 64 bits + */ + function toUint64(uint256 value) internal pure returns (uint64) { + if (value > type(uint64).max) { + revert SafeCastOverflowedUintDowncast(64, value); + } + return uint64(value); + } + + /** + * @dev Returns the downcasted uint56 from uint256, reverting on + * overflow (when the input is greater than largest uint56). + * + * Counterpart to Solidity's `uint56` operator. + * + * Requirements: + * + * - input must fit into 56 bits + */ + function toUint56(uint256 value) internal pure returns (uint56) { + if (value > type(uint56).max) { + revert SafeCastOverflowedUintDowncast(56, value); + } + return uint56(value); + } + + /** + * @dev Returns the downcasted uint48 from uint256, reverting on + * overflow (when the input is greater than largest uint48). + * + * Counterpart to Solidity's `uint48` operator. + * + * Requirements: + * + * - input must fit into 48 bits + */ + function toUint48(uint256 value) internal pure returns (uint48) { + if (value > type(uint48).max) { + revert SafeCastOverflowedUintDowncast(48, value); + } + return uint48(value); + } + + /** + * @dev Returns the downcasted uint40 from uint256, reverting on + * overflow (when the input is greater than largest uint40). + * + * Counterpart to Solidity's `uint40` operator. + * + * Requirements: + * + * - input must fit into 40 bits + */ + function toUint40(uint256 value) internal pure returns (uint40) { + if (value > type(uint40).max) { + revert SafeCastOverflowedUintDowncast(40, value); + } + return uint40(value); + } + + /** + * @dev Returns the downcasted uint32 from uint256, reverting on + * overflow (when the input is greater than largest uint32). + * + * Counterpart to Solidity's `uint32` operator. + * + * Requirements: + * + * - input must fit into 32 bits + */ + function toUint32(uint256 value) internal pure returns (uint32) { + if (value > type(uint32).max) { + revert SafeCastOverflowedUintDowncast(32, value); + } + return uint32(value); + } + + /** + * @dev Returns the downcasted uint24 from uint256, reverting on + * overflow (when the input is greater than largest uint24). + * + * Counterpart to Solidity's `uint24` operator. + * + * Requirements: + * + * - input must fit into 24 bits + */ + function toUint24(uint256 value) internal pure returns (uint24) { + if (value > type(uint24).max) { + revert SafeCastOverflowedUintDowncast(24, value); + } + return uint24(value); + } + + /** + * @dev Returns the downcasted uint16 from uint256, reverting on + * overflow (when the input is greater than largest uint16). + * + * Counterpart to Solidity's `uint16` operator. + * + * Requirements: + * + * - input must fit into 16 bits + */ + function toUint16(uint256 value) internal pure returns (uint16) { + if (value > type(uint16).max) { + revert SafeCastOverflowedUintDowncast(16, value); + } + return uint16(value); + } + + /** + * @dev Returns the downcasted uint8 from uint256, reverting on + * overflow (when the input is greater than largest uint8). + * + * Counterpart to Solidity's `uint8` operator. + * + * Requirements: + * + * - input must fit into 8 bits + */ + function toUint8(uint256 value) internal pure returns (uint8) { + if (value > type(uint8).max) { + revert SafeCastOverflowedUintDowncast(8, value); + } + return uint8(value); + } + + /** + * @dev Converts a signed int256 into an unsigned uint256. + * + * Requirements: + * + * - input must be greater than or equal to 0. + */ + function toUint256(int256 value) internal pure returns (uint256) { + if (value < 0) { + revert SafeCastOverflowedIntToUint(value); + } + return uint256(value); + } + + /** + * @dev Returns the downcasted int248 from int256, reverting on + * overflow (when the input is less than smallest int248 or + * greater than largest int248). + * + * Counterpart to Solidity's `int248` operator. + * + * Requirements: + * + * - input must fit into 248 bits + */ + function toInt248(int256 value) internal pure returns (int248 downcasted) { + downcasted = int248(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(248, value); + } + } + + /** + * @dev Returns the downcasted int240 from int256, reverting on + * overflow (when the input is less than smallest int240 or + * greater than largest int240). + * + * Counterpart to Solidity's `int240` operator. + * + * Requirements: + * + * - input must fit into 240 bits + */ + function toInt240(int256 value) internal pure returns (int240 downcasted) { + downcasted = int240(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(240, value); + } + } + + /** + * @dev Returns the downcasted int232 from int256, reverting on + * overflow (when the input is less than smallest int232 or + * greater than largest int232). + * + * Counterpart to Solidity's `int232` operator. + * + * Requirements: + * + * - input must fit into 232 bits + */ + function toInt232(int256 value) internal pure returns (int232 downcasted) { + downcasted = int232(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(232, value); + } + } + + /** + * @dev Returns the downcasted int224 from int256, reverting on + * overflow (when the input is less than smallest int224 or + * greater than largest int224). + * + * Counterpart to Solidity's `int224` operator. + * + * Requirements: + * + * - input must fit into 224 bits + */ + function toInt224(int256 value) internal pure returns (int224 downcasted) { + downcasted = int224(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(224, value); + } + } + + /** + * @dev Returns the downcasted int216 from int256, reverting on + * overflow (when the input is less than smallest int216 or + * greater than largest int216). + * + * Counterpart to Solidity's `int216` operator. + * + * Requirements: + * + * - input must fit into 216 bits + */ + function toInt216(int256 value) internal pure returns (int216 downcasted) { + downcasted = int216(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(216, value); + } + } + + /** + * @dev Returns the downcasted int208 from int256, reverting on + * overflow (when the input is less than smallest int208 or + * greater than largest int208). + * + * Counterpart to Solidity's `int208` operator. + * + * Requirements: + * + * - input must fit into 208 bits + */ + function toInt208(int256 value) internal pure returns (int208 downcasted) { + downcasted = int208(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(208, value); + } + } + + /** + * @dev Returns the downcasted int200 from int256, reverting on + * overflow (when the input is less than smallest int200 or + * greater than largest int200). + * + * Counterpart to Solidity's `int200` operator. + * + * Requirements: + * + * - input must fit into 200 bits + */ + function toInt200(int256 value) internal pure returns (int200 downcasted) { + downcasted = int200(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(200, value); + } + } + + /** + * @dev Returns the downcasted int192 from int256, reverting on + * overflow (when the input is less than smallest int192 or + * greater than largest int192). + * + * Counterpart to Solidity's `int192` operator. + * + * Requirements: + * + * - input must fit into 192 bits + */ + function toInt192(int256 value) internal pure returns (int192 downcasted) { + downcasted = int192(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(192, value); + } + } + + /** + * @dev Returns the downcasted int184 from int256, reverting on + * overflow (when the input is less than smallest int184 or + * greater than largest int184). + * + * Counterpart to Solidity's `int184` operator. + * + * Requirements: + * + * - input must fit into 184 bits + */ + function toInt184(int256 value) internal pure returns (int184 downcasted) { + downcasted = int184(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(184, value); + } + } + + /** + * @dev Returns the downcasted int176 from int256, reverting on + * overflow (when the input is less than smallest int176 or + * greater than largest int176). + * + * Counterpart to Solidity's `int176` operator. + * + * Requirements: + * + * - input must fit into 176 bits + */ + function toInt176(int256 value) internal pure returns (int176 downcasted) { + downcasted = int176(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(176, value); + } + } + + /** + * @dev Returns the downcasted int168 from int256, reverting on + * overflow (when the input is less than smallest int168 or + * greater than largest int168). + * + * Counterpart to Solidity's `int168` operator. + * + * Requirements: + * + * - input must fit into 168 bits + */ + function toInt168(int256 value) internal pure returns (int168 downcasted) { + downcasted = int168(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(168, value); + } + } + + /** + * @dev Returns the downcasted int160 from int256, reverting on + * overflow (when the input is less than smallest int160 or + * greater than largest int160). + * + * Counterpart to Solidity's `int160` operator. + * + * Requirements: + * + * - input must fit into 160 bits + */ + function toInt160(int256 value) internal pure returns (int160 downcasted) { + downcasted = int160(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(160, value); + } + } + + /** + * @dev Returns the downcasted int152 from int256, reverting on + * overflow (when the input is less than smallest int152 or + * greater than largest int152). + * + * Counterpart to Solidity's `int152` operator. + * + * Requirements: + * + * - input must fit into 152 bits + */ + function toInt152(int256 value) internal pure returns (int152 downcasted) { + downcasted = int152(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(152, value); + } + } + + /** + * @dev Returns the downcasted int144 from int256, reverting on + * overflow (when the input is less than smallest int144 or + * greater than largest int144). + * + * Counterpart to Solidity's `int144` operator. + * + * Requirements: + * + * - input must fit into 144 bits + */ + function toInt144(int256 value) internal pure returns (int144 downcasted) { + downcasted = int144(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(144, value); + } + } + + /** + * @dev Returns the downcasted int136 from int256, reverting on + * overflow (when the input is less than smallest int136 or + * greater than largest int136). + * + * Counterpart to Solidity's `int136` operator. + * + * Requirements: + * + * - input must fit into 136 bits + */ + function toInt136(int256 value) internal pure returns (int136 downcasted) { + downcasted = int136(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(136, value); + } + } + + /** + * @dev Returns the downcasted int128 from int256, reverting on + * overflow (when the input is less than smallest int128 or + * greater than largest int128). + * + * Counterpart to Solidity's `int128` operator. + * + * Requirements: + * + * - input must fit into 128 bits + */ + function toInt128(int256 value) internal pure returns (int128 downcasted) { + downcasted = int128(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(128, value); + } + } + + /** + * @dev Returns the downcasted int120 from int256, reverting on + * overflow (when the input is less than smallest int120 or + * greater than largest int120). + * + * Counterpart to Solidity's `int120` operator. + * + * Requirements: + * + * - input must fit into 120 bits + */ + function toInt120(int256 value) internal pure returns (int120 downcasted) { + downcasted = int120(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(120, value); + } + } + + /** + * @dev Returns the downcasted int112 from int256, reverting on + * overflow (when the input is less than smallest int112 or + * greater than largest int112). + * + * Counterpart to Solidity's `int112` operator. + * + * Requirements: + * + * - input must fit into 112 bits + */ + function toInt112(int256 value) internal pure returns (int112 downcasted) { + downcasted = int112(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(112, value); + } + } + + /** + * @dev Returns the downcasted int104 from int256, reverting on + * overflow (when the input is less than smallest int104 or + * greater than largest int104). + * + * Counterpart to Solidity's `int104` operator. + * + * Requirements: + * + * - input must fit into 104 bits + */ + function toInt104(int256 value) internal pure returns (int104 downcasted) { + downcasted = int104(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(104, value); + } + } + + /** + * @dev Returns the downcasted int96 from int256, reverting on + * overflow (when the input is less than smallest int96 or + * greater than largest int96). + * + * Counterpart to Solidity's `int96` operator. + * + * Requirements: + * + * - input must fit into 96 bits + */ + function toInt96(int256 value) internal pure returns (int96 downcasted) { + downcasted = int96(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(96, value); + } + } + + /** + * @dev Returns the downcasted int88 from int256, reverting on + * overflow (when the input is less than smallest int88 or + * greater than largest int88). + * + * Counterpart to Solidity's `int88` operator. + * + * Requirements: + * + * - input must fit into 88 bits + */ + function toInt88(int256 value) internal pure returns (int88 downcasted) { + downcasted = int88(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(88, value); + } + } + + /** + * @dev Returns the downcasted int80 from int256, reverting on + * overflow (when the input is less than smallest int80 or + * greater than largest int80). + * + * Counterpart to Solidity's `int80` operator. + * + * Requirements: + * + * - input must fit into 80 bits + */ + function toInt80(int256 value) internal pure returns (int80 downcasted) { + downcasted = int80(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(80, value); + } + } + + /** + * @dev Returns the downcasted int72 from int256, reverting on + * overflow (when the input is less than smallest int72 or + * greater than largest int72). + * + * Counterpart to Solidity's `int72` operator. + * + * Requirements: + * + * - input must fit into 72 bits + */ + function toInt72(int256 value) internal pure returns (int72 downcasted) { + downcasted = int72(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(72, value); + } + } + + /** + * @dev Returns the downcasted int64 from int256, reverting on + * overflow (when the input is less than smallest int64 or + * greater than largest int64). + * + * Counterpart to Solidity's `int64` operator. + * + * Requirements: + * + * - input must fit into 64 bits + */ + function toInt64(int256 value) internal pure returns (int64 downcasted) { + downcasted = int64(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(64, value); + } + } + + /** + * @dev Returns the downcasted int56 from int256, reverting on + * overflow (when the input is less than smallest int56 or + * greater than largest int56). + * + * Counterpart to Solidity's `int56` operator. + * + * Requirements: + * + * - input must fit into 56 bits + */ + function toInt56(int256 value) internal pure returns (int56 downcasted) { + downcasted = int56(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(56, value); + } + } + + /** + * @dev Returns the downcasted int48 from int256, reverting on + * overflow (when the input is less than smallest int48 or + * greater than largest int48). + * + * Counterpart to Solidity's `int48` operator. + * + * Requirements: + * + * - input must fit into 48 bits + */ + function toInt48(int256 value) internal pure returns (int48 downcasted) { + downcasted = int48(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(48, value); + } + } + + /** + * @dev Returns the downcasted int40 from int256, reverting on + * overflow (when the input is less than smallest int40 or + * greater than largest int40). + * + * Counterpart to Solidity's `int40` operator. + * + * Requirements: + * + * - input must fit into 40 bits + */ + function toInt40(int256 value) internal pure returns (int40 downcasted) { + downcasted = int40(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(40, value); + } + } + + /** + * @dev Returns the downcasted int32 from int256, reverting on + * overflow (when the input is less than smallest int32 or + * greater than largest int32). + * + * Counterpart to Solidity's `int32` operator. + * + * Requirements: + * + * - input must fit into 32 bits + */ + function toInt32(int256 value) internal pure returns (int32 downcasted) { + downcasted = int32(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(32, value); + } + } + + /** + * @dev Returns the downcasted int24 from int256, reverting on + * overflow (when the input is less than smallest int24 or + * greater than largest int24). + * + * Counterpart to Solidity's `int24` operator. + * + * Requirements: + * + * - input must fit into 24 bits + */ + function toInt24(int256 value) internal pure returns (int24 downcasted) { + downcasted = int24(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(24, value); + } + } + + /** + * @dev Returns the downcasted int16 from int256, reverting on + * overflow (when the input is less than smallest int16 or + * greater than largest int16). + * + * Counterpart to Solidity's `int16` operator. + * + * Requirements: + * + * - input must fit into 16 bits + */ + function toInt16(int256 value) internal pure returns (int16 downcasted) { + downcasted = int16(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(16, value); + } + } + + /** + * @dev Returns the downcasted int8 from int256, reverting on + * overflow (when the input is less than smallest int8 or + * greater than largest int8). + * + * Counterpart to Solidity's `int8` operator. + * + * Requirements: + * + * - input must fit into 8 bits + */ + function toInt8(int256 value) internal pure returns (int8 downcasted) { + downcasted = int8(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(8, value); + } + } + + /** + * @dev Converts an unsigned uint256 into a signed int256. + * + * Requirements: + * + * - input must be less than or equal to maxInt256. + */ + function toInt256(uint256 value) internal pure returns (int256) { + // Note: Unsafe cast below is okay because `type(int256).max` is guaranteed to be positive + if (value > uint256(type(int256).max)) { + revert SafeCastOverflowedUintToInt(value); + } + return int256(value); + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/math/SignedMath.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/math/SignedMath.sol new file mode 100644 index 00000000000..66a61516292 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/math/SignedMath.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/math/SignedMath.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Standard signed math utilities missing in the Solidity language. + */ +library SignedMath { + /** + * @dev Returns the largest of two signed numbers. + */ + function max(int256 a, int256 b) internal pure returns (int256) { + return a > b ? a : b; + } + + /** + * @dev Returns the smallest of two signed numbers. + */ + function min(int256 a, int256 b) internal pure returns (int256) { + return a < b ? a : b; + } + + /** + * @dev Returns the average of two signed numbers without overflow. + * The result is rounded towards zero. + */ + function average(int256 a, int256 b) internal pure returns (int256) { + // Formula from the book "Hacker's Delight" + int256 x = (a & b) + ((a ^ b) >> 1); + return x + (int256(uint256(x) >> 255) & (a ^ b)); + } + + /** + * @dev Returns the absolute unsigned value of a signed value. + */ + function abs(int256 n) internal pure returns (uint256) { + unchecked { + // must be unchecked in order to support `n = type(int256).min` + return uint256(n >= 0 ? n : -n); + } + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/structs/EnumerableMap.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/structs/EnumerableMap.sol new file mode 100644 index 00000000000..929ae7c536e --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/structs/EnumerableMap.sol @@ -0,0 +1,533 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/structs/EnumerableMap.sol) +// This file was procedurally generated from scripts/generate/templates/EnumerableMap.js. + +pragma solidity ^0.8.20; + +import {EnumerableSet} from "./EnumerableSet.sol"; + +/** + * @dev Library for managing an enumerable variant of Solidity's + * https://solidity.readthedocs.io/en/latest/types.html#mapping-types[`mapping`] + * type. + * + * Maps have the following properties: + * + * - Entries are added, removed, and checked for existence in constant time + * (O(1)). + * - Entries are enumerated in O(n). No guarantees are made on the ordering. + * + * ```solidity + * contract Example { + * // Add the library methods + * using EnumerableMap for EnumerableMap.UintToAddressMap; + * + * // Declare a set state variable + * EnumerableMap.UintToAddressMap private myMap; + * } + * ``` + * + * The following map types are supported: + * + * - `uint256 -> address` (`UintToAddressMap`) since v3.0.0 + * - `address -> uint256` (`AddressToUintMap`) since v4.6.0 + * - `bytes32 -> bytes32` (`Bytes32ToBytes32Map`) since v4.6.0 + * - `uint256 -> uint256` (`UintToUintMap`) since v4.7.0 + * - `bytes32 -> uint256` (`Bytes32ToUintMap`) since v4.7.0 + * + * [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 EnumerableMap, you can either remove all elements one by one or create a fresh instance using an + * array of EnumerableMap. + * ==== + */ +library EnumerableMap { + using EnumerableSet for EnumerableSet.Bytes32Set; + + // To implement this library for multiple types with as little code repetition as possible, we write it in + // terms of a generic Map type with bytes32 keys and values. The Map implementation uses private functions, + // and user-facing implementations such as `UintToAddressMap` are just wrappers around the underlying Map. + // This means that we can only create new EnumerableMaps for types that fit in bytes32. + + /** + * @dev Query for a nonexistent map key. + */ + error EnumerableMapNonexistentKey(bytes32 key); + + struct Bytes32ToBytes32Map { + // Storage of keys + EnumerableSet.Bytes32Set _keys; + mapping(bytes32 key => bytes32) _values; + } + + /** + * @dev Adds a key-value pair to a map, or updates the value for an existing + * key. O(1). + * + * Returns true if the key was added to the map, that is if it was not + * already present. + */ + function set(Bytes32ToBytes32Map storage map, bytes32 key, bytes32 value) internal returns (bool) { + map._values[key] = value; + return map._keys.add(key); + } + + /** + * @dev Removes a key-value pair from a map. O(1). + * + * Returns true if the key was removed from the map, that is if it was present. + */ + function remove(Bytes32ToBytes32Map storage map, bytes32 key) internal returns (bool) { + delete map._values[key]; + return map._keys.remove(key); + } + + /** + * @dev Returns true if the key is in the map. O(1). + */ + function contains(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bool) { + return map._keys.contains(key); + } + + /** + * @dev Returns the number of key-value pairs in the map. O(1). + */ + function length(Bytes32ToBytes32Map storage map) internal view returns (uint256) { + return map._keys.length(); + } + + /** + * @dev Returns the key-value pair stored at position `index` in the map. O(1). + * + * Note that there are no guarantees on the ordering of entries inside the + * array, and it may change when more entries are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(Bytes32ToBytes32Map storage map, uint256 index) internal view returns (bytes32, bytes32) { + bytes32 key = map._keys.at(index); + return (key, map._values[key]); + } + + /** + * @dev Tries to returns the value associated with `key`. O(1). + * Does not revert if `key` is not in the map. + */ + function tryGet(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bool, bytes32) { + bytes32 value = map._values[key]; + if (value == bytes32(0)) { + return (contains(map, key), bytes32(0)); + } else { + return (true, value); + } + } + + /** + * @dev Returns the value associated with `key`. O(1). + * + * Requirements: + * + * - `key` must be in the map. + */ + function get(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bytes32) { + bytes32 value = map._values[key]; + if (value == 0 && !contains(map, key)) { + revert EnumerableMapNonexistentKey(key); + } + return value; + } + + /** + * @dev Return the an array containing all the keys + * + * 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 map grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function keys(Bytes32ToBytes32Map storage map) internal view returns (bytes32[] memory) { + return map._keys.values(); + } + + // UintToUintMap + + struct UintToUintMap { + Bytes32ToBytes32Map _inner; + } + + /** + * @dev Adds a key-value pair to a map, or updates the value for an existing + * key. O(1). + * + * Returns true if the key was added to the map, that is if it was not + * already present. + */ + function set(UintToUintMap storage map, uint256 key, uint256 value) internal returns (bool) { + return set(map._inner, bytes32(key), bytes32(value)); + } + + /** + * @dev Removes a value from a map. O(1). + * + * Returns true if the key was removed from the map, that is if it was present. + */ + function remove(UintToUintMap storage map, uint256 key) internal returns (bool) { + return remove(map._inner, bytes32(key)); + } + + /** + * @dev Returns true if the key is in the map. O(1). + */ + function contains(UintToUintMap storage map, uint256 key) internal view returns (bool) { + return contains(map._inner, bytes32(key)); + } + + /** + * @dev Returns the number of elements in the map. O(1). + */ + function length(UintToUintMap storage map) internal view returns (uint256) { + return length(map._inner); + } + + /** + * @dev Returns the element stored at position `index` in the map. 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(UintToUintMap storage map, uint256 index) internal view returns (uint256, uint256) { + (bytes32 key, bytes32 value) = at(map._inner, index); + return (uint256(key), uint256(value)); + } + + /** + * @dev Tries to returns the value associated with `key`. O(1). + * Does not revert if `key` is not in the map. + */ + function tryGet(UintToUintMap storage map, uint256 key) internal view returns (bool, uint256) { + (bool success, bytes32 value) = tryGet(map._inner, bytes32(key)); + return (success, uint256(value)); + } + + /** + * @dev Returns the value associated with `key`. O(1). + * + * Requirements: + * + * - `key` must be in the map. + */ + function get(UintToUintMap storage map, uint256 key) internal view returns (uint256) { + return uint256(get(map._inner, bytes32(key))); + } + + /** + * @dev Return the an array containing all the keys + * + * 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 map grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function keys(UintToUintMap storage map) internal view returns (uint256[] memory) { + bytes32[] memory store = keys(map._inner); + uint256[] memory result; + + /// @solidity memory-safe-assembly + assembly { + result := store + } + + return result; + } + + // UintToAddressMap + + struct UintToAddressMap { + Bytes32ToBytes32Map _inner; + } + + /** + * @dev Adds a key-value pair to a map, or updates the value for an existing + * key. O(1). + * + * Returns true if the key was added to the map, that is if it was not + * already present. + */ + function set(UintToAddressMap storage map, uint256 key, address value) internal returns (bool) { + return set(map._inner, bytes32(key), bytes32(uint256(uint160(value)))); + } + + /** + * @dev Removes a value from a map. O(1). + * + * Returns true if the key was removed from the map, that is if it was present. + */ + function remove(UintToAddressMap storage map, uint256 key) internal returns (bool) { + return remove(map._inner, bytes32(key)); + } + + /** + * @dev Returns true if the key is in the map. O(1). + */ + function contains(UintToAddressMap storage map, uint256 key) internal view returns (bool) { + return contains(map._inner, bytes32(key)); + } + + /** + * @dev Returns the number of elements in the map. O(1). + */ + function length(UintToAddressMap storage map) internal view returns (uint256) { + return length(map._inner); + } + + /** + * @dev Returns the element stored at position `index` in the map. 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(UintToAddressMap storage map, uint256 index) internal view returns (uint256, address) { + (bytes32 key, bytes32 value) = at(map._inner, index); + return (uint256(key), address(uint160(uint256(value)))); + } + + /** + * @dev Tries to returns the value associated with `key`. O(1). + * Does not revert if `key` is not in the map. + */ + function tryGet(UintToAddressMap storage map, uint256 key) internal view returns (bool, address) { + (bool success, bytes32 value) = tryGet(map._inner, bytes32(key)); + return (success, address(uint160(uint256(value)))); + } + + /** + * @dev Returns the value associated with `key`. O(1). + * + * Requirements: + * + * - `key` must be in the map. + */ + function get(UintToAddressMap storage map, uint256 key) internal view returns (address) { + return address(uint160(uint256(get(map._inner, bytes32(key))))); + } + + /** + * @dev Return the an array containing all the keys + * + * 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 map grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function keys(UintToAddressMap storage map) internal view returns (uint256[] memory) { + bytes32[] memory store = keys(map._inner); + uint256[] memory result; + + /// @solidity memory-safe-assembly + assembly { + result := store + } + + return result; + } + + // AddressToUintMap + + struct AddressToUintMap { + Bytes32ToBytes32Map _inner; + } + + /** + * @dev Adds a key-value pair to a map, or updates the value for an existing + * key. O(1). + * + * Returns true if the key was added to the map, that is if it was not + * already present. + */ + function set(AddressToUintMap storage map, address key, uint256 value) internal returns (bool) { + return set(map._inner, bytes32(uint256(uint160(key))), bytes32(value)); + } + + /** + * @dev Removes a value from a map. O(1). + * + * Returns true if the key was removed from the map, that is if it was present. + */ + function remove(AddressToUintMap storage map, address key) internal returns (bool) { + return remove(map._inner, bytes32(uint256(uint160(key)))); + } + + /** + * @dev Returns true if the key is in the map. O(1). + */ + function contains(AddressToUintMap storage map, address key) internal view returns (bool) { + return contains(map._inner, bytes32(uint256(uint160(key)))); + } + + /** + * @dev Returns the number of elements in the map. O(1). + */ + function length(AddressToUintMap storage map) internal view returns (uint256) { + return length(map._inner); + } + + /** + * @dev Returns the element stored at position `index` in the map. 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(AddressToUintMap storage map, uint256 index) internal view returns (address, uint256) { + (bytes32 key, bytes32 value) = at(map._inner, index); + return (address(uint160(uint256(key))), uint256(value)); + } + + /** + * @dev Tries to returns the value associated with `key`. O(1). + * Does not revert if `key` is not in the map. + */ + function tryGet(AddressToUintMap storage map, address key) internal view returns (bool, uint256) { + (bool success, bytes32 value) = tryGet(map._inner, bytes32(uint256(uint160(key)))); + return (success, uint256(value)); + } + + /** + * @dev Returns the value associated with `key`. O(1). + * + * Requirements: + * + * - `key` must be in the map. + */ + function get(AddressToUintMap storage map, address key) internal view returns (uint256) { + return uint256(get(map._inner, bytes32(uint256(uint160(key))))); + } + + /** + * @dev Return the an array containing all the keys + * + * 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 map grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function keys(AddressToUintMap storage map) internal view returns (address[] memory) { + bytes32[] memory store = keys(map._inner); + address[] memory result; + + /// @solidity memory-safe-assembly + assembly { + result := store + } + + return result; + } + + // Bytes32ToUintMap + + struct Bytes32ToUintMap { + Bytes32ToBytes32Map _inner; + } + + /** + * @dev Adds a key-value pair to a map, or updates the value for an existing + * key. O(1). + * + * Returns true if the key was added to the map, that is if it was not + * already present. + */ + function set(Bytes32ToUintMap storage map, bytes32 key, uint256 value) internal returns (bool) { + return set(map._inner, key, bytes32(value)); + } + + /** + * @dev Removes a value from a map. O(1). + * + * Returns true if the key was removed from the map, that is if it was present. + */ + function remove(Bytes32ToUintMap storage map, bytes32 key) internal returns (bool) { + return remove(map._inner, key); + } + + /** + * @dev Returns true if the key is in the map. O(1). + */ + function contains(Bytes32ToUintMap storage map, bytes32 key) internal view returns (bool) { + return contains(map._inner, key); + } + + /** + * @dev Returns the number of elements in the map. O(1). + */ + function length(Bytes32ToUintMap storage map) internal view returns (uint256) { + return length(map._inner); + } + + /** + * @dev Returns the element stored at position `index` in the map. 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(Bytes32ToUintMap storage map, uint256 index) internal view returns (bytes32, uint256) { + (bytes32 key, bytes32 value) = at(map._inner, index); + return (key, uint256(value)); + } + + /** + * @dev Tries to returns the value associated with `key`. O(1). + * Does not revert if `key` is not in the map. + */ + function tryGet(Bytes32ToUintMap storage map, bytes32 key) internal view returns (bool, uint256) { + (bool success, bytes32 value) = tryGet(map._inner, key); + return (success, uint256(value)); + } + + /** + * @dev Returns the value associated with `key`. O(1). + * + * Requirements: + * + * - `key` must be in the map. + */ + function get(Bytes32ToUintMap storage map, bytes32 key) internal view returns (uint256) { + return uint256(get(map._inner, key)); + } + + /** + * @dev Return the an array containing all the keys + * + * 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 map grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function keys(Bytes32ToUintMap storage map) internal view returns (bytes32[] memory) { + bytes32[] memory store = keys(map._inner); + bytes32[] memory result; + + /// @solidity memory-safe-assembly + assembly { + result := store + } + + return result; + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/structs/EnumerableSet.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/structs/EnumerableSet.sol new file mode 100644 index 00000000000..4c7fc5e1d76 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/structs/EnumerableSet.sol @@ -0,0 +1,378 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/structs/EnumerableSet.sol) +// This file was procedurally generated from scripts/generate/templates/EnumerableSet.js. + +pragma solidity ^0.8.20; + +/** + * @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. + * + * ```solidity + * 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 is the index of the value in the `values` array plus 1. + // Position 0 is used to mean a value is not in the set. + mapping(bytes32 value => uint256) _positions; + } + + /** + * @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._positions[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 cache the value's position to prevent multiple reads from the same storage slot + uint256 position = set._positions[value]; + + if (position != 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 valueIndex = position - 1; + uint256 lastIndex = set._values.length - 1; + + if (valueIndex != lastIndex) { + bytes32 lastValue = set._values[lastIndex]; + + // Move the lastValue to the index where the value to delete is + set._values[valueIndex] = lastValue; + // Update the tracked position of the lastValue (that was just moved) + set._positions[lastValue] = position; + } + + // Delete the slot where the moved value was stored + set._values.pop(); + + // Delete the tracked position for the deleted slot + delete set._positions[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._positions[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; + } +} From 6ab3eb5b67739ff88d3c4cf8ea125fd8273bc2b1 Mon Sep 17 00:00:00 2001 From: "Abdelrahman Soliman (Boda)" <2677789+asoliman92@users.noreply.github.com> Date: Wed, 7 Aug 2024 22:32:07 +0400 Subject: [PATCH 5/9] [CCIP Merge] Capabilities [CCIP-2943] (#14068) * [CCIP Merge] Add ccip capabilities directory [CCIP-2943] (#14044) * Add ccip capabilities directory * [CCIP Merge] Capabilities fix [CCIP-2943] (#14048) * Fix compilation for launcher, diff Make application.go ready for adding more fixes * Fix launcher tests * ks-409 fix the mock trigger to ensure events are sent (#14047) * Add ccip to job orm * Add capabilities directory under BUSL license * Prep to instantiate separate registrysyncer for CCIP * Move registrySyncer creation into ccip delegate * [chore] Change registrysyncer config to bytes * Fix launcher diff tests after changing structs in syncer * Fix linting * MAke simulated backend client work with chains other than 1337 * core/capabilities/ccip: use OCR offchain config (#1264) We want to define and use the appropriate OCR offchain config for each plugin. Requires https://github.com/smartcontractkit/chainlink-ccip/pull/36/ * Cleaning up * Add capabilities types to mockery --------- Co-authored-by: Cedric Cordenier Co-authored-by: Matthew Pendrey Co-authored-by: Makram * make modgraph * Add changeset * Fix test with new TxMgr constructor --------- Co-authored-by: Cedric Cordenier Co-authored-by: Matthew Pendrey Co-authored-by: Makram --- .changeset/eight-radios-hear.md | 5 + .mockery.yaml | 4 + LICENSE | 2 +- .../ccip/ccip_integration_tests/.gitignore | 1 + .../ccipreader/ccipreader_test.go | 411 +++++++ .../chainreader/Makefile | 12 + .../chainreader/chainreader_test.go | 273 +++++ .../chainreader/mycontract.go | 519 ++++++++ .../chainreader/mycontract.sol | 31 + .../ccip/ccip_integration_tests/helpers.go | 938 +++++++++++++++ .../ccip_integration_tests/home_chain_test.go | 103 ++ .../integrationhelpers/integration_helpers.go | 304 +++++ .../ccip_integration_tests/ocr3_node_test.go | 281 +++++ .../ccip_integration_tests/ocr_node_helper.go | 316 +++++ .../ccip_integration_tests/ping_pong_test.go | 95 ++ core/capabilities/ccip/ccipevm/commitcodec.go | 138 +++ .../ccip/ccipevm/commitcodec_test.go | 135 +++ .../capabilities/ccip/ccipevm/executecodec.go | 181 +++ .../ccip/ccipevm/executecodec_test.go | 174 +++ core/capabilities/ccip/ccipevm/helpers.go | 33 + .../capabilities/ccip/ccipevm/helpers_test.go | 41 + core/capabilities/ccip/ccipevm/msghasher.go | 127 ++ .../ccip/ccipevm/msghasher_test.go | 189 +++ core/capabilities/ccip/common/common.go | 23 + core/capabilities/ccip/common/common_test.go | 51 + .../ccip/configs/evm/chain_writer.go | 75 ++ .../ccip/configs/evm/contract_reader.go | 219 ++++ core/capabilities/ccip/delegate.go | 321 +++++ core/capabilities/ccip/delegate_test.go | 1 + core/capabilities/ccip/launcher/README.md | 69 ++ core/capabilities/ccip/launcher/bluegreen.go | 178 +++ .../ccip/launcher/bluegreen_test.go | 1043 +++++++++++++++++ .../launcher/ccip_capability_launcher.png | Bin 0 -> 253433 bytes .../launcher/ccip_config_state_machine.png | Bin 0 -> 96958 bytes core/capabilities/ccip/launcher/diff.go | 141 +++ core/capabilities/ccip/launcher/diff_test.go | 352 ++++++ .../ccip/launcher/integration_test.go | 120 ++ core/capabilities/ccip/launcher/launcher.go | 432 +++++++ .../ccip/launcher/launcher_test.go | 472 ++++++++ .../ccip/launcher/test_helpers.go | 56 + .../ccip/ocrimpls/config_digester.go | 23 + .../ccip/ocrimpls/config_tracker.go | 77 ++ .../ccip/ocrimpls/contract_transmitter.go | 188 +++ .../ocrimpls/contract_transmitter_test.go | 691 +++++++++++ core/capabilities/ccip/ocrimpls/keyring.go | 61 + .../ccip/oraclecreator/inprocess.go | 371 ++++++ .../ccip/oraclecreator/inprocess_test.go | 239 ++++ .../ccip/types/mocks/ccip_oracle.go | 122 ++ .../ccip/types/mocks/home_chain_reader.go | 129 ++ .../ccip/types/mocks/oracle_creator.go | 152 +++ core/capabilities/ccip/types/types.go | 46 + core/capabilities/ccip/validate/validate.go | 94 ++ .../ccip/validate/validate_test.go | 58 + core/capabilities/launcher.go | 56 +- core/capabilities/launcher_test.go | 29 +- core/capabilities/registry.go | 10 +- .../evm/client/simulated_backend_client.go | 13 +- core/scripts/go.mod | 5 +- core/scripts/go.sum | 10 +- core/services/chainlink/application.go | 87 +- core/services/job/models.go | 50 + core/services/job/orm.go | 64 +- core/services/pipeline/common.go | 1 + .../services/registrysyncer/local_registry.go | 19 +- core/services/registrysyncer/syncer.go | 57 +- core/services/registrysyncer/syncer_test.go | 44 +- core/services/synchronization/common.go | 4 + core/services/workflows/engine_test.go | 26 +- core/web/presenters/job.go | 23 + core/web/presenters/job_test.go | 98 +- go.md | 4 + go.mod | 5 +- go.sum | 10 +- integration-tests/go.mod | 5 +- integration-tests/go.sum | 10 +- integration-tests/load/go.mod | 5 +- integration-tests/load/go.sum | 10 +- 77 files changed, 10565 insertions(+), 197 deletions(-) create mode 100644 .changeset/eight-radios-hear.md create mode 100644 core/capabilities/ccip/ccip_integration_tests/.gitignore create mode 100644 core/capabilities/ccip/ccip_integration_tests/ccipreader/ccipreader_test.go create mode 100644 core/capabilities/ccip/ccip_integration_tests/chainreader/Makefile create mode 100644 core/capabilities/ccip/ccip_integration_tests/chainreader/chainreader_test.go create mode 100644 core/capabilities/ccip/ccip_integration_tests/chainreader/mycontract.go create mode 100644 core/capabilities/ccip/ccip_integration_tests/chainreader/mycontract.sol create mode 100644 core/capabilities/ccip/ccip_integration_tests/helpers.go create mode 100644 core/capabilities/ccip/ccip_integration_tests/home_chain_test.go create mode 100644 core/capabilities/ccip/ccip_integration_tests/integrationhelpers/integration_helpers.go create mode 100644 core/capabilities/ccip/ccip_integration_tests/ocr3_node_test.go create mode 100644 core/capabilities/ccip/ccip_integration_tests/ocr_node_helper.go create mode 100644 core/capabilities/ccip/ccip_integration_tests/ping_pong_test.go create mode 100644 core/capabilities/ccip/ccipevm/commitcodec.go create mode 100644 core/capabilities/ccip/ccipevm/commitcodec_test.go create mode 100644 core/capabilities/ccip/ccipevm/executecodec.go create mode 100644 core/capabilities/ccip/ccipevm/executecodec_test.go create mode 100644 core/capabilities/ccip/ccipevm/helpers.go create mode 100644 core/capabilities/ccip/ccipevm/helpers_test.go create mode 100644 core/capabilities/ccip/ccipevm/msghasher.go create mode 100644 core/capabilities/ccip/ccipevm/msghasher_test.go create mode 100644 core/capabilities/ccip/common/common.go create mode 100644 core/capabilities/ccip/common/common_test.go create mode 100644 core/capabilities/ccip/configs/evm/chain_writer.go create mode 100644 core/capabilities/ccip/configs/evm/contract_reader.go create mode 100644 core/capabilities/ccip/delegate.go create mode 100644 core/capabilities/ccip/delegate_test.go create mode 100644 core/capabilities/ccip/launcher/README.md create mode 100644 core/capabilities/ccip/launcher/bluegreen.go create mode 100644 core/capabilities/ccip/launcher/bluegreen_test.go create mode 100644 core/capabilities/ccip/launcher/ccip_capability_launcher.png create mode 100644 core/capabilities/ccip/launcher/ccip_config_state_machine.png create mode 100644 core/capabilities/ccip/launcher/diff.go create mode 100644 core/capabilities/ccip/launcher/diff_test.go create mode 100644 core/capabilities/ccip/launcher/integration_test.go create mode 100644 core/capabilities/ccip/launcher/launcher.go create mode 100644 core/capabilities/ccip/launcher/launcher_test.go create mode 100644 core/capabilities/ccip/launcher/test_helpers.go create mode 100644 core/capabilities/ccip/ocrimpls/config_digester.go create mode 100644 core/capabilities/ccip/ocrimpls/config_tracker.go create mode 100644 core/capabilities/ccip/ocrimpls/contract_transmitter.go create mode 100644 core/capabilities/ccip/ocrimpls/contract_transmitter_test.go create mode 100644 core/capabilities/ccip/ocrimpls/keyring.go create mode 100644 core/capabilities/ccip/oraclecreator/inprocess.go create mode 100644 core/capabilities/ccip/oraclecreator/inprocess_test.go create mode 100644 core/capabilities/ccip/types/mocks/ccip_oracle.go create mode 100644 core/capabilities/ccip/types/mocks/home_chain_reader.go create mode 100644 core/capabilities/ccip/types/mocks/oracle_creator.go create mode 100644 core/capabilities/ccip/types/types.go create mode 100644 core/capabilities/ccip/validate/validate.go create mode 100644 core/capabilities/ccip/validate/validate_test.go diff --git a/.changeset/eight-radios-hear.md b/.changeset/eight-radios-hear.md new file mode 100644 index 00000000000..b422f378326 --- /dev/null +++ b/.changeset/eight-radios-hear.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +#added merging core/capabilities/ccip from https://github.com/smartcontractkit/ccip diff --git a/.mockery.yaml b/.mockery.yaml index 8fab61a5b9d..abb3105b136 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -43,6 +43,10 @@ packages: github.com/smartcontractkit/chainlink/v2/core/bridges: interfaces: ORM: + github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types: + interfaces: + CCIPOracle: + OracleCreator: github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/types: interfaces: Dispatcher: diff --git a/LICENSE b/LICENSE index 4a10bfc38b0..3af9faa6c6f 100644 --- a/LICENSE +++ b/LICENSE @@ -24,7 +24,7 @@ THE SOFTWARE. *All content residing under (1) “/contracts/src/v0.8/ccip”; (2) -“/core/gethwrappers/ccip”; (3) “/core/services/ocr2/plugins/ccip” are licensed +“/core/gethwrappers/ccip”; (3) “/core/services/ocr2/plugins/ccip”; (4) "/core/capabilities/ccip" are licensed under “Business Source License 1.1” with a Change Date of May 23, 2027 and Change License to “MIT License” diff --git a/core/capabilities/ccip/ccip_integration_tests/.gitignore b/core/capabilities/ccip/ccip_integration_tests/.gitignore new file mode 100644 index 00000000000..567609b1234 --- /dev/null +++ b/core/capabilities/ccip/ccip_integration_tests/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/core/capabilities/ccip/ccip_integration_tests/ccipreader/ccipreader_test.go b/core/capabilities/ccip/ccip_integration_tests/ccipreader/ccipreader_test.go new file mode 100644 index 00000000000..66c47f4741f --- /dev/null +++ b/core/capabilities/ccip/ccip_integration_tests/ccipreader/ccipreader_test.go @@ -0,0 +1,411 @@ +package ccipreader + +import ( + "context" + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" + "golang.org/x/exp/maps" + + "github.com/smartcontractkit/chainlink-common/pkg/types" + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/headtracker" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/ccip_reader_tester" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm" + evmtypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" + + "github.com/smartcontractkit/chainlink-ccip/pkg/consts" + "github.com/smartcontractkit/chainlink-ccip/pkg/contractreader" + ccipreaderpkg "github.com/smartcontractkit/chainlink-ccip/pkg/reader" + "github.com/smartcontractkit/chainlink-ccip/plugintypes" +) + +const ( + chainS1 = cciptypes.ChainSelector(1) + chainS2 = cciptypes.ChainSelector(2) + chainS3 = cciptypes.ChainSelector(3) + chainD = cciptypes.ChainSelector(4) +) + +func TestCCIPReader_CommitReportsGTETimestamp(t *testing.T) { + ctx := testutils.Context(t) + + cfg := evmtypes.ChainReaderConfig{ + Contracts: map[string]evmtypes.ChainContractReader{ + consts.ContractNameOffRamp: { + ContractPollingFilter: evmtypes.ContractPollingFilter{ + GenericEventNames: []string{consts.EventNameCommitReportAccepted}, + }, + ContractABI: ccip_reader_tester.CCIPReaderTesterABI, + Configs: map[string]*evmtypes.ChainReaderDefinition{ + consts.EventNameCommitReportAccepted: { + ChainSpecificName: consts.EventNameCommitReportAccepted, + ReadType: evmtypes.Event, + }, + }, + }, + }, + } + + s := testSetup(ctx, t, chainD, chainD, nil, cfg) + + tokenA := common.HexToAddress("123") + const numReports = 5 + + for i := uint8(0); i < numReports; i++ { + _, err := s.contract.EmitCommitReportAccepted(s.auth, ccip_reader_tester.EVM2EVMMultiOffRampCommitReport{ + PriceUpdates: ccip_reader_tester.InternalPriceUpdates{ + TokenPriceUpdates: []ccip_reader_tester.InternalTokenPriceUpdate{ + { + SourceToken: tokenA, + UsdPerToken: big.NewInt(1000), + }, + }, + GasPriceUpdates: []ccip_reader_tester.InternalGasPriceUpdate{ + { + DestChainSelector: uint64(chainD), + UsdPerUnitGas: big.NewInt(90), + }, + }, + }, + MerkleRoots: []ccip_reader_tester.EVM2EVMMultiOffRampMerkleRoot{ + { + SourceChainSelector: uint64(chainS1), + Interval: ccip_reader_tester.EVM2EVMMultiOffRampInterval{ + Min: 10, + Max: 20, + }, + MerkleRoot: [32]byte{i + 1}, + }, + }, + }) + assert.NoError(t, err) + s.sb.Commit() + } + + var reports []plugintypes.CommitPluginReportWithMeta + var err error + require.Eventually(t, func() bool { + reports, err = s.reader.CommitReportsGTETimestamp( + ctx, + chainD, + time.Unix(30, 0), // Skips first report, simulated backend report timestamps are [20, 30, 40, ...] + 10, + ) + require.NoError(t, err) + return len(reports) == numReports-1 + }, testutils.WaitTimeout(t), 50*time.Millisecond) + + assert.Len(t, reports[0].Report.MerkleRoots, 1) + assert.Equal(t, chainS1, reports[0].Report.MerkleRoots[0].ChainSel) + assert.Equal(t, cciptypes.SeqNum(10), reports[0].Report.MerkleRoots[0].SeqNumsRange.Start()) + assert.Equal(t, cciptypes.SeqNum(20), reports[0].Report.MerkleRoots[0].SeqNumsRange.End()) + assert.Equal(t, "0x0200000000000000000000000000000000000000000000000000000000000000", + reports[0].Report.MerkleRoots[0].MerkleRoot.String()) + + assert.Equal(t, tokenA.String(), string(reports[0].Report.PriceUpdates.TokenPriceUpdates[0].TokenID)) + assert.Equal(t, uint64(1000), reports[0].Report.PriceUpdates.TokenPriceUpdates[0].Price.Uint64()) + + assert.Equal(t, chainD, reports[0].Report.PriceUpdates.GasPriceUpdates[0].ChainSel) + assert.Equal(t, uint64(90), reports[0].Report.PriceUpdates.GasPriceUpdates[0].GasPrice.Uint64()) +} + +func TestCCIPReader_ExecutedMessageRanges(t *testing.T) { + ctx := testutils.Context(t) + cfg := evmtypes.ChainReaderConfig{ + Contracts: map[string]evmtypes.ChainContractReader{ + consts.ContractNameOffRamp: { + ContractPollingFilter: evmtypes.ContractPollingFilter{ + GenericEventNames: []string{consts.EventNameExecutionStateChanged}, + }, + ContractABI: ccip_reader_tester.CCIPReaderTesterABI, + Configs: map[string]*evmtypes.ChainReaderDefinition{ + consts.EventNameExecutionStateChanged: { + ChainSpecificName: consts.EventNameExecutionStateChanged, + ReadType: evmtypes.Event, + }, + }, + }, + }, + } + + s := testSetup(ctx, t, chainD, chainD, nil, cfg) + + _, err := s.contract.EmitExecutionStateChanged( + s.auth, + uint64(chainS1), + 14, + cciptypes.Bytes32{1, 0, 0, 1}, + 1, + []byte{1, 2, 3, 4}, + ) + assert.NoError(t, err) + s.sb.Commit() + + _, err = s.contract.EmitExecutionStateChanged( + s.auth, + uint64(chainS1), + 15, + cciptypes.Bytes32{1, 0, 0, 2}, + 1, + []byte{1, 2, 3, 4, 5}, + ) + assert.NoError(t, err) + s.sb.Commit() + + // Need to replay as sometimes the logs are not picked up by the log poller (?) + // Maybe another situation where chain reader doesn't register filters as expected. + require.NoError(t, s.lp.Replay(ctx, 1)) + + var executedRanges []cciptypes.SeqNumRange + require.Eventually(t, func() bool { + executedRanges, err = s.reader.ExecutedMessageRanges( + ctx, + chainS1, + chainD, + cciptypes.NewSeqNumRange(14, 15), + ) + require.NoError(t, err) + return len(executedRanges) == 2 + }, testutils.WaitTimeout(t), 50*time.Millisecond) + + assert.Equal(t, cciptypes.SeqNum(14), executedRanges[0].Start()) + assert.Equal(t, cciptypes.SeqNum(14), executedRanges[0].End()) + + assert.Equal(t, cciptypes.SeqNum(15), executedRanges[1].Start()) + assert.Equal(t, cciptypes.SeqNum(15), executedRanges[1].End()) +} + +func TestCCIPReader_MsgsBetweenSeqNums(t *testing.T) { + ctx := testutils.Context(t) + + cfg := evmtypes.ChainReaderConfig{ + Contracts: map[string]evmtypes.ChainContractReader{ + consts.ContractNameOnRamp: { + ContractPollingFilter: evmtypes.ContractPollingFilter{ + GenericEventNames: []string{consts.EventNameCCIPSendRequested}, + }, + ContractABI: ccip_reader_tester.CCIPReaderTesterABI, + Configs: map[string]*evmtypes.ChainReaderDefinition{ + consts.EventNameCCIPSendRequested: { + ChainSpecificName: consts.EventNameCCIPSendRequested, + ReadType: evmtypes.Event, + }, + }, + }, + }, + } + + s := testSetup(ctx, t, chainS1, chainD, nil, cfg) + + _, err := s.contract.EmitCCIPSendRequested(s.auth, uint64(chainD), ccip_reader_tester.InternalEVM2AnyRampMessage{ + Header: ccip_reader_tester.InternalRampMessageHeader{ + MessageId: [32]byte{1, 0, 0, 0, 0}, + SourceChainSelector: uint64(chainS1), + DestChainSelector: uint64(chainD), + SequenceNumber: 10, + }, + Sender: utils.RandomAddress(), + Data: make([]byte, 0), + Receiver: utils.RandomAddress().Bytes(), + ExtraArgs: make([]byte, 0), + FeeToken: utils.RandomAddress(), + FeeTokenAmount: big.NewInt(0), + TokenAmounts: make([]ccip_reader_tester.InternalRampTokenAmount, 0), + }) + assert.NoError(t, err) + + _, err = s.contract.EmitCCIPSendRequested(s.auth, uint64(chainD), ccip_reader_tester.InternalEVM2AnyRampMessage{ + Header: ccip_reader_tester.InternalRampMessageHeader{ + MessageId: [32]byte{1, 0, 0, 0, 1}, + SourceChainSelector: uint64(chainS1), + DestChainSelector: uint64(chainD), + SequenceNumber: 15, + }, + Sender: utils.RandomAddress(), + Data: make([]byte, 0), + Receiver: utils.RandomAddress().Bytes(), + ExtraArgs: make([]byte, 0), + FeeToken: utils.RandomAddress(), + FeeTokenAmount: big.NewInt(0), + TokenAmounts: make([]ccip_reader_tester.InternalRampTokenAmount, 0), + }) + assert.NoError(t, err) + + s.sb.Commit() + + var msgs []cciptypes.Message + require.Eventually(t, func() bool { + msgs, err = s.reader.MsgsBetweenSeqNums( + ctx, + chainS1, + cciptypes.NewSeqNumRange(5, 20), + ) + require.NoError(t, err) + return len(msgs) == 2 + }, 10*time.Second, 100*time.Millisecond) + + require.Len(t, msgs, 2) + require.Equal(t, cciptypes.SeqNum(10), msgs[0].Header.SequenceNumber) + require.Equal(t, cciptypes.SeqNum(15), msgs[1].Header.SequenceNumber) + for _, msg := range msgs { + require.Equal(t, chainS1, msg.Header.SourceChainSelector) + require.Equal(t, chainD, msg.Header.DestChainSelector) + } +} + +func TestCCIPReader_NextSeqNum(t *testing.T) { + ctx := testutils.Context(t) + + onChainSeqNums := map[cciptypes.ChainSelector]cciptypes.SeqNum{ + chainS1: 10, + chainS2: 20, + chainS3: 30, + } + + cfg := evmtypes.ChainReaderConfig{ + Contracts: map[string]evmtypes.ChainContractReader{ + consts.ContractNameOffRamp: { + ContractABI: ccip_reader_tester.CCIPReaderTesterABI, + Configs: map[string]*evmtypes.ChainReaderDefinition{ + consts.MethodNameGetSourceChainConfig: { + ChainSpecificName: "getSourceChainConfig", + ReadType: evmtypes.Method, + }, + }, + }, + }, + } + + s := testSetup(ctx, t, chainD, chainD, onChainSeqNums, cfg) + + seqNums, err := s.reader.NextSeqNum(ctx, []cciptypes.ChainSelector{chainS1, chainS2, chainS3}) + assert.NoError(t, err) + assert.Len(t, seqNums, 3) + assert.Equal(t, cciptypes.SeqNum(10), seqNums[0]) + assert.Equal(t, cciptypes.SeqNum(20), seqNums[1]) + assert.Equal(t, cciptypes.SeqNum(30), seqNums[2]) +} + +func testSetup(ctx context.Context, t *testing.T, readerChain, destChain cciptypes.ChainSelector, onChainSeqNums map[cciptypes.ChainSelector]cciptypes.SeqNum, cfg evmtypes.ChainReaderConfig) *testSetupData { + const chainID = 1337 + + // Generate a new key pair for the simulated account + privateKey, err := crypto.GenerateKey() + assert.NoError(t, err) + // Set up the genesis account with balance + blnc, ok := big.NewInt(0).SetString("999999999999999999999999999999999999", 10) + assert.True(t, ok) + alloc := map[common.Address]core.GenesisAccount{crypto.PubkeyToAddress(privateKey.PublicKey): {Balance: blnc}} + simulatedBackend := backends.NewSimulatedBackend(alloc, 0) + // Create a transactor + + auth, err := bind.NewKeyedTransactorWithChainID(privateKey, big.NewInt(chainID)) + assert.NoError(t, err) + auth.GasLimit = uint64(0) + + // Deploy the contract + address, _, _, err := ccip_reader_tester.DeployCCIPReaderTester(auth, simulatedBackend) + assert.NoError(t, err) + simulatedBackend.Commit() + + // Setup contract client + contract, err := ccip_reader_tester.NewCCIPReaderTester(address, simulatedBackend) + assert.NoError(t, err) + + lggr := logger.TestLogger(t) + lggr.SetLogLevel(zapcore.ErrorLevel) + db := pgtest.NewSqlxDB(t) + lpOpts := logpoller.Opts{ + PollPeriod: time.Millisecond, + FinalityDepth: 0, + BackfillBatchSize: 10, + RpcBatchSize: 10, + KeepFinalizedBlocksDepth: 100000, + } + cl := client.NewSimulatedBackendClient(t, simulatedBackend, big.NewInt(0).SetUint64(uint64(readerChain))) + headTracker := headtracker.NewSimulatedHeadTracker(cl, lpOpts.UseFinalityTag, lpOpts.FinalityDepth) + lp := logpoller.NewLogPoller(logpoller.NewORM(big.NewInt(0).SetUint64(uint64(readerChain)), db, lggr), + cl, + lggr, + headTracker, + lpOpts, + ) + assert.NoError(t, lp.Start(ctx)) + + for sourceChain, seqNum := range onChainSeqNums { + _, err1 := contract.SetSourceChainConfig(auth, uint64(sourceChain), ccip_reader_tester.EVM2EVMMultiOffRampSourceChainConfig{ + IsEnabled: true, + MinSeqNr: uint64(seqNum), + }) + assert.NoError(t, err1) + simulatedBackend.Commit() + scc, err1 := contract.GetSourceChainConfig(&bind.CallOpts{Context: ctx}, uint64(sourceChain)) + assert.NoError(t, err1) + assert.Equal(t, seqNum, cciptypes.SeqNum(scc.MinSeqNr)) + } + + contractNames := maps.Keys(cfg.Contracts) + assert.Len(t, contractNames, 1, "test setup assumes there is only one contract") + + cr, err := evm.NewChainReaderService(ctx, lggr, lp, headTracker, cl, cfg) + require.NoError(t, err) + + extendedCr := contractreader.NewExtendedContractReader(cr) + err = extendedCr.Bind(ctx, []types.BoundContract{ + { + Address: address.String(), + Name: contractNames[0], + }, + }) + require.NoError(t, err) + + err = cr.Start(ctx) + require.NoError(t, err) + + contractReaders := map[cciptypes.ChainSelector]contractreader.Extended{readerChain: extendedCr} + contractWriters := make(map[cciptypes.ChainSelector]types.ChainWriter) + reader := ccipreaderpkg.NewCCIPReaderWithExtendedContractReaders(lggr, contractReaders, contractWriters, destChain) + + t.Cleanup(func() { + require.NoError(t, cr.Close()) + require.NoError(t, lp.Close()) + require.NoError(t, db.Close()) + }) + + return &testSetupData{ + contractAddr: address, + contract: contract, + sb: simulatedBackend, + auth: auth, + lp: lp, + cl: cl, + reader: reader, + } +} + +type testSetupData struct { + contractAddr common.Address + contract *ccip_reader_tester.CCIPReaderTester + sb *backends.SimulatedBackend + auth *bind.TransactOpts + lp logpoller.LogPoller + cl client.Client + reader ccipreaderpkg.CCIPReader +} diff --git a/core/capabilities/ccip/ccip_integration_tests/chainreader/Makefile b/core/capabilities/ccip/ccip_integration_tests/chainreader/Makefile new file mode 100644 index 00000000000..e9c88564e69 --- /dev/null +++ b/core/capabilities/ccip/ccip_integration_tests/chainreader/Makefile @@ -0,0 +1,12 @@ + +# IMPORTANT: If you encounter any issues try using solc 0.8.18 and abigen 1.14.5 + +.PHONY: build +build: + rm -rf build/ + solc --evm-version paris --abi --bin mycontract.sol -o build + abigen --abi build/mycontract_sol_SimpleContract.abi --bin build/mycontract_sol_SimpleContract.bin --pkg=chainreader --out=mycontract.go + +.PHONY: test +test: build + go test -v --tags "playground" ./... diff --git a/core/capabilities/ccip/ccip_integration_tests/chainreader/chainreader_test.go b/core/capabilities/ccip/ccip_integration_tests/chainreader/chainreader_test.go new file mode 100644 index 00000000000..52a3de0dae9 --- /dev/null +++ b/core/capabilities/ccip/ccip_integration_tests/chainreader/chainreader_test.go @@ -0,0 +1,273 @@ +//go:build playground +// +build playground + +package chainreader + +import ( + "context" + _ "embed" + "math/big" + "strconv" + "sync" + "testing" + "time" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/assert" + + "github.com/smartcontractkit/chainlink-common/pkg/codec" + types2 "github.com/smartcontractkit/chainlink-common/pkg/types" + query2 "github.com/smartcontractkit/chainlink-common/pkg/types/query" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/headtracker" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest" + logger2 "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm" + evmtypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" +) + +const chainID = 1337 + +type testSetupData struct { + contractAddr common.Address + contract *Chainreader + sb *backends.SimulatedBackend + auth *bind.TransactOpts +} + +func TestChainReader(t *testing.T) { + ctx := testutils.Context(t) + lggr := logger2.NullLogger + d := testSetup(t, ctx) + + db := pgtest.NewSqlxDB(t) + lpOpts := logpoller.Opts{ + PollPeriod: time.Millisecond, + FinalityDepth: 0, + BackfillBatchSize: 10, + RpcBatchSize: 10, + KeepFinalizedBlocksDepth: 100000, + } + cl := client.NewSimulatedBackendClient(t, d.sb, big.NewInt(chainID)) + headTracker := headtracker.NewSimulatedHeadTracker(cl, lpOpts.UseFinalityTag, lpOpts.FinalityDepth) + lp := logpoller.NewLogPoller(logpoller.NewORM(big.NewInt(chainID), db, lggr), + cl, + lggr, + headTracker, + lpOpts, + ) + assert.NoError(t, lp.Start(ctx)) + + const ( + ContractNameAlias = "myCoolContract" + + FnAliasGetCount = "myCoolFunction" + FnGetCount = "getEventCount" + + FnAliasGetNumbers = "GetNumbers" + FnGetNumbers = "getNumbers" + + FnAliasGetPerson = "GetPerson" + FnGetPerson = "getPerson" + + EventNameAlias = "myCoolEvent" + EventName = "SimpleEvent" + ) + + // Initialize chainReader + cfg := evmtypes.ChainReaderConfig{ + Contracts: map[string]evmtypes.ChainContractReader{ + ContractNameAlias: { + ContractPollingFilter: evmtypes.ContractPollingFilter{ + GenericEventNames: []string{EventNameAlias}, + }, + ContractABI: ChainreaderMetaData.ABI, + Configs: map[string]*evmtypes.ChainReaderDefinition{ + EventNameAlias: { + ChainSpecificName: EventName, + ReadType: evmtypes.Event, + ConfidenceConfirmations: map[string]int{"0.0": 0, "1.0": 0}, + }, + FnAliasGetCount: { + ChainSpecificName: FnGetCount, + }, + FnAliasGetNumbers: { + ChainSpecificName: FnGetNumbers, + OutputModifications: codec.ModifiersConfig{}, + }, + FnAliasGetPerson: { + ChainSpecificName: FnGetPerson, + OutputModifications: codec.ModifiersConfig{ + &codec.RenameModifierConfig{ + Fields: map[string]string{"Name": "NameField"}, // solidity name -> go struct name + }, + }, + }, + }, + }, + }, + } + + cr, err := evm.NewChainReaderService(ctx, lggr, lp, cl, cfg) + assert.NoError(t, err) + err = cr.Bind(ctx, []types2.BoundContract{ + { + Address: d.contractAddr.String(), + Name: ContractNameAlias, + Pending: false, + }, + }) + assert.NoError(t, err) + + err = cr.Start(ctx) + assert.NoError(t, err) + for { + if err := cr.Ready(); err == nil { + break + } + } + + emitEvents(t, d, ctx) // Calls the contract to emit events + + // (hack) Sometimes LP logs are missing, commit several times and wait few seconds to make it work. + for i := 0; i < 100; i++ { + d.sb.Commit() + } + time.Sleep(5 * time.Second) + + t.Run("simple contract read", func(t *testing.T) { + var cnt big.Int + err = cr.GetLatestValue(ctx, ContractNameAlias, FnAliasGetCount, map[string]interface{}{}, &cnt) + assert.NoError(t, err) + assert.Equal(t, int64(10), cnt.Int64()) + }) + + t.Run("read array", func(t *testing.T) { + var nums []big.Int + err = cr.GetLatestValue(ctx, ContractNameAlias, FnAliasGetNumbers, map[string]interface{}{}, &nums) + assert.NoError(t, err) + assert.Len(t, nums, 10) + for i := 1; i <= 10; i++ { + assert.Equal(t, int64(i), nums[i-1].Int64()) + } + }) + + t.Run("read struct", func(t *testing.T) { + person := struct { + NameField string + Age *big.Int // WARN: specifying a wrong data type e.g. int instead of *big.Int fails silently with a default value of 0 + }{} + err = cr.GetLatestValue(ctx, ContractNameAlias, FnAliasGetPerson, map[string]interface{}{}, &person) + assert.Equal(t, "Dim", person.NameField) + assert.Equal(t, int64(18), person.Age.Int64()) + }) + + t.Run("read events", func(t *testing.T) { + var myDataType *big.Int + seq, err := cr.QueryKey( + ctx, + ContractNameAlias, + query2.KeyFilter{ + Key: EventNameAlias, + Expressions: []query2.Expression{}, + }, + query2.LimitAndSort{}, + myDataType, + ) + assert.NoError(t, err) + assert.Equal(t, 10, len(seq), "expected 10 events from chain reader") + for _, v := range seq { + // TODO: for some reason log poller does not populate event data + blockNum, err := strconv.ParseUint(v.Identifier, 10, 64) + assert.NoError(t, err) + assert.Positive(t, blockNum) + t.Logf("(chain reader) got event: (data=%v) (hash=%x)", v.Data, v.Hash) + } + }) +} + +func testSetup(t *testing.T, ctx context.Context) *testSetupData { + // Generate a new key pair for the simulated account + privateKey, err := crypto.GenerateKey() + assert.NoError(t, err) + // Set up the genesis account with balance + blnc, ok := big.NewInt(0).SetString("999999999999999999999999999999999999", 10) + assert.True(t, ok) + alloc := map[common.Address]core.GenesisAccount{crypto.PubkeyToAddress(privateKey.PublicKey): {Balance: blnc}} + simulatedBackend := backends.NewSimulatedBackend(alloc, 0) + // Create a transactor + + auth, err := bind.NewKeyedTransactorWithChainID(privateKey, big.NewInt(chainID)) + assert.NoError(t, err) + auth.GasLimit = uint64(0) + + // Deploy the contract + address, tx, _, err := DeployChainreader(auth, simulatedBackend) + assert.NoError(t, err) + simulatedBackend.Commit() + t.Logf("contract deployed: addr=%s tx=%s", address.Hex(), tx.Hash()) + + // Setup contract client + contract, err := NewChainreader(address, simulatedBackend) + assert.NoError(t, err) + + return &testSetupData{ + contractAddr: address, + contract: contract, + sb: simulatedBackend, + auth: auth, + } +} + +func emitEvents(t *testing.T, d *testSetupData, ctx context.Context) { + var wg sync.WaitGroup + wg.Add(2) + + // Start emitting events + go func() { + defer wg.Done() + for i := 0; i < 10; i++ { + _, err := d.contract.EmitEvent(d.auth) + assert.NoError(t, err) + d.sb.Commit() + } + }() + + // Listen events using go-ethereum lib + go func() { + query := ethereum.FilterQuery{ + FromBlock: big.NewInt(0), + Addresses: []common.Address{d.contractAddr}, + } + logs := make(chan types.Log) + sub, err := d.sb.SubscribeFilterLogs(ctx, query, logs) + assert.NoError(t, err) + + numLogs := 0 + defer wg.Done() + for { + // Wait for the events + select { + case err := <-sub.Err(): + assert.NoError(t, err, "got an unexpected error") + case vLog := <-logs: + assert.Equal(t, d.contractAddr, vLog.Address, "got an unexpected address") + t.Logf("(geth) got new log (cnt=%d) (data=%x) (topics=%s)", numLogs, vLog.Data, vLog.Topics) + numLogs++ + if numLogs == 10 { + return + } + } + } + }() + + wg.Wait() // wait for all the events to be consumed +} diff --git a/core/capabilities/ccip/ccip_integration_tests/chainreader/mycontract.go b/core/capabilities/ccip/ccip_integration_tests/chainreader/mycontract.go new file mode 100644 index 00000000000..c7d480eed46 --- /dev/null +++ b/core/capabilities/ccip/ccip_integration_tests/chainreader/mycontract.go @@ -0,0 +1,519 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package chainreader + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// SimpleContractPerson is an auto generated low-level Go binding around an user-defined struct. +type SimpleContractPerson struct { + Name string + Age *big.Int +} + +// ChainreaderMetaData contains all meta data concerning the Chainreader contract. +var ChainreaderMetaData = &bind.MetaData{ + ABI: "[{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"SimpleEvent\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"emitEvent\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"eventCount\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getEventCount\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getNumbers\",\"outputs\":[{\"internalType\":\"uint256[]\",\"name\":\"\",\"type\":\"uint256[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getPerson\",\"outputs\":[{\"components\":[{\"internalType\":\"string\",\"name\":\"name\",\"type\":\"string\"},{\"internalType\":\"uint256\",\"name\":\"age\",\"type\":\"uint256\"}],\"internalType\":\"structSimpleContract.Person\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"numbers\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]", + Bin: "0x608060405234801561001057600080fd5b506105a1806100206000396000f3fe608060405234801561001057600080fd5b50600436106100625760003560e01c806371be2e4a146100675780637b0cb8391461008557806389f915f61461008f5780638ec4dc95146100ad578063d39fa233146100cb578063d9e48f5c146100fb575b600080fd5b61006f610119565b60405161007c91906102ac565b60405180910390f35b61008d61011f565b005b61009761019c565b6040516100a49190610385565b60405180910390f35b6100b56101f4565b6040516100c29190610474565b60405180910390f35b6100e560048036038101906100e091906104c7565b61024c565b6040516100f291906102ac565b60405180910390f35b610103610270565b60405161011091906102ac565b60405180910390f35b60005481565b60008081548092919061013190610523565b9190505550600160005490806001815401808255809150506001900390600052602060002001600090919091909150557f12d199749b3f4c44df8d9386c63d725b7756ec47204f3aa0bf05ea832f89effb60005460405161019291906102ac565b60405180910390a1565b606060018054806020026020016040519081016040528092919081815260200182805480156101ea57602002820191906000526020600020905b8154815260200190600101908083116101d6575b5050505050905090565b6101fc610279565b60405180604001604052806040518060400160405280600381526020017f44696d000000000000000000000000000000000000000000000000000000000081525081526020016012815250905090565b6001818154811061025c57600080fd5b906000526020600020016000915090505481565b60008054905090565b604051806040016040528060608152602001600081525090565b6000819050919050565b6102a681610293565b82525050565b60006020820190506102c1600083018461029d565b92915050565b600081519050919050565b600082825260208201905092915050565b6000819050602082019050919050565b6102fc81610293565b82525050565b600061030e83836102f3565b60208301905092915050565b6000602082019050919050565b6000610332826102c7565b61033c81856102d2565b9350610347836102e3565b8060005b8381101561037857815161035f8882610302565b975061036a8361031a565b92505060018101905061034b565b5085935050505092915050565b6000602082019050818103600083015261039f8184610327565b905092915050565b600081519050919050565b600082825260208201905092915050565b60005b838110156103e15780820151818401526020810190506103c6565b60008484015250505050565b6000601f19601f8301169050919050565b6000610409826103a7565b61041381856103b2565b93506104238185602086016103c3565b61042c816103ed565b840191505092915050565b6000604083016000830151848203600086015261045482826103fe565b915050602083015161046960208601826102f3565b508091505092915050565b6000602082019050818103600083015261048e8184610437565b905092915050565b600080fd5b6104a481610293565b81146104af57600080fd5b50565b6000813590506104c18161049b565b92915050565b6000602082840312156104dd576104dc610496565b5b60006104eb848285016104b2565b91505092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600061052e82610293565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82036105605761055f6104f4565b5b60018201905091905056fea2646970667358221220f7986dc9efbc0d9ef58e2925ffddc62ea13a6bab8b3a2c03ad2d85d50653129664736f6c63430008120033", +} + +// ChainreaderABI is the input ABI used to generate the binding from. +// Deprecated: Use ChainreaderMetaData.ABI instead. +var ChainreaderABI = ChainreaderMetaData.ABI + +// ChainreaderBin is the compiled bytecode used for deploying new contracts. +// Deprecated: Use ChainreaderMetaData.Bin instead. +var ChainreaderBin = ChainreaderMetaData.Bin + +// DeployChainreader deploys a new Ethereum contract, binding an instance of Chainreader to it. +func DeployChainreader(auth *bind.TransactOpts, backend bind.ContractBackend) (common.Address, *types.Transaction, *Chainreader, error) { + parsed, err := ChainreaderMetaData.GetAbi() + if err != nil { + return common.Address{}, nil, nil, err + } + if parsed == nil { + return common.Address{}, nil, nil, errors.New("GetABI returned nil") + } + + address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(ChainreaderBin), backend) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, &Chainreader{ChainreaderCaller: ChainreaderCaller{contract: contract}, ChainreaderTransactor: ChainreaderTransactor{contract: contract}, ChainreaderFilterer: ChainreaderFilterer{contract: contract}}, nil +} + +// Chainreader is an auto generated Go binding around an Ethereum contract. +type Chainreader struct { + ChainreaderCaller // Read-only binding to the contract + ChainreaderTransactor // Write-only binding to the contract + ChainreaderFilterer // Log filterer for contract events +} + +// ChainreaderCaller is an auto generated read-only Go binding around an Ethereum contract. +type ChainreaderCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ChainreaderTransactor is an auto generated write-only Go binding around an Ethereum contract. +type ChainreaderTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ChainreaderFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type ChainreaderFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ChainreaderSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type ChainreaderSession struct { + Contract *Chainreader // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// ChainreaderCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type ChainreaderCallerSession struct { + Contract *ChainreaderCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// ChainreaderTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type ChainreaderTransactorSession struct { + Contract *ChainreaderTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// ChainreaderRaw is an auto generated low-level Go binding around an Ethereum contract. +type ChainreaderRaw struct { + Contract *Chainreader // Generic contract binding to access the raw methods on +} + +// ChainreaderCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type ChainreaderCallerRaw struct { + Contract *ChainreaderCaller // Generic read-only contract binding to access the raw methods on +} + +// ChainreaderTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type ChainreaderTransactorRaw struct { + Contract *ChainreaderTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewChainreader creates a new instance of Chainreader, bound to a specific deployed contract. +func NewChainreader(address common.Address, backend bind.ContractBackend) (*Chainreader, error) { + contract, err := bindChainreader(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &Chainreader{ChainreaderCaller: ChainreaderCaller{contract: contract}, ChainreaderTransactor: ChainreaderTransactor{contract: contract}, ChainreaderFilterer: ChainreaderFilterer{contract: contract}}, nil +} + +// NewChainreaderCaller creates a new read-only instance of Chainreader, bound to a specific deployed contract. +func NewChainreaderCaller(address common.Address, caller bind.ContractCaller) (*ChainreaderCaller, error) { + contract, err := bindChainreader(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &ChainreaderCaller{contract: contract}, nil +} + +// NewChainreaderTransactor creates a new write-only instance of Chainreader, bound to a specific deployed contract. +func NewChainreaderTransactor(address common.Address, transactor bind.ContractTransactor) (*ChainreaderTransactor, error) { + contract, err := bindChainreader(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &ChainreaderTransactor{contract: contract}, nil +} + +// NewChainreaderFilterer creates a new log filterer instance of Chainreader, bound to a specific deployed contract. +func NewChainreaderFilterer(address common.Address, filterer bind.ContractFilterer) (*ChainreaderFilterer, error) { + contract, err := bindChainreader(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &ChainreaderFilterer{contract: contract}, nil +} + +// bindChainreader binds a generic wrapper to an already deployed contract. +func bindChainreader(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := ChainreaderMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Chainreader *ChainreaderRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Chainreader.Contract.ChainreaderCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Chainreader *ChainreaderRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Chainreader.Contract.ChainreaderTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Chainreader *ChainreaderRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Chainreader.Contract.ChainreaderTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Chainreader *ChainreaderCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Chainreader.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Chainreader *ChainreaderTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Chainreader.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Chainreader *ChainreaderTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Chainreader.Contract.contract.Transact(opts, method, params...) +} + +// EventCount is a free data retrieval call binding the contract method 0x71be2e4a. +// +// Solidity: function eventCount() view returns(uint256) +func (_Chainreader *ChainreaderCaller) EventCount(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Chainreader.contract.Call(opts, &out, "eventCount") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// EventCount is a free data retrieval call binding the contract method 0x71be2e4a. +// +// Solidity: function eventCount() view returns(uint256) +func (_Chainreader *ChainreaderSession) EventCount() (*big.Int, error) { + return _Chainreader.Contract.EventCount(&_Chainreader.CallOpts) +} + +// EventCount is a free data retrieval call binding the contract method 0x71be2e4a. +// +// Solidity: function eventCount() view returns(uint256) +func (_Chainreader *ChainreaderCallerSession) EventCount() (*big.Int, error) { + return _Chainreader.Contract.EventCount(&_Chainreader.CallOpts) +} + +// GetEventCount is a free data retrieval call binding the contract method 0xd9e48f5c. +// +// Solidity: function getEventCount() view returns(uint256) +func (_Chainreader *ChainreaderCaller) GetEventCount(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Chainreader.contract.Call(opts, &out, "getEventCount") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// GetEventCount is a free data retrieval call binding the contract method 0xd9e48f5c. +// +// Solidity: function getEventCount() view returns(uint256) +func (_Chainreader *ChainreaderSession) GetEventCount() (*big.Int, error) { + return _Chainreader.Contract.GetEventCount(&_Chainreader.CallOpts) +} + +// GetEventCount is a free data retrieval call binding the contract method 0xd9e48f5c. +// +// Solidity: function getEventCount() view returns(uint256) +func (_Chainreader *ChainreaderCallerSession) GetEventCount() (*big.Int, error) { + return _Chainreader.Contract.GetEventCount(&_Chainreader.CallOpts) +} + +// GetNumbers is a free data retrieval call binding the contract method 0x89f915f6. +// +// Solidity: function getNumbers() view returns(uint256[]) +func (_Chainreader *ChainreaderCaller) GetNumbers(opts *bind.CallOpts) ([]*big.Int, error) { + var out []interface{} + err := _Chainreader.contract.Call(opts, &out, "getNumbers") + + if err != nil { + return *new([]*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new([]*big.Int)).(*[]*big.Int) + + return out0, err + +} + +// GetNumbers is a free data retrieval call binding the contract method 0x89f915f6. +// +// Solidity: function getNumbers() view returns(uint256[]) +func (_Chainreader *ChainreaderSession) GetNumbers() ([]*big.Int, error) { + return _Chainreader.Contract.GetNumbers(&_Chainreader.CallOpts) +} + +// GetNumbers is a free data retrieval call binding the contract method 0x89f915f6. +// +// Solidity: function getNumbers() view returns(uint256[]) +func (_Chainreader *ChainreaderCallerSession) GetNumbers() ([]*big.Int, error) { + return _Chainreader.Contract.GetNumbers(&_Chainreader.CallOpts) +} + +// GetPerson is a free data retrieval call binding the contract method 0x8ec4dc95. +// +// Solidity: function getPerson() pure returns((string,uint256)) +func (_Chainreader *ChainreaderCaller) GetPerson(opts *bind.CallOpts) (SimpleContractPerson, error) { + var out []interface{} + err := _Chainreader.contract.Call(opts, &out, "getPerson") + + if err != nil { + return *new(SimpleContractPerson), err + } + + out0 := *abi.ConvertType(out[0], new(SimpleContractPerson)).(*SimpleContractPerson) + + return out0, err + +} + +// GetPerson is a free data retrieval call binding the contract method 0x8ec4dc95. +// +// Solidity: function getPerson() pure returns((string,uint256)) +func (_Chainreader *ChainreaderSession) GetPerson() (SimpleContractPerson, error) { + return _Chainreader.Contract.GetPerson(&_Chainreader.CallOpts) +} + +// GetPerson is a free data retrieval call binding the contract method 0x8ec4dc95. +// +// Solidity: function getPerson() pure returns((string,uint256)) +func (_Chainreader *ChainreaderCallerSession) GetPerson() (SimpleContractPerson, error) { + return _Chainreader.Contract.GetPerson(&_Chainreader.CallOpts) +} + +// Numbers is a free data retrieval call binding the contract method 0xd39fa233. +// +// Solidity: function numbers(uint256 ) view returns(uint256) +func (_Chainreader *ChainreaderCaller) Numbers(opts *bind.CallOpts, arg0 *big.Int) (*big.Int, error) { + var out []interface{} + err := _Chainreader.contract.Call(opts, &out, "numbers", arg0) + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// Numbers is a free data retrieval call binding the contract method 0xd39fa233. +// +// Solidity: function numbers(uint256 ) view returns(uint256) +func (_Chainreader *ChainreaderSession) Numbers(arg0 *big.Int) (*big.Int, error) { + return _Chainreader.Contract.Numbers(&_Chainreader.CallOpts, arg0) +} + +// Numbers is a free data retrieval call binding the contract method 0xd39fa233. +// +// Solidity: function numbers(uint256 ) view returns(uint256) +func (_Chainreader *ChainreaderCallerSession) Numbers(arg0 *big.Int) (*big.Int, error) { + return _Chainreader.Contract.Numbers(&_Chainreader.CallOpts, arg0) +} + +// EmitEvent is a paid mutator transaction binding the contract method 0x7b0cb839. +// +// Solidity: function emitEvent() returns() +func (_Chainreader *ChainreaderTransactor) EmitEvent(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Chainreader.contract.Transact(opts, "emitEvent") +} + +// EmitEvent is a paid mutator transaction binding the contract method 0x7b0cb839. +// +// Solidity: function emitEvent() returns() +func (_Chainreader *ChainreaderSession) EmitEvent() (*types.Transaction, error) { + return _Chainreader.Contract.EmitEvent(&_Chainreader.TransactOpts) +} + +// EmitEvent is a paid mutator transaction binding the contract method 0x7b0cb839. +// +// Solidity: function emitEvent() returns() +func (_Chainreader *ChainreaderTransactorSession) EmitEvent() (*types.Transaction, error) { + return _Chainreader.Contract.EmitEvent(&_Chainreader.TransactOpts) +} + +// ChainreaderSimpleEventIterator is returned from FilterSimpleEvent and is used to iterate over the raw logs and unpacked data for SimpleEvent events raised by the Chainreader contract. +type ChainreaderSimpleEventIterator struct { + Event *ChainreaderSimpleEvent // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *ChainreaderSimpleEventIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(ChainreaderSimpleEvent) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(ChainreaderSimpleEvent) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *ChainreaderSimpleEventIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *ChainreaderSimpleEventIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// ChainreaderSimpleEvent represents a SimpleEvent event raised by the Chainreader contract. +type ChainreaderSimpleEvent struct { + Value *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterSimpleEvent is a free log retrieval operation binding the contract event 0x12d199749b3f4c44df8d9386c63d725b7756ec47204f3aa0bf05ea832f89effb. +// +// Solidity: event SimpleEvent(uint256 value) +func (_Chainreader *ChainreaderFilterer) FilterSimpleEvent(opts *bind.FilterOpts) (*ChainreaderSimpleEventIterator, error) { + + logs, sub, err := _Chainreader.contract.FilterLogs(opts, "SimpleEvent") + if err != nil { + return nil, err + } + return &ChainreaderSimpleEventIterator{contract: _Chainreader.contract, event: "SimpleEvent", logs: logs, sub: sub}, nil +} + +// WatchSimpleEvent is a free log subscription operation binding the contract event 0x12d199749b3f4c44df8d9386c63d725b7756ec47204f3aa0bf05ea832f89effb. +// +// Solidity: event SimpleEvent(uint256 value) +func (_Chainreader *ChainreaderFilterer) WatchSimpleEvent(opts *bind.WatchOpts, sink chan<- *ChainreaderSimpleEvent) (event.Subscription, error) { + + logs, sub, err := _Chainreader.contract.WatchLogs(opts, "SimpleEvent") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(ChainreaderSimpleEvent) + if err := _Chainreader.contract.UnpackLog(event, "SimpleEvent", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseSimpleEvent is a log parse operation binding the contract event 0x12d199749b3f4c44df8d9386c63d725b7756ec47204f3aa0bf05ea832f89effb. +// +// Solidity: event SimpleEvent(uint256 value) +func (_Chainreader *ChainreaderFilterer) ParseSimpleEvent(log types.Log) (*ChainreaderSimpleEvent, error) { + event := new(ChainreaderSimpleEvent) + if err := _Chainreader.contract.UnpackLog(event, "SimpleEvent", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} diff --git a/core/capabilities/ccip/ccip_integration_tests/chainreader/mycontract.sol b/core/capabilities/ccip/ccip_integration_tests/chainreader/mycontract.sol new file mode 100644 index 00000000000..0fae1f4baac --- /dev/null +++ b/core/capabilities/ccip/ccip_integration_tests/chainreader/mycontract.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.18; + +contract SimpleContract { + event SimpleEvent(uint256 value); + uint256 public eventCount; + uint[] public numbers; + + struct Person { + string name; + uint age; + } + + function emitEvent() public { + eventCount++; + numbers.push(eventCount); + emit SimpleEvent(eventCount); + } + + function getEventCount() public view returns (uint256) { + return eventCount; + } + + function getNumbers() public view returns (uint256[] memory) { + return numbers; + } + + function getPerson() public pure returns (Person memory) { + return Person("Dim", 18); + } +} diff --git a/core/capabilities/ccip/ccip_integration_tests/helpers.go b/core/capabilities/ccip/ccip_integration_tests/helpers.go new file mode 100644 index 00000000000..7606c8bbebc --- /dev/null +++ b/core/capabilities/ccip/ccip_integration_tests/helpers.go @@ -0,0 +1,938 @@ +package ccip_integration_tests + +import ( + "bytes" + "encoding/hex" + "math/big" + "sort" + "testing" + "time" + + "github.com/smartcontractkit/chainlink-ccip/chainconfig" + "github.com/smartcontractkit/chainlink-ccip/pluginconfig" + commonconfig "github.com/smartcontractkit/chainlink-common/pkg/config" + "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/ccip_integration_tests/integrationhelpers" + cctypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + + confighelper2 "github.com/smartcontractkit/libocr/offchainreporting2plus/confighelper" + "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3confighelper" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/arm_proxy_contract" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/ccip_config" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_multi_offramp" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_multi_onramp" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/maybe_revert_message_receiver" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/mock_arm_contract" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/nonce_manager" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/ocr3_config_encoder" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/price_registry" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/router" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/token_admin_registry" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/weth9" + kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/shared/generated/link_token" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + + chainsel "github.com/smartcontractkit/chain-selectors" + + "github.com/stretchr/testify/require" +) + +var ( + homeChainID = chainsel.GETH_TESTNET.EvmChainID + ccipSendRequestedTopic = evm_2_evm_multi_onramp.EVM2EVMMultiOnRampCCIPSendRequested{}.Topic() + commitReportAcceptedTopic = evm_2_evm_multi_offramp.EVM2EVMMultiOffRampCommitReportAccepted{}.Topic() + executionStateChangedTopic = evm_2_evm_multi_offramp.EVM2EVMMultiOffRampExecutionStateChanged{}.Topic() +) + +const ( + CapabilityLabelledName = "ccip" + CapabilityVersion = "v1.0.0" + NodeOperatorID = 1 + + // These constants drive what is set in the plugin offchain configs. + FirstBlockAge = 8 * time.Hour + RemoteGasPriceBatchWriteFrequency = 30 * time.Minute + BatchGasLimit = 6_500_000 + RelativeBoostPerWaitHour = 1.5 + InflightCacheExpiry = 10 * time.Minute + RootSnoozeTime = 30 * time.Minute + BatchingStrategyID = 0 + DeltaProgress = 30 * time.Second + DeltaResend = 10 * time.Second + DeltaInitial = 20 * time.Second + DeltaRound = 2 * time.Second + DeltaGrace = 2 * time.Second + DeltaCertifiedCommitRequest = 10 * time.Second + DeltaStage = 10 * time.Second + Rmax = 3 + MaxDurationQuery = 50 * time.Millisecond + MaxDurationObservation = 5 * time.Second + MaxDurationShouldAcceptAttestedReport = 10 * time.Second + MaxDurationShouldTransmitAcceptedReport = 10 * time.Second +) + +func e18Mult(amount uint64) *big.Int { + return new(big.Int).Mul(uBigInt(amount), uBigInt(1e18)) +} + +func uBigInt(i uint64) *big.Int { + return new(big.Int).SetUint64(i) +} + +type homeChain struct { + backend *backends.SimulatedBackend + owner *bind.TransactOpts + chainID uint64 + capabilityRegistry *kcr.CapabilitiesRegistry + ccipConfig *ccip_config.CCIPConfig +} + +type onchainUniverse struct { + backend *backends.SimulatedBackend + owner *bind.TransactOpts + chainID uint64 + linkToken *link_token.LinkToken + weth *weth9.WETH9 + router *router.Router + rmnProxy *arm_proxy_contract.ARMProxyContract + rmn *mock_arm_contract.MockARMContract + onramp *evm_2_evm_multi_onramp.EVM2EVMMultiOnRamp + offramp *evm_2_evm_multi_offramp.EVM2EVMMultiOffRamp + priceRegistry *price_registry.PriceRegistry + tokenAdminRegistry *token_admin_registry.TokenAdminRegistry + nonceManager *nonce_manager.NonceManager + receiver *maybe_revert_message_receiver.MaybeRevertMessageReceiver +} + +type requestData struct { + destChainSelector uint64 + receiverAddress common.Address + data []byte +} + +func (u *onchainUniverse) SendCCIPRequests(t *testing.T, requestDatas []requestData) { + for _, reqData := range requestDatas { + msg := router.ClientEVM2AnyMessage{ + Receiver: common.LeftPadBytes(reqData.receiverAddress.Bytes(), 32), + Data: reqData.data, + TokenAmounts: nil, // TODO: no tokens for now + FeeToken: u.weth.Address(), + ExtraArgs: nil, // TODO: no extra args for now, falls back to default + } + fee, err := u.router.GetFee(&bind.CallOpts{Context: testutils.Context(t)}, reqData.destChainSelector, msg) + require.NoError(t, err) + _, err = u.weth.Deposit(&bind.TransactOpts{ + From: u.owner.From, + Signer: u.owner.Signer, + Value: fee, + }) + require.NoError(t, err) + u.backend.Commit() + _, err = u.weth.Approve(u.owner, u.router.Address(), fee) + require.NoError(t, err) + u.backend.Commit() + + t.Logf("Sending CCIP request from chain %d (selector %d) to chain selector %d", + u.chainID, getSelector(u.chainID), reqData.destChainSelector) + _, err = u.router.CcipSend(u.owner, reqData.destChainSelector, msg) + require.NoError(t, err) + u.backend.Commit() + } +} + +type chainBase struct { + backend *backends.SimulatedBackend + owner *bind.TransactOpts +} + +// createUniverses does the following: +// 1. Creates 1 home chain and `numChains`-1 non-home chains +// 2. Sets up home chain with the capability registry and the CCIP config contract +// 2. Deploys the CCIP contracts to all chains. +// 3. Sets up the initial configurations for the contracts on all chains. +// 4. Wires the chains together. +// +// Conceptually one universe is ONE chain with all the contracts deployed on it and all the dependencies initialized. +func createUniverses( + t *testing.T, + numChains int, +) (homeChainUni homeChain, universes map[uint64]onchainUniverse) { + chains := createChains(t, numChains) + + homeChainBase, ok := chains[homeChainID] + require.True(t, ok, "home chain backend not available") + // Set up home chain first + homeChainUniverse := setupHomeChain(t, homeChainBase.owner, homeChainBase.backend) + + // deploy the ccip contracts on all chains + universes = make(map[uint64]onchainUniverse) + for chainID, base := range chains { + owner := base.owner + backend := base.backend + // deploy the CCIP contracts + linkToken := deployLinkToken(t, owner, backend, chainID) + rmn := deployMockARMContract(t, owner, backend, chainID) + rmnProxy := deployARMProxyContract(t, owner, backend, rmn.Address(), chainID) + weth := deployWETHContract(t, owner, backend, chainID) + rout := deployRouter(t, owner, backend, weth.Address(), rmnProxy.Address(), chainID) + priceRegistry := deployPriceRegistry(t, owner, backend, linkToken.Address(), weth.Address(), big.NewInt(1e18), chainID) + tokenAdminRegistry := deployTokenAdminRegistry(t, owner, backend, chainID) + nonceManager := deployNonceManager(t, owner, backend, chainID) + + // ====================================================================== + // OnRamp + // ====================================================================== + onRampAddr, _, _, err := evm_2_evm_multi_onramp.DeployEVM2EVMMultiOnRamp( + owner, + backend, + evm_2_evm_multi_onramp.EVM2EVMMultiOnRampStaticConfig{ + ChainSelector: getSelector(chainID), + RmnProxy: rmnProxy.Address(), + NonceManager: nonceManager.Address(), + TokenAdminRegistry: tokenAdminRegistry.Address(), + }, + evm_2_evm_multi_onramp.EVM2EVMMultiOnRampDynamicConfig{ + Router: rout.Address(), + PriceRegistry: priceRegistry.Address(), + // `withdrawFeeTokens` onRamp function is not part of the message flow + // so we can set this to any address + FeeAggregator: testutils.NewAddress(), + }, + ) + require.NoErrorf(t, err, "failed to deploy onramp on chain id %d", chainID) + backend.Commit() + onramp, err := evm_2_evm_multi_onramp.NewEVM2EVMMultiOnRamp(onRampAddr, backend) + require.NoError(t, err) + + // ====================================================================== + // OffRamp + // ====================================================================== + offrampAddr, _, _, err := evm_2_evm_multi_offramp.DeployEVM2EVMMultiOffRamp( + owner, + backend, + evm_2_evm_multi_offramp.EVM2EVMMultiOffRampStaticConfig{ + ChainSelector: getSelector(chainID), + RmnProxy: rmnProxy.Address(), + TokenAdminRegistry: tokenAdminRegistry.Address(), + NonceManager: nonceManager.Address(), + }, + evm_2_evm_multi_offramp.EVM2EVMMultiOffRampDynamicConfig{ + Router: rout.Address(), + PriceRegistry: priceRegistry.Address(), + }, + // Source chain configs will be set up later once we have all chains + []evm_2_evm_multi_offramp.EVM2EVMMultiOffRampSourceChainConfigArgs{}, + ) + require.NoErrorf(t, err, "failed to deploy offramp on chain id %d", chainID) + backend.Commit() + offramp, err := evm_2_evm_multi_offramp.NewEVM2EVMMultiOffRamp(offrampAddr, backend) + require.NoError(t, err) + + receiverAddress, _, _, err := maybe_revert_message_receiver.DeployMaybeRevertMessageReceiver( + owner, + backend, + false, + ) + require.NoError(t, err, "failed to deploy MaybeRevertMessageReceiver on chain id %d", chainID) + backend.Commit() + receiver, err := maybe_revert_message_receiver.NewMaybeRevertMessageReceiver(receiverAddress, backend) + require.NoError(t, err) + + universe := onchainUniverse{ + backend: backend, + owner: owner, + chainID: chainID, + linkToken: linkToken, + weth: weth, + router: rout, + rmnProxy: rmnProxy, + rmn: rmn, + onramp: onramp, + offramp: offramp, + priceRegistry: priceRegistry, + tokenAdminRegistry: tokenAdminRegistry, + nonceManager: nonceManager, + receiver: receiver, + } + // Set up the initial configurations for the contracts + setupUniverseBasics(t, universe) + + universes[chainID] = universe + } + + // Once we have all chains created and contracts deployed, we can set up the initial configurations and wire chains together + connectUniverses(t, universes) + + // print out all contract addresses for debugging purposes + for chainID, uni := range universes { + t.Logf("Chain ID: %d\n Chain Selector: %d\n LinkToken: %s\n WETH: %s\n Router: %s\n RMNProxy: %s\n RMN: %s\n OnRamp: %s\n OffRamp: %s\n PriceRegistry: %s\n TokenAdminRegistry: %s\n NonceManager: %s\n", + chainID, + getSelector(chainID), + uni.linkToken.Address().Hex(), + uni.weth.Address().Hex(), + uni.router.Address().Hex(), + uni.rmnProxy.Address().Hex(), + uni.rmn.Address().Hex(), + uni.onramp.Address().Hex(), + uni.offramp.Address().Hex(), + uni.priceRegistry.Address().Hex(), + uni.tokenAdminRegistry.Address().Hex(), + uni.nonceManager.Address().Hex(), + ) + } + + // print out topic hashes of relevant events for debugging purposes + t.Logf("Topic hash of CommitReportAccepted: %s", commitReportAcceptedTopic.Hex()) + t.Logf("Topic hash of ExecutionStateChanged: %s", executionStateChangedTopic.Hex()) + t.Logf("Topic hash of CCIPSendRequested: %s", ccipSendRequestedTopic.Hex()) + + return homeChainUniverse, universes +} + +// Creates 1 home chain and `numChains`-1 non-home chains +func createChains(t *testing.T, numChains int) map[uint64]chainBase { + chains := make(map[uint64]chainBase) + + homeChainOwner := testutils.MustNewSimTransactor(t) + homeChainBackend := backends.NewSimulatedBackend(core.GenesisAlloc{ + homeChainOwner.From: core.GenesisAccount{ + Balance: assets.Ether(10_000).ToInt(), + }, + }, 30e6) + tweakChainTimestamp(t, homeChainBackend, FirstBlockAge) + + chains[homeChainID] = chainBase{ + owner: homeChainOwner, + backend: homeChainBackend, + } + + for chainID := chainsel.TEST_90000001.EvmChainID; len(chains) < numChains && chainID < chainsel.TEST_90000020.EvmChainID; chainID++ { + owner := testutils.MustNewSimTransactor(t) + backend := backends.NewSimulatedBackend(core.GenesisAlloc{ + owner.From: core.GenesisAccount{ + Balance: assets.Ether(10_000).ToInt(), + }, + }, 30e6) + + tweakChainTimestamp(t, backend, FirstBlockAge) + + chains[chainID] = chainBase{ + owner: owner, + backend: backend, + } + } + + return chains +} + +// CCIP relies on block timestamps, but SimulatedBackend uses by default clock starting from 1970-01-01 +// This trick is used to move the clock closer to the current time. We set first block to be X hours ago. +// Tests create plenty of transactions so this number can't be too low, every new block mined will tick the clock, +// if you mine more than "X hours" transactions, SimulatedBackend will panic because generated timestamps will be in the future. +func tweakChainTimestamp(t *testing.T, backend *backends.SimulatedBackend, tweak time.Duration) { + blockTime := time.Unix(int64(backend.Blockchain().CurrentHeader().Time), 0) + sinceBlockTime := time.Since(blockTime) + diff := sinceBlockTime - tweak + err := backend.AdjustTime(diff) + require.NoError(t, err, "unable to adjust time on simulated chain") + backend.Commit() + backend.Commit() +} + +func setupHomeChain(t *testing.T, owner *bind.TransactOpts, backend *backends.SimulatedBackend) homeChain { + // deploy the capability registry on the home chain + crAddress, _, _, err := kcr.DeployCapabilitiesRegistry(owner, backend) + require.NoError(t, err, "failed to deploy capability registry on home chain") + backend.Commit() + + capabilityRegistry, err := kcr.NewCapabilitiesRegistry(crAddress, backend) + require.NoError(t, err) + + ccAddress, _, _, err := ccip_config.DeployCCIPConfig(owner, backend, crAddress) + require.NoError(t, err) + backend.Commit() + + capabilityConfig, err := ccip_config.NewCCIPConfig(ccAddress, backend) + require.NoError(t, err) + + _, err = capabilityRegistry.AddCapabilities(owner, []kcr.CapabilitiesRegistryCapability{ + { + LabelledName: CapabilityLabelledName, + Version: CapabilityVersion, + CapabilityType: 2, // consensus. not used (?) + ResponseType: 0, // report. not used (?) + ConfigurationContract: ccAddress, + }, + }) + require.NoError(t, err, "failed to add capabilities to the capability registry") + backend.Commit() + + // Add NodeOperator, for simplicity we'll add one NodeOperator only + // First NodeOperator will have NodeOperatorId = 1 + _, err = capabilityRegistry.AddNodeOperators(owner, []kcr.CapabilitiesRegistryNodeOperator{ + { + Admin: owner.From, + Name: "NodeOperator", + }, + }) + require.NoError(t, err, "failed to add node operator to the capability registry") + backend.Commit() + + return homeChain{ + backend: backend, + owner: owner, + chainID: homeChainID, + capabilityRegistry: capabilityRegistry, + ccipConfig: capabilityConfig, + } +} + +func sortP2PIDS(p2pIDs [][32]byte) { + sort.Slice(p2pIDs, func(i, j int) bool { + return bytes.Compare(p2pIDs[i][:], p2pIDs[j][:]) < 0 + }) +} + +func (h *homeChain) AddNodes( + t *testing.T, + p2pIDs [][32]byte, + capabilityIDs [][32]byte, +) { + // Need to sort, otherwise _checkIsValidUniqueSubset onChain will fail + sortP2PIDS(p2pIDs) + var nodeParams []kcr.CapabilitiesRegistryNodeParams + for _, p2pID := range p2pIDs { + nodeParam := kcr.CapabilitiesRegistryNodeParams{ + NodeOperatorId: NodeOperatorID, + Signer: p2pID, // Not used in tests + P2pId: p2pID, + HashedCapabilityIds: capabilityIDs, + } + nodeParams = append(nodeParams, nodeParam) + } + _, err := h.capabilityRegistry.AddNodes(h.owner, nodeParams) + require.NoError(t, err, "failed to add node operator oracles") + h.backend.Commit() +} + +func AddChainConfig( + t *testing.T, + h homeChain, + chainSelector uint64, + p2pIDs [][32]byte, + f uint8, +) ccip_config.CCIPConfigTypesChainConfigInfo { + // Need to sort, otherwise _checkIsValidUniqueSubset onChain will fail + sortP2PIDS(p2pIDs) + // First Add ChainConfig that includes all p2pIDs as readers + encodedExtraChainConfig, err := chainconfig.EncodeChainConfig(chainconfig.ChainConfig{ + GasPriceDeviationPPB: ccipocr3.NewBigIntFromInt64(1000), + DAGasPriceDeviationPPB: ccipocr3.NewBigIntFromInt64(0), + FinalityDepth: 10, + OptimisticConfirmations: 1, + }) + require.NoError(t, err) + chainConfig := integrationhelpers.SetupConfigInfo(chainSelector, p2pIDs, f, encodedExtraChainConfig) + inputConfig := []ccip_config.CCIPConfigTypesChainConfigInfo{ + chainConfig, + } + _, err = h.ccipConfig.ApplyChainConfigUpdates(h.owner, nil, inputConfig) + require.NoError(t, err) + h.backend.Commit() + return chainConfig +} + +func (h *homeChain) AddDON( + t *testing.T, + ccipCapabilityID [32]byte, + chainSelector uint64, + uni onchainUniverse, + f uint8, + bootstrapP2PID [32]byte, + p2pIDs [][32]byte, + oracles []confighelper2.OracleIdentityExtra, +) { + // Get OCR3 Config from helper + var schedule []int + for range oracles { + schedule = append(schedule, 1) + } + + tabi, err := ocr3_config_encoder.IOCR3ConfigEncoderMetaData.GetAbi() + require.NoError(t, err) + + // Add DON on capability registry contract + var ocr3Configs []ocr3_config_encoder.CCIPConfigTypesOCR3Config + for _, pluginType := range []cctypes.PluginType{cctypes.PluginTypeCCIPCommit, cctypes.PluginTypeCCIPExec} { + var encodedOffchainConfig []byte + var err2 error + if pluginType == cctypes.PluginTypeCCIPCommit { + encodedOffchainConfig, err2 = pluginconfig.EncodeCommitOffchainConfig(pluginconfig.CommitOffchainConfig{ + RemoteGasPriceBatchWriteFrequency: *commonconfig.MustNewDuration(RemoteGasPriceBatchWriteFrequency), + // TODO: implement token price writes + // TokenPriceBatchWriteFrequency: *commonconfig.MustNewDuration(tokenPriceBatchWriteFrequency), + }) + require.NoError(t, err2) + } else { + encodedOffchainConfig, err2 = pluginconfig.EncodeExecuteOffchainConfig(pluginconfig.ExecuteOffchainConfig{ + BatchGasLimit: BatchGasLimit, + RelativeBoostPerWaitHour: RelativeBoostPerWaitHour, + MessageVisibilityInterval: *commonconfig.MustNewDuration(FirstBlockAge), + InflightCacheExpiry: *commonconfig.MustNewDuration(InflightCacheExpiry), + RootSnoozeTime: *commonconfig.MustNewDuration(RootSnoozeTime), + BatchingStrategyID: BatchingStrategyID, + }) + require.NoError(t, err2) + } + signers, transmitters, configF, _, offchainConfigVersion, offchainConfig, err2 := ocr3confighelper.ContractSetConfigArgsForTests( + DeltaProgress, + DeltaResend, + DeltaInitial, + DeltaRound, + DeltaGrace, + DeltaCertifiedCommitRequest, + DeltaStage, + Rmax, + schedule, + oracles, + encodedOffchainConfig, + MaxDurationQuery, + MaxDurationObservation, + MaxDurationShouldAcceptAttestedReport, + MaxDurationShouldTransmitAcceptedReport, + int(f), + []byte{}, // empty OnChainConfig + ) + require.NoError(t, err2, "failed to create contract config") + + signersBytes := make([][]byte, len(signers)) + for i, signer := range signers { + signersBytes[i] = signer + } + + transmittersBytes := make([][]byte, len(transmitters)) + for i, transmitter := range transmitters { + // anotherErr because linting doesn't want to shadow err + parsed, anotherErr := common.ParseHexOrString(string(transmitter)) + require.NoError(t, anotherErr) + transmittersBytes[i] = parsed + } + + ocr3Configs = append(ocr3Configs, ocr3_config_encoder.CCIPConfigTypesOCR3Config{ + PluginType: uint8(pluginType), + ChainSelector: chainSelector, + F: configF, + OffchainConfigVersion: offchainConfigVersion, + OfframpAddress: uni.offramp.Address().Bytes(), + BootstrapP2PIds: [][32]byte{bootstrapP2PID}, + P2pIds: p2pIDs, + Signers: signersBytes, + Transmitters: transmittersBytes, + OffchainConfig: offchainConfig, + }) + } + + encodedCall, err := tabi.Pack("exposeOCR3Config", ocr3Configs) + require.NoError(t, err) + + // Trim first four bytes to remove function selector. + encodedConfigs := encodedCall[4:] + + // commit so that we have an empty block to filter events from + h.backend.Commit() + + _, err = h.capabilityRegistry.AddDON(h.owner, p2pIDs, []kcr.CapabilitiesRegistryCapabilityConfiguration{ + { + CapabilityId: ccipCapabilityID, + Config: encodedConfigs, + }, + }, false, false, f) + require.NoError(t, err) + h.backend.Commit() + + endBlock := h.backend.Blockchain().CurrentBlock().Number.Uint64() + iter, err := h.capabilityRegistry.FilterConfigSet(&bind.FilterOpts{ + Start: h.backend.Blockchain().CurrentBlock().Number.Uint64() - 1, + End: &endBlock, + }) + require.NoError(t, err, "failed to filter config set events") + var donID uint32 + for iter.Next() { + donID = iter.Event.DonId + break + } + require.NotZero(t, donID, "failed to get donID from config set event") + + var signerAddresses []common.Address + for _, oracle := range oracles { + signerAddresses = append(signerAddresses, common.BytesToAddress(oracle.OnchainPublicKey)) + } + + var transmitterAddresses []common.Address + for _, oracle := range oracles { + transmitterAddresses = append(transmitterAddresses, common.HexToAddress(string(oracle.TransmitAccount))) + } + + // get the config digest from the ccip config contract and set config on the offramp. + var offrampOCR3Configs []evm_2_evm_multi_offramp.MultiOCR3BaseOCRConfigArgs + for _, pluginType := range []cctypes.PluginType{cctypes.PluginTypeCCIPCommit, cctypes.PluginTypeCCIPExec} { + ocrConfig, err1 := h.ccipConfig.GetOCRConfig(&bind.CallOpts{ + Context: testutils.Context(t), + }, donID, uint8(pluginType)) + require.NoError(t, err1, "failed to get OCR3 config from ccip config contract") + require.Len(t, ocrConfig, 1, "expected exactly one OCR3 config") + offrampOCR3Configs = append(offrampOCR3Configs, evm_2_evm_multi_offramp.MultiOCR3BaseOCRConfigArgs{ + ConfigDigest: ocrConfig[0].ConfigDigest, + OcrPluginType: uint8(pluginType), + F: f, + IsSignatureVerificationEnabled: pluginType == cctypes.PluginTypeCCIPCommit, + Signers: signerAddresses, + Transmitters: transmitterAddresses, + }) + } + + uni.backend.Commit() + + _, err = uni.offramp.SetOCR3Configs(uni.owner, offrampOCR3Configs) + require.NoError(t, err, "failed to set ocr3 configs on offramp") + uni.backend.Commit() + + for _, pluginType := range []cctypes.PluginType{cctypes.PluginTypeCCIPCommit, cctypes.PluginTypeCCIPExec} { + ocrConfig, err := uni.offramp.LatestConfigDetails(&bind.CallOpts{ + Context: testutils.Context(t), + }, uint8(pluginType)) + require.NoError(t, err, "failed to get latest commit OCR3 config") + require.Equalf(t, offrampOCR3Configs[pluginType].ConfigDigest, ocrConfig.ConfigInfo.ConfigDigest, "%s OCR3 config digest mismatch", pluginType.String()) + require.Equalf(t, offrampOCR3Configs[pluginType].F, ocrConfig.ConfigInfo.F, "%s OCR3 config F mismatch", pluginType.String()) + require.Equalf(t, offrampOCR3Configs[pluginType].IsSignatureVerificationEnabled, ocrConfig.ConfigInfo.IsSignatureVerificationEnabled, "%s OCR3 config signature verification mismatch", pluginType.String()) + if pluginType == cctypes.PluginTypeCCIPCommit { + // only commit will set signers, exec doesn't need them. + require.Equalf(t, offrampOCR3Configs[pluginType].Signers, ocrConfig.Signers, "%s OCR3 config signers mismatch", pluginType.String()) + } + require.Equalf(t, offrampOCR3Configs[pluginType].Transmitters, ocrConfig.Transmitters, "%s OCR3 config transmitters mismatch", pluginType.String()) + } + + t.Logf("set ocr3 config on the offramp, signers: %+v, transmitters: %+v", signerAddresses, transmitterAddresses) +} + +func connectUniverses( + t *testing.T, + universes map[uint64]onchainUniverse, +) { + for _, uni := range universes { + wireRouter(t, uni, universes) + wirePriceRegistry(t, uni, universes) + wireOffRamp(t, uni, universes) + initRemoteChainsGasPrices(t, uni, universes) + } +} + +// setupUniverseBasics sets up the initial configurations for the CCIP contracts on a single chain. +// 1. Mint 1000 LINK to the owner +// 2. Set the price registry with local token prices +// 3. Authorize the onRamp and offRamp on the nonce manager +func setupUniverseBasics(t *testing.T, uni onchainUniverse) { + // ============================================================================= + // Universe specific updates/configs + // These updates are specific to each universe and are set up here + // These updates don't depend on other chains + // ============================================================================= + owner := uni.owner + // ============================================================================= + // Mint 1000 LINK to owner + // ============================================================================= + _, err := uni.linkToken.GrantMintRole(owner, owner.From) + require.NoError(t, err) + _, err = uni.linkToken.Mint(owner, owner.From, e18Mult(1000)) + require.NoError(t, err) + uni.backend.Commit() + + // ============================================================================= + // Price updates for tokens + // These are the prices of the fee tokens of local chain in USD + // ============================================================================= + tokenPriceUpdates := []price_registry.InternalTokenPriceUpdate{ + { + SourceToken: uni.linkToken.Address(), + UsdPerToken: e18Mult(20), + }, + { + SourceToken: uni.weth.Address(), + UsdPerToken: e18Mult(4000), + }, + } + _, err = uni.priceRegistry.UpdatePrices(owner, price_registry.InternalPriceUpdates{ + TokenPriceUpdates: tokenPriceUpdates, + }) + require.NoErrorf(t, err, "failed to update prices in price registry on chain id %d", uni.chainID) + uni.backend.Commit() + + _, err = uni.priceRegistry.ApplyAuthorizedCallerUpdates(owner, price_registry.AuthorizedCallersAuthorizedCallerArgs{ + AddedCallers: []common.Address{ + uni.offramp.Address(), + }, + }) + require.NoError(t, err, "failed to authorize offramp on price registry") + uni.backend.Commit() + + // ============================================================================= + // Authorize OnRamp & OffRamp on NonceManager + // Otherwise the onramp will not be able to call the nonceManager to get next Nonce + // ============================================================================= + authorizedCallersAuthorizedCallerArgs := nonce_manager.AuthorizedCallersAuthorizedCallerArgs{ + AddedCallers: []common.Address{ + uni.onramp.Address(), + uni.offramp.Address(), + }, + } + _, err = uni.nonceManager.ApplyAuthorizedCallerUpdates(owner, authorizedCallersAuthorizedCallerArgs) + require.NoError(t, err) + uni.backend.Commit() +} + +// As we can't change router contract. The contract was expecting onRamp and offRamp per lane and not per chain +// In the new architecture we have only one onRamp and one offRamp per chain. +// hence we add the mapping for all remote chains to the onRamp/offRamp contract of the local chain +func wireRouter(t *testing.T, uni onchainUniverse, universes map[uint64]onchainUniverse) { + owner := uni.owner + var ( + routerOnrampUpdates []router.RouterOnRamp + routerOfframpUpdates []router.RouterOffRamp + ) + for remoteChainID := range universes { + if remoteChainID == uni.chainID { + continue + } + routerOnrampUpdates = append(routerOnrampUpdates, router.RouterOnRamp{ + DestChainSelector: getSelector(remoteChainID), + OnRamp: uni.onramp.Address(), + }) + routerOfframpUpdates = append(routerOfframpUpdates, router.RouterOffRamp{ + SourceChainSelector: getSelector(remoteChainID), + OffRamp: uni.offramp.Address(), + }) + } + _, err := uni.router.ApplyRampUpdates(owner, routerOnrampUpdates, []router.RouterOffRamp{}, routerOfframpUpdates) + require.NoErrorf(t, err, "failed to apply ramp updates on router on chain id %d", uni.chainID) + uni.backend.Commit() +} + +// Setting OnRampDestChainConfigs +func wirePriceRegistry(t *testing.T, uni onchainUniverse, universes map[uint64]onchainUniverse) { + owner := uni.owner + var priceRegistryDestChainConfigArgs []price_registry.PriceRegistryDestChainConfigArgs + for remoteChainID := range universes { + if remoteChainID == uni.chainID { + continue + } + priceRegistryDestChainConfigArgs = append(priceRegistryDestChainConfigArgs, price_registry.PriceRegistryDestChainConfigArgs{ + DestChainSelector: getSelector(remoteChainID), + DestChainConfig: defaultPriceRegistryDestChainConfig(t), + }) + } + _, err := uni.priceRegistry.ApplyDestChainConfigUpdates(owner, priceRegistryDestChainConfigArgs) + require.NoErrorf(t, err, "failed to apply dest chain config updates on price registry on chain id %d", uni.chainID) + uni.backend.Commit() +} + +// Setting OffRampSourceChainConfigs +func wireOffRamp(t *testing.T, uni onchainUniverse, universes map[uint64]onchainUniverse) { + owner := uni.owner + var offrampSourceChainConfigArgs []evm_2_evm_multi_offramp.EVM2EVMMultiOffRampSourceChainConfigArgs + for remoteChainID, remoteUniverse := range universes { + if remoteChainID == uni.chainID { + continue + } + offrampSourceChainConfigArgs = append(offrampSourceChainConfigArgs, evm_2_evm_multi_offramp.EVM2EVMMultiOffRampSourceChainConfigArgs{ + SourceChainSelector: getSelector(remoteChainID), // for each destination chain, add a source chain config + IsEnabled: true, + OnRamp: remoteUniverse.onramp.Address().Bytes(), + }) + } + _, err := uni.offramp.ApplySourceChainConfigUpdates(owner, offrampSourceChainConfigArgs) + require.NoErrorf(t, err, "failed to apply source chain config updates on offramp on chain id %d", uni.chainID) + uni.backend.Commit() + for remoteChainID, remoteUniverse := range universes { + if remoteChainID == uni.chainID { + continue + } + sourceCfg, err2 := uni.offramp.GetSourceChainConfig(&bind.CallOpts{}, getSelector(remoteChainID)) + require.NoError(t, err2) + require.True(t, sourceCfg.IsEnabled, "source chain config should be enabled") + require.Equal(t, remoteUniverse.onramp.Address(), common.BytesToAddress(sourceCfg.OnRamp), "source chain config onRamp address mismatch") + } +} + +func getSelector(chainID uint64) uint64 { + selector, err := chainsel.SelectorFromChainId(chainID) + if err != nil { + panic(err) + } + return selector +} + +// initRemoteChainsGasPrices sets the gas prices for all chains except the local chain in the local price registry +func initRemoteChainsGasPrices(t *testing.T, uni onchainUniverse, universes map[uint64]onchainUniverse) { + var gasPriceUpdates []price_registry.InternalGasPriceUpdate + for remoteChainID := range universes { + if remoteChainID == uni.chainID { + continue + } + gasPriceUpdates = append(gasPriceUpdates, + price_registry.InternalGasPriceUpdate{ + DestChainSelector: getSelector(remoteChainID), + UsdPerUnitGas: big.NewInt(2e12), + }, + ) + } + _, err := uni.priceRegistry.UpdatePrices(uni.owner, price_registry.InternalPriceUpdates{ + GasPriceUpdates: gasPriceUpdates, + }) + require.NoError(t, err) +} + +func defaultPriceRegistryDestChainConfig(t *testing.T) price_registry.PriceRegistryDestChainConfig { + // https://github.com/smartcontractkit/ccip/blob/c4856b64bd766f1ddbaf5d13b42d3c4b12efde3a/contracts/src/v0.8/ccip/libraries/Internal.sol#L337-L337 + /* + ```Solidity + // bytes4(keccak256("CCIP ChainFamilySelector EVM")) + bytes4 public constant CHAIN_FAMILY_SELECTOR_EVM = 0x2812d52c; + ``` + */ + evmFamilySelector, err := hex.DecodeString("2812d52c") + require.NoError(t, err) + return price_registry.PriceRegistryDestChainConfig{ + IsEnabled: true, + MaxNumberOfTokensPerMsg: 10, + MaxDataBytes: 256, + MaxPerMsgGasLimit: 3_000_000, + DestGasOverhead: 50_000, + DefaultTokenFeeUSDCents: 1, + DestGasPerPayloadByte: 10, + DestDataAvailabilityOverheadGas: 0, + DestGasPerDataAvailabilityByte: 100, + DestDataAvailabilityMultiplierBps: 1, + DefaultTokenDestGasOverhead: 125_000, + DefaultTokenDestBytesOverhead: 32, + DefaultTxGasLimit: 200_000, + GasMultiplierWeiPerEth: 1, + NetworkFeeUSDCents: 1, + ChainFamilySelector: [4]byte(evmFamilySelector), + } +} + +func deployLinkToken(t *testing.T, owner *bind.TransactOpts, backend *backends.SimulatedBackend, chainID uint64) *link_token.LinkToken { + linkAddr, _, _, err := link_token.DeployLinkToken(owner, backend) + require.NoErrorf(t, err, "failed to deploy link token on chain id %d", chainID) + backend.Commit() + linkToken, err := link_token.NewLinkToken(linkAddr, backend) + require.NoError(t, err) + return linkToken +} + +func deployMockARMContract(t *testing.T, owner *bind.TransactOpts, backend *backends.SimulatedBackend, chainID uint64) *mock_arm_contract.MockARMContract { + rmnAddr, _, _, err := mock_arm_contract.DeployMockARMContract(owner, backend) + require.NoErrorf(t, err, "failed to deploy mock arm on chain id %d", chainID) + backend.Commit() + rmn, err := mock_arm_contract.NewMockARMContract(rmnAddr, backend) + require.NoError(t, err) + return rmn +} + +func deployARMProxyContract(t *testing.T, owner *bind.TransactOpts, backend *backends.SimulatedBackend, rmnAddr common.Address, chainID uint64) *arm_proxy_contract.ARMProxyContract { + rmnProxyAddr, _, _, err := arm_proxy_contract.DeployARMProxyContract(owner, backend, rmnAddr) + require.NoErrorf(t, err, "failed to deploy arm proxy on chain id %d", chainID) + backend.Commit() + rmnProxy, err := arm_proxy_contract.NewARMProxyContract(rmnProxyAddr, backend) + require.NoError(t, err) + return rmnProxy +} + +func deployWETHContract(t *testing.T, owner *bind.TransactOpts, backend *backends.SimulatedBackend, chainID uint64) *weth9.WETH9 { + wethAddr, _, _, err := weth9.DeployWETH9(owner, backend) + require.NoErrorf(t, err, "failed to deploy weth contract on chain id %d", chainID) + backend.Commit() + weth, err := weth9.NewWETH9(wethAddr, backend) + require.NoError(t, err) + return weth +} + +func deployRouter(t *testing.T, owner *bind.TransactOpts, backend *backends.SimulatedBackend, wethAddr, rmnProxyAddr common.Address, chainID uint64) *router.Router { + routerAddr, _, _, err := router.DeployRouter(owner, backend, wethAddr, rmnProxyAddr) + require.NoErrorf(t, err, "failed to deploy router on chain id %d", chainID) + backend.Commit() + rout, err := router.NewRouter(routerAddr, backend) + require.NoError(t, err) + return rout +} + +func deployPriceRegistry( + t *testing.T, + owner *bind.TransactOpts, + backend *backends.SimulatedBackend, + linkAddr, + wethAddr common.Address, + maxFeeJuelsPerMsg *big.Int, + chainID uint64, +) *price_registry.PriceRegistry { + priceRegistryAddr, _, _, err := price_registry.DeployPriceRegistry( + owner, + backend, + price_registry.PriceRegistryStaticConfig{ + MaxFeeJuelsPerMsg: maxFeeJuelsPerMsg, + LinkToken: linkAddr, + StalenessThreshold: 24 * 60 * 60, // 24 hours + }, + []common.Address{ + owner.From, // owner can update prices in this test + }, // price updaters, will be set to offramp later + []common.Address{linkAddr, wethAddr}, // fee tokens + // empty for now, need to fill in when testing token transfers + []price_registry.PriceRegistryTokenPriceFeedUpdate{}, + // empty for now, need to fill in when testing token transfers + []price_registry.PriceRegistryTokenTransferFeeConfigArgs{}, + []price_registry.PriceRegistryPremiumMultiplierWeiPerEthArgs{ + { + PremiumMultiplierWeiPerEth: 9e17, // 0.9 ETH + Token: linkAddr, + }, + { + PremiumMultiplierWeiPerEth: 1e18, + Token: wethAddr, + }, + }, + // Destination chain configs will be set up later once we have all chains + []price_registry.PriceRegistryDestChainConfigArgs{}, + ) + require.NoErrorf(t, err, "failed to deploy price registry on chain id %d", chainID) + backend.Commit() + priceRegistry, err := price_registry.NewPriceRegistry(priceRegistryAddr, backend) + require.NoError(t, err) + return priceRegistry +} + +func deployTokenAdminRegistry(t *testing.T, owner *bind.TransactOpts, backend *backends.SimulatedBackend, chainID uint64) *token_admin_registry.TokenAdminRegistry { + tarAddr, _, _, err := token_admin_registry.DeployTokenAdminRegistry(owner, backend) + require.NoErrorf(t, err, "failed to deploy token admin registry on chain id %d", chainID) + backend.Commit() + tokenAdminRegistry, err := token_admin_registry.NewTokenAdminRegistry(tarAddr, backend) + require.NoError(t, err) + return tokenAdminRegistry +} + +func deployNonceManager(t *testing.T, owner *bind.TransactOpts, backend *backends.SimulatedBackend, chainID uint64) *nonce_manager.NonceManager { + nonceManagerAddr, _, _, err := nonce_manager.DeployNonceManager(owner, backend, []common.Address{owner.From}) + require.NoErrorf(t, err, "failed to deploy nonce_manager on chain id %d", chainID) + backend.Commit() + nonceManager, err := nonce_manager.NewNonceManager(nonceManagerAddr, backend) + require.NoError(t, err) + return nonceManager +} diff --git a/core/capabilities/ccip/ccip_integration_tests/home_chain_test.go b/core/capabilities/ccip/ccip_integration_tests/home_chain_test.go new file mode 100644 index 00000000000..c78fd37b809 --- /dev/null +++ b/core/capabilities/ccip/ccip_integration_tests/home_chain_test.go @@ -0,0 +1,103 @@ +package ccip_integration_tests + +import ( + "testing" + "time" + + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/ccip_integration_tests/integrationhelpers" + + mapset "github.com/deckarep/golang-set/v2" + "github.com/onsi/gomega" + + libocrtypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "github.com/smartcontractkit/chainlink-ccip/chainconfig" + ccipreader "github.com/smartcontractkit/chainlink-ccip/pkg/reader" + + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" + + "github.com/stretchr/testify/require" + + capcfg "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/ccip_config" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/logger" +) + +func TestHomeChainReader(t *testing.T) { + ctx := testutils.Context(t) + lggr := logger.TestLogger(t) + uni := integrationhelpers.NewTestUniverse(ctx, t, lggr) + // We need 3*f + 1 p2pIDs to have enough nodes to bootstrap + var arr []int64 + n := int(integrationhelpers.FChainA*3 + 1) + for i := 0; i <= n; i++ { + arr = append(arr, int64(i)) + } + p2pIDs := integrationhelpers.P2pIDsFromInts(arr) + uni.AddCapability(p2pIDs) + //==============================Apply configs to Capability Contract================================= + encodedChainConfig, err := chainconfig.EncodeChainConfig(chainconfig.ChainConfig{ + GasPriceDeviationPPB: cciptypes.NewBigIntFromInt64(1000), + DAGasPriceDeviationPPB: cciptypes.NewBigIntFromInt64(1_000_000), + FinalityDepth: -1, + OptimisticConfirmations: 1, + }) + require.NoError(t, err) + chainAConf := integrationhelpers.SetupConfigInfo(integrationhelpers.ChainA, p2pIDs, integrationhelpers.FChainA, encodedChainConfig) + chainBConf := integrationhelpers.SetupConfigInfo(integrationhelpers.ChainB, p2pIDs[1:], integrationhelpers.FChainB, encodedChainConfig) + chainCConf := integrationhelpers.SetupConfigInfo(integrationhelpers.ChainC, p2pIDs[2:], integrationhelpers.FChainC, encodedChainConfig) + inputConfig := []capcfg.CCIPConfigTypesChainConfigInfo{ + chainAConf, + chainBConf, + chainCConf, + } + _, err = uni.CcipCfg.ApplyChainConfigUpdates(uni.Transactor, nil, inputConfig) + require.NoError(t, err) + uni.Backend.Commit() + //================================Setup HomeChainReader=============================== + + pollDuration := time.Second + homeChain := uni.HomeChainReader + + gomega.NewWithT(t).Eventually(func() bool { + configs, _ := homeChain.GetAllChainConfigs() + return configs != nil + }, testutils.WaitTimeout(t), pollDuration*5).Should(gomega.BeTrue()) + + t.Logf("homchain reader is ready") + //================================Test HomeChain Reader=============================== + expectedChainConfigs := map[cciptypes.ChainSelector]ccipreader.ChainConfig{} + for _, c := range inputConfig { + expectedChainConfigs[cciptypes.ChainSelector(c.ChainSelector)] = ccipreader.ChainConfig{ + FChain: int(c.ChainConfig.FChain), + SupportedNodes: toPeerIDs(c.ChainConfig.Readers), + Config: mustDecodeChainConfig(t, c.ChainConfig.Config), + } + } + configs, err := homeChain.GetAllChainConfigs() + require.NoError(t, err) + require.Equal(t, expectedChainConfigs, configs) + //=================================Remove ChainC from OnChainConfig========================================= + _, err = uni.CcipCfg.ApplyChainConfigUpdates(uni.Transactor, []uint64{integrationhelpers.ChainC}, nil) + require.NoError(t, err) + uni.Backend.Commit() + time.Sleep(pollDuration * 5) // Wait for the chain reader to update + configs, err = homeChain.GetAllChainConfigs() + require.NoError(t, err) + delete(expectedChainConfigs, cciptypes.ChainSelector(integrationhelpers.ChainC)) + require.Equal(t, expectedChainConfigs, configs) +} + +func toPeerIDs(readers [][32]byte) mapset.Set[libocrtypes.PeerID] { + peerIDs := mapset.NewSet[libocrtypes.PeerID]() + for _, r := range readers { + peerIDs.Add(r) + } + return peerIDs +} + +func mustDecodeChainConfig(t *testing.T, encodedChainConfig []byte) chainconfig.ChainConfig { + chainConfig, err := chainconfig.DecodeChainConfig(encodedChainConfig) + require.NoError(t, err) + return chainConfig +} diff --git a/core/capabilities/ccip/ccip_integration_tests/integrationhelpers/integration_helpers.go b/core/capabilities/ccip/ccip_integration_tests/integrationhelpers/integration_helpers.go new file mode 100644 index 00000000000..7520b126336 --- /dev/null +++ b/core/capabilities/ccip/ccip_integration_tests/integrationhelpers/integration_helpers.go @@ -0,0 +1,304 @@ +package integrationhelpers + +import ( + "context" + "encoding/json" + "fmt" + "math/big" + "sort" + "testing" + "time" + + configsevm "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/configs/evm" + cctypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + + "github.com/smartcontractkit/chainlink-ccip/pkg/consts" + ccipreader "github.com/smartcontractkit/chainlink-ccip/pkg/reader" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/headtracker" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/ccip_config" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/ocr3_config_encoder" + kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" + + "github.com/smartcontractkit/chainlink-common/pkg/types" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm" + evmrelaytypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" +) + +const chainID = 1337 + +func NewReader( + t *testing.T, + logPoller logpoller.LogPoller, + headTracker logpoller.HeadTracker, + client client.Client, + address common.Address, + chainReaderConfig evmrelaytypes.ChainReaderConfig, +) types.ContractReader { + cr, err := evm.NewChainReaderService(testutils.Context(t), logger.TestLogger(t), logPoller, headTracker, client, chainReaderConfig) + require.NoError(t, err) + err = cr.Bind(testutils.Context(t), []types.BoundContract{ + { + Address: address.String(), + Name: consts.ContractNameCCIPConfig, + }, + }) + require.NoError(t, err) + require.NoError(t, cr.Start(testutils.Context(t))) + for { + if err := cr.Ready(); err == nil { + break + } + } + + return cr +} + +const ( + ChainA uint64 = 1 + FChainA uint8 = 1 + + ChainB uint64 = 2 + FChainB uint8 = 2 + + ChainC uint64 = 3 + FChainC uint8 = 3 + + CcipCapabilityLabelledName = "ccip" + CcipCapabilityVersion = "v1.0" +) + +var CapabilityID = fmt.Sprintf("%s@%s", CcipCapabilityLabelledName, CcipCapabilityVersion) + +type TestUniverse struct { + Transactor *bind.TransactOpts + Backend *backends.SimulatedBackend + CapReg *kcr.CapabilitiesRegistry + CcipCfg *ccip_config.CCIPConfig + TestingT *testing.T + LogPoller logpoller.LogPoller + HeadTracker logpoller.HeadTracker + SimClient client.Client + HomeChainReader ccipreader.HomeChain +} + +func NewTestUniverse(ctx context.Context, t *testing.T, lggr logger.Logger) TestUniverse { + transactor := testutils.MustNewSimTransactor(t) + backend := backends.NewSimulatedBackend(core.GenesisAlloc{ + transactor.From: {Balance: assets.Ether(1000).ToInt()}, + }, 30e6) + + crAddress, _, _, err := kcr.DeployCapabilitiesRegistry(transactor, backend) + require.NoError(t, err) + backend.Commit() + + capReg, err := kcr.NewCapabilitiesRegistry(crAddress, backend) + require.NoError(t, err) + + ccAddress, _, _, err := ccip_config.DeployCCIPConfig(transactor, backend, crAddress) + require.NoError(t, err) + backend.Commit() + + cc, err := ccip_config.NewCCIPConfig(ccAddress, backend) + require.NoError(t, err) + + db := pgtest.NewSqlxDB(t) + lpOpts := logpoller.Opts{ + PollPeriod: time.Millisecond, + FinalityDepth: 0, + BackfillBatchSize: 10, + RpcBatchSize: 10, + KeepFinalizedBlocksDepth: 100000, + } + cl := client.NewSimulatedBackendClient(t, backend, big.NewInt(chainID)) + headTracker := headtracker.NewSimulatedHeadTracker(cl, lpOpts.UseFinalityTag, lpOpts.FinalityDepth) + if lpOpts.PollPeriod == 0 { + lpOpts.PollPeriod = 1 * time.Hour + } + lp := logpoller.NewLogPoller(logpoller.NewORM(big.NewInt(chainID), db, lggr), cl, logger.NullLogger, headTracker, lpOpts) + require.NoError(t, lp.Start(ctx)) + t.Cleanup(func() { require.NoError(t, lp.Close()) }) + + hcr := NewHomeChainReader(t, lp, headTracker, cl, ccAddress) + return TestUniverse{ + Transactor: transactor, + Backend: backend, + CapReg: capReg, + CcipCfg: cc, + TestingT: t, + SimClient: cl, + LogPoller: lp, + HeadTracker: headTracker, + HomeChainReader: hcr, + } +} + +func (t TestUniverse) NewContractReader(ctx context.Context, cfg []byte) (types.ContractReader, error) { + var config evmrelaytypes.ChainReaderConfig + err := json.Unmarshal(cfg, &config) + require.NoError(t.TestingT, err) + return evm.NewChainReaderService(ctx, logger.TestLogger(t.TestingT), t.LogPoller, t.HeadTracker, t.SimClient, config) +} + +func P2pIDsFromInts(ints []int64) [][32]byte { + var p2pIDs [][32]byte + for _, i := range ints { + p2pID := p2pkey.MustNewV2XXXTestingOnly(big.NewInt(i)).PeerID() + p2pIDs = append(p2pIDs, p2pID) + } + sort.Slice(p2pIDs, func(i, j int) bool { + for k := 0; k < 32; k++ { + if p2pIDs[i][k] < p2pIDs[j][k] { + return true + } else if p2pIDs[i][k] > p2pIDs[j][k] { + return false + } + } + return false + }) + return p2pIDs +} + +func (t *TestUniverse) AddCapability(p2pIDs [][32]byte) { + _, err := t.CapReg.AddCapabilities(t.Transactor, []kcr.CapabilitiesRegistryCapability{ + { + LabelledName: CcipCapabilityLabelledName, + Version: CcipCapabilityVersion, + CapabilityType: 0, + ResponseType: 0, + ConfigurationContract: t.CcipCfg.Address(), + }, + }) + require.NoError(t.TestingT, err, "failed to add capability to registry") + t.Backend.Commit() + + ccipCapabilityID, err := t.CapReg.GetHashedCapabilityId(nil, CcipCapabilityLabelledName, CcipCapabilityVersion) + require.NoError(t.TestingT, err) + + for i := 0; i < len(p2pIDs); i++ { + _, err = t.CapReg.AddNodeOperators(t.Transactor, []kcr.CapabilitiesRegistryNodeOperator{ + { + Admin: t.Transactor.From, + Name: fmt.Sprintf("nop-%d", i), + }, + }) + require.NoError(t.TestingT, err) + t.Backend.Commit() + + // get the node operator id from the event + it, err := t.CapReg.FilterNodeOperatorAdded(nil, nil, nil) + require.NoError(t.TestingT, err) + var nodeOperatorID uint32 + for it.Next() { + if it.Event.Name == fmt.Sprintf("nop-%d", i) { + nodeOperatorID = it.Event.NodeOperatorId + break + } + } + require.NotZero(t.TestingT, nodeOperatorID) + + _, err = t.CapReg.AddNodes(t.Transactor, []kcr.CapabilitiesRegistryNodeParams{ + { + NodeOperatorId: nodeOperatorID, + Signer: testutils.Random32Byte(), + P2pId: p2pIDs[i], + HashedCapabilityIds: [][32]byte{ccipCapabilityID}, + }, + }) + require.NoError(t.TestingT, err) + t.Backend.Commit() + + // verify that the node was added successfully + nodeInfo, err := t.CapReg.GetNode(nil, p2pIDs[i]) + require.NoError(t.TestingT, err) + + require.Equal(t.TestingT, nodeOperatorID, nodeInfo.NodeOperatorId) + require.Equal(t.TestingT, p2pIDs[i][:], nodeInfo.P2pId[:]) + } +} + +func NewHomeChainReader(t *testing.T, logPoller logpoller.LogPoller, headTracker logpoller.HeadTracker, client client.Client, ccAddress common.Address) ccipreader.HomeChain { + cr := NewReader(t, logPoller, headTracker, client, ccAddress, configsevm.HomeChainReaderConfigRaw()) + + hcr := ccipreader.NewHomeChainReader(cr, logger.TestLogger(t), 500*time.Millisecond) + require.NoError(t, hcr.Start(testutils.Context(t))) + t.Cleanup(func() { require.NoError(t, hcr.Close()) }) + + return hcr +} + +func (t *TestUniverse) AddDONToRegistry( + ccipCapabilityID [32]byte, + chainSelector uint64, + f uint8, + bootstrapP2PID [32]byte, + p2pIDs [][32]byte, +) { + tabi, err := ocr3_config_encoder.IOCR3ConfigEncoderMetaData.GetAbi() + require.NoError(t.TestingT, err) + + var ( + signers [][]byte + transmitters [][]byte + ) + for range p2pIDs { + signers = append(signers, testutils.NewAddress().Bytes()) + transmitters = append(transmitters, testutils.NewAddress().Bytes()) + } + + var ocr3Configs []ocr3_config_encoder.CCIPConfigTypesOCR3Config + for _, pluginType := range []cctypes.PluginType{cctypes.PluginTypeCCIPCommit, cctypes.PluginTypeCCIPExec} { + ocr3Configs = append(ocr3Configs, ocr3_config_encoder.CCIPConfigTypesOCR3Config{ + PluginType: uint8(pluginType), + ChainSelector: chainSelector, + F: f, + OffchainConfigVersion: 30, + OfframpAddress: testutils.NewAddress().Bytes(), + BootstrapP2PIds: [][32]byte{bootstrapP2PID}, + P2pIds: p2pIDs, + Signers: signers, + Transmitters: transmitters, + OffchainConfig: []byte("offchain config"), + }) + } + + encodedCall, err := tabi.Pack("exposeOCR3Config", ocr3Configs) + require.NoError(t.TestingT, err) + + // Trim first four bytes to remove function selector. + encodedConfigs := encodedCall[4:] + + _, err = t.CapReg.AddDON(t.Transactor, p2pIDs, []kcr.CapabilitiesRegistryCapabilityConfiguration{ + { + CapabilityId: ccipCapabilityID, + Config: encodedConfigs, + }, + }, false, false, f) + require.NoError(t.TestingT, err) + t.Backend.Commit() +} + +func SetupConfigInfo(chainSelector uint64, readers [][32]byte, fChain uint8, cfg []byte) ccip_config.CCIPConfigTypesChainConfigInfo { + return ccip_config.CCIPConfigTypesChainConfigInfo{ + ChainSelector: chainSelector, + ChainConfig: ccip_config.CCIPConfigTypesChainConfig{ + Readers: readers, + FChain: fChain, + Config: cfg, + }, + } +} diff --git a/core/capabilities/ccip/ccip_integration_tests/ocr3_node_test.go b/core/capabilities/ccip/ccip_integration_tests/ocr3_node_test.go new file mode 100644 index 00000000000..8cafb901724 --- /dev/null +++ b/core/capabilities/ccip/ccip_integration_tests/ocr3_node_test.go @@ -0,0 +1,281 @@ +package ccip_integration_tests + +import ( + "fmt" + "math/big" + "sync" + "testing" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/hashicorp/consul/sdk/freeport" + "go.uber.org/zap/zapcore" + + "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_multi_offramp" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/services/chainlink" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" + + confighelper2 "github.com/smartcontractkit/libocr/offchainreporting2plus/confighelper" + ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + + "github.com/stretchr/testify/require" +) + +const STATE_SUCCESS = uint8(2) + +/* +* If you want to debug, set log level to info and use the following commands for easier logs filtering. +* +* // Run the test and redirect logs to logs.txt +* go test -v -run "^TestIntegration_OCR3Nodes" ./core/capabilities/ccip/ccip_integration_tests 2>&1 > logs.txt +* +* // Reads logs.txt as a stream and apply filters using grep +* tail -fn0 logs.txt | grep "CCIPExecPlugin" + */ +func TestIntegration_OCR3Nodes(t *testing.T) { + const ( + numChains = 3 // number of chains that this test will run on + numNodes = 4 // number of OCR3 nodes, test assumes that every node supports every chain + + simulatedBackendBlockTime = 900 * time.Millisecond // Simulated backend blocks committing interval + oraclesBootWaitTime = 30 * time.Second // Time to wait for oracles to come up (HACK) + fChain = 1 // fChain value for all the chains + oracleLogLevel = zapcore.InfoLevel // Log level for the oracle / plugins. + ) + + t.Logf("creating %d universes", numChains) + homeChainUni, universes := createUniverses(t, numChains) + + var ( + oracles = make(map[uint64][]confighelper2.OracleIdentityExtra) + apps []chainlink.Application + nodes []*ocr3Node + p2pIDs [][32]byte + + // The bootstrap node will be: nodes[0] + bootstrapPort int + bootstrapP2PID p2pkey.PeerID + ) + + ports := freeport.GetN(t, numNodes) + ctx := testutils.Context(t) + callCtx := &bind.CallOpts{Context: ctx} + + for i := 0; i < numNodes; i++ { + t.Logf("Setting up ocr3 node:%d at port:%d", i, ports[i]) + node := setupNodeOCR3(t, ports[i], universes, homeChainUni, oracleLogLevel) + + for chainID, transmitter := range node.transmitters { + identity := confighelper2.OracleIdentityExtra{ + OracleIdentity: confighelper2.OracleIdentity{ + OnchainPublicKey: node.keybundle.PublicKey(), // Different for each chain + TransmitAccount: ocrtypes.Account(transmitter.Hex()), + OffchainPublicKey: node.keybundle.OffchainPublicKey(), // Same for each family + PeerID: node.peerID, + }, + ConfigEncryptionPublicKey: node.keybundle.ConfigEncryptionPublicKey(), // Different for each chain + } + oracles[chainID] = append(oracles[chainID], identity) + } + + apps = append(apps, node.app) + nodes = append(nodes, node) + + peerID, err := p2pkey.MakePeerID(node.peerID) + require.NoError(t, err) + p2pIDs = append(p2pIDs, peerID) + } + + bootstrapPort = ports[0] + bootstrapP2PID = p2pIDs[0] + bootstrapAddr := fmt.Sprintf("127.0.0.1:%d", bootstrapPort) + t.Logf("[bootstrap node] peerID:%s p2pID:%d address:%s", nodes[0].peerID, bootstrapP2PID, bootstrapAddr) + + // Start committing periodically in the background for all the chains + tick := time.NewTicker(simulatedBackendBlockTime) + defer tick.Stop() + commitBlocksBackground(t, universes, tick) + + ccipCapabilityID, err := homeChainUni.capabilityRegistry.GetHashedCapabilityId( + callCtx, CapabilityLabelledName, CapabilityVersion) + require.NoError(t, err, "failed to get hashed capability id for ccip") + require.NotEqual(t, [32]byte{}, ccipCapabilityID, "ccip capability id is empty") + + // Need to Add nodes and assign capabilities to them before creating DONS + homeChainUni.AddNodes(t, p2pIDs, [][32]byte{ccipCapabilityID}) + + for _, uni := range universes { + t.Logf("Adding chainconfig for chain %d", uni.chainID) + AddChainConfig(t, homeChainUni, getSelector(uni.chainID), p2pIDs, fChain) + } + + cfgs, err := homeChainUni.ccipConfig.GetAllChainConfigs(callCtx) + require.NoError(t, err) + require.Len(t, cfgs, numChains) + + // Create a DON for each chain + for _, uni := range universes { + // Add nodes and give them the capability + t.Log("Adding DON for universe: ", uni.chainID) + chainSelector := getSelector(uni.chainID) + homeChainUni.AddDON( + t, + ccipCapabilityID, + chainSelector, + uni, + fChain, + bootstrapP2PID, + p2pIDs, + oracles[uni.chainID], + ) + } + + t.Log("Creating ocr3 jobs, starting oracles") + for i := 0; i < len(nodes); i++ { + err1 := nodes[i].app.Start(ctx) + require.NoError(t, err1) + tApp := apps[i] + t.Cleanup(func() { require.NoError(t, tApp.Stop()) }) + + jb := mustGetJobSpec(t, bootstrapP2PID, bootstrapPort, nodes[i].peerID, nodes[i].keybundle.ID()) + require.NoErrorf(t, tApp.AddJobV2(ctx, &jb), "Wasn't able to create ccip job for node %d", i) + } + + t.Logf("Sending ccip requests from each chain to all other chains") + for _, uni := range universes { + requests := genRequestData(uni.chainID, universes) + uni.SendCCIPRequests(t, requests) + } + + // Wait for the oracles to come up. + // TODO: We need some data driven way to do this e.g. wait until LP filters to be registered. + time.Sleep(oraclesBootWaitTime) + + // Replay the log poller on all the chains so that the logs are in the db. + // otherwise the plugins won't pick them up. + for _, node := range nodes { + for chainID := range universes { + t.Logf("Replaying logs for chain %d from block %d", chainID, 1) + require.NoError(t, node.app.ReplayFromBlock(big.NewInt(int64(chainID)), 1, false), "failed to replay logs") + } + } + + // with only one request sent from each chain to each other chain, + // and with sequence numbers on incrementing by 1 on a per-dest chain + // basis, we expect the min sequence number to be 1 on all chains. + expectedSeqNrRange := ccipocr3.NewSeqNumRange(1, 1) + var wg sync.WaitGroup + for _, uni := range universes { + for remoteSelector := range universes { + if remoteSelector == uni.chainID { + continue + } + wg.Add(1) + go func(uni onchainUniverse, remoteSelector uint64) { + defer wg.Done() + waitForCommitWithInterval(t, uni, getSelector(remoteSelector), expectedSeqNrRange) + }(uni, remoteSelector) + } + } + + start := time.Now() + wg.Wait() + t.Logf("All chains received the expected commit report in %s", time.Since(start)) + + // with only one request sent from each chain to each other chain, + // all ExecutionStateChanged events should have the sequence number 1. + expectedSeqNr := uint64(1) + for _, uni := range universes { + for remoteSelector := range universes { + if remoteSelector == uni.chainID { + continue + } + wg.Add(1) + go func(uni onchainUniverse, remoteSelector uint64) { + defer wg.Done() + waitForExecWithSeqNr(t, uni, getSelector(remoteSelector), expectedSeqNr) + }(uni, remoteSelector) + } + } + + start = time.Now() + wg.Wait() + t.Logf("All chains received the expected ExecutionStateChanged event in %s", time.Since(start)) +} + +func genRequestData(chainID uint64, universes map[uint64]onchainUniverse) []requestData { + var res []requestData + for destChainID, destUni := range universes { + if destChainID == chainID { + continue + } + res = append(res, requestData{ + destChainSelector: getSelector(destChainID), + receiverAddress: destUni.receiver.Address(), + data: []byte(fmt.Sprintf("msg from chain %d to chain %d", chainID, destChainID)), + }) + } + return res +} + +func waitForCommitWithInterval( + t *testing.T, + uni onchainUniverse, + expectedSourceChainSelector uint64, + expectedSeqNumRange ccipocr3.SeqNumRange, +) { + sink := make(chan *evm_2_evm_multi_offramp.EVM2EVMMultiOffRampCommitReportAccepted) + subscription, err := uni.offramp.WatchCommitReportAccepted(&bind.WatchOpts{ + Context: testutils.Context(t), + }, sink) + require.NoError(t, err) + + for { + select { + case <-time.After(10 * time.Second): + t.Logf("Waiting for commit report on chain id %d (selector %d) from source selector %d expected seq nr range %s", + uni.chainID, getSelector(uni.chainID), expectedSourceChainSelector, expectedSeqNumRange.String()) + case subErr := <-subscription.Err(): + t.Fatalf("Subscription error: %+v", subErr) + case report := <-sink: + if len(report.Report.MerkleRoots) > 0 { + // Check the interval of sequence numbers and make sure it matches + // the expected range. + for _, mr := range report.Report.MerkleRoots { + if mr.SourceChainSelector == expectedSourceChainSelector && + uint64(expectedSeqNumRange.Start()) == mr.Interval.Min && + uint64(expectedSeqNumRange.End()) == mr.Interval.Max { + t.Logf("Received commit report on chain id %d (selector %d) from source selector %d expected seq nr range %s", + uni.chainID, getSelector(uni.chainID), expectedSourceChainSelector, expectedSeqNumRange.String()) + return + } + } + } + } + } +} + +func waitForExecWithSeqNr(t *testing.T, uni onchainUniverse, expectedSourceChainSelector, expectedSeqNr uint64) { + for { + scc, err := uni.offramp.GetSourceChainConfig(nil, expectedSourceChainSelector) + require.NoError(t, err) + t.Logf("Waiting for ExecutionStateChanged on chain %d (selector %d) from chain %d with expected sequence number %d, current onchain minSeqNr: %d", + uni.chainID, getSelector(uni.chainID), expectedSourceChainSelector, expectedSeqNr, scc.MinSeqNr) + iter, err := uni.offramp.FilterExecutionStateChanged(nil, []uint64{expectedSourceChainSelector}, []uint64{expectedSeqNr}, nil) + require.NoError(t, err) + var count int + for iter.Next() { + if iter.Event.SequenceNumber == expectedSeqNr && iter.Event.SourceChainSelector == expectedSourceChainSelector { + count++ + } + } + if count == 1 { + t.Logf("Received ExecutionStateChanged on chain %d (selector %d) from chain %d with expected sequence number %d", + uni.chainID, getSelector(uni.chainID), expectedSourceChainSelector, expectedSeqNr) + return + } + time.Sleep(5 * time.Second) + } +} diff --git a/core/capabilities/ccip/ccip_integration_tests/ocr_node_helper.go b/core/capabilities/ccip/ccip_integration_tests/ocr_node_helper.go new file mode 100644 index 00000000000..75b0e0ee947 --- /dev/null +++ b/core/capabilities/ccip/ccip_integration_tests/ocr_node_helper.go @@ -0,0 +1,316 @@ +package ccip_integration_tests + +import ( + "context" + "fmt" + "math/big" + "net/http" + "strconv" + "sync" + "testing" + "time" + + coretypes "github.com/smartcontractkit/chainlink-common/pkg/types/core/mocks" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/validate" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" + "github.com/ethereum/go-ethereum/common" + gethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/jmoiron/sqlx" + + "github.com/smartcontractkit/chainlink/v2/core/services/job" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" + + "github.com/smartcontractkit/chainlink-common/pkg/config" + "github.com/smartcontractkit/chainlink-common/pkg/loop" + "github.com/smartcontractkit/chainlink-common/pkg/utils/mailbox" + "github.com/smartcontractkit/chainlink/v2/core/services/relay" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + v2toml "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/toml" + evmutils "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils/big" + "github.com/smartcontractkit/chainlink/v2/core/chains/legacyevm" + configv2 "github.com/smartcontractkit/chainlink/v2/core/config/toml" + "github.com/smartcontractkit/chainlink/v2/core/internal/cltest/heavyweight" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/logger/audit" + "github.com/smartcontractkit/chainlink/v2/core/services/chainlink" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/chaintype" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ocr2key" + "github.com/smartcontractkit/chainlink/v2/core/utils" + "github.com/smartcontractkit/chainlink/v2/plugins" + + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" +) + +type ocr3Node struct { + app chainlink.Application + peerID string + transmitters map[uint64]common.Address + keybundle ocr2key.KeyBundle + db *sqlx.DB +} + +// setupNodeOCR3 creates a chainlink node and any associated keys in order to run +// ccip. +func setupNodeOCR3( + t *testing.T, + port int, + universes map[uint64]onchainUniverse, + homeChainUniverse homeChain, + logLevel zapcore.Level, +) *ocr3Node { + // Do not want to load fixtures as they contain a dummy chainID. + cfg, db := heavyweight.FullTestDBNoFixturesV2(t, func(c *chainlink.Config, s *chainlink.Secrets) { + c.Insecure.OCRDevelopmentMode = ptr(true) // Disables ocr spec validation so we can have fast polling for the test. + + c.Feature.LogPoller = ptr(true) + + // P2P V2 configs. + c.P2P.V2.Enabled = ptr(true) + c.P2P.V2.DeltaDial = config.MustNewDuration(500 * time.Millisecond) + c.P2P.V2.DeltaReconcile = config.MustNewDuration(5 * time.Second) + c.P2P.V2.ListenAddresses = &[]string{fmt.Sprintf("127.0.0.1:%d", port)} + + // Enable Capabilities, This is a pre-requisite for registrySyncer to work. + c.Capabilities.ExternalRegistry.NetworkID = ptr(relay.NetworkEVM) + c.Capabilities.ExternalRegistry.ChainID = ptr(strconv.FormatUint(homeChainUniverse.chainID, 10)) + c.Capabilities.ExternalRegistry.Address = ptr(homeChainUniverse.capabilityRegistry.Address().String()) + + // OCR configs + c.OCR.Enabled = ptr(false) + c.OCR.DefaultTransactionQueueDepth = ptr(uint32(200)) + c.OCR2.Enabled = ptr(true) + c.OCR2.ContractPollInterval = config.MustNewDuration(5 * time.Second) + + c.Log.Level = ptr(configv2.LogLevel(logLevel)) + + var chains v2toml.EVMConfigs + for chainID := range universes { + chains = append(chains, createConfigV2Chain(uBigInt(chainID))) + } + c.EVM = chains + }) + + lggr := logger.TestLogger(t) + lggr.SetLogLevel(logLevel) + ctx := testutils.Context(t) + clients := make(map[uint64]client.Client) + + for chainID, uni := range universes { + clients[chainID] = client.NewSimulatedBackendClient(t, uni.backend, uBigInt(chainID)) + } + + master := keystore.New(db, utils.FastScryptParams, lggr) + + kStore := KeystoreSim{ + eks: &EthKeystoreSim{ + Eth: master.Eth(), + t: t, + }, + csa: master.CSA(), + } + mailMon := mailbox.NewMonitor("ccip", lggr.Named("mailbox")) + evmOpts := chainlink.EVMFactoryConfig{ + ChainOpts: legacyevm.ChainOpts{ + AppConfig: cfg, + GenEthClient: func(i *big.Int) client.Client { + client, ok := clients[i.Uint64()] + if !ok { + t.Fatal("no backend for chainID", i) + } + return client + }, + MailMon: mailMon, + DS: db, + }, + CSAETHKeystore: kStore, + } + relayerFactory := chainlink.RelayerFactory{ + Logger: lggr, + LoopRegistry: plugins.NewLoopRegistry(lggr.Named("LoopRegistry"), cfg.Tracing()), + GRPCOpts: loop.GRPCOpts{}, + CapabilitiesRegistry: coretypes.NewCapabilitiesRegistry(t), + } + initOps := []chainlink.CoreRelayerChainInitFunc{chainlink.InitEVM(testutils.Context(t), relayerFactory, evmOpts)} + rci, err := chainlink.NewCoreRelayerChainInteroperators(initOps...) + require.NoError(t, err) + + app, err := chainlink.NewApplication(chainlink.ApplicationOpts{ + Config: cfg, + DS: db, + KeyStore: master, + RelayerChainInteroperators: rci, + Logger: lggr, + ExternalInitiatorManager: nil, + CloseLogger: lggr.Sync, + UnrestrictedHTTPClient: &http.Client{}, + RestrictedHTTPClient: &http.Client{}, + AuditLogger: audit.NoopLogger, + MailMon: mailMon, + LoopRegistry: plugins.NewLoopRegistry(lggr, cfg.Tracing()), + }) + require.NoError(t, err) + require.NoError(t, app.GetKeyStore().Unlock(ctx, "password")) + _, err = app.GetKeyStore().P2P().Create(ctx) + require.NoError(t, err) + + p2pIDs, err := app.GetKeyStore().P2P().GetAll() + require.NoError(t, err) + require.Len(t, p2pIDs, 1) + peerID := p2pIDs[0].PeerID() + // create a transmitter for each chain + transmitters := make(map[uint64]common.Address) + for chainID, uni := range universes { + backend := uni.backend + owner := uni.owner + cID := uBigInt(chainID) + addrs, err2 := app.GetKeyStore().Eth().EnabledAddressesForChain(testutils.Context(t), cID) + require.NoError(t, err2) + if len(addrs) == 1 { + // just fund the address + fundAddress(t, owner, addrs[0], assets.Ether(10).ToInt(), backend) + transmitters[chainID] = addrs[0] + } else { + // create key and fund it + _, err3 := app.GetKeyStore().Eth().Create(testutils.Context(t), cID) + require.NoError(t, err3, "failed to create key for chain", chainID) + sendingKeys, err3 := app.GetKeyStore().Eth().EnabledAddressesForChain(testutils.Context(t), cID) + require.NoError(t, err3) + require.Len(t, sendingKeys, 1) + fundAddress(t, owner, sendingKeys[0], assets.Ether(10).ToInt(), backend) + transmitters[chainID] = sendingKeys[0] + } + } + require.Len(t, transmitters, len(universes)) + + keybundle, err := app.GetKeyStore().OCR2().Create(ctx, chaintype.EVM) + require.NoError(t, err) + + t.Cleanup(func() { + require.NoError(t, db.Close()) + }) + + return &ocr3Node{ + // can't use this app because it doesn't have the right toml config + // missing bootstrapp + app: app, + peerID: peerID.Raw(), + transmitters: transmitters, + keybundle: keybundle, + db: db, + } +} + +func ptr[T any](v T) *T { return &v } + +var _ keystore.Eth = &EthKeystoreSim{} + +type EthKeystoreSim struct { + keystore.Eth + t *testing.T +} + +// override +func (e *EthKeystoreSim) SignTx(ctx context.Context, address common.Address, tx *gethtypes.Transaction, chainID *big.Int) (*gethtypes.Transaction, error) { + // always sign with chain id 1337 for the simulated backend + return e.Eth.SignTx(ctx, address, tx, big.NewInt(1337)) +} + +type KeystoreSim struct { + eks keystore.Eth + csa keystore.CSA +} + +func (e KeystoreSim) Eth() keystore.Eth { + return e.eks +} + +func (e KeystoreSim) CSA() keystore.CSA { + return e.csa +} + +func fundAddress(t *testing.T, from *bind.TransactOpts, to common.Address, amount *big.Int, backend *backends.SimulatedBackend) { + nonce, err := backend.PendingNonceAt(testutils.Context(t), from.From) + require.NoError(t, err) + gp, err := backend.SuggestGasPrice(testutils.Context(t)) + require.NoError(t, err) + rawTx := gethtypes.NewTx(&gethtypes.LegacyTx{ + Nonce: nonce, + GasPrice: gp, + Gas: 21000, + To: &to, + Value: amount, + }) + signedTx, err := from.Signer(from.From, rawTx) + require.NoError(t, err) + err = backend.SendTransaction(testutils.Context(t), signedTx) + require.NoError(t, err) + backend.Commit() +} + +func createConfigV2Chain(chainID *big.Int) *v2toml.EVMConfig { + chain := v2toml.Defaults((*evmutils.Big)(chainID)) + chain.GasEstimator.LimitDefault = ptr(uint64(5e6)) + chain.LogPollInterval = config.MustNewDuration(100 * time.Millisecond) + chain.Transactions.ForwardersEnabled = ptr(false) + chain.FinalityDepth = ptr(uint32(2)) + return &v2toml.EVMConfig{ + ChainID: (*evmutils.Big)(chainID), + Enabled: ptr(true), + Chain: chain, + Nodes: v2toml.EVMNodes{&v2toml.Node{}}, + } +} + +// Commit blocks periodically in the background for all chains +func commitBlocksBackground(t *testing.T, universes map[uint64]onchainUniverse, tick *time.Ticker) { + t.Log("starting ticker to commit blocks") + tickCtx, tickCancel := context.WithCancel(testutils.Context(t)) + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + for { + select { + case <-tick.C: + for _, uni := range universes { + uni.backend.Commit() + } + case <-tickCtx.Done(): + return + } + } + }() + t.Cleanup(func() { + tickCancel() + wg.Wait() + }) +} + +// p2pKeyID: nodes p2p id +// ocrKeyBundleID: nodes ocr key bundle id +func mustGetJobSpec(t *testing.T, bootstrapP2PID p2pkey.PeerID, bootstrapPort int, p2pKeyID string, ocrKeyBundleID string) job.Job { + specArgs := validate.SpecArgs{ + P2PV2Bootstrappers: []string{ + fmt.Sprintf("%s@127.0.0.1:%d", bootstrapP2PID.Raw(), bootstrapPort), + }, + CapabilityVersion: CapabilityVersion, + CapabilityLabelledName: CapabilityLabelledName, + OCRKeyBundleIDs: map[string]string{ + relay.NetworkEVM: ocrKeyBundleID, + }, + P2PKeyID: p2pKeyID, + PluginConfig: map[string]any{}, + } + specToml, err := validate.NewCCIPSpecToml(specArgs) + require.NoError(t, err) + jb, err := validate.ValidatedCCIPSpec(specToml) + require.NoError(t, err) + return jb +} diff --git a/core/capabilities/ccip/ccip_integration_tests/ping_pong_test.go b/core/capabilities/ccip/ccip_integration_tests/ping_pong_test.go new file mode 100644 index 00000000000..8a65ff5167d --- /dev/null +++ b/core/capabilities/ccip/ccip_integration_tests/ping_pong_test.go @@ -0,0 +1,95 @@ +package ccip_integration_tests + +import ( + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + gethcommon "github.com/ethereum/go-ethereum/common" + + "github.com/stretchr/testify/require" + + "golang.org/x/exp/maps" + + pp "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/ping_pong_demo" +) + +/* +* Test is setting up 3 chains (let's call them A, B, C), each chain deploys and starts 2 ping pong contracts for the other 2. +* A ---deploy+start---> (pingPongB, pingPongC) +* B ---deploy+start---> (pingPongA, pingPongC) +* C ---deploy+start---> (pingPongA, pingPongB) +* and then checks that each ping pong contract emitted `CCIPSendRequested` event from the expected source to destination. +* Test fails if any wiring between contracts is not correct. + */ +func TestPingPong(t *testing.T) { + _, universes := createUniverses(t, 3) + pingPongs := initializePingPongContracts(t, universes) + for chainID, universe := range universes { + for otherChain, pingPong := range pingPongs[chainID] { + t.Log("PingPong From: ", chainID, " To: ", otherChain) + _, err := pingPong.StartPingPong(universe.owner) + require.NoError(t, err) + universe.backend.Commit() + + logIter, err := universe.onramp.FilterCCIPSendRequested(&bind.FilterOpts{Start: 0}, nil) + require.NoError(t, err) + // Iterate until latest event + for logIter.Next() { + } + log := logIter.Event + require.Equal(t, getSelector(otherChain), log.DestChainSelector) + require.Equal(t, pingPong.Address(), log.Message.Sender) + chainPingPongAddr := pingPongs[otherChain][chainID].Address().Bytes() + // With chain agnostic addresses we need to pad the address to the correct length if the receiver is zero prefixed + paddedAddr := gethcommon.LeftPadBytes(chainPingPongAddr, len(log.Message.Receiver)) + require.Equal(t, paddedAddr, log.Message.Receiver) + } + } +} + +// InitializeContracts initializes ping pong contracts on all chains and +// connects them all to each other. +func initializePingPongContracts( + t *testing.T, + chainUniverses map[uint64]onchainUniverse, +) map[uint64]map[uint64]*pp.PingPongDemo { + pingPongs := make(map[uint64]map[uint64]*pp.PingPongDemo) + chainIDs := maps.Keys(chainUniverses) + // For each chain initialize N ping pong contracts, where N is the (number of chains - 1) + for chainID, universe := range chainUniverses { + pingPongs[chainID] = make(map[uint64]*pp.PingPongDemo) + for _, chainToConnect := range chainIDs { + if chainToConnect == chainID { + continue // don't connect chain to itself + } + backend := universe.backend + owner := universe.owner + pingPongAddr, _, _, err := pp.DeployPingPongDemo(owner, backend, universe.router.Address(), universe.linkToken.Address()) + require.NoError(t, err) + backend.Commit() + pingPong, err := pp.NewPingPongDemo(pingPongAddr, backend) + require.NoError(t, err) + backend.Commit() + // Fund the ping pong contract with LINK + _, err = universe.linkToken.Transfer(owner, pingPong.Address(), e18Mult(10)) + backend.Commit() + require.NoError(t, err) + pingPongs[chainID][chainToConnect] = pingPong + } + } + + // Set up each ping pong contract to its counterpart on the other chain + for chainID, universe := range chainUniverses { + for chainToConnect, pingPong := range pingPongs[chainID] { + _, err := pingPong.SetCounterpart( + universe.owner, + getSelector(chainUniverses[chainToConnect].chainID), + // This is the address of the ping pong contract on the other chain + pingPongs[chainToConnect][chainID].Address(), + ) + require.NoError(t, err) + universe.backend.Commit() + } + } + return pingPongs +} diff --git a/core/capabilities/ccip/ccipevm/commitcodec.go b/core/capabilities/ccip/ccipevm/commitcodec.go new file mode 100644 index 00000000000..928cecd0a41 --- /dev/null +++ b/core/capabilities/ccip/ccipevm/commitcodec.go @@ -0,0 +1,138 @@ +package ccipevm + +import ( + "context" + "fmt" + "math/big" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_multi_offramp" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" +) + +// CommitPluginCodecV1 is a codec for encoding and decoding commit plugin reports. +// Compatible with: +// - "EVM2EVMMultiOffRamp 1.6.0-dev" +type CommitPluginCodecV1 struct { + commitReportAcceptedEventInputs abi.Arguments +} + +func NewCommitPluginCodecV1() *CommitPluginCodecV1 { + abiParsed, err := abi.JSON(strings.NewReader(evm_2_evm_multi_offramp.EVM2EVMMultiOffRampABI)) + if err != nil { + panic(fmt.Errorf("parse multi offramp abi: %s", err)) + } + eventInputs := abihelpers.MustGetEventInputs("CommitReportAccepted", abiParsed) + return &CommitPluginCodecV1{commitReportAcceptedEventInputs: eventInputs} +} + +func (c *CommitPluginCodecV1) Encode(ctx context.Context, report cciptypes.CommitPluginReport) ([]byte, error) { + merkleRoots := make([]evm_2_evm_multi_offramp.EVM2EVMMultiOffRampMerkleRoot, 0, len(report.MerkleRoots)) + for _, root := range report.MerkleRoots { + merkleRoots = append(merkleRoots, evm_2_evm_multi_offramp.EVM2EVMMultiOffRampMerkleRoot{ + SourceChainSelector: uint64(root.ChainSel), + Interval: evm_2_evm_multi_offramp.EVM2EVMMultiOffRampInterval{ + Min: uint64(root.SeqNumsRange.Start()), + Max: uint64(root.SeqNumsRange.End()), + }, + MerkleRoot: root.MerkleRoot, + }) + } + + tokenPriceUpdates := make([]evm_2_evm_multi_offramp.InternalTokenPriceUpdate, 0, len(report.PriceUpdates.TokenPriceUpdates)) + for _, update := range report.PriceUpdates.TokenPriceUpdates { + if !common.IsHexAddress(string(update.TokenID)) { + return nil, fmt.Errorf("invalid token address: %s", update.TokenID) + } + if update.Price.IsEmpty() { + return nil, fmt.Errorf("empty price for token: %s", update.TokenID) + } + tokenPriceUpdates = append(tokenPriceUpdates, evm_2_evm_multi_offramp.InternalTokenPriceUpdate{ + SourceToken: common.HexToAddress(string(update.TokenID)), + UsdPerToken: update.Price.Int, + }) + } + + gasPriceUpdates := make([]evm_2_evm_multi_offramp.InternalGasPriceUpdate, 0, len(report.PriceUpdates.GasPriceUpdates)) + for _, update := range report.PriceUpdates.GasPriceUpdates { + if update.GasPrice.IsEmpty() { + return nil, fmt.Errorf("empty gas price for chain: %d", update.ChainSel) + } + + gasPriceUpdates = append(gasPriceUpdates, evm_2_evm_multi_offramp.InternalGasPriceUpdate{ + DestChainSelector: uint64(update.ChainSel), + UsdPerUnitGas: update.GasPrice.Int, + }) + } + + evmReport := evm_2_evm_multi_offramp.EVM2EVMMultiOffRampCommitReport{ + PriceUpdates: evm_2_evm_multi_offramp.InternalPriceUpdates{ + TokenPriceUpdates: tokenPriceUpdates, + GasPriceUpdates: gasPriceUpdates, + }, + MerkleRoots: merkleRoots, + } + + return c.commitReportAcceptedEventInputs.PackValues([]interface{}{evmReport}) +} + +func (c *CommitPluginCodecV1) Decode(ctx context.Context, bytes []byte) (cciptypes.CommitPluginReport, error) { + unpacked, err := c.commitReportAcceptedEventInputs.Unpack(bytes) + if err != nil { + return cciptypes.CommitPluginReport{}, err + } + if len(unpacked) != 1 { + return cciptypes.CommitPluginReport{}, fmt.Errorf("expected 1 argument, got %d", len(unpacked)) + } + + commitReportRaw := abi.ConvertType(unpacked[0], new(evm_2_evm_multi_offramp.EVM2EVMMultiOffRampCommitReport)) + commitReport, is := commitReportRaw.(*evm_2_evm_multi_offramp.EVM2EVMMultiOffRampCommitReport) + if !is { + return cciptypes.CommitPluginReport{}, + fmt.Errorf("expected EVM2EVMMultiOffRampCommitReport, got %T", unpacked[0]) + } + + merkleRoots := make([]cciptypes.MerkleRootChain, 0, len(commitReport.MerkleRoots)) + for _, root := range commitReport.MerkleRoots { + merkleRoots = append(merkleRoots, cciptypes.MerkleRootChain{ + ChainSel: cciptypes.ChainSelector(root.SourceChainSelector), + SeqNumsRange: cciptypes.NewSeqNumRange( + cciptypes.SeqNum(root.Interval.Min), + cciptypes.SeqNum(root.Interval.Max), + ), + MerkleRoot: root.MerkleRoot, + }) + } + + tokenPriceUpdates := make([]cciptypes.TokenPrice, 0, len(commitReport.PriceUpdates.TokenPriceUpdates)) + for _, update := range commitReport.PriceUpdates.TokenPriceUpdates { + tokenPriceUpdates = append(tokenPriceUpdates, cciptypes.TokenPrice{ + TokenID: types.Account(update.SourceToken.String()), + Price: cciptypes.NewBigInt(big.NewInt(0).Set(update.UsdPerToken)), + }) + } + + gasPriceUpdates := make([]cciptypes.GasPriceChain, 0, len(commitReport.PriceUpdates.GasPriceUpdates)) + for _, update := range commitReport.PriceUpdates.GasPriceUpdates { + gasPriceUpdates = append(gasPriceUpdates, cciptypes.GasPriceChain{ + GasPrice: cciptypes.NewBigInt(big.NewInt(0).Set(update.UsdPerUnitGas)), + ChainSel: cciptypes.ChainSelector(update.DestChainSelector), + }) + } + + return cciptypes.CommitPluginReport{ + MerkleRoots: merkleRoots, + PriceUpdates: cciptypes.PriceUpdates{ + TokenPriceUpdates: tokenPriceUpdates, + GasPriceUpdates: gasPriceUpdates, + }, + }, nil +} + +// Ensure CommitPluginCodec implements the CommitPluginCodec interface +var _ cciptypes.CommitPluginCodec = (*CommitPluginCodecV1)(nil) diff --git a/core/capabilities/ccip/ccipevm/commitcodec_test.go b/core/capabilities/ccip/ccipevm/commitcodec_test.go new file mode 100644 index 00000000000..737f7be1d6e --- /dev/null +++ b/core/capabilities/ccip/ccipevm/commitcodec_test.go @@ -0,0 +1,135 @@ +package ccipevm + +import ( + "math/big" + "math/rand" + "testing" + + "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" +) + +var randomCommitReport = func() cciptypes.CommitPluginReport { + return cciptypes.CommitPluginReport{ + MerkleRoots: []cciptypes.MerkleRootChain{ + { + ChainSel: cciptypes.ChainSelector(rand.Uint64()), + SeqNumsRange: cciptypes.NewSeqNumRange( + cciptypes.SeqNum(rand.Uint64()), + cciptypes.SeqNum(rand.Uint64()), + ), + MerkleRoot: utils.RandomBytes32(), + }, + { + ChainSel: cciptypes.ChainSelector(rand.Uint64()), + SeqNumsRange: cciptypes.NewSeqNumRange( + cciptypes.SeqNum(rand.Uint64()), + cciptypes.SeqNum(rand.Uint64()), + ), + MerkleRoot: utils.RandomBytes32(), + }, + }, + PriceUpdates: cciptypes.PriceUpdates{ + TokenPriceUpdates: []cciptypes.TokenPrice{ + { + TokenID: types.Account(utils.RandomAddress().String()), + Price: cciptypes.NewBigInt(utils.RandUint256()), + }, + }, + GasPriceUpdates: []cciptypes.GasPriceChain{ + {GasPrice: cciptypes.NewBigInt(utils.RandUint256()), ChainSel: cciptypes.ChainSelector(rand.Uint64())}, + {GasPrice: cciptypes.NewBigInt(utils.RandUint256()), ChainSel: cciptypes.ChainSelector(rand.Uint64())}, + {GasPrice: cciptypes.NewBigInt(utils.RandUint256()), ChainSel: cciptypes.ChainSelector(rand.Uint64())}, + }, + }, + } +} + +func TestCommitPluginCodecV1(t *testing.T) { + testCases := []struct { + name string + report func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport + expErr bool + }{ + { + name: "base report", + report: func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport { + return report + }, + }, + { + name: "empty token address", + report: func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport { + report.PriceUpdates.TokenPriceUpdates[0].TokenID = "" + return report + }, + expErr: true, + }, + { + name: "empty merkle root", + report: func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport { + report.MerkleRoots[0].MerkleRoot = cciptypes.Bytes32{} + return report + }, + }, + { + name: "zero token price", + report: func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport { + report.PriceUpdates.TokenPriceUpdates[0].Price = cciptypes.NewBigInt(big.NewInt(0)) + return report + }, + }, + { + name: "zero gas price", + report: func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport { + report.PriceUpdates.GasPriceUpdates[0].GasPrice = cciptypes.NewBigInt(big.NewInt(0)) + return report + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + report := tc.report(randomCommitReport()) + commitCodec := NewCommitPluginCodecV1() + ctx := testutils.Context(t) + encodedReport, err := commitCodec.Encode(ctx, report) + if tc.expErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + decodedReport, err := commitCodec.Decode(ctx, encodedReport) + require.NoError(t, err) + require.Equal(t, report, decodedReport) + }) + } +} + +func BenchmarkCommitPluginCodecV1_Encode(b *testing.B) { + commitCodec := NewCommitPluginCodecV1() + ctx := testutils.Context(b) + + rep := randomCommitReport() + for i := 0; i < b.N; i++ { + _, err := commitCodec.Encode(ctx, rep) + require.NoError(b, err) + } +} + +func BenchmarkCommitPluginCodecV1_Decode(b *testing.B) { + commitCodec := NewCommitPluginCodecV1() + ctx := testutils.Context(b) + encodedReport, err := commitCodec.Encode(ctx, randomCommitReport()) + require.NoError(b, err) + + for i := 0; i < b.N; i++ { + _, err := commitCodec.Decode(ctx, encodedReport) + require.NoError(b, err) + } +} diff --git a/core/capabilities/ccip/ccipevm/executecodec.go b/core/capabilities/ccip/ccipevm/executecodec.go new file mode 100644 index 00000000000..a64c775112c --- /dev/null +++ b/core/capabilities/ccip/ccipevm/executecodec.go @@ -0,0 +1,181 @@ +package ccipevm + +import ( + "context" + "fmt" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" + + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_multi_offramp" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" +) + +// ExecutePluginCodecV1 is a codec for encoding and decoding execute plugin reports. +// Compatible with: +// - "EVM2EVMMultiOffRamp 1.6.0-dev" +type ExecutePluginCodecV1 struct { + executeReportMethodInputs abi.Arguments +} + +func NewExecutePluginCodecV1() *ExecutePluginCodecV1 { + abiParsed, err := abi.JSON(strings.NewReader(evm_2_evm_multi_offramp.EVM2EVMMultiOffRampABI)) + if err != nil { + panic(fmt.Errorf("parse multi offramp abi: %s", err)) + } + methodInputs := abihelpers.MustGetMethodInputs("manuallyExecute", abiParsed) + if len(methodInputs) == 0 { + panic("no inputs found for method: manuallyExecute") + } + + return &ExecutePluginCodecV1{ + executeReportMethodInputs: methodInputs[:1], + } +} + +func (e *ExecutePluginCodecV1) Encode(ctx context.Context, report cciptypes.ExecutePluginReport) ([]byte, error) { + evmReport := make([]evm_2_evm_multi_offramp.InternalExecutionReportSingleChain, 0, len(report.ChainReports)) + + for _, chainReport := range report.ChainReports { + if chainReport.ProofFlagBits.IsEmpty() { + return nil, fmt.Errorf("proof flag bits are empty") + } + + evmProofs := make([][32]byte, 0, len(chainReport.Proofs)) + for _, proof := range chainReport.Proofs { + evmProofs = append(evmProofs, proof) + } + + evmMessages := make([]evm_2_evm_multi_offramp.InternalAny2EVMRampMessage, 0, len(chainReport.Messages)) + for _, message := range chainReport.Messages { + receiver := common.BytesToAddress(message.Receiver) + + tokenAmounts := make([]evm_2_evm_multi_offramp.InternalRampTokenAmount, 0, len(message.TokenAmounts)) + for _, tokenAmount := range message.TokenAmounts { + if tokenAmount.Amount.IsEmpty() { + return nil, fmt.Errorf("empty amount for token: %s", tokenAmount.DestTokenAddress) + } + + tokenAmounts = append(tokenAmounts, evm_2_evm_multi_offramp.InternalRampTokenAmount{ + SourcePoolAddress: tokenAmount.SourcePoolAddress, + DestTokenAddress: tokenAmount.DestTokenAddress, + ExtraData: tokenAmount.ExtraData, + Amount: tokenAmount.Amount.Int, + }) + } + + gasLimit, err := decodeExtraArgsV1V2(message.ExtraArgs) + if err != nil { + return nil, fmt.Errorf("decode extra args to get gas limit: %w", err) + } + + evmMessages = append(evmMessages, evm_2_evm_multi_offramp.InternalAny2EVMRampMessage{ + Header: evm_2_evm_multi_offramp.InternalRampMessageHeader{ + MessageId: message.Header.MessageID, + SourceChainSelector: uint64(message.Header.SourceChainSelector), + DestChainSelector: uint64(message.Header.DestChainSelector), + SequenceNumber: uint64(message.Header.SequenceNumber), + Nonce: message.Header.Nonce, + }, + Sender: message.Sender, + Data: message.Data, + Receiver: receiver, + GasLimit: gasLimit, + TokenAmounts: tokenAmounts, + }) + } + + evmChainReport := evm_2_evm_multi_offramp.InternalExecutionReportSingleChain{ + SourceChainSelector: uint64(chainReport.SourceChainSelector), + Messages: evmMessages, + OffchainTokenData: chainReport.OffchainTokenData, + Proofs: evmProofs, + ProofFlagBits: chainReport.ProofFlagBits.Int, + } + evmReport = append(evmReport, evmChainReport) + } + + return e.executeReportMethodInputs.PackValues([]interface{}{&evmReport}) +} + +func (e *ExecutePluginCodecV1) Decode(ctx context.Context, encodedReport []byte) (cciptypes.ExecutePluginReport, error) { + unpacked, err := e.executeReportMethodInputs.Unpack(encodedReport) + if err != nil { + return cciptypes.ExecutePluginReport{}, fmt.Errorf("unpack encoded report: %w", err) + } + if len(unpacked) != 1 { + return cciptypes.ExecutePluginReport{}, fmt.Errorf("unpacked report is empty") + } + + evmReportRaw := abi.ConvertType(unpacked[0], new([]evm_2_evm_multi_offramp.InternalExecutionReportSingleChain)) + evmReportPtr, is := evmReportRaw.(*[]evm_2_evm_multi_offramp.InternalExecutionReportSingleChain) + if !is { + return cciptypes.ExecutePluginReport{}, fmt.Errorf("got an unexpected report type %T", unpacked[0]) + } + if evmReportPtr == nil { + return cciptypes.ExecutePluginReport{}, fmt.Errorf("evm report is nil") + } + + evmReport := *evmReportPtr + executeReport := cciptypes.ExecutePluginReport{ + ChainReports: make([]cciptypes.ExecutePluginReportSingleChain, 0, len(evmReport)), + } + + for _, evmChainReport := range evmReport { + proofs := make([]cciptypes.Bytes32, 0, len(evmChainReport.Proofs)) + for _, proof := range evmChainReport.Proofs { + proofs = append(proofs, proof) + } + + messages := make([]cciptypes.Message, 0, len(evmChainReport.Messages)) + for _, evmMessage := range evmChainReport.Messages { + tokenAmounts := make([]cciptypes.RampTokenAmount, 0, len(evmMessage.TokenAmounts)) + for _, tokenAmount := range evmMessage.TokenAmounts { + tokenAmounts = append(tokenAmounts, cciptypes.RampTokenAmount{ + SourcePoolAddress: tokenAmount.SourcePoolAddress, + DestTokenAddress: tokenAmount.DestTokenAddress, + ExtraData: tokenAmount.ExtraData, + Amount: cciptypes.NewBigInt(tokenAmount.Amount), + }) + } + + message := cciptypes.Message{ + Header: cciptypes.RampMessageHeader{ + MessageID: evmMessage.Header.MessageId, + SourceChainSelector: cciptypes.ChainSelector(evmMessage.Header.SourceChainSelector), + DestChainSelector: cciptypes.ChainSelector(evmMessage.Header.DestChainSelector), + SequenceNumber: cciptypes.SeqNum(evmMessage.Header.SequenceNumber), + Nonce: evmMessage.Header.Nonce, + MsgHash: cciptypes.Bytes32{}, // <-- todo: info not available, but not required atm + OnRamp: cciptypes.Bytes{}, // <-- todo: info not available, but not required atm + }, + Sender: evmMessage.Sender, + Data: evmMessage.Data, + Receiver: evmMessage.Receiver.Bytes(), + ExtraArgs: cciptypes.Bytes{}, // <-- todo: info not available, but not required atm + FeeToken: cciptypes.Bytes{}, // <-- todo: info not available, but not required atm + FeeTokenAmount: cciptypes.BigInt{}, // <-- todo: info not available, but not required atm + TokenAmounts: tokenAmounts, + } + messages = append(messages, message) + } + + chainReport := cciptypes.ExecutePluginReportSingleChain{ + SourceChainSelector: cciptypes.ChainSelector(evmChainReport.SourceChainSelector), + Messages: messages, + OffchainTokenData: evmChainReport.OffchainTokenData, + Proofs: proofs, + ProofFlagBits: cciptypes.NewBigInt(evmChainReport.ProofFlagBits), + } + + executeReport.ChainReports = append(executeReport.ChainReports, chainReport) + } + + return executeReport, nil +} + +// Ensure ExecutePluginCodec implements the ExecutePluginCodec interface +var _ cciptypes.ExecutePluginCodec = (*ExecutePluginCodecV1)(nil) diff --git a/core/capabilities/ccip/ccipevm/executecodec_test.go b/core/capabilities/ccip/ccipevm/executecodec_test.go new file mode 100644 index 00000000000..4f207fdb0e2 --- /dev/null +++ b/core/capabilities/ccip/ccipevm/executecodec_test.go @@ -0,0 +1,174 @@ +package ccipevm + +import ( + "math/rand" + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" + "github.com/ethereum/go-ethereum/core" + + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/message_hasher" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/report_codec" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var randomExecuteReport = func(t *testing.T, d *testSetupData) cciptypes.ExecutePluginReport { + const numChainReports = 10 + const msgsPerReport = 10 + const numTokensPerMsg = 3 + + chainReports := make([]cciptypes.ExecutePluginReportSingleChain, numChainReports) + for i := 0; i < numChainReports; i++ { + reportMessages := make([]cciptypes.Message, msgsPerReport) + for j := 0; j < msgsPerReport; j++ { + data, err := cciptypes.NewBytesFromString(utils.RandomAddress().String()) + assert.NoError(t, err) + + tokenAmounts := make([]cciptypes.RampTokenAmount, numTokensPerMsg) + for z := 0; z < numTokensPerMsg; z++ { + tokenAmounts[z] = cciptypes.RampTokenAmount{ + SourcePoolAddress: utils.RandomAddress().Bytes(), + DestTokenAddress: utils.RandomAddress().Bytes(), + ExtraData: data, + Amount: cciptypes.NewBigInt(utils.RandUint256()), + } + } + + extraArgs, err := d.contract.EncodeEVMExtraArgsV1(nil, message_hasher.ClientEVMExtraArgsV1{ + GasLimit: utils.RandUint256(), + }) + assert.NoError(t, err) + + reportMessages[j] = cciptypes.Message{ + Header: cciptypes.RampMessageHeader{ + MessageID: utils.RandomBytes32(), + SourceChainSelector: cciptypes.ChainSelector(rand.Uint64()), + DestChainSelector: cciptypes.ChainSelector(rand.Uint64()), + SequenceNumber: cciptypes.SeqNum(rand.Uint64()), + Nonce: rand.Uint64(), + MsgHash: utils.RandomBytes32(), + OnRamp: utils.RandomAddress().Bytes(), + }, + Sender: utils.RandomAddress().Bytes(), + Data: data, + Receiver: utils.RandomAddress().Bytes(), + ExtraArgs: extraArgs, + FeeToken: utils.RandomAddress().Bytes(), + FeeTokenAmount: cciptypes.NewBigInt(utils.RandUint256()), + TokenAmounts: tokenAmounts, + } + } + + tokenData := make([][][]byte, numTokensPerMsg) + for j := 0; j < numTokensPerMsg; j++ { + tokenData[j] = [][]byte{{0x1}, {0x2, 0x3}} + } + + chainReports[i] = cciptypes.ExecutePluginReportSingleChain{ + SourceChainSelector: cciptypes.ChainSelector(rand.Uint64()), + Messages: reportMessages, + OffchainTokenData: tokenData, + Proofs: []cciptypes.Bytes32{utils.RandomBytes32(), utils.RandomBytes32()}, + ProofFlagBits: cciptypes.NewBigInt(utils.RandUint256()), + } + } + + return cciptypes.ExecutePluginReport{ChainReports: chainReports} +} + +func TestExecutePluginCodecV1(t *testing.T) { + d := testSetup(t) + + testCases := []struct { + name string + report func(report cciptypes.ExecutePluginReport) cciptypes.ExecutePluginReport + expErr bool + }{ + { + name: "base report", + report: func(report cciptypes.ExecutePluginReport) cciptypes.ExecutePluginReport { return report }, + expErr: false, + }, + { + name: "reports have empty msgs", + report: func(report cciptypes.ExecutePluginReport) cciptypes.ExecutePluginReport { + report.ChainReports[0].Messages = []cciptypes.Message{} + report.ChainReports[4].Messages = []cciptypes.Message{} + return report + }, + expErr: false, + }, + { + name: "reports have empty offchain token data", + report: func(report cciptypes.ExecutePluginReport) cciptypes.ExecutePluginReport { + report.ChainReports[0].OffchainTokenData = [][][]byte{} + report.ChainReports[4].OffchainTokenData[1] = [][]byte{} + return report + }, + expErr: false, + }, + } + + ctx := testutils.Context(t) + + // Deploy the contract + transactor := testutils.MustNewSimTransactor(t) + simulatedBackend := backends.NewSimulatedBackend(core.GenesisAlloc{ + transactor.From: {Balance: assets.Ether(1000).ToInt()}, + }, 30e6) + address, _, _, err := report_codec.DeployReportCodec(transactor, simulatedBackend) + require.NoError(t, err) + simulatedBackend.Commit() + contract, err := report_codec.NewReportCodec(address, simulatedBackend) + require.NoError(t, err) + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + codec := NewExecutePluginCodecV1() + report := tc.report(randomExecuteReport(t, d)) + bytes, err := codec.Encode(ctx, report) + if tc.expErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + + testSetup(t) + + // ignore msg hash in comparison + for i := range report.ChainReports { + for j := range report.ChainReports[i].Messages { + report.ChainReports[i].Messages[j].Header.MsgHash = cciptypes.Bytes32{} + report.ChainReports[i].Messages[j].Header.OnRamp = cciptypes.Bytes{} + report.ChainReports[i].Messages[j].FeeToken = cciptypes.Bytes{} + report.ChainReports[i].Messages[j].ExtraArgs = cciptypes.Bytes{} + report.ChainReports[i].Messages[j].FeeTokenAmount = cciptypes.BigInt{} + } + } + + // decode using the contract + contractDecodedReport, err := contract.DecodeExecuteReport(&bind.CallOpts{Context: ctx}, bytes) + assert.NoError(t, err) + assert.Equal(t, len(report.ChainReports), len(contractDecodedReport)) + for i, expReport := range report.ChainReports { + actReport := contractDecodedReport[i] + assert.Equal(t, expReport.OffchainTokenData, actReport.OffchainTokenData) + assert.Equal(t, len(expReport.Messages), len(actReport.Messages)) + assert.Equal(t, uint64(expReport.SourceChainSelector), actReport.SourceChainSelector) + } + + // decode using the codec + codecDecoded, err := codec.Decode(ctx, bytes) + assert.NoError(t, err) + assert.Equal(t, report, codecDecoded) + }) + } +} diff --git a/core/capabilities/ccip/ccipevm/helpers.go b/core/capabilities/ccip/ccipevm/helpers.go new file mode 100644 index 00000000000..ee83230a4ce --- /dev/null +++ b/core/capabilities/ccip/ccipevm/helpers.go @@ -0,0 +1,33 @@ +package ccipevm + +import ( + "bytes" + "fmt" + "math/big" +) + +func decodeExtraArgsV1V2(extraArgs []byte) (gasLimit *big.Int, err error) { + if len(extraArgs) < 4 { + return nil, fmt.Errorf("extra args too short: %d, should be at least 4 (i.e the extraArgs tag)", len(extraArgs)) + } + + var method string + if bytes.Equal(extraArgs[:4], evmExtraArgsV1Tag) { + method = "decodeEVMExtraArgsV1" + } else if bytes.Equal(extraArgs[:4], evmExtraArgsV2Tag) { + method = "decodeEVMExtraArgsV2" + } else { + return nil, fmt.Errorf("unknown extra args tag: %x", extraArgs) + } + ifaces, err := messageHasherABI.Methods[method].Inputs.UnpackValues(extraArgs[4:]) + if err != nil { + return nil, fmt.Errorf("abi decode extra args v1: %w", err) + } + // gas limit is always the first argument, and allow OOO isn't set explicitly + // on the message. + _, ok := ifaces[0].(*big.Int) + if !ok { + return nil, fmt.Errorf("expected *big.Int, got %T", ifaces[0]) + } + return ifaces[0].(*big.Int), nil +} diff --git a/core/capabilities/ccip/ccipevm/helpers_test.go b/core/capabilities/ccip/ccipevm/helpers_test.go new file mode 100644 index 00000000000..95a5d4439bb --- /dev/null +++ b/core/capabilities/ccip/ccipevm/helpers_test.go @@ -0,0 +1,41 @@ +package ccipevm + +import ( + "math/big" + "math/rand" + "testing" + + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/message_hasher" + + "github.com/stretchr/testify/require" +) + +func Test_decodeExtraArgs(t *testing.T) { + d := testSetup(t) + gasLimit := big.NewInt(rand.Int63()) + + t.Run("v1", func(t *testing.T) { + encoded, err := d.contract.EncodeEVMExtraArgsV1(nil, message_hasher.ClientEVMExtraArgsV1{ + GasLimit: gasLimit, + }) + require.NoError(t, err) + + decodedGasLimit, err := decodeExtraArgsV1V2(encoded) + require.NoError(t, err) + + require.Equal(t, gasLimit, decodedGasLimit) + }) + + t.Run("v2", func(t *testing.T) { + encoded, err := d.contract.EncodeEVMExtraArgsV2(nil, message_hasher.ClientEVMExtraArgsV2{ + GasLimit: gasLimit, + AllowOutOfOrderExecution: true, + }) + require.NoError(t, err) + + decodedGasLimit, err := decodeExtraArgsV1V2(encoded) + require.NoError(t, err) + + require.Equal(t, gasLimit, decodedGasLimit) + }) +} diff --git a/core/capabilities/ccip/ccipevm/msghasher.go b/core/capabilities/ccip/ccipevm/msghasher.go new file mode 100644 index 00000000000..0df0a8254ac --- /dev/null +++ b/core/capabilities/ccip/ccipevm/msghasher.go @@ -0,0 +1,127 @@ +package ccipevm + +import ( + "context" + "fmt" + + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/message_hasher" +) + +var ( + // bytes32 internal constant LEAF_DOMAIN_SEPARATOR = 0x0000000000000000000000000000000000000000000000000000000000000000; + leafDomainSeparator = [32]byte{} + + // bytes32 internal constant ANY_2_EVM_MESSAGE_HASH = keccak256("Any2EVMMessageHashV1"); + ANY_2_EVM_MESSAGE_HASH = utils.Keccak256Fixed([]byte("Any2EVMMessageHashV1")) + + messageHasherABI = types.MustGetABI(message_hasher.MessageHasherABI) + + // bytes4 public constant EVM_EXTRA_ARGS_V1_TAG = 0x97a657c9; + evmExtraArgsV1Tag = hexutil.MustDecode("0x97a657c9") + + // bytes4 public constant EVM_EXTRA_ARGS_V2_TAG = 0x181dcf10; + evmExtraArgsV2Tag = hexutil.MustDecode("0x181dcf10") +) + +// MessageHasherV1 implements the MessageHasher interface. +// Compatible with: +// - "EVM2EVMMultiOnRamp 1.6.0-dev" +type MessageHasherV1 struct{} + +func NewMessageHasherV1() *MessageHasherV1 { + return &MessageHasherV1{} +} + +// Hash implements the MessageHasher interface. +// It constructs all of the inputs to the final keccak256 hash in Internal._hash(Any2EVMRampMessage). +// The main structure of the hash is as follows: +/* + keccak256( + leafDomainSeparator, + keccak256(any_2_evm_message_hash, header.sourceChainSelector, header.destinationChainSelector, onRamp), + keccak256(fixedSizeMessageFields), + keccak256(messageData), + keccak256(encodedRampTokenAmounts), + ) +*/ +func (h *MessageHasherV1) Hash(_ context.Context, msg cciptypes.Message) (cciptypes.Bytes32, error) { + var rampTokenAmounts []message_hasher.InternalRampTokenAmount + for _, rta := range msg.TokenAmounts { + rampTokenAmounts = append(rampTokenAmounts, message_hasher.InternalRampTokenAmount{ + SourcePoolAddress: rta.SourcePoolAddress, + DestTokenAddress: rta.DestTokenAddress, + ExtraData: rta.ExtraData, + Amount: rta.Amount.Int, + }) + } + encodedRampTokenAmounts, err := abiEncode("encodeTokenAmountsHashPreimage", rampTokenAmounts) + if err != nil { + return [32]byte{}, fmt.Errorf("abi encode token amounts: %w", err) + } + + metaDataHashInput, err := abiEncode( + "encodeMetadataHashPreimage", + ANY_2_EVM_MESSAGE_HASH, + uint64(msg.Header.SourceChainSelector), + uint64(msg.Header.DestChainSelector), + []byte(msg.Header.OnRamp), + ) + if err != nil { + return [32]byte{}, fmt.Errorf("abi encode metadata hash input: %w", err) + } + + // Need to decode the extra args to get the gas limit. + // TODO: we assume that extra args is always abi-encoded for now, but we need + // to decode according to source chain selector family. We should add a family + // lookup API to the chain-selectors library. + gasLimit, err := decodeExtraArgsV1V2(msg.ExtraArgs) + if err != nil { + return [32]byte{}, fmt.Errorf("decode extra args: %w", err) + } + + fixedSizeFieldsEncoded, err := abiEncode( + "encodeFixedSizeFieldsHashPreimage", + msg.Header.MessageID, + []byte(msg.Sender), + common.BytesToAddress(msg.Receiver), + uint64(msg.Header.SequenceNumber), + gasLimit, + msg.Header.Nonce, + ) + if err != nil { + return [32]byte{}, fmt.Errorf("abi encode fixed size values: %w", err) + } + + packedValues, err := abiEncode( + "encodeFinalHashPreimage", + leafDomainSeparator, + utils.Keccak256Fixed(metaDataHashInput), + utils.Keccak256Fixed(fixedSizeFieldsEncoded), + utils.Keccak256Fixed(msg.Data), + utils.Keccak256Fixed(encodedRampTokenAmounts), + ) + if err != nil { + return [32]byte{}, fmt.Errorf("abi encode packed values: %w", err) + } + + return utils.Keccak256Fixed(packedValues), nil +} + +func abiEncode(method string, values ...interface{}) ([]byte, error) { + res, err := messageHasherABI.Pack(method, values...) + if err != nil { + return nil, err + } + // trim the method selector. + return res[4:], nil +} + +// Interface compliance check +var _ cciptypes.MessageHasher = (*MessageHasherV1)(nil) diff --git a/core/capabilities/ccip/ccipevm/msghasher_test.go b/core/capabilities/ccip/ccipevm/msghasher_test.go new file mode 100644 index 00000000000..911a10b26a5 --- /dev/null +++ b/core/capabilities/ccip/ccipevm/msghasher_test.go @@ -0,0 +1,189 @@ +package ccipevm + +import ( + "context" + cryptorand "crypto/rand" + "fmt" + "math/big" + "math/rand" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/stretchr/testify/require" + + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/message_hasher" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" +) + +// NOTE: these test cases are only EVM <-> EVM. +// Update these cases once we have non-EVM examples. +func TestMessageHasher_EVM2EVM(t *testing.T) { + ctx := testutils.Context(t) + d := testSetup(t) + + testCases := []evmExtraArgs{ + {version: "v1", gasLimit: big.NewInt(rand.Int63())}, + {version: "v2", gasLimit: big.NewInt(rand.Int63()), allowOOO: false}, + {version: "v2", gasLimit: big.NewInt(rand.Int63()), allowOOO: true}, + } + for i, tc := range testCases { + t.Run(fmt.Sprintf("tc_%d", i), func(tt *testing.T) { + testHasherEVM2EVM(ctx, tt, d, tc) + }) + } +} + +func testHasherEVM2EVM(ctx context.Context, t *testing.T, d *testSetupData, evmExtraArgs evmExtraArgs) { + ccipMsg := createEVM2EVMMessage(t, d.contract, evmExtraArgs) + + var tokenAmounts []message_hasher.InternalRampTokenAmount + for _, rta := range ccipMsg.TokenAmounts { + tokenAmounts = append(tokenAmounts, message_hasher.InternalRampTokenAmount{ + SourcePoolAddress: rta.SourcePoolAddress, + DestTokenAddress: rta.DestTokenAddress, + ExtraData: rta.ExtraData[:], + Amount: rta.Amount.Int, + }) + } + evmMsg := message_hasher.InternalAny2EVMRampMessage{ + Header: message_hasher.InternalRampMessageHeader{ + MessageId: ccipMsg.Header.MessageID, + SourceChainSelector: uint64(ccipMsg.Header.SourceChainSelector), + DestChainSelector: uint64(ccipMsg.Header.DestChainSelector), + SequenceNumber: uint64(ccipMsg.Header.SequenceNumber), + Nonce: ccipMsg.Header.Nonce, + }, + Sender: ccipMsg.Sender, + Receiver: common.BytesToAddress(ccipMsg.Receiver), + GasLimit: evmExtraArgs.gasLimit, + Data: ccipMsg.Data, + TokenAmounts: tokenAmounts, + } + + expectedHash, err := d.contract.Hash(&bind.CallOpts{Context: ctx}, evmMsg, ccipMsg.Header.OnRamp) + require.NoError(t, err) + + evmMsgHasher := NewMessageHasherV1() + actualHash, err := evmMsgHasher.Hash(ctx, ccipMsg) + require.NoError(t, err) + + require.Equal(t, fmt.Sprintf("%x", expectedHash), strings.TrimPrefix(actualHash.String(), "0x")) +} + +type evmExtraArgs struct { + version string + gasLimit *big.Int + allowOOO bool +} + +func createEVM2EVMMessage(t *testing.T, messageHasher *message_hasher.MessageHasher, evmExtraArgs evmExtraArgs) cciptypes.Message { + messageID := utils.RandomBytes32() + + sourceTokenData := make([]byte, rand.Intn(2048)) + _, err := cryptorand.Read(sourceTokenData) + require.NoError(t, err) + + sourceChain := rand.Uint64() + seqNum := rand.Uint64() + nonce := rand.Uint64() + destChain := rand.Uint64() + + var extraArgsBytes []byte + if evmExtraArgs.version == "v1" { + extraArgsBytes, err = messageHasher.EncodeEVMExtraArgsV1(nil, message_hasher.ClientEVMExtraArgsV1{ + GasLimit: evmExtraArgs.gasLimit, + }) + require.NoError(t, err) + } else if evmExtraArgs.version == "v2" { + extraArgsBytes, err = messageHasher.EncodeEVMExtraArgsV2(nil, message_hasher.ClientEVMExtraArgsV2{ + GasLimit: evmExtraArgs.gasLimit, + AllowOutOfOrderExecution: evmExtraArgs.allowOOO, + }) + require.NoError(t, err) + } else { + require.FailNowf(t, "unknown extra args version", "version: %s", evmExtraArgs.version) + } + + messageData := make([]byte, rand.Intn(2048)) + _, err = cryptorand.Read(messageData) + require.NoError(t, err) + + numTokens := rand.Intn(10) + var sourceTokenDatas [][]byte + for i := 0; i < numTokens; i++ { + sourceTokenDatas = append(sourceTokenDatas, sourceTokenData) + } + + var tokenAmounts []cciptypes.RampTokenAmount + for i := 0; i < len(sourceTokenDatas); i++ { + extraData := utils.RandomBytes32() + tokenAmounts = append(tokenAmounts, cciptypes.RampTokenAmount{ + SourcePoolAddress: abiEncodedAddress(t), + DestTokenAddress: abiEncodedAddress(t), + ExtraData: extraData[:], + Amount: cciptypes.NewBigInt(big.NewInt(0).SetUint64(rand.Uint64())), + }) + } + + return cciptypes.Message{ + Header: cciptypes.RampMessageHeader{ + MessageID: messageID, + SourceChainSelector: cciptypes.ChainSelector(sourceChain), + DestChainSelector: cciptypes.ChainSelector(destChain), + SequenceNumber: cciptypes.SeqNum(seqNum), + Nonce: nonce, + OnRamp: abiEncodedAddress(t), + }, + Sender: abiEncodedAddress(t), + Receiver: abiEncodedAddress(t), + Data: messageData, + TokenAmounts: tokenAmounts, + FeeToken: abiEncodedAddress(t), + FeeTokenAmount: cciptypes.NewBigInt(big.NewInt(0).SetUint64(rand.Uint64())), + ExtraArgs: extraArgsBytes, + } +} + +func abiEncodedAddress(t *testing.T) []byte { + addr := utils.RandomAddress() + encoded, err := utils.ABIEncode(`[{"type": "address"}]`, addr) + require.NoError(t, err) + return encoded +} + +type testSetupData struct { + contractAddr common.Address + contract *message_hasher.MessageHasher + sb *backends.SimulatedBackend + auth *bind.TransactOpts +} + +func testSetup(t *testing.T) *testSetupData { + transactor := testutils.MustNewSimTransactor(t) + simulatedBackend := backends.NewSimulatedBackend(core.GenesisAlloc{ + transactor.From: {Balance: assets.Ether(1000).ToInt()}, + }, 30e6) + + // Deploy the contract + address, _, _, err := message_hasher.DeployMessageHasher(transactor, simulatedBackend) + require.NoError(t, err) + simulatedBackend.Commit() + + // Setup contract client + contract, err := message_hasher.NewMessageHasher(address, simulatedBackend) + require.NoError(t, err) + + return &testSetupData{ + contractAddr: address, + contract: contract, + sb: simulatedBackend, + auth: transactor, + } +} diff --git a/core/capabilities/ccip/common/common.go b/core/capabilities/ccip/common/common.go new file mode 100644 index 00000000000..6409345ed93 --- /dev/null +++ b/core/capabilities/ccip/common/common.go @@ -0,0 +1,23 @@ +package common + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/crypto" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" +) + +// HashedCapabilityID returns the hashed capability id in a manner equivalent to the capability registry. +func HashedCapabilityID(capabilityLabelledName, capabilityVersion string) (r [32]byte, err error) { + // TODO: investigate how to avoid parsing the ABI everytime. + tabi := `[{"type": "string"}, {"type": "string"}]` + abiEncoded, err := utils.ABIEncode(tabi, capabilityLabelledName, capabilityVersion) + if err != nil { + return r, fmt.Errorf("failed to ABI encode capability version and labelled name: %w", err) + } + + h := crypto.Keccak256(abiEncoded) + copy(r[:], h) + return r, nil +} diff --git a/core/capabilities/ccip/common/common_test.go b/core/capabilities/ccip/common/common_test.go new file mode 100644 index 00000000000..a7484a83ad9 --- /dev/null +++ b/core/capabilities/ccip/common/common_test.go @@ -0,0 +1,51 @@ +package common_test + +import ( + "testing" + + capcommon "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/common" + + "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" +) + +func Test_HashedCapabilityId(t *testing.T) { + transactor := testutils.MustNewSimTransactor(t) + sb := backends.NewSimulatedBackend(core.GenesisAlloc{ + transactor.From: {Balance: assets.Ether(1000).ToInt()}, + }, 30e6) + + crAddress, _, _, err := kcr.DeployCapabilitiesRegistry(transactor, sb) + require.NoError(t, err) + sb.Commit() + + cr, err := kcr.NewCapabilitiesRegistry(crAddress, sb) + require.NoError(t, err) + + // add a capability, ignore cap config for simplicity. + _, err = cr.AddCapabilities(transactor, []kcr.CapabilitiesRegistryCapability{ + { + LabelledName: "ccip", + Version: "v1.0.0", + CapabilityType: 0, + ResponseType: 0, + ConfigurationContract: common.Address{}, + }, + }) + require.NoError(t, err) + sb.Commit() + + hidExpected, err := cr.GetHashedCapabilityId(nil, "ccip", "v1.0.0") + require.NoError(t, err) + + hid, err := capcommon.HashedCapabilityID("ccip", "v1.0.0") + require.NoError(t, err) + + require.Equal(t, hidExpected, hid) +} diff --git a/core/capabilities/ccip/configs/evm/chain_writer.go b/core/capabilities/ccip/configs/evm/chain_writer.go new file mode 100644 index 00000000000..6d3b73c6f5c --- /dev/null +++ b/core/capabilities/ccip/configs/evm/chain_writer.go @@ -0,0 +1,75 @@ +package evm + +import ( + "encoding/json" + "fmt" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink-ccip/pkg/consts" + "github.com/smartcontractkit/chainlink/v2/common/txmgr" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_multi_offramp" + evmrelaytypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" +) + +var ( + offrampABI = evmtypes.MustGetABI(evm_2_evm_multi_offramp.EVM2EVMMultiOffRampABI) +) + +func MustChainWriterConfig( + fromAddress common.Address, + maxGasPrice *assets.Wei, + commitGasLimit, + execBatchGasLimit uint64, +) []byte { + rawConfig := ChainWriterConfigRaw(fromAddress, maxGasPrice, commitGasLimit, execBatchGasLimit) + encoded, err := json.Marshal(rawConfig) + if err != nil { + panic(fmt.Errorf("failed to marshal ChainWriterConfig: %w", err)) + } + + return encoded +} + +// ChainWriterConfigRaw returns a ChainWriterConfig that can be used to transmit commit and execute reports. +func ChainWriterConfigRaw( + fromAddress common.Address, + maxGasPrice *assets.Wei, + commitGasLimit, + execBatchGasLimit uint64, +) evmrelaytypes.ChainWriterConfig { + return evmrelaytypes.ChainWriterConfig{ + Contracts: map[string]*evmrelaytypes.ContractConfig{ + consts.ContractNameOffRamp: { + ContractABI: evm_2_evm_multi_offramp.EVM2EVMMultiOffRampABI, + Configs: map[string]*evmrelaytypes.ChainWriterDefinition{ + consts.MethodCommit: { + ChainSpecificName: mustGetMethodName("commit", offrampABI), + FromAddress: fromAddress, + GasLimit: commitGasLimit, + }, + consts.MethodExecute: { + ChainSpecificName: mustGetMethodName("execute", offrampABI), + FromAddress: fromAddress, + GasLimit: execBatchGasLimit, + }, + }, + }, + }, + SendStrategy: txmgr.NewSendEveryStrategy(), + MaxGasPrice: maxGasPrice, + } +} + +// mustGetMethodName panics if the method name is not found in the provided ABI. +func mustGetMethodName(name string, tabi abi.ABI) (methodName string) { + m, ok := tabi.Methods[name] + if !ok { + panic(fmt.Sprintf("missing method %s in the abi", name)) + } + return m.Name +} diff --git a/core/capabilities/ccip/configs/evm/contract_reader.go b/core/capabilities/ccip/configs/evm/contract_reader.go new file mode 100644 index 00000000000..085729690d5 --- /dev/null +++ b/core/capabilities/ccip/configs/evm/contract_reader.go @@ -0,0 +1,219 @@ +package evm + +import ( + "encoding/json" + "fmt" + + "github.com/ethereum/go-ethereum/accounts/abi" + + "github.com/smartcontractkit/chainlink-ccip/pkg/consts" + + evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/ccip_config" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_multi_offramp" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_multi_onramp" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/price_registry" + kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" + evmrelaytypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" +) + +var ( + onrampABI = evmtypes.MustGetABI(evm_2_evm_multi_onramp.EVM2EVMMultiOnRampABI) + capabilitiesRegsitryABI = evmtypes.MustGetABI(kcr.CapabilitiesRegistryABI) + ccipConfigABI = evmtypes.MustGetABI(ccip_config.CCIPConfigABI) + priceRegistryABI = evmtypes.MustGetABI(price_registry.PriceRegistryABI) +) + +// MustSourceReaderConfig returns a ChainReaderConfig that can be used to read from the onramp. +// The configuration is marshaled into JSON so that it can be passed to the relayer NewContractReader() method. +func MustSourceReaderConfig() []byte { + rawConfig := SourceReaderConfig() + encoded, err := json.Marshal(rawConfig) + if err != nil { + panic(fmt.Errorf("failed to marshal ChainReaderConfig into JSON: %w", err)) + } + + return encoded +} + +// MustDestReaderConfig returns a ChainReaderConfig that can be used to read from the offramp. +// The configuration is marshaled into JSON so that it can be passed to the relayer NewContractReader() method. +func MustDestReaderConfig() []byte { + rawConfig := DestReaderConfig() + encoded, err := json.Marshal(rawConfig) + if err != nil { + panic(fmt.Errorf("failed to marshal ChainReaderConfig into JSON: %w", err)) + } + + return encoded +} + +// DestReaderConfig returns a ChainReaderConfig that can be used to read from the offramp. +func DestReaderConfig() evmrelaytypes.ChainReaderConfig { + return evmrelaytypes.ChainReaderConfig{ + Contracts: map[string]evmrelaytypes.ChainContractReader{ + consts.ContractNameOffRamp: { + ContractABI: evm_2_evm_multi_offramp.EVM2EVMMultiOffRampABI, + ContractPollingFilter: evmrelaytypes.ContractPollingFilter{ + GenericEventNames: []string{ + mustGetEventName(consts.EventNameExecutionStateChanged, offrampABI), + mustGetEventName(consts.EventNameCommitReportAccepted, offrampABI), + }, + }, + Configs: map[string]*evmrelaytypes.ChainReaderDefinition{ + consts.MethodNameGetExecutionState: { + ChainSpecificName: mustGetMethodName("getExecutionState", offrampABI), + ReadType: evmrelaytypes.Method, + }, + consts.MethodNameGetMerkleRoot: { + ChainSpecificName: mustGetMethodName("getMerkleRoot", offrampABI), + ReadType: evmrelaytypes.Method, + }, + consts.MethodNameIsBlessed: { + ChainSpecificName: mustGetMethodName("isBlessed", offrampABI), + ReadType: evmrelaytypes.Method, + }, + consts.MethodNameGetLatestPriceSequenceNumber: { + ChainSpecificName: mustGetMethodName("getLatestPriceSequenceNumber", offrampABI), + ReadType: evmrelaytypes.Method, + }, + consts.MethodNameOfframpGetStaticConfig: { + ChainSpecificName: mustGetMethodName("getStaticConfig", offrampABI), + ReadType: evmrelaytypes.Method, + }, + consts.MethodNameOfframpGetDynamicConfig: { + ChainSpecificName: mustGetMethodName("getDynamicConfig", offrampABI), + ReadType: evmrelaytypes.Method, + }, + consts.MethodNameGetSourceChainConfig: { + ChainSpecificName: mustGetMethodName("getSourceChainConfig", offrampABI), + ReadType: evmrelaytypes.Method, + }, + consts.EventNameCommitReportAccepted: { + ChainSpecificName: mustGetEventName(consts.EventNameCommitReportAccepted, offrampABI), + ReadType: evmrelaytypes.Event, + }, + consts.EventNameExecutionStateChanged: { + ChainSpecificName: mustGetEventName(consts.EventNameExecutionStateChanged, offrampABI), + ReadType: evmrelaytypes.Event, + }, + }, + }, + }, + } +} + +// SourceReaderConfig returns a ChainReaderConfig that can be used to read from the onramp. +func SourceReaderConfig() evmrelaytypes.ChainReaderConfig { + return evmrelaytypes.ChainReaderConfig{ + Contracts: map[string]evmrelaytypes.ChainContractReader{ + consts.ContractNameOnRamp: { + ContractABI: evm_2_evm_multi_onramp.EVM2EVMMultiOnRampABI, + ContractPollingFilter: evmrelaytypes.ContractPollingFilter{ + GenericEventNames: []string{ + mustGetEventName(consts.EventNameCCIPSendRequested, onrampABI), + }, + }, + Configs: map[string]*evmrelaytypes.ChainReaderDefinition{ + // all "{external|public} view" functions in the onramp except for getFee and getPoolBySourceToken are here. + // getFee is not expected to get called offchain and is only called by end-user contracts. + consts.MethodNameGetExpectedNextSequenceNumber: { + ChainSpecificName: mustGetMethodName("getExpectedNextSequenceNumber", onrampABI), + ReadType: evmrelaytypes.Method, + }, + consts.MethodNameOnrampGetStaticConfig: { + ChainSpecificName: mustGetMethodName("getStaticConfig", onrampABI), + ReadType: evmrelaytypes.Method, + }, + consts.MethodNameOnrampGetDynamicConfig: { + ChainSpecificName: mustGetMethodName("getDynamicConfig", onrampABI), + ReadType: evmrelaytypes.Method, + }, + consts.EventNameCCIPSendRequested: { + ChainSpecificName: mustGetEventName(consts.EventNameCCIPSendRequested, onrampABI), + ReadType: evmrelaytypes.Event, + EventDefinitions: &evmrelaytypes.EventDefinitions{ + GenericDataWordNames: map[string]uint8{ + consts.EventAttributeSequenceNumber: 5, + }, + }, + }, + }, + }, + consts.ContractNamePriceRegistry: { + ContractABI: price_registry.PriceRegistryABI, + Configs: map[string]*evmrelaytypes.ChainReaderDefinition{ + // TODO: update with the consts from https://github.com/smartcontractkit/chainlink-ccip/pull/39 + // in a followup. + "GetStaticConfig": { + ChainSpecificName: mustGetMethodName("getStaticConfig", priceRegistryABI), + ReadType: evmrelaytypes.Method, + }, + "GetDestChainConfig": { + ChainSpecificName: mustGetMethodName("getDestChainConfig", priceRegistryABI), + ReadType: evmrelaytypes.Method, + }, + "GetPremiumMultiplierWeiPerEth": { + ChainSpecificName: mustGetMethodName("getPremiumMultiplierWeiPerEth", priceRegistryABI), + ReadType: evmrelaytypes.Method, + }, + "GetTokenTransferFeeConfig": { + ChainSpecificName: mustGetMethodName("getTokenTransferFeeConfig", priceRegistryABI), + ReadType: evmrelaytypes.Method, + }, + "ProcessMessageArgs": { + ChainSpecificName: mustGetMethodName("processMessageArgs", priceRegistryABI), + ReadType: evmrelaytypes.Method, + }, + "ValidatePoolReturnData": { + ChainSpecificName: mustGetMethodName("validatePoolReturnData", priceRegistryABI), + ReadType: evmrelaytypes.Method, + }, + "GetValidatedTokenPrice": { + ChainSpecificName: mustGetMethodName("getValidatedTokenPrice", priceRegistryABI), + ReadType: evmrelaytypes.Method, + }, + "GetFeeTokens": { + ChainSpecificName: mustGetMethodName("getFeeTokens", priceRegistryABI), + ReadType: evmrelaytypes.Method, + }, + }, + }, + }, + } +} + +// HomeChainReaderConfigRaw returns a ChainReaderConfig that can be used to read from the home chain. +func HomeChainReaderConfigRaw() evmrelaytypes.ChainReaderConfig { + return evmrelaytypes.ChainReaderConfig{ + Contracts: map[string]evmrelaytypes.ChainContractReader{ + consts.ContractNameCapabilitiesRegistry: { + ContractABI: kcr.CapabilitiesRegistryABI, + Configs: map[string]*evmrelaytypes.ChainReaderDefinition{ + consts.MethodNameGetCapability: { + ChainSpecificName: mustGetMethodName("getCapability", capabilitiesRegsitryABI), + }, + }, + }, + consts.ContractNameCCIPConfig: { + ContractABI: ccip_config.CCIPConfigABI, + Configs: map[string]*evmrelaytypes.ChainReaderDefinition{ + consts.MethodNameGetAllChainConfigs: { + ChainSpecificName: mustGetMethodName("getAllChainConfigs", ccipConfigABI), + }, + consts.MethodNameGetOCRConfig: { + ChainSpecificName: mustGetMethodName("getOCRConfig", ccipConfigABI), + }, + }, + }, + }, + } +} + +func mustGetEventName(event string, tabi abi.ABI) string { + e, ok := tabi.Events[event] + if !ok { + panic(fmt.Sprintf("missing event %s in onrampABI", event)) + } + return e.Name +} diff --git a/core/capabilities/ccip/delegate.go b/core/capabilities/ccip/delegate.go new file mode 100644 index 00000000000..c9974d62e99 --- /dev/null +++ b/core/capabilities/ccip/delegate.go @@ -0,0 +1,321 @@ +package ccip + +import ( + "context" + "fmt" + "time" + + "github.com/smartcontractkit/chainlink-common/pkg/loop" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/common" + configsevm "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/configs/evm" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/launcher" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/oraclecreator" + "github.com/smartcontractkit/chainlink/v2/core/services/ocrcommon" + p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" + "github.com/smartcontractkit/chainlink/v2/core/services/registrysyncer" + + ragep2ptypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "github.com/smartcontractkit/chainlink-ccip/pkg/consts" + ccipreaderpkg "github.com/smartcontractkit/chainlink-ccip/pkg/reader" + "github.com/smartcontractkit/chainlink-common/pkg/sqlutil" + "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink-common/pkg/types/query/primitives" + + "github.com/smartcontractkit/chainlink/v2/core/chains/legacyevm" + "github.com/smartcontractkit/chainlink/v2/core/config" + kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/job" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ocr2key" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2" + "github.com/smartcontractkit/chainlink/v2/core/services/pipeline" + "github.com/smartcontractkit/chainlink/v2/core/services/relay" + "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm" + "github.com/smartcontractkit/chainlink/v2/core/services/telemetry" + "github.com/smartcontractkit/chainlink/v2/plugins" +) + +type RelayGetter interface { + Get(types.RelayID) (loop.Relayer, error) + GetIDToRelayerMap() (map[types.RelayID]loop.Relayer, error) +} + +type Delegate struct { + lggr logger.Logger + registrarConfig plugins.RegistrarConfig + pipelineRunner pipeline.Runner + chains legacyevm.LegacyChainContainer + relayers RelayGetter + keystore keystore.Master + ds sqlutil.DataSource + peerWrapper *ocrcommon.SingletonPeerWrapper + monitoringEndpointGen telemetry.MonitoringEndpointGenerator + capabilityConfig config.Capabilities + + isNewlyCreatedJob bool +} + +func NewDelegate( + lggr logger.Logger, + registrarConfig plugins.RegistrarConfig, + pipelineRunner pipeline.Runner, + chains legacyevm.LegacyChainContainer, + relayers RelayGetter, + keystore keystore.Master, + ds sqlutil.DataSource, + peerWrapper *ocrcommon.SingletonPeerWrapper, + monitoringEndpointGen telemetry.MonitoringEndpointGenerator, + capabilityConfig config.Capabilities, +) *Delegate { + return &Delegate{ + lggr: lggr, + registrarConfig: registrarConfig, + pipelineRunner: pipelineRunner, + chains: chains, + relayers: relayers, + ds: ds, + keystore: keystore, + peerWrapper: peerWrapper, + monitoringEndpointGen: monitoringEndpointGen, + capabilityConfig: capabilityConfig, + } +} + +func (d *Delegate) JobType() job.Type { + return job.CCIP +} + +func (d *Delegate) BeforeJobCreated(job.Job) { + // This is only called first time the job is created + d.isNewlyCreatedJob = true +} + +func (d *Delegate) ServicesForSpec(ctx context.Context, spec job.Job) (services []job.ServiceCtx, err error) { + // In general there should only be one P2P key but the node may have multiple. + // The job spec should specify the correct P2P key to use. + peerID, err := p2pkey.MakePeerID(spec.CCIPSpec.P2PKeyID) + if err != nil { + return nil, fmt.Errorf("failed to make peer ID from provided spec p2p id (%s): %w", spec.CCIPSpec.P2PKeyID, err) + } + + p2pID, err := d.keystore.P2P().Get(peerID) + if err != nil { + return nil, fmt.Errorf("failed to get all p2p keys: %w", err) + } + + cfg := d.capabilityConfig + rid := cfg.ExternalRegistry().RelayID() + relayer, err := d.relayers.Get(rid) + if err != nil { + return nil, fmt.Errorf("could not fetch relayer %s configured for capabilities registry: %w", rid, err) + } + registrySyncer, err := registrysyncer.New( + d.lggr, + func() (p2ptypes.PeerID, error) { + return p2ptypes.PeerID(p2pID.PeerID()), nil + }, + relayer, + cfg.ExternalRegistry().Address(), + ) + if err != nil { + return nil, fmt.Errorf("could not configure syncer: %w", err) + } + + ocrKeys, err := d.getOCRKeys(spec.CCIPSpec.OCRKeyBundleIDs) + if err != nil { + return nil, err + } + + transmitterKeys, err := d.getTransmitterKeys(ctx, d.chains) + if err != nil { + return nil, err + } + + bootstrapperLocators, err := ocrcommon.ParseBootstrapPeers(spec.CCIPSpec.P2PV2Bootstrappers) + if err != nil { + return nil, fmt.Errorf("failed to parse bootstrapper locators: %w", err) + } + + // NOTE: we can use the same DB for all plugin instances, + // since all queries are scoped by config digest. + ocrDB := ocr2.NewDB(d.ds, spec.ID, 0, d.lggr) + + homeChainContractReader, err := d.getHomeChainContractReader( + ctx, + d.chains, + spec.CCIPSpec.CapabilityLabelledName, + spec.CCIPSpec.CapabilityVersion) + if err != nil { + return nil, fmt.Errorf("failed to get home chain contract reader: %w", err) + } + + hcr := ccipreaderpkg.NewHomeChainReader( + homeChainContractReader, + d.lggr.Named("HomeChainReader"), + 100*time.Millisecond, + ) + + oracleCreator := oraclecreator.New( + ocrKeys, + transmitterKeys, + d.chains, + d.peerWrapper, + spec.ExternalJobID, + spec.ID, + d.isNewlyCreatedJob, + spec.CCIPSpec.PluginConfig, + ocrDB, + d.lggr, + d.monitoringEndpointGen, + bootstrapperLocators, + hcr, + ) + + capabilityID := fmt.Sprintf("%s@%s", spec.CCIPSpec.CapabilityLabelledName, spec.CCIPSpec.CapabilityVersion) + capLauncher := launcher.New( + capabilityID, + ragep2ptypes.PeerID(p2pID.PeerID()), + d.lggr, + hcr, + oracleCreator, + 12*time.Second, + ) + + // register the capability launcher with the registry syncer + registrySyncer.AddLauncher(capLauncher) + + return []job.ServiceCtx{ + registrySyncer, + hcr, + capLauncher, + }, nil +} + +func (d *Delegate) AfterJobCreated(spec job.Job) {} + +func (d *Delegate) BeforeJobDeleted(spec job.Job) {} + +func (d *Delegate) OnDeleteJob(ctx context.Context, spec job.Job) error { + // TODO: shut down needed services? + return nil +} + +func (d *Delegate) getOCRKeys(ocrKeyBundleIDs job.JSONConfig) (map[string]ocr2key.KeyBundle, error) { + ocrKeys := make(map[string]ocr2key.KeyBundle) + for networkType, bundleIDRaw := range ocrKeyBundleIDs { + if networkType != relay.NetworkEVM { + return nil, fmt.Errorf("unsupported chain type: %s", networkType) + } + + bundleID, ok := bundleIDRaw.(string) + if !ok { + return nil, fmt.Errorf("OCRKeyBundleIDs must be a map of chain types to OCR key bundle IDs, got: %T", bundleIDRaw) + } + + bundle, err2 := d.keystore.OCR2().Get(bundleID) + if err2 != nil { + return nil, fmt.Errorf("OCR key bundle with ID %s not found: %w", bundleID, err2) + } + + ocrKeys[networkType] = bundle + } + return ocrKeys, nil +} + +func (d *Delegate) getTransmitterKeys(ctx context.Context, chains legacyevm.LegacyChainContainer) (map[types.RelayID][]string, error) { + transmitterKeys := make(map[types.RelayID][]string) + for _, chain := range chains.Slice() { + relayID := types.NewRelayID(relay.NetworkEVM, chain.ID().String()) + ethKeys, err2 := d.keystore.Eth().EnabledAddressesForChain(ctx, chain.ID()) + if err2 != nil { + return nil, fmt.Errorf("error getting enabled addresses for chain: %s %w", chain.ID().String(), err2) + } + + transmitterKeys[relayID] = func() (r []string) { + for _, key := range ethKeys { + r = append(r, key.Hex()) + } + return + }() + } + return transmitterKeys, nil +} + +func (d *Delegate) getHomeChainContractReader( + ctx context.Context, + chains legacyevm.LegacyChainContainer, + capabilityLabelledName, + capabilityVersion string, +) (types.ContractReader, error) { + // home chain is where the capability registry is deployed, + // which should be set correctly in toml config. + homeChainRelayID := d.capabilityConfig.ExternalRegistry().RelayID() + homeChain, err := chains.Get(homeChainRelayID.ChainID) + if err != nil { + return nil, fmt.Errorf("home chain relayer not found, chain id: %s, err: %w", homeChainRelayID.String(), err) + } + + reader, err := evm.NewChainReaderService( + context.Background(), + d.lggr, + homeChain.LogPoller(), + homeChain.HeadTracker(), + homeChain.Client(), + configsevm.HomeChainReaderConfigRaw(), + ) + if err != nil { + return nil, fmt.Errorf("failed to create home chain contract reader: %w", err) + } + + reader, err = bindReader(ctx, reader, d.capabilityConfig.ExternalRegistry().Address(), capabilityLabelledName, capabilityVersion) + if err != nil { + return nil, fmt.Errorf("failed to bind home chain contract reader: %w", err) + } + + return reader, nil +} + +func bindReader(ctx context.Context, + reader types.ContractReader, + capRegAddress, + capabilityLabelledName, + capabilityVersion string) (types.ContractReader, error) { + err := reader.Bind(ctx, []types.BoundContract{ + { + Address: capRegAddress, + Name: consts.ContractNameCapabilitiesRegistry, + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to bind home chain contract reader: %w", err) + } + + hid, err := common.HashedCapabilityID(capabilityLabelledName, capabilityVersion) + if err != nil { + return nil, fmt.Errorf("failed to hash capability id: %w", err) + } + + var ccipCapabilityInfo kcr.CapabilitiesRegistryCapabilityInfo + err = reader.GetLatestValue(ctx, consts.ContractNameCapabilitiesRegistry, consts.MethodNameGetCapability, primitives.Unconfirmed, map[string]any{ + "hashedId": hid, + }, &ccipCapabilityInfo) + if err != nil { + return nil, fmt.Errorf("failed to get CCIP capability info from chain reader: %w", err) + } + + // bind the ccip capability configuration contract + err = reader.Bind(ctx, []types.BoundContract{ + { + Address: ccipCapabilityInfo.ConfigurationContract.String(), + Name: consts.ContractNameCCIPConfig, + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to bind CCIP capability configuration contract: %w", err) + } + + return reader, nil +} diff --git a/core/capabilities/ccip/delegate_test.go b/core/capabilities/ccip/delegate_test.go new file mode 100644 index 00000000000..dd8a5124b57 --- /dev/null +++ b/core/capabilities/ccip/delegate_test.go @@ -0,0 +1 @@ +package ccip diff --git a/core/capabilities/ccip/launcher/README.md b/core/capabilities/ccip/launcher/README.md new file mode 100644 index 00000000000..41fbecfdbd8 --- /dev/null +++ b/core/capabilities/ccip/launcher/README.md @@ -0,0 +1,69 @@ +# CCIP Capability Launcher + +The CCIP capability launcher is responsible for listening to +[Capabilities Registry](../../../../contracts/src/v0.8/keystone/CapabilitiesRegistry.sol) (CR) updates +for the particular CCIP capability (labelled name, version) pair and reacting to them. In +particular, there are three kinds of events that would affect a particular capability: + +1. DON Creation: when `addDON` is called on the CR, the capabilities of this new DON are specified. +If CCIP is one of those capabilities, the launcher will launch a commit and an execution plugin +with the OCR configuration specified in the DON creation process. See +[Types.sol](../../../../contracts/src/v0.8/ccip/capability/libraries/Types.sol) for more details +on what the OCR configuration contains. +2. DON update: when `updateDON` is called on the CR, capabilities of the DON can be updated. In the +CCIP use case specifically, `updateDON` is used to update OCR configuration of that DON. Updates +follow the blue/green deployment pattern (explained in detail below with a state diagram). In this +scenario the launcher must either launch brand new instances of the commit and execution plugins +(in the event a green deployment is made) or promote the currently running green instance to be +the blue instance. +3. DON deletion: when `deleteDON` is called on the CR, the launcher must shut down all running plugins +related to that DON. When a DON is deleted it effectively means that it should no longer function. +DON deletion is permanent. + +## Architecture Diagram + +![CCIP Capability Launcher](ccip_capability_launcher.png) + +The above diagram shows how the CCIP capability launcher interacts with the rest of the components +in the CCIP system. + +The CCIP capability job, which is created on the Chainlink node, will spin up the CCIP capability +launcher alongside the home chain reader, which reads the [CCIPConfig.sol](../../../../contracts/src/v0.8/ccip/capability/CCIPConfig.sol) +contract deployed on the home chain (typically Ethereum Mainnet, though could be "any chain" in theory). + +Injected into the launcher is the [OracleCreator](../types/types.go) object which knows how to spin up CCIP +oracles (both bootstrap and plugin oracles). This is used by the launcher at the appropriate time in order +to create oracle instances but not start them right away. + +After all the required oracles have been created, the launcher will start and shut them down as required +in order to match the configuration that was posted on-chain in the CR and the CCIPConfig.sol contract. + + +## Config State Diagram + +![CCIP Config State Machine](ccip_config_state_machine.png) + +CCIP's blue/green deployment paradigm is intentionally kept as simple as possible. + +Every CCIP DON starts in the `Init` state. Upon DON creation, which must provide a valid OCR +configuration, the CCIP DON will move into the `Running` state. In this state, the DON is +presumed to be fully functional from a configuration standpoint. + +When we want to update configuration, we propose a new configuration to the CR that consists of +an array of two OCR configurations: + +1. The first element of the array is the current OCR configuration that is running (termed "blue"). +2. The second element of the array is the future OCR configuration that we want to run (termed "green"). + +Various checks are done on-chain in order to validate this particular state transition, in particular, +related to config counts. Doing this will move the state of the configuration to the `Staging` state. + +In the `Staging` state, there are effectively four plugins running - one (commit, execution) pair for the +blue configuration, and one (commit, execution) pair for the green configuration. However, only the blue +configuration will actually be writing on-chain, where as the green configuration will be "dry running", +i.e doing everything except transmitting. + +This allows us to test out new configurations without committing to them immediately. + +Finally, from the `Staging` state, there is only one transition, which is to promote the green configuration +to be the new blue configuration, and go back into the `Running` state. diff --git a/core/capabilities/ccip/launcher/bluegreen.go b/core/capabilities/ccip/launcher/bluegreen.go new file mode 100644 index 00000000000..62458466291 --- /dev/null +++ b/core/capabilities/ccip/launcher/bluegreen.go @@ -0,0 +1,178 @@ +package launcher + +import ( + "fmt" + + cctypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" + + "go.uber.org/multierr" + + ccipreaderpkg "github.com/smartcontractkit/chainlink-ccip/pkg/reader" +) + +// blueGreenDeployment represents a blue-green deployment of OCR instances. +type blueGreenDeployment struct { + // blue is the blue OCR instance. + // blue must always be present. + blue cctypes.CCIPOracle + + // bootstrapBlue is the bootstrap node of the blue OCR instance. + // Only a subset of the DON will be running bootstrap instances, + // so this may be nil. + bootstrapBlue cctypes.CCIPOracle + + // green is the green OCR instance. + // green may or may not be present. + // green must never be present if blue is not present. + // TODO: should we enforce this invariant somehow? + green cctypes.CCIPOracle + + // bootstrapGreen is the bootstrap node of the green OCR instance. + // Only a subset of the DON will be running bootstrap instances, + // so this may be nil, even when green is not nil. + bootstrapGreen cctypes.CCIPOracle +} + +// ccipDeployment represents blue-green deployments of both commit and exec +// OCR instances. +type ccipDeployment struct { + commit blueGreenDeployment + exec blueGreenDeployment +} + +// Close shuts down all OCR instances in the deployment. +func (c *ccipDeployment) Close() error { + var err error + + // shutdown blue commit instances. + err = multierr.Append(err, c.commit.blue.Close()) + if c.commit.bootstrapBlue != nil { + err = multierr.Append(err, c.commit.bootstrapBlue.Close()) + } + + // shutdown green commit instances. + if c.commit.green != nil { + err = multierr.Append(err, c.commit.green.Close()) + } + if c.commit.bootstrapGreen != nil { + err = multierr.Append(err, c.commit.bootstrapGreen.Close()) + } + + // shutdown blue exec instances. + err = multierr.Append(err, c.exec.blue.Close()) + if c.exec.bootstrapBlue != nil { + err = multierr.Append(err, c.exec.bootstrapBlue.Close()) + } + + // shutdown green exec instances. + if c.exec.green != nil { + err = multierr.Append(err, c.exec.green.Close()) + } + if c.exec.bootstrapGreen != nil { + err = multierr.Append(err, c.exec.bootstrapGreen.Close()) + } + + return err +} + +// StartBlue starts the blue OCR instances. +func (c *ccipDeployment) StartBlue() error { + var err error + + err = multierr.Append(err, c.commit.blue.Start()) + if c.commit.bootstrapBlue != nil { + err = multierr.Append(err, c.commit.bootstrapBlue.Start()) + } + err = multierr.Append(err, c.exec.blue.Start()) + if c.exec.bootstrapBlue != nil { + err = multierr.Append(err, c.exec.bootstrapBlue.Start()) + } + + return err +} + +// CloseBlue shuts down the blue OCR instances. +func (c *ccipDeployment) CloseBlue() error { + var err error + + err = multierr.Append(err, c.commit.blue.Close()) + if c.commit.bootstrapBlue != nil { + err = multierr.Append(err, c.commit.bootstrapBlue.Close()) + } + err = multierr.Append(err, c.exec.blue.Close()) + if c.exec.bootstrapBlue != nil { + err = multierr.Append(err, c.exec.bootstrapBlue.Close()) + } + + return err +} + +// HandleBlueGreen handles the blue-green deployment transition. +// prevDeployment is the previous deployment state. +// there are two possible cases: +// +// 1. both blue and green are present in prevDeployment, but only blue is present in c. +// this is a promotion of green to blue, so we need to shut down the blue deployment +// and make green the new blue. In this case green is already running, so there's no +// need to start it. However, we need to shut down the blue deployment. +// +// 2. only blue is present in prevDeployment, both blue and green are present in c. +// In this case, blue is already running, so there's no need to start it. We need to +// start green. +func (c *ccipDeployment) HandleBlueGreen(prevDeployment *ccipDeployment) error { + if prevDeployment == nil { + return fmt.Errorf("previous deployment is nil") + } + + var err error + if prevDeployment.commit.green != nil && c.commit.green == nil { + err = multierr.Append(err, prevDeployment.commit.blue.Close()) + if prevDeployment.commit.bootstrapBlue != nil { + err = multierr.Append(err, prevDeployment.commit.bootstrapBlue.Close()) + } + } else if prevDeployment.commit.green == nil && c.commit.green != nil { + err = multierr.Append(err, c.commit.green.Start()) + if c.commit.bootstrapGreen != nil { + err = multierr.Append(err, c.commit.bootstrapGreen.Start()) + } + } else { + return fmt.Errorf("invalid blue-green deployment transition") + } + + if prevDeployment.exec.green != nil && c.exec.green == nil { + err = multierr.Append(err, prevDeployment.exec.blue.Close()) + if prevDeployment.exec.bootstrapBlue != nil { + err = multierr.Append(err, prevDeployment.exec.bootstrapBlue.Close()) + } + } else if prevDeployment.exec.green == nil && c.exec.green != nil { + err = multierr.Append(err, c.exec.green.Start()) + if c.exec.bootstrapGreen != nil { + err = multierr.Append(err, c.exec.bootstrapGreen.Start()) + } + } else { + return fmt.Errorf("invalid blue-green deployment transition") + } + + return err +} + +// HasGreenInstance returns true if the deployment has a green instance for the +// given plugin type. +func (c *ccipDeployment) HasGreenInstance(pluginType cctypes.PluginType) bool { + switch pluginType { + case cctypes.PluginTypeCCIPCommit: + return c.commit.green != nil + case cctypes.PluginTypeCCIPExec: + return c.exec.green != nil + default: + return false + } +} + +func isNewGreenInstance(pluginType cctypes.PluginType, ocrConfigs []ccipreaderpkg.OCR3ConfigWithMeta, prevDeployment ccipDeployment) bool { + return len(ocrConfigs) == 2 && !prevDeployment.HasGreenInstance(pluginType) +} + +func isPromotion(pluginType cctypes.PluginType, ocrConfigs []ccipreaderpkg.OCR3ConfigWithMeta, prevDeployment ccipDeployment) bool { + return len(ocrConfigs) == 1 && prevDeployment.HasGreenInstance(pluginType) +} diff --git a/core/capabilities/ccip/launcher/bluegreen_test.go b/core/capabilities/ccip/launcher/bluegreen_test.go new file mode 100644 index 00000000000..9fd71a0cb44 --- /dev/null +++ b/core/capabilities/ccip/launcher/bluegreen_test.go @@ -0,0 +1,1043 @@ +package launcher + +import ( + "errors" + "testing" + + cctypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" + mocktypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types/mocks" + + "github.com/stretchr/testify/require" + + ccipreaderpkg "github.com/smartcontractkit/chainlink-ccip/pkg/reader" +) + +func Test_ccipDeployment_Close(t *testing.T) { + type args struct { + commitBlue *mocktypes.CCIPOracle + commitBlueBootstrap *mocktypes.CCIPOracle + commitGreen *mocktypes.CCIPOracle + commitGreenBootstrap *mocktypes.CCIPOracle + execBlue *mocktypes.CCIPOracle + execBlueBootstrap *mocktypes.CCIPOracle + execGreen *mocktypes.CCIPOracle + execGreenBootstrap *mocktypes.CCIPOracle + } + tests := []struct { + name string + args args + expect func(t *testing.T, args args) + asserts func(t *testing.T, args args) + wantErr bool + }{ + { + name: "no errors, blue only", + args: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitGreen: nil, + commitGreenBootstrap: nil, + execBlue: mocktypes.NewCCIPOracle(t), + execGreen: nil, + execGreenBootstrap: nil, + }, + expect: func(t *testing.T, args args) { + args.commitBlue.On("Close").Return(nil).Once() + args.execBlue.On("Close").Return(nil).Once() + }, + asserts: func(t *testing.T, args args) { + args.commitBlue.AssertExpectations(t) + args.execBlue.AssertExpectations(t) + }, + wantErr: false, + }, + { + name: "no errors, blue and green", + args: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitGreen: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + execGreen: mocktypes.NewCCIPOracle(t), + }, + expect: func(t *testing.T, args args) { + args.commitBlue.On("Close").Return(nil).Once() + args.commitGreen.On("Close").Return(nil).Once() + args.execBlue.On("Close").Return(nil).Once() + args.execGreen.On("Close").Return(nil).Once() + }, + asserts: func(t *testing.T, args args) { + args.commitBlue.AssertExpectations(t) + args.commitGreen.AssertExpectations(t) + args.execBlue.AssertExpectations(t) + args.execGreen.AssertExpectations(t) + }, + wantErr: false, + }, + { + name: "error on commit blue", + args: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitGreen: nil, + execBlue: mocktypes.NewCCIPOracle(t), + execGreen: nil, + }, + expect: func(t *testing.T, args args) { + args.commitBlue.On("Close").Return(errors.New("failed")).Once() + args.execBlue.On("Close").Return(nil).Once() + }, + asserts: func(t *testing.T, args args) { + args.commitBlue.AssertExpectations(t) + args.execBlue.AssertExpectations(t) + }, + wantErr: true, + }, + { + name: "bootstrap blue also closed", + args: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + execBlueBootstrap: mocktypes.NewCCIPOracle(t), + }, + expect: func(t *testing.T, args args) { + args.commitBlue.On("Close").Return(nil).Once() + args.commitBlueBootstrap.On("Close").Return(nil).Once() + args.execBlue.On("Close").Return(nil).Once() + args.execBlueBootstrap.On("Close").Return(nil).Once() + }, + asserts: func(t *testing.T, args args) { + args.commitBlue.AssertExpectations(t) + args.commitBlueBootstrap.AssertExpectations(t) + args.execBlue.AssertExpectations(t) + args.execBlueBootstrap.AssertExpectations(t) + }, + wantErr: false, + }, + { + name: "bootstrap green also closed", + args: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: mocktypes.NewCCIPOracle(t), + commitGreen: mocktypes.NewCCIPOracle(t), + commitGreenBootstrap: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + execBlueBootstrap: mocktypes.NewCCIPOracle(t), + execGreen: mocktypes.NewCCIPOracle(t), + execGreenBootstrap: mocktypes.NewCCIPOracle(t), + }, + expect: func(t *testing.T, args args) { + args.commitBlue.On("Close").Return(nil).Once() + args.commitBlueBootstrap.On("Close").Return(nil).Once() + args.commitGreen.On("Close").Return(nil).Once() + args.commitGreenBootstrap.On("Close").Return(nil).Once() + args.execBlue.On("Close").Return(nil).Once() + args.execBlueBootstrap.On("Close").Return(nil).Once() + args.execGreen.On("Close").Return(nil).Once() + args.execGreenBootstrap.On("Close").Return(nil).Once() + }, + asserts: func(t *testing.T, args args) { + args.commitBlue.AssertExpectations(t) + args.commitBlueBootstrap.AssertExpectations(t) + args.commitGreen.AssertExpectations(t) + args.commitGreenBootstrap.AssertExpectations(t) + args.execBlue.AssertExpectations(t) + args.execBlueBootstrap.AssertExpectations(t) + args.execGreen.AssertExpectations(t) + args.execGreenBootstrap.AssertExpectations(t) + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &ccipDeployment{ + commit: blueGreenDeployment{ + blue: tt.args.commitBlue, + }, + exec: blueGreenDeployment{ + blue: tt.args.execBlue, + }, + } + if tt.args.commitGreen != nil { + c.commit.green = tt.args.commitGreen + } + if tt.args.commitBlueBootstrap != nil { + c.commit.bootstrapBlue = tt.args.commitBlueBootstrap + } + if tt.args.commitGreenBootstrap != nil { + c.commit.bootstrapGreen = tt.args.commitGreenBootstrap + } + + if tt.args.execGreen != nil { + c.exec.green = tt.args.execGreen + } + if tt.args.execBlueBootstrap != nil { + c.exec.bootstrapBlue = tt.args.execBlueBootstrap + } + if tt.args.execGreenBootstrap != nil { + c.exec.bootstrapGreen = tt.args.execGreenBootstrap + } + + tt.expect(t, tt.args) + defer tt.asserts(t, tt.args) + err := c.Close() + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func Test_ccipDeployment_StartBlue(t *testing.T) { + type args struct { + commitBlue *mocktypes.CCIPOracle + commitBlueBootstrap *mocktypes.CCIPOracle + execBlue *mocktypes.CCIPOracle + execBlueBootstrap *mocktypes.CCIPOracle + } + tests := []struct { + name string + args args + expect func(t *testing.T, args args) + asserts func(t *testing.T, args args) + wantErr bool + }{ + { + name: "no errors, no bootstrap", + args: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: nil, + execBlueBootstrap: nil, + }, + expect: func(t *testing.T, args args) { + args.commitBlue.On("Start").Return(nil).Once() + args.execBlue.On("Start").Return(nil).Once() + }, + asserts: func(t *testing.T, args args) { + args.commitBlue.AssertExpectations(t) + args.execBlue.AssertExpectations(t) + }, + wantErr: false, + }, + { + name: "no errors, with bootstrap", + args: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: mocktypes.NewCCIPOracle(t), + execBlueBootstrap: mocktypes.NewCCIPOracle(t), + }, + expect: func(t *testing.T, args args) { + args.commitBlue.On("Start").Return(nil).Once() + args.commitBlueBootstrap.On("Start").Return(nil).Once() + args.execBlue.On("Start").Return(nil).Once() + args.execBlueBootstrap.On("Start").Return(nil).Once() + }, + asserts: func(t *testing.T, args args) { + args.commitBlue.AssertExpectations(t) + args.commitBlueBootstrap.AssertExpectations(t) + args.execBlue.AssertExpectations(t) + args.execBlueBootstrap.AssertExpectations(t) + }, + wantErr: false, + }, + { + name: "error on commit blue", + args: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: nil, + execBlueBootstrap: nil, + }, + expect: func(t *testing.T, args args) { + args.commitBlue.On("Start").Return(errors.New("failed")).Once() + args.execBlue.On("Start").Return(nil).Once() + }, + asserts: func(t *testing.T, args args) { + args.commitBlue.AssertExpectations(t) + args.execBlue.AssertExpectations(t) + }, + wantErr: true, + }, + { + name: "error on exec blue", + args: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: nil, + execBlueBootstrap: nil, + }, + expect: func(t *testing.T, args args) { + args.commitBlue.On("Start").Return(nil).Once() + args.execBlue.On("Start").Return(errors.New("failed")).Once() + }, + asserts: func(t *testing.T, args args) { + args.commitBlue.AssertExpectations(t) + args.execBlue.AssertExpectations(t) + }, + wantErr: true, + }, + { + name: "error on commit blue bootstrap", + args: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: mocktypes.NewCCIPOracle(t), + execBlueBootstrap: nil, + }, + expect: func(t *testing.T, args args) { + args.commitBlue.On("Start").Return(nil).Once() + args.commitBlueBootstrap.On("Start").Return(errors.New("failed")).Once() + args.execBlue.On("Start").Return(nil).Once() + }, + asserts: func(t *testing.T, args args) { + args.commitBlue.AssertExpectations(t) + args.commitBlueBootstrap.AssertExpectations(t) + args.execBlue.AssertExpectations(t) + }, + wantErr: true, + }, + { + name: "error on exec blue bootstrap", + args: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: nil, + execBlueBootstrap: mocktypes.NewCCIPOracle(t), + }, + expect: func(t *testing.T, args args) { + args.commitBlue.On("Start").Return(nil).Once() + args.execBlue.On("Start").Return(nil).Once() + args.execBlueBootstrap.On("Start").Return(errors.New("failed")).Once() + }, + asserts: func(t *testing.T, args args) { + args.commitBlue.AssertExpectations(t) + args.execBlue.AssertExpectations(t) + args.execBlueBootstrap.AssertExpectations(t) + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &ccipDeployment{ + commit: blueGreenDeployment{ + blue: tt.args.commitBlue, + }, + exec: blueGreenDeployment{ + blue: tt.args.execBlue, + }, + } + if tt.args.commitBlueBootstrap != nil { + c.commit.bootstrapBlue = tt.args.commitBlueBootstrap + } + if tt.args.execBlueBootstrap != nil { + c.exec.bootstrapBlue = tt.args.execBlueBootstrap + } + + tt.expect(t, tt.args) + defer tt.asserts(t, tt.args) + err := c.StartBlue() + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func Test_ccipDeployment_CloseBlue(t *testing.T) { + type args struct { + commitBlue *mocktypes.CCIPOracle + commitBlueBootstrap *mocktypes.CCIPOracle + execBlue *mocktypes.CCIPOracle + execBlueBootstrap *mocktypes.CCIPOracle + } + tests := []struct { + name string + args args + expect func(t *testing.T, args args) + asserts func(t *testing.T, args args) + wantErr bool + }{ + { + name: "no errors, no bootstrap", + args: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: nil, + execBlueBootstrap: nil, + }, + expect: func(t *testing.T, args args) { + args.commitBlue.On("Close").Return(nil).Once() + args.execBlue.On("Close").Return(nil).Once() + }, + asserts: func(t *testing.T, args args) { + args.commitBlue.AssertExpectations(t) + args.execBlue.AssertExpectations(t) + }, + wantErr: false, + }, + { + name: "no errors, with bootstrap", + args: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: mocktypes.NewCCIPOracle(t), + execBlueBootstrap: mocktypes.NewCCIPOracle(t), + }, + expect: func(t *testing.T, args args) { + args.commitBlue.On("Close").Return(nil).Once() + args.commitBlueBootstrap.On("Close").Return(nil).Once() + args.execBlue.On("Close").Return(nil).Once() + args.execBlueBootstrap.On("Close").Return(nil).Once() + }, + asserts: func(t *testing.T, args args) { + args.commitBlue.AssertExpectations(t) + args.commitBlueBootstrap.AssertExpectations(t) + args.execBlue.AssertExpectations(t) + args.execBlueBootstrap.AssertExpectations(t) + }, + wantErr: false, + }, + { + name: "error on commit blue", + args: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: nil, + execBlueBootstrap: nil, + }, + expect: func(t *testing.T, args args) { + args.commitBlue.On("Close").Return(errors.New("failed")).Once() + args.execBlue.On("Close").Return(nil).Once() + }, + asserts: func(t *testing.T, args args) { + args.commitBlue.AssertExpectations(t) + args.execBlue.AssertExpectations(t) + }, + wantErr: true, + }, + { + name: "error on exec blue", + args: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: nil, + execBlueBootstrap: nil, + }, + expect: func(t *testing.T, args args) { + args.commitBlue.On("Close").Return(nil).Once() + args.execBlue.On("Close").Return(errors.New("failed")).Once() + }, + asserts: func(t *testing.T, args args) { + args.commitBlue.AssertExpectations(t) + args.execBlue.AssertExpectations(t) + }, + wantErr: true, + }, + { + name: "error on commit blue bootstrap", + args: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: mocktypes.NewCCIPOracle(t), + execBlueBootstrap: nil, + }, + expect: func(t *testing.T, args args) { + args.commitBlue.On("Close").Return(nil).Once() + args.commitBlueBootstrap.On("Close").Return(errors.New("failed")).Once() + args.execBlue.On("Close").Return(nil).Once() + }, + asserts: func(t *testing.T, args args) { + args.commitBlue.AssertExpectations(t) + args.commitBlueBootstrap.AssertExpectations(t) + args.execBlue.AssertExpectations(t) + }, + wantErr: true, + }, + { + name: "error on exec blue bootstrap", + args: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: nil, + execBlueBootstrap: mocktypes.NewCCIPOracle(t), + }, + expect: func(t *testing.T, args args) { + args.commitBlue.On("Close").Return(nil).Once() + args.execBlue.On("Close").Return(nil).Once() + args.execBlueBootstrap.On("Close").Return(errors.New("failed")).Once() + }, + asserts: func(t *testing.T, args args) { + args.commitBlue.AssertExpectations(t) + args.execBlue.AssertExpectations(t) + args.execBlueBootstrap.AssertExpectations(t) + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &ccipDeployment{ + commit: blueGreenDeployment{ + blue: tt.args.commitBlue, + }, + exec: blueGreenDeployment{ + blue: tt.args.execBlue, + }, + } + if tt.args.commitBlueBootstrap != nil { + c.commit.bootstrapBlue = tt.args.commitBlueBootstrap + } + if tt.args.execBlueBootstrap != nil { + c.exec.bootstrapBlue = tt.args.execBlueBootstrap + } + + tt.expect(t, tt.args) + defer tt.asserts(t, tt.args) + err := c.CloseBlue() + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func Test_ccipDeployment_HandleBlueGreen_PrevDeploymentNil(t *testing.T) { + require.Error(t, (&ccipDeployment{}).HandleBlueGreen(nil)) +} + +func Test_ccipDeployment_HandleBlueGreen(t *testing.T) { + type args struct { + commitBlue *mocktypes.CCIPOracle + commitBlueBootstrap *mocktypes.CCIPOracle + commitGreen *mocktypes.CCIPOracle + commitGreenBootstrap *mocktypes.CCIPOracle + execBlue *mocktypes.CCIPOracle + execBlueBootstrap *mocktypes.CCIPOracle + execGreen *mocktypes.CCIPOracle + execGreenBootstrap *mocktypes.CCIPOracle + } + tests := []struct { + name string + argsPrevDeployment args + argsFutureDeployment args + expect func(t *testing.T, args args, argsPrevDeployment args) + asserts func(t *testing.T, args args, argsPrevDeployment args) + wantErr bool + }{ + { + name: "promotion blue to green, no bootstrap", + argsPrevDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitGreen: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + execGreen: mocktypes.NewCCIPOracle(t), + }, + argsFutureDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitGreen: nil, + execBlue: mocktypes.NewCCIPOracle(t), + execGreen: nil, + }, + expect: func(t *testing.T, args args, argsPrevDeployment args) { + argsPrevDeployment.commitBlue.On("Close").Return(nil).Once() + argsPrevDeployment.execBlue.On("Close").Return(nil).Once() + }, + asserts: func(t *testing.T, args args, argsPrevDeployment args) { + argsPrevDeployment.commitBlue.AssertExpectations(t) + argsPrevDeployment.execBlue.AssertExpectations(t) + }, + wantErr: false, + }, + { + name: "promotion blue to green, with bootstrap", + argsPrevDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: mocktypes.NewCCIPOracle(t), + commitGreen: mocktypes.NewCCIPOracle(t), + commitGreenBootstrap: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + execBlueBootstrap: mocktypes.NewCCIPOracle(t), + execGreen: mocktypes.NewCCIPOracle(t), + execGreenBootstrap: mocktypes.NewCCIPOracle(t), + }, + argsFutureDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: mocktypes.NewCCIPOracle(t), + commitGreen: nil, + commitGreenBootstrap: nil, + execBlue: mocktypes.NewCCIPOracle(t), + execBlueBootstrap: mocktypes.NewCCIPOracle(t), + execGreen: nil, + execGreenBootstrap: nil, + }, + expect: func(t *testing.T, args args, argsPrevDeployment args) { + argsPrevDeployment.commitBlue.On("Close").Return(nil).Once() + argsPrevDeployment.commitBlueBootstrap.On("Close").Return(nil).Once() + argsPrevDeployment.execBlue.On("Close").Return(nil).Once() + argsPrevDeployment.execBlueBootstrap.On("Close").Return(nil).Once() + }, + asserts: func(t *testing.T, args args, argsPrevDeployment args) { + argsPrevDeployment.commitBlue.AssertExpectations(t) + argsPrevDeployment.commitBlueBootstrap.AssertExpectations(t) + argsPrevDeployment.execBlue.AssertExpectations(t) + argsPrevDeployment.execBlueBootstrap.AssertExpectations(t) + }, + wantErr: false, + }, + { + name: "new green deployment, no bootstrap", + argsPrevDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitGreen: nil, + execBlue: mocktypes.NewCCIPOracle(t), + execGreen: nil, + }, + argsFutureDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitGreen: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + execGreen: mocktypes.NewCCIPOracle(t), + }, + expect: func(t *testing.T, args args, argsPrevDeployment args) { + args.commitGreen.On("Start").Return(nil).Once() + args.execGreen.On("Start").Return(nil).Once() + }, + asserts: func(t *testing.T, args args, argsPrevDeployment args) { + args.commitGreen.AssertExpectations(t) + args.execGreen.AssertExpectations(t) + }, + wantErr: false, + }, + { + name: "new green deployment, with bootstrap", + argsPrevDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: mocktypes.NewCCIPOracle(t), + commitGreen: nil, + commitGreenBootstrap: nil, + execBlue: mocktypes.NewCCIPOracle(t), + execBlueBootstrap: mocktypes.NewCCIPOracle(t), + execGreen: nil, + execGreenBootstrap: nil, + }, + argsFutureDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: mocktypes.NewCCIPOracle(t), + commitGreen: mocktypes.NewCCIPOracle(t), + commitGreenBootstrap: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + execBlueBootstrap: mocktypes.NewCCIPOracle(t), + execGreen: mocktypes.NewCCIPOracle(t), + execGreenBootstrap: mocktypes.NewCCIPOracle(t), + }, + expect: func(t *testing.T, args args, argsPrevDeployment args) { + args.commitGreen.On("Start").Return(nil).Once() + args.commitGreenBootstrap.On("Start").Return(nil).Once() + args.execGreen.On("Start").Return(nil).Once() + args.execGreenBootstrap.On("Start").Return(nil).Once() + }, + asserts: func(t *testing.T, args args, argsPrevDeployment args) { + args.commitGreen.AssertExpectations(t) + args.commitGreenBootstrap.AssertExpectations(t) + args.execGreen.AssertExpectations(t) + args.execGreenBootstrap.AssertExpectations(t) + }, + wantErr: false, + }, + { + name: "error on commit green start", + argsPrevDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitGreen: nil, + execBlue: mocktypes.NewCCIPOracle(t), + execGreen: nil, + }, + argsFutureDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitGreen: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + execGreen: mocktypes.NewCCIPOracle(t), + }, + expect: func(t *testing.T, args args, argsPrevDeployment args) { + args.commitGreen.On("Start").Return(errors.New("failed")).Once() + args.execGreen.On("Start").Return(nil).Once() + }, + asserts: func(t *testing.T, args args, argsPrevDeployment args) { + args.commitGreen.AssertExpectations(t) + args.execGreen.AssertExpectations(t) + }, + wantErr: true, + }, + { + name: "error on exec green start", + argsPrevDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitGreen: nil, + execBlue: mocktypes.NewCCIPOracle(t), + execGreen: nil, + }, + argsFutureDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitGreen: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + execGreen: mocktypes.NewCCIPOracle(t), + }, + expect: func(t *testing.T, args args, argsPrevDeployment args) { + args.commitGreen.On("Start").Return(nil).Once() + args.execGreen.On("Start").Return(errors.New("failed")).Once() + }, + asserts: func(t *testing.T, args args, argsPrevDeployment args) { + args.commitGreen.AssertExpectations(t) + args.execGreen.AssertExpectations(t) + }, + wantErr: true, + }, + { + name: "error on commit green bootstrap start", + argsPrevDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: mocktypes.NewCCIPOracle(t), + commitGreen: nil, + commitGreenBootstrap: nil, + execBlue: mocktypes.NewCCIPOracle(t), + execBlueBootstrap: mocktypes.NewCCIPOracle(t), + execGreen: nil, + execGreenBootstrap: nil, + }, + argsFutureDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: mocktypes.NewCCIPOracle(t), + commitGreen: mocktypes.NewCCIPOracle(t), + commitGreenBootstrap: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + execBlueBootstrap: mocktypes.NewCCIPOracle(t), + execGreen: mocktypes.NewCCIPOracle(t), + execGreenBootstrap: mocktypes.NewCCIPOracle(t), + }, + expect: func(t *testing.T, args args, argsPrevDeployment args) { + args.commitGreen.On("Start").Return(nil).Once() + args.commitGreenBootstrap.On("Start").Return(errors.New("failed")).Once() + args.execGreen.On("Start").Return(nil).Once() + args.execGreenBootstrap.On("Start").Return(nil).Once() + }, + asserts: func(t *testing.T, args args, argsPrevDeployment args) { + args.commitGreen.AssertExpectations(t) + args.commitGreenBootstrap.AssertExpectations(t) + args.execGreen.AssertExpectations(t) + args.execGreenBootstrap.AssertExpectations(t) + }, + wantErr: true, + }, + { + name: "invalid blue-green deployment transition commit: both prev and future deployment have green", + argsPrevDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitGreen: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + execGreen: mocktypes.NewCCIPOracle(t), + }, + argsFutureDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitGreen: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + execGreen: mocktypes.NewCCIPOracle(t), + }, + expect: func(t *testing.T, args args, argsPrevDeployment args) {}, + asserts: func(t *testing.T, args args, argsPrevDeployment args) {}, + wantErr: true, + }, + { + name: "invalid blue-green deployment transition exec: both prev and future deployment have green", + argsPrevDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitGreen: nil, + execBlue: mocktypes.NewCCIPOracle(t), + execGreen: mocktypes.NewCCIPOracle(t), + }, + argsFutureDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitGreen: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + execGreen: mocktypes.NewCCIPOracle(t), + }, + expect: func(t *testing.T, args args, argsPrevDeployment args) { + args.commitGreen.On("Start").Return(nil).Once() + }, + asserts: func(t *testing.T, args args, argsPrevDeployment args) { + args.commitGreen.AssertExpectations(t) + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + futDeployment := &ccipDeployment{ + commit: blueGreenDeployment{ + blue: tt.argsFutureDeployment.commitBlue, + }, + exec: blueGreenDeployment{ + blue: tt.argsFutureDeployment.execBlue, + }, + } + if tt.argsFutureDeployment.commitGreen != nil { + futDeployment.commit.green = tt.argsFutureDeployment.commitGreen + } + if tt.argsFutureDeployment.commitBlueBootstrap != nil { + futDeployment.commit.bootstrapBlue = tt.argsFutureDeployment.commitBlueBootstrap + } + if tt.argsFutureDeployment.commitGreenBootstrap != nil { + futDeployment.commit.bootstrapGreen = tt.argsFutureDeployment.commitGreenBootstrap + } + if tt.argsFutureDeployment.execGreen != nil { + futDeployment.exec.green = tt.argsFutureDeployment.execGreen + } + if tt.argsFutureDeployment.execBlueBootstrap != nil { + futDeployment.exec.bootstrapBlue = tt.argsFutureDeployment.execBlueBootstrap + } + if tt.argsFutureDeployment.execGreenBootstrap != nil { + futDeployment.exec.bootstrapGreen = tt.argsFutureDeployment.execGreenBootstrap + } + + prevDeployment := &ccipDeployment{ + commit: blueGreenDeployment{ + blue: tt.argsPrevDeployment.commitBlue, + }, + exec: blueGreenDeployment{ + blue: tt.argsPrevDeployment.execBlue, + }, + } + if tt.argsPrevDeployment.commitGreen != nil { + prevDeployment.commit.green = tt.argsPrevDeployment.commitGreen + } + if tt.argsPrevDeployment.commitBlueBootstrap != nil { + prevDeployment.commit.bootstrapBlue = tt.argsPrevDeployment.commitBlueBootstrap + } + if tt.argsPrevDeployment.commitGreenBootstrap != nil { + prevDeployment.commit.bootstrapGreen = tt.argsPrevDeployment.commitGreenBootstrap + } + if tt.argsPrevDeployment.execGreen != nil { + prevDeployment.exec.green = tt.argsPrevDeployment.execGreen + } + if tt.argsPrevDeployment.execBlueBootstrap != nil { + prevDeployment.exec.bootstrapBlue = tt.argsPrevDeployment.execBlueBootstrap + } + if tt.argsPrevDeployment.execGreenBootstrap != nil { + prevDeployment.exec.bootstrapGreen = tt.argsPrevDeployment.execGreenBootstrap + } + + tt.expect(t, tt.argsFutureDeployment, tt.argsPrevDeployment) + defer tt.asserts(t, tt.argsFutureDeployment, tt.argsPrevDeployment) + err := futDeployment.HandleBlueGreen(prevDeployment) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func Test_isNewGreenInstance(t *testing.T) { + type args struct { + pluginType cctypes.PluginType + ocrConfigs []ccipreaderpkg.OCR3ConfigWithMeta + prevDeployment ccipDeployment + } + tests := []struct { + name string + args args + want bool + }{ + { + "prev deployment only blue", + args{ + pluginType: cctypes.PluginTypeCCIPCommit, + ocrConfigs: []ccipreaderpkg.OCR3ConfigWithMeta{ + {}, {}, + }, + prevDeployment: ccipDeployment{ + commit: blueGreenDeployment{ + blue: mocktypes.NewCCIPOracle(t), + }, + }, + }, + true, + }, + { + "green -> blue promotion", + args{ + pluginType: cctypes.PluginTypeCCIPCommit, + ocrConfigs: []ccipreaderpkg.OCR3ConfigWithMeta{ + {}, + }, + prevDeployment: ccipDeployment{ + commit: blueGreenDeployment{ + blue: mocktypes.NewCCIPOracle(t), + green: mocktypes.NewCCIPOracle(t), + }, + }, + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := isNewGreenInstance(tt.args.pluginType, tt.args.ocrConfigs, tt.args.prevDeployment) + require.Equal(t, tt.want, got) + }) + } +} + +func Test_isPromotion(t *testing.T) { + type args struct { + pluginType cctypes.PluginType + ocrConfigs []ccipreaderpkg.OCR3ConfigWithMeta + prevDeployment ccipDeployment + } + tests := []struct { + name string + args args + want bool + }{ + { + "prev deployment only blue", + args{ + pluginType: cctypes.PluginTypeCCIPCommit, + ocrConfigs: []ccipreaderpkg.OCR3ConfigWithMeta{ + {}, {}, + }, + prevDeployment: ccipDeployment{ + commit: blueGreenDeployment{ + blue: mocktypes.NewCCIPOracle(t), + }, + }, + }, + false, + }, + { + "green -> blue promotion", + args{ + pluginType: cctypes.PluginTypeCCIPCommit, + ocrConfigs: []ccipreaderpkg.OCR3ConfigWithMeta{ + {}, + }, + prevDeployment: ccipDeployment{ + commit: blueGreenDeployment{ + blue: mocktypes.NewCCIPOracle(t), + green: mocktypes.NewCCIPOracle(t), + }, + }, + }, + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isPromotion(tt.args.pluginType, tt.args.ocrConfigs, tt.args.prevDeployment); got != tt.want { + t.Errorf("isPromotion() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_ccipDeployment_HasGreenInstance(t *testing.T) { + type fields struct { + commit blueGreenDeployment + exec blueGreenDeployment + } + type args struct { + pluginType cctypes.PluginType + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + "commit green present", + fields{ + commit: blueGreenDeployment{ + blue: mocktypes.NewCCIPOracle(t), + green: mocktypes.NewCCIPOracle(t), + }, + }, + args{ + pluginType: cctypes.PluginTypeCCIPCommit, + }, + true, + }, + { + "commit green not present", + fields{ + commit: blueGreenDeployment{ + blue: mocktypes.NewCCIPOracle(t), + }, + }, + args{ + pluginType: cctypes.PluginTypeCCIPCommit, + }, + false, + }, + { + "exec green present", + fields{ + exec: blueGreenDeployment{ + blue: mocktypes.NewCCIPOracle(t), + green: mocktypes.NewCCIPOracle(t), + }, + }, + args{ + pluginType: cctypes.PluginTypeCCIPExec, + }, + true, + }, + { + "exec green not present", + fields{ + exec: blueGreenDeployment{ + blue: mocktypes.NewCCIPOracle(t), + }, + }, + args{ + pluginType: cctypes.PluginTypeCCIPExec, + }, + false, + }, + { + "invalid plugin type", + fields{}, + args{ + pluginType: cctypes.PluginType(100), + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &ccipDeployment{} + if tt.fields.commit.blue != nil { + c.commit.blue = tt.fields.commit.blue + } + if tt.fields.commit.green != nil { + c.commit.green = tt.fields.commit.green + } + if tt.fields.exec.blue != nil { + c.exec.blue = tt.fields.exec.blue + } + if tt.fields.exec.green != nil { + c.exec.green = tt.fields.exec.green + } + got := c.HasGreenInstance(tt.args.pluginType) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/core/capabilities/ccip/launcher/ccip_capability_launcher.png b/core/capabilities/ccip/launcher/ccip_capability_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..5e90d5ff7daa3643fde8185f49b4c83840b2c298 GIT binary patch literal 253433 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

}`U=fGbo%nS)w* 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%0DbN!YhqTG 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„JG$8DMFvpn~5IY)uJ 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

q)*yZZGJ|G0u*#`zvW(_ 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

esQpm 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

j#E!8a{;^ic**L3%PFs%{p91&%QJl)>04h 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 q^%lt6$0)pb>!L#|YvY`t>=p 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 literal 0 HcmV?d00001 diff --git a/core/capabilities/ccip/launcher/diff.go b/core/capabilities/ccip/launcher/diff.go new file mode 100644 index 00000000000..e631ea9fc78 --- /dev/null +++ b/core/capabilities/ccip/launcher/diff.go @@ -0,0 +1,141 @@ +package launcher + +import ( + "fmt" + + ragep2ptypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "github.com/smartcontractkit/chainlink/v2/core/services/registrysyncer" +) + +// diffResult contains the added, removed and updated CCIP DONs. +// It is determined by using the `diff` function below. +type diffResult struct { + added map[registrysyncer.DonID]registrysyncer.DON + removed map[registrysyncer.DonID]registrysyncer.DON + updated map[registrysyncer.DonID]registrysyncer.DON +} + +// diff compares the old and new state and returns the added, removed and updated CCIP DONs. +func diff( + capabilityID string, + oldState, + newState registrysyncer.LocalRegistry, +) (diffResult, error) { + ccipCapability, err := checkCapabilityPresence(capabilityID, newState) + if err != nil { + return diffResult{}, fmt.Errorf("failed to check capability presence: %w", err) + } + + newCCIPDONs, err := filterCCIPDONs(ccipCapability, newState) + if err != nil { + return diffResult{}, fmt.Errorf("failed to filter CCIP DONs from new state: %w", err) + } + + currCCIPDONs, err := filterCCIPDONs(ccipCapability, oldState) + if err != nil { + return diffResult{}, fmt.Errorf("failed to filter CCIP DONs from old state: %w", err) + } + + // compare curr with new and launch or update OCR instances as needed + diffRes, err := compareDONs(currCCIPDONs, newCCIPDONs) + if err != nil { + return diffResult{}, fmt.Errorf("failed to compare CCIP DONs: %w", err) + } + + return diffRes, nil +} + +// compareDONs compares the current and new CCIP DONs and returns the added, removed and updated DONs. +func compareDONs( + currCCIPDONs, + newCCIPDONs map[registrysyncer.DonID]registrysyncer.DON, +) ( + dr diffResult, + err error, +) { + added := make(map[registrysyncer.DonID]registrysyncer.DON) + removed := make(map[registrysyncer.DonID]registrysyncer.DON) + updated := make(map[registrysyncer.DonID]registrysyncer.DON) + + for id, don := range newCCIPDONs { + if currDONState, ok := currCCIPDONs[id]; !ok { + // Not in current state, so mark as added. + added[id] = don + } else { + // If its in the current state and the config count for the DON has changed, mark as updated. + // Since the registry returns the full state we need to compare the config count. + if don.ConfigVersion > currDONState.ConfigVersion { + updated[id] = don + } + } + } + + for id, don := range currCCIPDONs { + if _, ok := newCCIPDONs[id]; !ok { + // In current state but not in latest registry state, so should remove. + removed[id] = don + } + } + + return diffResult{ + added: added, + removed: removed, + updated: updated, + }, nil +} + +// filterCCIPDONs filters the CCIP DONs from the given state. +func filterCCIPDONs( + ccipCapability registrysyncer.Capability, + state registrysyncer.LocalRegistry, +) (map[registrysyncer.DonID]registrysyncer.DON, error) { + ccipDONs := make(map[registrysyncer.DonID]registrysyncer.DON) + for _, don := range state.IDsToDONs { + _, ok := don.CapabilityConfigurations[ccipCapability.ID] + if ok { + ccipDONs[registrysyncer.DonID(don.ID)] = don + } + } + + return ccipDONs, nil +} + +// checkCapabilityPresence checks if the capability with the given capabilityID +// is present in the given capability registry state. +func checkCapabilityPresence( + capabilityID string, + state registrysyncer.LocalRegistry, +) (registrysyncer.Capability, error) { + // Sanity check to make sure the capability registry has the capability we are looking for. + ccipCapability, ok := state.IDsToCapabilities[capabilityID] + if !ok { + return registrysyncer.Capability{}, + fmt.Errorf("failed to find capability with capabilityID %s in capability registry state", capabilityID) + } + + return ccipCapability, nil +} + +// isMemberOfDON returns true if and only if the given p2pID is a member of the given DON. +func isMemberOfDON(don registrysyncer.DON, p2pID ragep2ptypes.PeerID) bool { + for _, node := range don.Members { + if node == p2pID { + return true + } + } + return false +} + +// isMemberOfBootstrapSubcommittee returns true if and only if the given p2pID is a member of the given bootstrap subcommittee. +func isMemberOfBootstrapSubcommittee( + bootstrapP2PIDs [][32]byte, + p2pID ragep2ptypes.PeerID, +) bool { + for _, bootstrapID := range bootstrapP2PIDs { + if bootstrapID == p2pID { + return true + } + } + return false +} diff --git a/core/capabilities/ccip/launcher/diff_test.go b/core/capabilities/ccip/launcher/diff_test.go new file mode 100644 index 00000000000..f3dd327fe91 --- /dev/null +++ b/core/capabilities/ccip/launcher/diff_test.go @@ -0,0 +1,352 @@ +package launcher + +import ( + "math/big" + "reflect" + "testing" + + ragep2ptypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "github.com/stretchr/testify/require" + + kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" + "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" + "github.com/smartcontractkit/chainlink/v2/core/services/registrysyncer" +) + +func Test_diff(t *testing.T) { + type args struct { + capabilityID string + oldState registrysyncer.LocalRegistry + newState registrysyncer.LocalRegistry + } + tests := []struct { + name string + args args + want diffResult + wantErr bool + }{ + { + name: "no diff", + args: args{ + capabilityID: defaultCapability.ID, + oldState: registrysyncer.LocalRegistry{ + IDsToCapabilities: map[string]registrysyncer.Capability{ + defaultCapability.ID: defaultCapability, + }, + IDsToDONs: map[registrysyncer.DonID]registrysyncer.DON{ + 1: defaultRegistryDon, + }, + IDsToNodes: map[types.PeerID]kcr.CapabilitiesRegistryNodeInfo{}, + }, + newState: registrysyncer.LocalRegistry{ + IDsToCapabilities: map[string]registrysyncer.Capability{ + defaultCapability.ID: defaultCapability, + }, + IDsToDONs: map[registrysyncer.DonID]registrysyncer.DON{ + 1: defaultRegistryDon, + }, + IDsToNodes: map[types.PeerID]kcr.CapabilitiesRegistryNodeInfo{}, + }, + }, + want: diffResult{ + added: map[registrysyncer.DonID]registrysyncer.DON{}, + removed: map[registrysyncer.DonID]registrysyncer.DON{}, + updated: map[registrysyncer.DonID]registrysyncer.DON{}, + }, + }, + { + "capability not present", + args{ + capabilityID: defaultCapability.ID, + oldState: registrysyncer.LocalRegistry{ + IDsToCapabilities: map[string]registrysyncer.Capability{ + newCapability.ID: newCapability, + }, + }, + newState: registrysyncer.LocalRegistry{ + IDsToCapabilities: map[string]registrysyncer.Capability{ + newCapability.ID: newCapability, + }, + }, + }, + diffResult{}, + true, + }, + { + "diff present, new don", + args{ + capabilityID: defaultCapability.ID, + oldState: registrysyncer.LocalRegistry{ + IDsToCapabilities: map[string]registrysyncer.Capability{ + defaultCapability.ID: defaultCapability, + }, + IDsToDONs: map[registrysyncer.DonID]registrysyncer.DON{}, + }, + newState: registrysyncer.LocalRegistry{ + IDsToCapabilities: map[string]registrysyncer.Capability{ + defaultCapability.ID: defaultCapability, + }, + IDsToDONs: map[registrysyncer.DonID]registrysyncer.DON{ + 1: defaultRegistryDon, + }, + }, + }, + diffResult{ + added: map[registrysyncer.DonID]registrysyncer.DON{ + 1: defaultRegistryDon, + }, + removed: map[registrysyncer.DonID]registrysyncer.DON{}, + updated: map[registrysyncer.DonID]registrysyncer.DON{}, + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := diff(tt.args.capabilityID, tt.args.oldState, tt.args.newState) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.want, got) + } + }) + } +} + +func Test_compareDONs(t *testing.T) { + type args struct { + currCCIPDONs map[registrysyncer.DonID]registrysyncer.DON + newCCIPDONs map[registrysyncer.DonID]registrysyncer.DON + } + tests := []struct { + name string + args args + wantAdded map[registrysyncer.DonID]registrysyncer.DON + wantRemoved map[registrysyncer.DonID]registrysyncer.DON + wantUpdated map[registrysyncer.DonID]registrysyncer.DON + wantErr bool + }{ + { + "added dons", + args{ + currCCIPDONs: map[registrysyncer.DonID]registrysyncer.DON{}, + newCCIPDONs: map[registrysyncer.DonID]registrysyncer.DON{ + 1: defaultRegistryDon, + }, + }, + map[registrysyncer.DonID]registrysyncer.DON{ + 1: defaultRegistryDon, + }, + map[registrysyncer.DonID]registrysyncer.DON{}, + map[registrysyncer.DonID]registrysyncer.DON{}, + false, + }, + { + "removed dons", + args{ + currCCIPDONs: map[registrysyncer.DonID]registrysyncer.DON{ + 1: defaultRegistryDon, + }, + newCCIPDONs: map[registrysyncer.DonID]registrysyncer.DON{}, + }, + map[registrysyncer.DonID]registrysyncer.DON{}, + map[registrysyncer.DonID]registrysyncer.DON{ + 1: defaultRegistryDon, + }, + map[registrysyncer.DonID]registrysyncer.DON{}, + false, + }, + { + "updated dons", + args{ + currCCIPDONs: map[registrysyncer.DonID]registrysyncer.DON{ + 1: defaultRegistryDon, + }, + newCCIPDONs: map[registrysyncer.DonID]registrysyncer.DON{ + 1: { + DON: getDON(defaultRegistryDon.ID, defaultRegistryDon.Members, defaultRegistryDon.ConfigVersion+1), + CapabilityConfigurations: defaultCapCfgs, + }, + }, + }, + map[registrysyncer.DonID]registrysyncer.DON{}, + map[registrysyncer.DonID]registrysyncer.DON{}, + map[registrysyncer.DonID]registrysyncer.DON{ + 1: { + DON: getDON(defaultRegistryDon.ID, defaultRegistryDon.Members, defaultRegistryDon.ConfigVersion+1), + CapabilityConfigurations: defaultCapCfgs, + }, + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dr, err := compareDONs(tt.args.currCCIPDONs, tt.args.newCCIPDONs) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.wantAdded, dr.added) + require.Equal(t, tt.wantRemoved, dr.removed) + require.Equal(t, tt.wantUpdated, dr.updated) + } + }) + } +} + +func Test_filterCCIPDONs(t *testing.T) { + type args struct { + ccipCapability registrysyncer.Capability + state registrysyncer.LocalRegistry + } + tests := []struct { + name string + args args + want map[registrysyncer.DonID]registrysyncer.DON + wantErr bool + }{ + { + "one ccip don", + args{ + ccipCapability: defaultCapability, + state: registrysyncer.LocalRegistry{ + IDsToDONs: map[registrysyncer.DonID]registrysyncer.DON{ + 1: defaultRegistryDon, + }, + }, + }, + map[registrysyncer.DonID]registrysyncer.DON{ + 1: defaultRegistryDon, + }, + false, + }, + { + "no ccip dons - different capability", + args{ + ccipCapability: newCapability, + state: registrysyncer.LocalRegistry{ + IDsToDONs: map[registrysyncer.DonID]registrysyncer.DON{ + 1: defaultRegistryDon, + }, + }, + }, + map[registrysyncer.DonID]registrysyncer.DON{}, + false, + }, + { + "don with multiple capabilities, one of them ccip", + args{ + ccipCapability: defaultCapability, + state: registrysyncer.LocalRegistry{ + IDsToDONs: map[registrysyncer.DonID]registrysyncer.DON{ + 1: { + DON: getDON(1, []ragep2ptypes.PeerID{p2pID1}, 0), + CapabilityConfigurations: map[string]registrysyncer.CapabilityConfiguration{ + defaultCapability.ID: {}, + newCapability.ID: {}, + }, + }, + }, + }, + }, + map[registrysyncer.DonID]registrysyncer.DON{ + 1: { + DON: getDON(1, []ragep2ptypes.PeerID{p2pID1}, 0), + CapabilityConfigurations: map[string]registrysyncer.CapabilityConfiguration{ + defaultCapability.ID: {}, + newCapability.ID: {}, + }, + }, + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := filterCCIPDONs(tt.args.ccipCapability, tt.args.state) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.want, got) + } + }) + } +} + +func Test_checkCapabilityPresence(t *testing.T) { + type args struct { + capabilityID string + state registrysyncer.LocalRegistry + } + tests := []struct { + name string + args args + want registrysyncer.Capability + wantErr bool + }{ + { + "in registry state", + args{ + capabilityID: defaultCapability.ID, + state: registrysyncer.LocalRegistry{ + IDsToCapabilities: map[string]registrysyncer.Capability{ + defaultCapability.ID: defaultCapability, + }, + }, + }, + defaultCapability, + false, + }, + { + "not in registry state", + args{ + capabilityID: defaultCapability.ID, + state: registrysyncer.LocalRegistry{ + IDsToCapabilities: map[string]registrysyncer.Capability{ + newCapability.ID: newCapability, + }, + }, + }, + registrysyncer.Capability{}, + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := checkCapabilityPresence(tt.args.capabilityID, tt.args.state) + if (err != nil) != tt.wantErr { + t.Errorf("checkCapabilityPresence() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("checkCapabilityPresence() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_isMemberOfDON(t *testing.T) { + var p2pIDs []ragep2ptypes.PeerID + for i := range [4]struct{}{} { + p2pIDs = append(p2pIDs, ragep2ptypes.PeerID(p2pkey.MustNewV2XXXTestingOnly(big.NewInt(int64(i+1))).PeerID())) + } + don := registrysyncer.DON{ + DON: getDON(1, p2pIDs, 0), + } + require.True(t, isMemberOfDON(don, ragep2ptypes.PeerID(p2pkey.MustNewV2XXXTestingOnly(big.NewInt(1)).PeerID()))) + require.False(t, isMemberOfDON(don, ragep2ptypes.PeerID(p2pkey.MustNewV2XXXTestingOnly(big.NewInt(5)).PeerID()))) +} + +func Test_isMemberOfBootstrapSubcommittee(t *testing.T) { + var bootstrapKeys [][32]byte + for i := range [4]struct{}{} { + bootstrapKeys = append(bootstrapKeys, p2pkey.MustNewV2XXXTestingOnly(big.NewInt(int64(i+1))).PeerID()) + } + require.True(t, isMemberOfBootstrapSubcommittee(bootstrapKeys, p2pID1)) + require.False(t, isMemberOfBootstrapSubcommittee(bootstrapKeys, getP2PID(5))) +} diff --git a/core/capabilities/ccip/launcher/integration_test.go b/core/capabilities/ccip/launcher/integration_test.go new file mode 100644 index 00000000000..db3daf4d9b9 --- /dev/null +++ b/core/capabilities/ccip/launcher/integration_test.go @@ -0,0 +1,120 @@ +package launcher + +import ( + "testing" + "time" + + it "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/ccip_integration_tests/integrationhelpers" + cctypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" + p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" + + "github.com/onsi/gomega" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/ccip_config" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/registrysyncer" +) + +func TestIntegration_Launcher(t *testing.T) { + ctx := testutils.Context(t) + lggr := logger.TestLogger(t) + uni := it.NewTestUniverse(ctx, t, lggr) + // We need 3*f + 1 p2pIDs to have enough nodes to bootstrap + var arr []int64 + n := int(it.FChainA*3 + 1) + for i := 0; i <= n; i++ { + arr = append(arr, int64(i)) + } + p2pIDs := it.P2pIDsFromInts(arr) + uni.AddCapability(p2pIDs) + + regSyncer, err := registrysyncer.New(lggr, + func() (p2ptypes.PeerID, error) { + return p2pIDs[0], nil + }, + uni, + uni.CapReg.Address().String(), + ) + require.NoError(t, err) + + hcr := uni.HomeChainReader + + launcher := New( + it.CapabilityID, + p2pIDs[0], + logger.TestLogger(t), + hcr, + &oracleCreatorPrints{ + t: t, + }, + 1*time.Second, + ) + regSyncer.AddLauncher(launcher) + + require.NoError(t, launcher.Start(ctx)) + require.NoError(t, regSyncer.Start(ctx)) + t.Cleanup(func() { require.NoError(t, regSyncer.Close()) }) + t.Cleanup(func() { require.NoError(t, launcher.Close()) }) + + chainAConf := it.SetupConfigInfo(it.ChainA, p2pIDs, it.FChainA, []byte("ChainA")) + chainBConf := it.SetupConfigInfo(it.ChainB, p2pIDs[1:], it.FChainB, []byte("ChainB")) + chainCConf := it.SetupConfigInfo(it.ChainC, p2pIDs[2:], it.FChainC, []byte("ChainC")) + inputConfig := []ccip_config.CCIPConfigTypesChainConfigInfo{ + chainAConf, + chainBConf, + chainCConf, + } + _, err = uni.CcipCfg.ApplyChainConfigUpdates(uni.Transactor, nil, inputConfig) + require.NoError(t, err) + uni.Backend.Commit() + + ccipCapabilityID, err := uni.CapReg.GetHashedCapabilityId(nil, it.CcipCapabilityLabelledName, it.CcipCapabilityVersion) + require.NoError(t, err) + + uni.AddDONToRegistry( + ccipCapabilityID, + it.ChainA, + it.FChainA, + p2pIDs[1], + p2pIDs) + + gomega.NewWithT(t).Eventually(func() bool { + return len(launcher.runningDONIDs()) == 1 + }, testutils.WaitTimeout(t), testutils.TestInterval).Should(gomega.BeTrue()) +} + +type oraclePrints struct { + t *testing.T + pluginType cctypes.PluginType + config cctypes.OCR3ConfigWithMeta + isBootstrap bool +} + +func (o *oraclePrints) Start() error { + o.t.Logf("Starting oracle (pluginType: %s, isBootstrap: %t) with config %+v\n", o.pluginType, o.isBootstrap, o.config) + return nil +} + +func (o *oraclePrints) Close() error { + o.t.Logf("Closing oracle (pluginType: %s, isBootstrap: %t) with config %+v\n", o.pluginType, o.isBootstrap, o.config) + return nil +} + +type oracleCreatorPrints struct { + t *testing.T +} + +func (o *oracleCreatorPrints) CreatePluginOracle(pluginType cctypes.PluginType, config cctypes.OCR3ConfigWithMeta) (cctypes.CCIPOracle, error) { + o.t.Logf("Creating plugin oracle (pluginType: %s) with config %+v\n", pluginType, config) + return &oraclePrints{pluginType: pluginType, config: config, t: o.t}, nil +} + +func (o *oracleCreatorPrints) CreateBootstrapOracle(config cctypes.OCR3ConfigWithMeta) (cctypes.CCIPOracle, error) { + o.t.Logf("Creating bootstrap oracle with config %+v\n", config) + return &oraclePrints{pluginType: cctypes.PluginTypeCCIPCommit, config: config, isBootstrap: true, t: o.t}, nil +} + +var _ cctypes.OracleCreator = &oracleCreatorPrints{} +var _ cctypes.CCIPOracle = &oraclePrints{} diff --git a/core/capabilities/ccip/launcher/launcher.go b/core/capabilities/ccip/launcher/launcher.go new file mode 100644 index 00000000000..2dc1a1954f5 --- /dev/null +++ b/core/capabilities/ccip/launcher/launcher.go @@ -0,0 +1,432 @@ +package launcher + +import ( + "context" + "fmt" + "sync" + "time" + + cctypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/job" + p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" + "github.com/smartcontractkit/chainlink/v2/core/services/registrysyncer" + + "go.uber.org/multierr" + + ragep2ptypes "github.com/smartcontractkit/libocr/ragep2p/types" + + ccipreader "github.com/smartcontractkit/chainlink-ccip/pkg/reader" + + "github.com/smartcontractkit/chainlink-common/pkg/services" + + kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" +) + +var ( + _ job.ServiceCtx = (*launcher)(nil) + _ registrysyncer.Launcher = (*launcher)(nil) +) + +func New( + capabilityID string, + p2pID ragep2ptypes.PeerID, + lggr logger.Logger, + homeChainReader ccipreader.HomeChain, + oracleCreator cctypes.OracleCreator, + tickInterval time.Duration, +) *launcher { + return &launcher{ + p2pID: p2pID, + capabilityID: capabilityID, + lggr: lggr, + homeChainReader: homeChainReader, + regState: registrysyncer.LocalRegistry{ + IDsToDONs: make(map[registrysyncer.DonID]registrysyncer.DON), + IDsToNodes: make(map[p2ptypes.PeerID]kcr.CapabilitiesRegistryNodeInfo), + IDsToCapabilities: make(map[string]registrysyncer.Capability), + }, + oracleCreator: oracleCreator, + dons: make(map[registrysyncer.DonID]*ccipDeployment), + tickInterval: tickInterval, + } +} + +// launcher manages the lifecycles of the CCIP capability on all chains. +type launcher struct { + services.StateMachine + + capabilityID string + p2pID ragep2ptypes.PeerID + lggr logger.Logger + homeChainReader ccipreader.HomeChain + stopChan chan struct{} + // latestState is the latest capability registry state received from the syncer. + latestState registrysyncer.LocalRegistry + // regState is the latest capability registry state that we have successfully processed. + regState registrysyncer.LocalRegistry + oracleCreator cctypes.OracleCreator + lock sync.RWMutex + wg sync.WaitGroup + tickInterval time.Duration + + // dons is a map of CCIP DON IDs to the OCR instances that are running on them. + // we can have up to two OCR instances per CCIP plugin, since we are running two plugins, + // thats four OCR instances per CCIP DON maximum. + dons map[registrysyncer.DonID]*ccipDeployment +} + +// Launch implements registrysyncer.Launcher. +func (l *launcher) Launch(ctx context.Context, state *registrysyncer.LocalRegistry) error { + l.lock.Lock() + defer l.lock.Unlock() + l.lggr.Debugw("Received new state from syncer", "dons", state.IDsToDONs) + l.latestState = *state + return nil +} + +func (l *launcher) getLatestState() registrysyncer.LocalRegistry { + l.lock.RLock() + defer l.lock.RUnlock() + return l.latestState +} + +func (l *launcher) runningDONIDs() []registrysyncer.DonID { + l.lock.RLock() + defer l.lock.RUnlock() + var runningDONs []registrysyncer.DonID + for id := range l.dons { + runningDONs = append(runningDONs, id) + } + return runningDONs +} + +// Close implements job.ServiceCtx. +func (l *launcher) Close() error { + return l.StateMachine.StopOnce("launcher", func() error { + // shut down the monitor goroutine. + close(l.stopChan) + l.wg.Wait() + + // shut down all running oracles. + var err error + for _, ceDep := range l.dons { + err = multierr.Append(err, ceDep.Close()) + } + + return err + }) +} + +// Start implements job.ServiceCtx. +func (l *launcher) Start(context.Context) error { + return l.StartOnce("launcher", func() error { + l.stopChan = make(chan struct{}) + l.wg.Add(1) + go l.monitor() + return nil + }) +} + +func (l *launcher) monitor() { + defer l.wg.Done() + ticker := time.NewTicker(l.tickInterval) + for { + select { + case <-l.stopChan: + return + case <-ticker.C: + if err := l.tick(); err != nil { + l.lggr.Errorw("Failed to tick", "err", err) + } + } + } +} + +func (l *launcher) tick() error { + // Ensure that the home chain reader is healthy. + // For new jobs it may be possible that the home chain reader is not yet ready + // so we won't be able to fetch configs and start any OCR instances. + if ready := l.homeChainReader.Ready(); ready != nil { + return fmt.Errorf("home chain reader is not ready: %w", ready) + } + + // Fetch the latest state from the capability registry and determine if we need to + // launch or update any OCR instances. + latestState := l.getLatestState() + + diffRes, err := diff(l.capabilityID, l.regState, latestState) + if err != nil { + return fmt.Errorf("failed to diff capability registry states: %w", err) + } + + err = l.processDiff(diffRes) + if err != nil { + return fmt.Errorf("failed to process diff: %w", err) + } + + return nil +} + +// processDiff processes the diff between the current and latest capability registry states. +// for any added OCR instances, it will launch them. +// for any removed OCR instances, it will shut them down. +// for any updated OCR instances, it will restart them with the new configuration. +func (l *launcher) processDiff(diff diffResult) error { + err := l.processRemoved(diff.removed) + err = multierr.Append(err, l.processAdded(diff.added)) + err = multierr.Append(err, l.processUpdate(diff.updated)) + + return err +} + +func (l *launcher) processUpdate(updated map[registrysyncer.DonID]registrysyncer.DON) error { + l.lock.Lock() + defer l.lock.Unlock() + + for donID, don := range updated { + prevDeployment, ok := l.dons[registrysyncer.DonID(don.ID)] + if !ok { + return fmt.Errorf("invariant violation: expected to find CCIP DON %d in the map of running deployments", don.ID) + } + + futDeployment, err := updateDON( + l.lggr, + l.p2pID, + l.homeChainReader, + l.oracleCreator, + *prevDeployment, + don, + ) + if err != nil { + return err + } + if err := futDeployment.HandleBlueGreen(prevDeployment); err != nil { + // TODO: how to handle a failed blue-green deployment? + return fmt.Errorf("failed to handle blue-green deployment for CCIP DON %d: %w", donID, err) + } + + // update state. + l.dons[donID] = futDeployment + // update the state with the latest config. + // this way if one of the starts errors, we don't retry all of them. + l.regState.IDsToDONs[donID] = updated[donID] + } + + return nil +} + +func (l *launcher) processAdded(added map[registrysyncer.DonID]registrysyncer.DON) error { + l.lock.Lock() + defer l.lock.Unlock() + + for donID, don := range added { + dep, err := createDON( + l.lggr, + l.p2pID, + l.homeChainReader, + l.oracleCreator, + don, + ) + if err != nil { + return err + } + if dep == nil { + // not a member of this DON. + continue + } + + if err := dep.StartBlue(); err != nil { + if shutdownErr := dep.CloseBlue(); shutdownErr != nil { + l.lggr.Errorw("Failed to shutdown blue instance after failed start", "donId", donID, "err", shutdownErr) + } + return fmt.Errorf("failed to start oracles for CCIP DON %d: %w", donID, err) + } + + // update state. + l.dons[donID] = dep + // update the state with the latest config. + // this way if one of the starts errors, we don't retry all of them. + l.regState.IDsToDONs[donID] = added[donID] + } + + return nil +} + +func (l *launcher) processRemoved(removed map[registrysyncer.DonID]registrysyncer.DON) error { + l.lock.Lock() + defer l.lock.Unlock() + + for id := range removed { + ceDep, ok := l.dons[id] + if !ok { + // not running this particular DON. + continue + } + + if err := ceDep.Close(); err != nil { + return fmt.Errorf("failed to shutdown oracles for CCIP DON %d: %w", id, err) + } + + // after a successful shutdown we can safely remove the DON deployment from the map. + delete(l.dons, id) + delete(l.regState.IDsToDONs, id) + } + + return nil +} + +// updateDON is a pure function that handles the case where a DON in the capability registry +// has received a new configuration. +// It returns a new ccipDeployment that can then be used to perform the blue-green deployment, +// based on the previous deployment. +func updateDON( + lggr logger.Logger, + p2pID ragep2ptypes.PeerID, + homeChainReader ccipreader.HomeChain, + oracleCreator cctypes.OracleCreator, + prevDeployment ccipDeployment, + don registrysyncer.DON, +) (futDeployment *ccipDeployment, err error) { + if !isMemberOfDON(don, p2pID) { + lggr.Infow("Not a member of this DON, skipping", "donId", don.ID, "p2pId", p2pID.String()) + return nil, nil + } + + // this should be a retryable error. + commitOCRConfigs, err := homeChainReader.GetOCRConfigs(context.Background(), don.ID, uint8(cctypes.PluginTypeCCIPCommit)) + if err != nil { + return nil, fmt.Errorf("failed to fetch OCR configs for CCIP commit plugin (don id: %d) from home chain config contract: %w", + don.ID, err) + } + + execOCRConfigs, err := homeChainReader.GetOCRConfigs(context.Background(), don.ID, uint8(cctypes.PluginTypeCCIPExec)) + if err != nil { + return nil, fmt.Errorf("failed to fetch OCR configs for CCIP exec plugin (don id: %d) from home chain config contract: %w", + don.ID, err) + } + + commitBgd, err := createFutureBlueGreenDeployment(prevDeployment, commitOCRConfigs, oracleCreator, cctypes.PluginTypeCCIPCommit) + if err != nil { + return nil, fmt.Errorf("failed to create future blue-green deployment for CCIP commit plugin: %w, don id: %d", err, don.ID) + } + + execBgd, err := createFutureBlueGreenDeployment(prevDeployment, execOCRConfigs, oracleCreator, cctypes.PluginTypeCCIPExec) + if err != nil { + return nil, fmt.Errorf("failed to create future blue-green deployment for CCIP exec plugin: %w, don id: %d", err, don.ID) + } + + return &ccipDeployment{ + commit: commitBgd, + exec: execBgd, + }, nil +} + +// valid cases: +// a) len(ocrConfigs) == 2 && !prevDeployment.HasGreenInstance(pluginType): this is a new green instance. +// b) len(ocrConfigs) == 1 && prevDeployment.HasGreenInstance(): this is a promotion of green->blue. +// All other cases are invalid. This is enforced in the ccip config contract. +func createFutureBlueGreenDeployment( + prevDeployment ccipDeployment, + ocrConfigs []ccipreader.OCR3ConfigWithMeta, + oracleCreator cctypes.OracleCreator, + pluginType cctypes.PluginType, +) (blueGreenDeployment, error) { + var deployment blueGreenDeployment + if isNewGreenInstance(pluginType, ocrConfigs, prevDeployment) { + // this is a new green instance. + greenOracle, err := oracleCreator.CreatePluginOracle(pluginType, cctypes.OCR3ConfigWithMeta(ocrConfigs[1])) + if err != nil { + return blueGreenDeployment{}, fmt.Errorf("failed to create CCIP commit oracle: %w", err) + } + + deployment.blue = prevDeployment.commit.blue + deployment.green = greenOracle + } else if isPromotion(pluginType, ocrConfigs, prevDeployment) { + // this is a promotion of green->blue. + deployment.blue = prevDeployment.commit.green + } else { + return blueGreenDeployment{}, fmt.Errorf("invariant violation: expected 1 or 2 OCR configs for CCIP plugin (type: %d), got %d", pluginType, len(ocrConfigs)) + } + + return deployment, nil +} + +// createDON is a pure function that handles the case where a new DON is added to the capability registry. +// It returns a new ccipDeployment that can then be used to start the blue instance. +func createDON( + lggr logger.Logger, + p2pID ragep2ptypes.PeerID, + homeChainReader ccipreader.HomeChain, + oracleCreator cctypes.OracleCreator, + don registrysyncer.DON, +) (*ccipDeployment, error) { + if !isMemberOfDON(don, p2pID) { + lggr.Infow("Not a member of this DON, skipping", "donId", don.ID, "p2pId", p2pID.String()) + return nil, nil + } + + // this should be a retryable error. + commitOCRConfigs, err := homeChainReader.GetOCRConfigs(context.Background(), don.ID, uint8(cctypes.PluginTypeCCIPCommit)) + if err != nil { + return nil, fmt.Errorf("failed to fetch OCR configs for CCIP commit plugin (don id: %d) from home chain config contract: %w", + don.ID, err) + } + + execOCRConfigs, err := homeChainReader.GetOCRConfigs(context.Background(), don.ID, uint8(cctypes.PluginTypeCCIPExec)) + if err != nil { + return nil, fmt.Errorf("failed to fetch OCR configs for CCIP exec plugin (don id: %d) from home chain config contract: %w", + don.ID, err) + } + + // upon creation we should only have one OCR config per plugin type. + if len(commitOCRConfigs) != 1 { + return nil, fmt.Errorf("expected exactly one OCR config for CCIP commit plugin (don id: %d), got %d", don.ID, len(commitOCRConfigs)) + } + + if len(execOCRConfigs) != 1 { + return nil, fmt.Errorf("expected exactly one OCR config for CCIP exec plugin (don id: %d), got %d", don.ID, len(execOCRConfigs)) + } + + commitOracle, commitBootstrap, err := createOracle(p2pID, oracleCreator, cctypes.PluginTypeCCIPCommit, commitOCRConfigs) + if err != nil { + return nil, fmt.Errorf("failed to create CCIP commit oracle: %w", err) + } + + execOracle, execBootstrap, err := createOracle(p2pID, oracleCreator, cctypes.PluginTypeCCIPExec, execOCRConfigs) + if err != nil { + return nil, fmt.Errorf("failed to create CCIP exec oracle: %w", err) + } + + return &ccipDeployment{ + commit: blueGreenDeployment{ + blue: commitOracle, + bootstrapBlue: commitBootstrap, + }, + exec: blueGreenDeployment{ + blue: execOracle, + bootstrapBlue: execBootstrap, + }, + }, nil +} + +func createOracle( + p2pID ragep2ptypes.PeerID, + oracleCreator cctypes.OracleCreator, + pluginType cctypes.PluginType, + ocrConfigs []ccipreader.OCR3ConfigWithMeta, +) (pluginOracle, bootstrapOracle cctypes.CCIPOracle, err error) { + pluginOracle, err = oracleCreator.CreatePluginOracle(pluginType, cctypes.OCR3ConfigWithMeta(ocrConfigs[0])) + if err != nil { + return nil, nil, fmt.Errorf("failed to create CCIP plugin oracle (plugintype: %d): %w", pluginType, err) + } + + if isMemberOfBootstrapSubcommittee(ocrConfigs[0].Config.BootstrapP2PIds, p2pID) { + bootstrapOracle, err = oracleCreator.CreateBootstrapOracle(cctypes.OCR3ConfigWithMeta(ocrConfigs[0])) + if err != nil { + return nil, nil, fmt.Errorf("failed to create CCIP bootstrap oracle (plugintype: %d): %w", pluginType, err) + } + } + + return pluginOracle, bootstrapOracle, nil +} diff --git a/core/capabilities/ccip/launcher/launcher_test.go b/core/capabilities/ccip/launcher/launcher_test.go new file mode 100644 index 00000000000..242dd0be248 --- /dev/null +++ b/core/capabilities/ccip/launcher/launcher_test.go @@ -0,0 +1,472 @@ +package launcher + +import ( + "errors" + "math/big" + "reflect" + "testing" + + cctypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types/mocks" + + ragep2ptypes "github.com/smartcontractkit/libocr/ragep2p/types" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + ccipreaderpkg "github.com/smartcontractkit/chainlink-ccip/pkg/reader" + + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" + "github.com/smartcontractkit/chainlink/v2/core/services/registrysyncer" +) + +func Test_createOracle(t *testing.T) { + var p2pKeys []ragep2ptypes.PeerID + for i := 0; i < 3; i++ { + p2pKeys = append(p2pKeys, ragep2ptypes.PeerID(p2pkey.MustNewV2XXXTestingOnly(big.NewInt(int64(i+1))).PeerID())) + } + myP2PKey := p2pKeys[0] + type args struct { + p2pID ragep2ptypes.PeerID + oracleCreator *mocks.OracleCreator + pluginType cctypes.PluginType + ocrConfigs []ccipreaderpkg.OCR3ConfigWithMeta + } + tests := []struct { + name string + args args + expect func(t *testing.T, args args, oracleCreator *mocks.OracleCreator) + wantErr bool + }{ + { + "success, no bootstrap", + args{ + myP2PKey, + mocks.NewOracleCreator(t), + cctypes.PluginTypeCCIPCommit, + []ccipreaderpkg.OCR3ConfigWithMeta{ + { + Config: ccipreaderpkg.OCR3Config{}, + ConfigCount: 1, + ConfigDigest: testutils.Random32Byte(), + }, + }, + }, + func(t *testing.T, args args, oracleCreator *mocks.OracleCreator) { + oracleCreator. + On("CreatePluginOracle", cctypes.PluginTypeCCIPCommit, cctypes.OCR3ConfigWithMeta(args.ocrConfigs[0])). + Return(mocks.NewCCIPOracle(t), nil) + }, + false, + }, + { + "success, with bootstrap", + args{ + myP2PKey, + mocks.NewOracleCreator(t), + cctypes.PluginTypeCCIPCommit, + []ccipreaderpkg.OCR3ConfigWithMeta{ + { + Config: ccipreaderpkg.OCR3Config{ + BootstrapP2PIds: [][32]byte{myP2PKey}, + }, + ConfigCount: 1, + ConfigDigest: testutils.Random32Byte(), + }, + }, + }, + func(t *testing.T, args args, oracleCreator *mocks.OracleCreator) { + oracleCreator. + On("CreatePluginOracle", cctypes.PluginTypeCCIPCommit, cctypes.OCR3ConfigWithMeta(args.ocrConfigs[0])). + Return(mocks.NewCCIPOracle(t), nil) + oracleCreator. + On("CreateBootstrapOracle", cctypes.OCR3ConfigWithMeta(args.ocrConfigs[0])). + Return(mocks.NewCCIPOracle(t), nil) + }, + false, + }, + { + "error creating plugin oracle", + args{ + myP2PKey, + mocks.NewOracleCreator(t), + cctypes.PluginTypeCCIPCommit, + []ccipreaderpkg.OCR3ConfigWithMeta{ + { + Config: ccipreaderpkg.OCR3Config{}, + ConfigCount: 1, + ConfigDigest: testutils.Random32Byte(), + }, + }, + }, + func(t *testing.T, args args, oracleCreator *mocks.OracleCreator) { + oracleCreator. + On("CreatePluginOracle", cctypes.PluginTypeCCIPCommit, cctypes.OCR3ConfigWithMeta(args.ocrConfigs[0])). + Return(nil, errors.New("error creating oracle")) + }, + true, + }, + { + "error creating bootstrap oracle", + args{ + myP2PKey, + mocks.NewOracleCreator(t), + cctypes.PluginTypeCCIPCommit, + []ccipreaderpkg.OCR3ConfigWithMeta{ + { + Config: ccipreaderpkg.OCR3Config{ + BootstrapP2PIds: [][32]byte{myP2PKey}, + }, + ConfigCount: 1, + ConfigDigest: testutils.Random32Byte(), + }, + }, + }, + func(t *testing.T, args args, oracleCreator *mocks.OracleCreator) { + oracleCreator. + On("CreatePluginOracle", cctypes.PluginTypeCCIPCommit, cctypes.OCR3ConfigWithMeta(args.ocrConfigs[0])). + Return(mocks.NewCCIPOracle(t), nil) + oracleCreator. + On("CreateBootstrapOracle", cctypes.OCR3ConfigWithMeta(args.ocrConfigs[0])). + Return(nil, errors.New("error creating oracle")) + }, + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.expect(t, tt.args, tt.args.oracleCreator) + _, _, err := createOracle(tt.args.p2pID, tt.args.oracleCreator, tt.args.pluginType, tt.args.ocrConfigs) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func Test_createDON(t *testing.T) { + type args struct { + lggr logger.Logger + p2pID ragep2ptypes.PeerID + homeChainReader *mocks.HomeChainReader + oracleCreator *mocks.OracleCreator + don registrysyncer.DON + } + tests := []struct { + name string + args args + expect func(t *testing.T, args args, oracleCreator *mocks.OracleCreator, homeChainReader *mocks.HomeChainReader) + wantErr bool + }{ + { + "not a member of the DON", + args{ + logger.TestLogger(t), + p2pID1, + mocks.NewHomeChainReader(t), + mocks.NewOracleCreator(t), + registrysyncer.DON{ + DON: getDON(2, []ragep2ptypes.PeerID{p2pID2}, 0), + CapabilityConfigurations: defaultCapCfgs, + }, + }, + func(t *testing.T, args args, oracleCreator *mocks.OracleCreator, homeChainReader *mocks.HomeChainReader) { + }, + false, + }, + { + "success, no bootstrap", + args{ + logger.TestLogger(t), + p2pID1, + mocks.NewHomeChainReader(t), + mocks.NewOracleCreator(t), + defaultRegistryDon, + }, + func(t *testing.T, args args, oracleCreator *mocks.OracleCreator, homeChainReader *mocks.HomeChainReader) { + homeChainReader. + On("GetOCRConfigs", mock.Anything, uint32(1), uint8(cctypes.PluginTypeCCIPCommit)). + Return([]ccipreaderpkg.OCR3ConfigWithMeta{{}}, nil) + homeChainReader. + On("GetOCRConfigs", mock.Anything, uint32(1), uint8(cctypes.PluginTypeCCIPExec)). + Return([]ccipreaderpkg.OCR3ConfigWithMeta{{}}, nil) + oracleCreator. + On("CreatePluginOracle", cctypes.PluginTypeCCIPCommit, mock.Anything). + Return(mocks.NewCCIPOracle(t), nil) + oracleCreator. + On("CreatePluginOracle", cctypes.PluginTypeCCIPExec, mock.Anything). + Return(mocks.NewCCIPOracle(t), nil) + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.expect != nil { + tt.expect(t, tt.args, tt.args.oracleCreator, tt.args.homeChainReader) + } + _, err := createDON(tt.args.lggr, tt.args.p2pID, tt.args.homeChainReader, tt.args.oracleCreator, tt.args.don) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func Test_createFutureBlueGreenDeployment(t *testing.T) { + type args struct { + prevDeployment ccipDeployment + ocrConfigs []ccipreaderpkg.OCR3ConfigWithMeta + oracleCreator *mocks.OracleCreator + pluginType cctypes.PluginType + } + tests := []struct { + name string + args args + want blueGreenDeployment + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := createFutureBlueGreenDeployment(tt.args.prevDeployment, tt.args.ocrConfigs, tt.args.oracleCreator, tt.args.pluginType) + if (err != nil) != tt.wantErr { + t.Errorf("createFutureBlueGreenDeployment() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("createFutureBlueGreenDeployment() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_updateDON(t *testing.T) { + type args struct { + lggr logger.Logger + p2pID ragep2ptypes.PeerID + homeChainReader *mocks.HomeChainReader + oracleCreator *mocks.OracleCreator + prevDeployment ccipDeployment + don registrysyncer.DON + } + tests := []struct { + name string + args args + wantFutDeployment *ccipDeployment + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotFutDeployment, err := updateDON(tt.args.lggr, tt.args.p2pID, tt.args.homeChainReader, tt.args.oracleCreator, tt.args.prevDeployment, tt.args.don) + if (err != nil) != tt.wantErr { + t.Errorf("updateDON() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(gotFutDeployment, tt.wantFutDeployment) { + t.Errorf("updateDON() = %v, want %v", gotFutDeployment, tt.wantFutDeployment) + } + }) + } +} + +func Test_launcher_processDiff(t *testing.T) { + type fields struct { + lggr logger.Logger + p2pID ragep2ptypes.PeerID + homeChainReader *mocks.HomeChainReader + oracleCreator *mocks.OracleCreator + dons map[registrysyncer.DonID]*ccipDeployment + regState registrysyncer.LocalRegistry + } + type args struct { + diff diffResult + } + tests := []struct { + name string + fields fields + args args + assert func(t *testing.T, l *launcher) + wantErr bool + }{ + { + "don removed success", + fields{ + dons: map[registrysyncer.DonID]*ccipDeployment{ + 1: { + commit: blueGreenDeployment{ + blue: newMock(t, + func(t *testing.T) *mocks.CCIPOracle { return mocks.NewCCIPOracle(t) }, + func(m *mocks.CCIPOracle) { + m.On("Close").Return(nil) + }), + }, + exec: blueGreenDeployment{ + blue: newMock(t, + func(t *testing.T) *mocks.CCIPOracle { return mocks.NewCCIPOracle(t) }, + func(m *mocks.CCIPOracle) { + m.On("Close").Return(nil) + }), + }, + }, + }, + regState: registrysyncer.LocalRegistry{ + IDsToDONs: map[registrysyncer.DonID]registrysyncer.DON{ + 1: defaultRegistryDon, + }, + }, + }, + args{ + diff: diffResult{ + removed: map[registrysyncer.DonID]registrysyncer.DON{ + 1: defaultRegistryDon, + }, + }, + }, + func(t *testing.T, l *launcher) { + require.Len(t, l.dons, 0) + require.Len(t, l.regState.IDsToDONs, 0) + }, + false, + }, + { + "don added success", + fields{ + lggr: logger.TestLogger(t), + p2pID: p2pID1, + homeChainReader: newMock(t, func(t *testing.T) *mocks.HomeChainReader { + return mocks.NewHomeChainReader(t) + }, func(m *mocks.HomeChainReader) { + m.On("GetOCRConfigs", mock.Anything, uint32(1), uint8(cctypes.PluginTypeCCIPCommit)). + Return([]ccipreaderpkg.OCR3ConfigWithMeta{{}}, nil) + m.On("GetOCRConfigs", mock.Anything, uint32(1), uint8(cctypes.PluginTypeCCIPExec)). + Return([]ccipreaderpkg.OCR3ConfigWithMeta{{}}, nil) + }), + oracleCreator: newMock(t, func(t *testing.T) *mocks.OracleCreator { + return mocks.NewOracleCreator(t) + }, func(m *mocks.OracleCreator) { + commitOracle := mocks.NewCCIPOracle(t) + commitOracle.On("Start").Return(nil) + execOracle := mocks.NewCCIPOracle(t) + execOracle.On("Start").Return(nil) + m.On("CreatePluginOracle", cctypes.PluginTypeCCIPCommit, mock.Anything). + Return(commitOracle, nil) + m.On("CreatePluginOracle", cctypes.PluginTypeCCIPExec, mock.Anything). + Return(execOracle, nil) + }), + dons: map[registrysyncer.DonID]*ccipDeployment{}, + regState: registrysyncer.LocalRegistry{ + IDsToDONs: map[registrysyncer.DonID]registrysyncer.DON{}, + }, + }, + args{ + diff: diffResult{ + added: map[registrysyncer.DonID]registrysyncer.DON{ + 1: defaultRegistryDon, + }, + }, + }, + func(t *testing.T, l *launcher) { + require.Len(t, l.dons, 1) + require.Len(t, l.regState.IDsToDONs, 1) + }, + false, + }, + { + "don updated new green instance success", + fields{ + lggr: logger.TestLogger(t), + p2pID: p2pID1, + homeChainReader: newMock(t, func(t *testing.T) *mocks.HomeChainReader { + return mocks.NewHomeChainReader(t) + }, func(m *mocks.HomeChainReader) { + m.On("GetOCRConfigs", mock.Anything, uint32(1), uint8(cctypes.PluginTypeCCIPCommit)). + Return([]ccipreaderpkg.OCR3ConfigWithMeta{{}, {}}, nil) + m.On("GetOCRConfigs", mock.Anything, uint32(1), uint8(cctypes.PluginTypeCCIPExec)). + Return([]ccipreaderpkg.OCR3ConfigWithMeta{{}, {}}, nil) + }), + oracleCreator: newMock(t, func(t *testing.T) *mocks.OracleCreator { + return mocks.NewOracleCreator(t) + }, func(m *mocks.OracleCreator) { + commitOracle := mocks.NewCCIPOracle(t) + commitOracle.On("Start").Return(nil) + execOracle := mocks.NewCCIPOracle(t) + execOracle.On("Start").Return(nil) + m.On("CreatePluginOracle", cctypes.PluginTypeCCIPCommit, mock.Anything). + Return(commitOracle, nil) + m.On("CreatePluginOracle", cctypes.PluginTypeCCIPExec, mock.Anything). + Return(execOracle, nil) + }), + dons: map[registrysyncer.DonID]*ccipDeployment{ + 1: { + commit: blueGreenDeployment{ + blue: newMock(t, func(t *testing.T) *mocks.CCIPOracle { + return mocks.NewCCIPOracle(t) + }, func(m *mocks.CCIPOracle) {}), + }, + exec: blueGreenDeployment{ + blue: newMock(t, func(t *testing.T) *mocks.CCIPOracle { + return mocks.NewCCIPOracle(t) + }, func(m *mocks.CCIPOracle) {}), + }, + }, + }, + regState: registrysyncer.LocalRegistry{ + IDsToDONs: map[registrysyncer.DonID]registrysyncer.DON{ + 1: defaultRegistryDon, + }, + }, + }, + args{ + diff: diffResult{ + updated: map[registrysyncer.DonID]registrysyncer.DON{ + 1: { + // new Node in Don: p2pID2 + DON: getDON(1, []ragep2ptypes.PeerID{p2pID1, p2pID2}, 0), + CapabilityConfigurations: defaultCapCfgs, + }, + }, + }, + }, + func(t *testing.T, l *launcher) { + require.Len(t, l.dons, 1) + require.Len(t, l.regState.IDsToDONs, 1) + require.Len(t, l.regState.IDsToDONs[1].Members, 2) + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := &launcher{ + dons: tt.fields.dons, + regState: tt.fields.regState, + p2pID: tt.fields.p2pID, + lggr: tt.fields.lggr, + homeChainReader: tt.fields.homeChainReader, + oracleCreator: tt.fields.oracleCreator, + } + err := l.processDiff(tt.args.diff) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + tt.assert(t, l) + }) + } +} + +func newMock[T any](t *testing.T, newer func(t *testing.T) T, expect func(m T)) T { + o := newer(t) + expect(o) + return o +} diff --git a/core/capabilities/ccip/launcher/test_helpers.go b/core/capabilities/ccip/launcher/test_helpers.go new file mode 100644 index 00000000000..a2ebf3fdba9 --- /dev/null +++ b/core/capabilities/ccip/launcher/test_helpers.go @@ -0,0 +1,56 @@ +package launcher + +import ( + "fmt" + "math/big" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" + "github.com/smartcontractkit/chainlink/v2/core/services/registrysyncer" + + ragep2ptypes "github.com/smartcontractkit/libocr/ragep2p/types" +) + +const ( + ccipCapVersion = "v1.0.0" + ccipCapNewVersion = "v1.1.0" + ccipCapName = "ccip" +) + +var ( + defaultCapability = getCapability(ccipCapName, ccipCapVersion) + newCapability = getCapability(ccipCapName, ccipCapNewVersion) + p2pID1 = getP2PID(1) + p2pID2 = getP2PID(2) + defaultCapCfgs = map[string]registrysyncer.CapabilityConfiguration{ + defaultCapability.ID: registrysyncer.CapabilityConfiguration{}, + } + defaultRegistryDon = registrysyncer.DON{ + DON: getDON(1, []ragep2ptypes.PeerID{p2pID1}, 0), + CapabilityConfigurations: defaultCapCfgs, + } +) + +func getP2PID(id uint32) ragep2ptypes.PeerID { + return ragep2ptypes.PeerID(p2pkey.MustNewV2XXXTestingOnly(big.NewInt(int64(id))).PeerID()) +} + +func getCapability(ccipCapName, ccipCapVersion string) registrysyncer.Capability { + id := fmt.Sprintf("%s@%s", ccipCapName, ccipCapVersion) + return registrysyncer.Capability{ + CapabilityType: capabilities.CapabilityTypeTarget, + ID: id, + } +} + +func getDON(id uint32, members []ragep2ptypes.PeerID, cfgVersion uint32) capabilities.DON { + return capabilities.DON{ + ID: id, + ConfigVersion: cfgVersion, + F: uint8(1), + IsPublic: true, + AcceptsWorkflows: true, + Members: members, + } +} diff --git a/core/capabilities/ccip/ocrimpls/config_digester.go b/core/capabilities/ccip/ocrimpls/config_digester.go new file mode 100644 index 00000000000..ef0c5e7ca32 --- /dev/null +++ b/core/capabilities/ccip/ocrimpls/config_digester.go @@ -0,0 +1,23 @@ +package ocrimpls + +import "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + +type configDigester struct { + d types.ConfigDigest +} + +func NewConfigDigester(d types.ConfigDigest) *configDigester { + return &configDigester{d: d} +} + +// ConfigDigest implements types.OffchainConfigDigester. +func (c *configDigester) ConfigDigest(types.ContractConfig) (types.ConfigDigest, error) { + return c.d, nil +} + +// ConfigDigestPrefix implements types.OffchainConfigDigester. +func (c *configDigester) ConfigDigestPrefix() (types.ConfigDigestPrefix, error) { + return types.ConfigDigestPrefixCCIPMultiRole, nil +} + +var _ types.OffchainConfigDigester = (*configDigester)(nil) diff --git a/core/capabilities/ccip/ocrimpls/config_tracker.go b/core/capabilities/ccip/ocrimpls/config_tracker.go new file mode 100644 index 00000000000..3a6a27fa40c --- /dev/null +++ b/core/capabilities/ccip/ocrimpls/config_tracker.go @@ -0,0 +1,77 @@ +package ocrimpls + +import ( + "context" + + cctypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" + + gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3confighelper" + "github.com/smartcontractkit/libocr/offchainreporting2plus/types" +) + +type configTracker struct { + cfg cctypes.OCR3ConfigWithMeta +} + +func NewConfigTracker(cfg cctypes.OCR3ConfigWithMeta) *configTracker { + return &configTracker{cfg: cfg} +} + +// LatestBlockHeight implements types.ContractConfigTracker. +func (c *configTracker) LatestBlockHeight(ctx context.Context) (blockHeight uint64, err error) { + return 0, nil +} + +// LatestConfig implements types.ContractConfigTracker. +func (c *configTracker) LatestConfig(ctx context.Context, changedInBlock uint64) (types.ContractConfig, error) { + return c.contractConfig(), nil +} + +// LatestConfigDetails implements types.ContractConfigTracker. +func (c *configTracker) LatestConfigDetails(ctx context.Context) (changedInBlock uint64, configDigest types.ConfigDigest, err error) { + return 0, c.cfg.ConfigDigest, nil +} + +// Notify implements types.ContractConfigTracker. +func (c *configTracker) Notify() <-chan struct{} { + return nil +} + +func (c *configTracker) contractConfig() types.ContractConfig { + return types.ContractConfig{ + ConfigDigest: c.cfg.ConfigDigest, + ConfigCount: c.cfg.ConfigCount, + Signers: toOnchainPublicKeys(c.cfg.Config.Signers), + Transmitters: toOCRAccounts(c.cfg.Config.Transmitters), + F: c.cfg.Config.F, + OnchainConfig: []byte{}, + OffchainConfigVersion: c.cfg.Config.OffchainConfigVersion, + OffchainConfig: c.cfg.Config.OffchainConfig, + } +} + +// PublicConfig returns the OCR configuration as a PublicConfig so that we can +// access ReportingPluginConfig and other fields prior to launching the plugins. +func (c *configTracker) PublicConfig() (ocr3confighelper.PublicConfig, error) { + return ocr3confighelper.PublicConfigFromContractConfig(false, c.contractConfig()) +} + +func toOnchainPublicKeys(signers [][]byte) []types.OnchainPublicKey { + keys := make([]types.OnchainPublicKey, len(signers)) + for i, signer := range signers { + keys[i] = types.OnchainPublicKey(signer) + } + return keys +} + +func toOCRAccounts(transmitters [][]byte) []types.Account { + accounts := make([]types.Account, len(transmitters)) + for i, transmitter := range transmitters { + // TODO: string-encode the transmitter appropriately to the dest chain family. + accounts[i] = types.Account(gethcommon.BytesToAddress(transmitter).Hex()) + } + return accounts +} + +var _ types.ContractConfigTracker = (*configTracker)(nil) diff --git a/core/capabilities/ccip/ocrimpls/contract_transmitter.go b/core/capabilities/ccip/ocrimpls/contract_transmitter.go new file mode 100644 index 00000000000..fd8e206d0e3 --- /dev/null +++ b/core/capabilities/ccip/ocrimpls/contract_transmitter.go @@ -0,0 +1,188 @@ +package ocrimpls + +import ( + "context" + "errors" + "fmt" + "math/big" + + "github.com/google/uuid" + "github.com/smartcontractkit/libocr/offchainreporting2/chains/evmutil" + "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" + ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + + "github.com/smartcontractkit/chainlink-ccip/pkg/consts" + commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" +) + +type ToCalldataFunc func(rawReportCtx [3][32]byte, report []byte, rs, ss [][32]byte, vs [32]byte) any + +func ToCommitCalldata(rawReportCtx [3][32]byte, report []byte, rs, ss [][32]byte, vs [32]byte) any { + // Note that the name of the struct field is very important, since the encoder used + // by the chainwriter uses mapstructure, which will use the struct field name to map + // to the argument name in the function call. + // If, for whatever reason, we want to change the field name, make sure to add a `mapstructure:""` tag + // for that field. + return struct { + ReportContext [3][32]byte + Report []byte + Rs [][32]byte + Ss [][32]byte + RawVs [32]byte + }{ + ReportContext: rawReportCtx, + Report: report, + Rs: rs, + Ss: ss, + RawVs: vs, + } +} + +func ToExecCalldata(rawReportCtx [3][32]byte, report []byte, _, _ [][32]byte, _ [32]byte) any { + // Note that the name of the struct field is very important, since the encoder used + // by the chainwriter uses mapstructure, which will use the struct field name to map + // to the argument name in the function call. + // If, for whatever reason, we want to change the field name, make sure to add a `mapstructure:""` tag + // for that field. + return struct { + ReportContext [3][32]byte + Report []byte + }{ + ReportContext: rawReportCtx, + Report: report, + } +} + +var _ ocr3types.ContractTransmitter[[]byte] = &commitTransmitter[[]byte]{} + +type commitTransmitter[RI any] struct { + cw commontypes.ChainWriter + fromAccount ocrtypes.Account + contractName string + method string + offrampAddress string + toCalldataFn ToCalldataFunc +} + +func XXXNewContractTransmitterTestsOnly[RI any]( + cw commontypes.ChainWriter, + fromAccount ocrtypes.Account, + contractName string, + method string, + offrampAddress string, + toCalldataFn ToCalldataFunc, +) ocr3types.ContractTransmitter[RI] { + return &commitTransmitter[RI]{ + cw: cw, + fromAccount: fromAccount, + contractName: contractName, + method: method, + offrampAddress: offrampAddress, + toCalldataFn: toCalldataFn, + } +} + +func NewCommitContractTransmitter[RI any]( + cw commontypes.ChainWriter, + fromAccount ocrtypes.Account, + offrampAddress string, +) ocr3types.ContractTransmitter[RI] { + return &commitTransmitter[RI]{ + cw: cw, + fromAccount: fromAccount, + contractName: consts.ContractNameOffRamp, + method: consts.MethodCommit, + offrampAddress: offrampAddress, + toCalldataFn: ToCommitCalldata, + } +} + +func NewExecContractTransmitter[RI any]( + cw commontypes.ChainWriter, + fromAccount ocrtypes.Account, + offrampAddress string, +) ocr3types.ContractTransmitter[RI] { + return &commitTransmitter[RI]{ + cw: cw, + fromAccount: fromAccount, + contractName: consts.ContractNameOffRamp, + method: consts.MethodExecute, + offrampAddress: offrampAddress, + toCalldataFn: ToExecCalldata, + } +} + +// FromAccount implements ocr3types.ContractTransmitter. +func (c *commitTransmitter[RI]) FromAccount() (ocrtypes.Account, error) { + return c.fromAccount, nil +} + +// Transmit implements ocr3types.ContractTransmitter. +func (c *commitTransmitter[RI]) Transmit( + ctx context.Context, + configDigest ocrtypes.ConfigDigest, + seqNr uint64, + reportWithInfo ocr3types.ReportWithInfo[RI], + sigs []ocrtypes.AttributedOnchainSignature, +) error { + var rs [][32]byte + var ss [][32]byte + var vs [32]byte + if len(sigs) > 32 { + return errors.New("too many signatures, maximum is 32") + } + for i, as := range sigs { + r, s, v, err := evmutil.SplitSignature(as.Signature) + if err != nil { + return fmt.Errorf("failed to split signature: %w", err) + } + rs = append(rs, r) + ss = append(ss, s) + vs[i] = v + } + + // report ctx for OCR3 consists of the following + // reportContext[0]: ConfigDigest + // reportContext[1]: 24 byte padding, 8 byte sequence number + // reportContext[2]: unused + // convert seqNum, which is a uint64, into a uint32 epoch and uint8 round + // while this does truncate the sequence number, it is not a problem because + // it still gives us 2^40 - 1 possible sequence numbers. + // assuming a sequence number is generated every second, this gives us + // 1099511627775 seconds, or approximately 34,865 years, before we run out + // of sequence numbers. + epoch, round := uint64ToUint32AndUint8(seqNr) + rawReportCtx := evmutil.RawReportContext(ocrtypes.ReportContext{ + ReportTimestamp: ocrtypes.ReportTimestamp{ + ConfigDigest: configDigest, + Epoch: epoch, + Round: round, + }, + // ExtraData not used in OCR3 + }) + + if c.toCalldataFn == nil { + return errors.New("toCalldataFn is nil") + } + + // chain writer takes in the raw calldata and packs it on its own. + args := c.toCalldataFn(rawReportCtx, reportWithInfo.Report, rs, ss, vs) + + // TODO: no meta fields yet, what should we add? + // probably whats in the info part of the report? + meta := commontypes.TxMeta{} + txID, err := uuid.NewRandom() // NOTE: CW expects us to generate an ID, rather than return one + if err != nil { + return fmt.Errorf("failed to generate UUID: %w", err) + } + zero := big.NewInt(0) + if err := c.cw.SubmitTransaction(ctx, c.contractName, c.method, args, fmt.Sprintf("%s-%s-%s", c.contractName, c.offrampAddress, txID.String()), c.offrampAddress, &meta, zero); err != nil { + return fmt.Errorf("failed to submit transaction thru chainwriter: %w", err) + } + + return nil +} + +func uint64ToUint32AndUint8(x uint64) (uint32, uint8) { + return uint32(x >> 32), uint8(x) +} diff --git a/core/capabilities/ccip/ocrimpls/contract_transmitter_test.go b/core/capabilities/ccip/ocrimpls/contract_transmitter_test.go new file mode 100644 index 00000000000..871afbb6697 --- /dev/null +++ b/core/capabilities/ccip/ocrimpls/contract_transmitter_test.go @@ -0,0 +1,691 @@ +package ocrimpls_test + +import ( + "crypto/rand" + "math/big" + "net/url" + "testing" + "time" + + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/ocrimpls" + cctypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core" + "github.com/jmoiron/sqlx" + "github.com/onsi/gomega" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/libocr/commontypes" + "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" + ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + + "github.com/smartcontractkit/chainlink-common/pkg/utils/mailbox" + txmgrcommon "github.com/smartcontractkit/chainlink/v2/common/txmgr" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + evmconfig "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/chaintype" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/headtracker" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/keystore" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" + "github.com/smartcontractkit/chainlink/v2/core/config" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/multi_ocr3_helper" + "github.com/smartcontractkit/chainlink/v2/core/internal/cltest" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest" + "github.com/smartcontractkit/chainlink/v2/core/logger" + kschaintype "github.com/smartcontractkit/chainlink/v2/core/services/keystore/chaintype" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ocr2key" + "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm" + evmrelaytypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" +) + +func Test_ContractTransmitter_TransmitWithoutSignatures(t *testing.T) { + type testCase struct { + name string + pluginType uint8 + withSigs bool + expectedSigsEnabled bool + report []byte + } + + testCases := []testCase{ + { + "empty report with sigs", + uint8(cctypes.PluginTypeCCIPCommit), + true, + true, + []byte{}, + }, + { + "empty report without sigs", + uint8(cctypes.PluginTypeCCIPExec), + false, + false, + []byte{}, + }, + { + "report with data with sigs", + uint8(cctypes.PluginTypeCCIPCommit), + true, + true, + randomReport(t, 96), + }, + { + "report with data without sigs", + uint8(cctypes.PluginTypeCCIPExec), + false, + false, + randomReport(t, 96), + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc := tc + t.Parallel() + testTransmitter(t, tc.pluginType, tc.withSigs, tc.expectedSigsEnabled, tc.report) + }) + } +} + +func testTransmitter( + t *testing.T, + pluginType uint8, + withSigs bool, + expectedSigsEnabled bool, + report []byte, +) { + uni := newTestUniverse[[]byte](t, nil) + + c, err := uni.wrapper.LatestConfigDetails(nil, pluginType) + require.NoError(t, err, "failed to get latest config details") + configDigest := c.ConfigInfo.ConfigDigest + require.Equal(t, expectedSigsEnabled, c.ConfigInfo.IsSignatureVerificationEnabled, "signature verification enabled setting not correct") + + // set the plugin type on the helper so it fetches the right config info. + // the important aspect is whether signatures should be enabled or not. + _, err = uni.wrapper.SetTransmitOcrPluginType(uni.deployer, pluginType) + require.NoError(t, err, "failed to set plugin type") + uni.backend.Commit() + + // create attributed sigs + // only need f+1 which is 2 in this case + rwi := ocr3types.ReportWithInfo[[]byte]{ + Report: report, + Info: []byte{}, + } + seqNr := uint64(1) + attributedSigs := uni.SignReport(t, configDigest, rwi, seqNr) + + account, err := uni.transmitterWithSigs.FromAccount() + require.NoError(t, err, "failed to get from account") + require.Equal(t, ocrtypes.Account(uni.transmitters[0].Hex()), account, "from account mismatch") + if withSigs { + err = uni.transmitterWithSigs.Transmit(testutils.Context(t), configDigest, seqNr, rwi, attributedSigs) + } else { + err = uni.transmitterWithoutSigs.Transmit(testutils.Context(t), configDigest, seqNr, rwi, attributedSigs) + } + require.NoError(t, err, "failed to transmit") + uni.backend.Commit() + + var txStatus uint64 + gomega.NewWithT(t).Eventually(func() bool { + uni.backend.Commit() + rows, err := uni.db.QueryContext(testutils.Context(t), `SELECT hash FROM evm.tx_attempts LIMIT 1`) + require.NoError(t, err, "failed to query txes") + defer rows.Close() + var txHash []byte + for rows.Next() { + require.NoError(t, rows.Scan(&txHash), "failed to scan") + } + t.Log("txHash:", txHash) + receipt, err := uni.simClient.TransactionReceipt(testutils.Context(t), common.BytesToHash(txHash)) + if err != nil { + t.Log("tx not found yet:", hexutil.Encode(txHash)) + return false + } + t.Log("tx found:", hexutil.Encode(txHash), "status:", receipt.Status) + txStatus = receipt.Status + return true + }, testutils.WaitTimeout(t), 1*time.Second).Should(gomega.BeTrue()) + + // wait for receipt to be written to the db + gomega.NewWithT(t).Eventually(func() bool { + rows, err := uni.db.QueryContext(testutils.Context(t), `SELECT count(*) as cnt FROM evm.receipts LIMIT 1`) + require.NoError(t, err, "failed to query receipts") + defer rows.Close() + var count int + for rows.Next() { + require.NoError(t, rows.Scan(&count), "failed to scan") + } + return count == 1 + }, testutils.WaitTimeout(t), 2*time.Second).Should(gomega.BeTrue()) + + require.Equal(t, uint64(1), txStatus, "tx status should be success") + + // check that the event was emitted + events := uni.TransmittedEvents(t) + require.Len(t, events, 1, "expected 1 event") + require.Equal(t, configDigest, events[0].ConfigDigest, "config digest mismatch") + require.Equal(t, seqNr, events[0].SequenceNumber, "seq num mismatch") +} + +type testUniverse[RI any] struct { + simClient *client.SimulatedBackendClient + backend *backends.SimulatedBackend + deployer *bind.TransactOpts + transmitters []common.Address + signers []common.Address + wrapper *multi_ocr3_helper.MultiOCR3Helper + transmitterWithSigs ocr3types.ContractTransmitter[RI] + transmitterWithoutSigs ocr3types.ContractTransmitter[RI] + keyrings []ocr3types.OnchainKeyring[RI] + f uint8 + db *sqlx.DB + txm txmgr.TxManager + gasEstimator gas.EvmFeeEstimator +} + +type keyringsAndSigners[RI any] struct { + keyrings []ocr3types.OnchainKeyring[RI] + signers []common.Address +} + +func newTestUniverse[RI any](t *testing.T, ks *keyringsAndSigners[RI]) *testUniverse[RI] { + t.Helper() + + db := pgtest.NewSqlxDB(t) + owner := testutils.MustNewSimTransactor(t) + + // create many transmitters but only need to fund one, rest are to get + // setOCR3Config to pass. + keyStore := cltest.NewKeyStore(t, db) + var transmitters []common.Address + for i := 0; i < 4; i++ { + key, err := keyStore.Eth().Create(testutils.Context(t), big.NewInt(1337)) + require.NoError(t, err, "failed to create key") + transmitters = append(transmitters, key.Address) + } + + backend := backends.NewSimulatedBackend(core.GenesisAlloc{ + owner.From: core.GenesisAccount{ + Balance: assets.Ether(1000).ToInt(), + }, + transmitters[0]: core.GenesisAccount{ + Balance: assets.Ether(1000).ToInt(), + }, + }, 30e6) + + ocr3HelperAddr, _, _, err := multi_ocr3_helper.DeployMultiOCR3Helper(owner, backend) + require.NoError(t, err) + backend.Commit() + wrapper, err := multi_ocr3_helper.NewMultiOCR3Helper(ocr3HelperAddr, backend) + require.NoError(t, err) + + // create the oracle identities for setConfig + // need to create at least 4 identities otherwise setConfig will fail + var ( + keyrings []ocr3types.OnchainKeyring[RI] + signers []common.Address + ) + if ks != nil { + keyrings = ks.keyrings + signers = ks.signers + } else { + for i := 0; i < 4; i++ { + kb, err2 := ocr2key.New(kschaintype.EVM) + require.NoError(t, err2, "failed to create key") + kr := ocrimpls.NewOnchainKeyring[RI](kb, logger.TestLogger(t)) + signers = append(signers, common.BytesToAddress(kr.PublicKey())) + keyrings = append(keyrings, kr) + } + } + f := uint8(1) + commitConfigDigest := testutils.Random32Byte() + execConfigDigest := testutils.Random32Byte() + _, err = wrapper.SetOCR3Configs( + owner, + []multi_ocr3_helper.MultiOCR3BaseOCRConfigArgs{ + { + ConfigDigest: commitConfigDigest, + OcrPluginType: uint8(cctypes.PluginTypeCCIPCommit), + F: f, + IsSignatureVerificationEnabled: true, + Signers: signers, + Transmitters: []common.Address{ + transmitters[0], + transmitters[1], + transmitters[2], + transmitters[3], + }, + }, + { + ConfigDigest: execConfigDigest, + OcrPluginType: uint8(cctypes.PluginTypeCCIPExec), + F: f, + IsSignatureVerificationEnabled: false, + Signers: signers, + Transmitters: []common.Address{ + transmitters[0], + transmitters[1], + transmitters[2], + transmitters[3], + }, + }, + }, + ) + require.NoError(t, err) + backend.Commit() + + commitConfig, err := wrapper.LatestConfigDetails(nil, uint8(cctypes.PluginTypeCCIPCommit)) + require.NoError(t, err, "failed to get latest commit config") + require.Equal(t, commitConfigDigest, commitConfig.ConfigInfo.ConfigDigest, "commit config digest mismatch") + execConfig, err := wrapper.LatestConfigDetails(nil, uint8(cctypes.PluginTypeCCIPExec)) + require.NoError(t, err, "failed to get latest exec config") + require.Equal(t, execConfigDigest, execConfig.ConfigInfo.ConfigDigest, "exec config digest mismatch") + + simClient := client.NewSimulatedBackendClient(t, backend, testutils.SimulatedChainID) + + // create the chain writer service + txm, gasEstimator := makeTestEvmTxm(t, db, simClient, keyStore.Eth()) + require.NoError(t, txm.Start(testutils.Context(t)), "failed to start tx manager") + t.Cleanup(func() { require.NoError(t, txm.Close()) }) + + chainWriter, err := evm.NewChainWriterService( + logger.TestLogger(t), + simClient, + txm, + gasEstimator, + chainWriterConfigRaw(transmitters[0], assets.GWei(1))) + require.NoError(t, err, "failed to create chain writer") + require.NoError(t, chainWriter.Start(testutils.Context(t)), "failed to start chain writer") + t.Cleanup(func() { require.NoError(t, chainWriter.Close()) }) + + transmitterWithSigs := ocrimpls.XXXNewContractTransmitterTestsOnly[RI]( + chainWriter, + ocrtypes.Account(transmitters[0].Hex()), + contractName, + methodTransmitWithSignatures, + ocr3HelperAddr.Hex(), + ocrimpls.ToCommitCalldata, + ) + transmitterWithoutSigs := ocrimpls.XXXNewContractTransmitterTestsOnly[RI]( + chainWriter, + ocrtypes.Account(transmitters[0].Hex()), + contractName, + methodTransmitWithoutSignatures, + ocr3HelperAddr.Hex(), + ocrimpls.ToExecCalldata, + ) + + return &testUniverse[RI]{ + simClient: simClient, + backend: backend, + deployer: owner, + transmitters: transmitters, + signers: signers, + wrapper: wrapper, + transmitterWithSigs: transmitterWithSigs, + transmitterWithoutSigs: transmitterWithoutSigs, + keyrings: keyrings, + f: f, + db: db, + txm: txm, + gasEstimator: gasEstimator, + } +} + +func (uni testUniverse[RI]) SignReport(t *testing.T, configDigest ocrtypes.ConfigDigest, rwi ocr3types.ReportWithInfo[RI], seqNum uint64) []ocrtypes.AttributedOnchainSignature { + var attributedSigs []ocrtypes.AttributedOnchainSignature + for i := uint8(0); i < uni.f+1; i++ { + t.Log("signing report with", hexutil.Encode(uni.keyrings[i].PublicKey())) + sig, err := uni.keyrings[i].Sign(configDigest, seqNum, rwi) + require.NoError(t, err, "failed to sign report") + attributedSigs = append(attributedSigs, ocrtypes.AttributedOnchainSignature{ + Signature: sig, + Signer: commontypes.OracleID(i), + }) + } + return attributedSigs +} + +func (uni testUniverse[RI]) TransmittedEvents(t *testing.T) []*multi_ocr3_helper.MultiOCR3HelperTransmitted { + iter, err := uni.wrapper.FilterTransmitted(&bind.FilterOpts{ + Start: 0, + }, nil) + require.NoError(t, err, "failed to create filter iterator") + var events []*multi_ocr3_helper.MultiOCR3HelperTransmitted + for iter.Next() { + event := iter.Event + events = append(events, event) + } + return events +} + +func randomReport(t *testing.T, len int) []byte { + report := make([]byte, len) + _, err := rand.Reader.Read(report) + require.NoError(t, err, "failed to read random bytes") + return report +} + +const ( + contractName = "MultiOCR3Helper" + methodTransmitWithSignatures = "TransmitWithSignatures" + methodTransmitWithoutSignatures = "TransmitWithoutSignatures" +) + +func chainWriterConfigRaw(fromAddress common.Address, maxGasPrice *assets.Wei) evmrelaytypes.ChainWriterConfig { + return evmrelaytypes.ChainWriterConfig{ + Contracts: map[string]*evmrelaytypes.ContractConfig{ + contractName: { + ContractABI: multi_ocr3_helper.MultiOCR3HelperABI, + Configs: map[string]*evmrelaytypes.ChainWriterDefinition{ + methodTransmitWithSignatures: { + ChainSpecificName: "transmitWithSignatures", + GasLimit: 1e6, + FromAddress: fromAddress, + }, + methodTransmitWithoutSignatures: { + ChainSpecificName: "transmitWithoutSignatures", + GasLimit: 1e6, + FromAddress: fromAddress, + }, + }, + }, + }, + SendStrategy: txmgrcommon.NewSendEveryStrategy(), + MaxGasPrice: maxGasPrice, + } +} + +func makeTestEvmTxm( + t *testing.T, + db *sqlx.DB, + ethClient client.Client, + keyStore keystore.Eth) (txmgr.TxManager, gas.EvmFeeEstimator) { + config, dbConfig, evmConfig := MakeTestConfigs(t) + + estimator, err := gas.NewEstimator(logger.TestLogger(t), ethClient, config, evmConfig.GasEstimator()) + require.NoError(t, err, "failed to create gas estimator") + + lggr := logger.TestLogger(t) + lpOpts := logpoller.Opts{ + PollPeriod: 100 * time.Millisecond, + FinalityDepth: 2, + BackfillBatchSize: 3, + RpcBatchSize: 2, + KeepFinalizedBlocksDepth: 1000, + } + + chainID := big.NewInt(1337) + headSaver := headtracker.NewHeadSaver( + logger.NullLogger, + headtracker.NewORM(*chainID, db), + evmConfig, + evmConfig.HeadTrackerConfig, + ) + + broadcaster := headtracker.NewHeadBroadcaster(logger.NullLogger) + require.NoError(t, broadcaster.Start(testutils.Context(t)), "failed to start head broadcaster") + t.Cleanup(func() { require.NoError(t, broadcaster.Close()) }) + + ht := headtracker.NewHeadTracker( + logger.NullLogger, + ethClient, + evmConfig, + evmConfig.HeadTrackerConfig, + broadcaster, + headSaver, + mailbox.NewMonitor("contract_transmitter_test", logger.NullLogger), + ) + require.NoError(t, ht.Start(testutils.Context(t)), "failed to start head tracker") + t.Cleanup(func() { require.NoError(t, ht.Close()) }) + + lp := logpoller.NewLogPoller(logpoller.NewORM(testutils.FixtureChainID, db, logger.NullLogger), + ethClient, logger.NullLogger, ht, lpOpts) + require.NoError(t, lp.Start(testutils.Context(t)), "failed to start log poller") + t.Cleanup(func() { require.NoError(t, lp.Close()) }) + + // logic for building components (from evm/evm_txm.go) ------- + lggr.Infow("Initializing EVM transaction manager", + "bumpTxDepth", evmConfig.GasEstimator().BumpTxDepth(), + "maxInFlightTransactions", config.EvmConfig.Transactions().MaxInFlight(), + "maxQueuedTransactions", config.EvmConfig.Transactions().MaxQueued(), + "nonceAutoSync", evmConfig.NonceAutoSync(), + "limitDefault", evmConfig.GasEstimator().LimitDefault(), + ) + + txm, err := txmgr.NewTxm( + db, + config, + config.EvmConfig.GasEstimator(), + config.EvmConfig.Transactions(), + nil, + dbConfig, + dbConfig.Listener(), + ethClient, + lggr, + lp, + keyStore, + estimator, + ht) + require.NoError(t, err, "can't create tx manager") + + _, unsub := broadcaster.Subscribe(txm) + t.Cleanup(unsub) + + return txm, estimator +} + +// Code below copied/pasted and slightly modified in order to work from core/chains/evm/txmgr/test_helpers.go. + +func ptr[T any](t T) *T { return &t } + +type TestDatabaseConfig struct { + config.Database + defaultQueryTimeout time.Duration +} + +func (d *TestDatabaseConfig) DefaultQueryTimeout() time.Duration { + return d.defaultQueryTimeout +} + +func (d *TestDatabaseConfig) LogSQL() bool { + return false +} + +type TestListenerConfig struct { + config.Listener +} + +func (l *TestListenerConfig) FallbackPollInterval() time.Duration { + return 1 * time.Minute +} + +func (d *TestDatabaseConfig) Listener() config.Listener { + return &TestListenerConfig{} +} + +type TestHeadTrackerConfig struct{} + +// FinalityTagBypass implements config.HeadTracker. +func (t *TestHeadTrackerConfig) FinalityTagBypass() bool { + return false +} + +// HistoryDepth implements config.HeadTracker. +func (t *TestHeadTrackerConfig) HistoryDepth() uint32 { + return 50 +} + +// MaxAllowedFinalityDepth implements config.HeadTracker. +func (t *TestHeadTrackerConfig) MaxAllowedFinalityDepth() uint32 { + return 100 +} + +// MaxBufferSize implements config.HeadTracker. +func (t *TestHeadTrackerConfig) MaxBufferSize() uint32 { + return 100 +} + +// SamplingInterval implements config.HeadTracker. +func (t *TestHeadTrackerConfig) SamplingInterval() time.Duration { + return 1 * time.Second +} + +var _ evmconfig.HeadTracker = (*TestHeadTrackerConfig)(nil) + +type TestEvmConfig struct { + evmconfig.EVM + HeadTrackerConfig evmconfig.HeadTracker + MaxInFlight uint32 + ReaperInterval time.Duration + ReaperThreshold time.Duration + ResendAfterThreshold time.Duration + BumpThreshold uint64 + MaxQueued uint64 + Enabled bool + Threshold uint32 + MinAttempts uint32 + DetectionApiUrl *url.URL +} + +func (e *TestEvmConfig) FinalityTagEnabled() bool { + return false +} + +func (e *TestEvmConfig) FinalityDepth() uint32 { + return 42 +} + +func (e *TestEvmConfig) FinalizedBlockOffset() uint32 { + return 42 +} + +func (e *TestEvmConfig) BlockEmissionIdleWarningThreshold() time.Duration { + return 10 * time.Second +} + +func (e *TestEvmConfig) Transactions() evmconfig.Transactions { + return &transactionsConfig{e: e, autoPurge: &autoPurgeConfig{}} +} + +func (e *TestEvmConfig) NonceAutoSync() bool { return true } + +func (e *TestEvmConfig) ChainType() chaintype.ChainType { return "" } + +type TestGasEstimatorConfig struct { + bumpThreshold uint64 +} + +func (g *TestGasEstimatorConfig) BlockHistory() evmconfig.BlockHistory { + return &TestBlockHistoryConfig{} +} + +func (g *TestGasEstimatorConfig) EIP1559DynamicFees() bool { return false } +func (g *TestGasEstimatorConfig) LimitDefault() uint64 { return 1e6 } +func (g *TestGasEstimatorConfig) BumpPercent() uint16 { return 2 } +func (g *TestGasEstimatorConfig) BumpThreshold() uint64 { return g.bumpThreshold } +func (g *TestGasEstimatorConfig) BumpMin() *assets.Wei { return assets.GWei(1) } +func (g *TestGasEstimatorConfig) FeeCapDefault() *assets.Wei { return assets.GWei(1) } +func (g *TestGasEstimatorConfig) PriceDefault() *assets.Wei { return assets.GWei(1) } +func (g *TestGasEstimatorConfig) TipCapDefault() *assets.Wei { return assets.GWei(1) } +func (g *TestGasEstimatorConfig) TipCapMin() *assets.Wei { return assets.GWei(1) } +func (g *TestGasEstimatorConfig) LimitMax() uint64 { return 0 } +func (g *TestGasEstimatorConfig) LimitMultiplier() float32 { return 1 } +func (g *TestGasEstimatorConfig) BumpTxDepth() uint32 { return 42 } +func (g *TestGasEstimatorConfig) LimitTransfer() uint64 { return 42 } +func (g *TestGasEstimatorConfig) PriceMax() *assets.Wei { return assets.GWei(1) } +func (g *TestGasEstimatorConfig) PriceMin() *assets.Wei { return assets.GWei(1) } +func (g *TestGasEstimatorConfig) Mode() string { return "FixedPrice" } +func (g *TestGasEstimatorConfig) LimitJobType() evmconfig.LimitJobType { + return &TestLimitJobTypeConfig{} +} +func (g *TestGasEstimatorConfig) PriceMaxKey(addr common.Address) *assets.Wei { + return assets.GWei(1) +} + +func (e *TestEvmConfig) GasEstimator() evmconfig.GasEstimator { + return &TestGasEstimatorConfig{bumpThreshold: e.BumpThreshold} +} + +type TestLimitJobTypeConfig struct { +} + +func (l *TestLimitJobTypeConfig) OCR() *uint32 { return ptr(uint32(0)) } +func (l *TestLimitJobTypeConfig) OCR2() *uint32 { return ptr(uint32(0)) } +func (l *TestLimitJobTypeConfig) DR() *uint32 { return ptr(uint32(0)) } +func (l *TestLimitJobTypeConfig) FM() *uint32 { return ptr(uint32(0)) } +func (l *TestLimitJobTypeConfig) Keeper() *uint32 { return ptr(uint32(0)) } +func (l *TestLimitJobTypeConfig) VRF() *uint32 { return ptr(uint32(0)) } + +type TestBlockHistoryConfig struct { + evmconfig.BlockHistory +} + +func (b *TestBlockHistoryConfig) BatchSize() uint32 { return 42 } +func (b *TestBlockHistoryConfig) BlockDelay() uint16 { return 42 } +func (b *TestBlockHistoryConfig) BlockHistorySize() uint16 { return 42 } +func (b *TestBlockHistoryConfig) EIP1559FeeCapBufferBlocks() uint16 { return 42 } +func (b *TestBlockHistoryConfig) TransactionPercentile() uint16 { return 42 } + +type transactionsConfig struct { + evmconfig.Transactions + e *TestEvmConfig + autoPurge evmconfig.AutoPurgeConfig +} + +func (*transactionsConfig) ForwardersEnabled() bool { return false } +func (t *transactionsConfig) MaxInFlight() uint32 { return t.e.MaxInFlight } +func (t *transactionsConfig) MaxQueued() uint64 { return t.e.MaxQueued } +func (t *transactionsConfig) ReaperInterval() time.Duration { return t.e.ReaperInterval } +func (t *transactionsConfig) ReaperThreshold() time.Duration { return t.e.ReaperThreshold } +func (t *transactionsConfig) ResendAfterThreshold() time.Duration { return t.e.ResendAfterThreshold } +func (t *transactionsConfig) AutoPurge() evmconfig.AutoPurgeConfig { return t.autoPurge } + +type autoPurgeConfig struct { + evmconfig.AutoPurgeConfig +} + +func (a *autoPurgeConfig) Enabled() bool { return false } + +type MockConfig struct { + EvmConfig *TestEvmConfig + RpcDefaultBatchSize uint32 + finalityDepth uint32 + finalityTagEnabled bool +} + +func (c *MockConfig) EVM() evmconfig.EVM { + return c.EvmConfig +} + +func (c *MockConfig) NonceAutoSync() bool { return true } +func (c *MockConfig) ChainType() chaintype.ChainType { return "" } +func (c *MockConfig) FinalityDepth() uint32 { return c.finalityDepth } +func (c *MockConfig) SetFinalityDepth(fd uint32) { c.finalityDepth = fd } +func (c *MockConfig) FinalityTagEnabled() bool { return c.finalityTagEnabled } +func (c *MockConfig) RPCDefaultBatchSize() uint32 { return c.RpcDefaultBatchSize } + +func MakeTestConfigs(t *testing.T) (*MockConfig, *TestDatabaseConfig, *TestEvmConfig) { + db := &TestDatabaseConfig{defaultQueryTimeout: utils.DefaultQueryTimeout} + ec := &TestEvmConfig{ + HeadTrackerConfig: &TestHeadTrackerConfig{}, + BumpThreshold: 42, + MaxInFlight: uint32(42), + MaxQueued: uint64(0), + ReaperInterval: time.Duration(0), + ReaperThreshold: time.Duration(0), + } + config := &MockConfig{EvmConfig: ec} + return config, db, ec +} diff --git a/core/capabilities/ccip/ocrimpls/keyring.go b/core/capabilities/ccip/ocrimpls/keyring.go new file mode 100644 index 00000000000..4b15c75b09a --- /dev/null +++ b/core/capabilities/ccip/ocrimpls/keyring.go @@ -0,0 +1,61 @@ +package ocrimpls + +import ( + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" + "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + + "github.com/smartcontractkit/chainlink/v2/core/logger" +) + +var _ ocr3types.OnchainKeyring[[]byte] = &ocr3Keyring[[]byte]{} + +type ocr3Keyring[RI any] struct { + core types.OnchainKeyring + lggr logger.Logger +} + +func NewOnchainKeyring[RI any](keyring types.OnchainKeyring, lggr logger.Logger) *ocr3Keyring[RI] { + return &ocr3Keyring[RI]{ + core: keyring, + lggr: lggr.Named("OCR3Keyring"), + } +} + +func (w *ocr3Keyring[RI]) PublicKey() types.OnchainPublicKey { + return w.core.PublicKey() +} + +func (w *ocr3Keyring[RI]) MaxSignatureLength() int { + return w.core.MaxSignatureLength() +} + +func (w *ocr3Keyring[RI]) Sign(configDigest types.ConfigDigest, seqNr uint64, r ocr3types.ReportWithInfo[RI]) (signature []byte, err error) { + epoch, round := uint64ToUint32AndUint8(seqNr) + rCtx := types.ReportContext{ + ReportTimestamp: types.ReportTimestamp{ + ConfigDigest: configDigest, + Epoch: epoch, + Round: round, + }, + } + + w.lggr.Debugw("signing report", "configDigest", configDigest.Hex(), "seqNr", seqNr, "report", hexutil.Encode(r.Report)) + + return w.core.Sign(rCtx, r.Report) +} + +func (w *ocr3Keyring[RI]) Verify(key types.OnchainPublicKey, configDigest types.ConfigDigest, seqNr uint64, r ocr3types.ReportWithInfo[RI], signature []byte) bool { + epoch, round := uint64ToUint32AndUint8(seqNr) + rCtx := types.ReportContext{ + ReportTimestamp: types.ReportTimestamp{ + ConfigDigest: configDigest, + Epoch: epoch, + Round: round, + }, + } + + w.lggr.Debugw("verifying report", "configDigest", configDigest.Hex(), "seqNr", seqNr, "report", hexutil.Encode(r.Report)) + + return w.core.Verify(key, rCtx, r.Report, signature) +} diff --git a/core/capabilities/ccip/oraclecreator/inprocess.go b/core/capabilities/ccip/oraclecreator/inprocess.go new file mode 100644 index 00000000000..6616d356756 --- /dev/null +++ b/core/capabilities/ccip/oraclecreator/inprocess.go @@ -0,0 +1,371 @@ +package oraclecreator + +import ( + "context" + "fmt" + "time" + + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/ccipevm" + evmconfig "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/configs/evm" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/ocrimpls" + cctypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/google/uuid" + "github.com/prometheus/client_golang/prometheus" + + chainsel "github.com/smartcontractkit/chain-selectors" + + "github.com/smartcontractkit/chainlink-ccip/pkg/consts" + "github.com/smartcontractkit/chainlink-ccip/pluginconfig" + + "github.com/smartcontractkit/libocr/commontypes" + libocr3 "github.com/smartcontractkit/libocr/offchainreporting2plus" + "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" + ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" + + commitocr3 "github.com/smartcontractkit/chainlink-ccip/commit" + execocr3 "github.com/smartcontractkit/chainlink-ccip/execute" + ccipreaderpkg "github.com/smartcontractkit/chainlink-ccip/pkg/reader" + + "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink/v2/core/chains/legacyevm" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/job" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/chaintype" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ocr2key" + "github.com/smartcontractkit/chainlink/v2/core/services/ocrcommon" + "github.com/smartcontractkit/chainlink/v2/core/services/relay" + "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm" + evmrelaytypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" + "github.com/smartcontractkit/chainlink/v2/core/services/synchronization" + "github.com/smartcontractkit/chainlink/v2/core/services/telemetry" +) + +var _ cctypes.OracleCreator = &inprocessOracleCreator{} + +const ( + defaultCommitGasLimit = 500_000 +) + +// inprocessOracleCreator creates oracles that reference plugins running +// in the same process as the chainlink node, i.e not LOOPPs. +type inprocessOracleCreator struct { + ocrKeyBundles map[string]ocr2key.KeyBundle + transmitters map[types.RelayID][]string + chains legacyevm.LegacyChainContainer + peerWrapper *ocrcommon.SingletonPeerWrapper + externalJobID uuid.UUID + jobID int32 + isNewlyCreatedJob bool + pluginConfig job.JSONConfig + db ocr3types.Database + lggr logger.Logger + monitoringEndpointGen telemetry.MonitoringEndpointGenerator + bootstrapperLocators []commontypes.BootstrapperLocator + homeChainReader ccipreaderpkg.HomeChain +} + +func New( + ocrKeyBundles map[string]ocr2key.KeyBundle, + transmitters map[types.RelayID][]string, + chains legacyevm.LegacyChainContainer, + peerWrapper *ocrcommon.SingletonPeerWrapper, + externalJobID uuid.UUID, + jobID int32, + isNewlyCreatedJob bool, + pluginConfig job.JSONConfig, + db ocr3types.Database, + lggr logger.Logger, + monitoringEndpointGen telemetry.MonitoringEndpointGenerator, + bootstrapperLocators []commontypes.BootstrapperLocator, + homeChainReader ccipreaderpkg.HomeChain, +) cctypes.OracleCreator { + return &inprocessOracleCreator{ + ocrKeyBundles: ocrKeyBundles, + transmitters: transmitters, + chains: chains, + peerWrapper: peerWrapper, + externalJobID: externalJobID, + jobID: jobID, + isNewlyCreatedJob: isNewlyCreatedJob, + pluginConfig: pluginConfig, + db: db, + lggr: lggr, + monitoringEndpointGen: monitoringEndpointGen, + bootstrapperLocators: bootstrapperLocators, + homeChainReader: homeChainReader, + } +} + +// CreateBootstrapOracle implements types.OracleCreator. +func (i *inprocessOracleCreator) CreateBootstrapOracle(config cctypes.OCR3ConfigWithMeta) (cctypes.CCIPOracle, error) { + // Assuming that the chain selector is referring to an evm chain for now. + // TODO: add an api that returns chain family. + chainID, err := chainsel.ChainIdFromSelector(uint64(config.Config.ChainSelector)) + if err != nil { + return nil, fmt.Errorf("failed to get chain ID from selector: %w", err) + } + + destChainFamily := chaintype.EVM + destRelayID := types.NewRelayID(string(destChainFamily), fmt.Sprintf("%d", chainID)) + + bootstrapperArgs := libocr3.BootstrapperArgs{ + BootstrapperFactory: i.peerWrapper.Peer2, + V2Bootstrappers: i.bootstrapperLocators, + ContractConfigTracker: ocrimpls.NewConfigTracker(config), + Database: i.db, + LocalConfig: defaultLocalConfig(), + Logger: ocrcommon.NewOCRWrapper( + i.lggr. + Named("CCIPBootstrap"). + Named(destRelayID.String()). + Named(config.Config.ChainSelector.String()). + Named(hexutil.Encode(config.Config.OfframpAddress)), + false, /* traceLogging */ + func(ctx context.Context, msg string) {}), + MonitoringEndpoint: i.monitoringEndpointGen.GenMonitoringEndpoint( + string(destChainFamily), + destRelayID.ChainID, + hexutil.Encode(config.Config.OfframpAddress), + synchronization.OCR3CCIPBootstrap, + ), + OffchainConfigDigester: ocrimpls.NewConfigDigester(config.ConfigDigest), + } + bootstrapper, err := libocr3.NewBootstrapper(bootstrapperArgs) + if err != nil { + return nil, err + } + return bootstrapper, nil +} + +// CreatePluginOracle implements types.OracleCreator. +func (i *inprocessOracleCreator) CreatePluginOracle(pluginType cctypes.PluginType, config cctypes.OCR3ConfigWithMeta) (cctypes.CCIPOracle, error) { + // Assuming that the chain selector is referring to an evm chain for now. + // TODO: add an api that returns chain family. + destChainID, err := chainsel.ChainIdFromSelector(uint64(config.Config.ChainSelector)) + if err != nil { + return nil, fmt.Errorf("failed to get chain ID from selector %d: %w", config.Config.ChainSelector, err) + } + destChainFamily := relay.NetworkEVM + destRelayID := types.NewRelayID(destChainFamily, fmt.Sprintf("%d", destChainID)) + + configTracker := ocrimpls.NewConfigTracker(config) + publicConfig, err := configTracker.PublicConfig() + if err != nil { + return nil, fmt.Errorf("failed to get public config from OCR config: %w", err) + } + var execBatchGasLimit uint64 + if pluginType == cctypes.PluginTypeCCIPExec { + execOffchainConfig, err2 := pluginconfig.DecodeExecuteOffchainConfig(publicConfig.ReportingPluginConfig) + if err2 != nil { + return nil, fmt.Errorf("failed to decode execute offchain config: %w, raw: %s", + err2, string(publicConfig.ReportingPluginConfig)) + } + if execOffchainConfig.BatchGasLimit == 0 && destChainFamily == relay.NetworkEVM { + return nil, fmt.Errorf("BatchGasLimit not set in execute offchain config, must be > 0") + } + execBatchGasLimit = execOffchainConfig.BatchGasLimit + } + + // this is so that we can use the msg hasher and report encoder from that dest chain relayer's provider. + contractReaders := make(map[cciptypes.ChainSelector]types.ContractReader) + chainWriters := make(map[cciptypes.ChainSelector]types.ChainWriter) + for _, chain := range i.chains.Slice() { + var chainReaderConfig evmrelaytypes.ChainReaderConfig + if chain.ID().Uint64() == destChainID { + chainReaderConfig = evmconfig.DestReaderConfig() + } else { + chainReaderConfig = evmconfig.SourceReaderConfig() + } + cr, err2 := evm.NewChainReaderService( + context.Background(), + i.lggr. + Named("EVMChainReaderService"). + Named(chain.ID().String()). + Named(pluginType.String()), + chain.LogPoller(), + chain.HeadTracker(), + chain.Client(), + chainReaderConfig, + ) + if err2 != nil { + return nil, fmt.Errorf("failed to create contract reader for chain %s: %w", chain.ID(), err2) + } + + if chain.ID().Uint64() == destChainID { + // bind the chain reader to the dest chain's offramp. + offrampAddressHex := common.BytesToAddress(config.Config.OfframpAddress).Hex() + err3 := cr.Bind(context.Background(), []types.BoundContract{ + { + Address: offrampAddressHex, + Name: consts.ContractNameOffRamp, + }, + }) + if err3 != nil { + return nil, fmt.Errorf("failed to bind chain reader for dest chain %s's offramp at %s: %w", chain.ID(), offrampAddressHex, err3) + } + } + + // TODO: figure out shutdown. + // maybe from the plugin directly? + err2 = cr.Start(context.Background()) + if err2 != nil { + return nil, fmt.Errorf("failed to start contract reader for chain %s: %w", chain.ID(), err2) + } + + // Even though we only write to the dest chain, we need to create chain writers for all chains + // we know about in order to post gas prices on the dest. + var fromAddress common.Address + transmitter, ok := i.transmitters[types.NewRelayID(relay.NetworkEVM, chain.ID().String())] + if ok { + fromAddress = common.HexToAddress(transmitter[0]) + } + cw, err2 := evm.NewChainWriterService( + i.lggr.Named("EVMChainWriterService"). + Named(chain.ID().String()). + Named(pluginType.String()), + chain.Client(), + chain.TxManager(), + chain.GasEstimator(), + evmconfig.ChainWriterConfigRaw( + fromAddress, + chain.Config().EVM().GasEstimator().PriceMaxKey(fromAddress), + defaultCommitGasLimit, + execBatchGasLimit, + ), + ) + if err2 != nil { + return nil, fmt.Errorf("failed to create chain writer for chain %s: %w", chain.ID(), err2) + } + + // TODO: figure out shutdown. + // maybe from the plugin directly? + err2 = cw.Start(context.Background()) + if err2 != nil { + return nil, fmt.Errorf("failed to start chain writer for chain %s: %w", chain.ID(), err2) + } + + chainSelector, ok := chainsel.EvmChainIdToChainSelector()[chain.ID().Uint64()] + if !ok { + return nil, fmt.Errorf("failed to get chain selector from chain ID %s", chain.ID()) + } + + contractReaders[cciptypes.ChainSelector(chainSelector)] = cr + chainWriters[cciptypes.ChainSelector(chainSelector)] = cw + } + + // build the onchain keyring. it will be the signing key for the destination chain family. + keybundle, ok := i.ocrKeyBundles[destChainFamily] + if !ok { + return nil, fmt.Errorf("no OCR key bundle found for chain family %s, forgot to create one?", destChainFamily) + } + onchainKeyring := ocrimpls.NewOnchainKeyring[[]byte](keybundle, i.lggr) + + // build the contract transmitter + // assume that we are using the first account in the keybundle as the from account + // and that we are able to transmit to the dest chain. + // TODO: revisit this in the future, since not all oracles will be able to transmit to the dest chain. + destChainWriter, ok := chainWriters[config.Config.ChainSelector] + if !ok { + return nil, fmt.Errorf("no chain writer found for dest chain selector %d, can't create contract transmitter", + config.Config.ChainSelector) + } + destFromAccounts, ok := i.transmitters[destRelayID] + if !ok { + return nil, fmt.Errorf("no transmitter found for dest relay ID %s, can't create contract transmitter", destRelayID) + } + + // TODO: Extract the correct transmitter address from the destsFromAccount + var factory ocr3types.ReportingPluginFactory[[]byte] + var transmitter ocr3types.ContractTransmitter[[]byte] + if config.Config.PluginType == uint8(cctypes.PluginTypeCCIPCommit) { + factory = commitocr3.NewPluginFactory( + i.lggr. + Named("CCIPCommitPlugin"). + Named(destRelayID.String()). + Named(fmt.Sprintf("%d", config.Config.ChainSelector)). + Named(hexutil.Encode(config.Config.OfframpAddress)), + ccipreaderpkg.OCR3ConfigWithMeta(config), + ccipevm.NewCommitPluginCodecV1(), + ccipevm.NewMessageHasherV1(), + i.homeChainReader, + contractReaders, + chainWriters, + ) + transmitter = ocrimpls.NewCommitContractTransmitter[[]byte](destChainWriter, + ocrtypes.Account(destFromAccounts[0]), + hexutil.Encode(config.Config.OfframpAddress), // TODO: this works for evm only, how about non-evm? + ) + } else if config.Config.PluginType == uint8(cctypes.PluginTypeCCIPExec) { + factory = execocr3.NewPluginFactory( + i.lggr. + Named("CCIPExecPlugin"). + Named(destRelayID.String()). + Named(hexutil.Encode(config.Config.OfframpAddress)), + ccipreaderpkg.OCR3ConfigWithMeta(config), + ccipevm.NewExecutePluginCodecV1(), + ccipevm.NewMessageHasherV1(), + i.homeChainReader, + contractReaders, + chainWriters, + ) + transmitter = ocrimpls.NewExecContractTransmitter[[]byte](destChainWriter, + ocrtypes.Account(destFromAccounts[0]), + hexutil.Encode(config.Config.OfframpAddress), // TODO: this works for evm only, how about non-evm? + ) + } else { + return nil, fmt.Errorf("unsupported plugin type %d", config.Config.PluginType) + } + + oracleArgs := libocr3.OCR3OracleArgs[[]byte]{ + BinaryNetworkEndpointFactory: i.peerWrapper.Peer2, + Database: i.db, + V2Bootstrappers: i.bootstrapperLocators, + ContractConfigTracker: configTracker, + ContractTransmitter: transmitter, + LocalConfig: defaultLocalConfig(), + Logger: ocrcommon.NewOCRWrapper( + i.lggr. + Named(fmt.Sprintf("CCIP%sOCR3", pluginType.String())). + Named(destRelayID.String()). + Named(hexutil.Encode(config.Config.OfframpAddress)), + false, + func(ctx context.Context, msg string) {}), + MetricsRegisterer: prometheus.WrapRegistererWith(map[string]string{"name": fmt.Sprintf("commit-%d", config.Config.ChainSelector)}, prometheus.DefaultRegisterer), + MonitoringEndpoint: i.monitoringEndpointGen.GenMonitoringEndpoint( + destChainFamily, + destRelayID.ChainID, + string(config.Config.OfframpAddress), + synchronization.OCR3CCIPCommit, + ), + OffchainConfigDigester: ocrimpls.NewConfigDigester(config.ConfigDigest), + OffchainKeyring: keybundle, + OnchainKeyring: onchainKeyring, + ReportingPluginFactory: factory, + } + oracle, err := libocr3.NewOracle(oracleArgs) + if err != nil { + return nil, err + } + return oracle, nil +} + +func defaultLocalConfig() ocrtypes.LocalConfig { + return ocrtypes.LocalConfig{ + BlockchainTimeout: 10 * time.Second, + // Config tracking is handled by the launcher, since we're doing blue-green + // deployments we're not going to be using OCR's built-in config switching, + // which always shuts down the previous instance. + ContractConfigConfirmations: 1, + SkipContractConfigConfirmations: true, + ContractConfigTrackerPollInterval: 10 * time.Second, + ContractTransmitterTransmitTimeout: 10 * time.Second, + DatabaseTimeout: 10 * time.Second, + MinOCR2MaxDurationQuery: 1 * time.Second, + DevelopmentMode: "false", + } +} diff --git a/core/capabilities/ccip/oraclecreator/inprocess_test.go b/core/capabilities/ccip/oraclecreator/inprocess_test.go new file mode 100644 index 00000000000..639f01e62e3 --- /dev/null +++ b/core/capabilities/ccip/oraclecreator/inprocess_test.go @@ -0,0 +1,239 @@ +package oraclecreator_test + +import ( + "fmt" + "testing" + "time" + + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/oraclecreator" + cctypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/google/uuid" + "github.com/hashicorp/consul/sdk/freeport" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/guregu/null.v4" + + chainsel "github.com/smartcontractkit/chain-selectors" + "github.com/smartcontractkit/libocr/offchainreporting2/types" + confighelper2 "github.com/smartcontractkit/libocr/offchainreporting2plus/confighelper" + "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3confighelper" + + "github.com/smartcontractkit/chainlink-ccip/pkg/reader" + "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + + "github.com/smartcontractkit/libocr/commontypes" + + "github.com/smartcontractkit/chainlink/v2/core/bridges" + "github.com/smartcontractkit/chainlink/v2/core/internal/cltest" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/configtest" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/chainlink" + "github.com/smartcontractkit/chainlink/v2/core/services/job" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/chaintype" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2" + ocr2validate "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/validate" + "github.com/smartcontractkit/chainlink/v2/core/services/ocrcommon" + "github.com/smartcontractkit/chainlink/v2/core/services/pipeline" + "github.com/smartcontractkit/chainlink/v2/core/services/synchronization" + "github.com/smartcontractkit/chainlink/v2/core/services/telemetry" + "github.com/smartcontractkit/chainlink/v2/core/testdata/testspecs" + "github.com/smartcontractkit/chainlink/v2/core/utils" +) + +func TestOracleCreator_CreateBootstrap(t *testing.T) { + db := pgtest.NewSqlxDB(t) + + keyStore := keystore.New(db, utils.DefaultScryptParams, logger.NullLogger) + require.NoError(t, keyStore.Unlock(testutils.Context(t), cltest.Password), "unable to unlock keystore") + p2pKey, err := keyStore.P2P().Create(testutils.Context(t)) + require.NoError(t, err) + peerID := p2pKey.PeerID() + listenPort := freeport.GetOne(t) + generalConfig := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { + c.P2P.PeerID = ptr(peerID) + c.P2P.TraceLogging = ptr(false) + c.P2P.V2.Enabled = ptr(true) + c.P2P.V2.ListenAddresses = ptr([]string{fmt.Sprintf("127.0.0.1:%d", listenPort)}) + + c.OCR2.Enabled = ptr(true) + }) + peerWrapper := ocrcommon.NewSingletonPeerWrapper(keyStore, generalConfig.P2P(), generalConfig.OCR(), db, logger.NullLogger) + require.NoError(t, peerWrapper.Start(testutils.Context(t))) + t.Cleanup(func() { assert.NoError(t, peerWrapper.Close()) }) + + // NOTE: this is a bit of a hack to get the OCR2 job created in order to use the ocr db + // the ocr2_contract_configs table has a foreign key constraint on ocr2_oracle_spec_id + // which is passed into ocr2.NewDB. + pipelineORM := pipeline.NewORM(db, + logger.NullLogger, generalConfig.JobPipeline().MaxSuccessfulRuns()) + bridgesORM := bridges.NewORM(db) + + jobORM := job.NewORM(db, pipelineORM, bridgesORM, keyStore, logger.TestLogger(t)) + t.Cleanup(func() { assert.NoError(t, jobORM.Close()) }) + + jb, err := ocr2validate.ValidatedOracleSpecToml(testutils.Context(t), generalConfig.OCR2(), generalConfig.Insecure(), testspecs.GetOCR2EVMSpecMinimal(), nil) + require.NoError(t, err) + const juelsPerFeeCoinSource = ` + ds [type=http method=GET url="https://chain.link/ETH-USD"]; + ds_parse [type=jsonparse path="data.price" separator="."]; + ds_multiply [type=multiply times=100]; + ds -> ds_parse -> ds_multiply;` + + _, address := cltest.MustInsertRandomKey(t, keyStore.Eth()) + jb.Name = null.StringFrom("Job 1") + jb.OCR2OracleSpec.TransmitterID = null.StringFrom(address.String()) + jb.OCR2OracleSpec.PluginConfig["juelsPerFeeCoinSource"] = juelsPerFeeCoinSource + + err = jobORM.CreateJob(testutils.Context(t), &jb) + require.NoError(t, err) + + cltest.AssertCount(t, db, "ocr2_oracle_specs", 1) + cltest.AssertCount(t, db, "jobs", 1) + + var oracleSpecID int32 + err = db.Get(&oracleSpecID, "SELECT id FROM ocr2_oracle_specs LIMIT 1") + require.NoError(t, err) + + ocrdb := ocr2.NewDB(db, oracleSpecID, 0, logger.NullLogger) + + oc := oraclecreator.New( + nil, + nil, + nil, + peerWrapper, + uuid.Max, + 0, + false, + nil, + ocrdb, + logger.TestLogger(t), + &mockEndpointGen{}, + []commontypes.BootstrapperLocator{}, + nil, + ) + + chainSelector := chainsel.GETH_TESTNET.Selector + oracles, offchainConfig := ocrOffchainConfig(t, keyStore) + bootstrapP2PID, err := p2pkey.MakePeerID(oracles[0].PeerID) + require.NoError(t, err) + transmitters := func() [][]byte { + var transmitters [][]byte + for _, o := range oracles { + transmitters = append(transmitters, hexutil.MustDecode(string(o.TransmitAccount))) + } + return transmitters + }() + configDigest := ccipConfigDigest() + bootstrap, err := oc.CreateBootstrapOracle(cctypes.OCR3ConfigWithMeta{ + ConfigDigest: configDigest, + ConfigCount: 1, + Config: reader.OCR3Config{ + ChainSelector: ccipocr3.ChainSelector(chainSelector), + OfframpAddress: testutils.NewAddress().Bytes(), + PluginType: uint8(cctypes.PluginTypeCCIPCommit), + F: 1, + OffchainConfigVersion: 30, + BootstrapP2PIds: [][32]byte{bootstrapP2PID}, + P2PIds: func() [][32]byte { + var ids [][32]byte + for _, o := range oracles { + id, err2 := p2pkey.MakePeerID(o.PeerID) + require.NoError(t, err2) + ids = append(ids, id) + } + return ids + }(), + Signers: func() [][]byte { + var signers [][]byte + for _, o := range oracles { + signers = append(signers, o.OnchainPublicKey) + } + return signers + }(), + Transmitters: transmitters, + OffchainConfig: offchainConfig, + }, + }) + require.NoError(t, err) + require.NoError(t, bootstrap.Start()) + t.Cleanup(func() { assert.NoError(t, bootstrap.Close()) }) + + tests.AssertEventually(t, func() bool { + c, err := ocrdb.ReadConfig(testutils.Context(t)) + require.NoError(t, err) + return c.ConfigDigest == configDigest + }) +} + +func ccipConfigDigest() [32]byte { + rand32Bytes := testutils.Random32Byte() + // overwrite first four bytes to be 0x000a, to match the prefix in libocr. + rand32Bytes[0] = 0x00 + rand32Bytes[1] = 0x0a + return rand32Bytes +} + +type mockEndpointGen struct{} + +func (m *mockEndpointGen) GenMonitoringEndpoint(network string, chainID string, contractID string, telemType synchronization.TelemetryType) commontypes.MonitoringEndpoint { + return &telemetry.NoopAgent{} +} + +func ptr[T any](b T) *T { + return &b +} + +func ocrOffchainConfig(t *testing.T, ks keystore.Master) (oracles []confighelper2.OracleIdentityExtra, offchainConfig []byte) { + for i := 0; i < 4; i++ { + kb, err := ks.OCR2().Create(testutils.Context(t), chaintype.EVM) + require.NoError(t, err) + p2pKey, err := ks.P2P().Create(testutils.Context(t)) + require.NoError(t, err) + ethKey, err := ks.Eth().Create(testutils.Context(t)) + require.NoError(t, err) + oracles = append(oracles, confighelper2.OracleIdentityExtra{ + OracleIdentity: confighelper2.OracleIdentity{ + OffchainPublicKey: kb.OffchainPublicKey(), + OnchainPublicKey: types.OnchainPublicKey(kb.OnChainPublicKey()), + PeerID: p2pKey.ID(), + TransmitAccount: types.Account(ethKey.Address.Hex()), + }, + ConfigEncryptionPublicKey: kb.ConfigEncryptionPublicKey(), + }) + } + var schedule []int + for range oracles { + schedule = append(schedule, 1) + } + offchainConfig, onchainConfig := []byte{}, []byte{} + f := uint8(1) + + _, _, _, _, _, offchainConfig, err := ocr3confighelper.ContractSetConfigArgsForTests( + 30*time.Second, // deltaProgress + 10*time.Second, // deltaResend + 20*time.Second, // deltaInitial + 2*time.Second, // deltaRound + 20*time.Second, // deltaGrace + 10*time.Second, // deltaCertifiedCommitRequest + 10*time.Second, // deltaStage + 3, // rmax + schedule, + oracles, + offchainConfig, + 50*time.Millisecond, // maxDurationQuery + 5*time.Second, // maxDurationObservation + 10*time.Second, // maxDurationShouldAcceptAttestedReport + 10*time.Second, // maxDurationShouldTransmitAcceptedReport + int(f), + onchainConfig) + require.NoError(t, err, "failed to create contract config") + + return oracles, offchainConfig +} diff --git a/core/capabilities/ccip/types/mocks/ccip_oracle.go b/core/capabilities/ccip/types/mocks/ccip_oracle.go new file mode 100644 index 00000000000..c849b3d9414 --- /dev/null +++ b/core/capabilities/ccip/types/mocks/ccip_oracle.go @@ -0,0 +1,122 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// CCIPOracle is an autogenerated mock type for the CCIPOracle type +type CCIPOracle struct { + mock.Mock +} + +type CCIPOracle_Expecter struct { + mock *mock.Mock +} + +func (_m *CCIPOracle) EXPECT() *CCIPOracle_Expecter { + return &CCIPOracle_Expecter{mock: &_m.Mock} +} + +// Close provides a mock function with given fields: +func (_m *CCIPOracle) Close() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Close") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CCIPOracle_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' +type CCIPOracle_Close_Call struct { + *mock.Call +} + +// Close is a helper method to define mock.On call +func (_e *CCIPOracle_Expecter) Close() *CCIPOracle_Close_Call { + return &CCIPOracle_Close_Call{Call: _e.mock.On("Close")} +} + +func (_c *CCIPOracle_Close_Call) Run(run func()) *CCIPOracle_Close_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CCIPOracle_Close_Call) Return(_a0 error) *CCIPOracle_Close_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *CCIPOracle_Close_Call) RunAndReturn(run func() error) *CCIPOracle_Close_Call { + _c.Call.Return(run) + return _c +} + +// Start provides a mock function with given fields: +func (_m *CCIPOracle) Start() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Start") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CCIPOracle_Start_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Start' +type CCIPOracle_Start_Call struct { + *mock.Call +} + +// Start is a helper method to define mock.On call +func (_e *CCIPOracle_Expecter) Start() *CCIPOracle_Start_Call { + return &CCIPOracle_Start_Call{Call: _e.mock.On("Start")} +} + +func (_c *CCIPOracle_Start_Call) Run(run func()) *CCIPOracle_Start_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CCIPOracle_Start_Call) Return(_a0 error) *CCIPOracle_Start_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *CCIPOracle_Start_Call) RunAndReturn(run func() error) *CCIPOracle_Start_Call { + _c.Call.Return(run) + return _c +} + +// NewCCIPOracle creates a new instance of CCIPOracle. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewCCIPOracle(t interface { + mock.TestingT + Cleanup(func()) +}) *CCIPOracle { + mock := &CCIPOracle{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/capabilities/ccip/types/mocks/home_chain_reader.go b/core/capabilities/ccip/types/mocks/home_chain_reader.go new file mode 100644 index 00000000000..a5a581a1d2d --- /dev/null +++ b/core/capabilities/ccip/types/mocks/home_chain_reader.go @@ -0,0 +1,129 @@ +package mocks + +import ( + "context" + + mapset "github.com/deckarep/golang-set/v2" + "github.com/stretchr/testify/mock" + + ccipreaderpkg "github.com/smartcontractkit/chainlink-ccip/pkg/reader" + + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" + + "github.com/smartcontractkit/libocr/ragep2p/types" +) + +var _ ccipreaderpkg.HomeChain = (*HomeChainReader)(nil) + +type HomeChainReader struct { + mock.Mock +} + +func (_m *HomeChainReader) GetChainConfig(chainSelector cciptypes.ChainSelector) (ccipreaderpkg.ChainConfig, error) { + //TODO implement me + panic("implement me") +} + +func (_m *HomeChainReader) GetAllChainConfigs() (map[cciptypes.ChainSelector]ccipreaderpkg.ChainConfig, error) { + //TODO implement me + panic("implement me") +} + +func (_m *HomeChainReader) GetSupportedChainsForPeer(id types.PeerID) (mapset.Set[cciptypes.ChainSelector], error) { + //TODO implement me + panic("implement me") +} + +func (_m *HomeChainReader) GetKnownCCIPChains() (mapset.Set[cciptypes.ChainSelector], error) { + //TODO implement me + panic("implement me") +} + +func (_m *HomeChainReader) GetFChain() (map[cciptypes.ChainSelector]int, error) { + //TODO implement me + panic("implement me") +} + +func (_m *HomeChainReader) Start(ctx context.Context) error { + //TODO implement me + panic("implement me") +} + +func (_m *HomeChainReader) Close() error { + //TODO implement me + panic("implement me") +} + +func (_m *HomeChainReader) HealthReport() map[string]error { + //TODO implement me + panic("implement me") +} + +func (_m *HomeChainReader) Name() string { + //TODO implement me + panic("implement me") +} + +// GetOCRConfigs provides a mock function with given fields: ctx, donID, pluginType +func (_m *HomeChainReader) GetOCRConfigs(ctx context.Context, donID uint32, pluginType uint8) ([]ccipreaderpkg.OCR3ConfigWithMeta, error) { + ret := _m.Called(ctx, donID, pluginType) + + if len(ret) == 0 { + panic("no return value specified for GetOCRConfigs") + } + + var r0 []ccipreaderpkg.OCR3ConfigWithMeta + var r1 error + if rf, ok := ret.Get(0).(func(ctx context.Context, donID uint32, pluginType uint8) ([]ccipreaderpkg.OCR3ConfigWithMeta, error)); ok { + return rf(ctx, donID, pluginType) + } + if rf, ok := ret.Get(0).(func(ctx context.Context, donID uint32, pluginType uint8) []ccipreaderpkg.OCR3ConfigWithMeta); ok { + r0 = rf(ctx, donID, pluginType) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]ccipreaderpkg.OCR3ConfigWithMeta) + } + } + + if rf, ok := ret.Get(1).(func(ctx context.Context, donID uint32, pluginType uint8) error); ok { + r1 = rf(ctx, donID, pluginType) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +func (_m *HomeChainReader) Ready() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Ready") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewHomeChainReader creates a new instance of HomeChainReader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewHomeChainReader(t interface { + mock.TestingT + Cleanup(func()) +}) *HomeChainReader { + mock := &HomeChainReader{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/capabilities/ccip/types/mocks/oracle_creator.go b/core/capabilities/ccip/types/mocks/oracle_creator.go new file mode 100644 index 00000000000..d83ad042bfe --- /dev/null +++ b/core/capabilities/ccip/types/mocks/oracle_creator.go @@ -0,0 +1,152 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +package mocks + +import ( + types "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" + mock "github.com/stretchr/testify/mock" +) + +// OracleCreator is an autogenerated mock type for the OracleCreator type +type OracleCreator struct { + mock.Mock +} + +type OracleCreator_Expecter struct { + mock *mock.Mock +} + +func (_m *OracleCreator) EXPECT() *OracleCreator_Expecter { + return &OracleCreator_Expecter{mock: &_m.Mock} +} + +// CreateBootstrapOracle provides a mock function with given fields: config +func (_m *OracleCreator) CreateBootstrapOracle(config types.OCR3ConfigWithMeta) (types.CCIPOracle, error) { + ret := _m.Called(config) + + if len(ret) == 0 { + panic("no return value specified for CreateBootstrapOracle") + } + + var r0 types.CCIPOracle + var r1 error + if rf, ok := ret.Get(0).(func(types.OCR3ConfigWithMeta) (types.CCIPOracle, error)); ok { + return rf(config) + } + if rf, ok := ret.Get(0).(func(types.OCR3ConfigWithMeta) types.CCIPOracle); ok { + r0 = rf(config) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(types.CCIPOracle) + } + } + + if rf, ok := ret.Get(1).(func(types.OCR3ConfigWithMeta) error); ok { + r1 = rf(config) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// OracleCreator_CreateBootstrapOracle_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateBootstrapOracle' +type OracleCreator_CreateBootstrapOracle_Call struct { + *mock.Call +} + +// CreateBootstrapOracle is a helper method to define mock.On call +// - config types.OCR3ConfigWithMeta +func (_e *OracleCreator_Expecter) CreateBootstrapOracle(config interface{}) *OracleCreator_CreateBootstrapOracle_Call { + return &OracleCreator_CreateBootstrapOracle_Call{Call: _e.mock.On("CreateBootstrapOracle", config)} +} + +func (_c *OracleCreator_CreateBootstrapOracle_Call) Run(run func(config types.OCR3ConfigWithMeta)) *OracleCreator_CreateBootstrapOracle_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(types.OCR3ConfigWithMeta)) + }) + return _c +} + +func (_c *OracleCreator_CreateBootstrapOracle_Call) Return(_a0 types.CCIPOracle, _a1 error) *OracleCreator_CreateBootstrapOracle_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *OracleCreator_CreateBootstrapOracle_Call) RunAndReturn(run func(types.OCR3ConfigWithMeta) (types.CCIPOracle, error)) *OracleCreator_CreateBootstrapOracle_Call { + _c.Call.Return(run) + return _c +} + +// CreatePluginOracle provides a mock function with given fields: pluginType, config +func (_m *OracleCreator) CreatePluginOracle(pluginType types.PluginType, config types.OCR3ConfigWithMeta) (types.CCIPOracle, error) { + ret := _m.Called(pluginType, config) + + if len(ret) == 0 { + panic("no return value specified for CreatePluginOracle") + } + + var r0 types.CCIPOracle + var r1 error + if rf, ok := ret.Get(0).(func(types.PluginType, types.OCR3ConfigWithMeta) (types.CCIPOracle, error)); ok { + return rf(pluginType, config) + } + if rf, ok := ret.Get(0).(func(types.PluginType, types.OCR3ConfigWithMeta) types.CCIPOracle); ok { + r0 = rf(pluginType, config) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(types.CCIPOracle) + } + } + + if rf, ok := ret.Get(1).(func(types.PluginType, types.OCR3ConfigWithMeta) error); ok { + r1 = rf(pluginType, config) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// OracleCreator_CreatePluginOracle_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreatePluginOracle' +type OracleCreator_CreatePluginOracle_Call struct { + *mock.Call +} + +// CreatePluginOracle is a helper method to define mock.On call +// - pluginType types.PluginType +// - config types.OCR3ConfigWithMeta +func (_e *OracleCreator_Expecter) CreatePluginOracle(pluginType interface{}, config interface{}) *OracleCreator_CreatePluginOracle_Call { + return &OracleCreator_CreatePluginOracle_Call{Call: _e.mock.On("CreatePluginOracle", pluginType, config)} +} + +func (_c *OracleCreator_CreatePluginOracle_Call) Run(run func(pluginType types.PluginType, config types.OCR3ConfigWithMeta)) *OracleCreator_CreatePluginOracle_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(types.PluginType), args[1].(types.OCR3ConfigWithMeta)) + }) + return _c +} + +func (_c *OracleCreator_CreatePluginOracle_Call) Return(_a0 types.CCIPOracle, _a1 error) *OracleCreator_CreatePluginOracle_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *OracleCreator_CreatePluginOracle_Call) RunAndReturn(run func(types.PluginType, types.OCR3ConfigWithMeta) (types.CCIPOracle, error)) *OracleCreator_CreatePluginOracle_Call { + _c.Call.Return(run) + return _c +} + +// NewOracleCreator creates a new instance of OracleCreator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewOracleCreator(t interface { + mock.TestingT + Cleanup(func()) +}) *OracleCreator { + mock := &OracleCreator{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/capabilities/ccip/types/types.go b/core/capabilities/ccip/types/types.go new file mode 100644 index 00000000000..952b8fe4465 --- /dev/null +++ b/core/capabilities/ccip/types/types.go @@ -0,0 +1,46 @@ +package types + +import ( + ccipreaderpkg "github.com/smartcontractkit/chainlink-ccip/pkg/reader" +) + +// OCR3ConfigWithMeta is a type alias in order to generate correct mocks for the OracleCreator interface. +type OCR3ConfigWithMeta ccipreaderpkg.OCR3ConfigWithMeta + +// PluginType represents the type of CCIP plugin. +// It mirrors the OCRPluginType in Internal.sol. +type PluginType uint8 + +const ( + PluginTypeCCIPCommit PluginType = 0 + PluginTypeCCIPExec PluginType = 1 +) + +func (pt PluginType) String() string { + switch pt { + case PluginTypeCCIPCommit: + return "CCIPCommit" + case PluginTypeCCIPExec: + return "CCIPExec" + default: + return "Unknown" + } +} + +// CCIPOracle represents either a CCIP commit or exec oracle or a bootstrap node. +type CCIPOracle interface { + Close() error + Start() error +} + +// OracleCreator is an interface for creating CCIP oracles. +// Whether the oracle uses a LOOPP or not is an implementation detail. +type OracleCreator interface { + // CreatePlugin creates a new oracle that will run either the commit or exec ccip plugin. + // The oracle must be returned unstarted. + CreatePluginOracle(pluginType PluginType, config OCR3ConfigWithMeta) (CCIPOracle, error) + + // CreateBootstrapOracle creates a new bootstrap node with the given OCR config. + // The oracle must be returned unstarted. + CreateBootstrapOracle(config OCR3ConfigWithMeta) (CCIPOracle, error) +} diff --git a/core/capabilities/ccip/validate/validate.go b/core/capabilities/ccip/validate/validate.go new file mode 100644 index 00000000000..04f4f4a4959 --- /dev/null +++ b/core/capabilities/ccip/validate/validate.go @@ -0,0 +1,94 @@ +package validate + +import ( + "fmt" + + "github.com/google/uuid" + "github.com/pelletier/go-toml" + + "github.com/smartcontractkit/chainlink/v2/core/services/job" + "github.com/smartcontractkit/chainlink/v2/core/services/ocrcommon" +) + +// ValidatedCCIPSpec validates the given toml string as a CCIP spec. +func ValidatedCCIPSpec(tomlString string) (jb job.Job, err error) { + var spec job.CCIPSpec + tree, err := toml.Load(tomlString) + if err != nil { + return job.Job{}, fmt.Errorf("toml error on load: %w", err) + } + // Note this validates all the fields which implement an UnmarshalText + err = tree.Unmarshal(&spec) + if err != nil { + return job.Job{}, fmt.Errorf("toml unmarshal error on spec: %w", err) + } + err = tree.Unmarshal(&jb) + if err != nil { + return job.Job{}, fmt.Errorf("toml unmarshal error on job: %w", err) + } + jb.CCIPSpec = &spec + + if jb.Type != job.CCIP { + return job.Job{}, fmt.Errorf("the only supported type is currently 'ccip', got %s", jb.Type) + } + if jb.CCIPSpec.CapabilityLabelledName == "" { + return job.Job{}, fmt.Errorf("capabilityLabelledName must be set") + } + if jb.CCIPSpec.CapabilityVersion == "" { + return job.Job{}, fmt.Errorf("capabilityVersion must be set") + } + if jb.CCIPSpec.P2PKeyID == "" { + return job.Job{}, fmt.Errorf("p2pKeyID must be set") + } + if len(jb.CCIPSpec.P2PV2Bootstrappers) == 0 { + return job.Job{}, fmt.Errorf("p2pV2Bootstrappers must be set") + } + + // ensure that the P2PV2Bootstrappers is in the right format. + for _, bootstrapperLocator := range jb.CCIPSpec.P2PV2Bootstrappers { + // needs to be of the form @: + _, err := ocrcommon.ParseBootstrapPeers([]string{bootstrapperLocator}) + if err != nil { + return job.Job{}, fmt.Errorf("p2p v2 bootstrapper locator %s is not in the correct format: %w", bootstrapperLocator, err) + } + } + + return jb, nil +} + +type SpecArgs struct { + P2PV2Bootstrappers []string `toml:"p2pV2Bootstrappers"` + CapabilityVersion string `toml:"capabilityVersion"` + CapabilityLabelledName string `toml:"capabilityLabelledName"` + OCRKeyBundleIDs map[string]string `toml:"ocrKeyBundleIDs"` + P2PKeyID string `toml:"p2pKeyID"` + RelayConfigs map[string]any `toml:"relayConfigs"` + PluginConfig map[string]any `toml:"pluginConfig"` +} + +// NewCCIPSpecToml creates a new CCIP spec in toml format from the given spec args. +func NewCCIPSpecToml(spec SpecArgs) (string, error) { + type fullSpec struct { + SpecArgs + Type string `toml:"type"` + SchemaVersion uint64 `toml:"schemaVersion"` + Name string `toml:"name"` + ExternalJobID string `toml:"externalJobID"` + } + extJobID, err := uuid.NewRandom() + if err != nil { + return "", fmt.Errorf("failed to generate external job id: %w", err) + } + marshaled, err := toml.Marshal(fullSpec{ + SpecArgs: spec, + Type: "ccip", + SchemaVersion: 1, + Name: fmt.Sprintf("%s-%s", "ccip", extJobID.String()), + ExternalJobID: extJobID.String(), + }) + if err != nil { + return "", fmt.Errorf("failed to marshal spec into toml: %w", err) + } + + return string(marshaled), nil +} diff --git a/core/capabilities/ccip/validate/validate_test.go b/core/capabilities/ccip/validate/validate_test.go new file mode 100644 index 00000000000..97958f4cf9d --- /dev/null +++ b/core/capabilities/ccip/validate/validate_test.go @@ -0,0 +1,58 @@ +package validate_test + +import ( + "testing" + + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/validate" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/v2/core/services/job" +) + +func TestNewCCIPSpecToml(t *testing.T) { + tests := []struct { + name string + specArgs validate.SpecArgs + want string + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := validate.NewCCIPSpecToml(tt.specArgs) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.want, got) + } + }) + } +} + +func TestValidatedCCIPSpec(t *testing.T) { + type args struct { + tomlString string + } + tests := []struct { + name string + args args + wantJb job.Job + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotJb, err := validate.ValidatedCCIPSpec(tt.args.tomlString) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.wantJb, gotJb) + } + }) + } +} diff --git a/core/capabilities/launcher.go b/core/capabilities/launcher.go index b30477e4c83..3fc321087b8 100644 --- a/core/capabilities/launcher.go +++ b/core/capabilities/launcher.go @@ -7,13 +7,17 @@ import ( "strings" "time" + "google.golang.org/protobuf/proto" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/capabilities/triggers" "github.com/smartcontractkit/chainlink-common/pkg/services" + "github.com/smartcontractkit/chainlink-common/pkg/values" "github.com/smartcontractkit/libocr/ragep2p" ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote" "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/target" remotetypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/types" @@ -46,6 +50,42 @@ type launcher struct { subServices []services.Service } +func unmarshalCapabilityConfig(data []byte) (capabilities.CapabilityConfiguration, error) { + cconf := &capabilitiespb.CapabilityConfig{} + err := proto.Unmarshal(data, cconf) + if err != nil { + return capabilities.CapabilityConfiguration{}, err + } + + var remoteTriggerConfig *capabilities.RemoteTriggerConfig + var remoteTargetConfig *capabilities.RemoteTargetConfig + + switch cconf.GetRemoteConfig().(type) { + case *capabilitiespb.CapabilityConfig_RemoteTriggerConfig: + prtc := cconf.GetRemoteTriggerConfig() + remoteTriggerConfig = &capabilities.RemoteTriggerConfig{} + remoteTriggerConfig.RegistrationRefresh = prtc.RegistrationRefresh.AsDuration() + remoteTriggerConfig.RegistrationExpiry = prtc.RegistrationExpiry.AsDuration() + remoteTriggerConfig.MinResponsesToAggregate = prtc.MinResponsesToAggregate + remoteTriggerConfig.MessageExpiry = prtc.MessageExpiry.AsDuration() + case *capabilitiespb.CapabilityConfig_RemoteTargetConfig: + prtc := cconf.GetRemoteTargetConfig() + remoteTargetConfig = &capabilities.RemoteTargetConfig{} + remoteTargetConfig.RequestHashExcludedAttributes = prtc.RequestHashExcludedAttributes + } + + dc, err := values.FromMapValueProto(cconf.DefaultConfig) + if err != nil { + return capabilities.CapabilityConfiguration{}, err + } + + return capabilities.CapabilityConfiguration{ + DefaultConfig: dc, + RemoteTriggerConfig: remoteTriggerConfig, + RemoteTargetConfig: remoteTargetConfig, + }, nil +} + func NewLauncher( lggr logger.Logger, peerWrapper p2ptypes.PeerWrapper, @@ -196,6 +236,11 @@ func (w *launcher) addRemoteCapabilities(ctx context.Context, myDON registrysync return fmt.Errorf("could not find capability matching id %s", cid) } + capabilityConfig, err := unmarshalCapabilityConfig(c.Config) + if err != nil { + return fmt.Errorf("could not unmarshal capability config for id %s", cid) + } + switch capability.CapabilityType { case capabilities.CapabilityTypeTrigger: newTriggerFn := func(info capabilities.CapabilityInfo) (capabilityService, error) { @@ -224,7 +269,7 @@ func (w *launcher) addRemoteCapabilities(ctx context.Context, myDON registrysync // When this is solved, we can move to a generic aggregator // and remove this. triggerCap := remote.NewTriggerSubscriber( - c.RemoteTriggerConfig, + capabilityConfig.RemoteTriggerConfig, info, remoteDON.DON, myDON.DON, @@ -333,11 +378,16 @@ func (w *launcher) exposeCapabilities(ctx context.Context, myPeerID p2ptypes.Pee return fmt.Errorf("could not find capability matching id %s", cid) } + capabilityConfig, err := unmarshalCapabilityConfig(c.Config) + if err != nil { + return fmt.Errorf("could not unmarshal capability config for id %s", cid) + } + switch capability.CapabilityType { case capabilities.CapabilityTypeTrigger: newTriggerPublisher := func(capability capabilities.BaseCapability, info capabilities.CapabilityInfo) (receiverService, error) { publisher := remote.NewTriggerPublisher( - c.RemoteTriggerConfig, + capabilityConfig.RemoteTriggerConfig, capability.(capabilities.TriggerCapability), info, don.DON, @@ -359,7 +409,7 @@ func (w *launcher) exposeCapabilities(ctx context.Context, myPeerID p2ptypes.Pee case capabilities.CapabilityTypeTarget: newTargetServer := func(capability capabilities.BaseCapability, info capabilities.CapabilityInfo) (receiverService, error) { return target.NewServer( - c.RemoteTargetConfig, + capabilityConfig.RemoteTargetConfig, myPeerID, capability.(capabilities.TargetCapability), info, diff --git a/core/capabilities/launcher_test.go b/core/capabilities/launcher_test.go index 82b03edcecb..8bca3be0db1 100644 --- a/core/capabilities/launcher_test.go +++ b/core/capabilities/launcher_test.go @@ -4,14 +4,18 @@ import ( "context" "crypto/rand" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/durationpb" ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote" remoteMocks "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/types/mocks" @@ -121,7 +125,7 @@ func TestLauncher_WiresUpExternalCapabilities(t *testing.T) { AcceptsWorkflows: true, Members: nodes, }, - CapabilityConfigurations: map[string]capabilities.CapabilityConfiguration{ + CapabilityConfigurations: map[string]registrysyncer.CapabilityConfiguration{ fullTriggerCapID: {}, fullTargetID: {}, }, @@ -223,7 +227,7 @@ func TestSyncer_IgnoresCapabilitiesForPrivateDON(t *testing.T) { AcceptsWorkflows: true, Members: nodes, }, - CapabilityConfigurations: map[string]capabilities.CapabilityConfiguration{ + CapabilityConfigurations: map[string]registrysyncer.CapabilityConfiguration{ triggerID: {}, targetID: {}, }, @@ -326,6 +330,15 @@ func TestLauncher_WiresUpClientsForPublicWorkflowDON(t *testing.T) { rtc := &capabilities.RemoteTriggerConfig{} rtc.ApplyDefaults() + cfg, err := proto.Marshal(&capabilitiespb.CapabilityConfig{ + RemoteConfig: &capabilitiespb.CapabilityConfig_RemoteTriggerConfig{ + RemoteTriggerConfig: &capabilitiespb.RemoteTriggerConfig{ + RegistrationRefresh: durationpb.New(1 * time.Second), + }, + }, + }) + require.NoError(t, err) + state := ®istrysyncer.LocalRegistry{ IDsToDONs: map[registrysyncer.DonID]registrysyncer.DON{ registrysyncer.DonID(dID): { @@ -347,12 +360,12 @@ func TestLauncher_WiresUpClientsForPublicWorkflowDON(t *testing.T) { AcceptsWorkflows: false, Members: capabilityDonNodes, }, - CapabilityConfigurations: map[string]capabilities.CapabilityConfiguration{ + CapabilityConfigurations: map[string]registrysyncer.CapabilityConfiguration{ fullTriggerCapID: { - RemoteTriggerConfig: rtc, + Config: cfg, }, fullTargetID: { - RemoteTriggerConfig: rtc, + Config: cfg, }, }, }, @@ -496,7 +509,7 @@ func TestLauncher_WiresUpClientsForPublicWorkflowDONButIgnoresPrivateCapabilitie AcceptsWorkflows: false, Members: capabilityDonNodes, }, - CapabilityConfigurations: map[string]capabilities.CapabilityConfiguration{ + CapabilityConfigurations: map[string]registrysyncer.CapabilityConfiguration{ fullTriggerCapID: {}, }, }, @@ -509,7 +522,7 @@ func TestLauncher_WiresUpClientsForPublicWorkflowDONButIgnoresPrivateCapabilitie AcceptsWorkflows: false, Members: capabilityDonNodes, }, - CapabilityConfigurations: map[string]capabilities.CapabilityConfiguration{ + CapabilityConfigurations: map[string]registrysyncer.CapabilityConfiguration{ fullTargetID: {}, }, }, @@ -653,7 +666,7 @@ func TestLauncher_SucceedsEvenIfDispatcherAlreadyHasReceiver(t *testing.T) { AcceptsWorkflows: false, Members: capabilityDonNodes, }, - CapabilityConfigurations: map[string]capabilities.CapabilityConfiguration{ + CapabilityConfigurations: map[string]registrysyncer.CapabilityConfiguration{ fullTriggerCapID: {}, }, }, diff --git a/core/capabilities/registry.go b/core/capabilities/registry.go index d6891c81ab9..4da51a27b6b 100644 --- a/core/capabilities/registry.go +++ b/core/capabilities/registry.go @@ -8,6 +8,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/registrysyncer" ) var ( @@ -16,7 +17,7 @@ var ( type metadataRegistry interface { LocalNode(ctx context.Context) (capabilities.Node, error) - ConfigForCapability(ctx context.Context, capabilityID string, donID uint32) (capabilities.CapabilityConfiguration, error) + ConfigForCapability(ctx context.Context, capabilityID string, donID uint32) (registrysyncer.CapabilityConfiguration, error) } // Registry is a struct for the registry of capabilities. @@ -43,7 +44,12 @@ func (r *Registry) ConfigForCapability(ctx context.Context, capabilityID string, return capabilities.CapabilityConfiguration{}, errors.New("metadataRegistry information not available") } - return r.metadataRegistry.ConfigForCapability(ctx, capabilityID, donID) + cfc, err := r.metadataRegistry.ConfigForCapability(ctx, capabilityID, donID) + if err != nil { + return capabilities.CapabilityConfiguration{}, err + } + + return unmarshalCapabilityConfig(cfc.Config) } // SetLocalRegistry sets a local copy of the offchain registry for the registry to use. diff --git a/core/chains/evm/client/simulated_backend_client.go b/core/chains/evm/client/simulated_backend_client.go index 6bcc1f36960..7dfd39f444c 100644 --- a/core/chains/evm/client/simulated_backend_client.go +++ b/core/chains/evm/client/simulated_backend_client.go @@ -360,9 +360,18 @@ func (c *SimulatedBackendClient) SendTransactionReturnCode(ctx context.Context, // SendTransaction sends a transaction. func (c *SimulatedBackendClient) SendTransaction(ctx context.Context, tx *types.Transaction) error { - sender, err := types.Sender(types.NewLondonSigner(c.chainId), tx) + var ( + sender common.Address + err error + ) + // try to recover the sender from the transaction using the configured chain id + // first. if that fails, try again with the simulated chain id (1337) + sender, err = types.Sender(types.NewLondonSigner(c.chainId), tx) if err != nil { - logger.Test(c.t).Panic(fmt.Errorf("invalid transaction: %v (tx: %#v)", err, tx)) + sender, err = types.Sender(types.NewLondonSigner(big.NewInt(1337)), tx) + if err != nil { + logger.Test(c.t).Panic(fmt.Errorf("invalid transaction: %v (tx: %#v)", err, tx)) + } } pendingNonce, err := c.b.PendingNonceAt(ctx, sender) if err != nil { diff --git a/core/scripts/go.mod b/core/scripts/go.mod index 94504897ab0..cd136127431 100644 --- a/core/scripts/go.mod +++ b/core/scripts/go.mod @@ -102,7 +102,7 @@ require ( github.com/danieljoos/wincred v1.1.2 // indirect github.com/danielkov/gin-helmet v0.0.0-20171108135313-1387e224435e // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/deckarep/golang-set/v2 v2.3.0 // indirect + github.com/deckarep/golang-set/v2 v2.6.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/dfuse-io/logging v0.0.0-20210109005628-b97a57253f70 // indirect github.com/dgraph-io/badger/v2 v2.2007.4 // indirect @@ -270,6 +270,7 @@ require ( github.com/shirou/gopsutil v3.21.11+incompatible // indirect github.com/shirou/gopsutil/v3 v3.24.3 // indirect github.com/smartcontractkit/chain-selectors v1.0.10 // indirect + github.com/smartcontractkit/chainlink-ccip v0.0.0-20240806144315-04ac101e9c95 // indirect github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240710121324-3ed288aa9b45 // indirect github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240801131703-fd75761c982f // indirect github.com/smartcontractkit/chainlink-feeds v0.0.0-20240710170203-5b41615da827 // indirect @@ -330,7 +331,7 @@ require ( go.uber.org/zap v1.27.0 // indirect golang.org/x/arch v0.8.0 // indirect golang.org/x/crypto v0.25.0 // indirect - golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/mod v0.19.0 // indirect golang.org/x/net v0.27.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect diff --git a/core/scripts/go.sum b/core/scripts/go.sum index f770498cff8..d8ca90e8b47 100644 --- a/core/scripts/go.sum +++ b/core/scripts/go.sum @@ -330,8 +330,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/deckarep/golang-set/v2 v2.3.0 h1:qs18EKUfHm2X9fA50Mr/M5hccg2tNnVqsiBImnyDs0g= -github.com/deckarep/golang-set/v2 v2.3.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= @@ -1184,6 +1184,8 @@ github.com/smartcontractkit/chain-selectors v1.0.10 h1:t9kJeE6B6G+hKD0GYR4kGJSCq github.com/smartcontractkit/chain-selectors v1.0.10/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= github.com/smartcontractkit/chainlink-automation v1.0.4 h1:iyW181JjKHLNMnDleI8umfIfVVlwC7+n5izbLSFgjw8= github.com/smartcontractkit/chainlink-automation v1.0.4/go.mod h1:u4NbPZKJ5XiayfKHD/v3z3iflQWqvtdhj13jVZXj/cM= +github.com/smartcontractkit/chainlink-ccip v0.0.0-20240806144315-04ac101e9c95 h1:LAgJTg9Yr/uCo2g7Krp88Dco2U45Y6sbJVl8uKoLkys= +github.com/smartcontractkit/chainlink-ccip v0.0.0-20240806144315-04ac101e9c95/go.mod h1:/ZWraCBaDDgaIN1prixYcbVvIk/6HeED9+8zbWQ+TMo= github.com/smartcontractkit/chainlink-common v0.2.2-0.20240805160614-501c4f40b98c h1:3apUsez/6Pkp1ckXzSwIhzPRuWjDGjzMjKapEKi0Fcw= github.com/smartcontractkit/chainlink-common v0.2.2-0.20240805160614-501c4f40b98c/go.mod h1:Jg1sCTsbxg76YByI8ifpFby3FvVqISStHT8ypy9ocmY= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240710121324-3ed288aa9b45 h1:NBQLtqk8zsyY4qTJs+NElI3aDFTcAo83JHvqD04EvB0= @@ -1484,8 +1486,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7 h1:wDLEX9a7YQoKdKNQt88rtydkqDxeGaBUTnIYc3iG/mA= -golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/core/services/chainlink/application.go b/core/services/chainlink/application.go index c23ec08a692..6a381b1ffa8 100644 --- a/core/services/chainlink/application.go +++ b/core/services/chainlink/application.go @@ -23,15 +23,14 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/utils" "github.com/smartcontractkit/chainlink-common/pkg/utils/jsonserializable" "github.com/smartcontractkit/chainlink-common/pkg/utils/mailbox" - "github.com/smartcontractkit/chainlink/v2/core/capabilities" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" - "github.com/smartcontractkit/chainlink/v2/core/services/standardcapabilities" - "github.com/smartcontractkit/chainlink/v2/core/static" "github.com/smartcontractkit/chainlink/v2/core/bridges" "github.com/smartcontractkit/chainlink/v2/core/build" + "github.com/smartcontractkit/chainlink/v2/core/capabilities" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip" "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote" remotetypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/types" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr" evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" evmutils "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" @@ -61,6 +60,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/registrysyncer" "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/mercury" "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/mercury/wsrpc" + "github.com/smartcontractkit/chainlink/v2/core/services/standardcapabilities" "github.com/smartcontractkit/chainlink/v2/core/services/streams" "github.com/smartcontractkit/chainlink/v2/core/services/telemetry" "github.com/smartcontractkit/chainlink/v2/core/services/vrf" @@ -70,6 +70,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/sessions" "github.com/smartcontractkit/chainlink/v2/core/sessions/ldapauth" "github.com/smartcontractkit/chainlink/v2/core/sessions/localauth" + "github.com/smartcontractkit/chainlink/v2/core/static" "github.com/smartcontractkit/chainlink/v2/plugins" ) @@ -212,41 +213,51 @@ func NewApplication(opts ApplicationOpts) (Application, error) { externalPeer := externalp2p.NewExternalPeerWrapper(keyStore.P2P(), cfg.Capabilities().Peering(), opts.DS, globalLogger) signer := externalPeer externalPeerWrapper = externalPeer - dispatcher = remote.NewDispatcher(externalPeerWrapper, signer, opts.CapabilitiesRegistry, globalLogger) - srvcs = append(srvcs, externalPeerWrapper) // peer wrapper must be started before dispatcher - srvcs = append(srvcs, dispatcher) - } else { // tests only + remoteDispatcher := remote.NewDispatcher(externalPeerWrapper, signer, opts.CapabilitiesRegistry, globalLogger) + srvcs = append(srvcs, remoteDispatcher) + + dispatcher = remoteDispatcher + } else { dispatcher = opts.CapabilitiesDispatcher externalPeerWrapper = opts.CapabilitiesPeerWrapper - srvcs = append(srvcs, externalPeerWrapper) } - rid := cfg.Capabilities().ExternalRegistry().RelayID() - registryAddress := cfg.Capabilities().ExternalRegistry().Address() - relayer, err := relayerChainInterops.Get(rid) - if err != nil { - return nil, fmt.Errorf("could not fetch relayer %s configured for capabilities registry: %w", rid, err) - } + srvcs = append(srvcs, externalPeerWrapper, dispatcher) - registrySyncer, err := registrysyncer.New( - globalLogger, - externalPeerWrapper, - relayer, - registryAddress, - ) - if err != nil { - return nil, fmt.Errorf("could not configure syncer: %w", err) - } + if cfg.Capabilities().ExternalRegistry().Address() != "" { + rid := cfg.Capabilities().ExternalRegistry().RelayID() + registryAddress := cfg.Capabilities().ExternalRegistry().Address() + relayer, err := relayerChainInterops.Get(rid) + if err != nil { + return nil, fmt.Errorf("could not fetch relayer %s configured for capabilities registry: %w", rid, err) + } + registrySyncer, err := registrysyncer.New( + globalLogger, + func() (p2ptypes.PeerID, error) { + p := externalPeerWrapper.GetPeer() + if p == nil { + return p2ptypes.PeerID{}, errors.New("could not get peer") + } - wfLauncher := capabilities.NewLauncher( - globalLogger, - externalPeerWrapper, - dispatcher, - opts.CapabilitiesRegistry, - ) - registrySyncer.AddLauncher(wfLauncher) + return p.ID(), nil + }, + relayer, + registryAddress, + ) + if err != nil { + return nil, fmt.Errorf("could not configure syncer: %w", err) + } + + wfLauncher := capabilities.NewLauncher( + globalLogger, + externalPeerWrapper, + dispatcher, + opts.CapabilitiesRegistry, + ) + registrySyncer.AddLauncher(wfLauncher) - srvcs = append(srvcs, dispatcher, wfLauncher, registrySyncer) + srvcs = append(srvcs, wfLauncher, registrySyncer) + } } // LOOPs can be created as options, in the case of LOOP relayers, or @@ -512,6 +523,18 @@ func NewApplication(opts ApplicationOpts) (Application, error) { cfg.Insecure(), opts.RelayerChainInteroperators, ) + delegates[job.CCIP] = ccip.NewDelegate( + globalLogger, + loopRegistrarConfig, + pipelineRunner, + opts.RelayerChainInteroperators.LegacyEVMChains(), + relayerChainInterops, + opts.KeyStore, + opts.DS, + peerWrapper, + telemetryManager, + cfg.Capabilities(), + ) } else { globalLogger.Debug("Off-chain reporting v2 disabled") } diff --git a/core/services/job/models.go b/core/services/job/models.go index 2f864efe300..1c46d08c59c 100644 --- a/core/services/job/models.go +++ b/core/services/job/models.go @@ -38,6 +38,7 @@ const ( BlockhashStore Type = (Type)(pipeline.BlockhashStoreJobType) Bootstrap Type = (Type)(pipeline.BootstrapJobType) Cron Type = (Type)(pipeline.CronJobType) + CCIP Type = (Type)(pipeline.CCIPJobType) DirectRequest Type = (Type)(pipeline.DirectRequestJobType) FluxMonitor Type = (Type)(pipeline.FluxMonitorJobType) Gateway Type = (Type)(pipeline.GatewayJobType) @@ -78,6 +79,7 @@ var ( BlockhashStore: false, Bootstrap: false, Cron: true, + CCIP: false, DirectRequest: true, FluxMonitor: true, Gateway: false, @@ -97,6 +99,7 @@ var ( BlockhashStore: false, Bootstrap: false, Cron: true, + CCIP: false, DirectRequest: true, FluxMonitor: false, Gateway: false, @@ -116,6 +119,7 @@ var ( BlockhashStore: 1, Bootstrap: 1, Cron: 1, + CCIP: 1, DirectRequest: 1, FluxMonitor: 1, Gateway: 1, @@ -176,6 +180,7 @@ type Job struct { StandardCapabilitiesSpecID *int32 StandardCapabilitiesSpec *StandardCapabilitiesSpec CCIPSpecID *int32 + CCIPSpec *CCIPSpec CCIPBootstrapSpecID *int32 JobSpecErrors []SpecError Type Type `toml:"type"` @@ -910,3 +915,48 @@ func (w *StandardCapabilitiesSpec) SetID(value string) error { w.ID = int32(ID) return nil } + +type CCIPSpec struct { + ID int32 + CreatedAt time.Time `toml:"-"` + UpdatedAt time.Time `toml:"-"` + + // P2PV2Bootstrappers is a list of "peer_id@ip_address:port" strings that are used to + // identify the bootstrap nodes of the P2P network. + // These bootstrappers will be used to bootstrap all CCIP DONs. + P2PV2Bootstrappers pq.StringArray `toml:"p2pV2Bootstrappers" db:"p2pv2_bootstrappers"` + + // CapabilityVersion is the semantic version of the CCIP capability. + // This capability version must exist in the onchain capability registry. + CapabilityVersion string `toml:"capabilityVersion" db:"capability_version"` + + // CapabilityLabelledName is the labelled name of the CCIP capability. + // Corresponds to the labelled name of the capability in the onchain capability registry. + CapabilityLabelledName string `toml:"capabilityLabelledName" db:"capability_labelled_name"` + + // OCRKeyBundleIDs is a mapping from chain type to OCR key bundle ID. + // These are explicitly specified here so that we don't run into strange errors auto-detecting + // the valid bundle, since nops can create as many bundles as they want. + // This will most likely never change for a particular CCIP capability version, + // since new chain families will likely require a new capability version. + // {"evm": "evm_key_bundle_id", "solana": "solana_key_bundle_id", ... } + OCRKeyBundleIDs JSONConfig `toml:"ocrKeyBundleIDs" db:"ocr_key_bundle_ids"` + + // RelayConfigs consists of relay specific configuration. + // Chain reader configurations are stored here, and are defined on a chain family basis, e.g + // we will have one chain reader config for EVM, one for solana, starknet, etc. + // Chain writer configurations are also stored here, and are also defined on a chain family basis, + // e.g we will have one chain writer config for EVM, one for solana, starknet, etc. + // See tests for examples of relay configs in TOML. + // { "evm": {"chainReader": {...}, "chainWriter": {...}}, "solana": {...}, ... } + // see core/services/relay/evm/types/types.go for EVM configs. + RelayConfigs JSONConfig `toml:"relayConfigs" db:"relay_configs"` + + // P2PKeyID is the ID of the P2P key of the node. + // This must be present in the capability registry otherwise the job will not start correctly. + P2PKeyID string `toml:"p2pKeyID" db:"p2p_key_id"` + + // PluginConfig contains plugin-specific config, like token price pipelines + // and RMN network info for offchain blessing. + PluginConfig JSONConfig `toml:"pluginConfig"` +} diff --git a/core/services/job/orm.go b/core/services/job/orm.go index d13decc7208..ac3bb655306 100644 --- a/core/services/job/orm.go +++ b/core/services/job/orm.go @@ -425,7 +425,34 @@ func (o *orm) CreateJob(ctx context.Context, jb *Job) error { return errors.Wrap(err, "failed to create StandardCapabilities for jobSpec") } jb.StandardCapabilitiesSpecID = &specID - + case CCIP: + sql := `INSERT INTO ccip_specs ( + capability_version, + capability_labelled_name, + ocr_key_bundle_ids, + p2p_key_id, + p2pv2_bootstrappers, + relay_configs, + plugin_config, + created_at, + updated_at + ) VALUES ( + :capability_version, + :capability_labelled_name, + :ocr_key_bundle_ids, + :p2p_key_id, + :p2pv2_bootstrappers, + :relay_configs, + :plugin_config, + NOW(), + NOW() + ) + RETURNING id;` + specID, err := tx.prepareQuerySpecID(ctx, sql, jb.CCIPSpec) + if err != nil { + return errors.Wrap(err, "failed to create CCIPSpec for jobSpec") + } + jb.CCIPSpecID = &specID default: o.lggr.Panicf("Unsupported jb.Type: %v", jb.Type) } @@ -643,19 +670,19 @@ func (o *orm) InsertJob(ctx context.Context, job *Job) error { // if job has id, emplace otherwise insert with a new id. if job.ID == 0 { query = `INSERT INTO jobs (name, stream_id, schema_version, type, max_task_duration, ocr_oracle_spec_id, ocr2_oracle_spec_id, direct_request_spec_id, flux_monitor_spec_id, - keeper_spec_id, cron_spec_id, vrf_spec_id, webhook_spec_id, blockhash_store_spec_id, bootstrap_spec_id, block_header_feeder_spec_id, gateway_spec_id, - legacy_gas_station_server_spec_id, legacy_gas_station_sidecar_spec_id, workflow_spec_id, standard_capabilities_spec_id, external_job_id, gas_limit, forwarding_allowed, created_at) + keeper_spec_id, cron_spec_id, vrf_spec_id, webhook_spec_id, blockhash_store_spec_id, bootstrap_spec_id, block_header_feeder_spec_id, gateway_spec_id, + legacy_gas_station_server_spec_id, legacy_gas_station_sidecar_spec_id, workflow_spec_id, standard_capabilities_spec_id, ccip_spec_id, external_job_id, gas_limit, forwarding_allowed, created_at) VALUES (:name, :stream_id, :schema_version, :type, :max_task_duration, :ocr_oracle_spec_id, :ocr2_oracle_spec_id, :direct_request_spec_id, :flux_monitor_spec_id, - :keeper_spec_id, :cron_spec_id, :vrf_spec_id, :webhook_spec_id, :blockhash_store_spec_id, :bootstrap_spec_id, :block_header_feeder_spec_id, :gateway_spec_id, - :legacy_gas_station_server_spec_id, :legacy_gas_station_sidecar_spec_id, :workflow_spec_id, :standard_capabilities_spec_id, :external_job_id, :gas_limit, :forwarding_allowed, NOW()) + :keeper_spec_id, :cron_spec_id, :vrf_spec_id, :webhook_spec_id, :blockhash_store_spec_id, :bootstrap_spec_id, :block_header_feeder_spec_id, :gateway_spec_id, + :legacy_gas_station_server_spec_id, :legacy_gas_station_sidecar_spec_id, :workflow_spec_id, :standard_capabilities_spec_id, :ccip_spec_id, :external_job_id, :gas_limit, :forwarding_allowed, NOW()) RETURNING *;` } else { query = `INSERT INTO jobs (id, name, stream_id, schema_version, type, max_task_duration, ocr_oracle_spec_id, ocr2_oracle_spec_id, direct_request_spec_id, flux_monitor_spec_id, - keeper_spec_id, cron_spec_id, vrf_spec_id, webhook_spec_id, blockhash_store_spec_id, bootstrap_spec_id, block_header_feeder_spec_id, gateway_spec_id, - legacy_gas_station_server_spec_id, legacy_gas_station_sidecar_spec_id, workflow_spec_id, standard_capabilities_spec_id, external_job_id, gas_limit, forwarding_allowed, created_at) + keeper_spec_id, cron_spec_id, vrf_spec_id, webhook_spec_id, blockhash_store_spec_id, bootstrap_spec_id, block_header_feeder_spec_id, gateway_spec_id, + legacy_gas_station_server_spec_id, legacy_gas_station_sidecar_spec_id, workflow_spec_id, standard_capabilities_spec_id, ccip_spec_id, external_job_id, gas_limit, forwarding_allowed, created_at) VALUES (:id, :name, :stream_id, :schema_version, :type, :max_task_duration, :ocr_oracle_spec_id, :ocr2_oracle_spec_id, :direct_request_spec_id, :flux_monitor_spec_id, - :keeper_spec_id, :cron_spec_id, :vrf_spec_id, :webhook_spec_id, :blockhash_store_spec_id, :bootstrap_spec_id, :block_header_feeder_spec_id, :gateway_spec_id, - :legacy_gas_station_server_spec_id, :legacy_gas_station_sidecar_spec_id, :workflow_spec_id, :standard_capabilities_spec_id, :external_job_id, :gas_limit, :forwarding_allowed, NOW()) + :keeper_spec_id, :cron_spec_id, :vrf_spec_id, :webhook_spec_id, :blockhash_store_spec_id, :bootstrap_spec_id, :block_header_feeder_spec_id, :gateway_spec_id, + :legacy_gas_station_server_spec_id, :legacy_gas_station_sidecar_spec_id, :workflow_spec_id, :standard_capabilities_spec_id, :ccip_spec_id, :external_job_id, :gas_limit, :forwarding_allowed, NOW()) RETURNING *;` } query, args, err := tx.ds.BindNamed(query, job) @@ -699,7 +726,8 @@ func (o *orm) DeleteJob(ctx context.Context, id int32) error { block_header_feeder_spec_id, gateway_spec_id, workflow_spec_id, - standard_capabilities_spec_id + standard_capabilities_spec_id, + ccip_spec_id ), deleted_oracle_specs AS ( DELETE FROM ocr_oracle_specs WHERE id IN (SELECT ocr_oracle_spec_id FROM deleted_jobs) @@ -742,7 +770,10 @@ func (o *orm) DeleteJob(ctx context.Context, id int32) error { ), deleted_standardcapabilities_specs AS ( DELETE FROM standardcapabilities_specs WHERE id in (SELECT standard_capabilities_spec_id FROM deleted_jobs) - ), + ), + deleted_ccip_specs AS ( + DELETE FROM ccip_specs WHERE id in (SELECT ccip_spec_id FROM deleted_jobs) + ), deleted_job_pipeline_specs AS ( DELETE FROM job_pipeline_specs WHERE job_id IN (SELECT id FROM deleted_jobs) RETURNING pipeline_spec_id ) @@ -816,7 +847,7 @@ func (o *orm) FindJobs(ctx context.Context, offset, limit int) (jobs []Job, coun return fmt.Errorf("failed to query jobs count: %w", err) } - sql = `SELECT jobs.*, job_pipeline_specs.pipeline_spec_id as pipeline_spec_id + sql = `SELECT jobs.*, job_pipeline_specs.pipeline_spec_id as pipeline_spec_id FROM jobs JOIN job_pipeline_specs ON (jobs.id = job_pipeline_specs.job_id) ORDER BY jobs.created_at DESC, jobs.id DESC OFFSET $1 LIMIT $2;` @@ -1030,8 +1061,8 @@ func (o *orm) findJob(ctx context.Context, jb *Job, col string, arg interface{}) } func (o *orm) FindJobIDsWithBridge(ctx context.Context, name string) (jids []int32, err error) { - query := `SELECT - jobs.id, pipeline_specs.dot_dag_source + query := `SELECT + jobs.id, pipeline_specs.dot_dag_source FROM jobs JOIN job_pipeline_specs ON job_pipeline_specs.job_id = jobs.id JOIN pipeline_specs ON pipeline_specs.id = job_pipeline_specs.pipeline_spec_id @@ -1078,7 +1109,7 @@ func (o *orm) FindJobIDsWithBridge(ctx context.Context, name string) (jids []int func (o *orm) FindJobIDByWorkflow(ctx context.Context, spec WorkflowSpec) (jobID int32, err error) { stmt := ` SELECT jobs.id FROM jobs -INNER JOIN workflow_specs ws on jobs.workflow_spec_id = ws.id AND ws.workflow_owner = $1 AND ws.workflow_name = $2 +INNER JOIN workflow_specs ws on jobs.workflow_spec_id = ws.id AND ws.workflow_owner = $1 AND ws.workflow_name = $2 ` err = o.ds.GetContext(ctx, &jobID, stmt, spec.WorkflowOwner, spec.WorkflowName) if err != nil { @@ -1391,6 +1422,7 @@ func (o *orm) loadAllJobTypes(ctx context.Context, job *Job) error { o.loadJobType(ctx, job, "GatewaySpec", "gateway_specs", job.GatewaySpecID), o.loadJobType(ctx, job, "WorkflowSpec", "workflow_specs", job.WorkflowSpecID), o.loadJobType(ctx, job, "StandardCapabilitiesSpec", "standardcapabilities_specs", job.StandardCapabilitiesSpecID), + o.loadJobType(ctx, job, "CCIPSpec", "ccip_specs", job.CCIPSpecID), ) } @@ -1428,7 +1460,7 @@ func (o *orm) loadJobPipelineSpec(ctx context.Context, job *Job, id *int32) erro ctx, pipelineSpecRow, `SELECT pipeline_specs.*, job_pipeline_specs.job_id as job_id - FROM pipeline_specs + FROM pipeline_specs JOIN job_pipeline_specs ON(pipeline_specs.id = job_pipeline_specs.pipeline_spec_id) WHERE job_pipeline_specs.job_id = $1 AND job_pipeline_specs.pipeline_spec_id = $2`, job.ID, *id, diff --git a/core/services/pipeline/common.go b/core/services/pipeline/common.go index 763e50546fd..1b36c8a664b 100644 --- a/core/services/pipeline/common.go +++ b/core/services/pipeline/common.go @@ -29,6 +29,7 @@ const ( BlockhashStoreJobType string = "blockhashstore" BootstrapJobType string = "bootstrap" CronJobType string = "cron" + CCIPJobType string = "ccip" DirectRequestJobType string = "directrequest" FluxMonitorJobType string = "fluxmonitor" GatewayJobType string = "gateway" diff --git a/core/services/registrysyncer/local_registry.go b/core/services/registrysyncer/local_registry.go index 4e4a632bf87..8a0e471ccda 100644 --- a/core/services/registrysyncer/local_registry.go +++ b/core/services/registrysyncer/local_registry.go @@ -16,7 +16,11 @@ type DonID uint32 type DON struct { capabilities.DON - CapabilityConfigurations map[string]capabilities.CapabilityConfiguration + CapabilityConfigurations map[string]CapabilityConfiguration +} + +type CapabilityConfiguration struct { + Config []byte } type Capability struct { @@ -26,7 +30,7 @@ type Capability struct { type LocalRegistry struct { lggr logger.Logger - peerWrapper p2ptypes.PeerWrapper + getPeerID func() (p2ptypes.PeerID, error) IDsToDONs map[DonID]DON IDsToNodes map[p2ptypes.PeerID]kcr.CapabilitiesRegistryNodeInfo IDsToCapabilities map[string]Capability @@ -36,12 +40,11 @@ func (l *LocalRegistry) LocalNode(ctx context.Context) (capabilities.Node, error // Load the current nodes PeerWrapper, this gets us the current node's // PeerID, allowing us to contextualize registry information in terms of DON ownership // (eg. get my current DON configuration, etc). - if l.peerWrapper.GetPeer() == nil { + pid, err := l.getPeerID() + if err != nil { return capabilities.Node{}, errors.New("unable to get local node: peerWrapper hasn't started yet") } - pid := l.peerWrapper.GetPeer().ID() - var workflowDON capabilities.DON capabilityDONs := []capabilities.DON{} for _, d := range l.IDsToDONs { @@ -70,15 +73,15 @@ func (l *LocalRegistry) LocalNode(ctx context.Context) (capabilities.Node, error }, nil } -func (l *LocalRegistry) ConfigForCapability(ctx context.Context, capabilityID string, donID uint32) (capabilities.CapabilityConfiguration, error) { +func (l *LocalRegistry) ConfigForCapability(ctx context.Context, capabilityID string, donID uint32) (CapabilityConfiguration, error) { d, ok := l.IDsToDONs[DonID(donID)] if !ok { - return capabilities.CapabilityConfiguration{}, fmt.Errorf("could not find don %d", donID) + return CapabilityConfiguration{}, fmt.Errorf("could not find don %d", donID) } cc, ok := d.CapabilityConfigurations[capabilityID] if !ok { - return capabilities.CapabilityConfiguration{}, fmt.Errorf("could not find capability configuration for capability %s and donID %d", capabilityID, donID) + return CapabilityConfiguration{}, fmt.Errorf("could not find capability configuration for capability %s and donID %d", capabilityID, donID) } return cc, nil diff --git a/core/services/registrysyncer/syncer.go b/core/services/registrysyncer/syncer.go index 9675d86dc86..83f77e46d35 100644 --- a/core/services/registrysyncer/syncer.go +++ b/core/services/registrysyncer/syncer.go @@ -7,14 +7,10 @@ import ( "sync" "time" - "google.golang.org/protobuf/proto" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities" - capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/smartcontractkit/chainlink-common/pkg/types/query/primitives" - "github.com/smartcontractkit/chainlink-common/pkg/values" kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" "github.com/smartcontractkit/chainlink/v2/core/logger" @@ -39,7 +35,7 @@ type registrySyncer struct { initReader func(ctx context.Context, lggr logger.Logger, relayer contractReaderFactory, registryAddress string) (types.ContractReader, error) relayer contractReaderFactory registryAddress string - peerWrapper p2ptypes.PeerWrapper + getPeerID func() (p2ptypes.PeerID, error) wg sync.WaitGroup lggr logger.Logger @@ -55,7 +51,7 @@ var ( // New instantiates a new RegistrySyncer func New( lggr logger.Logger, - peerWrapper p2ptypes.PeerWrapper, + getPeerID func() (p2ptypes.PeerID, error), relayer contractReaderFactory, registryAddress string, ) (*registrySyncer, error) { @@ -66,7 +62,7 @@ func New( relayer: relayer, registryAddress: registryAddress, initReader: newReader, - peerWrapper: peerWrapper, + getPeerID: getPeerID, }, nil } @@ -158,42 +154,6 @@ func (s *registrySyncer) syncLoop() { } } -func unmarshalCapabilityConfig(data []byte) (capabilities.CapabilityConfiguration, error) { - cconf := &capabilitiespb.CapabilityConfig{} - err := proto.Unmarshal(data, cconf) - if err != nil { - return capabilities.CapabilityConfiguration{}, err - } - - var remoteTriggerConfig *capabilities.RemoteTriggerConfig - var remoteTargetConfig *capabilities.RemoteTargetConfig - - switch cconf.GetRemoteConfig().(type) { - case *capabilitiespb.CapabilityConfig_RemoteTriggerConfig: - prtc := cconf.GetRemoteTriggerConfig() - remoteTriggerConfig = &capabilities.RemoteTriggerConfig{} - remoteTriggerConfig.RegistrationRefresh = prtc.RegistrationRefresh.AsDuration() - remoteTriggerConfig.RegistrationExpiry = prtc.RegistrationExpiry.AsDuration() - remoteTriggerConfig.MinResponsesToAggregate = prtc.MinResponsesToAggregate - remoteTriggerConfig.MessageExpiry = prtc.MessageExpiry.AsDuration() - case *capabilitiespb.CapabilityConfig_RemoteTargetConfig: - prtc := cconf.GetRemoteTargetConfig() - remoteTargetConfig = &capabilities.RemoteTargetConfig{} - remoteTargetConfig.RequestHashExcludedAttributes = prtc.RequestHashExcludedAttributes - } - - dc, err := values.FromMapValueProto(cconf.DefaultConfig) - if err != nil { - return capabilities.CapabilityConfiguration{}, err - } - - return capabilities.CapabilityConfiguration{ - DefaultConfig: dc, - RemoteTriggerConfig: remoteTriggerConfig, - RemoteTargetConfig: remoteTargetConfig, - }, nil -} - func (s *registrySyncer) localRegistry(ctx context.Context) (*LocalRegistry, error) { caps := []kcr.CapabilitiesRegistryCapabilityInfo{} err := s.reader.GetLatestValue(ctx, "CapabilitiesRegistry", "getCapabilities", primitives.Unconfirmed, nil, &caps) @@ -221,19 +181,16 @@ func (s *registrySyncer) localRegistry(ctx context.Context) (*LocalRegistry, err idsToDONs := map[DonID]DON{} for _, d := range dons { - cc := map[string]capabilities.CapabilityConfiguration{} + cc := map[string]CapabilityConfiguration{} for _, dc := range d.CapabilityConfigurations { cid, ok := hashedIDsToCapabilityIDs[dc.CapabilityId] if !ok { return nil, fmt.Errorf("invariant violation: could not find full ID for hashed ID %s", dc.CapabilityId) } - cconf, innerErr := unmarshalCapabilityConfig(dc.Config) - if innerErr != nil { - return nil, innerErr + cc[cid] = CapabilityConfiguration{ + Config: dc.Config, } - - cc[cid] = cconf } idsToDONs[DonID(d.Id)] = DON{ @@ -255,7 +212,7 @@ func (s *registrySyncer) localRegistry(ctx context.Context) (*LocalRegistry, err return &LocalRegistry{ lggr: s.lggr, - peerWrapper: s.peerWrapper, + getPeerID: s.getPeerID, IDsToDONs: idsToDONs, IDsToCapabilities: idsToCapabilities, IDsToNodes: idsToNodes, diff --git a/core/services/registrysyncer/syncer_test.go b/core/services/registrysyncer/syncer_test.go index c13cc904909..cd8776d882c 100644 --- a/core/services/registrysyncer/syncer_test.go +++ b/core/services/registrysyncer/syncer_test.go @@ -33,7 +33,6 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest" "github.com/smartcontractkit/chainlink/v2/core/logger" p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" - "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types/mocks" "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm" evmrelaytypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" ) @@ -237,9 +236,8 @@ func TestReader_Integration(t *testing.T) { require.NoError(t, err) - wrapper := mocks.NewPeerWrapper(t) factory := newContractReaderFactory(t, sim) - syncer, err := New(logger.TestLogger(t), wrapper, factory, regAddress.Hex()) + syncer, err := New(logger.TestLogger(t), func() (p2ptypes.PeerID, error) { return p2ptypes.PeerID{}, nil }, factory, regAddress.Hex()) require.NoError(t, err) l := &launcher{} @@ -257,29 +255,17 @@ func TestReader_Integration(t *testing.T) { }, gotCap) assert.Len(t, s.IDsToDONs, 1) - rtc := &capabilities.RemoteTriggerConfig{ - RegistrationRefresh: 20 * time.Second, - MinResponsesToAggregate: 2, - RegistrationExpiry: 60 * time.Second, - MessageExpiry: 120 * time.Second, - } - expectedDON := DON{ - DON: capabilities.DON{ - ID: 1, - ConfigVersion: 1, - IsPublic: true, - AcceptsWorkflows: true, - F: 1, - Members: toPeerIDs(nodeSet), - }, - CapabilityConfigurations: map[string]capabilities.CapabilityConfiguration{ - cid: { - DefaultConfig: values.EmptyMap(), - RemoteTriggerConfig: rtc, - }, - }, + expectedDON := capabilities.DON{ + ID: 1, + ConfigVersion: 1, + IsPublic: true, + AcceptsWorkflows: true, + F: 1, + Members: toPeerIDs(nodeSet), } - assert.Equal(t, expectedDON, s.IDsToDONs[1]) + gotDon := s.IDsToDONs[1] + assert.Equal(t, expectedDON, gotDon.DON) + assert.Equal(t, configb, gotDon.CapabilityConfigurations[cid].Config) nodesInfo := []kcr.CapabilitiesRegistryNodeInfo{ { @@ -329,10 +315,6 @@ func TestSyncer_LocalNode(t *testing.T) { var pid p2ptypes.PeerID err := pid.UnmarshalText([]byte("12D3KooWBCF1XT5Wi8FzfgNCqRL76Swv8TRU3TiD4QiJm8NMNX7N")) require.NoError(t, err) - peer := mocks.NewPeer(t) - peer.On("ID").Return(pid) - wrapper := mocks.NewPeerWrapper(t) - wrapper.On("GetPeer").Return(peer) workflowDonNodes := []p2ptypes.PeerID{ pid, @@ -346,8 +328,8 @@ func TestSyncer_LocalNode(t *testing.T) { // which exposes the streams-trigger and write_chain capabilities. // We expect receivers to be wired up and both capabilities to be added to the registry. localRegistry := LocalRegistry{ - lggr: lggr, - peerWrapper: wrapper, + lggr: lggr, + getPeerID: func() (p2ptypes.PeerID, error) { return pid, nil }, IDsToDONs: map[DonID]DON{ DonID(dID): { DON: capabilities.DON{ diff --git a/core/services/synchronization/common.go b/core/services/synchronization/common.go index bfb9fba6de6..394830a76af 100644 --- a/core/services/synchronization/common.go +++ b/core/services/synchronization/common.go @@ -24,6 +24,10 @@ const ( OCR3Mercury TelemetryType = "ocr3-mercury" AutomationCustom TelemetryType = "automation-custom" OCR3Automation TelemetryType = "ocr3-automation" + OCR3Rebalancer TelemetryType = "ocr3-rebalancer" + OCR3CCIPCommit TelemetryType = "ocr3-ccip-commit" + OCR3CCIPExec TelemetryType = "ocr3-ccip-exec" + OCR3CCIPBootstrap TelemetryType = "ocr3-bootstrap" ) type TelemPayload struct { diff --git a/core/services/workflows/engine_test.go b/core/services/workflows/engine_test.go index 3af87284131..0a38bf719b2 100644 --- a/core/services/workflows/engine_test.go +++ b/core/services/workflows/engine_test.go @@ -11,8 +11,10 @@ import ( "github.com/shopspring/decimal" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" "github.com/smartcontractkit/chainlink-common/pkg/services/servicetest" "github.com/smartcontractkit/chainlink-common/pkg/values" "github.com/smartcontractkit/chainlink-common/pkg/workflows" @@ -22,6 +24,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/job" p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" + "github.com/smartcontractkit/chainlink/v2/core/services/registrysyncer" "github.com/smartcontractkit/chainlink/v2/core/services/workflows/store" ) @@ -101,7 +104,7 @@ func newTestDBStore(t *testing.T, clock clockwork.Clock) store.Store { type testConfigProvider struct { localNode func(ctx context.Context) (capabilities.Node, error) - configForCapability func(ctx context.Context, capabilityID string, donID uint32) (capabilities.CapabilityConfiguration, error) + configForCapability func(ctx context.Context, capabilityID string, donID uint32) (registrysyncer.CapabilityConfiguration, error) } func (t testConfigProvider) LocalNode(ctx context.Context) (capabilities.Node, error) { @@ -118,12 +121,12 @@ func (t testConfigProvider) LocalNode(ctx context.Context) (capabilities.Node, e }, nil } -func (t testConfigProvider) ConfigForCapability(ctx context.Context, capabilityID string, donID uint32) (capabilities.CapabilityConfiguration, error) { +func (t testConfigProvider) ConfigForCapability(ctx context.Context, capabilityID string, donID uint32) (registrysyncer.CapabilityConfiguration, error) { if t.configForCapability != nil { return t.configForCapability(ctx, capabilityID, donID) } - return capabilities.CapabilityConfiguration{}, nil + return registrysyncer.CapabilityConfiguration{}, nil } // newTestEngine creates a new engine with some test defaults. @@ -1028,11 +1031,9 @@ func TestEngine_MergesWorkflowConfigAndCRConfig(t *testing.T) { simpleWorkflow, ) reg.SetLocalRegistry(testConfigProvider{ - configForCapability: func(ctx context.Context, capabilityID string, donID uint32) (capabilities.CapabilityConfiguration, error) { + configForCapability: func(ctx context.Context, capabilityID string, donID uint32) (registrysyncer.CapabilityConfiguration, error) { if capabilityID != writeID { - return capabilities.CapabilityConfiguration{ - DefaultConfig: values.EmptyMap(), - }, nil + return registrysyncer.CapabilityConfiguration{}, nil } cm, err := values.WrapMap(map[string]any{ @@ -1040,12 +1041,15 @@ func TestEngine_MergesWorkflowConfigAndCRConfig(t *testing.T) { "schedule": "allAtOnce", }) if err != nil { - return capabilities.CapabilityConfiguration{}, err + return registrysyncer.CapabilityConfiguration{}, err } - return capabilities.CapabilityConfiguration{ - DefaultConfig: cm, - }, nil + cb, err := proto.Marshal(&capabilitiespb.CapabilityConfig{ + DefaultConfig: values.ProtoMap(cm), + }) + return registrysyncer.CapabilityConfiguration{ + Config: cb, + }, err }, }) diff --git a/core/web/presenters/job.go b/core/web/presenters/job.go index ad6bf617a82..bb518650516 100644 --- a/core/web/presenters/job.go +++ b/core/web/presenters/job.go @@ -468,6 +468,26 @@ func NewStandardCapabilitiesSpec(spec *job.StandardCapabilitiesSpec) *StandardCa } } +type CCIPSpec struct { + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + CapabilityVersion string `json:"capabilityVersion"` + CapabilityLabelledName string `json:"capabilityLabelledName"` + OCRKeyBundleIDs map[string]interface{} `json:"ocrKeyBundleIDs"` + P2PKeyID string `json:"p2pKeyID"` +} + +func NewCCIPSpec(spec *job.CCIPSpec) *CCIPSpec { + return &CCIPSpec{ + CreatedAt: spec.CreatedAt, + UpdatedAt: spec.UpdatedAt, + CapabilityVersion: spec.CapabilityVersion, + CapabilityLabelledName: spec.CapabilityLabelledName, + OCRKeyBundleIDs: spec.OCRKeyBundleIDs, + P2PKeyID: spec.P2PKeyID, + } +} + // JobError represents errors on the job type JobError struct { ID int64 `json:"id"` @@ -512,6 +532,7 @@ type JobResource struct { GatewaySpec *GatewaySpec `json:"gatewaySpec"` WorkflowSpec *WorkflowSpec `json:"workflowSpec"` StandardCapabilitiesSpec *StandardCapabilitiesSpec `json:"standardCapabilitiesSpec"` + CCIPSpec *CCIPSpec `json:"ccipSpec"` PipelineSpec PipelineSpec `json:"pipelineSpec"` Errors []JobError `json:"errors"` } @@ -562,6 +583,8 @@ func NewJobResource(j job.Job) *JobResource { resource.WorkflowSpec = NewWorkflowSpec(j.WorkflowSpec) case job.StandardCapabilities: resource.StandardCapabilitiesSpec = NewStandardCapabilitiesSpec(j.StandardCapabilitiesSpec) + case job.CCIP: + resource.CCIPSpec = NewCCIPSpec(j.CCIPSpec) case job.LegacyGasStationServer, job.LegacyGasStationSidecar: // unsupported } diff --git a/core/web/presenters/job_test.go b/core/web/presenters/job_test.go index 5de71f918e3..75697c6e068 100644 --- a/core/web/presenters/job_test.go +++ b/core/web/presenters/job_test.go @@ -130,6 +130,7 @@ func TestJob(t *testing.T) { "bootstrapSpec": null, "gatewaySpec": null, "standardCapabilitiesSpec": null, + "ccipSpec": null, "errors": [] } } @@ -208,6 +209,7 @@ func TestJob(t *testing.T) { "bootstrapSpec": null, "gatewaySpec": null, "standardCapabilitiesSpec": null, + "ccipSpec": null, "errors": [] } } @@ -296,6 +298,7 @@ func TestJob(t *testing.T) { "bootstrapSpec": null, "gatewaySpec": null, "standardCapabilitiesSpec": null, + "ccipSpec": null, "errors": [] } } @@ -361,6 +364,7 @@ func TestJob(t *testing.T) { "bootstrapSpec": null, "gatewaySpec": null, "standardCapabilitiesSpec": null, + "ccipSpec": null, "errors": [] } } @@ -423,6 +427,7 @@ func TestJob(t *testing.T) { "bootstrapSpec": null, "gatewaySpec": null, "standardCapabilitiesSpec": null, + "ccipSpec": null, "errors": [] } } @@ -481,6 +486,7 @@ func TestJob(t *testing.T) { "bootstrapSpec": null, "gatewaySpec": null, "standardCapabilitiesSpec": null, + "ccipSpec": null, "errors": [] } } @@ -566,7 +572,9 @@ func TestJob(t *testing.T) { "dotDagSource": "" }, "gatewaySpec": null, - "standardCapabilitiesSpec": null, + "standardCapabilitiesSpec": null, + "standardCapabilitiesSpec": null, + "ccipSpec": null, "errors": [] } } @@ -649,6 +657,7 @@ func TestJob(t *testing.T) { }, "gatewaySpec": null, "standardCapabilitiesSpec": null, + "ccipSpec": null, "errors": [] } } @@ -731,6 +740,7 @@ func TestJob(t *testing.T) { }, "gatewaySpec": null, "standardCapabilitiesSpec": null, + "ccipSpec": null, "errors": [] } } @@ -780,14 +790,14 @@ func TestJob(t *testing.T) { "blockhashStoreSpec": null, "blockHeaderFeederSpec": null, "bootstrapSpec": { - "blockchainTimeout":"0s", - "contractConfigConfirmations":0, - "contractConfigTrackerPollInterval":"0s", - "contractConfigTrackerSubscribeInterval":"0s", - "contractID":"0x16988483b46e695f6c8D58e6e1461DC703e008e1", - "createdAt":"0001-01-01T00:00:00Z", - "relay":"evm", - "relayConfig":{"chainID":1337}, + "blockchainTimeout":"0s", + "contractConfigConfirmations":0, + "contractConfigTrackerPollInterval":"0s", + "contractConfigTrackerSubscribeInterval":"0s", + "contractID":"0x16988483b46e695f6c8D58e6e1461DC703e008e1", + "createdAt":"0001-01-01T00:00:00Z", + "relay":"evm", + "relayConfig":{"chainID":1337}, "updatedAt":"0001-01-01T00:00:00Z" }, "pipelineSpec": { @@ -797,6 +807,7 @@ func TestJob(t *testing.T) { }, "gatewaySpec": null, "standardCapabilitiesSpec": null, + "ccipSpec": null, "errors": [] } } @@ -855,6 +866,7 @@ func TestJob(t *testing.T) { "updatedAt":"0001-01-01T00:00:00Z" }, "standardCapabilitiesSpec": null, + "ccipSpec": null, "pipelineSpec": { "id": 1, "jobID": 0, @@ -919,6 +931,7 @@ func TestJob(t *testing.T) { "bootstrapSpec": null, "gatewaySpec": null, "standardCapabilitiesSpec": null, + "ccipSpec": null, "pipelineSpec": { "id": 1, "jobID": 0, @@ -979,6 +992,72 @@ func TestJob(t *testing.T) { "createdAt":"0001-01-01T00:00:00Z", "updatedAt":"0001-01-01T00:00:00Z" }, + "ccipSpec": null, + "pipelineSpec": { + "id": 1, + "jobID": 0, + "dotDagSource": "" + }, + "errors": [] + } + } + }`, + }, + { + name: "ccip spec", + job: job.Job{ + ID: 1, + CCIPSpec: &job.CCIPSpec{ + ID: 3, + CreatedAt: timestamp, + UpdatedAt: timestamp, + CapabilityVersion: "4.5.9", + CapabilityLabelledName: "ccip", + }, + PipelineSpec: &pipeline.Spec{ + ID: 1, + DotDagSource: "", + }, + ExternalJobID: uuid.MustParse("0eec7e1d-d0d2-476c-a1a8-72dfb6633f46"), + Type: job.CCIP, + SchemaVersion: 1, + Name: null.StringFrom("ccip test"), + }, + want: ` + { + "data": { + "type": "jobs", + "id": "1", + "attributes": { + "name": "ccip test", + "type": "ccip", + "schemaVersion": 1, + "maxTaskDuration": "0s", + "externalJobID": "0eec7e1d-d0d2-476c-a1a8-72dfb6633f46", + "directRequestSpec": null, + "fluxMonitorSpec": null, + "gasLimit": null, + "forwardingAllowed": false, + "cronSpec": null, + "offChainReportingOracleSpec": null, + "offChainReporting2OracleSpec": null, + "keeperSpec": null, + "vrfSpec": null, + "webhookSpec": null, + "workflowSpec": null, + "blockhashStoreSpec": null, + "blockHeaderFeederSpec": null, + "bootstrapSpec": null, + "gatewaySpec": null, + "standardCapabilitiesSpec": null, + "ccipSpec": { + "capabilityVersion":"4.5.9", + "capabilityLabelledName":"ccip", + "ocrKeyBundleIDs": null, + "p2pKeyID": "", + "createdAt":"2000-01-01T00:00:00Z", + "updatedAt":"2000-01-01T00:00:00Z" + }, "pipelineSpec": { "id": 1, "jobID": 0, @@ -1058,6 +1137,7 @@ func TestJob(t *testing.T) { "bootstrapSpec": null, "gatewaySpec": null, "standardCapabilitiesSpec": null, + "ccipSpec": null, "errors": [{ "id": 200, "description": "some error", diff --git a/go.md b/go.md index d9ed0d0a660..697d6b52cea 100644 --- a/go.md +++ b/go.md @@ -28,6 +28,8 @@ flowchart LR click chain-selectors href "https://github.com/smartcontractkit/chain-selectors" chainlink/v2 --> chainlink-automation click chainlink-automation href "https://github.com/smartcontractkit/chainlink-automation" + chainlink/v2 --> chainlink-ccip + click chainlink-ccip href "https://github.com/smartcontractkit/chainlink-ccip" chainlink/v2 --> chainlink-common click chainlink-common href "https://github.com/smartcontractkit/chainlink-common" chainlink/v2 --> chainlink-cosmos @@ -50,6 +52,8 @@ flowchart LR click wsrpc href "https://github.com/smartcontractkit/wsrpc" chainlink-automation --> chainlink-common chainlink-automation --> libocr + chainlink-ccip --> chainlink-common + chainlink-ccip --> libocr chainlink-common --> libocr chainlink-cosmos --> chainlink-common chainlink-cosmos --> libocr diff --git a/go.mod b/go.mod index 2179ffc2d21..f89bfe7def5 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/cometbft/cometbft v0.37.2 github.com/cosmos/cosmos-sdk v0.47.4 github.com/danielkov/gin-helmet v0.0.0-20171108135313-1387e224435e - github.com/deckarep/golang-set/v2 v2.3.0 + github.com/deckarep/golang-set/v2 v2.6.0 github.com/dominikbraun/graph v0.23.0 github.com/esote/minmaxheap v1.0.0 github.com/ethereum/go-ethereum v1.13.8 @@ -74,6 +74,7 @@ require ( github.com/shopspring/decimal v1.4.0 github.com/smartcontractkit/chain-selectors v1.0.10 github.com/smartcontractkit/chainlink-automation v1.0.4 + github.com/smartcontractkit/chainlink-ccip v0.0.0-20240806144315-04ac101e9c95 github.com/smartcontractkit/chainlink-common v0.2.2-0.20240805160614-501c4f40b98c github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240710121324-3ed288aa9b45 github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240801131703-fd75761c982f @@ -102,7 +103,7 @@ require ( go.uber.org/multierr v1.11.0 go.uber.org/zap v1.27.0 golang.org/x/crypto v0.25.0 - golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7 + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 golang.org/x/mod v0.19.0 golang.org/x/net v0.27.0 golang.org/x/sync v0.7.0 diff --git a/go.sum b/go.sum index b953f315e92..9679a2da3af 100644 --- a/go.sum +++ b/go.sum @@ -315,8 +315,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/deckarep/golang-set/v2 v2.3.0 h1:qs18EKUfHm2X9fA50Mr/M5hccg2tNnVqsiBImnyDs0g= -github.com/deckarep/golang-set/v2 v2.3.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= @@ -1139,6 +1139,8 @@ github.com/smartcontractkit/chain-selectors v1.0.10 h1:t9kJeE6B6G+hKD0GYR4kGJSCq github.com/smartcontractkit/chain-selectors v1.0.10/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= github.com/smartcontractkit/chainlink-automation v1.0.4 h1:iyW181JjKHLNMnDleI8umfIfVVlwC7+n5izbLSFgjw8= github.com/smartcontractkit/chainlink-automation v1.0.4/go.mod h1:u4NbPZKJ5XiayfKHD/v3z3iflQWqvtdhj13jVZXj/cM= +github.com/smartcontractkit/chainlink-ccip v0.0.0-20240806144315-04ac101e9c95 h1:LAgJTg9Yr/uCo2g7Krp88Dco2U45Y6sbJVl8uKoLkys= +github.com/smartcontractkit/chainlink-ccip v0.0.0-20240806144315-04ac101e9c95/go.mod h1:/ZWraCBaDDgaIN1prixYcbVvIk/6HeED9+8zbWQ+TMo= github.com/smartcontractkit/chainlink-common v0.2.2-0.20240805160614-501c4f40b98c h1:3apUsez/6Pkp1ckXzSwIhzPRuWjDGjzMjKapEKi0Fcw= github.com/smartcontractkit/chainlink-common v0.2.2-0.20240805160614-501c4f40b98c/go.mod h1:Jg1sCTsbxg76YByI8ifpFby3FvVqISStHT8ypy9ocmY= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240710121324-3ed288aa9b45 h1:NBQLtqk8zsyY4qTJs+NElI3aDFTcAo83JHvqD04EvB0= @@ -1436,8 +1438,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7 h1:wDLEX9a7YQoKdKNQt88rtydkqDxeGaBUTnIYc3iG/mA= -golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/integration-tests/go.mod b/integration-tests/go.mod index ff60a8f78b3..7be6ea209f1 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -143,7 +143,7 @@ require ( github.com/crate-crypto/go-kzg-4844 v0.7.0 // indirect github.com/danieljoos/wincred v1.1.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/deckarep/golang-set/v2 v2.3.0 // indirect + github.com/deckarep/golang-set/v2 v2.6.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/dennwc/varint v1.0.0 // indirect github.com/dfuse-io/logging v0.0.0-20210109005628-b97a57253f70 // indirect @@ -377,6 +377,7 @@ require ( github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/smartcontractkit/chain-selectors v1.0.10 // indirect + github.com/smartcontractkit/chainlink-ccip v0.0.0-20240806144315-04ac101e9c95 // indirect github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240710121324-3ed288aa9b45 // indirect github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240801131703-fd75761c982f // indirect github.com/smartcontractkit/chainlink-feeds v0.0.0-20240710170203-5b41615da827 // indirect @@ -448,7 +449,7 @@ require ( go4.org/netipx v0.0.0-20230125063823-8449b0a6169f // indirect golang.org/x/arch v0.8.0 // indirect golang.org/x/crypto v0.25.0 // indirect - golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/mod v0.19.0 // indirect golang.org/x/net v0.27.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect diff --git a/integration-tests/go.sum b/integration-tests/go.sum index 5d15dfd92f6..372a5ee0145 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -415,8 +415,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/deckarep/golang-set/v2 v2.3.0 h1:qs18EKUfHm2X9fA50Mr/M5hccg2tNnVqsiBImnyDs0g= -github.com/deckarep/golang-set/v2 v2.3.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= @@ -1488,6 +1488,8 @@ github.com/smartcontractkit/chain-selectors v1.0.10 h1:t9kJeE6B6G+hKD0GYR4kGJSCq github.com/smartcontractkit/chain-selectors v1.0.10/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= github.com/smartcontractkit/chainlink-automation v1.0.4 h1:iyW181JjKHLNMnDleI8umfIfVVlwC7+n5izbLSFgjw8= github.com/smartcontractkit/chainlink-automation v1.0.4/go.mod h1:u4NbPZKJ5XiayfKHD/v3z3iflQWqvtdhj13jVZXj/cM= +github.com/smartcontractkit/chainlink-ccip v0.0.0-20240806144315-04ac101e9c95 h1:LAgJTg9Yr/uCo2g7Krp88Dco2U45Y6sbJVl8uKoLkys= +github.com/smartcontractkit/chainlink-ccip v0.0.0-20240806144315-04ac101e9c95/go.mod h1:/ZWraCBaDDgaIN1prixYcbVvIk/6HeED9+8zbWQ+TMo= github.com/smartcontractkit/chainlink-common v0.2.2-0.20240805160614-501c4f40b98c h1:3apUsez/6Pkp1ckXzSwIhzPRuWjDGjzMjKapEKi0Fcw= github.com/smartcontractkit/chainlink-common v0.2.2-0.20240805160614-501c4f40b98c/go.mod h1:Jg1sCTsbxg76YByI8ifpFby3FvVqISStHT8ypy9ocmY= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240710121324-3ed288aa9b45 h1:NBQLtqk8zsyY4qTJs+NElI3aDFTcAo83JHvqD04EvB0= @@ -1836,8 +1838,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7 h1:wDLEX9a7YQoKdKNQt88rtydkqDxeGaBUTnIYc3iG/mA= -golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/integration-tests/load/go.mod b/integration-tests/load/go.mod index c464231c745..11893540a39 100644 --- a/integration-tests/load/go.mod +++ b/integration-tests/load/go.mod @@ -39,6 +39,7 @@ require ( github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/smartcontractkit/chainlink-ccip v0.0.0-20240806144315-04ac101e9c95 // indirect github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240710121324-3ed288aa9b45 // indirect k8s.io/apimachinery v0.30.2 // indirect ) @@ -131,7 +132,7 @@ require ( github.com/crate-crypto/go-kzg-4844 v0.7.0 // indirect github.com/danieljoos/wincred v1.1.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/deckarep/golang-set/v2 v2.3.0 // indirect + github.com/deckarep/golang-set/v2 v2.6.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/dennwc/varint v1.0.0 // indirect github.com/dfuse-io/logging v0.0.0-20210109005628-b97a57253f70 // indirect @@ -445,7 +446,7 @@ require ( go4.org/netipx v0.0.0-20230125063823-8449b0a6169f // indirect golang.org/x/arch v0.8.0 // indirect golang.org/x/crypto v0.25.0 // indirect - golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/mod v0.19.0 // indirect golang.org/x/net v0.27.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect diff --git a/integration-tests/load/go.sum b/integration-tests/load/go.sum index d1d6f3a4d52..97a9dfc8ec7 100644 --- a/integration-tests/load/go.sum +++ b/integration-tests/load/go.sum @@ -405,8 +405,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/deckarep/golang-set/v2 v2.3.0 h1:qs18EKUfHm2X9fA50Mr/M5hccg2tNnVqsiBImnyDs0g= -github.com/deckarep/golang-set/v2 v2.3.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= @@ -1470,6 +1470,8 @@ github.com/smartcontractkit/chain-selectors v1.0.10 h1:t9kJeE6B6G+hKD0GYR4kGJSCq github.com/smartcontractkit/chain-selectors v1.0.10/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= github.com/smartcontractkit/chainlink-automation v1.0.4 h1:iyW181JjKHLNMnDleI8umfIfVVlwC7+n5izbLSFgjw8= github.com/smartcontractkit/chainlink-automation v1.0.4/go.mod h1:u4NbPZKJ5XiayfKHD/v3z3iflQWqvtdhj13jVZXj/cM= +github.com/smartcontractkit/chainlink-ccip v0.0.0-20240806144315-04ac101e9c95 h1:LAgJTg9Yr/uCo2g7Krp88Dco2U45Y6sbJVl8uKoLkys= +github.com/smartcontractkit/chainlink-ccip v0.0.0-20240806144315-04ac101e9c95/go.mod h1:/ZWraCBaDDgaIN1prixYcbVvIk/6HeED9+8zbWQ+TMo= github.com/smartcontractkit/chainlink-common v0.2.2-0.20240805160614-501c4f40b98c h1:3apUsez/6Pkp1ckXzSwIhzPRuWjDGjzMjKapEKi0Fcw= github.com/smartcontractkit/chainlink-common v0.2.2-0.20240805160614-501c4f40b98c/go.mod h1:Jg1sCTsbxg76YByI8ifpFby3FvVqISStHT8ypy9ocmY= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240710121324-3ed288aa9b45 h1:NBQLtqk8zsyY4qTJs+NElI3aDFTcAo83JHvqD04EvB0= @@ -1818,8 +1820,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7 h1:wDLEX9a7YQoKdKNQt88rtydkqDxeGaBUTnIYc3iG/mA= -golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= From 2ac9d980eb1527a2e1e631fb4515919f3b872333 Mon Sep 17 00:00:00 2001 From: FelixFan1992 Date: Wed, 7 Aug 2024 18:37:27 -0400 Subject: [PATCH 6/9] auto-11214: migrate more tests to foundry (#13934) * auto: migrate more tests to foundry * more tests * tests for migration permission * tests for upkeep and admin configs * tests for upkeep admin * tests for pause / unpause upkeeps * tests for upkeep configs * tests for payees * cancel upkeep tests * update * update * withdraw * update --- .../test/v2_3/AutomationRegistry2_3.t.sol | 784 ++++++++- .../v0.8/automation/test/v2_3/BaseTest.t.sol | 74 +- .../automation/AutomationRegistry2_3.test.ts | 1402 ++--------------- 3 files changed, 980 insertions(+), 1280 deletions(-) diff --git a/contracts/src/v0.8/automation/test/v2_3/AutomationRegistry2_3.t.sol b/contracts/src/v0.8/automation/test/v2_3/AutomationRegistry2_3.t.sol index dbc0c203c07..41aabf1bbe2 100644 --- a/contracts/src/v0.8/automation/test/v2_3/AutomationRegistry2_3.t.sol +++ b/contracts/src/v0.8/automation/test/v2_3/AutomationRegistry2_3.t.sol @@ -22,11 +22,11 @@ contract SetUp is BaseTest { AutomationRegistryBase2_3.OnchainConfig internal config; bytes internal constant offchainConfigBytes = abi.encode(1234, ZERO_ADDRESS); - uint256 linkUpkeepID; - uint256 linkUpkeepID2; // 2 upkeeps use the same billing token (LINK) to test migration scenario - uint256 usdUpkeepID18; // 1 upkeep uses ERC20 token with 18 decimals - uint256 usdUpkeepID6; // 1 upkeep uses ERC20 token with 6 decimals - uint256 nativeUpkeepID; + uint256 internal linkUpkeepID; + uint256 internal linkUpkeepID2; // 2 upkeeps use the same billing token (LINK) to test migration scenario + uint256 internal usdUpkeepID18; // 1 upkeep uses ERC20 token with 18 decimals + uint256 internal usdUpkeepID6; // 1 upkeep uses ERC20 token with 6 decimals + uint256 internal nativeUpkeepID; function setUp() public virtual override { super.setUp(); @@ -790,6 +790,7 @@ contract SetConfig is SetUp { } function testSetConfigOnTransmittersAndPayees() public { + registry.setPayees(PAYEES); AutomationRegistryBase2_3.TransmitterPayeeInfo[] memory transmitterPayeeInfos = registry .getTransmittersWithPayees(); assertEq(transmitterPayeeInfos.length, TRANSMITTERS.length); @@ -975,6 +976,7 @@ contract NOPsSettlement is SetUp { function testSettleNOPsOffchainSuccess() public { // deploy and configure a registry with OFF_CHAIN payout (Registry registry, ) = deployAndConfigureRegistryAndRegistrar(AutoBase.PayoutMode.OFF_CHAIN); + registry.setPayees(PAYEES); uint256[] memory payments = new uint256[](TRANSMITTERS.length); for (uint256 i = 0; i < TRANSMITTERS.length; i++) { @@ -991,6 +993,7 @@ contract NOPsSettlement is SetUp { function testSettleNOPsOffchainSuccessWithERC20MultiSteps() public { // deploy and configure a registry with OFF_CHAIN payout (Registry registry, ) = deployAndConfigureRegistryAndRegistrar(AutoBase.PayoutMode.OFF_CHAIN); + registry.setPayees(PAYEES); // register an upkeep and add funds uint256 id = registry.registerUpkeep(address(TARGET1), 1000000, UPKEEP_ADMIN, 0, address(usdToken18), "", "", ""); @@ -1186,6 +1189,7 @@ contract NOPsSettlement is SetUp { function testSinglePerformAndNodesCanWithdrawOnchain() public { // deploy and configure a registry with OFF_CHAIN payout (Registry registry, ) = deployAndConfigureRegistryAndRegistrar(AutoBase.PayoutMode.OFF_CHAIN); + registry.setPayees(PAYEES); // register an upkeep and add funds uint256 id = registry.registerUpkeep(address(TARGET1), 1000000, UPKEEP_ADMIN, 0, address(usdToken18), "", "", ""); @@ -1224,6 +1228,7 @@ contract NOPsSettlement is SetUp { function testMultiplePerformsAndNodesCanWithdrawOnchain() public { // deploy and configure a registry with OFF_CHAIN payout (Registry registry, ) = deployAndConfigureRegistryAndRegistrar(AutoBase.PayoutMode.OFF_CHAIN); + registry.setPayees(PAYEES); // register an upkeep and add funds uint256 id = registry.registerUpkeep(address(TARGET1), 1000000, UPKEEP_ADMIN, 0, address(usdToken18), "", "", ""); @@ -1977,3 +1982,772 @@ contract MigrateReceive is SetUp { vm.stopPrank(); } } + +contract Pause is SetUp { + function test_RevertsWhen_CalledByNonOwner() external { + vm.expectRevert(bytes("Only callable by owner")); + vm.prank(STRANGER); + registry.pause(); + } + + function test_CalledByOwner_success() external { + vm.startPrank(registry.owner()); + registry.pause(); + + (IAutomationV21PlusCommon.StateLegacy memory state, , , , ) = registry.getState(); + assertTrue(state.paused); + } + + function test_revertsWhen_transmitInPausedRegistry() external { + vm.startPrank(registry.owner()); + registry.pause(); + + _transmitAndExpectRevert(usdUpkeepID18, registry, Registry.RegistryPaused.selector); + } +} + +contract Unpause is SetUp { + function test_RevertsWhen_CalledByNonOwner() external { + vm.startPrank(registry.owner()); + registry.pause(); + + vm.expectRevert(bytes("Only callable by owner")); + vm.startPrank(STRANGER); + registry.unpause(); + } + + function test_CalledByOwner_success() external { + vm.startPrank(registry.owner()); + registry.pause(); + (IAutomationV21PlusCommon.StateLegacy memory state1, , , , ) = registry.getState(); + assertTrue(state1.paused); + + registry.unpause(); + (IAutomationV21PlusCommon.StateLegacy memory state2, , , , ) = registry.getState(); + assertFalse(state2.paused); + } +} + +contract CancelUpkeep is SetUp { + event UpkeepCanceled(uint256 indexed id, uint64 indexed atBlockHeight); + + function test_RevertsWhen_IdIsInvalid_CalledByAdmin() external { + vm.startPrank(UPKEEP_ADMIN); + vm.expectRevert(Registry.CannotCancel.selector); + registry.cancelUpkeep(1111111); + } + + function test_RevertsWhen_IdIsInvalid_CalledByOwner() external { + vm.startPrank(registry.owner()); + vm.expectRevert(Registry.CannotCancel.selector); + registry.cancelUpkeep(1111111); + } + + function test_RevertsWhen_NotCalledByOwnerOrAdmin() external { + vm.startPrank(STRANGER); + vm.expectRevert(Registry.OnlyCallableByOwnerOrAdmin.selector); + registry.cancelUpkeep(linkUpkeepID); + } + + function test_RevertsWhen_UpkeepAlreadyCanceledByAdmin_CalledByOwner() external { + uint256 bn = block.number; + vm.startPrank(UPKEEP_ADMIN); + registry.cancelUpkeep(linkUpkeepID); + + vm.startPrank(registry.owner()); + vm.expectRevert(Registry.UpkeepCancelled.selector); + registry.cancelUpkeep(linkUpkeepID); + } + + function test_RevertsWhen_UpkeepAlreadyCanceledByOwner_CalledByAdmin() external { + uint256 bn = block.number; + vm.startPrank(registry.owner()); + registry.cancelUpkeep(linkUpkeepID); + + vm.startPrank(UPKEEP_ADMIN); + vm.expectRevert(Registry.UpkeepCancelled.selector); + registry.cancelUpkeep(linkUpkeepID); + } + + function test_RevertsWhen_UpkeepAlreadyCanceledByAdmin_CalledByAdmin() external { + uint256 bn = block.number; + vm.startPrank(UPKEEP_ADMIN); + registry.cancelUpkeep(linkUpkeepID); + + vm.expectRevert(Registry.UpkeepCancelled.selector); + registry.cancelUpkeep(linkUpkeepID); + } + + function test_RevertsWhen_UpkeepAlreadyCanceledByOwner_CalledByOwner() external { + uint256 bn = block.number; + vm.startPrank(registry.owner()); + registry.cancelUpkeep(linkUpkeepID); + + vm.expectRevert(Registry.UpkeepCancelled.selector); + registry.cancelUpkeep(linkUpkeepID); + } + + function test_CancelUpkeep_SetMaxValidBlockNumber_CalledByAdmin() external { + uint256 bn = block.number; + vm.startPrank(UPKEEP_ADMIN); + registry.cancelUpkeep(linkUpkeepID); + + uint256 maxValidBlocknumber = uint256(registry.getUpkeep(linkUpkeepID).maxValidBlocknumber); + + // 50 is the cancellation delay + assertEq(bn + 50, maxValidBlocknumber); + } + + function test_CancelUpkeep_SetMaxValidBlockNumber_CalledByOwner() external { + uint256 bn = block.number; + vm.startPrank(registry.owner()); + registry.cancelUpkeep(linkUpkeepID); + + uint256 maxValidBlocknumber = uint256(registry.getUpkeep(linkUpkeepID).maxValidBlocknumber); + + // cancellation by registry owner is immediate and no cancellation delay is applied + assertEq(bn, maxValidBlocknumber); + } + + function test_CancelUpkeep_EmitEvent_CalledByAdmin() external { + uint256 bn = block.number; + vm.startPrank(UPKEEP_ADMIN); + + vm.expectEmit(); + emit UpkeepCanceled(linkUpkeepID, uint64(bn + 50)); + registry.cancelUpkeep(linkUpkeepID); + } + + function test_CancelUpkeep_EmitEvent_CalledByOwner() external { + uint256 bn = block.number; + vm.startPrank(registry.owner()); + + vm.expectEmit(); + emit UpkeepCanceled(linkUpkeepID, uint64(bn)); + registry.cancelUpkeep(linkUpkeepID); + } +} + +contract SetPeerRegistryMigrationPermission is SetUp { + function test_SetPeerRegistryMigrationPermission_CalledByOwner() external { + address peer = randomAddress(); + vm.startPrank(registry.owner()); + + uint8 permission = registry.getPeerRegistryMigrationPermission(peer); + assertEq(0, permission); + + registry.setPeerRegistryMigrationPermission(peer, 1); + permission = registry.getPeerRegistryMigrationPermission(peer); + assertEq(1, permission); + + registry.setPeerRegistryMigrationPermission(peer, 2); + permission = registry.getPeerRegistryMigrationPermission(peer); + assertEq(2, permission); + + registry.setPeerRegistryMigrationPermission(peer, 0); + permission = registry.getPeerRegistryMigrationPermission(peer); + assertEq(0, permission); + } + + function test_RevertsWhen_InvalidPermission_CalledByOwner() external { + address peer = randomAddress(); + vm.startPrank(registry.owner()); + + vm.expectRevert(); + registry.setPeerRegistryMigrationPermission(peer, 100); + } + + function test_RevertsWhen_CalledByNonOwner() external { + address peer = randomAddress(); + vm.startPrank(STRANGER); + + vm.expectRevert(bytes("Only callable by owner")); + registry.setPeerRegistryMigrationPermission(peer, 1); + } +} + +contract SetUpkeepPrivilegeConfig is SetUp { + function test_RevertsWhen_CalledByNonManager() external { + vm.startPrank(STRANGER); + + vm.expectRevert(Registry.OnlyCallableByUpkeepPrivilegeManager.selector); + registry.setUpkeepPrivilegeConfig(linkUpkeepID, hex"1233"); + } + + function test_UpkeepHasEmptyConfig() external { + bytes memory cfg = registry.getUpkeepPrivilegeConfig(linkUpkeepID); + assertEq(cfg, bytes("")); + } + + function test_SetUpkeepPrivilegeConfig_CalledByManager() external { + vm.startPrank(PRIVILEGE_MANAGER); + + registry.setUpkeepPrivilegeConfig(linkUpkeepID, hex"1233"); + + bytes memory cfg = registry.getUpkeepPrivilegeConfig(linkUpkeepID); + assertEq(cfg, hex"1233"); + } +} + +contract SetAdminPrivilegeConfig is SetUp { + function test_RevertsWhen_CalledByNonManager() external { + vm.startPrank(STRANGER); + + vm.expectRevert(Registry.OnlyCallableByUpkeepPrivilegeManager.selector); + registry.setAdminPrivilegeConfig(randomAddress(), hex"1233"); + } + + function test_UpkeepHasEmptyConfig() external { + bytes memory cfg = registry.getAdminPrivilegeConfig(randomAddress()); + assertEq(cfg, bytes("")); + } + + function test_SetAdminPrivilegeConfig_CalledByManager() external { + vm.startPrank(PRIVILEGE_MANAGER); + address admin = randomAddress(); + + registry.setAdminPrivilegeConfig(admin, hex"1233"); + + bytes memory cfg = registry.getAdminPrivilegeConfig(admin); + assertEq(cfg, hex"1233"); + } +} + +contract TransferUpkeepAdmin is SetUp { + event UpkeepAdminTransferRequested(uint256 indexed id, address indexed from, address indexed to); + + function test_RevertsWhen_NotCalledByAdmin() external { + vm.startPrank(STRANGER); + + vm.expectRevert(Registry.OnlyCallableByAdmin.selector); + registry.transferUpkeepAdmin(linkUpkeepID, randomAddress()); + } + + function test_RevertsWhen_TransferToSelf() external { + vm.startPrank(UPKEEP_ADMIN); + + vm.expectRevert(Registry.ValueNotChanged.selector); + registry.transferUpkeepAdmin(linkUpkeepID, UPKEEP_ADMIN); + } + + function test_RevertsWhen_UpkeepCanceled() external { + vm.startPrank(UPKEEP_ADMIN); + + registry.cancelUpkeep(linkUpkeepID); + + vm.expectRevert(Registry.UpkeepCancelled.selector); + registry.transferUpkeepAdmin(linkUpkeepID, randomAddress()); + } + + function test_DoesNotChangeUpkeepAdmin() external { + vm.startPrank(UPKEEP_ADMIN); + registry.transferUpkeepAdmin(linkUpkeepID, randomAddress()); + + assertEq(registry.getUpkeep(linkUpkeepID).admin, UPKEEP_ADMIN); + } + + function test_EmitEvent_CalledByAdmin() external { + vm.startPrank(UPKEEP_ADMIN); + address newAdmin = randomAddress(); + + vm.expectEmit(); + emit UpkeepAdminTransferRequested(linkUpkeepID, UPKEEP_ADMIN, newAdmin); + registry.transferUpkeepAdmin(linkUpkeepID, newAdmin); + + // transferring to the same propose admin won't yield another event + vm.recordLogs(); + registry.transferUpkeepAdmin(linkUpkeepID, newAdmin); + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(0, entries.length); + } + + function test_CancelTransfer_ByTransferToEmptyAddress() external { + vm.startPrank(UPKEEP_ADMIN); + address newAdmin = randomAddress(); + + vm.expectEmit(); + emit UpkeepAdminTransferRequested(linkUpkeepID, UPKEEP_ADMIN, newAdmin); + registry.transferUpkeepAdmin(linkUpkeepID, newAdmin); + + vm.expectEmit(); + emit UpkeepAdminTransferRequested(linkUpkeepID, UPKEEP_ADMIN, address(0)); + registry.transferUpkeepAdmin(linkUpkeepID, address(0)); + } +} + +contract AcceptUpkeepAdmin is SetUp { + event UpkeepAdminTransferred(uint256 indexed id, address indexed from, address indexed to); + + function test_RevertsWhen_NotCalledByProposedAdmin() external { + vm.startPrank(UPKEEP_ADMIN); + address newAdmin = randomAddress(); + registry.transferUpkeepAdmin(linkUpkeepID, newAdmin); + + vm.startPrank(STRANGER); + vm.expectRevert(Registry.OnlyCallableByProposedAdmin.selector); + registry.acceptUpkeepAdmin(linkUpkeepID); + } + + function test_RevertsWhen_UpkeepCanceled() external { + vm.startPrank(UPKEEP_ADMIN); + address newAdmin = randomAddress(); + registry.transferUpkeepAdmin(linkUpkeepID, newAdmin); + + registry.cancelUpkeep(linkUpkeepID); + + vm.startPrank(newAdmin); + vm.expectRevert(Registry.UpkeepCancelled.selector); + registry.acceptUpkeepAdmin(linkUpkeepID); + } + + function test_UpkeepAdminChanged() external { + vm.startPrank(UPKEEP_ADMIN); + address newAdmin = randomAddress(); + registry.transferUpkeepAdmin(linkUpkeepID, newAdmin); + + vm.startPrank(newAdmin); + vm.expectEmit(); + emit UpkeepAdminTransferred(linkUpkeepID, UPKEEP_ADMIN, newAdmin); + registry.acceptUpkeepAdmin(linkUpkeepID); + + assertEq(newAdmin, registry.getUpkeep(linkUpkeepID).admin); + } +} + +contract PauseUpkeep is SetUp { + event UpkeepPaused(uint256 indexed id); + + function test_RevertsWhen_NotCalledByUpkeepAdmin() external { + vm.startPrank(STRANGER); + + vm.expectRevert(Registry.OnlyCallableByAdmin.selector); + registry.pauseUpkeep(linkUpkeepID); + } + + function test_RevertsWhen_InvalidUpkeepId() external { + vm.startPrank(UPKEEP_ADMIN); + + vm.expectRevert(Registry.OnlyCallableByAdmin.selector); + registry.pauseUpkeep(linkUpkeepID + 1); + } + + function test_RevertsWhen_UpkeepAlreadyCanceled() external { + vm.startPrank(UPKEEP_ADMIN); + registry.cancelUpkeep(linkUpkeepID); + + vm.expectRevert(Registry.UpkeepCancelled.selector); + registry.pauseUpkeep(linkUpkeepID); + } + + function test_RevertsWhen_UpkeepAlreadyPaused() external { + vm.startPrank(UPKEEP_ADMIN); + registry.pauseUpkeep(linkUpkeepID); + + vm.expectRevert(Registry.OnlyUnpausedUpkeep.selector); + registry.pauseUpkeep(linkUpkeepID); + } + + function test_EmitEvent_CalledByAdmin() external { + vm.startPrank(UPKEEP_ADMIN); + + vm.expectEmit(); + emit UpkeepPaused(linkUpkeepID); + registry.pauseUpkeep(linkUpkeepID); + } +} + +contract UnpauseUpkeep is SetUp { + event UpkeepUnpaused(uint256 indexed id); + + function test_RevertsWhen_InvalidUpkeepId() external { + vm.startPrank(UPKEEP_ADMIN); + + vm.expectRevert(Registry.OnlyCallableByAdmin.selector); + registry.unpauseUpkeep(linkUpkeepID + 1); + } + + function test_RevertsWhen_UpkeepIsNotPaused() external { + vm.startPrank(UPKEEP_ADMIN); + + vm.expectRevert(Registry.OnlyPausedUpkeep.selector); + registry.unpauseUpkeep(linkUpkeepID); + } + + function test_RevertsWhen_UpkeepAlreadyCanceled() external { + vm.startPrank(UPKEEP_ADMIN); + registry.pauseUpkeep(linkUpkeepID); + + registry.cancelUpkeep(linkUpkeepID); + + vm.expectRevert(Registry.UpkeepCancelled.selector); + registry.unpauseUpkeep(linkUpkeepID); + } + + function test_RevertsWhen_NotCalledByUpkeepAdmin() external { + vm.startPrank(UPKEEP_ADMIN); + registry.pauseUpkeep(linkUpkeepID); + + vm.startPrank(STRANGER); + vm.expectRevert(Registry.OnlyCallableByAdmin.selector); + registry.unpauseUpkeep(linkUpkeepID); + } + + function test_UnpauseUpkeep_CalledByUpkeepAdmin() external { + vm.startPrank(UPKEEP_ADMIN); + registry.pauseUpkeep(linkUpkeepID); + + uint256[] memory ids1 = registry.getActiveUpkeepIDs(0, 0); + + vm.expectEmit(); + emit UpkeepUnpaused(linkUpkeepID); + registry.unpauseUpkeep(linkUpkeepID); + + uint256[] memory ids2 = registry.getActiveUpkeepIDs(0, 0); + assertEq(ids1.length + 1, ids2.length); + } +} + +contract SetUpkeepCheckData is SetUp { + event UpkeepCheckDataSet(uint256 indexed id, bytes newCheckData); + + function test_RevertsWhen_InvalidUpkeepId() external { + vm.startPrank(UPKEEP_ADMIN); + + vm.expectRevert(Registry.OnlyCallableByAdmin.selector); + registry.setUpkeepCheckData(linkUpkeepID + 1, hex"1234"); + } + + function test_RevertsWhen_UpkeepAlreadyCanceled() external { + vm.startPrank(UPKEEP_ADMIN); + registry.cancelUpkeep(linkUpkeepID); + + vm.expectRevert(Registry.UpkeepCancelled.selector); + registry.setUpkeepCheckData(linkUpkeepID, hex"1234"); + } + + function test_RevertsWhen_NewCheckDataTooLarge() external { + vm.startPrank(UPKEEP_ADMIN); + + vm.expectRevert(Registry.CheckDataExceedsLimit.selector); + registry.setUpkeepCheckData(linkUpkeepID, new bytes(10_000)); + } + + function test_RevertsWhen_NotCalledByAdmin() external { + vm.startPrank(STRANGER); + + vm.expectRevert(Registry.OnlyCallableByAdmin.selector); + registry.setUpkeepCheckData(linkUpkeepID, hex"1234"); + } + + function test_UpdateOffchainConfig_CalledByAdmin() external { + vm.startPrank(UPKEEP_ADMIN); + + vm.expectEmit(); + emit UpkeepCheckDataSet(linkUpkeepID, hex"1234"); + registry.setUpkeepCheckData(linkUpkeepID, hex"1234"); + + assertEq(registry.getUpkeep(linkUpkeepID).checkData, hex"1234"); + } + + function test_UpdateOffchainConfigOnPausedUpkeep_CalledByAdmin() external { + vm.startPrank(UPKEEP_ADMIN); + + registry.pauseUpkeep(linkUpkeepID); + + vm.expectEmit(); + emit UpkeepCheckDataSet(linkUpkeepID, hex"1234"); + registry.setUpkeepCheckData(linkUpkeepID, hex"1234"); + + assertTrue(registry.getUpkeep(linkUpkeepID).paused); + assertEq(registry.getUpkeep(linkUpkeepID).checkData, hex"1234"); + } +} + +contract SetUpkeepGasLimit is SetUp { + event UpkeepGasLimitSet(uint256 indexed id, uint96 gasLimit); + + function test_RevertsWhen_InvalidUpkeepId() external { + vm.startPrank(UPKEEP_ADMIN); + + vm.expectRevert(Registry.OnlyCallableByAdmin.selector); + registry.setUpkeepGasLimit(linkUpkeepID + 1, 1230000); + } + + function test_RevertsWhen_UpkeepAlreadyCanceled() external { + vm.startPrank(UPKEEP_ADMIN); + registry.cancelUpkeep(linkUpkeepID); + + vm.expectRevert(Registry.UpkeepCancelled.selector); + registry.setUpkeepGasLimit(linkUpkeepID, 1230000); + } + + function test_RevertsWhen_NewGasLimitOutOfRange() external { + vm.startPrank(UPKEEP_ADMIN); + + vm.expectRevert(Registry.GasLimitOutsideRange.selector); + registry.setUpkeepGasLimit(linkUpkeepID, 300); + + vm.expectRevert(Registry.GasLimitOutsideRange.selector); + registry.setUpkeepGasLimit(linkUpkeepID, 3000000000); + } + + function test_RevertsWhen_NotCalledByAdmin() external { + vm.startPrank(STRANGER); + + vm.expectRevert(Registry.OnlyCallableByAdmin.selector); + registry.setUpkeepGasLimit(linkUpkeepID, 1230000); + } + + function test_UpdateGasLimit_CalledByAdmin() external { + vm.startPrank(UPKEEP_ADMIN); + + vm.expectEmit(); + emit UpkeepGasLimitSet(linkUpkeepID, 1230000); + registry.setUpkeepGasLimit(linkUpkeepID, 1230000); + + assertEq(registry.getUpkeep(linkUpkeepID).performGas, 1230000); + } +} + +contract SetUpkeepOffchainConfig is SetUp { + event UpkeepOffchainConfigSet(uint256 indexed id, bytes offchainConfig); + + function test_RevertsWhen_InvalidUpkeepId() external { + vm.startPrank(UPKEEP_ADMIN); + + vm.expectRevert(Registry.OnlyCallableByAdmin.selector); + registry.setUpkeepOffchainConfig(linkUpkeepID + 1, hex"1233"); + } + + function test_RevertsWhen_UpkeepAlreadyCanceled() external { + vm.startPrank(UPKEEP_ADMIN); + registry.cancelUpkeep(linkUpkeepID); + + vm.expectRevert(Registry.UpkeepCancelled.selector); + registry.setUpkeepOffchainConfig(linkUpkeepID, hex"1234"); + } + + function test_RevertsWhen_NotCalledByAdmin() external { + vm.startPrank(STRANGER); + + vm.expectRevert(Registry.OnlyCallableByAdmin.selector); + registry.setUpkeepOffchainConfig(linkUpkeepID, hex"1234"); + } + + function test_UpdateOffchainConfig_CalledByAdmin() external { + vm.startPrank(UPKEEP_ADMIN); + + vm.expectEmit(); + emit UpkeepOffchainConfigSet(linkUpkeepID, hex"1234"); + registry.setUpkeepOffchainConfig(linkUpkeepID, hex"1234"); + + assertEq(registry.getUpkeep(linkUpkeepID).offchainConfig, hex"1234"); + } +} + +contract SetUpkeepTriggerConfig is SetUp { + event UpkeepTriggerConfigSet(uint256 indexed id, bytes triggerConfig); + + function test_RevertsWhen_InvalidUpkeepId() external { + vm.startPrank(UPKEEP_ADMIN); + + vm.expectRevert(Registry.OnlyCallableByAdmin.selector); + registry.setUpkeepTriggerConfig(linkUpkeepID + 1, hex"1233"); + } + + function test_RevertsWhen_UpkeepAlreadyCanceled() external { + vm.startPrank(UPKEEP_ADMIN); + registry.cancelUpkeep(linkUpkeepID); + + vm.expectRevert(Registry.UpkeepCancelled.selector); + registry.setUpkeepTriggerConfig(linkUpkeepID, hex"1234"); + } + + function test_RevertsWhen_NotCalledByAdmin() external { + vm.startPrank(STRANGER); + + vm.expectRevert(Registry.OnlyCallableByAdmin.selector); + registry.setUpkeepTriggerConfig(linkUpkeepID, hex"1234"); + } + + function test_UpdateTriggerConfig_CalledByAdmin() external { + vm.startPrank(UPKEEP_ADMIN); + + vm.expectEmit(); + emit UpkeepTriggerConfigSet(linkUpkeepID, hex"1234"); + registry.setUpkeepTriggerConfig(linkUpkeepID, hex"1234"); + + assertEq(registry.getUpkeepTriggerConfig(linkUpkeepID), hex"1234"); + } +} + +contract TransferPayeeship is SetUp { + event PayeeshipTransferRequested(address indexed transmitter, address indexed from, address indexed to); + + function test_RevertsWhen_NotCalledByPayee() external { + vm.startPrank(STRANGER); + + vm.expectRevert(Registry.OnlyCallableByPayee.selector); + registry.transferPayeeship(TRANSMITTERS[0], randomAddress()); + } + + function test_RevertsWhen_TransferToSelf() external { + registry.setPayees(PAYEES); + vm.startPrank(PAYEES[0]); + + vm.expectRevert(Registry.ValueNotChanged.selector); + registry.transferPayeeship(TRANSMITTERS[0], PAYEES[0]); + } + + function test_Transfer_DoesNotChangePayee() external { + registry.setPayees(PAYEES); + + vm.startPrank(PAYEES[0]); + + registry.transferPayeeship(TRANSMITTERS[0], randomAddress()); + + (, , , , address payee) = registry.getTransmitterInfo(TRANSMITTERS[0]); + assertEq(PAYEES[0], payee); + } + + function test_EmitEvent_CalledByPayee() external { + registry.setPayees(PAYEES); + + vm.startPrank(PAYEES[0]); + address newPayee = randomAddress(); + + vm.expectEmit(); + emit PayeeshipTransferRequested(TRANSMITTERS[0], PAYEES[0], newPayee); + registry.transferPayeeship(TRANSMITTERS[0], newPayee); + + // transferring to the same propose payee won't yield another event + vm.recordLogs(); + registry.transferPayeeship(TRANSMITTERS[0], newPayee); + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(0, entries.length); + } +} + +contract AcceptPayeeship is SetUp { + event PayeeshipTransferred(address indexed transmitter, address indexed from, address indexed to); + + function test_RevertsWhen_NotCalledByProposedPayee() external { + registry.setPayees(PAYEES); + + vm.startPrank(PAYEES[0]); + address newPayee = randomAddress(); + registry.transferPayeeship(TRANSMITTERS[0], newPayee); + + vm.startPrank(STRANGER); + vm.expectRevert(Registry.OnlyCallableByProposedPayee.selector); + registry.acceptPayeeship(TRANSMITTERS[0]); + } + + function test_PayeeChanged() external { + registry.setPayees(PAYEES); + + vm.startPrank(PAYEES[0]); + address newPayee = randomAddress(); + registry.transferPayeeship(TRANSMITTERS[0], newPayee); + + vm.startPrank(newPayee); + vm.expectEmit(); + emit PayeeshipTransferred(TRANSMITTERS[0], PAYEES[0], newPayee); + registry.acceptPayeeship(TRANSMITTERS[0]); + + (, , , , address payee) = registry.getTransmitterInfo(TRANSMITTERS[0]); + assertEq(newPayee, payee); + } +} + +contract SetPayees is SetUp { + event PayeesUpdated(address[] transmitters, address[] payees); + + function test_RevertsWhen_NotCalledByOwner() external { + vm.startPrank(STRANGER); + + vm.expectRevert(bytes("Only callable by owner")); + registry.setPayees(NEW_PAYEES); + } + + function test_RevertsWhen_PayeesLengthError() external { + vm.startPrank(registry.owner()); + + address[] memory payees = new address[](5); + vm.expectRevert(Registry.ParameterLengthError.selector); + registry.setPayees(payees); + } + + function test_RevertsWhen_InvalidPayee() external { + vm.startPrank(registry.owner()); + + NEW_PAYEES[0] = address(0); + vm.expectRevert(Registry.InvalidPayee.selector); + registry.setPayees(NEW_PAYEES); + } + + function test_SetPayees_WhenExistingPayeesAreEmpty() external { + (registry, ) = deployAndConfigureRegistryAndRegistrar(AutoBase.PayoutMode.ON_CHAIN); + + for (uint256 i = 0; i < TRANSMITTERS.length; i++) { + (, , , , address payee) = registry.getTransmitterInfo(TRANSMITTERS[i]); + assertEq(address(0), payee); + } + + vm.startPrank(registry.owner()); + + vm.expectEmit(); + emit PayeesUpdated(TRANSMITTERS, PAYEES); + registry.setPayees(PAYEES); + for (uint256 i = 0; i < TRANSMITTERS.length; i++) { + (bool active, , , , address payee) = registry.getTransmitterInfo(TRANSMITTERS[i]); + assertTrue(active); + assertEq(PAYEES[i], payee); + } + } + + function test_DotNotSetPayeesToIgnoredAddress() external { + address IGNORE_ADDRESS = 0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF; + (registry, ) = deployAndConfigureRegistryAndRegistrar(AutoBase.PayoutMode.ON_CHAIN); + PAYEES[0] = IGNORE_ADDRESS; + + registry.setPayees(PAYEES); + (bool active, , , , address payee) = registry.getTransmitterInfo(TRANSMITTERS[0]); + assertTrue(active); + assertEq(address(0), payee); + + (active, , , , payee) = registry.getTransmitterInfo(TRANSMITTERS[1]); + assertTrue(active); + assertEq(PAYEES[1], payee); + } +} + +contract GetActiveUpkeepIDs is SetUp { + function test_RevertsWhen_IndexOutOfRange() external { + vm.expectRevert(Registry.IndexOutOfRange.selector); + registry.getActiveUpkeepIDs(5, 0); + + vm.expectRevert(Registry.IndexOutOfRange.selector); + registry.getActiveUpkeepIDs(6, 0); + } + + function test_ReturnsAllUpkeeps_WhenMaxCountIsZero() external { + uint256[] memory uids = registry.getActiveUpkeepIDs(0, 0); + assertEq(5, uids.length); + + uids = registry.getActiveUpkeepIDs(2, 0); + assertEq(3, uids.length); + } + + function test_ReturnsAllRemainingUpkeeps_WhenMaxCountIsTooLarge() external { + uint256[] memory uids = registry.getActiveUpkeepIDs(2, 20); + assertEq(3, uids.length); + } + + function test_ReturnsUpkeeps_BoundByMaxCount() external { + uint256[] memory uids = registry.getActiveUpkeepIDs(1, 2); + assertEq(2, uids.length); + assertEq(uids[0], linkUpkeepID2); + assertEq(uids[1], usdUpkeepID18); + } +} diff --git a/contracts/src/v0.8/automation/test/v2_3/BaseTest.t.sol b/contracts/src/v0.8/automation/test/v2_3/BaseTest.t.sol index 9e46e7bb40d..e0d15daab6c 100644 --- a/contracts/src/v0.8/automation/test/v2_3/BaseTest.t.sol +++ b/contracts/src/v0.8/automation/test/v2_3/BaseTest.t.sol @@ -283,7 +283,6 @@ contract BaseTest is Test { billingTokenAddresses, billingTokenConfigs ); - registry.setPayees(PAYEES); return (registry, registrar); } @@ -356,40 +355,58 @@ contract BaseTest is Test { ); } + // tests single upkeep, expects success function _transmit(uint256 id, Registry registry) internal { uint256[] memory ids = new uint256[](1); ids[0] = id; - _transmit(ids, registry); + _handleTransmit(ids, registry, bytes4(0)); } + // tests multiple upkeeps, expects success function _transmit(uint256[] memory ids, Registry registry) internal { - uint256[] memory upkeepIds = new uint256[](ids.length); - uint256[] memory gasLimits = new uint256[](ids.length); - bytes[] memory performDatas = new bytes[](ids.length); - bytes[] memory triggers = new bytes[](ids.length); - for (uint256 i = 0; i < ids.length; i++) { - upkeepIds[i] = ids[i]; - gasLimits[i] = registry.getUpkeep(ids[i]).performGas; - performDatas[i] = new bytes(0); - uint8 triggerType = registry.getTriggerType(ids[i]); - if (triggerType == 0) { - triggers[i] = _encodeConditionalTrigger( - AutoBase.ConditionalTrigger(uint32(block.number - 1), blockhash(block.number - 1)) - ); - } else { - revert("not implemented"); + _handleTransmit(ids, registry, bytes4(0)); + } + + // tests single upkeep, expects revert + function _transmitAndExpectRevert(uint256 id, Registry registry, bytes4 selector) internal { + uint256[] memory ids = new uint256[](1); + ids[0] = id; + _handleTransmit(ids, registry, selector); + } + + // private function not exposed to actual testing contract + function _handleTransmit(uint256[] memory ids, Registry registry, bytes4 selector) private { + bytes memory reportBytes; + { + uint256[] memory upkeepIds = new uint256[](ids.length); + uint256[] memory gasLimits = new uint256[](ids.length); + bytes[] memory performDatas = new bytes[](ids.length); + bytes[] memory triggers = new bytes[](ids.length); + for (uint256 i = 0; i < ids.length; i++) { + upkeepIds[i] = ids[i]; + gasLimits[i] = registry.getUpkeep(ids[i]).performGas; + performDatas[i] = new bytes(0); + uint8 triggerType = registry.getTriggerType(ids[i]); + if (triggerType == 0) { + triggers[i] = _encodeConditionalTrigger( + AutoBase.ConditionalTrigger(uint32(block.number - 1), blockhash(block.number - 1)) + ); + } else { + revert("not implemented"); + } } - } - AutoBase.Report memory report = AutoBase.Report( - uint256(1000000000), - uint256(2000000000), - upkeepIds, - gasLimits, - triggers, - performDatas - ); - bytes memory reportBytes = _encodeReport(report); + AutoBase.Report memory report = AutoBase.Report( + uint256(1000000000), + uint256(2000000000), + upkeepIds, + gasLimits, + triggers, + performDatas + ); + + reportBytes = _encodeReport(report); + } (, , bytes32 configDigest) = registry.latestConfigDetails(); bytes32[3] memory reportContext = [configDigest, configDigest, configDigest]; uint256[] memory signerPKs = new uint256[](2); @@ -398,6 +415,9 @@ contract BaseTest is Test { (bytes32[] memory rs, bytes32[] memory ss, bytes32 vs) = _signReport(reportBytes, reportContext, signerPKs); vm.startPrank(TRANSMITTERS[0]); + if (selector != bytes4(0)) { + vm.expectRevert(selector); + } registry.transmit(reportContext, reportBytes, rs, ss, vs); vm.stopPrank(); } diff --git a/contracts/test/v0.8/automation/AutomationRegistry2_3.test.ts b/contracts/test/v0.8/automation/AutomationRegistry2_3.test.ts index f993271fbbc..3f28a4410b1 100644 --- a/contracts/test/v0.8/automation/AutomationRegistry2_3.test.ts +++ b/contracts/test/v0.8/automation/AutomationRegistry2_3.test.ts @@ -3106,44 +3106,6 @@ describe('AutomationRegistry2_3', () => { await getTransmitTx(registry, keeper1, [upkeepId2]) }) - it('reverts if called on a non existing ID', async () => { - await evmRevertCustomError( - registry - .connect(admin) - .withdrawFunds(upkeepId.add(1), await payee1.getAddress()), - registry, - 'OnlyCallableByAdmin', - ) - }) - - it('reverts if called by anyone but the admin', async () => { - await evmRevertCustomError( - registry - .connect(owner) - .withdrawFunds(upkeepId, await payee1.getAddress()), - registry, - 'OnlyCallableByAdmin', - ) - }) - - it('reverts if called on an uncanceled upkeep', async () => { - await evmRevertCustomError( - registry - .connect(admin) - .withdrawFunds(upkeepId, await payee1.getAddress()), - registry, - 'UpkeepNotCanceled', - ) - }) - - it('reverts if called with the 0 address', async () => { - await evmRevertCustomError( - registry.connect(admin).withdrawFunds(upkeepId, zeroAddress), - registry, - 'InvalidRecipient', - ) - }) - describe('after the registration is paused, then cancelled', () => { it('allows the admin to withdraw', async () => { const balance = await registry.getBalance(upkeepId) @@ -3513,46 +3475,6 @@ describe('AutomationRegistry2_3', () => { }) }) - describe('#getActiveUpkeepIDs', () => { - it('reverts if startIndex is out of bounds ', async () => { - await evmRevertCustomError( - registry.getActiveUpkeepIDs(numUpkeeps, 0), - registry, - 'IndexOutOfRange', - ) - await evmRevertCustomError( - registry.getActiveUpkeepIDs(numUpkeeps + 1, 0), - registry, - 'IndexOutOfRange', - ) - }) - - it('returns upkeep IDs bounded by maxCount', async () => { - let upkeepIds = await registry.getActiveUpkeepIDs(0, 1) - assert(upkeepIds.length == 1) - assert(upkeepIds[0].eq(upkeepId)) - upkeepIds = await registry.getActiveUpkeepIDs(1, 3) - assert(upkeepIds.length == 3) - expect(upkeepIds).to.deep.equal([ - afUpkeepId, - logUpkeepId, - streamsLookupUpkeepId, - ]) - }) - - it('returns as many ids as possible if maxCount > num available', async () => { - const upkeepIds = await registry.getActiveUpkeepIDs(1, numUpkeeps + 100) - assert(upkeepIds.length == numUpkeeps - 1) - }) - - it('returns all upkeep IDs if maxCount is 0', async () => { - let upkeepIds = await registry.getActiveUpkeepIDs(0, 0) - assert(upkeepIds.length == numUpkeeps) - upkeepIds = await registry.getActiveUpkeepIDs(2, 0) - assert(upkeepIds.length == numUpkeeps - 2) - }) - }) - describe('#getMaxPaymentForGas', () => { let maxl1CostWeiArbWithoutMultiplier: BigNumber let maxl1CostWeiOptWithoutMultiplier: BigNumber @@ -4224,1140 +4146,180 @@ describe('AutomationRegistry2_3', () => { }) }) - describe('#setPeerRegistryMigrationPermission() / #getPeerRegistryMigrationPermission()', () => { - const peer = randomAddress() - it('allows the owner to set the peer registries', async () => { - let permission = await registry.getPeerRegistryMigrationPermission(peer) - expect(permission).to.equal(0) - await registry.setPeerRegistryMigrationPermission(peer, 1) - permission = await registry.getPeerRegistryMigrationPermission(peer) - expect(permission).to.equal(1) - await registry.setPeerRegistryMigrationPermission(peer, 2) - permission = await registry.getPeerRegistryMigrationPermission(peer) - expect(permission).to.equal(2) - await registry.setPeerRegistryMigrationPermission(peer, 0) - permission = await registry.getPeerRegistryMigrationPermission(peer) - expect(permission).to.equal(0) - }) - it('reverts if passed an unsupported permission', async () => { - await expect( - registry.connect(admin).setPeerRegistryMigrationPermission(peer, 10), - ).to.be.reverted - }) - it('reverts if not called by the owner', async () => { - await expect( - registry.connect(admin).setPeerRegistryMigrationPermission(peer, 1), - ).to.be.revertedWith('Only callable by owner') - }) - }) - - describe('#pauseUpkeep', () => { - it('reverts if the registration does not exist', async () => { - await evmRevertCustomError( - registry.connect(keeper1).pauseUpkeep(upkeepId.add(1)), - registry, - 'OnlyCallableByAdmin', - ) - }) - - it('reverts if the upkeep is already canceled', async () => { - await registry.connect(admin).cancelUpkeep(upkeepId) - - await evmRevertCustomError( - registry.connect(admin).pauseUpkeep(upkeepId), - registry, - 'UpkeepCancelled', - ) - }) - - it('reverts if the upkeep is already paused', async () => { - await registry.connect(admin).pauseUpkeep(upkeepId) - - await evmRevertCustomError( - registry.connect(admin).pauseUpkeep(upkeepId), - registry, - 'OnlyUnpausedUpkeep', - ) - }) - - it('reverts if the caller is not the upkeep admin', async () => { - await evmRevertCustomError( - registry.connect(keeper1).pauseUpkeep(upkeepId), - registry, - 'OnlyCallableByAdmin', - ) - }) - - it('pauses the upkeep and emits an event', async () => { - const tx = await registry.connect(admin).pauseUpkeep(upkeepId) - await expect(tx).to.emit(registry, 'UpkeepPaused').withArgs(upkeepId) + describe('#cancelUpkeep', () => { + describe('when called by the owner', async () => { + it('immediately prevents upkeep', async () => { + await registry.connect(owner).cancelUpkeep(upkeepId) - const registration = await registry.getUpkeep(upkeepId) - assert.equal(registration.paused, true) + const tx = await getTransmitTx(registry, keeper1, [upkeepId]) + const receipt = await tx.wait() + const cancelledUpkeepReportLogs = + parseCancelledUpkeepReportLogs(receipt) + // exactly 1 CancelledUpkeepReport log should be emitted + assert.equal(cancelledUpkeepReportLogs.length, 1) + }) }) - }) - describe('#unpauseUpkeep', () => { - it('reverts if the registration does not exist', async () => { - await evmRevertCustomError( - registry.connect(keeper1).unpauseUpkeep(upkeepId.add(1)), - registry, - 'OnlyCallableByAdmin', - ) - }) + describe('when called by the admin', async () => { + it('immediately prevents upkeep', async () => { + await linkToken.connect(owner).approve(registry.address, toWei('100')) + await registry.connect(owner).addFunds(upkeepId, toWei('100')) + await registry.connect(admin).cancelUpkeep(upkeepId) - it('reverts if the upkeep is already canceled', async () => { - await registry.connect(owner).cancelUpkeep(upkeepId) + await getTransmitTx(registry, keeper1, [upkeepId]) - await evmRevertCustomError( - registry.connect(admin).unpauseUpkeep(upkeepId), - registry, - 'UpkeepCancelled', - ) - }) + for (let i = 0; i < cancellationDelay; i++) { + await ethers.provider.send('evm_mine', []) + } - it('marks the contract as paused', async () => { - assert.isFalse((await registry.getState()).state.paused) + const tx = await getTransmitTx(registry, keeper1, [upkeepId]) - await registry.connect(owner).pause() + const receipt = await tx.wait() + const cancelledUpkeepReportLogs = + parseCancelledUpkeepReportLogs(receipt) + // exactly 1 CancelledUpkeepReport log should be emitted + assert.equal(cancelledUpkeepReportLogs.length, 1) + }) - assert.isTrue((await registry.getState()).state.paused) - }) + describeMaybe('when an upkeep has been performed', async () => { + beforeEach(async () => { + await linkToken.connect(owner).approve(registry.address, toWei('100')) + await registry.connect(owner).addFunds(upkeepId, toWei('100')) + await getTransmitTx(registry, keeper1, [upkeepId]) + }) - it('reverts if the upkeep is not paused', async () => { - await evmRevertCustomError( - registry.connect(admin).unpauseUpkeep(upkeepId), - registry, - 'OnlyPausedUpkeep', - ) - }) + it('deducts a cancellation fee from the upkeep and adds to reserve', async () => { + const newMinUpkeepSpend = toWei('10') + const financeAdminAddress = await financeAdmin.getAddress() - it('reverts if the caller is not the upkeep admin', async () => { - await registry.connect(admin).pauseUpkeep(upkeepId) + await registry.connect(owner).setConfigTypeSafe( + signerAddresses, + keeperAddresses, + f, + { + checkGasLimit, + stalenessSeconds, + gasCeilingMultiplier, + maxCheckDataSize, + maxPerformDataSize, + maxRevertDataSize, + maxPerformGas, + fallbackGasPrice, + fallbackLinkPrice, + fallbackNativePrice, + transcoder: transcoder.address, + registrars: [], + upkeepPrivilegeManager: upkeepManager, + chainModule: chainModuleBase.address, + reorgProtectionEnabled: true, + financeAdmin: financeAdminAddress, + }, + offchainVersion, + offchainBytes, + [linkToken.address], + [ + { + gasFeePPB: paymentPremiumPPB, + flatFeeMilliCents, + priceFeed: linkUSDFeed.address, + fallbackPrice: fallbackLinkPrice, + minSpend: newMinUpkeepSpend, + decimals: 18, + }, + ], + ) - const registration = await registry.getUpkeep(upkeepId) + const payee1Before = await linkToken.balanceOf( + await payee1.getAddress(), + ) + const upkeepBefore = (await registry.getUpkeep(upkeepId)).balance + const ownerBefore = await registry.linkAvailableForPayment() - assert.equal(registration.paused, true) + const amountSpent = toWei('100').sub(upkeepBefore) + const cancellationFee = newMinUpkeepSpend.sub(amountSpent) - await evmRevertCustomError( - registry.connect(keeper1).unpauseUpkeep(upkeepId), - registry, - 'OnlyCallableByAdmin', - ) - }) + await registry.connect(admin).cancelUpkeep(upkeepId) - it('unpauses the upkeep and emits an event', async () => { - const originalCount = (await registry.getActiveUpkeepIDs(0, 0)).length + const payee1After = await linkToken.balanceOf( + await payee1.getAddress(), + ) + const upkeepAfter = (await registry.getUpkeep(upkeepId)).balance + const ownerAfter = await registry.linkAvailableForPayment() - await registry.connect(admin).pauseUpkeep(upkeepId) + // post upkeep balance should be previous balance minus cancellation fee + assert.isTrue(upkeepBefore.sub(cancellationFee).eq(upkeepAfter)) + // payee balance should not change + assert.isTrue(payee1Before.eq(payee1After)) + // owner should receive the cancellation fee + assert.isTrue(ownerAfter.sub(ownerBefore).eq(cancellationFee)) + }) - const tx = await registry.connect(admin).unpauseUpkeep(upkeepId) + it('deducts up to balance as cancellation fee', async () => { + // Very high min spend, should deduct whole balance as cancellation fees + const newMinUpkeepSpend = toWei('1000') + const financeAdminAddress = await financeAdmin.getAddress() - await expect(tx).to.emit(registry, 'UpkeepUnpaused').withArgs(upkeepId) + await registry.connect(owner).setConfigTypeSafe( + signerAddresses, + keeperAddresses, + f, + { + checkGasLimit, + stalenessSeconds, + gasCeilingMultiplier, + maxCheckDataSize, + maxPerformDataSize, + maxRevertDataSize, + maxPerformGas, + fallbackGasPrice, + fallbackLinkPrice, + fallbackNativePrice, + transcoder: transcoder.address, + registrars: [], + upkeepPrivilegeManager: upkeepManager, + chainModule: chainModuleBase.address, + reorgProtectionEnabled: true, + financeAdmin: financeAdminAddress, + }, + offchainVersion, + offchainBytes, + [linkToken.address], + [ + { + gasFeePPB: paymentPremiumPPB, + flatFeeMilliCents, + priceFeed: linkUSDFeed.address, + fallbackPrice: fallbackLinkPrice, + minSpend: newMinUpkeepSpend, + decimals: 18, + }, + ], + ) + const payee1Before = await linkToken.balanceOf( + await payee1.getAddress(), + ) + const upkeepBefore = (await registry.getUpkeep(upkeepId)).balance + const ownerBefore = await registry.linkAvailableForPayment() - const registration = await registry.getUpkeep(upkeepId) - assert.equal(registration.paused, false) + await registry.connect(admin).cancelUpkeep(upkeepId) + const payee1After = await linkToken.balanceOf( + await payee1.getAddress(), + ) + const ownerAfter = await registry.linkAvailableForPayment() + const upkeepAfter = (await registry.getUpkeep(upkeepId)).balance - const upkeepIds = await registry.getActiveUpkeepIDs(0, 0) - assert.equal(upkeepIds.length, originalCount) - }) - }) + // all upkeep balance is deducted for cancellation fee + assert.equal(upkeepAfter.toNumber(), 0) + // payee balance should not change + assert.isTrue(payee1After.eq(payee1Before)) + // all upkeep balance is transferred to the owner + assert.isTrue(ownerAfter.sub(ownerBefore).eq(upkeepBefore)) + }) - describe('#setUpkeepCheckData', () => { - it('reverts if the registration does not exist', async () => { - await evmRevertCustomError( - registry - .connect(keeper1) - .setUpkeepCheckData(upkeepId.add(1), randomBytes), - registry, - 'OnlyCallableByAdmin', - ) - }) - - it('reverts if the caller is not upkeep admin', async () => { - await evmRevertCustomError( - registry.connect(keeper1).setUpkeepCheckData(upkeepId, randomBytes), - registry, - 'OnlyCallableByAdmin', - ) - }) - - it('reverts if the upkeep is cancelled', async () => { - await registry.connect(admin).cancelUpkeep(upkeepId) - - await evmRevertCustomError( - registry.connect(admin).setUpkeepCheckData(upkeepId, randomBytes), - registry, - 'UpkeepCancelled', - ) - }) - - it('is allowed to update on paused upkeep', async () => { - await registry.connect(admin).pauseUpkeep(upkeepId) - await registry.connect(admin).setUpkeepCheckData(upkeepId, randomBytes) - - const registration = await registry.getUpkeep(upkeepId) - assert.equal(randomBytes, registration.checkData) - }) - - it('reverts if new data exceeds limit', async () => { - let longBytes = '0x' - for (let i = 0; i < 10000; i++) { - longBytes += '1' - } - - await evmRevertCustomError( - registry.connect(admin).setUpkeepCheckData(upkeepId, longBytes), - registry, - 'CheckDataExceedsLimit', - ) - }) - - it('updates the upkeep check data and emits an event', async () => { - const tx = await registry - .connect(admin) - .setUpkeepCheckData(upkeepId, randomBytes) - await expect(tx) - .to.emit(registry, 'UpkeepCheckDataSet') - .withArgs(upkeepId, randomBytes) - - const registration = await registry.getUpkeep(upkeepId) - assert.equal(randomBytes, registration.checkData) - }) - }) - - describe('#setUpkeepGasLimit', () => { - const newGasLimit = BigNumber.from('300000') - - it('reverts if the registration does not exist', async () => { - await evmRevertCustomError( - registry.connect(admin).setUpkeepGasLimit(upkeepId.add(1), newGasLimit), - registry, - 'OnlyCallableByAdmin', - ) - }) - - it('reverts if the upkeep is canceled', async () => { - await registry.connect(admin).cancelUpkeep(upkeepId) - await evmRevertCustomError( - registry.connect(admin).setUpkeepGasLimit(upkeepId, newGasLimit), - registry, - 'UpkeepCancelled', - ) - }) - - it('reverts if called by anyone but the admin', async () => { - await evmRevertCustomError( - registry.connect(owner).setUpkeepGasLimit(upkeepId, newGasLimit), - registry, - 'OnlyCallableByAdmin', - ) - }) - - it('reverts if new gas limit is out of bounds', async () => { - await evmRevertCustomError( - registry - .connect(admin) - .setUpkeepGasLimit(upkeepId, BigNumber.from('100')), - registry, - 'GasLimitOutsideRange', - ) - await evmRevertCustomError( - registry - .connect(admin) - .setUpkeepGasLimit(upkeepId, BigNumber.from('6000000')), - registry, - 'GasLimitOutsideRange', - ) - }) - - it('updates the gas limit successfully', async () => { - const initialGasLimit = (await registry.getUpkeep(upkeepId)).performGas - assert.equal(initialGasLimit, performGas.toNumber()) - await registry.connect(admin).setUpkeepGasLimit(upkeepId, newGasLimit) - const updatedGasLimit = (await registry.getUpkeep(upkeepId)).performGas - assert.equal(updatedGasLimit, newGasLimit.toNumber()) - }) - - it('emits a log', async () => { - const tx = await registry - .connect(admin) - .setUpkeepGasLimit(upkeepId, newGasLimit) - await expect(tx) - .to.emit(registry, 'UpkeepGasLimitSet') - .withArgs(upkeepId, newGasLimit) - }) - }) - - describe('#setUpkeepOffchainConfig', () => { - const newConfig = '0xc0ffeec0ffee' - - it('reverts if the registration does not exist', async () => { - await evmRevertCustomError( - registry - .connect(admin) - .setUpkeepOffchainConfig(upkeepId.add(1), newConfig), - registry, - 'OnlyCallableByAdmin', - ) - }) - - it('reverts if the upkeep is canceled', async () => { - await registry.connect(admin).cancelUpkeep(upkeepId) - await evmRevertCustomError( - registry.connect(admin).setUpkeepOffchainConfig(upkeepId, newConfig), - registry, - 'UpkeepCancelled', - ) - }) - - it('reverts if called by anyone but the admin', async () => { - await evmRevertCustomError( - registry.connect(owner).setUpkeepOffchainConfig(upkeepId, newConfig), - registry, - 'OnlyCallableByAdmin', - ) - }) - - it('updates the config successfully', async () => { - const initialConfig = (await registry.getUpkeep(upkeepId)).offchainConfig - assert.equal(initialConfig, '0x') - await registry.connect(admin).setUpkeepOffchainConfig(upkeepId, newConfig) - const updatedConfig = (await registry.getUpkeep(upkeepId)).offchainConfig - assert.equal(newConfig, updatedConfig) - }) - - it('emits a log', async () => { - const tx = await registry - .connect(admin) - .setUpkeepOffchainConfig(upkeepId, newConfig) - await expect(tx) - .to.emit(registry, 'UpkeepOffchainConfigSet') - .withArgs(upkeepId, newConfig) - }) - }) - - describe('#setUpkeepTriggerConfig', () => { - const newConfig = '0xdeadbeef' - - it('reverts if the registration does not exist', async () => { - await evmRevertCustomError( - registry - .connect(admin) - .setUpkeepTriggerConfig(upkeepId.add(1), newConfig), - registry, - 'OnlyCallableByAdmin', - ) - }) - - it('reverts if the upkeep is canceled', async () => { - await registry.connect(admin).cancelUpkeep(upkeepId) - await evmRevertCustomError( - registry.connect(admin).setUpkeepTriggerConfig(upkeepId, newConfig), - registry, - 'UpkeepCancelled', - ) - }) - - it('reverts if called by anyone but the admin', async () => { - await evmRevertCustomError( - registry.connect(owner).setUpkeepTriggerConfig(upkeepId, newConfig), - registry, - 'OnlyCallableByAdmin', - ) - }) - - it('emits a log', async () => { - const tx = await registry - .connect(admin) - .setUpkeepTriggerConfig(upkeepId, newConfig) - await expect(tx) - .to.emit(registry, 'UpkeepTriggerConfigSet') - .withArgs(upkeepId, newConfig) - }) - }) - - describe('#transferUpkeepAdmin', () => { - it('reverts when called by anyone but the current upkeep admin', async () => { - await evmRevertCustomError( - registry - .connect(payee1) - .transferUpkeepAdmin(upkeepId, await payee2.getAddress()), - registry, - 'OnlyCallableByAdmin', - ) - }) - - it('reverts when transferring to self', async () => { - await evmRevertCustomError( - registry - .connect(admin) - .transferUpkeepAdmin(upkeepId, await admin.getAddress()), - registry, - 'ValueNotChanged', - ) - }) - - it('reverts when the upkeep is cancelled', async () => { - await registry.connect(admin).cancelUpkeep(upkeepId) - - await evmRevertCustomError( - registry - .connect(admin) - .transferUpkeepAdmin(upkeepId, await keeper1.getAddress()), - registry, - 'UpkeepCancelled', - ) - }) - - it('allows cancelling transfer by reverting to zero address', async () => { - await registry - .connect(admin) - .transferUpkeepAdmin(upkeepId, await payee1.getAddress()) - const tx = await registry - .connect(admin) - .transferUpkeepAdmin(upkeepId, ethers.constants.AddressZero) - - await expect(tx) - .to.emit(registry, 'UpkeepAdminTransferRequested') - .withArgs( - upkeepId, - await admin.getAddress(), - ethers.constants.AddressZero, - ) - }) - - it('does not change the upkeep admin', async () => { - await registry - .connect(admin) - .transferUpkeepAdmin(upkeepId, await payee1.getAddress()) - - const upkeep = await registry.getUpkeep(upkeepId) - assert.equal(await admin.getAddress(), upkeep.admin) - }) - - it('emits an event announcing the new upkeep admin', async () => { - const tx = await registry - .connect(admin) - .transferUpkeepAdmin(upkeepId, await payee1.getAddress()) - - await expect(tx) - .to.emit(registry, 'UpkeepAdminTransferRequested') - .withArgs(upkeepId, await admin.getAddress(), await payee1.getAddress()) - }) - - it('does not emit an event when called with the same proposed upkeep admin', async () => { - await registry - .connect(admin) - .transferUpkeepAdmin(upkeepId, await payee1.getAddress()) - - const tx = await registry - .connect(admin) - .transferUpkeepAdmin(upkeepId, await payee1.getAddress()) - const receipt = await tx.wait() - assert.equal(receipt.logs.length, 0) - }) - }) - - describe('#acceptUpkeepAdmin', () => { - beforeEach(async () => { - // Start admin transfer to payee1 - await registry - .connect(admin) - .transferUpkeepAdmin(upkeepId, await payee1.getAddress()) - }) - - it('reverts when not called by the proposed upkeep admin', async () => { - await evmRevertCustomError( - registry.connect(payee2).acceptUpkeepAdmin(upkeepId), - registry, - 'OnlyCallableByProposedAdmin', - ) - }) - - it('reverts when the upkeep is cancelled', async () => { - await registry.connect(admin).cancelUpkeep(upkeepId) - - await evmRevertCustomError( - registry.connect(payee1).acceptUpkeepAdmin(upkeepId), - registry, - 'UpkeepCancelled', - ) - }) - - it('does change the admin', async () => { - await registry.connect(payee1).acceptUpkeepAdmin(upkeepId) - - const upkeep = await registry.getUpkeep(upkeepId) - assert.equal(await payee1.getAddress(), upkeep.admin) - }) - - it('emits an event announcing the new upkeep admin', async () => { - const tx = await registry.connect(payee1).acceptUpkeepAdmin(upkeepId) - await expect(tx) - .to.emit(registry, 'UpkeepAdminTransferred') - .withArgs(upkeepId, await admin.getAddress(), await payee1.getAddress()) - }) - }) - - describe('#withdrawOwnerFunds', () => { - it('can only be called by finance admin', async () => { - await evmRevertCustomError( - registry.connect(keeper1).withdrawLink(zeroAddress, 1), - registry, - 'OnlyFinanceAdmin', - ) - }) - - itMaybe('withdraws the collected fees to owner', async () => { - await registry.connect(admin).addFunds(upkeepId, toWei('100')) - const financeAdminAddress = await financeAdmin.getAddress() - // Very high min spend, whole balance as cancellation fees - const newMinUpkeepSpend = toWei('1000') - await registry.connect(owner).setConfigTypeSafe( - signerAddresses, - keeperAddresses, - f, - { - checkGasLimit, - stalenessSeconds, - gasCeilingMultiplier, - maxCheckDataSize, - maxPerformDataSize, - maxRevertDataSize, - maxPerformGas, - fallbackGasPrice, - fallbackLinkPrice, - fallbackNativePrice, - transcoder: transcoder.address, - registrars: [], - upkeepPrivilegeManager: upkeepManager, - chainModule: chainModuleBase.address, - reorgProtectionEnabled: true, - financeAdmin: financeAdminAddress, - }, - offchainVersion, - offchainBytes, - [linkToken.address], - [ - { - gasFeePPB: paymentPremiumPPB, - flatFeeMilliCents, - priceFeed: linkUSDFeed.address, - fallbackPrice: fallbackLinkPrice, - minSpend: newMinUpkeepSpend, - decimals: 18, - }, - ], - ) - const upkeepBalance = (await registry.getUpkeep(upkeepId)).balance - const ownerBefore = await linkToken.balanceOf(await owner.getAddress()) - - await registry.connect(owner).cancelUpkeep(upkeepId) - - // Transfered to owner balance on registry - let ownerRegistryBalance = await registry.linkAvailableForPayment() - assert.isTrue(ownerRegistryBalance.eq(upkeepBalance)) - - // Now withdraw - await registry - .connect(financeAdmin) - .withdrawLink(await owner.getAddress(), ownerRegistryBalance) - - ownerRegistryBalance = await registry.linkAvailableForPayment() - const ownerAfter = await linkToken.balanceOf(await owner.getAddress()) - - // Owner registry balance should be changed to 0 - assert.isTrue(ownerRegistryBalance.eq(BigNumber.from('0'))) - - // Owner should be credited with the balance - assert.isTrue(ownerBefore.add(upkeepBalance).eq(ownerAfter)) - }) - }) - - describe('#transferPayeeship', () => { - it('reverts when called by anyone but the current payee', async () => { - await evmRevertCustomError( - registry - .connect(payee2) - .transferPayeeship( - await keeper1.getAddress(), - await payee2.getAddress(), - ), - registry, - 'OnlyCallableByPayee', - ) - }) - - it('reverts when transferring to self', async () => { - await evmRevertCustomError( - registry - .connect(payee1) - .transferPayeeship( - await keeper1.getAddress(), - await payee1.getAddress(), - ), - registry, - 'ValueNotChanged', - ) - }) - - it('does not change the payee', async () => { - await registry - .connect(payee1) - .transferPayeeship( - await keeper1.getAddress(), - await payee2.getAddress(), - ) - - const info = await registry.getTransmitterInfo(await keeper1.getAddress()) - assert.equal(await payee1.getAddress(), info.payee) - }) - - it('emits an event announcing the new payee', async () => { - const tx = await registry - .connect(payee1) - .transferPayeeship( - await keeper1.getAddress(), - await payee2.getAddress(), - ) - await expect(tx) - .to.emit(registry, 'PayeeshipTransferRequested') - .withArgs( - await keeper1.getAddress(), - await payee1.getAddress(), - await payee2.getAddress(), - ) - }) - - it('does not emit an event when called with the same proposal', async () => { - await registry - .connect(payee1) - .transferPayeeship( - await keeper1.getAddress(), - await payee2.getAddress(), - ) - - const tx = await registry - .connect(payee1) - .transferPayeeship( - await keeper1.getAddress(), - await payee2.getAddress(), - ) - const receipt = await tx.wait() - assert.equal(receipt.logs.length, 0) - }) - }) - - describe('#acceptPayeeship', () => { - beforeEach(async () => { - await registry - .connect(payee1) - .transferPayeeship( - await keeper1.getAddress(), - await payee2.getAddress(), - ) - }) - - it('reverts when called by anyone but the proposed payee', async () => { - await evmRevertCustomError( - registry.connect(payee1).acceptPayeeship(await keeper1.getAddress()), - registry, - 'OnlyCallableByProposedPayee', - ) - }) - - it('emits an event announcing the new payee', async () => { - const tx = await registry - .connect(payee2) - .acceptPayeeship(await keeper1.getAddress()) - await expect(tx) - .to.emit(registry, 'PayeeshipTransferred') - .withArgs( - await keeper1.getAddress(), - await payee1.getAddress(), - await payee2.getAddress(), - ) - }) - - it('does change the payee', async () => { - await registry.connect(payee2).acceptPayeeship(await keeper1.getAddress()) - - const info = await registry.getTransmitterInfo(await keeper1.getAddress()) - assert.equal(await payee2.getAddress(), info.payee) - }) - }) - - describe('#pause', () => { - it('reverts if called by a non-owner', async () => { - await evmRevert( - registry.connect(keeper1).pause(), - 'Only callable by owner', - ) - }) - - it('marks the contract as paused', async () => { - assert.isFalse((await registry.getState()).state.paused) - - await registry.connect(owner).pause() - - assert.isTrue((await registry.getState()).state.paused) - }) - - it('Does not allow transmits when paused', async () => { - await registry.connect(owner).pause() - - await evmRevertCustomError( - getTransmitTx(registry, keeper1, [upkeepId]), - registry, - 'RegistryPaused', - ) - }) - - it('Does not allow creation of new upkeeps when paused', async () => { - await registry.connect(owner).pause() - - await evmRevertCustomError( - registry - .connect(owner) - .registerUpkeep( - mock.address, - performGas, - await admin.getAddress(), - Trigger.CONDITION, - linkToken.address, - '0x', - '0x', - '0x', - ), - registry, - 'RegistryPaused', - ) - }) - }) - - describe('#unpause', () => { - beforeEach(async () => { - await registry.connect(owner).pause() - }) - - it('reverts if called by a non-owner', async () => { - await evmRevert( - registry.connect(keeper1).unpause(), - 'Only callable by owner', - ) - }) - - it('marks the contract as not paused', async () => { - assert.isTrue((await registry.getState()).state.paused) - - await registry.connect(owner).unpause() - - assert.isFalse((await registry.getState()).state.paused) - }) - }) - - describe('#setPayees', () => { - const IGNORE_ADDRESS = '0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF' - - it('reverts when not called by the owner', async () => { - await evmRevert( - registry.connect(keeper1).setPayees(payees), - 'Only callable by owner', - ) - }) - - it('reverts with different numbers of payees than transmitters', async () => { - await evmRevertCustomError( - registry.connect(owner).setPayees([...payees, randomAddress()]), - registry, - 'ParameterLengthError', - ) - }) - - it('reverts if the payee is the zero address', async () => { - await blankRegistry.connect(owner).setConfigTypeSafe(...baseConfig) // used to test initial config - - await evmRevertCustomError( - blankRegistry // used to test initial config - .connect(owner) - .setPayees([ethers.constants.AddressZero, ...payees.slice(1)]), - registry, - 'InvalidPayee', - ) - }) - - itMaybe( - 'sets the payees when exisitng payees are zero address', - async () => { - //Initial payees should be zero address - await blankRegistry.connect(owner).setConfigTypeSafe(...baseConfig) // used to test initial config - - for (let i = 0; i < keeperAddresses.length; i++) { - const payee = ( - await blankRegistry.getTransmitterInfo(keeperAddresses[i]) - ).payee // used to test initial config - assert.equal(payee, zeroAddress) - } - - await blankRegistry.connect(owner).setPayees(payees) // used to test initial config - - for (let i = 0; i < keeperAddresses.length; i++) { - const payee = ( - await blankRegistry.getTransmitterInfo(keeperAddresses[i]) - ).payee - assert.equal(payee, payees[i]) - } - }, - ) - - it('does not change the payee if IGNORE_ADDRESS is used as payee', async () => { - const signers = Array.from({ length: 5 }, randomAddress) - const keepers = Array.from({ length: 5 }, randomAddress) - const payees = Array.from({ length: 5 }, randomAddress) - const newTransmitter = randomAddress() - const newPayee = randomAddress() - const ignoreAddresses = new Array(payees.length).fill(IGNORE_ADDRESS) - const newPayees = [...ignoreAddresses, newPayee] - // arbitrum registry - // configure registry with 5 keepers // optimism registry - await blankRegistry // used to test initial configurations - .connect(owner) - .setConfigTypeSafe( - signers, - keepers, - f, - config, - offchainVersion, - offchainBytes, - [], - [], - ) - // arbitrum registry - // set initial payees // optimism registry - await blankRegistry.connect(owner).setPayees(payees) // used to test initial configurations - // arbitrum registry - // add another keeper // optimism registry - await blankRegistry // used to test initial configurations - .connect(owner) - .setConfigTypeSafe( - [...signers, randomAddress()], - [...keepers, newTransmitter], - f, - config, - offchainVersion, - offchainBytes, - [], - [], - ) - // arbitrum registry - // update payee list // optimism registry // arbitrum registry - await blankRegistry.connect(owner).setPayees(newPayees) // used to test initial configurations // optimism registry - const ignored = await blankRegistry.getTransmitterInfo(newTransmitter) // used to test initial configurations - assert.equal(newPayee, ignored.payee) - assert.equal(ignored.active, true) - }) - - it('reverts if payee is non zero and owner tries to change payee', async () => { - const newPayees = [randomAddress(), ...payees.slice(1)] - - await evmRevertCustomError( - registry.connect(owner).setPayees(newPayees), - registry, - 'InvalidPayee', - ) - }) - - it('emits events for every payee added and removed', async () => { - const tx = await registry.connect(owner).setPayees(payees) - await expect(tx) - .to.emit(registry, 'PayeesUpdated') - .withArgs(keeperAddresses, payees) - }) - }) - - describe('#cancelUpkeep', () => { - it('reverts if the ID is not valid', async () => { - await evmRevertCustomError( - registry.connect(owner).cancelUpkeep(upkeepId.add(1)), - registry, - 'CannotCancel', - ) - }) - - it('reverts if called by a non-owner/non-admin', async () => { - await evmRevertCustomError( - registry.connect(keeper1).cancelUpkeep(upkeepId), - registry, - 'OnlyCallableByOwnerOrAdmin', - ) - }) - - describe('when called by the owner', async () => { - it('sets the registration to invalid immediately', async () => { - const tx = await registry.connect(owner).cancelUpkeep(upkeepId) - const receipt = await tx.wait() - const registration = await registry.getUpkeep(upkeepId) - assert.equal( - registration.maxValidBlocknumber.toNumber(), - receipt.blockNumber, - ) - }) - - it('emits an event', async () => { - const tx = await registry.connect(owner).cancelUpkeep(upkeepId) - const receipt = await tx.wait() - await expect(tx) - .to.emit(registry, 'UpkeepCanceled') - .withArgs(upkeepId, BigNumber.from(receipt.blockNumber)) - }) - - it('immediately prevents upkeep', async () => { - await registry.connect(owner).cancelUpkeep(upkeepId) - - const tx = await getTransmitTx(registry, keeper1, [upkeepId]) - const receipt = await tx.wait() - const cancelledUpkeepReportLogs = - parseCancelledUpkeepReportLogs(receipt) - // exactly 1 CancelledUpkeepReport log should be emitted - assert.equal(cancelledUpkeepReportLogs.length, 1) - }) - - it('does not revert if reverts if called multiple times', async () => { - await registry.connect(owner).cancelUpkeep(upkeepId) - await evmRevertCustomError( - registry.connect(owner).cancelUpkeep(upkeepId), - registry, - 'UpkeepCancelled', - ) - }) - - describe('when called by the owner when the admin has just canceled', () => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - // @ts-ignore - let oldExpiration: BigNumber - - beforeEach(async () => { - await registry.connect(admin).cancelUpkeep(upkeepId) - const registration = await registry.getUpkeep(upkeepId) - oldExpiration = registration.maxValidBlocknumber - }) - - it('reverts with proper error', async () => { - await evmRevertCustomError( - registry.connect(owner).cancelUpkeep(upkeepId), - registry, - 'UpkeepCancelled', - ) - }) - }) - }) - - describe('when called by the admin', async () => { - it('reverts if called again by the admin', async () => { - await registry.connect(admin).cancelUpkeep(upkeepId) - - await evmRevertCustomError( - registry.connect(admin).cancelUpkeep(upkeepId), - registry, - 'UpkeepCancelled', - ) - }) - - it('reverts if called by the owner after the timeout', async () => { - await registry.connect(admin).cancelUpkeep(upkeepId) - - for (let i = 0; i < cancellationDelay; i++) { - await ethers.provider.send('evm_mine', []) - } - - await evmRevertCustomError( - registry.connect(owner).cancelUpkeep(upkeepId), - registry, - 'UpkeepCancelled', - ) - }) - - it('sets the registration to invalid in 50 blocks', async () => { - const tx = await registry.connect(admin).cancelUpkeep(upkeepId) - const receipt = await tx.wait() - const registration = await registry.getUpkeep(upkeepId) - assert.equal( - registration.maxValidBlocknumber.toNumber(), - receipt.blockNumber + 50, - ) - }) - - it('emits an event', async () => { - const tx = await registry.connect(admin).cancelUpkeep(upkeepId) - const receipt = await tx.wait() - await expect(tx) - .to.emit(registry, 'UpkeepCanceled') - .withArgs( - upkeepId, - BigNumber.from(receipt.blockNumber + cancellationDelay), - ) - }) - - it('immediately prevents upkeep', async () => { - await linkToken.connect(owner).approve(registry.address, toWei('100')) - await registry.connect(owner).addFunds(upkeepId, toWei('100')) - await registry.connect(admin).cancelUpkeep(upkeepId) - - await getTransmitTx(registry, keeper1, [upkeepId]) - - for (let i = 0; i < cancellationDelay; i++) { - await ethers.provider.send('evm_mine', []) - } - - const tx = await getTransmitTx(registry, keeper1, [upkeepId]) - - const receipt = await tx.wait() - const cancelledUpkeepReportLogs = - parseCancelledUpkeepReportLogs(receipt) - // exactly 1 CancelledUpkeepReport log should be emitted - assert.equal(cancelledUpkeepReportLogs.length, 1) - }) - - describeMaybe('when an upkeep has been performed', async () => { - beforeEach(async () => { - await linkToken.connect(owner).approve(registry.address, toWei('100')) - await registry.connect(owner).addFunds(upkeepId, toWei('100')) - await getTransmitTx(registry, keeper1, [upkeepId]) - }) - - it('deducts a cancellation fee from the upkeep and adds to reserve', async () => { - const newMinUpkeepSpend = toWei('10') - const financeAdminAddress = await financeAdmin.getAddress() - - await registry.connect(owner).setConfigTypeSafe( - signerAddresses, - keeperAddresses, - f, - { - checkGasLimit, - stalenessSeconds, - gasCeilingMultiplier, - maxCheckDataSize, - maxPerformDataSize, - maxRevertDataSize, - maxPerformGas, - fallbackGasPrice, - fallbackLinkPrice, - fallbackNativePrice, - transcoder: transcoder.address, - registrars: [], - upkeepPrivilegeManager: upkeepManager, - chainModule: chainModuleBase.address, - reorgProtectionEnabled: true, - financeAdmin: financeAdminAddress, - }, - offchainVersion, - offchainBytes, - [linkToken.address], - [ - { - gasFeePPB: paymentPremiumPPB, - flatFeeMilliCents, - priceFeed: linkUSDFeed.address, - fallbackPrice: fallbackLinkPrice, - minSpend: newMinUpkeepSpend, - decimals: 18, - }, - ], - ) - - const payee1Before = await linkToken.balanceOf( - await payee1.getAddress(), - ) - const upkeepBefore = (await registry.getUpkeep(upkeepId)).balance - const ownerBefore = await registry.linkAvailableForPayment() - - const amountSpent = toWei('100').sub(upkeepBefore) - const cancellationFee = newMinUpkeepSpend.sub(amountSpent) - - await registry.connect(admin).cancelUpkeep(upkeepId) - - const payee1After = await linkToken.balanceOf( - await payee1.getAddress(), - ) - const upkeepAfter = (await registry.getUpkeep(upkeepId)).balance - const ownerAfter = await registry.linkAvailableForPayment() - - // post upkeep balance should be previous balance minus cancellation fee - assert.isTrue(upkeepBefore.sub(cancellationFee).eq(upkeepAfter)) - // payee balance should not change - assert.isTrue(payee1Before.eq(payee1After)) - // owner should receive the cancellation fee - assert.isTrue(ownerAfter.sub(ownerBefore).eq(cancellationFee)) - }) - - it('deducts up to balance as cancellation fee', async () => { - // Very high min spend, should deduct whole balance as cancellation fees - const newMinUpkeepSpend = toWei('1000') - const financeAdminAddress = await financeAdmin.getAddress() - - await registry.connect(owner).setConfigTypeSafe( - signerAddresses, - keeperAddresses, - f, - { - checkGasLimit, - stalenessSeconds, - gasCeilingMultiplier, - maxCheckDataSize, - maxPerformDataSize, - maxRevertDataSize, - maxPerformGas, - fallbackGasPrice, - fallbackLinkPrice, - fallbackNativePrice, - transcoder: transcoder.address, - registrars: [], - upkeepPrivilegeManager: upkeepManager, - chainModule: chainModuleBase.address, - reorgProtectionEnabled: true, - financeAdmin: financeAdminAddress, - }, - offchainVersion, - offchainBytes, - [linkToken.address], - [ - { - gasFeePPB: paymentPremiumPPB, - flatFeeMilliCents, - priceFeed: linkUSDFeed.address, - fallbackPrice: fallbackLinkPrice, - minSpend: newMinUpkeepSpend, - decimals: 18, - }, - ], - ) - const payee1Before = await linkToken.balanceOf( - await payee1.getAddress(), - ) - const upkeepBefore = (await registry.getUpkeep(upkeepId)).balance - const ownerBefore = await registry.linkAvailableForPayment() - - await registry.connect(admin).cancelUpkeep(upkeepId) - const payee1After = await linkToken.balanceOf( - await payee1.getAddress(), - ) - const ownerAfter = await registry.linkAvailableForPayment() - const upkeepAfter = (await registry.getUpkeep(upkeepId)).balance - - // all upkeep balance is deducted for cancellation fee - assert.equal(upkeepAfter.toNumber(), 0) - // payee balance should not change - assert.isTrue(payee1After.eq(payee1Before)) - // all upkeep balance is transferred to the owner - assert.isTrue(ownerAfter.sub(ownerBefore).eq(upkeepBefore)) - }) - - it('does not deduct cancellation fee if more than minUpkeepSpendDollars is spent', async () => { - // Very low min spend, already spent in one perform upkeep - const newMinUpkeepSpend = BigNumber.from(420) - const financeAdminAddress = await financeAdmin.getAddress() + it('does not deduct cancellation fee if more than minUpkeepSpendDollars is spent', async () => { + // Very low min spend, already spent in one perform upkeep + const newMinUpkeepSpend = BigNumber.from(420) + const financeAdminAddress = await financeAdmin.getAddress() await registry.connect(owner).setConfigTypeSafe( signerAddresses, @@ -5594,62 +4556,6 @@ describe('AutomationRegistry2_3', () => { }) }) - describe('#setUpkeepPrivilegeConfig() / #getUpkeepPrivilegeConfig()', () => { - it('reverts when non manager tries to set privilege config', async () => { - await evmRevertCustomError( - registry.connect(payee3).setUpkeepPrivilegeConfig(upkeepId, '0x1234'), - registry, - 'OnlyCallableByUpkeepPrivilegeManager', - ) - }) - - it('returns empty bytes for upkeep privilege config before setting', async () => { - const cfg = await registry.getUpkeepPrivilegeConfig(upkeepId) - assert.equal(cfg, '0x') - }) - - it('allows upkeep manager to set privilege config', async () => { - const tx = await registry - .connect(personas.Norbert) - .setUpkeepPrivilegeConfig(upkeepId, '0x1234') - await expect(tx) - .to.emit(registry, 'UpkeepPrivilegeConfigSet') - .withArgs(upkeepId, '0x1234') - - const cfg = await registry.getUpkeepPrivilegeConfig(upkeepId) - assert.equal(cfg, '0x1234') - }) - }) - - describe('#setAdminPrivilegeConfig() / #getAdminPrivilegeConfig()', () => { - const admin = randomAddress() - - it('reverts when non manager tries to set privilege config', async () => { - await evmRevertCustomError( - registry.connect(payee3).setAdminPrivilegeConfig(admin, '0x1234'), - registry, - 'OnlyCallableByUpkeepPrivilegeManager', - ) - }) - - it('returns empty bytes for upkeep privilege config before setting', async () => { - const cfg = await registry.getAdminPrivilegeConfig(admin) - assert.equal(cfg, '0x') - }) - - it('allows upkeep manager to set privilege config', async () => { - const tx = await registry - .connect(personas.Norbert) - .setAdminPrivilegeConfig(admin, '0x1234') - await expect(tx) - .to.emit(registry, 'AdminPrivilegeConfigSet') - .withArgs(admin, '0x1234') - - const cfg = await registry.getAdminPrivilegeConfig(admin) - assert.equal(cfg, '0x1234') - }) - }) - describe('transmitterPremiumSplit [ @skip-coverage ]', () => { beforeEach(async () => { await linkToken.connect(owner).approve(registry.address, toWei('100')) From 1257d33913d243c146bccbf4bda67a2bb1c7d5af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deividas=20Kar=C5=BEinauskas?= Date: Thu, 8 Aug 2024 05:30:17 +0300 Subject: [PATCH 7/9] Allow retrying failed transmissions (#14017) * Hardcoded value for call with exact gas * Record gasProvided in route function * Add a getter for transmission gas limit * Update snapshot * Changeset * Remove unused import * Rename to gas limit * Update gethwrappers * Uncomment test code * Remove copy/pasta comment * Slight rename * Allow retrying transmissions with more gas * Only allow retrying failed transmissions * Update snapshot * Fix state for InvalidReceiver check * Check for initial state * Actually store gas limit provided to receiver * Update gethwrappers * Remove unused struct * Correctly mark invalid receiver when receiver interface unsupported * Create TransmissionInfo struct * Update gethwrappers * Bump gas limit * Bump gas even more * Update KeystoneFeedsConsumer.sol to implement IERC165 * Use getTransmissionInfo * Use TransmissionState to determine if transmission should be created * Fix test * Fix trailing line * Update a mock to the GetLatestValue("getTransmissionInfo") call in a test * Remove TODO + replace panic with err * Remove redundant empty lines * Typo * Remove unused constant * Name mapping values --------- Co-authored-by: app-token-issuer-infra-releng[bot] <120227048+app-token-issuer-infra-releng[bot]@users.noreply.github.com> --- .changeset/rich-chairs-hug.md | 5 + contracts/.changeset/polite-masks-jog.md | 5 + contracts/.gas-snapshot | 112 ++++++++++++++++++ contracts/gas-snapshots/keystone.gas-snapshot | 24 ++-- .../v0.8/keystone/KeystoneFeedsConsumer.sol | 9 +- .../src/v0.8/keystone/KeystoneForwarder.sol | 94 +++++++++++---- .../v0.8/keystone/interfaces/IReceiver.sol | 5 + .../src/v0.8/keystone/interfaces/IRouter.sol | 42 ++++++- .../test/KeystoneForwarder_ReportTest.t.sol | 82 +++++++++---- .../test/KeystoneRouter_AccessTest.t.sol | 16 ++- .../src/v0.8/keystone/test/mocks/Receiver.sol | 10 +- core/capabilities/targets/write_target.go | 46 +++++-- .../capabilities/targets/write_target_test.go | 18 ++- .../feeds_consumer/feeds_consumer.go | 28 ++++- .../keystone/generated/forwarder/forwarder.go | 31 +++-- ...rapper-dependency-versions-do-not-edit.txt | 4 +- core/services/relay/evm/write_target.go | 9 +- core/services/relay/evm/write_target_test.go | 69 ++++++++++- 18 files changed, 501 insertions(+), 108 deletions(-) create mode 100644 .changeset/rich-chairs-hug.md create mode 100644 contracts/.changeset/polite-masks-jog.md create mode 100644 contracts/.gas-snapshot diff --git a/.changeset/rich-chairs-hug.md b/.changeset/rich-chairs-hug.md new file mode 100644 index 00000000000..0408383bd03 --- /dev/null +++ b/.changeset/rich-chairs-hug.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +#internal diff --git a/contracts/.changeset/polite-masks-jog.md b/contracts/.changeset/polite-masks-jog.md new file mode 100644 index 00000000000..93fba83b558 --- /dev/null +++ b/contracts/.changeset/polite-masks-jog.md @@ -0,0 +1,5 @@ +--- +'@chainlink/contracts': patch +--- + +#internal diff --git a/contracts/.gas-snapshot b/contracts/.gas-snapshot new file mode 100644 index 00000000000..3a0354d539c --- /dev/null +++ b/contracts/.gas-snapshot @@ -0,0 +1,112 @@ +CapabilitiesRegistry_AddCapabilitiesTest:test_AddCapability_NoConfigurationContract() (gas: 154832) +CapabilitiesRegistry_AddCapabilitiesTest:test_AddCapability_WithConfiguration() (gas: 178813) +CapabilitiesRegistry_AddCapabilitiesTest:test_RevertWhen_CalledByNonAdmin() (gas: 24723) +CapabilitiesRegistry_AddCapabilitiesTest:test_RevertWhen_CapabilityExists() (gas: 145703) +CapabilitiesRegistry_AddCapabilitiesTest:test_RevertWhen_ConfigurationContractDoesNotMatchInterface() (gas: 94606) +CapabilitiesRegistry_AddCapabilitiesTest:test_RevertWhen_ConfigurationContractNotDeployed() (gas: 92961) +CapabilitiesRegistry_AddDONTest:test_AddDON() (gas: 372302) +CapabilitiesRegistry_AddDONTest:test_RevertWhen_CalledByNonAdmin() (gas: 19273) +CapabilitiesRegistry_AddDONTest:test_RevertWhen_CapabilityDoesNotExist() (gas: 169752) +CapabilitiesRegistry_AddDONTest:test_RevertWhen_DeprecatedCapabilityAdded() (gas: 239789) +CapabilitiesRegistry_AddDONTest:test_RevertWhen_DuplicateCapabilityAdded() (gas: 249596) +CapabilitiesRegistry_AddDONTest:test_RevertWhen_DuplicateNodeAdded() (gas: 116890) +CapabilitiesRegistry_AddDONTest:test_RevertWhen_FaultToleranceIsZero() (gas: 43358) +CapabilitiesRegistry_AddDONTest:test_RevertWhen_NodeAlreadyBelongsToWorkflowDON() (gas: 343924) +CapabilitiesRegistry_AddDONTest:test_RevertWhen_NodeDoesNotSupportCapability() (gas: 180150) +CapabilitiesRegistry_AddNodeOperatorsTest:test_AddNodeOperators() (gas: 184135) +CapabilitiesRegistry_AddNodeOperatorsTest:test_RevertWhen_CalledByNonAdmin() (gas: 17602) +CapabilitiesRegistry_AddNodeOperatorsTest:test_RevertWhen_NodeOperatorAdminAddressZero() (gas: 18498) +CapabilitiesRegistry_AddNodesTest:test_AddsNodeParams() (gas: 358448) +CapabilitiesRegistry_AddNodesTest:test_OwnerCanAddNodes() (gas: 358414) +CapabilitiesRegistry_AddNodesTest:test_RevertWhen_AddingDuplicateP2PId() (gas: 301229) +CapabilitiesRegistry_AddNodesTest:test_RevertWhen_AddingNodeWithInvalidCapability() (gas: 55174) +CapabilitiesRegistry_AddNodesTest:test_RevertWhen_AddingNodeWithInvalidNodeOperator() (gas: 24895) +CapabilitiesRegistry_AddNodesTest:test_RevertWhen_AddingNodeWithoutCapabilities() (gas: 27669) +CapabilitiesRegistry_AddNodesTest:test_RevertWhen_CalledByNonNodeOperatorAdminAndNonOwner() (gas: 25108) +CapabilitiesRegistry_AddNodesTest:test_RevertWhen_P2PIDEmpty() (gas: 27408) +CapabilitiesRegistry_AddNodesTest:test_RevertWhen_SignerAddressEmpty() (gas: 27047) +CapabilitiesRegistry_AddNodesTest:test_RevertWhen_SignerAddressNotUnique() (gas: 309679) +CapabilitiesRegistry_DeprecateCapabilitiesTest:test_DeprecatesCapability() (gas: 89807) +CapabilitiesRegistry_DeprecateCapabilitiesTest:test_EmitsEvent() (gas: 89935) +CapabilitiesRegistry_DeprecateCapabilitiesTest:test_RevertWhen_CalledByNonAdmin() (gas: 22944) +CapabilitiesRegistry_DeprecateCapabilitiesTest:test_RevertWhen_CapabilityDoesNotExist() (gas: 16231) +CapabilitiesRegistry_DeprecateCapabilitiesTest:test_RevertWhen_CapabilityIsDeprecated() (gas: 91264) +CapabilitiesRegistry_GetCapabilitiesTest:test_ReturnsCapabilities() (gas: 135553) +CapabilitiesRegistry_GetDONsTest:test_CorrectlyFetchesDONs() (gas: 65468) +CapabilitiesRegistry_GetDONsTest:test_DoesNotIncludeRemovedDONs() (gas: 64924) +CapabilitiesRegistry_GetHashedCapabilityTest:test_CorrectlyGeneratesHashedCapabilityId() (gas: 11428) +CapabilitiesRegistry_GetHashedCapabilityTest:test_DoesNotCauseIncorrectClashes() (gas: 13087) +CapabilitiesRegistry_GetNodeOperatorsTest:test_CorrectlyFetchesNodeOperators() (gas: 36407) +CapabilitiesRegistry_GetNodeOperatorsTest:test_DoesNotIncludeRemovedNodeOperators() (gas: 38692) +CapabilitiesRegistry_GetNodesTest:test_CorrectlyFetchesNodes() (gas: 65288) +CapabilitiesRegistry_GetNodesTest:test_DoesNotIncludeRemovedNodes() (gas: 73533) +CapabilitiesRegistry_RemoveDONsTest:test_RemovesDON() (gas: 54761) +CapabilitiesRegistry_RemoveDONsTest:test_RevertWhen_CalledByNonAdmin() (gas: 15647) +CapabilitiesRegistry_RemoveDONsTest:test_RevertWhen_DONDoesNotExist() (gas: 16550) +CapabilitiesRegistry_RemoveNodeOperatorsTest:test_RemovesNodeOperator() (gas: 36122) +CapabilitiesRegistry_RemoveNodeOperatorsTest:test_RevertWhen_CalledByNonOwner() (gas: 15816) +CapabilitiesRegistry_RemoveNodesTest:test_CanAddNodeWithSameSignerAddressAfterRemoving() (gas: 115151) +CapabilitiesRegistry_RemoveNodesTest:test_CanRemoveWhenDONDeleted() (gas: 287716) +CapabilitiesRegistry_RemoveNodesTest:test_CanRemoveWhenNodeNoLongerPartOfDON() (gas: 561023) +CapabilitiesRegistry_RemoveNodesTest:test_OwnerCanRemoveNodes() (gas: 73376) +CapabilitiesRegistry_RemoveNodesTest:test_RemovesNode() (gas: 75211) +CapabilitiesRegistry_RemoveNodesTest:test_RevertWhen_CalledByNonNodeOperatorAdminAndNonOwner() (gas: 25053) +CapabilitiesRegistry_RemoveNodesTest:test_RevertWhen_NodeDoesNotExist() (gas: 18418) +CapabilitiesRegistry_RemoveNodesTest:test_RevertWhen_NodePartOfCapabilitiesDON() (gas: 385369) +CapabilitiesRegistry_RemoveNodesTest:test_RevertWhen_P2PIDEmpty() (gas: 18408) +CapabilitiesRegistry_TypeAndVersionTest:test_TypeAndVersion() (gas: 9796) +CapabilitiesRegistry_UpdateDONTest:test_RevertWhen_CalledByNonAdmin() (gas: 19415) +CapabilitiesRegistry_UpdateDONTest:test_RevertWhen_CapabilityDoesNotExist() (gas: 152914) +CapabilitiesRegistry_UpdateDONTest:test_RevertWhen_DONDoesNotExist() (gas: 17835) +CapabilitiesRegistry_UpdateDONTest:test_RevertWhen_DeprecatedCapabilityAdded() (gas: 222996) +CapabilitiesRegistry_UpdateDONTest:test_RevertWhen_DuplicateCapabilityAdded() (gas: 232804) +CapabilitiesRegistry_UpdateDONTest:test_RevertWhen_DuplicateNodeAdded() (gas: 107643) +CapabilitiesRegistry_UpdateDONTest:test_RevertWhen_NodeDoesNotSupportCapability() (gas: 163357) +CapabilitiesRegistry_UpdateDONTest:test_UpdatesDON() (gas: 371909) +CapabilitiesRegistry_UpdateNodeOperatorTest:test_RevertWhen_CalledByNonAdminAndNonOwner() (gas: 20728) +CapabilitiesRegistry_UpdateNodeOperatorTest:test_RevertWhen_NodeOperatorAdminIsZeroAddress() (gas: 20052) +CapabilitiesRegistry_UpdateNodeOperatorTest:test_RevertWhen_NodeOperatorDoesNotExist() (gas: 19790) +CapabilitiesRegistry_UpdateNodeOperatorTest:test_RevertWhen_NodeOperatorIdAndParamLengthsMismatch() (gas: 15430) +CapabilitiesRegistry_UpdateNodeOperatorTest:test_UpdatesNodeOperator() (gas: 37034) +CapabilitiesRegistry_UpdateNodesTest:test_CanUpdateParamsIfNodeSignerAddressNoLongerUsed() (gas: 256371) +CapabilitiesRegistry_UpdateNodesTest:test_OwnerCanUpdateNodes() (gas: 162166) +CapabilitiesRegistry_UpdateNodesTest:test_RevertWhen_AddingNodeWithInvalidCapability() (gas: 35873) +CapabilitiesRegistry_UpdateNodesTest:test_RevertWhen_CalledByAnotherNodeOperatorAdmin() (gas: 29200) +CapabilitiesRegistry_UpdateNodesTest:test_RevertWhen_CalledByNonNodeOperatorAdminAndNonOwner() (gas: 29377) +CapabilitiesRegistry_UpdateNodesTest:test_RevertWhen_NodeDoesNotExist() (gas: 29199) +CapabilitiesRegistry_UpdateNodesTest:test_RevertWhen_NodeSignerAlreadyAssignedToAnotherNode() (gas: 31326) +CapabilitiesRegistry_UpdateNodesTest:test_RevertWhen_P2PIDEmpty() (gas: 29165) +CapabilitiesRegistry_UpdateNodesTest:test_RevertWhen_RemovingCapabilityRequiredByCapabilityDON() (gas: 470910) +CapabilitiesRegistry_UpdateNodesTest:test_RevertWhen_RemovingCapabilityRequiredByWorkflowDON() (gas: 341191) +CapabilitiesRegistry_UpdateNodesTest:test_RevertWhen_SignerAddressEmpty() (gas: 29058) +CapabilitiesRegistry_UpdateNodesTest:test_RevertWhen_UpdatingNodeWithoutCapabilities() (gas: 27587) +CapabilitiesRegistry_UpdateNodesTest:test_UpdatesNodeParams() (gas: 162220) +KeystoneForwarder_ReportTest:test_Report_ConfigVersion() (gas: 2002057) +KeystoneForwarder_ReportTest:test_Report_FailedDeliveryWhenReceiverInterfaceNotSupported() (gas: 128934) +KeystoneForwarder_ReportTest:test_Report_FailedDeliveryWhenReceiverNotContract() (gas: 130621) +KeystoneForwarder_ReportTest:test_Report_SuccessfulDelivery() (gas: 359123) +KeystoneForwarder_ReportTest:test_Report_SuccessfulRetryWithMoreGas() (gas: 423982) +KeystoneForwarder_ReportTest:test_RevertWhen_AnySignatureIsInvalid() (gas: 86348) +KeystoneForwarder_ReportTest:test_RevertWhen_AnySignerIsInvalid() (gas: 118486) +KeystoneForwarder_ReportTest:test_RevertWhen_ReportHasDuplicateSignatures() (gas: 94516) +KeystoneForwarder_ReportTest:test_RevertWhen_ReportHasIncorrectDON() (gas: 75930) +KeystoneForwarder_ReportTest:test_RevertWhen_ReportHasInexistentConfigVersion() (gas: 76320) +KeystoneForwarder_ReportTest:test_RevertWhen_ReportIsMalformed() (gas: 45585) +KeystoneForwarder_ReportTest:test_RevertWhen_RetryingInvalidContractTransmission() (gas: 143354) +KeystoneForwarder_ReportTest:test_RevertWhen_RetryingSuccessfulTransmission() (gas: 353272) +KeystoneForwarder_ReportTest:test_RevertWhen_TooFewSignatures() (gas: 55292) +KeystoneForwarder_ReportTest:test_RevertWhen_TooManySignatures() (gas: 56050) +KeystoneForwarder_SetConfigTest:test_RevertWhen_ExcessSigners() (gas: 20184) +KeystoneForwarder_SetConfigTest:test_RevertWhen_FaultToleranceIsZero() (gas: 88057) +KeystoneForwarder_SetConfigTest:test_RevertWhen_InsufficientSigners() (gas: 14533) +KeystoneForwarder_SetConfigTest:test_RevertWhen_NotOwner() (gas: 88766) +KeystoneForwarder_SetConfigTest:test_RevertWhen_ProvidingDuplicateSigners() (gas: 114570) +KeystoneForwarder_SetConfigTest:test_RevertWhen_ProvidingZeroAddressSigner() (gas: 114225) +KeystoneForwarder_SetConfigTest:test_SetConfig_FirstTime() (gas: 1540541) +KeystoneForwarder_SetConfigTest:test_SetConfig_WhenSignersAreRemoved() (gas: 1535211) +KeystoneForwarder_TypeAndVersionTest:test_TypeAndVersion() (gas: 9641) +KeystoneRouter_SetConfigTest:test_AddForwarder_RevertWhen_NotOwner() (gas: 10978) +KeystoneRouter_SetConfigTest:test_RemoveForwarder_RevertWhen_NotOwner() (gas: 10923) +KeystoneRouter_SetConfigTest:test_RemoveForwarder_Success() (gas: 17599) +KeystoneRouter_SetConfigTest:test_Route_RevertWhen_UnauthorizedForwarder() (gas: 18552) +KeystoneRouter_SetConfigTest:test_Route_Success() (gas: 76407) \ No newline at end of file diff --git a/contracts/gas-snapshots/keystone.gas-snapshot b/contracts/gas-snapshots/keystone.gas-snapshot index 759e287b010..49b1d4add4b 100644 --- a/contracts/gas-snapshots/keystone.gas-snapshot +++ b/contracts/gas-snapshots/keystone.gas-snapshot @@ -81,17 +81,20 @@ CapabilitiesRegistry_UpdateNodesTest:test_RevertWhen_RemovingCapabilityRequiredB CapabilitiesRegistry_UpdateNodesTest:test_RevertWhen_SignerAddressEmpty() (gas: 29058) CapabilitiesRegistry_UpdateNodesTest:test_RevertWhen_UpdatingNodeWithoutCapabilities() (gas: 27587) CapabilitiesRegistry_UpdateNodesTest:test_UpdatesNodeParams() (gas: 162220) -KeystoneForwarder_ReportTest:test_Report_ConfigVersion() (gas: 1798375) -KeystoneForwarder_ReportTest:test_Report_FailedDeliveryWhenReceiverInterfaceNotSupported() (gas: 125910) -KeystoneForwarder_ReportTest:test_Report_FailedDeliveryWhenReceiverNotContract() (gas: 127403) -KeystoneForwarder_ReportTest:test_Report_SuccessfulDelivery() (gas: 155928) -KeystoneForwarder_ReportTest:test_RevertWhen_AlreadyAttempted() (gas: 152358) -KeystoneForwarder_ReportTest:test_RevertWhen_AnySignatureIsInvalid() (gas: 86348) -KeystoneForwarder_ReportTest:test_RevertWhen_AnySignerIsInvalid() (gas: 118486) +KeystoneForwarder_ReportTest:test_Report_ConfigVersion() (gas: 2003568) +KeystoneForwarder_ReportTest:test_Report_FailedDeliveryWhenReceiverInterfaceNotSupported() (gas: 124908) +KeystoneForwarder_ReportTest:test_Report_FailedDeliveryWhenReceiverNotContract() (gas: 126927) +KeystoneForwarder_ReportTest:test_Report_SuccessfulDelivery() (gas: 361243) +KeystoneForwarder_ReportTest:test_Report_SuccessfulRetryWithMoreGas() (gas: 501084) +KeystoneForwarder_ReportTest:test_RevertWhen_AnySignatureIsInvalid() (gas: 86326) +KeystoneForwarder_ReportTest:test_RevertWhen_AnySignerIsInvalid() (gas: 118521) +KeystoneForwarder_ReportTest:test_RevertWhen_AttemptingTransmissionWithInsufficientGas() (gas: 96279) KeystoneForwarder_ReportTest:test_RevertWhen_ReportHasDuplicateSignatures() (gas: 94516) KeystoneForwarder_ReportTest:test_RevertWhen_ReportHasIncorrectDON() (gas: 75930) KeystoneForwarder_ReportTest:test_RevertWhen_ReportHasInexistentConfigVersion() (gas: 76298) -KeystoneForwarder_ReportTest:test_RevertWhen_ReportIsMalformed() (gas: 45585) +KeystoneForwarder_ReportTest:test_RevertWhen_ReportIsMalformed() (gas: 45563) +KeystoneForwarder_ReportTest:test_RevertWhen_RetryingInvalidContractTransmission() (gas: 143591) +KeystoneForwarder_ReportTest:test_RevertWhen_RetryingSuccessfulTransmission() (gas: 354042) KeystoneForwarder_ReportTest:test_RevertWhen_TooFewSignatures() (gas: 55292) KeystoneForwarder_ReportTest:test_RevertWhen_TooManySignatures() (gas: 56050) KeystoneForwarder_SetConfigTest:test_RevertWhen_ExcessSigners() (gas: 20184) @@ -105,5 +108,6 @@ KeystoneForwarder_SetConfigTest:test_SetConfig_WhenSignersAreRemoved() (gas: 153 KeystoneForwarder_TypeAndVersionTest:test_TypeAndVersion() (gas: 9641) KeystoneRouter_SetConfigTest:test_AddForwarder_RevertWhen_NotOwner() (gas: 10978) KeystoneRouter_SetConfigTest:test_RemoveForwarder_RevertWhen_NotOwner() (gas: 10923) -KeystoneRouter_SetConfigTest:test_Route_RevertWhen_UnauthorizedForwarder() (gas: 18553) -KeystoneRouter_SetConfigTest:test_Route_Success() (gas: 75629) \ No newline at end of file +KeystoneRouter_SetConfigTest:test_RemoveForwarder_Success() (gas: 17599) +KeystoneRouter_SetConfigTest:test_Route_RevertWhen_UnauthorizedForwarder() (gas: 18552) +KeystoneRouter_SetConfigTest:test_Route_Success() (gas: 79379) \ No newline at end of file diff --git a/contracts/src/v0.8/keystone/KeystoneFeedsConsumer.sol b/contracts/src/v0.8/keystone/KeystoneFeedsConsumer.sol index 9dc6f67560e..ba1a7c6a8c3 100644 --- a/contracts/src/v0.8/keystone/KeystoneFeedsConsumer.sol +++ b/contracts/src/v0.8/keystone/KeystoneFeedsConsumer.sol @@ -1,10 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.24; -import {IReceiver} from "./interfaces/IReceiver.sol"; +import {IERC165} from "../vendor/openzeppelin-solidity/v4.8.3/contracts/interfaces/IERC165.sol"; import {OwnerIsCreator} from "../shared/access/OwnerIsCreator.sol"; +import {IReceiver} from "./interfaces/IReceiver.sol"; -contract KeystoneFeedsConsumer is IReceiver, OwnerIsCreator { +contract KeystoneFeedsConsumer is IReceiver, OwnerIsCreator, IERC165 { event FeedReceived(bytes32 indexed feedId, uint224 price, uint32 timestamp); error UnauthorizedSender(address sender); @@ -97,4 +98,8 @@ contract KeystoneFeedsConsumer is IReceiver, OwnerIsCreator { StoredFeedReport memory report = s_feedReports[feedId]; return (report.Price, report.Timestamp); } + + function supportsInterface(bytes4 interfaceId) public pure override returns (bool) { + return interfaceId == this.onReport.selector; + } } diff --git a/contracts/src/v0.8/keystone/KeystoneForwarder.sol b/contracts/src/v0.8/keystone/KeystoneForwarder.sol index b18e381cc6f..f92295cab97 100644 --- a/contracts/src/v0.8/keystone/KeystoneForwarder.sol +++ b/contracts/src/v0.8/keystone/KeystoneForwarder.sol @@ -1,12 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.24; -import {IReceiver} from "./interfaces/IReceiver.sol"; -import {IRouter} from "./interfaces/IRouter.sol"; -import {ITypeAndVersion} from "../shared/interfaces/ITypeAndVersion.sol"; +import {IERC165} from "../vendor/openzeppelin-solidity/v4.8.3/contracts/interfaces/IERC165.sol"; +import {ITypeAndVersion} from "../shared/interfaces/ITypeAndVersion.sol"; import {OwnerIsCreator} from "../shared/access/OwnerIsCreator.sol"; +import {IReceiver} from "./interfaces/IReceiver.sol"; +import {IRouter} from "./interfaces/IRouter.sol"; + /// @notice This is an entry point for `write_${chain}` Target capability. It /// allows nodes to determine if reports have been processed (successfully or /// not) in a decentralized and product-agnostic way by recording processed @@ -66,7 +68,7 @@ contract KeystoneForwarder is OwnerIsCreator, ITypeAndVersion, IRouter { /// @notice Contains the configuration for each DON ID // @param configId (uint64(donId) << 32) | configVersion - mapping(uint64 configId => OracleSet) internal s_configs; + mapping(uint64 configId => OracleSet oracleSet) internal s_configs; event ConfigSet(uint32 indexed donId, uint32 indexed configVersion, uint8 f, address[] signers); @@ -90,12 +92,16 @@ contract KeystoneForwarder is OwnerIsCreator, ITypeAndVersion, IRouter { uint256 internal constant FORWARDER_METADATA_LENGTH = 45; uint256 internal constant SIGNATURE_LENGTH = 65; + /// @dev The gas we require to revert in case of a revert in the call to the + /// receiver. This is more than enough and does not attempt to be exact. + uint256 internal constant REQUIRED_GAS_FOR_ROUTING = 40_000; + // ================================================================ // │ Router │ // ================================================================ - mapping(address forwarder => bool) internal s_forwarders; - mapping(bytes32 transmissionId => TransmissionInfo) internal s_transmissions; + mapping(address forwarder => bool isForwarder) internal s_forwarders; + mapping(bytes32 transmissionId => Transmission transmission) internal s_transmissions; function addForwarder(address forwarder) external onlyOwner { s_forwarders[forwarder] = true; @@ -114,19 +120,38 @@ contract KeystoneForwarder is OwnerIsCreator, ITypeAndVersion, IRouter { bytes calldata metadata, bytes calldata validatedReport ) public returns (bool) { - if (!s_forwarders[msg.sender]) { - revert UnauthorizedForwarder(); - } + if (!s_forwarders[msg.sender]) revert UnauthorizedForwarder(); + uint256 gasLeft = gasleft(); + if (gasLeft < REQUIRED_GAS_FOR_ROUTING) revert InsufficientGasForRouting(transmissionId); - if (s_transmissions[transmissionId].transmitter != address(0)) revert AlreadyAttempted(transmissionId); + Transmission memory transmission = s_transmissions[transmissionId]; + if (transmission.success || transmission.invalidReceiver) revert AlreadyAttempted(transmissionId); + + uint256 gasLimit = gasLeft - REQUIRED_GAS_FOR_ROUTING; s_transmissions[transmissionId].transmitter = transmitter; + s_transmissions[transmissionId].gasLimit = uint80(gasLimit); + + if (receiver.code.length == 0) { + s_transmissions[transmissionId].invalidReceiver = true; + return false; + } - if (receiver.code.length == 0) return false; + try IERC165(receiver).supportsInterface(type(IReceiver).interfaceId) { + bool success; + bytes memory payload = abi.encodeCall(IReceiver.onReport, (metadata, validatedReport)); - try IReceiver(receiver).onReport(metadata, validatedReport) { - s_transmissions[transmissionId].success = true; - return true; + assembly { + // call and return whether we succeeded. ignore return data + // call(gas,addr,value,argsOffset,argsLength,retOffset,retLength) + success := call(gasLimit, receiver, 0, add(payload, 0x20), mload(payload), 0x0, 0x0) + } + + if (success) { + s_transmissions[transmissionId].success = true; + } + return success; } catch { + s_transmissions[transmissionId].invalidReceiver = true; return false; } } @@ -141,26 +166,43 @@ contract KeystoneForwarder is OwnerIsCreator, ITypeAndVersion, IRouter { return keccak256(bytes.concat(bytes20(uint160(receiver)), workflowExecutionId, reportId)); } - /// @notice Get transmitter of a given report or 0x0 if it wasn't transmitted yet - function getTransmitter( + function getTransmissionInfo( address receiver, bytes32 workflowExecutionId, bytes2 reportId - ) external view returns (address) { - return s_transmissions[getTransmissionId(receiver, workflowExecutionId, reportId)].transmitter; + ) external view returns (TransmissionInfo memory) { + bytes32 transmissionId = getTransmissionId(receiver, workflowExecutionId, reportId); + + Transmission memory transmission = s_transmissions[transmissionId]; + + TransmissionState state; + + if (transmission.transmitter == address(0)) { + state = IRouter.TransmissionState.NOT_ATTEMPTED; + } else if (transmission.invalidReceiver) { + state = IRouter.TransmissionState.INVALID_RECEIVER; + } else { + state = transmission.success ? IRouter.TransmissionState.SUCCEEDED : IRouter.TransmissionState.FAILED; + } + + return + TransmissionInfo({ + gasLimit: transmission.gasLimit, + invalidReceiver: transmission.invalidReceiver, + state: state, + success: transmission.success, + transmissionId: transmissionId, + transmitter: transmission.transmitter + }); } - /// @notice Get delivery status of a given report - function getTransmissionState( + /// @notice Get transmitter of a given report or 0x0 if it wasn't transmitted yet + function getTransmitter( address receiver, bytes32 workflowExecutionId, bytes2 reportId - ) external view returns (IRouter.TransmissionState) { - bytes32 transmissionId = getTransmissionId(receiver, workflowExecutionId, reportId); - - if (s_transmissions[transmissionId].transmitter == address(0)) return IRouter.TransmissionState.NOT_ATTEMPTED; - return - s_transmissions[transmissionId].success ? IRouter.TransmissionState.SUCCEEDED : IRouter.TransmissionState.FAILED; + ) external view returns (address) { + return s_transmissions[getTransmissionId(receiver, workflowExecutionId, reportId)].transmitter; } function isForwarder(address forwarder) external view returns (bool) { diff --git a/contracts/src/v0.8/keystone/interfaces/IReceiver.sol b/contracts/src/v0.8/keystone/interfaces/IReceiver.sol index 3af340a1215..debe58feea4 100644 --- a/contracts/src/v0.8/keystone/interfaces/IReceiver.sol +++ b/contracts/src/v0.8/keystone/interfaces/IReceiver.sol @@ -3,5 +3,10 @@ pragma solidity 0.8.24; /// @title IReceiver - receives keystone reports interface IReceiver { + /// @notice Handles incoming keystone reports. + /// @dev If this function call reverts, it can be retried with a higher gas + /// limit. The receiver is responsible for discarding stale reports. + /// @param metadata Report's metadata. + /// @param report Workflow report. function onReport(bytes calldata metadata, bytes calldata report) external; } diff --git a/contracts/src/v0.8/keystone/interfaces/IRouter.sol b/contracts/src/v0.8/keystone/interfaces/IRouter.sol index 95d11b0bb3a..e40f3318679 100644 --- a/contracts/src/v0.8/keystone/interfaces/IRouter.sol +++ b/contracts/src/v0.8/keystone/interfaces/IRouter.sol @@ -4,6 +4,9 @@ pragma solidity 0.8.24; /// @title IRouter - delivers keystone reports to receiver interface IRouter { error UnauthorizedForwarder(); + /// @dev Thrown when the gas limit is insufficient for handling state after + /// calling the receiver function. + error InsufficientGasForRouting(bytes32 transmissionId); error AlreadyAttempted(bytes32 transmissionId); event ForwarderAdded(address indexed forwarder); @@ -12,12 +15,42 @@ interface IRouter { enum TransmissionState { NOT_ATTEMPTED, SUCCEEDED, + INVALID_RECEIVER, FAILED } + struct Transmission { + address transmitter; + // This is true if the receiver is not a contract or does not implement the + // `IReceiver` interface. + bool invalidReceiver; + // Whether the transmission attempt was successful. If `false`, the + // transmission can be retried with an increased gas limit. + bool success; + // The amount of gas allocated for the `IReceiver.onReport` call. uint80 + // allows storing gas for known EVM block gas limits. + // Ensures that the minimum gas requested by the user is available during + // the transmission attempt. If the transmission fails (indicated by a + // `false` success state), it can be retried with an increased gas limit. + uint80 gasLimit; + } + struct TransmissionInfo { + bytes32 transmissionId; + TransmissionState state; address transmitter; + // This is true if the receiver is not a contract or does not implement the + // `IReceiver` interface. + bool invalidReceiver; + // Whether the transmission attempt was successful. If `false`, the + // transmission can be retried with an increased gas limit. bool success; + // The amount of gas allocated for the `IReceiver.onReport` call. uint80 + // allows storing gas for known EVM block gas limits. + // Ensures that the minimum gas requested by the user is available during + // the transmission attempt. If the transmission fails (indicated by a + // `false` success state), it can be retried with an increased gas limit. + uint80 gasLimit; } function addForwarder(address forwarder) external; @@ -36,15 +69,14 @@ interface IRouter { bytes32 workflowExecutionId, bytes2 reportId ) external pure returns (bytes32); - function getTransmitter( + function getTransmissionInfo( address receiver, bytes32 workflowExecutionId, bytes2 reportId - ) external view returns (address); - function getTransmissionState( + ) external view returns (TransmissionInfo memory); + function getTransmitter( address receiver, bytes32 workflowExecutionId, bytes2 reportId - ) external view returns (TransmissionState); - function isForwarder(address forwarder) external view returns (bool); + ) external view returns (address); } diff --git a/contracts/src/v0.8/keystone/test/KeystoneForwarder_ReportTest.t.sol b/contracts/src/v0.8/keystone/test/KeystoneForwarder_ReportTest.t.sol index 56e421a8c94..5363d87e92b 100644 --- a/contracts/src/v0.8/keystone/test/KeystoneForwarder_ReportTest.t.sol +++ b/contracts/src/v0.8/keystone/test/KeystoneForwarder_ReportTest.t.sol @@ -141,15 +141,40 @@ contract KeystoneForwarder_ReportTest is BaseTest { s_forwarder.report(address(s_receiver), report, reportContext, signatures); } - function test_RevertWhen_AlreadyAttempted() public { - s_forwarder.report(address(s_receiver), report, reportContext, signatures); + function test_RevertWhen_RetryingSuccessfulTransmission() public { + s_forwarder.report{gas: 400_000}(address(s_receiver), report, reportContext, signatures); bytes32 transmissionId = s_forwarder.getTransmissionId(address(s_receiver), executionId, reportId); vm.expectRevert(abi.encodeWithSelector(IRouter.AlreadyAttempted.selector, transmissionId)); - s_forwarder.report(address(s_receiver), report, reportContext, signatures); + // Retyring with more gas + s_forwarder.report{gas: 450_000}(address(s_receiver), report, reportContext, signatures); + } + + function test_RevertWhen_RetryingInvalidContractTransmission() public { + // Receiver is not a contract + address receiver = address(404); + s_forwarder.report{gas: 400_000}(receiver, report, reportContext, signatures); + + bytes32 transmissionId = s_forwarder.getTransmissionId(receiver, executionId, reportId); + vm.expectRevert(abi.encodeWithSelector(IRouter.AlreadyAttempted.selector, transmissionId)); + // Retyring with more gas + s_forwarder.report{gas: 450_000}(receiver, report, reportContext, signatures); + } + + function test_RevertWhen_AttemptingTransmissionWithInsufficientGas() public { + bytes32 transmissionId = s_forwarder.getTransmissionId(address(s_receiver), executionId, reportId); + vm.expectRevert(abi.encodeWithSelector(IRouter.InsufficientGasForRouting.selector, transmissionId)); + s_forwarder.report{gas: 50_000}(address(s_receiver), report, reportContext, signatures); } function test_Report_SuccessfulDelivery() public { + IRouter.TransmissionInfo memory transmissionInfo = s_forwarder.getTransmissionInfo( + address(s_receiver), + executionId, + reportId + ); + assertEq(uint8(transmissionInfo.state), uint8(IRouter.TransmissionState.NOT_ATTEMPTED), "state mismatch"); + vm.expectEmit(address(s_receiver)); emit MessageReceived(metadata, mercuryReports); @@ -158,16 +183,31 @@ contract KeystoneForwarder_ReportTest is BaseTest { s_forwarder.report(address(s_receiver), report, reportContext, signatures); - assertEq( - s_forwarder.getTransmitter(address(s_receiver), executionId, reportId), - TRANSMITTER, - "transmitter mismatch" - ); - assertEq( - uint8(s_forwarder.getTransmissionState(address(s_receiver), executionId, reportId)), - uint8(IRouter.TransmissionState.SUCCEEDED), - "TransmissionState mismatch" + transmissionInfo = s_forwarder.getTransmissionInfo(address(s_receiver), executionId, reportId); + + assertEq(transmissionInfo.transmitter, TRANSMITTER, "transmitter mismatch"); + assertEq(uint8(transmissionInfo.state), uint8(IRouter.TransmissionState.SUCCEEDED), "state mismatch"); + assertGt(transmissionInfo.gasLimit, 100_000, "gas limit mismatch"); + } + + function test_Report_SuccessfulRetryWithMoreGas() public { + s_forwarder.report{gas: 200_000}(address(s_receiver), report, reportContext, signatures); + + IRouter.TransmissionInfo memory transmissionInfo = s_forwarder.getTransmissionInfo( + address(s_receiver), + executionId, + reportId ); + // Expect to fail with the receiver running out of gas + assertEq(uint8(transmissionInfo.state), uint8(IRouter.TransmissionState.FAILED), "state mismatch"); + assertGt(transmissionInfo.gasLimit, 100_000, "gas limit mismatch"); + + // Should succeed with more gas + s_forwarder.report{gas: 300_000}(address(s_receiver), report, reportContext, signatures); + + transmissionInfo = s_forwarder.getTransmissionInfo(address(s_receiver), executionId, reportId); + assertEq(uint8(transmissionInfo.state), uint8(IRouter.TransmissionState.SUCCEEDED), "state mismatch"); + assertGt(transmissionInfo.gasLimit, 200_000, "gas limit mismatch"); } function test_Report_FailedDeliveryWhenReceiverNotContract() public { @@ -179,29 +219,21 @@ contract KeystoneForwarder_ReportTest is BaseTest { s_forwarder.report(receiver, report, reportContext, signatures); - assertEq(s_forwarder.getTransmitter(receiver, executionId, reportId), TRANSMITTER, "transmitter mismatch"); - assertEq( - uint8(s_forwarder.getTransmissionState(receiver, executionId, reportId)), - uint8(IRouter.TransmissionState.FAILED), - "TransmissionState mismatch" - ); + IRouter.TransmissionInfo memory transmissionInfo = s_forwarder.getTransmissionInfo(receiver, executionId, reportId); + assertEq(uint8(transmissionInfo.state), uint8(IRouter.TransmissionState.INVALID_RECEIVER), "state mismatch"); } function test_Report_FailedDeliveryWhenReceiverInterfaceNotSupported() public { // Receiver is a contract but doesn't implement the required interface address receiver = address(s_forwarder); - vm.expectEmit(address(s_forwarder)); + vm.expectEmit(true, true, true, false); emit ReportProcessed(receiver, executionId, reportId, false); s_forwarder.report(receiver, report, reportContext, signatures); - assertEq(s_forwarder.getTransmitter(receiver, executionId, reportId), TRANSMITTER, "transmitter mismatch"); - assertEq( - uint8(s_forwarder.getTransmissionState(receiver, executionId, reportId)), - uint8(IRouter.TransmissionState.FAILED), - "TransmissionState mismatch" - ); + IRouter.TransmissionInfo memory transmissionInfo = s_forwarder.getTransmissionInfo(receiver, executionId, reportId); + assertEq(uint8(transmissionInfo.state), uint8(IRouter.TransmissionState.INVALID_RECEIVER), "state mismatch"); } function test_Report_ConfigVersion() public { diff --git a/contracts/src/v0.8/keystone/test/KeystoneRouter_AccessTest.t.sol b/contracts/src/v0.8/keystone/test/KeystoneRouter_AccessTest.t.sol index c126f7ce31d..0e43b72bdc1 100644 --- a/contracts/src/v0.8/keystone/test/KeystoneRouter_AccessTest.t.sol +++ b/contracts/src/v0.8/keystone/test/KeystoneRouter_AccessTest.t.sol @@ -5,6 +5,7 @@ import {Test} from "forge-std/Test.sol"; import {IReceiver} from "../interfaces/IReceiver.sol"; import {IRouter} from "../interfaces/IRouter.sol"; import {KeystoneForwarder} from "../KeystoneForwarder.sol"; +import {Receiver} from "./mocks/Receiver.sol"; contract KeystoneRouter_SetConfigTest is Test { address internal ADMIN = address(1); @@ -18,10 +19,12 @@ contract KeystoneRouter_SetConfigTest is Test { bytes32 internal id = hex"6d795f657865637574696f6e5f69640000000000000000000000000000000000"; KeystoneForwarder internal s_router; + Receiver internal s_receiver; function setUp() public virtual { vm.prank(ADMIN); s_router = new KeystoneForwarder(); + s_receiver = new Receiver(); } function test_AddForwarder_RevertWhen_NotOwner() public { @@ -36,6 +39,13 @@ contract KeystoneRouter_SetConfigTest is Test { s_router.removeForwarder(FORWARDER); } + function test_RemoveForwarder_Success() public { + vm.prank(ADMIN); + vm.expectEmit(true, false, false, false); + emit IRouter.ForwarderRemoved(FORWARDER); + s_router.removeForwarder(FORWARDER); + } + function test_Route_RevertWhen_UnauthorizedForwarder() public { vm.prank(STRANGER); vm.expectRevert(IRouter.UnauthorizedForwarder.selector); @@ -50,8 +60,8 @@ contract KeystoneRouter_SetConfigTest is Test { assertEq(s_router.isForwarder(FORWARDER), true); vm.prank(FORWARDER); - vm.mockCall(RECEIVER, abi.encodeCall(IReceiver.onReport, (metadata, report)), abi.encode()); - vm.expectCall(RECEIVER, abi.encodeCall(IReceiver.onReport, (metadata, report))); - s_router.route(id, TRANSMITTER, RECEIVER, metadata, report); + vm.mockCall(address(s_receiver), abi.encodeCall(IReceiver.onReport, (metadata, report)), abi.encode()); + vm.expectCall(address(s_receiver), abi.encodeCall(IReceiver.onReport, (metadata, report))); + s_router.route(id, TRANSMITTER, address(s_receiver), metadata, report); } } diff --git a/contracts/src/v0.8/keystone/test/mocks/Receiver.sol b/contracts/src/v0.8/keystone/test/mocks/Receiver.sol index 4d6bd2d3acf..3c1f157bc4d 100644 --- a/contracts/src/v0.8/keystone/test/mocks/Receiver.sol +++ b/contracts/src/v0.8/keystone/test/mocks/Receiver.sol @@ -1,16 +1,24 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.24; +import {IERC165} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/interfaces/IERC165.sol"; import {IReceiver} from "../../interfaces/IReceiver.sol"; -contract Receiver is IReceiver { +contract Receiver is IReceiver, IERC165 { event MessageReceived(bytes metadata, bytes[] mercuryReports); + bytes public latestReport; constructor() {} function onReport(bytes calldata metadata, bytes calldata rawReport) external { + latestReport = rawReport; + // parse actual report bytes[] memory mercuryReports = abi.decode(rawReport, (bytes[])); emit MessageReceived(metadata, mercuryReports); } + + function supportsInterface(bytes4 interfaceId) public pure override returns (bool) { + return interfaceId == this.onReport.selector; + } } diff --git a/core/capabilities/targets/write_target.go b/core/capabilities/targets/write_target.go index 330f15872d6..cc3a58b482e 100644 --- a/core/capabilities/targets/write_target.go +++ b/core/capabilities/targets/write_target.go @@ -28,6 +28,8 @@ type WriteTarget struct { cr commontypes.ContractReader cw commontypes.ChainWriter forwarderAddress string + // The minimum amount of gas that the receiver contract must get to process the forwarder report + receiverGasMinimum uint64 capabilities.CapabilityInfo lggr logger.Logger @@ -35,7 +37,21 @@ type WriteTarget struct { bound bool } -func NewWriteTarget(lggr logger.Logger, id string, cr commontypes.ContractReader, cw commontypes.ChainWriter, forwarderAddress string) *WriteTarget { +type TransmissionInfo struct { + GasLimit *big.Int + InvalidReceiver bool + State uint8 + Success bool + TransmissionId [32]byte + Transmitter common.Address +} + +// The gas cost of the forwarder contract logic, including state updates and event emission. +// This is a rough estimate and should be updated if the forwarder contract logic changes. +// TODO: Make this part of the on-chain capability configuration +const FORWARDER_CONTRACT_LOGIC_GAS_COST = 100_000 + +func NewWriteTarget(lggr logger.Logger, id string, cr commontypes.ContractReader, cw commontypes.ChainWriter, forwarderAddress string, txGasLimit uint64) *WriteTarget { info := capabilities.MustNewCapabilityInfo( id, capabilities.CapabilityTypeTarget, @@ -48,6 +64,7 @@ func NewWriteTarget(lggr logger.Logger, id string, cr commontypes.ContractReader cr, cw, forwarderAddress, + txGasLimit - FORWARDER_CONTRACT_LOGIC_GAS_COST, info, logger, false, @@ -131,16 +148,31 @@ func (cap *WriteTarget) Execute(ctx context.Context, request capabilities.Capabi WorkflowExecutionID: rawExecutionID, ReportId: inputs.ID, } - var transmitter common.Address - if err = cap.cr.GetLatestValue(ctx, "forwarder", "getTransmitter", primitives.Unconfirmed, queryInputs, &transmitter); err != nil { - return nil, fmt.Errorf("failed to getTransmitter latest value: %w", err) + var transmissionInfo TransmissionInfo + if err = cap.cr.GetLatestValue(ctx, "forwarder", "getTransmissionInfo", primitives.Unconfirmed, queryInputs, &transmissionInfo); err != nil { + return nil, fmt.Errorf("failed to getTransmissionInfo latest value: %w", err) } - if transmitter != common.HexToAddress("0x0") { - cap.lggr.Infow("WriteTarget report already onchain - returning without a tranmission attempt", "executionID", request.Metadata.WorkflowExecutionID) + + switch { + case transmissionInfo.State == 0: // NOT_ATTEMPTED + cap.lggr.Infow("non-empty report - tranasmission not attempted - attempting to push to txmgr", "request", request, "reportLen", len(inputs.Report), "reportContextLen", len(inputs.Context), "nSignatures", len(inputs.Signatures), "executionID", request.Metadata.WorkflowExecutionID) + case transmissionInfo.State == 1: // SUCCEEDED + cap.lggr.Infow("returning without a tranmission attempt - report already onchain ", "executionID", request.Metadata.WorkflowExecutionID) + return success(), nil + case transmissionInfo.State == 2: // INVALID_RECEIVER + cap.lggr.Infow("returning without a tranmission attempt - transmission already attempted, receiver was marked as invalid", "executionID", request.Metadata.WorkflowExecutionID) return success(), nil + case transmissionInfo.State == 3: // FAILED + if transmissionInfo.GasLimit.Uint64() > cap.receiverGasMinimum { + cap.lggr.Infow("returning without a tranmission attempt - transmission already attempted and failed, sufficient gas was provided", "executionID", request.Metadata.WorkflowExecutionID, "receiverGasMinimum", cap.receiverGasMinimum, "transmissionGasLimit", transmissionInfo.GasLimit) + return success(), nil + } else { + cap.lggr.Infow("non-empty report - retrying a failed transmission - attempting to push to txmgr", "request", request, "reportLen", len(inputs.Report), "reportContextLen", len(inputs.Context), "nSignatures", len(inputs.Signatures), "executionID", request.Metadata.WorkflowExecutionID, "receiverGasMinimum", cap.receiverGasMinimum, "transmissionGasLimit", transmissionInfo.GasLimit) + } + default: + return nil, fmt.Errorf("unexpected transmission state: %v", transmissionInfo.State) } - cap.lggr.Infow("WriteTarget non-empty report - attempting to push to txmgr", "request", request, "reportLen", len(inputs.Report), "reportContextLen", len(inputs.Context), "nSignatures", len(inputs.Signatures), "executionID", request.Metadata.WorkflowExecutionID) txID, err := uuid.NewUUID() // NOTE: CW expects us to generate an ID, rather than return one if err != nil { return nil, err diff --git a/core/capabilities/targets/write_target_test.go b/core/capabilities/targets/write_target_test.go index e1184331778..13305fe7ef9 100644 --- a/core/capabilities/targets/write_target_test.go +++ b/core/capabilities/targets/write_target_test.go @@ -3,6 +3,7 @@ package targets_test import ( "context" "errors" + "math/big" "testing" "github.com/ethereum/go-ethereum/common" @@ -29,7 +30,7 @@ func TestWriteTarget(t *testing.T) { forwarderA := testutils.NewAddress() forwarderAddr := forwarderA.Hex() - writeTarget := targets.NewWriteTarget(lggr, "test-write-target@1.0.0", cr, cw, forwarderAddr) + writeTarget := targets.NewWriteTarget(lggr, "test-write-target@1.0.0", cr, cw, forwarderAddr, 400_000) require.NotNil(t, writeTarget) config, err := values.NewMap(map[string]any{ @@ -52,9 +53,16 @@ func TestWriteTarget(t *testing.T) { }, }).Return(nil) - cr.On("GetLatestValue", mock.Anything, "forwarder", "getTransmitter", mock.Anything, mock.Anything, mock.Anything).Return(nil).Run(func(args mock.Arguments) { - transmitter := args.Get(5).(*common.Address) - *transmitter = common.HexToAddress("0x0") + cr.On("GetLatestValue", mock.Anything, "forwarder", "getTransmissionInfo", mock.Anything, mock.Anything, mock.Anything).Return(nil).Run(func(args mock.Arguments) { + transmissionInfo := args.Get(5).(*targets.TransmissionInfo) + *transmissionInfo = targets.TransmissionInfo{ + GasLimit: big.NewInt(0), + InvalidReceiver: false, + State: 0, + Success: false, + TransmissionId: [32]byte{}, + Transmitter: common.HexToAddress("0x0"), + } }).Once() cw.On("SubmitTransaction", mock.Anything, "forwarder", "report", mock.Anything, mock.Anything, forwarderAddr, mock.Anything, mock.Anything).Return(nil).Once() @@ -105,7 +113,7 @@ func TestWriteTarget(t *testing.T) { Config: config, Inputs: validInputs, } - cr.On("GetLatestValue", mock.Anything, "forwarder", "getTransmitter", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("reader error")) + cr.On("GetLatestValue", mock.Anything, "forwarder", "getTransmissionInfo", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("reader error")) _, err = writeTarget.Execute(ctx, req) require.Error(t, err) diff --git a/core/gethwrappers/keystone/generated/feeds_consumer/feeds_consumer.go b/core/gethwrappers/keystone/generated/feeds_consumer/feeds_consumer.go index f4d52eedb9d..2951835c8d6 100644 --- a/core/gethwrappers/keystone/generated/feeds_consumer/feeds_consumer.go +++ b/core/gethwrappers/keystone/generated/feeds_consumer/feeds_consumer.go @@ -31,8 +31,8 @@ var ( ) var KeystoneFeedsConsumerMetaData = &bind.MetaData{ - ABI: "[{\"inputs\":[{\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"}],\"name\":\"UnauthorizedSender\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes10\",\"name\":\"workflowName\",\"type\":\"bytes10\"}],\"name\":\"UnauthorizedWorkflowName\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"workflowOwner\",\"type\":\"address\"}],\"name\":\"UnauthorizedWorkflowOwner\",\"type\":\"error\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"feedId\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"uint224\",\"name\":\"price\",\"type\":\"uint224\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"timestamp\",\"type\":\"uint32\"}],\"name\":\"FeedReceived\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"OwnershipTransferRequested\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"OwnershipTransferred\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"acceptOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"feedId\",\"type\":\"bytes32\"}],\"name\":\"getPrice\",\"outputs\":[{\"internalType\":\"uint224\",\"name\":\"\",\"type\":\"uint224\"},{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"metadata\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"rawReport\",\"type\":\"bytes\"}],\"name\":\"onReport\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address[]\",\"name\":\"_allowedSendersList\",\"type\":\"address[]\"},{\"internalType\":\"address[]\",\"name\":\"_allowedWorkflowOwnersList\",\"type\":\"address[]\"},{\"internalType\":\"bytes10[]\",\"name\":\"_allowedWorkflowNamesList\",\"type\":\"bytes10[]\"}],\"name\":\"setConfig\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"transferOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", - Bin: "0x608060405234801561001057600080fd5b5033806000816100675760405162461bcd60e51b815260206004820152601860248201527f43616e6e6f7420736574206f776e657220746f207a65726f000000000000000060448201526064015b60405180910390fd5b600080546001600160a01b0319166001600160a01b0384811691909117909155811615610097576100978161009f565b505050610148565b336001600160a01b038216036100f75760405162461bcd60e51b815260206004820152601760248201527f43616e6e6f74207472616e7366657220746f2073656c66000000000000000000604482015260640161005e565b600180546001600160a01b0319166001600160a01b0383811691821790925560008054604051929316917fed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae12789190a350565b6111cf806101576000396000f3fe608060405234801561001057600080fd5b50600436106100725760003560e01c80638da5cb5b116100505780638da5cb5b1461014e578063e340171114610176578063f2fde38b1461018957600080fd5b806331d98b3f1461007757806379ba509714610131578063805f21321461013b575b600080fd5b6100f3610085366004610d64565b6000908152600260209081526040918290208251808401909352547bffffffffffffffffffffffffffffffffffffffffffffffffffffffff81168084527c010000000000000000000000000000000000000000000000000000000090910463ffffffff169290910182905291565b604080517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff909316835263ffffffff9091166020830152015b60405180910390f35b61013961019c565b005b610139610149366004610dc6565b61029e565b60005460405173ffffffffffffffffffffffffffffffffffffffff9091168152602001610128565b610139610184366004610e77565b61061d565b610139610197366004610f11565b610a5d565b60015473ffffffffffffffffffffffffffffffffffffffff163314610222576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601660248201527f4d7573742062652070726f706f736564206f776e65720000000000000000000060448201526064015b60405180910390fd5b60008054337fffffffffffffffffffffffff00000000000000000000000000000000000000008083168217845560018054909116905560405173ffffffffffffffffffffffffffffffffffffffff90921692909183917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e091a350565b3360009081526004602052604090205460ff166102e9576040517f3fcc3f17000000000000000000000000000000000000000000000000000000008152336004820152602401610219565b60008061032b86868080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250610a7192505050565b7fffffffffffffffffffff000000000000000000000000000000000000000000008216600090815260086020526040902054919350915060ff166103bf576040517f4b942f800000000000000000000000000000000000000000000000000000000081527fffffffffffffffffffff0000000000000000000000000000000000000000000083166004820152602401610219565b73ffffffffffffffffffffffffffffffffffffffff811660009081526006602052604090205460ff16610436576040517fbf24162300000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff82166004820152602401610219565b600061044484860186610ff5565b905060005b815181101561061357604051806040016040528083838151811061046f5761046f611107565b6020026020010151602001517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1681526020018383815181106104b0576104b0611107565b60200260200101516040015163ffffffff16815250600260008484815181106104db576104db611107565b602090810291909101810151518252818101929092526040016000208251929091015163ffffffff167c0100000000000000000000000000000000000000000000000000000000027bffffffffffffffffffffffffffffffffffffffffffffffffffffffff909216919091179055815182908290811061055d5761055d611107565b6020026020010151600001517f2c30f5cb3caf4239d0f994ce539d7ef24817fa550169c388e3a110f02e40197d83838151811061059c5761059c611107565b6020026020010151602001518484815181106105ba576105ba611107565b6020026020010151604001516040516106039291907bffffffffffffffffffffffffffffffffffffffffffffffffffffffff92909216825263ffffffff16602082015260400190565b60405180910390a2600101610449565b5050505050505050565b610625610a87565b60005b60035463ffffffff821610156106c65760006004600060038463ffffffff168154811061065757610657611107565b60009182526020808320919091015473ffffffffffffffffffffffffffffffffffffffff168352820192909252604001902080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00169115159190911790556106bf81611136565b9050610628565b5060005b63ffffffff811686111561076e5760016004600089898563ffffffff168181106106f6576106f6611107565b905060200201602081019061070b9190610f11565b73ffffffffffffffffffffffffffffffffffffffff168152602081019190915260400160002080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001691151591909117905561076781611136565b90506106ca565b5061077b60038787610bff565b5060005b60055463ffffffff8216101561081d5760006006600060058463ffffffff16815481106107ae576107ae611107565b60009182526020808320919091015473ffffffffffffffffffffffffffffffffffffffff168352820192909252604001902080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001691151591909117905561081681611136565b905061077f565b5060005b63ffffffff81168411156108c55760016006600087878563ffffffff1681811061084d5761084d611107565b90506020020160208101906108629190610f11565b73ffffffffffffffffffffffffffffffffffffffff168152602081019190915260400160002080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00169115159190911790556108be81611136565b9050610821565b506108d260058585610bff565b5060005b60075463ffffffff821610156109935760006008600060078463ffffffff168154811061090557610905611107565b600091825260208083206003808404909101549206600a026101000a90910460b01b7fffffffffffffffffffff00000000000000000000000000000000000000000000168352820192909252604001902080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001691151591909117905561098c81611136565b90506108d6565b5060005b63ffffffff8116821115610a475760016008600085858563ffffffff168181106109c3576109c3611107565b90506020020160208101906109d89190611180565b7fffffffffffffffffffff00000000000000000000000000000000000000000000168152602081019190915260400160002080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0016911515919091179055610a4081611136565b9050610997565b50610a5460078383610c87565b50505050505050565b610a65610a87565b610a6e81610b0a565b50565b6040810151604a90910151909160609190911c90565b60005473ffffffffffffffffffffffffffffffffffffffff163314610b08576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601660248201527f4f6e6c792063616c6c61626c65206279206f776e6572000000000000000000006044820152606401610219565b565b3373ffffffffffffffffffffffffffffffffffffffff821603610b89576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f43616e6e6f74207472616e7366657220746f2073656c660000000000000000006044820152606401610219565b600180547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83811691821790925560008054604051929316917fed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae12789190a350565b828054828255906000526020600020908101928215610c77579160200282015b82811115610c775781547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff843516178255602090920191600190910190610c1f565b50610c83929150610d4f565b5090565b82805482825590600052602060002090600201600390048101928215610c775791602002820160005b83821115610d1057833575ffffffffffffffffffffffffffffffffffffffffffff191683826101000a81548169ffffffffffffffffffff021916908360b01c02179055509260200192600a01602081600901049283019260010302610cb0565b8015610d465782816101000a81549069ffffffffffffffffffff0219169055600a01602081600901049283019260010302610d10565b5050610c839291505b5b80821115610c835760008155600101610d50565b600060208284031215610d7657600080fd5b5035919050565b60008083601f840112610d8f57600080fd5b50813567ffffffffffffffff811115610da757600080fd5b602083019150836020828501011115610dbf57600080fd5b9250929050565b60008060008060408587031215610ddc57600080fd5b843567ffffffffffffffff80821115610df457600080fd5b610e0088838901610d7d565b90965094506020870135915080821115610e1957600080fd5b50610e2687828801610d7d565b95989497509550505050565b60008083601f840112610e4457600080fd5b50813567ffffffffffffffff811115610e5c57600080fd5b6020830191508360208260051b8501011115610dbf57600080fd5b60008060008060008060608789031215610e9057600080fd5b863567ffffffffffffffff80821115610ea857600080fd5b610eb48a838b01610e32565b90985096506020890135915080821115610ecd57600080fd5b610ed98a838b01610e32565b90965094506040890135915080821115610ef257600080fd5b50610eff89828a01610e32565b979a9699509497509295939492505050565b600060208284031215610f2357600080fd5b813573ffffffffffffffffffffffffffffffffffffffff81168114610f4757600080fd5b9392505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6040516060810167ffffffffffffffff81118282101715610fa057610fa0610f4e565b60405290565b604051601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016810167ffffffffffffffff81118282101715610fed57610fed610f4e565b604052919050565b6000602080838503121561100857600080fd5b823567ffffffffffffffff8082111561102057600080fd5b818501915085601f83011261103457600080fd5b81358181111561104657611046610f4e565b611054848260051b01610fa6565b8181528481019250606091820284018501918883111561107357600080fd5b938501935b828510156110fb5780858a0312156110905760008081fd5b611098610f7d565b85358152868601357bffffffffffffffffffffffffffffffffffffffffffffffffffffffff811681146110cb5760008081fd5b8188015260408681013563ffffffff811681146110e85760008081fd5b9082015284529384019392850192611078565b50979650505050505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b600063ffffffff808316818103611176577f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6001019392505050565b60006020828403121561119257600080fd5b81357fffffffffffffffffffff0000000000000000000000000000000000000000000081168114610f4757600080fdfea164736f6c6343000818000a", + ABI: "[{\"inputs\":[{\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"}],\"name\":\"UnauthorizedSender\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes10\",\"name\":\"workflowName\",\"type\":\"bytes10\"}],\"name\":\"UnauthorizedWorkflowName\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"workflowOwner\",\"type\":\"address\"}],\"name\":\"UnauthorizedWorkflowOwner\",\"type\":\"error\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"feedId\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"uint224\",\"name\":\"price\",\"type\":\"uint224\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"timestamp\",\"type\":\"uint32\"}],\"name\":\"FeedReceived\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"OwnershipTransferRequested\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"OwnershipTransferred\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"acceptOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"feedId\",\"type\":\"bytes32\"}],\"name\":\"getPrice\",\"outputs\":[{\"internalType\":\"uint224\",\"name\":\"\",\"type\":\"uint224\"},{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"metadata\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"rawReport\",\"type\":\"bytes\"}],\"name\":\"onReport\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address[]\",\"name\":\"_allowedSendersList\",\"type\":\"address[]\"},{\"internalType\":\"address[]\",\"name\":\"_allowedWorkflowOwnersList\",\"type\":\"address[]\"},{\"internalType\":\"bytes10[]\",\"name\":\"_allowedWorkflowNamesList\",\"type\":\"bytes10[]\"}],\"name\":\"setConfig\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes4\",\"name\":\"interfaceId\",\"type\":\"bytes4\"}],\"name\":\"supportsInterface\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"transferOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", + Bin: "0x608060405234801561001057600080fd5b5033806000816100675760405162461bcd60e51b815260206004820152601860248201527f43616e6e6f7420736574206f776e657220746f207a65726f000000000000000060448201526064015b60405180910390fd5b600080546001600160a01b0319166001600160a01b0384811691909117909155811615610097576100978161009f565b505050610148565b336001600160a01b038216036100f75760405162461bcd60e51b815260206004820152601760248201527f43616e6e6f74207472616e7366657220746f2073656c66000000000000000000604482015260640161005e565b600180546001600160a01b0319166001600160a01b0383811691821790925560008054604051929316917fed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae12789190a350565b611281806101576000396000f3fe608060405234801561001057600080fd5b506004361061007d5760003560e01c8063805f21321161005b578063805f2132146101ab5780638da5cb5b146101be578063e3401711146101e6578063f2fde38b146101f957600080fd5b806301ffc9a71461008257806331d98b3f146100ec57806379ba5097146101a1575b600080fd5b6100d7610090366004610dd4565b7fffffffff00000000000000000000000000000000000000000000000000000000167f805f2132000000000000000000000000000000000000000000000000000000001490565b60405190151581526020015b60405180910390f35b6101686100fa366004610e1d565b6000908152600260209081526040918290208251808401909352547bffffffffffffffffffffffffffffffffffffffffffffffffffffffff81168084527c010000000000000000000000000000000000000000000000000000000090910463ffffffff169290910182905291565b604080517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff909316835263ffffffff9091166020830152016100e3565b6101a961020c565b005b6101a96101b9366004610e7f565b61030e565b60005460405173ffffffffffffffffffffffffffffffffffffffff90911681526020016100e3565b6101a96101f4366004610f30565b61068d565b6101a9610207366004610fca565b610acd565b60015473ffffffffffffffffffffffffffffffffffffffff163314610292576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601660248201527f4d7573742062652070726f706f736564206f776e65720000000000000000000060448201526064015b60405180910390fd5b60008054337fffffffffffffffffffffffff00000000000000000000000000000000000000008083168217845560018054909116905560405173ffffffffffffffffffffffffffffffffffffffff90921692909183917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e091a350565b3360009081526004602052604090205460ff16610359576040517f3fcc3f17000000000000000000000000000000000000000000000000000000008152336004820152602401610289565b60008061039b86868080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250610ae192505050565b7fffffffffffffffffffff000000000000000000000000000000000000000000008216600090815260086020526040902054919350915060ff1661042f576040517f4b942f800000000000000000000000000000000000000000000000000000000081527fffffffffffffffffffff0000000000000000000000000000000000000000000083166004820152602401610289565b73ffffffffffffffffffffffffffffffffffffffff811660009081526006602052604090205460ff166104a6576040517fbf24162300000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff82166004820152602401610289565b60006104b4848601866110a7565b905060005b81518110156106835760405180604001604052808383815181106104df576104df6111b9565b6020026020010151602001517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff168152602001838381518110610520576105206111b9565b60200260200101516040015163ffffffff168152506002600084848151811061054b5761054b6111b9565b602090810291909101810151518252818101929092526040016000208251929091015163ffffffff167c0100000000000000000000000000000000000000000000000000000000027bffffffffffffffffffffffffffffffffffffffffffffffffffffffff90921691909117905581518290829081106105cd576105cd6111b9565b6020026020010151600001517f2c30f5cb3caf4239d0f994ce539d7ef24817fa550169c388e3a110f02e40197d83838151811061060c5761060c6111b9565b60200260200101516020015184848151811061062a5761062a6111b9565b6020026020010151604001516040516106739291907bffffffffffffffffffffffffffffffffffffffffffffffffffffffff92909216825263ffffffff16602082015260400190565b60405180910390a26001016104b9565b5050505050505050565b610695610af7565b60005b60035463ffffffff821610156107365760006004600060038463ffffffff16815481106106c7576106c76111b9565b60009182526020808320919091015473ffffffffffffffffffffffffffffffffffffffff168352820192909252604001902080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001691151591909117905561072f816111e8565b9050610698565b5060005b63ffffffff81168611156107de5760016004600089898563ffffffff16818110610766576107666111b9565b905060200201602081019061077b9190610fca565b73ffffffffffffffffffffffffffffffffffffffff168152602081019190915260400160002080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00169115159190911790556107d7816111e8565b905061073a565b506107eb60038787610c6f565b5060005b60055463ffffffff8216101561088d5760006006600060058463ffffffff168154811061081e5761081e6111b9565b60009182526020808320919091015473ffffffffffffffffffffffffffffffffffffffff168352820192909252604001902080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0016911515919091179055610886816111e8565b90506107ef565b5060005b63ffffffff81168411156109355760016006600087878563ffffffff168181106108bd576108bd6111b9565b90506020020160208101906108d29190610fca565b73ffffffffffffffffffffffffffffffffffffffff168152602081019190915260400160002080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001691151591909117905561092e816111e8565b9050610891565b5061094260058585610c6f565b5060005b60075463ffffffff82161015610a035760006008600060078463ffffffff1681548110610975576109756111b9565b600091825260208083206003808404909101549206600a026101000a90910460b01b7fffffffffffffffffffff00000000000000000000000000000000000000000000168352820192909252604001902080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00169115159190911790556109fc816111e8565b9050610946565b5060005b63ffffffff8116821115610ab75760016008600085858563ffffffff16818110610a3357610a336111b9565b9050602002016020810190610a489190611232565b7fffffffffffffffffffff00000000000000000000000000000000000000000000168152602081019190915260400160002080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0016911515919091179055610ab0816111e8565b9050610a07565b50610ac460078383610cf7565b50505050505050565b610ad5610af7565b610ade81610b7a565b50565b6040810151604a90910151909160609190911c90565b60005473ffffffffffffffffffffffffffffffffffffffff163314610b78576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601660248201527f4f6e6c792063616c6c61626c65206279206f776e6572000000000000000000006044820152606401610289565b565b3373ffffffffffffffffffffffffffffffffffffffff821603610bf9576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f43616e6e6f74207472616e7366657220746f2073656c660000000000000000006044820152606401610289565b600180547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83811691821790925560008054604051929316917fed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae12789190a350565b828054828255906000526020600020908101928215610ce7579160200282015b82811115610ce75781547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff843516178255602090920191600190910190610c8f565b50610cf3929150610dbf565b5090565b82805482825590600052602060002090600201600390048101928215610ce75791602002820160005b83821115610d8057833575ffffffffffffffffffffffffffffffffffffffffffff191683826101000a81548169ffffffffffffffffffff021916908360b01c02179055509260200192600a01602081600901049283019260010302610d20565b8015610db65782816101000a81549069ffffffffffffffffffff0219169055600a01602081600901049283019260010302610d80565b5050610cf39291505b5b80821115610cf35760008155600101610dc0565b600060208284031215610de657600080fd5b81357fffffffff0000000000000000000000000000000000000000000000000000000081168114610e1657600080fd5b9392505050565b600060208284031215610e2f57600080fd5b5035919050565b60008083601f840112610e4857600080fd5b50813567ffffffffffffffff811115610e6057600080fd5b602083019150836020828501011115610e7857600080fd5b9250929050565b60008060008060408587031215610e9557600080fd5b843567ffffffffffffffff80821115610ead57600080fd5b610eb988838901610e36565b90965094506020870135915080821115610ed257600080fd5b50610edf87828801610e36565b95989497509550505050565b60008083601f840112610efd57600080fd5b50813567ffffffffffffffff811115610f1557600080fd5b6020830191508360208260051b8501011115610e7857600080fd5b60008060008060008060608789031215610f4957600080fd5b863567ffffffffffffffff80821115610f6157600080fd5b610f6d8a838b01610eeb565b90985096506020890135915080821115610f8657600080fd5b610f928a838b01610eeb565b90965094506040890135915080821115610fab57600080fd5b50610fb889828a01610eeb565b979a9699509497509295939492505050565b600060208284031215610fdc57600080fd5b813573ffffffffffffffffffffffffffffffffffffffff81168114610e1657600080fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6040516060810167ffffffffffffffff8111828210171561105257611052611000565b60405290565b604051601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016810167ffffffffffffffff8111828210171561109f5761109f611000565b604052919050565b600060208083850312156110ba57600080fd5b823567ffffffffffffffff808211156110d257600080fd5b818501915085601f8301126110e657600080fd5b8135818111156110f8576110f8611000565b611106848260051b01611058565b8181528481019250606091820284018501918883111561112557600080fd5b938501935b828510156111ad5780858a0312156111425760008081fd5b61114a61102f565b85358152868601357bffffffffffffffffffffffffffffffffffffffffffffffffffffffff8116811461117d5760008081fd5b8188015260408681013563ffffffff8116811461119a5760008081fd5b908201528452938401939285019261112a565b50979650505050505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b600063ffffffff808316818103611228577f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6001019392505050565b60006020828403121561124457600080fd5b81357fffffffffffffffffffff0000000000000000000000000000000000000000000081168114610e1657600080fdfea164736f6c6343000818000a", } var KeystoneFeedsConsumerABI = KeystoneFeedsConsumerMetaData.ABI @@ -216,6 +216,28 @@ func (_KeystoneFeedsConsumer *KeystoneFeedsConsumerCallerSession) Owner() (commo return _KeystoneFeedsConsumer.Contract.Owner(&_KeystoneFeedsConsumer.CallOpts) } +func (_KeystoneFeedsConsumer *KeystoneFeedsConsumerCaller) SupportsInterface(opts *bind.CallOpts, interfaceId [4]byte) (bool, error) { + var out []interface{} + err := _KeystoneFeedsConsumer.contract.Call(opts, &out, "supportsInterface", interfaceId) + + if err != nil { + return *new(bool), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + + return out0, err + +} + +func (_KeystoneFeedsConsumer *KeystoneFeedsConsumerSession) SupportsInterface(interfaceId [4]byte) (bool, error) { + return _KeystoneFeedsConsumer.Contract.SupportsInterface(&_KeystoneFeedsConsumer.CallOpts, interfaceId) +} + +func (_KeystoneFeedsConsumer *KeystoneFeedsConsumerCallerSession) SupportsInterface(interfaceId [4]byte) (bool, error) { + return _KeystoneFeedsConsumer.Contract.SupportsInterface(&_KeystoneFeedsConsumer.CallOpts, interfaceId) +} + func (_KeystoneFeedsConsumer *KeystoneFeedsConsumerTransactor) AcceptOwnership(opts *bind.TransactOpts) (*types.Transaction, error) { return _KeystoneFeedsConsumer.contract.Transact(opts, "acceptOwnership") } @@ -700,6 +722,8 @@ type KeystoneFeedsConsumerInterface interface { Owner(opts *bind.CallOpts) (common.Address, error) + SupportsInterface(opts *bind.CallOpts, interfaceId [4]byte) (bool, error) + AcceptOwnership(opts *bind.TransactOpts) (*types.Transaction, error) OnReport(opts *bind.TransactOpts, metadata []byte, rawReport []byte) (*types.Transaction, error) diff --git a/core/gethwrappers/keystone/generated/forwarder/forwarder.go b/core/gethwrappers/keystone/generated/forwarder/forwarder.go index 3b6fba5c7c2..a7a78ab67f9 100644 --- a/core/gethwrappers/keystone/generated/forwarder/forwarder.go +++ b/core/gethwrappers/keystone/generated/forwarder/forwarder.go @@ -30,9 +30,18 @@ var ( _ = abi.ConvertType ) +type IRouterTransmissionInfo struct { + TransmissionId [32]byte + State uint8 + Transmitter common.Address + InvalidReceiver bool + Success bool + GasLimit *big.Int +} + var KeystoneForwarderMetaData = &bind.MetaData{ - ABI: "[{\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"transmissionId\",\"type\":\"bytes32\"}],\"name\":\"AlreadyAttempted\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"signer\",\"type\":\"address\"}],\"name\":\"DuplicateSigner\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"numSigners\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"maxSigners\",\"type\":\"uint256\"}],\"name\":\"ExcessSigners\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"FaultToleranceMustBePositive\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"numSigners\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"minSigners\",\"type\":\"uint256\"}],\"name\":\"InsufficientSigners\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint64\",\"name\":\"configId\",\"type\":\"uint64\"}],\"name\":\"InvalidConfig\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidReport\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"signature\",\"type\":\"bytes\"}],\"name\":\"InvalidSignature\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"expected\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"received\",\"type\":\"uint256\"}],\"name\":\"InvalidSignatureCount\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"signer\",\"type\":\"address\"}],\"name\":\"InvalidSigner\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"UnauthorizedForwarder\",\"type\":\"error\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint32\",\"name\":\"donId\",\"type\":\"uint32\"},{\"indexed\":true,\"internalType\":\"uint32\",\"name\":\"configVersion\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint8\",\"name\":\"f\",\"type\":\"uint8\"},{\"indexed\":false,\"internalType\":\"address[]\",\"name\":\"signers\",\"type\":\"address[]\"}],\"name\":\"ConfigSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"forwarder\",\"type\":\"address\"}],\"name\":\"ForwarderAdded\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"forwarder\",\"type\":\"address\"}],\"name\":\"ForwarderRemoved\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"OwnershipTransferRequested\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"OwnershipTransferred\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"receiver\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"workflowExecutionId\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"bytes2\",\"name\":\"reportId\",\"type\":\"bytes2\"},{\"indexed\":false,\"internalType\":\"bool\",\"name\":\"result\",\"type\":\"bool\"}],\"name\":\"ReportProcessed\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"acceptOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"forwarder\",\"type\":\"address\"}],\"name\":\"addForwarder\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint32\",\"name\":\"donId\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"configVersion\",\"type\":\"uint32\"}],\"name\":\"clearConfig\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"receiver\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"workflowExecutionId\",\"type\":\"bytes32\"},{\"internalType\":\"bytes2\",\"name\":\"reportId\",\"type\":\"bytes2\"}],\"name\":\"getTransmissionId\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"receiver\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"workflowExecutionId\",\"type\":\"bytes32\"},{\"internalType\":\"bytes2\",\"name\":\"reportId\",\"type\":\"bytes2\"}],\"name\":\"getTransmissionState\",\"outputs\":[{\"internalType\":\"enumIRouter.TransmissionState\",\"name\":\"\",\"type\":\"uint8\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"receiver\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"workflowExecutionId\",\"type\":\"bytes32\"},{\"internalType\":\"bytes2\",\"name\":\"reportId\",\"type\":\"bytes2\"}],\"name\":\"getTransmitter\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"forwarder\",\"type\":\"address\"}],\"name\":\"isForwarder\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"forwarder\",\"type\":\"address\"}],\"name\":\"removeForwarder\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"receiver\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"rawReport\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"reportContext\",\"type\":\"bytes\"},{\"internalType\":\"bytes[]\",\"name\":\"signatures\",\"type\":\"bytes[]\"}],\"name\":\"report\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"transmissionId\",\"type\":\"bytes32\"},{\"internalType\":\"address\",\"name\":\"transmitter\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"receiver\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"metadata\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"validatedReport\",\"type\":\"bytes\"}],\"name\":\"route\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint32\",\"name\":\"donId\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"configVersion\",\"type\":\"uint32\"},{\"internalType\":\"uint8\",\"name\":\"f\",\"type\":\"uint8\"},{\"internalType\":\"address[]\",\"name\":\"signers\",\"type\":\"address[]\"}],\"name\":\"setConfig\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"transferOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"typeAndVersion\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]", - Bin: "0x608060405234801561001057600080fd5b5033806000816100675760405162461bcd60e51b815260206004820152601860248201527f43616e6e6f7420736574206f776e657220746f207a65726f000000000000000060448201526064015b60405180910390fd5b600080546001600160a01b0319166001600160a01b038481169190911790915581161561009757610097816100b9565b5050306000908152600360205260409020805460ff1916600117905550610162565b336001600160a01b038216036101115760405162461bcd60e51b815260206004820152601760248201527f43616e6e6f74207472616e7366657220746f2073656c66000000000000000000604482015260640161005e565b600180546001600160a01b0319166001600160a01b0383811691821790925560008054604051929316917fed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae12789190a350565b611b9180620001726000396000f3fe608060405234801561001057600080fd5b50600436106100ea5760003560e01c806379ba50971161008c578063abcef55411610066578063abcef5541461023e578063ee59d26c14610277578063ef6e17a01461028a578063f2fde38b1461029d57600080fd5b806379ba5097146101e05780638864b864146101e85780638da5cb5b1461022057600080fd5b8063354bdd66116100c8578063354bdd661461017957806343c164671461019a5780634d93172d146101ba5780635c41d2fe146101cd57600080fd5b806311289565146100ef578063181f5a7714610104578063233fd52d14610156575b600080fd5b6101026100fd3660046114d8565b6102b0565b005b6101406040518060400160405280601a81526020017f466f7277617264657220616e6420526f7574657220312e302e3000000000000081525081565b60405161014d9190611583565b60405180910390f35b6101696101643660046115f0565b61080d565b604051901515815260200161014d565b61018c610187366004611678565b610a00565b60405190815260200161014d565b6101ad6101a8366004611678565b610a84565b60405161014d91906116dd565b6101026101c836600461171e565b610b09565b6101026101db36600461171e565b610b85565b610102610c04565b6101fb6101f6366004611678565b610d01565b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200161014d565b60005473ffffffffffffffffffffffffffffffffffffffff166101fb565b61016961024c36600461171e565b73ffffffffffffffffffffffffffffffffffffffff1660009081526003602052604090205460ff1690565b61010261028536600461174d565b610d41565b6101026102983660046117cb565b61111e565b6101026102ab36600461171e565b6111be565b606d8510156102eb576040517fb55ac75400000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600080600061032f89898080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152506111d292505050565b67ffffffffffffffff8216600090815260026020526040812080549497509195509193509160ff16908190036103a2576040517fdf3b81ea00000000000000000000000000000000000000000000000000000000815267ffffffffffffffff841660048201526024015b60405180910390fd5b856103ae82600161182d565b60ff1614610400576103c181600161182d565b6040517fd6022e8e00000000000000000000000000000000000000000000000000000000815260ff909116600482015260248101879052604401610399565b60008b8b60405161041292919061184c565b60405190819003812061042b918c908c9060200161185c565b60405160208183030381529060405280519060200120905061044b611365565b60005b888110156106cd573660008b8b8481811061046b5761046b611876565b905060200281019061047d91906118a5565b9092509050604181146104c05781816040517f2adfdc30000000000000000000000000000000000000000000000000000000008152600401610399929190611953565b6000600186848460408181106104d8576104d8611876565b6104ea92013560f81c9050601b61182d565b6104f860206000878961196f565b61050191611999565b61050f60406020888a61196f565b61051891611999565b6040805160008152602081018083529590955260ff909316928401929092526060830152608082015260a0016020604051602081039080840390855afa158015610566573d6000803e3d6000fd5b5050604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0015173ffffffffffffffffffffffffffffffffffffffff8116600090815260028c0160205291822054909350915081900361060c576040517fbf18af4300000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff83166004820152602401610399565b600086826020811061062057610620611876565b602002015173ffffffffffffffffffffffffffffffffffffffff161461068a576040517fe021c4f200000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff83166004820152602401610399565b8186826020811061069d5761069d611876565b73ffffffffffffffffffffffffffffffffffffffff909216602092909202015250506001909201915061044e9050565b50505050505060003073ffffffffffffffffffffffffffffffffffffffff1663233fd52d6106fc8c8686610a00565b338d8d8d602d90606d926107129392919061196f565b8f8f606d9080926107259392919061196f565b6040518863ffffffff1660e01b815260040161074797969594939291906119d5565b6020604051808303816000875af1158015610766573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061078a9190611a36565b9050817dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916838b73ffffffffffffffffffffffffffffffffffffffff167f3617b009e9785c42daebadb6d3fb553243a4bf586d07ea72d65d80013ce116b5846040516107f9911515815260200190565b60405180910390a450505050505050505050565b3360009081526003602052604081205460ff16610856576040517fd79e123d00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60008881526004602052604090205473ffffffffffffffffffffffffffffffffffffffff16156108b5576040517fa53dc8ca00000000000000000000000000000000000000000000000000000000815260048101899052602401610399565b600088815260046020526040812080547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff8a81169190911790915587163b9003610917575060006109f5565b6040517f805f213200000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff87169063805f21329061096f908890889088908890600401611a58565b600060405180830381600087803b15801561098957600080fd5b505af192505050801561099a575060015b6109a6575060006109f5565b50600087815260046020526040902080547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff167401000000000000000000000000000000000000000017905560015b979650505050505050565b6040517fffffffffffffffffffffffffffffffffffffffff000000000000000000000000606085901b166020820152603481018390527fffff000000000000000000000000000000000000000000000000000000000000821660548201526000906056016040516020818303038152906040528051906020012090505b9392505050565b600080610a92858585610a00565b60008181526004602052604090205490915073ffffffffffffffffffffffffffffffffffffffff16610ac8576000915050610a7d565b60008181526004602052604090205474010000000000000000000000000000000000000000900460ff16610afd576002610b00565b60015b95945050505050565b610b116111ed565b73ffffffffffffffffffffffffffffffffffffffff811660008181526003602052604080822080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00169055517fb96d15bf9258c7b8df062753a6a262864611fc7b060a5ee2e57e79b85f898d389190a250565b610b8d6111ed565b73ffffffffffffffffffffffffffffffffffffffff811660008181526003602052604080822080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055517f0ea0ce2c048ff45a4a95f2947879de3fb94abec2f152190400cab2d1272a68e79190a250565b60015473ffffffffffffffffffffffffffffffffffffffff163314610c85576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601660248201527f4d7573742062652070726f706f736564206f776e6572000000000000000000006044820152606401610399565b60008054337fffffffffffffffffffffffff00000000000000000000000000000000000000008083168217845560018054909116905560405173ffffffffffffffffffffffffffffffffffffffff90921692909183917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e091a350565b600060046000610d12868686610a00565b815260208101919091526040016000205473ffffffffffffffffffffffffffffffffffffffff16949350505050565b610d496111ed565b8260ff16600003610d86576040517f0743bae600000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b601f811115610dcb576040517f61750f4000000000000000000000000000000000000000000000000000000000815260048101829052601f6024820152604401610399565b610dd6836003611a7f565b60ff168111610e345780610deb846003611a7f565b610df690600161182d565b6040517f9dd9e6d8000000000000000000000000000000000000000000000000000000008152600481019290925260ff166024820152604401610399565b67ffffffff00000000602086901b1663ffffffff85161760005b67ffffffffffffffff8216600090815260026020526040902060010154811015610ee45767ffffffffffffffff8216600090815260026020819052604082206001810180549190920192919084908110610eaa57610eaa611876565b600091825260208083209091015473ffffffffffffffffffffffffffffffffffffffff168352820192909252604001812055600101610e4e565b5060005b82811015611060576000848483818110610f0457610f04611876565b9050602002016020810190610f19919061171e565b905073ffffffffffffffffffffffffffffffffffffffff8116610f80576040517fbf18af4300000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff82166004820152602401610399565b67ffffffffffffffff8316600090815260026020818152604080842073ffffffffffffffffffffffffffffffffffffffff8616855290920190529020541561100c576040517fe021c4f200000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff82166004820152602401610399565b611017826001611aa2565b67ffffffffffffffff8416600090815260026020818152604080842073ffffffffffffffffffffffffffffffffffffffff90961684529490910190529190912055600101610ee8565b5067ffffffffffffffff81166000908152600260205260409020611088906001018484611384565b5067ffffffffffffffff81166000908152600260205260409081902080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001660ff87161790555163ffffffff86811691908816907f4120bd3b23957dd423555817d55654d4481b438aa15485c21b4180c784f1a4559061110e90889088908890611ab5565b60405180910390a3505050505050565b6111266111ed565b63ffffffff818116602084811b67ffffffff00000000168217600090815260028252604080822080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001690558051828152928301905291928516917f4120bd3b23957dd423555817d55654d4481b438aa15485c21b4180c784f1a455916040516111b2929190611b1b565b60405180910390a35050565b6111c66111ed565b6111cf81611270565b50565b60218101516045820151608b90920151909260c09290921c91565b60005473ffffffffffffffffffffffffffffffffffffffff16331461126e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601660248201527f4f6e6c792063616c6c61626c65206279206f776e6572000000000000000000006044820152606401610399565b565b3373ffffffffffffffffffffffffffffffffffffffff8216036112ef576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f43616e6e6f74207472616e7366657220746f2073656c660000000000000000006044820152606401610399565b600180547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83811691821790925560008054604051929316917fed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae12789190a350565b6040518061040001604052806020906020820280368337509192915050565b8280548282559060005260206000209081019282156113fc579160200282015b828111156113fc5781547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff8435161782556020909201916001909101906113a4565b5061140892915061140c565b5090565b5b80821115611408576000815560010161140d565b803573ffffffffffffffffffffffffffffffffffffffff8116811461144557600080fd5b919050565b60008083601f84011261145c57600080fd5b50813567ffffffffffffffff81111561147457600080fd5b60208301915083602082850101111561148c57600080fd5b9250929050565b60008083601f8401126114a557600080fd5b50813567ffffffffffffffff8111156114bd57600080fd5b6020830191508360208260051b850101111561148c57600080fd5b60008060008060008060006080888a0312156114f357600080fd5b6114fc88611421565b9650602088013567ffffffffffffffff8082111561151957600080fd5b6115258b838c0161144a565b909850965060408a013591508082111561153e57600080fd5b61154a8b838c0161144a565b909650945060608a013591508082111561156357600080fd5b506115708a828b01611493565b989b979a50959850939692959293505050565b60006020808352835180602085015260005b818110156115b157858101830151858201604001528201611595565b5060006040828601015260407fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f8301168501019250505092915050565b600080600080600080600060a0888a03121561160b57600080fd5b8735965061161b60208901611421565b955061162960408901611421565b9450606088013567ffffffffffffffff8082111561164657600080fd5b6116528b838c0161144a565b909650945060808a013591508082111561166b57600080fd5b506115708a828b0161144a565b60008060006060848603121561168d57600080fd5b61169684611421565b92506020840135915060408401357fffff000000000000000000000000000000000000000000000000000000000000811681146116d257600080fd5b809150509250925092565b6020810160038310611718577f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b91905290565b60006020828403121561173057600080fd5b610a7d82611421565b803563ffffffff8116811461144557600080fd5b60008060008060006080868803121561176557600080fd5b61176e86611739565b945061177c60208701611739565b9350604086013560ff8116811461179257600080fd5b9250606086013567ffffffffffffffff8111156117ae57600080fd5b6117ba88828901611493565b969995985093965092949392505050565b600080604083850312156117de57600080fd5b6117e783611739565b91506117f560208401611739565b90509250929050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60ff8181168382160190811115611846576118466117fe565b92915050565b8183823760009101908152919050565b838152818360208301376000910160200190815292915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b60008083357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe18436030181126118da57600080fd5b83018035915067ffffffffffffffff8211156118f557600080fd5b60200191503681900382131561148c57600080fd5b8183528181602085013750600060208284010152600060207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f840116840101905092915050565b60208152600061196760208301848661190a565b949350505050565b6000808585111561197f57600080fd5b8386111561198c57600080fd5b5050820193919092039150565b80356020831015611846577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff602084900360031b1b1692915050565b878152600073ffffffffffffffffffffffffffffffffffffffff808916602084015280881660408401525060a06060830152611a1560a08301868861190a565b8281036080840152611a2881858761190a565b9a9950505050505050505050565b600060208284031215611a4857600080fd5b81518015158114610a7d57600080fd5b604081526000611a6c60408301868861190a565b82810360208401526109f581858761190a565b60ff8181168382160290811690818114611a9b57611a9b6117fe565b5092915050565b80820180821115611846576118466117fe565b60ff8416815260406020808301829052908201839052600090849060608401835b86811015611b0f5773ffffffffffffffffffffffffffffffffffffffff611afc85611421565b1682529282019290820190600101611ad6565b50979650505050505050565b60006040820160ff8516835260206040602085015281855180845260608601915060208701935060005b81811015611b7757845173ffffffffffffffffffffffffffffffffffffffff1683529383019391830191600101611b45565b509097965050505050505056fea164736f6c6343000818000a", + ABI: "[{\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"transmissionId\",\"type\":\"bytes32\"}],\"name\":\"AlreadyAttempted\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"signer\",\"type\":\"address\"}],\"name\":\"DuplicateSigner\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"numSigners\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"maxSigners\",\"type\":\"uint256\"}],\"name\":\"ExcessSigners\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"FaultToleranceMustBePositive\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"transmissionId\",\"type\":\"bytes32\"}],\"name\":\"InsufficientGasForRouting\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"numSigners\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"minSigners\",\"type\":\"uint256\"}],\"name\":\"InsufficientSigners\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint64\",\"name\":\"configId\",\"type\":\"uint64\"}],\"name\":\"InvalidConfig\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidReport\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"signature\",\"type\":\"bytes\"}],\"name\":\"InvalidSignature\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"expected\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"received\",\"type\":\"uint256\"}],\"name\":\"InvalidSignatureCount\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"signer\",\"type\":\"address\"}],\"name\":\"InvalidSigner\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"UnauthorizedForwarder\",\"type\":\"error\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint32\",\"name\":\"donId\",\"type\":\"uint32\"},{\"indexed\":true,\"internalType\":\"uint32\",\"name\":\"configVersion\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint8\",\"name\":\"f\",\"type\":\"uint8\"},{\"indexed\":false,\"internalType\":\"address[]\",\"name\":\"signers\",\"type\":\"address[]\"}],\"name\":\"ConfigSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"forwarder\",\"type\":\"address\"}],\"name\":\"ForwarderAdded\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"forwarder\",\"type\":\"address\"}],\"name\":\"ForwarderRemoved\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"OwnershipTransferRequested\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"OwnershipTransferred\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"receiver\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"workflowExecutionId\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"bytes2\",\"name\":\"reportId\",\"type\":\"bytes2\"},{\"indexed\":false,\"internalType\":\"bool\",\"name\":\"result\",\"type\":\"bool\"}],\"name\":\"ReportProcessed\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"acceptOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"forwarder\",\"type\":\"address\"}],\"name\":\"addForwarder\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint32\",\"name\":\"donId\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"configVersion\",\"type\":\"uint32\"}],\"name\":\"clearConfig\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"receiver\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"workflowExecutionId\",\"type\":\"bytes32\"},{\"internalType\":\"bytes2\",\"name\":\"reportId\",\"type\":\"bytes2\"}],\"name\":\"getTransmissionId\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"receiver\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"workflowExecutionId\",\"type\":\"bytes32\"},{\"internalType\":\"bytes2\",\"name\":\"reportId\",\"type\":\"bytes2\"}],\"name\":\"getTransmissionInfo\",\"outputs\":[{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"transmissionId\",\"type\":\"bytes32\"},{\"internalType\":\"enumIRouter.TransmissionState\",\"name\":\"state\",\"type\":\"uint8\"},{\"internalType\":\"address\",\"name\":\"transmitter\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"invalidReceiver\",\"type\":\"bool\"},{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"},{\"internalType\":\"uint80\",\"name\":\"gasLimit\",\"type\":\"uint80\"}],\"internalType\":\"structIRouter.TransmissionInfo\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"receiver\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"workflowExecutionId\",\"type\":\"bytes32\"},{\"internalType\":\"bytes2\",\"name\":\"reportId\",\"type\":\"bytes2\"}],\"name\":\"getTransmitter\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"forwarder\",\"type\":\"address\"}],\"name\":\"isForwarder\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"forwarder\",\"type\":\"address\"}],\"name\":\"removeForwarder\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"receiver\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"rawReport\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"reportContext\",\"type\":\"bytes\"},{\"internalType\":\"bytes[]\",\"name\":\"signatures\",\"type\":\"bytes[]\"}],\"name\":\"report\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"transmissionId\",\"type\":\"bytes32\"},{\"internalType\":\"address\",\"name\":\"transmitter\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"receiver\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"metadata\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"validatedReport\",\"type\":\"bytes\"}],\"name\":\"route\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint32\",\"name\":\"donId\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"configVersion\",\"type\":\"uint32\"},{\"internalType\":\"uint8\",\"name\":\"f\",\"type\":\"uint8\"},{\"internalType\":\"address[]\",\"name\":\"signers\",\"type\":\"address[]\"}],\"name\":\"setConfig\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"transferOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"typeAndVersion\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]", + Bin: "0x60806040523480156200001157600080fd5b503380600081620000695760405162461bcd60e51b815260206004820152601860248201527f43616e6e6f7420736574206f776e657220746f207a65726f000000000000000060448201526064015b60405180910390fd5b600080546001600160a01b0319166001600160a01b03848116919091179091558116156200009c576200009c81620000bf565b5050306000908152600360205260409020805460ff19166001179055506200016a565b336001600160a01b03821603620001195760405162461bcd60e51b815260206004820152601760248201527f43616e6e6f74207472616e7366657220746f2073656c66000000000000000000604482015260640162000060565b600180546001600160a01b0319166001600160a01b0383811691821790925560008054604051929316917fed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae12789190a350565b612141806200017a6000396000f3fe608060405234801561001057600080fd5b50600436106100ea5760003560e01c806379ba50971161008c578063abcef55411610066578063abcef5541461035d578063ee59d26c14610396578063ef6e17a0146103a9578063f2fde38b146103bc57600080fd5b806379ba50971461025e5780638864b864146102665780638da5cb5b1461033f57600080fd5b8063272cbd93116100c8578063272cbd9314610179578063354bdd66146101995780634d93172d146102385780635c41d2fe1461024b57600080fd5b806311289565146100ef578063181f5a7714610104578063233fd52d14610156575b600080fd5b6101026100fd3660046119df565b6103cf565b005b6101406040518060400160405280601a81526020017f466f7277617264657220616e6420526f7574657220312e302e3000000000000081525081565b60405161014d9190611a8a565b60405180910390f35b610169610164366004611af7565b610989565b604051901515815260200161014d565b61018c610187366004611b7f565b610e4a565b60405161014d9190611c13565b61022a6101a7366004611b7f565b6040517fffffffffffffffffffffffffffffffffffffffff000000000000000000000000606085901b166020820152603481018390527fffff000000000000000000000000000000000000000000000000000000000000821660548201526000906056016040516020818303038152906040528051906020012090509392505050565b60405190815260200161014d565b610102610246366004611cbb565b611050565b610102610259366004611cbb565b6110cc565b61010261114b565b61031a610274366004611b7f565b6040805160609490941b7fffffffffffffffffffffffffffffffffffffffff0000000000000000000000001660208086019190915260348501939093527fffff000000000000000000000000000000000000000000000000000000000000919091166054840152805160368185030181526056909301815282519282019290922060009081526004909152205473ffffffffffffffffffffffffffffffffffffffff1690565b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200161014d565b60005473ffffffffffffffffffffffffffffffffffffffff1661031a565b61016961036b366004611cbb565b73ffffffffffffffffffffffffffffffffffffffff1660009081526003602052604090205460ff1690565b6101026103a4366004611cf1565b611248565b6101026103b7366004611d6f565b611625565b6101026103ca366004611cbb565b6116c5565b606d85101561040a576040517fb55ac75400000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600080600061044e89898080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152506116d992505050565b67ffffffffffffffff8216600090815260026020526040812080549497509195509193509160ff16908190036104c1576040517fdf3b81ea00000000000000000000000000000000000000000000000000000000815267ffffffffffffffff841660048201526024015b60405180910390fd5b856104cd826001611dd1565b60ff161461051f576104e0816001611dd1565b6040517fd6022e8e00000000000000000000000000000000000000000000000000000000815260ff9091166004820152602481018790526044016104b8565b60008b8b604051610531929190611df0565b60405190819003812061054a918c908c90602001611e00565b60405160208183030381529060405280519060200120905061056a61186c565b60005b888110156107ec573660008b8b8481811061058a5761058a611e1a565b905060200281019061059c9190611e49565b9092509050604181146105df5781816040517f2adfdc300000000000000000000000000000000000000000000000000000000081526004016104b8929190611ef7565b6000600186848460408181106105f7576105f7611e1a565b61060992013560f81c9050601b611dd1565b610617602060008789611f13565b61062091611f3d565b61062e60406020888a611f13565b61063791611f3d565b6040805160008152602081018083529590955260ff909316928401929092526060830152608082015260a0016020604051602081039080840390855afa158015610685573d6000803e3d6000fd5b5050604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0015173ffffffffffffffffffffffffffffffffffffffff8116600090815260028c0160205291822054909350915081900361072b576040517fbf18af4300000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff831660048201526024016104b8565b600086826020811061073f5761073f611e1a565b602002015173ffffffffffffffffffffffffffffffffffffffff16146107a9576040517fe021c4f200000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff831660048201526024016104b8565b818682602081106107bc576107bc611e1a565b73ffffffffffffffffffffffffffffffffffffffff909216602092909202015250506001909201915061056d9050565b50506040805160608f901b7fffffffffffffffffffffffffffffffffffffffff00000000000000000000000016602080830191909152603482018990527fffff0000000000000000000000000000000000000000000000000000000000008816605483015282516036818403018152605690920190925280519101206000945030935063233fd52d92509050338d8d8d602d90606d9261088e93929190611f13565b8f8f606d9080926108a193929190611f13565b6040518863ffffffff1660e01b81526004016108c39796959493929190611f79565b6020604051808303816000875af11580156108e2573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109069190611fda565b9050817dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916838b73ffffffffffffffffffffffffffffffffffffffff167f3617b009e9785c42daebadb6d3fb553243a4bf586d07ea72d65d80013ce116b584604051610975911515815260200190565b60405180910390a450505050505050505050565b3360009081526003602052604081205460ff166109d2576040517fd79e123d00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60005a9050619c40811015610a16576040517f0bfecd63000000000000000000000000000000000000000000000000000000008152600481018a90526024016104b8565b6000898152600460209081526040918290208251608081018452905473ffffffffffffffffffffffffffffffffffffffff8116825274010000000000000000000000000000000000000000810460ff90811615159383019390935275010000000000000000000000000000000000000000008104909216151592810183905276010000000000000000000000000000000000000000000090910469ffffffffffffffffffff1660608201529080610ace575080602001515b15610b08576040517fa53dc8ca000000000000000000000000000000000000000000000000000000008152600481018b90526024016104b8565b6000610b16619c4084611ffc565b905089600460008d815260200190815260200160002060000160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555080600460008d815260200190815260200160002060000160166101000a81548169ffffffffffffffffffff021916908369ffffffffffffffffffff1602179055508873ffffffffffffffffffffffffffffffffffffffff163b600003610c2257505050600088815260046020526040812080547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff16740100000000000000000000000000000000000000001790559050610e3f565b6040517f01ffc9a70000000000000000000000000000000000000000000000000000000081527f805f213200000000000000000000000000000000000000000000000000000000600482015273ffffffffffffffffffffffffffffffffffffffff8a16906301ffc9a790602401602060405180830381865afa925050508015610ce6575060408051601f3d9081017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0168201909252610ce391810190611fda565b60015b610d3f57505050600088815260046020526040812080547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff16740100000000000000000000000000000000000000001790559050610e3f565b5060008089898989604051602401610d5a949392919061200f565b604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08184030181529190526020810180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167f805f21320000000000000000000000000000000000000000000000000000000017815281519192506000918291828f88f191508115610e335760008d815260046020526040902080547fffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffff1675010000000000000000000000000000000000000000001790555b509350610e3f92505050565b979650505050505050565b6040805160c0810182526000808252602080830182905282840182905260608084018390526080840183905260a0840183905284519088901b7fffffffffffffffffffffffffffffffffffffffff0000000000000000000000001681830152603481018790527fffff000000000000000000000000000000000000000000000000000000000000861660548201528451603681830301815260568201808752815191840191909120808552600490935285842060d68301909652945473ffffffffffffffffffffffffffffffffffffffff811680875274010000000000000000000000000000000000000000820460ff9081161515607685015275010000000000000000000000000000000000000000008304161515609684015276010000000000000000000000000000000000000000000090910469ffffffffffffffffffff1660b69092019190915292939092909190610fa857506000610fd0565b816020015115610fba57506002610fd0565b8160400151610fca576003610fcd565b60015b90505b6040518060c00160405280848152602001826003811115610ff357610ff3611be4565b8152602001836000015173ffffffffffffffffffffffffffffffffffffffff168152602001836020015115158152602001836040015115158152602001836060015169ffffffffffffffffffff1681525093505050509392505050565b6110586116f4565b73ffffffffffffffffffffffffffffffffffffffff811660008181526003602052604080822080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00169055517fb96d15bf9258c7b8df062753a6a262864611fc7b060a5ee2e57e79b85f898d389190a250565b6110d46116f4565b73ffffffffffffffffffffffffffffffffffffffff811660008181526003602052604080822080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055517f0ea0ce2c048ff45a4a95f2947879de3fb94abec2f152190400cab2d1272a68e79190a250565b60015473ffffffffffffffffffffffffffffffffffffffff1633146111cc576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601660248201527f4d7573742062652070726f706f736564206f776e65720000000000000000000060448201526064016104b8565b60008054337fffffffffffffffffffffffff00000000000000000000000000000000000000008083168217845560018054909116905560405173ffffffffffffffffffffffffffffffffffffffff90921692909183917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e091a350565b6112506116f4565b8260ff1660000361128d576040517f0743bae600000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b601f8111156112d2576040517f61750f4000000000000000000000000000000000000000000000000000000000815260048101829052601f60248201526044016104b8565b6112dd836003612036565b60ff16811161133b57806112f2846003612036565b6112fd906001611dd1565b6040517f9dd9e6d8000000000000000000000000000000000000000000000000000000008152600481019290925260ff1660248201526044016104b8565b67ffffffff00000000602086901b1663ffffffff85161760005b67ffffffffffffffff82166000908152600260205260409020600101548110156113eb5767ffffffffffffffff82166000908152600260208190526040822060018101805491909201929190849081106113b1576113b1611e1a565b600091825260208083209091015473ffffffffffffffffffffffffffffffffffffffff168352820192909252604001812055600101611355565b5060005b8281101561156757600084848381811061140b5761140b611e1a565b90506020020160208101906114209190611cbb565b905073ffffffffffffffffffffffffffffffffffffffff8116611487576040517fbf18af4300000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff821660048201526024016104b8565b67ffffffffffffffff8316600090815260026020818152604080842073ffffffffffffffffffffffffffffffffffffffff86168552909201905290205415611513576040517fe021c4f200000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff821660048201526024016104b8565b61151e826001612052565b67ffffffffffffffff8416600090815260026020818152604080842073ffffffffffffffffffffffffffffffffffffffff909616845294909101905291909120556001016113ef565b5067ffffffffffffffff8116600090815260026020526040902061158f90600101848461188b565b5067ffffffffffffffff81166000908152600260205260409081902080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001660ff87161790555163ffffffff86811691908816907f4120bd3b23957dd423555817d55654d4481b438aa15485c21b4180c784f1a4559061161590889088908890612065565b60405180910390a3505050505050565b61162d6116f4565b63ffffffff818116602084811b67ffffffff00000000168217600090815260028252604080822080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001690558051828152928301905291928516917f4120bd3b23957dd423555817d55654d4481b438aa15485c21b4180c784f1a455916040516116b99291906120cb565b60405180910390a35050565b6116cd6116f4565b6116d681611777565b50565b60218101516045820151608b90920151909260c09290921c91565b60005473ffffffffffffffffffffffffffffffffffffffff163314611775576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601660248201527f4f6e6c792063616c6c61626c65206279206f776e65720000000000000000000060448201526064016104b8565b565b3373ffffffffffffffffffffffffffffffffffffffff8216036117f6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f43616e6e6f74207472616e7366657220746f2073656c6600000000000000000060448201526064016104b8565b600180547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83811691821790925560008054604051929316917fed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae12789190a350565b6040518061040001604052806020906020820280368337509192915050565b828054828255906000526020600020908101928215611903579160200282015b828111156119035781547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff8435161782556020909201916001909101906118ab565b5061190f929150611913565b5090565b5b8082111561190f5760008155600101611914565b803573ffffffffffffffffffffffffffffffffffffffff8116811461194c57600080fd5b919050565b60008083601f84011261196357600080fd5b50813567ffffffffffffffff81111561197b57600080fd5b60208301915083602082850101111561199357600080fd5b9250929050565b60008083601f8401126119ac57600080fd5b50813567ffffffffffffffff8111156119c457600080fd5b6020830191508360208260051b850101111561199357600080fd5b60008060008060008060006080888a0312156119fa57600080fd5b611a0388611928565b9650602088013567ffffffffffffffff80821115611a2057600080fd5b611a2c8b838c01611951565b909850965060408a0135915080821115611a4557600080fd5b611a518b838c01611951565b909650945060608a0135915080821115611a6a57600080fd5b50611a778a828b0161199a565b989b979a50959850939692959293505050565b60006020808352835180602085015260005b81811015611ab857858101830151858201604001528201611a9c565b5060006040828601015260407fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f8301168501019250505092915050565b600080600080600080600060a0888a031215611b1257600080fd5b87359650611b2260208901611928565b9550611b3060408901611928565b9450606088013567ffffffffffffffff80821115611b4d57600080fd5b611b598b838c01611951565b909650945060808a0135915080821115611b7257600080fd5b50611a778a828b01611951565b600080600060608486031215611b9457600080fd5b611b9d84611928565b92506020840135915060408401357fffff00000000000000000000000000000000000000000000000000000000000081168114611bd957600080fd5b809150509250925092565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b81518152602082015160c082019060048110611c58577f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b8060208401525073ffffffffffffffffffffffffffffffffffffffff604084015116604083015260608301511515606083015260808301511515608083015260a0830151611cb460a084018269ffffffffffffffffffff169052565b5092915050565b600060208284031215611ccd57600080fd5b611cd682611928565b9392505050565b803563ffffffff8116811461194c57600080fd5b600080600080600060808688031215611d0957600080fd5b611d1286611cdd565b9450611d2060208701611cdd565b9350604086013560ff81168114611d3657600080fd5b9250606086013567ffffffffffffffff811115611d5257600080fd5b611d5e8882890161199a565b969995985093965092949392505050565b60008060408385031215611d8257600080fd5b611d8b83611cdd565b9150611d9960208401611cdd565b90509250929050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60ff8181168382160190811115611dea57611dea611da2565b92915050565b8183823760009101908152919050565b838152818360208301376000910160200190815292915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b60008083357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe1843603018112611e7e57600080fd5b83018035915067ffffffffffffffff821115611e9957600080fd5b60200191503681900382131561199357600080fd5b8183528181602085013750600060208284010152600060207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f840116840101905092915050565b602081526000611f0b602083018486611eae565b949350505050565b60008085851115611f2357600080fd5b83861115611f3057600080fd5b5050820193919092039150565b80356020831015611dea577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff602084900360031b1b1692915050565b878152600073ffffffffffffffffffffffffffffffffffffffff808916602084015280881660408401525060a06060830152611fb960a083018688611eae565b8281036080840152611fcc818587611eae565b9a9950505050505050505050565b600060208284031215611fec57600080fd5b81518015158114611cd657600080fd5b81810381811115611dea57611dea611da2565b604081526000612023604083018688611eae565b8281036020840152610e3f818587611eae565b60ff8181168382160290811690818114611cb457611cb4611da2565b80820180821115611dea57611dea611da2565b60ff8416815260406020808301829052908201839052600090849060608401835b868110156120bf5773ffffffffffffffffffffffffffffffffffffffff6120ac85611928565b1682529282019290820190600101612086565b50979650505050505050565b60006040820160ff8516835260206040602085015281855180845260608601915060208701935060005b8181101561212757845173ffffffffffffffffffffffffffffffffffffffff16835293830193918301916001016120f5565b509097965050505050505056fea164736f6c6343000818000a", } var KeystoneForwarderABI = KeystoneForwarderMetaData.ABI @@ -193,26 +202,26 @@ func (_KeystoneForwarder *KeystoneForwarderCallerSession) GetTransmissionId(rece return _KeystoneForwarder.Contract.GetTransmissionId(&_KeystoneForwarder.CallOpts, receiver, workflowExecutionId, reportId) } -func (_KeystoneForwarder *KeystoneForwarderCaller) GetTransmissionState(opts *bind.CallOpts, receiver common.Address, workflowExecutionId [32]byte, reportId [2]byte) (uint8, error) { +func (_KeystoneForwarder *KeystoneForwarderCaller) GetTransmissionInfo(opts *bind.CallOpts, receiver common.Address, workflowExecutionId [32]byte, reportId [2]byte) (IRouterTransmissionInfo, error) { var out []interface{} - err := _KeystoneForwarder.contract.Call(opts, &out, "getTransmissionState", receiver, workflowExecutionId, reportId) + err := _KeystoneForwarder.contract.Call(opts, &out, "getTransmissionInfo", receiver, workflowExecutionId, reportId) if err != nil { - return *new(uint8), err + return *new(IRouterTransmissionInfo), err } - out0 := *abi.ConvertType(out[0], new(uint8)).(*uint8) + out0 := *abi.ConvertType(out[0], new(IRouterTransmissionInfo)).(*IRouterTransmissionInfo) return out0, err } -func (_KeystoneForwarder *KeystoneForwarderSession) GetTransmissionState(receiver common.Address, workflowExecutionId [32]byte, reportId [2]byte) (uint8, error) { - return _KeystoneForwarder.Contract.GetTransmissionState(&_KeystoneForwarder.CallOpts, receiver, workflowExecutionId, reportId) +func (_KeystoneForwarder *KeystoneForwarderSession) GetTransmissionInfo(receiver common.Address, workflowExecutionId [32]byte, reportId [2]byte) (IRouterTransmissionInfo, error) { + return _KeystoneForwarder.Contract.GetTransmissionInfo(&_KeystoneForwarder.CallOpts, receiver, workflowExecutionId, reportId) } -func (_KeystoneForwarder *KeystoneForwarderCallerSession) GetTransmissionState(receiver common.Address, workflowExecutionId [32]byte, reportId [2]byte) (uint8, error) { - return _KeystoneForwarder.Contract.GetTransmissionState(&_KeystoneForwarder.CallOpts, receiver, workflowExecutionId, reportId) +func (_KeystoneForwarder *KeystoneForwarderCallerSession) GetTransmissionInfo(receiver common.Address, workflowExecutionId [32]byte, reportId [2]byte) (IRouterTransmissionInfo, error) { + return _KeystoneForwarder.Contract.GetTransmissionInfo(&_KeystoneForwarder.CallOpts, receiver, workflowExecutionId, reportId) } func (_KeystoneForwarder *KeystoneForwarderCaller) GetTransmitter(opts *bind.CallOpts, receiver common.Address, workflowExecutionId [32]byte, reportId [2]byte) (common.Address, error) { @@ -1260,7 +1269,7 @@ func (_KeystoneForwarder *KeystoneForwarder) Address() common.Address { type KeystoneForwarderInterface interface { GetTransmissionId(opts *bind.CallOpts, receiver common.Address, workflowExecutionId [32]byte, reportId [2]byte) ([32]byte, error) - GetTransmissionState(opts *bind.CallOpts, receiver common.Address, workflowExecutionId [32]byte, reportId [2]byte) (uint8, error) + GetTransmissionInfo(opts *bind.CallOpts, receiver common.Address, workflowExecutionId [32]byte, reportId [2]byte) (IRouterTransmissionInfo, error) GetTransmitter(opts *bind.CallOpts, receiver common.Address, workflowExecutionId [32]byte, reportId [2]byte) (common.Address, error) diff --git a/core/gethwrappers/keystone/generation/generated-wrapper-dependency-versions-do-not-edit.txt b/core/gethwrappers/keystone/generation/generated-wrapper-dependency-versions-do-not-edit.txt index 98d0a4bd02c..cae02cda285 100644 --- a/core/gethwrappers/keystone/generation/generated-wrapper-dependency-versions-do-not-edit.txt +++ b/core/gethwrappers/keystone/generation/generated-wrapper-dependency-versions-do-not-edit.txt @@ -1,5 +1,5 @@ GETH_VERSION: 1.13.8 capabilities_registry: ../../../contracts/solc/v0.8.24/CapabilitiesRegistry/CapabilitiesRegistry.abi ../../../contracts/solc/v0.8.24/CapabilitiesRegistry/CapabilitiesRegistry.bin 6d2e3aa3a6f3aed2cf24b613743bb9ae4b9558f48a6864dc03b8b0ebb37235e3 -feeds_consumer: ../../../contracts/solc/v0.8.24/KeystoneFeedsConsumer/KeystoneFeedsConsumer.abi ../../../contracts/solc/v0.8.24/KeystoneFeedsConsumer/KeystoneFeedsConsumer.bin f098e25df6afc100425fcad7f5107aec0844cc98315117e49da139a179d0eead -forwarder: ../../../contracts/solc/v0.8.24/KeystoneForwarder/KeystoneForwarder.abi ../../../contracts/solc/v0.8.24/KeystoneForwarder/KeystoneForwarder.bin 21a203d62a69338a5ca260907a31727421114ca25679330ada5d68f0092725bf +feeds_consumer: ../../../contracts/solc/v0.8.24/KeystoneFeedsConsumer/KeystoneFeedsConsumer.abi ../../../contracts/solc/v0.8.24/KeystoneFeedsConsumer/KeystoneFeedsConsumer.bin 8c3a2b18a80be41e7c40d2bc3a4c8d1b5e18d55c1fd20ad5af68cebb66109fc5 +forwarder: ../../../contracts/solc/v0.8.24/KeystoneForwarder/KeystoneForwarder.abi ../../../contracts/solc/v0.8.24/KeystoneForwarder/KeystoneForwarder.bin 45d9b866c64b41c1349a90b6764aee42a6d078b454d38f369b5fe02b23b9d16e ocr3_capability: ../../../contracts/solc/v0.8.24/OCR3Capability/OCR3Capability.abi ../../../contracts/solc/v0.8.24/OCR3Capability/OCR3Capability.bin 8bf0f53f222efce7143dea6134552eb26ea1eef845407b4475a0d79b7d7ba9f8 diff --git a/core/services/relay/evm/write_target.go b/core/services/relay/evm/write_target.go index fb1c694a2e7..6a584413dbe 100644 --- a/core/services/relay/evm/write_target.go +++ b/core/services/relay/evm/write_target.go @@ -31,8 +31,8 @@ func NewWriteTarget(ctx context.Context, relayer *Relayer, chain legacyevm.Chain "forwarder": { ContractABI: forwarder.KeystoneForwarderABI, Configs: map[string]*relayevmtypes.ChainReaderDefinition{ - "getTransmitter": { - ChainSpecificName: "getTransmitter", + "getTransmissionInfo": { + ChainSpecificName: "getTransmissionInfo", }, }, }, @@ -46,6 +46,7 @@ func NewWriteTarget(ctx context.Context, relayer *Relayer, chain legacyevm.Chain return nil, err } + var gasLimit uint64 = 400_000 chainWriterConfig := relayevmtypes.ChainWriterConfig{ Contracts: map[string]*relayevmtypes.ContractConfig{ "forwarder": { @@ -55,7 +56,7 @@ func NewWriteTarget(ctx context.Context, relayer *Relayer, chain legacyevm.Chain ChainSpecificName: "report", Checker: "simulate", FromAddress: config.FromAddress().Address(), - GasLimit: 200_000, + GasLimit: gasLimit, }, }, }, @@ -73,5 +74,5 @@ func NewWriteTarget(ctx context.Context, relayer *Relayer, chain legacyevm.Chain return nil, err } - return targets.NewWriteTarget(lggr, id, cr, cw, config.ForwarderAddress().String()), nil + return targets.NewWriteTarget(lggr.Named("WriteTarget"), id, cr, cw, config.ForwarderAddress().String(), gasLimit), nil } diff --git a/core/services/relay/evm/write_target_test.go b/core/services/relay/evm/write_target_test.go index f3dcae220eb..57a0f80f8d3 100644 --- a/core/services/relay/evm/write_target_test.go +++ b/core/services/relay/evm/write_target_test.go @@ -1,7 +1,9 @@ package evm_test import ( + "bytes" "errors" + "fmt" "math/big" "testing" @@ -9,6 +11,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/values" "github.com/smartcontractkit/chainlink/v2/common/headtracker/mocks" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/targets" "github.com/smartcontractkit/chainlink/v2/core/internal/cltest" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest" "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm" @@ -36,16 +39,72 @@ import ( var forwardABI = types.MustGetABI(forwarder.KeystoneForwarderMetaData.ABI) +func newMockedEncodeTransmissionInfo() ([]byte, error) { + info := targets.TransmissionInfo{ + GasLimit: big.NewInt(0), + InvalidReceiver: false, + State: 0, + Success: false, + TransmissionId: [32]byte{}, + Transmitter: common.HexToAddress("0x0"), + } + + var buffer bytes.Buffer + gasLimitBytes := info.GasLimit.Bytes() + if len(gasLimitBytes) > 80 { + return nil, fmt.Errorf("GasLimit too large") + } + paddedGasLimit := make([]byte, 80-len(gasLimitBytes)) + buffer.Write(paddedGasLimit) + buffer.Write(gasLimitBytes) + + // Encode InvalidReceiver (as uint8) + if info.InvalidReceiver { + buffer.WriteByte(1) + } else { + buffer.WriteByte(0) + } + + // Padding for InvalidReceiver to fit into 32 bytes + padInvalidReceiver := make([]byte, 31) + buffer.Write(padInvalidReceiver) + + // Encode State (as uint8) + buffer.WriteByte(info.State) + + // Padding for State to fit into 32 bytes + padState := make([]byte, 31) + buffer.Write(padState) + + // Encode Success (as uint8) + if info.Success { + buffer.WriteByte(1) + } else { + buffer.WriteByte(0) + } + + // Padding for Success to fit into 32 bytes + padSuccess := make([]byte, 31) + buffer.Write(padSuccess) + + // Encode TransmissionId (as bytes32) + buffer.Write(info.TransmissionId[:]) + + // Encode Transmitter (as address) + buffer.Write(info.Transmitter.Bytes()) + + return buffer.Bytes(), nil +} + func TestEvmWrite(t *testing.T) { chain := evmmocks.NewChain(t) txManager := txmmocks.NewMockEvmTxManager(t) evmClient := evmclimocks.NewClient(t) - // This probably isn't the best way to do this, but couldn't find a simpler way to mock the CallContract response - var mockCall []byte - for i := 0; i < 32; i++ { - mockCall = append(mockCall, byte(0)) - } + // This is a very error-prone way to mock an on-chain response to a GetLatestValue("getTransmissionInfo") call + // It's a bit of a hack, but it's the best way to do it without a lot of refactoring + mockCall, err := newMockedEncodeTransmissionInfo() + require.NoError(t, err) evmClient.On("CallContract", mock.Anything, mock.Anything, mock.Anything).Return(mockCall, nil).Maybe() evmClient.On("CodeAt", mock.Anything, mock.Anything, mock.Anything).Return([]byte("test"), nil) From 40f4becb1eab96920d8bfd59019cdb9358a94122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deividas=20Kar=C5=BEinauskas?= Date: Thu, 8 Aug 2024 17:22:15 +0300 Subject: [PATCH 8/9] Fix write target nil dereferences (#14059) * Hardcoded value for call with exact gas * Record gasProvided in route function * Add a getter for transmission gas limit * Update snapshot * Changeset * Remove unused import * Rename to gas limit * Update gethwrappers * Uncomment test code * Remove copy/pasta comment * Slight rename * Allow retrying transmissions with more gas * Only allow retrying failed transmissions * Update snapshot * Fix state for InvalidReceiver check * Check for initial state * Actually store gas limit provided to receiver * Update gethwrappers * Remove unused struct * Correctly mark invalid receiver when receiver interface unsupported * Create TransmissionInfo struct * Update gethwrappers * Bump gas limit * Bump gas even more * Update KeystoneFeedsConsumer.sol to implement IERC165 * Use getTransmissionInfo * Use TransmissionState to determine if transmission should be created * Fix test * Fix trailing line * Update a mock to the GetLatestValue("getTransmissionInfo") call in a test * Remove TODO + replace panic with err * Remove redundant empty lines * Typo * Fix nil pointer dereference in write target implementation * Remove unused constant * Name mapping values * Add changeset --------- Co-authored-by: app-token-issuer-infra-releng[bot] <120227048+app-token-issuer-infra-releng[bot]@users.noreply.github.com> --- .changeset/shy-windows-juggle.md | 5 +++ core/capabilities/targets/write_target.go | 8 +++++ .../capabilities/targets/write_target_test.go | 32 ++++++++++++++++--- 3 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 .changeset/shy-windows-juggle.md diff --git a/.changeset/shy-windows-juggle.md b/.changeset/shy-windows-juggle.md new file mode 100644 index 00000000000..0408383bd03 --- /dev/null +++ b/.changeset/shy-windows-juggle.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +#internal diff --git a/core/capabilities/targets/write_target.go b/core/capabilities/targets/write_target.go index cc3a58b482e..4524c4fd449 100644 --- a/core/capabilities/targets/write_target.go +++ b/core/capabilities/targets/write_target.go @@ -76,6 +76,10 @@ type EvmConfig struct { } func parseConfig(rawConfig *values.Map) (config EvmConfig, err error) { + if rawConfig == nil { + return config, fmt.Errorf("missing config field") + } + if err := rawConfig.UnwrapTo(&config); err != nil { return config, err } @@ -117,6 +121,10 @@ func (cap *WriteTarget) Execute(ctx context.Context, request capabilities.Capabi return nil, err } + if request.Inputs == nil { + return nil, fmt.Errorf("missing inputs field") + } + signedReport, ok := request.Inputs.Underlying[signedReportField] if !ok { return nil, fmt.Errorf("missing required field %s", signedReportField) diff --git a/core/capabilities/targets/write_target_test.go b/core/capabilities/targets/write_target_test.go index 13305fe7ef9..0fa750911dd 100644 --- a/core/capabilities/targets/write_target_test.go +++ b/core/capabilities/targets/write_target_test.go @@ -134,10 +134,10 @@ func TestWriteTarget(t *testing.T) { }) t.Run("fails with invalid config", func(t *testing.T) { - invalidConfig, err := values.NewMap(map[string]any{ + invalidConfig, err2 := values.NewMap(map[string]any{ "Address": "invalid-address", }) - require.NoError(t, err) + require.NoError(t, err2) req := capabilities.CapabilityRequest{ Metadata: capabilities.RequestMetadata{ @@ -146,7 +146,31 @@ func TestWriteTarget(t *testing.T) { Config: invalidConfig, Inputs: validInputs, } - _, err = writeTarget.Execute(ctx, req) - require.Error(t, err) + _, err2 = writeTarget.Execute(ctx, req) + require.Error(t, err2) + }) + + t.Run("fails with nil config", func(t *testing.T) { + req := capabilities.CapabilityRequest{ + Metadata: capabilities.RequestMetadata{ + WorkflowID: "test-id", + }, + Config: nil, + Inputs: validInputs, + } + _, err2 := writeTarget.Execute(ctx, req) + require.Error(t, err2) + }) + + t.Run("fails with nil inputs", func(t *testing.T) { + req := capabilities.CapabilityRequest{ + Metadata: capabilities.RequestMetadata{ + WorkflowID: "test-id", + }, + Config: config, + Inputs: nil, + } + _, err2 := writeTarget.Execute(ctx, req) + require.Error(t, err2) }) } From 4f0f7802a884e831cd76d9578ee5c4a7134034db Mon Sep 17 00:00:00 2001 From: Dylan Tinianov Date: Thu, 8 Aug 2024 10:33:44 -0400 Subject: [PATCH 9/9] Add Mantle errors (#14053) * Add Mantle errors * Add tests for Mantle errors * changeset * Update seven-kiwis-run.md --- .changeset/seven-kiwis-run.md | 5 +++++ core/chains/evm/client/errors.go | 7 ++++++- core/chains/evm/client/errors_test.go | 3 +++ 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 .changeset/seven-kiwis-run.md diff --git a/.changeset/seven-kiwis-run.md b/.changeset/seven-kiwis-run.md new file mode 100644 index 00000000000..3b56117c469 --- /dev/null +++ b/.changeset/seven-kiwis-run.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +Added custom client error messages for Mantle to capture InsufficientEth and Fatal errors. #added diff --git a/core/chains/evm/client/errors.go b/core/chains/evm/client/errors.go index 22fac5f7287..5d684d1d17c 100644 --- a/core/chains/evm/client/errors.go +++ b/core/chains/evm/client/errors.go @@ -250,6 +250,11 @@ var zkEvm = ClientErrors{ TerminallyStuck: regexp.MustCompile(`(?:: |^)not enough .* counters to continue the execution$`), } +var mantle = ClientErrors{ + InsufficientEth: regexp.MustCompile(`(: |^)'*insufficient funds for gas \* price \+ value`), + Fatal: regexp.MustCompile(`(: |^)'*invalid sender`), +} + const TerminallyStuckMsg = "transaction terminally stuck" // Tx.Error messages that are set internally so they are not chain or client specific @@ -257,7 +262,7 @@ var internal = ClientErrors{ TerminallyStuck: regexp.MustCompile(TerminallyStuckMsg), } -var clients = []ClientErrors{parity, geth, arbitrum, metis, substrate, avalanche, nethermind, harmony, besu, erigon, klaytn, celo, zkSync, zkEvm, internal} +var clients = []ClientErrors{parity, geth, arbitrum, metis, substrate, avalanche, nethermind, harmony, besu, erigon, klaytn, celo, zkSync, zkEvm, mantle, internal} // ClientErrorRegexes returns a map of compiled regexes for each error type func ClientErrorRegexes(errsRegex config.ClientErrors) *ClientErrors { diff --git a/core/chains/evm/client/errors_test.go b/core/chains/evm/client/errors_test.go index cca54c2a4a9..0d8dddf2a0f 100644 --- a/core/chains/evm/client/errors_test.go +++ b/core/chains/evm/client/errors_test.go @@ -214,6 +214,7 @@ func Test_Eth_Errors(t *testing.T) { {"insufficient funds for gas + value. balance: 42719769622667482000, fee: 48098250000000, value: 42719769622667482000", true, "celo"}, {"client error insufficient eth", true, "tomlConfig"}, {"transaction would cause overdraft", true, "Geth"}, + {"failed to forward tx to sequencer, please try again. Error message: 'insufficient funds for gas * price + value'", true, "Mantle"}, } for _, test := range tests { err = evmclient.NewSendErrorS(test.message) @@ -379,6 +380,8 @@ func Test_Eth_Errors_Fatal(t *testing.T) { {"Failed to serialize transaction: max priority fee per gas higher than 2^64-1", true, "zkSync"}, {"Failed to serialize transaction: oversized data. max: 1000000; actual: 1000000", true, "zkSync"}, + {"failed to forward tx to sequencer, please try again. Error message: 'invalid sender'", true, "Mantle"}, + {"client error fatal", true, "tomlConfig"}, }