From 737d349144c672f94daa2a1d830453de347646c3 Mon Sep 17 00:00:00 2001 From: Ryan Hall Date: Tue, 23 Jan 2024 14:15:14 -0500 Subject: [PATCH] Auto 8378 create copy of 2 1 contracts and test suits (#11853) * create automation v2.2 contracts * update licenses * adjust solhit rules * update native compilation step * fix foundry tests and AutomationRegistry file name * fix failing foundry test * update nate compile all * fix import path bug * replace automation registry contract * fix typo --- contracts/.solhint.json | 1 + contracts/.solhintignore | 17 +- contracts/package.json | 2 +- .../generate-automation-master-interface.ts | 12 +- .../native_solc_compile_all_automation | 12 +- .../{v2_1 => }/AutomationForwarder.sol | 2 +- .../{v2_1 => }/AutomationForwarderLogic.sol | 4 +- .../v2_2/IAutomationRegistryMaster.sol | 316 + .../dev/v2_2/AutomationRegistrar2_2.sol | 535 ++ .../dev/v2_2/AutomationRegistry2_2.sol | 397 ++ .../dev/v2_2/AutomationRegistryBase2_2.sol | 959 +++ .../dev/v2_2/AutomationRegistryLogicA2_2.sol | 438 ++ .../dev/v2_2/AutomationRegistryLogicB2_2.sol | 516 ++ .../dev/v2_2/AutomationUtils2_2.sol | 40 + .../src/v0.8/automation/dev/v2_2/LICENSE | 57 + .../src/v0.8/automation/dev/v2_2/README.md | 40 + .../{v2_1 => }/test/AutomationForwarder.t.sol | 10 +- .../src/v0.8/automation/test/BaseTest.t.sol | 2 +- .../{v2_1 => }/test/StructFactory.sol | 2 +- .../v2_1/KeeperRegistryLogicA2_1.sol | 2 +- contracts/src/v0.8/automation/v2_1/LICENSE | 3 +- .../v0.8/automation/v2_1/test/BaseTest.t.sol | 12 - .../automation/AutomationRegistrar2_2.test.ts | 1013 +++ .../automation/AutomationRegistry2_2.test.ts | 5757 +++++++++++++++++ 24 files changed, 10115 insertions(+), 34 deletions(-) rename contracts/src/v0.8/automation/{v2_1 => }/AutomationForwarder.sol (97%) rename contracts/src/v0.8/automation/{v2_1 => }/AutomationForwarderLogic.sol (80%) create mode 100644 contracts/src/v0.8/automation/dev/interfaces/v2_2/IAutomationRegistryMaster.sol create mode 100644 contracts/src/v0.8/automation/dev/v2_2/AutomationRegistrar2_2.sol create mode 100644 contracts/src/v0.8/automation/dev/v2_2/AutomationRegistry2_2.sol create mode 100644 contracts/src/v0.8/automation/dev/v2_2/AutomationRegistryBase2_2.sol create mode 100644 contracts/src/v0.8/automation/dev/v2_2/AutomationRegistryLogicA2_2.sol create mode 100644 contracts/src/v0.8/automation/dev/v2_2/AutomationRegistryLogicB2_2.sol create mode 100644 contracts/src/v0.8/automation/dev/v2_2/AutomationUtils2_2.sol create mode 100644 contracts/src/v0.8/automation/dev/v2_2/LICENSE create mode 100644 contracts/src/v0.8/automation/dev/v2_2/README.md rename contracts/src/v0.8/automation/{v2_1 => }/test/AutomationForwarder.t.sol (88%) rename contracts/src/v0.8/automation/{v2_1 => }/test/StructFactory.sol (85%) delete mode 100644 contracts/src/v0.8/automation/v2_1/test/BaseTest.t.sol create mode 100644 contracts/test/v0.8/automation/AutomationRegistrar2_2.test.ts create mode 100644 contracts/test/v0.8/automation/AutomationRegistry2_2.test.ts diff --git a/contracts/.solhint.json b/contracts/.solhint.json index e66b915d679..ea220d0a030 100644 --- a/contracts/.solhint.json +++ b/contracts/.solhint.json @@ -9,6 +9,7 @@ "func-named-parameters": "off", "immutable-vars-naming": "off", "no-inline-assembly": "off", + "contract-name-camelcase": "off", "no-unused-import": "error", "func-visibility": [ "error", diff --git a/contracts/.solhintignore b/contracts/.solhintignore index 569c8c51df3..4246bbd77ca 100644 --- a/contracts/.solhintignore +++ b/contracts/.solhintignore @@ -6,6 +6,21 @@ ./src/v0.8/automation/v1_3 ./src/v0.8/automation/v2_0 ./src/v0.8/automation/v2_1 +./src/v0.8/automation/interfaces/v2_1/ +./src/v0.8/automation/interfaces/MigratableKeeperRegistryInterface.sol +./src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol +./src/v0.8/automation/upkeeps/LinkAvailableBalanceMonitor.sol +./src/v0.8/automation/upkeeps/EthBalanceMonitor.sol +./src/v0.8/automation/upkeeps/ERC20BalanceMonitor.sol +./src/v0.8/automation/upkeeps/CronUpkeepFactory.sol +./src/v0.8/automation/upkeeps/CronUpkeepDelegate.sol +./src/v0.8/automation/upkeeps/CronUpkeep.sol +./src/v0.8/automation/libraries/internal/Cron.sol +./src/v0.8/automation/AutomationForwarder.sol +./src/v0.8/automation/AutomationForwarderLogic.sol + + +# Ignore tests / test helpers (for now) ./src/v0.8/automation/mocks ./src/v0.8/automation/testhelpers @@ -22,4 +37,4 @@ # Always ignore vendor ./src/v0.8/vendor -./node_modules/ \ No newline at end of file +./node_modules/ diff --git a/contracts/package.json b/contracts/package.json index 4dabbdb0509..5d4b6324eee 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -18,7 +18,7 @@ "prepublishOnly": "pnpm compile && ./scripts/prepublish_generate_abi_folder", "publish-beta": "pnpm publish --tag beta", "publish-prod": "npm dist-tag add @chainlink/contracts@0.8.0 latest", - "solhint": "solhint --max-warnings 83 \"./src/v0.8/**/*.sol\"" + "solhint": "solhint --max-warnings 33 \"./src/v0.8/**/*.sol\"" }, "files": [ "src/v0.8", diff --git a/contracts/scripts/generate-automation-master-interface.ts b/contracts/scripts/generate-automation-master-interface.ts index de71c56806b..2837fc34923 100644 --- a/contracts/scripts/generate-automation-master-interface.ts +++ b/contracts/scripts/generate-automation-master-interface.ts @@ -2,15 +2,15 @@ * @description this script generates a master interface for interacting with the automation registry * @notice run this script with pnpm ts-node ./scripts/generate-automation-master-interface.ts */ -import { KeeperRegistry2_1__factory as KeeperRegistry } from '../typechain/factories/KeeperRegistry2_1__factory' -import { KeeperRegistryLogicA2_1__factory as KeeperRegistryLogicA } from '../typechain/factories/KeeperRegistryLogicA2_1__factory' -import { KeeperRegistryLogicB2_1__factory as KeeperRegistryLogicB } from '../typechain/factories/KeeperRegistryLogicB2_1__factory' +import { KeeperRegistry2_2__factory as KeeperRegistry } from '../typechain/factories/KeeperRegistry2_2__factory' +import { KeeperRegistryLogicA2_2__factory as KeeperRegistryLogicA } from '../typechain/factories/KeeperRegistryLogicA2_2__factory' +import { KeeperRegistryLogicB2_2__factory as KeeperRegistryLogicB } from '../typechain/factories/KeeperRegistryLogicB2_2__factory' import { utils } from 'ethers' import fs from 'fs' import { exec } from 'child_process' -const dest = 'src/v0.8/automation/interfaces/v2_1' -const srcDest = `${dest}/IKeeperRegistryMaster.sol` +const dest = 'src/v0.8/automation/dev/interfaces/v2_2' +const srcDest = `${dest}/IAutomationRegistryMaster.sol` const tmpDest = `${dest}/tmp.txt` const combinedABI = [] @@ -44,7 +44,7 @@ const checksum = utils.id(abis.join('')) fs.writeFileSync(`${tmpDest}`, JSON.stringify(combinedABI)) const cmd = ` -cat ${tmpDest} | pnpm abi-to-sol --solidity-version ^0.8.4 --license MIT > ${srcDest} IKeeperRegistryMaster; +cat ${tmpDest} | pnpm abi-to-sol --solidity-version ^0.8.4 --license MIT > ${srcDest} IAutomationRegistryMaster; echo "// abi-checksum: ${checksum}" | cat - ${srcDest} > ${tmpDest} && mv ${tmpDest} ${srcDest}; pnpm prettier --write ${srcDest}; ` diff --git a/contracts/scripts/native_solc_compile_all_automation b/contracts/scripts/native_solc_compile_all_automation index ddf6c2c8bfa..379b647ffe5 100755 --- a/contracts/scripts/native_solc_compile_all_automation +++ b/contracts/scripts/native_solc_compile_all_automation @@ -57,10 +57,18 @@ compileContract automation/v2_1/AutomationRegistrar2_1.sol compileContract automation/v2_1/KeeperRegistry2_1.sol compileContract automation/v2_1/KeeperRegistryLogicA2_1.sol compileContract automation/v2_1/KeeperRegistryLogicB2_1.sol +compileContract automation/v2_1/AutomationUtils2_1.sol compileContract automation/interfaces/v2_1/IKeeperRegistryMaster.sol + +compileContract automation/dev/v2_2/AutomationRegistrar2_2.sol +compileContract automation/dev/v2_2/AutomationRegistry2_2.sol +compileContract automation/dev/v2_2/AutomationRegistryLogicA2_2.sol +compileContract automation/dev/v2_2/AutomationRegistryLogicB2_2.sol +compileContract automation/dev/v2_2/AutomationUtils2_2.sol +compileContract automation/dev/interfaces/v2_2/IAutomationRegistryMaster.sol + compileContract automation/interfaces/ILogAutomation.sol -compileContract automation/v2_1/AutomationUtils2_1.sol -compileContract automation/v2_1/AutomationForwarderLogic.sol +compileContract automation/AutomationForwarderLogic.sol compileContract automation/testhelpers/LogTriggeredStreamsLookup.sol compileContract automation/testhelpers/DummyProtocol.sol diff --git a/contracts/src/v0.8/automation/v2_1/AutomationForwarder.sol b/contracts/src/v0.8/automation/AutomationForwarder.sol similarity index 97% rename from contracts/src/v0.8/automation/v2_1/AutomationForwarder.sol rename to contracts/src/v0.8/automation/AutomationForwarder.sol index e5a80ce3252..f4088ed07fd 100644 --- a/contracts/src/v0.8/automation/v2_1/AutomationForwarder.sol +++ b/contracts/src/v0.8/automation/AutomationForwarder.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.16; -import {IAutomationRegistryConsumer} from "../interfaces/IAutomationRegistryConsumer.sol"; +import {IAutomationRegistryConsumer} from "./interfaces/IAutomationRegistryConsumer.sol"; uint256 constant PERFORM_GAS_CUSHION = 5_000; diff --git a/contracts/src/v0.8/automation/v2_1/AutomationForwarderLogic.sol b/contracts/src/v0.8/automation/AutomationForwarderLogic.sol similarity index 80% rename from contracts/src/v0.8/automation/v2_1/AutomationForwarderLogic.sol rename to contracts/src/v0.8/automation/AutomationForwarderLogic.sol index a7903c379df..76da5f68cb9 100644 --- a/contracts/src/v0.8/automation/v2_1/AutomationForwarderLogic.sol +++ b/contracts/src/v0.8/automation/AutomationForwarderLogic.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.16; -import {IAutomationRegistryConsumer} from "../interfaces/IAutomationRegistryConsumer.sol"; -import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; +import {IAutomationRegistryConsumer} from "./interfaces/IAutomationRegistryConsumer.sol"; +import {ITypeAndVersion} from "../shared/interfaces/ITypeAndVersion.sol"; contract AutomationForwarderLogic is ITypeAndVersion { IAutomationRegistryConsumer private s_registry; diff --git a/contracts/src/v0.8/automation/dev/interfaces/v2_2/IAutomationRegistryMaster.sol b/contracts/src/v0.8/automation/dev/interfaces/v2_2/IAutomationRegistryMaster.sol new file mode 100644 index 00000000000..b967c7d4b60 --- /dev/null +++ b/contracts/src/v0.8/automation/dev/interfaces/v2_2/IAutomationRegistryMaster.sol @@ -0,0 +1,316 @@ +// abi-checksum: 0x0ed34e4b36bd7b4a5447152c2d61491e6ba7ed944b11e4dfef4fea184708975e +// SPDX-License-Identifier: MIT +// !! THIS FILE WAS AUTOGENERATED BY abi-to-sol v0.6.6. SEE SOURCE BELOW. !! +pragma solidity ^0.8.4; + +interface IAutomationRegistryMaster { + 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 InsufficientFunds(); + error InvalidDataLength(); + error InvalidPayee(); + error InvalidRecipient(); + error InvalidReport(); + error InvalidSigner(); + error InvalidTransmitter(); + error InvalidTrigger(); + error InvalidTriggerType(); + error MaxCheckDataSizeCanOnlyIncrease(); + error MaxPerformDataSizeCanOnlyIncrease(); + error MigrationNotPermitted(); + error NotAContract(); + error OnlyActiveSigners(); + error OnlyActiveTransmitters(); + error OnlyCallableByAdmin(); + error OnlyCallableByLINKToken(); + error OnlyCallableByOwnerOrAdmin(); + error OnlyCallableByOwnerOrRegistrar(); + error OnlyCallableByPayee(); + error OnlyCallableByProposedAdmin(); + error OnlyCallableByProposedPayee(); + error OnlyCallableByUpkeepPrivilegeManager(); + error OnlyPausedUpkeep(); + error OnlySimulatedBackend(); + error OnlyUnpausedUpkeep(); + error ParameterLengthError(); + error PaymentGreaterThanAllLINK(); + error ReentrantCall(); + error RegistryPaused(); + error RepeatedSigner(); + error RepeatedTransmitter(); + error TargetCheckReverted(bytes reason); + error TooManyOracles(); + error TranscoderNotSet(); + error UpkeepAlreadyExists(); + error UpkeepCancelled(); + error UpkeepNotCanceled(); + error UpkeepNotNeeded(); + error ValueNotChanged(); + event AdminPrivilegeConfigSet(address indexed admin, bytes privilegeConfig); + event CancelledUpkeepReport(uint256 indexed id, bytes trigger); + event ConfigSet( + uint32 previousConfigBlockNumber, + bytes32 configDigest, + uint64 configCount, + address[] signers, + address[] transmitters, + uint8 f, + bytes onchainConfig, + uint64 offchainConfigVersion, + bytes offchainConfig + ); + event DedupKeyAdded(bytes32 indexed dedupKey); + 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 OwnerFundsWithdrawn(uint96 amount); + event OwnershipTransferRequested(address indexed from, address indexed to); + event OwnershipTransferred(address indexed from, address indexed to); + 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 Transmitted(bytes32 configDigest, uint32 epoch); + event Unpaused(address account); + event UpkeepAdminTransferRequested(uint256 indexed id, address indexed from, address indexed to); + event UpkeepAdminTransferred(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 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); + + fallback() external; + function acceptOwnership() external; + function fallbackTo() external view returns (address); + function latestConfigDetails() external view returns (uint32 configCount, uint32 blockNumber, bytes32 configDigest); + function latestConfigDigestAndEpoch() external view returns (bool scanLogs, bytes32 configDigest, uint32 epoch); + function onTokenTransfer(address sender, uint256 amount, bytes memory data) external; + function owner() external view returns (address); + function setConfig( + address[] memory signers, + address[] memory transmitters, + uint8 f, + bytes memory onchainConfigBytes, + uint64 offchainConfigVersion, + bytes memory offchainConfig + ) external; + function setConfigTypeSafe( + address[] memory signers, + address[] memory transmitters, + uint8 f, + KeeperRegistryBase2_2.OnchainConfig memory onchainConfig, + uint64 offchainConfigVersion, + bytes memory offchainConfig + ) external; + function simulatePerformUpkeep( + uint256 id, + bytes memory performData + ) external view returns (bool success, uint256 gasUsed); + function transferOwnership(address to) external; + function transmit( + bytes32[3] memory reportContext, + bytes memory rawReport, + bytes32[] memory rs, + bytes32[] memory ss, + bytes32 rawVs + ) external; + function typeAndVersion() external view returns (string memory); + + function addFunds(uint256 id, uint96 amount) external; + function cancelUpkeep(uint256 id) external; + function checkCallback( + uint256 id, + bytes[] memory values, + bytes memory extraData + ) external view returns (bool upkeepNeeded, bytes memory performData, uint8 upkeepFailureReason, uint256 gasUsed); + function checkUpkeep( + uint256 id, + bytes memory triggerData + ) + external + view + returns ( + bool upkeepNeeded, + bytes memory performData, + uint8 upkeepFailureReason, + uint256 gasUsed, + uint256 gasLimit, + uint256 fastGasWei, + uint256 linkNative + ); + function checkUpkeep( + uint256 id + ) + external + view + returns ( + bool upkeepNeeded, + bytes memory performData, + uint8 upkeepFailureReason, + uint256 gasUsed, + uint256 gasLimit, + uint256 fastGasWei, + uint256 linkNative + ); + function executeCallback( + uint256 id, + bytes memory payload + ) external returns (bool upkeepNeeded, bytes memory performData, uint8 upkeepFailureReason, uint256 gasUsed); + function migrateUpkeeps(uint256[] memory ids, address destination) external; + function receiveUpkeeps(bytes memory encodedUpkeeps) external; + function registerUpkeep( + address target, + uint32 gasLimit, + address admin, + uint8 triggerType, + bytes memory checkData, + bytes memory triggerConfig, + bytes memory offchainConfig + ) external returns (uint256 id); + function registerUpkeep( + address target, + uint32 gasLimit, + address admin, + bytes memory checkData, + bytes memory offchainConfig + ) external returns (uint256 id); + function setUpkeepTriggerConfig(uint256 id, bytes memory triggerConfig) external; + + function acceptPayeeship(address transmitter) external; + function acceptUpkeepAdmin(uint256 id) external; + function getActiveUpkeepIDs(uint256 startIndex, uint256 maxCount) external view returns (uint256[] memory); + function getAdminPrivilegeConfig(address admin) external view returns (bytes memory); + function getAutomationForwarderLogic() external view returns (address); + function getBalance(uint256 id) external view returns (uint96 balance); + function getCancellationDelay() external pure returns (uint256); + function getConditionalGasOverhead() external pure returns (uint256); + function getFastGasFeedAddress() external view returns (address); + function getForwarder(uint256 upkeepID) external view returns (address); + function getLinkAddress() external view returns (address); + function getLinkNativeFeedAddress() external view returns (address); + function getLogGasOverhead() external pure returns (uint256); + function getMaxPaymentForGas(uint8 triggerType, uint32 gasLimit) external view returns (uint96 maxPayment); + function getMinBalance(uint256 id) external view returns (uint96); + function getMinBalanceForUpkeep(uint256 id) external view returns (uint96 minBalance); + function getMode() external view returns (uint8); + function getPeerRegistryMigrationPermission(address peer) external view returns (uint8); + function getPerPerformByteGasOverhead() external pure returns (uint256); + function getPerSignerGasOverhead() external pure returns (uint256); + function getSignerInfo(address query) external view returns (bool active, uint8 index); + function getState() + external + view + returns ( + KeeperRegistryBase2_2.State memory state, + KeeperRegistryBase2_2.OnchainConfig memory config, + address[] memory signers, + address[] memory transmitters, + uint8 f + ); + function getTransmitterInfo( + address query + ) external view returns (bool active, uint8 index, uint96 balance, uint96 lastCollected, address payee); + function getTriggerType(uint256 upkeepId) external pure returns (uint8); + function getUpkeep(uint256 id) external view returns (KeeperRegistryBase2_2.UpkeepInfo memory upkeepInfo); + function getUpkeepPrivilegeConfig(uint256 upkeepId) external view returns (bytes memory); + function getUpkeepTriggerConfig(uint256 upkeepId) external view returns (bytes memory); + function hasDedupKey(bytes32 dedupKey) external view returns (bool); + function pause() external; + function pauseUpkeep(uint256 id) external; + function recoverFunds() external; + function setAdminPrivilegeConfig(address admin, bytes memory newPrivilegeConfig) external; + function setPayees(address[] memory payees) external; + function setPeerRegistryMigrationPermission(address peer, uint8 permission) external; + function setUpkeepCheckData(uint256 id, bytes memory newCheckData) external; + function setUpkeepGasLimit(uint256 id, uint32 gasLimit) external; + function setUpkeepOffchainConfig(uint256 id, bytes memory config) external; + function setUpkeepPrivilegeConfig(uint256 upkeepId, bytes memory newPrivilegeConfig) external; + function transferPayeeship(address transmitter, address proposed) external; + function transferUpkeepAdmin(uint256 id, address proposed) external; + function unpause() external; + function unpauseUpkeep(uint256 id) external; + function upkeepTranscoderVersion() external pure returns (uint8); + function upkeepVersion() external pure returns (uint8); + function withdrawFunds(uint256 id, address to) external; + function withdrawOwnerFunds() external; + function withdrawPayment(address from, address to) external; +} + +interface KeeperRegistryBase2_2 { + struct OnchainConfig { + uint32 paymentPremiumPPB; + uint32 flatFeeMicroLink; + uint32 checkGasLimit; + uint24 stalenessSeconds; + uint16 gasCeilingMultiplier; + uint96 minUpkeepSpend; + uint32 maxPerformGas; + uint32 maxCheckDataSize; + uint32 maxPerformDataSize; + uint32 maxRevertDataSize; + uint256 fallbackGasPrice; + uint256 fallbackLinkPrice; + address transcoder; + address[] registrars; + address upkeepPrivilegeManager; + } + + struct State { + uint32 nonce; + uint96 ownerLinkBalance; + uint256 expectedLinkBalance; + uint96 totalPremium; + uint256 numUpkeeps; + uint32 configCount; + uint32 latestConfigBlockNumber; + bytes32 latestConfigDigest; + uint32 latestEpoch; + bool paused; + } + + struct UpkeepInfo { + address target; + uint32 performGas; + bytes checkData; + uint96 balance; + address admin; + uint64 maxValidBlocknumber; + uint32 lastPerformedBlockNumber; + uint96 amountSpent; + bool paused; + bytes offchainConfig; + } +} + +// THIS FILE WAS AUTOGENERATED FROM THE FOLLOWING ABI JSON: +/* +[{"inputs":[{"internalType":"contract KeeperRegistryLogicB2_2","name":"logicA","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"ArrayHasNoEntries","type":"error"},{"inputs":[],"name":"CannotCancel","type":"error"},{"inputs":[],"name":"CheckDataExceedsLimit","type":"error"},{"inputs":[],"name":"ConfigDigestMismatch","type":"error"},{"inputs":[],"name":"DuplicateEntry","type":"error"},{"inputs":[],"name":"DuplicateSigners","type":"error"},{"inputs":[],"name":"GasLimitCanOnlyIncrease","type":"error"},{"inputs":[],"name":"GasLimitOutsideRange","type":"error"},{"inputs":[],"name":"IncorrectNumberOfFaultyOracles","type":"error"},{"inputs":[],"name":"IncorrectNumberOfSignatures","type":"error"},{"inputs":[],"name":"IncorrectNumberOfSigners","type":"error"},{"inputs":[],"name":"IndexOutOfRange","type":"error"},{"inputs":[],"name":"InsufficientFunds","type":"error"},{"inputs":[],"name":"InvalidDataLength","type":"error"},{"inputs":[],"name":"InvalidPayee","type":"error"},{"inputs":[],"name":"InvalidRecipient","type":"error"},{"inputs":[],"name":"InvalidReport","type":"error"},{"inputs":[],"name":"InvalidSigner","type":"error"},{"inputs":[],"name":"InvalidTransmitter","type":"error"},{"inputs":[],"name":"InvalidTrigger","type":"error"},{"inputs":[],"name":"InvalidTriggerType","type":"error"},{"inputs":[],"name":"MaxCheckDataSizeCanOnlyIncrease","type":"error"},{"inputs":[],"name":"MaxPerformDataSizeCanOnlyIncrease","type":"error"},{"inputs":[],"name":"MigrationNotPermitted","type":"error"},{"inputs":[],"name":"NotAContract","type":"error"},{"inputs":[],"name":"OnlyActiveSigners","type":"error"},{"inputs":[],"name":"OnlyActiveTransmitters","type":"error"},{"inputs":[],"name":"OnlyCallableByAdmin","type":"error"},{"inputs":[],"name":"OnlyCallableByLINKToken","type":"error"},{"inputs":[],"name":"OnlyCallableByOwnerOrAdmin","type":"error"},{"inputs":[],"name":"OnlyCallableByOwnerOrRegistrar","type":"error"},{"inputs":[],"name":"OnlyCallableByPayee","type":"error"},{"inputs":[],"name":"OnlyCallableByProposedAdmin","type":"error"},{"inputs":[],"name":"OnlyCallableByProposedPayee","type":"error"},{"inputs":[],"name":"OnlyCallableByUpkeepPrivilegeManager","type":"error"},{"inputs":[],"name":"OnlyPausedUpkeep","type":"error"},{"inputs":[],"name":"OnlySimulatedBackend","type":"error"},{"inputs":[],"name":"OnlyUnpausedUpkeep","type":"error"},{"inputs":[],"name":"ParameterLengthError","type":"error"},{"inputs":[],"name":"PaymentGreaterThanAllLINK","type":"error"},{"inputs":[],"name":"ReentrantCall","type":"error"},{"inputs":[],"name":"RegistryPaused","type":"error"},{"inputs":[],"name":"RepeatedSigner","type":"error"},{"inputs":[],"name":"RepeatedTransmitter","type":"error"},{"inputs":[{"internalType":"bytes","name":"reason","type":"bytes"}],"name":"TargetCheckReverted","type":"error"},{"inputs":[],"name":"TooManyOracles","type":"error"},{"inputs":[],"name":"TranscoderNotSet","type":"error"},{"inputs":[],"name":"UpkeepAlreadyExists","type":"error"},{"inputs":[],"name":"UpkeepCancelled","type":"error"},{"inputs":[],"name":"UpkeepNotCanceled","type":"error"},{"inputs":[],"name":"UpkeepNotNeeded","type":"error"},{"inputs":[],"name":"ValueNotChanged","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"admin","type":"address"},{"indexed":false,"internalType":"bytes","name":"privilegeConfig","type":"bytes"}],"name":"AdminPrivilegeConfigSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"trigger","type":"bytes"}],"name":"CancelledUpkeepReport","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint32","name":"previousConfigBlockNumber","type":"uint32"},{"indexed":false,"internalType":"bytes32","name":"configDigest","type":"bytes32"},{"indexed":false,"internalType":"uint64","name":"configCount","type":"uint64"},{"indexed":false,"internalType":"address[]","name":"signers","type":"address[]"},{"indexed":false,"internalType":"address[]","name":"transmitters","type":"address[]"},{"indexed":false,"internalType":"uint8","name":"f","type":"uint8"},{"indexed":false,"internalType":"bytes","name":"onchainConfig","type":"bytes"},{"indexed":false,"internalType":"uint64","name":"offchainConfigVersion","type":"uint64"},{"indexed":false,"internalType":"bytes","name":"offchainConfig","type":"bytes"}],"name":"ConfigSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"dedupKey","type":"bytes32"}],"name":"DedupKeyAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":false,"internalType":"uint96","name":"amount","type":"uint96"}],"name":"FundsAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":false,"internalType":"address","name":"to","type":"address"}],"name":"FundsWithdrawn","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"trigger","type":"bytes"}],"name":"InsufficientFundsUpkeepReport","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint96","name":"amount","type":"uint96"}],"name":"OwnerFundsWithdrawn","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":false,"internalType":"address","name":"account","type":"address"}],"name":"Paused","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address[]","name":"transmitters","type":"address[]"},{"indexed":false,"internalType":"address[]","name":"payees","type":"address[]"}],"name":"PayeesUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"transmitter","type":"address"},{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"}],"name":"PayeeshipTransferRequested","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"transmitter","type":"address"},{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"}],"name":"PayeeshipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"transmitter","type":"address"},{"indexed":true,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"address","name":"payee","type":"address"}],"name":"PaymentWithdrawn","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"trigger","type":"bytes"}],"name":"ReorgedUpkeepReport","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"trigger","type":"bytes"}],"name":"StaleUpkeepReport","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes32","name":"configDigest","type":"bytes32"},{"indexed":false,"internalType":"uint32","name":"epoch","type":"uint32"}],"name":"Transmitted","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"account","type":"address"}],"name":"Unpaused","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"}],"name":"UpkeepAdminTransferRequested","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"}],"name":"UpkeepAdminTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":true,"internalType":"uint64","name":"atBlockHeight","type":"uint64"}],"name":"UpkeepCanceled","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"newCheckData","type":"bytes"}],"name":"UpkeepCheckDataSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":false,"internalType":"uint96","name":"gasLimit","type":"uint96"}],"name":"UpkeepGasLimitSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"remainingBalance","type":"uint256"},{"indexed":false,"internalType":"address","name":"destination","type":"address"}],"name":"UpkeepMigrated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"offchainConfig","type":"bytes"}],"name":"UpkeepOffchainConfigSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"}],"name":"UpkeepPaused","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":true,"internalType":"bool","name":"success","type":"bool"},{"indexed":false,"internalType":"uint96","name":"totalPayment","type":"uint96"},{"indexed":false,"internalType":"uint256","name":"gasUsed","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"gasOverhead","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"trigger","type":"bytes"}],"name":"UpkeepPerformed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"privilegeConfig","type":"bytes"}],"name":"UpkeepPrivilegeConfigSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"startingBalance","type":"uint256"},{"indexed":false,"internalType":"address","name":"importedFrom","type":"address"}],"name":"UpkeepReceived","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":false,"internalType":"uint32","name":"performGas","type":"uint32"},{"indexed":false,"internalType":"address","name":"admin","type":"address"}],"name":"UpkeepRegistered","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"triggerConfig","type":"bytes"}],"name":"UpkeepTriggerConfigSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"}],"name":"UpkeepUnpaused","type":"event"},{"stateMutability":"nonpayable","type":"fallback"},{"inputs":[],"name":"acceptOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"fallbackTo","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"latestConfigDetails","outputs":[{"internalType":"uint32","name":"configCount","type":"uint32"},{"internalType":"uint32","name":"blockNumber","type":"uint32"},{"internalType":"bytes32","name":"configDigest","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"latestConfigDigestAndEpoch","outputs":[{"internalType":"bool","name":"scanLogs","type":"bool"},{"internalType":"bytes32","name":"configDigest","type":"bytes32"},{"internalType":"uint32","name":"epoch","type":"uint32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"onTokenTransfer","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address[]","name":"signers","type":"address[]"},{"internalType":"address[]","name":"transmitters","type":"address[]"},{"internalType":"uint8","name":"f","type":"uint8"},{"internalType":"bytes","name":"onchainConfigBytes","type":"bytes"},{"internalType":"uint64","name":"offchainConfigVersion","type":"uint64"},{"internalType":"bytes","name":"offchainConfig","type":"bytes"}],"name":"setConfig","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"signers","type":"address[]"},{"internalType":"address[]","name":"transmitters","type":"address[]"},{"internalType":"uint8","name":"f","type":"uint8"},{"components":[{"internalType":"uint32","name":"paymentPremiumPPB","type":"uint32"},{"internalType":"uint32","name":"flatFeeMicroLink","type":"uint32"},{"internalType":"uint32","name":"checkGasLimit","type":"uint32"},{"internalType":"uint24","name":"stalenessSeconds","type":"uint24"},{"internalType":"uint16","name":"gasCeilingMultiplier","type":"uint16"},{"internalType":"uint96","name":"minUpkeepSpend","type":"uint96"},{"internalType":"uint32","name":"maxPerformGas","type":"uint32"},{"internalType":"uint32","name":"maxCheckDataSize","type":"uint32"},{"internalType":"uint32","name":"maxPerformDataSize","type":"uint32"},{"internalType":"uint32","name":"maxRevertDataSize","type":"uint32"},{"internalType":"uint256","name":"fallbackGasPrice","type":"uint256"},{"internalType":"uint256","name":"fallbackLinkPrice","type":"uint256"},{"internalType":"address","name":"transcoder","type":"address"},{"internalType":"address[]","name":"registrars","type":"address[]"},{"internalType":"address","name":"upkeepPrivilegeManager","type":"address"}],"internalType":"struct KeeperRegistryBase2_2.OnchainConfig","name":"onchainConfig","type":"tuple"},{"internalType":"uint64","name":"offchainConfigVersion","type":"uint64"},{"internalType":"bytes","name":"offchainConfig","type":"bytes"}],"name":"setConfigTypeSafe","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"bytes","name":"performData","type":"bytes"}],"name":"simulatePerformUpkeep","outputs":[{"internalType":"bool","name":"success","type":"bool"},{"internalType":"uint256","name":"gasUsed","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32[3]","name":"reportContext","type":"bytes32[3]"},{"internalType":"bytes","name":"rawReport","type":"bytes"},{"internalType":"bytes32[]","name":"rs","type":"bytes32[]"},{"internalType":"bytes32[]","name":"ss","type":"bytes32[]"},{"internalType":"bytes32","name":"rawVs","type":"bytes32"}],"name":"transmit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"typeAndVersion","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"contract KeeperRegistryLogicB2_2","name":"logicB","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"uint96","name":"amount","type":"uint96"}],"name":"addFunds","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"cancelUpkeep","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"bytes[]","name":"values","type":"bytes[]"},{"internalType":"bytes","name":"extraData","type":"bytes"}],"name":"checkCallback","outputs":[{"internalType":"bool","name":"upkeepNeeded","type":"bool"},{"internalType":"bytes","name":"performData","type":"bytes"},{"internalType":"enum KeeperRegistryBase2_2.UpkeepFailureReason","name":"upkeepFailureReason","type":"uint8"},{"internalType":"uint256","name":"gasUsed","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"bytes","name":"triggerData","type":"bytes"}],"name":"checkUpkeep","outputs":[{"internalType":"bool","name":"upkeepNeeded","type":"bool"},{"internalType":"bytes","name":"performData","type":"bytes"},{"internalType":"enum KeeperRegistryBase2_2.UpkeepFailureReason","name":"upkeepFailureReason","type":"uint8"},{"internalType":"uint256","name":"gasUsed","type":"uint256"},{"internalType":"uint256","name":"gasLimit","type":"uint256"},{"internalType":"uint256","name":"fastGasWei","type":"uint256"},{"internalType":"uint256","name":"linkNative","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"checkUpkeep","outputs":[{"internalType":"bool","name":"upkeepNeeded","type":"bool"},{"internalType":"bytes","name":"performData","type":"bytes"},{"internalType":"enum KeeperRegistryBase2_2.UpkeepFailureReason","name":"upkeepFailureReason","type":"uint8"},{"internalType":"uint256","name":"gasUsed","type":"uint256"},{"internalType":"uint256","name":"gasLimit","type":"uint256"},{"internalType":"uint256","name":"fastGasWei","type":"uint256"},{"internalType":"uint256","name":"linkNative","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"bytes","name":"payload","type":"bytes"}],"name":"executeCallback","outputs":[{"internalType":"bool","name":"upkeepNeeded","type":"bool"},{"internalType":"bytes","name":"performData","type":"bytes"},{"internalType":"enum KeeperRegistryBase2_2.UpkeepFailureReason","name":"upkeepFailureReason","type":"uint8"},{"internalType":"uint256","name":"gasUsed","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"ids","type":"uint256[]"},{"internalType":"address","name":"destination","type":"address"}],"name":"migrateUpkeeps","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes","name":"encodedUpkeeps","type":"bytes"}],"name":"receiveUpkeeps","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"target","type":"address"},{"internalType":"uint32","name":"gasLimit","type":"uint32"},{"internalType":"address","name":"admin","type":"address"},{"internalType":"enum KeeperRegistryBase2_2.Trigger","name":"triggerType","type":"uint8"},{"internalType":"bytes","name":"checkData","type":"bytes"},{"internalType":"bytes","name":"triggerConfig","type":"bytes"},{"internalType":"bytes","name":"offchainConfig","type":"bytes"}],"name":"registerUpkeep","outputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"target","type":"address"},{"internalType":"uint32","name":"gasLimit","type":"uint32"},{"internalType":"address","name":"admin","type":"address"},{"internalType":"bytes","name":"checkData","type":"bytes"},{"internalType":"bytes","name":"offchainConfig","type":"bytes"}],"name":"registerUpkeep","outputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"bytes","name":"triggerConfig","type":"bytes"}],"name":"setUpkeepTriggerConfig","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"enum KeeperRegistryBase2_2.Mode","name":"mode","type":"uint8"},{"internalType":"address","name":"link","type":"address"},{"internalType":"address","name":"linkNativeFeed","type":"address"},{"internalType":"address","name":"fastGasFeed","type":"address"},{"internalType":"address","name":"automationForwarderLogic","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"address","name":"transmitter","type":"address"}],"name":"acceptPayeeship","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"acceptUpkeepAdmin","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"startIndex","type":"uint256"},{"internalType":"uint256","name":"maxCount","type":"uint256"}],"name":"getActiveUpkeepIDs","outputs":[{"internalType":"uint256[]","name":"","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"admin","type":"address"}],"name":"getAdminPrivilegeConfig","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getAutomationForwarderLogic","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"getBalance","outputs":[{"internalType":"uint96","name":"balance","type":"uint96"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCancellationDelay","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"getConditionalGasOverhead","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"getFastGasFeedAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"upkeepID","type":"uint256"}],"name":"getForwarder","outputs":[{"internalType":"contract IAutomationForwarder","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLinkAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLinkNativeFeedAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLogGasOverhead","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"enum KeeperRegistryBase2_2.Trigger","name":"triggerType","type":"uint8"},{"internalType":"uint32","name":"gasLimit","type":"uint32"}],"name":"getMaxPaymentForGas","outputs":[{"internalType":"uint96","name":"maxPayment","type":"uint96"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"getMinBalance","outputs":[{"internalType":"uint96","name":"","type":"uint96"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"getMinBalanceForUpkeep","outputs":[{"internalType":"uint96","name":"minBalance","type":"uint96"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getMode","outputs":[{"internalType":"enum KeeperRegistryBase2_2.Mode","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"peer","type":"address"}],"name":"getPeerRegistryMigrationPermission","outputs":[{"internalType":"enum KeeperRegistryBase2_2.MigrationPermission","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getPerPerformByteGasOverhead","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"getPerSignerGasOverhead","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"query","type":"address"}],"name":"getSignerInfo","outputs":[{"internalType":"bool","name":"active","type":"bool"},{"internalType":"uint8","name":"index","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getState","outputs":[{"components":[{"internalType":"uint32","name":"nonce","type":"uint32"},{"internalType":"uint96","name":"ownerLinkBalance","type":"uint96"},{"internalType":"uint256","name":"expectedLinkBalance","type":"uint256"},{"internalType":"uint96","name":"totalPremium","type":"uint96"},{"internalType":"uint256","name":"numUpkeeps","type":"uint256"},{"internalType":"uint32","name":"configCount","type":"uint32"},{"internalType":"uint32","name":"latestConfigBlockNumber","type":"uint32"},{"internalType":"bytes32","name":"latestConfigDigest","type":"bytes32"},{"internalType":"uint32","name":"latestEpoch","type":"uint32"},{"internalType":"bool","name":"paused","type":"bool"}],"internalType":"struct KeeperRegistryBase2_2.State","name":"state","type":"tuple"},{"components":[{"internalType":"uint32","name":"paymentPremiumPPB","type":"uint32"},{"internalType":"uint32","name":"flatFeeMicroLink","type":"uint32"},{"internalType":"uint32","name":"checkGasLimit","type":"uint32"},{"internalType":"uint24","name":"stalenessSeconds","type":"uint24"},{"internalType":"uint16","name":"gasCeilingMultiplier","type":"uint16"},{"internalType":"uint96","name":"minUpkeepSpend","type":"uint96"},{"internalType":"uint32","name":"maxPerformGas","type":"uint32"},{"internalType":"uint32","name":"maxCheckDataSize","type":"uint32"},{"internalType":"uint32","name":"maxPerformDataSize","type":"uint32"},{"internalType":"uint32","name":"maxRevertDataSize","type":"uint32"},{"internalType":"uint256","name":"fallbackGasPrice","type":"uint256"},{"internalType":"uint256","name":"fallbackLinkPrice","type":"uint256"},{"internalType":"address","name":"transcoder","type":"address"},{"internalType":"address[]","name":"registrars","type":"address[]"},{"internalType":"address","name":"upkeepPrivilegeManager","type":"address"}],"internalType":"struct KeeperRegistryBase2_2.OnchainConfig","name":"config","type":"tuple"},{"internalType":"address[]","name":"signers","type":"address[]"},{"internalType":"address[]","name":"transmitters","type":"address[]"},{"internalType":"uint8","name":"f","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"query","type":"address"}],"name":"getTransmitterInfo","outputs":[{"internalType":"bool","name":"active","type":"bool"},{"internalType":"uint8","name":"index","type":"uint8"},{"internalType":"uint96","name":"balance","type":"uint96"},{"internalType":"uint96","name":"lastCollected","type":"uint96"},{"internalType":"address","name":"payee","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"upkeepId","type":"uint256"}],"name":"getTriggerType","outputs":[{"internalType":"enum KeeperRegistryBase2_2.Trigger","name":"","type":"uint8"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"getUpkeep","outputs":[{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"uint32","name":"performGas","type":"uint32"},{"internalType":"bytes","name":"checkData","type":"bytes"},{"internalType":"uint96","name":"balance","type":"uint96"},{"internalType":"address","name":"admin","type":"address"},{"internalType":"uint64","name":"maxValidBlocknumber","type":"uint64"},{"internalType":"uint32","name":"lastPerformedBlockNumber","type":"uint32"},{"internalType":"uint96","name":"amountSpent","type":"uint96"},{"internalType":"bool","name":"paused","type":"bool"},{"internalType":"bytes","name":"offchainConfig","type":"bytes"}],"internalType":"struct KeeperRegistryBase2_2.UpkeepInfo","name":"upkeepInfo","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"upkeepId","type":"uint256"}],"name":"getUpkeepPrivilegeConfig","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"upkeepId","type":"uint256"}],"name":"getUpkeepTriggerConfig","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"dedupKey","type":"bytes32"}],"name":"hasDedupKey","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"pause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"pauseUpkeep","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"recoverFunds","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"admin","type":"address"},{"internalType":"bytes","name":"newPrivilegeConfig","type":"bytes"}],"name":"setAdminPrivilegeConfig","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"payees","type":"address[]"}],"name":"setPayees","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"peer","type":"address"},{"internalType":"enum KeeperRegistryBase2_2.MigrationPermission","name":"permission","type":"uint8"}],"name":"setPeerRegistryMigrationPermission","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"bytes","name":"newCheckData","type":"bytes"}],"name":"setUpkeepCheckData","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"uint32","name":"gasLimit","type":"uint32"}],"name":"setUpkeepGasLimit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"bytes","name":"config","type":"bytes"}],"name":"setUpkeepOffchainConfig","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"upkeepId","type":"uint256"},{"internalType":"bytes","name":"newPrivilegeConfig","type":"bytes"}],"name":"setUpkeepPrivilegeConfig","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"transmitter","type":"address"},{"internalType":"address","name":"proposed","type":"address"}],"name":"transferPayeeship","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"address","name":"proposed","type":"address"}],"name":"transferUpkeepAdmin","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"unpause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"unpauseUpkeep","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"upkeepTranscoderVersion","outputs":[{"internalType":"enum UpkeepFormat","name":"","type":"uint8"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"upkeepVersion","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"address","name":"to","type":"address"}],"name":"withdrawFunds","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"withdrawOwnerFunds","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"}],"name":"withdrawPayment","outputs":[],"stateMutability":"nonpayable","type":"function"}] +*/ diff --git a/contracts/src/v0.8/automation/dev/v2_2/AutomationRegistrar2_2.sol b/contracts/src/v0.8/automation/dev/v2_2/AutomationRegistrar2_2.sol new file mode 100644 index 00000000000..1fa1feeb578 --- /dev/null +++ b/contracts/src/v0.8/automation/dev/v2_2/AutomationRegistrar2_2.sol @@ -0,0 +1,535 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.16; + +import {LinkTokenInterface} from "../../../shared/interfaces/LinkTokenInterface.sol"; +import {IAutomationRegistryMaster} from "../interfaces/v2_2/IAutomationRegistryMaster.sol"; +import {TypeAndVersionInterface} from "../../../interfaces/TypeAndVersionInterface.sol"; +import {ConfirmedOwner} from "../../../shared/access/ConfirmedOwner.sol"; +import {IERC677Receiver} from "../../../shared/interfaces/IERC677Receiver.sol"; + +/** + * @notice Contract to accept requests for upkeep registrations + * @dev There are 2 registration workflows in this contract + * Flow 1. auto approve OFF / manual registration - UI calls `register` function on this contract, this contract owner at a later time then manually + * calls `approve` to register upkeep and emit events to inform UI and others interested. + * Flow 2. auto approve ON / real time registration - UI calls `register` function as before, which calls the `registerUpkeep` function directly on + * keeper registry and then emits approved event to finish the flow automatically without manual intervention. + * The idea is to have same interface(functions,events) for UI or anyone using this contract irrespective of auto approve being enabled or not. + * they can just listen to `RegistrationRequested` & `RegistrationApproved` events and know the status on registrations. + */ +contract AutomationRegistrar2_2 is TypeAndVersionInterface, ConfirmedOwner, IERC677Receiver { + /** + * DISABLED: No auto approvals, all new upkeeps should be approved manually. + * ENABLED_SENDER_ALLOWLIST: Auto approvals for allowed senders subject to max allowed. Manual for rest. + * ENABLED_ALL: Auto approvals for all new upkeeps subject to max allowed. + */ + enum AutoApproveType { + DISABLED, + ENABLED_SENDER_ALLOWLIST, + ENABLED_ALL + } + + bytes4 private constant REGISTER_REQUEST_SELECTOR = this.register.selector; + + mapping(bytes32 => PendingRequest) private s_pendingRequests; + mapping(uint8 => TriggerRegistrationStorage) private s_triggerRegistrations; + + LinkTokenInterface public immutable LINK; + + /** + * @notice versions: + * - KeeperRegistrar 2.1.0: Update for compatability with registry 2.1.0 + * Add auto approval levels by type + * - KeeperRegistrar 2.0.0: Remove source from register + * Breaks our example of "Register an Upkeep using your own deployed contract" + * - KeeperRegistrar 1.1.0: Add functionality for sender allowlist in auto approve + * : Remove rate limit and add max allowed for auto approve + * - KeeperRegistrar 1.0.0: initial release + */ + string public constant override typeAndVersion = "AutomationRegistrar 2.1.0"; + + /** + * @notice TriggerRegistrationStorage stores the auto-approval levels for upkeeps by type + * @member autoApproveType the auto approval setting (see enum) + * @member autoApproveMaxAllowed the max number of upkeeps that can be auto approved of this type + * @member approvedCount the count of upkeeps auto approved of this type + */ + struct TriggerRegistrationStorage { + AutoApproveType autoApproveType; + uint32 autoApproveMaxAllowed; + uint32 approvedCount; + } + + /** + * @notice InitialTriggerConfig configures the auto-approval levels for upkeeps by trigger type + * @dev this struct is only used in the constructor to set the initial values for various trigger configs + * @member triggerType the upkeep type to configure + * @member autoApproveType the auto approval setting (see enum) + * @member autoApproveMaxAllowed the max number of upkeeps that can be auto approved of this type + */ + struct InitialTriggerConfig { + uint8 triggerType; + AutoApproveType autoApproveType; + uint32 autoApproveMaxAllowed; + } + + struct RegistrarConfig { + IAutomationRegistryMaster AutomationRegistry; + uint96 minLINKJuels; + } + + struct PendingRequest { + address admin; + uint96 balance; + } + + struct RegistrationParams { + string name; + bytes encryptedEmail; + address upkeepContract; + uint32 gasLimit; + address adminAddress; + uint8 triggerType; + bytes checkData; + bytes triggerConfig; + bytes offchainConfig; + uint96 amount; + } + + RegistrarConfig private s_config; + // Only applicable if s_config.configType is ENABLED_SENDER_ALLOWLIST + mapping(address => bool) private s_autoApproveAllowedSenders; + + event RegistrationRequested( + bytes32 indexed hash, + string name, + bytes encryptedEmail, + address indexed upkeepContract, + uint32 gasLimit, + address adminAddress, + uint8 triggerType, + bytes triggerConfig, + bytes offchainConfig, + bytes checkData, + uint96 amount + ); + + event RegistrationApproved(bytes32 indexed hash, string displayName, uint256 indexed upkeepId); + + event RegistrationRejected(bytes32 indexed hash); + + event AutoApproveAllowedSenderSet(address indexed senderAddress, bool allowed); + + event ConfigChanged(address AutomationRegistry, uint96 minLINKJuels); + + event TriggerConfigSet(uint8 triggerType, AutoApproveType autoApproveType, uint32 autoApproveMaxAllowed); + + error InvalidAdminAddress(); + error RequestNotFound(); + error HashMismatch(); + error OnlyAdminOrOwner(); + error InsufficientPayment(); + error RegistrationRequestFailed(); + error OnlyLink(); + error AmountMismatch(); + error SenderMismatch(); + error FunctionNotPermitted(); + error LinkTransferFailed(address to); + error InvalidDataLength(); + + /** + * @param LINKAddress Address of Link token + * @param AutomationRegistry keeper registry address + * @param minLINKJuels minimum LINK that new registrations should fund their upkeep with + * @param triggerConfigs the initial config for individual triggers + */ + constructor( + address LINKAddress, + address AutomationRegistry, + uint96 minLINKJuels, + InitialTriggerConfig[] memory triggerConfigs + ) ConfirmedOwner(msg.sender) { + LINK = LinkTokenInterface(LINKAddress); + setConfig(AutomationRegistry, minLINKJuels); + for (uint256 idx = 0; idx < triggerConfigs.length; idx++) { + setTriggerConfig( + triggerConfigs[idx].triggerType, + triggerConfigs[idx].autoApproveType, + triggerConfigs[idx].autoApproveMaxAllowed + ); + } + } + + //EXTERNAL + + /** + * @notice register can only be called through transferAndCall on LINK contract + * @param name string of the upkeep to be registered + * @param encryptedEmail email address of upkeep contact + * @param upkeepContract address to perform upkeep on + * @param gasLimit amount of gas to provide the target contract when performing upkeep + * @param adminAddress address to cancel upkeep and withdraw remaining funds + * @param triggerType the type of trigger for the upkeep + * @param checkData data passed to the contract when checking for upkeep + * @param triggerConfig the config for the trigger + * @param offchainConfig offchainConfig for upkeep in bytes + * @param amount quantity of LINK upkeep is funded with (specified in Juels) + * @param sender address of the sender making the request + */ + function register( + string memory name, + bytes calldata encryptedEmail, + address upkeepContract, + uint32 gasLimit, + address adminAddress, + uint8 triggerType, + bytes memory checkData, + bytes memory triggerConfig, + bytes memory offchainConfig, + uint96 amount, + address sender + ) external onlyLINK { + _register( + RegistrationParams({ + name: name, + encryptedEmail: encryptedEmail, + upkeepContract: upkeepContract, + gasLimit: gasLimit, + adminAddress: adminAddress, + triggerType: triggerType, + checkData: checkData, + triggerConfig: triggerConfig, + offchainConfig: offchainConfig, + amount: amount + }), + sender + ); + } + + /** + * @notice Allows external users to register upkeeps; assumes amount is approved for transfer by the contract + * @param requestParams struct of all possible registration parameters + */ + function registerUpkeep(RegistrationParams calldata requestParams) external returns (uint256) { + if (requestParams.amount < s_config.minLINKJuels) { + revert InsufficientPayment(); + } + + LINK.transferFrom(msg.sender, address(this), requestParams.amount); + + return _register(requestParams, msg.sender); + } + + /** + * @dev register upkeep on AutomationRegistry contract and emit RegistrationApproved event + */ + function approve( + string memory name, + address upkeepContract, + uint32 gasLimit, + address adminAddress, + uint8 triggerType, + bytes calldata checkData, + bytes memory triggerConfig, + bytes calldata offchainConfig, + bytes32 hash + ) external onlyOwner { + PendingRequest memory request = s_pendingRequests[hash]; + if (request.admin == address(0)) { + revert RequestNotFound(); + } + bytes32 expectedHash = keccak256( + abi.encode(upkeepContract, gasLimit, adminAddress, triggerType, checkData, triggerConfig, offchainConfig) + ); + if (hash != expectedHash) { + revert HashMismatch(); + } + delete s_pendingRequests[hash]; + _approve( + RegistrationParams({ + name: name, + encryptedEmail: "", + upkeepContract: upkeepContract, + gasLimit: gasLimit, + adminAddress: adminAddress, + triggerType: triggerType, + checkData: checkData, + triggerConfig: triggerConfig, + offchainConfig: offchainConfig, + amount: request.balance + }), + expectedHash + ); + } + + /** + * @notice cancel will remove a registration request and return the refunds to the request.admin + * @param hash the request hash + */ + function cancel(bytes32 hash) external { + PendingRequest memory request = s_pendingRequests[hash]; + if (!(msg.sender == request.admin || msg.sender == owner())) { + revert OnlyAdminOrOwner(); + } + if (request.admin == address(0)) { + revert RequestNotFound(); + } + delete s_pendingRequests[hash]; + bool success = LINK.transfer(request.admin, request.balance); + if (!success) { + revert LinkTransferFailed(request.admin); + } + emit RegistrationRejected(hash); + } + + /** + * @notice owner calls this function to set contract config + * @param AutomationRegistry new keeper registry address + * @param minLINKJuels minimum LINK that new registrations should fund their upkeep with + */ + function setConfig(address AutomationRegistry, uint96 minLINKJuels) public onlyOwner { + s_config = RegistrarConfig({ + minLINKJuels: minLINKJuels, + AutomationRegistry: IAutomationRegistryMaster(AutomationRegistry) + }); + emit ConfigChanged(AutomationRegistry, minLINKJuels); + } + + /** + * @notice owner calls to set the config for this upkeep type + * @param triggerType the upkeep type to configure + * @param autoApproveType the auto approval setting (see enum) + * @param autoApproveMaxAllowed the max number of upkeeps that can be auto approved of this type + */ + function setTriggerConfig( + uint8 triggerType, + AutoApproveType autoApproveType, + uint32 autoApproveMaxAllowed + ) public onlyOwner { + s_triggerRegistrations[triggerType].autoApproveType = autoApproveType; + s_triggerRegistrations[triggerType].autoApproveMaxAllowed = autoApproveMaxAllowed; + emit TriggerConfigSet(triggerType, autoApproveType, autoApproveMaxAllowed); + } + + /** + * @notice owner calls this function to set allowlist status for senderAddress + * @param senderAddress senderAddress to set the allowlist status for + * @param allowed true if senderAddress needs to be added to allowlist, false if needs to be removed + */ + function setAutoApproveAllowedSender(address senderAddress, bool allowed) external onlyOwner { + s_autoApproveAllowedSenders[senderAddress] = allowed; + + emit AutoApproveAllowedSenderSet(senderAddress, allowed); + } + + /** + * @notice read the allowlist status of senderAddress + * @param senderAddress address to read the allowlist status for + */ + function getAutoApproveAllowedSender(address senderAddress) external view returns (bool) { + return s_autoApproveAllowedSenders[senderAddress]; + } + + /** + * @notice read the current registration configuration + */ + function getConfig() external view returns (address AutomationRegistry, uint256 minLINKJuels) { + RegistrarConfig memory config = s_config; + return (address(config.AutomationRegistry), config.minLINKJuels); + } + + /** + * @notice read the config for this upkeep type + * @param triggerType upkeep type to read config for + */ + function getTriggerRegistrationDetails(uint8 triggerType) external view returns (TriggerRegistrationStorage memory) { + return s_triggerRegistrations[triggerType]; + } + + /** + * @notice gets the admin address and the current balance of a registration request + */ + function getPendingRequest(bytes32 hash) external view returns (address, uint96) { + PendingRequest memory request = s_pendingRequests[hash]; + return (request.admin, request.balance); + } + + /** + * @notice Called when LINK is sent to the contract via `transferAndCall` + * @param sender Address of the sender transfering LINK + * @param amount Amount of LINK sent (specified in Juels) + * @param data Payload of the transaction + */ + function onTokenTransfer( + address sender, + uint256 amount, + bytes calldata data + ) + external + override + onlyLINK + permittedFunctionsForLINK(data) + isActualAmount(amount, data) + isActualSender(sender, data) + { + if (amount < s_config.minLINKJuels) { + revert InsufficientPayment(); + } + (bool success, ) = address(this).delegatecall(data); + // calls register + if (!success) { + revert RegistrationRequestFailed(); + } + } + + // ================================================================ + // | PRIVATE | + // ================================================================ + + /** + * @dev verify registration request and emit RegistrationRequested event + */ + function _register(RegistrationParams memory params, address sender) private returns (uint256) { + if (params.adminAddress == address(0)) { + revert InvalidAdminAddress(); + } + bytes32 hash = keccak256( + abi.encode( + params.upkeepContract, + params.gasLimit, + params.adminAddress, + params.triggerType, + params.checkData, + params.triggerConfig, + params.offchainConfig + ) + ); + + emit RegistrationRequested( + hash, + params.name, + params.encryptedEmail, + params.upkeepContract, + params.gasLimit, + params.adminAddress, + params.triggerType, + params.triggerConfig, + params.offchainConfig, + params.checkData, + params.amount + ); + + uint256 upkeepId; + if (_shouldAutoApprove(s_triggerRegistrations[params.triggerType], sender)) { + s_triggerRegistrations[params.triggerType].approvedCount++; + upkeepId = _approve(params, hash); + } else { + uint96 newBalance = s_pendingRequests[hash].balance + params.amount; + s_pendingRequests[hash] = PendingRequest({admin: params.adminAddress, balance: newBalance}); + } + + return upkeepId; + } + + /** + * @dev register upkeep on AutomationRegistry contract and emit RegistrationApproved event + */ + function _approve(RegistrationParams memory params, bytes32 hash) private returns (uint256) { + IAutomationRegistryMaster AutomationRegistry = s_config.AutomationRegistry; + uint256 upkeepId = AutomationRegistry.registerUpkeep( + params.upkeepContract, + params.gasLimit, + params.adminAddress, + params.triggerType, + params.checkData, + params.triggerConfig, + params.offchainConfig + ); + bool success = LINK.transferAndCall(address(AutomationRegistry), params.amount, abi.encode(upkeepId)); + if (!success) { + revert LinkTransferFailed(address(AutomationRegistry)); + } + emit RegistrationApproved(hash, params.name, upkeepId); + return upkeepId; + } + + /** + * @dev verify sender allowlist if needed and check max limit + */ + function _shouldAutoApprove(TriggerRegistrationStorage memory config, address sender) private view returns (bool) { + if (config.autoApproveType == AutoApproveType.DISABLED) { + return false; + } + if (config.autoApproveType == AutoApproveType.ENABLED_SENDER_ALLOWLIST && (!s_autoApproveAllowedSenders[sender])) { + return false; + } + if (config.approvedCount < config.autoApproveMaxAllowed) { + return true; + } + return false; + } + + // ================================================================ + // | MODIFIERS | + // ================================================================ + + /** + * @dev Reverts if not sent from the LINK token + */ + modifier onlyLINK() { + if (msg.sender != address(LINK)) { + revert OnlyLink(); + } + _; + } + + /** + * @dev Reverts if the given data does not begin with the `register` function selector + * @param _data The data payload of the request + */ + modifier permittedFunctionsForLINK(bytes memory _data) { + bytes4 funcSelector; + assembly { + // solhint-disable-next-line avoid-low-level-calls + funcSelector := mload(add(_data, 32)) // First 32 bytes contain length of data + } + if (funcSelector != REGISTER_REQUEST_SELECTOR) { + revert FunctionNotPermitted(); + } + _; + } + + /** + * @dev Reverts if the actual amount passed does not match the expected amount + * @param expected amount that should match the actual amount + * @param data bytes + */ + modifier isActualAmount(uint256 expected, bytes calldata data) { + // decode register function arguments to get actual amount + (, , , , , , , , , uint96 amount, ) = abi.decode( + data[4:], + (string, bytes, address, uint32, address, uint8, bytes, bytes, bytes, uint96, address) + ); + if (expected != amount) { + revert AmountMismatch(); + } + _; + } + + /** + * @dev Reverts if the actual sender address does not match the expected sender address + * @param expected address that should match the actual sender address + * @param data bytes + */ + modifier isActualSender(address expected, bytes calldata data) { + // decode register function arguments to get actual sender + (, , , , , , , , , , address sender) = abi.decode( + data[4:], + (string, bytes, address, uint32, address, uint8, bytes, bytes, bytes, uint96, address) + ); + if (expected != sender) { + revert SenderMismatch(); + } + _; + } +} diff --git a/contracts/src/v0.8/automation/dev/v2_2/AutomationRegistry2_2.sol b/contracts/src/v0.8/automation/dev/v2_2/AutomationRegistry2_2.sol new file mode 100644 index 00000000000..e21387936e0 --- /dev/null +++ b/contracts/src/v0.8/automation/dev/v2_2/AutomationRegistry2_2.sol @@ -0,0 +1,397 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.16; + +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 {AutomationRegistryBase2_2} from "./AutomationRegistryBase2_2.sol"; +import {AutomationRegistryLogicB2_2} from "./AutomationRegistryLogicB2_2.sol"; +import {Chainable} from "../../Chainable.sol"; +import {IERC677Receiver} from "../../../shared/interfaces/IERC677Receiver.sol"; +import {OCR2Abstract} from "../../../shared/ocr2/OCR2Abstract.sol"; + +/** + * @notice Registry for adding work for Chainlink nodes to perform on client + * contracts. Clients must support the AutomationCompatibleInterface interface. + */ +contract AutomationRegistry2_2 is AutomationRegistryBase2_2, OCR2Abstract, Chainable, IERC677Receiver { + using Address for address; + using EnumerableSet for EnumerableSet.UintSet; + using EnumerableSet for EnumerableSet.AddressSet; + + /** + * @notice versions: + * AutomationRegistry 2.2.0: moves chain-spicific 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.2.0"; + + /** + * @param logicA the address of the first logic contract, but cast as logicB in order to call logicB functions + */ + constructor( + AutomationRegistryLogicB2_2 logicA + ) + AutomationRegistryBase2_2( + logicA.getMode(), + logicA.getLinkAddress(), + logicA.getLinkNativeFeedAddress(), + logicA.getFastGasFeedAddress(), + logicA.getAutomationForwarderLogic() + ) + Chainable(address(logicA)) + {} + + // ================================================================ + // | 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(); + 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); + UpkeepTransmitInfo[] memory upkeepTransmitInfo = new UpkeepTransmitInfo[](report.upkeepIds.length); + uint16 numUpkeepsPassedChecks; + + 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].maxLinkPayment = _getMaxLinkPayment( + hotVars, + upkeepTransmitInfo[i].triggerType, + uint32(report.gasLimits[i]), + uint32(report.performDatas[i].length), + report.fastGasWei, + report.linkNative, + true + ); + (upkeepTransmitInfo[i].earlyChecksPassed, upkeepTransmitInfo[i].dedupID) = _prePerformChecks( + report.upkeepIds[i], + report.triggers[i], + upkeepTransmitInfo[i] + ); + + if (upkeepTransmitInfo[i].earlyChecksPassed) { + 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] + ); + + // Deduct that gasUsed by upkeep from our running counter + gasOverhead -= upkeepTransmitInfo[i].gasUsed; + + // Store last perform block number / deduping key for upkeep + _updateTriggerMarker(report.upkeepIds[i], upkeepTransmitInfo[i]); + } + // No upkeeps to be performed in this report + if (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, which is approximated to be reportLength + // Rest of msg.data is accounted for in accounting overheads + gasOverhead = + (gasOverhead - gasleft() + 16 * rawReport.length) + + ACCOUNTING_FIXED_GAS_OVERHEAD + + (ACCOUNTING_PER_SIGNER_GAS_OVERHEAD * (hotVars.f + 1)); + gasOverhead = gasOverhead / numUpkeepsPassedChecks + ACCOUNTING_PER_UPKEEP_GAS_OVERHEAD; + + uint96 totalReimbursement; + uint96 totalPremium; + { + uint96 reimbursement; + uint96 premium; + for (uint256 i = 0; i < report.upkeepIds.length; i++) { + if (upkeepTransmitInfo[i].earlyChecksPassed) { + upkeepTransmitInfo[i].gasOverhead = _getCappedGasOverhead( + gasOverhead, + upkeepTransmitInfo[i].triggerType, + uint32(report.performDatas[i].length), + hotVars.f + ); + + (reimbursement, premium) = _postPerformPayment( + hotVars, + report.upkeepIds[i], + upkeepTransmitInfo[i], + report.fastGasWei, + report.linkNative, + numUpkeepsPassedChecks + ); + totalPremium += premium; + totalReimbursement += reimbursement; + + emit UpkeepPerformed( + report.upkeepIds[i], + upkeepTransmitInfo[i].performSuccess, + reimbursement + premium, + upkeepTransmitInfo[i].gasUsed, + upkeepTransmitInfo[i].gasOverhead, + report.triggers[i] + ); + } + } + } + // record payments + s_transmitters[msg.sender].balance += totalReimbursement; + s_hotVars.totalPremium += totalPremium; + + uint40 epochAndRound = uint40(uint256(reportContext[1])); + uint32 epoch = uint32(epochAndRound >> 8); + if (epoch > hotVars.latestEpoch) { + s_hotVars.latestEpoch = epoch; + } + } + + /** + * @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 cannotExecute returns (bool success, uint256 gasUsed) { + if (s_hotVars.paused) revert RegistryPaused(); + Upkeep memory upkeep = s_upkeep[id]; + (success, gasUsed) = _performUpkeep(upkeep.forwarder, upkeep.performGas, performData); + return (success, gasUsed); + } + + /** + * @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(); + s_upkeep[id].balance = s_upkeep[id].balance + uint96(amount); + s_expectedLinkBalance = s_expectedLinkBalance + amount; + emit FundsAdded(id, sender, uint96(amount)); + } + + // ================================================================ + // | SETTERS | + // ================================================================ + + /** + * @inheritdoc OCR2Abstract + * @dev prefer the type-safe version of setConfig (below) whenever possible + */ + function setConfig( + address[] memory signers, + address[] memory transmitters, + uint8 f, + bytes memory onchainConfigBytes, + uint64 offchainConfigVersion, + bytes memory offchainConfig + ) external override { + setConfigTypeSafe( + signers, + transmitters, + f, + abi.decode(onchainConfigBytes, (OnchainConfig)), + offchainConfigVersion, + offchainConfig + ); + } + + function setConfigTypeSafe( + address[] memory signers, + address[] memory transmitters, + uint8 f, + OnchainConfig memory onchainConfig, + uint64 offchainConfigVersion, + bytes memory offchainConfig + ) 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(); + + // move all pooled payments out of the pool to each transmitter's balance + uint96 totalPremium = s_hotVars.totalPremium; + uint96 oldLength = uint96(s_transmittersList.length); + for (uint256 i = 0; i < oldLength; i++) { + _updateTransmitterBalanceFromPool(s_transmittersList[i], totalPremium, oldLength); + } + + // remove any old signer/transmitter addresses + address signerAddress; + address transmitterAddress; + for (uint256 i = 0; i < oldLength; i++) { + signerAddress = s_signersList[i]; + transmitterAddress = s_transmittersList[i]; + delete s_signers[signerAddress]; + // Do not delete the whole transmitter struct as it has balance information stored + s_transmitters[transmitterAddress].active = false; + } + delete s_signersList; + delete s_transmittersList; + + // add new signer/transmitter addresses + { + Transmitter memory transmitter; + address temp; + 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)}); + + temp = transmitters[i]; + if (temp == ZERO_ADDRESS) revert InvalidTransmitter(); + transmitter = s_transmitters[temp]; + 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 = totalPremium; + s_transmitters[temp] = transmitter; + } + } + s_signersList = signers; + s_transmittersList = transmitters; + + s_hotVars = HotVars({ + f: f, + paymentPremiumPPB: onchainConfig.paymentPremiumPPB, + flatFeeMicroLink: onchainConfig.flatFeeMicroLink, + stalenessSeconds: onchainConfig.stalenessSeconds, + gasCeilingMultiplier: onchainConfig.gasCeilingMultiplier, + paused: s_hotVars.paused, + reentrancyGuard: s_hotVars.reentrancyGuard, + totalPremium: totalPremium, + latestEpoch: 0 // DON restarts epoch + }); + + s_storage = Storage({ + checkGasLimit: onchainConfig.checkGasLimit, + minUpkeepSpend: onchainConfig.minUpkeepSpend, + maxPerformGas: onchainConfig.maxPerformGas, + transcoder: onchainConfig.transcoder, + maxCheckDataSize: onchainConfig.maxCheckDataSize, + maxPerformDataSize: onchainConfig.maxPerformDataSize, + maxRevertDataSize: onchainConfig.maxRevertDataSize, + upkeepPrivilegeManager: onchainConfig.upkeepPrivilegeManager, + nonce: s_storage.nonce, + configCount: s_storage.configCount, + latestConfigBlockNumber: s_storage.latestConfigBlockNumber, + ownerLinkBalance: s_storage.ownerLinkBalance + }); + s_fallbackGasPrice = onchainConfig.fallbackGasPrice; + s_fallbackLinkPrice = onchainConfig.fallbackLinkPrice; + + uint32 previousConfigBlockNumber = s_storage.latestConfigBlockNumber; + s_storage.latestConfigBlockNumber = uint32(_blockNum()); + s_storage.configCount += 1; + + 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 = 0; idx < s_registrars.length(); idx++) { + s_registrars.remove(s_registrars.at(idx)); + } + + 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 + ); + } + + // ================================================================ + // | GETTERS | + // ================================================================ + + /** + * @inheritdoc OCR2Abstract + */ + function latestConfigDetails() + external + view + override + returns (uint32 configCount, uint32 blockNumber, bytes32 configDigest) + { + return (s_storage.configCount, s_storage.latestConfigBlockNumber, s_latestConfigDigest); + } + + /** + * @inheritdoc OCR2Abstract + */ + 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/dev/v2_2/AutomationRegistryBase2_2.sol b/contracts/src/v0.8/automation/dev/v2_2/AutomationRegistryBase2_2.sol new file mode 100644 index 00000000000..b3e970e6c35 --- /dev/null +++ b/contracts/src/v0.8/automation/dev/v2_2/AutomationRegistryBase2_2.sol @@ -0,0 +1,959 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.16; + +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 {ArbGasInfo} from "../../../vendor/@arbitrum/nitro-contracts/src/precompiles/ArbGasInfo.sol"; +import {OVM_GasPriceOracle} from "../../../vendor/@eth-optimism/contracts/v0.8.9/contracts/L2/predeploys/OVM_GasPriceOracle.sol"; +import {ExecutionPrevention} from "../../ExecutionPrevention.sol"; +import {ArbSys} from "../../../vendor/@arbitrum/nitro-contracts/src/precompiles/ArbSys.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 {UpkeepFormat} from "../../interfaces/UpkeepTranscoderInterface.sol"; + +/** + * @notice Base Keeper Registry contract, contains shared logic between + * AutomationRegistry and AutomationRegistryLogic + * @dev all errors, events, and internal functions should live here + */ +abstract contract AutomationRegistryBase2_2 is ConfirmedOwner, ExecutionPrevention { + 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; + uint96 internal constant LINK_TOTAL_SUPPLY = 1e27; + // The first byte of the mask can be 0, because we only ever have 31 oracles + uint256 internal constant ORACLE_MASK = 0x0001010101010101010101010101010101010101010101010101010101010101; + /** + * @dev UPKEEP_TRANSCODER_VERSION_BASE is temporary necessity for backwards compatibility with + * MigratableAutomationRegistryInterfaceV1 - it should be removed in future versions in favor of + * UPKEEP_VERSION_BASE and MigratableAutomationRegistryInterfaceV2 + */ + UpkeepFormat internal constant UPKEEP_TRANSCODER_VERSION_BASE = UpkeepFormat.V1; + uint8 internal constant UPKEEP_VERSION_BASE = 3; + // L1_FEE_DATA_PADDING includes 35 bytes for L1 data padding for Optimism + bytes internal constant L1_FEE_DATA_PADDING = + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; + + uint256 internal constant REGISTRY_CONDITIONAL_OVERHEAD = 90_000; // Used in maxPayment estimation, and in capping overheads during actual payment + uint256 internal constant REGISTRY_LOG_OVERHEAD = 110_000; // Used only in maxPayment estimation, and in capping overheads during actual payment. + uint256 internal constant REGISTRY_PER_PERFORM_BYTE_GAS_OVERHEAD = 20; // Used only in maxPayment estimation, and in capping overheads during actual payment. Value scales with performData length. + uint256 internal constant REGISTRY_PER_SIGNER_GAS_OVERHEAD = 7_500; // Used only in maxPayment estimation, and in capping overheads during actual payment. Value scales with f. + + uint256 internal constant ACCOUNTING_FIXED_GAS_OVERHEAD = 27_500; // Used in actual payment. Fixed overhead per tx + uint256 internal constant ACCOUNTING_PER_SIGNER_GAS_OVERHEAD = 1_100; // Used in actual payment. overhead per signer + uint256 internal constant ACCOUNTING_PER_UPKEEP_GAS_OVERHEAD = 7_000; // Used in actual payment. overhead per upkeep performed + + OVM_GasPriceOracle internal constant OPTIMISM_ORACLE = OVM_GasPriceOracle(0x420000000000000000000000000000000000000F); + ArbGasInfo internal constant ARB_NITRO_ORACLE = ArbGasInfo(0x000000000000000000000000000000000000006C); + ArbSys internal constant ARB_SYS = ArbSys(0x0000000000000000000000000000000000000064); + + LinkTokenInterface internal immutable i_link; + AggregatorV3Interface internal immutable i_linkNativeFeed; + AggregatorV3Interface internal immutable i_fastGasFeed; + Mode internal immutable i_mode; + address internal immutable i_automationForwarderLogic; + + /** + * @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 + 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_expectedLinkBalance; // Used in case of erroneous LINK transfers to contract + 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 + + 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 InsufficientFunds(); + error InvalidDataLength(); + error InvalidTrigger(); + error InvalidPayee(); + error InvalidRecipient(); + error InvalidReport(); + error InvalidSigner(); + error InvalidTransmitter(); + error InvalidTriggerType(); + error MaxCheckDataSizeCanOnlyIncrease(); + error MaxPerformDataSizeCanOnlyIncrease(); + error MigrationNotPermitted(); + error NotAContract(); + error OnlyActiveSigners(); + error OnlyActiveTransmitters(); + error OnlyCallableByAdmin(); + error OnlyCallableByLINKToken(); + error OnlyCallableByOwnerOrAdmin(); + error OnlyCallableByOwnerOrRegistrar(); + error OnlyCallableByPayee(); + error OnlyCallableByProposedAdmin(); + error OnlyCallableByProposedPayee(); + error OnlyCallableByUpkeepPrivilegeManager(); + error OnlyPausedUpkeep(); + error OnlyUnpausedUpkeep(); + error ParameterLengthError(); + error PaymentGreaterThanAllLINK(); + error ReentrantCall(); + error RegistryPaused(); + error RepeatedSigner(); + error RepeatedTransmitter(); + error TargetCheckReverted(bytes reason); + error TooManyOracles(); + error TranscoderNotSet(); + error UpkeepAlreadyExists(); + error UpkeepCancelled(); + error UpkeepNotCanceled(); + error UpkeepNotNeeded(); + error ValueNotChanged(); + + enum MigrationPermission { + NONE, + OUTGOING, + INCOMING, + BIDIRECTIONAL + } + + enum Mode { + DEFAULT, + ARBITRUM, + OPTIMISM + } + + 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 + } + + /** + * @notice OnchainConfig of the registry + * @dev only used in params and return values + * @member paymentPremiumPPB payment premium rate oracles receive on top of + * being reimbursed for gas, measured in parts per billion + * @member flatFeeMicroLink flat fee paid to oracles for performing upkeeps, + * priced in MicroLink; can be used in conjunction with or independently of + * paymentPremiumPPB + * @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 minUpkeepSpend minimum LINK that an upkeep must spend before cancelling + * @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 + */ + struct OnchainConfig { + uint32 paymentPremiumPPB; + uint32 flatFeeMicroLink; // min 0.000001 LINK, max 4294 LINK + uint32 checkGasLimit; + uint24 stalenessSeconds; + uint16 gasCeilingMultiplier; + uint96 minUpkeepSpend; + uint32 maxPerformGas; + uint32 maxCheckDataSize; + uint32 maxPerformDataSize; + uint32 maxRevertDataSize; + uint256 fallbackGasPrice; + uint256 fallbackLinkPrice; + address transcoder; + address[] registrars; + address upkeepPrivilegeManager; + } + + /** + * @notice state of the registry + * @dev only used in params and return values + * @dev this will likely be deprecated in a future version of the registry in favor of individual getters + * @member nonce used for ID generation + * @member ownerLinkBalance withdrawable balance of LINK by contract owner + * @member expectedLinkBalance the expected balance of LINK of the registry + * @member totalPremium the total premium collected on registry so far + * @member numUpkeeps total number of upkeeps on the registry + * @member configCount ordinal number of current config, out of all configs applied to this contract so far + * @member latestConfigBlockNumber last block at which this config was set + * @member latestConfigDigest domain-separation tag for current config + * @member latestEpoch for which a report was transmitted + * @member paused freeze on execution scoped to the entire registry + */ + struct State { + uint32 nonce; + uint96 ownerLinkBalance; + uint256 expectedLinkBalance; + uint96 totalPremium; + uint256 numUpkeeps; + uint32 configCount; + uint32 latestConfigBlockNumber; + bytes32 latestConfigDigest; + uint32 latestEpoch; + bool paused; + } + + /** + * @notice relevant state of an upkeep which is used in transmit function + * @member paused if this upkeep has been paused + * @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 + * @member balance the balance of this upkeep + * @member lastPerformedBlockNumber the last block number when this upkeep was performed + */ + struct Upkeep { + bool paused; + uint32 performGas; + uint32 maxValidBlocknumber; + IAutomationForwarder forwarder; + // 0 bytes left in 1st EVM word - not written to in transmit + uint96 amountSpent; + uint96 balance; + uint32 lastPerformedBlockNumber; + // 2 bytes left in 2nd EVM word - written in transmit path + } + + /** + * @notice all information about an upkeep + * @dev only used in return values + * @dev this will likely be deprecated in a future version of the registry + * @member target the contract which needs to be serviced + * @member performGas the gas limit of upkeep execution + * @member checkData the checkData bytes for this upkeep + * @member balance the balance of this upkeep + * @member admin for this upkeep + * @member maxValidBlocknumber until which block this upkeep is valid + * @member lastPerformedBlockNumber the last block number when this upkeep was performed + * @member amountSpent the amount this upkeep has spent + * @member paused if this upkeep has been paused + * @member offchainConfig the off-chain config of this upkeep + */ + struct UpkeepInfo { + address target; + uint32 performGas; + bytes checkData; + uint96 balance; + address admin; + uint64 maxValidBlocknumber; + uint32 lastPerformedBlockNumber; + uint96 amountSpent; + bool paused; + bytes offchainConfig; + } + + /// @dev Config + State storage struct which is on hot transmit path + struct HotVars { + uint8 f; // maximum number of faulty oracles + uint32 paymentPremiumPPB; // premium percentage charged to user over tx cost + uint32 flatFeeMicroLink; // flat fee charged to user for every perform + uint24 stalenessSeconds; // Staleness tolerance for feeds + uint16 gasCeilingMultiplier; // multiplier on top of fast gas feed for upper bound + bool paused; // pause switch for all upkeeps in the registry + bool reentrancyGuard; // guard against reentrancy + uint96 totalPremium; // total historical payment to oracles for premium + uint32 latestEpoch; // latest epoch for which a report was transmitted + // 1 EVM word full + } + + /// @dev Config + State storage struct which is not on hot transmit path + struct Storage { + uint96 minUpkeepSpend; // Minimum amount an upkeep must spend + address transcoder; // Address of transcoder contract used in migrations + // 1 EVM word full + uint96 ownerLinkBalance; // Balance of owner, accumulates minUpkeepSpend in case it is not spent + 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 + 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 + // 2 EVM word full + uint32 maxCheckDataSize; // max length of checkData bytes + uint32 maxPerformDataSize; // max length of performData bytes + uint32 maxRevertDataSize; // max length of revertData bytes + address upkeepPrivilegeManager; // address which can set privilege for upkeeps + // 3 EVM word full + } + + /// @dev Report transmitted by OCR to transmit function + struct Report { + uint256 fastGasWei; + uint256 linkNative; + 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 maxLinkPayment the max amount this upkeep could pay for work + * @member performSuccess whether the perform was successful + * @member triggerType the type of trigger + * @member gasUsed gasUsed by this upkeep in perform + * @member gasOverhead gasOverhead for this upkeep + * @member dedupID unique ID used to dedup an upkeep/trigger combo + */ + struct UpkeepTransmitInfo { + Upkeep upkeep; + bool earlyChecksPassed; + uint96 maxLinkPayment; + bool performSuccess; + Trigger triggerType; + uint256 gasUsed; + uint256 gasOverhead; + bytes32 dedupID; + } + + struct Transmitter { + bool active; + uint8 index; // Index of oracle in s_signersList/s_transmittersList + uint96 balance; + uint96 lastCollected; + } + + 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; + } + + event AdminPrivilegeConfigSet(address indexed admin, bytes privilegeConfig); + event CancelledUpkeepReport(uint256 indexed id, bytes trigger); + event DedupKeyAdded(bytes32 indexed dedupKey); + 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 OwnerFundsWithdrawn(uint96 amount); + 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 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 mode the contract mode of default, Arbitrum, or Optimism + * @param link address of the LINK Token + * @param linkNativeFeed address of the LINK/Native price feed + * @param fastGasFeed address of the Fast Gas price feed + */ + constructor( + Mode mode, + address link, + address linkNativeFeed, + address fastGasFeed, + address automationForwarderLogic + ) ConfirmedOwner(msg.sender) { + i_mode = mode; + i_link = LinkTokenInterface(link); + i_linkNativeFeed = AggregatorV3Interface(linkNativeFeed); + i_fastGasFeed = AggregatorV3Interface(fastGasFeed); + i_automationForwarderLogic = automationForwarderLogic; + } + + // ================================================================ + // | 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(); + s_upkeep[id] = upkeep; + s_upkeepAdmin[id] = admin; + s_checkData[id] = checkData; + s_expectedLinkBalance = s_expectedLinkBalance + 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; + bytes memory idBytes = abi.encodePacked( + keccak256(abi.encode(_blockHash(_blockNum() - 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 linkNative) { + 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_linkNativeFeed.latestRoundData(); + if ( + feedValue <= 0 || block.timestamp < timestamp || (staleFallback && stalenessSeconds < block.timestamp - timestamp) + ) { + linkNative = s_fallbackLinkPrice; + } else { + linkNative = uint256(feedValue); + } + return (gasWei, linkNative); + } + + /** + * @dev calculates LINK paid for gas spent plus a configure premium percentage + * @param gasLimit the amount of gas used + * @param gasOverhead the amount of gas overhead + * @param fastGasWei the fast gas price + * @param linkNative the exchange ratio between LINK and Native token + * @param numBatchedUpkeeps the number of upkeeps in this batch. Used to divide the L1 cost + * @param isExecution if this is triggered by a perform upkeep function + */ + function _calculatePaymentAmount( + HotVars memory hotVars, + uint256 gasLimit, + uint256 gasOverhead, + uint256 fastGasWei, + uint256 linkNative, + uint16 numBatchedUpkeeps, + bool isExecution + ) internal view returns (uint96, uint96) { + uint256 gasWei = fastGasWei * hotVars.gasCeilingMultiplier; + // in case it's actual execution use actual gas price, capped by fastGasWei * gasCeilingMultiplier + if (isExecution && tx.gasprice < gasWei) { + gasWei = tx.gasprice; + } + + uint256 l1CostWei = 0; + if (i_mode == Mode.OPTIMISM) { + bytes memory txCallData = new bytes(0); + if (isExecution) { + txCallData = bytes.concat(msg.data, L1_FEE_DATA_PADDING); + } else { + // fee is 4 per 0 byte, 16 per non-zero byte. Worst case we can have + // s_storage.maxPerformDataSize non zero-bytes. Instead of setting bytes to non-zero + // we initialize 'new bytes' of length 4*maxPerformDataSize to cover for zero bytes. + txCallData = new bytes(4 * s_storage.maxPerformDataSize); + } + l1CostWei = OPTIMISM_ORACLE.getL1Fee(txCallData); + } else if (i_mode == Mode.ARBITRUM) { + if (isExecution) { + l1CostWei = ARB_NITRO_ORACLE.getCurrentTxL1GasFees(); + } else { + // fee is 4 per 0 byte, 16 per non-zero byte - we assume all non-zero and + // max data size to calculate max payment + (, uint256 perL1CalldataUnit, , , , ) = ARB_NITRO_ORACLE.getPricesInWei(); + l1CostWei = perL1CalldataUnit * s_storage.maxPerformDataSize * 16; + } + } + // if it's not performing upkeeps, use gas ceiling multiplier to estimate the upper bound + if (!isExecution) { + l1CostWei = hotVars.gasCeilingMultiplier * l1CostWei; + } + // Divide l1CostWei among all batched upkeeps. Spare change from division is not charged + l1CostWei = l1CostWei / numBatchedUpkeeps; + + uint256 gasPayment = ((gasWei * (gasLimit + gasOverhead) + l1CostWei) * 1e18) / linkNative; + uint256 premium = (((gasWei * gasLimit) + l1CostWei) * 1e9 * hotVars.paymentPremiumPPB) / + linkNative + + uint256(hotVars.flatFeeMicroLink) * + 1e12; + // LINK_TOTAL_SUPPLY < UINT96_MAX + if (gasPayment + premium > LINK_TOTAL_SUPPLY) revert PaymentGreaterThanAllLINK(); + return (uint96(gasPayment), uint96(premium)); + } + + /** + * @dev calculates the max LINK payment for an upkeep + */ + function _getMaxLinkPayment( + HotVars memory hotVars, + Trigger triggerType, + uint32 performGas, + uint32 performDataLength, + uint256 fastGasWei, + uint256 linkNative, + bool isExecution // Whether this is an actual perform execution or just a simulation + ) internal view returns (uint96) { + uint256 gasOverhead = _getMaxGasOverhead(triggerType, performDataLength, hotVars.f); + (uint96 reimbursement, uint96 premium) = _calculatePaymentAmount( + hotVars, + performGas, + gasOverhead, + fastGasWei, + linkNative, + 1, // Consider only 1 upkeep in batch to get maxPayment + isExecution + ); + + return reimbursement + premium; + } + + /** + * @dev returns the max gas overhead that can be charged for an upkeep + */ + function _getMaxGasOverhead(Trigger triggerType, uint32 performDataLength, uint8 f) internal pure returns (uint256) { + // performData causes additional overhead in report length and memory operations + uint256 baseOverhead; + if (triggerType == Trigger.CONDITION) { + baseOverhead = REGISTRY_CONDITIONAL_OVERHEAD; + } else if (triggerType == Trigger.LOG) { + baseOverhead = REGISTRY_LOG_OVERHEAD; + } else { + revert InvalidTriggerType(); + } + return + baseOverhead + + (REGISTRY_PER_SIGNER_GAS_OVERHEAD * (f + 1)) + + (REGISTRY_PER_PERFORM_BYTE_GAS_OVERHEAD * performDataLength); + } + + /** + * @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, + bytes memory rawTrigger, + UpkeepTransmitInfo memory transmitInfo + ) internal returns (bool, bytes32) { + bytes32 dedupID; + if (transmitInfo.triggerType == Trigger.CONDITION) { + if (!_validateConditionalTrigger(upkeepId, rawTrigger, transmitInfo)) return (false, dedupID); + } else if (transmitInfo.triggerType == Trigger.LOG) { + bool valid; + (valid, dedupID) = _validateLogTrigger(upkeepId, rawTrigger, transmitInfo); + if (!valid) return (false, dedupID); + } else { + revert InvalidTriggerType(); + } + if (transmitInfo.upkeep.maxValidBlocknumber <= _blockNum()) { + // 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); + } + if (transmitInfo.upkeep.balance < transmitInfo.maxLinkPayment) { + // Can happen due to fluctuations in gas / link prices + emit InsufficientFundsUpkeepReport(upkeepId, rawTrigger); + return (false, dedupID); + } + return (true, dedupID); + } + + /** + * @dev Does some early sanity checks before actually performing an upkeep + */ + function _validateConditionalTrigger( + uint256 upkeepId, + bytes memory rawTrigger, + UpkeepTransmitInfo memory transmitInfo + ) 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 ( + (trigger.blockHash != bytes32("") && _blockHash(trigger.blockNum) != trigger.blockHash) || + trigger.blockNum >= _blockNum() + ) { + // 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, + bytes memory rawTrigger, + UpkeepTransmitInfo memory transmitInfo + ) internal returns (bool, bytes32) { + LogTrigger memory trigger = abi.decode(rawTrigger, (LogTrigger)); + bytes32 dedupID = keccak256(abi.encodePacked(upkeepId, trigger.logBlockHash, trigger.txHash, trigger.logIndex)); + if ( + (trigger.blockHash != bytes32("") && _blockHash(trigger.blockNum) != trigger.blockHash) || + trigger.blockNum >= _blockNum() + ) { + // 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, UpkeepTransmitInfo memory upkeepTransmitInfo) internal { + if (upkeepTransmitInfo.triggerType == Trigger.CONDITION) { + s_upkeep[upkeepID].lastPerformedBlockNumber = uint32(_blockNum()); + } 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 does postPerform payment processing for an upkeep. Deducts upkeep's balance and increases + * amount spent. + */ + function _postPerformPayment( + HotVars memory hotVars, + uint256 upkeepId, + UpkeepTransmitInfo memory upkeepTransmitInfo, + uint256 fastGasWei, + uint256 linkNative, + uint16 numBatchedUpkeeps + ) internal returns (uint96 gasReimbursement, uint96 premium) { + (gasReimbursement, premium) = _calculatePaymentAmount( + hotVars, + upkeepTransmitInfo.gasUsed, + upkeepTransmitInfo.gasOverhead, + fastGasWei, + linkNative, + numBatchedUpkeeps, + true + ); + + uint96 payment = gasReimbursement + premium; + s_upkeep[upkeepId].balance -= payment; + s_upkeep[upkeepId].amountSpent += payment; + + return (gasReimbursement, premium); + } + + /** + * @dev Caps the gas overhead by the constant overhead used within initial payment checks in order to + * prevent a revert in payment processing. + */ + function _getCappedGasOverhead( + uint256 calculatedGasOverhead, + Trigger triggerType, + uint32 performDataLength, + uint8 f + ) internal pure returns (uint256 cappedGasOverhead) { + cappedGasOverhead = _getMaxGasOverhead(triggerType, performDataLength, f); + if (calculatedGasOverhead < cappedGasOverhead) { + return calculatedGasOverhead; + } + return cappedGasOverhead; + } + + /** + * @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 returns the current block number in a chain agnostic manner + */ + function _blockNum() internal view returns (uint256) { + if (i_mode == Mode.ARBITRUM) { + return ARB_SYS.arbBlockNumber(); + } else { + return block.number; + } + } + + /** + * @dev returns the blockhash of the provided block number in a chain agnostic manner + * @param n the blocknumber to retrieve the blockhash for + * @return blockhash the blockhash of block number n, or 0 if n is out queryable of range + */ + function _blockHash(uint256 n) internal view returns (bytes32) { + if (i_mode == Mode.ARBITRUM) { + uint256 blockNum = ARB_SYS.arbBlockNumber(); + if (n >= blockNum || blockNum - n > 256) { + return ""; + } + return ARB_SYS.arbBlockHash(n); + } else { + return blockhash(n); + } + } + + /** + * @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; + } +} diff --git a/contracts/src/v0.8/automation/dev/v2_2/AutomationRegistryLogicA2_2.sol b/contracts/src/v0.8/automation/dev/v2_2/AutomationRegistryLogicA2_2.sol new file mode 100644 index 00000000000..8c2183c4e23 --- /dev/null +++ b/contracts/src/v0.8/automation/dev/v2_2/AutomationRegistryLogicA2_2.sol @@ -0,0 +1,438 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.16; + +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 {AutomationRegistryBase2_2} from "./AutomationRegistryBase2_2.sol"; +import {AutomationRegistryLogicB2_2} from "./AutomationRegistryLogicB2_2.sol"; +import {Chainable} from "../../Chainable.sol"; +import {AutomationForwarder} from "../../AutomationForwarder.sol"; +import {IAutomationForwarder} from "../../interfaces/IAutomationForwarder.sol"; +import {UpkeepTranscoderInterfaceV2} from "../../interfaces/UpkeepTranscoderInterfaceV2.sol"; +import {MigratableKeeperRegistryInterfaceV2} from "../../interfaces/MigratableKeeperRegistryInterfaceV2.sol"; + +/** + * @notice Logic contract, works in tandem with AutomationRegistry as a proxy + */ +contract AutomationRegistryLogicA2_2 is AutomationRegistryBase2_2, Chainable { + using Address for address; + using EnumerableSet for EnumerableSet.UintSet; + using EnumerableSet for EnumerableSet.AddressSet; + + /** + * @param logicB the address of the second logic contract + */ + constructor( + AutomationRegistryLogicB2_2 logicB + ) + AutomationRegistryBase2_2( + logicB.getMode(), + logicB.getLinkAddress(), + logicB.getLinkNativeFeedAddress(), + logicB.getFastGasFeedAddress(), + logicB.getAutomationForwarderLogic() + ) + Chainable(address(logicB)) + {} + + /** + * @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 + cannotExecute + returns ( + bool upkeepNeeded, + bytes memory performData, + UpkeepFailureReason upkeepFailureReason, + uint256 gasUsed, + uint256 gasLimit, + uint256 fastGasWei, + uint256 linkNative + ) + { + Trigger triggerType = _getTriggerType(id); + HotVars memory hotVars = s_hotVars; + Upkeep memory upkeep = s_upkeep[id]; + + 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, linkNative) = _getFeedData(hotVars); + uint96 maxLinkPayment = _getMaxLinkPayment( + hotVars, + triggerType, + upkeep.performGas, + s_storage.maxPerformDataSize, + fastGasWei, + linkNative, + false + ); + if (upkeep.balance < maxLinkPayment) { + return (false, bytes(""), UpkeepFailureReason.INSUFFICIENT_BALANCE, 0, upkeep.performGas, 0, 0); + } + + bytes memory callData = _checkPayload(id, triggerType, triggerData); + + gasUsed = gasleft(); + (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, + linkNative + ); + } + return ( + upkeepNeeded, + result, + UpkeepFailureReason.TARGET_CHECK_REVERTED, + gasUsed, + upkeep.performGas, + fastGasWei, + linkNative + ); + } + + (upkeepNeeded, performData) = abi.decode(result, (bool, bytes)); + if (!upkeepNeeded) + return ( + false, + bytes(""), + UpkeepFailureReason.UPKEEP_NOT_NEEDED, + gasUsed, + upkeep.performGas, + fastGasWei, + linkNative + ); + + if (performData.length > s_storage.maxPerformDataSize) + return ( + false, + bytes(""), + UpkeepFailureReason.PERFORM_DATA_EXCEEDS_LIMIT, + gasUsed, + upkeep.performGas, + fastGasWei, + linkNative + ); + + return (upkeepNeeded, performData, upkeepFailureReason, gasUsed, upkeep.performGas, fastGasWei, linkNative); + } + + /** + * @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 linkNative + ) + { + 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 + cannotExecute + 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 + cannotExecute + returns (bool upkeepNeeded, bytes memory performData, UpkeepFailureReason upkeepFailureReason, uint256 gasUsed) + { + Upkeep memory upkeep = s_upkeep[id]; + gasUsed = gasleft(); + (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 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 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, + 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 AutomationForwarder(target, address(this), i_automationForwarderLogic)) + ); + _createUpkeep( + id, + Upkeep({ + performGas: gasLimit, + balance: 0, + maxValidBlocknumber: UINT32_MAX, + lastPerformedBlockNumber: 0, + amountSpent: 0, + paused: false, + forwarder: forwarder + }), + 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 this function registers a conditional upkeep, using a backwards compatible function signature + * @dev this function is backwards compatible with versions <=2.0, but may be removed in a future version + */ + function registerUpkeep( + address target, + uint32 gasLimit, + address admin, + bytes calldata checkData, + bytes calldata offchainConfig + ) external returns (uint256 id) { + return registerUpkeep(target, gasLimit, admin, Trigger.CONDITION, checkData, bytes(""), offchainConfig); + } + + /** + * @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 canceled = upkeep.maxValidBlocknumber != UINT32_MAX; + bool isOwner = msg.sender == owner(); + + if (canceled && !(isOwner && upkeep.maxValidBlocknumber > _blockNum())) revert CannotCancel(); + if (!isOwner && msg.sender != s_upkeepAdmin[id]) revert OnlyCallableByOwnerOrAdmin(); + + uint256 height = _blockNum(); + if (!isOwner) { + height = height + CANCELLATION_DELAY; + } + s_upkeep[id].maxValidBlocknumber = uint32(height); + s_upkeepIDs.remove(id); + + // charge the cancellation fee if the minUpkeepSpend is not met + uint96 minUpkeepSpend = s_storage.minUpkeepSpend; + uint96 cancellationFee = 0; + // cancellationFee is supposed to be min(max(minUpkeepSpend - amountSpent,0), amountLeft) + if (upkeep.amountSpent < minUpkeepSpend) { + cancellationFee = minUpkeepSpend - upkeep.amountSpent; + if (cancellationFee > upkeep.balance) { + cancellationFee = upkeep.balance; + } + } + s_upkeep[id].balance = upkeep.balance - cancellationFee; + s_storage.ownerLinkBalance = s_storage.ownerLinkBalance + cancellationFee; + + emit UpkeepCanceled(id, uint64(height)); + } + + /** + * @notice adds fund to an upkeep + * @param id the upkeepID + * @param amount the amount of LINK to fund, in jules (jules = "wei" of LINK) + */ + function addFunds(uint256 id, uint96 amount) external { + Upkeep memory upkeep = s_upkeep[id]; + if (upkeep.maxValidBlocknumber != UINT32_MAX) revert UpkeepCancelled(); + s_upkeep[id].balance = upkeep.balance + amount; + s_expectedLinkBalance = s_expectedLinkBalance + amount; + i_link.transferFrom(msg.sender, address(this), amount); + emit FundsAdded(id, msg.sender, amount); + } + + /** + * @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 + */ + 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(); + uint256 id; + Upkeep memory upkeep; + uint256 totalBalanceRemaining; + 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]; + _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]; + totalBalanceRemaining = totalBalanceRemaining + upkeep.balance; + 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]; + s_upkeepIDs.remove(id); + emit UpkeepMigrated(id, upkeep.balance, destination); + } + s_expectedLinkBalance = s_expectedLinkBalance - totalBalanceRemaining; + 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 + ) + ); + i_link.transfer(destination, totalBalanceRemaining); + } + + /** + * @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 + */ + 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 AutomationForwarder(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); + } + } + + /** + * @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); + } +} diff --git a/contracts/src/v0.8/automation/dev/v2_2/AutomationRegistryLogicB2_2.sol b/contracts/src/v0.8/automation/dev/v2_2/AutomationRegistryLogicB2_2.sol new file mode 100644 index 00000000000..0724fbd53f0 --- /dev/null +++ b/contracts/src/v0.8/automation/dev/v2_2/AutomationRegistryLogicB2_2.sol @@ -0,0 +1,516 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.16; + +import {AutomationRegistryBase2_2} from "./AutomationRegistryBase2_2.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 {UpkeepFormat} from "../../interfaces/UpkeepTranscoderInterface.sol"; +import {IAutomationForwarder} from "../../interfaces/IAutomationForwarder.sol"; + +contract AutomationRegistryLogicB2_2 is AutomationRegistryBase2_2 { + using Address for address; + using EnumerableSet for EnumerableSet.UintSet; + using EnumerableSet for EnumerableSet.AddressSet; + + /** + * @dev see AutomationRegistry master contract for constructor description + */ + constructor( + Mode mode, + address link, + address linkNativeFeed, + address fastGasFeed, + address automationForwarderLogic + ) AutomationRegistryBase2_2(mode, link, linkNativeFeed, fastGasFeed, automationForwarderLogic) {} + + // ================================================================ + // | UPKEEP MANAGEMENT | + // ================================================================ + + /** + * @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 withdraws LINK 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 > _blockNum()) revert UpkeepNotCanceled(); + uint96 amountToWithdraw = s_upkeep[id].balance; + s_expectedLinkBalance = s_expectedLinkBalance - amountToWithdraw; + s_upkeep[id].balance = 0; + i_link.transfer(to, amountToWithdraw); + emit FundsWithdrawn(id, amountToWithdraw, to); + } + + // ================================================================ + // | NODE MANAGEMENT | + // ================================================================ + + /** + * @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 withdraws LINK received as payment for work performed + */ + function withdrawPayment(address from, address to) external { + if (to == ZERO_ADDRESS) revert InvalidRecipient(); + 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_expectedLinkBalance = s_expectedLinkBalance - balance; + i_link.transfer(to, balance); + 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 { + if (msg.sender != s_storage.upkeepPrivilegeManager) { + revert OnlyCallableByUpkeepPrivilegeManager(); + } + s_upkeepPrivilegeConfig[upkeepId] = newPrivilegeConfig; + emit UpkeepPrivilegeConfigSet(upkeepId, newPrivilegeConfig); + } + + /** + * @notice withdraws the owner's LINK balance + */ + function withdrawOwnerFunds() external onlyOwner { + uint96 amount = s_storage.ownerLinkBalance; + s_expectedLinkBalance = s_expectedLinkBalance - amount; + s_storage.ownerLinkBalance = 0; + emit OwnerFundsWithdrawn(amount); + i_link.transfer(msg.sender, amount); + } + + /** + * @notice allows the owner to withdraw any LINK accidentally sent to the contract + */ + function recoverFunds() external onlyOwner { + uint256 total = i_link.balanceOf(address(this)); + i_link.transfer(msg.sender, total - s_expectedLinkBalance); + } + + /** + * @notice sets the payees for the 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 { + if (msg.sender != s_storage.upkeepPrivilegeManager) { + revert OnlyCallableByUpkeepPrivilegeManager(); + } + s_adminPrivilegeConfig[admin] = newPrivilegeConfig; + emit AdminPrivilegeConfigSet(admin, newPrivilegeConfig); + } + + // ================================================================ + // | 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 getCancellationDelay() external pure returns (uint256) { + return CANCELLATION_DELAY; + } + + function getMode() external view returns (Mode) { + return i_mode; + } + + function getLinkAddress() external view returns (address) { + return address(i_link); + } + + function getLinkNativeFeedAddress() external view returns (address) { + return address(i_linkNativeFeed); + } + + function getFastGasFeedAddress() external view returns (address) { + return address(i_fastGasFeed); + } + + function getAutomationForwarderLogic() external view returns (address) { + return i_automationForwarderLogic; + } + + function upkeepTranscoderVersion() public pure returns (UpkeepFormat) { + return UPKEEP_TRANSCODER_VERSION_BASE; + } + + function upkeepVersion() public pure returns (uint8) { + return UPKEEP_VERSION_BASE; + } + + /** + * @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 (UpkeepInfo memory upkeepInfo) { + Upkeep memory reg = s_upkeep[id]; + address target = address(reg.forwarder) == address(0) ? address(0) : reg.forwarder.getTarget(); + upkeepInfo = UpkeepInfo({ + target: target, + performGas: reg.performGas, + checkData: s_checkData[id], + balance: reg.balance, + admin: s_upkeepAdmin[id], + maxValidBlocknumber: reg.maxValidBlocknumber, + lastPerformedBlockNumber: reg.lastPerformedBlockNumber, + amountSpent: reg.amountSpent, + 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 state of the registry + */ + function getState() + external + view + returns ( + State memory state, + OnchainConfig memory config, + address[] memory signers, + address[] memory transmitters, + uint8 f + ) + { + state = State({ + nonce: s_storage.nonce, + ownerLinkBalance: s_storage.ownerLinkBalance, + expectedLinkBalance: s_expectedLinkBalance, + 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 = OnchainConfig({ + paymentPremiumPPB: s_hotVars.paymentPremiumPPB, + flatFeeMicroLink: s_hotVars.flatFeeMicroLink, + checkGasLimit: s_storage.checkGasLimit, + stalenessSeconds: s_hotVars.stalenessSeconds, + gasCeilingMultiplier: s_hotVars.gasCeilingMultiplier, + minUpkeepSpend: s_storage.minUpkeepSpend, + 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 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) { + return getMaxPaymentForGas(_getTriggerType(id), s_upkeep[id].performGas); + } + + /** + * @notice calculates the maximum payment for a given gas limit + * @param gasLimit the gas to calculate payment for + */ + function getMaxPaymentForGas(Trigger triggerType, uint32 gasLimit) public view returns (uint96 maxPayment) { + HotVars memory hotVars = s_hotVars; + (uint256 fastGasWei, uint256 linkNative) = _getFeedData(hotVars); + return + _getMaxLinkPayment(hotVars, triggerType, gasLimit, s_storage.maxPerformDataSize, fastGasWei, linkNative, false); + } + + /** + * @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 upkeep 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 the upkeep's forwarder contract + */ + function hasDedupKey(bytes32 dedupKey) external view returns (bool) { + return s_dedupKeys[dedupKey]; + } +} diff --git a/contracts/src/v0.8/automation/dev/v2_2/AutomationUtils2_2.sol b/contracts/src/v0.8/automation/dev/v2_2/AutomationUtils2_2.sol new file mode 100644 index 00000000000..2b8fd8df6ac --- /dev/null +++ b/contracts/src/v0.8/automation/dev/v2_2/AutomationUtils2_2.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.16; + +import {AutomationRegistryBase2_2} from "./AutomationRegistryBase2_2.sol"; +import {Log} from "../../interfaces/ILogAutomation.sol"; + +/** + * @notice this file exposes structs that are otherwise internal to the automation registry + * doing this allows those structs to be encoded and decoded with type safety in offchain code + * and tests because generated wrappers are made available + */ + +/** + * @notice structure of trigger for log triggers + */ +struct LogTriggerConfig { + address contractAddress; + uint8 filterSelector; // denotes which topics apply to filter ex 000, 101, 111...only last 3 bits apply + bytes32 topic0; + bytes32 topic1; + bytes32 topic2; + bytes32 topic3; +} + +contract AutomationUtils2_2 { + /** + * @dev this can be removed as OnchainConfig is now exposed directly from the registry + */ + function _onChainConfig(AutomationRegistryBase2_2.OnchainConfig memory) external {} // 0x2ff92a81 + + function _report(AutomationRegistryBase2_2.Report memory) external {} // 0xe65d6546 + + function _logTriggerConfig(LogTriggerConfig memory) external {} // 0x21f373d7 + + function _logTrigger(AutomationRegistryBase2_2.LogTrigger memory) external {} // 0x1c8d8260 + + function _conditionalTrigger(AutomationRegistryBase2_2.ConditionalTrigger memory) external {} // 0x4b6df294 + + function _log(Log memory) external {} // 0xe9720a49 +} diff --git a/contracts/src/v0.8/automation/dev/v2_2/LICENSE b/contracts/src/v0.8/automation/dev/v2_2/LICENSE new file mode 100644 index 00000000000..9230d392a98 --- /dev/null +++ b/contracts/src/v0.8/automation/dev/v2_2/LICENSE @@ -0,0 +1,57 @@ +Business Source License 1.1 + +License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved. +"Business Source License" is a trademark of MariaDB Corporation Ab. + +--- + +Parameters + +Licensor: SmartContract Chainlink Limited SEZC + +Licensed Work: Automation v2.2 +The Licensed Work is (c) 2024 SmartContract Chainlink Limited SEZC + +Additional Use Grant(s): +You may make use of Automation v2.1, v2.2 (which is available subject to the license here the “Licensed Work”) solely for purposes listed below: +https://github.com/smartcontractkit/chainlink-automation/tree/main/Automation_Grants.md + +Change Date: January 22, 2028 + +Change License: MIT + +--- + +Terms + +The Licensor hereby grants you the right to copy, modify, create derivative works, redistribute, and make non-production use of the Licensed Work. The Licensor may make an Additional Use Grant, above, permitting limited production use. + +Effective on the Change Date, or the fourth anniversary of the first publicly available distribution of a specific version of the Licensed Work under this License, whichever comes first, the Licensor hereby grants you rights under the terms of the Change License, and the rights granted in the paragraph above terminate. + +If your use of the Licensed Work does not comply with the requirements currently in effect as described in this License, you must purchase a commercial license from the Licensor, its affiliated entities, or authorized resellers, or you must refrain from using the Licensed Work. + +All copies of the original and modified Licensed Work, and derivative works of the Licensed Work, are subject to this License. This License applies separately for each version of the Licensed Work and the Change Date may vary for each version of the Licensed Work released by Licensor. + +You must conspicuously display this License on each original or modified copy of the Licensed Work. If you receive the Licensed Work in original or modified form from a third party, the terms and conditions set forth in this License apply to your use of that work. + +Any use of the Licensed Work in violation of this License will automatically terminate your rights under this License for the current and all other versions of the Licensed Work. + +This License does not grant you any right in any trademark or logo of Licensor or its affiliates (provided that you may use a trademark or logo of Licensor as expressly required by this License). + +TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND TITLE. + +MariaDB hereby grants you permission to use this License’s text to license your works, and to refer to it using the trademark "Business Source License", as long as you comply with the Covenants of Licensor below. + +--- + +Covenants of Licensor + +In consideration of the right to use this License’s text and the "Business Source License" name and trademark, Licensor covenants to MariaDB, and to all other recipients of the licensed work to be provided by Licensor: + +1. To specify as the Change License the GPL Version 2.0 or any later version, or a license that is compatible with GPL Version 2.0 or a later version, where "compatible" means that software provided under the Change License can be included in a program with software provided under GPL Version 2.0 or a later version. Licensor may specify additional Change Licenses without limitation. + +2. To either: (a) specify an additional grant of rights to use that does not impose any additional restriction on the right granted in this License, as the Additional Use Grant; or (b) insert the text "None". + +3. To specify a Change Date. + +4. Not to modify this License in any other way. diff --git a/contracts/src/v0.8/automation/dev/v2_2/README.md b/contracts/src/v0.8/automation/dev/v2_2/README.md new file mode 100644 index 00000000000..abd3eb0ca84 --- /dev/null +++ b/contracts/src/v0.8/automation/dev/v2_2/README.md @@ -0,0 +1,40 @@ +# Automation Contract Structure + +The on-chain component of Chainlink automation is too large to fit into the [size requirements][size-limit-eip] of a single contract. It is also too large to fit into 2 contracts, a solution that works for most large projects. Therefore, we included this explanation of how the pieces fit together and various tradeoffs incurred. + +### Glossary + +**Master Contract** - also known as the “storage” contract. This is the contract whose state we care about. It is the entry-point into the chain of delegatecalls. (We avoid the term "proxy" because it is commonly associated with upgradability, and this system _is not upgradable_ even though it relies on some of the same mechanics.) + +**Logic Contract** - this a contract whose sole purpose is to hold code. We use the code at this address and execute it in the context of the master contract in order to increase our total capacity for on-chain code. + +### Overview + +We chain multiple logic contracts together using [fallback functions][fallback] and [delegatecall][delegatecall]. If a function definition is not found on one contract, we fall back to the next, always executing the function in the scope of the master contract. The actual implementation of this is based off of [OZ's Proxy contract][oz-proxy]. + +### Diagram + +```mermaid +graph LR + Master -- delegatecall --> la[Logic A] + la -- delegatecall --> lb[Logic B] + lb -. delegatecall .-> lx[Logic X] +``` + +### Special Considerations + +- functions on the master contract have the least gas overhead, therefore, our most price-sensitive functions live there +- functions on the master contract have first-class support from tools like etherscan and tenderly - functions that we (or users) call often to debug should live there +- etherscan supports executing logic contract functions that are once removed from the master - therefore we give secondary preference to the first logic contract for user and debugging functions +- functions on logic A through logic X (as of writing) have no support on etherscan and will essentially be "invisible" to everyone but advanced users - we will try to reserve this space for uncommon interactions that are mostly done progamatically +- We use Logic A, B, C... to avoid confusion with the version ex `AutomationRegistryLogicA2_2.sol` --> Logic Contract A verion 2.1 +- Storage locations for logic contract addresses MUST BE BYTECODE (this is done by marking them as "immutable") otherwise the chaining mechanism will break + +### Master Interface + +The Master Interface is a deduped combination of all the interfaces from all contracts in the chain. We generate this interface programatically using the script `generate-automation-master-interface.ts`. This process is not a hardened one. Users of this script should take great care to ensure it's efficacy. + +[size-limit-eip]: https://eips.ethereum.org/EIPS/eip-170 +[fallback]: https://docs.soliditylang.org/en/v0.8.12/contracts.html#fallback-function +[delegatecall]: https://docs.soliditylang.org/en/v0.8.12/introduction-to-smart-contracts.html?highlight=delegatecall#delegatecall-callcode-and-libraries +[oz-proxy]: https://docs.openzeppelin.com/contracts/4.x/api/proxy#Proxy diff --git a/contracts/src/v0.8/automation/v2_1/test/AutomationForwarder.t.sol b/contracts/src/v0.8/automation/test/AutomationForwarder.t.sol similarity index 88% rename from contracts/src/v0.8/automation/v2_1/test/AutomationForwarder.t.sol rename to contracts/src/v0.8/automation/test/AutomationForwarder.t.sol index 078d22d6c96..aee2fcfbd29 100644 --- a/contracts/src/v0.8/automation/v2_1/test/AutomationForwarder.t.sol +++ b/contracts/src/v0.8/automation/test/AutomationForwarder.t.sol @@ -1,16 +1,16 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.16; -import {IAutomationRegistryConsumer} from "../../interfaces/IAutomationRegistryConsumer.sol"; -import {IAutomationForwarder} from "../../interfaces/IAutomationForwarder.sol"; +import {IAutomationRegistryConsumer} from "../interfaces/IAutomationRegistryConsumer.sol"; +import {IAutomationForwarder} from "../interfaces/IAutomationForwarder.sol"; import {AutomationForwarder} from "../AutomationForwarder.sol"; import {AutomationForwarderLogic} from "../AutomationForwarderLogic.sol"; -import {MockKeeperRegistry2_1} from "../../mocks/MockKeeperRegistry2_1.sol"; -import {UpkeepCounter} from "../../testhelpers/UpkeepCounter.sol"; +import {MockKeeperRegistry2_1} from "../mocks/MockKeeperRegistry2_1.sol"; +import {UpkeepCounter} from "../testhelpers/UpkeepCounter.sol"; import {BaseTest} from "./BaseTest.t.sol"; // in contracts directory, run -// forge test --match-path src/v0.8/automation/v2_1/test/AutomationForwarder.t.sol +// forge test --match-path src/v0.8/automation/test/AutomationForwarder.t.sol contract AutomationForwarderSetUp is BaseTest { IAutomationForwarder internal forwarder; diff --git a/contracts/src/v0.8/automation/test/BaseTest.t.sol b/contracts/src/v0.8/automation/test/BaseTest.t.sol index bcde8a11480..ecba6521c64 100644 --- a/contracts/src/v0.8/automation/test/BaseTest.t.sol +++ b/contracts/src/v0.8/automation/test/BaseTest.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.6; +pragma solidity ^0.8.0; import "forge-std/Test.sol"; diff --git a/contracts/src/v0.8/automation/v2_1/test/StructFactory.sol b/contracts/src/v0.8/automation/test/StructFactory.sol similarity index 85% rename from contracts/src/v0.8/automation/v2_1/test/StructFactory.sol rename to contracts/src/v0.8/automation/test/StructFactory.sol index 83bb9e89050..9317244a31e 100644 --- a/contracts/src/v0.8/automation/v2_1/test/StructFactory.sol +++ b/contracts/src/v0.8/automation/test/StructFactory.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.16; +pragma solidity ^0.8.0; contract StructFactory { address internal OWNER; diff --git a/contracts/src/v0.8/automation/v2_1/KeeperRegistryLogicA2_1.sol b/contracts/src/v0.8/automation/v2_1/KeeperRegistryLogicA2_1.sol index e3845ce63bd..517d14d8c9b 100644 --- a/contracts/src/v0.8/automation/v2_1/KeeperRegistryLogicA2_1.sol +++ b/contracts/src/v0.8/automation/v2_1/KeeperRegistryLogicA2_1.sol @@ -6,7 +6,7 @@ import {Address} from "../../vendor/openzeppelin-solidity/v4.7.3/contracts/utils import {KeeperRegistryBase2_1} from "./KeeperRegistryBase2_1.sol"; import {KeeperRegistryLogicB2_1} from "./KeeperRegistryLogicB2_1.sol"; import {Chainable} from "../Chainable.sol"; -import {AutomationForwarder} from "./AutomationForwarder.sol"; +import {AutomationForwarder} from "../AutomationForwarder.sol"; import {IAutomationForwarder} from "../interfaces/IAutomationForwarder.sol"; import {UpkeepTranscoderInterfaceV2} from "../interfaces/UpkeepTranscoderInterfaceV2.sol"; import {MigratableKeeperRegistryInterfaceV2} from "../interfaces/MigratableKeeperRegistryInterfaceV2.sol"; diff --git a/contracts/src/v0.8/automation/v2_1/LICENSE b/contracts/src/v0.8/automation/v2_1/LICENSE index 7d09d647f5b..f60ab657175 100644 --- a/contracts/src/v0.8/automation/v2_1/LICENSE +++ b/contracts/src/v0.8/automation/v2_1/LICENSE @@ -12,7 +12,8 @@ Licensor: SmartContract Chainlink Limited SEZC Licensed Work: Automation v2.1 The Licensed Work is (c) 2023 SmartContract Chainlink Limited SEZC -Additional Use Grant: Any uses listed and defined at https://github.com/smartcontractkit/chainlink-automation/tree/main/pkg/v3/Automation_v2.1_Grants.md +Additional Use Grant: Any uses listed and defined at https://github.com/smartcontractkit/chainlink-automation/tree/main/Automation_Grants.md + Change Date: September 12, 2027 diff --git a/contracts/src/v0.8/automation/v2_1/test/BaseTest.t.sol b/contracts/src/v0.8/automation/v2_1/test/BaseTest.t.sol deleted file mode 100644 index f5bc74286a4..00000000000 --- a/contracts/src/v0.8/automation/v2_1/test/BaseTest.t.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.16; - -import {StructFactory} from "./StructFactory.sol"; -import "forge-std/Test.sol"; - -contract BaseTest is Test, StructFactory { - function setUp() public virtual { - vm.startPrank(OWNER); - deal(OWNER, 1e20); - } -} diff --git a/contracts/test/v0.8/automation/AutomationRegistrar2_2.test.ts b/contracts/test/v0.8/automation/AutomationRegistrar2_2.test.ts new file mode 100644 index 00000000000..b7585462067 --- /dev/null +++ b/contracts/test/v0.8/automation/AutomationRegistrar2_2.test.ts @@ -0,0 +1,1013 @@ +import { ethers } from 'hardhat' +import { ContractFactory, Contract } from 'ethers' +import { assert, expect } from 'chai' +import { evmRevert } from '../../test-helpers/matchers' +import { getUsers, Personas } from '../../test-helpers/setup' +import { BigNumber, Signer } from 'ethers' +import { MockV3Aggregator__factory as MockV3AggregatorFactory } from '../../../typechain/factories/MockV3Aggregator__factory' +import { UpkeepMock__factory as UpkeepMockFactory } from '../../../typechain/factories/UpkeepMock__factory' +import { MockV3Aggregator } from '../../../typechain/MockV3Aggregator' +import { UpkeepMock } from '../../../typechain/UpkeepMock' +import { toWei } from '../../test-helpers/helpers' +import { IKeeperRegistryMaster as IKeeperRegistry } from '../../../typechain/IKeeperRegistryMaster' +import { AutomationRegistrar2_2 as Registrar } from '../../../typechain/AutomationRegistrar2_2' +import { deployRegistry21 } from './helpers' + +// copied from KeeperRegistryBase2_2.sol +enum Trigger { + CONDITION, + LOG, +} + +let linkTokenFactory: ContractFactory +let mockV3AggregatorFactory: MockV3AggregatorFactory +let upkeepMockFactory: UpkeepMockFactory + +let personas: Personas + +before(async () => { + personas = (await getUsers()).personas + + linkTokenFactory = await ethers.getContractFactory( + 'src/v0.4/LinkToken.sol:LinkToken', + ) + mockV3AggregatorFactory = (await ethers.getContractFactory( + 'src/v0.8/tests/MockV3Aggregator.sol:MockV3Aggregator', + )) as unknown as MockV3AggregatorFactory + upkeepMockFactory = await ethers.getContractFactory('UpkeepMock') +}) + +const errorMsgs = { + onlyOwner: 'revert Only callable by owner', + onlyAdmin: 'OnlyAdminOrOwner()', + hashPayload: 'HashMismatch()', + requestNotFound: 'RequestNotFound()', +} + +describe('AutomationRegistrar2_2', () => { + const upkeepName = 'SampleUpkeep' + + const linkEth = BigNumber.from(300000000) + const gasWei = BigNumber.from(100) + const performGas = BigNumber.from(100000) + const paymentPremiumPPB = BigNumber.from(250000000) + const flatFeeMicroLink = BigNumber.from(0) + const maxAllowedAutoApprove = 5 + const trigger = '0xdeadbeef' + const offchainConfig = '0x01234567' + + const emptyBytes = '0x00' + const stalenessSeconds = BigNumber.from(43820) + const gasCeilingMultiplier = BigNumber.from(1) + const checkGasLimit = BigNumber.from(20000000) + const fallbackGasPrice = BigNumber.from(200) + const fallbackLinkPrice = BigNumber.from(200000000) + const maxCheckDataSize = BigNumber.from(10000) + const maxPerformDataSize = BigNumber.from(10000) + const maxRevertDataSize = BigNumber.from(1000) + const maxPerformGas = BigNumber.from(5000000) + const minUpkeepSpend = BigNumber.from('1000000000000000000') + const amount = BigNumber.from('5000000000000000000') + const amount1 = BigNumber.from('6000000000000000000') + const transcoder = ethers.constants.AddressZero + const upkeepManager = ethers.Wallet.createRandom().address + + // Enum values are not auto exported in ABI so have to manually declare + const autoApproveType_DISABLED = 0 + const autoApproveType_ENABLED_SENDER_ALLOWLIST = 1 + const autoApproveType_ENABLED_ALL = 2 + + let owner: Signer + let admin: Signer + let someAddress: Signer + let registrarOwner: Signer + let stranger: Signer + let requestSender: Signer + + let linkToken: Contract + let linkEthFeed: MockV3Aggregator + let gasPriceFeed: MockV3Aggregator + let mock: UpkeepMock + let registry: IKeeperRegistry + let registrar: Registrar + + beforeEach(async () => { + owner = personas.Default + admin = personas.Neil + someAddress = personas.Ned + registrarOwner = personas.Nelly + stranger = personas.Nancy + requestSender = personas.Norbert + + linkToken = await linkTokenFactory.connect(owner).deploy() + gasPriceFeed = await mockV3AggregatorFactory + .connect(owner) + .deploy(0, gasWei) + linkEthFeed = await mockV3AggregatorFactory + .connect(owner) + .deploy(9, linkEth) + + registry = await deployRegistry21( + owner, + 0, + linkToken.address, + linkEthFeed.address, + gasPriceFeed.address, + ) + + mock = await upkeepMockFactory.deploy() + + const registrarFactory = await ethers.getContractFactory( + 'AutomationRegistrar2_2', + ) + registrar = await registrarFactory + .connect(registrarOwner) + .deploy(linkToken.address, registry.address, minUpkeepSpend, [ + { + triggerType: Trigger.CONDITION, + autoApproveType: autoApproveType_DISABLED, + autoApproveMaxAllowed: 0, + }, + { + triggerType: Trigger.LOG, + autoApproveType: autoApproveType_DISABLED, + autoApproveMaxAllowed: 0, + }, + ]) + + await linkToken + .connect(owner) + .transfer(await requestSender.getAddress(), toWei('1000')) + + const keepers = [ + await personas.Carol.getAddress(), + await personas.Nancy.getAddress(), + await personas.Ned.getAddress(), + await personas.Neil.getAddress(), + ] + const onchainConfig = { + paymentPremiumPPB, + flatFeeMicroLink, + checkGasLimit, + stalenessSeconds, + gasCeilingMultiplier, + minUpkeepSpend, + maxCheckDataSize, + maxPerformDataSize, + maxRevertDataSize, + maxPerformGas, + fallbackGasPrice, + fallbackLinkPrice, + transcoder, + registrars: [registrar.address], + upkeepPrivilegeManager: upkeepManager, + } + await registry + .connect(owner) + .setConfigTypeSafe(keepers, keepers, 1, onchainConfig, 1, '0x') + }) + + describe('#typeAndVersion', () => { + it('uses the correct type and version', async () => { + const typeAndVersion = await registrar.typeAndVersion() + assert.equal(typeAndVersion, 'AutomationRegistrar 2.1.0') + }) + }) + + describe('#register', () => { + it('reverts if not called by the LINK token', async () => { + await evmRevert( + registrar + .connect(someAddress) + .register( + upkeepName, + emptyBytes, + mock.address, + performGas, + await admin.getAddress(), + 0, + emptyBytes, + trigger, + offchainConfig, + amount, + await requestSender.getAddress(), + ), + 'OnlyLink()', + ) + }) + + it('reverts if the amount passed in data mismatches actual amount sent', async () => { + await registrar + .connect(registrarOwner) + .setTriggerConfig( + Trigger.CONDITION, + autoApproveType_ENABLED_ALL, + maxAllowedAutoApprove, + ) + + const abiEncodedBytes = registrar.interface.encodeFunctionData( + 'register', + [ + upkeepName, + emptyBytes, + mock.address, + performGas, + await admin.getAddress(), + 0, + emptyBytes, + trigger, + offchainConfig, + amount1, + await requestSender.getAddress(), + ], + ) + + await evmRevert( + linkToken + .connect(requestSender) + .transferAndCall(registrar.address, amount, abiEncodedBytes), + 'AmountMismatch()', + ) + }) + + it('reverts if the sender passed in data mismatches actual sender', async () => { + const abiEncodedBytes = registrar.interface.encodeFunctionData( + 'register', + [ + upkeepName, + emptyBytes, + mock.address, + performGas, + await admin.getAddress(), + 0, + emptyBytes, + trigger, + offchainConfig, + amount, + await admin.getAddress(), // Should have been requestSender.getAddress() + ], + ) + await evmRevert( + linkToken + .connect(requestSender) + .transferAndCall(registrar.address, amount, abiEncodedBytes), + 'SenderMismatch()', + ) + }) + + it('reverts if the admin address is 0x0000...', async () => { + const abiEncodedBytes = registrar.interface.encodeFunctionData( + 'register', + [ + upkeepName, + emptyBytes, + mock.address, + performGas, + '0x0000000000000000000000000000000000000000', + 0, + emptyBytes, + trigger, + offchainConfig, + amount, + await requestSender.getAddress(), + ], + ) + + await evmRevert( + linkToken + .connect(requestSender) + .transferAndCall(registrar.address, amount, abiEncodedBytes), + 'RegistrationRequestFailed()', + ) + }) + + it('Auto Approve ON - registers an upkeep on KeeperRegistry instantly and emits both RegistrationRequested and RegistrationApproved events', async () => { + //set auto approve ON with high threshold limits + await registrar + .connect(registrarOwner) + .setTriggerConfig( + Trigger.CONDITION, + autoApproveType_ENABLED_ALL, + maxAllowedAutoApprove, + ) + + //register with auto approve ON + const abiEncodedBytes = registrar.interface.encodeFunctionData( + 'register', + [ + upkeepName, + emptyBytes, + mock.address, + performGas, + await admin.getAddress(), + 0, + emptyBytes, + trigger, + offchainConfig, + amount, + await requestSender.getAddress(), + ], + ) + const tx = await linkToken + .connect(requestSender) + .transferAndCall(registrar.address, amount, abiEncodedBytes) + + const [id] = await registry.getActiveUpkeepIDs(0, 1) + + //confirm if a new upkeep has been registered and the details are the same as the one just registered + const newupkeep = await registry.getUpkeep(id) + assert.equal(newupkeep.target, mock.address) + assert.equal(newupkeep.admin, await admin.getAddress()) + assert.equal(newupkeep.checkData, emptyBytes) + assert.equal(newupkeep.balance.toString(), amount.toString()) + assert.equal(newupkeep.performGas, performGas.toNumber()) + assert.equal(newupkeep.offchainConfig, offchainConfig) + + await expect(tx).to.emit(registrar, 'RegistrationRequested') + await expect(tx).to.emit(registrar, 'RegistrationApproved') + }) + + it('Auto Approve OFF - does not registers an upkeep on KeeperRegistry, emits only RegistrationRequested event', async () => { + //get upkeep count before attempting registration + const beforeCount = (await registry.getState()).state.numUpkeeps + + //set auto approve OFF, threshold limits dont matter in this case + await registrar + .connect(registrarOwner) + .setTriggerConfig( + Trigger.CONDITION, + autoApproveType_DISABLED, + maxAllowedAutoApprove, + ) + + //register with auto approve OFF + const abiEncodedBytes = registrar.interface.encodeFunctionData( + 'register', + [ + upkeepName, + emptyBytes, + mock.address, + performGas, + await admin.getAddress(), + 0, + emptyBytes, + trigger, + offchainConfig, + amount, + await requestSender.getAddress(), + ], + ) + const tx = await linkToken + .connect(requestSender) + .transferAndCall(registrar.address, amount, abiEncodedBytes) + const receipt = await tx.wait() + + //get upkeep count after attempting registration + const afterCount = (await registry.getState()).state.numUpkeeps + //confirm that a new upkeep has NOT been registered and upkeep count is still the same + assert.deepEqual(beforeCount, afterCount) + + //confirm that only RegistrationRequested event is emitted and RegistrationApproved event is not + await expect(tx).to.emit(registrar, 'RegistrationRequested') + await expect(tx).not.to.emit(registrar, 'RegistrationApproved') + + const hash = receipt.logs[2].topics[1] + const pendingRequest = await registrar.getPendingRequest(hash) + assert.equal(await admin.getAddress(), pendingRequest[0]) + assert.ok(amount.eq(pendingRequest[1])) + }) + + it('Auto Approve ON - Throttle max approvals - does not register an upkeep on KeeperRegistry beyond the max limit, emits only RegistrationRequested event after limit is hit', async () => { + assert.equal((await registry.getState()).state.numUpkeeps.toNumber(), 0) + + //set auto approve on, with max 1 allowed + await registrar + .connect(registrarOwner) + .setTriggerConfig(Trigger.CONDITION, autoApproveType_ENABLED_ALL, 1) + + //set auto approve on, with max 1 allowed + await registrar + .connect(registrarOwner) + .setTriggerConfig(Trigger.LOG, autoApproveType_ENABLED_ALL, 1) + + // register within threshold, new upkeep should be registered + let abiEncodedBytes = registrar.interface.encodeFunctionData('register', [ + upkeepName, + emptyBytes, + mock.address, + performGas, + await admin.getAddress(), + 0, + emptyBytes, + trigger, + offchainConfig, + amount, + await requestSender.getAddress(), + ]) + await linkToken + .connect(requestSender) + .transferAndCall(registrar.address, amount, abiEncodedBytes) + assert.equal((await registry.getState()).state.numUpkeeps.toNumber(), 1) // 0 -> 1 + + // try registering another one, new upkeep should not be registered + abiEncodedBytes = registrar.interface.encodeFunctionData('register', [ + upkeepName, + emptyBytes, + mock.address, + performGas.toNumber() + 1, // make unique hash + await admin.getAddress(), + 0, + emptyBytes, + trigger, + offchainConfig, + amount, + await requestSender.getAddress(), + ]) + await linkToken + .connect(requestSender) + .transferAndCall(registrar.address, amount, abiEncodedBytes) + assert.equal((await registry.getState()).state.numUpkeeps.toNumber(), 1) // Still 1 + + // register a second type of upkeep, different limit + abiEncodedBytes = registrar.interface.encodeFunctionData('register', [ + upkeepName, + emptyBytes, + mock.address, + performGas, + await admin.getAddress(), + Trigger.LOG, + emptyBytes, + trigger, + offchainConfig, + amount, + await requestSender.getAddress(), + ]) + await linkToken + .connect(requestSender) + .transferAndCall(registrar.address, amount, abiEncodedBytes) + assert.equal((await registry.getState()).state.numUpkeeps.toNumber(), 2) // 1 -> 2 + + // Now set new max limit to 2. One more upkeep should get auto approved + await registrar + .connect(registrarOwner) + .setTriggerConfig(Trigger.CONDITION, autoApproveType_ENABLED_ALL, 2) + + abiEncodedBytes = registrar.interface.encodeFunctionData('register', [ + upkeepName, + emptyBytes, + mock.address, + performGas.toNumber() + 2, // make unique hash + await admin.getAddress(), + 0, + emptyBytes, + trigger, + offchainConfig, + amount, + await requestSender.getAddress(), + ]) + await linkToken + .connect(requestSender) + .transferAndCall(registrar.address, amount, abiEncodedBytes) + assert.equal((await registry.getState()).state.numUpkeeps.toNumber(), 3) // 2 -> 3 + + // One more upkeep should not get registered + abiEncodedBytes = registrar.interface.encodeFunctionData('register', [ + upkeepName, + emptyBytes, + mock.address, + performGas.toNumber() + 3, // make unique hash + await admin.getAddress(), + 0, + emptyBytes, + trigger, + offchainConfig, + amount, + await requestSender.getAddress(), + ]) + await linkToken + .connect(requestSender) + .transferAndCall(registrar.address, amount, abiEncodedBytes) + assert.equal((await registry.getState()).state.numUpkeeps.toNumber(), 3) // Still 3 + }) + + it('Auto Approve Sender Allowlist - sender in allowlist - registers an upkeep on KeeperRegistry instantly and emits both RegistrationRequested and RegistrationApproved events', async () => { + const senderAddress = await requestSender.getAddress() + + //set auto approve to ENABLED_SENDER_ALLOWLIST type with high threshold limits + await registrar + .connect(registrarOwner) + .setTriggerConfig( + Trigger.CONDITION, + autoApproveType_ENABLED_SENDER_ALLOWLIST, + maxAllowedAutoApprove, + ) + + // Add sender to allowlist + await registrar + .connect(registrarOwner) + .setAutoApproveAllowedSender(senderAddress, true) + + //register with auto approve ON + const abiEncodedBytes = registrar.interface.encodeFunctionData( + 'register', + [ + upkeepName, + emptyBytes, + mock.address, + performGas, + await admin.getAddress(), + 0, + emptyBytes, + trigger, + offchainConfig, + amount, + await requestSender.getAddress(), + ], + ) + const tx = await linkToken + .connect(requestSender) + .transferAndCall(registrar.address, amount, abiEncodedBytes) + + const [id] = await registry.getActiveUpkeepIDs(0, 1) + + //confirm if a new upkeep has been registered and the details are the same as the one just registered + const newupkeep = await registry.getUpkeep(id) + assert.equal(newupkeep.target, mock.address) + assert.equal(newupkeep.admin, await admin.getAddress()) + assert.equal(newupkeep.checkData, emptyBytes) + assert.equal(newupkeep.balance.toString(), amount.toString()) + assert.equal(newupkeep.performGas, performGas.toNumber()) + + await expect(tx).to.emit(registrar, 'RegistrationRequested') + await expect(tx).to.emit(registrar, 'RegistrationApproved') + }) + + it('Auto Approve Sender Allowlist - sender NOT in allowlist - does not registers an upkeep on KeeperRegistry, emits only RegistrationRequested event', async () => { + const beforeCount = (await registry.getState()).state.numUpkeeps + const senderAddress = await requestSender.getAddress() + + //set auto approve to ENABLED_SENDER_ALLOWLIST type with high threshold limits + await registrar + .connect(registrarOwner) + .setTriggerConfig( + Trigger.CONDITION, + autoApproveType_ENABLED_SENDER_ALLOWLIST, + maxAllowedAutoApprove, + ) + + // Explicitly remove sender from allowlist + await registrar + .connect(registrarOwner) + .setAutoApproveAllowedSender(senderAddress, false) + + //register. auto approve shouldn't happen + const abiEncodedBytes = registrar.interface.encodeFunctionData( + 'register', + [ + upkeepName, + emptyBytes, + mock.address, + performGas, + await admin.getAddress(), + 0, + emptyBytes, + trigger, + offchainConfig, + amount, + await requestSender.getAddress(), + ], + ) + const tx = await linkToken + .connect(requestSender) + .transferAndCall(registrar.address, amount, abiEncodedBytes) + const receipt = await tx.wait() + + //get upkeep count after attempting registration + const afterCount = (await registry.getState()).state.numUpkeeps + //confirm that a new upkeep has NOT been registered and upkeep count is still the same + assert.deepEqual(beforeCount, afterCount) + + //confirm that only RegistrationRequested event is emitted and RegistrationApproved event is not + await expect(tx).to.emit(registrar, 'RegistrationRequested') + await expect(tx).not.to.emit(registrar, 'RegistrationApproved') + + const hash = receipt.logs[2].topics[1] + const pendingRequest = await registrar.getPendingRequest(hash) + assert.equal(await admin.getAddress(), pendingRequest[0]) + assert.ok(amount.eq(pendingRequest[1])) + }) + }) + + describe('#registerUpkeep', () => { + it('reverts with empty message if amount sent is not available in LINK allowance', async () => { + await evmRevert( + registrar.connect(someAddress).registerUpkeep({ + name: upkeepName, + upkeepContract: mock.address, + gasLimit: performGas, + adminAddress: await admin.getAddress(), + triggerType: 0, + checkData: emptyBytes, + triggerConfig: trigger, + offchainConfig: emptyBytes, + amount, + encryptedEmail: emptyBytes, + }), + '', + ) + }) + + it('reverts if the amount passed in data is less than configured minimum', async () => { + await registrar + .connect(registrarOwner) + .setTriggerConfig( + Trigger.CONDITION, + autoApproveType_ENABLED_ALL, + maxAllowedAutoApprove, + ) + + // amt is one order of magnitude less than minUpkeepSpend + const amt = BigNumber.from('100000000000000000') + + await evmRevert( + registrar.connect(someAddress).registerUpkeep({ + name: upkeepName, + upkeepContract: mock.address, + gasLimit: performGas, + adminAddress: await admin.getAddress(), + triggerType: 0, + checkData: emptyBytes, + triggerConfig: trigger, + offchainConfig: emptyBytes, + amount: amt, + encryptedEmail: emptyBytes, + }), + 'InsufficientPayment()', + ) + }) + + it('Auto Approve ON - registers an upkeep on KeeperRegistry instantly and emits both RegistrationRequested and RegistrationApproved events', async () => { + //set auto approve ON with high threshold limits + await registrar + .connect(registrarOwner) + .setTriggerConfig( + Trigger.CONDITION, + autoApproveType_ENABLED_ALL, + maxAllowedAutoApprove, + ) + + await linkToken.connect(requestSender).approve(registrar.address, amount) + + const tx = await registrar.connect(requestSender).registerUpkeep({ + name: upkeepName, + upkeepContract: mock.address, + gasLimit: performGas, + adminAddress: await admin.getAddress(), + triggerType: 0, + checkData: emptyBytes, + triggerConfig: trigger, + offchainConfig, + amount, + encryptedEmail: emptyBytes, + }) + assert.equal((await registry.getState()).state.numUpkeeps.toNumber(), 1) // 0 -> 1 + + //confirm if a new upkeep has been registered and the details are the same as the one just registered + const [id] = await registry.getActiveUpkeepIDs(0, 1) + const newupkeep = await registry.getUpkeep(id) + assert.equal(newupkeep.target, mock.address) + assert.equal(newupkeep.admin, await admin.getAddress()) + assert.equal(newupkeep.checkData, emptyBytes) + assert.equal(newupkeep.balance.toString(), amount.toString()) + assert.equal(newupkeep.performGas, performGas.toNumber()) + assert.equal(newupkeep.offchainConfig, offchainConfig) + + await expect(tx).to.emit(registrar, 'RegistrationRequested') + await expect(tx).to.emit(registrar, 'RegistrationApproved') + }) + }) + + describe('#setAutoApproveAllowedSender', () => { + it('reverts if not called by the owner', async () => { + const tx = registrar + .connect(stranger) + .setAutoApproveAllowedSender(await admin.getAddress(), false) + await evmRevert(tx, 'Only callable by owner') + }) + + it('sets the allowed status correctly and emits log', async () => { + const senderAddress = await stranger.getAddress() + let tx = await registrar + .connect(registrarOwner) + .setAutoApproveAllowedSender(senderAddress, true) + await expect(tx) + .to.emit(registrar, 'AutoApproveAllowedSenderSet') + .withArgs(senderAddress, true) + + let senderAllowedStatus = await registrar + .connect(owner) + .getAutoApproveAllowedSender(senderAddress) + assert.isTrue(senderAllowedStatus) + + tx = await registrar + .connect(registrarOwner) + .setAutoApproveAllowedSender(senderAddress, false) + await expect(tx) + .to.emit(registrar, 'AutoApproveAllowedSenderSet') + .withArgs(senderAddress, false) + + senderAllowedStatus = await registrar + .connect(owner) + .getAutoApproveAllowedSender(senderAddress) + assert.isFalse(senderAllowedStatus) + }) + }) + + describe('#setTriggerConfig', () => { + it('reverts if not called by the owner', async () => { + const tx = registrar + .connect(stranger) + .setTriggerConfig(Trigger.LOG, autoApproveType_ENABLED_ALL, 100) + await evmRevert(tx, 'Only callable by owner') + }) + + it('changes the config', async () => { + const tx = await registrar + .connect(registrarOwner) + .setTriggerConfig(Trigger.LOG, autoApproveType_ENABLED_ALL, 100) + await registrar.getTriggerRegistrationDetails(Trigger.LOG) + await expect(tx) + .to.emit(registrar, 'TriggerConfigSet') + .withArgs(Trigger.LOG, autoApproveType_ENABLED_ALL, 100) + }) + }) + + describe('#approve', () => { + let hash: string + + beforeEach(async () => { + await registrar + .connect(registrarOwner) + .setTriggerConfig( + Trigger.CONDITION, + autoApproveType_DISABLED, + maxAllowedAutoApprove, + ) + + //register with auto approve OFF + const abiEncodedBytes = registrar.interface.encodeFunctionData( + 'register', + [ + upkeepName, + emptyBytes, + mock.address, + performGas, + await admin.getAddress(), + 0, + emptyBytes, + trigger, + offchainConfig, + amount, + await requestSender.getAddress(), + ], + ) + + const tx = await linkToken + .connect(requestSender) + .transferAndCall(registrar.address, amount, abiEncodedBytes) + const receipt = await tx.wait() + hash = receipt.logs[2].topics[1] + }) + + it('reverts if not called by the owner', async () => { + const tx = registrar + .connect(stranger) + .approve( + upkeepName, + mock.address, + performGas, + await admin.getAddress(), + 0, + emptyBytes, + trigger, + emptyBytes, + hash, + ) + await evmRevert(tx, 'Only callable by owner') + }) + + it('reverts if the hash does not exist', async () => { + const tx = registrar + .connect(registrarOwner) + .approve( + upkeepName, + mock.address, + performGas, + await admin.getAddress(), + 0, + emptyBytes, + trigger, + emptyBytes, + '0x000000000000000000000000322813fd9a801c5507c9de605d63cea4f2ce6c44', + ) + await evmRevert(tx, errorMsgs.requestNotFound) + }) + + it('reverts if any member of the payload changes', async () => { + let tx = registrar + .connect(registrarOwner) + .approve( + upkeepName, + ethers.Wallet.createRandom().address, + performGas, + await admin.getAddress(), + 0, + emptyBytes, + trigger, + emptyBytes, + hash, + ) + await evmRevert(tx, errorMsgs.hashPayload) + tx = registrar + .connect(registrarOwner) + .approve( + upkeepName, + mock.address, + 10000, + await admin.getAddress(), + 0, + emptyBytes, + trigger, + emptyBytes, + hash, + ) + await evmRevert(tx, errorMsgs.hashPayload) + tx = registrar + .connect(registrarOwner) + .approve( + upkeepName, + mock.address, + performGas, + ethers.Wallet.createRandom().address, + 0, + emptyBytes, + trigger, + emptyBytes, + hash, + ) + await evmRevert(tx, errorMsgs.hashPayload) + tx = registrar + .connect(registrarOwner) + .approve( + upkeepName, + mock.address, + performGas, + await admin.getAddress(), + 0, + '0x1234', + trigger, + emptyBytes, + hash, + ) + await evmRevert(tx, errorMsgs.hashPayload) + }) + + it('approves an existing registration request', async () => { + const tx = await registrar + .connect(registrarOwner) + .approve( + upkeepName, + mock.address, + performGas, + await admin.getAddress(), + 0, + emptyBytes, + trigger, + offchainConfig, + hash, + ) + await expect(tx).to.emit(registrar, 'RegistrationApproved') + }) + + it('deletes the request afterwards / reverts if the request DNE', async () => { + await registrar + .connect(registrarOwner) + .approve( + upkeepName, + mock.address, + performGas, + await admin.getAddress(), + 0, + emptyBytes, + trigger, + offchainConfig, + hash, + ) + const tx = registrar + .connect(registrarOwner) + .approve( + upkeepName, + mock.address, + performGas, + await admin.getAddress(), + 0, + emptyBytes, + trigger, + offchainConfig, + hash, + ) + await evmRevert(tx, errorMsgs.requestNotFound) + }) + }) + + describe('#cancel', () => { + let hash: string + + beforeEach(async () => { + await registrar + .connect(registrarOwner) + .setTriggerConfig( + Trigger.CONDITION, + autoApproveType_DISABLED, + maxAllowedAutoApprove, + ) + + //register with auto approve OFF + const abiEncodedBytes = registrar.interface.encodeFunctionData( + 'register', + [ + upkeepName, + emptyBytes, + mock.address, + performGas, + await admin.getAddress(), + 0, + emptyBytes, + trigger, + offchainConfig, + amount, + await requestSender.getAddress(), + ], + ) + const tx = await linkToken + .connect(requestSender) + .transferAndCall(registrar.address, amount, abiEncodedBytes) + const receipt = await tx.wait() + hash = receipt.logs[2].topics[1] + // submit duplicate request (increase balance) + await linkToken + .connect(requestSender) + .transferAndCall(registrar.address, amount, abiEncodedBytes) + }) + + it('reverts if not called by the admin / owner', async () => { + const tx = registrar.connect(stranger).cancel(hash) + await evmRevert(tx, errorMsgs.onlyAdmin) + }) + + it('reverts if the hash does not exist', async () => { + const tx = registrar + .connect(registrarOwner) + .cancel( + '0x000000000000000000000000322813fd9a801c5507c9de605d63cea4f2ce6c44', + ) + await evmRevert(tx, errorMsgs.requestNotFound) + }) + + it('refunds the total request balance to the admin address if owner cancels', async () => { + const before = await linkToken.balanceOf(await admin.getAddress()) + const tx = await registrar.connect(registrarOwner).cancel(hash) + const after = await linkToken.balanceOf(await admin.getAddress()) + assert.isTrue(after.sub(before).eq(amount.mul(BigNumber.from(2)))) + await expect(tx).to.emit(registrar, 'RegistrationRejected') + }) + + it('refunds the total request balance to the admin address if admin cancels', async () => { + const before = await linkToken.balanceOf(await admin.getAddress()) + const tx = await registrar.connect(admin).cancel(hash) + const after = await linkToken.balanceOf(await admin.getAddress()) + assert.isTrue(after.sub(before).eq(amount.mul(BigNumber.from(2)))) + await expect(tx).to.emit(registrar, 'RegistrationRejected') + }) + + it('deletes the request hash', async () => { + await registrar.connect(registrarOwner).cancel(hash) + let tx = registrar.connect(registrarOwner).cancel(hash) + await evmRevert(tx, errorMsgs.requestNotFound) + tx = registrar + .connect(registrarOwner) + .approve( + upkeepName, + mock.address, + performGas, + await admin.getAddress(), + 0, + emptyBytes, + trigger, + emptyBytes, + hash, + ) + await evmRevert(tx, errorMsgs.requestNotFound) + }) + }) +}) diff --git a/contracts/test/v0.8/automation/AutomationRegistry2_2.test.ts b/contracts/test/v0.8/automation/AutomationRegistry2_2.test.ts new file mode 100644 index 00000000000..3822d362240 --- /dev/null +++ b/contracts/test/v0.8/automation/AutomationRegistry2_2.test.ts @@ -0,0 +1,5757 @@ +import { ethers } from 'hardhat' +import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' +import { assert, expect } from 'chai' +import { + BigNumber, + BigNumberish, + BytesLike, + Contract, + ContractFactory, + ContractReceipt, + ContractTransaction, + Signer, + Wallet, +} from 'ethers' +import { evmRevert } from '../../test-helpers/matchers' +import { getUsers, Personas } from '../../test-helpers/setup' +import { randomAddress, toWei } from '../../test-helpers/helpers' +import { StreamsLookupUpkeep__factory as StreamsLookupUpkeepFactory } from '../../../typechain/factories/StreamsLookupUpkeep__factory' +import { MockV3Aggregator__factory as MockV3AggregatorFactory } from '../../../typechain/factories/MockV3Aggregator__factory' +import { UpkeepMock__factory as UpkeepMockFactory } from '../../../typechain/factories/UpkeepMock__factory' +import { UpkeepAutoFunder__factory as UpkeepAutoFunderFactory } from '../../../typechain/factories/UpkeepAutoFunder__factory' +import { MockArbGasInfo__factory as MockArbGasInfoFactory } from '../../../typechain/factories/MockArbGasInfo__factory' +import { MockOVMGasPriceOracle__factory as MockOVMGasPriceOracleFactory } from '../../../typechain/factories/MockOVMGasPriceOracle__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 { AutomationUtils2_2 as AutomationUtils } from '../../../typechain/AutomationUtils2_2' +import { StreamsLookupUpkeep } from '../../../typechain/StreamsLookupUpkeep' +import { MockV3Aggregator } from '../../../typechain/MockV3Aggregator' +import { UpkeepMock } from '../../../typechain/UpkeepMock' +import { MockArbGasInfo } from '../../../typechain/MockArbGasInfo' +import { MockOVMGasPriceOracle } from '../../../typechain/MockOVMGasPriceOracle' +import { UpkeepTranscoder } from '../../../typechain/UpkeepTranscoder' +import { UpkeepAutoFunder } from '../../../typechain' +import { + CancelledUpkeepReportEvent, + IKeeperRegistryMaster as IKeeperRegistry, + InsufficientFundsUpkeepReportEvent, + ReorgedUpkeepReportEvent, + StaleUpkeepReportEvent, + UpkeepPerformedEvent, +} from '../../../typechain/IKeeperRegistryMaster' +import { + deployMockContract, + MockContract, +} from '@ethereum-waffle/mock-contract' +import { deployRegistry21 } from './helpers' + +const describeMaybe = process.env.SKIP_SLOW ? describe.skip : describe +const itMaybe = process.env.SKIP_SLOW ? it.skip : it + +// copied from AutomationRegistryInterface2_2.sol +enum UpkeepFailureReason { + NONE, + UPKEEP_CANCELLED, + UPKEEP_PAUSED, + TARGET_CHECK_REVERTED, + UPKEEP_NOT_NEEDED, + PERFORM_DATA_EXCEEDS_LIMIT, + INSUFFICIENT_BALANCE, + CHECK_CALLBACK_REVERTED, + REVERT_DATA_EXCEEDS_LIMIT, + REGISTRY_PAUSED, +} + +// copied from AutomationRegistryInterface2_2.sol +enum Mode { + DEFAULT, + ARBITRUM, + OPTIMISM, +} + +// copied from KeeperRegistryBase2_2.sol +enum Trigger { + CONDITION, + LOG, +} + +// un-exported types that must be extracted from the utils contract +type Report = Parameters[0] +type OnChainConfig = Parameters[0] +type LogTrigger = Parameters[0] +type ConditionalTrigger = Parameters[0] +type Log = Parameters[0] + +// ----------------------------------------------------------------------------------------------- + +// These values should match the constants declared in registry +let registryConditionalOverhead: BigNumber +let registryLogOverhead: BigNumber +let registryPerSignerGasOverhead: BigNumber +let registryPerPerformByteGasOverhead: BigNumber +let cancellationDelay: number + +// This is the margin for gas that we test for. Gas charged should always be greater +// than total gas used in tx but should not increase beyond this margin +const gasCalculationMargin = BigNumber.from(8000) + +const linkEth = BigNumber.from(5000000000000000) // 1 Link = 0.005 Eth +const gasWei = BigNumber.from(1000000000) // 1 gwei +// ----------------------------------------------------------------------------------------------- +// test-wide configs for upkeeps +const linkDivisibility = BigNumber.from('1000000000000000000') +const performGas = BigNumber.from('1000000') +const paymentPremiumBase = BigNumber.from('1000000000') +const paymentPremiumPPB = BigNumber.from('250000000') +const flatFeeMicroLink = BigNumber.from(0) + +const randomBytes = '0x1234abcd' +const emptyBytes = '0x' +const emptyBytes32 = + '0x0000000000000000000000000000000000000000000000000000000000000000' + +const transmitGasOverhead = 1_000_000 +const checkGasOverhead = 400_000 + +const stalenessSeconds = BigNumber.from(43820) +const gasCeilingMultiplier = BigNumber.from(2) +const checkGasLimit = BigNumber.from(10000000) +const fallbackGasPrice = gasWei.mul(BigNumber.from('2')) +const fallbackLinkPrice = linkEth.div(BigNumber.from('2')) +const maxCheckDataSize = BigNumber.from(1000) +const maxPerformDataSize = BigNumber.from(1000) +const maxRevertDataSize = BigNumber.from(1000) +const maxPerformGas = BigNumber.from(5000000) +const minUpkeepSpend = BigNumber.from(0) +const f = 1 +const offchainVersion = 1 +const offchainBytes = '0x' +const zeroAddress = ethers.constants.AddressZero +const epochAndRound5_1 = + '0x0000000000000000000000000000000000000000000000000000000000000501' + +let logTriggerConfig: string + +// ----------------------------------------------------------------------------------------------- + +// Smart contract factories +let linkTokenFactory: ContractFactory +let mockV3AggregatorFactory: MockV3AggregatorFactory +let upkeepMockFactory: UpkeepMockFactory +let upkeepAutoFunderFactory: UpkeepAutoFunderFactory +let mockArbGasInfoFactory: MockArbGasInfoFactory +let mockOVMGasPriceOracleFactory: MockOVMGasPriceOracleFactory +let streamsLookupUpkeepFactory: StreamsLookupUpkeepFactory +let personas: Personas + +// contracts +let linkToken: Contract +let linkEthFeed: MockV3Aggregator +let gasPriceFeed: MockV3Aggregator +let registry: IKeeperRegistry // default registry, used for most tests +let arbRegistry: IKeeperRegistry // arbitrum registry +let opRegistry: IKeeperRegistry // optimism registry +let mgRegistry: IKeeperRegistry // "migrate registry" used in migration tests +let blankRegistry: IKeeperRegistry // used to test initial configurations +let mock: UpkeepMock +let autoFunderUpkeep: UpkeepAutoFunder +let ltUpkeep: MockContract +let transcoder: UpkeepTranscoder +let mockArbGasInfo: MockArbGasInfo +let mockOVMGasPriceOracle: MockOVMGasPriceOracle +let streamsLookupUpkeep: StreamsLookupUpkeep +let automationUtils: AutomationUtils + +function now() { + return Math.floor(Date.now() / 1000) +} + +async function getUpkeepID(tx: ContractTransaction): Promise { + const receipt = await tx.wait() + for (const event of receipt.events || []) { + if ( + event.args && + event.eventSignature == 'UpkeepRegistered(uint256,uint32,address)' + ) { + return event.args[0] + } + } + throw new Error('could not find upkeep ID in tx event logs') +} + +const getTriggerType = (upkeepId: BigNumber): Trigger => { + const hexBytes = ethers.utils.defaultAbiCoder.encode(['uint256'], [upkeepId]) + const bytes = ethers.utils.arrayify(hexBytes) + for (let idx = 4; idx < 15; idx++) { + if (bytes[idx] != 0) { + return Trigger.CONDITION + } + } + return bytes[15] as Trigger +} + +const encodeConfig = (onchainConfig: OnChainConfig) => { + return ( + '0x' + + automationUtils.interface + .encodeFunctionData('_onChainConfig', [onchainConfig]) + .slice(10) + ) +} + +const encodeBlockTrigger = (conditionalTrigger: ConditionalTrigger) => { + return ( + '0x' + + automationUtils.interface + .encodeFunctionData('_conditionalTrigger', [conditionalTrigger]) + .slice(10) + ) +} + +const encodeLogTrigger = (logTrigger: LogTrigger) => { + return ( + '0x' + + automationUtils.interface + .encodeFunctionData('_logTrigger', [logTrigger]) + .slice(10) + ) +} + +const encodeLog = (log: Log) => { + return ( + '0x' + automationUtils.interface.encodeFunctionData('_log', [log]).slice(10) + ) +} + +const encodeReport = (report: Report) => { + return ( + '0x' + + automationUtils.interface.encodeFunctionData('_report', [report]).slice(10) + ) +} + +type UpkeepData = { + Id: BigNumberish + performGas: BigNumberish + performData: BytesLike + trigger: BytesLike +} + +const makeReport = (upkeeps: UpkeepData[]) => { + const upkeepIds = upkeeps.map((u) => u.Id) + const performGases = upkeeps.map((u) => u.performGas) + const triggers = upkeeps.map((u) => u.trigger) + const performDatas = upkeeps.map((u) => u.performData) + return encodeReport({ + fastGasWei: gasWei, + linkNative: linkEth, + upkeepIds, + gasLimits: performGases, + triggers, + performDatas, + }) +} + +const makeLatestBlockReport = async (upkeepsIDs: BigNumberish[]) => { + const latestBlock = await ethers.provider.getBlock('latest') + const upkeeps: UpkeepData[] = [] + for (let i = 0; i < upkeepsIDs.length; i++) { + upkeeps.push({ + Id: upkeepsIDs[i], + performGas, + trigger: encodeBlockTrigger({ + blockNum: latestBlock.number, + blockHash: latestBlock.hash, + }), + performData: '0x', + }) + } + return makeReport(upkeeps) +} + +const signReport = ( + reportContext: string[], + report: any, + signers: Wallet[], +) => { + const reportDigest = ethers.utils.keccak256(report) + const packedArgs = ethers.utils.solidityPack( + ['bytes32', 'bytes32[3]'], + [reportDigest, reportContext], + ) + const packedDigest = ethers.utils.keccak256(packedArgs) + + const signatures = [] + for (const signer of signers) { + signatures.push(signer._signingKey().signDigest(packedDigest)) + } + const vs = signatures.map((i) => '0' + (i.v - 27).toString(16)).join('') + return { + vs: '0x' + vs.padEnd(64, '0'), + rs: signatures.map((i) => i.r), + ss: signatures.map((i) => i.s), + } +} + +const parseUpkeepPerformedLogs = (receipt: ContractReceipt) => { + const parsedLogs = [] + for (const rawLog of receipt.logs) { + try { + const log = registry.interface.parseLog(rawLog) + if ( + log.name == + registry.interface.events[ + 'UpkeepPerformed(uint256,bool,uint96,uint256,uint256,bytes)' + ].name + ) { + parsedLogs.push(log as unknown as UpkeepPerformedEvent) + } + } catch { + continue + } + } + return parsedLogs +} + +const parseReorgedUpkeepReportLogs = (receipt: ContractReceipt) => { + const parsedLogs = [] + for (const rawLog of receipt.logs) { + try { + const log = registry.interface.parseLog(rawLog) + if ( + log.name == + registry.interface.events['ReorgedUpkeepReport(uint256,bytes)'].name + ) { + parsedLogs.push(log as unknown as ReorgedUpkeepReportEvent) + } + } catch { + continue + } + } + return parsedLogs +} + +const parseStaleUpkeepReportLogs = (receipt: ContractReceipt) => { + const parsedLogs = [] + for (const rawLog of receipt.logs) { + try { + const log = registry.interface.parseLog(rawLog) + if ( + log.name == + registry.interface.events['StaleUpkeepReport(uint256,bytes)'].name + ) { + parsedLogs.push(log as unknown as StaleUpkeepReportEvent) + } + } catch { + continue + } + } + return parsedLogs +} + +const parseInsufficientFundsUpkeepReportLogs = (receipt: ContractReceipt) => { + const parsedLogs = [] + for (const rawLog of receipt.logs) { + try { + const log = registry.interface.parseLog(rawLog) + if ( + log.name == + registry.interface.events[ + 'InsufficientFundsUpkeepReport(uint256,bytes)' + ].name + ) { + parsedLogs.push(log as unknown as InsufficientFundsUpkeepReportEvent) + } + } catch { + continue + } + } + return parsedLogs +} + +const parseCancelledUpkeepReportLogs = (receipt: ContractReceipt) => { + const parsedLogs = [] + for (const rawLog of receipt.logs) { + try { + const log = registry.interface.parseLog(rawLog) + if ( + log.name == + registry.interface.events['CancelledUpkeepReport(uint256,bytes)'].name + ) { + parsedLogs.push(log as unknown as CancelledUpkeepReportEvent) + } + } catch { + continue + } + } + return parsedLogs +} + +describe('KeeperRegistry2_2', () => { + let owner: Signer + let keeper1: Signer + let keeper2: Signer + let keeper3: Signer + let keeper4: Signer + let keeper5: Signer + let nonkeeper: Signer + let signer1: Wallet + let signer2: Wallet + let signer3: Wallet + let signer4: Wallet + let signer5: Wallet + let admin: Signer + let payee1: Signer + let payee2: Signer + let payee3: Signer + let payee4: Signer + let payee5: Signer + + let upkeepId: BigNumber // conditional upkeep + let afUpkeepId: BigNumber // auto funding upkeep + let logUpkeepId: BigNumber // log trigger upkeepID + let streamsLookupUpkeepId: BigNumber // streams lookup upkeep + const numUpkeeps = 4 // see above + let keeperAddresses: string[] + let payees: string[] + let signers: Wallet[] + let signerAddresses: string[] + let config: any + let baseConfig: Parameters + let upkeepManager: string + + before(async () => { + personas = (await getUsers()).personas + + const utilsFactory = await ethers.getContractFactory('AutomationUtils2_2') + automationUtils = await utilsFactory.deploy() + + linkTokenFactory = await ethers.getContractFactory( + 'src/v0.4/LinkToken.sol:LinkToken', + ) + // need full path because there are two contracts with name MockV3Aggregator + mockV3AggregatorFactory = (await ethers.getContractFactory( + 'src/v0.8/tests/MockV3Aggregator.sol:MockV3Aggregator', + )) as unknown as MockV3AggregatorFactory + upkeepMockFactory = await ethers.getContractFactory('UpkeepMock') + upkeepAutoFunderFactory = + await ethers.getContractFactory('UpkeepAutoFunder') + mockArbGasInfoFactory = await ethers.getContractFactory('MockArbGasInfo') + mockOVMGasPriceOracleFactory = await ethers.getContractFactory( + 'MockOVMGasPriceOracle', + ) + streamsLookupUpkeepFactory = await ethers.getContractFactory( + 'StreamsLookupUpkeep', + ) + + owner = personas.Default + keeper1 = personas.Carol + keeper2 = personas.Eddy + keeper3 = personas.Nancy + keeper4 = personas.Norbert + keeper5 = personas.Nick + nonkeeper = personas.Ned + admin = personas.Neil + payee1 = personas.Nelly + payee2 = personas.Norbert + payee3 = personas.Nick + payee4 = personas.Eddy + payee5 = personas.Carol + upkeepManager = await personas.Norbert.getAddress() + // signers + signer1 = new ethers.Wallet( + '0x7777777000000000000000000000000000000000000000000000000000000001', + ) + signer2 = new ethers.Wallet( + '0x7777777000000000000000000000000000000000000000000000000000000002', + ) + signer3 = new ethers.Wallet( + '0x7777777000000000000000000000000000000000000000000000000000000003', + ) + signer4 = new ethers.Wallet( + '0x7777777000000000000000000000000000000000000000000000000000000004', + ) + signer5 = new ethers.Wallet( + '0x7777777000000000000000000000000000000000000000000000000000000005', + ) + + keeperAddresses = [ + await keeper1.getAddress(), + await keeper2.getAddress(), + await keeper3.getAddress(), + await keeper4.getAddress(), + await keeper5.getAddress(), + ] + payees = [ + await payee1.getAddress(), + await payee2.getAddress(), + await payee3.getAddress(), + await payee4.getAddress(), + await payee5.getAddress(), + ] + signers = [signer1, signer2, signer3, signer4, signer5] + + // We append 26 random addresses to keepers, payees and signers to get a system of 31 oracles + // This allows f value of 1 - 10 + for (let i = 0; i < 26; i++) { + keeperAddresses.push(randomAddress()) + payees.push(randomAddress()) + signers.push(ethers.Wallet.createRandom()) + } + signerAddresses = [] + for (const signer of signers) { + signerAddresses.push(await signer.getAddress()) + } + + logTriggerConfig = + '0x' + + automationUtils.interface + .encodeFunctionData('_logTriggerConfig', [ + { + contractAddress: randomAddress(), + filterSelector: 0, + topic0: ethers.utils.randomBytes(32), + topic1: ethers.utils.randomBytes(32), + topic2: ethers.utils.randomBytes(32), + topic3: ethers.utils.randomBytes(32), + }, + ]) + .slice(10) + }) + + const linkForGas = ( + upkeepGasSpent: BigNumber, + gasOverhead: BigNumber, + gasMultiplier: BigNumber, + premiumPPB: BigNumber, + flatFee: BigNumber, + l1CostWei?: BigNumber, + numUpkeepsBatch?: BigNumber, + ) => { + l1CostWei = l1CostWei === undefined ? BigNumber.from(0) : l1CostWei + numUpkeepsBatch = + numUpkeepsBatch === undefined ? BigNumber.from(1) : numUpkeepsBatch + + const gasSpent = gasOverhead.add(BigNumber.from(upkeepGasSpent)) + const base = gasWei + .mul(gasMultiplier) + .mul(gasSpent) + .mul(linkDivisibility) + .div(linkEth) + const l1Fee = l1CostWei + .mul(gasMultiplier) + .div(numUpkeepsBatch) + .mul(linkDivisibility) + .div(linkEth) + const gasPayment = base.add(l1Fee) + + const premium = gasWei + .mul(gasMultiplier) + .mul(upkeepGasSpent) + .add(l1CostWei.mul(gasMultiplier).div(numUpkeepsBatch)) + .mul(linkDivisibility) + .div(linkEth) + .mul(premiumPPB) + .div(paymentPremiumBase) + .add(BigNumber.from(flatFee).mul('1000000000000')) + + return { + total: gasPayment.add(premium), + gasPaymemnt: gasPayment, + premium, + } + } + + const verifyMaxPayment = async ( + registry: IKeeperRegistry, + l1CostWei?: BigNumber, + ) => { + type TestCase = { + name: string + multiplier: number + gas: number + premium: number + flatFee: number + } + + const tests: TestCase[] = [ + { + name: 'no fees', + multiplier: 1, + gas: 100000, + premium: 0, + flatFee: 0, + }, + { + name: 'basic fees', + multiplier: 1, + gas: 100000, + premium: 250000000, + flatFee: 1000000, + }, + { + name: 'max fees', + multiplier: 3, + gas: 10000000, + premium: 250000000, + flatFee: 1000000, + }, + ] + + const fPlusOne = BigNumber.from(f + 1) + const totalConditionalOverhead = registryConditionalOverhead + .add(registryPerSignerGasOverhead.mul(fPlusOne)) + .add(registryPerPerformByteGasOverhead.mul(maxPerformDataSize)) + const totalLogOverhead = registryLogOverhead + .add(registryPerSignerGasOverhead.mul(fPlusOne)) + .add(registryPerPerformByteGasOverhead.mul(maxPerformDataSize)) + + for (const test of tests) { + await registry.connect(owner).setConfig( + signerAddresses, + keeperAddresses, + f, + encodeConfig({ + paymentPremiumPPB: test.premium, + flatFeeMicroLink: test.flatFee, + checkGasLimit, + stalenessSeconds, + gasCeilingMultiplier: test.multiplier, + minUpkeepSpend, + maxCheckDataSize, + maxPerformDataSize, + maxRevertDataSize, + maxPerformGas, + fallbackGasPrice, + fallbackLinkPrice, + transcoder: transcoder.address, + registrars: [], + upkeepPrivilegeManager: upkeepManager, + }), + offchainVersion, + offchainBytes, + ) + + const conditionalPrice = await registry.getMaxPaymentForGas( + Trigger.CONDITION, + test.gas, + ) + expect(conditionalPrice).to.equal( + linkForGas( + BigNumber.from(test.gas), + totalConditionalOverhead, + BigNumber.from(test.multiplier), + BigNumber.from(test.premium), + BigNumber.from(test.flatFee), + l1CostWei, + ).total, + ) + + const logPrice = await registry.getMaxPaymentForGas(Trigger.LOG, test.gas) + expect(logPrice).to.equal( + linkForGas( + BigNumber.from(test.gas), + totalLogOverhead, + BigNumber.from(test.multiplier), + BigNumber.from(test.premium), + BigNumber.from(test.flatFee), + l1CostWei, + ).total, + ) + } + } + + const verifyConsistentAccounting = async ( + maxAllowedSpareChange: BigNumber, + ) => { + const expectedLinkBalance = (await registry.getState()).state + .expectedLinkBalance + const linkTokenBalance = await linkToken.balanceOf(registry.address) + const upkeepIdBalance = (await registry.getUpkeep(upkeepId)).balance + let totalKeeperBalance = BigNumber.from(0) + for (let i = 0; i < keeperAddresses.length; i++) { + totalKeeperBalance = totalKeeperBalance.add( + (await registry.getTransmitterInfo(keeperAddresses[i])).balance, + ) + } + const ownerBalance = (await registry.getState()).state.ownerLinkBalance + assert.isTrue(expectedLinkBalance.eq(linkTokenBalance)) + assert.isTrue( + upkeepIdBalance + .add(totalKeeperBalance) + .add(ownerBalance) + .lte(expectedLinkBalance), + ) + assert.isTrue( + expectedLinkBalance + .sub(upkeepIdBalance) + .sub(totalKeeperBalance) + .sub(ownerBalance) + .lte(maxAllowedSpareChange), + ) + } + + interface GetTransmitTXOptions { + numSigners?: number + startingSignerIndex?: number + gasLimit?: BigNumberish + gasPrice?: BigNumberish + performGas?: BigNumberish + performData?: string + checkBlockNum?: number + checkBlockHash?: string + logBlockHash?: BytesLike + txHash?: BytesLike + logIndex?: number + timestamp?: number + } + + const getTransmitTx = async ( + registry: IKeeperRegistry, + transmitter: Signer, + upkeepIds: BigNumber[], + overrides: GetTransmitTXOptions = {}, + ) => { + const latestBlock = await ethers.provider.getBlock('latest') + const configDigest = (await registry.getState()).state.latestConfigDigest + const config = { + numSigners: f + 1, + startingSignerIndex: 0, + performData: '0x', + performGas, + checkBlockNum: latestBlock.number, + checkBlockHash: latestBlock.hash, + logIndex: 0, + txHash: undefined, // assigned uniquely below + logBlockHash: undefined, // assigned uniquely below + timestamp: now(), + gasLimit: undefined, + gasPrice: undefined, + } + Object.assign(config, overrides) + const upkeeps: UpkeepData[] = [] + for (let i = 0; i < upkeepIds.length; i++) { + let trigger: string + switch (getTriggerType(upkeepIds[i])) { + case Trigger.CONDITION: + trigger = encodeBlockTrigger({ + blockNum: config.checkBlockNum, + blockHash: config.checkBlockHash, + }) + break + case Trigger.LOG: + trigger = encodeLogTrigger({ + logBlockHash: config.logBlockHash || ethers.utils.randomBytes(32), + txHash: config.txHash || ethers.utils.randomBytes(32), + logIndex: config.logIndex, + blockNum: config.checkBlockNum, + blockHash: config.checkBlockHash, + }) + break + } + upkeeps.push({ + Id: upkeepIds[i], + performGas: config.performGas, + trigger, + performData: config.performData, + }) + } + + const report = makeReport(upkeeps) + const reportContext = [configDigest, epochAndRound5_1, emptyBytes32] + const sigs = signReport( + reportContext, + report, + signers.slice( + config.startingSignerIndex, + config.startingSignerIndex + config.numSigners, + ), + ) + + type txOverride = { + gasLimit?: BigNumberish | Promise + gasPrice?: BigNumberish | Promise + } + const txOverrides: txOverride = {} + if (config.gasLimit) { + txOverrides.gasLimit = config.gasLimit + } + if (config.gasPrice) { + txOverrides.gasPrice = config.gasPrice + } + + return registry + .connect(transmitter) + .transmit( + [configDigest, epochAndRound5_1, emptyBytes32], + report, + sigs.rs, + sigs.ss, + sigs.vs, + txOverrides, + ) + } + + const getTransmitTxWithReport = async ( + registry: IKeeperRegistry, + transmitter: Signer, + report: BytesLike, + ) => { + const configDigest = (await registry.getState()).state.latestConfigDigest + const reportContext = [configDigest, epochAndRound5_1, emptyBytes32] + const sigs = signReport(reportContext, report, signers.slice(0, f + 1)) + + return registry + .connect(transmitter) + .transmit( + [configDigest, epochAndRound5_1, emptyBytes32], + report, + sigs.rs, + sigs.ss, + sigs.vs, + ) + } + + const setup = async () => { + linkToken = await linkTokenFactory.connect(owner).deploy() + gasPriceFeed = await mockV3AggregatorFactory + .connect(owner) + .deploy(0, gasWei) + linkEthFeed = await mockV3AggregatorFactory + .connect(owner) + .deploy(9, linkEth) + const upkeepTranscoderFactory = await ethers.getContractFactory( + 'UpkeepTranscoder4_0', + ) + transcoder = await upkeepTranscoderFactory.connect(owner).deploy() + mockArbGasInfo = await mockArbGasInfoFactory.connect(owner).deploy() + mockOVMGasPriceOracle = await mockOVMGasPriceOracleFactory + .connect(owner) + .deploy() + streamsLookupUpkeep = await streamsLookupUpkeepFactory + .connect(owner) + .deploy( + BigNumber.from('10000'), + BigNumber.from('100'), + false /* useArbBlock */, + true /* staging */, + false /* verify mercury response */, + ) + + const arbOracleCode = await ethers.provider.send('eth_getCode', [ + mockArbGasInfo.address, + ]) + await ethers.provider.send('hardhat_setCode', [ + '0x000000000000000000000000000000000000006C', + arbOracleCode, + ]) + + const optOracleCode = await ethers.provider.send('eth_getCode', [ + mockOVMGasPriceOracle.address, + ]) + await ethers.provider.send('hardhat_setCode', [ + '0x420000000000000000000000000000000000000F', + optOracleCode, + ]) + + const mockArbSys = await new MockArbSysFactory(owner).deploy() + const arbSysCode = await ethers.provider.send('eth_getCode', [ + mockArbSys.address, + ]) + await ethers.provider.send('hardhat_setCode', [ + '0x0000000000000000000000000000000000000064', + arbSysCode, + ]) + + config = { + paymentPremiumPPB, + flatFeeMicroLink, + checkGasLimit, + stalenessSeconds, + gasCeilingMultiplier, + minUpkeepSpend, + maxCheckDataSize, + maxPerformDataSize, + maxRevertDataSize, + maxPerformGas, + fallbackGasPrice, + fallbackLinkPrice, + transcoder: transcoder.address, + registrars: [], + upkeepPrivilegeManager: upkeepManager, + } + + baseConfig = [ + signerAddresses, + keeperAddresses, + f, + encodeConfig(config), + offchainVersion, + offchainBytes, + ] + + registry = await deployRegistry21( + owner, + Mode.DEFAULT, + linkToken.address, + linkEthFeed.address, + gasPriceFeed.address, + ) + + arbRegistry = await deployRegistry21( + owner, + Mode.ARBITRUM, + linkToken.address, + linkEthFeed.address, + gasPriceFeed.address, + ) + + opRegistry = await deployRegistry21( + owner, + Mode.OPTIMISM, + linkToken.address, + linkEthFeed.address, + gasPriceFeed.address, + ) + + mgRegistry = await deployRegistry21( + owner, + Mode.DEFAULT, + linkToken.address, + linkEthFeed.address, + gasPriceFeed.address, + ) + + blankRegistry = await deployRegistry21( + owner, + Mode.DEFAULT, + linkToken.address, + linkEthFeed.address, + gasPriceFeed.address, + ) + + registryConditionalOverhead = await registry.getConditionalGasOverhead() + registryLogOverhead = await registry.getLogGasOverhead() + registryPerSignerGasOverhead = await registry.getPerSignerGasOverhead() + registryPerPerformByteGasOverhead = + await registry.getPerPerformByteGasOverhead() + cancellationDelay = (await registry.getCancellationDelay()).toNumber() + + for (const reg of [registry, arbRegistry, opRegistry, mgRegistry]) { + await reg.connect(owner).setConfig(...baseConfig) + await reg.connect(owner).setPayees(payees) + await linkToken.connect(admin).approve(reg.address, toWei('1000')) + await linkToken.connect(owner).approve(reg.address, toWei('1000')) + } + + mock = await upkeepMockFactory.deploy() + await linkToken + .connect(owner) + .transfer(await admin.getAddress(), toWei('1000')) + let tx = await registry + .connect(owner) + ['registerUpkeep(address,uint32,address,bytes,bytes)']( + mock.address, + performGas, + await admin.getAddress(), + randomBytes, + '0x', + ) + upkeepId = await getUpkeepID(tx) + + autoFunderUpkeep = await upkeepAutoFunderFactory + .connect(owner) + .deploy(linkToken.address, registry.address) + tx = await registry + .connect(owner) + ['registerUpkeep(address,uint32,address,bytes,bytes)']( + autoFunderUpkeep.address, + performGas, + autoFunderUpkeep.address, + randomBytes, + '0x', + ) + afUpkeepId = await getUpkeepID(tx) + + ltUpkeep = await deployMockContract(owner, ILogAutomationactory.abi) + tx = await registry + .connect(owner) + ['registerUpkeep(address,uint32,address,uint8,bytes,bytes,bytes)']( + ltUpkeep.address, + performGas, + await admin.getAddress(), + Trigger.LOG, + '0x', + logTriggerConfig, + emptyBytes, + ) + logUpkeepId = await getUpkeepID(tx) + + await autoFunderUpkeep.setUpkeepId(afUpkeepId) + // Give enough funds for upkeep as well as to the upkeep contract + await linkToken + .connect(owner) + .transfer(autoFunderUpkeep.address, toWei('1000')) + + tx = await registry + .connect(owner) + ['registerUpkeep(address,uint32,address,bytes,bytes)']( + streamsLookupUpkeep.address, + performGas, + await admin.getAddress(), + randomBytes, + '0x', + ) + streamsLookupUpkeepId = await getUpkeepID(tx) + } + + const getMultipleUpkeepsDeployedAndFunded = async ( + numPassingConditionalUpkeeps: number, + numPassingLogUpkeeps: number, + numFailingUpkeeps: number, + ) => { + const passingConditionalUpkeepIds = [] + const passingLogUpkeepIds = [] + const failingUpkeepIds = [] + for (let i = 0; i < numPassingConditionalUpkeeps; i++) { + const mock = await upkeepMockFactory.deploy() + await mock.setCanPerform(true) + await mock.setPerformGasToBurn(BigNumber.from('0')) + const tx = await registry + .connect(owner) + ['registerUpkeep(address,uint32,address,bytes,bytes)']( + mock.address, + performGas, + await admin.getAddress(), + randomBytes, + '0x', + ) + const condUpkeepId = await getUpkeepID(tx) + passingConditionalUpkeepIds.push(condUpkeepId) + + // Add funds to passing upkeeps + await registry.connect(admin).addFunds(condUpkeepId, toWei('100')) + } + for (let i = 0; i < numPassingLogUpkeeps; i++) { + const mock = await upkeepMockFactory.deploy() + await mock.setCanPerform(true) + await mock.setPerformGasToBurn(BigNumber.from('0')) + const tx = await registry + .connect(owner) + ['registerUpkeep(address,uint32,address,uint8,bytes,bytes,bytes)']( + mock.address, + performGas, + await admin.getAddress(), + Trigger.LOG, + '0x', + logTriggerConfig, + emptyBytes, + ) + const logUpkeepId = await getUpkeepID(tx) + passingLogUpkeepIds.push(logUpkeepId) + + // Add funds to passing upkeeps + await registry.connect(admin).addFunds(logUpkeepId, toWei('100')) + } + for (let i = 0; i < numFailingUpkeeps; i++) { + const mock = await upkeepMockFactory.deploy() + await mock.setCanPerform(true) + await mock.setPerformGasToBurn(BigNumber.from('0')) + const tx = await registry + .connect(owner) + ['registerUpkeep(address,uint32,address,bytes,bytes)']( + mock.address, + performGas, + await admin.getAddress(), + randomBytes, + '0x', + ) + const failingUpkeepId = await getUpkeepID(tx) + failingUpkeepIds.push(failingUpkeepId) + } + return { + passingConditionalUpkeepIds, + passingLogUpkeepIds, + failingUpkeepIds, + } + } + + beforeEach(async () => { + await loadFixture(setup) + }) + + describe('#transmit', () => { + const fArray = [1, 5, 10] + + it('reverts when registry is paused', async () => { + await registry.connect(owner).pause() + await evmRevert( + getTransmitTx(registry, keeper1, [upkeepId]), + 'RegistryPaused()', + ) + }) + + it('reverts when called by non active transmitter', async () => { + await evmRevert( + getTransmitTx(registry, payee1, [upkeepId]), + 'OnlyActiveTransmitters()', + ) + }) + + it('reverts when report data lengths mismatches', async () => { + const upkeepIds = [] + const gasLimits: BigNumber[] = [] + const triggers: string[] = [] + const performDatas = [] + + upkeepIds.push(upkeepId) + gasLimits.push(performGas) + triggers.push('0x') + performDatas.push('0x') + // Push an extra perform data + performDatas.push('0x') + + const report = encodeReport({ + fastGasWei: 0, + linkNative: 0, + upkeepIds, + gasLimits, + triggers, + performDatas, + }) + + await evmRevert( + getTransmitTxWithReport(registry, keeper1, report), + 'InvalidReport()', + ) + }) + + it('returns early when invalid upkeepIds are included in report', async () => { + const tx = await getTransmitTx(registry, keeper1, [ + upkeepId.add(BigNumber.from('1')), + ]) + + const receipt = await tx.wait() + const cancelledUpkeepReportLogs = parseCancelledUpkeepReportLogs(receipt) + // exactly 1 CancelledUpkeepReport log should be emitted + assert.equal(cancelledUpkeepReportLogs.length, 1) + }) + + it('returns early when upkeep has insufficient funds', async () => { + const tx = await getTransmitTx(registry, keeper1, [upkeepId]) + const receipt = await tx.wait() + const insufficientFundsUpkeepReportLogs = + parseInsufficientFundsUpkeepReportLogs(receipt) + // exactly 1 InsufficientFundsUpkeepReportLogs log should be emitted + assert.equal(insufficientFundsUpkeepReportLogs.length, 1) + }) + + it('permits retrying log triggers after funds are added', async () => { + const txHash = ethers.utils.randomBytes(32) + let tx = await getTransmitTx(registry, keeper1, [logUpkeepId], { + txHash, + logIndex: 0, + }) + let receipt = await tx.wait() + const insufficientFundsLogs = + parseInsufficientFundsUpkeepReportLogs(receipt) + assert.equal(insufficientFundsLogs.length, 1) + registry.connect(admin).addFunds(logUpkeepId, toWei('100')) + tx = await getTransmitTx(registry, keeper1, [logUpkeepId], { + txHash, + logIndex: 0, + }) + receipt = await tx.wait() + const performedLogs = parseUpkeepPerformedLogs(receipt) + assert.equal(performedLogs.length, 1) + }) + + context('When the upkeep is funded', async () => { + beforeEach(async () => { + // Fund the upkeep + await Promise.all([ + registry.connect(admin).addFunds(upkeepId, toWei('100')), + registry.connect(admin).addFunds(logUpkeepId, toWei('100')), + ]) + }) + + it('handles duplicate upkeepIDs', async () => { + const tests: [string, BigNumber, number, number][] = [ + // [name, upkeep, num stale, num performed] + ['conditional', upkeepId, 1, 1], // checkBlocks must be sequential + ['log-trigger', logUpkeepId, 0, 2], // logs are deduped based on the "trigger ID" + ] + for (const [type, id, nStale, nPerformed] of tests) { + const tx = await getTransmitTx(registry, keeper1, [id, id]) + const receipt = await tx.wait() + const staleUpkeepReport = parseStaleUpkeepReportLogs(receipt) + const upkeepPerformedLogs = parseUpkeepPerformedLogs(receipt) + assert.equal( + staleUpkeepReport.length, + nStale, + `wrong log count for ${type} upkeep`, + ) + assert.equal( + upkeepPerformedLogs.length, + nPerformed, + `wrong log count for ${type} upkeep`, + ) + } + }) + + it('handles duplicate log triggers', async () => { + const logBlockHash = ethers.utils.randomBytes(32) + const txHash = ethers.utils.randomBytes(32) + const logIndex = 0 + const expectedDedupKey = ethers.utils.solidityKeccak256( + ['uint256', 'bytes32', 'bytes32', 'uint32'], + [logUpkeepId, logBlockHash, txHash, logIndex], + ) + assert.isFalse(await registry.hasDedupKey(expectedDedupKey)) + const tx = await getTransmitTx( + registry, + keeper1, + [logUpkeepId, logUpkeepId], + { logBlockHash, txHash, logIndex }, // will result in the same dedup key + ) + const receipt = await tx.wait() + const staleUpkeepReport = parseStaleUpkeepReportLogs(receipt) + const upkeepPerformedLogs = parseUpkeepPerformedLogs(receipt) + assert.equal(staleUpkeepReport.length, 1) + assert.equal(upkeepPerformedLogs.length, 1) + assert.isTrue(await registry.hasDedupKey(expectedDedupKey)) + await expect(tx) + .to.emit(registry, 'DedupKeyAdded') + .withArgs(expectedDedupKey) + }) + + it('returns early when check block number is less than last perform (block)', async () => { + // First perform an upkeep to put last perform block number on upkeep state + const tx = await getTransmitTx(registry, keeper1, [upkeepId]) + await tx.wait() + const lastPerformed = (await registry.getUpkeep(upkeepId)) + .lastPerformedBlockNumber + const lastPerformBlock = await ethers.provider.getBlock(lastPerformed) + assert.equal(lastPerformed.toString(), tx.blockNumber?.toString()) + // Try to transmit a report which has checkBlockNumber = lastPerformed-1, should result in stale report + const transmitTx = await getTransmitTx(registry, keeper1, [upkeepId], { + checkBlockNum: lastPerformBlock.number - 1, + checkBlockHash: lastPerformBlock.parentHash, + }) + const receipt = await transmitTx.wait() + const staleUpkeepReportLogs = parseStaleUpkeepReportLogs(receipt) + // exactly 1 StaleUpkeepReportLogs log should be emitted + assert.equal(staleUpkeepReportLogs.length, 1) + }) + + it('handles case when check block hash does not match', async () => { + const tests: [string, BigNumber][] = [ + ['conditional', upkeepId], + ['log-trigger', logUpkeepId], + ] + for (const [type, id] of tests) { + const latestBlock = await ethers.provider.getBlock('latest') + // Try to transmit a report which has incorrect checkBlockHash + const tx = await getTransmitTx(registry, keeper1, [id], { + checkBlockNum: latestBlock.number - 1, + checkBlockHash: latestBlock.hash, // should be latestBlock.parentHash + }) + + const receipt = await tx.wait() + const reorgedUpkeepReportLogs = parseReorgedUpkeepReportLogs(receipt) + // exactly 1 ReorgedUpkeepReportLogs log should be emitted + assert.equal( + reorgedUpkeepReportLogs.length, + 1, + `wrong log count for ${type} upkeep`, + ) + } + }) + + it('handles case when check block number is older than 256 blocks', async () => { + for (let i = 0; i < 256; i++) { + await ethers.provider.send('evm_mine', []) + } + const tests: [string, BigNumber][] = [ + ['conditional', upkeepId], + ['log-trigger', logUpkeepId], + ] + for (const [type, id] of tests) { + const latestBlock = await ethers.provider.getBlock('latest') + const old = await ethers.provider.getBlock(latestBlock.number - 256) + // Try to transmit a report which has incorrect checkBlockHash + const tx = await getTransmitTx(registry, keeper1, [id], { + checkBlockNum: old.number, + checkBlockHash: old.hash, + }) + + const receipt = await tx.wait() + const reorgedUpkeepReportLogs = parseReorgedUpkeepReportLogs(receipt) + // exactly 1 ReorgedUpkeepReportLogs log should be emitted + assert.equal( + reorgedUpkeepReportLogs.length, + 1, + `wrong log count for ${type} upkeep`, + ) + } + }) + + it('allows bypassing reorg protection with empty blockhash', async () => { + const tests: [string, BigNumber][] = [ + ['conditional', upkeepId], + ['log-trigger', logUpkeepId], + ] + for (const [type, id] of tests) { + const latestBlock = await ethers.provider.getBlock('latest') + const tx = await getTransmitTx(registry, keeper1, [id], { + checkBlockNum: latestBlock.number, + checkBlockHash: emptyBytes32, + }) + const receipt = await tx.wait() + const upkeepPerformedLogs = parseUpkeepPerformedLogs(receipt) + assert.equal( + upkeepPerformedLogs.length, + 1, + `wrong log count for ${type} upkeep`, + ) + } + }) + + it('allows very old trigger block numbers when bypassing reorg protection with empty blockhash', async () => { + // mine enough blocks so that blockhash(1) is unavailable + for (let i = 0; i <= 256; i++) { + await ethers.provider.send('evm_mine', []) + } + const tests: [string, BigNumber][] = [ + ['conditional', upkeepId], + ['log-trigger', logUpkeepId], + ] + for (const [type, id] of tests) { + const tx = await getTransmitTx(registry, keeper1, [id], { + checkBlockNum: 1, + checkBlockHash: emptyBytes32, + }) + const receipt = await tx.wait() + const upkeepPerformedLogs = parseUpkeepPerformedLogs(receipt) + assert.equal( + upkeepPerformedLogs.length, + 1, + `wrong log count for ${type} upkeep`, + ) + } + }) + + it('returns early when future block number is provided as trigger, irrespective of blockhash being present', async () => { + const tests: [string, BigNumber][] = [ + ['conditional', upkeepId], + ['log-trigger', logUpkeepId], + ] + for (const [type, id] of tests) { + const latestBlock = await ethers.provider.getBlock('latest') + + // Should fail when blockhash is empty + let tx = await getTransmitTx(registry, keeper1, [id], { + checkBlockNum: latestBlock.number + 100, + checkBlockHash: emptyBytes32, + }) + let receipt = await tx.wait() + let reorgedUpkeepReportLogs = parseReorgedUpkeepReportLogs(receipt) + // exactly 1 ReorgedUpkeepReportLogs log should be emitted + assert.equal( + reorgedUpkeepReportLogs.length, + 1, + `wrong log count for ${type} upkeep`, + ) + + // Should also fail when blockhash is not empty + tx = await getTransmitTx(registry, keeper1, [id], { + checkBlockNum: latestBlock.number + 100, + checkBlockHash: latestBlock.hash, + }) + receipt = await tx.wait() + reorgedUpkeepReportLogs = parseReorgedUpkeepReportLogs(receipt) + // exactly 1 ReorgedUpkeepReportLogs log should be emitted + assert.equal( + reorgedUpkeepReportLogs.length, + 1, + `wrong log count for ${type} upkeep`, + ) + } + }) + + it('returns early when upkeep is cancelled and cancellation delay has gone', async () => { + const latestBlockReport = await makeLatestBlockReport([upkeepId]) + await registry.connect(admin).cancelUpkeep(upkeepId) + + for (let i = 0; i < cancellationDelay; i++) { + await ethers.provider.send('evm_mine', []) + } + + const tx = await getTransmitTxWithReport( + registry, + keeper1, + latestBlockReport, + ) + + 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 the target cannot execute', async () => { + await mock.setCanPerform(false) + const tx = await getTransmitTx(registry, keeper1, [upkeepId]) + + const receipt = await tx.wait() + const upkeepPerformedLogs = parseUpkeepPerformedLogs(receipt) + // exactly 1 Upkeep Performed should be emitted + assert.equal(upkeepPerformedLogs.length, 1) + const upkeepPerformedLog = upkeepPerformedLogs[0] + + const success = upkeepPerformedLog.args.success + assert.equal(success, false) + }) + + it('does not revert if the target runs out of gas', async () => { + await mock.setCanPerform(false) + + const tx = await getTransmitTx(registry, keeper1, [upkeepId], { + performGas: 10, // too little gas + }) + + const receipt = await tx.wait() + const upkeepPerformedLogs = parseUpkeepPerformedLogs(receipt) + // exactly 1 Upkeep Performed should be emitted + assert.equal(upkeepPerformedLogs.length, 1) + const upkeepPerformedLog = upkeepPerformedLogs[0] + + const success = upkeepPerformedLog.args.success + assert.equal(success, false) + }) + + it('reverts if not enough gas supplied', async () => { + await evmRevert( + getTransmitTx(registry, keeper1, [upkeepId], { + gasLimit: performGas, + }), + ) + }) + + it('executes the data passed to the registry', async () => { + await mock.setCanPerform(true) + + const tx = await getTransmitTx(registry, keeper1, [upkeepId], { + performData: randomBytes, + }) + const receipt = await tx.wait() + + const upkeepPerformedWithABI = [ + 'event UpkeepPerformedWith(bytes upkeepData)', + ] + const iface = new ethers.utils.Interface(upkeepPerformedWithABI) + const parsedLogs = [] + for (let i = 0; i < receipt.logs.length; i++) { + const log = receipt.logs[i] + try { + parsedLogs.push(iface.parseLog(log)) + } catch (e) { + // ignore log + } + } + assert.equal(parsedLogs.length, 1) + assert.equal(parsedLogs[0].args.upkeepData, randomBytes) + }) + + it('uses actual execution price for payment and premium calculation', async () => { + // Actual multiplier is 2, but we set gasPrice to be 1x gasWei + const gasPrice = gasWei.mul(BigNumber.from('1')) + await mock.setCanPerform(true) + const registryPremiumBefore = (await registry.getState()).state + .totalPremium + const tx = await getTransmitTx(registry, keeper1, [upkeepId], { + gasPrice, + }) + const receipt = await tx.wait() + const registryPremiumAfter = (await registry.getState()).state + .totalPremium + const premium = registryPremiumAfter.sub(registryPremiumBefore) + + const upkeepPerformedLogs = parseUpkeepPerformedLogs(receipt) + // exactly 1 Upkeep Performed should be emitted + assert.equal(upkeepPerformedLogs.length, 1) + const upkeepPerformedLog = upkeepPerformedLogs[0] + + const gasUsed = upkeepPerformedLog.args.gasUsed + const gasOverhead = upkeepPerformedLog.args.gasOverhead + const totalPayment = upkeepPerformedLog.args.totalPayment + + assert.equal( + linkForGas( + gasUsed, + gasOverhead, + BigNumber.from('1'), // Not the config multiplier, but the actual gas used + paymentPremiumPPB, + flatFeeMicroLink, + ).total.toString(), + totalPayment.toString(), + ) + + assert.equal( + linkForGas( + gasUsed, + gasOverhead, + BigNumber.from('1'), // Not the config multiplier, but the actual gas used + paymentPremiumPPB, + flatFeeMicroLink, + ).premium.toString(), + premium.toString(), + ) + }) + + it('only pays at a rate up to the gas ceiling [ @skip-coverage ]', async () => { + // Actual multiplier is 2, but we set gasPrice to be 10x + const gasPrice = gasWei.mul(BigNumber.from('10')) + await mock.setCanPerform(true) + + const tx = await getTransmitTx(registry, keeper1, [upkeepId], { + gasPrice, + }) + const receipt = await tx.wait() + const upkeepPerformedLogs = parseUpkeepPerformedLogs(receipt) + // exactly 1 Upkeep Performed should be emitted + assert.equal(upkeepPerformedLogs.length, 1) + const upkeepPerformedLog = upkeepPerformedLogs[0] + + const gasUsed = upkeepPerformedLog.args.gasUsed + const gasOverhead = upkeepPerformedLog.args.gasOverhead + const totalPayment = upkeepPerformedLog.args.totalPayment + + assert.equal( + linkForGas( + gasUsed, + gasOverhead, + gasCeilingMultiplier, // Should be same with exisitng multiplier + paymentPremiumPPB, + flatFeeMicroLink, + ).total.toString(), + totalPayment.toString(), + ) + }) + + it('correctly accounts for l payment', async () => { + await mock.setCanPerform(true) + // Same as MockArbGasInfo.sol + const l1CostWeiArb = BigNumber.from(1000000) + + let tx = await arbRegistry + .connect(owner) + ['registerUpkeep(address,uint32,address,bytes,bytes)']( + mock.address, + performGas, + await admin.getAddress(), + randomBytes, + '0x', + ) + const testUpkeepId = await getUpkeepID(tx) + await arbRegistry.connect(owner).addFunds(testUpkeepId, toWei('100')) + + // Do the thing + tx = await getTransmitTx( + arbRegistry, + keeper1, + [testUpkeepId], + + { gasPrice: gasWei.mul('5') }, // High gas price so that it gets capped + ) + const receipt = await tx.wait() + const upkeepPerformedLogs = parseUpkeepPerformedLogs(receipt) + // exactly 1 Upkeep Performed should be emitted + assert.equal(upkeepPerformedLogs.length, 1) + const upkeepPerformedLog = upkeepPerformedLogs[0] + + const gasUsed = upkeepPerformedLog.args.gasUsed + const gasOverhead = upkeepPerformedLog.args.gasOverhead + const totalPayment = upkeepPerformedLog.args.totalPayment + + assert.equal( + linkForGas( + gasUsed, + gasOverhead, + gasCeilingMultiplier, + paymentPremiumPPB, + flatFeeMicroLink, + l1CostWeiArb.div(gasCeilingMultiplier), // Dividing by gasCeilingMultiplier as it gets multiplied later + ).total.toString(), + totalPayment.toString(), + ) + }) + + itMaybe('can self fund', async () => { + const maxPayment = await registry.getMaxPaymentForGas( + Trigger.CONDITION, + performGas, + ) + + // First set auto funding amount to 0 and verify that balance is deducted upon performUpkeep + let initialBalance = toWei('100') + await registry.connect(owner).addFunds(afUpkeepId, initialBalance) + await autoFunderUpkeep.setAutoFundLink(0) + await autoFunderUpkeep.setIsEligible(true) + await getTransmitTx(registry, keeper1, [afUpkeepId]) + + let postUpkeepBalance = (await registry.getUpkeep(afUpkeepId)).balance + assert.isTrue(postUpkeepBalance.lt(initialBalance)) // Balance should be deducted + assert.isTrue(postUpkeepBalance.gte(initialBalance.sub(maxPayment))) // Balance should not be deducted more than maxPayment + + // Now set auto funding amount to 100 wei and verify that the balance increases + initialBalance = postUpkeepBalance + const autoTopupAmount = toWei('100') + await autoFunderUpkeep.setAutoFundLink(autoTopupAmount) + await autoFunderUpkeep.setIsEligible(true) + await getTransmitTx(registry, keeper1, [afUpkeepId]) + + postUpkeepBalance = (await registry.getUpkeep(afUpkeepId)).balance + // Balance should increase by autoTopupAmount and decrease by max maxPayment + assert.isTrue( + postUpkeepBalance.gte( + initialBalance.add(autoTopupAmount).sub(maxPayment), + ), + ) + }) + + it('can self cancel', async () => { + await registry.connect(owner).addFunds(afUpkeepId, toWei('100')) + + await autoFunderUpkeep.setIsEligible(true) + await autoFunderUpkeep.setShouldCancel(true) + + let registration = await registry.getUpkeep(afUpkeepId) + const oldExpiration = registration.maxValidBlocknumber + + // Do the thing + await getTransmitTx(registry, keeper1, [afUpkeepId]) + + // Verify upkeep gets cancelled + registration = await registry.getUpkeep(afUpkeepId) + const newExpiration = registration.maxValidBlocknumber + assert.isTrue(newExpiration.lt(oldExpiration)) + }) + + it('reverts when configDigest mismatches', async () => { + const report = await makeLatestBlockReport([upkeepId]) + const reportContext = [emptyBytes32, epochAndRound5_1, emptyBytes32] // wrong config digest + const sigs = signReport(reportContext, report, signers.slice(0, f + 1)) + await evmRevert( + registry + .connect(keeper1) + .transmit( + [reportContext[0], reportContext[1], reportContext[2]], + report, + sigs.rs, + sigs.ss, + sigs.vs, + ), + 'ConfigDigestMismatch()', + ) + }) + + it('reverts with incorrect number of signatures', async () => { + const configDigest = (await registry.getState()).state + .latestConfigDigest + const report = await makeLatestBlockReport([upkeepId]) + const reportContext = [configDigest, epochAndRound5_1, emptyBytes32] // wrong config digest + const sigs = signReport(reportContext, report, signers.slice(0, f + 2)) + await evmRevert( + registry + .connect(keeper1) + .transmit( + [reportContext[0], reportContext[1], reportContext[2]], + report, + sigs.rs, + sigs.ss, + sigs.vs, + ), + 'IncorrectNumberOfSignatures()', + ) + }) + + it('reverts with invalid signature for inactive signers', async () => { + const configDigest = (await registry.getState()).state + .latestConfigDigest + const report = await makeLatestBlockReport([upkeepId]) + const reportContext = [configDigest, epochAndRound5_1, emptyBytes32] // wrong config digest + const sigs = signReport(reportContext, report, [ + new ethers.Wallet(ethers.Wallet.createRandom()), + new ethers.Wallet(ethers.Wallet.createRandom()), + ]) + await evmRevert( + registry + .connect(keeper1) + .transmit( + [reportContext[0], reportContext[1], reportContext[2]], + report, + sigs.rs, + sigs.ss, + sigs.vs, + ), + 'OnlyActiveSigners()', + ) + }) + + it('reverts with invalid signature for duplicated signers', async () => { + const configDigest = (await registry.getState()).state + .latestConfigDigest + const report = await makeLatestBlockReport([upkeepId]) + const reportContext = [configDigest, epochAndRound5_1, emptyBytes32] // wrong config digest + const sigs = signReport(reportContext, report, [signer1, signer1]) + await evmRevert( + registry + .connect(keeper1) + .transmit( + [reportContext[0], reportContext[1], reportContext[2]], + report, + sigs.rs, + sigs.ss, + sigs.vs, + ), + 'DuplicateSigners()', + ) + }) + + itMaybe( + 'has a large enough gas overhead to cover upkeep that use all its gas [ @skip-coverage ]', + async () => { + await registry.connect(owner).setConfigTypeSafe( + signerAddresses, + keeperAddresses, + 10, // maximise f to maximise overhead + config, + offchainVersion, + offchainBytes, + ) + const tx = await registry + .connect(owner) + ['registerUpkeep(address,uint32,address,bytes,bytes)']( + mock.address, + maxPerformGas, // max allowed gas + await admin.getAddress(), + randomBytes, + '0x', + ) + const testUpkeepId = await getUpkeepID(tx) + await registry.connect(admin).addFunds(testUpkeepId, toWei('100')) + + let performData = '0x' + for (let i = 0; i < maxPerformDataSize.toNumber(); i++) { + performData += '11' + } // max allowed performData + + await mock.setCanPerform(true) + await mock.setPerformGasToBurn(maxPerformGas) + + await getTransmitTx(registry, keeper1, [testUpkeepId], { + gasLimit: maxPerformGas.add(transmitGasOverhead), + numSigners: 11, + performData, + }) // Should not revert + }, + ) + + itMaybe( + 'performs upkeep, deducts payment, updates lastPerformed and emits events', + async () => { + await mock.setCanPerform(true) + + for (const i in fArray) { + const newF = fArray[i] + await registry + .connect(owner) + .setConfigTypeSafe( + signerAddresses, + keeperAddresses, + newF, + config, + offchainVersion, + offchainBytes, + ) + const checkBlock = await ethers.provider.getBlock('latest') + + const keeperBefore = await registry.getTransmitterInfo( + await keeper1.getAddress(), + ) + const registrationBefore = await registry.getUpkeep(upkeepId) + const registryPremiumBefore = (await registry.getState()).state + .totalPremium + const keeperLinkBefore = await linkToken.balanceOf( + await keeper1.getAddress(), + ) + const registryLinkBefore = await linkToken.balanceOf( + registry.address, + ) + + // Do the thing + const tx = await getTransmitTx(registry, keeper1, [upkeepId], { + checkBlockNum: checkBlock.number, + checkBlockHash: checkBlock.hash, + numSigners: newF + 1, + }) + + const receipt = await tx.wait() + + const upkeepPerformedLogs = parseUpkeepPerformedLogs(receipt) + // exactly 1 Upkeep Performed should be emitted + assert.equal(upkeepPerformedLogs.length, 1) + const upkeepPerformedLog = upkeepPerformedLogs[0] + + const id = upkeepPerformedLog.args.id + const success = upkeepPerformedLog.args.success + const trigger = upkeepPerformedLog.args.trigger + const gasUsed = upkeepPerformedLog.args.gasUsed + const gasOverhead = upkeepPerformedLog.args.gasOverhead + const totalPayment = upkeepPerformedLog.args.totalPayment + assert.equal(id.toString(), upkeepId.toString()) + assert.equal(success, true) + assert.equal( + trigger, + encodeBlockTrigger({ + blockNum: checkBlock.number, + blockHash: checkBlock.hash, + }), + ) + assert.isTrue(gasUsed.gt(BigNumber.from('0'))) + assert.isTrue(gasOverhead.gt(BigNumber.from('0'))) + assert.isTrue(totalPayment.gt(BigNumber.from('0'))) + + const keeperAfter = await registry.getTransmitterInfo( + await keeper1.getAddress(), + ) + const registrationAfter = await registry.getUpkeep(upkeepId) + const keeperLinkAfter = await linkToken.balanceOf( + await keeper1.getAddress(), + ) + const registryLinkAfter = await linkToken.balanceOf( + registry.address, + ) + const registryPremiumAfter = (await registry.getState()).state + .totalPremium + const premium = registryPremiumAfter.sub(registryPremiumBefore) + // Keeper payment is gasPayment + premium / num keepers + const keeperPayment = totalPayment + .sub(premium) + .add(premium.div(BigNumber.from(keeperAddresses.length))) + + assert.equal( + keeperAfter.balance.sub(keeperPayment).toString(), + keeperBefore.balance.toString(), + ) + assert.equal( + registrationBefore.balance.sub(totalPayment).toString(), + registrationAfter.balance.toString(), + ) + assert.isTrue(keeperLinkAfter.eq(keeperLinkBefore)) + assert.isTrue(registryLinkBefore.eq(registryLinkAfter)) + + // Amount spent should be updated correctly + assert.equal( + registrationAfter.amountSpent.sub(totalPayment).toString(), + registrationBefore.amountSpent.toString(), + ) + assert.isTrue( + registrationAfter.amountSpent + .sub(registrationBefore.amountSpent) + .eq(registrationBefore.balance.sub(registrationAfter.balance)), + ) + // Last perform block number should be updated + assert.equal( + registrationAfter.lastPerformedBlockNumber.toString(), + tx.blockNumber?.toString(), + ) + + // Latest epoch should be 5 + assert.equal((await registry.getState()).state.latestEpoch, 5) + } + }, + ) + + describeMaybe( + 'Gas benchmarking conditional upkeeps [ @skip-coverage ]', + function () { + const fs = [1, 10] + fs.forEach(function (newF) { + it( + 'When f=' + + newF + + ' calculates gas overhead appropriately within a margin for different scenarios', + async () => { + // Perform the upkeep once to remove non-zero storage slots and have predictable gas measurement + let tx = await getTransmitTx(registry, keeper1, [upkeepId]) + await tx.wait() + + // Different test scenarios + let longBytes = '0x' + for (let i = 0; i < maxPerformDataSize.toNumber(); i++) { + longBytes += '11' + } + const upkeepSuccessArray = [true, false] + const performGasArray = [5000, performGas] + const performDataArray = ['0x', longBytes] + + for (const i in upkeepSuccessArray) { + for (const j in performGasArray) { + for (const k in performDataArray) { + const upkeepSuccess = upkeepSuccessArray[i] + const performGas = performGasArray[j] + const performData = performDataArray[k] + + await mock.setCanPerform(upkeepSuccess) + await mock.setPerformGasToBurn(performGas) + await registry + .connect(owner) + .setConfigTypeSafe( + signerAddresses, + keeperAddresses, + newF, + config, + offchainVersion, + offchainBytes, + ) + tx = await getTransmitTx(registry, keeper1, [upkeepId], { + numSigners: newF + 1, + performData, + }) + const receipt = await tx.wait() + const upkeepPerformedLogs = + parseUpkeepPerformedLogs(receipt) + // exactly 1 Upkeep Performed should be emitted + assert.equal(upkeepPerformedLogs.length, 1) + const upkeepPerformedLog = upkeepPerformedLogs[0] + + const upkeepGasUsed = upkeepPerformedLog.args.gasUsed + const chargedGasOverhead = + upkeepPerformedLog.args.gasOverhead + const actualGasOverhead = + receipt.gasUsed.sub(upkeepGasUsed) + + assert.isTrue(upkeepGasUsed.gt(BigNumber.from('0'))) + assert.isTrue(chargedGasOverhead.gt(BigNumber.from('0'))) + + console.log( + 'Gas Benchmarking conditional upkeeps:', + 'upkeepSuccess=', + upkeepSuccess, + 'performGas=', + performGas.toString(), + 'performData length=', + performData.length / 2 - 1, + 'sig verification ( f =', + newF, + '): calculated overhead: ', + chargedGasOverhead.toString(), + ' actual overhead: ', + actualGasOverhead.toString(), + ' margin over gasUsed: ', + chargedGasOverhead.sub(actualGasOverhead).toString(), + ) + + // Overhead should not get capped + const gasOverheadCap = registryConditionalOverhead + .add( + registryPerSignerGasOverhead.mul( + BigNumber.from(newF + 1), + ), + ) + .add( + BigNumber.from( + registryPerPerformByteGasOverhead.toNumber() * + performData.length, + ), + ) + const gasCapMinusOverhead = + gasOverheadCap.sub(chargedGasOverhead) + assert.isTrue( + gasCapMinusOverhead.gt(BigNumber.from(0)), + 'Gas overhead got capped. Verify gas overhead variables in test match those in the registry. To not have the overheads capped increase REGISTRY_GAS_OVERHEAD by atleast ' + + gasCapMinusOverhead.toString(), + ) + // total gas charged should be greater than tx gas but within gasCalculationMargin + assert.isTrue( + chargedGasOverhead.gt(actualGasOverhead), + 'Gas overhead calculated is too low, increase account gas variables (ACCOUNTING_FIXED_GAS_OVERHEAD/ACCOUNTING_PER_SIGNER_GAS_OVERHEAD) by atleast ' + + actualGasOverhead.sub(chargedGasOverhead).toString(), + ) + + assert.isTrue( + chargedGasOverhead + .sub(actualGasOverhead) + .lt(gasCalculationMargin), + ), + 'Gas overhead calculated is too high, decrease account gas variables (ACCOUNTING_FIXED_GAS_OVERHEAD/ACCOUNTING_PER_SIGNER_GAS_OVERHEAD) by atleast ' + + chargedGasOverhead + .sub(chargedGasOverhead) + .sub(gasCalculationMargin) + .toString() + } + } + } + }, + ) + }) + }, + ) + + describeMaybe( + 'Gas benchmarking log upkeeps [ @skip-coverage ]', + function () { + const fs = [1, 10] + fs.forEach(function (newF) { + it( + 'When f=' + + newF + + ' calculates gas overhead appropriately within a margin', + async () => { + // Perform the upkeep once to remove non-zero storage slots and have predictable gas measurement + let tx = await getTransmitTx(registry, keeper1, [logUpkeepId]) + await tx.wait() + const performData = '0x' + await mock.setCanPerform(true) + await mock.setPerformGasToBurn(performGas) + await registry.setConfigTypeSafe( + signerAddresses, + keeperAddresses, + newF, + config, + offchainVersion, + offchainBytes, + ) + tx = await getTransmitTx(registry, keeper1, [logUpkeepId], { + numSigners: newF + 1, + performData, + }) + const receipt = await tx.wait() + const upkeepPerformedLogs = parseUpkeepPerformedLogs(receipt) + // exactly 1 Upkeep Performed should be emitted + assert.equal(upkeepPerformedLogs.length, 1) + const upkeepPerformedLog = upkeepPerformedLogs[0] + + const upkeepGasUsed = upkeepPerformedLog.args.gasUsed + const chargedGasOverhead = upkeepPerformedLog.args.gasOverhead + const actualGasOverhead = receipt.gasUsed.sub(upkeepGasUsed) + + assert.isTrue(upkeepGasUsed.gt(BigNumber.from('0'))) + assert.isTrue(chargedGasOverhead.gt(BigNumber.from('0'))) + + console.log( + 'Gas Benchmarking log upkeeps:', + 'upkeepSuccess=', + true, + 'performGas=', + performGas.toString(), + 'performData length=', + performData.length / 2 - 1, + 'sig verification ( f =', + newF, + '): calculated overhead: ', + chargedGasOverhead.toString(), + ' actual overhead: ', + actualGasOverhead.toString(), + ' margin over gasUsed: ', + chargedGasOverhead.sub(actualGasOverhead).toString(), + ) + + // Overhead should not get capped + const gasOverheadCap = registryLogOverhead + .add( + registryPerSignerGasOverhead.mul(BigNumber.from(newF + 1)), + ) + .add( + BigNumber.from( + registryPerPerformByteGasOverhead.toNumber() * + performData.length, + ), + ) + const gasCapMinusOverhead = + gasOverheadCap.sub(chargedGasOverhead) + assert.isTrue( + gasCapMinusOverhead.gt(BigNumber.from(0)), + 'Gas overhead got capped. Verify gas overhead variables in test match those in the registry. To not have the overheads capped increase REGISTRY_GAS_OVERHEAD by atleast ' + + gasCapMinusOverhead.toString(), + ) + // total gas charged should be greater than tx gas but within gasCalculationMargin + assert.isTrue( + chargedGasOverhead.gt(actualGasOverhead), + 'Gas overhead calculated is too low, increase account gas variables (ACCOUNTING_FIXED_GAS_OVERHEAD/ACCOUNTING_PER_SIGNER_GAS_OVERHEAD) by atleast ' + + actualGasOverhead.sub(chargedGasOverhead).toString(), + ) + + assert.isTrue( + chargedGasOverhead + .sub(actualGasOverhead) + .lt(gasCalculationMargin), + ), + 'Gas overhead calculated is too high, decrease account gas variables (ACCOUNTING_FIXED_GAS_OVERHEAD/ACCOUNTING_PER_SIGNER_GAS_OVERHEAD) by atleast ' + + chargedGasOverhead + .sub(chargedGasOverhead) + .sub(gasCalculationMargin) + .toString() + }, + ) + }) + }, + ) + }) + }) + + describeMaybe( + '#transmit with upkeep batches [ @skip-coverage ]', + function () { + const numPassingConditionalUpkeepsArray = [0, 1, 5] + const numPassingLogUpkeepsArray = [0, 1, 5] + const numFailingUpkeepsArray = [0, 3] + + for (let idx = 0; idx < numPassingConditionalUpkeepsArray.length; idx++) { + for (let jdx = 0; jdx < numPassingLogUpkeepsArray.length; jdx++) { + for (let kdx = 0; kdx < numFailingUpkeepsArray.length; kdx++) { + const numPassingConditionalUpkeeps = + numPassingConditionalUpkeepsArray[idx] + const numPassingLogUpkeeps = numPassingLogUpkeepsArray[jdx] + const numFailingUpkeeps = numFailingUpkeepsArray[kdx] + if ( + numPassingConditionalUpkeeps == 0 && + numPassingLogUpkeeps == 0 + ) { + continue + } + it( + '[Conditional:' + + numPassingConditionalUpkeeps + + ',Log:' + + numPassingLogUpkeeps + + ',Failures:' + + numFailingUpkeeps + + '] performs successful upkeeps and does not charge failing upkeeps', + async () => { + const allUpkeeps = await getMultipleUpkeepsDeployedAndFunded( + numPassingConditionalUpkeeps, + numPassingLogUpkeeps, + numFailingUpkeeps, + ) + const passingConditionalUpkeepIds = + allUpkeeps.passingConditionalUpkeepIds + const passingLogUpkeepIds = allUpkeeps.passingLogUpkeepIds + const failingUpkeepIds = allUpkeeps.failingUpkeepIds + + const keeperBefore = await registry.getTransmitterInfo( + await keeper1.getAddress(), + ) + const keeperLinkBefore = await linkToken.balanceOf( + await keeper1.getAddress(), + ) + const registryLinkBefore = await linkToken.balanceOf( + registry.address, + ) + const registryPremiumBefore = (await registry.getState()).state + .totalPremium + const registrationConditionalPassingBefore = await Promise.all( + passingConditionalUpkeepIds.map(async (id) => { + const reg = await registry.getUpkeep(BigNumber.from(id)) + assert.equal(reg.lastPerformedBlockNumber.toString(), '0') + return reg + }), + ) + const registrationLogPassingBefore = await Promise.all( + passingLogUpkeepIds.map(async (id) => { + const reg = await registry.getUpkeep(BigNumber.from(id)) + assert.equal(reg.lastPerformedBlockNumber.toString(), '0') + return reg + }), + ) + const registrationFailingBefore = await Promise.all( + failingUpkeepIds.map(async (id) => { + const reg = await registry.getUpkeep(BigNumber.from(id)) + assert.equal(reg.lastPerformedBlockNumber.toString(), '0') + return reg + }), + ) + + const tx = await getTransmitTx( + registry, + keeper1, + passingConditionalUpkeepIds.concat( + passingLogUpkeepIds.concat(failingUpkeepIds), + ), + ) + + const receipt = await tx.wait() + const upkeepPerformedLogs = parseUpkeepPerformedLogs(receipt) + // exactly numPassingUpkeeps Upkeep Performed should be emitted + assert.equal( + upkeepPerformedLogs.length, + numPassingConditionalUpkeeps + numPassingLogUpkeeps, + ) + const insufficientFundsLogs = + parseInsufficientFundsUpkeepReportLogs(receipt) + // exactly numFailingUpkeeps Upkeep Performed should be emitted + assert.equal(insufficientFundsLogs.length, numFailingUpkeeps) + + const keeperAfter = await registry.getTransmitterInfo( + await keeper1.getAddress(), + ) + const keeperLinkAfter = await linkToken.balanceOf( + await keeper1.getAddress(), + ) + const registryLinkAfter = await linkToken.balanceOf( + registry.address, + ) + const registrationConditionalPassingAfter = await Promise.all( + passingConditionalUpkeepIds.map(async (id) => { + return await registry.getUpkeep(BigNumber.from(id)) + }), + ) + const registrationLogPassingAfter = await Promise.all( + passingLogUpkeepIds.map(async (id) => { + return await registry.getUpkeep(BigNumber.from(id)) + }), + ) + const registrationFailingAfter = await Promise.all( + failingUpkeepIds.map(async (id) => { + return await registry.getUpkeep(BigNumber.from(id)) + }), + ) + const registryPremiumAfter = (await registry.getState()).state + .totalPremium + const premium = registryPremiumAfter.sub(registryPremiumBefore) + + let netPayment = BigNumber.from('0') + for (let i = 0; i < numPassingConditionalUpkeeps; i++) { + const id = upkeepPerformedLogs[i].args.id + const gasUsed = upkeepPerformedLogs[i].args.gasUsed + const gasOverhead = upkeepPerformedLogs[i].args.gasOverhead + const totalPayment = upkeepPerformedLogs[i].args.totalPayment + + expect(id).to.equal(passingConditionalUpkeepIds[i]) + assert.isTrue(gasUsed.gt(BigNumber.from('0'))) + assert.isTrue(gasOverhead.gt(BigNumber.from('0'))) + assert.isTrue(totalPayment.gt(BigNumber.from('0'))) + + // Balance should be deducted + assert.equal( + registrationConditionalPassingBefore[i].balance + .sub(totalPayment) + .toString(), + registrationConditionalPassingAfter[i].balance.toString(), + ) + + // Amount spent should be updated correctly + assert.equal( + registrationConditionalPassingAfter[i].amountSpent + .sub(totalPayment) + .toString(), + registrationConditionalPassingBefore[ + i + ].amountSpent.toString(), + ) + + // Last perform block number should be updated + assert.equal( + registrationConditionalPassingAfter[ + i + ].lastPerformedBlockNumber.toString(), + tx.blockNumber?.toString(), + ) + + netPayment = netPayment.add(totalPayment) + } + + for (let i = 0; i < numPassingLogUpkeeps; i++) { + const id = + upkeepPerformedLogs[numPassingConditionalUpkeeps + i].args + .id + const gasUsed = + upkeepPerformedLogs[numPassingConditionalUpkeeps + i].args + .gasUsed + const gasOverhead = + upkeepPerformedLogs[numPassingConditionalUpkeeps + i].args + .gasOverhead + const totalPayment = + upkeepPerformedLogs[numPassingConditionalUpkeeps + i].args + .totalPayment + + expect(id).to.equal(passingLogUpkeepIds[i]) + assert.isTrue(gasUsed.gt(BigNumber.from('0'))) + assert.isTrue(gasOverhead.gt(BigNumber.from('0'))) + assert.isTrue(totalPayment.gt(BigNumber.from('0'))) + + // Balance should be deducted + assert.equal( + registrationLogPassingBefore[i].balance + .sub(totalPayment) + .toString(), + registrationLogPassingAfter[i].balance.toString(), + ) + + // Amount spent should be updated correctly + assert.equal( + registrationLogPassingAfter[i].amountSpent + .sub(totalPayment) + .toString(), + registrationLogPassingBefore[i].amountSpent.toString(), + ) + + // Last perform block number should not be updated for log triggers + assert.equal( + registrationLogPassingAfter[ + i + ].lastPerformedBlockNumber.toString(), + '0', + ) + + netPayment = netPayment.add(totalPayment) + } + + for (let i = 0; i < numFailingUpkeeps; i++) { + // InsufficientFunds log should be emitted + const id = insufficientFundsLogs[i].args.id + expect(id).to.equal(failingUpkeepIds[i]) + + // Balance and amount spent should be same + assert.equal( + registrationFailingBefore[i].balance.toString(), + registrationFailingAfter[i].balance.toString(), + ) + assert.equal( + registrationFailingBefore[i].amountSpent.toString(), + registrationFailingAfter[i].amountSpent.toString(), + ) + + // Last perform block number should not be updated + assert.equal( + registrationFailingAfter[ + i + ].lastPerformedBlockNumber.toString(), + '0', + ) + } + + // Keeper payment is gasPayment + premium / num keepers + const keeperPayment = netPayment + .sub(premium) + .add(premium.div(BigNumber.from(keeperAddresses.length))) + + // Keeper should be paid net payment for all passed upkeeps + assert.equal( + keeperAfter.balance.sub(keeperPayment).toString(), + keeperBefore.balance.toString(), + ) + + assert.isTrue(keeperLinkAfter.eq(keeperLinkBefore)) + assert.isTrue(registryLinkBefore.eq(registryLinkAfter)) + }, + ) + + it( + '[Conditional:' + + numPassingConditionalUpkeeps + + ',Log' + + numPassingLogUpkeeps + + ',Failures:' + + numFailingUpkeeps + + '] splits gas overhead appropriately among performed upkeeps [ @skip-coverage ]', + async () => { + const allUpkeeps = await getMultipleUpkeepsDeployedAndFunded( + numPassingConditionalUpkeeps, + numPassingLogUpkeeps, + numFailingUpkeeps, + ) + const passingConditionalUpkeepIds = + allUpkeeps.passingConditionalUpkeepIds + const passingLogUpkeepIds = allUpkeeps.passingLogUpkeepIds + const failingUpkeepIds = allUpkeeps.failingUpkeepIds + + // Perform the upkeeps once to remove non-zero storage slots and have predictable gas measurement + let tx = await getTransmitTx( + registry, + keeper1, + passingConditionalUpkeepIds.concat( + passingLogUpkeepIds.concat(failingUpkeepIds), + ), + ) + + await tx.wait() + + // Do the actual thing + + tx = await getTransmitTx( + registry, + keeper1, + passingConditionalUpkeepIds.concat( + passingLogUpkeepIds.concat(failingUpkeepIds), + ), + ) + + const receipt = await tx.wait() + const upkeepPerformedLogs = parseUpkeepPerformedLogs(receipt) + // exactly numPassingUpkeeps Upkeep Performed should be emitted + assert.equal( + upkeepPerformedLogs.length, + numPassingConditionalUpkeeps + numPassingLogUpkeeps, + ) + + const gasConditionalOverheadCap = + registryConditionalOverhead.add( + registryPerSignerGasOverhead.mul(BigNumber.from(f + 1)), + ) + const gasLogOverheadCap = registryLogOverhead.add( + registryPerSignerGasOverhead.mul(BigNumber.from(f + 1)), + ) + + const overheadCanGetCapped = + numFailingUpkeeps > 0 && + numPassingConditionalUpkeeps <= 1 && + numPassingLogUpkeeps <= 1 + // Can happen if there are failing upkeeps and only 1 successful upkeep of each type + let netGasUsedPlusOverhead = BigNumber.from('0') + + for (let i = 0; i < numPassingConditionalUpkeeps; i++) { + const gasUsed = upkeepPerformedLogs[i].args.gasUsed + const gasOverhead = upkeepPerformedLogs[i].args.gasOverhead + + assert.isTrue(gasUsed.gt(BigNumber.from('0'))) + assert.isTrue(gasOverhead.gt(BigNumber.from('0'))) + + // Overhead should not exceed capped + assert.isTrue(gasOverhead.lte(gasConditionalOverheadCap)) + + // Overhead should be same for every upkeep since they have equal performData, hence same caps + assert.isTrue( + gasOverhead.eq(upkeepPerformedLogs[0].args.gasOverhead), + ) + + netGasUsedPlusOverhead = netGasUsedPlusOverhead + .add(gasUsed) + .add(gasOverhead) + } + for (let i = 0; i < numPassingLogUpkeeps; i++) { + const gasUsed = + upkeepPerformedLogs[numPassingConditionalUpkeeps + i].args + .gasUsed + const gasOverhead = + upkeepPerformedLogs[numPassingConditionalUpkeeps + i].args + .gasOverhead + + assert.isTrue(gasUsed.gt(BigNumber.from('0'))) + assert.isTrue(gasOverhead.gt(BigNumber.from('0'))) + + // Overhead should not exceed capped + assert.isTrue(gasOverhead.lte(gasLogOverheadCap)) + + // Overhead should be same for every upkeep since they have equal performData, hence same caps + assert.isTrue( + gasOverhead.eq( + upkeepPerformedLogs[numPassingConditionalUpkeeps].args + .gasOverhead, + ), + ) + + netGasUsedPlusOverhead = netGasUsedPlusOverhead + .add(gasUsed) + .add(gasOverhead) + } + + const overheadsGotCapped = + (numPassingConditionalUpkeeps > 0 && + upkeepPerformedLogs[0].args.gasOverhead.eq( + gasConditionalOverheadCap, + )) || + (numPassingLogUpkeeps > 0 && + upkeepPerformedLogs[ + numPassingConditionalUpkeeps + ].args.gasOverhead.eq(gasLogOverheadCap)) + // Should only get capped in certain scenarios + if (overheadsGotCapped) { + assert.isTrue( + overheadCanGetCapped, + 'Gas overhead got capped. Verify gas overhead variables in test match those in the registry. To not have the overheads capped increase REGISTRY_GAS_OVERHEAD', + ) + } + + console.log( + 'Gas Benchmarking - batching (passedConditionalUpkeeps: ', + numPassingConditionalUpkeeps, + 'passedLogUpkeeps:', + numPassingLogUpkeeps, + 'failedUpkeeps:', + numFailingUpkeeps, + '): ', + 'overheadsGotCapped', + overheadsGotCapped, + numPassingConditionalUpkeeps > 0 + ? 'calculated conditional overhead' + : '', + numPassingConditionalUpkeeps > 0 + ? upkeepPerformedLogs[0].args.gasOverhead.toString() + : '', + numPassingLogUpkeeps > 0 ? 'calculated log overhead' : '', + numPassingLogUpkeeps > 0 + ? upkeepPerformedLogs[ + numPassingConditionalUpkeeps + ].args.gasOverhead.toString() + : '', + ' margin over gasUsed', + netGasUsedPlusOverhead.sub(receipt.gasUsed).toString(), + ) + + // If overheads dont get capped then total gas charged should be greater than tx gas + // We don't check whether the net is within gasMargin as the margin changes with numFailedUpkeeps + // Which is ok, as long as individual gas overhead is capped + if (!overheadsGotCapped) { + assert.isTrue( + netGasUsedPlusOverhead.gt(receipt.gasUsed), + 'Gas overhead is too low, increase ACCOUNTING_PER_UPKEEP_GAS_OVERHEAD', + ) + } + }, + ) + } + } + } + + it('has enough perform gas overhead for large batches [ @skip-coverage ]', async () => { + const numUpkeeps = 20 + const upkeepIds: BigNumber[] = [] + let totalPerformGas = BigNumber.from('0') + for (let i = 0; i < numUpkeeps; i++) { + const mock = await upkeepMockFactory.deploy() + const tx = await registry + .connect(owner) + ['registerUpkeep(address,uint32,address,bytes,bytes)']( + mock.address, + performGas, + await admin.getAddress(), + randomBytes, + '0x', + ) + const testUpkeepId = await getUpkeepID(tx) + upkeepIds.push(testUpkeepId) + + // Add funds to passing upkeeps + await registry.connect(owner).addFunds(testUpkeepId, toWei('10')) + + await mock.setCanPerform(true) + await mock.setPerformGasToBurn(performGas) + + totalPerformGas = totalPerformGas.add(performGas) + } + + // Should revert with no overhead added + await evmRevert( + getTransmitTx(registry, keeper1, upkeepIds, { + gasLimit: totalPerformGas, + }), + ) + // Should not revert with overhead added + await getTransmitTx(registry, keeper1, upkeepIds, { + gasLimit: totalPerformGas.add(transmitGasOverhead), + }) + }) + + it('splits l2 payment among performed upkeeps', async () => { + const numUpkeeps = 7 + const upkeepIds: BigNumber[] = [] + // Same as MockArbGasInfo.sol + const l1CostWeiArb = BigNumber.from(1000000) + + for (let i = 0; i < numUpkeeps; i++) { + const mock = await upkeepMockFactory.deploy() + const tx = await arbRegistry + .connect(owner) + ['registerUpkeep(address,uint32,address,bytes,bytes)']( + mock.address, + performGas, + await admin.getAddress(), + randomBytes, + '0x', + ) + const testUpkeepId = await getUpkeepID(tx) + upkeepIds.push(testUpkeepId) + + // Add funds to passing upkeeps + await arbRegistry.connect(owner).addFunds(testUpkeepId, toWei('100')) + } + + // Do the thing + const tx = await getTransmitTx( + arbRegistry, + keeper1, + upkeepIds, + + { gasPrice: gasWei.mul('5') }, // High gas price so that it gets capped + ) + + const receipt = await tx.wait() + const upkeepPerformedLogs = parseUpkeepPerformedLogs(receipt) + // exactly numPassingUpkeeps Upkeep Performed should be emitted + assert.equal(upkeepPerformedLogs.length, numUpkeeps) + + // Verify the payment calculation in upkeepPerformed[0] + const upkeepPerformedLog = upkeepPerformedLogs[0] + + const gasUsed = upkeepPerformedLog.args.gasUsed + const gasOverhead = upkeepPerformedLog.args.gasOverhead + const totalPayment = upkeepPerformedLog.args.totalPayment + + assert.equal( + linkForGas( + gasUsed, + gasOverhead, + gasCeilingMultiplier, + paymentPremiumPPB, + flatFeeMicroLink, + l1CostWeiArb.div(gasCeilingMultiplier), // Dividing by gasCeilingMultiplier as it gets multiplied later + BigNumber.from(numUpkeeps), + ).total.toString(), + totalPayment.toString(), + ) + }) + }, + ) + + describe('#recoverFunds', () => { + const sent = toWei('7') + + beforeEach(async () => { + await linkToken.connect(admin).approve(registry.address, toWei('100')) + await linkToken + .connect(owner) + .transfer(await keeper1.getAddress(), toWei('1000')) + + // add funds to upkeep 1 and perform and withdraw some payment + const tx = await registry + .connect(owner) + ['registerUpkeep(address,uint32,address,bytes,bytes)']( + mock.address, + performGas, + await admin.getAddress(), + emptyBytes, + emptyBytes, + ) + + const id1 = await getUpkeepID(tx) + await registry.connect(admin).addFunds(id1, toWei('5')) + + await getTransmitTx(registry, keeper1, [id1]) + await getTransmitTx(registry, keeper2, [id1]) + await getTransmitTx(registry, keeper3, [id1]) + + await registry + .connect(payee1) + .withdrawPayment( + await keeper1.getAddress(), + await nonkeeper.getAddress(), + ) + + // transfer funds directly to the registry + await linkToken.connect(keeper1).transfer(registry.address, sent) + + // add funds to upkeep 2 and perform and withdraw some payment + const tx2 = await registry + .connect(owner) + ['registerUpkeep(address,uint32,address,bytes,bytes)']( + mock.address, + performGas, + await admin.getAddress(), + emptyBytes, + emptyBytes, + ) + const id2 = await getUpkeepID(tx2) + await registry.connect(admin).addFunds(id2, toWei('5')) + + await getTransmitTx(registry, keeper1, [id2]) + await getTransmitTx(registry, keeper2, [id2]) + await getTransmitTx(registry, keeper3, [id2]) + + await registry + .connect(payee2) + .withdrawPayment( + await keeper2.getAddress(), + await nonkeeper.getAddress(), + ) + + // transfer funds using onTokenTransfer + const data = ethers.utils.defaultAbiCoder.encode(['uint256'], [id2]) + await linkToken + .connect(owner) + .transferAndCall(registry.address, toWei('1'), data) + + // withdraw some funds + await registry.connect(owner).cancelUpkeep(id1) + await registry + .connect(admin) + .withdrawFunds(id1, await nonkeeper.getAddress()) + }) + + it('reverts if not called by owner', async () => { + await evmRevert( + registry.connect(keeper1).recoverFunds(), + 'Only callable by owner', + ) + }) + + it('allows any funds that have been accidentally transfered to be moved', async () => { + const balanceBefore = await linkToken.balanceOf(registry.address) + const ownerBefore = await linkToken.balanceOf(await owner.getAddress()) + + await registry.connect(owner).recoverFunds() + + const balanceAfter = await linkToken.balanceOf(registry.address) + const ownerAfter = await linkToken.balanceOf(await owner.getAddress()) + + assert.isTrue(balanceBefore.eq(balanceAfter.add(sent))) + assert.isTrue(ownerAfter.eq(ownerBefore.add(sent))) + }) + }) + + describe('#getMinBalanceForUpkeep / #checkUpkeep / #transmit', () => { + it('calculates the minimum balance appropriately', async () => { + await mock.setCanCheck(true) + + const oneWei = BigNumber.from(1) + const minBalance = await registry.getMinBalanceForUpkeep(upkeepId) + const tooLow = minBalance.sub(oneWei) + + await registry.connect(admin).addFunds(upkeepId, tooLow) + let checkUpkeepResult = await registry + .connect(zeroAddress) + .callStatic['checkUpkeep(uint256)'](upkeepId) + + assert.equal(checkUpkeepResult.upkeepNeeded, false) + assert.equal( + checkUpkeepResult.upkeepFailureReason, + UpkeepFailureReason.INSUFFICIENT_BALANCE, + ) + + await registry.connect(admin).addFunds(upkeepId, oneWei) + checkUpkeepResult = await registry + .connect(zeroAddress) + .callStatic['checkUpkeep(uint256)'](upkeepId) + assert.equal(checkUpkeepResult.upkeepNeeded, true) + }) + + it('uses maxPerformData size in checkUpkeep but actual performDataSize in transmit', async () => { + const tx1 = await registry + .connect(owner) + ['registerUpkeep(address,uint32,address,bytes,bytes)']( + mock.address, + performGas, + await admin.getAddress(), + randomBytes, + '0x', + ) + const upkeepID1 = await getUpkeepID(tx1) + const tx2 = await registry + .connect(owner) + ['registerUpkeep(address,uint32,address,bytes,bytes)']( + mock.address, + performGas, + await admin.getAddress(), + randomBytes, + '0x', + ) + const upkeepID2 = await getUpkeepID(tx2) + await mock.setCanCheck(true) + await mock.setCanPerform(true) + + // upkeep 1 is underfunded, 2 is fully funded + const minBalance1 = ( + await registry.getMinBalanceForUpkeep(upkeepID1) + ).sub(1) + const minBalance2 = await registry.getMinBalanceForUpkeep(upkeepID2) + await registry.connect(owner).addFunds(upkeepID1, minBalance1) + await registry.connect(owner).addFunds(upkeepID2, minBalance2) + + // upkeep 1 check should return false, 2 should return true + let checkUpkeepResult = await registry + .connect(zeroAddress) + .callStatic['checkUpkeep(uint256)'](upkeepID1) + assert.equal(checkUpkeepResult.upkeepNeeded, false) + assert.equal( + checkUpkeepResult.upkeepFailureReason, + UpkeepFailureReason.INSUFFICIENT_BALANCE, + ) + + checkUpkeepResult = await registry + .connect(zeroAddress) + .callStatic['checkUpkeep(uint256)'](upkeepID2) + assert.equal(checkUpkeepResult.upkeepNeeded, true) + + // upkeep 1 perform should return with insufficient balance using max performData size + let maxPerformData = '0x' + for (let i = 0; i < maxPerformDataSize.toNumber(); i++) { + maxPerformData += '11' + } + + const tx = await getTransmitTx(registry, keeper1, [upkeepID1], { + gasPrice: gasWei.mul(gasCeilingMultiplier), + performData: maxPerformData, + }) + + const receipt = await tx.wait() + const insufficientFundsUpkeepReportLogs = + parseInsufficientFundsUpkeepReportLogs(receipt) + // exactly 1 InsufficientFundsUpkeepReportLogs log should be emitted + assert.equal(insufficientFundsUpkeepReportLogs.length, 1) + + // upkeep 1 perform should succeed with empty performData + await getTransmitTx(registry, keeper1, [upkeepID1], { + gasPrice: gasWei.mul(gasCeilingMultiplier), + }), + // upkeep 2 perform should succeed with max performData size + await getTransmitTx(registry, keeper1, [upkeepID2], { + gasPrice: gasWei.mul(gasCeilingMultiplier), + performData: maxPerformData, + }) + }) + }) + + describe('#withdrawFunds', () => { + let upkeepId2: BigNumber + + beforeEach(async () => { + const tx = await registry + .connect(owner) + ['registerUpkeep(address,uint32,address,bytes,bytes)']( + mock.address, + performGas, + await admin.getAddress(), + randomBytes, + '0x', + ) + upkeepId2 = await getUpkeepID(tx) + + await registry.connect(admin).addFunds(upkeepId, toWei('100')) + await registry.connect(admin).addFunds(upkeepId2, toWei('100')) + + // Do a perform so that upkeep is charged some amount + await getTransmitTx(registry, keeper1, [upkeepId]) + await getTransmitTx(registry, keeper1, [upkeepId2]) + }) + + it('reverts if called on a non existing ID', async () => { + await evmRevert( + registry + .connect(admin) + .withdrawFunds(upkeepId.add(1), await payee1.getAddress()), + 'OnlyCallableByAdmin()', + ) + }) + + it('reverts if called by anyone but the admin', async () => { + await evmRevert( + registry + .connect(owner) + .withdrawFunds(upkeepId, await payee1.getAddress()), + 'OnlyCallableByAdmin()', + ) + }) + + it('reverts if called on an uncanceled upkeep', async () => { + await evmRevert( + registry + .connect(admin) + .withdrawFunds(upkeepId, await payee1.getAddress()), + 'UpkeepNotCanceled()', + ) + }) + + it('reverts if called with the 0 address', async () => { + await evmRevert( + registry.connect(admin).withdrawFunds(upkeepId, zeroAddress), + 'InvalidRecipient()', + ) + }) + + describe('after the registration is paused, then cancelled', () => { + it('allows the admin to withdraw', async () => { + const balance = await registry.getBalance(upkeepId) + const payee = await payee1.getAddress() + await registry.connect(admin).pauseUpkeep(upkeepId) + await registry.connect(owner).cancelUpkeep(upkeepId) + await expect(() => + registry.connect(admin).withdrawFunds(upkeepId, payee), + ).to.changeTokenBalance(linkToken, payee1, balance) + }) + }) + + describe('after the registration is cancelled', () => { + beforeEach(async () => { + await registry.connect(owner).cancelUpkeep(upkeepId) + await registry.connect(owner).cancelUpkeep(upkeepId2) + }) + + it('can be called successively on two upkeeps', async () => { + await registry + .connect(admin) + .withdrawFunds(upkeepId, await payee1.getAddress()) + await registry + .connect(admin) + .withdrawFunds(upkeepId2, await payee1.getAddress()) + }) + + it('moves the funds out and updates the balance and emits an event', async () => { + const payee1Before = await linkToken.balanceOf( + await payee1.getAddress(), + ) + const registryBefore = await linkToken.balanceOf(registry.address) + + let registration = await registry.getUpkeep(upkeepId) + const previousBalance = registration.balance + + const tx = await registry + .connect(admin) + .withdrawFunds(upkeepId, await payee1.getAddress()) + await expect(tx) + .to.emit(registry, 'FundsWithdrawn') + .withArgs(upkeepId, previousBalance, await payee1.getAddress()) + + const payee1After = await linkToken.balanceOf(await payee1.getAddress()) + const registryAfter = await linkToken.balanceOf(registry.address) + + assert.isTrue(payee1Before.add(previousBalance).eq(payee1After)) + assert.isTrue(registryBefore.sub(previousBalance).eq(registryAfter)) + + registration = await registry.getUpkeep(upkeepId) + assert.equal(0, registration.balance.toNumber()) + }) + }) + }) + + describe('#simulatePerformUpkeep', () => { + it('reverts if called by non zero address', async () => { + await evmRevert( + registry + .connect(await owner.getAddress()) + .callStatic.simulatePerformUpkeep(upkeepId, '0x'), + 'OnlySimulatedBackend()', + ) + }) + + it('reverts when registry is paused', async () => { + await registry.connect(owner).pause() + await evmRevert( + registry + .connect(zeroAddress) + .callStatic.simulatePerformUpkeep(upkeepId, '0x'), + 'RegistryPaused()', + ) + }) + + it('returns false and gasUsed when perform fails', async () => { + await mock.setCanPerform(false) + + const simulatePerformResult = await registry + .connect(zeroAddress) + .callStatic.simulatePerformUpkeep(upkeepId, '0x') + + assert.equal(simulatePerformResult.success, false) + assert.isTrue(simulatePerformResult.gasUsed.gt(BigNumber.from('0'))) // Some gas should be used + }) + + it('returns true, gasUsed, and performGas when perform succeeds', async () => { + await mock.setCanPerform(true) + + const simulatePerformResult = await registry + .connect(zeroAddress) + .callStatic.simulatePerformUpkeep(upkeepId, '0x') + + assert.equal(simulatePerformResult.success, true) + assert.isTrue(simulatePerformResult.gasUsed.gt(BigNumber.from('0'))) // Some gas should be used + }) + + it('returns correct amount of gasUsed when perform succeeds', async () => { + await mock.setCanPerform(true) + await mock.setPerformGasToBurn(performGas) + + const simulatePerformResult = await registry + .connect(zeroAddress) + .callStatic.simulatePerformUpkeep(upkeepId, '0x') + + assert.equal(simulatePerformResult.success, true) + // Full execute gas should be used, with some performGasBuffer(1000) + assert.isTrue( + simulatePerformResult.gasUsed.gt( + performGas.sub(BigNumber.from('1000')), + ), + ) + }) + }) + + describe('#checkUpkeep', () => { + it('reverts if called by non zero address', async () => { + await evmRevert( + registry + .connect(await owner.getAddress()) + .callStatic['checkUpkeep(uint256)'](upkeepId), + 'OnlySimulatedBackend()', + ) + }) + + it('returns false and error code if the upkeep is cancelled by admin', async () => { + await registry.connect(admin).cancelUpkeep(upkeepId) + + const checkUpkeepResult = await registry + .connect(zeroAddress) + .callStatic['checkUpkeep(uint256)'](upkeepId) + + assert.equal(checkUpkeepResult.upkeepNeeded, false) + assert.equal(checkUpkeepResult.performData, '0x') + assert.equal( + checkUpkeepResult.upkeepFailureReason, + UpkeepFailureReason.UPKEEP_CANCELLED, + ) + expect(checkUpkeepResult.gasUsed).to.equal(0) + expect(checkUpkeepResult.gasLimit).to.equal(performGas) + }) + + it('returns false and error code if the upkeep is cancelled by owner', async () => { + await registry.connect(owner).cancelUpkeep(upkeepId) + + const checkUpkeepResult = await registry + .connect(zeroAddress) + .callStatic['checkUpkeep(uint256)'](upkeepId) + + assert.equal(checkUpkeepResult.upkeepNeeded, false) + assert.equal(checkUpkeepResult.performData, '0x') + assert.equal( + checkUpkeepResult.upkeepFailureReason, + UpkeepFailureReason.UPKEEP_CANCELLED, + ) + expect(checkUpkeepResult.gasUsed).to.equal(0) + expect(checkUpkeepResult.gasLimit).to.equal(performGas) + }) + + it('returns false and error code if the registry is paused', async () => { + await registry.connect(owner).pause() + + const checkUpkeepResult = await registry + .connect(zeroAddress) + .callStatic['checkUpkeep(uint256)'](upkeepId) + + assert.equal(checkUpkeepResult.upkeepNeeded, false) + assert.equal(checkUpkeepResult.performData, '0x') + assert.equal( + checkUpkeepResult.upkeepFailureReason, + UpkeepFailureReason.REGISTRY_PAUSED, + ) + expect(checkUpkeepResult.gasUsed).to.equal(0) + expect(checkUpkeepResult.gasLimit).to.equal(performGas) + }) + + it('returns false and error code if the upkeep is paused', async () => { + await registry.connect(admin).pauseUpkeep(upkeepId) + + const checkUpkeepResult = await registry + .connect(zeroAddress) + .callStatic['checkUpkeep(uint256)'](upkeepId) + + assert.equal(checkUpkeepResult.upkeepNeeded, false) + assert.equal(checkUpkeepResult.performData, '0x') + assert.equal( + checkUpkeepResult.upkeepFailureReason, + UpkeepFailureReason.UPKEEP_PAUSED, + ) + expect(checkUpkeepResult.gasUsed).to.equal(0) + expect(checkUpkeepResult.gasLimit).to.equal(performGas) + }) + + it('returns false and error code if user is out of funds', async () => { + const checkUpkeepResult = await registry + .connect(zeroAddress) + .callStatic['checkUpkeep(uint256)'](upkeepId) + + assert.equal(checkUpkeepResult.upkeepNeeded, false) + assert.equal(checkUpkeepResult.performData, '0x') + assert.equal( + checkUpkeepResult.upkeepFailureReason, + UpkeepFailureReason.INSUFFICIENT_BALANCE, + ) + expect(checkUpkeepResult.gasUsed).to.equal(0) + expect(checkUpkeepResult.gasLimit).to.equal(performGas) + }) + + context('when the registration is funded', () => { + beforeEach(async () => { + await linkToken.connect(admin).approve(registry.address, toWei('200')) + await registry.connect(admin).addFunds(upkeepId, toWei('100')) + await registry.connect(admin).addFunds(logUpkeepId, toWei('100')) + }) + + it('returns false, error code, and revert data if the target check reverts', async () => { + await mock.setShouldRevertCheck(true) + await mock.setCheckRevertReason( + 'custom revert error, clever way to insert offchain data', + ) + const checkUpkeepResult = await registry + .connect(zeroAddress) + .callStatic['checkUpkeep(uint256)'](upkeepId) + assert.equal(checkUpkeepResult.upkeepNeeded, false) + + const revertReasonBytes = `0x${checkUpkeepResult.performData.slice(10)}` // remove sighash + assert.equal( + ethers.utils.defaultAbiCoder.decode(['string'], revertReasonBytes)[0], + 'custom revert error, clever way to insert offchain data', + ) + assert.equal( + checkUpkeepResult.upkeepFailureReason, + UpkeepFailureReason.TARGET_CHECK_REVERTED, + ) + assert.isTrue(checkUpkeepResult.gasUsed.gt(BigNumber.from('0'))) // Some gas should be used + expect(checkUpkeepResult.gasLimit).to.equal(performGas) + // Feed data should be returned here + assert.isTrue(checkUpkeepResult.fastGasWei.gt(BigNumber.from('0'))) + assert.isTrue(checkUpkeepResult.linkNative.gt(BigNumber.from('0'))) + }) + + it('returns false, error code, and no revert data if the target check revert data exceeds maxRevertDataSize', async () => { + await mock.setShouldRevertCheck(true) + let longRevertReason = '' + for (let i = 0; i <= maxRevertDataSize.toNumber(); i++) { + longRevertReason += 'x' + } + await mock.setCheckRevertReason(longRevertReason) + const checkUpkeepResult = await registry + .connect(zeroAddress) + .callStatic['checkUpkeep(uint256)'](upkeepId) + assert.equal(checkUpkeepResult.upkeepNeeded, false) + + assert.equal(checkUpkeepResult.performData, '0x') + assert.equal( + checkUpkeepResult.upkeepFailureReason, + UpkeepFailureReason.REVERT_DATA_EXCEEDS_LIMIT, + ) + assert.isTrue(checkUpkeepResult.gasUsed.gt(BigNumber.from('0'))) // Some gas should be used + expect(checkUpkeepResult.gasLimit).to.equal(performGas) + }) + + it('returns false and error code if the upkeep is not needed', async () => { + await mock.setCanCheck(false) + const checkUpkeepResult = await registry + .connect(zeroAddress) + .callStatic['checkUpkeep(uint256)'](upkeepId) + + assert.equal(checkUpkeepResult.upkeepNeeded, false) + assert.equal(checkUpkeepResult.performData, '0x') + assert.equal( + checkUpkeepResult.upkeepFailureReason, + UpkeepFailureReason.UPKEEP_NOT_NEEDED, + ) + assert.isTrue(checkUpkeepResult.gasUsed.gt(BigNumber.from('0'))) // Some gas should be used + expect(checkUpkeepResult.gasLimit).to.equal(performGas) + }) + + it('returns false and error code if the performData exceeds limit', async () => { + let longBytes = '0x' + for (let i = 0; i < 5000; i++) { + longBytes += '1' + } + await mock.setCanCheck(true) + await mock.setPerformData(longBytes) + + const checkUpkeepResult = await registry + .connect(zeroAddress) + .callStatic['checkUpkeep(uint256)'](upkeepId) + + assert.equal(checkUpkeepResult.upkeepNeeded, false) + assert.equal(checkUpkeepResult.performData, '0x') + assert.equal( + checkUpkeepResult.upkeepFailureReason, + UpkeepFailureReason.PERFORM_DATA_EXCEEDS_LIMIT, + ) + assert.isTrue(checkUpkeepResult.gasUsed.gt(BigNumber.from('0'))) // Some gas should be used + expect(checkUpkeepResult.gasLimit).to.equal(performGas) + }) + + it('returns true with gas used if the target can execute', async () => { + await mock.setCanCheck(true) + await mock.setPerformData(randomBytes) + + const latestBlock = await ethers.provider.getBlock('latest') + + const checkUpkeepResult = await registry + .connect(zeroAddress) + .callStatic['checkUpkeep(uint256)'](upkeepId, { + blockTag: latestBlock.number, + }) + + assert.equal(checkUpkeepResult.upkeepNeeded, true) + assert.equal(checkUpkeepResult.performData, randomBytes) + assert.equal( + checkUpkeepResult.upkeepFailureReason, + UpkeepFailureReason.NONE, + ) + assert.isTrue(checkUpkeepResult.gasUsed.gt(BigNumber.from('0'))) // Some gas should be used + expect(checkUpkeepResult.gasLimit).to.equal(performGas) + assert.isTrue(checkUpkeepResult.fastGasWei.eq(gasWei)) + assert.isTrue(checkUpkeepResult.linkNative.eq(linkEth)) + }) + + it('calls checkLog for log-trigger upkeeps', async () => { + const log: Log = { + index: 0, + timestamp: 0, + txHash: ethers.utils.randomBytes(32), + blockNumber: 100, + blockHash: ethers.utils.randomBytes(32), + source: randomAddress(), + topics: [ethers.utils.randomBytes(32), ethers.utils.randomBytes(32)], + data: ethers.utils.randomBytes(1000), + } + + await ltUpkeep.mock.checkLog.withArgs(log, '0x').returns(true, '0x1234') + + const checkData = encodeLog(log) + + const checkUpkeepResult = await registry + .connect(zeroAddress) + .callStatic['checkUpkeep(uint256,bytes)'](logUpkeepId, checkData) + + expect(checkUpkeepResult.upkeepNeeded).to.be.true + expect(checkUpkeepResult.performData).to.equal('0x1234') + }) + + itMaybe( + 'has a large enough gas overhead to cover upkeeps that use all their gas [ @skip-coverage ]', + async () => { + await mock.setCanCheck(true) + await mock.setCheckGasToBurn(checkGasLimit) + const gas = checkGasLimit.add(checkGasOverhead) + const checkUpkeepResult = await registry + .connect(zeroAddress) + .callStatic['checkUpkeep(uint256)'](upkeepId, { + gasLimit: gas, + }) + + assert.equal(checkUpkeepResult.upkeepNeeded, true) + }, + ) + }) + }) + + describe('#addFunds', () => { + const amount = toWei('1') + + it('reverts if the registration does not exist', async () => { + await evmRevert( + registry.connect(keeper1).addFunds(upkeepId.add(1), amount), + 'UpkeepCancelled()', + ) + }) + + it('adds to the balance of the registration', async () => { + await registry.connect(admin).addFunds(upkeepId, amount) + const registration = await registry.getUpkeep(upkeepId) + assert.isTrue(amount.eq(registration.balance)) + }) + + it('lets anyone add funds to an upkeep not just admin', async () => { + await linkToken.connect(owner).transfer(await payee1.getAddress(), amount) + await linkToken.connect(payee1).approve(registry.address, amount) + + await registry.connect(payee1).addFunds(upkeepId, amount) + const registration = await registry.getUpkeep(upkeepId) + assert.isTrue(amount.eq(registration.balance)) + }) + + it('emits a log', async () => { + const tx = await registry.connect(admin).addFunds(upkeepId, amount) + await expect(tx) + .to.emit(registry, 'FundsAdded') + .withArgs(upkeepId, await admin.getAddress(), amount) + }) + + it('reverts if the upkeep is canceled', async () => { + await registry.connect(admin).cancelUpkeep(upkeepId) + await evmRevert( + registry.connect(keeper1).addFunds(upkeepId, amount), + 'UpkeepCancelled()', + ) + }) + }) + + describe('#getActiveUpkeepIDs', () => { + it('reverts if startIndex is out of bounds ', async () => { + await evmRevert( + registry.getActiveUpkeepIDs(numUpkeeps, 0), + 'IndexOutOfRange()', + ) + await evmRevert( + registry.getActiveUpkeepIDs(numUpkeeps + 1, 0), + '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', () => { + const arbL1PriceinWei = BigNumber.from(1000) // Same as MockArbGasInfo.sol + const l1CostWeiArb = arbL1PriceinWei.mul(16).mul(maxPerformDataSize) + const l1CostWeiOpt = BigNumber.from(2000000) // Same as MockOVMGasPriceOracle.sol + itMaybe('calculates the max fee appropriately', async () => { + await verifyMaxPayment(registry) + }) + + itMaybe('calculates the max fee appropriately for Arbitrum', async () => { + await verifyMaxPayment(arbRegistry, l1CostWeiArb) + }) + + itMaybe('calculates the max fee appropriately for Optimism', async () => { + await verifyMaxPayment(opRegistry, l1CostWeiOpt) + }) + + it('uses the fallback gas price if the feed has issues', async () => { + const expectedFallbackMaxPayment = linkForGas( + performGas, + registryConditionalOverhead + .add(registryPerSignerGasOverhead.mul(f + 1)) + .add(maxPerformDataSize.mul(registryPerPerformByteGasOverhead)), + gasCeilingMultiplier.mul('2'), // fallbackGasPrice is 2x gas price + paymentPremiumPPB, + flatFeeMicroLink, + ).total + + // Stale feed + let roundId = 99 + const answer = 100 + let updatedAt = 946684800 // New Years 2000 🥳 + let startedAt = 946684799 + await gasPriceFeed + .connect(owner) + .updateRoundData(roundId, answer, updatedAt, startedAt) + + assert.equal( + expectedFallbackMaxPayment.toString(), + ( + await registry.getMaxPaymentForGas(Trigger.CONDITION, performGas) + ).toString(), + ) + + // Negative feed price + roundId = 100 + updatedAt = now() + startedAt = 946684799 + await gasPriceFeed + .connect(owner) + .updateRoundData(roundId, -100, updatedAt, startedAt) + + assert.equal( + expectedFallbackMaxPayment.toString(), + ( + await registry.getMaxPaymentForGas(Trigger.CONDITION, performGas) + ).toString(), + ) + + // Zero feed price + roundId = 101 + updatedAt = now() + startedAt = 946684799 + await gasPriceFeed + .connect(owner) + .updateRoundData(roundId, 0, updatedAt, startedAt) + + assert.equal( + expectedFallbackMaxPayment.toString(), + ( + await registry.getMaxPaymentForGas(Trigger.CONDITION, performGas) + ).toString(), + ) + }) + + it('uses the fallback link price if the feed has issues', async () => { + const expectedFallbackMaxPayment = linkForGas( + performGas, + registryConditionalOverhead + .add(registryPerSignerGasOverhead.mul(f + 1)) + .add(maxPerformDataSize.mul(registryPerPerformByteGasOverhead)), + gasCeilingMultiplier.mul('2'), // fallbackLinkPrice is 1/2 link price, so multiply by 2 + paymentPremiumPPB, + flatFeeMicroLink, + ).total + + // Stale feed + let roundId = 99 + const answer = 100 + let updatedAt = 946684800 // New Years 2000 🥳 + let startedAt = 946684799 + await linkEthFeed + .connect(owner) + .updateRoundData(roundId, answer, updatedAt, startedAt) + + assert.equal( + expectedFallbackMaxPayment.toString(), + ( + await registry.getMaxPaymentForGas(Trigger.CONDITION, performGas) + ).toString(), + ) + + // Negative feed price + roundId = 100 + updatedAt = now() + startedAt = 946684799 + await linkEthFeed + .connect(owner) + .updateRoundData(roundId, -100, updatedAt, startedAt) + + assert.equal( + expectedFallbackMaxPayment.toString(), + ( + await registry.getMaxPaymentForGas(Trigger.CONDITION, performGas) + ).toString(), + ) + + // Zero feed price + roundId = 101 + updatedAt = now() + startedAt = 946684799 + await linkEthFeed + .connect(owner) + .updateRoundData(roundId, 0, updatedAt, startedAt) + + assert.equal( + expectedFallbackMaxPayment.toString(), + ( + await registry.getMaxPaymentForGas(Trigger.CONDITION, performGas) + ).toString(), + ) + }) + }) + + describe('#typeAndVersion', () => { + it('uses the correct type and version', async () => { + const typeAndVersion = await registry.typeAndVersion() + assert.equal(typeAndVersion, 'KeeperRegistry 2.1.0') + }) + }) + + describe('#onTokenTransfer', () => { + const amount = toWei('1') + + it('reverts if not called by the LINK token', async () => { + const data = ethers.utils.defaultAbiCoder.encode(['uint256'], [upkeepId]) + + await evmRevert( + registry + .connect(keeper1) + .onTokenTransfer(await keeper1.getAddress(), amount, data), + 'OnlyCallableByLINKToken()', + ) + }) + + it('reverts if not called with more or less than 32 bytes', async () => { + const longData = ethers.utils.defaultAbiCoder.encode( + ['uint256', 'uint256'], + ['33', '34'], + ) + const shortData = '0x12345678' + + await evmRevert( + linkToken + .connect(owner) + .transferAndCall(registry.address, amount, longData), + ) + await evmRevert( + linkToken + .connect(owner) + .transferAndCall(registry.address, amount, shortData), + ) + }) + + it('reverts if the upkeep is canceled', async () => { + await registry.connect(admin).cancelUpkeep(upkeepId) + await evmRevert( + registry.connect(keeper1).addFunds(upkeepId, amount), + 'UpkeepCancelled()', + ) + }) + + it('updates the funds of the job id passed', async () => { + const data = ethers.utils.defaultAbiCoder.encode(['uint256'], [upkeepId]) + + const before = (await registry.getUpkeep(upkeepId)).balance + await linkToken + .connect(owner) + .transferAndCall(registry.address, amount, data) + const after = (await registry.getUpkeep(upkeepId)).balance + + assert.isTrue(before.add(amount).eq(after)) + }) + }) + + describeMaybe('#setConfig - onchain', () => { + const payment = BigNumber.from(1) + const flatFee = BigNumber.from(2) + const maxGas = BigNumber.from(6) + const staleness = BigNumber.from(4) + const ceiling = BigNumber.from(5) + const newMinUpkeepSpend = BigNumber.from(9) + const newMaxCheckDataSize = BigNumber.from(10000) + const newMaxPerformDataSize = BigNumber.from(10000) + const newMaxRevertDataSize = BigNumber.from(10000) + const newMaxPerformGas = BigNumber.from(10000000) + const fbGasEth = BigNumber.from(7) + const fbLinkEth = BigNumber.from(8) + const newTranscoder = randomAddress() + const newRegistrars = [randomAddress(), randomAddress()] + const upkeepManager = randomAddress() + + const newConfig: OnChainConfig = { + paymentPremiumPPB: payment, + flatFeeMicroLink: flatFee, + checkGasLimit: maxGas, + stalenessSeconds: staleness, + gasCeilingMultiplier: ceiling, + minUpkeepSpend: newMinUpkeepSpend, + maxCheckDataSize: newMaxCheckDataSize, + maxPerformDataSize: newMaxPerformDataSize, + maxRevertDataSize: newMaxRevertDataSize, + maxPerformGas: newMaxPerformGas, + fallbackGasPrice: fbGasEth, + fallbackLinkPrice: fbLinkEth, + transcoder: newTranscoder, + registrars: newRegistrars, + upkeepPrivilegeManager: upkeepManager, + } + + it('reverts when called by anyone but the proposed owner', async () => { + await evmRevert( + registry + .connect(payee1) + .setConfigTypeSafe( + signerAddresses, + keeperAddresses, + f, + newConfig, + offchainVersion, + offchainBytes, + ), + 'Only callable by owner', + ) + }) + + it('reverts if signers or transmitters are the zero address', async () => { + await evmRevert( + registry + .connect(owner) + .setConfigTypeSafe( + [randomAddress(), randomAddress(), randomAddress(), zeroAddress], + [ + randomAddress(), + randomAddress(), + randomAddress(), + randomAddress(), + ], + f, + newConfig, + offchainVersion, + offchainBytes, + ), + 'InvalidSigner()', + ) + + await evmRevert( + registry + .connect(owner) + .setConfigTypeSafe( + [ + randomAddress(), + randomAddress(), + randomAddress(), + randomAddress(), + ], + [randomAddress(), randomAddress(), randomAddress(), zeroAddress], + f, + newConfig, + offchainVersion, + offchainBytes, + ), + 'InvalidTransmitter()', + ) + }) + + it('updates the onchainConfig and configDigest', async () => { + const old = await registry.getState() + const oldConfig = old.config + const oldState = old.state + assert.isTrue(paymentPremiumPPB.eq(oldConfig.paymentPremiumPPB)) + assert.isTrue(flatFeeMicroLink.eq(oldConfig.flatFeeMicroLink)) + assert.isTrue(stalenessSeconds.eq(oldConfig.stalenessSeconds)) + assert.isTrue(gasCeilingMultiplier.eq(oldConfig.gasCeilingMultiplier)) + + await registry + .connect(owner) + .setConfigTypeSafe( + signerAddresses, + keeperAddresses, + f, + newConfig, + offchainVersion, + offchainBytes, + ) + + const updated = await registry.getState() + const updatedConfig = updated.config + const updatedState = updated.state + assert.equal(updatedConfig.paymentPremiumPPB, payment.toNumber()) + assert.equal(updatedConfig.flatFeeMicroLink, flatFee.toNumber()) + assert.equal(updatedConfig.stalenessSeconds, staleness.toNumber()) + assert.equal(updatedConfig.gasCeilingMultiplier, ceiling.toNumber()) + assert.equal( + updatedConfig.minUpkeepSpend.toString(), + newMinUpkeepSpend.toString(), + ) + assert.equal( + updatedConfig.maxCheckDataSize, + newMaxCheckDataSize.toNumber(), + ) + assert.equal( + updatedConfig.maxPerformDataSize, + newMaxPerformDataSize.toNumber(), + ) + assert.equal( + updatedConfig.maxRevertDataSize, + newMaxRevertDataSize.toNumber(), + ) + assert.equal(updatedConfig.maxPerformGas, newMaxPerformGas.toNumber()) + assert.equal(updatedConfig.checkGasLimit, maxGas.toNumber()) + assert.equal( + updatedConfig.fallbackGasPrice.toNumber(), + fbGasEth.toNumber(), + ) + assert.equal( + updatedConfig.fallbackLinkPrice.toNumber(), + fbLinkEth.toNumber(), + ) + assert.equal(updatedState.latestEpoch, 0) + + assert(oldState.configCount + 1 == updatedState.configCount) + assert( + oldState.latestConfigBlockNumber != + updatedState.latestConfigBlockNumber, + ) + assert(oldState.latestConfigDigest != updatedState.latestConfigDigest) + + assert.equal(updatedConfig.transcoder, newTranscoder) + assert.deepEqual(updatedConfig.registrars, newRegistrars) + assert.equal(updatedConfig.upkeepPrivilegeManager, upkeepManager) + }) + + it('maintains paused state when config is changed', async () => { + await registry.pause() + const old = await registry.getState() + assert.isTrue(old.state.paused) + + await registry + .connect(owner) + .setConfigTypeSafe( + signerAddresses, + keeperAddresses, + f, + newConfig, + offchainVersion, + offchainBytes, + ) + + const updated = await registry.getState() + assert.isTrue(updated.state.paused) + }) + + it('emits an event', async () => { + const tx = await registry + .connect(owner) + .setConfigTypeSafe( + signerAddresses, + keeperAddresses, + f, + newConfig, + offchainVersion, + offchainBytes, + ) + await expect(tx).to.emit(registry, 'ConfigSet') + }) + }) + + describe('#setConfig - offchain', () => { + let newKeepers: string[] + + beforeEach(async () => { + newKeepers = [ + await personas.Eddy.getAddress(), + await personas.Nick.getAddress(), + await personas.Neil.getAddress(), + await personas.Carol.getAddress(), + ] + }) + + it('reverts when called by anyone but the owner', async () => { + await evmRevert( + registry + .connect(payee1) + .setConfigTypeSafe( + newKeepers, + newKeepers, + f, + config, + offchainVersion, + offchainBytes, + ), + 'Only callable by owner', + ) + }) + + it('reverts if too many keeperAddresses set', async () => { + for (let i = 0; i < 40; i++) { + newKeepers.push(randomAddress()) + } + await evmRevert( + registry + .connect(owner) + .setConfigTypeSafe( + newKeepers, + newKeepers, + f, + config, + offchainVersion, + offchainBytes, + ), + 'TooManyOracles()', + ) + }) + + it('reverts if f=0', async () => { + await evmRevert( + registry + .connect(owner) + .setConfigTypeSafe( + newKeepers, + newKeepers, + 0, + config, + offchainVersion, + offchainBytes, + ), + 'IncorrectNumberOfFaultyOracles()', + ) + }) + + it('reverts if signers != transmitters length', async () => { + const signers = [randomAddress()] + await evmRevert( + registry + .connect(owner) + .setConfigTypeSafe( + signers, + newKeepers, + f, + config, + offchainVersion, + offchainBytes, + ), + 'IncorrectNumberOfSigners()', + ) + }) + + it('reverts if signers <= 3f', async () => { + newKeepers.pop() + await evmRevert( + registry + .connect(owner) + .setConfigTypeSafe( + newKeepers, + newKeepers, + f, + config, + offchainVersion, + offchainBytes, + ), + 'IncorrectNumberOfSigners()', + ) + }) + + it('reverts on repeated signers', async () => { + const newSigners = [ + await personas.Eddy.getAddress(), + await personas.Eddy.getAddress(), + await personas.Eddy.getAddress(), + await personas.Eddy.getAddress(), + ] + await evmRevert( + registry + .connect(owner) + .setConfigTypeSafe( + newSigners, + newKeepers, + f, + config, + offchainVersion, + offchainBytes, + ), + 'RepeatedSigner()', + ) + }) + + it('reverts on repeated transmitters', async () => { + const newTransmitters = [ + await personas.Eddy.getAddress(), + await personas.Eddy.getAddress(), + await personas.Eddy.getAddress(), + await personas.Eddy.getAddress(), + ] + await evmRevert( + registry + .connect(owner) + .setConfigTypeSafe( + newKeepers, + newTransmitters, + f, + config, + offchainVersion, + offchainBytes, + ), + 'RepeatedTransmitter()', + ) + }) + + itMaybe('stores new config and emits event', async () => { + // Perform an upkeep so that totalPremium is updated + await registry.connect(admin).addFunds(upkeepId, toWei('100')) + let tx = await getTransmitTx(registry, keeper1, [upkeepId]) + await tx.wait() + + const newOffChainVersion = BigNumber.from('2') + const newOffChainConfig = '0x1122' + + const old = await registry.getState() + const oldState = old.state + assert(oldState.totalPremium.gt(BigNumber.from('0'))) + + const newSigners = newKeepers + tx = await registry + .connect(owner) + .setConfigTypeSafe( + newSigners, + newKeepers, + f, + config, + newOffChainVersion, + newOffChainConfig, + ) + + const updated = await registry.getState() + const updatedState = updated.state + assert(oldState.totalPremium.eq(updatedState.totalPremium)) + + // Old signer addresses which are not in new signers should be non active + for (let i = 0; i < signerAddresses.length; i++) { + const signer = signerAddresses[i] + if (!newSigners.includes(signer)) { + assert((await registry.getSignerInfo(signer)).active == false) + assert((await registry.getSignerInfo(signer)).index == 0) + } + } + // New signer addresses should be active + for (let i = 0; i < newSigners.length; i++) { + const signer = newSigners[i] + assert((await registry.getSignerInfo(signer)).active == true) + assert((await registry.getSignerInfo(signer)).index == i) + } + // Old transmitter addresses which are not in new transmitter should be non active, update lastCollected but retain other info + for (let i = 0; i < keeperAddresses.length; i++) { + const transmitter = keeperAddresses[i] + if (!newKeepers.includes(transmitter)) { + assert( + (await registry.getTransmitterInfo(transmitter)).active == false, + ) + assert((await registry.getTransmitterInfo(transmitter)).index == i) + assert( + (await registry.getTransmitterInfo(transmitter)).lastCollected.eq( + oldState.totalPremium.sub( + oldState.totalPremium.mod(keeperAddresses.length), + ), + ), + ) + } + } + // New transmitter addresses should be active + for (let i = 0; i < newKeepers.length; i++) { + const transmitter = newKeepers[i] + assert((await registry.getTransmitterInfo(transmitter)).active == true) + assert((await registry.getTransmitterInfo(transmitter)).index == i) + assert( + (await registry.getTransmitterInfo(transmitter)).lastCollected.eq( + oldState.totalPremium, + ), + ) + } + + // config digest should be updated + assert(oldState.configCount + 1 == updatedState.configCount) + assert( + oldState.latestConfigBlockNumber != + updatedState.latestConfigBlockNumber, + ) + assert(oldState.latestConfigDigest != updatedState.latestConfigDigest) + + //New config should be updated + assert.deepEqual(updated.signers, newKeepers) + assert.deepEqual(updated.transmitters, newKeepers) + + // Event should have been emitted + await expect(tx).to.emit(registry, 'ConfigSet') + }) + }) + + 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('#registerUpkeep', () => { + it('reverts when registry is paused', async () => { + await registry.connect(owner).pause() + await evmRevert( + registry + .connect(owner) + ['registerUpkeep(address,uint32,address,bytes,bytes)']( + mock.address, + performGas, + await admin.getAddress(), + emptyBytes, + '0x', + ), + 'RegistryPaused()', + ) + }) + + it('reverts if the target is not a contract', async () => { + await evmRevert( + registry + .connect(owner) + ['registerUpkeep(address,uint32,address,bytes,bytes)']( + zeroAddress, + performGas, + await admin.getAddress(), + emptyBytes, + '0x', + ), + 'NotAContract()', + ) + }) + + it('reverts if called by a non-owner', async () => { + await evmRevert( + registry + .connect(keeper1) + ['registerUpkeep(address,uint32,address,bytes,bytes)']( + mock.address, + performGas, + await admin.getAddress(), + emptyBytes, + '0x', + ), + 'OnlyCallableByOwnerOrRegistrar()', + ) + }) + + it('reverts if execute gas is too low', async () => { + await evmRevert( + registry + .connect(owner) + ['registerUpkeep(address,uint32,address,bytes,bytes)']( + mock.address, + 2299, + await admin.getAddress(), + emptyBytes, + '0x', + ), + 'GasLimitOutsideRange()', + ) + }) + + it('reverts if execute gas is too high', async () => { + await evmRevert( + registry + .connect(owner) + ['registerUpkeep(address,uint32,address,bytes,bytes)']( + mock.address, + 5000001, + await admin.getAddress(), + emptyBytes, + '0x', + ), + 'GasLimitOutsideRange()', + ) + }) + + it('reverts if checkData is too long', async () => { + let longBytes = '0x' + for (let i = 0; i < 10000; i++) { + longBytes += '1' + } + await evmRevert( + registry + .connect(owner) + ['registerUpkeep(address,uint32,address,bytes,bytes)']( + mock.address, + performGas, + await admin.getAddress(), + longBytes, + '0x', + ), + 'CheckDataExceedsLimit()', + ) + }) + + it('creates a record of the registration', async () => { + const performGases = [100000, 500000] + const checkDatas = [emptyBytes, '0x12'] + + for (let jdx = 0; jdx < performGases.length; jdx++) { + const performGas = performGases[jdx] + for (let kdx = 0; kdx < checkDatas.length; kdx++) { + const checkData = checkDatas[kdx] + const tx = await registry + .connect(owner) + ['registerUpkeep(address,uint32,address,bytes,bytes)']( + mock.address, + performGas, + await admin.getAddress(), + checkData, + '0x', + ) + + //confirm the upkeep details and verify emitted events + const testUpkeepId = await getUpkeepID(tx) + await expect(tx) + .to.emit(registry, 'UpkeepRegistered') + .withArgs(testUpkeepId, performGas, await admin.getAddress()) + + await expect(tx) + .to.emit(registry, 'UpkeepCheckDataSet') + .withArgs(testUpkeepId, checkData) + await expect(tx) + .to.emit(registry, 'UpkeepTriggerConfigSet') + .withArgs(testUpkeepId, '0x') + + const registration = await registry.getUpkeep(testUpkeepId) + + assert.equal(mock.address, registration.target) + assert.notEqual( + ethers.constants.AddressZero, + await registry.getForwarder(testUpkeepId), + ) + assert.equal( + performGas.toString(), + registration.performGas.toString(), + ) + assert.equal(await admin.getAddress(), registration.admin) + assert.equal(0, registration.balance.toNumber()) + assert.equal(0, registration.amountSpent.toNumber()) + assert.equal(0, registration.lastPerformedBlockNumber) + assert.equal(checkData, registration.checkData) + assert.equal(registration.paused, false) + assert.equal(registration.offchainConfig, '0x') + assert(registration.maxValidBlocknumber.eq('0xffffffff')) + } + } + }) + }) + + describe('#pauseUpkeep', () => { + it('reverts if the registration does not exist', async () => { + await evmRevert( + registry.connect(keeper1).pauseUpkeep(upkeepId.add(1)), + 'OnlyCallableByAdmin()', + ) + }) + + it('reverts if the upkeep is already canceled', async () => { + await registry.connect(admin).cancelUpkeep(upkeepId) + + await evmRevert( + registry.connect(admin).pauseUpkeep(upkeepId), + 'UpkeepCancelled()', + ) + }) + + it('reverts if the upkeep is already paused', async () => { + await registry.connect(admin).pauseUpkeep(upkeepId) + + await evmRevert( + registry.connect(admin).pauseUpkeep(upkeepId), + 'OnlyUnpausedUpkeep()', + ) + }) + + it('reverts if the caller is not the upkeep admin', async () => { + await evmRevert( + registry.connect(keeper1).pauseUpkeep(upkeepId), + '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) + + const registration = await registry.getUpkeep(upkeepId) + assert.equal(registration.paused, true) + }) + }) + + describe('#unpauseUpkeep', () => { + it('reverts if the registration does not exist', async () => { + await evmRevert( + registry.connect(keeper1).unpauseUpkeep(upkeepId.add(1)), + 'OnlyCallableByAdmin()', + ) + }) + + it('reverts if the upkeep is already canceled', async () => { + await registry.connect(owner).cancelUpkeep(upkeepId) + + await evmRevert( + registry.connect(admin).unpauseUpkeep(upkeepId), + 'UpkeepCancelled()', + ) + }) + + 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('reverts if the upkeep is not paused', async () => { + await evmRevert( + registry.connect(admin).unpauseUpkeep(upkeepId), + 'OnlyPausedUpkeep()', + ) + }) + + it('reverts if the caller is not the upkeep admin', async () => { + await registry.connect(admin).pauseUpkeep(upkeepId) + + const registration = await registry.getUpkeep(upkeepId) + + assert.equal(registration.paused, true) + + await evmRevert( + registry.connect(keeper1).unpauseUpkeep(upkeepId), + 'OnlyCallableByAdmin()', + ) + }) + + it('unpauses the upkeep and emits an event', async () => { + const originalCount = (await registry.getActiveUpkeepIDs(0, 0)).length + + await registry.connect(admin).pauseUpkeep(upkeepId) + + const tx = await registry.connect(admin).unpauseUpkeep(upkeepId) + + await expect(tx).to.emit(registry, 'UpkeepUnpaused').withArgs(upkeepId) + + const registration = await registry.getUpkeep(upkeepId) + assert.equal(registration.paused, false) + + const upkeepIds = await registry.getActiveUpkeepIDs(0, 0) + assert.equal(upkeepIds.length, originalCount) + }) + }) + + describe('#setUpkeepCheckData', () => { + it('reverts if the registration does not exist', async () => { + await evmRevert( + registry + .connect(keeper1) + .setUpkeepCheckData(upkeepId.add(1), randomBytes), + 'OnlyCallableByAdmin()', + ) + }) + + it('reverts if the caller is not upkeep admin', async () => { + await evmRevert( + registry.connect(keeper1).setUpkeepCheckData(upkeepId, randomBytes), + 'OnlyCallableByAdmin()', + ) + }) + + it('reverts if the upkeep is cancelled', async () => { + await registry.connect(admin).cancelUpkeep(upkeepId) + + await evmRevert( + registry.connect(admin).setUpkeepCheckData(upkeepId, randomBytes), + '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 evmRevert( + registry.connect(admin).setUpkeepCheckData(upkeepId, longBytes), + '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 evmRevert( + registry.connect(admin).setUpkeepGasLimit(upkeepId.add(1), newGasLimit), + 'OnlyCallableByAdmin()', + ) + }) + + it('reverts if the upkeep is canceled', async () => { + await registry.connect(admin).cancelUpkeep(upkeepId) + await evmRevert( + registry.connect(admin).setUpkeepGasLimit(upkeepId, newGasLimit), + 'UpkeepCancelled()', + ) + }) + + it('reverts if called by anyone but the admin', async () => { + await evmRevert( + registry.connect(owner).setUpkeepGasLimit(upkeepId, newGasLimit), + 'OnlyCallableByAdmin()', + ) + }) + + it('reverts if new gas limit is out of bounds', async () => { + await evmRevert( + registry + .connect(admin) + .setUpkeepGasLimit(upkeepId, BigNumber.from('100')), + 'GasLimitOutsideRange()', + ) + await evmRevert( + registry + .connect(admin) + .setUpkeepGasLimit(upkeepId, BigNumber.from('6000000')), + '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 evmRevert( + registry + .connect(admin) + .setUpkeepOffchainConfig(upkeepId.add(1), newConfig), + 'OnlyCallableByAdmin()', + ) + }) + + it('reverts if the upkeep is canceled', async () => { + await registry.connect(admin).cancelUpkeep(upkeepId) + await evmRevert( + registry.connect(admin).setUpkeepOffchainConfig(upkeepId, newConfig), + 'UpkeepCancelled()', + ) + }) + + it('reverts if called by anyone but the admin', async () => { + await evmRevert( + registry.connect(owner).setUpkeepOffchainConfig(upkeepId, newConfig), + '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 evmRevert( + registry + .connect(admin) + .setUpkeepTriggerConfig(upkeepId.add(1), newConfig), + 'OnlyCallableByAdmin()', + ) + }) + + it('reverts if the upkeep is canceled', async () => { + await registry.connect(admin).cancelUpkeep(upkeepId) + await evmRevert( + registry.connect(admin).setUpkeepTriggerConfig(upkeepId, newConfig), + 'UpkeepCancelled()', + ) + }) + + it('reverts if called by anyone but the admin', async () => { + await evmRevert( + registry.connect(owner).setUpkeepTriggerConfig(upkeepId, newConfig), + '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 evmRevert( + registry + .connect(payee1) + .transferUpkeepAdmin(upkeepId, await payee2.getAddress()), + 'OnlyCallableByAdmin()', + ) + }) + + it('reverts when transferring to self', async () => { + await evmRevert( + registry + .connect(admin) + .transferUpkeepAdmin(upkeepId, await admin.getAddress()), + 'ValueNotChanged()', + ) + }) + + it('reverts when the upkeep is cancelled', async () => { + await registry.connect(admin).cancelUpkeep(upkeepId) + + await evmRevert( + registry + .connect(admin) + .transferUpkeepAdmin(upkeepId, await keeper1.getAddress()), + '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(0, receipt.logs.length) + }) + }) + + 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 evmRevert( + registry.connect(payee2).acceptUpkeepAdmin(upkeepId), + 'OnlyCallableByProposedAdmin()', + ) + }) + + it('reverts when the upkeep is cancelled', async () => { + await registry.connect(admin).cancelUpkeep(upkeepId) + + await evmRevert( + registry.connect(payee1).acceptUpkeepAdmin(upkeepId), + '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 owner', async () => { + await evmRevert( + registry.connect(keeper1).withdrawOwnerFunds(), + 'Only callable by owner', + ) + }) + + itMaybe('withdraws the collected fees to owner', async () => { + await registry.connect(admin).addFunds(upkeepId, toWei('100')) + // Very high min spend, whole balance as cancellation fees + const minUpkeepSpend = toWei('1000') + await registry.connect(owner).setConfigTypeSafe( + signerAddresses, + keeperAddresses, + f, + { + paymentPremiumPPB, + flatFeeMicroLink, + checkGasLimit, + stalenessSeconds, + gasCeilingMultiplier, + minUpkeepSpend, + maxCheckDataSize, + maxPerformDataSize, + maxRevertDataSize, + maxPerformGas, + fallbackGasPrice, + fallbackLinkPrice, + transcoder: transcoder.address, + registrars: [], + upkeepPrivilegeManager: upkeepManager, + }, + offchainVersion, + offchainBytes, + ) + 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.getState()).state + .ownerLinkBalance + assert.isTrue(ownerRegistryBalance.eq(upkeepBalance)) + + // Now withdraw + await registry.connect(owner).withdrawOwnerFunds() + + ownerRegistryBalance = (await registry.getState()).state.ownerLinkBalance + 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 evmRevert( + registry + .connect(payee2) + .transferPayeeship( + await keeper1.getAddress(), + await payee2.getAddress(), + ), + 'OnlyCallableByPayee()', + ) + }) + + it('reverts when transferring to self', async () => { + await evmRevert( + registry + .connect(payee1) + .transferPayeeship( + await keeper1.getAddress(), + await payee1.getAddress(), + ), + '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(0, receipt.logs.length) + }) + }) + + 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 evmRevert( + registry.connect(payee1).acceptPayeeship(await keeper1.getAddress()), + '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 evmRevert( + getTransmitTx(registry, keeper1, [upkeepId]), + 'RegistryPaused()', + ) + }) + + it('Does not allow creation of new upkeeps when paused', async () => { + await registry.connect(owner).pause() + + await evmRevert( + registry + .connect(owner) + ['registerUpkeep(address,uint32,address,bytes,bytes)']( + mock.address, + performGas, + await admin.getAddress(), + emptyBytes, + '0x', + ), + '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('#migrateUpkeeps() / #receiveUpkeeps()', async () => { + context('when permissions are set', () => { + beforeEach(async () => { + await linkToken.connect(owner).approve(registry.address, toWei('100')) + await registry.connect(owner).addFunds(upkeepId, toWei('100')) + await registry.setPeerRegistryMigrationPermission(mgRegistry.address, 1) + await mgRegistry.setPeerRegistryMigrationPermission(registry.address, 2) + }) + + it('migrates an upkeep', async () => { + const offchainBytes = '0x987654abcd' + await registry + .connect(admin) + .setUpkeepOffchainConfig(upkeepId, offchainBytes) + const reg1Upkeep = await registry.getUpkeep(upkeepId) + const forwarderAddress = await registry.getForwarder(upkeepId) + expect(reg1Upkeep.balance).to.equal(toWei('100')) + expect(reg1Upkeep.checkData).to.equal(randomBytes) + expect(forwarderAddress).to.not.equal(ethers.constants.AddressZero) + expect(reg1Upkeep.offchainConfig).to.equal(offchainBytes) + expect((await registry.getState()).state.numUpkeeps).to.equal( + numUpkeeps, + ) + const forwarder = await IAutomationForwarderFactory.connect( + forwarderAddress, + owner, + ) + expect(await forwarder.getRegistry()).to.equal(registry.address) + // Set an upkeep admin transfer in progress too + await registry + .connect(admin) + .transferUpkeepAdmin(upkeepId, await payee1.getAddress()) + + // migrate + await registry + .connect(admin) + .migrateUpkeeps([upkeepId], mgRegistry.address) + expect((await registry.getState()).state.numUpkeeps).to.equal( + numUpkeeps - 1, + ) + expect((await mgRegistry.getState()).state.numUpkeeps).to.equal(1) + expect((await registry.getUpkeep(upkeepId)).balance).to.equal(0) + expect((await registry.getUpkeep(upkeepId)).checkData).to.equal('0x') + expect((await mgRegistry.getUpkeep(upkeepId)).balance).to.equal( + toWei('100'), + ) + expect( + (await mgRegistry.getState()).state.expectedLinkBalance, + ).to.equal(toWei('100')) + expect((await mgRegistry.getUpkeep(upkeepId)).checkData).to.equal( + randomBytes, + ) + expect((await mgRegistry.getUpkeep(upkeepId)).offchainConfig).to.equal( + offchainBytes, + ) + expect(await mgRegistry.getForwarder(upkeepId)).to.equal( + forwarderAddress, + ) + // test that registry is updated on forwarder + expect(await forwarder.getRegistry()).to.equal(mgRegistry.address) + // migration will delete the upkeep and nullify admin transfer + await expect( + registry.connect(payee1).acceptUpkeepAdmin(upkeepId), + ).to.be.revertedWith('UpkeepCancelled()') + await expect( + mgRegistry.connect(payee1).acceptUpkeepAdmin(upkeepId), + ).to.be.revertedWith('OnlyCallableByProposedAdmin()') + }) + + it('migrates a paused upkeep', async () => { + expect((await registry.getUpkeep(upkeepId)).balance).to.equal( + toWei('100'), + ) + expect((await registry.getUpkeep(upkeepId)).checkData).to.equal( + randomBytes, + ) + expect((await registry.getState()).state.numUpkeeps).to.equal( + numUpkeeps, + ) + await registry.connect(admin).pauseUpkeep(upkeepId) + // verify the upkeep is paused + expect((await registry.getUpkeep(upkeepId)).paused).to.equal(true) + // migrate + await registry + .connect(admin) + .migrateUpkeeps([upkeepId], mgRegistry.address) + expect((await registry.getState()).state.numUpkeeps).to.equal( + numUpkeeps - 1, + ) + expect((await mgRegistry.getState()).state.numUpkeeps).to.equal(1) + expect((await registry.getUpkeep(upkeepId)).balance).to.equal(0) + expect((await mgRegistry.getUpkeep(upkeepId)).balance).to.equal( + toWei('100'), + ) + expect((await registry.getUpkeep(upkeepId)).checkData).to.equal('0x') + expect((await mgRegistry.getUpkeep(upkeepId)).checkData).to.equal( + randomBytes, + ) + expect( + (await mgRegistry.getState()).state.expectedLinkBalance, + ).to.equal(toWei('100')) + // verify the upkeep is still paused after migration + expect((await mgRegistry.getUpkeep(upkeepId)).paused).to.equal(true) + }) + + it('emits an event on both contracts', async () => { + expect((await registry.getUpkeep(upkeepId)).balance).to.equal( + toWei('100'), + ) + expect((await registry.getUpkeep(upkeepId)).checkData).to.equal( + randomBytes, + ) + expect((await registry.getState()).state.numUpkeeps).to.equal( + numUpkeeps, + ) + const tx = registry + .connect(admin) + .migrateUpkeeps([upkeepId], mgRegistry.address) + await expect(tx) + .to.emit(registry, 'UpkeepMigrated') + .withArgs(upkeepId, toWei('100'), mgRegistry.address) + await expect(tx) + .to.emit(mgRegistry, 'UpkeepReceived') + .withArgs(upkeepId, toWei('100'), registry.address) + }) + + it('is only migratable by the admin', async () => { + await expect( + registry + .connect(owner) + .migrateUpkeeps([upkeepId], mgRegistry.address), + ).to.be.revertedWith('OnlyCallableByAdmin()') + await registry + .connect(admin) + .migrateUpkeeps([upkeepId], mgRegistry.address) + }) + }) + + context('when permissions are not set', () => { + it('reverts', async () => { + // no permissions + await registry.setPeerRegistryMigrationPermission(mgRegistry.address, 0) + await mgRegistry.setPeerRegistryMigrationPermission(registry.address, 0) + await expect(registry.migrateUpkeeps([upkeepId], mgRegistry.address)).to + .be.reverted + // only outgoing permissions + await registry.setPeerRegistryMigrationPermission(mgRegistry.address, 1) + await mgRegistry.setPeerRegistryMigrationPermission(registry.address, 0) + await expect(registry.migrateUpkeeps([upkeepId], mgRegistry.address)).to + .be.reverted + // only incoming permissions + await registry.setPeerRegistryMigrationPermission(mgRegistry.address, 0) + await mgRegistry.setPeerRegistryMigrationPermission(registry.address, 2) + await expect(registry.migrateUpkeeps([upkeepId], mgRegistry.address)).to + .be.reverted + // permissions opposite direction + await registry.setPeerRegistryMigrationPermission(mgRegistry.address, 2) + await mgRegistry.setPeerRegistryMigrationPermission(registry.address, 1) + await expect(registry.migrateUpkeeps([upkeepId], mgRegistry.address)).to + .be.reverted + }) + }) + }) + + 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 evmRevert( + registry.connect(owner).setPayees([...payees, randomAddress()]), + 'ParameterLengthError()', + ) + }) + + it('reverts if the payee is the zero address', async () => { + await blankRegistry.connect(owner).setConfig(...baseConfig) // used to test initial config + + await evmRevert( + blankRegistry // used to test initial config + .connect(owner) + .setPayees([ethers.constants.AddressZero, ...payees.slice(1)]), + 'InvalidPayee()', + ) + }) + + itMaybe( + 'sets the payees when exisitng payees are zero address', + async () => { + //Initial payees should be zero address + await blankRegistry.connect(owner).setConfig(...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(true, ignored.active) + }) + + it('reverts if payee is non zero and owner tries to change payee', async () => { + const newPayees = [randomAddress(), ...payees.slice(1)] + + await evmRevert( + registry.connect(owner).setPayees(newPayees), + '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 evmRevert( + registry.connect(owner).cancelUpkeep(upkeepId.add(1)), + 'CannotCancel()', + ) + }) + + it('reverts if called by a non-owner/non-admin', async () => { + await evmRevert( + registry.connect(keeper1).cancelUpkeep(upkeepId), + '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 evmRevert( + registry.connect(owner).cancelUpkeep(upkeepId), + 'CannotCancel()', + ) + }) + + describe('when called by the owner when the admin has just canceled', () => { + let oldExpiration: BigNumber + + beforeEach(async () => { + await registry.connect(admin).cancelUpkeep(upkeepId) + const registration = await registry.getUpkeep(upkeepId) + oldExpiration = registration.maxValidBlocknumber + }) + + it('allows the owner to cancel it more quickly', async () => { + await registry.connect(owner).cancelUpkeep(upkeepId) + + const registration = await registry.getUpkeep(upkeepId) + const newExpiration = registration.maxValidBlocknumber + assert.isTrue(newExpiration.lt(oldExpiration)) + }) + }) + }) + + describe('when called by the admin', async () => { + it('reverts if called again by the admin', async () => { + await registry.connect(admin).cancelUpkeep(upkeepId) + + await evmRevert( + registry.connect(admin).cancelUpkeep(upkeepId), + 'CannotCancel()', + ) + }) + + 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 evmRevert( + registry.connect(owner).cancelUpkeep(upkeepId), + 'CannotCancel()', + ) + }) + + 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 gives to owner', async () => { + const minUpkeepSpend = toWei('10') + + await registry.connect(owner).setConfigTypeSafe( + signerAddresses, + keeperAddresses, + f, + { + paymentPremiumPPB, + flatFeeMicroLink, + checkGasLimit, + stalenessSeconds, + gasCeilingMultiplier, + minUpkeepSpend, + maxCheckDataSize, + maxPerformDataSize, + maxRevertDataSize, + maxPerformGas, + fallbackGasPrice, + fallbackLinkPrice, + transcoder: transcoder.address, + registrars: [], + upkeepPrivilegeManager: upkeepManager, + }, + offchainVersion, + offchainBytes, + ) + + const payee1Before = await linkToken.balanceOf( + await payee1.getAddress(), + ) + const upkeepBefore = (await registry.getUpkeep(upkeepId)).balance + const ownerBefore = (await registry.getState()).state.ownerLinkBalance + + const amountSpent = toWei('100').sub(upkeepBefore) + const cancellationFee = minUpkeepSpend.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.getState()).state.ownerLinkBalance + + // 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 minUpkeepSpend = toWei('1000') + await registry.connect(owner).setConfigTypeSafe( + signerAddresses, + keeperAddresses, + f, + { + paymentPremiumPPB, + flatFeeMicroLink, + checkGasLimit, + stalenessSeconds, + gasCeilingMultiplier, + minUpkeepSpend, + maxCheckDataSize, + maxPerformDataSize, + maxRevertDataSize, + maxPerformGas, + fallbackGasPrice, + fallbackLinkPrice, + transcoder: transcoder.address, + registrars: [], + upkeepPrivilegeManager: upkeepManager, + }, + offchainVersion, + offchainBytes, + ) + const payee1Before = await linkToken.balanceOf( + await payee1.getAddress(), + ) + const upkeepBefore = (await registry.getUpkeep(upkeepId)).balance + const ownerBefore = (await registry.getState()).state.ownerLinkBalance + + await registry.connect(admin).cancelUpkeep(upkeepId) + const payee1After = await linkToken.balanceOf( + await payee1.getAddress(), + ) + const ownerAfter = (await registry.getState()).state.ownerLinkBalance + const upkeepAfter = (await registry.getUpkeep(upkeepId)).balance + + // all upkeep balance is deducted for cancellation fee + assert.equal(0, upkeepAfter.toNumber()) + // 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 minUpkeepSpend is spent', async () => { + // Very low min spend, already spent in one perform upkeep + const minUpkeepSpend = BigNumber.from(420) + await registry.connect(owner).setConfigTypeSafe( + signerAddresses, + keeperAddresses, + f, + { + paymentPremiumPPB, + flatFeeMicroLink, + checkGasLimit, + stalenessSeconds, + gasCeilingMultiplier, + minUpkeepSpend, + maxCheckDataSize, + maxPerformDataSize, + maxRevertDataSize, + maxPerformGas, + fallbackGasPrice, + fallbackLinkPrice, + transcoder: transcoder.address, + registrars: [], + upkeepPrivilegeManager: upkeepManager, + }, + offchainVersion, + offchainBytes, + ) + const payee1Before = await linkToken.balanceOf( + await payee1.getAddress(), + ) + const upkeepBefore = (await registry.getUpkeep(upkeepId)).balance + const ownerBefore = (await registry.getState()).state.ownerLinkBalance + + await registry.connect(admin).cancelUpkeep(upkeepId) + const payee1After = await linkToken.balanceOf( + await payee1.getAddress(), + ) + const ownerAfter = (await registry.getState()).state.ownerLinkBalance + const upkeepAfter = (await registry.getUpkeep(upkeepId)).balance + + // upkeep does not pay cancellation fee after cancellation because minimum upkeep spent is met + assert.isTrue(upkeepBefore.eq(upkeepAfter)) + // owner balance does not change + assert.isTrue(ownerAfter.eq(ownerBefore)) + // payee balance does not change + assert.isTrue(payee1Before.eq(payee1After)) + }) + }) + }) + }) + + describe('#withdrawPayment', () => { + 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 called by anyone but the payee', async () => { + await evmRevert( + registry + .connect(payee2) + .withdrawPayment( + await keeper1.getAddress(), + await nonkeeper.getAddress(), + ), + 'OnlyCallableByPayee()', + ) + }) + + it('reverts if called with the 0 address', async () => { + await evmRevert( + registry + .connect(payee2) + .withdrawPayment(await keeper1.getAddress(), zeroAddress), + 'InvalidRecipient()', + ) + }) + + it('updates the balances', async () => { + const to = await nonkeeper.getAddress() + const keeperBefore = await registry.getTransmitterInfo( + await keeper1.getAddress(), + ) + const registrationBefore = (await registry.getUpkeep(upkeepId)).balance + const toLinkBefore = await linkToken.balanceOf(to) + const registryLinkBefore = await linkToken.balanceOf(registry.address) + const registryPremiumBefore = (await registry.getState()).state + .totalPremium + const ownerBefore = (await registry.getState()).state.ownerLinkBalance + + // Withdrawing for first time, last collected = 0 + assert.equal(keeperBefore.lastCollected.toString(), '0') + + //// Do the thing + await registry + .connect(payee1) + .withdrawPayment(await keeper1.getAddress(), to) + + const keeperAfter = await registry.getTransmitterInfo( + await keeper1.getAddress(), + ) + const registrationAfter = (await registry.getUpkeep(upkeepId)).balance + const toLinkAfter = await linkToken.balanceOf(to) + const registryLinkAfter = await linkToken.balanceOf(registry.address) + const registryPremiumAfter = (await registry.getState()).state + .totalPremium + const ownerAfter = (await registry.getState()).state.ownerLinkBalance + + // registry total premium should not change + assert.isTrue(registryPremiumBefore.eq(registryPremiumAfter)) + + // Last collected should be updated to premium-change + assert.isTrue( + keeperAfter.lastCollected.eq( + registryPremiumBefore.sub( + registryPremiumBefore.mod(keeperAddresses.length), + ), + ), + ) + + // owner balance should remain unchanged + assert.isTrue(ownerAfter.eq(ownerBefore)) + + assert.isTrue(keeperAfter.balance.eq(BigNumber.from(0))) + assert.isTrue(registrationBefore.eq(registrationAfter)) + assert.isTrue(toLinkBefore.add(keeperBefore.balance).eq(toLinkAfter)) + assert.isTrue( + registryLinkBefore.sub(keeperBefore.balance).eq(registryLinkAfter), + ) + }) + + it('emits a log announcing the withdrawal', async () => { + const balance = ( + await registry.getTransmitterInfo(await keeper1.getAddress()) + ).balance + const tx = await registry + .connect(payee1) + .withdrawPayment( + await keeper1.getAddress(), + await nonkeeper.getAddress(), + ) + await expect(tx) + .to.emit(registry, 'PaymentWithdrawn') + .withArgs( + await keeper1.getAddress(), + balance, + await nonkeeper.getAddress(), + await payee1.getAddress(), + ) + }) + }) + + describe('#checkCallback', () => { + it('returns false with appropriate failure reason when target callback reverts', async () => { + await streamsLookupUpkeep.setShouldRevertCallback(true) + + const values: any[] = ['0x1234', '0xabcd'] + const res = await registry + .connect(zeroAddress) + .callStatic.checkCallback(streamsLookupUpkeepId, values, '0x') + + assert.isFalse(res.upkeepNeeded) + assert.equal(res.performData, '0x') + assert.equal( + res.upkeepFailureReason, + UpkeepFailureReason.CHECK_CALLBACK_REVERTED, + ) + assert.isTrue(res.gasUsed.gt(BigNumber.from('0'))) // Some gas should be used + }) + + it('returns false with appropriate failure reason when target callback returns big performData', async () => { + let longBytes = '0x' + for (let i = 0; i <= maxPerformDataSize.toNumber(); i++) { + longBytes += '11' + } + const values: any[] = [longBytes, longBytes] + const res = await registry + .connect(zeroAddress) + .callStatic.checkCallback(streamsLookupUpkeepId, values, '0x') + + assert.isFalse(res.upkeepNeeded) + assert.equal(res.performData, '0x') + assert.equal( + res.upkeepFailureReason, + UpkeepFailureReason.PERFORM_DATA_EXCEEDS_LIMIT, + ) + assert.isTrue(res.gasUsed.gt(BigNumber.from('0'))) // Some gas should be used + }) + + it('returns false with appropriate failure reason when target callback returns false', async () => { + await streamsLookupUpkeep.setCallbackReturnBool(false) + const values: any[] = ['0x1234', '0xabcd'] + const res = await registry + .connect(zeroAddress) + .callStatic.checkCallback(streamsLookupUpkeepId, values, '0x') + + assert.isFalse(res.upkeepNeeded) + assert.equal(res.performData, '0x') + assert.equal( + res.upkeepFailureReason, + UpkeepFailureReason.UPKEEP_NOT_NEEDED, + ) + assert.isTrue(res.gasUsed.gt(BigNumber.from('0'))) // Some gas should be used + }) + + it('succeeds with upkeep needed', async () => { + const values: any[] = ['0x1234', '0xabcd'] + + const res = await registry + .connect(zeroAddress) + .callStatic.checkCallback(streamsLookupUpkeepId, values, '0x') + const expectedPerformData = ethers.utils.defaultAbiCoder.encode( + ['bytes[]', 'bytes'], + [values, '0x'], + ) + + assert.isTrue(res.upkeepNeeded) + assert.equal(res.performData, expectedPerformData) + assert.equal(res.upkeepFailureReason, UpkeepFailureReason.NONE) + assert.isTrue(res.gasUsed.gt(BigNumber.from('0'))) // Some gas should be used + }) + }) + + describe('#setUpkeepPrivilegeConfig() / #getUpkeepPrivilegeConfig()', () => { + it('reverts when non manager tries to set privilege config', async () => { + await evmRevert( + registry.connect(payee3).setUpkeepPrivilegeConfig(upkeepId, '0x1234'), + '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 evmRevert( + registry.connect(payee3).setAdminPrivilegeConfig(admin, '0x1234'), + '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')) + await registry.connect(owner).addFunds(upkeepId, toWei('100')) + }) + + it('splits premium evenly across transmitters', async () => { + // Do a transmit from keeper1 + await getTransmitTx(registry, keeper1, [upkeepId]) + + const registryPremium = (await registry.getState()).state.totalPremium + assert.isTrue(registryPremium.gt(BigNumber.from(0))) + + const premiumPerTransmitter = registryPremium.div( + BigNumber.from(keeperAddresses.length), + ) + const k1Balance = ( + await registry.getTransmitterInfo(await keeper1.getAddress()) + ).balance + // transmitter should be reimbursed for gas and get the premium + assert.isTrue(k1Balance.gt(premiumPerTransmitter)) + const k1GasReimbursement = k1Balance.sub(premiumPerTransmitter) + + const k2Balance = ( + await registry.getTransmitterInfo(await keeper2.getAddress()) + ).balance + // non transmitter should get its share of premium + assert.isTrue(k2Balance.eq(premiumPerTransmitter)) + + // Now do a transmit from keeper 2 + await getTransmitTx(registry, keeper2, [upkeepId]) + const registryPremiumNew = (await registry.getState()).state.totalPremium + assert.isTrue(registryPremiumNew.gt(registryPremium)) + const premiumPerTransmitterNew = registryPremiumNew.div( + BigNumber.from(keeperAddresses.length), + ) + const additionalPremium = premiumPerTransmitterNew.sub( + premiumPerTransmitter, + ) + + const k1BalanceNew = ( + await registry.getTransmitterInfo(await keeper1.getAddress()) + ).balance + // k1 should get the new premium + assert.isTrue( + k1BalanceNew.eq(k1GasReimbursement.add(premiumPerTransmitterNew)), + ) + + const k2BalanceNew = ( + await registry.getTransmitterInfo(await keeper2.getAddress()) + ).balance + // k2 should get gas reimbursement in addition to new premium + assert.isTrue(k2BalanceNew.gt(k2Balance.add(additionalPremium))) + }) + + it('updates last collected upon payment withdrawn', async () => { + // Do a transmit from keeper1 + await getTransmitTx(registry, keeper1, [upkeepId]) + + const registryPremium = (await registry.getState()).state.totalPremium + const k1 = await registry.getTransmitterInfo(await keeper1.getAddress()) + const k2 = await registry.getTransmitterInfo(await keeper2.getAddress()) + + // Withdrawing for first time, last collected = 0 + assert.isTrue(k1.lastCollected.eq(BigNumber.from(0))) + assert.isTrue(k2.lastCollected.eq(BigNumber.from(0))) + + //// Do the thing + await registry + .connect(payee1) + .withdrawPayment( + await keeper1.getAddress(), + await nonkeeper.getAddress(), + ) + + const k1New = await registry.getTransmitterInfo( + await keeper1.getAddress(), + ) + const k2New = await registry.getTransmitterInfo( + await keeper2.getAddress(), + ) + + // transmitter info lastCollected should be updated for k1, not for k2 + assert.isTrue( + k1New.lastCollected.eq( + registryPremium.sub(registryPremium.mod(keeperAddresses.length)), + ), + ) + assert.isTrue(k2New.lastCollected.eq(BigNumber.from(0))) + }) + + itMaybe( + 'maintains consistent balance information across all parties', + async () => { + // throughout transmits, withdrawals, setConfigs total claim on balances should remain less than expected balance + // some spare change can get lost but it should be less than maxAllowedSpareChange + + let maxAllowedSpareChange = BigNumber.from('0') + await verifyConsistentAccounting(maxAllowedSpareChange) + + await getTransmitTx(registry, keeper1, [upkeepId]) + maxAllowedSpareChange = maxAllowedSpareChange.add(BigNumber.from('31')) + await verifyConsistentAccounting(maxAllowedSpareChange) + + await registry + .connect(payee1) + .withdrawPayment( + await keeper1.getAddress(), + await nonkeeper.getAddress(), + ) + await verifyConsistentAccounting(maxAllowedSpareChange) + + await registry + .connect(payee2) + .withdrawPayment( + await keeper2.getAddress(), + await nonkeeper.getAddress(), + ) + await verifyConsistentAccounting(maxAllowedSpareChange) + + await getTransmitTx(registry, keeper1, [upkeepId]) + maxAllowedSpareChange = maxAllowedSpareChange.add(BigNumber.from('31')) + await verifyConsistentAccounting(maxAllowedSpareChange) + + await registry.connect(owner).setConfigTypeSafe( + signerAddresses.slice(2, 15), // only use 2-14th index keepers + keeperAddresses.slice(2, 15), + f, + config, + offchainVersion, + offchainBytes, + ) + await verifyConsistentAccounting(maxAllowedSpareChange) + + await getTransmitTx(registry, keeper3, [upkeepId], { + startingSignerIndex: 2, + }) + maxAllowedSpareChange = maxAllowedSpareChange.add(BigNumber.from('13')) + await verifyConsistentAccounting(maxAllowedSpareChange) + + await registry + .connect(payee1) + .withdrawPayment( + await keeper1.getAddress(), + await nonkeeper.getAddress(), + ) + await verifyConsistentAccounting(maxAllowedSpareChange) + + await registry + .connect(payee3) + .withdrawPayment( + await keeper3.getAddress(), + await nonkeeper.getAddress(), + ) + await verifyConsistentAccounting(maxAllowedSpareChange) + + await registry.connect(owner).setConfigTypeSafe( + signerAddresses.slice(0, 4), // only use 0-3rd index keepers + keeperAddresses.slice(0, 4), + f, + config, + offchainVersion, + offchainBytes, + ) + await verifyConsistentAccounting(maxAllowedSpareChange) + await getTransmitTx(registry, keeper1, [upkeepId]) + maxAllowedSpareChange = maxAllowedSpareChange.add(BigNumber.from('4')) + await getTransmitTx(registry, keeper3, [upkeepId]) + maxAllowedSpareChange = maxAllowedSpareChange.add(BigNumber.from('4')) + + await verifyConsistentAccounting(maxAllowedSpareChange) + await registry + .connect(payee5) + .withdrawPayment( + await keeper5.getAddress(), + await nonkeeper.getAddress(), + ) + await verifyConsistentAccounting(maxAllowedSpareChange) + + await registry + .connect(payee1) + .withdrawPayment( + await keeper1.getAddress(), + await nonkeeper.getAddress(), + ) + await verifyConsistentAccounting(maxAllowedSpareChange) + }, + ) + }) +})