From 55886d005ddbac0fe5a1e2710681d1d2037f6f88 Mon Sep 17 00:00:00 2001 From: PatrickAlphaC Date: Wed, 24 Apr 2024 03:01:23 +0000 Subject: [PATCH] 1.1.0 --- contracts/abi/v0.8/AuthorizedReceiver.json | 15 - .../abi/v0.8/AutomationRegistrar2_3.json | 360 ++- contracts/abi/v0.8/AutomationRegistry2_2.json | 2 +- contracts/abi/v0.8/AutomationRegistry2_3.json | 232 +- .../abi/v0.8/AutomationRegistryBase2_3.json | 102 +- .../v0.8/AutomationRegistryBaseInterface.json | 80 + ...AutomationRegistryExecutableInterface.json | 80 + .../abi/v0.8/AutomationRegistryInterface.json | 80 + .../abi/v0.8/AutomationRegistryLogicA2_2.json | 2 +- .../abi/v0.8/AutomationRegistryLogicA2_3.json | 370 +-- .../abi/v0.8/AutomationRegistryLogicB2_3.json | 1204 ++------ .../abi/v0.8/AutomationRegistryLogicC2_3.json | 2462 +++++++++++++++++ contracts/abi/v0.8/AutomationUtils2_3.json | 302 -- contracts/abi/v0.8/BasicConsumer.json | 211 ++ .../abi/v0.8/BatchVRFCoordinatorV2Plus.json | 2 +- contracts/abi/v0.8/Chainable.json | 2 +- contracts/abi/v0.8/ChainlinkClientHelper.json | 125 + contracts/abi/v0.8/Chainlinked.json | 41 + .../abi/v0.8/ConfirmedOwnerTestHelper.json | 91 + contracts/abi/v0.8/Consumer.json | 190 ++ contracts/abi/v0.8/EmptyOracle.json | 213 ++ .../abi/v0.8/ExposedVRFCoordinatorV2_5.json | 22 +- contracts/abi/v0.8/GasGuzzlingConsumer.json | 273 ++ contracts/abi/v0.8/GetterSetter.json | 229 ++ contracts/abi/v0.8/Greeter.json | 93 +- .../v0.8/IAutomationRegistryMaster2_3.json | 718 ++++- contracts/abi/v0.8/IERC165.json | 2 +- .../v0.8/IVRFCoordinatorV2PlusFulfill.json | 110 + contracts/abi/v0.8/IVRFV2PlusWrapper.json | 57 +- contracts/abi/v0.8/IVerifierProxy.json | 6 +- contracts/abi/v0.8/IWrappedNative.json | 205 ++ contracts/abi/v0.8/KeeperRegistry2_1.json | 2 +- .../abi/v0.8/KeeperRegistryLogicA2_1.json | 2 +- contracts/abi/v0.8/LinkTokenTestHelper.json | 763 +++++ contracts/abi/v0.8/MaliciousChainlinked.json | 41 + contracts/abi/v0.8/MaliciousConsumer.json | 194 ++ .../abi/v0.8/MaliciousMultiWordConsumer.json | 194 ++ contracts/abi/v0.8/MaliciousRequester.json | 167 ++ contracts/abi/v0.8/MockUpkeep.json | 218 ++ contracts/abi/v0.8/MultiWordConsumer.json | 471 ++++ contracts/abi/v0.8/OCR2Base.json | 19 +- .../abi/v0.8/SimpleLogUpkeepCounter.json | 182 +- contracts/abi/v0.8/SubscriptionAPI.json | 2 +- contracts/abi/v0.8/UpkeepAutoFunder.json | 261 ++ contracts/abi/v0.8/UpkeepMock.json | 239 ++ contracts/abi/v0.8/UpkeepReverter.json | 44 + contracts/abi/v0.8/UpkeepTranscoder5_0.json | 49 + .../VRFCoordinatorV2PlusUpgradedVersion.json | 2 +- contracts/abi/v0.8/VRFCoordinatorV2_5.json | 20 +- .../v0.8/VRFV2PlusLoadTestWithMetrics.json | 43 + contracts/abi/v0.8/VRFV2PlusWrapper.json | 162 +- .../v0.8/VRFV2PlusWrapperConsumerBase.json | 31 - .../v0.8/VRFV2PlusWrapperConsumerExample.json | 36 - .../VRFV2PlusWrapperLoadTestConsumer.json | 79 +- contracts/abi/v0.8/WETH9.json | 309 +++ contracts/package.json | 44 +- contracts/src/v0.8/ChainSpecificUtil.sol | 4 +- .../src/v0.8/ChainSpecificUtil_v0_8_6.sol | 161 ++ contracts/src/v0.8/ChainlinkClient.sol | 2 +- contracts/src/v0.8/Flags.sol | 2 +- contracts/src/v0.8/ValidatorProxy.sol | 2 +- contracts/src/v0.8/automation/Chainable.sol | 3 +- .../v0.8/automation/HeartbeatRequester.sol | 1 - contracts/src/v0.8/automation/README.md | 25 +- .../src/v0.8/automation/UpkeepTranscoder.sol | 1 - .../v0.8/automation/dev/MercuryRegistry.sol | 2 +- .../dev/MercuryRegistryBatchUpkeep.sol | 2 +- .../v2_3/IAutomationRegistryMaster2_3.sol | 160 +- .../dev/interfaces/v2_3/IWrappedNative.sol | 10 + .../dev/test/AutomationRegistrar2_3.t.sol | 214 ++ .../dev/test/AutomationRegistry2_3.t.sol | 1473 ++++++++-- .../v0.8/automation/dev/test/BaseTest.t.sol | 446 ++- .../src/v0.8/automation/dev/test/WETH9.sol | 93 + .../dev/v2_3/AutomationRegistrar2_3.sol | 348 +-- .../dev/v2_3/AutomationRegistry2_3.sol | 175 +- .../dev/v2_3/AutomationRegistryBase2_3.sol | 486 ++-- .../dev/v2_3/AutomationRegistryLogicA2_3.sol | 267 +- .../dev/v2_3/AutomationRegistryLogicB2_3.sol | 727 ++--- .../dev/v2_3/AutomationRegistryLogicC2_3.sol | 577 ++++ .../dev/v2_3/AutomationUtils2_3.sol | 29 +- .../dev/v2_3/UpkeepTranscoder5_0.sol | 46 + .../src/v0.8/automation/mocks/MockUpkeep.sol | 53 + .../testhelpers/SimpleLogUpkeepCounter.sol | 82 +- .../testhelpers/UpkeepAutoFunder.sol | 60 + .../automation/testhelpers/UpkeepMock.sol | 81 + .../automation/testhelpers/UpkeepReverter.sol | 17 + .../upkeeps/LinkAvailableBalanceMonitor.sol | 4 +- .../functions/dev/v1_X/FunctionsBilling.sol | 10 +- .../functions/dev/v1_X/FunctionsClient.sol | 3 +- .../dev/v1_X/FunctionsCoordinator.sol | 1 - .../functions/dev/v1_X/FunctionsRouter.sol | 1 - .../accessControl/TermsOfServiceAllowList.sol | 7 +- .../dev/v1_X/interfaces/IFunctionsBilling.sol | 2 +- .../v0.8/functions/dev/v1_X/ocr/OCR2Base.sol | 10 +- .../functions/v1_1_0/FunctionsCoordinator.sol | 1 - .../functions/v1_3_0/FunctionsBilling.sol | 440 +++ .../v0.8/functions/v1_3_0/FunctionsClient.sol | 62 + .../functions/v1_3_0/FunctionsCoordinator.sol | 228 ++ .../accessControl/TermsOfServiceAllowList.sol | 205 ++ .../interfaces/ITermsOfServiceAllowList.sol | 80 + .../v1_3_0/interfaces/IFunctionsBilling.sol | 73 + .../functions/v1_3_0/ocr/OCR2Abstract.sol | 103 + .../v0.8/functions/v1_3_0/ocr/OCR2Base.sol | 356 +++ .../src/v0.8/keystone/KeystoneForwarder.sol | 2 +- .../src/v0.8/keystone/libraries/Utils.sol | 2 +- .../src/v0.8/l2ep/dev/CrossDomainOwnable.sol | 6 +- contracts/src/v0.8/l2ep/dev/Flags.sol | 2 +- .../arbitrum/ArbitrumCrossDomainForwarder.sol | 4 +- .../arbitrum/ArbitrumCrossDomainGovernor.sol | 2 +- .../l2ep/dev/arbitrum/ArbitrumValidator.sol | 12 +- .../optimism/OptimismCrossDomainForwarder.sol | 10 +- .../optimism/OptimismCrossDomainGovernor.sol | 4 +- .../l2ep/dev/optimism/OptimismValidator.sol | 4 +- .../dev/scroll/ScrollCrossDomainForwarder.sol | 11 +- .../dev/scroll/ScrollCrossDomainGovernor.sol | 15 +- .../dev/scroll/ScrollSequencerUptimeFeed.sol | 1 - .../v0.8/l2ep/dev/scroll/ScrollValidator.sol | 7 +- .../optimism/MockOVMCrossDomainMessenger.sol | 3 +- .../dev/AuthorizedForwarder.sol | 3 +- .../dev/AuthorizedReceiver.sol | 2 +- .../dev/LinkTokenReceiver.sol | 2 +- .../v0.8/operatorforwarder/dev/Operator.sol | 23 +- .../operatorforwarder/dev/OperatorFactory.sol | 3 +- .../operatorforwarder/dev/test/operator.t.sol | 100 + .../dev/test/testhelpers/BasicConsumer.sol | 12 + .../testhelpers/ChainlinkClientHelper.sol | 22 + .../dev/test/testhelpers/Chainlinked.sol | 127 + .../dev/test/testhelpers/Consumer.sol | 55 + .../dev/test/testhelpers/EmptyOracle.sol | 28 + .../test/testhelpers/GasGuzzlingConsumer.sol | 41 + .../dev/test/testhelpers/GetterSetter.sol | 46 + .../test/testhelpers/MaliciousChainlink.sol | 67 + .../test/testhelpers/MaliciousChainlinked.sol | 116 + .../test/testhelpers/MaliciousConsumer.sol | 49 + .../MaliciousMultiWordConsumer.sol | 52 + .../test/testhelpers/MaliciousRequester.sol | 52 + .../test/testhelpers/MultiWordConsumer.sol | 128 + .../access/ConfirmedOwnerWithProposal.sol | 8 +- .../access/SimpleWriteAccessController.sol | 2 +- .../src/v0.8/shared/mocks/WERC20Mock.sol | 2 +- contracts/src/v0.8/shared/ocr2/OCR2Base.sol | 2 +- .../test/helpers/ChainReaderTestContract.sol | 2 +- .../test/helpers/LinkTokenTestHelper.sol | 12 + .../testhelpers/ConfirmedOwnerTestHelper.sol | 14 + .../src/v0.8/shared/token/ERC677/ERC677.sol | 5 +- .../transmission/dev/ERC-4337/Paymaster.sol | 2 +- .../v0.8/transmission/dev/ERC-4337/SCA.sol | 3 +- .../transmission/dev/ERC-4337/SCALibrary.sol | 2 +- .../ERC-4337/SmartContractAccountFactory.sol | 2 +- .../transmission/dev/testhelpers/Greeter.sol | 2 +- .../SmartContractAccountHelper.sol | 2 +- .../src/v0.8/transmission/test/BaseTest.t.sol | 17 + .../transmission/test/EIP_712_1014_4337.t.sol | 365 +++ .../src/v0.8/vrf/BatchBlockhashStore.sol | 4 +- .../src/v0.8/vrf/BatchVRFCoordinatorV2.sol | 2 +- contracts/src/v0.8/vrf/KeepersVRFConsumer.sol | 2 +- contracts/src/v0.8/vrf/VRF.sol | 30 +- contracts/src/v0.8/vrf/VRFConsumerBase.sol | 2 +- contracts/src/v0.8/vrf/VRFCoordinatorV2.sol | 3 +- contracts/src/v0.8/vrf/VRFOwner.sol | 2 +- contracts/src/v0.8/vrf/VRFTypes.sol | 2 +- contracts/src/v0.8/vrf/VRFV2Wrapper.sol | 16 +- .../src/v0.8/vrf/VRFV2WrapperConsumerBase.sol | 2 +- .../vrf/dev/BatchVRFCoordinatorV2Plus.sol | 12 +- contracts/src/v0.8/vrf/dev/BlockhashStore.sol | 8 +- .../src/v0.8/vrf/dev/SubscriptionAPI.sol | 27 +- .../v0.8/vrf/dev/TrustedBlockhashStore.sol | 2 +- .../v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol | 8 +- .../vrf/dev/VRFConsumerBaseV2Upgradeable.sol | 2 +- .../src/v0.8/vrf/dev/VRFCoordinatorV2_5.sol | 55 +- .../vrf/dev/VRFSubscriptionBalanceMonitor.sol | 8 +- .../src/v0.8/vrf/dev/VRFV2PlusWrapper.sol | 331 ++- .../vrf/dev/VRFV2PlusWrapperConsumerBase.sol | 33 +- .../dev/interfaces/IVRFCoordinatorV2Plus.sol | 2 +- .../IVRFMigratableConsumerV2Plus.sol | 2 +- .../dev/interfaces/IVRFSubscriptionV2Plus.sol | 4 +- .../vrf/dev/interfaces/IVRFV2PlusWrapper.sol | 11 +- .../testhelpers/ExposedVRFCoordinatorV2_5.sol | 3 +- .../VRFConsumerV2PlusUpgradeableExample.sol | 6 +- .../VRFCoordinatorV2PlusUpgradedVersion.sol | 14 +- .../VRFMaliciousConsumerV2Plus.sol | 2 +- .../testhelpers/VRFV2PlusConsumerExample.sol | 10 +- .../VRFV2PlusExternalSubOwnerExample.sol | 2 +- .../VRFV2PlusLoadTestWithMetrics.sol | 19 + .../testhelpers/VRFV2PlusRevertingExample.sol | 6 +- .../VRFV2PlusSingleConsumerExample.sol | 2 +- .../VRFV2PlusWrapperConsumerExample.sol | 13 +- .../VRFV2PlusWrapperLoadTestConsumer.sol | 31 +- .../src/v0.8/vrf/mocks/VRFCoordinatorMock.sol | 2 +- .../v0.8/vrf/mocks/VRFCoordinatorV2Mock.sol | 2 +- contracts/src/v0.8/vrf/test/BaseTest.t.sol | 17 + .../src/v0.8/vrf/test/ChainSpecificUtil.t.sol | 196 ++ .../v0.8/vrf/test/TrustedBlockhashStore.t.sol | 89 + .../v0.8/vrf/test/VRFCoordinatorV2Mock.t.sol | 381 +++ .../test/VRFCoordinatorV2Plus_Migration.t.sol | 352 +++ contracts/src/v0.8/vrf/test/VRFV2Plus.t.sol | 1099 ++++++++ .../vrf/test/VRFV2PlusSubscriptionAPI.t.sol | 649 +++++ .../src/v0.8/vrf/test/VRFV2PlusWrapper.t.sol | 458 +++ .../vrf/test/VRFV2PlusWrapper_Migration.t.sol | 380 +++ .../testhelpers/ChainSpecificUtilHelper.sol | 2 +- .../testhelpers/VRFV2LoadTestWithMetrics.sol | 2 +- .../testhelpers/VRFV2OwnerTestConsumer.sol | 2 +- .../vrf/testhelpers/VRFV2RevertingExample.sol | 6 +- .../VRFV2WrapperConsumerExample.sol | 4 +- .../VRFV2WrapperLoadTestConsumer.sol | 6 +- package.json | 2 +- version.txt | 2 +- yarn.lock | 32 +- 208 files changed, 20161 insertions(+), 4245 deletions(-) create mode 100644 contracts/abi/v0.8/AutomationRegistryLogicC2_3.json create mode 100644 contracts/abi/v0.8/BasicConsumer.json create mode 100644 contracts/abi/v0.8/ChainlinkClientHelper.json create mode 100644 contracts/abi/v0.8/Chainlinked.json create mode 100644 contracts/abi/v0.8/ConfirmedOwnerTestHelper.json create mode 100644 contracts/abi/v0.8/Consumer.json create mode 100644 contracts/abi/v0.8/EmptyOracle.json create mode 100644 contracts/abi/v0.8/GasGuzzlingConsumer.json create mode 100644 contracts/abi/v0.8/GetterSetter.json create mode 100644 contracts/abi/v0.8/IVRFCoordinatorV2PlusFulfill.json create mode 100644 contracts/abi/v0.8/IWrappedNative.json create mode 100644 contracts/abi/v0.8/LinkTokenTestHelper.json create mode 100644 contracts/abi/v0.8/MaliciousChainlinked.json create mode 100644 contracts/abi/v0.8/MaliciousConsumer.json create mode 100644 contracts/abi/v0.8/MaliciousMultiWordConsumer.json create mode 100644 contracts/abi/v0.8/MaliciousRequester.json create mode 100644 contracts/abi/v0.8/MockUpkeep.json create mode 100644 contracts/abi/v0.8/MultiWordConsumer.json create mode 100644 contracts/abi/v0.8/UpkeepAutoFunder.json create mode 100644 contracts/abi/v0.8/UpkeepMock.json create mode 100644 contracts/abi/v0.8/UpkeepReverter.json create mode 100644 contracts/abi/v0.8/UpkeepTranscoder5_0.json create mode 100644 contracts/abi/v0.8/WETH9.json create mode 100644 contracts/src/v0.8/ChainSpecificUtil_v0_8_6.sol create mode 100644 contracts/src/v0.8/automation/dev/interfaces/v2_3/IWrappedNative.sol create mode 100644 contracts/src/v0.8/automation/dev/test/AutomationRegistrar2_3.t.sol create mode 100644 contracts/src/v0.8/automation/dev/test/WETH9.sol create mode 100644 contracts/src/v0.8/automation/dev/v2_3/AutomationRegistryLogicC2_3.sol create mode 100644 contracts/src/v0.8/automation/dev/v2_3/UpkeepTranscoder5_0.sol create mode 100644 contracts/src/v0.8/automation/mocks/MockUpkeep.sol create mode 100644 contracts/src/v0.8/automation/testhelpers/UpkeepAutoFunder.sol create mode 100644 contracts/src/v0.8/automation/testhelpers/UpkeepMock.sol create mode 100644 contracts/src/v0.8/automation/testhelpers/UpkeepReverter.sol create mode 100644 contracts/src/v0.8/functions/v1_3_0/FunctionsBilling.sol create mode 100644 contracts/src/v0.8/functions/v1_3_0/FunctionsClient.sol create mode 100644 contracts/src/v0.8/functions/v1_3_0/FunctionsCoordinator.sol create mode 100644 contracts/src/v0.8/functions/v1_3_0/accessControl/TermsOfServiceAllowList.sol create mode 100644 contracts/src/v0.8/functions/v1_3_0/accessControl/interfaces/ITermsOfServiceAllowList.sol create mode 100644 contracts/src/v0.8/functions/v1_3_0/interfaces/IFunctionsBilling.sol create mode 100644 contracts/src/v0.8/functions/v1_3_0/ocr/OCR2Abstract.sol create mode 100644 contracts/src/v0.8/functions/v1_3_0/ocr/OCR2Base.sol create mode 100644 contracts/src/v0.8/operatorforwarder/dev/test/operator.t.sol create mode 100644 contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/BasicConsumer.sol create mode 100644 contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/ChainlinkClientHelper.sol create mode 100644 contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/Chainlinked.sol create mode 100644 contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/Consumer.sol create mode 100644 contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/EmptyOracle.sol create mode 100644 contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/GasGuzzlingConsumer.sol create mode 100644 contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/GetterSetter.sol create mode 100644 contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/MaliciousChainlink.sol create mode 100644 contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/MaliciousChainlinked.sol create mode 100644 contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/MaliciousConsumer.sol create mode 100644 contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/MaliciousMultiWordConsumer.sol create mode 100644 contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/MaliciousRequester.sol create mode 100644 contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/MultiWordConsumer.sol create mode 100644 contracts/src/v0.8/shared/test/helpers/LinkTokenTestHelper.sol create mode 100644 contracts/src/v0.8/shared/test/testhelpers/ConfirmedOwnerTestHelper.sol create mode 100644 contracts/src/v0.8/transmission/test/BaseTest.t.sol create mode 100644 contracts/src/v0.8/transmission/test/EIP_712_1014_4337.t.sol create mode 100644 contracts/src/v0.8/vrf/test/BaseTest.t.sol create mode 100644 contracts/src/v0.8/vrf/test/ChainSpecificUtil.t.sol create mode 100644 contracts/src/v0.8/vrf/test/TrustedBlockhashStore.t.sol create mode 100644 contracts/src/v0.8/vrf/test/VRFCoordinatorV2Mock.t.sol create mode 100644 contracts/src/v0.8/vrf/test/VRFCoordinatorV2Plus_Migration.t.sol create mode 100644 contracts/src/v0.8/vrf/test/VRFV2Plus.t.sol create mode 100644 contracts/src/v0.8/vrf/test/VRFV2PlusSubscriptionAPI.t.sol create mode 100644 contracts/src/v0.8/vrf/test/VRFV2PlusWrapper.t.sol create mode 100644 contracts/src/v0.8/vrf/test/VRFV2PlusWrapper_Migration.t.sol diff --git a/contracts/abi/v0.8/AuthorizedReceiver.json b/contracts/abi/v0.8/AuthorizedReceiver.json index aad6646a..946d5ed3 100644 --- a/contracts/abi/v0.8/AuthorizedReceiver.json +++ b/contracts/abi/v0.8/AuthorizedReceiver.json @@ -1,19 +1,4 @@ [ - { - "inputs": [], - "name": "EmptySendersList", - "type": "error" - }, - { - "inputs": [], - "name": "NotAllowedToSetSenders", - "type": "error" - }, - { - "inputs": [], - "name": "UnauthorizedSender", - "type": "error" - }, { "anonymous": false, "inputs": [ diff --git a/contracts/abi/v0.8/AutomationRegistrar2_3.json b/contracts/abi/v0.8/AutomationRegistrar2_3.json index 1ba8fdb4..90178da3 100644 --- a/contracts/abi/v0.8/AutomationRegistrar2_3.json +++ b/contracts/abi/v0.8/AutomationRegistrar2_3.json @@ -7,15 +7,10 @@ "type": "address" }, { - "internalType": "address", - "name": "AutomationRegistry", + "internalType": "contract IAutomationRegistryMaster2_3", + "name": "registry", "type": "address" }, - { - "internalType": "uint96", - "name": "minLINKJuels", - "type": "uint96" - }, { "components": [ { @@ -37,21 +32,26 @@ "internalType": "struct AutomationRegistrar2_3.InitialTriggerConfig[]", "name": "triggerConfigs", "type": "tuple[]" + }, + { + "internalType": "contract IERC20[]", + "name": "billingTokens", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "minRegistrationFees", + "type": "uint256[]" + }, + { + "internalType": "contract IWrappedNative", + "name": "wrappedNativeToken", + "type": "address" } ], "stateMutability": "nonpayable", "type": "constructor" }, - { - "inputs": [], - "name": "AmountMismatch", - "type": "error" - }, - { - "inputs": [], - "name": "FunctionNotPermitted", - "type": "error" - }, { "inputs": [], "name": "HashMismatch", @@ -69,18 +69,12 @@ }, { "inputs": [], - "name": "InvalidDataLength", + "name": "InvalidBillingToken", "type": "error" }, { - "inputs": [ - { - "internalType": "address", - "name": "to", - "type": "address" - } - ], - "name": "LinkTransferFailed", + "inputs": [], + "name": "InvalidDataLength", "type": "error" }, { @@ -93,19 +87,20 @@ "name": "OnlyLink", "type": "error" }, - { - "inputs": [], - "name": "RegistrationRequestFailed", - "type": "error" - }, { "inputs": [], "name": "RequestNotFound", "type": "error" }, { - "inputs": [], - "name": "SenderMismatch", + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "TransferFailed", "type": "error" }, { @@ -129,20 +124,7 @@ }, { "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "AutomationRegistry", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint96", - "name": "minLINKJuels", - "type": "uint96" - } - ], + "inputs": [], "name": "ConfigChanged", "type": "event" }, @@ -320,19 +302,6 @@ "name": "TriggerConfigSet", "type": "event" }, - { - "inputs": [], - "name": "LINK", - "outputs": [ - { - "internalType": "contract LinkTokenInterface", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "acceptOwnership", @@ -343,44 +312,66 @@ { "inputs": [ { - "internalType": "string", - "name": "name", - "type": "string" - }, - { - "internalType": "address", - "name": "upkeepContract", - "type": "address" - }, - { - "internalType": "uint32", - "name": "gasLimit", - "type": "uint32" - }, - { - "internalType": "address", - "name": "adminAddress", - "type": "address" - }, - { - "internalType": "uint8", - "name": "triggerType", - "type": "uint8" - }, - { - "internalType": "bytes", - "name": "checkData", - "type": "bytes" - }, - { - "internalType": "bytes", - "name": "triggerConfig", - "type": "bytes" - }, - { - "internalType": "bytes", - "name": "offchainConfig", - "type": "bytes" + "components": [ + { + "internalType": "address", + "name": "upkeepContract", + "type": "address" + }, + { + "internalType": "uint96", + "name": "amount", + "type": "uint96" + }, + { + "internalType": "address", + "name": "adminAddress", + "type": "address" + }, + { + "internalType": "uint32", + "name": "gasLimit", + "type": "uint32" + }, + { + "internalType": "uint8", + "name": "triggerType", + "type": "uint8" + }, + { + "internalType": "contract IERC20", + "name": "billingToken", + "type": "address" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "bytes", + "name": "encryptedEmail", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "checkData", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "triggerConfig", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "offchainConfig", + "type": "bytes" + } + ], + "internalType": "struct AutomationRegistrar2_3.RegistrationParams", + "name": "requestParams", + "type": "tuple" }, { "internalType": "bytes32", @@ -426,17 +417,18 @@ "type": "function" }, { - "inputs": [], - "name": "getConfig", - "outputs": [ + "inputs": [ { - "internalType": "address", - "name": "AutomationRegistry", + "internalType": "contract IERC20", + "name": "billingToken", "type": "address" - }, + } + ], + "name": "getMinimumRegistrationAmount", + "outputs": [ { "internalType": "uint256", - "name": "minLINKJuels", + "name": "", "type": "uint256" } ], @@ -467,6 +459,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "getRegistry", + "outputs": [ + { + "internalType": "contract IAutomationRegistryMaster2_3", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -504,34 +509,24 @@ "type": "function" }, { - "inputs": [ + "inputs": [], + "name": "i_LINK", + "outputs": [ { - "internalType": "address", - "name": "sender", + "internalType": "contract LinkTokenInterface", + "name": "", "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, - { - "internalType": "bytes", - "name": "data", - "type": "bytes" } ], - "name": "onTokenTransfer", - "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "view", "type": "function" }, { "inputs": [], - "name": "owner", + "name": "i_WRAPPED_NATIVE_TOKEN", "outputs": [ { - "internalType": "address", + "internalType": "contract IWrappedNative", "name": "", "type": "address" } @@ -541,65 +536,38 @@ }, { "inputs": [ - { - "internalType": "string", - "name": "name", - "type": "string" - }, - { - "internalType": "bytes", - "name": "encryptedEmail", - "type": "bytes" - }, { "internalType": "address", - "name": "upkeepContract", - "type": "address" - }, - { - "internalType": "uint32", - "name": "gasLimit", - "type": "uint32" - }, - { - "internalType": "address", - "name": "adminAddress", + "name": "sender", "type": "address" }, { - "internalType": "uint8", - "name": "triggerType", - "type": "uint8" - }, - { - "internalType": "bytes", - "name": "checkData", - "type": "bytes" - }, - { - "internalType": "bytes", - "name": "triggerConfig", - "type": "bytes" + "internalType": "uint256", + "name": "amount", + "type": "uint256" }, { "internalType": "bytes", - "name": "offchainConfig", + "name": "data", "type": "bytes" - }, - { - "internalType": "uint96", - "name": "amount", - "type": "uint96" - }, + } + ], + "name": "onTokenTransfer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ { "internalType": "address", - "name": "sender", + "name": "", "type": "address" } ], - "name": "register", - "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "view", "type": "function" }, { @@ -607,18 +575,18 @@ { "components": [ { - "internalType": "string", - "name": "name", - "type": "string" + "internalType": "address", + "name": "upkeepContract", + "type": "address" }, { - "internalType": "bytes", - "name": "encryptedEmail", - "type": "bytes" + "internalType": "uint96", + "name": "amount", + "type": "uint96" }, { "internalType": "address", - "name": "upkeepContract", + "name": "adminAddress", "type": "address" }, { @@ -626,16 +594,26 @@ "name": "gasLimit", "type": "uint32" }, - { - "internalType": "address", - "name": "adminAddress", - "type": "address" - }, { "internalType": "uint8", "name": "triggerType", "type": "uint8" }, + { + "internalType": "contract IERC20", + "name": "billingToken", + "type": "address" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "bytes", + "name": "encryptedEmail", + "type": "bytes" + }, { "internalType": "bytes", "name": "checkData", @@ -650,11 +628,6 @@ "internalType": "bytes", "name": "offchainConfig", "type": "bytes" - }, - { - "internalType": "uint96", - "name": "amount", - "type": "uint96" } ], "internalType": "struct AutomationRegistrar2_3.RegistrationParams", @@ -670,7 +643,7 @@ "type": "uint256" } ], - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "function" }, { @@ -694,14 +667,19 @@ { "inputs": [ { - "internalType": "address", - "name": "AutomationRegistry", + "internalType": "contract IAutomationRegistryMaster2_3", + "name": "registry", "type": "address" }, { - "internalType": "uint96", - "name": "minLINKJuels", - "type": "uint96" + "internalType": "contract IERC20[]", + "name": "billingTokens", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "minBalances", + "type": "uint256[]" } ], "name": "setConfig", diff --git a/contracts/abi/v0.8/AutomationRegistry2_2.json b/contracts/abi/v0.8/AutomationRegistry2_2.json index e5a4183c..15eee022 100644 --- a/contracts/abi/v0.8/AutomationRegistry2_2.json +++ b/contracts/abi/v0.8/AutomationRegistry2_2.json @@ -1008,7 +1008,7 @@ "type": "event" }, { - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "fallback" }, { diff --git a/contracts/abi/v0.8/AutomationRegistry2_3.json b/contracts/abi/v0.8/AutomationRegistry2_3.json index bec19832..e2fa3c21 100644 --- a/contracts/abi/v0.8/AutomationRegistry2_3.json +++ b/contracts/abi/v0.8/AutomationRegistry2_3.json @@ -2,7 +2,7 @@ { "inputs": [ { - "internalType": "contract AutomationRegistryLogicB2_3", + "internalType": "contract AutomationRegistryLogicA2_3", "name": "logicA", "type": "address" } @@ -86,6 +86,11 @@ "name": "InsufficientBalance", "type": "error" }, + { + "inputs": [], + "name": "InsufficientLinkLiquidity", + "type": "error" + }, { "inputs": [], "name": "InvalidDataLength", @@ -116,6 +121,11 @@ "name": "InvalidSigner", "type": "error" }, + { + "inputs": [], + "name": "InvalidToken", + "type": "error" + }, { "inputs": [], "name": "InvalidTransmitter", @@ -133,17 +143,17 @@ }, { "inputs": [], - "name": "MaxCheckDataSizeCanOnlyIncrease", + "name": "MigrationNotPermitted", "type": "error" }, { "inputs": [], - "name": "MaxPerformDataSizeCanOnlyIncrease", + "name": "MustSettleOffchain", "type": "error" }, { "inputs": [], - "name": "MigrationNotPermitted", + "name": "MustSettleOnchain", "type": "error" }, { @@ -226,11 +236,6 @@ "name": "ParameterLengthError", "type": "error" }, - { - "inputs": [], - "name": "PaymentGreaterThanAllLINK", - "type": "error" - }, { "inputs": [], "name": "ReentrantCall", @@ -326,6 +331,50 @@ "name": "AdminPrivilegeConfigSet", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "gasFeePPB", + "type": "uint32" + }, + { + "internalType": "uint24", + "name": "flatFeeMilliCents", + "type": "uint24" + } + ], + "indexed": false, + "internalType": "struct AutomationRegistryBase2_3.BillingOverrides", + "name": "overrides", + "type": "tuple" + } + ], + "name": "BillingConfigOverridden", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "BillingConfigOverrideRemoved", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -344,13 +393,23 @@ }, { "internalType": "uint24", - "name": "flatFeeMicroLink", + "name": "flatFeeMilliCents", "type": "uint24" }, { - "internalType": "address", + "internalType": "contract AggregatorV3Interface", "name": "priceFeed", "type": "address" + }, + { + "internalType": "uint256", + "name": "fallbackPrice", + "type": "uint256" + }, + { + "internalType": "uint96", + "name": "minSpend", + "type": "uint96" } ], "indexed": false, @@ -474,13 +533,13 @@ { "indexed": true, "internalType": "address", - "name": "recipient", + "name": "assetAddress", "type": "address" }, { "indexed": true, "internalType": "address", - "name": "assetAddress", + "name": "recipient", "type": "address" }, { @@ -562,6 +621,25 @@ "name": "InsufficientFundsUpkeepReport", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address[]", + "name": "payees", + "type": "address[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "payments", + "type": "uint256[]" + } + ], + "name": "NOPsSettledOffchain", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -1092,7 +1170,7 @@ "type": "event" }, { - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "fallback" }, { @@ -1102,6 +1180,24 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "uint96", + "name": "amount", + "type": "uint96" + } + ], + "name": "addFunds", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, { "inputs": [], "name": "fallbackTo", @@ -1256,53 +1352,58 @@ "components": [ { "internalType": "uint32", - "name": "paymentPremiumPPB", + "name": "checkGasLimit", "type": "uint32" }, { "internalType": "uint32", - "name": "flatFeeMicroLink", + "name": "maxPerformGas", "type": "uint32" }, { "internalType": "uint32", - "name": "checkGasLimit", + "name": "maxCheckDataSize", "type": "uint32" }, { - "internalType": "uint24", - "name": "stalenessSeconds", - "type": "uint24" + "internalType": "address", + "name": "transcoder", + "type": "address" }, { - "internalType": "uint16", - "name": "gasCeilingMultiplier", - "type": "uint16" + "internalType": "bool", + "name": "reorgProtectionEnabled", + "type": "bool" }, { - "internalType": "uint96", - "name": "minUpkeepSpend", - "type": "uint96" + "internalType": "uint24", + "name": "stalenessSeconds", + "type": "uint24" }, { "internalType": "uint32", - "name": "maxPerformGas", + "name": "maxPerformDataSize", "type": "uint32" }, { "internalType": "uint32", - "name": "maxCheckDataSize", + "name": "maxRevertDataSize", "type": "uint32" }, { - "internalType": "uint32", - "name": "maxPerformDataSize", - "type": "uint32" + "internalType": "address", + "name": "upkeepPrivilegeManager", + "type": "address" }, { - "internalType": "uint32", - "name": "maxRevertDataSize", - "type": "uint32" + "internalType": "uint16", + "name": "gasCeilingMultiplier", + "type": "uint16" + }, + { + "internalType": "address", + "name": "financeAdmin", + "type": "address" }, { "internalType": "uint256", @@ -1319,35 +1420,15 @@ "name": "fallbackNativePrice", "type": "uint256" }, - { - "internalType": "address", - "name": "transcoder", - "type": "address" - }, { "internalType": "address[]", "name": "registrars", "type": "address[]" }, - { - "internalType": "address", - "name": "upkeepPrivilegeManager", - "type": "address" - }, { "internalType": "contract IChainModule", "name": "chainModule", "type": "address" - }, - { - "internalType": "bool", - "name": "reorgProtectionEnabled", - "type": "bool" - }, - { - "internalType": "address", - "name": "financeAdmin", - "type": "address" } ], "internalType": "struct AutomationRegistryBase2_3.OnchainConfig", @@ -1378,13 +1459,23 @@ }, { "internalType": "uint24", - "name": "flatFeeMicroLink", + "name": "flatFeeMilliCents", "type": "uint24" }, { - "internalType": "address", + "internalType": "contract AggregatorV3Interface", "name": "priceFeed", "type": "address" + }, + { + "internalType": "uint256", + "name": "fallbackPrice", + "type": "uint256" + }, + { + "internalType": "uint96", + "name": "minSpend", + "type": "uint96" } ], "internalType": "struct AutomationRegistryBase2_3.BillingConfig[]", @@ -1397,35 +1488,6 @@ "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": "nonpayable", - "type": "function" - }, { "inputs": [ { diff --git a/contracts/abi/v0.8/AutomationRegistryBase2_3.json b/contracts/abi/v0.8/AutomationRegistryBase2_3.json index 84f57ae7..11726877 100644 --- a/contracts/abi/v0.8/AutomationRegistryBase2_3.json +++ b/contracts/abi/v0.8/AutomationRegistryBase2_3.json @@ -75,6 +75,11 @@ "name": "InsufficientBalance", "type": "error" }, + { + "inputs": [], + "name": "InsufficientLinkLiquidity", + "type": "error" + }, { "inputs": [], "name": "InvalidDataLength", @@ -105,6 +110,11 @@ "name": "InvalidSigner", "type": "error" }, + { + "inputs": [], + "name": "InvalidToken", + "type": "error" + }, { "inputs": [], "name": "InvalidTransmitter", @@ -122,17 +132,17 @@ }, { "inputs": [], - "name": "MaxCheckDataSizeCanOnlyIncrease", + "name": "MigrationNotPermitted", "type": "error" }, { "inputs": [], - "name": "MaxPerformDataSizeCanOnlyIncrease", + "name": "MustSettleOffchain", "type": "error" }, { "inputs": [], - "name": "MigrationNotPermitted", + "name": "MustSettleOnchain", "type": "error" }, { @@ -215,11 +225,6 @@ "name": "ParameterLengthError", "type": "error" }, - { - "inputs": [], - "name": "PaymentGreaterThanAllLINK", - "type": "error" - }, { "inputs": [], "name": "ReentrantCall", @@ -315,6 +320,50 @@ "name": "AdminPrivilegeConfigSet", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "gasFeePPB", + "type": "uint32" + }, + { + "internalType": "uint24", + "name": "flatFeeMilliCents", + "type": "uint24" + } + ], + "indexed": false, + "internalType": "struct AutomationRegistryBase2_3.BillingOverrides", + "name": "overrides", + "type": "tuple" + } + ], + "name": "BillingConfigOverridden", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "BillingConfigOverrideRemoved", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -333,13 +382,23 @@ }, { "internalType": "uint24", - "name": "flatFeeMicroLink", + "name": "flatFeeMilliCents", "type": "uint24" }, { - "internalType": "address", + "internalType": "contract AggregatorV3Interface", "name": "priceFeed", "type": "address" + }, + { + "internalType": "uint256", + "name": "fallbackPrice", + "type": "uint256" + }, + { + "internalType": "uint96", + "name": "minSpend", + "type": "uint96" } ], "indexed": false, @@ -402,13 +461,13 @@ { "indexed": true, "internalType": "address", - "name": "recipient", + "name": "assetAddress", "type": "address" }, { "indexed": true, "internalType": "address", - "name": "assetAddress", + "name": "recipient", "type": "address" }, { @@ -490,6 +549,25 @@ "name": "InsufficientFundsUpkeepReport", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address[]", + "name": "payees", + "type": "address[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "payments", + "type": "uint256[]" + } + ], + "name": "NOPsSettledOffchain", + "type": "event" + }, { "anonymous": false, "inputs": [ diff --git a/contracts/abi/v0.8/AutomationRegistryBaseInterface.json b/contracts/abi/v0.8/AutomationRegistryBaseInterface.json index 6598f642..fa6a5975 100644 --- a/contracts/abi/v0.8/AutomationRegistryBaseInterface.json +++ b/contracts/abi/v0.8/AutomationRegistryBaseInterface.json @@ -1,4 +1,17 @@ [ + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "acceptUpkeepAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -239,11 +252,29 @@ "internalType": "uint96", "name": "amountSpent", "type": "uint96" + }, + { + "internalType": "bool", + "name": "paused", + "type": "bool" } ], "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "pauseUpkeep", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -319,5 +350,54 @@ "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": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "unpauseUpkeep", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "newCheckData", + "type": "bytes" + } + ], + "name": "updateCheckData", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" } ] diff --git a/contracts/abi/v0.8/AutomationRegistryExecutableInterface.json b/contracts/abi/v0.8/AutomationRegistryExecutableInterface.json index 36b9515c..d90e80ee 100644 --- a/contracts/abi/v0.8/AutomationRegistryExecutableInterface.json +++ b/contracts/abi/v0.8/AutomationRegistryExecutableInterface.json @@ -1,4 +1,17 @@ [ + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "acceptUpkeepAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -283,11 +296,29 @@ "internalType": "uint96", "name": "amountSpent", "type": "uint96" + }, + { + "internalType": "bool", + "name": "paused", + "type": "bool" } ], "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "pauseUpkeep", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -363,5 +394,54 @@ "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": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "unpauseUpkeep", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "newCheckData", + "type": "bytes" + } + ], + "name": "updateCheckData", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" } ] diff --git a/contracts/abi/v0.8/AutomationRegistryInterface.json b/contracts/abi/v0.8/AutomationRegistryInterface.json index 784dfee1..f4401b7e 100644 --- a/contracts/abi/v0.8/AutomationRegistryInterface.json +++ b/contracts/abi/v0.8/AutomationRegistryInterface.json @@ -1,4 +1,17 @@ [ + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "acceptUpkeepAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -283,11 +296,29 @@ "internalType": "uint96", "name": "amountSpent", "type": "uint96" + }, + { + "internalType": "bool", + "name": "paused", + "type": "bool" } ], "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "pauseUpkeep", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -363,5 +394,54 @@ "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": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "unpauseUpkeep", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "newCheckData", + "type": "bytes" + } + ], + "name": "updateCheckData", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" } ] diff --git a/contracts/abi/v0.8/AutomationRegistryLogicA2_2.json b/contracts/abi/v0.8/AutomationRegistryLogicA2_2.json index 12b945dc..dc01d9ec 100644 --- a/contracts/abi/v0.8/AutomationRegistryLogicA2_2.json +++ b/contracts/abi/v0.8/AutomationRegistryLogicA2_2.json @@ -928,7 +928,7 @@ "type": "event" }, { - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "fallback" }, { diff --git a/contracts/abi/v0.8/AutomationRegistryLogicA2_3.json b/contracts/abi/v0.8/AutomationRegistryLogicA2_3.json index 8312221b..6dec1224 100644 --- a/contracts/abi/v0.8/AutomationRegistryLogicA2_3.json +++ b/contracts/abi/v0.8/AutomationRegistryLogicA2_3.json @@ -86,6 +86,11 @@ "name": "InsufficientBalance", "type": "error" }, + { + "inputs": [], + "name": "InsufficientLinkLiquidity", + "type": "error" + }, { "inputs": [], "name": "InvalidDataLength", @@ -116,6 +121,11 @@ "name": "InvalidSigner", "type": "error" }, + { + "inputs": [], + "name": "InvalidToken", + "type": "error" + }, { "inputs": [], "name": "InvalidTransmitter", @@ -133,17 +143,17 @@ }, { "inputs": [], - "name": "MaxCheckDataSizeCanOnlyIncrease", + "name": "MigrationNotPermitted", "type": "error" }, { "inputs": [], - "name": "MaxPerformDataSizeCanOnlyIncrease", + "name": "MustSettleOffchain", "type": "error" }, { "inputs": [], - "name": "MigrationNotPermitted", + "name": "MustSettleOnchain", "type": "error" }, { @@ -226,11 +236,6 @@ "name": "ParameterLengthError", "type": "error" }, - { - "inputs": [], - "name": "PaymentGreaterThanAllLINK", - "type": "error" - }, { "inputs": [], "name": "ReentrantCall", @@ -326,6 +331,50 @@ "name": "AdminPrivilegeConfigSet", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "gasFeePPB", + "type": "uint32" + }, + { + "internalType": "uint24", + "name": "flatFeeMilliCents", + "type": "uint24" + } + ], + "indexed": false, + "internalType": "struct AutomationRegistryBase2_3.BillingOverrides", + "name": "overrides", + "type": "tuple" + } + ], + "name": "BillingConfigOverridden", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "BillingConfigOverrideRemoved", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -344,13 +393,23 @@ }, { "internalType": "uint24", - "name": "flatFeeMicroLink", + "name": "flatFeeMilliCents", "type": "uint24" }, { - "internalType": "address", + "internalType": "contract AggregatorV3Interface", "name": "priceFeed", "type": "address" + }, + { + "internalType": "uint256", + "name": "fallbackPrice", + "type": "uint256" + }, + { + "internalType": "uint96", + "name": "minSpend", + "type": "uint96" } ], "indexed": false, @@ -413,13 +472,13 @@ { "indexed": true, "internalType": "address", - "name": "recipient", + "name": "assetAddress", "type": "address" }, { "indexed": true, "internalType": "address", - "name": "assetAddress", + "name": "recipient", "type": "address" }, { @@ -501,6 +560,25 @@ "name": "InsufficientFundsUpkeepReport", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address[]", + "name": "payees", + "type": "address[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "payments", + "type": "uint256[]" + } + ], + "name": "NOPsSettledOffchain", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -1012,7 +1090,7 @@ "type": "event" }, { - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "fallback" }, { @@ -1022,24 +1100,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "id", - "type": "uint256" - }, - { - "internalType": "uint96", - "name": "amount", - "type": "uint96" - } - ], - "name": "addFunds", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [ { @@ -1053,192 +1113,6 @@ "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 AutomationRegistryBase2_3.UpkeepFailureReason", - "name": "upkeepFailureReason", - "type": "uint8" - }, - { - "internalType": "uint256", - "name": "gasUsed", - "type": "uint256" - } - ], - "stateMutability": "nonpayable", - "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 AutomationRegistryBase2_3.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": "linkUSD", - "type": "uint256" - } - ], - "stateMutability": "nonpayable", - "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 AutomationRegistryBase2_3.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": "linkUSD", - "type": "uint256" - } - ], - "stateMutability": "nonpayable", - "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 AutomationRegistryBase2_3.UpkeepFailureReason", - "name": "upkeepFailureReason", - "type": "uint8" - }, - { - "internalType": "uint256", - "name": "gasUsed", - "type": "uint256" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [], "name": "fallbackTo", @@ -1319,52 +1193,18 @@ "type": "uint8" }, { - "internalType": "bytes", - "name": "checkData", - "type": "bytes" - }, - { - "internalType": "bytes", - "name": "triggerConfig", - "type": "bytes" + "internalType": "contract IERC20", + "name": "billingToken", + "type": "address" }, { "internalType": "bytes", - "name": "offchainConfig", + "name": "checkData", "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", + "name": "triggerConfig", "type": "bytes" }, { @@ -1384,24 +1224,6 @@ "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": [ { diff --git a/contracts/abi/v0.8/AutomationRegistryLogicB2_3.json b/contracts/abi/v0.8/AutomationRegistryLogicB2_3.json index 4e9645c3..642a8f1b 100644 --- a/contracts/abi/v0.8/AutomationRegistryLogicB2_3.json +++ b/contracts/abi/v0.8/AutomationRegistryLogicB2_3.json @@ -2,33 +2,8 @@ { "inputs": [ { - "internalType": "address", - "name": "link", - "type": "address" - }, - { - "internalType": "address", - "name": "linkUSDFeed", - "type": "address" - }, - { - "internalType": "address", - "name": "nativeUSDFeed", - "type": "address" - }, - { - "internalType": "address", - "name": "fastGasFeed", - "type": "address" - }, - { - "internalType": "address", - "name": "automationForwarderLogic", - "type": "address" - }, - { - "internalType": "address", - "name": "allowedReadOnlyAddress", + "internalType": "contract AutomationRegistryLogicC2_3", + "name": "logicC", "type": "address" } ], @@ -111,6 +86,11 @@ "name": "InsufficientBalance", "type": "error" }, + { + "inputs": [], + "name": "InsufficientLinkLiquidity", + "type": "error" + }, { "inputs": [], "name": "InvalidDataLength", @@ -141,6 +121,11 @@ "name": "InvalidSigner", "type": "error" }, + { + "inputs": [], + "name": "InvalidToken", + "type": "error" + }, { "inputs": [], "name": "InvalidTransmitter", @@ -158,17 +143,17 @@ }, { "inputs": [], - "name": "MaxCheckDataSizeCanOnlyIncrease", + "name": "MigrationNotPermitted", "type": "error" }, { "inputs": [], - "name": "MaxPerformDataSizeCanOnlyIncrease", + "name": "MustSettleOffchain", "type": "error" }, { "inputs": [], - "name": "MigrationNotPermitted", + "name": "MustSettleOnchain", "type": "error" }, { @@ -251,11 +236,6 @@ "name": "ParameterLengthError", "type": "error" }, - { - "inputs": [], - "name": "PaymentGreaterThanAllLINK", - "type": "error" - }, { "inputs": [], "name": "ReentrantCall", @@ -351,6 +331,50 @@ "name": "AdminPrivilegeConfigSet", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "gasFeePPB", + "type": "uint32" + }, + { + "internalType": "uint24", + "name": "flatFeeMilliCents", + "type": "uint24" + } + ], + "indexed": false, + "internalType": "struct AutomationRegistryBase2_3.BillingOverrides", + "name": "overrides", + "type": "tuple" + } + ], + "name": "BillingConfigOverridden", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "BillingConfigOverrideRemoved", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -369,13 +393,23 @@ }, { "internalType": "uint24", - "name": "flatFeeMicroLink", + "name": "flatFeeMilliCents", "type": "uint24" }, { - "internalType": "address", + "internalType": "contract AggregatorV3Interface", "name": "priceFeed", "type": "address" + }, + { + "internalType": "uint256", + "name": "fallbackPrice", + "type": "uint256" + }, + { + "internalType": "uint96", + "name": "minSpend", + "type": "uint96" } ], "indexed": false, @@ -438,13 +472,13 @@ { "indexed": true, "internalType": "address", - "name": "recipient", + "name": "assetAddress", "type": "address" }, { "indexed": true, "internalType": "address", - "name": "assetAddress", + "name": "recipient", "type": "address" }, { @@ -526,6 +560,25 @@ "name": "InsufficientFundsUpkeepReport", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address[]", + "name": "payees", + "type": "address[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "payments", + "type": "uint256[]" + } + ], + "name": "NOPsSettledOffchain", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -1036,6 +1089,10 @@ "name": "UpkeepUnpaused", "type": "event" }, + { + "stateMutability": "payable", + "type": "fallback" + }, { "inputs": [], "name": "acceptOwnership", @@ -1046,12 +1103,12 @@ { "inputs": [ { - "internalType": "address", - "name": "transmitter", - "type": "address" + "internalType": "uint256", + "name": "id", + "type": "uint256" } ], - "name": "acceptPayeeship", + "name": "acceptUpkeepAdmin", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -1062,10 +1119,41 @@ "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 AutomationRegistryBase2_3.UpkeepFailureReason", + "name": "upkeepFailureReason", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "gasUsed", + "type": "uint256" } ], - "name": "acceptUpkeepAdmin", - "outputs": [], "stateMutability": "nonpayable", "type": "function" }, @@ -1073,48 +1161,147 @@ "inputs": [ { "internalType": "uint256", - "name": "startIndex", + "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 AutomationRegistryBase2_3.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": "maxCount", + "name": "linkUSD", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", "type": "uint256" } ], - "name": "getActiveUpkeepIDs", + "name": "checkUpkeep", "outputs": [ { - "internalType": "uint256[]", - "name": "", - "type": "uint256[]" + "internalType": "bool", + "name": "upkeepNeeded", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "performData", + "type": "bytes" + }, + { + "internalType": "enum AutomationRegistryBase2_3.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": "linkUSD", + "type": "uint256" } ], - "stateMutability": "view", + "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { - "internalType": "address", - "name": "admin", - "type": "address" + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "payload", + "type": "bytes" } ], - "name": "getAdminPrivilegeConfig", + "name": "executeCallback", "outputs": [ + { + "internalType": "bool", + "name": "upkeepNeeded", + "type": "bool" + }, { "internalType": "bytes", - "name": "", + "name": "performData", "type": "bytes" + }, + { + "internalType": "enum AutomationRegistryBase2_3.UpkeepFailureReason", + "name": "upkeepFailureReason", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "gasUsed", + "type": "uint256" } ], - "stateMutability": "view", + "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], - "name": "getAllowedReadOnlyAddress", + "name": "fallbackTo", "outputs": [ { "internalType": "address", @@ -1127,7 +1314,7 @@ }, { "inputs": [], - "name": "getAutomationForwarderLogic", + "name": "owner", "outputs": [ { "internalType": "address", @@ -1146,590 +1333,22 @@ "type": "uint256" } ], - "name": "getBalance", - "outputs": [ + "name": "pauseUpkeep", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ { - "internalType": "uint96", - "name": "balance", - "type": "uint96" + "internalType": "uint256", + "name": "id", + "type": "uint256" } ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "contract IERC20", - "name": "token", - "type": "address" - } - ], - "name": "getBillingTokenConfig", - "outputs": [ - { - "components": [ - { - "internalType": "uint32", - "name": "gasFeePPB", - "type": "uint32" - }, - { - "internalType": "uint24", - "name": "flatFeeMicroLink", - "type": "uint24" - }, - { - "internalType": "address", - "name": "priceFeed", - "type": "address" - } - ], - "internalType": "struct AutomationRegistryBase2_3.BillingConfig", - "name": "", - "type": "tuple" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getBillingTokens", - "outputs": [ - { - "internalType": "contract IERC20[]", - "name": "", - "type": "address[]" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getCancellationDelay", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [], - "name": "getChainModule", - "outputs": [ - { - "internalType": "contract IChainModule", - "name": "chainModule", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getConditionalGasOverhead", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [], - "name": "getFallbackNativePrice", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "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": "getLinkUSDFeedAddress", - "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 AutomationRegistryBase2_3.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": "getNativeUSDFeedAddress", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "peer", - "type": "address" - } - ], - "name": "getPeerRegistryMigrationPermission", - "outputs": [ - { - "internalType": "enum AutomationRegistryBase2_3.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": [], - "name": "getReorgProtectionEnabled", - "outputs": [ - { - "internalType": "bool", - "name": "reorgProtectionEnabled", - "type": "bool" - } - ], - "stateMutability": "view", - "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 AutomationRegistryBase2_3.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 AutomationRegistryBase2_3.OnchainConfigLegacy", - "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": [], - "name": "getTransmitCalldataFixedBytesOverhead", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [], - "name": "getTransmitCalldataPerSignerBytesOverhead", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "pure", - "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 AutomationRegistryBase2_3.Trigger", - "name": "", - "type": "uint8" - } - ], - "stateMutability": "pure", + "name": "removeBillingOverrides", + "outputs": [], + "stateMutability": "nonpayable", "type": "function" }, { @@ -1738,157 +1357,26 @@ "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", + "name": "gasFeePPB", "type": "uint32" }, { - "internalType": "uint96", - "name": "amountSpent", - "type": "uint96" - }, - { - "internalType": "bool", - "name": "paused", - "type": "bool" - }, - { - "internalType": "bytes", - "name": "offchainConfig", - "type": "bytes" + "internalType": "uint24", + "name": "flatFeeMilliCents", + "type": "uint24" } ], - "internalType": "struct AutomationRegistryBase2_3.UpkeepInfo", - "name": "upkeepInfo", + "internalType": "struct AutomationRegistryBase2_3.BillingOverrides", + "name": "billingOverrides", "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": "linkAvailableForPayment", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "owner", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "pause", + "name": "setBillingOverrides", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -1899,40 +1387,14 @@ "internalType": "uint256", "name": "id", "type": "uint256" - } - ], - "name": "pauseUpkeep", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "admin", - "type": "address" }, { "internalType": "bytes", - "name": "newPrivilegeConfig", + "name": "newCheckData", "type": "bytes" } ], - "name": "setAdminPrivilegeConfig", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address[]", - "name": "payees", - "type": "address[]" - } - ], - "name": "setPayees", + "name": "setUpkeepCheckData", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -1940,17 +1402,17 @@ { "inputs": [ { - "internalType": "address", - "name": "peer", - "type": "address" + "internalType": "uint256", + "name": "id", + "type": "uint256" }, { - "internalType": "enum AutomationRegistryBase2_3.MigrationPermission", - "name": "permission", - "type": "uint8" + "internalType": "uint32", + "name": "gasLimit", + "type": "uint32" } ], - "name": "setPeerRegistryMigrationPermission", + "name": "setUpkeepGasLimit", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -1964,11 +1426,11 @@ }, { "internalType": "bytes", - "name": "newCheckData", + "name": "config", "type": "bytes" } ], - "name": "setUpkeepCheckData", + "name": "setUpkeepOffchainConfig", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -1981,12 +1443,12 @@ "type": "uint256" }, { - "internalType": "uint32", - "name": "gasLimit", - "type": "uint32" + "internalType": "bytes", + "name": "triggerConfig", + "type": "bytes" } ], - "name": "setUpkeepGasLimit", + "name": "setUpkeepTriggerConfig", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -2000,30 +1462,23 @@ }, { "internalType": "bytes", - "name": "config", + "name": "performData", "type": "bytes" } ], - "name": "setUpkeepOffchainConfig", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ + "name": "simulatePerformUpkeep", + "outputs": [ { - "internalType": "uint256", - "name": "upkeepId", - "type": "uint256" + "internalType": "bool", + "name": "success", + "type": "bool" }, { - "internalType": "bytes", - "name": "newPrivilegeConfig", - "type": "bytes" + "internalType": "uint256", + "name": "gasUsed", + "type": "uint256" } ], - "name": "setUpkeepPrivilegeConfig", - "outputs": [], "stateMutability": "nonpayable", "type": "function" }, @@ -2040,24 +1495,6 @@ "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": [ { @@ -2076,13 +1513,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [], - "name": "unpause", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [ { @@ -2096,37 +1526,11 @@ "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": "address", - "name": "assetAddress", + "internalType": "contract IERC20", + "name": "asset", "type": "address" }, { @@ -2176,25 +1580,7 @@ "type": "uint256" } ], - "name": "withdrawLinkFees", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "internalType": "address", - "name": "to", - "type": "address" - } - ], - "name": "withdrawPayment", + "name": "withdrawLink", "outputs": [], "stateMutability": "nonpayable", "type": "function" diff --git a/contracts/abi/v0.8/AutomationRegistryLogicC2_3.json b/contracts/abi/v0.8/AutomationRegistryLogicC2_3.json new file mode 100644 index 00000000..4bdbc9cd --- /dev/null +++ b/contracts/abi/v0.8/AutomationRegistryLogicC2_3.json @@ -0,0 +1,2462 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "link", + "type": "address" + }, + { + "internalType": "address", + "name": "linkUSDFeed", + "type": "address" + }, + { + "internalType": "address", + "name": "nativeUSDFeed", + "type": "address" + }, + { + "internalType": "address", + "name": "fastGasFeed", + "type": "address" + }, + { + "internalType": "address", + "name": "automationForwarderLogic", + "type": "address" + }, + { + "internalType": "address", + "name": "allowedReadOnlyAddress", + "type": "address" + }, + { + "internalType": "enum AutomationRegistryBase2_3.PayoutMode", + "name": "payoutMode", + "type": "uint8" + }, + { + "internalType": "address", + "name": "wrappedNativeTokenAddress", + "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": [ + { + "internalType": "uint256", + "name": "available", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "requested", + "type": "uint256" + } + ], + "name": "InsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientLinkLiquidity", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidDataLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidFeed", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidPayee", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidRecipient", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidReport", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidSigner", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidToken", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidTransmitter", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidTrigger", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidTriggerType", + "type": "error" + }, + { + "inputs": [], + "name": "MigrationNotPermitted", + "type": "error" + }, + { + "inputs": [], + "name": "MustSettleOffchain", + "type": "error" + }, + { + "inputs": [], + "name": "MustSettleOnchain", + "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": "OnlyFinanceAdmin", + "type": "error" + }, + { + "inputs": [], + "name": "OnlyPausedUpkeep", + "type": "error" + }, + { + "inputs": [], + "name": "OnlySimulatedBackend", + "type": "error" + }, + { + "inputs": [], + "name": "OnlyUnpausedUpkeep", + "type": "error" + }, + { + "inputs": [], + "name": "ParameterLengthError", + "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": "TransferFailed", + "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" + }, + { + "inputs": [], + "name": "ZeroAddressNotAllowed", + "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" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "gasFeePPB", + "type": "uint32" + }, + { + "internalType": "uint24", + "name": "flatFeeMilliCents", + "type": "uint24" + } + ], + "indexed": false, + "internalType": "struct AutomationRegistryBase2_3.BillingOverrides", + "name": "overrides", + "type": "tuple" + } + ], + "name": "BillingConfigOverridden", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "BillingConfigOverrideRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "gasFeePPB", + "type": "uint32" + }, + { + "internalType": "uint24", + "name": "flatFeeMilliCents", + "type": "uint24" + }, + { + "internalType": "contract AggregatorV3Interface", + "name": "priceFeed", + "type": "address" + }, + { + "internalType": "uint256", + "name": "fallbackPrice", + "type": "uint256" + }, + { + "internalType": "uint96", + "name": "minSpend", + "type": "uint96" + } + ], + "indexed": false, + "internalType": "struct AutomationRegistryBase2_3.BillingConfig", + "name": "config", + "type": "tuple" + } + ], + "name": "BillingConfigSet", + "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": "address", + "name": "newModule", + "type": "address" + } + ], + "name": "ChainSpecificModuleUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "dedupKey", + "type": "bytes32" + } + ], + "name": "DedupKeyAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "assetAddress", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "FeesWithdrawn", + "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": "address[]", + "name": "payees", + "type": "address[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "payments", + "type": "uint256[]" + } + ], + "name": "NOPsSettledOffchain", + "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": "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" + }, + { + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "transmitter", + "type": "address" + } + ], + "name": "acceptPayeeship", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "disableOffchainPayments", + "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": "getAllowedReadOnlyAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "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": [ + { + "internalType": "uint256", + "name": "upkeepID", + "type": "uint256" + } + ], + "name": "getBillingToken", + "outputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + } + ], + "name": "getBillingTokenConfig", + "outputs": [ + { + "components": [ + { + "internalType": "uint32", + "name": "gasFeePPB", + "type": "uint32" + }, + { + "internalType": "uint24", + "name": "flatFeeMilliCents", + "type": "uint24" + }, + { + "internalType": "contract AggregatorV3Interface", + "name": "priceFeed", + "type": "address" + }, + { + "internalType": "uint256", + "name": "fallbackPrice", + "type": "uint256" + }, + { + "internalType": "uint96", + "name": "minSpend", + "type": "uint96" + } + ], + "internalType": "struct AutomationRegistryBase2_3.BillingConfig", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getBillingTokens", + "outputs": [ + { + "internalType": "contract IERC20[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getCancellationDelay", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "getChainModule", + "outputs": [ + { + "internalType": "contract IChainModule", + "name": "chainModule", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getConditionalGasOverhead", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "getConfig", + "outputs": [ + { + "components": [ + { + "internalType": "uint32", + "name": "checkGasLimit", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "maxPerformGas", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "maxCheckDataSize", + "type": "uint32" + }, + { + "internalType": "address", + "name": "transcoder", + "type": "address" + }, + { + "internalType": "bool", + "name": "reorgProtectionEnabled", + "type": "bool" + }, + { + "internalType": "uint24", + "name": "stalenessSeconds", + "type": "uint24" + }, + { + "internalType": "uint32", + "name": "maxPerformDataSize", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "maxRevertDataSize", + "type": "uint32" + }, + { + "internalType": "address", + "name": "upkeepPrivilegeManager", + "type": "address" + }, + { + "internalType": "uint16", + "name": "gasCeilingMultiplier", + "type": "uint16" + }, + { + "internalType": "address", + "name": "financeAdmin", + "type": "address" + }, + { + "internalType": "uint256", + "name": "fallbackGasPrice", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "fallbackLinkPrice", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "fallbackNativePrice", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "registrars", + "type": "address[]" + }, + { + "internalType": "contract IChainModule", + "name": "chainModule", + "type": "address" + } + ], + "internalType": "struct AutomationRegistryBase2_3.OnchainConfig", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getFallbackNativePrice", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "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": "getHotVars", + "outputs": [ + { + "components": [ + { + "internalType": "uint96", + "name": "totalPremium", + "type": "uint96" + }, + { + "internalType": "uint32", + "name": "latestEpoch", + "type": "uint32" + }, + { + "internalType": "uint24", + "name": "stalenessSeconds", + "type": "uint24" + }, + { + "internalType": "uint16", + "name": "gasCeilingMultiplier", + "type": "uint16" + }, + { + "internalType": "uint8", + "name": "f", + "type": "uint8" + }, + { + "internalType": "bool", + "name": "paused", + "type": "bool" + }, + { + "internalType": "bool", + "name": "reentrancyGuard", + "type": "bool" + }, + { + "internalType": "bool", + "name": "reorgProtectionEnabled", + "type": "bool" + }, + { + "internalType": "contract IChainModule", + "name": "chainModule", + "type": "address" + } + ], + "internalType": "struct AutomationRegistryBase2_3.HotVars", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getLinkAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getLinkUSDFeedAddress", + "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": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "enum AutomationRegistryBase2_3.Trigger", + "name": "triggerType", + "type": "uint8" + }, + { + "internalType": "uint32", + "name": "gasLimit", + "type": "uint32" + }, + { + "internalType": "contract IERC20", + "name": "billingToken", + "type": "address" + } + ], + "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": "getNativeUSDFeedAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getNumUpkeeps", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getPayoutMode", + "outputs": [ + { + "internalType": "enum AutomationRegistryBase2_3.PayoutMode", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "peer", + "type": "address" + } + ], + "name": "getPeerRegistryMigrationPermission", + "outputs": [ + { + "internalType": "enum AutomationRegistryBase2_3.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": [], + "name": "getReorgProtectionEnabled", + "outputs": [ + { + "internalType": "bool", + "name": "reorgProtectionEnabled", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "billingToken", + "type": "address" + } + ], + "name": "getReserveAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "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 IAutomationV21PlusCommon.StateLegacy", + "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 IAutomationV21PlusCommon.OnchainConfigLegacy", + "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": [], + "name": "getStorage", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "transcoder", + "type": "address" + }, + { + "internalType": "uint32", + "name": "checkGasLimit", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "maxPerformGas", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "nonce", + "type": "uint32" + }, + { + "internalType": "address", + "name": "upkeepPrivilegeManager", + "type": "address" + }, + { + "internalType": "uint32", + "name": "configCount", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "latestConfigBlockNumber", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "maxCheckDataSize", + "type": "uint32" + }, + { + "internalType": "address", + "name": "financeAdmin", + "type": "address" + }, + { + "internalType": "uint32", + "name": "maxPerformDataSize", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "maxRevertDataSize", + "type": "uint32" + } + ], + "internalType": "struct AutomationRegistryBase2_3.Storage", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getTransmitCalldataFixedBytesOverhead", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "getTransmitCalldataPerSignerBytesOverhead", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "pure", + "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 AutomationRegistryBase2_3.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 IAutomationV21PlusCommon.UpkeepInfoLegacy", + "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": [], + "name": "getWrappedNativeTokenAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "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": "linkAvailableForPayment", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "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 AutomationRegistryBase2_3.MigrationPermission", + "name": "permission", + "type": "uint8" + } + ], + "name": "setPeerRegistryMigrationPermission", + "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": [], + "name": "settleNOPsOffchain", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + } + ], + "name": "supportsBillingToken", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "transferOwnership", + "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": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "upkeepVersion", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "pure", + "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/abi/v0.8/AutomationUtils2_3.json b/contracts/abi/v0.8/AutomationUtils2_3.json index 2044d7a2..9f044b0c 100644 --- a/contracts/abi/v0.8/AutomationUtils2_3.json +++ b/contracts/abi/v0.8/AutomationUtils2_3.json @@ -1,306 +1,4 @@ [ - { - "inputs": [ - { - "components": [ - { - "internalType": "uint32", - "name": "blockNum", - "type": "uint32" - }, - { - "internalType": "bytes32", - "name": "blockHash", - "type": "bytes32" - } - ], - "internalType": "struct AutomationRegistryBase2_3.ConditionalTrigger", - "name": "", - "type": "tuple" - } - ], - "name": "_conditionalTrigger", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "uint256", - "name": "index", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "timestamp", - "type": "uint256" - }, - { - "internalType": "bytes32", - "name": "txHash", - "type": "bytes32" - }, - { - "internalType": "uint256", - "name": "blockNumber", - "type": "uint256" - }, - { - "internalType": "bytes32", - "name": "blockHash", - "type": "bytes32" - }, - { - "internalType": "address", - "name": "source", - "type": "address" - }, - { - "internalType": "bytes32[]", - "name": "topics", - "type": "bytes32[]" - }, - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - } - ], - "internalType": "struct Log", - "name": "", - "type": "tuple" - } - ], - "name": "_log", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "bytes32", - "name": "logBlockHash", - "type": "bytes32" - }, - { - "internalType": "bytes32", - "name": "txHash", - "type": "bytes32" - }, - { - "internalType": "uint32", - "name": "logIndex", - "type": "uint32" - }, - { - "internalType": "uint32", - "name": "blockNum", - "type": "uint32" - }, - { - "internalType": "bytes32", - "name": "blockHash", - "type": "bytes32" - } - ], - "internalType": "struct AutomationRegistryBase2_3.LogTrigger", - "name": "", - "type": "tuple" - } - ], - "name": "_logTrigger", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "address", - "name": "contractAddress", - "type": "address" - }, - { - "internalType": "uint8", - "name": "filterSelector", - "type": "uint8" - }, - { - "internalType": "bytes32", - "name": "topic0", - "type": "bytes32" - }, - { - "internalType": "bytes32", - "name": "topic1", - "type": "bytes32" - }, - { - "internalType": "bytes32", - "name": "topic2", - "type": "bytes32" - }, - { - "internalType": "bytes32", - "name": "topic3", - "type": "bytes32" - } - ], - "internalType": "struct LogTriggerConfig", - "name": "", - "type": "tuple" - } - ], - "name": "_logTriggerConfig", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "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": "uint256", - "name": "fallbackNativePrice", - "type": "uint256" - }, - { - "internalType": "address", - "name": "transcoder", - "type": "address" - }, - { - "internalType": "address[]", - "name": "registrars", - "type": "address[]" - }, - { - "internalType": "address", - "name": "upkeepPrivilegeManager", - "type": "address" - }, - { - "internalType": "contract IChainModule", - "name": "chainModule", - "type": "address" - }, - { - "internalType": "bool", - "name": "reorgProtectionEnabled", - "type": "bool" - }, - { - "internalType": "address", - "name": "financeAdmin", - "type": "address" - } - ], - "internalType": "struct AutomationRegistryBase2_3.OnchainConfig", - "name": "", - "type": "tuple" - }, - { - "internalType": "address[]", - "name": "", - "type": "address[]" - }, - { - "components": [ - { - "internalType": "uint32", - "name": "gasFeePPB", - "type": "uint32" - }, - { - "internalType": "uint24", - "name": "flatFeeMicroLink", - "type": "uint24" - }, - { - "internalType": "address", - "name": "priceFeed", - "type": "address" - } - ], - "internalType": "struct AutomationRegistryBase2_3.BillingConfig[]", - "name": "", - "type": "tuple[]" - } - ], - "name": "_onChainConfig", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [ { diff --git a/contracts/abi/v0.8/BasicConsumer.json b/contracts/abi/v0.8/BasicConsumer.json new file mode 100644 index 00000000..dacf2bb6 --- /dev/null +++ b/contracts/abi/v0.8/BasicConsumer.json @@ -0,0 +1,211 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_link", + "type": "address" + }, + { + "internalType": "address", + "name": "_oracle", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "_specId", + "type": "bytes32" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "ChainlinkCancelled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "ChainlinkFulfilled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "ChainlinkRequested", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "requestId", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "price", + "type": "bytes32" + } + ], + "name": "RequestFulfilled", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_oracle", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "_requestId", + "type": "bytes32" + } + ], + "name": "addExternalRequest", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_oracle", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "_requestId", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "_payment", + "type": "uint256" + }, + { + "internalType": "bytes4", + "name": "_callbackFunctionId", + "type": "bytes4" + }, + { + "internalType": "uint256", + "name": "_expiration", + "type": "uint256" + } + ], + "name": "cancelRequest", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "currentPrice", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_requestId", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "_price", + "type": "bytes32" + } + ], + "name": "fulfill", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "_currency", + "type": "string" + }, + { + "internalType": "uint256", + "name": "_payment", + "type": "uint256" + } + ], + "name": "requestEthereumPrice", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "_currency", + "type": "string" + }, + { + "internalType": "uint256", + "name": "_payment", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_callback", + "type": "address" + } + ], + "name": "requestEthereumPriceByCallback", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "withdrawLink", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/contracts/abi/v0.8/BatchVRFCoordinatorV2Plus.json b/contracts/abi/v0.8/BatchVRFCoordinatorV2Plus.json index 95ea71cd..9f5642c7 100644 --- a/contracts/abi/v0.8/BatchVRFCoordinatorV2Plus.json +++ b/contracts/abi/v0.8/BatchVRFCoordinatorV2Plus.json @@ -53,7 +53,7 @@ "name": "COORDINATOR", "outputs": [ { - "internalType": "contract IVRFCoordinatorV2Plus", + "internalType": "contract IVRFCoordinatorV2PlusFulfill", "name": "", "type": "address" } diff --git a/contracts/abi/v0.8/Chainable.json b/contracts/abi/v0.8/Chainable.json index 49f2b625..6037a4b3 100644 --- a/contracts/abi/v0.8/Chainable.json +++ b/contracts/abi/v0.8/Chainable.json @@ -11,7 +11,7 @@ "type": "constructor" }, { - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "fallback" }, { diff --git a/contracts/abi/v0.8/ChainlinkClientHelper.json b/contracts/abi/v0.8/ChainlinkClientHelper.json new file mode 100644 index 00000000..46fdd707 --- /dev/null +++ b/contracts/abi/v0.8/ChainlinkClientHelper.json @@ -0,0 +1,125 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "link", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "ChainlinkCancelled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "ChainlinkFulfilled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "ChainlinkRequested", + "type": "event" + }, + { + "inputs": [], + "name": "FULFILSELECTOR", + "outputs": [ + { + "internalType": "bytes4", + "name": "", + "type": "bytes4" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "requestId", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "payment", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "expiration", + "type": "uint256" + } + ], + "name": "cancelRequest", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "fulfill", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "op", + "type": "address" + }, + { + "internalType": "uint256", + "name": "payment", + "type": "uint256" + } + ], + "name": "sendRequest", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/contracts/abi/v0.8/Chainlinked.json b/contracts/abi/v0.8/Chainlinked.json new file mode 100644 index 00000000..57999bd7 --- /dev/null +++ b/contracts/abi/v0.8/Chainlinked.json @@ -0,0 +1,41 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "ChainlinkCancelled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "ChainlinkFulfilled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "ChainlinkRequested", + "type": "event" + } +] diff --git a/contracts/abi/v0.8/ConfirmedOwnerTestHelper.json b/contracts/abi/v0.8/ConfirmedOwnerTestHelper.json new file mode 100644 index 00000000..81f35ae2 --- /dev/null +++ b/contracts/abi/v0.8/ConfirmedOwnerTestHelper.json @@ -0,0 +1,91 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [], + "name": "Here", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "OwnershipTransferRequested", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "modifierOnlyOwner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/contracts/abi/v0.8/Consumer.json b/contracts/abi/v0.8/Consumer.json new file mode 100644 index 00000000..25e2c977 --- /dev/null +++ b/contracts/abi/v0.8/Consumer.json @@ -0,0 +1,190 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "ChainlinkCancelled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "ChainlinkFulfilled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "ChainlinkRequested", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "requestId", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "price", + "type": "bytes32" + } + ], + "name": "RequestFulfilled", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_oracle", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "_requestId", + "type": "bytes32" + } + ], + "name": "addExternalRequest", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_oracle", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "_requestId", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "_payment", + "type": "uint256" + }, + { + "internalType": "bytes4", + "name": "_callbackFunctionId", + "type": "bytes4" + }, + { + "internalType": "uint256", + "name": "_expiration", + "type": "uint256" + } + ], + "name": "cancelRequest", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "currentPrice", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_requestId", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "_price", + "type": "bytes32" + } + ], + "name": "fulfill", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "_currency", + "type": "string" + }, + { + "internalType": "uint256", + "name": "_payment", + "type": "uint256" + } + ], + "name": "requestEthereumPrice", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "_currency", + "type": "string" + }, + { + "internalType": "uint256", + "name": "_payment", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_callback", + "type": "address" + } + ], + "name": "requestEthereumPriceByCallback", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "withdrawLink", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/contracts/abi/v0.8/EmptyOracle.json b/contracts/abi/v0.8/EmptyOracle.json new file mode 100644 index 00000000..dc9fef17 --- /dev/null +++ b/contracts/abi/v0.8/EmptyOracle.json @@ -0,0 +1,213 @@ +[ + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes4", + "name": "", + "type": "bytes4" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "cancelOracleRequest", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "bytes4", + "name": "", + "type": "bytes4" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "fulfillOracleRequest", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "getAuthorizationStatus", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "onTokenTransfer", + "outputs": [], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "bytes4", + "name": "", + "type": "bytes4" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "oracleRequest", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "name": "setFulfillmentPermission", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "withdrawable", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/contracts/abi/v0.8/ExposedVRFCoordinatorV2_5.json b/contracts/abi/v0.8/ExposedVRFCoordinatorV2_5.json index 12f4c5ab..aa959403 100644 --- a/contracts/abi/v0.8/ExposedVRFCoordinatorV2_5.json +++ b/contracts/abi/v0.8/ExposedVRFCoordinatorV2_5.json @@ -116,22 +116,6 @@ "name": "InsufficientBalance", "type": "error" }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "have", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "want", - "type": "uint256" - } - ], - "name": "InsufficientGasForConsumer", - "type": "error" - }, { "inputs": [], "name": "InvalidCalldata", @@ -1218,7 +1202,7 @@ "type": "bytes" } ], - "internalType": "struct VRFCoordinatorV2_5.RequestCommitment", + "internalType": "struct VRFTypes.RequestCommitmentV2Plus", "name": "rc", "type": "tuple" }, @@ -1376,7 +1360,7 @@ "type": "bytes" } ], - "internalType": "struct VRFCoordinatorV2_5.RequestCommitment", + "internalType": "struct VRFTypes.RequestCommitmentV2Plus", "name": "rc", "type": "tuple" } @@ -1448,7 +1432,7 @@ }, { "internalType": "address", - "name": "owner", + "name": "subOwner", "type": "address" }, { diff --git a/contracts/abi/v0.8/GasGuzzlingConsumer.json b/contracts/abi/v0.8/GasGuzzlingConsumer.json new file mode 100644 index 00000000..ce7e9daa --- /dev/null +++ b/contracts/abi/v0.8/GasGuzzlingConsumer.json @@ -0,0 +1,273 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_link", + "type": "address" + }, + { + "internalType": "address", + "name": "_oracle", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "_specId", + "type": "bytes32" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "ChainlinkCancelled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "ChainlinkFulfilled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "ChainlinkRequested", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "requestId", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "price", + "type": "bytes32" + } + ], + "name": "RequestFulfilled", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_oracle", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "_requestId", + "type": "bytes32" + } + ], + "name": "addExternalRequest", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_oracle", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "_requestId", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "_payment", + "type": "uint256" + }, + { + "internalType": "bytes4", + "name": "_callbackFunctionId", + "type": "bytes4" + }, + { + "internalType": "uint256", + "name": "_expiration", + "type": "uint256" + } + ], + "name": "cancelRequest", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "currentPrice", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_requestId", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "_price", + "type": "bytes32" + } + ], + "name": "fulfill", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_requestId", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "gassyFulfill", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_requestId", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "gassyMultiWordFulfill", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_payment", + "type": "uint256" + } + ], + "name": "gassyMultiWordRequest", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_payment", + "type": "uint256" + } + ], + "name": "gassyRequestEthereumPrice", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "_currency", + "type": "string" + }, + { + "internalType": "uint256", + "name": "_payment", + "type": "uint256" + } + ], + "name": "requestEthereumPrice", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "_currency", + "type": "string" + }, + { + "internalType": "uint256", + "name": "_payment", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_callback", + "type": "address" + } + ], + "name": "requestEthereumPriceByCallback", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "withdrawLink", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/contracts/abi/v0.8/GetterSetter.json b/contracts/abi/v0.8/GetterSetter.json new file mode 100644 index 00000000..4f05038d --- /dev/null +++ b/contracts/abi/v0.8/GetterSetter.json @@ -0,0 +1,229 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes32", + "name": "b32", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "u256", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "b322", + "type": "bytes32" + } + ], + "name": "Output", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "value", + "type": "bytes" + } + ], + "name": "SetBytes", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "value", + "type": "bytes32" + } + ], + "name": "SetBytes32", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "SetUint256", + "type": "event" + }, + { + "inputs": [], + "name": "getBytes", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getBytes32", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getUint256", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "requestId", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_requestId", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "_value", + "type": "bytes" + } + ], + "name": "requestedBytes", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_requestId", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "_value", + "type": "bytes32" + } + ], + "name": "requestedBytes32", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_requestId", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + } + ], + "name": "requestedUint256", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "_value", + "type": "bytes" + } + ], + "name": "setBytes", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_value", + "type": "bytes32" + } + ], + "name": "setBytes32", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + } + ], + "name": "setUint256", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/contracts/abi/v0.8/Greeter.json b/contracts/abi/v0.8/Greeter.json index 61fc34a4..edf4dea4 100644 --- a/contracts/abi/v0.8/Greeter.json +++ b/contracts/abi/v0.8/Greeter.json @@ -1,7 +1,63 @@ [ + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "OwnershipTransferRequested", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, { "inputs": [], - "name": "getGreeting", + "name": "acceptOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "greeting", "outputs": [ { "internalType": "string", @@ -12,11 +68,24 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { "internalType": "string", - "name": "greeting", + "name": "_greeting", "type": "string" } ], @@ -24,5 +93,25 @@ "outputs": [], "stateMutability": "nonpayable", "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "triggerRevert", + "outputs": [], + "stateMutability": "pure", + "type": "function" } ] diff --git a/contracts/abi/v0.8/IAutomationRegistryMaster2_3.json b/contracts/abi/v0.8/IAutomationRegistryMaster2_3.json index d4d0fb25..fe73d68a 100644 --- a/contracts/abi/v0.8/IAutomationRegistryMaster2_3.json +++ b/contracts/abi/v0.8/IAutomationRegistryMaster2_3.json @@ -75,6 +75,11 @@ "name": "InsufficientBalance", "type": "error" }, + { + "inputs": [], + "name": "InsufficientLinkLiquidity", + "type": "error" + }, { "inputs": [], "name": "InvalidDataLength", @@ -105,6 +110,11 @@ "name": "InvalidSigner", "type": "error" }, + { + "inputs": [], + "name": "InvalidToken", + "type": "error" + }, { "inputs": [], "name": "InvalidTransmitter", @@ -122,17 +132,17 @@ }, { "inputs": [], - "name": "MaxCheckDataSizeCanOnlyIncrease", + "name": "MigrationNotPermitted", "type": "error" }, { "inputs": [], - "name": "MaxPerformDataSizeCanOnlyIncrease", + "name": "MustSettleOffchain", "type": "error" }, { "inputs": [], - "name": "MigrationNotPermitted", + "name": "MustSettleOnchain", "type": "error" }, { @@ -215,11 +225,6 @@ "name": "ParameterLengthError", "type": "error" }, - { - "inputs": [], - "name": "PaymentGreaterThanAllLINK", - "type": "error" - }, { "inputs": [], "name": "ReentrantCall", @@ -315,6 +320,50 @@ "name": "AdminPrivilegeConfigSet", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "gasFeePPB", + "type": "uint32" + }, + { + "internalType": "uint24", + "name": "flatFeeMilliCents", + "type": "uint24" + } + ], + "indexed": false, + "internalType": "struct AutomationRegistryBase2_3.BillingOverrides", + "name": "overrides", + "type": "tuple" + } + ], + "name": "BillingConfigOverridden", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "BillingConfigOverrideRemoved", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -333,13 +382,23 @@ }, { "internalType": "uint24", - "name": "flatFeeMicroLink", + "name": "flatFeeMilliCents", "type": "uint24" }, { "internalType": "address", "name": "priceFeed", "type": "address" + }, + { + "internalType": "uint256", + "name": "fallbackPrice", + "type": "uint256" + }, + { + "internalType": "uint96", + "name": "minSpend", + "type": "uint96" } ], "indexed": false, @@ -463,13 +522,13 @@ { "indexed": true, "internalType": "address", - "name": "recipient", + "name": "assetAddress", "type": "address" }, { "indexed": true, "internalType": "address", - "name": "assetAddress", + "name": "recipient", "type": "address" }, { @@ -551,6 +610,25 @@ "name": "InsufficientFundsUpkeepReport", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address[]", + "name": "payees", + "type": "address[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "payments", + "type": "uint256[]" + } + ], + "name": "NOPsSettledOffchain", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -1081,7 +1159,7 @@ "type": "event" }, { - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "fallback" }, { @@ -1132,7 +1210,7 @@ ], "name": "addFunds", "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "function" }, { @@ -1295,6 +1373,13 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "disableOffchainPayments", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -1435,6 +1520,25 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "upkeepID", + "type": "uint256" + } + ], + "name": "getBillingToken", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -1454,13 +1558,23 @@ }, { "internalType": "uint24", - "name": "flatFeeMicroLink", + "name": "flatFeeMilliCents", "type": "uint24" }, { "internalType": "address", "name": "priceFeed", "type": "address" + }, + { + "internalType": "uint256", + "name": "fallbackPrice", + "type": "uint256" + }, + { + "internalType": "uint96", + "name": "minSpend", + "type": "uint96" } ], "internalType": "struct AutomationRegistryBase2_3.BillingConfig", @@ -1525,12 +1639,94 @@ }, { "inputs": [], - "name": "getFallbackNativePrice", + "name": "getConfig", "outputs": [ { - "internalType": "uint256", + "components": [ + { + "internalType": "uint32", + "name": "checkGasLimit", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "maxPerformGas", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "maxCheckDataSize", + "type": "uint32" + }, + { + "internalType": "address", + "name": "transcoder", + "type": "address" + }, + { + "internalType": "bool", + "name": "reorgProtectionEnabled", + "type": "bool" + }, + { + "internalType": "uint24", + "name": "stalenessSeconds", + "type": "uint24" + }, + { + "internalType": "uint32", + "name": "maxPerformDataSize", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "maxRevertDataSize", + "type": "uint32" + }, + { + "internalType": "address", + "name": "upkeepPrivilegeManager", + "type": "address" + }, + { + "internalType": "uint16", + "name": "gasCeilingMultiplier", + "type": "uint16" + }, + { + "internalType": "address", + "name": "financeAdmin", + "type": "address" + }, + { + "internalType": "uint256", + "name": "fallbackGasPrice", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "fallbackLinkPrice", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "fallbackNativePrice", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "registrars", + "type": "address[]" + }, + { + "internalType": "address", + "name": "chainModule", + "type": "address" + } + ], + "internalType": "struct AutomationRegistryBase2_3.OnchainConfig", "name": "", - "type": "uint256" + "type": "tuple" } ], "stateMutability": "view", @@ -1538,26 +1734,20 @@ }, { "inputs": [], - "name": "getFastGasFeedAddress", + "name": "getFallbackNativePrice", "outputs": [ { - "internalType": "address", + "internalType": "uint256", "name": "", - "type": "address" + "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { - "inputs": [ - { - "internalType": "uint256", - "name": "upkeepID", - "type": "uint256" - } - ], - "name": "getForwarder", + "inputs": [], + "name": "getFastGasFeedAddress", "outputs": [ { "internalType": "address", @@ -1569,21 +1759,14 @@ "type": "function" }, { - "inputs": [], - "name": "getLinkAddress", - "outputs": [ + "inputs": [ { - "internalType": "address", - "name": "", - "type": "address" + "internalType": "uint256", + "name": "upkeepID", + "type": "uint256" } ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getLinkUSDFeedAddress", + "name": "getForwarder", "outputs": [ { "internalType": "address", @@ -1596,28 +1779,124 @@ }, { "inputs": [], - "name": "getLogGasOverhead", + "name": "getHotVars", "outputs": [ { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint8", - "name": "triggerType", - "type": "uint8" - }, - { - "internalType": "uint32", + "components": [ + { + "internalType": "uint96", + "name": "totalPremium", + "type": "uint96" + }, + { + "internalType": "uint32", + "name": "latestEpoch", + "type": "uint32" + }, + { + "internalType": "uint24", + "name": "stalenessSeconds", + "type": "uint24" + }, + { + "internalType": "uint16", + "name": "gasCeilingMultiplier", + "type": "uint16" + }, + { + "internalType": "uint8", + "name": "f", + "type": "uint8" + }, + { + "internalType": "bool", + "name": "paused", + "type": "bool" + }, + { + "internalType": "bool", + "name": "reentrancyGuard", + "type": "bool" + }, + { + "internalType": "bool", + "name": "reorgProtectionEnabled", + "type": "bool" + }, + { + "internalType": "address", + "name": "chainModule", + "type": "address" + } + ], + "internalType": "struct AutomationRegistryBase2_3.HotVars", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getLinkAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getLinkUSDFeedAddress", + "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": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "triggerType", + "type": "uint8" + }, + { + "internalType": "uint32", "name": "gasLimit", "type": "uint32" + }, + { + "internalType": "address", + "name": "billingToken", + "type": "address" } ], "name": "getMaxPaymentForGas", @@ -1682,6 +1961,32 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "getNumUpkeeps", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getPayoutMode", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -1740,6 +2045,25 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "billingToken", + "type": "address" + } + ], + "name": "getReserveAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -1821,7 +2145,7 @@ "type": "bool" } ], - "internalType": "struct AutomationRegistryBase2_3.State", + "internalType": "struct IAutomationV21PlusCommon.StateLegacy", "name": "state", "type": "tuple" }, @@ -1903,7 +2227,7 @@ "type": "address" } ], - "internalType": "struct AutomationRegistryBase2_3.OnchainConfigLegacy", + "internalType": "struct IAutomationV21PlusCommon.OnchainConfigLegacy", "name": "config", "type": "tuple" }, @@ -1926,6 +2250,76 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "getStorage", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "transcoder", + "type": "address" + }, + { + "internalType": "uint32", + "name": "checkGasLimit", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "maxPerformGas", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "nonce", + "type": "uint32" + }, + { + "internalType": "address", + "name": "upkeepPrivilegeManager", + "type": "address" + }, + { + "internalType": "uint32", + "name": "configCount", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "latestConfigBlockNumber", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "maxCheckDataSize", + "type": "uint32" + }, + { + "internalType": "address", + "name": "financeAdmin", + "type": "address" + }, + { + "internalType": "uint32", + "name": "maxPerformDataSize", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "maxRevertDataSize", + "type": "uint32" + } + ], + "internalType": "struct AutomationRegistryBase2_3.Storage", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "getTransmitCalldataFixedBytesOverhead", @@ -2073,7 +2467,7 @@ "type": "bytes" } ], - "internalType": "struct AutomationRegistryBase2_3.UpkeepInfo", + "internalType": "struct IAutomationV21PlusCommon.UpkeepInfoLegacy", "name": "upkeepInfo", "type": "tuple" } @@ -2119,6 +2513,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "getWrappedNativeTokenAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -2189,9 +2596,9 @@ "name": "linkAvailableForPayment", "outputs": [ { - "internalType": "uint256", + "internalType": "int256", "name": "", - "type": "uint256" + "type": "int256" } ], "stateMutability": "view", @@ -2306,6 +2713,11 @@ "name": "triggerType", "type": "uint8" }, + { + "internalType": "address", + "name": "billingToken", + "type": "address" + }, { "internalType": "bytes", "name": "checkData", @@ -2335,40 +2747,14 @@ }, { "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" } ], + "name": "removeBillingOverrides", + "outputs": [], "stateMutability": "nonpayable", "type": "function" }, @@ -2390,6 +2776,36 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "gasFeePPB", + "type": "uint32" + }, + { + "internalType": "uint24", + "name": "flatFeeMilliCents", + "type": "uint24" + } + ], + "internalType": "struct AutomationRegistryBase2_3.BillingOverrides", + "name": "billingOverrides", + "type": "tuple" + } + ], + "name": "setBillingOverrides", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -2449,53 +2865,58 @@ "components": [ { "internalType": "uint32", - "name": "paymentPremiumPPB", + "name": "checkGasLimit", "type": "uint32" }, { "internalType": "uint32", - "name": "flatFeeMicroLink", + "name": "maxPerformGas", "type": "uint32" }, { "internalType": "uint32", - "name": "checkGasLimit", + "name": "maxCheckDataSize", "type": "uint32" }, { - "internalType": "uint24", - "name": "stalenessSeconds", - "type": "uint24" + "internalType": "address", + "name": "transcoder", + "type": "address" }, { - "internalType": "uint16", - "name": "gasCeilingMultiplier", - "type": "uint16" + "internalType": "bool", + "name": "reorgProtectionEnabled", + "type": "bool" }, { - "internalType": "uint96", - "name": "minUpkeepSpend", - "type": "uint96" + "internalType": "uint24", + "name": "stalenessSeconds", + "type": "uint24" }, { "internalType": "uint32", - "name": "maxPerformGas", + "name": "maxPerformDataSize", "type": "uint32" }, { "internalType": "uint32", - "name": "maxCheckDataSize", + "name": "maxRevertDataSize", "type": "uint32" }, { - "internalType": "uint32", - "name": "maxPerformDataSize", - "type": "uint32" + "internalType": "address", + "name": "upkeepPrivilegeManager", + "type": "address" }, { - "internalType": "uint32", - "name": "maxRevertDataSize", - "type": "uint32" + "internalType": "uint16", + "name": "gasCeilingMultiplier", + "type": "uint16" + }, + { + "internalType": "address", + "name": "financeAdmin", + "type": "address" }, { "internalType": "uint256", @@ -2512,35 +2933,15 @@ "name": "fallbackNativePrice", "type": "uint256" }, - { - "internalType": "address", - "name": "transcoder", - "type": "address" - }, { "internalType": "address[]", "name": "registrars", "type": "address[]" }, - { - "internalType": "address", - "name": "upkeepPrivilegeManager", - "type": "address" - }, { "internalType": "address", "name": "chainModule", "type": "address" - }, - { - "internalType": "bool", - "name": "reorgProtectionEnabled", - "type": "bool" - }, - { - "internalType": "address", - "name": "financeAdmin", - "type": "address" } ], "internalType": "struct AutomationRegistryBase2_3.OnchainConfig", @@ -2571,13 +2972,23 @@ }, { "internalType": "uint24", - "name": "flatFeeMicroLink", + "name": "flatFeeMilliCents", "type": "uint24" }, { "internalType": "address", "name": "priceFeed", "type": "address" + }, + { + "internalType": "uint256", + "name": "fallbackPrice", + "type": "uint256" + }, + { + "internalType": "uint96", + "name": "minSpend", + "type": "uint96" } ], "internalType": "struct AutomationRegistryBase2_3.BillingConfig[]", @@ -2711,6 +3122,13 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [], + "name": "settleNOPsOffchain", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -2740,6 +3158,25 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "supportsBillingToken", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -2855,19 +3292,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [], - "name": "upkeepTranscoderVersion", - "outputs": [ - { - "internalType": "uint8", - "name": "", - "type": "uint8" - } - ], - "stateMutability": "pure", - "type": "function" - }, { "inputs": [], "name": "upkeepVersion", @@ -2885,7 +3309,7 @@ "inputs": [ { "internalType": "address", - "name": "assetAddress", + "name": "asset", "type": "address" }, { @@ -2935,7 +3359,7 @@ "type": "uint256" } ], - "name": "withdrawLinkFees", + "name": "withdrawLink", "outputs": [], "stateMutability": "nonpayable", "type": "function" diff --git a/contracts/abi/v0.8/IERC165.json b/contracts/abi/v0.8/IERC165.json index 0656ec3a..53fa0007 100644 --- a/contracts/abi/v0.8/IERC165.json +++ b/contracts/abi/v0.8/IERC165.json @@ -3,7 +3,7 @@ "inputs": [ { "internalType": "bytes4", - "name": "interfaceID", + "name": "interfaceId", "type": "bytes4" } ], diff --git a/contracts/abi/v0.8/IVRFCoordinatorV2PlusFulfill.json b/contracts/abi/v0.8/IVRFCoordinatorV2PlusFulfill.json new file mode 100644 index 00000000..6fb3e618 --- /dev/null +++ b/contracts/abi/v0.8/IVRFCoordinatorV2PlusFulfill.json @@ -0,0 +1,110 @@ +[ + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256[2]", + "name": "pk", + "type": "uint256[2]" + }, + { + "internalType": "uint256[2]", + "name": "gamma", + "type": "uint256[2]" + }, + { + "internalType": "uint256", + "name": "c", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "s", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "seed", + "type": "uint256" + }, + { + "internalType": "address", + "name": "uWitness", + "type": "address" + }, + { + "internalType": "uint256[2]", + "name": "cGammaWitness", + "type": "uint256[2]" + }, + { + "internalType": "uint256[2]", + "name": "sHashWitness", + "type": "uint256[2]" + }, + { + "internalType": "uint256", + "name": "zInv", + "type": "uint256" + } + ], + "internalType": "struct VRFTypes.Proof", + "name": "proof", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "uint64", + "name": "blockNum", + "type": "uint64" + }, + { + "internalType": "uint256", + "name": "subId", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "callbackGasLimit", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "numWords", + "type": "uint32" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "bytes", + "name": "extraArgs", + "type": "bytes" + } + ], + "internalType": "struct VRFTypes.RequestCommitmentV2Plus", + "name": "rc", + "type": "tuple" + }, + { + "internalType": "bool", + "name": "onlyPremium", + "type": "bool" + } + ], + "name": "fulfillRandomWords", + "outputs": [ + { + "internalType": "uint96", + "name": "", + "type": "uint96" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/contracts/abi/v0.8/IVRFV2PlusWrapper.json b/contracts/abi/v0.8/IVRFV2PlusWrapper.json index 0c0e2d38..800c9fb1 100644 --- a/contracts/abi/v0.8/IVRFV2PlusWrapper.json +++ b/contracts/abi/v0.8/IVRFV2PlusWrapper.json @@ -17,7 +17,13 @@ { "indexed": false, "internalType": "uint8", - "name": "wrapperPremiumPercentage", + "name": "coordinatorNativePremiumPercentage", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "coordinatorLinkPremiumPercentage", "type": "uint8" }, { @@ -47,13 +53,13 @@ { "indexed": false, "internalType": "uint32", - "name": "fulfillmentFlatFeeLinkPPM", + "name": "fulfillmentFlatFeeNativePPM", "type": "uint32" }, { "indexed": false, "internalType": "uint32", - "name": "fulfillmentFlatFeeNativePPM", + "name": "fulfillmentFlatFeeLinkDiscountPPM", "type": "uint32" } ], @@ -104,25 +110,6 @@ "name": "FulfillmentTxSizeSet", "type": "event" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "link", - "type": "address" - }, - { - "indexed": false, - "internalType": "address", - "name": "linkNativeFeed", - "type": "address" - } - ], - "name": "LinkAndLinkNativeFeedSet", - "type": "event" - }, { "anonymous": false, "inputs": [ @@ -260,6 +247,32 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "link", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "linkNativeFeed", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { diff --git a/contracts/abi/v0.8/IVerifierProxy.json b/contracts/abi/v0.8/IVerifierProxy.json index 1c5d405d..263abee5 100644 --- a/contracts/abi/v0.8/IVerifierProxy.json +++ b/contracts/abi/v0.8/IVerifierProxy.json @@ -3,7 +3,7 @@ "inputs": [ { "internalType": "bytes", - "name": "payload", + "name": "signedReport", "type": "bytes" } ], @@ -11,11 +11,11 @@ "outputs": [ { "internalType": "bytes", - "name": "verifiedReport", + "name": "verifierResponse", "type": "bytes" } ], - "stateMutability": "payable", + "stateMutability": "nonpayable", "type": "function" } ] diff --git a/contracts/abi/v0.8/IWrappedNative.json b/contracts/abi/v0.8/IWrappedNative.json new file mode 100644 index 00000000..8a2916f4 --- /dev/null +++ b/contracts/abi/v0.8/IWrappedNative.json @@ -0,0 +1,205 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "deposit", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/contracts/abi/v0.8/KeeperRegistry2_1.json b/contracts/abi/v0.8/KeeperRegistry2_1.json index 84123d9d..e94e2d70 100644 --- a/contracts/abi/v0.8/KeeperRegistry2_1.json +++ b/contracts/abi/v0.8/KeeperRegistry2_1.json @@ -1000,7 +1000,7 @@ "type": "event" }, { - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "fallback" }, { diff --git a/contracts/abi/v0.8/KeeperRegistryLogicA2_1.json b/contracts/abi/v0.8/KeeperRegistryLogicA2_1.json index 34fc2fec..ba56eaa4 100644 --- a/contracts/abi/v0.8/KeeperRegistryLogicA2_1.json +++ b/contracts/abi/v0.8/KeeperRegistryLogicA2_1.json @@ -920,7 +920,7 @@ "type": "event" }, { - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "fallback" }, { diff --git a/contracts/abi/v0.8/LinkTokenTestHelper.json b/contracts/abi/v0.8/LinkTokenTestHelper.json new file mode 100644 index 00000000..1abff809 --- /dev/null +++ b/contracts/abi/v0.8/LinkTokenTestHelper.json @@ -0,0 +1,763 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "supplyAfterMint", + "type": "uint256" + } + ], + "name": "MaxSupplyExceeded", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "SenderNotBurner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "SenderNotMinter", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "burner", + "type": "address" + } + ], + "name": "BurnAccessGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "burner", + "type": "address" + } + ], + "name": "BurnAccessRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "minter", + "type": "address" + } + ], + "name": "MintAccessGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "minter", + "type": "address" + } + ], + "name": "MintAccessRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "OwnershipTransferRequested", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "burn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "burn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "burnFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "subtractedValue", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "subtractedValue", + "type": "uint256" + } + ], + "name": "decreaseApproval", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "getBurners", + "outputs": [ + { + "internalType": "address[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getMinters", + "outputs": [ + { + "internalType": "address[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "burner", + "type": "address" + } + ], + "name": "grantBurnRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "burnAndMinter", + "type": "address" + } + ], + "name": "grantMintAndBurnRoles", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "minter", + "type": "address" + } + ], + "name": "grantMintRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "addedValue", + "type": "uint256" + } + ], + "name": "increaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "addedValue", + "type": "uint256" + } + ], + "name": "increaseApproval", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "burner", + "type": "address" + } + ], + "name": "isBurner", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "minter", + "type": "address" + } + ], + "name": "isMinter", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "maxSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "burner", + "type": "address" + } + ], + "name": "revokeBurnRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "minter", + "type": "address" + } + ], + "name": "revokeMintRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "transferAndCall", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/contracts/abi/v0.8/MaliciousChainlinked.json b/contracts/abi/v0.8/MaliciousChainlinked.json new file mode 100644 index 00000000..57999bd7 --- /dev/null +++ b/contracts/abi/v0.8/MaliciousChainlinked.json @@ -0,0 +1,41 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "ChainlinkCancelled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "ChainlinkFulfilled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "ChainlinkRequested", + "type": "event" + } +] diff --git a/contracts/abi/v0.8/MaliciousConsumer.json b/contracts/abi/v0.8/MaliciousConsumer.json new file mode 100644 index 00000000..394fcb44 --- /dev/null +++ b/contracts/abi/v0.8/MaliciousConsumer.json @@ -0,0 +1,194 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_link", + "type": "address" + }, + { + "internalType": "address", + "name": "_oracle", + "type": "address" + } + ], + "stateMutability": "payable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "ChainlinkCancelled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "ChainlinkFulfilled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "ChainlinkRequested", + "type": "event" + }, + { + "stateMutability": "payable", + "type": "fallback" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "assertFail", + "outputs": [], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_requestId", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "cancelRequestOnFulfill", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "doesNothing", + "outputs": [], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "remove", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_id", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "_callbackFunc", + "type": "bytes" + } + ], + "name": "requestData", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_requestId", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "stealEthCall", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_requestId", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "stealEthSend", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_requestId", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "stealEthTransfer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/contracts/abi/v0.8/MaliciousMultiWordConsumer.json b/contracts/abi/v0.8/MaliciousMultiWordConsumer.json new file mode 100644 index 00000000..3f3c0635 --- /dev/null +++ b/contracts/abi/v0.8/MaliciousMultiWordConsumer.json @@ -0,0 +1,194 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_link", + "type": "address" + }, + { + "internalType": "address", + "name": "_oracle", + "type": "address" + } + ], + "stateMutability": "payable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "ChainlinkCancelled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "ChainlinkFulfilled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "ChainlinkRequested", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "assertFail", + "outputs": [], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_requestId", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "cancelRequestOnFulfill", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "doesNothing", + "outputs": [], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "remove", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_id", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "_callbackFunc", + "type": "bytes" + } + ], + "name": "requestData", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_requestId", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "stealEthCall", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_requestId", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "stealEthSend", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_requestId", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "stealEthTransfer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +] diff --git a/contracts/abi/v0.8/MaliciousRequester.json b/contracts/abi/v0.8/MaliciousRequester.json new file mode 100644 index 00000000..ef0dd9de --- /dev/null +++ b/contracts/abi/v0.8/MaliciousRequester.json @@ -0,0 +1,167 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_link", + "type": "address" + }, + { + "internalType": "address", + "name": "_oracle", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "ChainlinkCancelled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "ChainlinkFulfilled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "ChainlinkRequested", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "doesNothing", + "outputs": [], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_id", + "type": "bytes32" + } + ], + "name": "maliciousPrice", + "outputs": [ + { + "internalType": "bytes32", + "name": "requestId", + "type": "bytes32" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_id", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "_callbackFunc", + "type": "bytes" + } + ], + "name": "maliciousRequestCancel", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_target", + "type": "address" + } + ], + "name": "maliciousTargetConsumer", + "outputs": [ + { + "internalType": "bytes32", + "name": "requestId", + "type": "bytes32" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "maliciousWithdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_id", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "_target", + "type": "address" + }, + { + "internalType": "bytes", + "name": "_callbackFunc", + "type": "bytes" + } + ], + "name": "request", + "outputs": [ + { + "internalType": "bytes32", + "name": "requestId", + "type": "bytes32" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/contracts/abi/v0.8/MockUpkeep.json b/contracts/abi/v0.8/MockUpkeep.json new file mode 100644 index 00000000..d3fc208d --- /dev/null +++ b/contracts/abi/v0.8/MockUpkeep.json @@ -0,0 +1,218 @@ +[ + { + "inputs": [], + "name": "CheckRevert", + "type": "error" + }, + { + "inputs": [], + "name": "PerformRevert", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes", + "name": "upkeepData", + "type": "bytes" + } + ], + "name": "UpkeepPerformedWith", + "type": "event" + }, + { + "inputs": [], + "name": "checkGasToBurn", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "checkResult", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "checkUpkeep", + "outputs": [ + { + "internalType": "bool", + "name": "callable", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "executedata", + "type": "bytes" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "performData", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "performGasToBurn", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "performUpkeep", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "setCheckGasToBurn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "value", + "type": "bool" + } + ], + "name": "setCheckResult", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "setPerformData", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "setPerformGasToBurn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "value", + "type": "bool" + } + ], + "name": "setShouldCheckRevert", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "value", + "type": "bool" + } + ], + "name": "setShouldPerformRevert", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "shouldCheckRevert", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "shouldPerformRevert", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/contracts/abi/v0.8/MultiWordConsumer.json b/contracts/abi/v0.8/MultiWordConsumer.json new file mode 100644 index 00000000..fc9da5ce --- /dev/null +++ b/contracts/abi/v0.8/MultiWordConsumer.json @@ -0,0 +1,471 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_link", + "type": "address" + }, + { + "internalType": "address", + "name": "_oracle", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "_specId", + "type": "bytes32" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "ChainlinkCancelled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "ChainlinkFulfilled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "ChainlinkRequested", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "requestId", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes", + "name": "price", + "type": "bytes" + } + ], + "name": "RequestFulfilled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "requestId", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "usd", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "eur", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "jpy", + "type": "bytes32" + } + ], + "name": "RequestMultipleFulfilled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "requestId", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "usd", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "eur", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "jpy", + "type": "uint256" + } + ], + "name": "RequestMultipleFulfilledWithCustomURLs", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_oracle", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "_requestId", + "type": "bytes32" + } + ], + "name": "addExternalRequest", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_oracle", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "_requestId", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "_payment", + "type": "uint256" + }, + { + "internalType": "bytes4", + "name": "_callbackFunctionId", + "type": "bytes4" + }, + { + "internalType": "uint256", + "name": "_expiration", + "type": "uint256" + } + ], + "name": "cancelRequest", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "currentPrice", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "eur", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "eurInt", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_requestId", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "_price", + "type": "bytes" + } + ], + "name": "fulfillBytes", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_requestId", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "_usd", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "_eur", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "_jpy", + "type": "bytes32" + } + ], + "name": "fulfillMultipleParameters", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_requestId", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "_usd", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_eur", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_jpy", + "type": "uint256" + } + ], + "name": "fulfillMultipleParametersWithCustomURLs", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "jpy", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "jpyInt", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "publicGetNextRequestCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + }, + { + "internalType": "uint256", + "name": "_payment", + "type": "uint256" + } + ], + "name": "requestEthereumPrice", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + }, + { + "internalType": "uint256", + "name": "_payment", + "type": "uint256" + } + ], + "name": "requestMultipleParameters", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "_urlUSD", + "type": "string" + }, + { + "internalType": "string", + "name": "_pathUSD", + "type": "string" + }, + { + "internalType": "string", + "name": "_urlEUR", + "type": "string" + }, + { + "internalType": "string", + "name": "_pathEUR", + "type": "string" + }, + { + "internalType": "string", + "name": "_urlJPY", + "type": "string" + }, + { + "internalType": "string", + "name": "_pathJPY", + "type": "string" + }, + { + "internalType": "uint256", + "name": "_payment", + "type": "uint256" + } + ], + "name": "requestMultipleParametersWithCustomURLs", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_specId", + "type": "bytes32" + } + ], + "name": "setSpecID", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "usd", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "usdInt", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "withdrawLink", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/contracts/abi/v0.8/OCR2Base.json b/contracts/abi/v0.8/OCR2Base.json index 8089c1b4..f05c2791 100644 --- a/contracts/abi/v0.8/OCR2Base.json +++ b/contracts/abi/v0.8/OCR2Base.json @@ -1,23 +1,6 @@ [ { - "inputs": [ - { - "internalType": "string", - "name": "message", - "type": "string" - } - ], - "name": "InvalidConfig", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "string", - "name": "message", - "type": "string" - } - ], + "inputs": [], "name": "ReportInvalid", "type": "error" }, diff --git a/contracts/abi/v0.8/SimpleLogUpkeepCounter.json b/contracts/abi/v0.8/SimpleLogUpkeepCounter.json index b3269fd9..803b2a42 100644 --- a/contracts/abi/v0.8/SimpleLogUpkeepCounter.json +++ b/contracts/abi/v0.8/SimpleLogUpkeepCounter.json @@ -1,9 +1,46 @@ [ { - "inputs": [], + "inputs": [ + { + "internalType": "bool", + "name": "_isStreamsLookup", + "type": "bool" + } + ], "stateMutability": "nonpayable", "type": "constructor" }, + { + "inputs": [ + { + "internalType": "string", + "name": "feedParamKey", + "type": "string" + }, + { + "internalType": "string[]", + "name": "feeds", + "type": "string[]" + }, + { + "internalType": "string", + "name": "timeParamKey", + "type": "string" + }, + { + "internalType": "uint256", + "name": "time", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "extraData", + "type": "bytes" + } + ], + "name": "StreamsLookup", + "type": "error" + }, { "anonymous": false, "inputs": [ @@ -71,6 +108,11 @@ "internalType": "bytes32", "name": "eventSig", "type": "bytes32" + }, + { + "internalType": "string[]", + "name": "feeds", + "type": "string[]" } ], "internalType": "struct CheckData", @@ -83,6 +125,64 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "values", + "type": "bytes[]" + }, + { + "internalType": "bytes", + "name": "extraData", + "type": "bytes" + } + ], + "name": "checkCallback", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "errCode", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "extraData", + "type": "bytes" + } + ], + "name": "checkErrorHandler", + "outputs": [ + { + "internalType": "bool", + "name": "upkeepNeeded", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "performData", + "type": "bytes" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -186,6 +286,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "feedParamKey", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "initialBlock", @@ -201,7 +314,7 @@ }, { "inputs": [], - "name": "isRecovered", + "name": "isStreamsLookup", "outputs": [ { "internalType": "bool", @@ -251,6 +364,71 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "string", + "name": "feedParam", + "type": "string" + } + ], + "name": "setFeedParamKey", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "value", + "type": "bool" + } + ], + "name": "setShouldRetryOnErrorBool", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "timeParam", + "type": "string" + } + ], + "name": "setTimeParamKey", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "shouldRetryOnError", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "timeParamKey", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "timeToPerform", diff --git a/contracts/abi/v0.8/SubscriptionAPI.json b/contracts/abi/v0.8/SubscriptionAPI.json index b7f94ae8..1c262bc6 100644 --- a/contracts/abi/v0.8/SubscriptionAPI.json +++ b/contracts/abi/v0.8/SubscriptionAPI.json @@ -549,7 +549,7 @@ }, { "internalType": "address", - "name": "owner", + "name": "subOwner", "type": "address" }, { diff --git a/contracts/abi/v0.8/UpkeepAutoFunder.json b/contracts/abi/v0.8/UpkeepAutoFunder.json new file mode 100644 index 00000000..a680f224 --- /dev/null +++ b/contracts/abi/v0.8/UpkeepAutoFunder.json @@ -0,0 +1,261 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "linkAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "registryAddress", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "OnlySimulatedBackend", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "OwnershipTransferRequested", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "inputs": [], + "name": "LINK", + "outputs": [ + { + "internalType": "contract LinkTokenInterface", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "checkUpkeep", + "outputs": [ + { + "internalType": "bool", + "name": "callable", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "executedata", + "type": "bytes" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "performUpkeep", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "s_autoFundLink", + "outputs": [ + { + "internalType": "uint96", + "name": "", + "type": "uint96" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "s_isEligible", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "s_keeperRegistry", + "outputs": [ + { + "internalType": "contract AutomationRegistryBaseInterface", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "s_shouldCancel", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "s_upkeepId", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint96", + "name": "value", + "type": "uint96" + } + ], + "name": "setAutoFundLink", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "value", + "type": "bool" + } + ], + "name": "setIsEligible", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "value", + "type": "bool" + } + ], + "name": "setShouldCancel", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "setUpkeepId", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/contracts/abi/v0.8/UpkeepMock.json b/contracts/abi/v0.8/UpkeepMock.json new file mode 100644 index 00000000..18ad60e8 --- /dev/null +++ b/contracts/abi/v0.8/UpkeepMock.json @@ -0,0 +1,239 @@ +[ + { + "inputs": [], + "name": "OnlySimulatedBackend", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes", + "name": "upkeepData", + "type": "bytes" + } + ], + "name": "UpkeepPerformedWith", + "type": "event" + }, + { + "inputs": [], + "name": "canCheck", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "canPerform", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "checkGasToBurn", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "checkRevertReason", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "checkUpkeep", + "outputs": [ + { + "internalType": "bool", + "name": "callable", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "executedata", + "type": "bytes" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "performData", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "performGasToBurn", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "performUpkeep", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "value", + "type": "bool" + } + ], + "name": "setCanCheck", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "value", + "type": "bool" + } + ], + "name": "setCanPerform", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "setCheckGasToBurn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "value", + "type": "string" + } + ], + "name": "setCheckRevertReason", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "setPerformData", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "setPerformGasToBurn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "value", + "type": "bool" + } + ], + "name": "setShouldRevertCheck", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "shouldRevertCheck", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/contracts/abi/v0.8/UpkeepReverter.json b/contracts/abi/v0.8/UpkeepReverter.json new file mode 100644 index 00000000..dd3c3e38 --- /dev/null +++ b/contracts/abi/v0.8/UpkeepReverter.json @@ -0,0 +1,44 @@ +[ + { + "inputs": [], + "name": "OnlySimulatedBackend", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "checkUpkeep", + "outputs": [ + { + "internalType": "bool", + "name": "callable", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "executedata", + "type": "bytes" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "performUpkeep", + "outputs": [], + "stateMutability": "pure", + "type": "function" + } +] diff --git a/contracts/abi/v0.8/UpkeepTranscoder5_0.json b/contracts/abi/v0.8/UpkeepTranscoder5_0.json new file mode 100644 index 00000000..2f7d062a --- /dev/null +++ b/contracts/abi/v0.8/UpkeepTranscoder5_0.json @@ -0,0 +1,49 @@ +[ + { + "inputs": [], + "name": "InvalidTranscoding", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "fromVersion", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "toVersion", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "encodedUpkeeps", + "type": "bytes" + } + ], + "name": "transcodeUpkeeps", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "typeAndVersion", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/contracts/abi/v0.8/VRFCoordinatorV2PlusUpgradedVersion.json b/contracts/abi/v0.8/VRFCoordinatorV2PlusUpgradedVersion.json index af7f7511..406c0a58 100644 --- a/contracts/abi/v0.8/VRFCoordinatorV2PlusUpgradedVersion.json +++ b/contracts/abi/v0.8/VRFCoordinatorV2PlusUpgradedVersion.json @@ -1098,7 +1098,7 @@ }, { "internalType": "address", - "name": "owner", + "name": "subOwner", "type": "address" }, { diff --git a/contracts/abi/v0.8/VRFCoordinatorV2_5.json b/contracts/abi/v0.8/VRFCoordinatorV2_5.json index 9d1cfdf6..51e13a5c 100644 --- a/contracts/abi/v0.8/VRFCoordinatorV2_5.json +++ b/contracts/abi/v0.8/VRFCoordinatorV2_5.json @@ -116,22 +116,6 @@ "name": "InsufficientBalance", "type": "error" }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "have", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "want", - "type": "uint256" - } - ], - "name": "InsufficientGasForConsumer", - "type": "error" - }, { "inputs": [], "name": "InvalidCalldata", @@ -1140,7 +1124,7 @@ "type": "bytes" } ], - "internalType": "struct VRFCoordinatorV2_5.RequestCommitment", + "internalType": "struct VRFTypes.RequestCommitmentV2Plus", "name": "rc", "type": "tuple" }, @@ -1225,7 +1209,7 @@ }, { "internalType": "address", - "name": "owner", + "name": "subOwner", "type": "address" }, { diff --git a/contracts/abi/v0.8/VRFV2PlusLoadTestWithMetrics.json b/contracts/abi/v0.8/VRFV2PlusLoadTestWithMetrics.json index a7d3e338..6301bab4 100644 --- a/contracts/abi/v0.8/VRFV2PlusLoadTestWithMetrics.json +++ b/contracts/abi/v0.8/VRFV2PlusLoadTestWithMetrics.json @@ -110,6 +110,30 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "offset", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "quantity", + "type": "uint256" + } + ], + "name": "getRequestBlockTimes", + "outputs": [ + { + "internalType": "uint32[]", + "name": "", + "type": "uint32[]" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -300,6 +324,25 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "s_requestBlockTimes", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "s_requestCount", diff --git a/contracts/abi/v0.8/VRFV2PlusWrapper.json b/contracts/abi/v0.8/VRFV2PlusWrapper.json index 7ac75005..b58d9869 100644 --- a/contracts/abi/v0.8/VRFV2PlusWrapper.json +++ b/contracts/abi/v0.8/VRFV2PlusWrapper.json @@ -15,6 +15,11 @@ "internalType": "address", "name": "_coordinator", "type": "address" + }, + { + "internalType": "uint256", + "name": "_subId", + "type": "uint256" } ], "stateMutability": "nonpayable", @@ -41,6 +46,22 @@ "name": "IncorrectExtraArgsLength", "type": "error" }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "premiumPercentage", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "max", + "type": "uint8" + } + ], + "name": "InvalidPremiumPercentage", + "type": "error" + }, { "inputs": [], "name": "LINKPaymentInRequestRandomWordsInNative", @@ -51,6 +72,22 @@ "name": "LinkAlreadySet", "type": "error" }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "flatFeeLinkDiscountPPM", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "flatFeeNativePPM", + "type": "uint32" + } + ], + "name": "LinkDiscountTooHigh", + "type": "error" + }, { "inputs": [], "name": "NativePaymentInOnTokenTransfer", @@ -93,6 +130,11 @@ "name": "OnlyOwnerOrCoordinator", "type": "error" }, + { + "inputs": [], + "name": "SubscriptionIdMissing", + "type": "error" + }, { "inputs": [], "name": "ZeroAddress", @@ -116,7 +158,13 @@ { "indexed": false, "internalType": "uint8", - "name": "wrapperPremiumPercentage", + "name": "coordinatorNativePremiumPercentage", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "coordinatorLinkPremiumPercentage", "type": "uint8" }, { @@ -146,13 +194,13 @@ { "indexed": false, "internalType": "uint32", - "name": "fulfillmentFlatFeeLinkPPM", + "name": "fulfillmentFlatFeeNativePPM", "type": "uint32" }, { "indexed": false, "internalType": "uint32", - "name": "fulfillmentFlatFeeNativePPM", + "name": "fulfillmentFlatFeeLinkDiscountPPM", "type": "uint32" } ], @@ -216,25 +264,6 @@ "name": "FulfillmentTxSizeSet", "type": "event" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "link", - "type": "address" - }, - { - "indexed": false, - "internalType": "address", - "name": "linkNativeFeed", - "type": "address" - } - ], - "name": "LinkAndLinkNativeFeedSet", - "type": "event" - }, { "anonymous": false, "inputs": [ @@ -484,12 +513,12 @@ }, { "internalType": "uint32", - "name": "fulfillmentFlatFeeLinkPPM", + "name": "fulfillmentFlatFeeNativePPM", "type": "uint32" }, { "internalType": "uint32", - "name": "fulfillmentFlatFeeNativePPM", + "name": "fulfillmentFlatFeeLinkDiscountPPM", "type": "uint32" }, { @@ -504,7 +533,12 @@ }, { "internalType": "uint8", - "name": "wrapperPremiumPercentage", + "name": "wrapperNativePremiumPercentage", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "wrapperLinkPremiumPercentage", "type": "uint8" }, { @@ -535,16 +569,29 @@ "type": "function" }, { - "inputs": [ + "inputs": [], + "name": "link", + "outputs": [ { "internalType": "address", - "name": "newCoordinator", + "name": "", "type": "address" } ], - "name": "migrate", - "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "linkNativeFeed", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", "type": "function" }, { @@ -703,32 +750,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [], - "name": "s_link", - "outputs": [ - { - "internalType": "contract LinkTokenInterface", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "s_linkNativeFeed", - "outputs": [ - { - "internalType": "contract AggregatorV3Interface", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "s_vrfCoordinator", @@ -756,7 +777,12 @@ }, { "internalType": "uint8", - "name": "_wrapperPremiumPercentage", + "name": "_coordinatorNativePremiumPercentage", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "_coordinatorLinkPremiumPercentage", "type": "uint8" }, { @@ -781,12 +807,12 @@ }, { "internalType": "uint32", - "name": "_fulfillmentFlatFeeLinkPPM", + "name": "_fulfillmentFlatFeeNativePPM", "type": "uint32" }, { "internalType": "uint32", - "name": "_fulfillmentFlatFeeNativePPM", + "name": "_fulfillmentFlatFeeLinkDiscountPPM", "type": "uint32" } ], @@ -821,24 +847,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "link", - "type": "address" - }, - { - "internalType": "address", - "name": "linkNativeFeed", - "type": "address" - } - ], - "name": "setLinkAndLinkNativeFeed", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [ { diff --git a/contracts/abi/v0.8/VRFV2PlusWrapperConsumerBase.json b/contracts/abi/v0.8/VRFV2PlusWrapperConsumerBase.json index 84b49bfc..f5a54bb6 100644 --- a/contracts/abi/v0.8/VRFV2PlusWrapperConsumerBase.json +++ b/contracts/abi/v0.8/VRFV2PlusWrapperConsumerBase.json @@ -1,9 +1,4 @@ [ - { - "inputs": [], - "name": "LINKAlreadySet", - "type": "error" - }, { "inputs": [ { @@ -20,19 +15,6 @@ "name": "OnlyVRFWrapperCanFulfill", "type": "error" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "link", - "type": "address" - } - ], - "name": "LinkTokenSet", - "type": "event" - }, { "inputs": [], "name": "getBalance", @@ -89,18 +71,5 @@ "outputs": [], "stateMutability": "nonpayable", "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_link", - "type": "address" - } - ], - "name": "setLinkToken", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" } ] diff --git a/contracts/abi/v0.8/VRFV2PlusWrapperConsumerExample.json b/contracts/abi/v0.8/VRFV2PlusWrapperConsumerExample.json index 8792b778..56e96ee1 100644 --- a/contracts/abi/v0.8/VRFV2PlusWrapperConsumerExample.json +++ b/contracts/abi/v0.8/VRFV2PlusWrapperConsumerExample.json @@ -1,11 +1,6 @@ [ { "inputs": [ - { - "internalType": "address", - "name": "_link", - "type": "address" - }, { "internalType": "address", "name": "_vrfV2Wrapper", @@ -15,11 +10,6 @@ "stateMutability": "nonpayable", "type": "constructor" }, - { - "inputs": [], - "name": "LINKAlreadySet", - "type": "error" - }, { "inputs": [ { @@ -36,19 +26,6 @@ "name": "OnlyVRFWrapperCanFulfill", "type": "error" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "link", - "type": "address" - } - ], - "name": "LinkTokenSet", - "type": "event" - }, { "anonymous": false, "inputs": [ @@ -324,19 +301,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "_link", - "type": "address" - } - ], - "name": "setLinkToken", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [ { diff --git a/contracts/abi/v0.8/VRFV2PlusWrapperLoadTestConsumer.json b/contracts/abi/v0.8/VRFV2PlusWrapperLoadTestConsumer.json index d1987c54..3ba6702d 100644 --- a/contracts/abi/v0.8/VRFV2PlusWrapperLoadTestConsumer.json +++ b/contracts/abi/v0.8/VRFV2PlusWrapperLoadTestConsumer.json @@ -1,11 +1,6 @@ [ { "inputs": [ - { - "internalType": "address", - "name": "_link", - "type": "address" - }, { "internalType": "address", "name": "_vrfV2PlusWrapper", @@ -15,11 +10,6 @@ "stateMutability": "nonpayable", "type": "constructor" }, - { - "inputs": [], - "name": "LINKAlreadySet", - "type": "error" - }, { "inputs": [ { @@ -36,19 +26,6 @@ "name": "OnlyVRFWrapperCanFulfill", "type": "error" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "link", - "type": "address" - } - ], - "name": "LinkTokenSet", - "type": "event" - }, { "anonymous": false, "inputs": [ @@ -164,6 +141,30 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "offset", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "quantity", + "type": "uint256" + } + ], + "name": "getRequestBlockTimes", + "outputs": [ + { + "internalType": "uint32[]", + "name": "", + "type": "uint32[]" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -359,6 +360,25 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "s_requestBlockTimes", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "s_requestCount", @@ -447,19 +467,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "_link", - "type": "address" - } - ], - "name": "setLinkToken", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [ { diff --git a/contracts/abi/v0.8/WETH9.json b/contracts/abi/v0.8/WETH9.json new file mode 100644 index 00000000..9cf2ec79 --- /dev/null +++ b/contracts/abi/v0.8/WETH9.json @@ -0,0 +1,309 @@ +[ + { + "inputs": [], + "name": "InsufficientBalance", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "src", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "guy", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "dst", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "Deposit", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "src", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "dst", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "src", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "Withdrawal", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "guy", + "type": "address" + }, + { + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "deposit", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "dst", + "type": "address" + }, + { + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "src", + "type": "address" + }, + { + "internalType": "address", + "name": "dst", + "type": "address" + }, + { + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +] diff --git a/contracts/package.json b/contracts/package.json index 24e6614c..460aff19 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -1,6 +1,6 @@ { "name": "@chainlink/contracts", - "version": "1.0.0", + "version": "1.1.0", "description": "Chainlink smart contracts", "author": "Chainlink devs", "license": "MIT", @@ -15,60 +15,50 @@ "@ethersproject/bignumber": "~5.7.0", "@ethersproject/contracts": "~5.7.0", "@ethersproject/providers": "~5.7.2", - "@ethersproject/random": "~5.7.0", + "@nomicfoundation/hardhat-chai-matchers": "^1.0.6", + "@nomicfoundation/hardhat-ethers": "^3.0.5", "@nomicfoundation/hardhat-network-helpers": "^1.0.9", - "@nomiclabs/hardhat-ethers": "^2.2.3", - "@nomiclabs/hardhat-etherscan": "^3.1.7", - "@nomiclabs/hardhat-waffle": "2.0.6", - "@openzeppelin/hardhat-upgrades": "1.28.0", - "@openzeppelin/test-helpers": "^0.5.16", + "@nomicfoundation/hardhat-verify": "^2.0.5", "@typechain/ethers-v5": "^7.2.0", "@typechain/hardhat": "^7.0.0", "@types/cbor": "5.0.1", - "@types/chai": "^4.3.11", + "@types/chai": "^4.3.14", "@types/debug": "^4.1.12", "@types/deep-equal-in-any-order": "^1.0.3", "@types/mocha": "^10.0.6", - "@types/node": "^16.18.80", + "@types/node": "^16.18.91", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "abi-to-sol": "^0.6.6", "cbor": "^5.2.0", "chai": "^4.3.10", "debug": "^4.3.4", + "deep-equal-in-any-order": "^2.0.6", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", - "deep-equal-in-any-order": "^2.0.6", "eslint-plugin-prettier": "^5.1.3", - "ethereum-waffle": "^3.4.4", "ethers": "~5.7.2", - "hardhat": "~2.19.2", + "hardhat": "~2.20.1", "hardhat-abi-exporter": "^2.10.1", - "hardhat-contract-sizer": "^2.10.0", - "hardhat-gas-reporter": "^1.0.9", "hardhat-ignore-warnings": "^0.2.6", - "istanbul": "^0.4.5", - "moment": "^2.29.4", + "moment": "^2.30.1", "prettier": "^3.2.5", "prettier-plugin-solidity": "1.3.1", - "rlp": "^2.2.7", - "solhint": "^4.1.1", + "solhint": "^4.5.2", "solhint-plugin-chainlink-solidity": "git+https://github.com/smartcontractkit/chainlink-solhint-rules.git#v1.2.1", "solhint-plugin-prettier": "^0.1.0", - "solidity-coverage": "^0.8.5", "ts-node": "^10.9.2", - "tslib": "^2.6.2", "typechain": "^8.2.1", - "typescript": "^5.3.3" + "typescript": "^5.4.3" }, "dependencies": { + "@changesets/changelog-github": "^0.4.8", + "@changesets/cli": "~2.26.2", "@eth-optimism/contracts": "0.6.0", - "@scroll-tech/contracts": "0.1.0", "@openzeppelin/contracts": "4.9.3", "@openzeppelin/contracts-upgradeable": "4.9.3", - "@changesets/changelog-github": "^0.4.8", - "@changesets/cli": "~2.26.2", - "semver": "^7.5.4" + "@scroll-tech/contracts": "0.1.0", + "semver": "^7.6.0" }, "scripts": { "test": "hardhat test --parallel", @@ -81,7 +71,7 @@ "compile": "hardhat compile", "coverage": "hardhat coverage", "publish-beta": "pnpm publish --tag beta", - "publish-prod": "npm dist-tag add @chainlink/contracts@1.0.0 latest", - "solhint": "solhint --max-warnings 20 \"./src/v0.8/**/*.sol\"" + "publish-prod": "pnpm publish --tag latest", + "solhint": "solhint --max-warnings 85 \"./src/v0.8/**/*.sol\"" } } \ No newline at end of file diff --git a/contracts/src/v0.8/ChainSpecificUtil.sol b/contracts/src/v0.8/ChainSpecificUtil.sol index 172d8c52..e8e52c8e 100644 --- a/contracts/src/v0.8/ChainSpecificUtil.sol +++ b/contracts/src/v0.8/ChainSpecificUtil.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.4; +pragma solidity ^0.8.9; import {ArbSys} from "./vendor/@arbitrum/nitro-contracts/src/precompiles/ArbSys.sol"; import {ArbGasInfo} from "./vendor/@arbitrum/nitro-contracts/src/precompiles/ArbGasInfo.sol"; -import {OVM_GasPriceOracle} from "./vendor/@eth-optimism/contracts/v0.8.6/contracts/L2/predeploys/OVM_GasPriceOracle.sol"; +import {OVM_GasPriceOracle} from "./vendor/@eth-optimism/contracts/v0.8.9/contracts/L2/predeploys/OVM_GasPriceOracle.sol"; /// @dev A library that abstracts out opcodes that behave differently across chains. /// @dev The methods below return values that are pertinent to the given chain. diff --git a/contracts/src/v0.8/ChainSpecificUtil_v0_8_6.sol b/contracts/src/v0.8/ChainSpecificUtil_v0_8_6.sol new file mode 100644 index 00000000..f87c75fe --- /dev/null +++ b/contracts/src/v0.8/ChainSpecificUtil_v0_8_6.sol @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.6; + +import {ArbSys} from "./vendor/@arbitrum/nitro-contracts/src/precompiles/ArbSys.sol"; +import {ArbGasInfo} from "./vendor/@arbitrum/nitro-contracts/src/precompiles/ArbGasInfo.sol"; +import {OVM_GasPriceOracle} from "./vendor/@eth-optimism/contracts/v0.8.6/contracts/L2/predeploys/OVM_GasPriceOracle.sol"; + +/// @dev A library that abstracts out opcodes that behave differently across chains. +/// @dev The methods below return values that are pertinent to the given chain. +/// @dev For instance, ChainSpecificUtil.getBlockNumber() returns L2 block number in L2 chains +library ChainSpecificUtil { + // ------------ Start Arbitrum Constants ------------ + + /// @dev ARBSYS_ADDR is the address of the ArbSys precompile on Arbitrum. + /// @dev reference: https://github.com/OffchainLabs/nitro/blob/v2.0.14/contracts/src/precompiles/ArbSys.sol#L10 + address private constant ARBSYS_ADDR = address(0x0000000000000000000000000000000000000064); + ArbSys private constant ARBSYS = ArbSys(ARBSYS_ADDR); + + /// @dev ARBGAS_ADDR is the address of the ArbGasInfo precompile on Arbitrum. + /// @dev reference: https://github.com/OffchainLabs/nitro/blob/v2.0.14/contracts/src/precompiles/ArbGasInfo.sol#L10 + address private constant ARBGAS_ADDR = address(0x000000000000000000000000000000000000006C); + ArbGasInfo private constant ARBGAS = ArbGasInfo(ARBGAS_ADDR); + + uint256 private constant ARB_MAINNET_CHAIN_ID = 42161; + uint256 private constant ARB_GOERLI_TESTNET_CHAIN_ID = 421613; + uint256 private constant ARB_SEPOLIA_TESTNET_CHAIN_ID = 421614; + + // ------------ End Arbitrum Constants ------------ + + // ------------ Start Optimism Constants ------------ + /// @dev L1_FEE_DATA_PADDING includes 35 bytes for L1 data padding for Optimism + bytes internal constant L1_FEE_DATA_PADDING = + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; + /// @dev OVM_GASPRICEORACLE_ADDR is the address of the OVM_GasPriceOracle precompile on Optimism. + /// @dev reference: https://community.optimism.io/docs/developers/build/transaction-fees/#estimating-the-l1-data-fee + address private constant OVM_GASPRICEORACLE_ADDR = address(0x420000000000000000000000000000000000000F); + OVM_GasPriceOracle private constant OVM_GASPRICEORACLE = OVM_GasPriceOracle(OVM_GASPRICEORACLE_ADDR); + + uint256 private constant OP_MAINNET_CHAIN_ID = 10; + uint256 private constant OP_GOERLI_CHAIN_ID = 420; + uint256 private constant OP_SEPOLIA_CHAIN_ID = 11155420; + + /// @dev Base is a OP stack based rollup and follows the same L1 pricing logic as Optimism. + uint256 private constant BASE_MAINNET_CHAIN_ID = 8453; + uint256 private constant BASE_GOERLI_CHAIN_ID = 84531; + + // ------------ End Optimism Constants ------------ + + /** + * @notice Returns the blockhash for the given blockNumber. + * @notice If the blockNumber is more than 256 blocks in the past, returns the empty string. + * @notice When on a known Arbitrum chain, it uses ArbSys.arbBlockHash to get the blockhash. + * @notice Otherwise, it uses the blockhash opcode. + * @notice Note that the blockhash opcode will return the L2 blockhash on Optimism. + */ + function _getBlockhash(uint64 blockNumber) internal view returns (bytes32) { + uint256 chainid = block.chainid; + if (_isArbitrumChainId(chainid)) { + if ((_getBlockNumber() - blockNumber) > 256 || blockNumber >= _getBlockNumber()) { + return ""; + } + return ARBSYS.arbBlockHash(blockNumber); + } + return blockhash(blockNumber); + } + + /** + * @notice Returns the block number of the current block. + * @notice When on a known Arbitrum chain, it uses ArbSys.arbBlockNumber to get the block number. + * @notice Otherwise, it uses the block.number opcode. + * @notice Note that the block.number opcode will return the L2 block number on Optimism. + */ + function _getBlockNumber() internal view returns (uint256) { + uint256 chainid = block.chainid; + if (_isArbitrumChainId(chainid)) { + return ARBSYS.arbBlockNumber(); + } + return block.number; + } + + /** + * @notice Returns the L1 fees that will be paid for the current transaction, given any calldata + * @notice for the current transaction. + * @notice When on a known Arbitrum chain, it uses ArbGas.getCurrentTxL1GasFees to get the fees. + * @notice On Arbitrum, the provided calldata is not used to calculate the fees. + * @notice On Optimism, the provided calldata is passed to the OVM_GasPriceOracle predeploy + * @notice and getL1Fee is called to get the fees. + */ + function _getCurrentTxL1GasFees(bytes memory txCallData) internal view returns (uint256) { + uint256 chainid = block.chainid; + if (_isArbitrumChainId(chainid)) { + return ARBGAS.getCurrentTxL1GasFees(); + } else if (_isOptimismChainId(chainid)) { + return OVM_GASPRICEORACLE.getL1Fee(bytes.concat(txCallData, L1_FEE_DATA_PADDING)); + } + return 0; + } + + /** + * @notice Returns the gas cost in wei of calldataSizeBytes of calldata being posted + * @notice to L1. + */ + function _getL1CalldataGasCost(uint256 calldataSizeBytes) internal view returns (uint256) { + uint256 chainid = block.chainid; + if (_isArbitrumChainId(chainid)) { + (, uint256 l1PricePerByte, , , , ) = ARBGAS.getPricesInWei(); + // see https://developer.arbitrum.io/devs-how-tos/how-to-estimate-gas#where-do-we-get-all-this-information-from + // for the justification behind the 140 number. + return l1PricePerByte * (calldataSizeBytes + 140); + } else if (_isOptimismChainId(chainid)) { + return _calculateOptimismL1DataFee(calldataSizeBytes); + } + return 0; + } + + /** + * @notice Return true if and only if the provided chain ID is an Arbitrum chain ID. + */ + function _isArbitrumChainId(uint256 chainId) internal pure returns (bool) { + return + chainId == ARB_MAINNET_CHAIN_ID || + chainId == ARB_GOERLI_TESTNET_CHAIN_ID || + chainId == ARB_SEPOLIA_TESTNET_CHAIN_ID; + } + + /** + * @notice Return true if and only if the provided chain ID is an Optimism chain ID. + * @notice Note that optimism chain id's are also OP stack chain id's. + */ + function _isOptimismChainId(uint256 chainId) internal pure returns (bool) { + return + chainId == OP_MAINNET_CHAIN_ID || + chainId == OP_GOERLI_CHAIN_ID || + chainId == OP_SEPOLIA_CHAIN_ID || + chainId == BASE_MAINNET_CHAIN_ID || + chainId == BASE_GOERLI_CHAIN_ID; + } + + function _calculateOptimismL1DataFee(uint256 calldataSizeBytes) internal view returns (uint256) { + // from: https://community.optimism.io/docs/developers/build/transaction-fees/#the-l1-data-fee + // l1_data_fee = l1_gas_price * (tx_data_gas + fixed_overhead) * dynamic_overhead + // tx_data_gas = count_zero_bytes(tx_data) * 4 + count_non_zero_bytes(tx_data) * 16 + // note we conservatively assume all non-zero bytes. + uint256 l1BaseFeeWei = OVM_GASPRICEORACLE.l1BaseFee(); + uint256 numZeroBytes = 0; + uint256 numNonzeroBytes = calldataSizeBytes - numZeroBytes; + uint256 txDataGas = numZeroBytes * 4 + numNonzeroBytes * 16; + uint256 fixedOverhead = OVM_GASPRICEORACLE.overhead(); + + // The scalar is some value like 0.684, but is represented as + // that times 10 ^ number of scalar decimals. + // e.g scalar = 0.684 * 10^6 + // The divisor is used to divide that and have a net result of the true scalar. + uint256 scalar = OVM_GASPRICEORACLE.scalar(); + uint256 scalarDecimals = OVM_GASPRICEORACLE.decimals(); + uint256 divisor = 10 ** scalarDecimals; + + uint256 l1DataFee = (l1BaseFeeWei * (txDataGas + fixedOverhead) * scalar) / divisor; + return l1DataFee; + } +} diff --git a/contracts/src/v0.8/ChainlinkClient.sol b/contracts/src/v0.8/ChainlinkClient.sol index ef7f7943..1d8640a2 100644 --- a/contracts/src/v0.8/ChainlinkClient.sol +++ b/contracts/src/v0.8/ChainlinkClient.sol @@ -14,7 +14,7 @@ import {ENSResolver as ENSResolver_Chainlink} from "./vendor/ENSResolver.sol"; * @notice Contract writers can inherit this contract in order to create requests for the * Chainlink network */ -// solhint-disable custom-errors +// solhint-disable gas-custom-errors abstract contract ChainlinkClient { using Chainlink for Chainlink.Request; diff --git a/contracts/src/v0.8/Flags.sol b/contracts/src/v0.8/Flags.sol index 7cd5a54b..de14583b 100644 --- a/contracts/src/v0.8/Flags.sol +++ b/contracts/src/v0.8/Flags.sol @@ -13,7 +13,7 @@ import {FlagsInterface} from "./interfaces/FlagsInterface.sol"; * to allow addresses to raise flags on themselves, so if you are subscribing to * FlagOn events you should filter for addresses you care about. */ -// solhint-disable custom-errors +// solhint-disable gas-custom-errors contract Flags is FlagsInterface, SimpleReadAccessController { AccessControllerInterface public raisingAccessController; diff --git a/contracts/src/v0.8/ValidatorProxy.sol b/contracts/src/v0.8/ValidatorProxy.sol index 4584bb02..58e0e28a 100644 --- a/contracts/src/v0.8/ValidatorProxy.sol +++ b/contracts/src/v0.8/ValidatorProxy.sol @@ -5,7 +5,7 @@ import {ConfirmedOwner} from "./shared/access/ConfirmedOwner.sol"; import {AggregatorValidatorInterface} from "./shared/interfaces/AggregatorValidatorInterface.sol"; import {TypeAndVersionInterface} from "./interfaces/TypeAndVersionInterface.sol"; -// solhint-disable custom-errors +// solhint-disable gas-custom-errors contract ValidatorProxy is AggregatorValidatorInterface, TypeAndVersionInterface, ConfirmedOwner { /// @notice Uses a single storage slot to store the current address struct AggregatorConfiguration { diff --git a/contracts/src/v0.8/automation/Chainable.sol b/contracts/src/v0.8/automation/Chainable.sol index 9ebc8c34..469ea91a 100644 --- a/contracts/src/v0.8/automation/Chainable.sol +++ b/contracts/src/v0.8/automation/Chainable.sol @@ -30,9 +30,8 @@ contract Chainable { * @notice the fallback function routes the call to the next contract in the chain * @dev most of the implementation is copied directly from OZ's Proxy contract */ - // solhint-disable payable-fallback // solhint-disable-next-line no-complex-fallback - fallback() external { + fallback() external payable { // copy to memory for assembly access address next = i_FALLBACK_ADDRESS; // copied directly from OZ's Proxy contract diff --git a/contracts/src/v0.8/automation/HeartbeatRequester.sol b/contracts/src/v0.8/automation/HeartbeatRequester.sol index aa390738..8ef7fa44 100644 --- a/contracts/src/v0.8/automation/HeartbeatRequester.sol +++ b/contracts/src/v0.8/automation/HeartbeatRequester.sol @@ -33,7 +33,6 @@ contract HeartbeatRequester is TypeAndVersionInterface, ConfirmedOwner { * - HeartbeatRequester 1.0.0: The requester fetches the latest aggregator address from proxy, and request a new round * using the aggregator address. */ - // solhint-disable-next-line chainlink-solidity/all-caps-constant-storage-variables string public constant override typeAndVersion = "HeartbeatRequester 1.0.0"; constructor() ConfirmedOwner(msg.sender) {} diff --git a/contracts/src/v0.8/automation/README.md b/contracts/src/v0.8/automation/README.md index 9714ccb9..717e7411 100644 --- a/contracts/src/v0.8/automation/README.md +++ b/contracts/src/v0.8/automation/README.md @@ -1,38 +1,41 @@ # 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. +The on-chain component of Chainlink automation is too large to fit into the [size requirements][size-limit-eip] of a single contract. Therefore, we include 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.) +**Root Contract** - also known as the “storage” contract. This is the contract whose state we care about. It is the on-chain entry-point into the system. The root contract uses `delegatecall` to execute code at various logic contracts. (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. +**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 root 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]. +We chain multiple logic contracts together using [fallback functions][fallback] and [delegatecall][delegatecall]. If a function definition is not found on one contract, we fallback to the next, always executing the function in the scope of the root 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] + Root -- delegatecall --> la[Logic A] la -- delegatecall --> lb[Logic B] - lb -. delegatecall .-> lx[Logic X] + lb -. delegatecall .-> lx[Logic Z] ``` ### 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 +- functions on the root contract have the least gas overhead. Therefore, our most price-sensitive functions live there. We have 3 functions that we consider hot paths. Ideally, these would all live on the root contract to minimize gas overhead. + - `transmit()` - this is the most important code path + - `registerUpkeep()` + - `addFunds()` +- functions on the root 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 root - therefore we give secondary preference to the first logic contract for user and debugging functions +- functions on logic B through logic Z (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_1.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. +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`. [size-limit-eip]: https://eips.ethereum.org/EIPS/eip-170 [fallback]: https://docs.soliditylang.org/en/v0.8.12/contracts.html#fallback-function diff --git a/contracts/src/v0.8/automation/UpkeepTranscoder.sol b/contracts/src/v0.8/automation/UpkeepTranscoder.sol index 144a96c7..03f40d89 100644 --- a/contracts/src/v0.8/automation/UpkeepTranscoder.sol +++ b/contracts/src/v0.8/automation/UpkeepTranscoder.sol @@ -17,7 +17,6 @@ contract UpkeepTranscoder is UpkeepTranscoderInterface, TypeAndVersionInterface * @notice versions: * - UpkeepTranscoder 1.0.0: placeholder to allow new formats in the future */ - // solhint-disable-next-line chainlink-solidity/all-caps-constant-storage-variables string public constant override typeAndVersion = "UpkeepTranscoder 1.0.0"; /** diff --git a/contracts/src/v0.8/automation/dev/MercuryRegistry.sol b/contracts/src/v0.8/automation/dev/MercuryRegistry.sol index e6b6920f..247301a7 100644 --- a/contracts/src/v0.8/automation/dev/MercuryRegistry.sol +++ b/contracts/src/v0.8/automation/dev/MercuryRegistry.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.6; +pragma solidity 0.8.19; import {ConfirmedOwner} from "../../shared/access/ConfirmedOwner.sol"; import {AutomationCompatibleInterface} from "../interfaces/AutomationCompatibleInterface.sol"; diff --git a/contracts/src/v0.8/automation/dev/MercuryRegistryBatchUpkeep.sol b/contracts/src/v0.8/automation/dev/MercuryRegistryBatchUpkeep.sol index 518275f3..2d3daeae 100644 --- a/contracts/src/v0.8/automation/dev/MercuryRegistryBatchUpkeep.sol +++ b/contracts/src/v0.8/automation/dev/MercuryRegistryBatchUpkeep.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.6; +pragma solidity 0.8.19; import {ConfirmedOwner} from "../../shared/access/ConfirmedOwner.sol"; import {AutomationCompatibleInterface} from "../interfaces/AutomationCompatibleInterface.sol"; diff --git a/contracts/src/v0.8/automation/dev/interfaces/v2_3/IAutomationRegistryMaster2_3.sol b/contracts/src/v0.8/automation/dev/interfaces/v2_3/IAutomationRegistryMaster2_3.sol index c4d3bb48..8570c213 100644 --- a/contracts/src/v0.8/automation/dev/interfaces/v2_3/IAutomationRegistryMaster2_3.sol +++ b/contracts/src/v0.8/automation/dev/interfaces/v2_3/IAutomationRegistryMaster2_3.sol @@ -1,4 +1,4 @@ -// abi-checksum: 0x5f06e4e00f3041d11b2f2109257ecaf84cb540cbd4f7246c30d5dca752b51e53 +// abi-checksum: 0x0dcdf05637762c60acb4616a251bdc40d85ba134ec4b1b1e9f66646ba3132055 // SPDX-License-Identifier: MIT // !! THIS FILE WAS AUTOGENERATED BY abi-to-sol v0.6.6. SEE SOURCE BELOW. !! pragma solidity ^0.8.4; @@ -17,18 +17,20 @@ interface IAutomationRegistryMaster2_3 { error IncorrectNumberOfSigners(); error IndexOutOfRange(); error InsufficientBalance(uint256 available, uint256 requested); + error InsufficientLinkLiquidity(); error InvalidDataLength(); error InvalidFeed(); error InvalidPayee(); error InvalidRecipient(); error InvalidReport(); error InvalidSigner(); + error InvalidToken(); error InvalidTransmitter(); error InvalidTrigger(); error InvalidTriggerType(); - error MaxCheckDataSizeCanOnlyIncrease(); - error MaxPerformDataSizeCanOnlyIncrease(); error MigrationNotPermitted(); + error MustSettleOffchain(); + error MustSettleOnchain(); error NotAContract(); error OnlyActiveSigners(); error OnlyActiveTransmitters(); @@ -45,7 +47,6 @@ interface IAutomationRegistryMaster2_3 { error OnlySimulatedBackend(); error OnlyUnpausedUpkeep(); error ParameterLengthError(); - error PaymentGreaterThanAllLINK(); error ReentrantCall(); error RegistryPaused(); error RepeatedSigner(); @@ -61,6 +62,8 @@ interface IAutomationRegistryMaster2_3 { error ValueNotChanged(); error ZeroAddressNotAllowed(); event AdminPrivilegeConfigSet(address indexed admin, bytes privilegeConfig); + event BillingConfigOverridden(uint256 indexed id, AutomationRegistryBase2_3.BillingOverrides overrides); + event BillingConfigOverrideRemoved(uint256 indexed id); event BillingConfigSet(address indexed token, AutomationRegistryBase2_3.BillingConfig config); event CancelledUpkeepReport(uint256 indexed id, bytes trigger); event ChainSpecificModuleUpdated(address newModule); @@ -76,10 +79,11 @@ interface IAutomationRegistryMaster2_3 { bytes offchainConfig ); event DedupKeyAdded(bytes32 indexed dedupKey); - event FeesWithdrawn(address indexed recipient, address indexed assetAddress, uint256 amount); + event FeesWithdrawn(address indexed assetAddress, address indexed recipient, uint256 amount); event FundsAdded(uint256 indexed id, address indexed from, uint96 amount); event FundsWithdrawn(uint256 indexed id, uint256 amount, address to); event InsufficientFundsUpkeepReport(uint256 indexed id, bytes trigger); + event NOPsSettledOffchain(address[] payees, uint256[] payments); event OwnershipTransferRequested(address indexed from, address indexed to); event OwnershipTransferred(address indexed from, address indexed to); event Paused(address account); @@ -112,8 +116,9 @@ interface IAutomationRegistryMaster2_3 { event UpkeepRegistered(uint256 indexed id, uint32 performGas, address admin); event UpkeepTriggerConfigSet(uint256 indexed id, bytes triggerConfig); event UpkeepUnpaused(uint256 indexed id); - fallback() external; + fallback() external payable; function acceptOwnership() external; + function addFunds(uint256 id, uint96 amount) external payable; 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); @@ -137,10 +142,6 @@ interface IAutomationRegistryMaster2_3 { address[] memory billingTokens, AutomationRegistryBase2_3.BillingConfig[] memory billingConfigs ) 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, @@ -151,8 +152,21 @@ interface IAutomationRegistryMaster2_3 { ) external; function typeAndVersion() external view returns (string memory); - function addFunds(uint256 id, uint96 amount) external; function cancelUpkeep(uint256 id) external; + function migrateUpkeeps(uint256[] memory ids, address destination) external; + function receiveUpkeeps(bytes memory encodedUpkeeps) external; + function registerUpkeep( + address target, + uint32 gasLimit, + address admin, + uint8 triggerType, + address billingToken, + bytes memory checkData, + bytes memory triggerConfig, + bytes memory offchainConfig + ) external returns (uint256 id); + + function acceptUpkeepAdmin(uint256 id) external; function checkCallback( uint256 id, bytes[] memory values, @@ -191,125 +205,159 @@ interface IAutomationRegistryMaster2_3 { 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 pauseUpkeep(uint256 id) external; + function removeBillingOverrides(uint256 id) external; + function setBillingOverrides(uint256 id, AutomationRegistryBase2_3.BillingOverrides memory billingOverrides) 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 setUpkeepTriggerConfig(uint256 id, bytes memory triggerConfig) external; + function simulatePerformUpkeep( + uint256 id, + bytes memory performData + ) external view returns (bool success, uint256 gasUsed); + function transferUpkeepAdmin(uint256 id, address proposed) external; + function unpauseUpkeep(uint256 id) external; + function withdrawERC20Fees(address asset, address to, uint256 amount) external; + function withdrawFunds(uint256 id, address to) external; + function withdrawLink(address to, uint256 amount) external; function acceptPayeeship(address transmitter) external; - function acceptUpkeepAdmin(uint256 id) external; + function disableOffchainPayments() external; function getActiveUpkeepIDs(uint256 startIndex, uint256 maxCount) external view returns (uint256[] memory); function getAdminPrivilegeConfig(address admin) external view returns (bytes memory); function getAllowedReadOnlyAddress() external view returns (address); function getAutomationForwarderLogic() external view returns (address); function getBalance(uint256 id) external view returns (uint96 balance); + function getBillingToken(uint256 upkeepID) external view returns (address); function getBillingTokenConfig(address token) external view returns (AutomationRegistryBase2_3.BillingConfig memory); function getBillingTokens() external view returns (address[] memory); function getCancellationDelay() external pure returns (uint256); function getChainModule() external view returns (address chainModule); function getConditionalGasOverhead() external pure returns (uint256); + function getConfig() external view returns (AutomationRegistryBase2_3.OnchainConfig memory); function getFallbackNativePrice() external view returns (uint256); function getFastGasFeedAddress() external view returns (address); function getForwarder(uint256 upkeepID) external view returns (address); + function getHotVars() external view returns (AutomationRegistryBase2_3.HotVars memory); function getLinkAddress() external view returns (address); function getLinkUSDFeedAddress() external view returns (address); function getLogGasOverhead() external pure returns (uint256); - function getMaxPaymentForGas(uint8 triggerType, uint32 gasLimit) external view returns (uint96 maxPayment); + function getMaxPaymentForGas( + uint256 id, + uint8 triggerType, + uint32 gasLimit, + address billingToken + ) external view returns (uint96 maxPayment); function getMinBalance(uint256 id) external view returns (uint96); function getMinBalanceForUpkeep(uint256 id) external view returns (uint96 minBalance); function getNativeUSDFeedAddress() external view returns (address); + function getNumUpkeeps() external view returns (uint256); + function getPayoutMode() 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 getReorgProtectionEnabled() external view returns (bool reorgProtectionEnabled); + function getReserveAmount(address billingToken) external view returns (uint256); function getSignerInfo(address query) external view returns (bool active, uint8 index); function getState() external view returns ( - AutomationRegistryBase2_3.State memory state, - AutomationRegistryBase2_3.OnchainConfigLegacy memory config, + IAutomationV21PlusCommon.StateLegacy memory state, + IAutomationV21PlusCommon.OnchainConfigLegacy memory config, address[] memory signers, address[] memory transmitters, uint8 f ); + function getStorage() external view returns (AutomationRegistryBase2_3.Storage memory); function getTransmitCalldataFixedBytesOverhead() external pure returns (uint256); function getTransmitCalldataPerSignerBytesOverhead() external pure returns (uint256); 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 (AutomationRegistryBase2_3.UpkeepInfo memory upkeepInfo); + function getUpkeep(uint256 id) external view returns (IAutomationV21PlusCommon.UpkeepInfoLegacy memory upkeepInfo); function getUpkeepPrivilegeConfig(uint256 upkeepId) external view returns (bytes memory); function getUpkeepTriggerConfig(uint256 upkeepId) external view returns (bytes memory); + function getWrappedNativeTokenAddress() external view returns (address); function hasDedupKey(bytes32 dedupKey) external view returns (bool); - function linkAvailableForPayment() external view returns (uint256); + function linkAvailableForPayment() external view returns (int256); function pause() external; - function pauseUpkeep(uint256 id) 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 settleNOPsOffchain() external; + function supportsBillingToken(address token) external view returns (bool); 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 withdrawERC20Fees(address assetAddress, address to, uint256 amount) external; - function withdrawFunds(uint256 id, address to) external; - function withdrawLinkFees(address to, uint256 amount) external; function withdrawPayment(address from, address to) external; } interface AutomationRegistryBase2_3 { + struct BillingOverrides { + uint32 gasFeePPB; + uint24 flatFeeMilliCents; + } + struct BillingConfig { uint32 gasFeePPB; - uint24 flatFeeMicroLink; + uint24 flatFeeMilliCents; address priceFeed; + uint256 fallbackPrice; + uint96 minSpend; } struct OnchainConfig { - uint32 paymentPremiumPPB; - uint32 flatFeeMicroLink; uint32 checkGasLimit; - uint24 stalenessSeconds; - uint16 gasCeilingMultiplier; - uint96 minUpkeepSpend; uint32 maxPerformGas; uint32 maxCheckDataSize; + address transcoder; + bool reorgProtectionEnabled; + uint24 stalenessSeconds; uint32 maxPerformDataSize; uint32 maxRevertDataSize; + address upkeepPrivilegeManager; + uint16 gasCeilingMultiplier; + address financeAdmin; uint256 fallbackGasPrice; uint256 fallbackLinkPrice; uint256 fallbackNativePrice; - address transcoder; address[] registrars; - address upkeepPrivilegeManager; address chainModule; + } + + struct HotVars { + uint96 totalPremium; + uint32 latestEpoch; + uint24 stalenessSeconds; + uint16 gasCeilingMultiplier; + uint8 f; + bool paused; + bool reentrancyGuard; bool reorgProtectionEnabled; + address chainModule; + } + + struct Storage { + address transcoder; + uint32 checkGasLimit; + uint32 maxPerformGas; + uint32 nonce; + address upkeepPrivilegeManager; + uint32 configCount; + uint32 latestConfigBlockNumber; + uint32 maxCheckDataSize; address financeAdmin; + uint32 maxPerformDataSize; + uint32 maxRevertDataSize; } +} - struct State { +interface IAutomationV21PlusCommon { + struct StateLegacy { uint32 nonce; uint96 ownerLinkBalance; uint256 expectedLinkBalance; @@ -340,7 +388,7 @@ interface AutomationRegistryBase2_3 { address upkeepPrivilegeManager; } - struct UpkeepInfo { + struct UpkeepInfoLegacy { address target; uint32 performGas; bytes checkData; @@ -356,5 +404,5 @@ interface AutomationRegistryBase2_3 { // THIS FILE WAS AUTOGENERATED FROM THE FOLLOWING ABI JSON: /* -[{"inputs":[{"internalType":"contract AutomationRegistryLogicB2_3","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":[{"internalType":"uint256","name":"available","type":"uint256"},{"internalType":"uint256","name":"requested","type":"uint256"}],"name":"InsufficientBalance","type":"error"},{"inputs":[],"name":"InvalidDataLength","type":"error"},{"inputs":[],"name":"InvalidFeed","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":"OnlyFinanceAdmin","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":"TransferFailed","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"},{"inputs":[],"name":"ZeroAddressNotAllowed","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":"contract IERC20","name":"token","type":"address"},{"components":[{"internalType":"uint32","name":"gasFeePPB","type":"uint32"},{"internalType":"uint24","name":"flatFeeMicroLink","type":"uint24"},{"internalType":"address","name":"priceFeed","type":"address"}],"indexed":false,"internalType":"struct AutomationRegistryBase2_3.BillingConfig","name":"config","type":"tuple"}],"name":"BillingConfigSet","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":"address","name":"newModule","type":"address"}],"name":"ChainSpecificModuleUpdated","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":"address","name":"recipient","type":"address"},{"indexed":true,"internalType":"address","name":"assetAddress","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"FeesWithdrawn","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":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":"uint256","name":"fallbackNativePrice","type":"uint256"},{"internalType":"address","name":"transcoder","type":"address"},{"internalType":"address[]","name":"registrars","type":"address[]"},{"internalType":"address","name":"upkeepPrivilegeManager","type":"address"},{"internalType":"contract IChainModule","name":"chainModule","type":"address"},{"internalType":"bool","name":"reorgProtectionEnabled","type":"bool"},{"internalType":"address","name":"financeAdmin","type":"address"}],"internalType":"struct AutomationRegistryBase2_3.OnchainConfig","name":"onchainConfig","type":"tuple"},{"internalType":"uint64","name":"offchainConfigVersion","type":"uint64"},{"internalType":"bytes","name":"offchainConfig","type":"bytes"},{"internalType":"contract IERC20[]","name":"billingTokens","type":"address[]"},{"components":[{"internalType":"uint32","name":"gasFeePPB","type":"uint32"},{"internalType":"uint24","name":"flatFeeMicroLink","type":"uint24"},{"internalType":"address","name":"priceFeed","type":"address"}],"internalType":"struct AutomationRegistryBase2_3.BillingConfig[]","name":"billingConfigs","type":"tuple[]"}],"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 AutomationRegistryLogicB2_3","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 AutomationRegistryBase2_3.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 AutomationRegistryBase2_3.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":"linkUSD","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 AutomationRegistryBase2_3.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":"linkUSD","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 AutomationRegistryBase2_3.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 AutomationRegistryBase2_3.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":"address","name":"link","type":"address"},{"internalType":"address","name":"linkUSDFeed","type":"address"},{"internalType":"address","name":"nativeUSDFeed","type":"address"},{"internalType":"address","name":"fastGasFeed","type":"address"},{"internalType":"address","name":"automationForwarderLogic","type":"address"},{"internalType":"address","name":"allowedReadOnlyAddress","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":"getAllowedReadOnlyAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"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":[{"internalType":"contract IERC20","name":"token","type":"address"}],"name":"getBillingTokenConfig","outputs":[{"components":[{"internalType":"uint32","name":"gasFeePPB","type":"uint32"},{"internalType":"uint24","name":"flatFeeMicroLink","type":"uint24"},{"internalType":"address","name":"priceFeed","type":"address"}],"internalType":"struct AutomationRegistryBase2_3.BillingConfig","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getBillingTokens","outputs":[{"internalType":"contract IERC20[]","name":"","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCancellationDelay","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"getChainModule","outputs":[{"internalType":"contract IChainModule","name":"chainModule","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getConditionalGasOverhead","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"getFallbackNativePrice","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","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":"getLinkUSDFeedAddress","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 AutomationRegistryBase2_3.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":"getNativeUSDFeedAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"peer","type":"address"}],"name":"getPeerRegistryMigrationPermission","outputs":[{"internalType":"enum AutomationRegistryBase2_3.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":[],"name":"getReorgProtectionEnabled","outputs":[{"internalType":"bool","name":"reorgProtectionEnabled","type":"bool"}],"stateMutability":"view","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 AutomationRegistryBase2_3.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 AutomationRegistryBase2_3.OnchainConfigLegacy","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":[],"name":"getTransmitCalldataFixedBytesOverhead","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"getTransmitCalldataPerSignerBytesOverhead","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","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 AutomationRegistryBase2_3.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 AutomationRegistryBase2_3.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":"linkAvailableForPayment","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"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":[{"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 AutomationRegistryBase2_3.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":"address","name":"assetAddress","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"withdrawERC20Fees","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"address","name":"to","type":"address"}],"name":"withdrawFunds","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"withdrawLinkFees","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"}] +[{"inputs":[{"internalType":"contract AutomationRegistryLogicA2_3","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":[{"internalType":"uint256","name":"available","type":"uint256"},{"internalType":"uint256","name":"requested","type":"uint256"}],"name":"InsufficientBalance","type":"error"},{"inputs":[],"name":"InsufficientLinkLiquidity","type":"error"},{"inputs":[],"name":"InvalidDataLength","type":"error"},{"inputs":[],"name":"InvalidFeed","type":"error"},{"inputs":[],"name":"InvalidPayee","type":"error"},{"inputs":[],"name":"InvalidRecipient","type":"error"},{"inputs":[],"name":"InvalidReport","type":"error"},{"inputs":[],"name":"InvalidSigner","type":"error"},{"inputs":[],"name":"InvalidToken","type":"error"},{"inputs":[],"name":"InvalidTransmitter","type":"error"},{"inputs":[],"name":"InvalidTrigger","type":"error"},{"inputs":[],"name":"InvalidTriggerType","type":"error"},{"inputs":[],"name":"MigrationNotPermitted","type":"error"},{"inputs":[],"name":"MustSettleOffchain","type":"error"},{"inputs":[],"name":"MustSettleOnchain","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":"OnlyFinanceAdmin","type":"error"},{"inputs":[],"name":"OnlyPausedUpkeep","type":"error"},{"inputs":[],"name":"OnlySimulatedBackend","type":"error"},{"inputs":[],"name":"OnlyUnpausedUpkeep","type":"error"},{"inputs":[],"name":"ParameterLengthError","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":"TransferFailed","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"},{"inputs":[],"name":"ZeroAddressNotAllowed","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"},{"components":[{"internalType":"uint32","name":"gasFeePPB","type":"uint32"},{"internalType":"uint24","name":"flatFeeMilliCents","type":"uint24"}],"indexed":false,"internalType":"struct AutomationRegistryBase2_3.BillingOverrides","name":"overrides","type":"tuple"}],"name":"BillingConfigOverridden","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"}],"name":"BillingConfigOverrideRemoved","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"contract IERC20","name":"token","type":"address"},{"components":[{"internalType":"uint32","name":"gasFeePPB","type":"uint32"},{"internalType":"uint24","name":"flatFeeMilliCents","type":"uint24"},{"internalType":"contract AggregatorV3Interface","name":"priceFeed","type":"address"},{"internalType":"uint256","name":"fallbackPrice","type":"uint256"},{"internalType":"uint96","name":"minSpend","type":"uint96"}],"indexed":false,"internalType":"struct AutomationRegistryBase2_3.BillingConfig","name":"config","type":"tuple"}],"name":"BillingConfigSet","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":"address","name":"newModule","type":"address"}],"name":"ChainSpecificModuleUpdated","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":"address","name":"assetAddress","type":"address"},{"indexed":true,"internalType":"address","name":"recipient","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"FeesWithdrawn","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":"address[]","name":"payees","type":"address[]"},{"indexed":false,"internalType":"uint256[]","name":"payments","type":"uint256[]"}],"name":"NOPsSettledOffchain","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":"payable","type":"fallback"},{"inputs":[],"name":"acceptOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"uint96","name":"amount","type":"uint96"}],"name":"addFunds","outputs":[],"stateMutability":"payable","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":"checkGasLimit","type":"uint32"},{"internalType":"uint32","name":"maxPerformGas","type":"uint32"},{"internalType":"uint32","name":"maxCheckDataSize","type":"uint32"},{"internalType":"address","name":"transcoder","type":"address"},{"internalType":"bool","name":"reorgProtectionEnabled","type":"bool"},{"internalType":"uint24","name":"stalenessSeconds","type":"uint24"},{"internalType":"uint32","name":"maxPerformDataSize","type":"uint32"},{"internalType":"uint32","name":"maxRevertDataSize","type":"uint32"},{"internalType":"address","name":"upkeepPrivilegeManager","type":"address"},{"internalType":"uint16","name":"gasCeilingMultiplier","type":"uint16"},{"internalType":"address","name":"financeAdmin","type":"address"},{"internalType":"uint256","name":"fallbackGasPrice","type":"uint256"},{"internalType":"uint256","name":"fallbackLinkPrice","type":"uint256"},{"internalType":"uint256","name":"fallbackNativePrice","type":"uint256"},{"internalType":"address[]","name":"registrars","type":"address[]"},{"internalType":"contract IChainModule","name":"chainModule","type":"address"}],"internalType":"struct AutomationRegistryBase2_3.OnchainConfig","name":"onchainConfig","type":"tuple"},{"internalType":"uint64","name":"offchainConfigVersion","type":"uint64"},{"internalType":"bytes","name":"offchainConfig","type":"bytes"},{"internalType":"contract IERC20[]","name":"billingTokens","type":"address[]"},{"components":[{"internalType":"uint32","name":"gasFeePPB","type":"uint32"},{"internalType":"uint24","name":"flatFeeMilliCents","type":"uint24"},{"internalType":"contract AggregatorV3Interface","name":"priceFeed","type":"address"},{"internalType":"uint256","name":"fallbackPrice","type":"uint256"},{"internalType":"uint96","name":"minSpend","type":"uint96"}],"internalType":"struct AutomationRegistryBase2_3.BillingConfig[]","name":"billingConfigs","type":"tuple[]"}],"name":"setConfigTypeSafe","outputs":[],"stateMutability":"nonpayable","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 AutomationRegistryLogicB2_3","name":"logicB","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"cancelUpkeep","outputs":[],"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 AutomationRegistryBase2_3.Trigger","name":"triggerType","type":"uint8"},{"internalType":"contract IERC20","name":"billingToken","type":"address"},{"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":"contract AutomationRegistryLogicC2_3","name":"logicC","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"acceptUpkeepAdmin","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 AutomationRegistryBase2_3.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 AutomationRegistryBase2_3.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":"linkUSD","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 AutomationRegistryBase2_3.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":"linkUSD","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 AutomationRegistryBase2_3.UpkeepFailureReason","name":"upkeepFailureReason","type":"uint8"},{"internalType":"uint256","name":"gasUsed","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"pauseUpkeep","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"removeBillingOverrides","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"components":[{"internalType":"uint32","name":"gasFeePPB","type":"uint32"},{"internalType":"uint24","name":"flatFeeMilliCents","type":"uint24"}],"internalType":"struct AutomationRegistryBase2_3.BillingOverrides","name":"billingOverrides","type":"tuple"}],"name":"setBillingOverrides","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":"id","type":"uint256"},{"internalType":"bytes","name":"triggerConfig","type":"bytes"}],"name":"setUpkeepTriggerConfig","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":"uint256","name":"id","type":"uint256"},{"internalType":"address","name":"proposed","type":"address"}],"name":"transferUpkeepAdmin","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"unpauseUpkeep","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"contract IERC20","name":"asset","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"withdrawERC20Fees","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"address","name":"to","type":"address"}],"name":"withdrawFunds","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"withdrawLink","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"link","type":"address"},{"internalType":"address","name":"linkUSDFeed","type":"address"},{"internalType":"address","name":"nativeUSDFeed","type":"address"},{"internalType":"address","name":"fastGasFeed","type":"address"},{"internalType":"address","name":"automationForwarderLogic","type":"address"},{"internalType":"address","name":"allowedReadOnlyAddress","type":"address"},{"internalType":"enum AutomationRegistryBase2_3.PayoutMode","name":"payoutMode","type":"uint8"},{"internalType":"address","name":"wrappedNativeTokenAddress","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"address","name":"transmitter","type":"address"}],"name":"acceptPayeeship","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"disableOffchainPayments","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":"getAllowedReadOnlyAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"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":[{"internalType":"uint256","name":"upkeepID","type":"uint256"}],"name":"getBillingToken","outputs":[{"internalType":"contract IERC20","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"contract IERC20","name":"token","type":"address"}],"name":"getBillingTokenConfig","outputs":[{"components":[{"internalType":"uint32","name":"gasFeePPB","type":"uint32"},{"internalType":"uint24","name":"flatFeeMilliCents","type":"uint24"},{"internalType":"contract AggregatorV3Interface","name":"priceFeed","type":"address"},{"internalType":"uint256","name":"fallbackPrice","type":"uint256"},{"internalType":"uint96","name":"minSpend","type":"uint96"}],"internalType":"struct AutomationRegistryBase2_3.BillingConfig","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getBillingTokens","outputs":[{"internalType":"contract IERC20[]","name":"","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCancellationDelay","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"getChainModule","outputs":[{"internalType":"contract IChainModule","name":"chainModule","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getConditionalGasOverhead","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"getConfig","outputs":[{"components":[{"internalType":"uint32","name":"checkGasLimit","type":"uint32"},{"internalType":"uint32","name":"maxPerformGas","type":"uint32"},{"internalType":"uint32","name":"maxCheckDataSize","type":"uint32"},{"internalType":"address","name":"transcoder","type":"address"},{"internalType":"bool","name":"reorgProtectionEnabled","type":"bool"},{"internalType":"uint24","name":"stalenessSeconds","type":"uint24"},{"internalType":"uint32","name":"maxPerformDataSize","type":"uint32"},{"internalType":"uint32","name":"maxRevertDataSize","type":"uint32"},{"internalType":"address","name":"upkeepPrivilegeManager","type":"address"},{"internalType":"uint16","name":"gasCeilingMultiplier","type":"uint16"},{"internalType":"address","name":"financeAdmin","type":"address"},{"internalType":"uint256","name":"fallbackGasPrice","type":"uint256"},{"internalType":"uint256","name":"fallbackLinkPrice","type":"uint256"},{"internalType":"uint256","name":"fallbackNativePrice","type":"uint256"},{"internalType":"address[]","name":"registrars","type":"address[]"},{"internalType":"contract IChainModule","name":"chainModule","type":"address"}],"internalType":"struct AutomationRegistryBase2_3.OnchainConfig","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getFallbackNativePrice","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","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":"getHotVars","outputs":[{"components":[{"internalType":"uint96","name":"totalPremium","type":"uint96"},{"internalType":"uint32","name":"latestEpoch","type":"uint32"},{"internalType":"uint24","name":"stalenessSeconds","type":"uint24"},{"internalType":"uint16","name":"gasCeilingMultiplier","type":"uint16"},{"internalType":"uint8","name":"f","type":"uint8"},{"internalType":"bool","name":"paused","type":"bool"},{"internalType":"bool","name":"reentrancyGuard","type":"bool"},{"internalType":"bool","name":"reorgProtectionEnabled","type":"bool"},{"internalType":"contract IChainModule","name":"chainModule","type":"address"}],"internalType":"struct AutomationRegistryBase2_3.HotVars","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLinkAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLinkUSDFeedAddress","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":"uint256","name":"id","type":"uint256"},{"internalType":"enum AutomationRegistryBase2_3.Trigger","name":"triggerType","type":"uint8"},{"internalType":"uint32","name":"gasLimit","type":"uint32"},{"internalType":"contract IERC20","name":"billingToken","type":"address"}],"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":"getNativeUSDFeedAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getNumUpkeeps","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getPayoutMode","outputs":[{"internalType":"enum AutomationRegistryBase2_3.PayoutMode","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"peer","type":"address"}],"name":"getPeerRegistryMigrationPermission","outputs":[{"internalType":"enum AutomationRegistryBase2_3.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":[],"name":"getReorgProtectionEnabled","outputs":[{"internalType":"bool","name":"reorgProtectionEnabled","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"contract IERC20","name":"billingToken","type":"address"}],"name":"getReserveAmount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","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 IAutomationV21PlusCommon.StateLegacy","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 IAutomationV21PlusCommon.OnchainConfigLegacy","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":[],"name":"getStorage","outputs":[{"components":[{"internalType":"address","name":"transcoder","type":"address"},{"internalType":"uint32","name":"checkGasLimit","type":"uint32"},{"internalType":"uint32","name":"maxPerformGas","type":"uint32"},{"internalType":"uint32","name":"nonce","type":"uint32"},{"internalType":"address","name":"upkeepPrivilegeManager","type":"address"},{"internalType":"uint32","name":"configCount","type":"uint32"},{"internalType":"uint32","name":"latestConfigBlockNumber","type":"uint32"},{"internalType":"uint32","name":"maxCheckDataSize","type":"uint32"},{"internalType":"address","name":"financeAdmin","type":"address"},{"internalType":"uint32","name":"maxPerformDataSize","type":"uint32"},{"internalType":"uint32","name":"maxRevertDataSize","type":"uint32"}],"internalType":"struct AutomationRegistryBase2_3.Storage","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getTransmitCalldataFixedBytesOverhead","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"getTransmitCalldataPerSignerBytesOverhead","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","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 AutomationRegistryBase2_3.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 IAutomationV21PlusCommon.UpkeepInfoLegacy","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":[],"name":"getWrappedNativeTokenAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"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":"linkAvailableForPayment","outputs":[{"internalType":"int256","name":"","type":"int256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"pause","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 AutomationRegistryBase2_3.MigrationPermission","name":"permission","type":"uint8"}],"name":"setPeerRegistryMigrationPermission","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":[],"name":"settleNOPsOffchain","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"contract IERC20","name":"token","type":"address"}],"name":"supportsBillingToken","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"transmitter","type":"address"},{"internalType":"address","name":"proposed","type":"address"}],"name":"transferPayeeship","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"unpause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"upkeepVersion","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"pure","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/interfaces/v2_3/IWrappedNative.sol b/contracts/src/v0.8/automation/dev/interfaces/v2_3/IWrappedNative.sol new file mode 100644 index 00000000..5b03b2ef --- /dev/null +++ b/contracts/src/v0.8/automation/dev/interfaces/v2_3/IWrappedNative.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IERC20} from "../../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +interface IWrappedNative is IERC20 { + function deposit() external payable; + + function withdraw(uint256 wad) external; +} diff --git a/contracts/src/v0.8/automation/dev/test/AutomationRegistrar2_3.t.sol b/contracts/src/v0.8/automation/dev/test/AutomationRegistrar2_3.t.sol new file mode 100644 index 00000000..850d2955 --- /dev/null +++ b/contracts/src/v0.8/automation/dev/test/AutomationRegistrar2_3.t.sol @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.19; + +import {BaseTest} from "./BaseTest.t.sol"; +import {IAutomationRegistryMaster2_3} from "../interfaces/v2_3/IAutomationRegistryMaster2_3.sol"; +import {AutomationRegistrar2_3} from "../v2_3/AutomationRegistrar2_3.sol"; +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {AutomationRegistryBase2_3 as AutoBase} from "../v2_3/AutomationRegistryBase2_3.sol"; +import {IWrappedNative} from "../interfaces/v2_3/IWrappedNative.sol"; + +// forge test --match-path src/v0.8/automation/dev/test/AutomationRegistrar2_3.t.sol + +contract SetUp is BaseTest { + IAutomationRegistryMaster2_3 internal registry; + AutomationRegistrar2_3 internal registrar; + + function setUp() public override { + super.setUp(); + (registry, registrar) = deployAndConfigureRegistryAndRegistrar(AutoBase.PayoutMode.ON_CHAIN); + vm.stopPrank(); // reset identity at the start of each test + } +} + +contract RegisterUpkeep is SetUp { + function testLink_autoApproveOff_happy() external { + vm.startPrank(UPKEEP_ADMIN); + + uint96 amount = uint96(registrar.getMinimumRegistrationAmount(IERC20(address(linkToken)))); + linkToken.approve(address(registrar), amount); + + registrar.registerUpkeep( + AutomationRegistrar2_3.RegistrationParams({ + upkeepContract: address(TARGET1), + amount: amount, + adminAddress: UPKEEP_ADMIN, + gasLimit: 10_000, + triggerType: 0, + billingToken: IERC20(address(linkToken)), + name: "foobar", + encryptedEmail: "", + checkData: bytes("check data"), + triggerConfig: "", + offchainConfig: "" + }) + ); + + assertEq(linkToken.balanceOf(address(registrar)), amount); + assertEq(registry.getNumUpkeeps(), 0); + } + + function testUSDToken_autoApproveOff_happy() external { + vm.startPrank(UPKEEP_ADMIN); + + uint96 amount = uint96(registrar.getMinimumRegistrationAmount(usdToken)); + usdToken.approve(address(registrar), amount); + + registrar.registerUpkeep( + AutomationRegistrar2_3.RegistrationParams({ + upkeepContract: address(TARGET1), + amount: amount, + adminAddress: UPKEEP_ADMIN, + gasLimit: 10_000, + triggerType: 0, + billingToken: usdToken, + name: "foobar", + encryptedEmail: "", + checkData: bytes("check data"), + triggerConfig: "", + offchainConfig: "" + }) + ); + + assertEq(usdToken.balanceOf(address(registrar)), amount); + assertEq(registry.getNumUpkeeps(), 0); + } + + function testLink_autoApproveOn_happy() external { + registrar.setTriggerConfig(0, AutomationRegistrar2_3.AutoApproveType.ENABLED_ALL, 1000); + + vm.startPrank(UPKEEP_ADMIN); + uint96 amount = uint96(registrar.getMinimumRegistrationAmount(IERC20(address(linkToken)))); + linkToken.approve(address(registrar), amount); + + registrar.registerUpkeep( + AutomationRegistrar2_3.RegistrationParams({ + upkeepContract: address(TARGET1), + amount: amount, + adminAddress: UPKEEP_ADMIN, + gasLimit: 10_000, + triggerType: 0, + billingToken: IERC20(address(linkToken)), + name: "foobar", + encryptedEmail: "", + checkData: bytes("check data"), + triggerConfig: "", + offchainConfig: "" + }) + ); + + assertEq(linkToken.balanceOf(address(registrar)), 0); + assertEq(linkToken.balanceOf(address(registry)), amount); + assertEq(registry.getNumUpkeeps(), 1); + } + + function testUSDToken_autoApproveOn_happy() external { + registrar.setTriggerConfig(0, AutomationRegistrar2_3.AutoApproveType.ENABLED_ALL, 1000); + + vm.startPrank(UPKEEP_ADMIN); + uint96 amount = uint96(registrar.getMinimumRegistrationAmount(usdToken)); + usdToken.approve(address(registrar), amount); + + registrar.registerUpkeep( + AutomationRegistrar2_3.RegistrationParams({ + upkeepContract: address(TARGET1), + amount: amount, + adminAddress: UPKEEP_ADMIN, + gasLimit: 10_000, + triggerType: 0, + billingToken: usdToken, + name: "foobar", + encryptedEmail: "", + checkData: bytes("check data"), + triggerConfig: "", + offchainConfig: "" + }) + ); + + assertEq(usdToken.balanceOf(address(registrar)), 0); + assertEq(usdToken.balanceOf(address(registry)), amount); + assertEq(registry.getNumUpkeeps(), 1); + } + + function testNative_autoApproveOn_happy() external { + registrar.setTriggerConfig(0, AutomationRegistrar2_3.AutoApproveType.ENABLED_ALL, 1000); + + vm.startPrank(UPKEEP_ADMIN); + uint96 amount = uint96(registrar.getMinimumRegistrationAmount(IERC20(address(weth)))); + IWrappedNative(address(weth)).approve(address(registrar), amount); + + registrar.registerUpkeep{value: amount}( + AutomationRegistrar2_3.RegistrationParams({ + upkeepContract: address(TARGET1), + amount: 0, + adminAddress: UPKEEP_ADMIN, + gasLimit: 10_000, + triggerType: 0, + billingToken: IERC20(address(weth)), + name: "foobar", + encryptedEmail: "", + checkData: bytes("check data"), + triggerConfig: "", + offchainConfig: "" + }) + ); + + assertEq(weth.balanceOf(address(registrar)), 0); + assertEq(weth.balanceOf(address(registry)), amount); + assertEq(registry.getNumUpkeeps(), 1); + } + + // when msg.value is 0, it uses the ERC20 payment path + function testNative_autoApproveOff_msgValue0() external { + vm.startPrank(UPKEEP_ADMIN); + + uint96 amount = uint96(registrar.getMinimumRegistrationAmount(IERC20(address(weth)))); + IWrappedNative(address(weth)).approve(address(registrar), amount); + + registrar.registerUpkeep( + AutomationRegistrar2_3.RegistrationParams({ + upkeepContract: address(TARGET1), + amount: amount, + adminAddress: UPKEEP_ADMIN, + gasLimit: 10_000, + triggerType: 0, + billingToken: IERC20(address(weth)), + name: "foobar", + encryptedEmail: "", + checkData: bytes("check data"), + triggerConfig: "", + offchainConfig: "" + }) + ); + + assertEq(weth.balanceOf(address(registrar)), amount); + assertEq(registry.getNumUpkeeps(), 0); + } + + // when msg.value is not 0, it uses the native payment path + function testNative_autoApproveOff_msgValueNot0() external { + vm.startPrank(UPKEEP_ADMIN); + + uint96 amount = uint96(registrar.getMinimumRegistrationAmount(IERC20(address(weth)))); + IWrappedNative(address(weth)).approve(address(registrar), amount); + + registrar.registerUpkeep{value: amount}( + AutomationRegistrar2_3.RegistrationParams({ + upkeepContract: address(TARGET1), + amount: 0, + adminAddress: UPKEEP_ADMIN, + gasLimit: 10_000, + triggerType: 0, + billingToken: IERC20(address(weth)), + name: "foobar", + encryptedEmail: "", + checkData: bytes("check data"), + triggerConfig: "", + offchainConfig: "" + }) + ); + + assertEq(weth.balanceOf(address(registrar)), amount); + assertEq(registry.getNumUpkeeps(), 0); + } +} diff --git a/contracts/src/v0.8/automation/dev/test/AutomationRegistry2_3.t.sol b/contracts/src/v0.8/automation/dev/test/AutomationRegistry2_3.t.sol index 24a4dada..1f8fa42f 100644 --- a/contracts/src/v0.8/automation/dev/test/AutomationRegistry2_3.t.sol +++ b/contracts/src/v0.8/automation/dev/test/AutomationRegistry2_3.t.sol @@ -1,241 +1,298 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.19; -import {AutomationForwarderLogic} from "../../AutomationForwarderLogic.sol"; +import {Vm} from "forge-std/Test.sol"; import {BaseTest} from "./BaseTest.t.sol"; -import {AutomationRegistry2_3} from "../v2_3/AutomationRegistry2_3.sol"; -import {AutomationRegistryLogicA2_3} from "../v2_3/AutomationRegistryLogicA2_3.sol"; -import {AutomationRegistryLogicB2_3} from "../v2_3/AutomationRegistryLogicB2_3.sol"; -import {IAutomationRegistryMaster2_3, AutomationRegistryBase2_3} from "../interfaces/v2_3/IAutomationRegistryMaster2_3.sol"; +import {AutomationRegistryBase2_3 as AutoBase} from "../v2_3/AutomationRegistryBase2_3.sol"; +import {AutomationRegistrar2_3 as Registrar} from "../v2_3/AutomationRegistrar2_3.sol"; +import {IAutomationRegistryMaster2_3 as Registry, AutomationRegistryBase2_3} from "../interfaces/v2_3/IAutomationRegistryMaster2_3.sol"; import {ChainModuleBase} from "../../chains/ChainModuleBase.sol"; -import {MockV3Aggregator} from "../../../tests/MockV3Aggregator.sol"; -import {ERC20Mock} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/mocks/ERC20Mock.sol"; - -contract AutomationRegistry2_3_SetUp is BaseTest { - address internal LINK_USD_FEED; - address internal NATIVE_USD_FEED; - address internal FAST_GAS_FEED; - address internal constant FINANCE_ADMIN_ADDRESS = 0x1111111111111111111111111111111111111114; - address internal constant ZERO_ADDRESS = address(0); - address internal constant UPKEEP_ADMIN = address(uint160(uint256(keccak256("ADMIN")))); - - // Signer private keys used for these test - uint256 internal constant PRIVATE0 = 0x7b2e97fe057e6de99d6872a2ef2abf52c9b4469bc848c2465ac3fcd8d336e81d; - uint256 internal constant PRIVATE1 = 0xab56160806b05ef1796789248e1d7f34a6465c5280899159d645218cd216cee6; - uint256 internal constant PRIVATE2 = 0x6ec7caa8406a49b76736602810e0a2871959fbbb675e23a8590839e4717f1f7f; - uint256 internal constant PRIVATE3 = 0x80f14b11da94ae7f29d9a7713ea13dc838e31960a5c0f2baf45ed458947b730a; - - uint64 internal constant OFFCHAIN_CONFIG_VERSION = 30; // 2 for OCR2 - uint8 internal constant F = 1; - - address[] internal s_valid_signers; - address[] internal s_valid_transmitters; - address[] internal s_registrars; - - IAutomationRegistryMaster2_3 internal registryMaster; - ERC20Mock internal link; // the link token - ERC20Mock internal mockERC20; // the supported ERC20 tokens except link +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {IWrappedNative} from "../interfaces/v2_3/IWrappedNative.sol"; - function setUp() public override { - LINK_USD_FEED = address(new MockV3Aggregator(8, 2_000_000_000)); // $20 - NATIVE_USD_FEED = address(new MockV3Aggregator(8, 400_000_000_000)); // $4,000 - FAST_GAS_FEED = address(new MockV3Aggregator(0, 1_000_000_000)); // 1 gwei - - link = new ERC20Mock("LINK", "LINK", UPKEEP_ADMIN, 0); - mockERC20 = new ERC20Mock("MOCK_ERC20", "MOCK_ERC20", UPKEEP_ADMIN, 0); +// forge test --match-path src/v0.8/automation/dev/test/AutomationRegistry2_3.t.sol - s_valid_transmitters = new address[](4); - for (uint160 i = 0; i < 4; ++i) { - s_valid_transmitters[i] = address(4 + i); - } +enum Trigger { + CONDITION, + LOG +} - s_valid_signers = new address[](4); - s_valid_signers[0] = vm.addr(PRIVATE0); //0xc110458BE52CaA6bB68E66969C3218A4D9Db0211 - s_valid_signers[1] = vm.addr(PRIVATE1); //0xc110a19c08f1da7F5FfB281dc93630923F8E3719 - s_valid_signers[2] = vm.addr(PRIVATE2); //0xc110fdF6e8fD679C7Cc11602d1cd829211A18e9b - s_valid_signers[3] = vm.addr(PRIVATE3); //0xc11028017c9b445B6bF8aE7da951B5cC28B326C0 +contract SetUp is BaseTest { + Registry internal registry; + AutomationRegistryBase2_3.OnchainConfig internal config; + bytes internal constant offchainConfigBytes = abi.encode(1234, ZERO_ADDRESS); + + uint256 linkUpkeepID; + uint256 usdUpkeepID; + uint256 nativeUpkeepID; + + function setUp() public virtual override { + super.setUp(); + + (registry, ) = deployAndConfigureRegistryAndRegistrar(AutoBase.PayoutMode.ON_CHAIN); + config = registry.getConfig(); + + vm.startPrank(OWNER); + linkToken.approve(address(registry), type(uint256).max); + usdToken.approve(address(registry), type(uint256).max); + weth.approve(address(registry), type(uint256).max); + vm.startPrank(UPKEEP_ADMIN); + linkToken.approve(address(registry), type(uint256).max); + usdToken.approve(address(registry), type(uint256).max); + weth.approve(address(registry), type(uint256).max); + vm.startPrank(STRANGER); + linkToken.approve(address(registry), type(uint256).max); + usdToken.approve(address(registry), type(uint256).max); + weth.approve(address(registry), type(uint256).max); + vm.stopPrank(); - s_registrars = new address[](1); - s_registrars[0] = 0x3a0eDE26aa188BFE00b9A0C9A431A1a0CA5f7966; + linkUpkeepID = registry.registerUpkeep( + address(TARGET1), + config.maxPerformGas, + UPKEEP_ADMIN, + uint8(Trigger.CONDITION), + address(linkToken), + "", + "", + "" + ); - AutomationForwarderLogic forwarderLogic = new AutomationForwarderLogic(); - AutomationRegistryLogicB2_3 logicB2_3 = new AutomationRegistryLogicB2_3( - address(link), - LINK_USD_FEED, - NATIVE_USD_FEED, - FAST_GAS_FEED, - address(forwarderLogic), - ZERO_ADDRESS + usdUpkeepID = registry.registerUpkeep( + address(TARGET1), + config.maxPerformGas, + UPKEEP_ADMIN, + uint8(Trigger.CONDITION), + address(usdToken), + "", + "", + "" ); - AutomationRegistryLogicA2_3 logicA2_3 = new AutomationRegistryLogicA2_3(logicB2_3); - registryMaster = IAutomationRegistryMaster2_3( - address(new AutomationRegistry2_3(AutomationRegistryLogicB2_3(address(logicA2_3)))) + + nativeUpkeepID = registry.registerUpkeep( + address(TARGET1), + config.maxPerformGas, + UPKEEP_ADMIN, + uint8(Trigger.CONDITION), + address(weth), + "", + "", + "" ); + + vm.startPrank(OWNER); + registry.addFunds(linkUpkeepID, registry.getMinBalanceForUpkeep(linkUpkeepID)); + registry.addFunds(usdUpkeepID, registry.getMinBalanceForUpkeep(usdUpkeepID)); + registry.addFunds(nativeUpkeepID, registry.getMinBalanceForUpkeep(nativeUpkeepID)); + vm.stopPrank(); } } -contract AutomationRegistry2_3_LatestConfigDetails is AutomationRegistry2_3_SetUp { +contract LatestConfigDetails is SetUp { function testGet() public { - (uint32 configCount, uint32 blockNumber, bytes32 configDigest) = registryMaster.latestConfigDetails(); - assertEq(configCount, 0); - assertEq(blockNumber, 0); - assertEq(configDigest, ""); + (uint32 configCount, uint32 blockNumber, bytes32 configDigest) = registry.latestConfigDetails(); + assertEq(configCount, 1); + assertTrue(blockNumber > 0); + assertNotEq(configDigest, ""); } } -contract AutomationRegistry2_3_CheckUpkeep is AutomationRegistry2_3_SetUp { +contract CheckUpkeep is SetUp { function testPreventExecutionOnCheckUpkeep() public { uint256 id = 1; bytes memory triggerData = abi.encodePacked("trigger_data"); // The tx.origin is the DEFAULT_SENDER (0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38) of foundry // Expecting a revert since the tx.origin is not address(0) - vm.expectRevert(abi.encodeWithSelector(IAutomationRegistryMaster2_3.OnlySimulatedBackend.selector)); - registryMaster.checkUpkeep(id, triggerData); + vm.expectRevert(abi.encodeWithSelector(Registry.OnlySimulatedBackend.selector)); + registry.checkUpkeep(id, triggerData); } } -contract AutomationRegistry2_3_Withdraw is AutomationRegistry2_3_SetUp { - address internal aMockAddress = address(0x1111111111111111111111111111111111111113); +contract AddFunds is SetUp { + event FundsAdded(uint256 indexed id, address indexed from, uint96 amount); + + // when msg.value is 0, it uses the ERC20 payment path + function testNative_msgValue0() external { + vm.startPrank(OWNER); + uint256 startRegistryBalance = registry.getBalance(nativeUpkeepID); + uint256 startTokenBalance = registry.getBalance(nativeUpkeepID); + registry.addFunds(nativeUpkeepID, 1); + assertEq(registry.getBalance(nativeUpkeepID), startRegistryBalance + 1); + assertEq(weth.balanceOf(address(registry)), startTokenBalance + 1); + } - function mintLink(address recipient, uint256 amount) public { - vm.prank(UPKEEP_ADMIN); - //mint the link to the recipient - link.mint(recipient, amount); + // when msg.value is not 0, it uses the native payment path + function testNative_msgValueNot0() external { + uint256 startRegistryBalance = registry.getBalance(nativeUpkeepID); + uint256 startTokenBalance = registry.getBalance(nativeUpkeepID); + registry.addFunds{value: 1}(nativeUpkeepID, 1000); // parameter amount should be ignored + assertEq(registry.getBalance(nativeUpkeepID), startRegistryBalance + 1); + assertEq(weth.balanceOf(address(registry)), startTokenBalance + 1); + } + + // it fails when the billing token is not native, but trying to pay with native + function test_RevertsWhen_NativePaymentDoesntMatchBillingToken() external { + vm.expectRevert(abi.encodeWithSelector(Registry.InvalidToken.selector)); + registry.addFunds{value: 1}(linkUpkeepID, 0); + } + + function test_RevertsWhen_UpkeepDoesNotExist() public { + vm.expectRevert(Registry.UpkeepCancelled.selector); + registry.addFunds(randomNumber(), 1); } - function mintERC20(address recipient, uint256 amount) public { + function test_RevertsWhen_UpkeepIsCanceled() public { + registry.cancelUpkeep(linkUpkeepID); + vm.expectRevert(Registry.UpkeepCancelled.selector); + registry.addFunds(linkUpkeepID, 1); + } + + function test_anyoneCanAddFunds() public { + uint256 startAmount = registry.getBalance(linkUpkeepID); vm.prank(UPKEEP_ADMIN); - //mint the ERC20 to the recipient - mockERC20.mint(recipient, amount); + registry.addFunds(linkUpkeepID, 1); + assertEq(registry.getBalance(linkUpkeepID), startAmount + 1); + vm.prank(STRANGER); + registry.addFunds(linkUpkeepID, 1); + assertEq(registry.getBalance(linkUpkeepID), startAmount + 2); } - function setConfigForWithdraw() public { - address module = address(new ChainModuleBase()); - AutomationRegistryBase2_3.OnchainConfig memory cfg = AutomationRegistryBase2_3.OnchainConfig({ - paymentPremiumPPB: 10_000, - flatFeeMicroLink: 40_000, - checkGasLimit: 5_000_000, - stalenessSeconds: 90_000, - gasCeilingMultiplier: 0, - minUpkeepSpend: 0, - maxPerformGas: 10_000_000, - maxCheckDataSize: 5_000, - maxPerformDataSize: 5_000, - maxRevertDataSize: 5_000, - fallbackGasPrice: 20_000_000_000, - fallbackLinkPrice: 2_000_000_000, // $20 - fallbackNativePrice: 400_000_000_000, // $4,000 - transcoder: 0xB1e66855FD67f6e85F0f0fA38cd6fBABdf00923c, - registrars: s_registrars, - upkeepPrivilegeManager: 0xD9c855F08A7e460691F41bBDDe6eC310bc0593D8, - chainModule: module, - reorgProtectionEnabled: true, - financeAdmin: FINANCE_ADMIN_ADDRESS - }); - bytes memory offchainConfigBytes = abi.encode(1234, ZERO_ADDRESS); + function test_movesFundFromCorrectToken() public { + vm.startPrank(UPKEEP_ADMIN); + + uint256 startBalanceLINK = linkToken.balanceOf(address(registry)); + uint256 startBalanceUSDToken = usdToken.balanceOf(address(registry)); + uint256 startLinkUpkeepBalance = registry.getBalance(linkUpkeepID); + uint256 startUSDUpkeepBalance = registry.getBalance(usdUpkeepID); + + registry.addFunds(linkUpkeepID, 1); + assertEq(registry.getBalance(linkUpkeepID), startBalanceLINK + 1); + assertEq(registry.getBalance(usdUpkeepID), startBalanceUSDToken); + assertEq(linkToken.balanceOf(address(registry)), startLinkUpkeepBalance + 1); + assertEq(usdToken.balanceOf(address(registry)), startUSDUpkeepBalance); + + registry.addFunds(usdUpkeepID, 2); + assertEq(registry.getBalance(linkUpkeepID), startBalanceLINK + 1); + assertEq(registry.getBalance(usdUpkeepID), startBalanceUSDToken + 2); + assertEq(linkToken.balanceOf(address(registry)), startLinkUpkeepBalance + 1); + assertEq(usdToken.balanceOf(address(registry)), startUSDUpkeepBalance + 2); + } - registryMaster.setConfigTypeSafe( - s_valid_signers, - s_valid_transmitters, - F, - cfg, - OFFCHAIN_CONFIG_VERSION, - offchainConfigBytes, - new address[](0), - new AutomationRegistryBase2_3.BillingConfig[](0) - ); + function test_emitsAnEvent() public { + vm.startPrank(UPKEEP_ADMIN); + vm.expectEmit(); + emit FundsAdded(linkUpkeepID, address(UPKEEP_ADMIN), 100); + registry.addFunds(linkUpkeepID, 100); } +} + +contract Withdraw is SetUp { + address internal aMockAddress = randomAddress(); function testLinkAvailableForPaymentReturnsLinkBalance() public { + uint256 startBalance = linkToken.balanceOf(address(registry)); + int256 startLinkAvailable = registry.linkAvailableForPayment(); + //simulate a deposit of link to the liquidity pool - mintLink(address(registryMaster), 1e10); + _mintLink(address(registry), 1e10); //check there's a balance - assertGt(link.balanceOf(address(registryMaster)), 0); + assertEq(linkToken.balanceOf(address(registry)), startBalance + 1e10); - //check the link available for payment is the link balance - assertEq(registryMaster.linkAvailableForPayment(), link.balanceOf(address(registryMaster))); + //check the link available has increased by the same amount + assertEq(uint256(registry.linkAvailableForPayment()), uint256(startLinkAvailable) + 1e10); } - function testWithdrawLinkFeesRevertsBecauseOnlyFinanceAdminAllowed() public { - // set config with the finance admin - setConfigForWithdraw(); - - vm.expectRevert(abi.encodeWithSelector(IAutomationRegistryMaster2_3.OnlyFinanceAdmin.selector)); - registryMaster.withdrawLinkFees(aMockAddress, 1); + function testWithdrawLinkRevertsBecauseOnlyFinanceAdminAllowed() public { + vm.expectRevert(abi.encodeWithSelector(Registry.OnlyFinanceAdmin.selector)); + registry.withdrawLink(aMockAddress, 1); } - function testWithdrawLinkFeesRevertsBecauseOfInsufficientBalance() public { - // set config with the finance admin - setConfigForWithdraw(); - - vm.startPrank(FINANCE_ADMIN_ADDRESS); + function testWithdrawLinkRevertsBecauseOfInsufficientBalance() public { + vm.startPrank(FINANCE_ADMIN); // try to withdraw 1 link while there is 0 balance - vm.expectRevert(abi.encodeWithSelector(IAutomationRegistryMaster2_3.InsufficientBalance.selector, 0, 1)); - registryMaster.withdrawLinkFees(aMockAddress, 1); + vm.expectRevert(abi.encodeWithSelector(Registry.InsufficientBalance.selector, 0, 1)); + registry.withdrawLink(aMockAddress, 1); vm.stopPrank(); } - function testWithdrawLinkFeesRevertsBecauseOfInvalidRecipient() public { - // set config with the finance admin - setConfigForWithdraw(); - - vm.startPrank(FINANCE_ADMIN_ADDRESS); + function testWithdrawLinkRevertsBecauseOfInvalidRecipient() public { + vm.startPrank(FINANCE_ADMIN); // try to withdraw 1 link while there is 0 balance - vm.expectRevert(abi.encodeWithSelector(IAutomationRegistryMaster2_3.InvalidRecipient.selector)); - registryMaster.withdrawLinkFees(ZERO_ADDRESS, 1); + vm.expectRevert(abi.encodeWithSelector(Registry.InvalidRecipient.selector)); + registry.withdrawLink(ZERO_ADDRESS, 1); vm.stopPrank(); } - function testWithdrawLinkFeeSuccess() public { - // set config with the finance admin - setConfigForWithdraw(); - + function testWithdrawLinkSuccess() public { //simulate a deposit of link to the liquidity pool - mintLink(address(registryMaster), 1e10); + _mintLink(address(registry), 1e10); + uint256 startBalance = linkToken.balanceOf(address(registry)); - //check there's a balance - assertGt(link.balanceOf(address(registryMaster)), 0); - - vm.startPrank(FINANCE_ADMIN_ADDRESS); + vm.startPrank(FINANCE_ADMIN); // try to withdraw 1 link while there is a ton of link available - registryMaster.withdrawLinkFees(aMockAddress, 1); + registry.withdrawLink(aMockAddress, 1); vm.stopPrank(); - assertEq(link.balanceOf(address(aMockAddress)), 1); - assertEq(link.balanceOf(address(registryMaster)), 1e10 - 1); + assertEq(linkToken.balanceOf(address(aMockAddress)), 1); + assertEq(linkToken.balanceOf(address(registry)), startBalance - 1); } - function testWithdrawERC20FeeSuccess() public { - // set config with the finance admin - setConfigForWithdraw(); + function test_WithdrawERC20Fees_RespectsReserveAmount() public { + assertEq(registry.getBalance(usdUpkeepID), registry.getReserveAmount(address(usdToken))); + vm.startPrank(FINANCE_ADMIN); + vm.expectRevert(abi.encodeWithSelector(Registry.InsufficientBalance.selector, 0, 1)); + registry.withdrawERC20Fees(address(usdToken), FINANCE_ADMIN, 1); + } - // simulate a deposit of ERC20 to the liquidity pool - mintERC20(address(registryMaster), 1e10); + function test_WithdrawERC20Fees_RevertsWhen_AttemptingToWithdrawLINK() public { + _mintLink(address(registry), 1e10); + vm.startPrank(FINANCE_ADMIN); + vm.expectRevert(Registry.InvalidToken.selector); + registry.withdrawERC20Fees(address(linkToken), FINANCE_ADMIN, 1); // should revert + registry.withdrawLink(FINANCE_ADMIN, 1); // but using link withdraw functions succeeds + } - // check there's a balance - assertGt(mockERC20.balanceOf(address(registryMaster)), 0); + function test_WithdrawERC20Fees_RevertsWhen_LinkAvailableForPaymentIsNegative() public { + _transmit(usdUpkeepID, registry); // adds USD token to finance withdrawable, and gives NOPs a LINK balance + require(registry.linkAvailableForPayment() < 0, "linkAvailableForPayment should be negative"); + vm.expectRevert(Registry.InsufficientLinkLiquidity.selector); + vm.prank(FINANCE_ADMIN); + registry.withdrawERC20Fees(address(usdToken), FINANCE_ADMIN, 1); // should revert + _mintLink(address(registry), uint256(registry.linkAvailableForPayment() * -10)); // top up LINK liquidity pool + vm.prank(FINANCE_ADMIN); + registry.withdrawERC20Fees(address(usdToken), FINANCE_ADMIN, 1); // now finance can withdraw + } - vm.startPrank(FINANCE_ADMIN_ADDRESS); + function testWithdrawERC20FeeSuccess() public { + // deposit excess USDToken to the registry (this goes to the "finance withdrawable" pool be default) + uint256 startReserveAmount = registry.getReserveAmount(address(usdToken)); + uint256 startAmount = usdToken.balanceOf(address(registry)); + _mintERC20(address(registry), 1e10); - // try to withdraw 1 link while there is a ton of link available - registryMaster.withdrawERC20Fees(address(mockERC20), aMockAddress, 1); + // depositing shouldn't change reserve amount + assertEq(registry.getReserveAmount(address(usdToken)), startReserveAmount); + + vm.startPrank(FINANCE_ADMIN); + + // try to withdraw 1 USDToken + registry.withdrawERC20Fees(address(usdToken), aMockAddress, 1); vm.stopPrank(); - assertEq(mockERC20.balanceOf(address(aMockAddress)), 1); - assertEq(mockERC20.balanceOf(address(registryMaster)), 1e10 - 1); + assertEq(usdToken.balanceOf(address(aMockAddress)), 1); + assertEq(usdToken.balanceOf(address(registry)), startAmount + 1e10 - 1); + assertEq(registry.getReserveAmount(address(usdToken)), startReserveAmount); } } -contract AutomationRegistry2_3_SetConfig is AutomationRegistry2_3_SetUp { +contract SetConfig is SetUp { event ConfigSet( uint32 previousConfigBlockNumber, bytes32 configDigest, @@ -251,12 +308,9 @@ contract AutomationRegistry2_3_SetConfig is AutomationRegistry2_3_SetUp { address module = address(new ChainModuleBase()); AutomationRegistryBase2_3.OnchainConfig cfg = AutomationRegistryBase2_3.OnchainConfig({ - paymentPremiumPPB: 10_000, - flatFeeMicroLink: 40_000, checkGasLimit: 5_000_000, stalenessSeconds: 90_000, gasCeilingMultiplier: 0, - minUpkeepSpend: 0, maxPerformGas: 10_000_000, maxCheckDataSize: 5_000, maxPerformDataSize: 5_000, @@ -265,16 +319,16 @@ contract AutomationRegistry2_3_SetConfig is AutomationRegistry2_3_SetUp { fallbackLinkPrice: 2_000_000_000, // $20 fallbackNativePrice: 400_000_000_000, // $4,000 transcoder: 0xB1e66855FD67f6e85F0f0fA38cd6fBABdf00923c, - registrars: s_registrars, - upkeepPrivilegeManager: 0xD9c855F08A7e460691F41bBDDe6eC310bc0593D8, + registrars: new address[](0), + upkeepPrivilegeManager: PRIVILEGE_MANAGER, chainModule: module, reorgProtectionEnabled: true, - financeAdmin: FINANCE_ADMIN_ADDRESS + financeAdmin: FINANCE_ADMIN }); function testSetConfigSuccess() public { - (uint32 configCount, , ) = registryMaster.latestConfigDetails(); - assertEq(configCount, 0); + (uint32 configCount, uint32 blockNumber, ) = registry.latestConfigDetails(); + assertEq(configCount, 1); address billingTokenAddress = address(0x1111111111111111111111111111111111111111); address[] memory billingTokens = new address[](1); @@ -283,22 +337,21 @@ contract AutomationRegistry2_3_SetConfig is AutomationRegistry2_3_SetUp { AutomationRegistryBase2_3.BillingConfig[] memory billingConfigs = new AutomationRegistryBase2_3.BillingConfig[](1); billingConfigs[0] = AutomationRegistryBase2_3.BillingConfig({ gasFeePPB: 5_000, - flatFeeMicroLink: 20_000, - priceFeed: 0x2222222222222222222222222222222222222222 + flatFeeMilliCents: 20_000, + priceFeed: 0x2222222222222222222222222222222222222222, + fallbackPrice: 2_000_000_000, // $20 + minSpend: 100_000 }); bytes memory onchainConfigBytes = abi.encode(cfg); bytes memory onchainConfigBytesWithBilling = abi.encode(cfg, billingTokens, billingConfigs); - uint256 a = 1234; - address b = ZERO_ADDRESS; - bytes memory offchainConfigBytes = abi.encode(a, b); bytes32 configDigest = _configDigestFromConfigData( block.chainid, - address(registryMaster), + address(registry), ++configCount, - s_valid_signers, - s_valid_transmitters, + SIGNERS, + TRANSMITTERS, F, onchainConfigBytes, OFFCHAIN_CONFIG_VERSION, @@ -307,44 +360,45 @@ contract AutomationRegistry2_3_SetConfig is AutomationRegistry2_3_SetUp { vm.expectEmit(); emit ConfigSet( - 0, + blockNumber, configDigest, configCount, - s_valid_signers, - s_valid_transmitters, + SIGNERS, + TRANSMITTERS, F, onchainConfigBytes, OFFCHAIN_CONFIG_VERSION, offchainConfigBytes ); - registryMaster.setConfig( - s_valid_signers, - s_valid_transmitters, + registry.setConfig( + SIGNERS, + TRANSMITTERS, F, onchainConfigBytesWithBilling, OFFCHAIN_CONFIG_VERSION, offchainConfigBytes ); - (, , address[] memory signers, address[] memory transmitters, uint8 f) = registryMaster.getState(); + (, , address[] memory signers, address[] memory transmitters, uint8 f) = registry.getState(); - assertEq(signers, s_valid_signers); - assertEq(transmitters, s_valid_transmitters); + assertEq(signers, SIGNERS); + assertEq(transmitters, TRANSMITTERS); assertEq(f, F); - AutomationRegistryBase2_3.BillingConfig memory config = registryMaster.getBillingTokenConfig(billingTokenAddress); + AutomationRegistryBase2_3.BillingConfig memory config = registry.getBillingTokenConfig(billingTokenAddress); assertEq(config.gasFeePPB, 5_000); - assertEq(config.flatFeeMicroLink, 20_000); + assertEq(config.flatFeeMilliCents, 20_000); assertEq(config.priceFeed, 0x2222222222222222222222222222222222222222); + assertEq(config.minSpend, 100_000); - address[] memory tokens = registryMaster.getBillingTokens(); + address[] memory tokens = registry.getBillingTokens(); assertEq(tokens.length, 1); } function testSetConfigMultipleBillingConfigsSuccess() public { - (uint32 configCount, , ) = registryMaster.latestConfigDetails(); - assertEq(configCount, 0); + (uint32 configCount, , ) = registry.latestConfigDetails(); + assertEq(configCount, 1); address billingTokenAddress1 = address(0x1111111111111111111111111111111111111111); address billingTokenAddress2 = address(0x1111111111111111111111111111111111111112); @@ -355,53 +409,57 @@ contract AutomationRegistry2_3_SetConfig is AutomationRegistry2_3_SetUp { AutomationRegistryBase2_3.BillingConfig[] memory billingConfigs = new AutomationRegistryBase2_3.BillingConfig[](2); billingConfigs[0] = AutomationRegistryBase2_3.BillingConfig({ gasFeePPB: 5_001, - flatFeeMicroLink: 20_001, - priceFeed: 0x2222222222222222222222222222222222222221 + flatFeeMilliCents: 20_001, + priceFeed: 0x2222222222222222222222222222222222222221, + fallbackPrice: 100, + minSpend: 100 }); billingConfigs[1] = AutomationRegistryBase2_3.BillingConfig({ gasFeePPB: 5_002, - flatFeeMicroLink: 20_002, - priceFeed: 0x2222222222222222222222222222222222222222 + flatFeeMilliCents: 20_002, + priceFeed: 0x2222222222222222222222222222222222222222, + fallbackPrice: 200, + minSpend: 200 }); bytes memory onchainConfigBytesWithBilling = abi.encode(cfg, billingTokens, billingConfigs); - uint256 a = 1234; - address b = ZERO_ADDRESS; - bytes memory offchainConfigBytes = abi.encode(a, b); - - registryMaster.setConfig( - s_valid_signers, - s_valid_transmitters, + registry.setConfig( + SIGNERS, + TRANSMITTERS, F, onchainConfigBytesWithBilling, OFFCHAIN_CONFIG_VERSION, offchainConfigBytes ); - (, , address[] memory signers, address[] memory transmitters, uint8 f) = registryMaster.getState(); + (, , address[] memory signers, address[] memory transmitters, uint8 f) = registry.getState(); - assertEq(signers, s_valid_signers); - assertEq(transmitters, s_valid_transmitters); + assertEq(signers, SIGNERS); + assertEq(transmitters, TRANSMITTERS); assertEq(f, F); - AutomationRegistryBase2_3.BillingConfig memory config1 = registryMaster.getBillingTokenConfig(billingTokenAddress1); + AutomationRegistryBase2_3.BillingConfig memory config1 = registry.getBillingTokenConfig(billingTokenAddress1); assertEq(config1.gasFeePPB, 5_001); - assertEq(config1.flatFeeMicroLink, 20_001); + assertEq(config1.flatFeeMilliCents, 20_001); assertEq(config1.priceFeed, 0x2222222222222222222222222222222222222221); + assertEq(config1.fallbackPrice, 100); + assertEq(config1.minSpend, 100); - AutomationRegistryBase2_3.BillingConfig memory config2 = registryMaster.getBillingTokenConfig(billingTokenAddress2); + AutomationRegistryBase2_3.BillingConfig memory config2 = registry.getBillingTokenConfig(billingTokenAddress2); assertEq(config2.gasFeePPB, 5_002); - assertEq(config2.flatFeeMicroLink, 20_002); + assertEq(config2.flatFeeMilliCents, 20_002); assertEq(config2.priceFeed, 0x2222222222222222222222222222222222222222); + assertEq(config2.fallbackPrice, 200); + assertEq(config2.minSpend, 200); - address[] memory tokens = registryMaster.getBillingTokens(); + address[] memory tokens = registry.getBillingTokens(); assertEq(tokens.length, 2); } function testSetConfigTwiceAndLastSetOverwrites() public { - (uint32 configCount, , ) = registryMaster.latestConfigDetails(); - assertEq(configCount, 0); + (uint32 configCount, , ) = registry.latestConfigDetails(); + assertEq(configCount, 1); // BillingConfig1 address billingTokenAddress1 = address(0x1111111111111111111111111111111111111111); @@ -411,8 +469,10 @@ contract AutomationRegistry2_3_SetConfig is AutomationRegistry2_3_SetUp { AutomationRegistryBase2_3.BillingConfig[] memory billingConfigs1 = new AutomationRegistryBase2_3.BillingConfig[](1); billingConfigs1[0] = AutomationRegistryBase2_3.BillingConfig({ gasFeePPB: 5_001, - flatFeeMicroLink: 20_001, - priceFeed: 0x2222222222222222222222222222222222222221 + flatFeeMilliCents: 20_001, + priceFeed: 0x2222222222222222222222222222222222222221, + fallbackPrice: 100, + minSpend: 100 }); bytes memory onchainConfigBytesWithBilling1 = abi.encode(cfg, billingTokens1, billingConfigs1); @@ -425,20 +485,18 @@ contract AutomationRegistry2_3_SetConfig is AutomationRegistry2_3_SetUp { AutomationRegistryBase2_3.BillingConfig[] memory billingConfigs2 = new AutomationRegistryBase2_3.BillingConfig[](1); billingConfigs2[0] = AutomationRegistryBase2_3.BillingConfig({ gasFeePPB: 5_002, - flatFeeMicroLink: 20_002, - priceFeed: 0x2222222222222222222222222222222222222222 + flatFeeMilliCents: 20_002, + priceFeed: 0x2222222222222222222222222222222222222222, + fallbackPrice: 200, + minSpend: 200 }); bytes memory onchainConfigBytesWithBilling2 = abi.encode(cfg, billingTokens2, billingConfigs2); - uint256 a = 1234; - address b = ZERO_ADDRESS; - bytes memory offchainConfigBytes = abi.encode(a, b); - // set config once - registryMaster.setConfig( - s_valid_signers, - s_valid_transmitters, + registry.setConfig( + SIGNERS, + TRANSMITTERS, F, onchainConfigBytesWithBilling1, OFFCHAIN_CONFIG_VERSION, @@ -446,33 +504,35 @@ contract AutomationRegistry2_3_SetConfig is AutomationRegistry2_3_SetUp { ); // set config twice - registryMaster.setConfig( - s_valid_signers, - s_valid_transmitters, + registry.setConfig( + SIGNERS, + TRANSMITTERS, F, onchainConfigBytesWithBilling2, OFFCHAIN_CONFIG_VERSION, offchainConfigBytes ); - (, , address[] memory signers, address[] memory transmitters, uint8 f) = registryMaster.getState(); + (, , address[] memory signers, address[] memory transmitters, uint8 f) = registry.getState(); - assertEq(signers, s_valid_signers); - assertEq(transmitters, s_valid_transmitters); + assertEq(signers, SIGNERS); + assertEq(transmitters, TRANSMITTERS); assertEq(f, F); - AutomationRegistryBase2_3.BillingConfig memory config2 = registryMaster.getBillingTokenConfig(billingTokenAddress2); + AutomationRegistryBase2_3.BillingConfig memory config2 = registry.getBillingTokenConfig(billingTokenAddress2); assertEq(config2.gasFeePPB, 5_002); - assertEq(config2.flatFeeMicroLink, 20_002); + assertEq(config2.flatFeeMilliCents, 20_002); assertEq(config2.priceFeed, 0x2222222222222222222222222222222222222222); + assertEq(config2.fallbackPrice, 200); + assertEq(config2.minSpend, 200); - address[] memory tokens = registryMaster.getBillingTokens(); + address[] memory tokens = registry.getBillingTokens(); assertEq(tokens.length, 1); } function testSetConfigDuplicateBillingConfigFailure() public { - (uint32 configCount, , ) = registryMaster.latestConfigDetails(); - assertEq(configCount, 0); + (uint32 configCount, , ) = registry.latestConfigDetails(); + assertEq(configCount, 1); address billingTokenAddress1 = address(0x1111111111111111111111111111111111111111); address billingTokenAddress2 = address(0x1111111111111111111111111111111111111111); @@ -483,26 +543,26 @@ contract AutomationRegistry2_3_SetConfig is AutomationRegistry2_3_SetUp { AutomationRegistryBase2_3.BillingConfig[] memory billingConfigs = new AutomationRegistryBase2_3.BillingConfig[](2); billingConfigs[0] = AutomationRegistryBase2_3.BillingConfig({ gasFeePPB: 5_001, - flatFeeMicroLink: 20_001, - priceFeed: 0x2222222222222222222222222222222222222221 + flatFeeMilliCents: 20_001, + priceFeed: 0x2222222222222222222222222222222222222221, + fallbackPrice: 100, + minSpend: 100 }); billingConfigs[1] = AutomationRegistryBase2_3.BillingConfig({ gasFeePPB: 5_002, - flatFeeMicroLink: 20_002, - priceFeed: 0x2222222222222222222222222222222222222222 + flatFeeMilliCents: 20_002, + priceFeed: 0x2222222222222222222222222222222222222222, + fallbackPrice: 200, + minSpend: 200 }); bytes memory onchainConfigBytesWithBilling = abi.encode(cfg, billingTokens, billingConfigs); - uint256 a = 1234; - address b = ZERO_ADDRESS; - bytes memory offchainConfigBytes = abi.encode(a, b); - // expect revert because of duplicate tokens - vm.expectRevert(abi.encodeWithSelector(IAutomationRegistryMaster2_3.DuplicateEntry.selector)); - registryMaster.setConfig( - s_valid_signers, - s_valid_transmitters, + vm.expectRevert(abi.encodeWithSelector(Registry.DuplicateEntry.selector)); + registry.setConfig( + SIGNERS, + TRANSMITTERS, F, onchainConfigBytesWithBilling, OFFCHAIN_CONFIG_VERSION, @@ -510,6 +570,138 @@ contract AutomationRegistry2_3_SetConfig is AutomationRegistry2_3_SetUp { ); } + function testSetConfigRevertDueToInvalidToken() public { + address[] memory billingTokens = new address[](1); + billingTokens[0] = address(linkToken); + + AutomationRegistryBase2_3.BillingConfig[] memory billingConfigs = new AutomationRegistryBase2_3.BillingConfig[](1); + billingConfigs[0] = AutomationRegistryBase2_3.BillingConfig({ + gasFeePPB: 5_000, + flatFeeMilliCents: 20_000, + priceFeed: 0x2222222222222222222222222222222222222222, + fallbackPrice: 2_000_000_000, // $20 + minSpend: 100_000 + }); + + // deploy registry with OFF_CHAIN payout mode + registry = deployRegistry(AutoBase.PayoutMode.OFF_CHAIN); + + vm.expectRevert(abi.encodeWithSelector(Registry.InvalidToken.selector)); + registry.setConfigTypeSafe( + SIGNERS, + TRANSMITTERS, + F, + cfg, + OFFCHAIN_CONFIG_VERSION, + offchainConfigBytes, + billingTokens, + billingConfigs + ); + } + + function testSetConfigWithNewTransmittersSuccess() public { + registry = deployRegistry(AutoBase.PayoutMode.OFF_CHAIN); + + (uint32 configCount, uint32 blockNumber, ) = registry.latestConfigDetails(); + assertEq(configCount, 0); + + address billingTokenAddress = address(0x1111111111111111111111111111111111111111); + address[] memory billingTokens = new address[](1); + billingTokens[0] = billingTokenAddress; + + AutomationRegistryBase2_3.BillingConfig[] memory billingConfigs = new AutomationRegistryBase2_3.BillingConfig[](1); + billingConfigs[0] = AutomationRegistryBase2_3.BillingConfig({ + gasFeePPB: 5_000, + flatFeeMilliCents: 20_000, + priceFeed: 0x2222222222222222222222222222222222222222, + fallbackPrice: 2_000_000_000, // $20 + minSpend: 100_000 + }); + + bytes memory onchainConfigBytes = abi.encode(cfg); + + bytes32 configDigest = _configDigestFromConfigData( + block.chainid, + address(registry), + ++configCount, + SIGNERS, + TRANSMITTERS, + F, + onchainConfigBytes, + OFFCHAIN_CONFIG_VERSION, + offchainConfigBytes + ); + + vm.expectEmit(); + emit ConfigSet( + blockNumber, + configDigest, + configCount, + SIGNERS, + TRANSMITTERS, + F, + onchainConfigBytes, + OFFCHAIN_CONFIG_VERSION, + offchainConfigBytes + ); + + registry.setConfigTypeSafe( + SIGNERS, + TRANSMITTERS, + F, + cfg, + OFFCHAIN_CONFIG_VERSION, + offchainConfigBytes, + billingTokens, + billingConfigs + ); + + (, , address[] memory signers, address[] memory transmitters, ) = registry.getState(); + assertEq(signers, SIGNERS); + assertEq(transmitters, TRANSMITTERS); + + (configCount, blockNumber, ) = registry.latestConfigDetails(); + configDigest = _configDigestFromConfigData( + block.chainid, + address(registry), + ++configCount, + SIGNERS, + NEW_TRANSMITTERS, + F, + onchainConfigBytes, + OFFCHAIN_CONFIG_VERSION, + offchainConfigBytes + ); + + vm.expectEmit(); + emit ConfigSet( + blockNumber, + configDigest, + configCount, + SIGNERS, + NEW_TRANSMITTERS, + F, + onchainConfigBytes, + OFFCHAIN_CONFIG_VERSION, + offchainConfigBytes + ); + + registry.setConfigTypeSafe( + SIGNERS, + NEW_TRANSMITTERS, + F, + cfg, + OFFCHAIN_CONFIG_VERSION, + offchainConfigBytes, + billingTokens, + billingConfigs + ); + + (, , signers, transmitters, ) = registry.getState(); + assertEq(signers, SIGNERS); + assertEq(transmitters, NEW_TRANSMITTERS); + } + function _configDigestFromConfigData( uint256 chainId, address contractAddress, @@ -541,3 +733,792 @@ contract AutomationRegistry2_3_SetConfig is AutomationRegistry2_3_SetUp { return bytes32((prefix & prefixMask) | (h & ~prefixMask)); } } + +contract NOPsSettlement is SetUp { + event NOPsSettledOffchain(address[] payees, uint256[] payments); + event FundsWithdrawn(uint256 indexed id, uint256 amount, address to); + event PaymentWithdrawn(address indexed transmitter, uint256 indexed amount, address indexed to, address payee); + + function testSettleNOPsOffchainRevertDueToUnauthorizedCaller() public { + (Registry registry, ) = deployAndConfigureRegistryAndRegistrar(AutoBase.PayoutMode.ON_CHAIN); + + vm.expectRevert(abi.encodeWithSelector(Registry.OnlyFinanceAdmin.selector)); + registry.settleNOPsOffchain(); + } + + function testSettleNOPsOffchainRevertDueToOffchainSettlementDisabled() public { + (Registry registry, ) = deployAndConfigureRegistryAndRegistrar(AutoBase.PayoutMode.OFF_CHAIN); + + vm.prank(registry.owner()); + registry.disableOffchainPayments(); + + vm.prank(FINANCE_ADMIN); + vm.expectRevert(abi.encodeWithSelector(Registry.MustSettleOnchain.selector)); + registry.settleNOPsOffchain(); + } + + function testSettleNOPsOffchainSuccess() public { + // deploy and configure a registry with OFF_CHAIN payout + (Registry registry, ) = deployAndConfigureRegistryAndRegistrar(AutoBase.PayoutMode.OFF_CHAIN); + + uint256[] memory payments = new uint256[](TRANSMITTERS.length); + for (uint256 i = 0; i < TRANSMITTERS.length; i++) { + payments[i] = 0; + } + + vm.startPrank(FINANCE_ADMIN); + vm.expectEmit(); + emit NOPsSettledOffchain(PAYEES, payments); + registry.settleNOPsOffchain(); + } + + function testSettleNOPsOffchainSuccessTransmitterBalanceZeroed() public { + // deploy and configure a registry with OFF_CHAIN payout + (Registry registry, ) = deployAndConfigureRegistryAndRegistrar(AutoBase.PayoutMode.OFF_CHAIN); + + // register an upkeep and add funds + uint256 id = registry.registerUpkeep(address(TARGET1), 1000000, UPKEEP_ADMIN, 0, address(usdToken), "", "", ""); + _mintERC20(UPKEEP_ADMIN, 1e20); + vm.startPrank(UPKEEP_ADMIN); + usdToken.approve(address(registry), 1e20); + registry.addFunds(id, 1e20); + + // manually create a transmit so transmitters earn some rewards + _transmit(id, registry); + + // verify transmitters have positive balances + uint256[] memory payments = new uint256[](TRANSMITTERS.length); + for (uint256 i = 0; i < TRANSMITTERS.length; i++) { + (bool active, uint8 index, uint96 balance, uint96 lastCollected, ) = registry.getTransmitterInfo(TRANSMITTERS[i]); + assertTrue(active); + assertEq(i, index); + assertTrue(balance > 0); + assertEq(0, lastCollected); + + payments[i] = balance; + } + + // verify offchain settlement will emit NOPs' balances + vm.startPrank(FINANCE_ADMIN); + vm.expectEmit(); + emit NOPsSettledOffchain(PAYEES, payments); + registry.settleNOPsOffchain(); + + // verify that transmitters balance has been zeroed out + for (uint256 i = 0; i < TRANSMITTERS.length; i++) { + (bool active, uint8 index, uint96 balance, , ) = registry.getTransmitterInfo(TRANSMITTERS[i]); + assertTrue(active); + assertEq(i, index); + assertEq(0, balance); + } + } + + function testSettleNOPsOffchainForDeactivatedTransmittersSuccess() public { + // deploy and configure a registry with OFF_CHAIN payout + (Registry registry, Registrar registrar) = deployAndConfigureRegistryAndRegistrar(AutoBase.PayoutMode.OFF_CHAIN); + + // register an upkeep and add funds + uint256 id = registry.registerUpkeep(address(TARGET1), 1000000, UPKEEP_ADMIN, 0, address(usdToken), "", "", ""); + _mintERC20(UPKEEP_ADMIN, 1e20); + vm.startPrank(UPKEEP_ADMIN); + usdToken.approve(address(registry), 1e20); + registry.addFunds(id, 1e20); + + // manually create a transmit so TRANSMITTERS earn some rewards + _transmit(id, registry); + + // TRANSMITTERS have positive balance now + // configure the registry to use NEW_TRANSMITTERS + _configureWithNewTransmitters(registry, registrar); + + _transmit(id, registry); + + // verify all transmitters have positive balances + address[] memory expectedPayees = new address[](6); + uint256[] memory expectedPayments = new uint256[](6); + for (uint256 i = 0; i < NEW_TRANSMITTERS.length; i++) { + (bool active, uint8 index, uint96 balance, uint96 lastCollected, address payee) = registry.getTransmitterInfo( + NEW_TRANSMITTERS[i] + ); + assertTrue(active); + assertEq(i, index); + assertTrue(lastCollected > 0); + expectedPayments[i] = balance; + expectedPayees[i] = payee; + } + for (uint256 i = 2; i < TRANSMITTERS.length; i++) { + (bool active, uint8 index, uint96 balance, uint96 lastCollected, address payee) = registry.getTransmitterInfo( + TRANSMITTERS[i] + ); + assertFalse(active); + assertEq(i, index); + assertTrue(balance > 0); + assertTrue(lastCollected > 0); + expectedPayments[2 + i] = balance; + expectedPayees[2 + i] = payee; + } + + // verify offchain settlement will emit NOPs' balances + vm.startPrank(FINANCE_ADMIN); + + // simply expectEmit won't work here because s_deactivatedTransmitters is an enumerable set so the order of these + // deactivated transmitters is not guaranteed. To handle this, we record logs and decode data field manually. + vm.recordLogs(); + registry.settleNOPsOffchain(); + Vm.Log[] memory entries = vm.getRecordedLogs(); + + assertEq(entries.length, 1); + Vm.Log memory l = entries[0]; + assertEq(l.topics[0], keccak256("NOPsSettledOffchain(address[],uint256[])")); + (address[] memory actualPayees, uint256[] memory actualPayments) = abi.decode(l.data, (address[], uint256[])); + assertEq(actualPayees.length, 6); + assertEq(actualPayments.length, 6); + + // first 4 payees and payments are for NEW_TRANSMITTERS and they are ordered. + for (uint256 i = 0; i < NEW_TRANSMITTERS.length; i++) { + assertEq(actualPayees[i], expectedPayees[i]); + assertEq(actualPayments[i], expectedPayments[i]); + } + + // the last 2 payees and payments for TRANSMITTERS[2] and TRANSMITTERS[3] and they are not ordered + assertTrue( + (actualPayments[5] == expectedPayments[5] && + actualPayees[5] == expectedPayees[5] && + actualPayments[4] == expectedPayments[4] && + actualPayees[4] == expectedPayees[4]) || + (actualPayments[5] == expectedPayments[4] && + actualPayees[5] == expectedPayees[4] && + actualPayments[4] == expectedPayments[5] && + actualPayees[4] == expectedPayees[5]) + ); + + // verify that new transmitters balance has been zeroed out + for (uint256 i = 0; i < NEW_TRANSMITTERS.length; i++) { + (bool active, uint8 index, uint96 balance, , ) = registry.getTransmitterInfo(NEW_TRANSMITTERS[i]); + assertTrue(active); + assertEq(i, index); + assertEq(0, balance); + } + // verify that deactivated transmitters (TRANSMITTERS[2] and TRANSMITTERS[3]) balance has been zeroed out + for (uint256 i = 2; i < TRANSMITTERS.length; i++) { + (bool active, uint8 index, uint96 balance, , ) = registry.getTransmitterInfo(TRANSMITTERS[i]); + assertFalse(active); + assertEq(i, index); + assertEq(0, balance); + } + } + + function testDisableOffchainPaymentsRevertDueToUnauthorizedCaller() public { + (Registry registry, ) = deployAndConfigureRegistryAndRegistrar(AutoBase.PayoutMode.OFF_CHAIN); + + vm.startPrank(FINANCE_ADMIN); + vm.expectRevert(bytes("Only callable by owner")); + registry.disableOffchainPayments(); + } + + function testDisableOffchainPaymentsSuccess() public { + (Registry registry, ) = deployAndConfigureRegistryAndRegistrar(AutoBase.PayoutMode.OFF_CHAIN); + + vm.startPrank(registry.owner()); + registry.disableOffchainPayments(); + + assertEq(uint8(AutoBase.PayoutMode.ON_CHAIN), registry.getPayoutMode()); + } + + function testSinglePerformAndNodesCanWithdrawOnchain() public { + // deploy and configure a registry with OFF_CHAIN payout + (Registry registry, ) = deployAndConfigureRegistryAndRegistrar(AutoBase.PayoutMode.OFF_CHAIN); + + // register an upkeep and add funds + uint256 id = registry.registerUpkeep(address(TARGET1), 1000000, UPKEEP_ADMIN, 0, address(usdToken), "", "", ""); + _mintERC20(UPKEEP_ADMIN, 1e20); + vm.startPrank(UPKEEP_ADMIN); + usdToken.approve(address(registry), 1e20); + registry.addFunds(id, 1e20); + + // manually create a transmit so transmitters earn some rewards + _transmit(id, registry); + + // disable offchain payments + _mintLink(address(registry), 1e19); + vm.prank(registry.owner()); + registry.disableOffchainPayments(); + + // payees should be able to withdraw onchain + for (uint256 i = 0; i < TRANSMITTERS.length; i++) { + (, , uint96 balance, , address payee) = registry.getTransmitterInfo(TRANSMITTERS[i]); + vm.prank(payee); + vm.expectEmit(); + emit PaymentWithdrawn(TRANSMITTERS[i], balance, payee, payee); + registry.withdrawPayment(TRANSMITTERS[i], payee); + } + + // allow upkeep admin to withdraw funds + vm.startPrank(UPKEEP_ADMIN); + registry.cancelUpkeep(id); + vm.roll(100 + block.number); + vm.expectEmit(); + // the upkeep spent less than minimum spending limit so upkeep admin can only withdraw upkeep balance - min spend value + emit FundsWithdrawn(id, 9.9e19, UPKEEP_ADMIN); + registry.withdrawFunds(id, UPKEEP_ADMIN); + } + + function testMultiplePerformsAndNodesCanWithdrawOnchain() public { + // deploy and configure a registry with OFF_CHAIN payout + (Registry registry, ) = deployAndConfigureRegistryAndRegistrar(AutoBase.PayoutMode.OFF_CHAIN); + + // register an upkeep and add funds + uint256 id = registry.registerUpkeep(address(TARGET1), 1000000, UPKEEP_ADMIN, 0, address(usdToken), "", "", ""); + _mintERC20(UPKEEP_ADMIN, 1e20); + vm.startPrank(UPKEEP_ADMIN); + usdToken.approve(address(registry), 1e20); + registry.addFunds(id, 1e20); + + // manually call transmit so transmitters earn some rewards + for (uint256 i = 0; i < 50; i++) { + vm.roll(100 + block.number); + _transmit(id, registry); + } + + // disable offchain payments + _mintLink(address(registry), 1e19); + vm.prank(registry.owner()); + registry.disableOffchainPayments(); + + // manually call transmit after offchain payment is disabled + for (uint256 i = 0; i < 50; i++) { + vm.roll(100 + block.number); + _transmit(id, registry); + } + + // payees should be able to withdraw onchain + for (uint256 i = 0; i < TRANSMITTERS.length; i++) { + (, , uint96 balance, , address payee) = registry.getTransmitterInfo(TRANSMITTERS[i]); + vm.prank(payee); + vm.expectEmit(); + emit PaymentWithdrawn(TRANSMITTERS[i], balance, payee, payee); + registry.withdrawPayment(TRANSMITTERS[i], payee); + } + + // allow upkeep admin to withdraw funds + vm.startPrank(UPKEEP_ADMIN); + registry.cancelUpkeep(id); + vm.roll(100 + block.number); + uint256 balance = registry.getBalance(id); + vm.expectEmit(); + emit FundsWithdrawn(id, balance, UPKEEP_ADMIN); + registry.withdrawFunds(id, UPKEEP_ADMIN); + } + + function _configureWithNewTransmitters(Registry registry, Registrar registrar) internal { + IERC20[] memory billingTokens = new IERC20[](1); + billingTokens[0] = IERC20(address(usdToken)); + uint256[] memory minRegistrationFees = new uint256[](billingTokens.length); + minRegistrationFees[0] = 100000000000000000000; // 100 USD + address[] memory billingTokenAddresses = new address[](billingTokens.length); + for (uint256 i = 0; i < billingTokens.length; i++) { + billingTokenAddresses[i] = address(billingTokens[i]); + } + AutomationRegistryBase2_3.BillingConfig[] + memory billingTokenConfigs = new AutomationRegistryBase2_3.BillingConfig[](billingTokens.length); + billingTokenConfigs[0] = AutomationRegistryBase2_3.BillingConfig({ + gasFeePPB: 10_000_000, // 15% + flatFeeMilliCents: 2_000, // 2 cents + priceFeed: address(USDTOKEN_USD_FEED), + fallbackPrice: 100_000_000, // $1 + minSpend: 1000000000000000000 // 1 USD + }); + + address[] memory registrars; + registrars = new address[](1); + registrars[0] = address(registrar); + AutomationRegistryBase2_3.OnchainConfig memory cfg = AutomationRegistryBase2_3.OnchainConfig({ + checkGasLimit: 5_000_000, + stalenessSeconds: 90_000, + gasCeilingMultiplier: 2, + maxPerformGas: 10_000_000, + maxCheckDataSize: 5_000, + maxPerformDataSize: 5_000, + maxRevertDataSize: 5_000, + fallbackGasPrice: 20_000_000_000, + fallbackLinkPrice: 2_000_000_000, // $20 + fallbackNativePrice: 400_000_000_000, // $4,000 + transcoder: 0xB1e66855FD67f6e85F0f0fA38cd6fBABdf00923c, + registrars: registrars, + upkeepPrivilegeManager: PRIVILEGE_MANAGER, + chainModule: address(new ChainModuleBase()), + reorgProtectionEnabled: true, + financeAdmin: FINANCE_ADMIN + }); + registry.setConfigTypeSafe( + SIGNERS, + NEW_TRANSMITTERS, + F, + cfg, + OFFCHAIN_CONFIG_VERSION, + "", + billingTokenAddresses, + billingTokenConfigs + ); + registry.setPayees(NEW_PAYEES); + } +} + +contract WithdrawPayment is SetUp { + function testWithdrawPaymentRevertDueToOffchainPayoutMode() public { + registry = deployRegistry(AutoBase.PayoutMode.OFF_CHAIN); + vm.expectRevert(abi.encodeWithSelector(Registry.MustSettleOffchain.selector)); + vm.prank(TRANSMITTERS[0]); + registry.withdrawPayment(TRANSMITTERS[0], TRANSMITTERS[0]); + } +} + +contract RegisterUpkeep is SetUp { + function test_RevertsWhen_Paused() public { + registry.pause(); + vm.expectRevert(Registry.RegistryPaused.selector); + registry.registerUpkeep( + address(TARGET1), + config.maxPerformGas, + UPKEEP_ADMIN, + uint8(Trigger.CONDITION), + address(linkToken), + "", + "", + "" + ); + } + + function test_RevertsWhen_TargetIsNotAContract() public { + vm.expectRevert(Registry.NotAContract.selector); + registry.registerUpkeep( + randomAddress(), + config.maxPerformGas, + UPKEEP_ADMIN, + uint8(Trigger.CONDITION), + address(linkToken), + "", + "", + "" + ); + } + + function test_RevertsWhen_CalledByNonOwner() public { + vm.prank(STRANGER); + vm.expectRevert(Registry.OnlyCallableByOwnerOrRegistrar.selector); + registry.registerUpkeep( + address(TARGET1), + config.maxPerformGas, + UPKEEP_ADMIN, + uint8(Trigger.CONDITION), + address(linkToken), + "", + "", + "" + ); + } + + function test_RevertsWhen_ExecuteGasIsTooLow() public { + vm.expectRevert(Registry.GasLimitOutsideRange.selector); + registry.registerUpkeep( + address(TARGET1), + 2299, // 1 less than min + UPKEEP_ADMIN, + uint8(Trigger.CONDITION), + address(linkToken), + "", + "", + "" + ); + } + + function test_RevertsWhen_ExecuteGasIsTooHigh() public { + vm.expectRevert(Registry.GasLimitOutsideRange.selector); + registry.registerUpkeep( + address(TARGET1), + config.maxPerformGas + 1, + UPKEEP_ADMIN, + uint8(Trigger.CONDITION), + address(linkToken), + "", + "", + "" + ); + } + + function test_RevertsWhen_TheBillingTokenIsNotConfigured() public { + vm.expectRevert(Registry.InvalidToken.selector); + registry.registerUpkeep( + address(TARGET1), + config.maxPerformGas, + UPKEEP_ADMIN, + uint8(Trigger.CONDITION), + randomAddress(), + "", + "", + "" + ); + } + + function test_RevertsWhen_CheckDataIsTooLarge() public { + vm.expectRevert(Registry.CheckDataExceedsLimit.selector); + registry.registerUpkeep( + address(TARGET1), + config.maxPerformGas, + UPKEEP_ADMIN, + uint8(Trigger.CONDITION), + address(linkToken), + randomBytes(config.maxCheckDataSize + 1), + "", + "" + ); + } + + function test_Happy() public { + bytes memory checkData = randomBytes(config.maxCheckDataSize); + bytes memory trigggerConfig = randomBytes(100); + bytes memory offchainConfig = randomBytes(100); + + uint256 upkeepCount = registry.getNumUpkeeps(); + + uint256 upkeepID = registry.registerUpkeep( + address(TARGET1), + config.maxPerformGas, + UPKEEP_ADMIN, + uint8(Trigger.LOG), + address(linkToken), + checkData, + trigggerConfig, + offchainConfig + ); + + assertEq(registry.getNumUpkeeps(), upkeepCount + 1); + assertEq(registry.getUpkeep(upkeepID).target, address(TARGET1)); + assertEq(registry.getUpkeep(upkeepID).performGas, config.maxPerformGas); + assertEq(registry.getUpkeep(upkeepID).checkData, checkData); + assertEq(registry.getUpkeep(upkeepID).balance, 0); + assertEq(registry.getUpkeep(upkeepID).admin, UPKEEP_ADMIN); + assertEq(registry.getUpkeep(upkeepID).offchainConfig, offchainConfig); + assertEq(registry.getUpkeepTriggerConfig(upkeepID), trigggerConfig); + assertEq(uint8(registry.getTriggerType(upkeepID)), uint8(Trigger.LOG)); + } +} + +contract OnTokenTransfer is SetUp { + function test_RevertsWhen_NotCalledByTheLinkToken() public { + vm.expectRevert(Registry.OnlyCallableByLINKToken.selector); + registry.onTokenTransfer(UPKEEP_ADMIN, 100, abi.encode(linkUpkeepID)); + } + + function test_RevertsWhen_NotCalledWithExactly32Bytes() public { + vm.startPrank(address(linkToken)); + vm.expectRevert(Registry.InvalidDataLength.selector); + registry.onTokenTransfer(UPKEEP_ADMIN, 100, randomBytes(31)); + vm.expectRevert(Registry.InvalidDataLength.selector); + registry.onTokenTransfer(UPKEEP_ADMIN, 100, randomBytes(33)); + } + + function test_RevertsWhen_TheUpkeepIsCancelledOrDNE() public { + vm.startPrank(address(linkToken)); + vm.expectRevert(Registry.UpkeepCancelled.selector); + registry.onTokenTransfer(UPKEEP_ADMIN, 100, abi.encode(randomNumber())); + } + + function test_RevertsWhen_TheUpkeepDoesNotUseLINKAsItsBillingToken() public { + vm.startPrank(address(linkToken)); + vm.expectRevert(Registry.InvalidToken.selector); + registry.onTokenTransfer(UPKEEP_ADMIN, 100, abi.encode(usdUpkeepID)); + } + + function test_Happy() public { + vm.startPrank(address(linkToken)); + uint256 beforeBalance = registry.getBalance(linkUpkeepID); + registry.onTokenTransfer(UPKEEP_ADMIN, 100, abi.encode(linkUpkeepID)); + assertEq(registry.getBalance(linkUpkeepID), beforeBalance + 100); + } +} + +contract GetMinBalanceForUpkeep is SetUp { + function test_accountsForFlatFee() public { + // set fee to 0 + AutomationRegistryBase2_3.BillingConfig memory usdTokenConfig = registry.getBillingTokenConfig(address(usdToken)); + usdTokenConfig.flatFeeMilliCents = 0; + _updateBillingTokenConfig(registry, address(usdToken), usdTokenConfig); + + uint256 minBalanceBefore = registry.getMinBalanceForUpkeep(usdUpkeepID); + + // set fee to non-zero + usdTokenConfig.flatFeeMilliCents = 100; + _updateBillingTokenConfig(registry, address(usdToken), usdTokenConfig); + + uint256 minBalanceAfter = registry.getMinBalanceForUpkeep(usdUpkeepID); + assertEq(minBalanceAfter, minBalanceBefore + (uint256(usdTokenConfig.flatFeeMilliCents) * 1e13)); + } +} + +contract BillingOverrides is SetUp { + event BillingConfigOverridden(uint256 indexed id, AutomationRegistryBase2_3.BillingOverrides overrides); + event BillingConfigOverrideRemoved(uint256 indexed id); + + function test_RevertsWhen_NotPrivilegeManager() public { + AutomationRegistryBase2_3.BillingOverrides memory billingOverrides = AutomationRegistryBase2_3.BillingOverrides({ + gasFeePPB: 5_000, + flatFeeMilliCents: 20_000 + }); + + vm.expectRevert(Registry.OnlyCallableByUpkeepPrivilegeManager.selector); + registry.setBillingOverrides(linkUpkeepID, billingOverrides); + } + + function test_RevertsWhen_UpkeepCancelled() public { + AutomationRegistryBase2_3.BillingOverrides memory billingOverrides = AutomationRegistryBase2_3.BillingOverrides({ + gasFeePPB: 5_000, + flatFeeMilliCents: 20_000 + }); + + registry.cancelUpkeep(linkUpkeepID); + + vm.startPrank(PRIVILEGE_MANAGER); + vm.expectRevert(Registry.UpkeepCancelled.selector); + registry.setBillingOverrides(linkUpkeepID, billingOverrides); + } + + function test_Happy_SetBillingOverrides() public { + AutomationRegistryBase2_3.BillingOverrides memory billingOverrides = AutomationRegistryBase2_3.BillingOverrides({ + gasFeePPB: 5_000, + flatFeeMilliCents: 20_000 + }); + + vm.startPrank(PRIVILEGE_MANAGER); + + vm.expectEmit(); + emit BillingConfigOverridden(linkUpkeepID, billingOverrides); + registry.setBillingOverrides(linkUpkeepID, billingOverrides); + } + + function test_Happy_RemoveBillingOverrides() public { + vm.startPrank(PRIVILEGE_MANAGER); + + vm.expectEmit(); + emit BillingConfigOverrideRemoved(linkUpkeepID); + registry.removeBillingOverrides(linkUpkeepID); + } + + function test_Happy_MaxGasPayment_WithBillingOverrides() public { + uint96 maxPayment1 = registry.getMaxPaymentForGas(linkUpkeepID, 0, 5_000_000, address(linkToken)); + + // Double the two billing values + AutomationRegistryBase2_3.BillingOverrides memory billingOverrides = AutomationRegistryBase2_3.BillingOverrides({ + gasFeePPB: DEFAULT_GAS_FEE_PPB * 2, + flatFeeMilliCents: DEFAULT_FLAT_FEE_MILLI_CENTS * 2 + }); + + vm.startPrank(PRIVILEGE_MANAGER); + registry.setBillingOverrides(linkUpkeepID, billingOverrides); + + // maxPayment2 should be greater than maxPayment1 after the overrides + // The 2 numbers should follow this: maxPayment2 - maxPayment1 == 2 * recepit.premium + // We do not apply the exact equation since we couldn't get the receipt.premium value + uint96 maxPayment2 = registry.getMaxPaymentForGas(linkUpkeepID, 0, 5_000_000, address(linkToken)); + assertGt(maxPayment2, maxPayment1); + } +} + +contract Transmit is SetUp { + function test_handlesMixedBatchOfBillingTokens() external { + uint256[] memory prevUpkeepBalances = new uint256[](3); + prevUpkeepBalances[0] = registry.getBalance(linkUpkeepID); + prevUpkeepBalances[1] = registry.getBalance(usdUpkeepID); + prevUpkeepBalances[2] = registry.getBalance(nativeUpkeepID); + uint256[] memory prevTokenBalances = new uint256[](3); + prevTokenBalances[0] = linkToken.balanceOf(address(registry)); + prevTokenBalances[1] = usdToken.balanceOf(address(registry)); + prevTokenBalances[2] = weth.balanceOf(address(registry)); + uint256[] memory prevReserveBalances = new uint256[](3); + prevReserveBalances[0] = registry.getReserveAmount(address(linkToken)); + prevReserveBalances[1] = registry.getReserveAmount(address(usdToken)); + prevReserveBalances[2] = registry.getReserveAmount(address(weth)); + uint256[] memory upkeepIDs = new uint256[](3); + upkeepIDs[0] = linkUpkeepID; + upkeepIDs[1] = usdUpkeepID; + upkeepIDs[2] = nativeUpkeepID; + // do the thing + _transmit(upkeepIDs, registry); + // assert upkeep balances have decreased + require(prevUpkeepBalances[0] > registry.getBalance(linkUpkeepID), "link upkeep balance should have decreased"); + require(prevUpkeepBalances[1] > registry.getBalance(usdUpkeepID), "usd upkeep balance should have decreased"); + require(prevUpkeepBalances[2] > registry.getBalance(nativeUpkeepID), "native upkeep balance should have decreased"); + // assert token balances have not changed + assertEq(prevTokenBalances[0], linkToken.balanceOf(address(registry))); + assertEq(prevTokenBalances[1], usdToken.balanceOf(address(registry))); + assertEq(prevTokenBalances[2], weth.balanceOf(address(registry))); + // assert reserve amounts have adjusted accordingly + require( + prevReserveBalances[0] < registry.getReserveAmount(address(linkToken)), + "usd reserve amount should have increased" + ); // link reserve amount increases in value equal to the decrease of the other reserve amounts + require( + prevReserveBalances[1] > registry.getReserveAmount(address(usdToken)), + "usd reserve amount should have decreased" + ); + require( + prevReserveBalances[2] > registry.getReserveAmount(address(weth)), + "native reserve amount should have decreased" + ); + } +} + +contract MigrateReceive is SetUp { + event UpkeepMigrated(uint256 indexed id, uint256 remainingBalance, address destination); + event UpkeepReceived(uint256 indexed id, uint256 startingBalance, address importedFrom); + + Registry newRegistry; + uint256[] idsToMigrate; + + function setUp() public override { + super.setUp(); + (newRegistry, ) = deployAndConfigureRegistryAndRegistrar(AutoBase.PayoutMode.ON_CHAIN); + idsToMigrate.push(linkUpkeepID); + idsToMigrate.push(usdUpkeepID); + idsToMigrate.push(nativeUpkeepID); + registry.setPeerRegistryMigrationPermission(address(newRegistry), 1); + newRegistry.setPeerRegistryMigrationPermission(address(registry), 2); + } + + function test_RevertsWhen_PermissionsNotSet() external { + // no permissions + registry.setPeerRegistryMigrationPermission(address(newRegistry), 0); + newRegistry.setPeerRegistryMigrationPermission(address(registry), 0); + vm.expectRevert(Registry.MigrationNotPermitted.selector); + vm.prank(UPKEEP_ADMIN); + registry.migrateUpkeeps(idsToMigrate, address(newRegistry)); + + // only outgoing permissions + registry.setPeerRegistryMigrationPermission(address(newRegistry), 1); + newRegistry.setPeerRegistryMigrationPermission(address(registry), 0); + vm.expectRevert(Registry.MigrationNotPermitted.selector); + vm.prank(UPKEEP_ADMIN); + registry.migrateUpkeeps(idsToMigrate, address(newRegistry)); + + // only incoming permissions + registry.setPeerRegistryMigrationPermission(address(newRegistry), 0); + newRegistry.setPeerRegistryMigrationPermission(address(registry), 2); + vm.expectRevert(Registry.MigrationNotPermitted.selector); + vm.prank(UPKEEP_ADMIN); + registry.migrateUpkeeps(idsToMigrate, address(newRegistry)); + + // permissions opposite direction + registry.setPeerRegistryMigrationPermission(address(newRegistry), 2); + newRegistry.setPeerRegistryMigrationPermission(address(registry), 1); + vm.expectRevert(Registry.MigrationNotPermitted.selector); + vm.prank(UPKEEP_ADMIN); + registry.migrateUpkeeps(idsToMigrate, address(newRegistry)); + } + + function test_RevertsWhen_ReceivingRegistryDoesNotSupportToken() external { + _removeBillingTokenConfig(newRegistry, address(weth)); + vm.expectRevert(Registry.InvalidToken.selector); + vm.prank(UPKEEP_ADMIN); + registry.migrateUpkeeps(idsToMigrate, address(newRegistry)); + idsToMigrate.pop(); // remove native upkeep id + vm.prank(UPKEEP_ADMIN); + registry.migrateUpkeeps(idsToMigrate, address(newRegistry)); // should succeed now + } + + function test_RevertsWhen_CalledByNonAdmin() external { + vm.expectRevert(Registry.OnlyCallableByAdmin.selector); + vm.prank(STRANGER); + registry.migrateUpkeeps(idsToMigrate, address(newRegistry)); + } + + function test_Success() external { + vm.startPrank(UPKEEP_ADMIN); + + // add some changes in upkeep data to the mix + registry.pauseUpkeep(usdUpkeepID); + registry.setUpkeepTriggerConfig(linkUpkeepID, randomBytes(100)); + registry.setUpkeepCheckData(nativeUpkeepID, randomBytes(25)); + + // record previous state + uint256[] memory prevUpkeepBalances = new uint256[](3); + prevUpkeepBalances[0] = registry.getBalance(linkUpkeepID); + prevUpkeepBalances[1] = registry.getBalance(usdUpkeepID); + prevUpkeepBalances[2] = registry.getBalance(nativeUpkeepID); + uint256[] memory prevReserveBalances = new uint256[](3); + prevReserveBalances[0] = registry.getReserveAmount(address(linkToken)); + prevReserveBalances[1] = registry.getReserveAmount(address(usdToken)); + prevReserveBalances[2] = registry.getReserveAmount(address(weth)); + uint256[] memory prevTokenBalances = new uint256[](3); + prevTokenBalances[0] = linkToken.balanceOf(address(registry)); + prevTokenBalances[1] = usdToken.balanceOf(address(registry)); + prevTokenBalances[2] = weth.balanceOf(address(registry)); + bytes[] memory prevUpkeepData = new bytes[](3); + prevUpkeepData[0] = abi.encode(registry.getUpkeep(linkUpkeepID)); + prevUpkeepData[1] = abi.encode(registry.getUpkeep(usdUpkeepID)); + prevUpkeepData[2] = abi.encode(registry.getUpkeep(nativeUpkeepID)); + bytes[] memory prevUpkeepTriggerData = new bytes[](3); + prevUpkeepTriggerData[0] = registry.getUpkeepTriggerConfig(linkUpkeepID); + prevUpkeepTriggerData[1] = registry.getUpkeepTriggerConfig(usdUpkeepID); + prevUpkeepTriggerData[2] = registry.getUpkeepTriggerConfig(nativeUpkeepID); + + // event expectations + vm.expectEmit(address(registry)); + emit UpkeepMigrated(linkUpkeepID, prevUpkeepBalances[0], address(newRegistry)); + vm.expectEmit(address(registry)); + emit UpkeepMigrated(usdUpkeepID, prevUpkeepBalances[1], address(newRegistry)); + vm.expectEmit(address(registry)); + emit UpkeepMigrated(nativeUpkeepID, prevUpkeepBalances[2], address(newRegistry)); + vm.expectEmit(address(newRegistry)); + emit UpkeepReceived(linkUpkeepID, prevUpkeepBalances[0], address(registry)); + vm.expectEmit(address(newRegistry)); + emit UpkeepReceived(usdUpkeepID, prevUpkeepBalances[1], address(registry)); + vm.expectEmit(address(newRegistry)); + emit UpkeepReceived(nativeUpkeepID, prevUpkeepBalances[2], address(registry)); + + // do the thing + registry.migrateUpkeeps(idsToMigrate, address(newRegistry)); + + // assert upkeep balances have been migrated + assertEq(registry.getBalance(linkUpkeepID), 0); + assertEq(registry.getBalance(usdUpkeepID), 0); + assertEq(registry.getBalance(nativeUpkeepID), 0); + assertEq(newRegistry.getBalance(linkUpkeepID), prevUpkeepBalances[0]); + assertEq(newRegistry.getBalance(usdUpkeepID), prevUpkeepBalances[1]); + assertEq(newRegistry.getBalance(nativeUpkeepID), prevUpkeepBalances[2]); + + // assert reserve balances have been adjusted + assertEq(newRegistry.getReserveAmount(address(linkToken)), newRegistry.getBalance(linkUpkeepID)); + assertEq(newRegistry.getReserveAmount(address(usdToken)), newRegistry.getBalance(usdUpkeepID)); + assertEq(newRegistry.getReserveAmount(address(weth)), newRegistry.getBalance(nativeUpkeepID)); + assertEq( + newRegistry.getReserveAmount(address(linkToken)), + prevReserveBalances[0] - registry.getReserveAmount(address(linkToken)) + ); + assertEq( + newRegistry.getReserveAmount(address(usdToken)), + prevReserveBalances[1] - registry.getReserveAmount(address(usdToken)) + ); + assertEq( + newRegistry.getReserveAmount(address(weth)), + prevReserveBalances[2] - registry.getReserveAmount(address(weth)) + ); + + // assert token have been transfered + assertEq(linkToken.balanceOf(address(newRegistry)), newRegistry.getBalance(linkUpkeepID)); + assertEq(usdToken.balanceOf(address(newRegistry)), newRegistry.getBalance(usdUpkeepID)); + assertEq(weth.balanceOf(address(newRegistry)), newRegistry.getBalance(nativeUpkeepID)); + assertEq(linkToken.balanceOf(address(registry)), prevTokenBalances[0] - linkToken.balanceOf(address(newRegistry))); + assertEq(usdToken.balanceOf(address(registry)), prevTokenBalances[1] - usdToken.balanceOf(address(newRegistry))); + assertEq(weth.balanceOf(address(registry)), prevTokenBalances[2] - weth.balanceOf(address(newRegistry))); + + // assert upkeep data matches + assertEq(prevUpkeepData[0], abi.encode(newRegistry.getUpkeep(linkUpkeepID))); + assertEq(prevUpkeepData[1], abi.encode(newRegistry.getUpkeep(usdUpkeepID))); + assertEq(prevUpkeepData[2], abi.encode(newRegistry.getUpkeep(nativeUpkeepID))); + assertEq(prevUpkeepTriggerData[0], newRegistry.getUpkeepTriggerConfig(linkUpkeepID)); + assertEq(prevUpkeepTriggerData[1], newRegistry.getUpkeepTriggerConfig(usdUpkeepID)); + assertEq(prevUpkeepTriggerData[2], newRegistry.getUpkeepTriggerConfig(nativeUpkeepID)); + + vm.stopPrank(); + } +} diff --git a/contracts/src/v0.8/automation/dev/test/BaseTest.t.sol b/contracts/src/v0.8/automation/dev/test/BaseTest.t.sol index 790afcff..eae07a8b 100644 --- a/contracts/src/v0.8/automation/dev/test/BaseTest.t.sol +++ b/contracts/src/v0.8/automation/dev/test/BaseTest.t.sol @@ -3,11 +3,453 @@ pragma solidity 0.8.19; import "forge-std/Test.sol"; +import {LinkToken} from "../../../shared/token/ERC677/LinkToken.sol"; +import {ERC20Mock} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/mocks/ERC20Mock.sol"; +import {MockV3Aggregator} from "../../../tests/MockV3Aggregator.sol"; +import {AutomationForwarderLogic} from "../../AutomationForwarderLogic.sol"; +import {UpkeepTranscoder5_0 as Transcoder} from "../v2_3/UpkeepTranscoder5_0.sol"; +import {AutomationRegistry2_3} from "../v2_3/AutomationRegistry2_3.sol"; +import {AutomationRegistryBase2_3 as AutoBase} from "../v2_3/AutomationRegistryBase2_3.sol"; +import {AutomationRegistryLogicA2_3} from "../v2_3/AutomationRegistryLogicA2_3.sol"; +import {AutomationRegistryLogicB2_3} from "../v2_3/AutomationRegistryLogicB2_3.sol"; +import {AutomationRegistryLogicC2_3} from "../v2_3/AutomationRegistryLogicC2_3.sol"; +import {IAutomationRegistryMaster2_3 as Registry, AutomationRegistryBase2_3} from "../interfaces/v2_3/IAutomationRegistryMaster2_3.sol"; +import {AutomationRegistrar2_3} from "../v2_3/AutomationRegistrar2_3.sol"; +import {ChainModuleBase} from "../../chains/ChainModuleBase.sol"; +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {MockUpkeep} from "../../mocks/MockUpkeep.sol"; +import {IWrappedNative} from "../interfaces/v2_3/IWrappedNative.sol"; +import {WETH9} from "./WETH9.sol"; + +/** + * @title BaseTest provides basic test setup procedures and dependencies for use by other + * unit tests + */ contract BaseTest is Test { - address internal OWNER = 0x00007e64E1fB0C487F25dd6D3601ff6aF8d32e4e; + // test state (not exposed to derrived tests) + uint256 private nonce; + + // constants + address internal constant ZERO_ADDRESS = address(0); + uint32 internal constant DEFAULT_GAS_FEE_PPB = 10_000_000; + uint24 internal constant DEFAULT_FLAT_FEE_MILLI_CENTS = 2_000; + + // config + uint8 internal constant F = 1; // number of faulty nodes + uint64 internal constant OFFCHAIN_CONFIG_VERSION = 30; // 2 for OCR2 + + // contracts + LinkToken internal linkToken; + ERC20Mock internal usdToken; + WETH9 internal weth; + MockV3Aggregator internal LINK_USD_FEED; + MockV3Aggregator internal NATIVE_USD_FEED; + MockV3Aggregator internal USDTOKEN_USD_FEED; + MockV3Aggregator internal FAST_GAS_FEED; + MockUpkeep internal TARGET1; + MockUpkeep internal TARGET2; + Transcoder internal TRANSCODER; + + // roles + address internal constant OWNER = address(uint160(uint256(keccak256("OWNER")))); + address internal constant UPKEEP_ADMIN = address(uint160(uint256(keccak256("UPKEEP_ADMIN")))); + address internal constant FINANCE_ADMIN = address(uint160(uint256(keccak256("FINANCE_ADMIN")))); + address internal constant STRANGER = address(uint160(uint256(keccak256("STRANGER")))); + address internal constant BROKE_USER = address(uint160(uint256(keccak256("BROKE_USER")))); // do not mint to this address + address internal constant PRIVILEGE_MANAGER = address(uint160(uint256(keccak256("PRIVILEGE_MANAGER")))); + + // nodes + uint256 internal constant SIGNING_KEY0 = 0x7b2e97fe057e6de99d6872a2ef2abf52c9b4469bc848c2465ac3fcd8d336e81d; + uint256 internal constant SIGNING_KEY1 = 0xab56160806b05ef1796789248e1d7f34a6465c5280899159d645218cd216cee6; + uint256 internal constant SIGNING_KEY2 = 0x6ec7caa8406a49b76736602810e0a2871959fbbb675e23a8590839e4717f1f7f; + uint256 internal constant SIGNING_KEY3 = 0x80f14b11da94ae7f29d9a7713ea13dc838e31960a5c0f2baf45ed458947b730a; + address[] internal SIGNERS = new address[](4); + address[] internal TRANSMITTERS = new address[](4); + address[] internal NEW_TRANSMITTERS = new address[](4); + address[] internal PAYEES = new address[](4); + address[] internal NEW_PAYEES = new address[](4); function setUp() public virtual { vm.startPrank(OWNER); - deal(OWNER, 1e20); + linkToken = new LinkToken(); + linkToken.grantMintRole(OWNER); + usdToken = new ERC20Mock("MOCK_ERC20", "MOCK_ERC20", OWNER, 0); + weth = new WETH9(); + + LINK_USD_FEED = new MockV3Aggregator(8, 2_000_000_000); // $20 + NATIVE_USD_FEED = new MockV3Aggregator(8, 400_000_000_000); // $4,000 + USDTOKEN_USD_FEED = new MockV3Aggregator(8, 100_000_000); // $1 + FAST_GAS_FEED = new MockV3Aggregator(0, 1_000_000_000); // 1 gwei + + TARGET1 = new MockUpkeep(); + TARGET2 = new MockUpkeep(); + + TRANSCODER = new Transcoder(); + + SIGNERS[0] = vm.addr(SIGNING_KEY0); //0xc110458BE52CaA6bB68E66969C3218A4D9Db0211 + SIGNERS[1] = vm.addr(SIGNING_KEY1); //0xc110a19c08f1da7F5FfB281dc93630923F8E3719 + SIGNERS[2] = vm.addr(SIGNING_KEY2); //0xc110fdF6e8fD679C7Cc11602d1cd829211A18e9b + SIGNERS[3] = vm.addr(SIGNING_KEY3); //0xc11028017c9b445B6bF8aE7da951B5cC28B326C0 + + TRANSMITTERS[0] = address(uint160(uint256(keccak256("TRANSMITTER1")))); + TRANSMITTERS[1] = address(uint160(uint256(keccak256("TRANSMITTER2")))); + TRANSMITTERS[2] = address(uint160(uint256(keccak256("TRANSMITTER3")))); + TRANSMITTERS[3] = address(uint160(uint256(keccak256("TRANSMITTER4")))); + NEW_TRANSMITTERS[0] = address(uint160(uint256(keccak256("TRANSMITTER1")))); + NEW_TRANSMITTERS[1] = address(uint160(uint256(keccak256("TRANSMITTER2")))); + NEW_TRANSMITTERS[2] = address(uint160(uint256(keccak256("TRANSMITTER5")))); + NEW_TRANSMITTERS[3] = address(uint160(uint256(keccak256("TRANSMITTER6")))); + + PAYEES[0] = address(100); + PAYEES[1] = address(101); + PAYEES[2] = address(102); + PAYEES[3] = address(103); + NEW_PAYEES[0] = address(100); + NEW_PAYEES[1] = address(101); + NEW_PAYEES[2] = address(106); + NEW_PAYEES[3] = address(107); + + // mint funds + vm.deal(OWNER, 100 ether); + vm.deal(UPKEEP_ADMIN, 100 ether); + vm.deal(FINANCE_ADMIN, 100 ether); + vm.deal(STRANGER, 100 ether); + linkToken.mint(OWNER, 1000e18); + linkToken.mint(UPKEEP_ADMIN, 1000e18); + linkToken.mint(FINANCE_ADMIN, 1000e18); + linkToken.mint(STRANGER, 1000e18); + usdToken.mint(OWNER, 1000e18); + usdToken.mint(UPKEEP_ADMIN, 1000e18); + usdToken.mint(FINANCE_ADMIN, 1000e18); + usdToken.mint(STRANGER, 1000e18); + weth.mint(OWNER, 1000e18); + weth.mint(UPKEEP_ADMIN, 1000e18); + weth.mint(FINANCE_ADMIN, 1000e18); + weth.mint(STRANGER, 1000e18); + + vm.stopPrank(); + } + + /// @notice deploys the component parts of a registry, but nothing more + function deployRegistry(AutoBase.PayoutMode payoutMode) internal returns (Registry) { + AutomationForwarderLogic forwarderLogic = new AutomationForwarderLogic(); + AutomationRegistryLogicC2_3 logicC2_3 = new AutomationRegistryLogicC2_3( + address(linkToken), + address(LINK_USD_FEED), + address(NATIVE_USD_FEED), + address(FAST_GAS_FEED), + address(forwarderLogic), + ZERO_ADDRESS, + payoutMode, + address(weth) + ); + AutomationRegistryLogicB2_3 logicB2_3 = new AutomationRegistryLogicB2_3(logicC2_3); + AutomationRegistryLogicA2_3 logicA2_3 = new AutomationRegistryLogicA2_3(logicB2_3); + return Registry(payable(address(new AutomationRegistry2_3(logicA2_3)))); + } + + /// @notice deploys and configures a registry, registrar, and everything needed for most tests + function deployAndConfigureRegistryAndRegistrar( + AutoBase.PayoutMode payoutMode + ) internal returns (Registry, AutomationRegistrar2_3) { + Registry registry = deployRegistry(payoutMode); + + IERC20[] memory billingTokens = new IERC20[](3); + billingTokens[0] = IERC20(address(usdToken)); + billingTokens[1] = IERC20(address(weth)); + billingTokens[2] = IERC20(address(linkToken)); + uint256[] memory minRegistrationFees = new uint256[](billingTokens.length); + minRegistrationFees[0] = 100000000000000000000; // 100 USD + minRegistrationFees[1] = 5000000000000000000; // 5 Native + minRegistrationFees[2] = 5000000000000000000; // 5 LINK + address[] memory billingTokenAddresses = new address[](billingTokens.length); + for (uint256 i = 0; i < billingTokens.length; i++) { + billingTokenAddresses[i] = address(billingTokens[i]); + } + AutomationRegistryBase2_3.BillingConfig[] + memory billingTokenConfigs = new AutomationRegistryBase2_3.BillingConfig[](billingTokens.length); + billingTokenConfigs[0] = AutomationRegistryBase2_3.BillingConfig({ + gasFeePPB: DEFAULT_GAS_FEE_PPB, // 15% + flatFeeMilliCents: DEFAULT_FLAT_FEE_MILLI_CENTS, // 2 cents + priceFeed: address(USDTOKEN_USD_FEED), + fallbackPrice: 100_000_000, // $1 + minSpend: 1000000000000000000 // 1 USD + }); + billingTokenConfigs[1] = AutomationRegistryBase2_3.BillingConfig({ + gasFeePPB: DEFAULT_GAS_FEE_PPB, // 15% + flatFeeMilliCents: DEFAULT_FLAT_FEE_MILLI_CENTS, // 2 cents + priceFeed: address(NATIVE_USD_FEED), + fallbackPrice: 100_000_000, // $1 + minSpend: 5000000000000000000 // 5 Native + }); + billingTokenConfigs[2] = AutomationRegistryBase2_3.BillingConfig({ + gasFeePPB: DEFAULT_GAS_FEE_PPB, // 10% + flatFeeMilliCents: DEFAULT_FLAT_FEE_MILLI_CENTS, // 2 cents + priceFeed: address(LINK_USD_FEED), + fallbackPrice: 1_000_000_000, // $10 + minSpend: 1000000000000000000 // 1 LINK + }); + + if (payoutMode == AutoBase.PayoutMode.OFF_CHAIN) { + // remove LINK as a payment method if we are settling offchain + assembly { + mstore(billingTokens, 2) + mstore(minRegistrationFees, 2) + mstore(billingTokenAddresses, 2) + mstore(billingTokenConfigs, 2) + } + } + + // deploy registrar + AutomationRegistrar2_3.InitialTriggerConfig[] + memory triggerConfigs = new AutomationRegistrar2_3.InitialTriggerConfig[](2); + triggerConfigs[0] = AutomationRegistrar2_3.InitialTriggerConfig({ + triggerType: 0, // condition + autoApproveType: AutomationRegistrar2_3.AutoApproveType.DISABLED, + autoApproveMaxAllowed: 0 + }); + triggerConfigs[1] = AutomationRegistrar2_3.InitialTriggerConfig({ + triggerType: 1, // log + autoApproveType: AutomationRegistrar2_3.AutoApproveType.DISABLED, + autoApproveMaxAllowed: 0 + }); + AutomationRegistrar2_3 registrar = new AutomationRegistrar2_3( + address(linkToken), + registry, + triggerConfigs, + billingTokens, + minRegistrationFees, + IWrappedNative(address(weth)) + ); + + address[] memory registrars; + registrars = new address[](1); + registrars[0] = address(registrar); + + AutomationRegistryBase2_3.OnchainConfig memory cfg = AutomationRegistryBase2_3.OnchainConfig({ + checkGasLimit: 5_000_000, + stalenessSeconds: 90_000, + gasCeilingMultiplier: 2, + maxPerformGas: 10_000_000, + maxCheckDataSize: 5_000, + maxPerformDataSize: 5_000, + maxRevertDataSize: 5_000, + fallbackGasPrice: 20_000_000_000, + fallbackLinkPrice: 2_000_000_000, // $20 + fallbackNativePrice: 400_000_000_000, // $4,000 + transcoder: address(TRANSCODER), + registrars: registrars, + upkeepPrivilegeManager: PRIVILEGE_MANAGER, + chainModule: address(new ChainModuleBase()), + reorgProtectionEnabled: true, + financeAdmin: FINANCE_ADMIN + }); + + registry.setConfigTypeSafe( + SIGNERS, + TRANSMITTERS, + F, + cfg, + OFFCHAIN_CONFIG_VERSION, + "", + billingTokenAddresses, + billingTokenConfigs + ); + registry.setPayees(PAYEES); + return (registry, registrar); + } + + /// @notice this function updates the billing config for the provided token on the provided registry, + /// and throws an error if the token is not found + function _updateBillingTokenConfig( + Registry registry, + address billingToken, + AutomationRegistryBase2_3.BillingConfig memory newConfig + ) internal { + (, , address[] memory signers, address[] memory transmitters, uint8 f) = registry.getState(); + AutomationRegistryBase2_3.OnchainConfig memory config = registry.getConfig(); + address[] memory billingTokens = registry.getBillingTokens(); + + AutomationRegistryBase2_3.BillingConfig[] + memory billingTokenConfigs = new AutomationRegistryBase2_3.BillingConfig[](billingTokens.length); + + bool found = false; + for (uint256 i = 0; i < billingTokens.length; i++) { + if (billingTokens[i] == billingToken) { + found = true; + billingTokenConfigs[i] = newConfig; + } else { + billingTokenConfigs[i] = registry.getBillingTokenConfig(billingTokens[i]); + } + } + require(found, "could not find billing token provided on registry"); + + registry.setConfigTypeSafe( + signers, + transmitters, + f, + config, + OFFCHAIN_CONFIG_VERSION, + "", + billingTokens, + billingTokenConfigs + ); + } + + /// @notice this function removes a billing token from the registry + function _removeBillingTokenConfig(Registry registry, address billingToken) internal { + (, , address[] memory signers, address[] memory transmitters, uint8 f) = registry.getState(); + AutomationRegistryBase2_3.OnchainConfig memory config = registry.getConfig(); + address[] memory billingTokens = registry.getBillingTokens(); + + address[] memory newBillingTokens = new address[](billingTokens.length - 1); + AutomationRegistryBase2_3.BillingConfig[] + memory billingTokenConfigs = new AutomationRegistryBase2_3.BillingConfig[](billingTokens.length - 1); + + uint256 j = 0; + for (uint256 i = 0; i < billingTokens.length; i++) { + if (billingTokens[i] != billingToken) { + if (j == newBillingTokens.length) revert("could not find billing token provided on registry"); + newBillingTokens[j] = billingTokens[i]; + billingTokenConfigs[j] = registry.getBillingTokenConfig(billingTokens[i]); + j++; + } + } + + registry.setConfigTypeSafe( + signers, + transmitters, + f, + config, + OFFCHAIN_CONFIG_VERSION, + "", + newBillingTokens, + billingTokenConfigs + ); + } + + function _transmit(uint256 id, Registry registry) internal { + uint256[] memory ids = new uint256[](1); + ids[0] = id; + _transmit(ids, registry); + } + + function _transmit(uint256[] memory ids, Registry registry) internal { + uint256[] memory upkeepIds = new uint256[](ids.length); + uint256[] memory gasLimits = new uint256[](ids.length); + bytes[] memory performDatas = new bytes[](ids.length); + bytes[] memory triggers = new bytes[](ids.length); + for (uint256 i = 0; i < ids.length; i++) { + upkeepIds[i] = ids[i]; + gasLimits[i] = registry.getUpkeep(ids[i]).performGas; + performDatas[i] = new bytes(0); + uint8 triggerType = registry.getTriggerType(ids[i]); + if (triggerType == 0) { + triggers[i] = _encodeConditionalTrigger( + AutoBase.ConditionalTrigger(uint32(block.number - 1), blockhash(block.number - 1)) + ); + } else { + revert("not implemented"); + } + } + AutoBase.Report memory report = AutoBase.Report( + uint256(1000000000), + uint256(2000000000), + upkeepIds, + gasLimits, + triggers, + performDatas + ); + + bytes memory reportBytes = _encodeReport(report); + (, , bytes32 configDigest) = registry.latestConfigDetails(); + bytes32[3] memory reportContext = [configDigest, configDigest, configDigest]; + uint256[] memory signerPKs = new uint256[](2); + signerPKs[0] = SIGNING_KEY0; + signerPKs[1] = SIGNING_KEY1; + (bytes32[] memory rs, bytes32[] memory ss, bytes32 vs) = _signReport(reportBytes, reportContext, signerPKs); + + vm.startPrank(TRANSMITTERS[0]); + registry.transmit(reportContext, reportBytes, rs, ss, vs); + vm.stopPrank(); + } + + /// @notice Gather signatures on report data + /// @param report - Report bytes generated from `_buildReport` + /// @param reportContext - Report context bytes32 generated from `_buildReport` + /// @param signerPrivateKeys - One or more addresses that will sign the report data + /// @return rawRs - Signature rs + /// @return rawSs - Signature ss + /// @return rawVs - Signature vs + function _signReport( + bytes memory report, + bytes32[3] memory reportContext, + uint256[] memory signerPrivateKeys + ) internal pure returns (bytes32[] memory, bytes32[] memory, bytes32) { + bytes32[] memory rs = new bytes32[](signerPrivateKeys.length); + bytes32[] memory ss = new bytes32[](signerPrivateKeys.length); + bytes memory vs = new bytes(signerPrivateKeys.length); + + bytes32 reportDigest = keccak256(abi.encodePacked(keccak256(report), reportContext)); + + for (uint256 i = 0; i < signerPrivateKeys.length; i++) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKeys[i], reportDigest); + rs[i] = r; + ss[i] = s; + vs[i] = bytes1(v - 27); + } + + return (rs, ss, bytes32(vs)); + } + + function _encodeReport(AutoBase.Report memory report) internal pure returns (bytes memory reportBytes) { + return abi.encode(report); + } + + function _encodeConditionalTrigger( + AutoBase.ConditionalTrigger memory trigger + ) internal pure returns (bytes memory triggerBytes) { + return abi.encode(trigger.blockNum, trigger.blockHash); + } + + /// @dev mints LINK to the recipient + function _mintLink(address recipient, uint256 amount) internal { + vm.prank(OWNER); + linkToken.mint(recipient, amount); + } + + /// @dev mints USDToken to the recipient + function _mintERC20(address recipient, uint256 amount) internal { + vm.prank(OWNER); + usdToken.mint(recipient, amount); + } + + /// @dev returns a pseudo-random 32 bytes + function _random() private returns (bytes32) { + nonce++; + return keccak256(abi.encode(block.timestamp, nonce)); + } + + /// @dev returns a pseudo-random number + function randomNumber() internal returns (uint256) { + return uint256(_random()); + } + + /// @dev returns a pseudo-random address + function randomAddress() internal returns (address) { + return address(uint160(randomNumber())); + } + + /// @dev returns a pseudo-random byte array + function randomBytes(uint256 length) internal returns (bytes memory) { + bytes memory result = new bytes(length); + bytes32 entropy; + for (uint256 i = 0; i < length; i++) { + if (i % 32 == 0) { + entropy = _random(); + } + result[i] = entropy[i % 32]; + } + return result; } } diff --git a/contracts/src/v0.8/automation/dev/test/WETH9.sol b/contracts/src/v0.8/automation/dev/test/WETH9.sol new file mode 100644 index 00000000..b8452d83 --- /dev/null +++ b/contracts/src/v0.8/automation/dev/test/WETH9.sol @@ -0,0 +1,93 @@ +// Submitted for verification at Etherscan.io on 2017-12-12 + +// Copyright (C) 2015, 2016, 2017 Dapphub + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +pragma solidity 0.8.19; + +contract WETH9 { + string public name = "Wrapped Ether"; + string public symbol = "WETH"; + uint8 public decimals = 18; + + event Approval(address indexed src, address indexed guy, uint256 wad); + event Transfer(address indexed src, address indexed dst, uint256 wad); + event Deposit(address indexed dst, uint256 wad); + event Withdrawal(address indexed src, uint256 wad); + + error InsufficientBalance(); + + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + + receive() external payable { + _deposit(); + } + + function _deposit() internal { + balanceOf[msg.sender] += msg.value; + emit Deposit(msg.sender, msg.value); + } + + function deposit() external payable { + _deposit(); + } + + function mint(address account, uint256 amount) public { + balanceOf[account] += amount; + } + + function withdraw(uint256 wad) external { + if (balanceOf[msg.sender] < wad) { + revert InsufficientBalance(); + } + balanceOf[msg.sender] -= wad; + payable(msg.sender).transfer(wad); + emit Withdrawal(msg.sender, wad); + } + + function totalSupply() public view returns (uint256) { + return address(this).balance; + } + + function approve(address guy, uint256 wad) public returns (bool) { + allowance[msg.sender][guy] = wad; + emit Approval(msg.sender, guy, wad); + return true; + } + + function transfer(address dst, uint256 wad) public returns (bool) { + return transferFrom(msg.sender, dst, wad); + } + + function transferFrom(address src, address dst, uint256 wad) public returns (bool) { + if (balanceOf[src] < wad) { + revert InsufficientBalance(); + } + + if (src != msg.sender && allowance[src][msg.sender] != type(uint128).max) { + if (allowance[src][msg.sender] < wad) { + revert InsufficientBalance(); + } + allowance[src][msg.sender] -= wad; + } + + balanceOf[src] -= wad; + balanceOf[dst] += wad; + + emit Transfer(src, dst, wad); + + return true; + } +} diff --git a/contracts/src/v0.8/automation/dev/v2_3/AutomationRegistrar2_3.sol b/contracts/src/v0.8/automation/dev/v2_3/AutomationRegistrar2_3.sol index 6614a5fa..ed0dd717 100644 --- a/contracts/src/v0.8/automation/dev/v2_3/AutomationRegistrar2_3.sol +++ b/contracts/src/v0.8/automation/dev/v2_3/AutomationRegistrar2_3.sol @@ -6,6 +6,9 @@ import {IAutomationRegistryMaster2_3} from "../interfaces/v2_3/IAutomationRegist import {TypeAndVersionInterface} from "../../../interfaces/TypeAndVersionInterface.sol"; import {ConfirmedOwner} from "../../../shared/access/ConfirmedOwner.sol"; import {IERC677Receiver} from "../../../shared/interfaces/IERC677Receiver.sol"; +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {IWrappedNative} from "../interfaces/v2_3/IWrappedNative.sol"; +import {SafeCast} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/math/SafeCast.sol"; /** * @notice Contract to accept requests for upkeep registrations @@ -29,13 +32,6 @@ contract AutomationRegistrar2_3 is TypeAndVersionInterface, ConfirmedOwner, IERC 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.3.0: Update for compatability with registry 2.3.0 @@ -75,32 +71,49 @@ contract AutomationRegistrar2_3 is TypeAndVersionInterface, ConfirmedOwner, IERC uint32 autoApproveMaxAllowed; } - struct RegistrarConfig { - IAutomationRegistryMaster2_3 AutomationRegistry; - uint96 minLINKJuels; - } - struct PendingRequest { address admin; uint96 balance; } - + /** + * @member upkeepContract address to perform upkeep on + * @member amount quantity of LINK upkeep is funded with (specified in wei) + * @member adminAddress address to cancel upkeep and withdraw remaining funds + * @member gasLimit amount of gas to provide the target contract when performing upkeep + * @member triggerType the type of trigger for the upkeep + * @member billingToken the token to pay with + * @member name string of the upkeep to be registered + * @member encryptedEmail email address of upkeep contact + * @member checkData data passed to the contract when checking for upkeep + * @member triggerConfig the config for the trigger + * @member offchainConfig offchainConfig for upkeep in bytes + */ struct RegistrationParams { - string name; - bytes encryptedEmail; address upkeepContract; - uint32 gasLimit; + uint96 amount; + // 1 word full address adminAddress; + uint32 gasLimit; uint8 triggerType; + // 7 bytes left in 2nd word + IERC20 billingToken; + // 12 bytes left in 3rd word + string name; + bytes encryptedEmail; bytes checkData; bytes triggerConfig; bytes offchainConfig; - uint96 amount; } - RegistrarConfig private s_config; - // Only applicable if s_config.configType is ENABLED_SENDER_ALLOWLIST + LinkTokenInterface public immutable i_LINK; + IWrappedNative public immutable i_WRAPPED_NATIVE_TOKEN; + IAutomationRegistryMaster2_3 private s_registry; + + // Only applicable if trigger config is set to ENABLED_SENDER_ALLOWLIST mapping(address => bool) private s_autoApproveAllowedSenders; + mapping(IERC20 => uint256) private s_minRegistrationAmounts; + mapping(bytes32 => PendingRequest) private s_pendingRequests; + mapping(uint8 => TriggerRegistrationStorage) private s_triggerRegistrations; event RegistrationRequested( bytes32 indexed hash, @@ -122,37 +135,39 @@ contract AutomationRegistrar2_3 is TypeAndVersionInterface, ConfirmedOwner, IERC event AutoApproveAllowedSenderSet(address indexed senderAddress, bool allowed); - event ConfigChanged(address AutomationRegistry, uint96 minLINKJuels); + event ConfigChanged(); 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 InvalidAdminAddress(); + error InvalidBillingToken(); error InvalidDataLength(); + error TransferFailed(address to); + error OnlyAdminOrOwner(); + error OnlyLink(); + error RequestNotFound(); /** * @param LINKAddress Address of Link token - * @param AutomationRegistry keeper registry address - * @param minLINKJuels minimum LINK that new registrations should fund their upkeep with + * @param registry keeper registry address * @param triggerConfigs the initial config for individual triggers + * @param billingTokens the tokens allowed for billing + * @param minRegistrationFees the minimum amount for registering with each billing token + * @param wrappedNativeToken wrapped native token */ constructor( address LINKAddress, - address AutomationRegistry, - uint96 minLINKJuels, - InitialTriggerConfig[] memory triggerConfigs + IAutomationRegistryMaster2_3 registry, + InitialTriggerConfig[] memory triggerConfigs, + IERC20[] memory billingTokens, + uint256[] memory minRegistrationFees, + IWrappedNative wrappedNativeToken ) ConfirmedOwner(msg.sender) { - LINK = LinkTokenInterface(LINKAddress); - setConfig(AutomationRegistry, minLINKJuels); + i_LINK = LinkTokenInterface(LINKAddress); + i_WRAPPED_NATIVE_TOKEN = wrappedNativeToken; + setConfig(registry, billingTokens, minRegistrationFees); for (uint256 idx = 0; idx < triggerConfigs.length; idx++) { setTriggerConfig( triggerConfigs[idx].triggerType, @@ -164,104 +179,41 @@ contract AutomationRegistrar2_3 is TypeAndVersionInterface, ConfirmedOwner, IERC //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(); + function registerUpkeep(RegistrationParams memory requestParams) external payable returns (uint256) { + if (requestParams.billingToken == IERC20(i_WRAPPED_NATIVE_TOKEN) && msg.value != 0) { + requestParams.amount = SafeCast.toUint96(msg.value); + // wrap and send native payment + i_WRAPPED_NATIVE_TOKEN.deposit{value: msg.value}(); + } else { + // send ERC20 payment, including wrapped native token + if (!requestParams.billingToken.transferFrom(msg.sender, address(this), requestParams.amount)) { + revert TransferFailed(address(this)); + } } - LINK.transferFrom(msg.sender, address(this), requestParams.amount); - return _register(requestParams, msg.sender); } /** * @dev register upkeep on AutomationRegistry contract and emit RegistrationApproved event + * @param requestParams struct of all possible registration parameters + * @param hash the committment of the registration request */ - 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 { + function approve(RegistrationParams calldata requestParams, 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) - ); + bytes32 expectedHash = keccak256(abi.encode(requestParams)); 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 - ); + _approve(requestParams, expectedHash); } /** @@ -277,24 +229,30 @@ contract AutomationRegistrar2_3 is TypeAndVersionInterface, ConfirmedOwner, IERC revert RequestNotFound(); } delete s_pendingRequests[hash]; - bool success = LINK.transfer(request.admin, request.balance); + bool success = i_LINK.transfer(request.admin, request.balance); if (!success) { - revert LinkTransferFailed(request.admin); + revert TransferFailed(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 + * @param registry new keeper registry address + * @param billingTokens the billing tokens that this registrar supports (registy must also support these) + * @param minBalances minimum balances that users must supply to register with the corresponding billing token */ - function setConfig(address AutomationRegistry, uint96 minLINKJuels) public onlyOwner { - s_config = RegistrarConfig({ - minLINKJuels: minLINKJuels, - AutomationRegistry: IAutomationRegistryMaster2_3(AutomationRegistry) - }); - emit ConfigChanged(AutomationRegistry, minLINKJuels); + function setConfig( + IAutomationRegistryMaster2_3 registry, + IERC20[] memory billingTokens, + uint256[] memory minBalances + ) public onlyOwner { + if (billingTokens.length != minBalances.length) revert InvalidDataLength(); + s_registry = registry; + for (uint256 i = 0; i < billingTokens.length; i++) { + s_minRegistrationAmounts[billingTokens[i]] = minBalances[i]; + } + emit ConfigChanged(); } /** @@ -333,11 +291,17 @@ contract AutomationRegistrar2_3 is TypeAndVersionInterface, ConfirmedOwner, IERC } /** - * @notice read the current registration configuration + * @notice gets the registry that this registrar is pointed to */ - function getConfig() external view returns (address AutomationRegistry, uint256 minLINKJuels) { - RegistrarConfig memory config = s_config; - return (address(config.AutomationRegistry), config.minLINKJuels); + function getRegistry() external view returns (IAutomationRegistryMaster2_3) { + return s_registry; + } + + /** + * @notice get the minimum registration fee for a particular billing token + */ + function getMinimumRegistrationAmount(IERC20 billingToken) external view returns (uint256) { + return s_minRegistrationAmounts[billingToken]; } /** @@ -362,26 +326,12 @@ contract AutomationRegistrar2_3 is TypeAndVersionInterface, ConfirmedOwner, IERC * @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(); - } + function onTokenTransfer(address sender, uint256 amount, bytes calldata data) external override { + if (msg.sender != address(i_LINK)) revert OnlyLink(); + RegistrationParams memory params = abi.decode(data, (RegistrationParams)); + if (address(params.billingToken) != address(i_LINK)) revert OnlyLink(); + params.amount = uint96(amount); // ignore whatever is sent in registration params, use actual value; casting safe because max supply LINK < 2^96 + _register(params, sender); } // ================================================================ @@ -390,22 +340,21 @@ contract AutomationRegistrar2_3 is TypeAndVersionInterface, ConfirmedOwner, IERC /** * @dev verify registration request and emit RegistrationRequested event + * @dev we currently allow multiple duplicate registrations by adding to the original registration's balance + * we could make this much simpler by using a nonce to differentiate otherwise identical requests and then + * we don't have to worry about identical registrations */ function _register(RegistrationParams memory params, address sender) private returns (uint256) { + if (params.amount < s_minRegistrationAmounts[params.billingToken]) { + revert InsufficientPayment(); + } 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 - ) - ); + if (!s_registry.supportsBillingToken(address(params.billingToken))) { + revert InvalidBillingToken(); + } + bytes32 hash = keccak256(abi.encode(params)); emit RegistrationRequested( hash, @@ -437,19 +386,28 @@ contract AutomationRegistrar2_3 is TypeAndVersionInterface, ConfirmedOwner, IERC * @dev register upkeep on AutomationRegistry contract and emit RegistrationApproved event */ function _approve(RegistrationParams memory params, bytes32 hash) private returns (uint256) { - IAutomationRegistryMaster2_3 AutomationRegistry = s_config.AutomationRegistry; - uint256 upkeepId = AutomationRegistry.registerUpkeep( + IAutomationRegistryMaster2_3 registry = s_registry; + uint256 upkeepId = registry.registerUpkeep( params.upkeepContract, params.gasLimit, params.adminAddress, params.triggerType, + address(params.billingToken), // have to cast as address because master interface doesn't use contract types params.checkData, params.triggerConfig, params.offchainConfig ); - bool success = LINK.transferAndCall(address(AutomationRegistry), params.amount, abi.encode(upkeepId)); + bool success; + if (address(params.billingToken) == address(i_LINK)) { + success = i_LINK.transferAndCall(address(registry), params.amount, abi.encode(upkeepId)); + } else { + success = params.billingToken.approve(address(registry), params.amount); + if (success) { + registry.addFunds(upkeepId, params.amount); + } + } if (!success) { - revert LinkTransferFailed(address(AutomationRegistry)); + revert TransferFailed(address(registry)); } emit RegistrationApproved(hash, params.name, upkeepId); return upkeepId; @@ -470,68 +428,4 @@ contract AutomationRegistrar2_3 is TypeAndVersionInterface, ConfirmedOwner, IERC } 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_3/AutomationRegistry2_3.sol b/contracts/src/v0.8/automation/dev/v2_3/AutomationRegistry2_3.sol index 2b9a6a4f..074d9c93 100644 --- a/contracts/src/v0.8/automation/dev/v2_3/AutomationRegistry2_3.sol +++ b/contracts/src/v0.8/automation/dev/v2_3/AutomationRegistry2_3.sol @@ -4,11 +4,13 @@ pragma solidity 0.8.19; import {EnumerableSet} from "../../../vendor/openzeppelin-solidity/v4.7.3/contracts/utils/structs/EnumerableSet.sol"; import {Address} from "../../../vendor/openzeppelin-solidity/v4.7.3/contracts/utils/Address.sol"; import {AutomationRegistryBase2_3} from "./AutomationRegistryBase2_3.sol"; -import {AutomationRegistryLogicB2_3} from "./AutomationRegistryLogicB2_3.sol"; +import {AutomationRegistryLogicA2_3} from "./AutomationRegistryLogicA2_3.sol"; +import {AutomationRegistryLogicC2_3} from "./AutomationRegistryLogicC2_3.sol"; import {Chainable} from "../../Chainable.sol"; import {IERC677Receiver} from "../../../shared/interfaces/IERC677Receiver.sol"; import {OCR2Abstract} from "../../../shared/ocr2/OCR2Abstract.sol"; import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {SafeCast} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/math/SafeCast.sol"; /** * @notice Registry for adding work for Chainlink nodes to perform on client @@ -22,6 +24,8 @@ contract AutomationRegistry2_3 is AutomationRegistryBase2_3, OCR2Abstract, Chain /** * @notice versions: * AutomationRegistry 2.3.0: supports native and ERC20 billing + * changes flat fee to USD-denominated + * adds support for custom billing overrides * AutomationRegistry 2.2.0: moves chain-specific integration code into a separate module * KeeperRegistry 2.1.0: introduces support for log triggers * removes the need for "wrapped perform data" @@ -44,18 +48,21 @@ contract AutomationRegistry2_3 is AutomationRegistryBase2_3, OCR2Abstract, Chain string public constant override typeAndVersion = "AutomationRegistry 2.3.0"; /** - * @param logicA the address of the first logic contract, but cast as logicB in order to call logicB functions (via fallback) + * @param logicA the address of the first logic contract + * @dev we cast the contract to logicC in order to call logicC functions (via fallback) */ constructor( - AutomationRegistryLogicB2_3 logicA + AutomationRegistryLogicA2_3 logicA ) AutomationRegistryBase2_3( - logicA.getLinkAddress(), - logicA.getLinkUSDFeedAddress(), - logicA.getNativeUSDFeedAddress(), - logicA.getFastGasFeedAddress(), - logicA.getAutomationForwarderLogic(), - logicA.getAllowedReadOnlyAddress() + AutomationRegistryLogicC2_3(address(logicA)).getLinkAddress(), + AutomationRegistryLogicC2_3(address(logicA)).getLinkUSDFeedAddress(), + AutomationRegistryLogicC2_3(address(logicA)).getNativeUSDFeedAddress(), + AutomationRegistryLogicC2_3(address(logicA)).getFastGasFeedAddress(), + AutomationRegistryLogicC2_3(address(logicA)).getAutomationForwarderLogic(), + AutomationRegistryLogicC2_3(address(logicA)).getAllowedReadOnlyAddress(), + AutomationRegistryLogicC2_3(address(logicA)).getPayoutMode(), + AutomationRegistryLogicC2_3(address(logicA)).getWrappedNativeTokenAddress() ) Chainable(address(logicA)) {} @@ -65,13 +72,13 @@ contract AutomationRegistry2_3 is AutomationRegistryBase2_3, OCR2Abstract, Chain */ struct TransmitVars { uint16 numUpkeepsPassedChecks; - uint256 totalCalldataWeight; uint96 totalReimbursement; uint96 totalPremium; + uint256 totalCalldataWeight; } // ================================================================ - // | ACTIONS | + // | HOT PATH ACTIONS | // ================================================================ /** @@ -107,6 +114,15 @@ contract AutomationRegistry2_3 is AutomationRegistryBase2_3, OCR2Abstract, Chain } } + /** + * @notice handles the report by performing the upkeeps and updating the state + * @param hotVars the hot variables of the registry + * @param report the report to be handled (already verified and decoded) + * @param gasOverhead the running tally of gas overhead to be split across the upkeeps + * @dev had to split this function from transmit() to avoid stack too deep errors + * @dev all other internal / private functions are generally defined in the Base contract + * we leave this here because it is essentially a continuation of the transmit() function, + */ function _handleReport(HotVars memory hotVars, Report memory report, uint256 gasOverhead) private { UpkeepTransmitInfo[] memory upkeepTransmitInfo = new UpkeepTransmitInfo[](report.upkeepIds.length); TransmitVars memory transmitVars = TransmitVars({ @@ -152,7 +168,7 @@ contract AutomationRegistry2_3 is AutomationRegistryBase2_3, OCR2Abstract, Chain (TRANSMIT_CALLDATA_PER_SIGNER_BYTES_OVERHEAD * (hotVars.f + 1)); transmitVars.totalCalldataWeight += upkeepTransmitInfo[i].calldataWeight; - // Deduct that gasUsed by upkeep from our running counter + // Deduct the gasUsed by upkeep from the overhead tally - upkeeps pay for their own gas individually gasOverhead -= upkeepTransmitInfo[i].gasUsed; // Store last perform block number / deduping key for upkeep @@ -169,8 +185,13 @@ contract AutomationRegistry2_3 is AutomationRegistryBase2_3, OCR2Abstract, Chain gasOverhead = gasOverhead / transmitVars.numUpkeepsPassedChecks + ACCOUNTING_PER_UPKEEP_GAS_OVERHEAD; { + BillingTokenPaymentParams memory billingTokenParams; + uint256 nativeUSD = _getNativeUSD(hotVars); for (uint256 i = 0; i < report.upkeepIds.length; i++) { if (upkeepTransmitInfo[i].earlyChecksPassed) { + if (i == 0 || upkeepTransmitInfo[i].upkeep.billingToken != upkeepTransmitInfo[i - 1].upkeep.billingToken) { + billingTokenParams = _getBillingTokenPaymentParams(hotVars, upkeepTransmitInfo[i].upkeep.billingToken); + } PaymentReceipt memory receipt = _handlePayment( hotVars, PaymentParams({ @@ -179,18 +200,21 @@ contract AutomationRegistry2_3 is AutomationRegistryBase2_3, OCR2Abstract, Chain l1CostWei: (l1Fee * upkeepTransmitInfo[i].calldataWeight) / transmitVars.totalCalldataWeight, fastGasWei: report.fastGasWei, linkUSD: report.linkUSD, - nativeUSD: _getNativeUSD(hotVars), + nativeUSD: nativeUSD, + billingToken: upkeepTransmitInfo[i].upkeep.billingToken, + billingTokenParams: billingTokenParams, isTransaction: true }), - report.upkeepIds[i] + report.upkeepIds[i], + upkeepTransmitInfo[i].upkeep ); - transmitVars.totalPremium += receipt.premium; - transmitVars.totalReimbursement += receipt.reimbursement; + transmitVars.totalPremium += receipt.premiumJuels; + transmitVars.totalReimbursement += receipt.gasReimbursementJuels; emit UpkeepPerformed( report.upkeepIds[i], upkeepTransmitInfo[i].performSuccess, - receipt.reimbursement + receipt.premium, + receipt.gasReimbursementJuels + receipt.premiumJuels, // TODO - this is currently the LINK amount, but may change to billing token upkeepTransmitInfo[i].gasUsed, gasOverhead, report.triggers[i] @@ -201,25 +225,38 @@ contract AutomationRegistry2_3 is AutomationRegistryBase2_3, OCR2Abstract, Chain // record payments s_transmitters[msg.sender].balance += transmitVars.totalReimbursement; s_hotVars.totalPremium += transmitVars.totalPremium; + s_reserveAmounts[IERC20(address(i_link))] += transmitVars.totalReimbursement + transmitVars.totalPremium; } /** - * @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 + * @notice adds fund to an upkeep + * @param id the upkeepID + * @param amount the amount of funds to add, in the upkeep's billing token */ - function simulatePerformUpkeep( - uint256 id, - bytes calldata performData - ) external returns (bool success, uint256 gasUsed) { - _preventExecution(); - - if (s_hotVars.paused) revert RegistryPaused(); + function addFunds(uint256 id, uint96 amount) external payable { Upkeep memory upkeep = s_upkeep[id]; - (success, gasUsed) = _performUpkeep(upkeep.forwarder, upkeep.performGas, performData); - return (success, gasUsed); + if (upkeep.maxValidBlocknumber != UINT32_MAX) revert UpkeepCancelled(); + + if (msg.value != 0) { + if (upkeep.billingToken != IERC20(i_wrappedNativeToken)) { + revert InvalidToken(); + } + amount = SafeCast.toUint96(msg.value); + } + + s_upkeep[id].balance = upkeep.balance + amount; + s_reserveAmounts[upkeep.billingToken] = s_reserveAmounts[upkeep.billingToken] + amount; + + if (msg.value == 0) { + // ERC20 payment + bool success = upkeep.billingToken.transferFrom(msg.sender, address(this), amount); + if (!success) revert TransferFailed(); + } else { + // native payment + i_wrappedNativeToken.deposit{value: amount}(); + } + + emit FundsAdded(id, msg.sender, amount); } /** @@ -233,18 +270,20 @@ contract AutomationRegistry2_3 is AutomationRegistryBase2_3, OCR2Abstract, Chain if (data.length != 32) revert InvalidDataLength(); uint256 id = abi.decode(data, (uint256)); if (s_upkeep[id].maxValidBlocknumber != UINT32_MAX) revert UpkeepCancelled(); + if (address(s_upkeep[id].billingToken) != address(i_link)) revert InvalidToken(); s_upkeep[id].balance = s_upkeep[id].balance + uint96(amount); - s_reserveAmounts[address(i_link)] = s_reserveAmounts[address(i_link)] + amount; + s_reserveAmounts[IERC20(address(i_link))] = s_reserveAmounts[IERC20(address(i_link))] + amount; emit FundsAdded(id, sender, uint96(amount)); } // ================================================================ - // | SETTERS | + // | OCR2ABSTRACT | // ================================================================ /** * @inheritdoc OCR2Abstract * @dev prefer the type-safe version of setConfig (below) whenever possible. The OnchainConfig could differ between registry versions + * @dev this function takes up precious space on the root contract, but must be implemented to conform to the OCR2Abstract interface */ function setConfig( address[] memory signers, @@ -271,6 +310,17 @@ contract AutomationRegistry2_3 is AutomationRegistryBase2_3, OCR2Abstract, Chain ); } + /** + * @notice sets the configuration for the registry + * @param signers the list of permitted signers + * @param transmitters the list of permitted transmitters + * @param f the maximum tolerance for faulty nodes + * @param onchainConfig configuration values that are used on-chain + * @param offchainConfigVersion the version of the offchainConfig + * @param offchainConfig configuration values that are used off-chain + * @param billingTokens the list of valid billing tokens + * @param billingConfigs the configurations for each billing token + */ function setConfigTypeSafe( address[] memory signers, address[] memory transmitters, @@ -288,56 +338,10 @@ contract AutomationRegistry2_3 is AutomationRegistryBase2_3, OCR2Abstract, Chain // set billing config for tokens _setBillingConfig(billingTokens, billingConfigs); - // move all pooled payments out of the pool to each transmitter's balance - for (uint256 i = 0; i < s_transmittersList.length; i++) { - _updateTransmitterBalanceFromPool( - s_transmittersList[i], - s_hotVars.totalPremium, - uint96(s_transmittersList.length) - ); - } - - // remove any old signer/transmitter addresses - address signerAddress; - address transmitterAddress; - for (uint256 i = 0; i < s_transmittersList.length; 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 = s_hotVars.totalPremium; - s_transmitters[temp] = transmitter; - } - } - s_signersList = signers; - s_transmittersList = transmitters; + _updateTransmitters(signers, transmitters); s_hotVars = HotVars({ f: f, - paymentPremiumPPB: onchainConfig.paymentPremiumPPB, - flatFeeMicroLink: onchainConfig.flatFeeMicroLink, stalenessSeconds: onchainConfig.stalenessSeconds, gasCeilingMultiplier: onchainConfig.gasCeilingMultiplier, paused: s_hotVars.paused, @@ -350,7 +354,6 @@ contract AutomationRegistry2_3 is AutomationRegistryBase2_3, OCR2Abstract, Chain s_storage = Storage({ checkGasLimit: onchainConfig.checkGasLimit, - minUpkeepSpend: onchainConfig.minUpkeepSpend, maxPerformGas: onchainConfig.maxPerformGas, transcoder: onchainConfig.transcoder, maxCheckDataSize: onchainConfig.maxCheckDataSize, @@ -384,9 +387,7 @@ contract AutomationRegistry2_3 is AutomationRegistryBase2_3, OCR2Abstract, Chain offchainConfig ); - for (uint256 idx = 0; idx < s_registrars.length(); idx++) { - s_registrars.remove(s_registrars.at(idx)); - } + delete s_registrars; for (uint256 idx = 0; idx < onchainConfig.registrars.length; idx++) { s_registrars.add(onchainConfig.registrars[idx]); @@ -405,12 +406,9 @@ contract AutomationRegistry2_3 is AutomationRegistryBase2_3, OCR2Abstract, Chain ); } - // ================================================================ - // | GETTERS | - // ================================================================ - /** * @inheritdoc OCR2Abstract + * @dev this function takes up precious space on the root contract, but must be implemented to conform to the OCR2Abstract interface */ function latestConfigDetails() external @@ -423,6 +421,7 @@ contract AutomationRegistry2_3 is AutomationRegistryBase2_3, OCR2Abstract, Chain /** * @inheritdoc OCR2Abstract + * @dev this function takes up precious space on the root contract, but must be implemented to conform to the OCR2Abstract interface */ function latestConfigDigestAndEpoch() external diff --git a/contracts/src/v0.8/automation/dev/v2_3/AutomationRegistryBase2_3.sol b/contracts/src/v0.8/automation/dev/v2_3/AutomationRegistryBase2_3.sol index eaca9d09..087d907a 100644 --- a/contracts/src/v0.8/automation/dev/v2_3/AutomationRegistryBase2_3.sol +++ b/contracts/src/v0.8/automation/dev/v2_3/AutomationRegistryBase2_3.sol @@ -10,9 +10,10 @@ 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"; import {IChainModule} from "../../interfaces/IChainModule.sol"; import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {SafeCast} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/math/SafeCast.sol"; +import {IWrappedNative} from "../interfaces/v2_3/IWrappedNative.sol"; /** * @notice Base Keeper Registry contract, contains shared logic between @@ -35,22 +36,15 @@ abstract contract AutomationRegistryBase2_3 is ConfirmedOwner { 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; + uint8 internal constant UPKEEP_VERSION_BASE = 4; // Next block of constants are only used in maxPayment estimation during checkUpkeep simulation // These values are calibrated using hardhat tests which simulates various cases and verifies that // the variables result in accurate estimation - uint256 internal constant REGISTRY_CONDITIONAL_OVERHEAD = 60_000; // Fixed gas overhead for conditional upkeeps - uint256 internal constant REGISTRY_LOG_OVERHEAD = 85_000; // Fixed gas overhead for log upkeeps + uint256 internal constant REGISTRY_CONDITIONAL_OVERHEAD = 93_000; // Fixed gas overhead for conditional upkeeps + uint256 internal constant REGISTRY_LOG_OVERHEAD = 118_000; // Fixed gas overhead for log upkeeps uint256 internal constant REGISTRY_PER_SIGNER_GAS_OVERHEAD = 5_600; // Value scales with f uint256 internal constant REGISTRY_PER_PERFORM_BYTE_GAS_OVERHEAD = 24; // Per perform data byte overhead @@ -64,8 +58,8 @@ abstract contract AutomationRegistryBase2_3 is ConfirmedOwner { // tx itself, but since payment processing itself takes gas, and it needs the overhead as input, we use fixed constants // to account for gas used in payment processing. These values are calibrated using hardhat tests which simulates various cases and verifies that // the variables result in accurate estimation - uint256 internal constant ACCOUNTING_FIXED_GAS_OVERHEAD = 22_000; // Fixed overhead per tx - uint256 internal constant ACCOUNTING_PER_UPKEEP_GAS_OVERHEAD = 7_000; // Overhead per upkeep performed in batch + uint256 internal constant ACCOUNTING_FIXED_GAS_OVERHEAD = 51_000; // Fixed overhead per tx + uint256 internal constant ACCOUNTING_PER_UPKEEP_GAS_OVERHEAD = 9_000; // Overhead per upkeep performed in batch LinkTokenInterface internal immutable i_link; AggregatorV3Interface internal immutable i_linkUSDFeed; @@ -73,6 +67,7 @@ abstract contract AutomationRegistryBase2_3 is ConfirmedOwner { AggregatorV3Interface internal immutable i_fastGasFeed; address internal immutable i_automationForwarderLogic; address internal immutable i_allowedReadOnlyAddress; + IWrappedNative internal immutable i_wrappedNativeToken; /** * @dev - The storage is gas optimised for one and only one function - transmit. All the storage accessed in transmit @@ -92,6 +87,7 @@ abstract contract AutomationRegistryBase2_3 is ConfirmedOwner { mapping(address => Signer) internal s_signers; address[] internal s_signersList; // s_signersList contains the signing address of each oracle address[] internal s_transmittersList; // s_transmittersList contains the transmission address of each oracle + EnumerableSet.AddressSet internal s_deactivatedTransmitters; mapping(address => address) internal s_transmitterPayees; // s_payees contains the mapping from transmitter to payee. mapping(address => address) internal s_proposedPayee; // proposed payee for a transmitter bytes32 internal s_latestConfigDigest; // Read on transmit path in case of signature verification @@ -100,15 +96,17 @@ abstract contract AutomationRegistryBase2_3 is ConfirmedOwner { uint256 internal s_fallbackGasPrice; uint256 internal s_fallbackLinkPrice; uint256 internal s_fallbackNativePrice; - mapping(address billingToken => uint256 reserveAmount) internal s_reserveAmounts; // unspent user deposits + unwithdrawn NOP payments mapping(address => MigrationPermission) internal s_peerRegistryMigrationPermission; // Permissions for migration to and fro mapping(uint256 => bytes) internal s_upkeepTriggerConfig; // upkeep triggers mapping(uint256 => bytes) internal s_upkeepOffchainConfig; // general config set by users for each upkeep mapping(uint256 => bytes) internal s_upkeepPrivilegeConfig; // general config set by an administrative role for an upkeep mapping(address => bytes) internal s_adminPrivilegeConfig; // general config set by an administrative role for an admin // billing + mapping(IERC20 billingToken => uint256 reserveAmount) internal s_reserveAmounts; // unspent user deposits + unwithdrawn NOP payments mapping(IERC20 billingToken => BillingConfig billingConfig) internal s_billingConfigs; // billing configurations for different tokens + mapping(uint256 upkeepID => BillingOverrides billingOverrides) internal s_billingOverrides; // billing overrides for specific upkeeps IERC20[] internal s_billingTokens; // list of billing tokens + PayoutMode internal s_payoutMode; error ArrayHasNoEntries(); error CannotCancel(); @@ -123,6 +121,7 @@ abstract contract AutomationRegistryBase2_3 is ConfirmedOwner { error IncorrectNumberOfSigners(); error IndexOutOfRange(); error InsufficientBalance(uint256 available, uint256 requested); + error InsufficientLinkLiquidity(); error InvalidDataLength(); error InvalidFeed(); error InvalidTrigger(); @@ -130,11 +129,12 @@ abstract contract AutomationRegistryBase2_3 is ConfirmedOwner { error InvalidRecipient(); error InvalidReport(); error InvalidSigner(); + error InvalidToken(); error InvalidTransmitter(); error InvalidTriggerType(); - error MaxCheckDataSizeCanOnlyIncrease(); - error MaxPerformDataSizeCanOnlyIncrease(); error MigrationNotPermitted(); + error MustSettleOffchain(); + error MustSettleOnchain(); error NotAContract(); error OnlyActiveSigners(); error OnlyActiveTransmitters(); @@ -151,7 +151,6 @@ abstract contract AutomationRegistryBase2_3 is ConfirmedOwner { error OnlySimulatedBackend(); error OnlyUnpausedUpkeep(); error ParameterLengthError(); - error PaymentGreaterThanAllLINK(); error ReentrantCall(); error RegistryPaused(); error RepeatedSigner(); @@ -192,62 +191,19 @@ abstract contract AutomationRegistryBase2_3 is ConfirmedOwner { REGISTRY_PAUSED } - /** - * @notice OnchainConfigLegacy 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 OnchainConfigLegacy { - 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; + enum PayoutMode { + ON_CHAIN, + OFF_CHAIN } /** * @notice OnchainConfig of the registry * @dev used only in setConfig() - * @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 @@ -261,137 +217,82 @@ abstract contract AutomationRegistryBase2_3 is ConfirmedOwner { * @member chainModule the chain specific module */ 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; + address transcoder; + // 1 word full + bool reorgProtectionEnabled; + uint24 stalenessSeconds; uint32 maxPerformDataSize; uint32 maxRevertDataSize; + address upkeepPrivilegeManager; + // 2 words full + uint16 gasCeilingMultiplier; + address financeAdmin; + // 3 words uint256 fallbackGasPrice; uint256 fallbackLinkPrice; uint256 fallbackNativePrice; - address transcoder; address[] registrars; - address upkeepPrivilegeManager; IChainModule chainModule; - bool reorgProtectionEnabled; - address financeAdmin; // TODO: pack this struct better - } - - /** - * @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 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 overridesEnabled if this upkeep has overrides enabled * @member performGas the gas limit of upkeep execution * @member maxValidBlocknumber until which block this upkeep is valid * @member forwarder the forwarder contract to use for this upkeep - * @member amountSpent the amount this upkeep has spent + * @member amountSpent the amount this upkeep has spent, in the upkeep's billing token * @member balance the balance of this upkeep * @member lastPerformedBlockNumber the last block number when this upkeep was performed */ struct Upkeep { bool paused; + bool overridesEnabled; uint32 performGas; uint32 maxValidBlocknumber; IAutomationForwarder forwarder; - // 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; + // 2 bytes left in 1st EVM word - read in transmit path + uint128 amountSpent; uint96 balance; - address admin; - uint64 maxValidBlocknumber; uint32 lastPerformedBlockNumber; - uint96 amountSpent; - bool paused; - bytes offchainConfig; + // 0 bytes left in 2nd EVM word - written in transmit path + IERC20 billingToken; + // 12 bytes left in 3rd EVM word - read in transmit path } /// @dev Config + State storage struct which is on hot transmit path struct HotVars { uint96 totalPremium; // ─────────╮ total historical payment to oracles for premium - uint32 paymentPremiumPPB; // │ premium percentage charged to user over tx cost - uint32 flatFeeMicroLink; // │ flat fee charged to user for every perform uint32 latestEpoch; // │ latest epoch for which a report was transmitted uint24 stalenessSeconds; // │ Staleness tolerance for feeds uint16 gasCeilingMultiplier; // │ multiplier on top of fast gas feed for upper bound uint8 f; // │ maximum number of faulty oracles bool paused; // │ pause switch for all upkeeps in the registry - bool reentrancyGuard; // ────────╯ guard against reentrancy - bool reorgProtectionEnabled; // if this registry should enable re-org protection mechanism + bool reentrancyGuard; // | guard against reentrancy + bool reorgProtectionEnabled; // ─╯ if this registry should enable the re-org protection mechanism IChainModule chainModule; // the interface of chain specific module } /// @dev Config + State storage struct which is not on hot transmit path struct Storage { - uint96 minUpkeepSpend; // Minimum amount an upkeep must spend address transcoder; // Address of transcoder contract used in migrations - // 1 EVM word full 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. + // 1 EVM word full + address upkeepPrivilegeManager; // address which can set privilege for upkeeps + uint32 configCount; // incremented each time a new config is posted, The count is incorporated into the config digest to prevent replay attacks. uint32 latestConfigBlockNumber; // makes it easier for offchain systems to extract config from logs - // 2 EVM word full uint32 maxCheckDataSize; // max length of checkData bytes + // 2 EVM word full + address financeAdmin; // address which can withdraw funds from the contract uint32 maxPerformDataSize; // max length of performData bytes uint32 maxRevertDataSize; // max length of revertData bytes - address upkeepPrivilegeManager; // address which can set privilege for upkeeps - // 3 EVM word full - address financeAdmin; // address which can withdraw funds from the contract + // 4 bytes left in 3rd EVM word } /// @dev Report transmitted by OCR to transmit function @@ -424,9 +325,17 @@ abstract contract AutomationRegistryBase2_3 is ConfirmedOwner { bytes32 dedupID; } + /** + * @notice holds information about a transmiter / node in the DON + * @member active can this transmitter submit reports + * @member index of oracle in s_signersList/s_transmittersList + * @member balance a node's balance in LINK + * @member lastCollected the total balance at which the node last withdrew + @ @dev uint96 is safe for balance / last collected because transmitters are only ever paid in LINK + */ struct Transmitter { bool active; - uint8 index; // Index of oracle in s_signersList/s_transmittersList + uint8 index; uint96 balance; uint96 lastCollected; } @@ -460,11 +369,35 @@ abstract contract AutomationRegistryBase2_3 is ConfirmedOwner { /** * @notice the billing config of a token + * @dev this is a storage struct */ struct BillingConfig { uint32 gasFeePPB; - uint24 flatFeeMicroLink; - address priceFeed; + uint24 flatFeeMilliCents; // min fee is $0.00001, max fee is $167 + AggregatorV3Interface priceFeed; + // 1st word, read in calculating BillingTokenPaymentParams + uint256 fallbackPrice; + // 2nd word only read if stale + uint96 minSpend; + // 3rd word only read during cancellation + } + + /** + * @notice override-able billing params of a billing token + */ + struct BillingOverrides { + uint32 gasFeePPB; + uint24 flatFeeMilliCents; + } + + /** + * @notice pricing params for a billing token + * @dev this is a memory-only struct, so struct packing is less important + */ + struct BillingTokenPaymentParams { + uint32 gasFeePPB; + uint24 flatFeeMilliCents; + uint256 priceUSD; } /** @@ -475,6 +408,8 @@ abstract contract AutomationRegistryBase2_3 is ConfirmedOwner { * @member fastGasWei the fast gas price * @member linkUSD the exchange ratio between LINK and USD * @member nativeUSD the exchange ratio between the chain's native token and USD + * @member billingToken the billing token + * @member billingTokenParams the payment params specific to a particular payment token * @member isTransaction is this an eth_call or a transaction */ struct PaymentParams { @@ -484,26 +419,37 @@ abstract contract AutomationRegistryBase2_3 is ConfirmedOwner { uint256 fastGasWei; uint256 linkUSD; uint256 nativeUSD; + IERC20 billingToken; + BillingTokenPaymentParams billingTokenParams; bool isTransaction; } /** - * @notice struct containing receipt information after a payment is made - * @member reimbursement the amount to reimburse a node for gas spent + * @notice struct containing receipt information about a payment or cost estimation + * @member gasCharge the amount to charge a user for gas spent * @member premium the premium charged to the user, shared between all nodes + * @member gasReimbursementJuels the amount to reimburse a node for gas spent + * @member premiumJuels the premium paid to NOPs, shared between all nodes */ struct PaymentReceipt { - uint96 reimbursement; + uint96 gasCharge; uint96 premium; + uint96 gasReimbursementJuels; + uint96 premiumJuels; } event AdminPrivilegeConfigSet(address indexed admin, bytes privilegeConfig); + event BillingConfigOverridden(uint256 indexed id, BillingOverrides overrides); + event BillingConfigOverrideRemoved(uint256 indexed id); + event BillingConfigSet(IERC20 indexed token, BillingConfig config); event CancelledUpkeepReport(uint256 indexed id, bytes trigger); event ChainSpecificModuleUpdated(address newModule); event DedupKeyAdded(bytes32 indexed dedupKey); + event FeesWithdrawn(address indexed assetAddress, address indexed recipient, uint256 amount); event FundsAdded(uint256 indexed id, address indexed from, uint96 amount); event FundsWithdrawn(uint256 indexed id, uint256 amount, address to); event InsufficientFundsUpkeepReport(uint256 indexed id, bytes trigger); + event NOPsSettledOffchain(address[] payees, uint256[] payments); event Paused(address account); event PayeesUpdated(address[] transmitters, address[] payees); event PayeeshipTransferRequested(address indexed transmitter, address indexed from, address indexed to); @@ -533,9 +479,6 @@ abstract contract AutomationRegistryBase2_3 is ConfirmedOwner { event UpkeepTriggerConfigSet(uint256 indexed id, bytes triggerConfig); event UpkeepUnpaused(uint256 indexed id); event Unpaused(address account); - // Event to emit when a billing configuration is set - event BillingConfigSet(IERC20 indexed token, BillingConfig config); - event FeesWithdrawn(address indexed recipient, address indexed assetAddress, uint256 amount); /** * @param link address of the LINK Token @@ -544,6 +487,7 @@ abstract contract AutomationRegistryBase2_3 is ConfirmedOwner { * @param fastGasFeed address of the Fast Gas price feed * @param automationForwarderLogic the address of automation forwarder logic * @param allowedReadOnlyAddress the address of the allowed read only address + * @param payoutMode the payout mode */ constructor( address link, @@ -551,7 +495,9 @@ abstract contract AutomationRegistryBase2_3 is ConfirmedOwner { address nativeUSDFeed, address fastGasFeed, address automationForwarderLogic, - address allowedReadOnlyAddress + address allowedReadOnlyAddress, + PayoutMode payoutMode, + address wrappedNativeTokenAddress ) ConfirmedOwner(msg.sender) { i_link = LinkTokenInterface(link); i_linkUSDFeed = AggregatorV3Interface(linkUSDFeed); @@ -559,6 +505,8 @@ abstract contract AutomationRegistryBase2_3 is ConfirmedOwner { i_fastGasFeed = AggregatorV3Interface(fastGasFeed); i_automationForwarderLogic = automationForwarderLogic; i_allowedReadOnlyAddress = allowedReadOnlyAddress; + s_payoutMode = payoutMode; + i_wrappedNativeToken = IWrappedNative(wrappedNativeTokenAddress); if (i_linkUSDFeed.decimals() != i_nativeUSDFeed.decimals()) { revert InvalidFeed(); } @@ -590,10 +538,11 @@ abstract contract AutomationRegistryBase2_3 is ConfirmedOwner { if (upkeep.performGas < PERFORM_GAS_MIN || upkeep.performGas > s_storage.maxPerformGas) revert GasLimitOutsideRange(); if (address(s_upkeep[id].forwarder) != address(0)) revert UpkeepAlreadyExists(); + if (address(s_billingConfigs[upkeep.billingToken].priceFeed) == address(0)) revert InvalidToken(); s_upkeep[id] = upkeep; s_upkeepAdmin[id] = admin; s_checkData[id] = checkData; - s_reserveAmounts[address(i_link)] = s_reserveAmounts[address(i_link)] + upkeep.balance; + s_reserveAmounts[upkeep.billingToken] = s_reserveAmounts[upkeep.billingToken] + upkeep.balance; s_upkeepTriggerConfig[id] = triggerConfig; s_upkeepOffchainConfig[id] = offchainConfig; s_upkeepIDs.add(id); @@ -653,7 +602,6 @@ abstract contract AutomationRegistryBase2_3 is ConfirmedOwner { } else { linkUSD = uint256(feedValue); } - (, feedValue, , timestamp, ) = i_nativeUSDFeed.latestRoundData(); return (gasWei, linkUSD, _getNativeUSD(hotVars)); } @@ -675,66 +623,109 @@ abstract contract AutomationRegistryBase2_3 is ConfirmedOwner { } } + /** + * @dev gets the price and billing params for a specific billing token + */ + function _getBillingTokenPaymentParams( + HotVars memory hotVars, + IERC20 billingToken + ) internal view returns (BillingTokenPaymentParams memory paymentParams) { + BillingConfig storage config = s_billingConfigs[billingToken]; + paymentParams.flatFeeMilliCents = config.flatFeeMilliCents; + paymentParams.gasFeePPB = config.gasFeePPB; + (, int256 feedValue, , uint256 timestamp, ) = config.priceFeed.latestRoundData(); + if ( + feedValue <= 0 || + block.timestamp < timestamp || + (hotVars.stalenessSeconds > 0 && hotVars.stalenessSeconds < block.timestamp - timestamp) + ) { + paymentParams.priceUSD = config.fallbackPrice; + } else { + paymentParams.priceUSD = uint256(feedValue); + } + return paymentParams; + } + /** * @dev calculates LINK paid for gas spent plus a configure premium percentage * @param hotVars the hot path variables * @param paymentParams the pricing data and gas usage data - * @dev use of PaymentParams is solely to avoid stack too deep errors + * @return receipt the receipt of payment with pricing breakdown + * @dev use of PaymentParams struct is necessary to avoid stack too deep errors + * @dev 1 USD = 1e18 attoUSD + * @dev 1 USD = 1e26 hexaicosaUSD (had to borrow this prefix from geometry because there is no metric prefix for 1e-26) + * @dev 1 millicent = 1e-5 USD = 1e13 attoUSD */ function _calculatePaymentAmount( HotVars memory hotVars, PaymentParams memory paymentParams - ) internal view returns (uint96, uint96) { + ) internal view returns (PaymentReceipt memory receipt) { uint256 gasWei = paymentParams.fastGasWei * hotVars.gasCeilingMultiplier; // in case it's actual execution use actual gas price, capped by fastGasWei * gasCeilingMultiplier if (paymentParams.isTransaction && tx.gasprice < gasWei) { gasWei = tx.gasprice; } - uint256 gasPayment = ((gasWei * (paymentParams.gasLimit + paymentParams.gasOverhead) + paymentParams.l1CostWei) * - paymentParams.nativeUSD) / paymentParams.linkUSD; - uint256 premium = (((gasWei * paymentParams.gasLimit) + paymentParams.l1CostWei) * - hotVars.paymentPremiumPPB * - paymentParams.nativeUSD) / - (paymentParams.linkUSD * 1e9) + - uint256(hotVars.flatFeeMicroLink) * - 1e12; - // LINK_TOTAL_SUPPLY < UINT96_MAX - if (gasPayment + premium > LINK_TOTAL_SUPPLY) revert PaymentGreaterThanAllLINK(); - return (uint96(gasPayment), uint96(premium)); + + uint256 gasPaymentHexaicosaUSD = (gasWei * + (paymentParams.gasLimit + paymentParams.gasOverhead) + + paymentParams.l1CostWei) * paymentParams.nativeUSD; // gasPaymentHexaicosaUSD has an extra 8 zeros because of decimals on nativeUSD feed + receipt.gasCharge = SafeCast.toUint96(gasPaymentHexaicosaUSD / paymentParams.billingTokenParams.priceUSD); // has units of attoBillingToken, or "wei" + receipt.gasReimbursementJuels = SafeCast.toUint96(gasPaymentHexaicosaUSD / paymentParams.linkUSD); + uint256 flatFeeHexaicosaUSD = uint256(paymentParams.billingTokenParams.flatFeeMilliCents) * 1e21; // 1e13 for milliCents to attoUSD and 1e8 for attoUSD to hexaicosaUSD + uint256 premiumHexaicosaUSD = ((((gasWei * paymentParams.gasLimit) + paymentParams.l1CostWei) * + paymentParams.billingTokenParams.gasFeePPB * + paymentParams.nativeUSD) / 1e9) + flatFeeHexaicosaUSD; + receipt.premium = SafeCast.toUint96(premiumHexaicosaUSD / paymentParams.billingTokenParams.priceUSD); + receipt.premiumJuels = SafeCast.toUint96(premiumHexaicosaUSD / paymentParams.linkUSD); + + return receipt; } /** - * @dev calculates the max LINK payment for an upkeep. Called during checkUpkeep simulation and assumes + * @dev calculates the max payment for an upkeep. Called during checkUpkeep simulation and assumes * maximum gas overhead, L1 fee */ - function _getMaxLinkPayment( + function _getMaxPayment( + uint256 upkeepId, HotVars memory hotVars, Trigger triggerType, uint32 performGas, uint256 fastGasWei, uint256 linkUSD, - uint256 nativeUSD + uint256 nativeUSD, + IERC20 billingToken ) internal view returns (uint96) { + uint256 maxL1Fee; uint256 maxGasOverhead; - if (triggerType == Trigger.CONDITION) { - maxGasOverhead = REGISTRY_CONDITIONAL_OVERHEAD; - } else if (triggerType == Trigger.LOG) { - maxGasOverhead = REGISTRY_LOG_OVERHEAD; - } else { - revert InvalidTriggerType(); + + { + if (triggerType == Trigger.CONDITION) { + maxGasOverhead = REGISTRY_CONDITIONAL_OVERHEAD; + } else if (triggerType == Trigger.LOG) { + maxGasOverhead = REGISTRY_LOG_OVERHEAD; + } else { + revert InvalidTriggerType(); + } + uint256 maxCalldataSize = s_storage.maxPerformDataSize + + TRANSMIT_CALLDATA_FIXED_BYTES_OVERHEAD + + (TRANSMIT_CALLDATA_PER_SIGNER_BYTES_OVERHEAD * (hotVars.f + 1)); + (uint256 chainModuleFixedOverhead, uint256 chainModulePerByteOverhead) = s_hotVars.chainModule.getGasOverhead(); + maxGasOverhead += + (REGISTRY_PER_SIGNER_GAS_OVERHEAD * (hotVars.f + 1)) + + ((REGISTRY_PER_PERFORM_BYTE_GAS_OVERHEAD + chainModulePerByteOverhead) * maxCalldataSize) + + chainModuleFixedOverhead; + maxL1Fee = hotVars.gasCeilingMultiplier * hotVars.chainModule.getMaxL1Fee(maxCalldataSize); } - uint256 maxCalldataSize = s_storage.maxPerformDataSize + - TRANSMIT_CALLDATA_FIXED_BYTES_OVERHEAD + - (TRANSMIT_CALLDATA_PER_SIGNER_BYTES_OVERHEAD * (hotVars.f + 1)); - (uint256 chainModuleFixedOverhead, uint256 chainModulePerByteOverhead) = s_hotVars.chainModule.getGasOverhead(); - maxGasOverhead += - (REGISTRY_PER_SIGNER_GAS_OVERHEAD * (hotVars.f + 1)) + - ((REGISTRY_PER_PERFORM_BYTE_GAS_OVERHEAD + chainModulePerByteOverhead) * maxCalldataSize) + - chainModuleFixedOverhead; - - uint256 maxL1Fee = hotVars.gasCeilingMultiplier * hotVars.chainModule.getMaxL1Fee(maxCalldataSize); - - (uint96 reimbursement, uint96 premium) = _calculatePaymentAmount( + + BillingTokenPaymentParams memory paymentParams = _getBillingTokenPaymentParams(hotVars, billingToken); + if (s_upkeep[upkeepId].overridesEnabled) { + BillingOverrides memory billingOverrides = s_billingOverrides[upkeepId]; + // use the overridden configs + paymentParams.gasFeePPB = billingOverrides.gasFeePPB; + paymentParams.flatFeeMilliCents = billingOverrides.flatFeeMilliCents; + } + + PaymentReceipt memory receipt = _calculatePaymentAmount( hotVars, PaymentParams({ gasLimit: performGas, @@ -743,11 +734,13 @@ abstract contract AutomationRegistryBase2_3 is ConfirmedOwner { fastGasWei: fastGasWei, linkUSD: linkUSD, nativeUSD: nativeUSD, + billingToken: billingToken, + billingTokenParams: paymentParams, isTransaction: false }) ); - return reimbursement + premium; + return receipt.gasCharge + receipt.premium; } /** @@ -970,28 +963,43 @@ abstract contract AutomationRegistryBase2_3 is ConfirmedOwner { function _handlePayment( HotVars memory hotVars, PaymentParams memory paymentParams, - uint256 upkeepId + uint256 upkeepId, + Upkeep memory upkeep ) internal returns (PaymentReceipt memory) { - (uint96 gasReimbursement, uint96 premium) = _calculatePaymentAmount(hotVars, paymentParams); + if (upkeep.overridesEnabled) { + BillingOverrides memory billingOverrides = s_billingOverrides[upkeepId]; + // use the overridden configs + paymentParams.billingTokenParams.gasFeePPB = billingOverrides.gasFeePPB; + paymentParams.billingTokenParams.flatFeeMilliCents = billingOverrides.flatFeeMilliCents; + } + + PaymentReceipt memory receipt = _calculatePaymentAmount(hotVars, paymentParams); - uint96 balance = s_upkeep[upkeepId].balance; - uint96 payment = gasReimbursement + premium; + uint96 balance = upkeep.balance; + uint96 payment = receipt.gasCharge + receipt.premium; // this shouldn't happen, but in rare edge cases, we charge the full balance in case the user // can't cover the amount owed - if (balance < gasReimbursement) { + if (balance < receipt.gasCharge) { + // if the user can't cover the gas fee, then direct all of the payment to the transmitter and distribute no premium to the DON payment = balance; - gasReimbursement = balance; - premium = 0; + receipt.gasReimbursementJuels = SafeCast.toUint96( + (balance * paymentParams.billingTokenParams.priceUSD) / paymentParams.linkUSD + ); + receipt.premiumJuels = 0; } else if (balance < payment) { + // if the user can cover the gas fee, but not the premium, then reduce the premium payment = balance; - premium = payment - gasReimbursement; + receipt.premiumJuels = SafeCast.toUint96( + ((balance * paymentParams.billingTokenParams.priceUSD) / paymentParams.linkUSD) - receipt.gasReimbursementJuels + ); } s_upkeep[upkeepId].balance -= payment; s_upkeep[upkeepId].amountSpent += payment; + s_reserveAmounts[paymentParams.billingToken] -= payment; - return PaymentReceipt({reimbursement: gasReimbursement, premium: premium}); + return receipt; } /** @@ -1030,6 +1038,15 @@ abstract contract AutomationRegistryBase2_3 is ConfirmedOwner { } } + /** + * @notice only allows privilege manager to call the function + */ + function _onlyPrivilegeManagerAllowed() internal view { + if (msg.sender != s_storage.upkeepPrivilegeManager) { + revert OnlyCallableByUpkeepPrivilegeManager(); + } + } + /** * @notice sets billing configuration for a token * @param billingTokens the addresses of tokens @@ -1042,16 +1059,21 @@ abstract contract AutomationRegistryBase2_3 is ConfirmedOwner { } delete s_billingTokens; + PayoutMode mode = s_payoutMode; for (uint256 i = 0; i < billingTokens.length; i++) { IERC20 token = billingTokens[i]; BillingConfig memory config = billingConfigs[i]; - if (address(token) == ZERO_ADDRESS || config.priceFeed == ZERO_ADDRESS) { + // if LINK is a billing option, payout mode must be ON_CHAIN + if (address(token) == address(i_link) && mode == PayoutMode.OFF_CHAIN) { + revert InvalidToken(); + } + if (address(token) == ZERO_ADDRESS || address(config.priceFeed) == ZERO_ADDRESS) { revert ZeroAddressNotAllowed(); } // if this is a new token, add it to tokens list. Otherwise revert - if (s_billingConfigs[token].priceFeed != ZERO_ADDRESS) { + if (address(s_billingConfigs[token].priceFeed) != ZERO_ADDRESS) { revert DuplicateEntry(); } s_billingTokens.push(token); @@ -1062,4 +1084,66 @@ abstract contract AutomationRegistryBase2_3 is ConfirmedOwner { emit BillingConfigSet(token, config); } } + + /** + * @notice updates the signers and transmitters lists + */ + function _updateTransmitters(address[] memory signers, address[] memory transmitters) internal { + // move all pooled payments out of the pool to each transmitter's balance + for (uint256 i = 0; i < s_transmittersList.length; i++) { + _updateTransmitterBalanceFromPool( + s_transmittersList[i], + s_hotVars.totalPremium, + uint96(s_transmittersList.length) + ); + } + + // remove any old signer/transmitter addresses + address transmitterAddress; + PayoutMode mode = s_payoutMode; + for (uint256 i = 0; i < s_transmittersList.length; i++) { + transmitterAddress = s_transmittersList[i]; + delete s_signers[s_signersList[i]]; + // Do not delete the whole transmitter struct as it has balance information stored + s_transmitters[transmitterAddress].active = false; + if (mode == PayoutMode.OFF_CHAIN && s_transmitters[transmitterAddress].balance > 0) { + s_deactivatedTransmitters.add(transmitterAddress); + } + } + delete s_signersList; + delete s_transmittersList; + + // add new signer/transmitter addresses + Transmitter memory transmitter; + for (uint256 i = 0; i < signers.length; i++) { + if (s_signers[signers[i]].active) revert RepeatedSigner(); + if (signers[i] == ZERO_ADDRESS) revert InvalidSigner(); + s_signers[signers[i]] = Signer({active: true, index: uint8(i)}); + + transmitterAddress = transmitters[i]; + if (transmitterAddress == ZERO_ADDRESS) revert InvalidTransmitter(); + transmitter = s_transmitters[transmitterAddress]; + if (transmitter.active) revert RepeatedTransmitter(); + transmitter.active = true; + transmitter.index = uint8(i); + // new transmitters start afresh from current totalPremium + // some spare change of premium from previous pool will be forfeited + transmitter.lastCollected = s_hotVars.totalPremium; + s_transmitters[transmitterAddress] = transmitter; + if (mode == PayoutMode.OFF_CHAIN) { + s_deactivatedTransmitters.remove(transmitterAddress); + } + } + + s_signersList = signers; + s_transmittersList = transmitters; + } + + /** + * @notice returns the size of the LINK liquidity pool + # @dev LINK max supply < 2^96, so casting to int256 is safe + */ + function _linkAvailableForPayment() internal view returns (int256) { + return int256(i_link.balanceOf(address(this))) - int256(s_reserveAmounts[IERC20(address(i_link))]); + } } diff --git a/contracts/src/v0.8/automation/dev/v2_3/AutomationRegistryLogicA2_3.sol b/contracts/src/v0.8/automation/dev/v2_3/AutomationRegistryLogicA2_3.sol index f0c19b99..22753cc4 100644 --- a/contracts/src/v0.8/automation/dev/v2_3/AutomationRegistryLogicA2_3.sol +++ b/contracts/src/v0.8/automation/dev/v2_3/AutomationRegistryLogicA2_3.sol @@ -4,12 +4,15 @@ pragma solidity 0.8.19; import {EnumerableSet} from "../../../vendor/openzeppelin-solidity/v4.7.3/contracts/utils/structs/EnumerableSet.sol"; import {Address} from "../../../vendor/openzeppelin-solidity/v4.7.3/contracts/utils/Address.sol"; import {AutomationRegistryBase2_3} from "./AutomationRegistryBase2_3.sol"; +import {AutomationRegistryLogicC2_3} from "./AutomationRegistryLogicC2_3.sol"; import {AutomationRegistryLogicB2_3} from "./AutomationRegistryLogicB2_3.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"; +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; /** * @notice Logic contract, works in tandem with AutomationRegistry as a proxy @@ -18,186 +21,31 @@ contract AutomationRegistryLogicA2_3 is AutomationRegistryBase2_3, Chainable { using Address for address; using EnumerableSet for EnumerableSet.UintSet; using EnumerableSet for EnumerableSet.AddressSet; + using SafeERC20 for IERC20; /** * @param logicB the address of the second logic contract + * @dev we cast the contract to logicC in order to call logicC functions (via fallback) */ constructor( AutomationRegistryLogicB2_3 logicB ) AutomationRegistryBase2_3( - logicB.getLinkAddress(), - logicB.getLinkUSDFeedAddress(), - logicB.getNativeUSDFeedAddress(), - logicB.getFastGasFeedAddress(), - logicB.getAutomationForwarderLogic(), - logicB.getAllowedReadOnlyAddress() + AutomationRegistryLogicC2_3(address(logicB)).getLinkAddress(), + AutomationRegistryLogicC2_3(address(logicB)).getLinkUSDFeedAddress(), + AutomationRegistryLogicC2_3(address(logicB)).getNativeUSDFeedAddress(), + AutomationRegistryLogicC2_3(address(logicB)).getFastGasFeedAddress(), + AutomationRegistryLogicC2_3(address(logicB)).getAutomationForwarderLogic(), + AutomationRegistryLogicC2_3(address(logicB)).getAllowedReadOnlyAddress(), + AutomationRegistryLogicC2_3(address(logicB)).getPayoutMode(), + AutomationRegistryLogicC2_3(address(logicB)).getWrappedNativeTokenAddress() ) 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 - returns ( - bool upkeepNeeded, - bytes memory performData, - UpkeepFailureReason upkeepFailureReason, - uint256 gasUsed, - uint256 gasLimit, - uint256 fastGasWei, - uint256 linkUSD - ) - { - _preventExecution(); - - Trigger triggerType = _getTriggerType(id); - HotVars memory hotVars = s_hotVars; - Upkeep memory upkeep = s_upkeep[id]; - - { - uint256 nativeUSD; - uint96 maxLinkPayment; - if (hotVars.paused) return (false, bytes(""), UpkeepFailureReason.REGISTRY_PAUSED, 0, upkeep.performGas, 0, 0); - if (upkeep.maxValidBlocknumber != UINT32_MAX) - return (false, bytes(""), UpkeepFailureReason.UPKEEP_CANCELLED, 0, upkeep.performGas, 0, 0); - if (upkeep.paused) return (false, bytes(""), UpkeepFailureReason.UPKEEP_PAUSED, 0, upkeep.performGas, 0, 0); - (fastGasWei, linkUSD, nativeUSD) = _getFeedData(hotVars); - maxLinkPayment = _getMaxLinkPayment(hotVars, triggerType, upkeep.performGas, fastGasWei, linkUSD, nativeUSD); - 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, - linkUSD - ); - } - return ( - upkeepNeeded, - result, - UpkeepFailureReason.TARGET_CHECK_REVERTED, - gasUsed, - upkeep.performGas, - fastGasWei, - linkUSD - ); - } - - (upkeepNeeded, performData) = abi.decode(result, (bool, bytes)); - if (!upkeepNeeded) - return (false, bytes(""), UpkeepFailureReason.UPKEEP_NOT_NEEDED, gasUsed, upkeep.performGas, fastGasWei, linkUSD); - - if (performData.length > s_storage.maxPerformDataSize) - return ( - false, - bytes(""), - UpkeepFailureReason.PERFORM_DATA_EXCEEDS_LIMIT, - gasUsed, - upkeep.performGas, - fastGasWei, - linkUSD - ); - - return (upkeepNeeded, performData, upkeepFailureReason, gasUsed, upkeep.performGas, fastGasWei, linkUSD); - } - - /** - * @notice see other checkUpkeep function for description - * @dev this function may be deprecated in a future version of chainlink automation - */ - function checkUpkeep( - uint256 id - ) - external - returns ( - bool upkeepNeeded, - bytes memory performData, - UpkeepFailureReason upkeepFailureReason, - uint256 gasUsed, - uint256 gasLimit, - uint256 fastGasWei, - uint256 linkUSD - ) - { - return checkUpkeep(id, bytes("")); - } - - /** - * @dev checkCallback is used specifically for automation data streams lookups (see StreamsLookupCompatibleInterface.sol) - * @param id the upkeepID to execute a callback for - * @param values the values returned from the data streams lookup - * @param extraData the user-provided extra context data - */ - function checkCallback( - uint256 id, - bytes[] memory values, - bytes calldata extraData - ) - external - returns (bool upkeepNeeded, bytes memory performData, UpkeepFailureReason upkeepFailureReason, uint256 gasUsed) - { - bytes memory payload = abi.encodeWithSelector(CHECK_CALLBACK_SELECTOR, values, extraData); - return executeCallback(id, payload); - } - - /** - * @notice this is a generic callback executor that forwards a call to a user's contract with the configured - * gas limit - * @param id the upkeepID to execute a callback for - * @param payload the data (including function selector) to call on the upkeep target contract - */ - function executeCallback( - uint256 id, - bytes memory payload - ) - public - returns (bool upkeepNeeded, bytes memory performData, UpkeepFailureReason upkeepFailureReason, uint256 gasUsed) - { - _preventExecution(); - - Upkeep memory upkeep = s_upkeep[id]; - gasUsed = gasleft(); - (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); - } + // ================================================================ + // | UPKEEP MANAGEMENT | + // ================================================================ /** * @notice adds a new upkeep @@ -206,6 +54,7 @@ contract AutomationRegistryLogicA2_3 is AutomationRegistryBase2_3, Chainable { * performing upkeep * @param admin address to cancel upkeep and withdraw remaining funds * @param triggerType the trigger for the upkeep + * @param billingToken the billing token for the upkeep * @param checkData data passed to the contract when checking for upkeep * @param triggerConfig the config for the trigger * @param offchainConfig arbitrary offchain config for the upkeep @@ -215,6 +64,7 @@ contract AutomationRegistryLogicA2_3 is AutomationRegistryBase2_3, Chainable { uint32 gasLimit, address admin, Trigger triggerType, + IERC20 billingToken, bytes calldata checkData, bytes memory triggerConfig, bytes memory offchainConfig @@ -228,13 +78,15 @@ contract AutomationRegistryLogicA2_3 is AutomationRegistryBase2_3, Chainable { _createUpkeep( id, Upkeep({ + overridesEnabled: false, performGas: gasLimit, balance: 0, maxValidBlocknumber: UINT32_MAX, lastPerformedBlockNumber: 0, amountSpent: 0, paused: false, - forwarder: forwarder + forwarder: forwarder, + billingToken: billingToken }), admin, checkData, @@ -249,20 +101,6 @@ contract AutomationRegistryLogicA2_3 is AutomationRegistryBase2_3, Chainable { 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 @@ -272,6 +110,7 @@ contract AutomationRegistryLogicA2_3 is AutomationRegistryBase2_3, Chainable { function cancelUpkeep(uint256 id) external { Upkeep memory upkeep = s_upkeep[id]; bool isOwner = msg.sender == owner(); + uint96 minSpend = s_billingConfigs[upkeep.billingToken].minSpend; uint256 height = s_hotVars.chainModule.blockNumber(); if (upkeep.maxValidBlocknumber == 0) revert CannotCancel(); @@ -284,36 +123,21 @@ contract AutomationRegistryLogicA2_3 is AutomationRegistryBase2_3, Chainable { 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; + // charge the cancellation fee if the minSpend is not met uint96 cancellationFee = 0; - // cancellationFee is supposed to be min(max(minUpkeepSpend - amountSpent,0), amountLeft) - if (upkeep.amountSpent < minUpkeepSpend) { - cancellationFee = minUpkeepSpend - upkeep.amountSpent; + // cancellationFee is min(max(minSpend - amountSpent, 0), amountLeft) + if (upkeep.amountSpent < minSpend) { + cancellationFee = minSpend - uint96(upkeep.amountSpent); if (cancellationFee > upkeep.balance) { cancellationFee = upkeep.balance; } } s_upkeep[id].balance = upkeep.balance - cancellationFee; - s_reserveAmounts[address(i_link)] = s_reserveAmounts[address(i_link)] - cancellationFee; + s_reserveAmounts[upkeep.billingToken] = s_reserveAmounts[upkeep.billingToken] - 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_reserveAmounts[address(i_link)] = s_reserveAmounts[address(i_link)] + 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 @@ -321,6 +145,7 @@ contract AutomationRegistryLogicA2_3 is AutomationRegistryBase2_3, Chainable { * @dev a transcoder must be set in order to enable migration * @dev migration permissions must be set on *both* sending and receiving registries * @dev only an upkeep admin can migrate their upkeeps + * @dev this function is most gas-efficient if upkeepIDs are sorted by billing token */ function migrateUpkeeps(uint256[] calldata ids, address destination) external { if ( @@ -329,9 +154,10 @@ contract AutomationRegistryLogicA2_3 is AutomationRegistryBase2_3, Chainable { ) revert MigrationNotPermitted(); if (s_storage.transcoder == ZERO_ADDRESS) revert TranscoderNotSet(); if (ids.length == 0) revert ArrayHasNoEntries(); + IERC20 billingToken; + uint256 balanceToTransfer; uint256 id; Upkeep memory upkeep; - uint256 totalBalanceRemaining; address[] memory admins = new address[](ids.length); Upkeep[] memory upkeeps = new Upkeep[](ids.length); bytes[] memory checkDatas = new bytes[](ids.length); @@ -340,6 +166,19 @@ contract AutomationRegistryLogicA2_3 is AutomationRegistryBase2_3, Chainable { for (uint256 idx = 0; idx < ids.length; idx++) { id = ids[idx]; upkeep = s_upkeep[id]; + + if (idx == 0) { + billingToken = s_upkeep[id].billingToken; + balanceToTransfer = upkeep.balance; + } + + // if we encounter a new billing token, send the sum from the last billing token to the destination registry + if (upkeep.billingToken != billingToken) { + s_reserveAmounts[billingToken] = s_reserveAmounts[billingToken] - balanceToTransfer; + billingToken.safeTransfer(destination, balanceToTransfer); + billingToken = upkeep.billingToken; + balanceToTransfer = upkeep.balance; + } _requireAdminAndNotCancelled(id); upkeep.forwarder.updateRegistry(destination); upkeeps[idx] = upkeep; @@ -347,7 +186,6 @@ contract AutomationRegistryLogicA2_3 is AutomationRegistryBase2_3, Chainable { 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]; @@ -356,8 +194,13 @@ contract AutomationRegistryLogicA2_3 is AutomationRegistryBase2_3, Chainable { delete s_proposedAdmin[id]; s_upkeepIDs.remove(id); emit UpkeepMigrated(id, upkeep.balance, destination); + + // always transfer the rolling sum at the end of the array + if (idx == ids.length - 1) { + s_reserveAmounts[billingToken] = s_reserveAmounts[billingToken] - balanceToTransfer; + billingToken.safeTransfer(destination, balanceToTransfer); + } } - s_reserveAmounts[address(i_link)] = s_reserveAmounts[address(i_link)] - totalBalanceRemaining; bytes memory encodedUpkeeps = abi.encode( ids, upkeeps, @@ -374,7 +217,6 @@ contract AutomationRegistryLogicA2_3 is AutomationRegistryBase2_3, Chainable { encodedUpkeeps ) ); - i_link.transfer(destination, totalBalanceRemaining); } /** @@ -413,15 +255,4 @@ contract AutomationRegistryLogicA2_3 is AutomationRegistryBase2_3, Chainable { 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_3/AutomationRegistryLogicB2_3.sol b/contracts/src/v0.8/automation/dev/v2_3/AutomationRegistryLogicB2_3.sol index e2710c8f..5063bd48 100644 --- a/contracts/src/v0.8/automation/dev/v2_3/AutomationRegistryLogicB2_3.sol +++ b/contracts/src/v0.8/automation/dev/v2_3/AutomationRegistryLogicB2_3.sol @@ -4,41 +4,261 @@ pragma solidity 0.8.19; import {AutomationRegistryBase2_3} from "./AutomationRegistryBase2_3.sol"; import {EnumerableSet} from "../../../vendor/openzeppelin-solidity/v4.7.3/contracts/utils/structs/EnumerableSet.sol"; import {Address} from "../../../vendor/openzeppelin-solidity/v4.7.3/contracts/utils/Address.sol"; -import {UpkeepFormat} from "../../interfaces/UpkeepTranscoderInterface.sol"; -import {IAutomationForwarder} from "../../interfaces/IAutomationForwarder.sol"; -import {IChainModule} from "../../interfaces/IChainModule.sol"; +import {AutomationRegistryLogicC2_3} from "./AutomationRegistryLogicC2_3.sol"; +import {Chainable} from "../../Chainable.sol"; import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; -contract AutomationRegistryLogicB2_3 is AutomationRegistryBase2_3 { +contract AutomationRegistryLogicB2_3 is AutomationRegistryBase2_3, Chainable { using Address for address; using EnumerableSet for EnumerableSet.UintSet; using EnumerableSet for EnumerableSet.AddressSet; + using SafeERC20 for IERC20; /** - * @dev see AutomationRegistry master contract for constructor description + * @param logicC the address of the third logic contract */ constructor( - address link, - address linkUSDFeed, - address nativeUSDFeed, - address fastGasFeed, - address automationForwarderLogic, - address allowedReadOnlyAddress + AutomationRegistryLogicC2_3 logicC ) AutomationRegistryBase2_3( - link, - linkUSDFeed, - nativeUSDFeed, - fastGasFeed, - automationForwarderLogic, - allowedReadOnlyAddress + logicC.getLinkAddress(), + logicC.getLinkUSDFeedAddress(), + logicC.getNativeUSDFeedAddress(), + logicC.getFastGasFeedAddress(), + logicC.getAutomationForwarderLogic(), + logicC.getAllowedReadOnlyAddress(), + logicC.getPayoutMode(), + logicC.getWrappedNativeTokenAddress() ) + Chainable(address(logicC)) {} + // ================================================================ + // | PIPELINE FUNCTIONS | + // ================================================================ + + /** + * @notice called by the automation DON to check if work is needed + * @param id the upkeep ID to check for work needed + * @param triggerData extra contextual data about the trigger (not used in all code paths) + * @dev this one of the core functions called in the hot path + * @dev there is a 2nd checkUpkeep function (below) that is being maintained for backwards compatibility + * @dev there is an incongruency on what gets returned during failure modes + * ex sometimes we include price data, sometimes we omit it depending on the failure + */ + function checkUpkeep( + uint256 id, + bytes memory triggerData + ) + public + returns ( + bool upkeepNeeded, + bytes memory performData, + UpkeepFailureReason upkeepFailureReason, + uint256 gasUsed, + uint256 gasLimit, + uint256 fastGasWei, + uint256 linkUSD + ) + { + _preventExecution(); + + Trigger triggerType = _getTriggerType(id); + HotVars memory hotVars = s_hotVars; + Upkeep memory upkeep = s_upkeep[id]; + + { + uint256 nativeUSD; + uint96 maxPayment; + if (hotVars.paused) return (false, bytes(""), UpkeepFailureReason.REGISTRY_PAUSED, 0, upkeep.performGas, 0, 0); + if (upkeep.maxValidBlocknumber != UINT32_MAX) + return (false, bytes(""), UpkeepFailureReason.UPKEEP_CANCELLED, 0, upkeep.performGas, 0, 0); + if (upkeep.paused) return (false, bytes(""), UpkeepFailureReason.UPKEEP_PAUSED, 0, upkeep.performGas, 0, 0); + (fastGasWei, linkUSD, nativeUSD) = _getFeedData(hotVars); + maxPayment = _getMaxPayment( + id, + hotVars, + triggerType, + upkeep.performGas, + fastGasWei, + linkUSD, + nativeUSD, + upkeep.billingToken + ); + if (upkeep.balance < maxPayment) { + return (false, bytes(""), UpkeepFailureReason.INSUFFICIENT_BALANCE, 0, upkeep.performGas, 0, 0); + } + } + + bytes memory callData = _checkPayload(id, triggerType, triggerData); + + gasUsed = gasleft(); + (bool success, bytes memory result) = upkeep.forwarder.getTarget().call{gas: s_storage.checkGasLimit}(callData); + gasUsed = gasUsed - gasleft(); + + if (!success) { + // User's target check reverted. We capture the revert data here and pass it within performData + if (result.length > s_storage.maxRevertDataSize) { + return ( + false, + bytes(""), + UpkeepFailureReason.REVERT_DATA_EXCEEDS_LIMIT, + gasUsed, + upkeep.performGas, + fastGasWei, + linkUSD + ); + } + return ( + upkeepNeeded, + result, + UpkeepFailureReason.TARGET_CHECK_REVERTED, + gasUsed, + upkeep.performGas, + fastGasWei, + linkUSD + ); + } + + (upkeepNeeded, performData) = abi.decode(result, (bool, bytes)); + if (!upkeepNeeded) + return (false, bytes(""), UpkeepFailureReason.UPKEEP_NOT_NEEDED, gasUsed, upkeep.performGas, fastGasWei, linkUSD); + + if (performData.length > s_storage.maxPerformDataSize) + return ( + false, + bytes(""), + UpkeepFailureReason.PERFORM_DATA_EXCEEDS_LIMIT, + gasUsed, + upkeep.performGas, + fastGasWei, + linkUSD + ); + + return (upkeepNeeded, performData, upkeepFailureReason, gasUsed, upkeep.performGas, fastGasWei, linkUSD); + } + + /** + * @notice see other checkUpkeep function for description + * @dev this function may be deprecated in a future version of chainlink automation + */ + function checkUpkeep( + uint256 id + ) + external + returns ( + bool upkeepNeeded, + bytes memory performData, + UpkeepFailureReason upkeepFailureReason, + uint256 gasUsed, + uint256 gasLimit, + uint256 fastGasWei, + uint256 linkUSD + ) + { + return checkUpkeep(id, bytes("")); + } + + /** + * @dev checkCallback is used specifically for automation data streams lookups (see StreamsLookupCompatibleInterface.sol) + * @param id the upkeepID to execute a callback for + * @param values the values returned from the data streams lookup + * @param extraData the user-provided extra context data + */ + function checkCallback( + uint256 id, + bytes[] memory values, + bytes calldata extraData + ) + external + returns (bool upkeepNeeded, bytes memory performData, UpkeepFailureReason upkeepFailureReason, uint256 gasUsed) + { + bytes memory payload = abi.encodeWithSelector(CHECK_CALLBACK_SELECTOR, values, extraData); + return executeCallback(id, payload); + } + + /** + * @notice this is a generic callback executor that forwards a call to a user's contract with the configured + * gas limit + * @param id the upkeepID to execute a callback for + * @param payload the data (including function selector) to call on the upkeep target contract + */ + function executeCallback( + uint256 id, + bytes memory payload + ) + public + returns (bool upkeepNeeded, bytes memory performData, UpkeepFailureReason upkeepFailureReason, uint256 gasUsed) + { + _preventExecution(); + + Upkeep memory upkeep = s_upkeep[id]; + gasUsed = gasleft(); + (bool success, bytes memory result) = upkeep.forwarder.getTarget().call{gas: s_storage.checkGasLimit}(payload); + gasUsed = gasUsed - gasleft(); + if (!success) { + return (false, bytes(""), UpkeepFailureReason.CALLBACK_REVERTED, gasUsed); + } + (upkeepNeeded, performData) = abi.decode(result, (bool, bytes)); + if (!upkeepNeeded) { + return (false, bytes(""), UpkeepFailureReason.UPKEEP_NOT_NEEDED, gasUsed); + } + if (performData.length > s_storage.maxPerformDataSize) { + return (false, bytes(""), UpkeepFailureReason.PERFORM_DATA_EXCEEDS_LIMIT, gasUsed); + } + return (upkeepNeeded, performData, upkeepFailureReason, gasUsed); + } + + /** + * @notice simulates the upkeep with the perform data returned from checkUpkeep + * @param id identifier of the upkeep to execute the data with. + * @param performData calldata parameter to be passed to the target upkeep. + * @return success whether the call reverted or not + * @return gasUsed the amount of gas the target contract consumed + */ + function simulatePerformUpkeep( + uint256 id, + bytes calldata performData + ) external returns (bool success, uint256 gasUsed) { + _preventExecution(); + + if (s_hotVars.paused) revert RegistryPaused(); + Upkeep memory upkeep = s_upkeep[id]; + (success, gasUsed) = _performUpkeep(upkeep.forwarder, upkeep.performGas, performData); + return (success, gasUsed); + } + // ================================================================ // | UPKEEP MANAGEMENT | // ================================================================ + /** + * @notice overrides the billing config for an upkeep + * @param id the upkeepID + * @param billingOverrides the override-able billing config + */ + function setBillingOverrides(uint256 id, BillingOverrides calldata billingOverrides) external { + _onlyPrivilegeManagerAllowed(); + if (s_upkeep[id].maxValidBlocknumber != UINT32_MAX) revert UpkeepCancelled(); + + s_upkeep[id].overridesEnabled = true; + s_billingOverrides[id] = billingOverrides; + emit BillingConfigOverridden(id, billingOverrides); + } + + /** + * @notice remove the overridden billing config for an upkeep + * @param id the upkeepID + */ + function removeBillingOverrides(uint256 id) external { + _onlyPrivilegeManagerAllowed(); + + s_upkeep[id].overridesEnabled = false; + delete s_billingOverrides[id]; + emit BillingConfigOverrideRemoved(id); + } + /** * @notice transfers the address of an admin for an upkeep */ @@ -121,7 +341,18 @@ contract AutomationRegistryLogicB2_3 is AutomationRegistryBase2_3 { } /** - * @notice withdraws LINK funds from an upkeep + * @notice sets the upkeep trigger config + * @param id the upkeepID to change the trigger for + * @param triggerConfig the new trigger config + */ + function setUpkeepTriggerConfig(uint256 id, bytes calldata triggerConfig) external { + _requireAdminAndNotCancelled(id); + s_upkeepTriggerConfig[id] = triggerConfig; + emit UpkeepTriggerConfigSet(id, triggerConfig); + } + + /** + * @notice withdraws an upkeep's funds from an upkeep * @dev note that an upkeep must be cancelled first!! */ function withdrawFunds(uint256 id, address to) external nonReentrant { @@ -130,454 +361,56 @@ contract AutomationRegistryLogicB2_3 is AutomationRegistryBase2_3 { if (s_upkeepAdmin[id] != msg.sender) revert OnlyCallableByAdmin(); if (upkeep.maxValidBlocknumber > s_hotVars.chainModule.blockNumber()) revert UpkeepNotCanceled(); uint96 amountToWithdraw = s_upkeep[id].balance; - s_reserveAmounts[address(i_link)] = s_reserveAmounts[address(i_link)] - amountToWithdraw; + s_reserveAmounts[upkeep.billingToken] = s_reserveAmounts[upkeep.billingToken] - amountToWithdraw; s_upkeep[id].balance = 0; - i_link.transfer(to, amountToWithdraw); + upkeep.billingToken.safeTransfer(to, amountToWithdraw); emit FundsWithdrawn(id, amountToWithdraw, to); } + // ================================================================ + // | FINANCE ACTIONS | + // ================================================================ + /** - * @notice LINK available to withdraw by the finance team + * @notice withdraws excess LINK from the liquidity pool + * @param to the address to send the fees to + * @param amount the amount to withdraw */ - function linkAvailableForPayment() public view returns (uint256) { - return i_link.balanceOf(address(this)) - s_reserveAmounts[address(i_link)]; - } - - function withdrawLinkFees(address to, uint256 amount) external { + function withdrawLink(address to, uint256 amount) external { _onlyFinanceAdminAllowed(); if (to == ZERO_ADDRESS) revert InvalidRecipient(); - uint256 available = linkAvailableForPayment(); - if (amount > available) revert InsufficientBalance(available, amount); - - bool transferStatus = i_link.transfer(to, amount); - if (!transferStatus) { - revert TransferFailed(); + int256 available = _linkAvailableForPayment(); + if (available < 0) { + revert InsufficientBalance(0, amount); + } else if (amount > uint256(available)) { + revert InsufficientBalance(uint256(available), amount); } - emit FeesWithdrawn(to, address(i_link), amount); - } - - function withdrawERC20Fees(address assetAddress, address to, uint256 amount) external { - _onlyFinanceAdminAllowed(); - if (to == ZERO_ADDRESS) revert InvalidRecipient(); - bool transferStatus = IERC20(assetAddress).transfer(to, amount); + bool transferStatus = i_link.transfer(to, amount); if (!transferStatus) { revert TransferFailed(); } - - emit FeesWithdrawn(to, assetAddress, amount); - } - - // ================================================================ - // | 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); + emit FeesWithdrawn(address(i_link), to, amount); } /** - * @notice withdraws LINK received as payment for work performed + * @notice withdraws non-LINK fees earned by the contract + * @param asset the asset to withdraw + * @param to the address to send the fees to + * @param amount the amount to withdraw + * @dev we prevent withdrawing non-LINK fees unless there is sufficient LINK liquidity + * to cover all outstanding debts on the registry */ - function withdrawPayment(address from, address to) external { + function withdrawERC20Fees(IERC20 asset, address to, uint256 amount) external { + _onlyFinanceAdminAllowed(); 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_reserveAmounts[address(i_link)] = s_reserveAmounts[address(i_link)] - 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 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 getTransmitCalldataFixedBytesOverhead() external pure returns (uint256) { - return TRANSMIT_CALLDATA_FIXED_BYTES_OVERHEAD; - } - - function getTransmitCalldataPerSignerBytesOverhead() external pure returns (uint256) { - return TRANSMIT_CALLDATA_PER_SIGNER_BYTES_OVERHEAD; - } - - function getCancellationDelay() external pure returns (uint256) { - return CANCELLATION_DELAY; - } - - function getLinkAddress() external view returns (address) { - return address(i_link); - } - - function getLinkUSDFeedAddress() external view returns (address) { - return address(i_linkUSDFeed); - } - - function getNativeUSDFeedAddress() external view returns (address) { - return address(i_nativeUSDFeed); - } - - function getFastGasFeedAddress() external view returns (address) { - return address(i_fastGasFeed); - } - - function getAutomationForwarderLogic() external view returns (address) { - return i_automationForwarderLogic; - } - - function getAllowedReadOnlyAddress() external view returns (address) { - return i_allowedReadOnlyAddress; - } - - function getBillingTokens() external view returns (IERC20[] memory) { - return s_billingTokens; - } - - function getBillingTokenConfig(IERC20 token) external view returns (BillingConfig memory) { - return s_billingConfigs[token]; - } - - 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 - * @dev this function is deprecated - */ - function getState() - external - view - returns ( - State memory state, - OnchainConfigLegacy memory config, - address[] memory signers, - address[] memory transmitters, - uint8 f - ) - { - state = State({ - nonce: s_storage.nonce, - ownerLinkBalance: 0, - expectedLinkBalance: s_reserveAmounts[address(i_link)], - 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 = OnchainConfigLegacy({ - 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 get the chain module - */ - function getChainModule() external view returns (IChainModule chainModule) { - return s_hotVars.chainModule; - } - - /** - * @notice if this registry has reorg protection enabled - */ - function getReorgProtectionEnabled() external view returns (bool reorgProtectionEnabled) { - return s_hotVars.reorgProtectionEnabled; - } - - /** - * @notice calculates the minimum balance required for an upkeep to remain eligible - * @param id the upkeep id to calculate minimum balance for - */ - function getBalance(uint256 id) external view returns (uint96 balance) { - return s_upkeep[id].balance; - } - - /** - * @notice calculates the minimum balance required for an upkeep to remain eligible - * @param id the upkeep id to calculate minimum balance for - */ - function getMinBalance(uint256 id) external view returns (uint96) { - return getMinBalanceForUpkeep(id); - } - - /** - * @notice calculates the minimum balance required for an upkeep to remain eligible - * @param id the upkeep id to calculate minimum balance for - * @dev this will be deprecated in a future version in favor of getMinBalance - */ - function getMinBalanceForUpkeep(uint256 id) public view returns (uint96 minBalance) { - 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 linkUSD, uint256 nativeUSD) = _getFeedData(hotVars); - return _getMaxLinkPayment(hotVars, triggerType, gasLimit, fastGasWei, linkUSD, nativeUSD); - } - - /** - * @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]; - } + if (address(asset) == address(i_link)) revert InvalidToken(); + if (_linkAvailableForPayment() < 0) revert InsufficientLinkLiquidity(); + uint256 available = asset.balanceOf(address(this)) - s_reserveAmounts[asset]; + if (amount > available) revert InsufficientBalance(available, amount); - /** - * @notice returns the fallback native price - */ - function getFallbackNativePrice() external view returns (uint256) { - return s_fallbackNativePrice; + asset.safeTransfer(to, amount); + emit FeesWithdrawn(address(asset), to, amount); } } diff --git a/contracts/src/v0.8/automation/dev/v2_3/AutomationRegistryLogicC2_3.sol b/contracts/src/v0.8/automation/dev/v2_3/AutomationRegistryLogicC2_3.sol new file mode 100644 index 00000000..ad8512be --- /dev/null +++ b/contracts/src/v0.8/automation/dev/v2_3/AutomationRegistryLogicC2_3.sol @@ -0,0 +1,577 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.19; + +import {AutomationRegistryBase2_3} from "./AutomationRegistryBase2_3.sol"; +import {EnumerableSet} from "../../../vendor/openzeppelin-solidity/v4.7.3/contracts/utils/structs/EnumerableSet.sol"; +import {Address} from "../../../vendor/openzeppelin-solidity/v4.7.3/contracts/utils/Address.sol"; +import {IAutomationForwarder} from "../../interfaces/IAutomationForwarder.sol"; +import {IChainModule} from "../../interfaces/IChainModule.sol"; +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {IAutomationV21PlusCommon} from "../../interfaces/IAutomationV21PlusCommon.sol"; + +contract AutomationRegistryLogicC2_3 is AutomationRegistryBase2_3 { + using Address for address; + using EnumerableSet for EnumerableSet.UintSet; + using EnumerableSet for EnumerableSet.AddressSet; + + /** + * @dev see AutomationRegistry master contract for constructor description + */ + constructor( + address link, + address linkUSDFeed, + address nativeUSDFeed, + address fastGasFeed, + address automationForwarderLogic, + address allowedReadOnlyAddress, + PayoutMode payoutMode, + address wrappedNativeTokenAddress + ) + AutomationRegistryBase2_3( + link, + linkUSDFeed, + nativeUSDFeed, + fastGasFeed, + automationForwarderLogic, + allowedReadOnlyAddress, + payoutMode, + wrappedNativeTokenAddress + ) + {} + + // ================================================================ + // | NODE ACTIONS | + // ================================================================ + + /** + * @notice transfers the address of payee for a transmitter + */ + function transferPayeeship(address transmitter, address proposed) external { + if (s_transmitterPayees[transmitter] != msg.sender) revert OnlyCallableByPayee(); + if (proposed == msg.sender) revert ValueNotChanged(); + + if (s_proposedPayee[transmitter] != proposed) { + s_proposedPayee[transmitter] = proposed; + emit PayeeshipTransferRequested(transmitter, msg.sender, proposed); + } + } + + /** + * @notice accepts the transfer of the payee + */ + function acceptPayeeship(address transmitter) external { + if (s_proposedPayee[transmitter] != msg.sender) revert OnlyCallableByProposedPayee(); + address past = s_transmitterPayees[transmitter]; + s_transmitterPayees[transmitter] = msg.sender; + s_proposedPayee[transmitter] = ZERO_ADDRESS; + + emit PayeeshipTransferred(transmitter, past, msg.sender); + } + + /** + * @notice withdraws LINK received as payment for work performed + */ + function withdrawPayment(address from, address to) external { + if (to == ZERO_ADDRESS) revert InvalidRecipient(); + if (s_payoutMode == PayoutMode.OFF_CHAIN) revert MustSettleOffchain(); + if (s_transmitterPayees[from] != msg.sender) revert OnlyCallableByPayee(); + uint96 balance = _updateTransmitterBalanceFromPool(from, s_hotVars.totalPremium, uint96(s_transmittersList.length)); + s_transmitters[from].balance = 0; + s_reserveAmounts[IERC20(address(i_link))] = s_reserveAmounts[IERC20(address(i_link))] - balance; + 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 { + _onlyPrivilegeManagerAllowed(); + s_upkeepPrivilegeConfig[upkeepId] = newPrivilegeConfig; + emit UpkeepPrivilegeConfigSet(upkeepId, newPrivilegeConfig); + } + + /** + * @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 { + _onlyPrivilegeManagerAllowed(); + s_adminPrivilegeConfig[admin] = newPrivilegeConfig; + emit AdminPrivilegeConfigSet(admin, newPrivilegeConfig); + } + + /** + * @notice settles NOPs' LINK payment offchain + */ + function settleNOPsOffchain() external { + _onlyFinanceAdminAllowed(); + if (s_payoutMode == PayoutMode.ON_CHAIN) revert MustSettleOnchain(); + + uint256 activeTransmittersLength = s_transmittersList.length; + uint256 deactivatedTransmittersLength = s_deactivatedTransmitters.length(); + uint256 length = activeTransmittersLength + deactivatedTransmittersLength; + uint256[] memory payments = new uint256[](length); + address[] memory payees = new address[](length); + for (uint256 i = 0; i < activeTransmittersLength; i++) { + address transmitterAddr = s_transmittersList[i]; + uint96 balance = _updateTransmitterBalanceFromPool( + transmitterAddr, + s_hotVars.totalPremium, + uint96(activeTransmittersLength) + ); + payments[i] = balance; + payees[i] = s_transmitterPayees[transmitterAddr]; + s_transmitters[transmitterAddr].balance = 0; + } + for (uint256 i = 0; i < deactivatedTransmittersLength; i++) { + address deactivatedAddr = s_deactivatedTransmitters.at(i); + Transmitter memory transmitter = s_transmitters[deactivatedAddr]; + payees[i + activeTransmittersLength] = s_transmitterPayees[deactivatedAddr]; + payments[i + activeTransmittersLength] = transmitter.balance; + s_transmitters[deactivatedAddr].balance = 0; + } + delete s_deactivatedTransmitters; + + emit NOPsSettledOffchain(payees, payments); + } + + /** + * @notice disables offchain payment for NOPs + */ + function disableOffchainPayments() external onlyOwner { + s_payoutMode = PayoutMode.ON_CHAIN; + } + + // ================================================================ + // | GETTERS | + // ================================================================ + + function getConditionalGasOverhead() external pure returns (uint256) { + return REGISTRY_CONDITIONAL_OVERHEAD; + } + + function getLogGasOverhead() external pure returns (uint256) { + return REGISTRY_LOG_OVERHEAD; + } + + function getPerPerformByteGasOverhead() external pure returns (uint256) { + return REGISTRY_PER_PERFORM_BYTE_GAS_OVERHEAD; + } + + function getPerSignerGasOverhead() external pure returns (uint256) { + return REGISTRY_PER_SIGNER_GAS_OVERHEAD; + } + + function getTransmitCalldataFixedBytesOverhead() external pure returns (uint256) { + return TRANSMIT_CALLDATA_FIXED_BYTES_OVERHEAD; + } + + function getTransmitCalldataPerSignerBytesOverhead() external pure returns (uint256) { + return TRANSMIT_CALLDATA_PER_SIGNER_BYTES_OVERHEAD; + } + + function getCancellationDelay() external pure returns (uint256) { + return CANCELLATION_DELAY; + } + + function getLinkAddress() external view returns (address) { + return address(i_link); + } + + function getLinkUSDFeedAddress() external view returns (address) { + return address(i_linkUSDFeed); + } + + function getNativeUSDFeedAddress() external view returns (address) { + return address(i_nativeUSDFeed); + } + + function getFastGasFeedAddress() external view returns (address) { + return address(i_fastGasFeed); + } + + function getAutomationForwarderLogic() external view returns (address) { + return i_automationForwarderLogic; + } + + function getAllowedReadOnlyAddress() external view returns (address) { + return i_allowedReadOnlyAddress; + } + + function getWrappedNativeTokenAddress() external view returns (address) { + return address(i_wrappedNativeToken); + } + + function getBillingToken(uint256 upkeepID) external view returns (IERC20) { + return s_upkeep[upkeepID].billingToken; + } + + function getBillingTokens() external view returns (IERC20[] memory) { + return s_billingTokens; + } + + function supportsBillingToken(IERC20 token) external view returns (bool) { + return address(s_billingConfigs[token].priceFeed) != address(0); + } + + function getBillingTokenConfig(IERC20 token) external view returns (BillingConfig memory) { + return s_billingConfigs[token]; + } + + function getPayoutMode() external view returns (PayoutMode) { + return s_payoutMode; + } + + function upkeepVersion() public pure returns (uint8) { + return UPKEEP_VERSION_BASE; + } + + /** + * @notice gets the number of upkeeps on the registry + */ + function getNumUpkeeps() external view returns (uint256) { + return s_upkeepIDs.length(); + } + + /** + * @notice read all of the details about an upkeep + * @dev this function may be deprecated in a future version of automation in favor of individual + * getters for each field + */ + function getUpkeep(uint256 id) external view returns (IAutomationV21PlusCommon.UpkeepInfoLegacy memory upkeepInfo) { + Upkeep memory reg = s_upkeep[id]; + address target = address(reg.forwarder) == address(0) ? address(0) : reg.forwarder.getTarget(); + upkeepInfo = IAutomationV21PlusCommon.UpkeepInfoLegacy({ + target: target, + performGas: reg.performGas, + checkData: s_checkData[id], + balance: reg.balance, + admin: s_upkeepAdmin[id], + maxValidBlocknumber: reg.maxValidBlocknumber, + lastPerformedBlockNumber: reg.lastPerformedBlockNumber, + amountSpent: uint96(reg.amountSpent), // force casting to uint96 for backwards compatibility. Not an issue if it overflows. + paused: reg.paused, + offchainConfig: s_upkeepOffchainConfig[id] + }); + return upkeepInfo; + } + + /** + * @notice retrieve active upkeep IDs. Active upkeep is defined as an upkeep which is not paused and not canceled. + * @param startIndex starting index in list + * @param maxCount max count to retrieve (0 = unlimited) + * @dev the order of IDs in the list is **not guaranteed**, therefore, if making successive calls, one + * should consider keeping the blockheight constant to ensure a holistic picture of the contract state + */ + function getActiveUpkeepIDs(uint256 startIndex, uint256 maxCount) external view returns (uint256[] memory) { + uint256 numUpkeeps = s_upkeepIDs.length(); + if (startIndex >= numUpkeeps) revert IndexOutOfRange(); + uint256 endIndex = startIndex + maxCount; + endIndex = endIndex > numUpkeeps || maxCount == 0 ? numUpkeeps : endIndex; + uint256[] memory ids = new uint256[](endIndex - startIndex); + for (uint256 idx = 0; idx < ids.length; idx++) { + ids[idx] = s_upkeepIDs.at(idx + startIndex); + } + return ids; + } + + /** + * @notice returns the upkeep's trigger type + */ + function getTriggerType(uint256 upkeepId) external pure returns (Trigger) { + return _getTriggerType(upkeepId); + } + + /** + * @notice returns the trigger config for an upkeeep + */ + function getUpkeepTriggerConfig(uint256 upkeepId) public view returns (bytes memory) { + return s_upkeepTriggerConfig[upkeepId]; + } + + /** + * @notice read the current info about any transmitter address + */ + function getTransmitterInfo( + address query + ) external view returns (bool active, uint8 index, uint96 balance, uint96 lastCollected, address payee) { + Transmitter memory transmitter = s_transmitters[query]; + + uint96 pooledShare = 0; + if (transmitter.active) { + uint96 totalDifference = s_hotVars.totalPremium - transmitter.lastCollected; + pooledShare = totalDifference / uint96(s_transmittersList.length); + } + + return ( + transmitter.active, + transmitter.index, + (transmitter.balance + pooledShare), + transmitter.lastCollected, + s_transmitterPayees[query] + ); + } + + /** + * @notice read the current info about any signer address + */ + function getSignerInfo(address query) external view returns (bool active, uint8 index) { + Signer memory signer = s_signers[query]; + return (signer.active, signer.index); + } + + /** + * @notice read the current on-chain config of the registry + * @dev this function will change between versions, it should never be used where + * backwards compatibility matters! + */ + function getConfig() external view returns (OnchainConfig memory) { + return + OnchainConfig({ + checkGasLimit: s_storage.checkGasLimit, + stalenessSeconds: s_hotVars.stalenessSeconds, + gasCeilingMultiplier: s_hotVars.gasCeilingMultiplier, + maxPerformGas: s_storage.maxPerformGas, + maxCheckDataSize: s_storage.maxCheckDataSize, + maxPerformDataSize: s_storage.maxPerformDataSize, + maxRevertDataSize: s_storage.maxRevertDataSize, + fallbackGasPrice: s_fallbackGasPrice, + fallbackLinkPrice: s_fallbackLinkPrice, + fallbackNativePrice: s_fallbackNativePrice, + transcoder: s_storage.transcoder, + registrars: s_registrars.values(), + upkeepPrivilegeManager: s_storage.upkeepPrivilegeManager, + chainModule: s_hotVars.chainModule, + reorgProtectionEnabled: s_hotVars.reorgProtectionEnabled, + financeAdmin: s_storage.financeAdmin + }); + } + + /** + * @notice read the current state of the registry + * @dev this function is deprecated + */ + function getState() + external + view + returns ( + IAutomationV21PlusCommon.StateLegacy memory state, + IAutomationV21PlusCommon.OnchainConfigLegacy memory config, + address[] memory signers, + address[] memory transmitters, + uint8 f + ) + { + state = IAutomationV21PlusCommon.StateLegacy({ + nonce: s_storage.nonce, + ownerLinkBalance: 0, // deprecated + expectedLinkBalance: 0, // deprecated + totalPremium: s_hotVars.totalPremium, + numUpkeeps: s_upkeepIDs.length(), + configCount: s_storage.configCount, + latestConfigBlockNumber: s_storage.latestConfigBlockNumber, + latestConfigDigest: s_latestConfigDigest, + latestEpoch: s_hotVars.latestEpoch, + paused: s_hotVars.paused + }); + + config = IAutomationV21PlusCommon.OnchainConfigLegacy({ + paymentPremiumPPB: 0, // deprecated + flatFeeMicroLink: 0, // deprecated + checkGasLimit: s_storage.checkGasLimit, + stalenessSeconds: s_hotVars.stalenessSeconds, + gasCeilingMultiplier: s_hotVars.gasCeilingMultiplier, + minUpkeepSpend: 0, // deprecated + maxPerformGas: s_storage.maxPerformGas, + maxCheckDataSize: s_storage.maxCheckDataSize, + maxPerformDataSize: s_storage.maxPerformDataSize, + maxRevertDataSize: s_storage.maxRevertDataSize, + fallbackGasPrice: s_fallbackGasPrice, + fallbackLinkPrice: s_fallbackLinkPrice, + transcoder: s_storage.transcoder, + registrars: s_registrars.values(), + upkeepPrivilegeManager: s_storage.upkeepPrivilegeManager + }); + + return (state, config, s_signersList, s_transmittersList, s_hotVars.f); + } + + /** + * @notice read the Storage data + * @dev this function signature will change with each version of automation + * this should not be treated as a stable function + */ + function getStorage() external view returns (Storage memory) { + return s_storage; + } + + /** + * @notice read the HotVars data + * @dev this function signature will change with each version of automation + * this should not be treated as a stable function + */ + function getHotVars() external view returns (HotVars memory) { + return s_hotVars; + } + + /** + * @notice get the chain module + */ + function getChainModule() external view returns (IChainModule chainModule) { + return s_hotVars.chainModule; + } + + /** + * @notice if this registry has reorg protection enabled + */ + function getReorgProtectionEnabled() external view returns (bool reorgProtectionEnabled) { + return s_hotVars.reorgProtectionEnabled; + } + + /** + * @notice calculates the minimum balance required for an upkeep to remain eligible + * @param id the upkeep id to calculate minimum balance for + */ + function getBalance(uint256 id) external view returns (uint96 balance) { + return s_upkeep[id].balance; + } + + /** + * @notice calculates the minimum balance required for an upkeep to remain eligible + * @param id the upkeep id to calculate minimum balance for + */ + function getMinBalance(uint256 id) external view returns (uint96) { + return getMinBalanceForUpkeep(id); + } + + /** + * @notice calculates the minimum balance required for an upkeep to remain eligible + * @param id the upkeep id to calculate minimum balance for + * @dev this will be deprecated in a future version in favor of getMinBalance + */ + function getMinBalanceForUpkeep(uint256 id) public view returns (uint96 minBalance) { + Upkeep memory upkeep = s_upkeep[id]; + return getMaxPaymentForGas(id, _getTriggerType(id), upkeep.performGas, upkeep.billingToken); + } + + /** + * @notice calculates the maximum payment for a given gas limit + * @param gasLimit the gas to calculate payment for + */ + function getMaxPaymentForGas( + uint256 id, + Trigger triggerType, + uint32 gasLimit, + IERC20 billingToken + ) public view returns (uint96 maxPayment) { + HotVars memory hotVars = s_hotVars; + (uint256 fastGasWei, uint256 linkUSD, uint256 nativeUSD) = _getFeedData(hotVars); + return _getMaxPayment(id, hotVars, triggerType, gasLimit, fastGasWei, linkUSD, nativeUSD, billingToken); + } + + /** + * @notice retrieves the migration permission for a peer registry + */ + function getPeerRegistryMigrationPermission(address peer) external view returns (MigrationPermission) { + return s_peerRegistryMigrationPermission[peer]; + } + + /** + * @notice returns the upkeep privilege config + */ + function getUpkeepPrivilegeConfig(uint256 upkeepId) external view returns (bytes memory) { + return s_upkeepPrivilegeConfig[upkeepId]; + } + + /** + * @notice returns the 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]; + } + + /** + * @notice returns the fallback native price + */ + function getFallbackNativePrice() external view returns (uint256) { + return s_fallbackNativePrice; + } + + /** + * @notice returns the amount of a particular token that is reserved as + * user deposits / NOP payments + */ + function getReserveAmount(IERC20 billingToken) external view returns (uint256) { + return s_reserveAmounts[billingToken]; + } + + /** + * @notice returns the size of the LINK liquidity pool + */ + function linkAvailableForPayment() public view returns (int256) { + return _linkAvailableForPayment(); + } +} diff --git a/contracts/src/v0.8/automation/dev/v2_3/AutomationUtils2_3.sol b/contracts/src/v0.8/automation/dev/v2_3/AutomationUtils2_3.sol index 5f0a4052..59081b7f 100644 --- a/contracts/src/v0.8/automation/dev/v2_3/AutomationUtils2_3.sol +++ b/contracts/src/v0.8/automation/dev/v2_3/AutomationUtils2_3.sol @@ -2,7 +2,6 @@ pragma solidity 0.8.19; import {AutomationRegistryBase2_3} from "./AutomationRegistryBase2_3.sol"; -import {Log} from "../../interfaces/ILogAutomation.sol"; /** * @notice this file exposes structs that are otherwise internal to the automation registry @@ -10,35 +9,9 @@ import {Log} from "../../interfaces/ILogAutomation.sol"; * 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_3 { /** - * @dev this can be removed as OnchainConfig is now exposed directly from the registry + * @dev this uses the v2.3 Report, which uses linkUSD instead of linkNative (as in v2.2 and prior). This should be used only in typescript tests. */ - function _onChainConfig( - AutomationRegistryBase2_3.OnchainConfig memory, - address[] memory, - AutomationRegistryBase2_3.BillingConfig[] memory - ) external {} - function _report(AutomationRegistryBase2_3.Report memory) external {} // 0xe65d6546 - - function _logTriggerConfig(LogTriggerConfig memory) external {} // 0x21f373d7 - - function _logTrigger(AutomationRegistryBase2_3.LogTrigger memory) external {} // 0x1c8d8260 - - function _conditionalTrigger(AutomationRegistryBase2_3.ConditionalTrigger memory) external {} // 0x4b6df294 - - function _log(Log memory) external {} // 0xe9720a49 } diff --git a/contracts/src/v0.8/automation/dev/v2_3/UpkeepTranscoder5_0.sol b/contracts/src/v0.8/automation/dev/v2_3/UpkeepTranscoder5_0.sol new file mode 100644 index 00000000..6f3d5567 --- /dev/null +++ b/contracts/src/v0.8/automation/dev/v2_3/UpkeepTranscoder5_0.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity 0.8.19; + +import {UpkeepTranscoderInterfaceV2} from "../../interfaces/UpkeepTranscoderInterfaceV2.sol"; +import {TypeAndVersionInterface} from "../../../interfaces/TypeAndVersionInterface.sol"; + +enum RegistryVersion { + V12, + V13, + V20, + V21, + V23 +} + +/** + * @notice UpkeepTranscoder is a contract that allows converting upkeep data from previous registry versions to newer versions + * @dev it currently only supports 2.3 -> 2.3 migrations + */ +contract UpkeepTranscoder5_0 is UpkeepTranscoderInterfaceV2, TypeAndVersionInterface { + error InvalidTranscoding(); + + string public constant override typeAndVersion = "UpkeepTranscoder 5.0.0"; + + /** + * @notice transcodeUpkeeps transforms upkeep data from the format expected by + * one registry to the format expected by another. It future-proofs migrations + * by allowing automation team to customize migration paths and set sensible defaults + * when new fields are added + * @param fromVersion version the upkeep is migrating from + * @param toVersion version the upkeep is migrating to + * @param encodedUpkeeps encoded upkeep data + * @dev this transcoder should ONLY be use for V23->V23 migrations for now + */ + function transcodeUpkeeps( + uint8 fromVersion, + uint8 toVersion, + bytes calldata encodedUpkeeps + ) external view override returns (bytes memory) { + if (toVersion == uint8(RegistryVersion.V23) && fromVersion == uint8(RegistryVersion.V23)) { + return encodedUpkeeps; + } + + revert InvalidTranscoding(); + } +} diff --git a/contracts/src/v0.8/automation/mocks/MockUpkeep.sol b/contracts/src/v0.8/automation/mocks/MockUpkeep.sol new file mode 100644 index 00000000..17899f4c --- /dev/null +++ b/contracts/src/v0.8/automation/mocks/MockUpkeep.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract MockUpkeep { + bool public shouldCheckRevert; + bool public shouldPerformRevert; + bool public checkResult = true; + bytes public performData; + uint256 public checkGasToBurn; + uint256 public performGasToBurn; + + event UpkeepPerformedWith(bytes upkeepData); + error CheckRevert(); + error PerformRevert(); + + function setShouldCheckRevert(bool value) public { + shouldCheckRevert = value; + } + + function setShouldPerformRevert(bool value) public { + shouldPerformRevert = value; + } + + function setCheckResult(bool value) public { + checkResult = value; + } + + function setPerformData(bytes calldata data) public { + performData = data; + } + + function setCheckGasToBurn(uint256 value) public { + checkGasToBurn = value; + } + + function setPerformGasToBurn(uint256 value) public { + performGasToBurn = value; + } + + function checkUpkeep(bytes calldata) external view returns (bool callable, bytes memory executedata) { + if (shouldCheckRevert) revert CheckRevert(); + uint256 startGas = gasleft(); + while (startGas - gasleft() < checkGasToBurn) {} // burn gas + return (checkResult, performData); + } + + function performUpkeep(bytes calldata data) external { + if (shouldPerformRevert) revert PerformRevert(); + uint256 startGas = gasleft(); + while (startGas - gasleft() < performGasToBurn) {} // burn gas + emit UpkeepPerformedWith(data); + } +} diff --git a/contracts/src/v0.8/automation/testhelpers/SimpleLogUpkeepCounter.sol b/contracts/src/v0.8/automation/testhelpers/SimpleLogUpkeepCounter.sol index 979cc613..c4d57713 100644 --- a/contracts/src/v0.8/automation/testhelpers/SimpleLogUpkeepCounter.sol +++ b/contracts/src/v0.8/automation/testhelpers/SimpleLogUpkeepCounter.sol @@ -3,14 +3,16 @@ pragma solidity 0.8.6; import {ILogAutomation, Log} from "../interfaces/ILogAutomation.sol"; +import "../interfaces/StreamsLookupCompatibleInterface.sol"; struct CheckData { uint256 checkBurnAmount; uint256 performBurnAmount; bytes32 eventSig; + string[] feeds; } -contract SimpleLogUpkeepCounter is ILogAutomation { +contract SimpleLogUpkeepCounter is ILogAutomation, StreamsLookupCompatibleInterface { event PerformingUpkeep( address indexed from, uint256 initialBlock, @@ -27,36 +29,77 @@ contract SimpleLogUpkeepCounter is ILogAutomation { uint256 public initialBlock; uint256 public counter; uint256 public timeToPerform; - bool public isRecovered; + bool internal isRecovered; + bool public isStreamsLookup; + bool public shouldRetryOnError; + string public feedParamKey = "feedIDs"; + string public timeParamKey = "timestamp"; - constructor() { + constructor(bool _isStreamsLookup) { previousPerformBlock = 0; lastBlock = block.number; initialBlock = 0; counter = 0; + isStreamsLookup = _isStreamsLookup; } function _checkDataConfig(CheckData memory) external {} + function setTimeParamKey(string memory timeParam) external { + timeParamKey = timeParam; + } + + function setFeedParamKey(string memory feedParam) external { + feedParamKey = feedParam; + } + + function setShouldRetryOnErrorBool(bool value) public { + shouldRetryOnError = value; + } + function checkLog(Log calldata log, bytes calldata checkData) external view override returns (bool, bytes memory) { - (uint256 checkBurnAmount, uint256 performBurnAmount, bytes32 eventSig) = abi.decode( - checkData, - (uint256, uint256, bytes32) - ); + CheckData memory _checkData = abi.decode(checkData, (CheckData)); uint256 startGas = gasleft(); bytes32 dummyIndex = blockhash(block.number - 1); bool dummy; // burn gas - if (checkBurnAmount > 0) { - while (startGas - gasleft() < checkBurnAmount) { + if (_checkData.checkBurnAmount > 0) { + while (startGas - gasleft() < _checkData.checkBurnAmount) { dummy = dummy && dummyMap[dummyIndex]; // arbitrary storage reads dummyIndex = keccak256(abi.encode(dummyIndex, address(this))); } } - if (log.topics[2] == eventSig) { - return (true, abi.encode(log, block.number, checkData)); + bytes[] memory values = new bytes[](2); + values[0] = abi.encode(0x00); + values[1] = abi.encode(0x00); + bytes memory extraData = abi.encode(log, block.number, checkData); + if (log.topics[2] == _checkData.eventSig) { + if (isStreamsLookup) { + revert StreamsLookup(feedParamKey, _checkData.feeds, timeParamKey, block.timestamp, extraData); + } + return (true, abi.encode(values, extraData)); } - return (false, abi.encode(log, block.number, checkData)); + return (false, abi.encode(values, extraData)); + } + + function checkCallback( + bytes[] memory values, + bytes memory extraData + ) external view override returns (bool, bytes memory) { + // do sth about the chainlinkBlob data in values and extraData + bytes memory performData = abi.encode(values, extraData); + return (true, performData); + } + + function checkErrorHandler( + uint256 errCode, + bytes memory extraData + ) external view override returns (bool upkeepNeeded, bytes memory performData) { + bytes[] memory values = new bytes[](2); + values[0] = abi.encode(errCode); + values[1] = abi.encode(extraData); + bytes memory returnData = abi.encode(values, extraData); + return (shouldRetryOnError, returnData); } function performUpkeep(bytes calldata performData) external override { @@ -66,22 +109,23 @@ contract SimpleLogUpkeepCounter is ILogAutomation { lastBlock = block.number; counter = counter + 1; previousPerformBlock = lastBlock; - (Log memory log, uint256 checkBlock, bytes memory extraData) = abi.decode(performData, (Log, uint256, bytes)); + (, bytes memory extraData) = abi.decode(performData, (bytes[], bytes)); + (Log memory log, uint256 checkBlock, bytes memory checkData) = abi.decode(extraData, (Log, uint256, bytes)); timeToPerform = block.timestamp - log.timestamp; isRecovered = false; if (checkBlock != log.blockNumber) { isRecovered = true; } - (uint256 checkBurnAmount, uint256 performBurnAmount, bytes32 eventSig) = abi.decode( - extraData, - (uint256, uint256, bytes32) - ); + CheckData memory _checkData = abi.decode(checkData, (CheckData)); uint256 startGas = gasleft(); bytes32 dummyIndex = blockhash(block.number - 1); bool dummy; + if (log.topics[2] != _checkData.eventSig) { + revert("Invalid event signature"); + } // burn gas - if (performBurnAmount > 0) { - while (startGas - gasleft() < performBurnAmount) { + if (_checkData.performBurnAmount > 0) { + while (startGas - gasleft() < _checkData.performBurnAmount) { dummy = dummy && dummyMap[dummyIndex]; // arbitrary storage reads dummyIndex = keccak256(abi.encode(dummyIndex, address(this))); } diff --git a/contracts/src/v0.8/automation/testhelpers/UpkeepAutoFunder.sol b/contracts/src/v0.8/automation/testhelpers/UpkeepAutoFunder.sol new file mode 100644 index 00000000..263aad54 --- /dev/null +++ b/contracts/src/v0.8/automation/testhelpers/UpkeepAutoFunder.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {AutomationCompatible} from "../AutomationCompatible.sol"; +import {LinkTokenInterface} from "../../shared/interfaces/LinkTokenInterface.sol"; +import {ConfirmedOwner} from "../../shared/access/ConfirmedOwner.sol"; +import {AutomationRegistryBaseInterface} from "../interfaces/v2_0/AutomationRegistryInterface2_0.sol"; + +contract UpkeepAutoFunder is AutomationCompatible, ConfirmedOwner { + bool public s_isEligible; + bool public s_shouldCancel; + uint256 public s_upkeepId; + uint96 public s_autoFundLink; + LinkTokenInterface public immutable LINK; + AutomationRegistryBaseInterface public immutable s_keeperRegistry; + + constructor(address linkAddress, address registryAddress) ConfirmedOwner(msg.sender) { + LINK = LinkTokenInterface(linkAddress); + s_keeperRegistry = AutomationRegistryBaseInterface(registryAddress); + + s_isEligible = false; + s_shouldCancel = false; + s_upkeepId = 0; + s_autoFundLink = 0; + } + + function setShouldCancel(bool value) external onlyOwner { + s_shouldCancel = value; + } + + function setIsEligible(bool value) external onlyOwner { + s_isEligible = value; + } + + function setAutoFundLink(uint96 value) external onlyOwner { + s_autoFundLink = value; + } + + function setUpkeepId(uint256 value) external onlyOwner { + s_upkeepId = value; + } + + function checkUpkeep( + bytes calldata data + ) external override cannotExecute returns (bool callable, bytes calldata executedata) { + return (s_isEligible, data); + } + + function performUpkeep(bytes calldata data) external override { + require(s_isEligible, "Upkeep should be eligible"); + s_isEligible = false; // Allow upkeep only once until it is set again + + // Topup upkeep so it can be called again + LINK.transferAndCall(address(s_keeperRegistry), s_autoFundLink, abi.encode(s_upkeepId)); + + if (s_shouldCancel) { + s_keeperRegistry.cancelUpkeep(s_upkeepId); + } + } +} diff --git a/contracts/src/v0.8/automation/testhelpers/UpkeepMock.sol b/contracts/src/v0.8/automation/testhelpers/UpkeepMock.sol new file mode 100644 index 00000000..392700ea --- /dev/null +++ b/contracts/src/v0.8/automation/testhelpers/UpkeepMock.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {AutomationCompatible} from "../AutomationCompatible.sol"; + +contract UpkeepMock is AutomationCompatible { + bool public shouldRevertCheck; + bool public canCheck; + bool public canPerform; + bytes public performData; + uint256 public checkGasToBurn; + uint256 public performGasToBurn; + string public checkRevertReason; + + uint256 constant checkGasBuffer = 6000; // use all but this amount in gas burn loops + uint256 constant performGasBuffer = 1000; // use all but this amount in gas burn loops + + event UpkeepPerformedWith(bytes upkeepData); + + function setShouldRevertCheck(bool value) public { + shouldRevertCheck = value; + } + + function setPerformData(bytes calldata data) public { + performData = data; + } + + function setCanCheck(bool value) public { + canCheck = value; + } + + function setCanPerform(bool value) public { + canPerform = value; + } + + function setCheckRevertReason(string calldata value) public { + checkRevertReason = value; + } + + function setCheckGasToBurn(uint256 value) public { + require(value > checkGasBuffer || value == 0, "checkGasToBurn must be 0 (disabled) or greater than buffer"); + if (value > 0) { + checkGasToBurn = value - checkGasBuffer; + } else { + checkGasToBurn = 0; + } + } + + function setPerformGasToBurn(uint256 value) public { + require(value > performGasBuffer || value == 0, "performGasToBurn must be 0 (disabled) or greater than buffer"); + if (value > 0) { + performGasToBurn = value - performGasBuffer; + } else { + performGasToBurn = 0; + } + } + + function checkUpkeep( + bytes calldata + ) external override cannotExecute returns (bool callable, bytes memory executedata) { + require(!shouldRevertCheck, checkRevertReason); + uint256 startGas = gasleft(); + bool couldCheck = canCheck; + + setCanCheck(false); // test that state modifications don't stick + + while (startGas - gasleft() < checkGasToBurn) {} // burn gas + + return (couldCheck, performData); + } + + function performUpkeep(bytes calldata data) external override { + uint256 startGas = gasleft(); + + require(canPerform, "Cannot perform"); + + emit UpkeepPerformedWith(data); + + while (startGas - gasleft() < performGasToBurn) {} // burn gas + } +} diff --git a/contracts/src/v0.8/automation/testhelpers/UpkeepReverter.sol b/contracts/src/v0.8/automation/testhelpers/UpkeepReverter.sol new file mode 100644 index 00000000..1d140ccf --- /dev/null +++ b/contracts/src/v0.8/automation/testhelpers/UpkeepReverter.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {AutomationCompatible} from "../AutomationCompatible.sol"; + +contract UpkeepReverter is AutomationCompatible { + function checkUpkeep( + bytes calldata data + ) public view override cannotExecute returns (bool callable, bytes calldata executedata) { + require(false, "!working"); + return (true, data); + } + + function performUpkeep(bytes calldata) external pure override { + require(false, "!working"); + } +} diff --git a/contracts/src/v0.8/automation/upkeeps/LinkAvailableBalanceMonitor.sol b/contracts/src/v0.8/automation/upkeeps/LinkAvailableBalanceMonitor.sol index ea01678f..6bf74de2 100644 --- a/contracts/src/v0.8/automation/upkeeps/LinkAvailableBalanceMonitor.sol +++ b/contracts/src/v0.8/automation/upkeeps/LinkAvailableBalanceMonitor.sol @@ -266,6 +266,7 @@ contract LinkAvailableBalanceMonitor is AccessControl, AutomationCompatibleInter for (uint256 idx = 0; idx < targetAddresses.length; idx++) { address targetAddress = targetAddresses[idx]; contractToFund = s_targets[targetAddress]; + s_targets[targetAddress].lastTopUpTimestamp = uint56(block.timestamp); if ( localBalance >= contractToFund.topUpAmount && _needsFunding( @@ -278,12 +279,13 @@ contract LinkAvailableBalanceMonitor is AccessControl, AutomationCompatibleInter bool success = i_linkToken.transfer(targetAddress, contractToFund.topUpAmount); if (success) { localBalance -= contractToFund.topUpAmount; - s_targets[targetAddress].lastTopUpTimestamp = uint56(block.timestamp); emit TopUpSucceeded(targetAddress); } else { + s_targets[targetAddress].lastTopUpTimestamp = contractToFund.lastTopUpTimestamp; emit TopUpFailed(targetAddress); } } else { + s_targets[targetAddress].lastTopUpTimestamp = contractToFund.lastTopUpTimestamp; emit TopUpBlocked(targetAddress); } } diff --git a/contracts/src/v0.8/functions/dev/v1_X/FunctionsBilling.sol b/contracts/src/v0.8/functions/dev/v1_X/FunctionsBilling.sol index b6b65c4e..c973f55a 100644 --- a/contracts/src/v0.8/functions/dev/v1_X/FunctionsBilling.sol +++ b/contracts/src/v0.8/functions/dev/v1_X/FunctionsBilling.sol @@ -106,13 +106,13 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling { /// @inheritdoc IFunctionsBilling function getDONFeeJuels(bytes memory /* requestData */) public view override returns (uint72) { - // s_config.donFee is in cents of USD. Get Juel amount then convert to dollars. + // s_config.donFee is in cents of USD. Convert to dollars amount then get amount of Juels. return SafeCast.toUint72(_getJuelsFromUsd(s_config.donFeeCentsUsd) / 100); } /// @inheritdoc IFunctionsBilling function getOperationFeeJuels() public view override returns (uint72) { - // s_config.donFee is in cents of USD. Get Juel amount then convert to dollars. + // s_config.donFee is in cents of USD. Convert to dollars then get amount of Juels. return SafeCast.toUint72(_getJuelsFromUsd(s_config.operationFeeCentsUsd) / 100); } @@ -124,6 +124,7 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling { /// @inheritdoc IFunctionsBilling function getWeiPerUnitLink() public view returns (uint256) { (, int256 weiPerUnitLink, , uint256 timestamp, ) = s_linkToNativeFeed.latestRoundData(); + // Only fallback if feedStalenessSeconds is set // solhint-disable-next-line not-rely-on-time if (s_config.feedStalenessSeconds < block.timestamp - timestamp && s_config.feedStalenessSeconds > 0) { return s_config.fallbackNativePerUnitLink; @@ -143,6 +144,7 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling { /// @inheritdoc IFunctionsBilling function getUsdPerUnitLink() public view returns (uint256, uint8) { (, int256 usdPerUnitLink, , uint256 timestamp, ) = s_linkToUsdFeed.latestRoundData(); + // Only fallback if feedStalenessSeconds is set // solhint-disable-next-line not-rely-on-time if (s_config.feedStalenessSeconds < block.timestamp - timestamp && s_config.feedStalenessSeconds > 0) { return (s_config.fallbackUsdPerUnitLink, s_config.fallbackUsdPerUnitLinkDecimals); @@ -420,6 +422,10 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling { revert NoTransmittersSet(); } uint96 feePoolShare = s_feePool / uint96(numberOfTransmitters); + if (feePoolShare == 0) { + // Dust cannot be evenly distributed to all transmitters + return; + } // Bounded by "maxNumOracles" on OCR2Abstract.sol for (uint256 i = 0; i < numberOfTransmitters; ++i) { s_withdrawableTokens[transmitters[i]] += feePoolShare; diff --git a/contracts/src/v0.8/functions/dev/v1_X/FunctionsClient.sol b/contracts/src/v0.8/functions/dev/v1_X/FunctionsClient.sol index 4aabef01..378714da 100644 --- a/contracts/src/v0.8/functions/dev/v1_X/FunctionsClient.sol +++ b/contracts/src/v0.8/functions/dev/v1_X/FunctionsClient.sol @@ -25,7 +25,8 @@ abstract contract FunctionsClient is IFunctionsClient { /// @notice Sends a Chainlink Functions request /// @param data The CBOR encoded bytes data for a Functions request /// @param subscriptionId The subscription ID that will be charged to service the request - /// @param callbackGasLimit the amount of gas that will be available for the fulfillment callback + /// @param callbackGasLimit - The amount of gas that will be available for the fulfillment callback + /// @param donId - An identifier used to determine which route to send the request along /// @return requestId The generated request ID for this request function _sendRequest( bytes memory data, diff --git a/contracts/src/v0.8/functions/dev/v1_X/FunctionsCoordinator.sol b/contracts/src/v0.8/functions/dev/v1_X/FunctionsCoordinator.sol index a70a8a75..f0bec7c3 100644 --- a/contracts/src/v0.8/functions/dev/v1_X/FunctionsCoordinator.sol +++ b/contracts/src/v0.8/functions/dev/v1_X/FunctionsCoordinator.sol @@ -16,7 +16,6 @@ contract FunctionsCoordinator is OCR2Base, IFunctionsCoordinator, FunctionsBilli using FunctionsResponse for FunctionsResponse.FulfillResult; /// @inheritdoc ITypeAndVersion - // solhint-disable-next-line chainlink-solidity/all-caps-constant-storage-variables string public constant override typeAndVersion = "Functions Coordinator v1.3.0"; event OracleRequest( diff --git a/contracts/src/v0.8/functions/dev/v1_X/FunctionsRouter.sol b/contracts/src/v0.8/functions/dev/v1_X/FunctionsRouter.sol index d86d8811..5cdb1193 100644 --- a/contracts/src/v0.8/functions/dev/v1_X/FunctionsRouter.sol +++ b/contracts/src/v0.8/functions/dev/v1_X/FunctionsRouter.sol @@ -18,7 +18,6 @@ contract FunctionsRouter is IFunctionsRouter, FunctionsSubscriptions, Pausable, using FunctionsResponse for FunctionsResponse.Commitment; using FunctionsResponse for FunctionsResponse.FulfillResult; - // solhint-disable-next-line chainlink-solidity/all-caps-constant-storage-variables string public constant override typeAndVersion = "Functions Router v2.0.0"; // We limit return data to a selector plus 4 words. This is to avoid diff --git a/contracts/src/v0.8/functions/dev/v1_X/accessControl/TermsOfServiceAllowList.sol b/contracts/src/v0.8/functions/dev/v1_X/accessControl/TermsOfServiceAllowList.sol index fe4ebe98..04604e67 100644 --- a/contracts/src/v0.8/functions/dev/v1_X/accessControl/TermsOfServiceAllowList.sol +++ b/contracts/src/v0.8/functions/dev/v1_X/accessControl/TermsOfServiceAllowList.sol @@ -16,7 +16,6 @@ contract TermsOfServiceAllowList is ITermsOfServiceAllowList, IAccessController, using EnumerableSet for EnumerableSet.AddressSet; /// @inheritdoc ITypeAndVersion - // solhint-disable-next-line chainlink-solidity/all-caps-constant-storage-variables string public constant override typeAndVersion = "Functions Terms of Service Allow List v1.1.0"; EnumerableSet.AddressSet private s_allowedSenders; @@ -128,11 +127,7 @@ contract TermsOfServiceAllowList is ITermsOfServiceAllowList, IAccessController, uint64 allowedSenderIdxStart, uint64 allowedSenderIdxEnd ) external view override returns (address[] memory allowedSenders) { - if ( - allowedSenderIdxStart > allowedSenderIdxEnd || - allowedSenderIdxEnd >= s_allowedSenders.length() || - s_allowedSenders.length() == 0 - ) { + if (allowedSenderIdxStart > allowedSenderIdxEnd || allowedSenderIdxEnd >= s_allowedSenders.length()) { revert InvalidCalldata(); } diff --git a/contracts/src/v0.8/functions/dev/v1_X/interfaces/IFunctionsBilling.sol b/contracts/src/v0.8/functions/dev/v1_X/interfaces/IFunctionsBilling.sol index 79806f1e..ecf15c68 100644 --- a/contracts/src/v0.8/functions/dev/v1_X/interfaces/IFunctionsBilling.sol +++ b/contracts/src/v0.8/functions/dev/v1_X/interfaces/IFunctionsBilling.sol @@ -59,7 +59,7 @@ interface IFunctionsBilling { struct FunctionsBillingConfig { uint32 fulfillmentGasPriceOverEstimationBP; // ══╗ Percentage of gas price overestimation to account for changes in gas price between request and response. Held as basis points (one hundredth of 1 percentage point) - uint32 feedStalenessSeconds; // ║ How long before we consider the feed price to be stale and fallback to fallbackNativePerUnitLink. + uint32 feedStalenessSeconds; // ║ How long before we consider the feed price to be stale and fallback to fallbackNativePerUnitLink. Default of 0 means no fallback. uint32 gasOverheadBeforeCallback; // ║ Represents the average gas execution cost before the fulfillment callback. This amount is always billed for every request. uint32 gasOverheadAfterCallback; // ║ Represents the average gas execution cost after the fulfillment callback. This amount is always billed for every request. uint40 minimumEstimateGasPriceWei; // ║ The lowest amount of wei that will be used as the tx.gasprice when estimating the cost to fulfill the request diff --git a/contracts/src/v0.8/functions/dev/v1_X/ocr/OCR2Base.sol b/contracts/src/v0.8/functions/dev/v1_X/ocr/OCR2Base.sol index cf461bdb..02ea5cf3 100644 --- a/contracts/src/v0.8/functions/dev/v1_X/ocr/OCR2Base.sol +++ b/contracts/src/v0.8/functions/dev/v1_X/ocr/OCR2Base.sol @@ -22,12 +22,12 @@ abstract contract OCR2Base is ConfirmedOwner, OCR2Abstract { // to extract config from logs. // Storing these fields used on the hot path in a ConfigInfo variable reduces the - // retrieval of all of them to a single SLOAD. If any further fields are - // added, make sure that storage of the struct still takes at most 32 bytes. + // retrieval of all of them into two SLOADs. If any further fields are + // added, make sure that storage of the struct still takes at most 64 bytes. struct ConfigInfo { bytes32 latestConfigDigest; - uint8 f; // TODO: could be optimized by squeezing into one slot - uint8 n; + uint8 f; // ───╮ + uint8 n; // ───╯ } ConfigInfo internal s_configInfo; @@ -215,7 +215,7 @@ abstract contract OCR2Base is ConfirmedOwner, OCR2Abstract { ); uint256 prefixMask = type(uint256).max << (256 - 16); // 0xFFFF00..00 uint256 prefix = 0x0001 << (256 - 16); // 0x000100..00 - return bytes32((prefix & prefixMask) | (h & ~prefixMask)); + return bytes32(prefix | (h & ~prefixMask)); } /** diff --git a/contracts/src/v0.8/functions/v1_1_0/FunctionsCoordinator.sol b/contracts/src/v0.8/functions/v1_1_0/FunctionsCoordinator.sol index 0a5da643..188e217b 100644 --- a/contracts/src/v0.8/functions/v1_1_0/FunctionsCoordinator.sol +++ b/contracts/src/v0.8/functions/v1_1_0/FunctionsCoordinator.sol @@ -16,7 +16,6 @@ contract FunctionsCoordinator is OCR2Base, IFunctionsCoordinator, FunctionsBilli using FunctionsResponse for FunctionsResponse.FulfillResult; /// @inheritdoc ITypeAndVersion - // solhint-disable-next-line chainlink-solidity/all-caps-constant-storage-variables string public constant override typeAndVersion = "Functions Coordinator v1.1.0"; event OracleRequest( diff --git a/contracts/src/v0.8/functions/v1_3_0/FunctionsBilling.sol b/contracts/src/v0.8/functions/v1_3_0/FunctionsBilling.sol new file mode 100644 index 00000000..49ecf3d6 --- /dev/null +++ b/contracts/src/v0.8/functions/v1_3_0/FunctionsBilling.sol @@ -0,0 +1,440 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IFunctionsSubscriptions} from "../v1_0_0/interfaces/IFunctionsSubscriptions.sol"; +import {AggregatorV3Interface} from "../../shared/interfaces/AggregatorV3Interface.sol"; +import {IFunctionsBilling, FunctionsBillingConfig} from "./interfaces/IFunctionsBilling.sol"; + +import {Routable} from "../v1_0_0/Routable.sol"; +import {FunctionsResponse} from "../v1_0_0/libraries/FunctionsResponse.sol"; + +import {SafeCast} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/math/SafeCast.sol"; + +import {ChainSpecificUtil} from "../v1_1_0/libraries/ChainSpecificUtil.sol"; + +/// @title Functions Billing contract +/// @notice Contract that calculates payment from users to the nodes of the Decentralized Oracle Network (DON). +abstract contract FunctionsBilling is Routable, IFunctionsBilling { + using FunctionsResponse for FunctionsResponse.RequestMeta; + using FunctionsResponse for FunctionsResponse.Commitment; + using FunctionsResponse for FunctionsResponse.FulfillResult; + + uint256 private constant REASONABLE_GAS_PRICE_CEILING = 1_000_000_000_000_000; // 1 million gwei + + event RequestBilled( + bytes32 indexed requestId, + uint96 juelsPerGas, + uint256 l1FeeShareWei, + uint96 callbackCostJuels, + uint72 donFeeJuels, + uint72 adminFeeJuels, + uint72 operationFeeJuels + ); + + // ================================================================ + // | Request Commitment state | + // ================================================================ + + mapping(bytes32 requestId => bytes32 commitmentHash) private s_requestCommitments; + + event CommitmentDeleted(bytes32 requestId); + + FunctionsBillingConfig private s_config; + + event ConfigUpdated(FunctionsBillingConfig config); + + error UnsupportedRequestDataVersion(); + error InsufficientBalance(); + error InvalidSubscription(); + error UnauthorizedSender(); + error MustBeSubOwner(address owner); + error InvalidLinkWeiPrice(int256 linkWei); + error InvalidUsdLinkPrice(int256 usdLink); + error PaymentTooLarge(); + error NoTransmittersSet(); + error InvalidCalldata(); + + // ================================================================ + // | Balance state | + // ================================================================ + + mapping(address transmitter => uint96 balanceJuelsLink) private s_withdrawableTokens; + // Pool together collected DON fees + // Disperse them on withdrawal or change in OCR configuration + uint96 internal s_feePool; + + AggregatorV3Interface private s_linkToNativeFeed; + AggregatorV3Interface private s_linkToUsdFeed; + + // ================================================================ + // | Initialization | + // ================================================================ + constructor( + address router, + FunctionsBillingConfig memory config, + address linkToNativeFeed, + address linkToUsdFeed + ) Routable(router) { + s_linkToNativeFeed = AggregatorV3Interface(linkToNativeFeed); + s_linkToUsdFeed = AggregatorV3Interface(linkToUsdFeed); + + updateConfig(config); + } + + // ================================================================ + // | Configuration | + // ================================================================ + + /// @notice Gets the Chainlink Coordinator's billing configuration + /// @return config + function getConfig() external view returns (FunctionsBillingConfig memory) { + return s_config; + } + + /// @notice Sets the Chainlink Coordinator's billing configuration + /// @param config - See the contents of the FunctionsBillingConfig struct in IFunctionsBilling.sol for more information + function updateConfig(FunctionsBillingConfig memory config) public { + _onlyOwner(); + + s_config = config; + emit ConfigUpdated(config); + } + + // ================================================================ + // | Fee Calculation | + // ================================================================ + + /// @inheritdoc IFunctionsBilling + function getDONFeeJuels(bytes memory /* requestData */) public view override returns (uint72) { + // s_config.donFee is in cents of USD. Get Juel amount then convert to dollars. + return SafeCast.toUint72(_getJuelsFromUsd(s_config.donFeeCentsUsd) / 100); + } + + /// @inheritdoc IFunctionsBilling + function getOperationFeeJuels() public view override returns (uint72) { + // s_config.donFee is in cents of USD. Get Juel amount then convert to dollars. + return SafeCast.toUint72(_getJuelsFromUsd(s_config.operationFeeCentsUsd) / 100); + } + + /// @inheritdoc IFunctionsBilling + function getAdminFeeJuels() public view override returns (uint72) { + return _getRouter().getAdminFee(); + } + + /// @inheritdoc IFunctionsBilling + function getWeiPerUnitLink() public view returns (uint256) { + (, int256 weiPerUnitLink, , uint256 timestamp, ) = s_linkToNativeFeed.latestRoundData(); + // solhint-disable-next-line not-rely-on-time + if (s_config.feedStalenessSeconds < block.timestamp - timestamp && s_config.feedStalenessSeconds > 0) { + return s_config.fallbackNativePerUnitLink; + } + if (weiPerUnitLink <= 0) { + revert InvalidLinkWeiPrice(weiPerUnitLink); + } + return uint256(weiPerUnitLink); + } + + function _getJuelsFromWei(uint256 amountWei) private view returns (uint96) { + // (1e18 juels/link) * wei / (wei/link) = juels + // There are only 1e9*1e18 = 1e27 juels in existence, should not exceed uint96 (2^96 ~ 7e28) + return SafeCast.toUint96((1e18 * amountWei) / getWeiPerUnitLink()); + } + + /// @inheritdoc IFunctionsBilling + function getUsdPerUnitLink() public view returns (uint256, uint8) { + (, int256 usdPerUnitLink, , uint256 timestamp, ) = s_linkToUsdFeed.latestRoundData(); + // solhint-disable-next-line not-rely-on-time + if (s_config.feedStalenessSeconds < block.timestamp - timestamp && s_config.feedStalenessSeconds > 0) { + return (s_config.fallbackUsdPerUnitLink, s_config.fallbackUsdPerUnitLinkDecimals); + } + if (usdPerUnitLink <= 0) { + revert InvalidUsdLinkPrice(usdPerUnitLink); + } + return (uint256(usdPerUnitLink), s_linkToUsdFeed.decimals()); + } + + function _getJuelsFromUsd(uint256 amountUsd) private view returns (uint96) { + (uint256 usdPerLink, uint8 decimals) = getUsdPerUnitLink(); + // (usd) * (10**18 juels/link) * (10**decimals) / (link / usd) = juels + // There are only 1e9*1e18 = 1e27 juels in existence, should not exceed uint96 (2^96 ~ 7e28) + return SafeCast.toUint96((amountUsd * 10 ** (18 + decimals)) / usdPerLink); + } + + // ================================================================ + // | Cost Estimation | + // ================================================================ + + /// @inheritdoc IFunctionsBilling + function estimateCost( + uint64 subscriptionId, + bytes calldata data, + uint32 callbackGasLimit, + uint256 gasPriceWei + ) external view override returns (uint96) { + _getRouter().isValidCallbackGasLimit(subscriptionId, callbackGasLimit); + // Reasonable ceilings to prevent integer overflows + if (gasPriceWei > REASONABLE_GAS_PRICE_CEILING) { + revert InvalidCalldata(); + } + uint72 adminFee = getAdminFeeJuels(); + uint72 donFee = getDONFeeJuels(data); + uint72 operationFee = getOperationFeeJuels(); + return _calculateCostEstimate(callbackGasLimit, gasPriceWei, donFee, adminFee, operationFee); + } + + /// @notice Estimate the cost in Juels of LINK + // that will be charged to a subscription to fulfill a Functions request + // Gas Price can be overestimated to account for flucuations between request and response time + function _calculateCostEstimate( + uint32 callbackGasLimit, + uint256 gasPriceWei, + uint72 donFeeJuels, + uint72 adminFeeJuels, + uint72 operationFeeJuels + ) internal view returns (uint96) { + // If gas price is less than the minimum fulfillment gas price, override to using the minimum + if (gasPriceWei < s_config.minimumEstimateGasPriceWei) { + gasPriceWei = s_config.minimumEstimateGasPriceWei; + } + + uint256 gasPriceWithOverestimation = gasPriceWei + + ((gasPriceWei * s_config.fulfillmentGasPriceOverEstimationBP) / 10_000); + /// @NOTE: Basis Points are 1/100th of 1%, divide by 10_000 to bring back to original units + + uint256 executionGas = s_config.gasOverheadBeforeCallback + s_config.gasOverheadAfterCallback + callbackGasLimit; + uint256 l1FeeWei = ChainSpecificUtil._getCurrentTxL1GasFees(msg.data); + uint96 estimatedGasReimbursementJuels = _getJuelsFromWei((gasPriceWithOverestimation * executionGas) + l1FeeWei); + + uint96 feesJuels = uint96(donFeeJuels) + uint96(adminFeeJuels) + uint96(operationFeeJuels); + + return estimatedGasReimbursementJuels + feesJuels; + } + + // ================================================================ + // | Billing | + // ================================================================ + + /// @notice Initiate the billing process for an Functions request + /// @dev Only callable by the Functions Router + /// @param request - Chainlink Functions request data, see FunctionsResponse.RequestMeta for the structure + /// @return commitment - The parameters of the request that must be held consistent at response time + function _startBilling( + FunctionsResponse.RequestMeta memory request + ) internal returns (FunctionsResponse.Commitment memory commitment, uint72 operationFee) { + // Nodes should support all past versions of the structure + if (request.dataVersion > s_config.maxSupportedRequestDataVersion) { + revert UnsupportedRequestDataVersion(); + } + + uint72 donFee = getDONFeeJuels(request.data); + operationFee = getOperationFeeJuels(); + uint96 estimatedTotalCostJuels = _calculateCostEstimate( + request.callbackGasLimit, + tx.gasprice, + donFee, + request.adminFee, + operationFee + ); + + // Check that subscription can afford the estimated cost + if ((request.availableBalance) < estimatedTotalCostJuels) { + revert InsufficientBalance(); + } + + uint32 timeoutTimestamp = uint32(block.timestamp + s_config.requestTimeoutSeconds); + bytes32 requestId = keccak256( + abi.encode( + address(this), + request.requestingContract, + request.subscriptionId, + request.initiatedRequests + 1, + keccak256(request.data), + request.dataVersion, + request.callbackGasLimit, + estimatedTotalCostJuels, + timeoutTimestamp, + // solhint-disable-next-line avoid-tx-origin + tx.origin + ) + ); + + commitment = FunctionsResponse.Commitment({ + adminFee: request.adminFee, + coordinator: address(this), + client: request.requestingContract, + subscriptionId: request.subscriptionId, + callbackGasLimit: request.callbackGasLimit, + estimatedTotalCostJuels: estimatedTotalCostJuels, + timeoutTimestamp: timeoutTimestamp, + requestId: requestId, + donFee: donFee, + gasOverheadBeforeCallback: s_config.gasOverheadBeforeCallback, + gasOverheadAfterCallback: s_config.gasOverheadAfterCallback + }); + + s_requestCommitments[requestId] = keccak256(abi.encode(commitment)); + + return (commitment, operationFee); + } + + /// @notice Finalize billing process for an Functions request by sending a callback to the Client contract and then charging the subscription + /// @param requestId identifier for the request that was generated by the Registry in the beginBilling commitment + /// @param response response data from DON consensus + /// @param err error from DON consensus + /// @param reportBatchSize the number of fulfillments in the transmitter's report + /// @return result fulfillment result + /// @dev Only callable by a node that has been approved on the Coordinator + /// @dev simulated offchain to determine if sufficient balance is present to fulfill the request + function _fulfillAndBill( + bytes32 requestId, + bytes memory response, + bytes memory err, + bytes memory onchainMetadata, + bytes memory /* offchainMetadata TODO: use in getDonFee() for dynamic billing */, + uint8 reportBatchSize + ) internal returns (FunctionsResponse.FulfillResult) { + FunctionsResponse.Commitment memory commitment = abi.decode(onchainMetadata, (FunctionsResponse.Commitment)); + + uint256 gasOverheadWei = (commitment.gasOverheadBeforeCallback + commitment.gasOverheadAfterCallback) * tx.gasprice; + uint256 l1FeeShareWei = ChainSpecificUtil._getCurrentTxL1GasFees(msg.data) / reportBatchSize; + // Gas overhead without callback + uint96 gasOverheadJuels = _getJuelsFromWei(gasOverheadWei + l1FeeShareWei); + uint96 juelsPerGas = _getJuelsFromWei(tx.gasprice); + + // The Functions Router will perform the callback to the client contract + (FunctionsResponse.FulfillResult resultCode, uint96 callbackCostJuels) = _getRouter().fulfill( + response, + err, + juelsPerGas, + // The following line represents: "cost without callback or admin fee, those will be added by the Router" + // But because the _offchain_ Commitment is using operation fee in the place of the admin fee, this now adds admin fee (actually operation fee) + // Admin fee is configured to 0 in the Router + gasOverheadJuels + commitment.donFee + commitment.adminFee, + msg.sender, + FunctionsResponse.Commitment({ + adminFee: 0, // The Router should have adminFee set to 0. If it does not this will cause fulfillments to fail with INVALID_COMMITMENT instead of carrying out incorrect bookkeeping. + coordinator: commitment.coordinator, + client: commitment.client, + subscriptionId: commitment.subscriptionId, + callbackGasLimit: commitment.callbackGasLimit, + estimatedTotalCostJuels: commitment.estimatedTotalCostJuels, + timeoutTimestamp: commitment.timeoutTimestamp, + requestId: commitment.requestId, + donFee: commitment.donFee, + gasOverheadBeforeCallback: commitment.gasOverheadBeforeCallback, + gasOverheadAfterCallback: commitment.gasOverheadAfterCallback + }) + ); + + // The router will only pay the DON on successfully processing the fulfillment + // In these two fulfillment results the user has been charged + // Otherwise, the Coordinator should hold on to the request commitment + if ( + resultCode == FunctionsResponse.FulfillResult.FULFILLED || + resultCode == FunctionsResponse.FulfillResult.USER_CALLBACK_ERROR + ) { + delete s_requestCommitments[requestId]; + // Reimburse the transmitter for the fulfillment gas cost + s_withdrawableTokens[msg.sender] += gasOverheadJuels + callbackCostJuels; + // Put donFee into the pool of fees, to be split later + // Saves on storage writes that would otherwise be charged to the user + s_feePool += commitment.donFee; + // Pay the operation fee to the Coordinator owner + s_withdrawableTokens[_owner()] += commitment.adminFee; // OperationFee is used in the slot for Admin Fee in the Offchain Commitment. Admin Fee is set to 0 in the Router (enforced by line 316 in FunctionsBilling.sol). + emit RequestBilled({ + requestId: requestId, + juelsPerGas: juelsPerGas, + l1FeeShareWei: l1FeeShareWei, + callbackCostJuels: callbackCostJuels, + donFeeJuels: commitment.donFee, + // The following two lines are because of OperationFee being used in the Offchain Commitment + adminFeeJuels: 0, + operationFeeJuels: commitment.adminFee + }); + } + return resultCode; + } + + // ================================================================ + // | Request Timeout | + // ================================================================ + + /// @inheritdoc IFunctionsBilling + /// @dev Only callable by the Router + /// @dev Used by FunctionsRouter.sol during timeout of a request + function deleteCommitment(bytes32 requestId) external override onlyRouter { + // Delete commitment + delete s_requestCommitments[requestId]; + emit CommitmentDeleted(requestId); + } + + // ================================================================ + // | Fund withdrawal | + // ================================================================ + + /// @inheritdoc IFunctionsBilling + function oracleWithdraw(address recipient, uint96 amount) external { + _disperseFeePool(); + + if (amount == 0) { + amount = s_withdrawableTokens[msg.sender]; + } else if (s_withdrawableTokens[msg.sender] < amount) { + revert InsufficientBalance(); + } + s_withdrawableTokens[msg.sender] -= amount; + IFunctionsSubscriptions(address(_getRouter())).oracleWithdraw(recipient, amount); + } + + /// @inheritdoc IFunctionsBilling + /// @dev Only callable by the Coordinator owner + function oracleWithdrawAll() external { + _onlyOwner(); + _disperseFeePool(); + + address[] memory transmitters = _getTransmitters(); + + // Bounded by "maxNumOracles" on OCR2Abstract.sol + for (uint256 i = 0; i < transmitters.length; ++i) { + uint96 balance = s_withdrawableTokens[transmitters[i]]; + if (balance > 0) { + s_withdrawableTokens[transmitters[i]] = 0; + IFunctionsSubscriptions(address(_getRouter())).oracleWithdraw(transmitters[i], balance); + } + } + } + + // Overriden in FunctionsCoordinator, which has visibility into transmitters + function _getTransmitters() internal view virtual returns (address[] memory); + + // DON fees are collected into a pool s_feePool + // When OCR configuration changes, or any oracle withdraws, this must be dispersed + function _disperseFeePool() internal { + if (s_feePool == 0) { + return; + } + // All transmitters are assumed to also be observers + // Pay out the DON fee to all transmitters + address[] memory transmitters = _getTransmitters(); + uint256 numberOfTransmitters = transmitters.length; + if (numberOfTransmitters == 0) { + revert NoTransmittersSet(); + } + uint96 feePoolShare = s_feePool / uint96(numberOfTransmitters); + // Bounded by "maxNumOracles" on OCR2Abstract.sol + for (uint256 i = 0; i < numberOfTransmitters; ++i) { + s_withdrawableTokens[transmitters[i]] += feePoolShare; + } + s_feePool -= feePoolShare * uint96(numberOfTransmitters); + } + + // Overriden in FunctionsCoordinator.sol + function _onlyOwner() internal view virtual; + + // Used in FunctionsCoordinator.sol + function _isExistingRequest(bytes32 requestId) internal view returns (bool) { + return s_requestCommitments[requestId] != bytes32(0); + } + + // Overriden in FunctionsCoordinator.sol + function _owner() internal view virtual returns (address owner); +} diff --git a/contracts/src/v0.8/functions/v1_3_0/FunctionsClient.sol b/contracts/src/v0.8/functions/v1_3_0/FunctionsClient.sol new file mode 100644 index 00000000..84b64146 --- /dev/null +++ b/contracts/src/v0.8/functions/v1_3_0/FunctionsClient.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IFunctionsRouter} from "../v1_0_0/interfaces/IFunctionsRouter.sol"; +import {IFunctionsClient} from "../v1_0_0/interfaces/IFunctionsClient.sol"; + +import {FunctionsRequest} from "../v1_0_0/libraries/FunctionsRequest.sol"; + +/// @title The Chainlink Functions client contract +/// @notice Contract developers can inherit this contract in order to make Chainlink Functions requests +abstract contract FunctionsClient is IFunctionsClient { + using FunctionsRequest for FunctionsRequest.Request; + + IFunctionsRouter internal immutable i_functionsRouter; + + event RequestSent(bytes32 indexed id); + event RequestFulfilled(bytes32 indexed id); + + error OnlyRouterCanFulfill(); + + constructor(address router) { + i_functionsRouter = IFunctionsRouter(router); + } + + /// @notice Sends a Chainlink Functions request + /// @param data The CBOR encoded bytes data for a Functions request + /// @param subscriptionId The subscription ID that will be charged to service the request + /// @param callbackGasLimit the amount of gas that will be available for the fulfillment callback + /// @return requestId The generated request ID for this request + function _sendRequest( + bytes memory data, + uint64 subscriptionId, + uint32 callbackGasLimit, + bytes32 donId + ) internal returns (bytes32) { + bytes32 requestId = i_functionsRouter.sendRequest( + subscriptionId, + data, + FunctionsRequest.REQUEST_DATA_VERSION, + callbackGasLimit, + donId + ); + emit RequestSent(requestId); + return requestId; + } + + /// @notice User defined function to handle a response from the DON + /// @param requestId The request ID, returned by sendRequest() + /// @param response Aggregated response from the execution of the user's source code + /// @param err Aggregated error from the execution of the user code or from the execution pipeline + /// @dev Either response or error parameter will be set, but never both + function _fulfillRequest(bytes32 requestId, bytes memory response, bytes memory err) internal virtual; + + /// @inheritdoc IFunctionsClient + function handleOracleFulfillment(bytes32 requestId, bytes memory response, bytes memory err) external override { + if (msg.sender != address(i_functionsRouter)) { + revert OnlyRouterCanFulfill(); + } + _fulfillRequest(requestId, response, err); + emit RequestFulfilled(requestId); + } +} diff --git a/contracts/src/v0.8/functions/v1_3_0/FunctionsCoordinator.sol b/contracts/src/v0.8/functions/v1_3_0/FunctionsCoordinator.sol new file mode 100644 index 00000000..9c7f3598 --- /dev/null +++ b/contracts/src/v0.8/functions/v1_3_0/FunctionsCoordinator.sol @@ -0,0 +1,228 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IFunctionsCoordinator} from "../v1_0_0/interfaces/IFunctionsCoordinator.sol"; +import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; + +import {FunctionsBilling, FunctionsBillingConfig} from "./FunctionsBilling.sol"; +import {OCR2Base} from "./ocr/OCR2Base.sol"; +import {FunctionsResponse} from "../v1_0_0/libraries/FunctionsResponse.sol"; + +/// @title Functions Coordinator contract +/// @notice Contract that nodes of a Decentralized Oracle Network (DON) interact with +contract FunctionsCoordinator is OCR2Base, IFunctionsCoordinator, FunctionsBilling { + using FunctionsResponse for FunctionsResponse.RequestMeta; + using FunctionsResponse for FunctionsResponse.Commitment; + using FunctionsResponse for FunctionsResponse.FulfillResult; + + /// @inheritdoc ITypeAndVersion + // solhint-disable-next-line chainlink-solidity/all-caps-constant-storage-variables + string public constant override typeAndVersion = "Functions Coordinator v1.3.0"; + + event OracleRequest( + bytes32 indexed requestId, + address indexed requestingContract, + address requestInitiator, + uint64 subscriptionId, + address subscriptionOwner, + bytes data, + uint16 dataVersion, + bytes32 flags, + uint64 callbackGasLimit, + FunctionsResponse.Commitment commitment + ); + event OracleResponse(bytes32 indexed requestId, address transmitter); + + error InconsistentReportData(); + error EmptyPublicKey(); + error UnauthorizedPublicKeyChange(); + + bytes private s_donPublicKey; + bytes private s_thresholdPublicKey; + + constructor( + address router, + FunctionsBillingConfig memory config, + address linkToNativeFeed, + address linkToUsdFeed + ) OCR2Base() FunctionsBilling(router, config, linkToNativeFeed, linkToUsdFeed) {} + + /// @inheritdoc IFunctionsCoordinator + function getThresholdPublicKey() external view override returns (bytes memory) { + if (s_thresholdPublicKey.length == 0) { + revert EmptyPublicKey(); + } + return s_thresholdPublicKey; + } + + /// @inheritdoc IFunctionsCoordinator + function setThresholdPublicKey(bytes calldata thresholdPublicKey) external override onlyOwner { + if (thresholdPublicKey.length == 0) { + revert EmptyPublicKey(); + } + s_thresholdPublicKey = thresholdPublicKey; + } + + /// @inheritdoc IFunctionsCoordinator + function getDONPublicKey() external view override returns (bytes memory) { + if (s_donPublicKey.length == 0) { + revert EmptyPublicKey(); + } + return s_donPublicKey; + } + + /// @inheritdoc IFunctionsCoordinator + function setDONPublicKey(bytes calldata donPublicKey) external override onlyOwner { + if (donPublicKey.length == 0) { + revert EmptyPublicKey(); + } + s_donPublicKey = donPublicKey; + } + + /// @dev check if node is in current transmitter list + function _isTransmitter(address node) internal view returns (bool) { + // Bounded by "maxNumOracles" on OCR2Abstract.sol + for (uint256 i = 0; i < s_transmitters.length; ++i) { + if (s_transmitters[i] == node) { + return true; + } + } + return false; + } + + /// @inheritdoc IFunctionsCoordinator + function startRequest( + FunctionsResponse.RequestMeta calldata request + ) external override onlyRouter returns (FunctionsResponse.Commitment memory commitment) { + uint72 operationFee; + (commitment, operationFee) = _startBilling(request); + + emit OracleRequest( + commitment.requestId, + request.requestingContract, + // solhint-disable-next-line avoid-tx-origin + tx.origin, + request.subscriptionId, + request.subscriptionOwner, + request.data, + request.dataVersion, + request.flags, + request.callbackGasLimit, + FunctionsResponse.Commitment({ + coordinator: commitment.coordinator, + client: commitment.client, + subscriptionId: commitment.subscriptionId, + callbackGasLimit: commitment.callbackGasLimit, + estimatedTotalCostJuels: commitment.estimatedTotalCostJuels, + timeoutTimestamp: commitment.timeoutTimestamp, + requestId: commitment.requestId, + donFee: commitment.donFee, + gasOverheadBeforeCallback: commitment.gasOverheadBeforeCallback, + gasOverheadAfterCallback: commitment.gasOverheadAfterCallback, + // The following line is done to use the Coordinator's operationFee in place of the Router's operation fee + // With this in place the Router.adminFee must be set to 0 in the Router. + adminFee: operationFee + }) + ); + + return commitment; + } + + /// @dev DON fees are pooled together. If the OCR configuration is going to change, these need to be distributed. + function _beforeSetConfig(uint8 /* _f */, bytes memory /* _onchainConfig */) internal override { + if (_getTransmitters().length > 0) { + _disperseFeePool(); + } + } + + /// @dev Used by FunctionsBilling.sol + function _getTransmitters() internal view override returns (address[] memory) { + return s_transmitters; + } + + function _beforeTransmit( + bytes calldata report + ) internal view override returns (bool shouldStop, DecodedReport memory decodedReport) { + ( + bytes32[] memory requestIds, + bytes[] memory results, + bytes[] memory errors, + bytes[] memory onchainMetadata, + bytes[] memory offchainMetadata + ) = abi.decode(report, (bytes32[], bytes[], bytes[], bytes[], bytes[])); + uint256 numberOfFulfillments = uint8(requestIds.length); + + if ( + numberOfFulfillments == 0 || + numberOfFulfillments != results.length || + numberOfFulfillments != errors.length || + numberOfFulfillments != onchainMetadata.length || + numberOfFulfillments != offchainMetadata.length + ) { + revert ReportInvalid("Fields must be equal length"); + } + + for (uint256 i = 0; i < numberOfFulfillments; ++i) { + if (_isExistingRequest(requestIds[i])) { + // If there is an existing request, validate report + // Leave shouldStop to default, false + break; + } + if (i == numberOfFulfillments - 1) { + // If the last fulfillment on the report does not exist, then all are duplicates + // Indicate that it's safe to stop to save on the gas of validating the report + shouldStop = true; + } + } + + return ( + shouldStop, + DecodedReport({ + requestIds: requestIds, + results: results, + errors: errors, + onchainMetadata: onchainMetadata, + offchainMetadata: offchainMetadata + }) + ); + } + + /// @dev Report hook called within OCR2Base.sol + function _report(DecodedReport memory decodedReport) internal override { + uint256 numberOfFulfillments = uint8(decodedReport.requestIds.length); + + // Bounded by "MaxRequestBatchSize" on the Job's ReportingPluginConfig + for (uint256 i = 0; i < numberOfFulfillments; ++i) { + FunctionsResponse.FulfillResult result = FunctionsResponse.FulfillResult( + _fulfillAndBill( + decodedReport.requestIds[i], + decodedReport.results[i], + decodedReport.errors[i], + decodedReport.onchainMetadata[i], + decodedReport.offchainMetadata[i], + uint8(numberOfFulfillments) // will not exceed "MaxRequestBatchSize" on the Job's ReportingPluginConfig + ) + ); + + // Emit on successfully processing the fulfillment + // In these two fulfillment results the user has been charged + // Otherwise, the DON will re-try + if ( + result == FunctionsResponse.FulfillResult.FULFILLED || + result == FunctionsResponse.FulfillResult.USER_CALLBACK_ERROR + ) { + emit OracleResponse(decodedReport.requestIds[i], msg.sender); + } + } + } + + /// @dev Used in FunctionsBilling.sol + function _onlyOwner() internal view override { + _validateOwnership(); + } + + /// @dev Used in FunctionsBilling.sol + function _owner() internal view override returns (address owner) { + return this.owner(); + } +} diff --git a/contracts/src/v0.8/functions/v1_3_0/accessControl/TermsOfServiceAllowList.sol b/contracts/src/v0.8/functions/v1_3_0/accessControl/TermsOfServiceAllowList.sol new file mode 100644 index 00000000..1d9a3b91 --- /dev/null +++ b/contracts/src/v0.8/functions/v1_3_0/accessControl/TermsOfServiceAllowList.sol @@ -0,0 +1,205 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {ITermsOfServiceAllowList, TermsOfServiceAllowListConfig} from "./interfaces/ITermsOfServiceAllowList.sol"; +import {IAccessController} from "../../../shared/interfaces/IAccessController.sol"; +import {ITypeAndVersion} from "../../../shared/interfaces/ITypeAndVersion.sol"; + +import {ConfirmedOwner} from "../../../shared/access/ConfirmedOwner.sol"; + +import {Address} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/Address.sol"; +import {EnumerableSet} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/structs/EnumerableSet.sol"; + +/// @notice A contract to handle access control of subscription management dependent on signing a Terms of Service +contract TermsOfServiceAllowList is ITermsOfServiceAllowList, IAccessController, ITypeAndVersion, ConfirmedOwner { + using Address for address; + using EnumerableSet for EnumerableSet.AddressSet; + + /// @inheritdoc ITypeAndVersion + // solhint-disable-next-line chainlink-solidity/all-caps-constant-storage-variables + string public constant override typeAndVersion = "Functions Terms of Service Allow List v1.1.0"; + + EnumerableSet.AddressSet private s_allowedSenders; + EnumerableSet.AddressSet private s_blockedSenders; + + event AddedAccess(address user); + event BlockedAccess(address user); + event UnblockedAccess(address user); + + error InvalidSignature(); + error InvalidUsage(); + error RecipientIsBlocked(); + error InvalidCalldata(); + + TermsOfServiceAllowListConfig private s_config; + + event ConfigUpdated(TermsOfServiceAllowListConfig config); + + // ================================================================ + // | Initialization | + // ================================================================ + + constructor( + TermsOfServiceAllowListConfig memory config, + address[] memory initialAllowedSenders, + address[] memory initialBlockedSenders + ) ConfirmedOwner(msg.sender) { + updateConfig(config); + + for (uint256 i = 0; i < initialAllowedSenders.length; ++i) { + s_allowedSenders.add(initialAllowedSenders[i]); + } + + for (uint256 j = 0; j < initialBlockedSenders.length; ++j) { + if (s_allowedSenders.contains(initialBlockedSenders[j])) { + // Allowed senders cannot also be blocked + revert InvalidCalldata(); + } + s_blockedSenders.add(initialBlockedSenders[j]); + } + } + + // ================================================================ + // | Configuration | + // ================================================================ + + /// @notice Gets the contracts's configuration + /// @return config + function getConfig() external view returns (TermsOfServiceAllowListConfig memory) { + return s_config; + } + + /// @notice Sets the contracts's configuration + /// @param config - See the contents of the TermsOfServiceAllowListConfig struct in ITermsOfServiceAllowList.sol for more information + function updateConfig(TermsOfServiceAllowListConfig memory config) public onlyOwner { + s_config = config; + emit ConfigUpdated(config); + } + + // ================================================================ + // | Allow methods | + // ================================================================ + + /// @inheritdoc ITermsOfServiceAllowList + function getMessage(address acceptor, address recipient) public pure override returns (bytes32) { + return keccak256(abi.encodePacked(acceptor, recipient)); + } + + /// @inheritdoc ITermsOfServiceAllowList + function acceptTermsOfService(address acceptor, address recipient, bytes32 r, bytes32 s, uint8 v) external override { + if (s_blockedSenders.contains(recipient)) { + revert RecipientIsBlocked(); + } + + // Validate that the signature is correct and the correct data has been signed + bytes32 prefixedMessage = keccak256( + abi.encodePacked("\x19Ethereum Signed Message:\n32", getMessage(acceptor, recipient)) + ); + if (ecrecover(prefixedMessage, v, r, s) != s_config.signerPublicKey) { + revert InvalidSignature(); + } + + // If contract, validate that msg.sender == recipient + // This is to prevent EoAs from claiming contracts that they are not in control of + // If EoA, validate that msg.sender == acceptor == recipient + // This is to prevent EoAs from accepting for other EoAs + if (msg.sender != recipient || (msg.sender != acceptor && !msg.sender.isContract())) { + revert InvalidUsage(); + } + + // Add recipient to the allow list + if (s_allowedSenders.add(recipient)) { + emit AddedAccess(recipient); + } + } + + /// @inheritdoc ITermsOfServiceAllowList + function getAllAllowedSenders() external view override returns (address[] memory) { + return s_allowedSenders.values(); + } + + /// @inheritdoc ITermsOfServiceAllowList + function getAllowedSendersCount() external view override returns (uint64) { + return uint64(s_allowedSenders.length()); + } + + /// @inheritdoc ITermsOfServiceAllowList + function getAllowedSendersInRange( + uint64 allowedSenderIdxStart, + uint64 allowedSenderIdxEnd + ) external view override returns (address[] memory allowedSenders) { + if ( + allowedSenderIdxStart > allowedSenderIdxEnd || + allowedSenderIdxEnd >= s_allowedSenders.length() || + s_allowedSenders.length() == 0 + ) { + revert InvalidCalldata(); + } + + allowedSenders = new address[]((allowedSenderIdxEnd - allowedSenderIdxStart) + 1); + for (uint256 i = 0; i <= allowedSenderIdxEnd - allowedSenderIdxStart; ++i) { + allowedSenders[i] = s_allowedSenders.at(uint256(allowedSenderIdxStart + i)); + } + + return allowedSenders; + } + + /// @inheritdoc IAccessController + function hasAccess(address user, bytes calldata /* data */) external view override returns (bool) { + if (!s_config.enabled) { + return true; + } + return s_allowedSenders.contains(user); + } + + // ================================================================ + // | Block methods | + // ================================================================ + + /// @inheritdoc ITermsOfServiceAllowList + function isBlockedSender(address sender) external view override returns (bool) { + if (!s_config.enabled) { + return false; + } + return s_blockedSenders.contains(sender); + } + + /// @inheritdoc ITermsOfServiceAllowList + function blockSender(address sender) external override onlyOwner { + s_allowedSenders.remove(sender); + s_blockedSenders.add(sender); + emit BlockedAccess(sender); + } + + /// @inheritdoc ITermsOfServiceAllowList + function unblockSender(address sender) external override onlyOwner { + s_blockedSenders.remove(sender); + emit UnblockedAccess(sender); + } + + /// @inheritdoc ITermsOfServiceAllowList + function getBlockedSendersCount() external view override returns (uint64) { + return uint64(s_blockedSenders.length()); + } + + /// @inheritdoc ITermsOfServiceAllowList + function getBlockedSendersInRange( + uint64 blockedSenderIdxStart, + uint64 blockedSenderIdxEnd + ) external view override returns (address[] memory blockedSenders) { + if ( + blockedSenderIdxStart > blockedSenderIdxEnd || + blockedSenderIdxEnd >= s_blockedSenders.length() || + s_blockedSenders.length() == 0 + ) { + revert InvalidCalldata(); + } + + blockedSenders = new address[]((blockedSenderIdxEnd - blockedSenderIdxStart) + 1); + for (uint256 i = 0; i <= blockedSenderIdxEnd - blockedSenderIdxStart; ++i) { + blockedSenders[i] = s_blockedSenders.at(uint256(blockedSenderIdxStart + i)); + } + + return blockedSenders; + } +} diff --git a/contracts/src/v0.8/functions/v1_3_0/accessControl/interfaces/ITermsOfServiceAllowList.sol b/contracts/src/v0.8/functions/v1_3_0/accessControl/interfaces/ITermsOfServiceAllowList.sol new file mode 100644 index 00000000..65db9c42 --- /dev/null +++ b/contracts/src/v0.8/functions/v1_3_0/accessControl/interfaces/ITermsOfServiceAllowList.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/// @notice A contract to handle access control of subscription management dependent on signing a Terms of Service +interface ITermsOfServiceAllowList { + /// @notice Return the message data for the proof given to accept the Terms of Service + /// @param acceptor - The wallet address that has accepted the Terms of Service on the UI + /// @param recipient - The recipient address that the acceptor is taking responsibility for + /// @return Hash of the message data + function getMessage(address acceptor, address recipient) external pure returns (bytes32); + + /// @notice Check if the address is blocked for usage + /// @param sender The transaction sender's address + /// @return True or false + function isBlockedSender(address sender) external returns (bool); + + /// @notice Get a list of all allowed senders + /// @dev WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + /// to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + /// this function has an unbounded cost, and using it as part of a state-changing function may render the function + /// uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + /// @return addresses - all allowed addresses + function getAllAllowedSenders() external view returns (address[] memory); + + /// @notice Get details about the total number of allowed senders + /// @return count - total number of allowed senders in the system + function getAllowedSendersCount() external view returns (uint64); + + /// @notice Retrieve a list of allowed senders using an inclusive range + /// @dev WARNING: getAllowedSendersInRange uses EnumerableSet .length() and .at() methods to iterate over the list + /// without the need for an extra mapping. These method can not guarantee the ordering when new elements are added. + /// Evaluate if eventual consistency will satisfy your usecase before using it. + /// @param allowedSenderIdxStart - index of the allowed sender to start the range at + /// @param allowedSenderIdxEnd - index of the allowed sender to end the range at + /// @return allowedSenders - allowed addresses in the range provided + function getAllowedSendersInRange( + uint64 allowedSenderIdxStart, + uint64 allowedSenderIdxEnd + ) external view returns (address[] memory allowedSenders); + + /// @notice Allows access to the sender based on acceptance of the Terms of Service + /// @param acceptor - The wallet address that has accepted the Terms of Service on the UI + /// @param recipient - The recipient address that the acceptor is taking responsibility for + /// @param r - ECDSA signature r data produced by the Chainlink Functions Subscription UI + /// @param s - ECDSA signature s produced by the Chainlink Functions Subscription UI + /// @param v - ECDSA signature v produced by the Chainlink Functions Subscription UI + function acceptTermsOfService(address acceptor, address recipient, bytes32 r, bytes32 s, uint8 v) external; + + /// @notice Removes a sender's access if already authorized, and disallows re-accepting the Terms of Service + /// @param sender - Address of the sender to block + function blockSender(address sender) external; + + /// @notice Re-allows a previously blocked sender to accept the Terms of Service + /// @param sender - Address of the sender to unblock + function unblockSender(address sender) external; + + /// @notice Get details about the total number of blocked senders + /// @return count - total number of blocked senders in the system + function getBlockedSendersCount() external view returns (uint64); + + /// @notice Retrieve a list of blocked senders using an inclusive range + /// @dev WARNING: getBlockedSendersInRange uses EnumerableSet .length() and .at() methods to iterate over the list + /// without the need for an extra mapping. These method can not guarantee the ordering when new elements are added. + /// Evaluate if eventual consistency will satisfy your usecase before using it. + /// @param blockedSenderIdxStart - index of the blocked sender to start the range at + /// @param blockedSenderIdxEnd - index of the blocked sender to end the range at + /// @return blockedSenders - blocked addresses in the range provided + function getBlockedSendersInRange( + uint64 blockedSenderIdxStart, + uint64 blockedSenderIdxEnd + ) external view returns (address[] memory blockedSenders); +} + +// ================================================================ +// | Configuration state | +// ================================================================ +struct TermsOfServiceAllowListConfig { + bool enabled; // ═════════════╗ When enabled, access will be checked against s_allowedSenders. When disabled, all access will be allowed. + address signerPublicKey; // ══╝ The key pair that needs to sign the acceptance data +} diff --git a/contracts/src/v0.8/functions/v1_3_0/interfaces/IFunctionsBilling.sol b/contracts/src/v0.8/functions/v1_3_0/interfaces/IFunctionsBilling.sol new file mode 100644 index 00000000..79806f1e --- /dev/null +++ b/contracts/src/v0.8/functions/v1_3_0/interfaces/IFunctionsBilling.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/// @title Chainlink Functions DON billing interface. +interface IFunctionsBilling { + /// @notice Return the current conversion from WEI of ETH to LINK from the configured Chainlink data feed + /// @return weiPerUnitLink - The amount of WEI in one LINK + function getWeiPerUnitLink() external view returns (uint256); + + /// @notice Return the current conversion from LINK to USD from the configured Chainlink data feed + /// @return weiPerUnitLink - The amount of USD that one LINK is worth + /// @return decimals - The number of decimals that should be represented in the price feed's response + function getUsdPerUnitLink() external view returns (uint256, uint8); + + /// @notice Determine the fee that will be split between Node Operators for servicing a request + /// @param requestCBOR - CBOR encoded Chainlink Functions request data, use FunctionsRequest library to encode a request + /// @return fee - Cost in Juels (1e18) of LINK + function getDONFeeJuels(bytes memory requestCBOR) external view returns (uint72); + + /// @notice Determine the fee that will be paid to the Coordinator owner for operating the network + /// @return fee - Cost in Juels (1e18) of LINK + function getOperationFeeJuels() external view returns (uint72); + + /// @notice Determine the fee that will be paid to the Router owner for operating the network + /// @return fee - Cost in Juels (1e18) of LINK + function getAdminFeeJuels() external view returns (uint72); + + /// @notice Estimate the total cost that will be charged to a subscription to make a request: transmitter gas re-reimbursement, plus DON fee, plus Registry fee + /// @param - subscriptionId An identifier of the billing account + /// @param - data Encoded Chainlink Functions request data, use FunctionsClient API to encode a request + /// @param - callbackGasLimit Gas limit for the fulfillment callback + /// @param - gasPriceWei The blockchain's gas price to estimate with + /// @return - billedCost Cost in Juels (1e18) of LINK + function estimateCost( + uint64 subscriptionId, + bytes calldata data, + uint32 callbackGasLimit, + uint256 gasPriceWei + ) external view returns (uint96); + + /// @notice Remove a request commitment that the Router has determined to be stale + /// @param requestId - The request ID to remove + function deleteCommitment(bytes32 requestId) external; + + /// @notice Oracle withdraw LINK earned through fulfilling requests + /// @notice If amount is 0 the full balance will be withdrawn + /// @param recipient where to send the funds + /// @param amount amount to withdraw + function oracleWithdraw(address recipient, uint96 amount) external; + + /// @notice Withdraw all LINK earned by Oracles through fulfilling requests + /// @dev transmitter addresses must support LINK tokens to avoid tokens from getting stuck as oracleWithdrawAll() calls will forward tokens directly to transmitters + function oracleWithdrawAll() external; +} + +// ================================================================ +// | Configuration state | +// ================================================================ + +struct FunctionsBillingConfig { + uint32 fulfillmentGasPriceOverEstimationBP; // ══╗ Percentage of gas price overestimation to account for changes in gas price between request and response. Held as basis points (one hundredth of 1 percentage point) + uint32 feedStalenessSeconds; // ║ How long before we consider the feed price to be stale and fallback to fallbackNativePerUnitLink. + uint32 gasOverheadBeforeCallback; // ║ Represents the average gas execution cost before the fulfillment callback. This amount is always billed for every request. + uint32 gasOverheadAfterCallback; // ║ Represents the average gas execution cost after the fulfillment callback. This amount is always billed for every request. + uint40 minimumEstimateGasPriceWei; // ║ The lowest amount of wei that will be used as the tx.gasprice when estimating the cost to fulfill the request + uint16 maxSupportedRequestDataVersion; // ║ The highest support request data version supported by the node. All lower versions should also be supported. + uint64 fallbackUsdPerUnitLink; // ║ Fallback LINK / USD conversion rate if the data feed is stale + uint8 fallbackUsdPerUnitLinkDecimals; // ════════╝ Fallback LINK / USD conversion rate decimal places if the data feed is stale + uint224 fallbackNativePerUnitLink; // ═══════════╗ Fallback NATIVE CURRENCY / LINK conversion rate if the data feed is stale + uint32 requestTimeoutSeconds; // ════════════════╝ How many seconds it takes before we consider a request to be timed out + uint16 donFeeCentsUsd; // ═══════════════════════════════╗ Additional flat fee (denominated in cents of USD, paid as LINK) that will be split between Node Operators. + uint16 operationFeeCentsUsd; // ═════════════════════════╝ Additional flat fee (denominated in cents of USD, paid as LINK) that will be paid to the owner of the Coordinator contract. +} diff --git a/contracts/src/v0.8/functions/v1_3_0/ocr/OCR2Abstract.sol b/contracts/src/v0.8/functions/v1_3_0/ocr/OCR2Abstract.sol new file mode 100644 index 00000000..4182227d --- /dev/null +++ b/contracts/src/v0.8/functions/v1_3_0/ocr/OCR2Abstract.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {ITypeAndVersion} from "../../../shared/interfaces/ITypeAndVersion.sol"; + +abstract contract OCR2Abstract is ITypeAndVersion { + // Maximum number of oracles the offchain reporting protocol is designed for + uint256 internal constant MAX_NUM_ORACLES = 31; + + /** + * @notice triggers a new run of the offchain reporting protocol + * @param previousConfigBlockNumber block in which the previous config was set, to simplify historic analysis + * @param configDigest configDigest of this configuration + * @param configCount ordinal number of this config setting among all config settings over the life of this contract + * @param signers ith element is address ith oracle uses to sign a report + * @param transmitters ith element is address ith oracle uses to transmit a report via the transmit method + * @param f maximum number of faulty/dishonest oracles the protocol can tolerate while still working correctly + * @param onchainConfig serialized configuration used by the contract (and possibly oracles) + * @param offchainConfigVersion version of the serialization format used for "offchainConfig" parameter + * @param offchainConfig serialized configuration used by the oracles exclusively and only passed through the contract + */ + event ConfigSet( + uint32 previousConfigBlockNumber, + bytes32 configDigest, + uint64 configCount, + address[] signers, + address[] transmitters, + uint8 f, + bytes onchainConfig, + uint64 offchainConfigVersion, + bytes offchainConfig + ); + + /** + * @notice sets offchain reporting protocol configuration incl. participating oracles + * @param signers addresses with which oracles sign the reports + * @param transmitters addresses oracles use to transmit the reports + * @param f number of faulty oracles the system can tolerate + * @param onchainConfig serialized configuration used by the contract (and possibly oracles) + * @param offchainConfigVersion version number for offchainEncoding schema + * @param offchainConfig serialized configuration used by the oracles exclusively and only passed through the contract + */ + function setConfig( + address[] memory signers, + address[] memory transmitters, + uint8 f, + bytes memory onchainConfig, + uint64 offchainConfigVersion, + bytes memory offchainConfig + ) external virtual; + + /** + * @notice information about current offchain reporting protocol configuration + * @return configCount ordinal number of current config, out of all configs applied to this contract so far + * @return blockNumber block at which this config was set + * @return configDigest domain-separation tag for current config (see _configDigestFromConfigData) + */ + function latestConfigDetails() + external + view + virtual + returns (uint32 configCount, uint32 blockNumber, bytes32 configDigest); + + /** + * @notice optionally emited to indicate the latest configDigest and epoch for + which a report was successfully transmited. Alternatively, the contract may + use latestConfigDigestAndEpoch with scanLogs set to false. + */ + event Transmitted(bytes32 configDigest, uint32 epoch); + + /** + * @notice optionally returns the latest configDigest and epoch for which a + report was successfully transmitted. Alternatively, the contract may return + scanLogs set to true and use Transmitted events to provide this information + to offchain watchers. + * @return scanLogs indicates whether to rely on the configDigest and epoch + returned or whether to scan logs for the Transmitted event instead. + * @return configDigest + * @return epoch + */ + function latestConfigDigestAndEpoch() + external + view + virtual + returns (bool scanLogs, bytes32 configDigest, uint32 epoch); + + /** + * @notice transmit is called to post a new report to the contract + * @param report serialized report, which the signatures are signing. + * @param rs ith element is the R components of the ith signature on report. Must have at most maxNumOracles entries + * @param ss ith element is the S components of the ith signature on report. Must have at most maxNumOracles entries + * @param rawVs ith element is the the V component of the ith signature + */ + function transmit( + // NOTE: If these parameters are changed, expectedMsgDataLength and/or + // TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT need to be changed accordingly + bytes32[3] calldata reportContext, + bytes calldata report, + bytes32[] calldata rs, + bytes32[] calldata ss, + bytes32 rawVs // signatures + ) external virtual; +} diff --git a/contracts/src/v0.8/functions/v1_3_0/ocr/OCR2Base.sol b/contracts/src/v0.8/functions/v1_3_0/ocr/OCR2Base.sol new file mode 100644 index 00000000..310107f2 --- /dev/null +++ b/contracts/src/v0.8/functions/v1_3_0/ocr/OCR2Base.sol @@ -0,0 +1,356 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {ConfirmedOwner} from "../../../shared/access/ConfirmedOwner.sol"; +import {OCR2Abstract} from "./OCR2Abstract.sol"; + +/** + * @notice Onchain verification of reports from the offchain reporting protocol + * @dev For details on its operation, see the offchain reporting protocol design + * doc, which refers to this contract as simply the "contract". + */ +abstract contract OCR2Base is ConfirmedOwner, OCR2Abstract { + error ReportInvalid(string message); + error InvalidConfig(string message); + + constructor() ConfirmedOwner(msg.sender) {} + + // incremented each time a new config is posted. This count is incorporated + // into the config digest, to prevent replay attacks. + uint32 internal s_configCount; + uint32 internal s_latestConfigBlockNumber; // makes it easier for offchain systems + // to extract config from logs. + + // Storing these fields used on the hot path in a ConfigInfo variable reduces the + // retrieval of all of them to a single SLOAD. If any further fields are + // added, make sure that storage of the struct still takes at most 32 bytes. + struct ConfigInfo { + bytes32 latestConfigDigest; + uint8 f; // TODO: could be optimized by squeezing into one slot + uint8 n; + } + ConfigInfo internal s_configInfo; + + // Used for s_oracles[a].role, where a is an address, to track the purpose + // of the address, or to indicate that the address is unset. + enum Role { + // No oracle role has been set for address a + Unset, + // Signing address for the s_oracles[a].index'th oracle. I.e., report + // signatures from this oracle should ecrecover back to address a. + Signer, + // Transmission address for the s_oracles[a].index'th oracle. I.e., if a + // report is received by OCR2Aggregator.transmit in which msg.sender is + // a, it is attributed to the s_oracles[a].index'th oracle. + Transmitter + } + + struct Oracle { + uint8 index; // Index of oracle in s_signers/s_transmitters + Role role; // Role of the address which mapped to this struct + } + + mapping(address signerOrTransmitter => Oracle) internal s_oracles; + + // s_signers contains the signing address of each oracle + address[] internal s_signers; + + // s_transmitters contains the transmission address of each oracle, + // i.e. the address the oracle actually sends transactions to the contract from + address[] internal s_transmitters; + + struct DecodedReport { + bytes32[] requestIds; + bytes[] results; + bytes[] errors; + bytes[] onchainMetadata; + bytes[] offchainMetadata; + } + + /* + * Config logic + */ + + // Reverts transaction if config args are invalid + modifier checkConfigValid( + uint256 numSigners, + uint256 numTransmitters, + uint256 f + ) { + if (numSigners > MAX_NUM_ORACLES) revert InvalidConfig("too many signers"); + if (f == 0) revert InvalidConfig("f must be positive"); + if (numSigners != numTransmitters) revert InvalidConfig("oracle addresses out of registration"); + if (numSigners <= 3 * f) revert InvalidConfig("faulty-oracle f too high"); + _; + } + + struct SetConfigArgs { + address[] signers; + address[] transmitters; + uint8 f; + bytes onchainConfig; + uint64 offchainConfigVersion; + bytes offchainConfig; + } + + /// @inheritdoc OCR2Abstract + function latestConfigDigestAndEpoch() + external + view + virtual + override + returns (bool scanLogs, bytes32 configDigest, uint32 epoch) + { + return (true, bytes32(0), uint32(0)); + } + + /** + * @notice sets offchain reporting protocol configuration incl. participating oracles + * @param _signers addresses with which oracles sign the reports + * @param _transmitters addresses oracles use to transmit the reports + * @param _f number of faulty oracles the system can tolerate + * @param _onchainConfig encoded on-chain contract configuration + * @param _offchainConfigVersion version number for offchainEncoding schema + * @param _offchainConfig encoded off-chain oracle configuration + */ + function setConfig( + address[] memory _signers, + address[] memory _transmitters, + uint8 _f, + bytes memory _onchainConfig, + uint64 _offchainConfigVersion, + bytes memory _offchainConfig + ) external override checkConfigValid(_signers.length, _transmitters.length, _f) onlyOwner { + SetConfigArgs memory args = SetConfigArgs({ + signers: _signers, + transmitters: _transmitters, + f: _f, + onchainConfig: _onchainConfig, + offchainConfigVersion: _offchainConfigVersion, + offchainConfig: _offchainConfig + }); + + _beforeSetConfig(args.f, args.onchainConfig); + + while (s_signers.length != 0) { + // remove any old signer/transmitter addresses + uint256 lastIdx = s_signers.length - 1; + address signer = s_signers[lastIdx]; + address transmitter = s_transmitters[lastIdx]; + delete s_oracles[signer]; + delete s_oracles[transmitter]; + s_signers.pop(); + s_transmitters.pop(); + } + + // Bounded by MAX_NUM_ORACLES in OCR2Abstract.sol + for (uint256 i = 0; i < args.signers.length; i++) { + if (args.signers[i] == address(0)) revert InvalidConfig("signer must not be empty"); + if (args.transmitters[i] == address(0)) revert InvalidConfig("transmitter must not be empty"); + // add new signer/transmitter addresses + if (s_oracles[args.signers[i]].role != Role.Unset) revert InvalidConfig("repeated signer address"); + s_oracles[args.signers[i]] = Oracle(uint8(i), Role.Signer); + if (s_oracles[args.transmitters[i]].role != Role.Unset) revert InvalidConfig("repeated transmitter address"); + s_oracles[args.transmitters[i]] = Oracle(uint8(i), Role.Transmitter); + s_signers.push(args.signers[i]); + s_transmitters.push(args.transmitters[i]); + } + s_configInfo.f = args.f; + uint32 previousConfigBlockNumber = s_latestConfigBlockNumber; + s_latestConfigBlockNumber = uint32(block.number); + s_configCount += 1; + { + s_configInfo.latestConfigDigest = _configDigestFromConfigData( + block.chainid, + address(this), + s_configCount, + args.signers, + args.transmitters, + args.f, + args.onchainConfig, + args.offchainConfigVersion, + args.offchainConfig + ); + } + s_configInfo.n = uint8(args.signers.length); + + emit ConfigSet( + previousConfigBlockNumber, + s_configInfo.latestConfigDigest, + s_configCount, + args.signers, + args.transmitters, + args.f, + args.onchainConfig, + args.offchainConfigVersion, + args.offchainConfig + ); + } + + function _configDigestFromConfigData( + uint256 _chainId, + address _contractAddress, + uint64 _configCount, + address[] memory _signers, + address[] memory _transmitters, + uint8 _f, + bytes memory _onchainConfig, + uint64 _encodedConfigVersion, + bytes memory _encodedConfig + ) internal pure returns (bytes32) { + uint256 h = uint256( + keccak256( + abi.encode( + _chainId, + _contractAddress, + _configCount, + _signers, + _transmitters, + _f, + _onchainConfig, + _encodedConfigVersion, + _encodedConfig + ) + ) + ); + uint256 prefixMask = type(uint256).max << (256 - 16); // 0xFFFF00..00 + uint256 prefix = 0x0001 << (256 - 16); // 0x000100..00 + return bytes32((prefix & prefixMask) | (h & ~prefixMask)); + } + + /** + * @notice information about current offchain reporting protocol configuration + * @return configCount ordinal number of current config, out of all configs applied to this contract so far + * @return blockNumber block at which this config was set + * @return configDigest domain-separation tag for current config (see __configDigestFromConfigData) + */ + function latestConfigDetails() + external + view + override + returns (uint32 configCount, uint32 blockNumber, bytes32 configDigest) + { + return (s_configCount, s_latestConfigBlockNumber, s_configInfo.latestConfigDigest); + } + + /** + * @return list of addresses permitted to transmit reports to this contract + * @dev The list will match the order used to specify the transmitter during setConfig + */ + function transmitters() external view returns (address[] memory) { + return s_transmitters; + } + + function _beforeSetConfig(uint8 _f, bytes memory _onchainConfig) internal virtual; + + /** + * @dev hook called after the report has been fully validated + * for the extending contract to handle additional logic, such as oracle payment + * @param decodedReport decodedReport + */ + function _report(DecodedReport memory decodedReport) internal virtual; + + // The constant-length components of the msg.data sent to transmit. + // See the "If we wanted to call sam" example on for example reasoning + // https://solidity.readthedocs.io/en/v0.7.2/abi-spec.html + uint16 private constant TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT = + 4 + // function selector + 32 * + 3 + // 3 words containing reportContext + 32 + // word containing start location of abiencoded report value + 32 + // word containing location start of abiencoded rs value + 32 + // word containing start location of abiencoded ss value + 32 + // rawVs value + 32 + // word containing length of report + 32 + // word containing length rs + 32 + // word containing length of ss + 0; // placeholder + + function _requireExpectedMsgDataLength( + bytes calldata report, + bytes32[] calldata rs, + bytes32[] calldata ss + ) private pure { + // calldata will never be big enough to make this overflow + uint256 expected = uint256(TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT) + + report.length + // one byte pure entry in _report + rs.length * + 32 + // 32 bytes per entry in _rs + ss.length * + 32 + // 32 bytes per entry in _ss + 0; // placeholder + if (msg.data.length != expected) revert ReportInvalid("calldata length mismatch"); + } + + function _beforeTransmit( + bytes calldata report + ) internal virtual returns (bool shouldStop, DecodedReport memory decodedReport); + + /** + * @notice transmit is called to post a new report to the contract + * @param report serialized report, which the signatures are signing. + * @param rs ith element is the R components of the ith signature on report. Must have at most maxNumOracles entries + * @param ss ith element is the S components of the ith signature on report. Must have at most maxNumOracles entries + * @param rawVs ith element is the the V component of the ith signature + */ + function transmit( + // NOTE: If these parameters are changed, expectedMsgDataLength and/or + // TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT need to be changed accordingly + bytes32[3] calldata reportContext, + bytes calldata report, + bytes32[] calldata rs, + bytes32[] calldata ss, + bytes32 rawVs // signatures + ) external override { + (bool shouldStop, DecodedReport memory decodedReport) = _beforeTransmit(report); + + if (shouldStop) { + return; + } + + { + // reportContext consists of: + // reportContext[0]: ConfigDigest + // reportContext[1]: 27 byte padding, 4-byte epoch and 1-byte round + // reportContext[2]: ExtraHash + bytes32 configDigest = reportContext[0]; + uint32 epochAndRound = uint32(uint256(reportContext[1])); + + emit Transmitted(configDigest, uint32(epochAndRound >> 8)); + + // The following check is disabled to allow both current and proposed routes to submit reports using the same OCR config digest + // Chainlink Functions uses globally unique request IDs. Metadata about the request is stored and checked in the Coordinator and Router + // require(configInfo.latestConfigDigest == configDigest, "configDigest mismatch"); + + _requireExpectedMsgDataLength(report, rs, ss); + + uint256 expectedNumSignatures = (s_configInfo.n + s_configInfo.f) / 2 + 1; + + if (rs.length != expectedNumSignatures) revert ReportInvalid("wrong number of signatures"); + if (rs.length != ss.length) revert ReportInvalid("report rs and ss must be of equal length"); + + Oracle memory transmitter = s_oracles[msg.sender]; + if (transmitter.role != Role.Transmitter && msg.sender != s_transmitters[transmitter.index]) + revert ReportInvalid("unauthorized transmitter"); + } + + address[MAX_NUM_ORACLES] memory signed; + + { + // Verify signatures attached to report + bytes32 h = keccak256(abi.encodePacked(keccak256(report), reportContext)); + + Oracle memory o; + // Bounded by MAX_NUM_ORACLES in OCR2Abstract.sol + for (uint256 i = 0; i < rs.length; ++i) { + address signer = ecrecover(h, uint8(rawVs[i]) + 27, rs[i], ss[i]); + o = s_oracles[signer]; + if (o.role != Role.Signer) revert ReportInvalid("address not authorized to sign"); + if (signed[o.index] != address(0)) revert ReportInvalid("non-unique signature"); + signed[o.index] = signer; + } + } + + _report(decodedReport); + } +} diff --git a/contracts/src/v0.8/keystone/KeystoneForwarder.sol b/contracts/src/v0.8/keystone/KeystoneForwarder.sol index 2fa3304a..b4a9501e 100644 --- a/contracts/src/v0.8/keystone/KeystoneForwarder.sol +++ b/contracts/src/v0.8/keystone/KeystoneForwarder.sol @@ -6,7 +6,7 @@ import {ConfirmedOwner} from "../shared/access/ConfirmedOwner.sol"; import {TypeAndVersionInterface} from "../interfaces/TypeAndVersionInterface.sol"; import {Utils} from "./libraries/Utils.sol"; -// solhint-disable custom-errors, no-unused-vars +// solhint-disable gas-custom-errors, no-unused-vars contract KeystoneForwarder is IForwarder, ConfirmedOwner, TypeAndVersionInterface { error ReentrantCall(); diff --git a/contracts/src/v0.8/keystone/libraries/Utils.sol b/contracts/src/v0.8/keystone/libraries/Utils.sol index 3a11c079..66e2635b 100644 --- a/contracts/src/v0.8/keystone/libraries/Utils.sol +++ b/contracts/src/v0.8/keystone/libraries/Utils.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -// solhint-disable custom-errors +// solhint-disable gas-custom-errors library Utils { // solhint-disable avoid-low-level-calls, chainlink-solidity/explicit-returns function _splitSignature(bytes memory sig) internal pure returns (bytes32 r, bytes32 s, uint8 v) { diff --git a/contracts/src/v0.8/l2ep/dev/CrossDomainOwnable.sol b/contracts/src/v0.8/l2ep/dev/CrossDomainOwnable.sol index b9a435a7..f861da32 100644 --- a/contracts/src/v0.8/l2ep/dev/CrossDomainOwnable.sol +++ b/contracts/src/v0.8/l2ep/dev/CrossDomainOwnable.sol @@ -42,7 +42,7 @@ contract CrossDomainOwnable is CrossDomainOwnableInterface, ConfirmedOwner { * @notice validate, transfer ownership, and emit relevant events */ function _transferL1Ownership(address to) internal { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(to != msg.sender, "Cannot transfer to self"); s_l1PendingOwner = to; @@ -65,7 +65,7 @@ contract CrossDomainOwnable is CrossDomainOwnableInterface, ConfirmedOwner { * @notice Reverts if called by anyone other than the L1 owner. */ modifier onlyL1Owner() virtual { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(msg.sender == s_l1Owner, "Only callable by L1 owner"); _; } @@ -74,7 +74,7 @@ contract CrossDomainOwnable is CrossDomainOwnableInterface, ConfirmedOwner { * @notice Reverts if called by anyone other than the L1 owner. */ modifier onlyProposedL1Owner() virtual { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(msg.sender == s_l1PendingOwner, "Only callable by proposed L1 owner"); _; } diff --git a/contracts/src/v0.8/l2ep/dev/Flags.sol b/contracts/src/v0.8/l2ep/dev/Flags.sol index b943c06d..0fcd095a 100644 --- a/contracts/src/v0.8/l2ep/dev/Flags.sol +++ b/contracts/src/v0.8/l2ep/dev/Flags.sol @@ -17,7 +17,7 @@ import {FlagsInterface} from "./interfaces/FlagsInterface.sol"; * An expected pattern is to allow addresses to raise flags on themselves, so if you are subscribing to * FlagOn events you should filter for addresses you care about. */ -// solhint-disable custom-errors +// solhint-disable gas-custom-errors contract Flags is ITypeAndVersion, FlagsInterface, SimpleReadAccessController { AccessControllerInterface public raisingAccessController; AccessControllerInterface public loweringAccessController; diff --git a/contracts/src/v0.8/l2ep/dev/arbitrum/ArbitrumCrossDomainForwarder.sol b/contracts/src/v0.8/l2ep/dev/arbitrum/ArbitrumCrossDomainForwarder.sol index cdab6d49..158ffcc3 100644 --- a/contracts/src/v0.8/l2ep/dev/arbitrum/ArbitrumCrossDomainForwarder.sol +++ b/contracts/src/v0.8/l2ep/dev/arbitrum/ArbitrumCrossDomainForwarder.sol @@ -56,7 +56,7 @@ contract ArbitrumCrossDomainForwarder is TypeAndVersionInterface, CrossDomainFor * @notice The call MUST come from the L1 owner (via cross-chain message.) Reverts otherwise. */ modifier onlyL1Owner() override { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(msg.sender == crossDomainMessenger(), "Sender is not the L2 messenger"); _; } @@ -65,7 +65,7 @@ contract ArbitrumCrossDomainForwarder is TypeAndVersionInterface, CrossDomainFor * @notice The call MUST come from the proposed L1 owner (via cross-chain message.) Reverts otherwise. */ modifier onlyProposedL1Owner() override { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(msg.sender == AddressAliasHelper.applyL1ToL2Alias(s_l1PendingOwner), "Must be proposed L1 owner"); _; } diff --git a/contracts/src/v0.8/l2ep/dev/arbitrum/ArbitrumCrossDomainGovernor.sol b/contracts/src/v0.8/l2ep/dev/arbitrum/ArbitrumCrossDomainGovernor.sol index 2f1d775e..ebf579b8 100644 --- a/contracts/src/v0.8/l2ep/dev/arbitrum/ArbitrumCrossDomainGovernor.sol +++ b/contracts/src/v0.8/l2ep/dev/arbitrum/ArbitrumCrossDomainGovernor.sol @@ -56,7 +56,7 @@ contract ArbitrumCrossDomainGovernor is DelegateForwarderInterface, ArbitrumCros * @notice The call MUST come from either the L1 owner (via cross-chain message) or the L2 owner. Reverts otherwise. */ modifier onlyLocalOrCrossDomainOwner() { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(msg.sender == crossDomainMessenger() || msg.sender == owner(), "Sender is not the L2 messenger or owner"); _; } diff --git a/contracts/src/v0.8/l2ep/dev/arbitrum/ArbitrumValidator.sol b/contracts/src/v0.8/l2ep/dev/arbitrum/ArbitrumValidator.sol index 66fee505..edcb62ca 100644 --- a/contracts/src/v0.8/l2ep/dev/arbitrum/ArbitrumValidator.sol +++ b/contracts/src/v0.8/l2ep/dev/arbitrum/ArbitrumValidator.sol @@ -98,9 +98,9 @@ contract ArbitrumValidator is TypeAndVersionInterface, AggregatorValidatorInterf address gasPriceL1FeedAddr, PaymentStrategy _paymentStrategy ) { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(crossDomainMessengerAddr != address(0), "Invalid xDomain Messenger address"); - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(l2ArbitrumSequencerUptimeFeedAddr != address(0), "Invalid ArbitrumSequencerUptimeFeed contract address"); CROSS_DOMAIN_MESSENGER = crossDomainMessengerAddr; L2_SEQ_STATUS_RECORDER = l2ArbitrumSequencerUptimeFeedAddr; @@ -301,11 +301,11 @@ contract ArbitrumValidator is TypeAndVersionInterface, AggregatorValidatorInterf /// @notice internal method that stores the gas configuration function _setGasConfig(uint256 maxGas, uint256 gasPriceBid, uint256 baseFee, address gasPriceL1FeedAddr) internal { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(maxGas > 0, "Max gas is zero"); - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(gasPriceBid > 0, "Gas price bid is zero"); - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(gasPriceL1FeedAddr != address(0), "Gas price Aggregator is zero address"); s_gasConfig = GasConfig(maxGas, gasPriceBid, baseFee, gasPriceL1FeedAddr); emit GasConfigSet(maxGas, gasPriceBid, gasPriceL1FeedAddr); @@ -345,7 +345,7 @@ contract ArbitrumValidator is TypeAndVersionInterface, AggregatorValidatorInterf /// @dev reverts if the caller does not have access to change the configuration modifier onlyOwnerOrConfigAccess() { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require( msg.sender == owner() || (address(s_configAC) != address(0) && s_configAC.hasAccess(msg.sender, msg.data)), "No access" diff --git a/contracts/src/v0.8/l2ep/dev/optimism/OptimismCrossDomainForwarder.sol b/contracts/src/v0.8/l2ep/dev/optimism/OptimismCrossDomainForwarder.sol index 67272015..37d9260b 100644 --- a/contracts/src/v0.8/l2ep/dev/optimism/OptimismCrossDomainForwarder.sol +++ b/contracts/src/v0.8/l2ep/dev/optimism/OptimismCrossDomainForwarder.sol @@ -29,7 +29,7 @@ contract OptimismCrossDomainForwarder is TypeAndVersionInterface, CrossDomainFor * @param l1OwnerAddr the L1 owner address that will be allowed to call the forward fn */ constructor(iOVM_CrossDomainMessenger crossDomainMessengerAddr, address l1OwnerAddr) CrossDomainOwnable(l1OwnerAddr) { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(address(crossDomainMessengerAddr) != address(0), "Invalid xDomain Messenger address"); OVM_CROSS_DOMAIN_MESSENGER = crossDomainMessengerAddr; } @@ -65,9 +65,9 @@ contract OptimismCrossDomainForwarder is TypeAndVersionInterface, CrossDomainFor * @notice The call MUST come from the L1 owner (via cross-chain message.) Reverts otherwise. */ modifier onlyL1Owner() override { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(msg.sender == crossDomainMessenger(), "Sender is not the L2 messenger"); - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require( iOVM_CrossDomainMessenger(crossDomainMessenger()).xDomainMessageSender() == l1Owner(), "xDomain sender is not the L1 owner" @@ -80,9 +80,9 @@ contract OptimismCrossDomainForwarder is TypeAndVersionInterface, CrossDomainFor */ modifier onlyProposedL1Owner() override { address messenger = crossDomainMessenger(); - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(msg.sender == messenger, "Sender is not the L2 messenger"); - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require( iOVM_CrossDomainMessenger(messenger).xDomainMessageSender() == s_l1PendingOwner, "Must be proposed L1 owner" diff --git a/contracts/src/v0.8/l2ep/dev/optimism/OptimismCrossDomainGovernor.sol b/contracts/src/v0.8/l2ep/dev/optimism/OptimismCrossDomainGovernor.sol index 1f630a3f..ad780946 100644 --- a/contracts/src/v0.8/l2ep/dev/optimism/OptimismCrossDomainGovernor.sol +++ b/contracts/src/v0.8/l2ep/dev/optimism/OptimismCrossDomainGovernor.sol @@ -59,11 +59,11 @@ contract OptimismCrossDomainGovernor is DelegateForwarderInterface, OptimismCros modifier onlyLocalOrCrossDomainOwner() { address messenger = crossDomainMessenger(); // 1. The delegatecall MUST come from either the L1 owner (via cross-chain message) or the L2 owner - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(msg.sender == messenger || msg.sender == owner(), "Sender is not the L2 messenger or owner"); // 2. The L2 Messenger's caller MUST be the L1 Owner if (msg.sender == messenger) { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require( iOVM_CrossDomainMessenger(messenger).xDomainMessageSender() == l1Owner(), "xDomain sender is not the L1 owner" diff --git a/contracts/src/v0.8/l2ep/dev/optimism/OptimismValidator.sol b/contracts/src/v0.8/l2ep/dev/optimism/OptimismValidator.sol index e41c61a4..a54a56ee 100644 --- a/contracts/src/v0.8/l2ep/dev/optimism/OptimismValidator.sol +++ b/contracts/src/v0.8/l2ep/dev/optimism/OptimismValidator.sol @@ -33,9 +33,9 @@ contract OptimismValidator is TypeAndVersionInterface, AggregatorValidatorInterf * @param gasLimit the gasLimit to use for sending a message from L1 to L2 */ constructor(address l1CrossDomainMessengerAddress, address l2UptimeFeedAddr, uint32 gasLimit) { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(l1CrossDomainMessengerAddress != address(0), "Invalid xDomain Messenger address"); - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(l2UptimeFeedAddr != address(0), "Invalid OptimismSequencerUptimeFeed contract address"); L1_CROSS_DOMAIN_MESSENGER_ADDRESS = l1CrossDomainMessengerAddress; L2_UPTIME_FEED_ADDR = l2UptimeFeedAddr; diff --git a/contracts/src/v0.8/l2ep/dev/scroll/ScrollCrossDomainForwarder.sol b/contracts/src/v0.8/l2ep/dev/scroll/ScrollCrossDomainForwarder.sol index f18f7c32..4ec51fc6 100644 --- a/contracts/src/v0.8/l2ep/dev/scroll/ScrollCrossDomainForwarder.sol +++ b/contracts/src/v0.8/l2ep/dev/scroll/ScrollCrossDomainForwarder.sol @@ -15,7 +15,6 @@ import {Address} from "../../../vendor/openzeppelin-solidity/v4.7.3/contracts/ut /// @dev Any other L2 contract which uses this contract's address as a privileged position, /// can be considered to be owned by the `l1Owner` contract ScrollCrossDomainForwarder is TypeAndVersionInterface, CrossDomainForwarder { - // solhint-disable-next-line chainlink-solidity/all-caps-constant-storage-variables string public constant override typeAndVersion = "ScrollCrossDomainForwarder 1.0.0"; address internal immutable i_scrollCrossDomainMessenger; @@ -23,7 +22,7 @@ contract ScrollCrossDomainForwarder is TypeAndVersionInterface, CrossDomainForwa /// @param crossDomainMessengerAddr the xDomain bridge messenger (Scroll bridge L2) contract address /// @param l1OwnerAddr the L1 owner address that will be allowed to call the forward fn constructor(IScrollMessenger crossDomainMessengerAddr, address l1OwnerAddr) CrossDomainOwnable(l1OwnerAddr) { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(address(crossDomainMessengerAddr) != address(0), "Invalid xDomain Messenger address"); i_scrollCrossDomainMessenger = address(crossDomainMessengerAddr); } @@ -41,9 +40,9 @@ contract ScrollCrossDomainForwarder is TypeAndVersionInterface, CrossDomainForwa /// @notice The call MUST come from the L1 owner (via cross-chain message.) Reverts otherwise. modifier onlyL1Owner() override { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(msg.sender == i_scrollCrossDomainMessenger, "Sender is not the L2 messenger"); - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require( IScrollMessenger(i_scrollCrossDomainMessenger).xDomainMessageSender() == l1Owner(), "xDomain sender is not the L1 owner" @@ -53,9 +52,9 @@ contract ScrollCrossDomainForwarder is TypeAndVersionInterface, CrossDomainForwa /// @notice The call MUST come from the proposed L1 owner (via cross-chain message.) Reverts otherwise. modifier onlyProposedL1Owner() override { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(msg.sender == i_scrollCrossDomainMessenger, "Sender is not the L2 messenger"); - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require( IScrollMessenger(i_scrollCrossDomainMessenger).xDomainMessageSender() == s_l1PendingOwner, "Must be proposed L1 owner" diff --git a/contracts/src/v0.8/l2ep/dev/scroll/ScrollCrossDomainGovernor.sol b/contracts/src/v0.8/l2ep/dev/scroll/ScrollCrossDomainGovernor.sol index 00ef9219..f7d13059 100644 --- a/contracts/src/v0.8/l2ep/dev/scroll/ScrollCrossDomainGovernor.sol +++ b/contracts/src/v0.8/l2ep/dev/scroll/ScrollCrossDomainGovernor.sol @@ -17,7 +17,6 @@ import {Address} from "../../../vendor/openzeppelin-solidity/v4.7.3/contracts/ut /// @dev Any other L2 contract which uses this contract's address as a privileged position, /// can be considered to be simultaneously owned by the `l1Owner` and L2 `owner` contract ScrollCrossDomainGovernor is DelegateForwarderInterface, TypeAndVersionInterface, CrossDomainForwarder { - // solhint-disable-next-line chainlink-solidity/all-caps-constant-storage-variables string public constant override typeAndVersion = "ScrollCrossDomainGovernor 1.0.0"; address internal immutable i_scrollCrossDomainMessenger; @@ -25,7 +24,7 @@ contract ScrollCrossDomainGovernor is DelegateForwarderInterface, TypeAndVersion /// @param crossDomainMessengerAddr the xDomain bridge messenger (Scroll bridge L2) contract address /// @param l1OwnerAddr the L1 owner address that will be allowed to call the forward fn constructor(IScrollMessenger crossDomainMessengerAddr, address l1OwnerAddr) CrossDomainOwnable(l1OwnerAddr) { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(address(crossDomainMessengerAddr) != address(0), "Invalid xDomain Messenger address"); i_scrollCrossDomainMessenger = address(crossDomainMessengerAddr); } @@ -49,9 +48,9 @@ contract ScrollCrossDomainGovernor is DelegateForwarderInterface, TypeAndVersion /// @notice The call MUST come from the L1 owner (via cross-chain message.) Reverts otherwise. modifier onlyL1Owner() override { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(msg.sender == i_scrollCrossDomainMessenger, "Sender is not the L2 messenger"); - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require( IScrollMessenger(i_scrollCrossDomainMessenger).xDomainMessageSender() == l1Owner(), "xDomain sender is not the L1 owner" @@ -62,14 +61,14 @@ contract ScrollCrossDomainGovernor is DelegateForwarderInterface, TypeAndVersion /// @notice The call MUST come from either the L1 owner (via cross-chain message) or the L2 owner. Reverts otherwise. modifier onlyLocalOrCrossDomainOwner() { // 1. The delegatecall MUST come from either the L1 owner (via cross-chain message) or the L2 owner - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require( msg.sender == i_scrollCrossDomainMessenger || msg.sender == owner(), "Sender is not the L2 messenger or owner" ); // 2. The L2 Messenger's caller MUST be the L1 Owner if (msg.sender == i_scrollCrossDomainMessenger) { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require( IScrollMessenger(i_scrollCrossDomainMessenger).xDomainMessageSender() == l1Owner(), "xDomain sender is not the L1 owner" @@ -80,9 +79,9 @@ contract ScrollCrossDomainGovernor is DelegateForwarderInterface, TypeAndVersion /// @notice The call MUST come from the proposed L1 owner (via cross-chain message.) Reverts otherwise. modifier onlyProposedL1Owner() override { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(msg.sender == i_scrollCrossDomainMessenger, "Sender is not the L2 messenger"); - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require( IScrollMessenger(i_scrollCrossDomainMessenger).xDomainMessageSender() == s_l1PendingOwner, "Must be proposed L1 owner" diff --git a/contracts/src/v0.8/l2ep/dev/scroll/ScrollSequencerUptimeFeed.sol b/contracts/src/v0.8/l2ep/dev/scroll/ScrollSequencerUptimeFeed.sol index 9df2b612..e60e8703 100644 --- a/contracts/src/v0.8/l2ep/dev/scroll/ScrollSequencerUptimeFeed.sol +++ b/contracts/src/v0.8/l2ep/dev/scroll/ScrollSequencerUptimeFeed.sol @@ -19,7 +19,6 @@ contract ScrollSequencerUptimeFeed is TypeAndVersionInterface, SimpleReadAccessController { - // solhint-disable-next-line chainlink-solidity/all-caps-constant-storage-variables string public constant override typeAndVersion = "ScrollSequencerUptimeFeed 1.0.0"; /// @dev Round info (for uptime history) diff --git a/contracts/src/v0.8/l2ep/dev/scroll/ScrollValidator.sol b/contracts/src/v0.8/l2ep/dev/scroll/ScrollValidator.sol index 968b891b..9df4a12a 100644 --- a/contracts/src/v0.8/l2ep/dev/scroll/ScrollValidator.sol +++ b/contracts/src/v0.8/l2ep/dev/scroll/ScrollValidator.sol @@ -19,7 +19,6 @@ contract ScrollValidator is TypeAndVersionInterface, AggregatorValidatorInterfac // solhint-disable-next-line chainlink-solidity/prefix-immutable-variables-with-i address public immutable L1_MSG_QUEUE_ADDR; - // solhint-disable-next-line chainlink-solidity/all-caps-constant-storage-variables string public constant override typeAndVersion = "ScrollValidator 1.0.0"; int256 private constant ANSWER_SEQ_OFFLINE = 1; uint32 private s_gasLimit; @@ -37,11 +36,11 @@ contract ScrollValidator is TypeAndVersionInterface, AggregatorValidatorInterfac address l1MessageQueueAddr, uint32 gasLimit ) { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(l1CrossDomainMessengerAddress != address(0), "Invalid xDomain Messenger address"); - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(l1MessageQueueAddr != address(0), "Invalid L1 message queue address"); - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(l2UptimeFeedAddr != address(0), "Invalid ScrollSequencerUptimeFeed contract address"); L1_CROSS_DOMAIN_MESSENGER_ADDRESS = l1CrossDomainMessengerAddress; L2_UPTIME_FEED_ADDR = l2UptimeFeedAddr; diff --git a/contracts/src/v0.8/l2ep/test/mocks/optimism/MockOVMCrossDomainMessenger.sol b/contracts/src/v0.8/l2ep/test/mocks/optimism/MockOVMCrossDomainMessenger.sol index 3a45cba3..0c4193a3 100644 --- a/contracts/src/v0.8/l2ep/test/mocks/optimism/MockOVMCrossDomainMessenger.sol +++ b/contracts/src/v0.8/l2ep/test/mocks/optimism/MockOVMCrossDomainMessenger.sol @@ -1,6 +1,5 @@ // SPDX-License-Identifier: MIT - -pragma solidity >=0.7.6 <0.9.0; +pragma solidity ^0.8.0; import {iOVM_CrossDomainMessenger} from "../../../../vendor/@eth-optimism/contracts/v0.4.7/contracts/optimistic-ethereum/iOVM/bridge/messaging/iOVM_CrossDomainMessenger.sol"; diff --git a/contracts/src/v0.8/operatorforwarder/dev/AuthorizedForwarder.sol b/contracts/src/v0.8/operatorforwarder/dev/AuthorizedForwarder.sol index 1fe5e8f0..824ffce6 100644 --- a/contracts/src/v0.8/operatorforwarder/dev/AuthorizedForwarder.sol +++ b/contracts/src/v0.8/operatorforwarder/dev/AuthorizedForwarder.sol @@ -5,7 +5,7 @@ import {ConfirmedOwnerWithProposal} from "../../shared/access/ConfirmedOwnerWith import {AuthorizedReceiver} from "./AuthorizedReceiver.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -// solhint-disable custom-errors +// solhint-disable gas-custom-errors contract AuthorizedForwarder is ConfirmedOwnerWithProposal, AuthorizedReceiver { using Address for address; @@ -27,7 +27,6 @@ contract AuthorizedForwarder is ConfirmedOwnerWithProposal, AuthorizedReceiver { } } - // solhint-disable-next-line chainlink-solidity/all-caps-constant-storage-variables string public constant typeAndVersion = "AuthorizedForwarder 1.1.0"; // @notice Forward a call to another contract diff --git a/contracts/src/v0.8/operatorforwarder/dev/AuthorizedReceiver.sol b/contracts/src/v0.8/operatorforwarder/dev/AuthorizedReceiver.sol index bc5f1c0e..b7411188 100644 --- a/contracts/src/v0.8/operatorforwarder/dev/AuthorizedReceiver.sol +++ b/contracts/src/v0.8/operatorforwarder/dev/AuthorizedReceiver.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.19; import {AuthorizedReceiverInterface} from "./interfaces/AuthorizedReceiverInterface.sol"; -// solhint-disable custom-errors +// solhint-disable gas-custom-errors abstract contract AuthorizedReceiver is AuthorizedReceiverInterface { mapping(address sender => bool authorized) private s_authorizedSenders; address[] private s_authorizedSenderList; diff --git a/contracts/src/v0.8/operatorforwarder/dev/LinkTokenReceiver.sol b/contracts/src/v0.8/operatorforwarder/dev/LinkTokenReceiver.sol index cfde9a4d..dab259ca 100644 --- a/contracts/src/v0.8/operatorforwarder/dev/LinkTokenReceiver.sol +++ b/contracts/src/v0.8/operatorforwarder/dev/LinkTokenReceiver.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.19; -// solhint-disable custom-errors +// solhint-disable gas-custom-errors abstract contract LinkTokenReceiver { // @notice Called when LINK is sent to the contract via `transferAndCall` // @dev The data payload's first 2 words will be overwritten by the `sender` and `amount` diff --git a/contracts/src/v0.8/operatorforwarder/dev/Operator.sol b/contracts/src/v0.8/operatorforwarder/dev/Operator.sol index 26295b27..e68df5fd 100644 --- a/contracts/src/v0.8/operatorforwarder/dev/Operator.sol +++ b/contracts/src/v0.8/operatorforwarder/dev/Operator.sol @@ -10,15 +10,12 @@ import {OperatorInterface} from "../../interfaces/OperatorInterface.sol"; import {IOwnable} from "../../shared/interfaces/IOwnable.sol"; import {WithdrawalInterface} from "./interfaces/WithdrawalInterface.sol"; import {OracleInterface} from "../../interfaces/OracleInterface.sol"; -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {SafeCast} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/math/SafeCast.sol"; // @title The Chainlink Operator contract // @notice Node operators can deploy this contract to fulfill requests sent to them -// solhint-disable custom-errors +// solhint-disable gas-custom-errors contract Operator is AuthorizedReceiver, ConfirmedOwner, LinkTokenReceiver, OperatorInterface, WithdrawalInterface { - using Address for address; - struct Commitment { bytes31 paramsHash; uint8 dataVersion; @@ -72,7 +69,6 @@ contract Operator is AuthorizedReceiver, ConfirmedOwner, LinkTokenReceiver, Oper i_linkToken = LinkTokenInterface(link); // external but already deployed and unalterable } - // solhint-disable-next-line chainlink-solidity/all-caps-constant-storage-variables string public constant typeAndVersion = "Operator 1.0.0"; // @notice Creates the Chainlink request. This is a backwards compatible API @@ -286,7 +282,7 @@ contract Operator is AuthorizedReceiver, ConfirmedOwner, LinkTokenReceiver, Oper // @param to address // @param data to forward function ownerForward(address to, bytes calldata data) external onlyOwner validateNotToLINK(to) { - require(to.isContract(), "Must forward to a contract"); + require(to.code.length != 0, "Must forward to a contract"); // solhint-disable-next-line avoid-low-level-calls (bool status, ) = to.call(data); require(status, "Forwarded call failed"); @@ -337,7 +333,7 @@ contract Operator is AuthorizedReceiver, ConfirmedOwner, LinkTokenReceiver, Oper uint256 payment, bytes4 callbackFunc, uint256 expiration - ) external override { + ) public override { bytes31 paramsHash = _buildParamsHash(payment, msg.sender, callbackFunc, expiration); require(s_commitments[requestId].paramsHash == paramsHash, "Params do not match request ID"); // solhint-disable-next-line not-rely-on-time @@ -346,6 +342,8 @@ contract Operator is AuthorizedReceiver, ConfirmedOwner, LinkTokenReceiver, Oper delete s_commitments[requestId]; emit CancelOracleRequest(requestId); + // Free up the escrowed funds, as we're sending them back to the requester + s_tokensInEscrow -= payment; i_linkToken.transfer(msg.sender, payment); } @@ -363,16 +361,7 @@ contract Operator is AuthorizedReceiver, ConfirmedOwner, LinkTokenReceiver, Oper bytes4 callbackFunc, uint256 expiration ) external { - bytes32 requestId = keccak256(abi.encodePacked(msg.sender, nonce)); - bytes31 paramsHash = _buildParamsHash(payment, msg.sender, callbackFunc, expiration); - require(s_commitments[requestId].paramsHash == paramsHash, "Params do not match request ID"); - // solhint-disable-next-line not-rely-on-time - require(expiration <= block.timestamp, "Request is not expired"); - - delete s_commitments[requestId]; - emit CancelOracleRequest(requestId); - - i_linkToken.transfer(msg.sender, payment); + cancelOracleRequest(keccak256(abi.encodePacked(msg.sender, nonce)), payment, callbackFunc, expiration); } // @notice Returns the address of the LINK token diff --git a/contracts/src/v0.8/operatorforwarder/dev/OperatorFactory.sol b/contracts/src/v0.8/operatorforwarder/dev/OperatorFactory.sol index 0ff4bb65..15035355 100644 --- a/contracts/src/v0.8/operatorforwarder/dev/OperatorFactory.sol +++ b/contracts/src/v0.8/operatorforwarder/dev/OperatorFactory.sol @@ -6,7 +6,7 @@ import {AuthorizedForwarder} from "./AuthorizedForwarder.sol"; // @title Operator Factory // @notice Creates Operator contracts for node operators -// solhint-disable custom-errors +// solhint-disable gas-custom-errors contract OperatorFactory { // solhint-disable-next-line chainlink-solidity/prefix-immutable-variables-with-i address public immutable linkToken; @@ -20,7 +20,6 @@ contract OperatorFactory { linkToken = linkAddress; } - // solhint-disable-next-line chainlink-solidity/all-caps-constant-storage-variables string public constant typeAndVersion = "OperatorFactory 1.0.0"; // @notice creates a new Operator contract with the msg.sender as owner diff --git a/contracts/src/v0.8/operatorforwarder/dev/test/operator.t.sol b/contracts/src/v0.8/operatorforwarder/dev/test/operator.t.sol new file mode 100644 index 00000000..96975a2b --- /dev/null +++ b/contracts/src/v0.8/operatorforwarder/dev/test/operator.t.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {Operator} from "../Operator.sol"; +import {ChainlinkClientHelper} from "./testhelpers/ChainlinkClientHelper.sol"; +import {LinkToken} from "../../../shared/token/ERC677/LinkToken.sol"; + +contract Operator_cancelRequest is Test { + address public s_link; + ChainlinkClientHelper public s_client; + Operator public s_operator; + + function setUp() public { + s_link = address(new LinkToken()); + s_client = new ChainlinkClientHelper(s_link); + + address[] memory auth = new address[](1); + auth[0] = address(this); + s_operator = new Operator(s_link, address(this)); + s_operator.setAuthorizedSenders(auth); + } + + function test_Success(uint96 payment) public { + payment = uint96(bound(payment, 1, type(uint96).max)); + deal(s_link, address(s_client), payment); + // We're going to cancel one request and fulfil the other + bytes32 requestIdToCancel = s_client.sendRequest(address(s_operator), payment); + + // Nothing withdrawable + // 1 payment in escrow + // Client has zero link + assertEq(s_operator.withdrawable(), 0); + assertEq(LinkToken(s_link).balanceOf(address(s_operator)), payment); + assertEq(LinkToken(s_link).balanceOf(address(s_client)), 0); + + // Advance time so we can cancel + uint256 expiration = block.timestamp + s_operator.EXPIRYTIME(); + vm.warp(expiration + 1); + s_client.cancelRequest(requestIdToCancel, payment, expiration); + + // 1 payment has been returned due to the cancellation. + assertEq(s_operator.withdrawable(), 0); + assertEq(LinkToken(s_link).balanceOf(address(s_operator)), 0); + assertEq(LinkToken(s_link).balanceOf(address(s_client)), payment); + } + + function test_afterSuccessfulRequestSucess(uint96 payment) public { + payment = uint96(bound(payment, 1, type(uint96).max) / 2); + deal(s_link, address(s_client), 2 * payment); + + // Initial state, client has 2 payments, zero in escrow, zero in the operator, zeero withdrawable + assertEq(s_operator.withdrawable(), 0); + assertEq(LinkToken(s_link).balanceOf(address(s_operator)), 0); + assertEq(LinkToken(s_link).balanceOf(address(s_client)), 2 * payment); + + // We're going to cancel one request and fulfil the other + bytes32 requestId = s_client.sendRequest(address(s_operator), payment); + bytes32 requestIdToCancel = s_client.sendRequest(address(s_operator), payment); + + // Nothing withdrawable + // Operator now has the 2 payments in escrow + // Client has zero payments + assertEq(s_operator.withdrawable(), 0); + assertEq(LinkToken(s_link).balanceOf(address(s_operator)), 2 * payment); + assertEq(LinkToken(s_link).balanceOf(address(s_client)), 0); + + // Fulfill one request + uint256 expiration = block.timestamp + s_operator.EXPIRYTIME(); + s_operator.fulfillOracleRequest( + requestId, + payment, + address(s_client), + s_client.FULFILSELECTOR(), + expiration, + bytes32(hex"01") + ); + // 1 payment withdrawable from fulfilling `requestId`, 1 payment in escrow + assertEq(s_operator.withdrawable(), payment); + assertEq(LinkToken(s_link).balanceOf(address(s_operator)), 2 * payment); + assertEq(LinkToken(s_link).balanceOf(address(s_client)), 0); + + // Advance time so we can cancel + vm.warp(expiration + 1); + s_client.cancelRequest(requestIdToCancel, payment, expiration); + + // 1 payment has been returned due to the cancellation, 1 payment should be withdrawable + assertEq(s_operator.withdrawable(), payment); + assertEq(LinkToken(s_link).balanceOf(address(s_operator)), payment); + assertEq(LinkToken(s_link).balanceOf(address(s_client)), payment); + + // Withdraw the remaining payment + s_operator.withdraw(address(s_client), payment); + + // End state is exactly the same as the initial state. + assertEq(s_operator.withdrawable(), 0); + assertEq(LinkToken(s_link).balanceOf(address(s_operator)), 0); + assertEq(LinkToken(s_link).balanceOf(address(s_client)), 2 * payment); + } +} diff --git a/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/BasicConsumer.sol b/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/BasicConsumer.sol new file mode 100644 index 00000000..7004b534 --- /dev/null +++ b/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/BasicConsumer.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Consumer} from "./Consumer.sol"; + +contract BasicConsumer is Consumer { + constructor(address _link, address _oracle, bytes32 _specId) { + _setChainlinkToken(_link); + _setChainlinkOracle(_oracle); + s_specId = _specId; + } +} diff --git a/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/ChainlinkClientHelper.sol b/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/ChainlinkClientHelper.sol new file mode 100644 index 00000000..d15eb07c --- /dev/null +++ b/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/ChainlinkClientHelper.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {ChainlinkClient} from "../../../../ChainlinkClient.sol"; + +contract ChainlinkClientHelper is ChainlinkClient { + bytes4 public constant FULFILSELECTOR = this.fulfill.selector; + + constructor(address link) { + _setChainlinkToken(link); + } + + function sendRequest(address op, uint256 payment) external returns (bytes32) { + return _sendChainlinkRequestTo(op, _buildOperatorRequest(bytes32(hex"10"), FULFILSELECTOR), payment); + } + + function cancelRequest(bytes32 requestId, uint256 payment, uint256 expiration) external { + _cancelChainlinkRequest(requestId, payment, this.fulfill.selector, expiration); + } + + function fulfill(bytes32) external {} +} diff --git a/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/Chainlinked.sol b/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/Chainlinked.sol new file mode 100644 index 00000000..86dc474e --- /dev/null +++ b/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/Chainlinked.sol @@ -0,0 +1,127 @@ +pragma solidity ^0.8.0; + +import {ChainlinkClient, Chainlink} from "../../../../ChainlinkClient.sol"; + +/** + * @title The Chainlinked contract + * @notice Contract writers can inherit this contract in order to create requests for the + * Chainlink network. ChainlinkClient is an alias of the Chainlinked contract. + */ +// solhint-disable +contract Chainlinked is ChainlinkClient { + /** + * @notice Creates a request that can hold additional parameters + * @param _specId The Job Specification ID that the request will be created for + * @param _callbackAddress The callback address that the response will be sent to + * @param _callbackFunctionSignature The callback function signature to use for the callback address + * @return A Chainlink Request struct in memory + */ + function newRequest( + bytes32 _specId, + address _callbackAddress, + bytes4 _callbackFunctionSignature + ) internal pure returns (Chainlink.Request memory) { + return _buildChainlinkRequest(_specId, _callbackAddress, _callbackFunctionSignature); + } + + /** + * @notice Creates a Chainlink request to the stored oracle address + * @dev Calls `sendChainlinkRequestTo` with the stored oracle address + * @param _req The initialized Chainlink Request + * @param _payment The amount of LINK to send for the request + * @return The request ID + */ + function chainlinkRequest(Chainlink.Request memory _req, uint256 _payment) internal returns (bytes32) { + return _sendChainlinkRequest(_req, _payment); + } + + /** + * @notice Creates a Chainlink request to the specified oracle address + * @dev Generates and stores a request ID, increments the local nonce, and uses `transferAndCall` to + * send LINK which creates a request on the target oracle contract. + * Emits ChainlinkRequested event. + * @param _oracle The address of the oracle for the request + * @param _req The initialized Chainlink Request + * @param _payment The amount of LINK to send for the request + * @return requestId The request ID + */ + function chainlinkRequestTo( + address _oracle, + Chainlink.Request memory _req, + uint256 _payment + ) internal returns (bytes32 requestId) { + return _sendChainlinkRequestTo(_oracle, _req, _payment); + } + + /** + * @notice Sets the stored oracle address + * @param _oracle The address of the oracle contract + */ + function setOracle(address _oracle) internal { + _setChainlinkOracle(_oracle); + } + + /** + * @notice Sets the LINK token address + * @param _link The address of the LINK token contract + */ + function setLinkToken(address _link) internal { + _setChainlinkToken(_link); + } + + /** + * @notice Retrieves the stored address of the LINK token + * @return The address of the LINK token + */ + function chainlinkToken() internal view returns (address) { + return _chainlinkTokenAddress(); + } + + /** + * @notice Retrieves the stored address of the oracle contract + * @return The address of the oracle contract + */ + function oracleAddress() internal view returns (address) { + return _chainlinkOracleAddress(); + } + + /** + * @notice Ensures that the fulfillment is valid for this contract + * @dev Use if the contract developer prefers methods instead of modifiers for validation + * @param _requestId The request ID for fulfillment + */ + function fulfillChainlinkRequest( + bytes32 _requestId + ) + internal + recordChainlinkFulfillment(_requestId) // solhint-disable-next-line no-empty-blocks + {} + + /** + * @notice Sets the stored oracle and LINK token contracts with the addresses resolved by ENS + * @dev Accounts for subnodes having different resolvers + * @param _ens The address of the ENS contract + * @param _node The ENS node hash + */ + function setChainlinkWithENS(address _ens, bytes32 _node) internal { + _useChainlinkWithENS(_ens, _node); + } + + /** + * @notice Sets the stored oracle contract with the address resolved by ENS + * @dev This may be called on its own as long as `setChainlinkWithENS` has been called previously + */ + function setOracleWithENS() internal { + _updateChainlinkOracleWithENS(); + } + + /** + * @notice Allows for a request which was created on another contract to be fulfilled + * on this contract + * @param _oracle The address of the oracle contract that will fulfill the request + * @param _requestId The request ID used for the response + */ + function addExternalRequest(address _oracle, bytes32 _requestId) internal { + _addChainlinkExternalRequest(_oracle, _requestId); + } +} diff --git a/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/Consumer.sol b/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/Consumer.sol new file mode 100644 index 00000000..0d01778e --- /dev/null +++ b/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/Consumer.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {ChainlinkClient, ChainlinkRequestInterface, LinkTokenInterface} from "../../../../ChainlinkClient.sol"; +import {Chainlink} from "../../../../Chainlink.sol"; + +contract Consumer is ChainlinkClient { + using Chainlink for Chainlink.Request; + + bytes32 internal s_specId; + bytes32 public currentPrice; + + event RequestFulfilled( + bytes32 indexed requestId, // User-defined ID + bytes32 indexed price + ); + + function requestEthereumPrice(string memory _currency, uint256 _payment) public { + requestEthereumPriceByCallback(_currency, _payment, address(this)); + } + + function requestEthereumPriceByCallback(string memory _currency, uint256 _payment, address _callback) public { + Chainlink.Request memory req = _buildChainlinkRequest(s_specId, _callback, this.fulfill.selector); + req._add("get", "https://min-api.cryptocompare.com/data/price?fsym=ETH&tsyms=USD,EUR,JPY"); + string[] memory path = new string[](1); + path[0] = _currency; + req._addStringArray("path", path); + _sendChainlinkRequest(req, _payment); + } + + function cancelRequest( + address _oracle, + bytes32 _requestId, + uint256 _payment, + bytes4 _callbackFunctionId, + uint256 _expiration + ) public { + ChainlinkRequestInterface requested = ChainlinkRequestInterface(_oracle); + requested.cancelOracleRequest(_requestId, _payment, _callbackFunctionId, _expiration); + } + + function withdrawLink() public { + LinkTokenInterface _link = LinkTokenInterface(_chainlinkTokenAddress()); + require(_link.transfer(msg.sender, _link.balanceOf(address(this))), "Unable to transfer"); + } + + function addExternalRequest(address _oracle, bytes32 _requestId) external { + _addChainlinkExternalRequest(_oracle, _requestId); + } + + function fulfill(bytes32 _requestId, bytes32 _price) public recordChainlinkFulfillment(_requestId) { + emit RequestFulfilled(_requestId, _price); + currentPrice = _price; + } +} diff --git a/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/EmptyOracle.sol b/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/EmptyOracle.sol new file mode 100644 index 00000000..2abe3931 --- /dev/null +++ b/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/EmptyOracle.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {ChainlinkRequestInterface} from "../../../../interfaces/ChainlinkRequestInterface.sol"; +import {OracleInterface} from "../../../../interfaces/OracleInterface.sol"; + +/* solhint-disable no-empty-blocks */ +contract EmptyOracle is ChainlinkRequestInterface, OracleInterface { + function cancelOracleRequest(bytes32, uint256, bytes4, uint256) external override {} + function fulfillOracleRequest(bytes32, uint256, address, bytes4, uint256, bytes32) external override returns (bool) {} + function getAuthorizationStatus(address) external pure returns (bool) { + return false; + } + function onTokenTransfer(address, uint256, bytes calldata) external pure {} + function oracleRequest( + address, + uint256, + bytes32, + address, + bytes4, + uint256, + uint256, + bytes calldata + ) external override {} + function setFulfillmentPermission(address, bool) external {} + function withdraw(address, uint256) external override {} + function withdrawable() external view override returns (uint256) {} +} diff --git a/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/GasGuzzlingConsumer.sol b/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/GasGuzzlingConsumer.sol new file mode 100644 index 00000000..54ff0e30 --- /dev/null +++ b/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/GasGuzzlingConsumer.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Consumer} from "./Consumer.sol"; +import {Chainlink} from "../../../../Chainlink.sol"; + +contract GasGuzzlingConsumer is Consumer { + using Chainlink for Chainlink.Request; + + constructor(address _link, address _oracle, bytes32 _specId) { + _setChainlinkToken(_link); + _setChainlinkOracle(_oracle); + s_specId = _specId; + } + + function gassyRequestEthereumPrice(uint256 _payment) public { + Chainlink.Request memory req = _buildChainlinkRequest(s_specId, address(this), this.gassyFulfill.selector); + req._add("get", "https://min-api.cryptocompare.com/data/price?fsym=ETH&tsyms=USD,EUR,JPY"); + string[] memory path = new string[](1); + path[0] = "USD"; + req._addStringArray("path", path); + _sendChainlinkRequest(req, _payment); + } + + function gassyFulfill(bytes32 _requestId, bytes32) public recordChainlinkFulfillment(_requestId) { + while (true) {} + } + + function gassyMultiWordRequest(uint256 _payment) public { + Chainlink.Request memory req = _buildChainlinkRequest(s_specId, address(this), this.gassyMultiWordFulfill.selector); + req._add("get", "https://min-api.cryptocompare.com/data/price?fsym=ETH&tsyms=USD,EUR,JPY"); + string[] memory path = new string[](1); + path[0] = "USD"; + req._addStringArray("path", path); + _sendChainlinkRequest(req, _payment); + } + + function gassyMultiWordFulfill(bytes32 _requestId, bytes memory) public recordChainlinkFulfillment(_requestId) { + while (true) {} + } +} diff --git a/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/GetterSetter.sol b/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/GetterSetter.sol new file mode 100644 index 00000000..494da582 --- /dev/null +++ b/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/GetterSetter.sol @@ -0,0 +1,46 @@ +pragma solidity ^0.8.0; + +// GetterSetter is a contract to aid debugging and testing during development. +// solhint-disable +contract GetterSetter { + bytes32 public getBytes32; + uint256 public getUint256; + bytes32 public requestId; + bytes public getBytes; + + event SetBytes32(address indexed from, bytes32 indexed value); + event SetUint256(address indexed from, uint256 indexed value); + event SetBytes(address indexed from, bytes value); + + event Output(bytes32 b32, uint256 u256, bytes32 b322); + + function setBytes32(bytes32 _value) public { + getBytes32 = _value; + emit SetBytes32(msg.sender, _value); + } + + function requestedBytes32(bytes32 _requestId, bytes32 _value) public { + requestId = _requestId; + setBytes32(_value); + } + + function setBytes(bytes memory _value) public { + getBytes = _value; + emit SetBytes(msg.sender, _value); + } + + function requestedBytes(bytes32 _requestId, bytes memory _value) public { + requestId = _requestId; + setBytes(_value); + } + + function setUint256(uint256 _value) public { + getUint256 = _value; + emit SetUint256(msg.sender, _value); + } + + function requestedUint256(bytes32 _requestId, uint256 _value) public { + requestId = _requestId; + setUint256(_value); + } +} diff --git a/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/MaliciousChainlink.sol b/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/MaliciousChainlink.sol new file mode 100644 index 00000000..5cc343aa --- /dev/null +++ b/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/MaliciousChainlink.sol @@ -0,0 +1,67 @@ +pragma solidity ^0.8.0; + +import {CBORChainlink as CBOR_Chainlink} from "../../../../vendor/CBORChainlink.sol"; +import {BufferChainlink as Buffer_Chainlink} from "../../../../vendor/BufferChainlink.sol"; + +// solhint-disable +library MaliciousChainlink { + using CBOR_Chainlink for Buffer_Chainlink.buffer; + + struct Request { + bytes32 specId; + address callbackAddress; + bytes4 callbackFunctionId; + uint256 nonce; + Buffer_Chainlink.buffer buf; + } + + struct WithdrawRequest { + bytes32 specId; + address callbackAddress; + bytes4 callbackFunctionId; + uint256 nonce; + Buffer_Chainlink.buffer buf; + } + + function initializeWithdraw( + WithdrawRequest memory self, + bytes32 _specId, + address _callbackAddress, + bytes4 _callbackFunction + ) internal pure returns (MaliciousChainlink.WithdrawRequest memory) { + Buffer_Chainlink.init(self.buf, 128); + self.specId = _specId; + self.callbackAddress = _callbackAddress; + self.callbackFunctionId = _callbackFunction; + return self; + } + + function add(Request memory self, string memory _key, string memory _value) internal pure { + self.buf.encodeString(_key); + self.buf.encodeString(_value); + } + + function addBytes(Request memory self, string memory _key, bytes memory _value) internal pure { + self.buf.encodeString(_key); + self.buf.encodeBytes(_value); + } + + function addInt(Request memory self, string memory _key, int256 _value) internal pure { + self.buf.encodeString(_key); + self.buf.encodeInt(_value); + } + + function addUint(Request memory self, string memory _key, uint256 _value) internal pure { + self.buf.encodeString(_key); + self.buf.encodeUInt(_value); + } + + function addStringArray(Request memory self, string memory _key, string[] memory _values) internal pure { + self.buf.encodeString(_key); + self.buf.startArray(); + for (uint256 i = 0; i < _values.length; i++) { + self.buf.encodeString(_values[i]); + } + self.buf.endSequence(); + } +} diff --git a/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/MaliciousChainlinked.sol b/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/MaliciousChainlinked.sol new file mode 100644 index 00000000..722fbdd5 --- /dev/null +++ b/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/MaliciousChainlinked.sol @@ -0,0 +1,116 @@ +pragma solidity ^0.8.0; + +import {MaliciousChainlink} from "./MaliciousChainlink.sol"; +import {Chainlinked, Chainlink} from "./Chainlinked.sol"; +import {LinkTokenInterface} from "../../../../shared/interfaces/LinkTokenInterface.sol"; + +// solhint-disable +contract MaliciousChainlinked is Chainlinked { + using MaliciousChainlink for MaliciousChainlink.Request; + using MaliciousChainlink for MaliciousChainlink.WithdrawRequest; + using Chainlink for Chainlink.Request; + + uint256 private maliciousRequests = 1; + mapping(bytes32 => address) private maliciousPendingRequests; + + function newWithdrawRequest( + bytes32 _specId, + address _callbackAddress, + bytes4 _callbackFunction + ) internal pure returns (MaliciousChainlink.WithdrawRequest memory) { + MaliciousChainlink.WithdrawRequest memory req; + return req.initializeWithdraw(_specId, _callbackAddress, _callbackFunction); + } + + function chainlinkTargetRequest( + address _target, + Chainlink.Request memory _req, + uint256 _amount + ) internal returns (bytes32 requestId) { + requestId = keccak256(abi.encodePacked(_target, maliciousRequests)); + _req.nonce = maliciousRequests; + maliciousPendingRequests[requestId] = oracleAddress(); + emit ChainlinkRequested(requestId); + LinkTokenInterface link = LinkTokenInterface(chainlinkToken()); + require( + link.transferAndCall(oracleAddress(), _amount, encodeTargetRequest(_req)), + "Unable to transferAndCall to oracle" + ); + maliciousRequests += 1; + + return requestId; + } + + function chainlinkPriceRequest(Chainlink.Request memory _req, uint256 _amount) internal returns (bytes32 requestId) { + requestId = keccak256(abi.encodePacked(this, maliciousRequests)); + _req.nonce = maliciousRequests; + maliciousPendingRequests[requestId] = oracleAddress(); + emit ChainlinkRequested(requestId); + LinkTokenInterface link = LinkTokenInterface(chainlinkToken()); + require( + link.transferAndCall(oracleAddress(), _amount, encodePriceRequest(_req)), + "Unable to transferAndCall to oracle" + ); + maliciousRequests += 1; + + return requestId; + } + + function chainlinkWithdrawRequest( + MaliciousChainlink.WithdrawRequest memory _req, + uint256 _wei + ) internal returns (bytes32 requestId) { + requestId = keccak256(abi.encodePacked(this, maliciousRequests)); + _req.nonce = maliciousRequests; + maliciousPendingRequests[requestId] = oracleAddress(); + emit ChainlinkRequested(requestId); + LinkTokenInterface link = LinkTokenInterface(chainlinkToken()); + require( + link.transferAndCall(oracleAddress(), _wei, encodeWithdrawRequest(_req)), + "Unable to transferAndCall to oracle" + ); + maliciousRequests += 1; + return requestId; + } + + function encodeWithdrawRequest(MaliciousChainlink.WithdrawRequest memory _req) internal pure returns (bytes memory) { + return + abi.encodeWithSelector( + bytes4(keccak256("withdraw(address,uint256)")), + _req.callbackAddress, + _req.callbackFunctionId, + _req.nonce, + _req.buf.buf + ); + } + + function encodeTargetRequest(Chainlink.Request memory _req) internal pure returns (bytes memory) { + return + abi.encodeWithSelector( + bytes4(keccak256("oracleRequest(address,uint256,bytes32,address,bytes4,uint256,uint256,bytes)")), + 0, // overridden by onTokenTransfer + 0, // overridden by onTokenTransfer + _req.id, + _req.callbackAddress, + _req.callbackFunctionId, + _req.nonce, + 1, + _req.buf.buf + ); + } + + function encodePriceRequest(Chainlink.Request memory _req) internal pure returns (bytes memory) { + return + abi.encodeWithSelector( + bytes4(keccak256("oracleRequest(address,uint256,bytes32,address,bytes4,uint256,uint256,bytes)")), + 0, // overridden by onTokenTransfer + 2000000000000000000, // overridden by onTokenTransfer + _req.id, + _req.callbackAddress, + _req.callbackFunctionId, + _req.nonce, + 1, + _req.buf.buf + ); + } +} diff --git a/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/MaliciousConsumer.sol b/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/MaliciousConsumer.sol new file mode 100644 index 00000000..003e6288 --- /dev/null +++ b/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/MaliciousConsumer.sol @@ -0,0 +1,49 @@ +pragma solidity ^0.8.0; + +import {Chainlinked, Chainlink} from "./Chainlinked.sol"; + +// solhint-disable +contract MaliciousConsumer is Chainlinked { + uint256 private constant ORACLE_PAYMENT = 1 ether; + uint256 private expiration; + + constructor(address _link, address _oracle) public payable { + setLinkToken(_link); + setOracle(_oracle); + } + + fallback() external payable {} // solhint-disable-line no-empty-blocks + + function requestData(bytes32 _id, bytes memory _callbackFunc) public { + Chainlink.Request memory req = newRequest(_id, address(this), bytes4(keccak256(_callbackFunc))); + expiration = block.timestamp + 5 minutes; + chainlinkRequest(req, ORACLE_PAYMENT); + } + + function assertFail(bytes32, bytes32) public pure { + assert(1 == 2); + } + + function cancelRequestOnFulfill(bytes32 _requestId, bytes32) public { + _cancelChainlinkRequest(_requestId, ORACLE_PAYMENT, this.cancelRequestOnFulfill.selector, expiration); + } + + function remove() public { + selfdestruct(payable(address(0))); + } + + function stealEthCall(bytes32 _requestId, bytes32) public recordChainlinkFulfillment(_requestId) { + (bool success, ) = address(this).call{value: 100}(""); + require(success, "Call failed"); + } + + function stealEthSend(bytes32 _requestId, bytes32) public recordChainlinkFulfillment(_requestId) { + require(payable(address(this)).send(100), "Send failed"); + } + + function stealEthTransfer(bytes32 _requestId, bytes32) public recordChainlinkFulfillment(_requestId) { + payable(address(this)).transfer(100); + } + + function doesNothing(bytes32, bytes32) public pure {} // solhint-disable-line no-empty-blocks +} diff --git a/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/MaliciousMultiWordConsumer.sol b/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/MaliciousMultiWordConsumer.sol new file mode 100644 index 00000000..272361f2 --- /dev/null +++ b/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/MaliciousMultiWordConsumer.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {ChainlinkClient} from "../../../../ChainlinkClient.sol"; +import {Chainlink} from "../../../../Chainlink.sol"; + +contract MaliciousMultiWordConsumer is ChainlinkClient { + uint256 private constant ORACLE_PAYMENT = 1 ether; + uint256 private s_expiration; + + constructor(address _link, address _oracle) public payable { + _setChainlinkToken(_link); + _setChainlinkOracle(_oracle); + } + + receive() external payable {} // solhint-disable-line no-empty-blocks + + function requestData(bytes32 _id, bytes memory _callbackFunc) public { + Chainlink.Request memory req = _buildChainlinkRequest(_id, address(this), bytes4(keccak256(_callbackFunc))); + s_expiration = block.timestamp + 5 minutes; // solhint-disable-line not-rely-on-time + _sendChainlinkRequest(req, ORACLE_PAYMENT); + } + + function assertFail(bytes32, bytes memory) public pure { + assert(1 == 2); + } + + function cancelRequestOnFulfill(bytes32 _requestId, bytes memory) public { + _cancelChainlinkRequest(_requestId, ORACLE_PAYMENT, this.cancelRequestOnFulfill.selector, s_expiration); + } + + function remove() public { + selfdestruct(payable(address(0))); + } + + function stealEthCall(bytes32 _requestId, bytes memory) public recordChainlinkFulfillment(_requestId) { + (bool success, ) = address(this).call{value: 100}(""); // solhint-disable-line avoid-call-value + require(success, "Call failed"); + } + + function stealEthSend(bytes32 _requestId, bytes memory) public recordChainlinkFulfillment(_requestId) { + // solhint-disable-next-line check-send-result + bool success = payable(address(this)).send(100); // solhint-disable-line multiple-sends + require(success, "Send failed"); + } + + function stealEthTransfer(bytes32 _requestId, bytes memory) public recordChainlinkFulfillment(_requestId) { + payable(address(this)).transfer(100); + } + + function doesNothing(bytes32, bytes memory) public pure {} // solhint-disable-line no-empty-blocks +} diff --git a/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/MaliciousRequester.sol b/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/MaliciousRequester.sol new file mode 100644 index 00000000..9b196537 --- /dev/null +++ b/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/MaliciousRequester.sol @@ -0,0 +1,52 @@ +pragma solidity ^0.8.0; + +import {MaliciousChainlink} from "./MaliciousChainlink.sol"; +import {MaliciousChainlinked, Chainlink} from "./MaliciousChainlinked.sol"; +import {ChainlinkRequestInterface} from "../../../../interfaces/ChainlinkRequestInterface.sol"; + +contract MaliciousRequester is MaliciousChainlinked { + uint256 private constant ORACLE_PAYMENT = 1 ether; + uint256 private s_expiration; + + constructor(address _link, address _oracle) { + setLinkToken(_link); + setOracle(_oracle); + } + + function maliciousWithdraw() public { + MaliciousChainlink.WithdrawRequest memory req = newWithdrawRequest( + "specId", + address(this), + this.doesNothing.selector + ); + chainlinkWithdrawRequest(req, ORACLE_PAYMENT); + } + + function request(bytes32 _id, address _target, bytes memory _callbackFunc) public returns (bytes32 requestId) { + Chainlink.Request memory req = newRequest(_id, _target, bytes4(keccak256(_callbackFunc))); + s_expiration = block.timestamp + 5 minutes; // solhint-disable-line not-rely-on-time + return chainlinkRequest(req, ORACLE_PAYMENT); + } + + function maliciousPrice(bytes32 _id) public returns (bytes32 requestId) { + Chainlink.Request memory req = newRequest(_id, address(this), this.doesNothing.selector); + return chainlinkPriceRequest(req, ORACLE_PAYMENT); + } + + function maliciousTargetConsumer(address _target) public returns (bytes32 requestId) { + Chainlink.Request memory req = newRequest("specId", _target, bytes4(keccak256("fulfill(bytes32,bytes32)"))); + return chainlinkTargetRequest(_target, req, ORACLE_PAYMENT); + } + + function maliciousRequestCancel(bytes32 _id, bytes memory _callbackFunc) public { + ChainlinkRequestInterface oracle = ChainlinkRequestInterface(oracleAddress()); + oracle.cancelOracleRequest( + request(_id, address(this), _callbackFunc), + ORACLE_PAYMENT, + this.maliciousRequestCancel.selector, + s_expiration + ); + } + + function doesNothing(bytes32, bytes32) public pure {} // solhint-disable-line no-empty-blocks +} diff --git a/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/MultiWordConsumer.sol b/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/MultiWordConsumer.sol new file mode 100644 index 00000000..ce2bf190 --- /dev/null +++ b/contracts/src/v0.8/operatorforwarder/dev/test/testhelpers/MultiWordConsumer.sol @@ -0,0 +1,128 @@ +pragma solidity ^0.8.0; + +import {ChainlinkClient, ChainlinkRequestInterface, LinkTokenInterface} from "../../../../ChainlinkClient.sol"; +import {Chainlink} from "../../../../Chainlink.sol"; + +contract MultiWordConsumer is ChainlinkClient { + using Chainlink for Chainlink.Request; + + bytes32 internal s_specId; + bytes public currentPrice; + + bytes32 public usd; + bytes32 public eur; + bytes32 public jpy; + + uint256 public usdInt; + uint256 public eurInt; + uint256 public jpyInt; + + event RequestFulfilled( + bytes32 indexed requestId, // User-defined ID + bytes indexed price + ); + + event RequestMultipleFulfilled(bytes32 indexed requestId, bytes32 indexed usd, bytes32 indexed eur, bytes32 jpy); + + event RequestMultipleFulfilledWithCustomURLs( + bytes32 indexed requestId, + uint256 indexed usd, + uint256 indexed eur, + uint256 jpy + ); + + constructor(address _link, address _oracle, bytes32 _specId) { + _setChainlinkToken(_link); + _setChainlinkOracle(_oracle); + s_specId = _specId; + } + + function setSpecID(bytes32 _specId) public { + s_specId = _specId; + } + + function requestEthereumPrice(string memory, uint256 _payment) public { + Chainlink.Request memory req = _buildOperatorRequest(s_specId, this.fulfillBytes.selector); + _sendOperatorRequest(req, _payment); + } + + function requestMultipleParameters(string memory, uint256 _payment) public { + Chainlink.Request memory req = _buildOperatorRequest(s_specId, this.fulfillMultipleParameters.selector); + _sendOperatorRequest(req, _payment); + } + + function requestMultipleParametersWithCustomURLs( + string memory _urlUSD, + string memory _pathUSD, + string memory _urlEUR, + string memory _pathEUR, + string memory _urlJPY, + string memory _pathJPY, + uint256 _payment + ) public { + Chainlink.Request memory req = _buildOperatorRequest( + s_specId, + this.fulfillMultipleParametersWithCustomURLs.selector + ); + req._add("urlUSD", _urlUSD); + req._add("pathUSD", _pathUSD); + req._add("urlEUR", _urlEUR); + req._add("pathEUR", _pathEUR); + req._add("urlJPY", _urlJPY); + req._add("pathJPY", _pathJPY); + _sendOperatorRequest(req, _payment); + } + + function cancelRequest( + address _oracle, + bytes32 _requestId, + uint256 _payment, + bytes4 _callbackFunctionId, + uint256 _expiration + ) public { + ChainlinkRequestInterface requested = ChainlinkRequestInterface(_oracle); + requested.cancelOracleRequest(_requestId, _payment, _callbackFunctionId, _expiration); + } + + function withdrawLink() public { + LinkTokenInterface _link = LinkTokenInterface(_chainlinkTokenAddress()); + require(_link.transfer(msg.sender, _link.balanceOf(address(this))), "Unable to transfer"); + } + + function addExternalRequest(address _oracle, bytes32 _requestId) external { + _addChainlinkExternalRequest(_oracle, _requestId); + } + + function fulfillMultipleParameters( + bytes32 _requestId, + bytes32 _usd, + bytes32 _eur, + bytes32 _jpy + ) public recordChainlinkFulfillment(_requestId) { + emit RequestMultipleFulfilled(_requestId, _usd, _eur, _jpy); + usd = _usd; + eur = _eur; + jpy = _jpy; + } + + function fulfillMultipleParametersWithCustomURLs( + bytes32 _requestId, + uint256 _usd, + uint256 _eur, + uint256 _jpy + ) public recordChainlinkFulfillment(_requestId) { + emit RequestMultipleFulfilledWithCustomURLs(_requestId, _usd, _eur, _jpy); + usdInt = _usd; + eurInt = _eur; + jpyInt = _jpy; + } + + function fulfillBytes(bytes32 _requestId, bytes memory _price) public recordChainlinkFulfillment(_requestId) { + emit RequestFulfilled(_requestId, _price); + currentPrice = _price; + } + + function publicGetNextRequestCount() external view returns (uint256) { + return _getNextRequestCount(); + } +} diff --git a/contracts/src/v0.8/shared/access/ConfirmedOwnerWithProposal.sol b/contracts/src/v0.8/shared/access/ConfirmedOwnerWithProposal.sol index 7b684187..2a6dd94e 100644 --- a/contracts/src/v0.8/shared/access/ConfirmedOwnerWithProposal.sol +++ b/contracts/src/v0.8/shared/access/ConfirmedOwnerWithProposal.sol @@ -13,7 +13,7 @@ contract ConfirmedOwnerWithProposal is IOwnable { event OwnershipTransferred(address indexed from, address indexed to); constructor(address newOwner, address pendingOwner) { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(newOwner != address(0), "Cannot set owner to zero"); s_owner = newOwner; @@ -29,7 +29,7 @@ contract ConfirmedOwnerWithProposal is IOwnable { /// @notice Allows an ownership transfer to be completed by the recipient. function acceptOwnership() external override { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(msg.sender == s_pendingOwner, "Must be proposed owner"); address oldOwner = s_owner; @@ -46,7 +46,7 @@ contract ConfirmedOwnerWithProposal is IOwnable { /// @notice validate, transfer ownership, and emit relevant events function _transferOwnership(address to) private { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(to != msg.sender, "Cannot transfer to self"); s_pendingOwner = to; @@ -56,7 +56,7 @@ contract ConfirmedOwnerWithProposal is IOwnable { /// @notice validate access function _validateOwnership() internal view { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(msg.sender == s_owner, "Only callable by owner"); } diff --git a/contracts/src/v0.8/shared/access/SimpleWriteAccessController.sol b/contracts/src/v0.8/shared/access/SimpleWriteAccessController.sol index b431331b..5a53bdf6 100644 --- a/contracts/src/v0.8/shared/access/SimpleWriteAccessController.sol +++ b/contracts/src/v0.8/shared/access/SimpleWriteAccessController.sol @@ -66,7 +66,7 @@ contract SimpleWriteAccessController is AccessControllerInterface, ConfirmedOwne /// @dev reverts if the caller does not have access modifier checkAccess() { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(hasAccess(msg.sender, msg.data), "No access"); _; } diff --git a/contracts/src/v0.8/shared/mocks/WERC20Mock.sol b/contracts/src/v0.8/shared/mocks/WERC20Mock.sol index cee7fa7f..6155b38b 100644 --- a/contracts/src/v0.8/shared/mocks/WERC20Mock.sol +++ b/contracts/src/v0.8/shared/mocks/WERC20Mock.sol @@ -19,7 +19,7 @@ contract WERC20Mock is ERC20 { } function withdraw(uint256 wad) public { - // solhint-disable-next-line custom-errors, reason-string + // solhint-disable-next-line gas-custom-errors, reason-string require(balanceOf(msg.sender) >= wad); _burn(msg.sender, wad); payable(msg.sender).transfer(wad); diff --git a/contracts/src/v0.8/shared/ocr2/OCR2Base.sol b/contracts/src/v0.8/shared/ocr2/OCR2Base.sol index baedac77..7884d481 100644 --- a/contracts/src/v0.8/shared/ocr2/OCR2Base.sol +++ b/contracts/src/v0.8/shared/ocr2/OCR2Base.sol @@ -12,7 +12,7 @@ import {OCR2Abstract} from "./OCR2Abstract.sol"; /// However, for actual production contracts, it is expected that most of the logic of this contract /// will be folded directly into the application contract. Inheritance prevents us from doing lots /// of juicy storage layout optimizations, leading to a substantial increase in gas cost. -// solhint-disable custom-errors +// solhint-disable gas-custom-errors abstract contract OCR2Base is OwnerIsCreator, OCR2Abstract { error ReportInvalid(); diff --git a/contracts/src/v0.8/shared/test/helpers/ChainReaderTestContract.sol b/contracts/src/v0.8/shared/test/helpers/ChainReaderTestContract.sol index 050f5fb3..217901c1 100644 --- a/contracts/src/v0.8/shared/test/helpers/ChainReaderTestContract.sol +++ b/contracts/src/v0.8/shared/test/helpers/ChainReaderTestContract.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8; +pragma solidity ^0.8.0; struct TestStruct { int32 Field; diff --git a/contracts/src/v0.8/shared/test/helpers/LinkTokenTestHelper.sol b/contracts/src/v0.8/shared/test/helpers/LinkTokenTestHelper.sol new file mode 100644 index 00000000..f2b2f19c --- /dev/null +++ b/contracts/src/v0.8/shared/test/helpers/LinkTokenTestHelper.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {LinkToken} from "../../token/ERC677/LinkToken.sol"; + +// This contract exists to mirror the functionality of the old token, which +// always deployed with 1b tokens sent to the deployer. +contract LinkTokenTestHelper is LinkToken { + constructor() { + _mint(msg.sender, 1e27); + } +} diff --git a/contracts/src/v0.8/shared/test/testhelpers/ConfirmedOwnerTestHelper.sol b/contracts/src/v0.8/shared/test/testhelpers/ConfirmedOwnerTestHelper.sol new file mode 100644 index 00000000..47ecbb0c --- /dev/null +++ b/contracts/src/v0.8/shared/test/testhelpers/ConfirmedOwnerTestHelper.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {ConfirmedOwner} from "../../access/ConfirmedOwner.sol"; + +contract ConfirmedOwnerTestHelper is ConfirmedOwner { + event Here(); + + constructor() ConfirmedOwner(msg.sender) {} + + function modifierOnlyOwner() public onlyOwner { + emit Here(); + } +} diff --git a/contracts/src/v0.8/shared/token/ERC677/ERC677.sol b/contracts/src/v0.8/shared/token/ERC677/ERC677.sol index aa75a117..5876ddfc 100644 --- a/contracts/src/v0.8/shared/token/ERC677/ERC677.sol +++ b/contracts/src/v0.8/shared/token/ERC677/ERC677.sol @@ -4,19 +4,16 @@ pragma solidity ^0.8.0; import {IERC677} from "./IERC677.sol"; import {IERC677Receiver} from "../../interfaces/IERC677Receiver.sol"; -import {Address} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/Address.sol"; import {ERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/ERC20.sol"; contract ERC677 is IERC677, ERC20 { - using Address for address; - constructor(string memory name, string memory symbol) ERC20(name, symbol) {} /// @inheritdoc IERC677 function transferAndCall(address to, uint256 amount, bytes memory data) public returns (bool success) { super.transfer(to, amount); emit Transfer(msg.sender, to, amount, data); - if (to.isContract()) { + if (to.code.length > 0) { IERC677Receiver(to).onTokenTransfer(msg.sender, amount, data); } return true; diff --git a/contracts/src/v0.8/transmission/dev/ERC-4337/Paymaster.sol b/contracts/src/v0.8/transmission/dev/ERC-4337/Paymaster.sol index 38b6fb57..932d3500 100644 --- a/contracts/src/v0.8/transmission/dev/ERC-4337/Paymaster.sol +++ b/contracts/src/v0.8/transmission/dev/ERC-4337/Paymaster.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.15; +pragma solidity ^0.8.19; import {IPaymaster} from "../../../vendor/entrypoint/interfaces/IPaymaster.sol"; import {SCALibrary} from "./SCALibrary.sol"; diff --git a/contracts/src/v0.8/transmission/dev/ERC-4337/SCA.sol b/contracts/src/v0.8/transmission/dev/ERC-4337/SCA.sol index 6a11eecf..589c55f5 100644 --- a/contracts/src/v0.8/transmission/dev/ERC-4337/SCA.sol +++ b/contracts/src/v0.8/transmission/dev/ERC-4337/SCA.sol @@ -1,6 +1,5 @@ // SPDX-License-Identifier: MIT -/// TODO: decide on a compiler version. Must not be dynamic, and must be > 0.8.12. -pragma solidity 0.8.15; +pragma solidity 0.8.19; import {IAccount} from "../../../vendor/entrypoint/interfaces/IAccount.sol"; import {SCALibrary} from "./SCALibrary.sol"; diff --git a/contracts/src/v0.8/transmission/dev/ERC-4337/SCALibrary.sol b/contracts/src/v0.8/transmission/dev/ERC-4337/SCALibrary.sol index 35d666a2..095a3428 100644 --- a/contracts/src/v0.8/transmission/dev/ERC-4337/SCALibrary.sol +++ b/contracts/src/v0.8/transmission/dev/ERC-4337/SCALibrary.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.15; +pragma solidity ^0.8.19; library SCALibrary { // keccak256("EIP712Domain(uint256 chainId, address verifyingContract)"); diff --git a/contracts/src/v0.8/transmission/dev/ERC-4337/SmartContractAccountFactory.sol b/contracts/src/v0.8/transmission/dev/ERC-4337/SmartContractAccountFactory.sol index bb0f2dbd..f27c8e15 100644 --- a/contracts/src/v0.8/transmission/dev/ERC-4337/SmartContractAccountFactory.sol +++ b/contracts/src/v0.8/transmission/dev/ERC-4337/SmartContractAccountFactory.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.15; +pragma solidity ^0.8.19; contract SmartContractAccountFactory { event ContractCreated(address scaAddress); diff --git a/contracts/src/v0.8/transmission/dev/testhelpers/Greeter.sol b/contracts/src/v0.8/transmission/dev/testhelpers/Greeter.sol index 92e50b80..5851c865 100644 --- a/contracts/src/v0.8/transmission/dev/testhelpers/Greeter.sol +++ b/contracts/src/v0.8/transmission/dev/testhelpers/Greeter.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.15; +pragma solidity ^0.8.19; /// @dev Ownerless greeter contract. contract Greeter { diff --git a/contracts/src/v0.8/transmission/dev/testhelpers/SmartContractAccountHelper.sol b/contracts/src/v0.8/transmission/dev/testhelpers/SmartContractAccountHelper.sol index 014f296f..b080484d 100644 --- a/contracts/src/v0.8/transmission/dev/testhelpers/SmartContractAccountHelper.sol +++ b/contracts/src/v0.8/transmission/dev/testhelpers/SmartContractAccountHelper.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.15; +pragma solidity ^0.8.19; import {SCA} from "../ERC-4337/SCA.sol"; import {SmartContractAccountFactory} from "../ERC-4337/SmartContractAccountFactory.sol"; diff --git a/contracts/src/v0.8/transmission/test/BaseTest.t.sol b/contracts/src/v0.8/transmission/test/BaseTest.t.sol new file mode 100644 index 00000000..4da698d1 --- /dev/null +++ b/contracts/src/v0.8/transmission/test/BaseTest.t.sol @@ -0,0 +1,17 @@ +pragma solidity ^0.8.0; + +import {Test} from "forge-std/Test.sol"; + +contract BaseTest is Test { + bool private s_baseTestInitialized; + address internal constant OWNER = 0x00007e64E1fB0C487F25dd6D3601ff6aF8d32e4e; + + function setUp() public virtual { + // BaseTest.setUp is often called multiple times from tests' setUp due to inheritance. + if (s_baseTestInitialized) return; + s_baseTestInitialized = true; + + // Set msg.sender to OWNER until changePrank or stopPrank is called + vm.startPrank(OWNER); + } +} diff --git a/contracts/src/v0.8/transmission/test/EIP_712_1014_4337.t.sol b/contracts/src/v0.8/transmission/test/EIP_712_1014_4337.t.sol new file mode 100644 index 00000000..fdfe190d --- /dev/null +++ b/contracts/src/v0.8/transmission/test/EIP_712_1014_4337.t.sol @@ -0,0 +1,365 @@ +pragma solidity 0.8.19; + +import "../../shared/interfaces/LinkTokenInterface.sol"; + +import "./BaseTest.t.sol"; +import "../dev/ERC-4337/SmartContractAccountFactory.sol"; +import "../dev/testhelpers/SmartContractAccountHelper.sol"; +import "../dev/ERC-4337/SCA.sol"; +import "../dev/testhelpers/Greeter.sol"; +import "../dev/ERC-4337/Paymaster.sol"; +import "../../transmission/dev/ERC-4337/SCALibrary.sol"; +import "../../mocks/MockLinkToken.sol"; +import "../../tests/MockV3Aggregator.sol"; +import "../../vrf/mocks/VRFCoordinatorMock.sol"; +import "../../vrf/testhelpers/VRFConsumer.sol"; + +import "../../vendor/entrypoint/interfaces/UserOperation.sol"; +import "../../vendor/entrypoint/core/EntryPoint.sol"; +import "../../vendor/entrypoint/interfaces/IEntryPoint.sol"; + +/*--------------------------------------------------------------------------------------------------------------------+ +| EIP 712 + 1014 + 4337 | +| ________________ | +| This implementation allows for meta-transactions to be signed by end-users and posted on-chain by executors. It | +| utilizes the following components: | +| - EIP-712: The method by which meta-transactions are authorized. | +| - EIP-1014: The method by which the Smart Contract Account is generated. | +| - EIP-4337: The method by which meta-transactions are executed. | +| | +| The below tests illustrate end-user flows for interacting with this meta-transaction system. For users with | +| existing Smart Contract Accounts (SCAs), they simply sign off on the operation, after which the executor | +| invokes the EntryPoint that authorizes the operation on the end-user's SCA, and then execute the transaction | +| as the SCA. For users without existing SCAs, EIP-1014 ensures that the address of an SCA can be known in advance, | +| so users can sign-off on transactions that will be executed by a not-yet-deployed SCA. The EntryPoint contract | +| takes advantage of this functionality and allows for the SCA to be created in the same user operation that invokes | +| it, and the end-user signs off on this creation-and-execution flow. After the initial creation-and-execution, the | +| SCA is reused for future transactions. | +| | +| End-Dapps/protocols do not need to be EIP-2771-compliant or accommodate any other kind of transaction standard. | +| They can be interacted with out-of-the-box through the SCA, which acts in place of the user's EOA as their | +| immutable identity. | +| | +-+---------------------------------------------------------------------------------------------------------------------*/ + +contract EIP_712_1014_4337 is BaseTest { + event RandomnessRequest(address indexed sender, bytes32 indexed keyHash, uint256 indexed seed, uint256 fee); + + address internal constant LINK_WHALE = 0xD883a6A1C22fC4AbFE938a5aDF9B2Cc31b1BF18B; + address internal ENTRY_POINT; + + Greeter greeter; + EntryPoint entryPoint; + MockV3Aggregator linkEthFeed; + + // Randomly generated private/public key pair. + uint256 END_USER_PKEY = uint256(bytes32(hex"99d518dbfea4b4ec301390f7e26d53d711fa1ca0c1a6e4cbed89617d4c578a8e")); + address END_USER = 0xB6708257D4E1bf0b8C144793fc2Ff3193C737ed1; + + function setUp() public override { + BaseTest.setUp(); + // Fund user accounts; + vm.deal(END_USER, 10_000 ether); + vm.deal(LINK_WHALE, 10_000 ether); + + // Impersonate a LINK whale. + changePrank(LINK_WHALE); + + // Create simple greeter contract. + greeter = new Greeter(); + assertEq("", greeter.getGreeting()); + + // Create entry point contract. + entryPoint = new EntryPoint(); + ENTRY_POINT = address(entryPoint); + + // Deploy link/eth feed. + linkEthFeed = new MockV3Aggregator(18, 5000000000000000); // .005 ETH + } + + /// @dev Test case for user that already has a Smart Contract Account. + /// @dev EntryPoint.sol should use the existing SCA to execute the meta transaction. + function testEIP712EIP4337WithExistingSmartContractAccount() public { + // Pre-calculate user smart contract account address. + SmartContractAccountFactory factory = new SmartContractAccountFactory(); + address toDeployAddress = SmartContractAccountHelper.calculateSmartContractAccountAddress( + END_USER, + ENTRY_POINT, + address(factory) + ); + + // Deploy the end-contract. + bytes32 salt = bytes32(uint256(uint160(END_USER)) << 96); + bytes memory fullInitializeCode = SmartContractAccountHelper.getSCAInitCodeWithConstructor(END_USER, ENTRY_POINT); + factory.deploySmartContractAccount(salt, fullInitializeCode); + changePrank(END_USER); + + // Ensure a correct deployment and a functioning end-contract. + uint256 contractCodeSize; + assembly { + contractCodeSize := extcodesize(toDeployAddress) + } + assertTrue(contractCodeSize > 0); + assertEq(END_USER, SCA(toDeployAddress).i_owner()); + + // Create the calldata for a setGreeting call. + string memory greeting = "hi"; + bytes memory encodedGreetingCall = bytes.concat(Greeter.setGreeting.selector, abi.encode(greeting)); // abi.encodeWithSelector equivalent + + // Produce the final full end-tx encoding, to be used as calldata in the user operation. + bytes memory fullEncoding = SmartContractAccountHelper.getFullEndTxEncoding( + address(greeter), + uint256(0), + 0, + encodedGreetingCall + ); + + // Construct the user operation. + UserOperation memory op = UserOperation({ + sender: toDeployAddress, + nonce: 0, + initCode: "", + callData: fullEncoding, + callGasLimit: 1_000_000, + verificationGasLimit: 1_000_000, + preVerificationGas: 10_000, + maxFeePerGas: 100, + maxPriorityFeePerGas: 200, + paymasterAndData: "", + signature: "" + }); + + // Sign user operation. + bytes32 userOpHash = entryPoint.getUserOpHash(op); + bytes32 fullHash = SCALibrary._getUserOpFullHash(userOpHash, toDeployAddress); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(END_USER_PKEY, fullHash); + op.signature = abi.encodePacked(r, s, v - 27); + + // Deposit funds for the transaction. + entryPoint.depositTo{value: 10 ether}(toDeployAddress); + + // Execute the user operation. + UserOperation[] memory operations = new UserOperation[](1); + operations[0] = op; + entryPoint.handleOps(operations, payable(END_USER)); + + // Assert that the greeting was set. + assertEq("hi", Greeter(greeter).getGreeting()); + assertEq(SCA(toDeployAddress).s_nonce(), uint256(1)); + } + + /// @dev Test case for fresh user, EntryPoint.sol should generate a + /// @dev Smart Contract Account for them and execute the meta transaction. + function testEIP712EIP4337AndCreateSmartContractAccount() public { + // Pre-calculate user smart contract account address. + SmartContractAccountFactory factory = new SmartContractAccountFactory(); + address toDeployAddress = SmartContractAccountHelper.calculateSmartContractAccountAddress( + END_USER, + ENTRY_POINT, + address(factory) + ); + + // Construct initCode byte array. + bytes memory fullInitializeCode = SmartContractAccountHelper.getInitCode(address(factory), END_USER, ENTRY_POINT); + + // Create the calldata for a setGreeting call. + string memory greeting = "bye"; + bytes memory encodedGreetingCall = bytes.concat(Greeter.setGreeting.selector, abi.encode(greeting)); + + // Produce the final full end-tx encoding, to be used as calldata in the user operation. + bytes memory fullEncoding = SmartContractAccountHelper.getFullEndTxEncoding( + address(greeter), + uint256(0), + 0, + encodedGreetingCall + ); + + // Construct the user operation. + UserOperation memory op = UserOperation({ + sender: toDeployAddress, + nonce: 0, + initCode: fullInitializeCode, + callData: fullEncoding, + callGasLimit: 1_000_000, + verificationGasLimit: 1_000_000, + preVerificationGas: 10_000, + maxFeePerGas: 100, + maxPriorityFeePerGas: 200, + paymasterAndData: "", + signature: "" + }); + + // Sign user operation. + bytes32 userOpHash = entryPoint.getUserOpHash(op); + bytes32 fullHash = SCALibrary._getUserOpFullHash(userOpHash, toDeployAddress); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(END_USER_PKEY, fullHash); + op.signature = abi.encodePacked(r, s, v - 27); + + // Deposit funds for the transaction. + entryPoint.depositTo{value: 10 ether}(toDeployAddress); + + // Execute the user operation. + UserOperation[] memory operations = new UserOperation[](1); + operations[0] = op; + entryPoint.handleOps(operations, payable(END_USER)); + + // Assert that the greeting was set. + assertEq("bye", Greeter(greeter).getGreeting()); + assertEq(SCA(toDeployAddress).s_nonce(), uint256(1)); + assertEq(SCA(toDeployAddress).i_owner(), END_USER); + } + + /// @dev Test case for a user executing a setGreeting with a LINK token paymaster. + function testEIP712EIP4337AndCreateSmartContractAccountWithPaymaster() public { + // Pre-calculate user smart contract account address. + SmartContractAccountFactory factory = new SmartContractAccountFactory(); + address toDeployAddress = SmartContractAccountHelper.calculateSmartContractAccountAddress( + END_USER, + ENTRY_POINT, + address(factory) + ); + + // Construct initCode byte array. + bytes memory fullInitializeCode = SmartContractAccountHelper.getInitCode(address(factory), END_USER, ENTRY_POINT); + + // Create the calldata for a setGreeting call. + string memory greeting = "good day"; + bytes memory encodedGreetingCall = bytes.concat(Greeter.setGreeting.selector, abi.encode(greeting)); + + // Produce the final full end-tx encoding, to be used as calldata in the user operation. + bytes memory fullEncoding = SmartContractAccountHelper.getFullEndTxEncoding( + address(greeter), + uint256(0), + 0, + encodedGreetingCall + ); + + // Create Link token, and deposit into paymaster. + MockLinkToken linkToken = new MockLinkToken(); + Paymaster paymaster = new Paymaster(LinkTokenInterface(address(linkToken)), linkEthFeed, ENTRY_POINT); + linkToken.transferAndCall(address(paymaster), 1000 ether, abi.encode(address(toDeployAddress))); + + // Construct the user opeartion. + UserOperation memory op = UserOperation({ + sender: toDeployAddress, + nonce: 0, + initCode: fullInitializeCode, + callData: fullEncoding, + callGasLimit: 1_000_000, + verificationGasLimit: 1_500_000, + preVerificationGas: 10_000, + maxFeePerGas: 100, + maxPriorityFeePerGas: 200, + paymasterAndData: abi.encodePacked(address(paymaster)), + signature: "" + }); + + // Sign user operation. + bytes32 userOpHash = entryPoint.getUserOpHash(op); + bytes32 fullHash = SCALibrary._getUserOpFullHash(userOpHash, toDeployAddress); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(END_USER_PKEY, fullHash); + op.signature = abi.encodePacked(r, s, v - 27); + + // Deposit funds for the transaction. + entryPoint.depositTo{value: 10 ether}(address(paymaster)); + + // Execute the user operation. + UserOperation[] memory operations = new UserOperation[](1); + operations[0] = op; + entryPoint.handleOps(operations, payable(END_USER)); + + // Assert that the greeting was set. + assertEq("good day", Greeter(greeter).getGreeting()); + assertEq(SCA(toDeployAddress).s_nonce(), uint256(1)); + } + + /// @dev Test case for a VRF Request via LINK token paymaster and an SCA. + function testEIP712EIP4337AndCreateSmartContractAccountWithPaymasterForVRFRequest() public { + // Pre-calculate user smart contract account address. + SmartContractAccountFactory factory = new SmartContractAccountFactory(); + address toDeployAddress = SmartContractAccountHelper.calculateSmartContractAccountAddress( + END_USER, + ENTRY_POINT, + address(factory) + ); + + // Construct initCode byte array. + bytes memory fullInitializeCode = SmartContractAccountHelper.getInitCode(address(factory), END_USER, ENTRY_POINT); + + // Create the calldata for a VRF request. + bytes32 keyhash = bytes32(uint256(123)); + uint256 fee = 1 ether; + bytes memory encodedVRFRequestCallData = bytes.concat( + VRFConsumer.doRequestRandomness.selector, + abi.encode(keyhash, fee) + ); + + // Create the VRF Contracts + MockLinkToken linkToken = new MockLinkToken(); + VRFCoordinatorMock vrfCoordinator = new VRFCoordinatorMock(address(linkToken)); + VRFConsumer vrfConsumer = new VRFConsumer(address(vrfCoordinator), address(linkToken)); + + // Produce the final full end-tx encoding, to be used as calldata in the user operation. + bytes memory fullEncoding = SmartContractAccountHelper.getFullEndTxEncoding( + address(vrfConsumer), // end-contract + uint256(0), // value + 0, // timeout (seconds) + encodedVRFRequestCallData + ); + + // Create Link token, and deposit into paymaster. + Paymaster paymaster = new Paymaster(LinkTokenInterface(address(linkToken)), linkEthFeed, ENTRY_POINT); + linkToken.transferAndCall(address(paymaster), 1000 ether, abi.encode(address(toDeployAddress))); + + // Construct direct funding data. + SCALibrary.DirectFundingData memory directFundingData = SCALibrary.DirectFundingData({ + recipient: address(vrfConsumer), + topupThreshold: 1, + topupAmount: 10 ether + }); + + // Construct the user operation. + UserOperation memory op = UserOperation({ + sender: toDeployAddress, + nonce: 0, + initCode: fullInitializeCode, + callData: fullEncoding, + callGasLimit: 200_000, + verificationGasLimit: 1_000_000, + preVerificationGas: 10_000, + maxFeePerGas: 10, + maxPriorityFeePerGas: 10, + paymasterAndData: abi.encodePacked(address(paymaster), uint8(0), abi.encode(directFundingData)), + signature: "" + }); + + // Sign user operation. + bytes32 fullHash = SCALibrary._getUserOpFullHash(entryPoint.getUserOpHash(op), toDeployAddress); + op.signature = getSignature(fullHash); + + // Deposit funds for the transaction. + entryPoint.depositTo{value: 10 ether}(address(paymaster)); + + // Assert correct log is emitted for the end-contract vrf request. + vm.expectEmit(true, true, true, true); + emit RandomnessRequest( + address(vrfConsumer), + keyhash, + 0, // seed - we use a zero seed + fee + ); + + // Execute the user operation. + UserOperation[] memory operations = new UserOperation[](1); + operations[0] = op; + + // Execute user operation and ensure correct outcome. + entryPoint.handleOps(operations, payable(END_USER)); + assertEq(SCA(toDeployAddress).s_nonce(), uint256(1)); + } + + function getSignature(bytes32 h) internal view returns (bytes memory) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(END_USER_PKEY, h); + return abi.encodePacked(r, s, v - 27); + } +} diff --git a/contracts/src/v0.8/vrf/BatchBlockhashStore.sol b/contracts/src/v0.8/vrf/BatchBlockhashStore.sol index e5561692..1cbf2aa0 100644 --- a/contracts/src/v0.8/vrf/BatchBlockhashStore.sol +++ b/contracts/src/v0.8/vrf/BatchBlockhashStore.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // solhint-disable-next-line one-contract-per-file -pragma solidity 0.8.6; +pragma solidity 0.8.19; import {ChainSpecificUtil} from "../ChainSpecificUtil.sol"; @@ -42,7 +42,7 @@ contract BatchBlockhashStore { * @param headers the rlp-encoded block headers of blockNumbers[i] + 1. */ function storeVerifyHeader(uint256[] memory blockNumbers, bytes[] memory headers) public { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(blockNumbers.length == headers.length, "input array arg lengths mismatch"); for (uint256 i = 0; i < blockNumbers.length; i++) { BHS.storeVerifyHeader(blockNumbers[i], headers[i]); diff --git a/contracts/src/v0.8/vrf/BatchVRFCoordinatorV2.sol b/contracts/src/v0.8/vrf/BatchVRFCoordinatorV2.sol index b35df41d..2cb6948a 100644 --- a/contracts/src/v0.8/vrf/BatchVRFCoordinatorV2.sol +++ b/contracts/src/v0.8/vrf/BatchVRFCoordinatorV2.sol @@ -26,7 +26,7 @@ contract BatchVRFCoordinatorV2 { * @param rcs the request commitments corresponding to the randomness proofs. */ function fulfillRandomWords(VRFTypes.Proof[] memory proofs, VRFTypes.RequestCommitment[] memory rcs) external { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(proofs.length == rcs.length, "input array arg lengths mismatch"); for (uint256 i = 0; i < proofs.length; i++) { try COORDINATOR.fulfillRandomWords(proofs[i], rcs[i]) returns (uint96 /* payment */) { diff --git a/contracts/src/v0.8/vrf/KeepersVRFConsumer.sol b/contracts/src/v0.8/vrf/KeepersVRFConsumer.sol index 20fd806b..161800fc 100644 --- a/contracts/src/v0.8/vrf/KeepersVRFConsumer.sol +++ b/contracts/src/v0.8/vrf/KeepersVRFConsumer.sol @@ -89,7 +89,7 @@ contract KeepersVRFConsumer is KeeperCompatibleInterface, VRFConsumerBaseV2 { function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override { // Check that the request exists. If not, revert. RequestRecord memory record = s_requests[requestId]; - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(record.requestId == requestId, "request ID not found in map"); // Update the randomness in the record, and increment the response counter. diff --git a/contracts/src/v0.8/vrf/VRF.sol b/contracts/src/v0.8/vrf/VRF.sol index f7d62a27..efa7df44 100644 --- a/contracts/src/v0.8/vrf/VRF.sol +++ b/contracts/src/v0.8/vrf/VRF.sol @@ -163,7 +163,7 @@ contract VRF { ) } if (callResult == 0) { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors revert("bigModExp failure!"); } return output[0]; @@ -189,9 +189,9 @@ contract VRF { function _isOnCurve(uint256[2] memory p) internal pure returns (bool) { // Section 2.3.6. in https://www.secg.org/sec1-v2.pdf // requires each ordinate to be in [0, ..., FIELD_SIZE-1] - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(p[0] < FIELD_SIZE, "invalid x-ordinate"); - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(p[1] < FIELD_SIZE, "invalid y-ordinate"); return _ySquared(p[0]) == mulmod(p[1], p[1], FIELD_SIZE); } @@ -268,7 +268,7 @@ contract VRF { uint256 scalar, uint256[2] memory product ) internal pure returns (bool verifies) { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(scalar != 0, "zero scalar"); // Rules out an ecrecover failure case uint256 x = multiplicand[0]; // x ordinate of multiplicand uint8 v = multiplicand[1] % 2 == 0 ? 27 : 28; // parity of y ordinate @@ -409,7 +409,7 @@ contract VRF { uint256 y; uint256 z; (x, y, z) = _projectiveECAdd(p1[0], p1[1], p2[0], p2[1]); - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(mulmod(z, invZ, FIELD_SIZE) == 1, "invZ must be inverse of z"); // Clear the z ordinate of the projective representation by dividing through // by it, to obtain the affine representation @@ -426,7 +426,7 @@ contract VRF { ) internal pure returns (bool) { // Rule out ecrecover failure modes which return address 0. unchecked { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(lcWitness != address(0), "bad witness"); uint8 v = (p[1] % 2 == 0) ? 27 : 28; // parity of y-ordinate of p // Note this cannot wrap (X - Y % X), but we use unchecked to save @@ -462,11 +462,11 @@ contract VRF { ) internal pure returns (uint256[2] memory) { unchecked { // Note we are relying on the wrap around here - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require((cp1Witness[0] % FIELD_SIZE) != (sp2Witness[0] % FIELD_SIZE), "points in sum must be distinct"); - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(_ecmulVerify(p1, c, cp1Witness), "First mul check failed"); - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(_ecmulVerify(p2, s, sp2Witness), "Second mul check failed"); return _affineECAdd(cp1Witness, sp2Witness, zInv); } @@ -518,20 +518,20 @@ contract VRF { uint256 zInv ) internal view { unchecked { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(_isOnCurve(pk), "public key is not on curve"); - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(_isOnCurve(gamma), "gamma is not on curve"); - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(_isOnCurve(cGammaWitness), "cGammaWitness is not on curve"); - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(_isOnCurve(sHashWitness), "sHashWitness is not on curve"); // Step 5. of IETF draft section 5.3 (pk corresponds to 5.3's Y, and here // we use the address of u instead of u itself. Also, here we add the // terms instead of taking the difference, and in the proof construction in // vrf.GenerateProof, we correspondingly take the difference instead of // taking the sum as they do in step 7 of section 5.1.) - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(_verifyLinearCombinationWithGenerator(c, pk, s, uWitness), "addr(c*pk+s*g)!=_uWitness"); // Step 4. of IETF draft section 5.3 (pk corresponds to Y, seed to alpha_string) uint256[2] memory hash = _hashToCurve(pk, seed); @@ -539,7 +539,7 @@ contract VRF { uint256[2] memory v = _linearCombination(c, gamma, cGammaWitness, s, hash, sHashWitness, zInv); // Steps 7. and 8. of IETF draft section 5.3 uint256 derivedC = _scalarFromCurvePoints(hash, pk, gamma, uWitness, v); - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(c == derivedC, "invalid proof"); } } diff --git a/contracts/src/v0.8/vrf/VRFConsumerBase.sol b/contracts/src/v0.8/vrf/VRFConsumerBase.sol index 7661ad40..3d73b70c 100644 --- a/contracts/src/v0.8/vrf/VRFConsumerBase.sol +++ b/contracts/src/v0.8/vrf/VRFConsumerBase.sol @@ -193,7 +193,7 @@ abstract contract VRFConsumerBase is VRFRequestIDBase { // proof. rawFulfillRandomness then calls fulfillRandomness, after validating // the origin of the call function rawFulfillRandomness(bytes32 requestId, uint256 randomness) external { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(msg.sender == vrfCoordinator, "Only VRFCoordinator can fulfill"); fulfillRandomness(requestId, randomness); } diff --git a/contracts/src/v0.8/vrf/VRFCoordinatorV2.sol b/contracts/src/v0.8/vrf/VRFCoordinatorV2.sol index 5dfb51a4..f3e569f5 100644 --- a/contracts/src/v0.8/vrf/VRFCoordinatorV2.sol +++ b/contracts/src/v0.8/vrf/VRFCoordinatorV2.sol @@ -10,8 +10,7 @@ import {IERC677Receiver} from "../shared/interfaces/IERC677Receiver.sol"; import {VRF} from "./VRF.sol"; import {ConfirmedOwner} from "../shared/access/ConfirmedOwner.sol"; import {VRFConsumerBaseV2} from "./VRFConsumerBaseV2.sol"; -import {ChainSpecificUtil} from "../ChainSpecificUtil.sol"; - +import {ChainSpecificUtil} from "../ChainSpecificUtil_v0_8_6.sol"; contract VRFCoordinatorV2 is VRF, ConfirmedOwner, TypeAndVersionInterface, VRFCoordinatorV2Interface, IERC677Receiver { // solhint-disable-next-line chainlink-solidity/prefix-immutable-variables-with-i LinkTokenInterface public immutable LINK; diff --git a/contracts/src/v0.8/vrf/VRFOwner.sol b/contracts/src/v0.8/vrf/VRFOwner.sol index 3b35eae8..366b85c4 100644 --- a/contracts/src/v0.8/vrf/VRFOwner.sol +++ b/contracts/src/v0.8/vrf/VRFOwner.sol @@ -110,7 +110,7 @@ contract VRFOwner is ConfirmedOwner, AuthorizedReceiver { event RandomWordsForced(uint256 indexed requestId, uint64 indexed subId, address indexed sender); constructor(address _vrfCoordinator) ConfirmedOwner(msg.sender) { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(_vrfCoordinator != address(0), "vrf coordinator address must be non-zero"); s_vrfCoordinator = IVRFCoordinatorV2(_vrfCoordinator); } diff --git a/contracts/src/v0.8/vrf/VRFTypes.sol b/contracts/src/v0.8/vrf/VRFTypes.sol index be26051f..d1b35a26 100644 --- a/contracts/src/v0.8/vrf/VRFTypes.sol +++ b/contracts/src/v0.8/vrf/VRFTypes.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.6; +pragma solidity ^0.8.6; /** * @title VRFTypes diff --git a/contracts/src/v0.8/vrf/VRFV2Wrapper.sol b/contracts/src/v0.8/vrf/VRFV2Wrapper.sol index abe479cb..ae0e3cc8 100644 --- a/contracts/src/v0.8/vrf/VRFV2Wrapper.sol +++ b/contracts/src/v0.8/vrf/VRFV2Wrapper.sol @@ -10,7 +10,7 @@ import {AggregatorV3Interface} from "../shared/interfaces/AggregatorV3Interface. import {VRFCoordinatorV2Interface} from "./interfaces/VRFCoordinatorV2Interface.sol"; import {VRFV2WrapperInterface} from "./interfaces/VRFV2WrapperInterface.sol"; import {VRFV2WrapperConsumerBase} from "./VRFV2WrapperConsumerBase.sol"; -import {ChainSpecificUtil} from "../ChainSpecificUtil.sol"; +import {ChainSpecificUtil} from "../ChainSpecificUtil_v0_8_6.sol"; /** * @notice A wrapper for VRFCoordinatorV2 that provides an interface better suited to one-off @@ -278,7 +278,7 @@ contract VRFV2Wrapper is ConfirmedOwner, TypeAndVersionInterface, VRFConsumerBas * uint16 requestConfirmations, and uint32 numWords. */ function onTokenTransfer(address _sender, uint256 _amount, bytes calldata _data) external onlyConfiguredNotDisabled { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(msg.sender == address(LINK), "only callable from LINK"); (uint32 callbackGasLimit, uint16 requestConfirmations, uint32 numWords) = abi.decode( @@ -288,9 +288,9 @@ contract VRFV2Wrapper is ConfirmedOwner, TypeAndVersionInterface, VRFConsumerBas uint32 eip150Overhead = _getEIP150Overhead(callbackGasLimit); int256 weiPerUnitLink = _getFeedData(); uint256 price = _calculateRequestPrice(callbackGasLimit, tx.gasprice, weiPerUnitLink); - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(_amount >= price, "fee too low"); - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(numWords <= s_maxNumWords, "numWords too high"); uint256 requestId = COORDINATOR.requestRandomWords( @@ -340,7 +340,7 @@ contract VRFV2Wrapper is ConfirmedOwner, TypeAndVersionInterface, VRFConsumerBas function fulfillRandomWords(uint256 _requestId, uint256[] memory _randomWords) internal override { Callback memory callback = s_callbacks[_requestId]; delete s_callbacks[_requestId]; - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(callback.callbackAddress != address(0), "request not found"); // This should never happen VRFV2WrapperConsumerBase c; @@ -361,7 +361,7 @@ contract VRFV2Wrapper is ConfirmedOwner, TypeAndVersionInterface, VRFConsumerBas if (staleFallback && s_stalenessSeconds < block.timestamp - timestamp) { weiPerUnitLink = s_fallbackWeiPerUnitLink; } - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(weiPerUnitLink >= 0, "Invalid LINK wei price"); return weiPerUnitLink; } @@ -411,9 +411,9 @@ contract VRFV2Wrapper is ConfirmedOwner, TypeAndVersionInterface, VRFConsumerBas } modifier onlyConfiguredNotDisabled() { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(s_configured, "wrapper is not configured"); - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(!s_disabled, "wrapper is disabled"); _; } diff --git a/contracts/src/v0.8/vrf/VRFV2WrapperConsumerBase.sol b/contracts/src/v0.8/vrf/VRFV2WrapperConsumerBase.sol index 2876b19d..79104318 100644 --- a/contracts/src/v0.8/vrf/VRFV2WrapperConsumerBase.sol +++ b/contracts/src/v0.8/vrf/VRFV2WrapperConsumerBase.sol @@ -79,7 +79,7 @@ abstract contract VRFV2WrapperConsumerBase { function fulfillRandomWords(uint256 _requestId, uint256[] memory _randomWords) internal virtual; function rawFulfillRandomWords(uint256 _requestId, uint256[] memory _randomWords) external { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(msg.sender == address(VRF_V2_WRAPPER), "only VRF V2 wrapper can fulfill"); fulfillRandomWords(_requestId, _randomWords); } diff --git a/contracts/src/v0.8/vrf/dev/BatchVRFCoordinatorV2Plus.sol b/contracts/src/v0.8/vrf/dev/BatchVRFCoordinatorV2Plus.sol index 3e6a5095..b626aeb6 100644 --- a/contracts/src/v0.8/vrf/dev/BatchVRFCoordinatorV2Plus.sol +++ b/contracts/src/v0.8/vrf/dev/BatchVRFCoordinatorV2Plus.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // solhint-disable-next-line one-contract-per-file -pragma solidity 0.8.6; +pragma solidity 0.8.19; import {VRFTypes} from "../VRFTypes.sol"; @@ -11,13 +11,13 @@ import {VRFTypes} from "../VRFTypes.sol"; */ contract BatchVRFCoordinatorV2Plus { // solhint-disable-next-line chainlink-solidity/prefix-immutable-variables-with-i - IVRFCoordinatorV2Plus public immutable COORDINATOR; + IVRFCoordinatorV2PlusFulfill public immutable COORDINATOR; event ErrorReturned(uint256 indexed requestId, string reason); event RawErrorReturned(uint256 indexed requestId, bytes lowLevelData); constructor(address coordinatorAddr) { - COORDINATOR = IVRFCoordinatorV2Plus(coordinatorAddr); + COORDINATOR = IVRFCoordinatorV2PlusFulfill(coordinatorAddr); } /** @@ -26,9 +26,9 @@ contract BatchVRFCoordinatorV2Plus { * @param rcs the request commitments corresponding to the randomness proofs. */ function fulfillRandomWords(VRFTypes.Proof[] memory proofs, VRFTypes.RequestCommitmentV2Plus[] memory rcs) external { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(proofs.length == rcs.length, "input array arg lengths mismatch"); - for (uint256 i = 0; i < proofs.length; i++) { + for (uint256 i = 0; i < proofs.length; ++i) { try COORDINATOR.fulfillRandomWords(proofs[i], rcs[i], false) returns (uint96 /* payment */) { continue; } catch Error(string memory reason) { @@ -59,7 +59,7 @@ contract BatchVRFCoordinatorV2Plus { } } -interface IVRFCoordinatorV2Plus { +interface IVRFCoordinatorV2PlusFulfill { function fulfillRandomWords( VRFTypes.Proof memory proof, VRFTypes.RequestCommitmentV2Plus memory rc, diff --git a/contracts/src/v0.8/vrf/dev/BlockhashStore.sol b/contracts/src/v0.8/vrf/dev/BlockhashStore.sol index b6389c9b..0bef7aea 100644 --- a/contracts/src/v0.8/vrf/dev/BlockhashStore.sol +++ b/contracts/src/v0.8/vrf/dev/BlockhashStore.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.6; +pragma solidity 0.8.19; import {ChainSpecificUtil} from "../../ChainSpecificUtil.sol"; @@ -22,7 +22,7 @@ contract BlockhashStore { */ function store(uint256 n) public { bytes32 h = ChainSpecificUtil._getBlockhash(uint64(n)); - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(h != 0x0, "blockhash(n) failed"); s_blockhashes[n] = h; } @@ -41,7 +41,7 @@ contract BlockhashStore { * that it hashes to a stored blockhash, and then extract parentHash to get the n-th blockhash. */ function storeVerifyHeader(uint256 n, bytes memory header) public { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(keccak256(header) == s_blockhashes[n + 1], "header has unknown blockhash"); // At this point, we know that header is the correct blockheader for block n+1. @@ -74,7 +74,7 @@ contract BlockhashStore { */ function getBlockhash(uint256 n) external view returns (bytes32) { bytes32 h = s_blockhashes[n]; - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(h != 0x0, "blockhash not found in store"); return h; } diff --git a/contracts/src/v0.8/vrf/dev/SubscriptionAPI.sol b/contracts/src/v0.8/vrf/dev/SubscriptionAPI.sol index 0ac1e903..d57af429 100644 --- a/contracts/src/v0.8/vrf/dev/SubscriptionAPI.sol +++ b/contracts/src/v0.8/vrf/dev/SubscriptionAPI.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.19; import {EnumerableSet} from "../../vendor/openzeppelin-solidity/v4.7.3/contracts/utils/structs/EnumerableSet.sol"; import {LinkTokenInterface} from "../../shared/interfaces/LinkTokenInterface.sol"; @@ -63,8 +63,9 @@ abstract contract SubscriptionAPI is ConfirmedOwner, IERC677Receiver, IVRFSubscr struct ConsumerConfig { bool active; uint64 nonce; + uint64 pendingReqCount; } - // Note a nonce of 0 indicates an the consumer is not assigned to that subscription. + // Note a nonce of 0 indicates the consumer is not assigned to that subscription. mapping(address => mapping(uint256 => ConsumerConfig)) /* consumerAddress */ /* subId */ /* consumerConfig */ internal s_consumers; mapping(uint256 => SubscriptionConfig) /* subId */ /* subscriptionConfig */ internal s_subscriptionConfigs; @@ -170,11 +171,11 @@ abstract contract SubscriptionAPI is ConfirmedOwner, IERC677Receiver, IVRFSubscr * @dev notably can be called even if there are pending requests, outstanding ones may fail onchain */ function ownerCancelSubscription(uint256 subId) external onlyOwner { - address owner = s_subscriptionConfigs[subId].owner; - if (owner == address(0)) { + address subOwner = s_subscriptionConfigs[subId].owner; + if (subOwner == address(0)) { revert InvalidSubscription(); } - _cancelSubscriptionHelper(subId, owner); + _cancelSubscriptionHelper(subId, subOwner); } /** @@ -310,17 +311,17 @@ abstract contract SubscriptionAPI is ConfirmedOwner, IERC677Receiver, IVRFSubscr public view override - returns (uint96 balance, uint96 nativeBalance, uint64 reqCount, address owner, address[] memory consumers) + returns (uint96 balance, uint96 nativeBalance, uint64 reqCount, address subOwner, address[] memory consumers) { - owner = s_subscriptionConfigs[subId].owner; - if (owner == address(0)) { + subOwner = s_subscriptionConfigs[subId].owner; + if (subOwner == address(0)) { revert InvalidSubscription(); } return ( s_subscriptions[subId].balance, s_subscriptions[subId].nativeBalance, s_subscriptions[subId].reqCount, - owner, + subOwner, s_subscriptionConfigs[subId].consumers ); } @@ -471,12 +472,12 @@ abstract contract SubscriptionAPI is ConfirmedOwner, IERC677Receiver, IVRFSubscr } function _onlySubOwner(uint256 subId) internal view { - address owner = s_subscriptionConfigs[subId].owner; - if (owner == address(0)) { + address subOwner = s_subscriptionConfigs[subId].owner; + if (subOwner == address(0)) { revert InvalidSubscription(); } - if (msg.sender != owner) { - revert MustBeSubOwner(owner); + if (msg.sender != subOwner) { + revert MustBeSubOwner(subOwner); } } } diff --git a/contracts/src/v0.8/vrf/dev/TrustedBlockhashStore.sol b/contracts/src/v0.8/vrf/dev/TrustedBlockhashStore.sol index b1a53b57..b3b77c80 100644 --- a/contracts/src/v0.8/vrf/dev/TrustedBlockhashStore.sol +++ b/contracts/src/v0.8/vrf/dev/TrustedBlockhashStore.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.6; +pragma solidity 0.8.19; import {ChainSpecificUtil} from "../../ChainSpecificUtil.sol"; import {ConfirmedOwner} from "../../shared/access/ConfirmedOwner.sol"; diff --git a/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol b/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol index d666fc35..5bff4b63 100644 --- a/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol +++ b/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol @@ -111,6 +111,9 @@ abstract contract VRFConsumerBaseV2Plus is IVRFMigratableConsumerV2Plus, Confirm * @param _vrfCoordinator address of VRFCoordinator contract */ constructor(address _vrfCoordinator) ConfirmedOwner(msg.sender) { + if (_vrfCoordinator == address(0)) { + revert ZeroAddress(); + } s_vrfCoordinator = IVRFCoordinatorV2Plus(_vrfCoordinator); } @@ -144,7 +147,10 @@ abstract contract VRFConsumerBaseV2Plus is IVRFMigratableConsumerV2Plus, Confirm /** * @inheritdoc IVRFMigratableConsumerV2Plus */ - function setCoordinator(address _vrfCoordinator) public override onlyOwnerOrCoordinator { + function setCoordinator(address _vrfCoordinator) external override onlyOwnerOrCoordinator { + if (_vrfCoordinator == address(0)) { + revert ZeroAddress(); + } s_vrfCoordinator = IVRFCoordinatorV2Plus(_vrfCoordinator); emit CoordinatorSet(_vrfCoordinator); diff --git a/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Upgradeable.sol b/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Upgradeable.sol index e05e0190..0de1b42c 100644 --- a/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Upgradeable.sol +++ b/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Upgradeable.sol @@ -122,7 +122,7 @@ abstract contract VRFConsumerBaseV2Upgradeable is Initializable { // solhint-disable-next-line func-name-mixedcase function __VRFConsumerBaseV2_init(address _vrfCoordinator) internal onlyInitializing { if (_vrfCoordinator == address(0)) { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors revert("must give valid coordinator address"); } diff --git a/contracts/src/v0.8/vrf/dev/VRFCoordinatorV2_5.sol b/contracts/src/v0.8/vrf/dev/VRFCoordinatorV2_5.sol index 2712dd27..c070c7d1 100644 --- a/contracts/src/v0.8/vrf/dev/VRFCoordinatorV2_5.sol +++ b/contracts/src/v0.8/vrf/dev/VRFCoordinatorV2_5.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.4; +pragma solidity 0.8.19; import {BlockhashStoreInterface} from "../interfaces/BlockhashStoreInterface.sol"; import {VRF} from "../../vrf/VRF.sol"; +import {VRFTypes} from "../VRFTypes.sol"; import {VRFConsumerBaseV2Plus, IVRFMigratableConsumerV2Plus} from "./VRFConsumerBaseV2Plus.sol"; import {ChainSpecificUtil} from "../../ChainSpecificUtil.sol"; import {SubscriptionAPI} from "./SubscriptionAPI.sol"; @@ -35,21 +36,12 @@ contract VRFCoordinatorV2_5 is VRF, SubscriptionAPI, IVRFCoordinatorV2Plus { error InvalidLinkWeiPrice(int256 linkWei); error LinkDiscountTooHigh(uint32 flatFeeLinkDiscountPPM, uint32 flatFeeNativePPM); error InvalidPremiumPercentage(uint8 premiumPercentage, uint8 max); - error InsufficientGasForConsumer(uint256 have, uint256 want); error NoCorrespondingRequest(); error IncorrectCommitment(); error BlockhashNotInStore(uint256 blockNum); error PaymentTooLarge(); error InvalidExtraArgsTag(); error GasPriceExceeded(uint256 gasPrice, uint256 maxGas); - struct RequestCommitment { - uint64 blockNum; - uint256 subId; - uint32 callbackGasLimit; - uint32 numWords; - address sender; - bytes extraArgs; - } struct ProvingKey { bool exists; // proving key exists @@ -293,6 +285,7 @@ contract VRFCoordinatorV2_5 is VRF, SubscriptionAPI, IVRFCoordinatorV2Plus { // The consequence for users is that they can send requests // for invalid keyHashes which will simply not be fulfilled. ++consumerConfig.nonce; + ++consumerConfig.pendingReqCount; uint256 preSeed; (requestId, preSeed) = _computeRequestId(req.keyHash, msg.sender, subId, consumerConfig.nonce); @@ -375,7 +368,7 @@ contract VRFCoordinatorV2_5 is VRF, SubscriptionAPI, IVRFCoordinatorV2Plus { function _getRandomnessFromProof( Proof memory proof, - RequestCommitment memory rc + VRFTypes.RequestCommitmentV2Plus memory rc ) internal view returns (Output memory) { bytes32 keyHash = hashOfKey(proof.pk); ProvingKey memory key = s_provingKeys[keyHash]; @@ -424,7 +417,7 @@ contract VRFCoordinatorV2_5 is VRF, SubscriptionAPI, IVRFCoordinatorV2Plus { function _deliverRandomness( uint256 requestId, - RequestCommitment memory rc, + VRFTypes.RequestCommitmentV2Plus memory rc, uint256[] memory randomWords ) internal returns (bool success) { VRFConsumerBaseV2Plus v; @@ -451,7 +444,7 @@ contract VRFCoordinatorV2_5 is VRF, SubscriptionAPI, IVRFCoordinatorV2Plus { */ function fulfillRandomWords( Proof memory proof, - RequestCommitment memory rc, + VRFTypes.RequestCommitmentV2Plus memory rc, bool onlyPremium ) external nonReentrant returns (uint96 payment) { uint256 startGas = gasleft(); @@ -500,14 +493,18 @@ contract VRFCoordinatorV2_5 is VRF, SubscriptionAPI, IVRFCoordinatorV2Plus { // Increment the req count for the subscription. ++s_subscriptions[rc.subId].reqCount; + // Decrement the pending req count for the consumer. + --s_consumers[rc.sender][rc.subId].pendingReqCount; bool nativePayment = uint8(rc.extraArgs[rc.extraArgs.length - 1]) == 1; // stack too deep error { - // We want to charge users exactly for how much gas they use in their callback. - // The gasAfterPaymentCalculation is meant to cover these additional operations where we - // decrement the subscription balance and increment the oracles withdrawable balance. + // We want to charge users exactly for how much gas they use in their callback with + // an additional premium. If onlyPremium is true, only premium is charged without + // the gas cost. The gasAfterPaymentCalculation is meant to cover these additional + // operations where we decrement the subscription balance and increment the + // withdrawable balance. bool isFeedStale; (payment, isFeedStale) = _calculatePaymentAmount(startGas, gasPrice, nativePayment, onlyPremium); if (isFeedStale) { @@ -625,19 +622,9 @@ contract VRFCoordinatorV2_5 is VRF, SubscriptionAPI, IVRFCoordinatorV2Plus { if (consumersLength == 0) { return false; } - uint256 provingKeyHashesLength = s_provingKeyHashes.length; for (uint256 i = 0; i < consumersLength; ++i) { - address consumer = consumers[i]; - for (uint256 j = 0; j < provingKeyHashesLength; ++j) { - (uint256 reqId, ) = _computeRequestId( - s_provingKeyHashes[j], - consumer, - subId, - s_consumers[consumer][subId].nonce - ); - if (s_requestCommitments[reqId] != 0) { - return true; - } + if (s_consumers[consumers[i]][subId].pendingReqCount > 0) { + return true; } } return false; @@ -748,16 +735,16 @@ contract VRFCoordinatorV2_5 is VRF, SubscriptionAPI, IVRFCoordinatorV2Plus { if (!_isTargetRegistered(newCoordinator)) { revert CoordinatorNotRegistered(newCoordinator); } - (uint96 balance, uint96 nativeBalance, , address owner, address[] memory consumers) = getSubscription(subId); - // solhint-disable-next-line custom-errors - require(owner == msg.sender, "Not subscription owner"); - // solhint-disable-next-line custom-errors + (uint96 balance, uint96 nativeBalance, , address subOwner, address[] memory consumers) = getSubscription(subId); + // solhint-disable-next-line gas-custom-errors + require(subOwner == msg.sender, "Not subscription owner"); + // solhint-disable-next-line gas-custom-errors require(!pendingRequestExists(subId), "Pending request exists"); V1MigrationData memory migrationData = V1MigrationData({ fromVersion: 1, subId: subId, - subOwner: owner, + subOwner: subOwner, consumers: consumers, linkBalance: balance, nativeBalance: nativeBalance @@ -768,7 +755,7 @@ contract VRFCoordinatorV2_5 is VRF, SubscriptionAPI, IVRFCoordinatorV2Plus { // Only transfer LINK if the token is active and there is a balance. if (address(LINK) != address(0) && balance != 0) { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(LINK.transfer(address(newCoordinator), balance), "insufficient funds"); } diff --git a/contracts/src/v0.8/vrf/dev/VRFSubscriptionBalanceMonitor.sol b/contracts/src/v0.8/vrf/dev/VRFSubscriptionBalanceMonitor.sol index 2dd44c8b..58dd25c6 100644 --- a/contracts/src/v0.8/vrf/dev/VRFSubscriptionBalanceMonitor.sol +++ b/contracts/src/v0.8/vrf/dev/VRFSubscriptionBalanceMonitor.sol @@ -197,7 +197,7 @@ contract VRFSubscriptionBalanceMonitor is ConfirmedOwner, Pausable, KeeperCompat * @param payee the address to pay */ function withdraw(uint256 amount, address payable payee) external onlyOwner { - // solhint-disable-next-line custom-errors, reason-string + // solhint-disable-next-line gas-custom-errors, reason-string require(payee != address(0)); emit FundsWithdrawn(amount, payee); LINKTOKEN.transfer(payee, amount); @@ -207,7 +207,7 @@ contract VRFSubscriptionBalanceMonitor is ConfirmedOwner, Pausable, KeeperCompat * @notice Sets the LINK token address. */ function setLinkTokenAddress(address linkTokenAddress) public onlyOwner { - // solhint-disable-next-line custom-errors, reason-string + // solhint-disable-next-line gas-custom-errors, reason-string require(linkTokenAddress != address(0)); emit LinkTokenAddressUpdated(address(LINKTOKEN), linkTokenAddress); LINKTOKEN = LinkTokenInterface(linkTokenAddress); @@ -217,7 +217,7 @@ contract VRFSubscriptionBalanceMonitor is ConfirmedOwner, Pausable, KeeperCompat * @notice Sets the VRF coordinator address. */ function setVRFCoordinatorV2Address(address coordinatorAddress) public onlyOwner { - // solhint-disable-next-line custom-errors, reason-string + // solhint-disable-next-line gas-custom-errors, reason-string require(coordinatorAddress != address(0)); emit VRFCoordinatorV2AddressUpdated(address(COORDINATOR), coordinatorAddress); COORDINATOR = VRFCoordinatorV2Interface(coordinatorAddress); @@ -227,7 +227,7 @@ contract VRFSubscriptionBalanceMonitor is ConfirmedOwner, Pausable, KeeperCompat * @notice Sets the keeper registry address. */ function setKeeperRegistryAddress(address keeperRegistryAddress) public onlyOwner { - // solhint-disable-next-line custom-errors, reason-string + // solhint-disable-next-line gas-custom-errors, reason-string require(keeperRegistryAddress != address(0)); emit KeeperRegistryAddressUpdated(s_keeperRegistryAddress, keeperRegistryAddress); s_keeperRegistryAddress = keeperRegistryAddress; diff --git a/contracts/src/v0.8/vrf/dev/VRFV2PlusWrapper.sol b/contracts/src/v0.8/vrf/dev/VRFV2PlusWrapper.sol index d2cfdb4c..1b80cc88 100644 --- a/contracts/src/v0.8/vrf/dev/VRFV2PlusWrapper.sol +++ b/contracts/src/v0.8/vrf/dev/VRFV2PlusWrapper.sol @@ -1,9 +1,8 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.6; +pragma solidity 0.8.19; import {ConfirmedOwner} from "../../shared/access/ConfirmedOwner.sol"; import {TypeAndVersionInterface} from "../../interfaces/TypeAndVersionInterface.sol"; -import {IVRFV2PlusMigrate} from "./interfaces/IVRFV2PlusMigrate.sol"; import {VRFConsumerBaseV2Plus} from "./VRFConsumerBaseV2Plus.sol"; import {LinkTokenInterface} from "../../shared/interfaces/LinkTokenInterface.sol"; import {AggregatorV3Interface} from "../../shared/interfaces/AggregatorV3Interface.sol"; @@ -20,58 +19,68 @@ import {ChainSpecificUtil} from "../../ChainSpecificUtil.sol"; contract VRFV2PlusWrapper is ConfirmedOwner, TypeAndVersionInterface, VRFConsumerBaseV2Plus, IVRFV2PlusWrapper { event WrapperFulfillmentFailed(uint256 indexed requestId, address indexed consumer); + // upper bound limit for premium percentages to make sure fee calculations don't overflow + uint8 private constant PREMIUM_PERCENTAGE_MAX = 155; + + // 5k is plenty for an EXTCODESIZE call (2600) + warm CALL (100) + // and some arithmetic operations. + uint256 private constant GAS_FOR_CALL_EXACT_CHECK = 5_000; + uint16 private constant EXPECTED_MIN_LENGTH = 36; + + // solhint-disable-next-line chainlink-solidity/prefix-immutable-variables-with-i + uint256 public immutable SUBSCRIPTION_ID; + LinkTokenInterface internal immutable i_link; + AggregatorV3Interface internal immutable i_link_native_feed; + error LinkAlreadySet(); + error LinkDiscountTooHigh(uint32 flatFeeLinkDiscountPPM, uint32 flatFeeNativePPM); + error InvalidPremiumPercentage(uint8 premiumPercentage, uint8 max); error FailedToTransferLink(); error IncorrectExtraArgsLength(uint16 expectedMinimumLength, uint16 actualLength); error NativePaymentInOnTokenTransfer(); error LINKPaymentInRequestRandomWordsInNative(); + error SubscriptionIdMissing(); /* Storage Slot 1: BEGIN */ - // s_keyHash is the key hash to use when requesting randomness. Fees are paid based on current gas - // fees, so this should be set to the highest gas lane on the network. - bytes32 internal s_keyHash; + // 20 bytes used by VRFConsumerBaseV2Plus.s_vrfCoordinator + + // s_configured tracks whether this contract has been configured. If not configured, randomness + // requests cannot be made. + bool public s_configured; + + // s_disabled disables the contract when true. When disabled, new VRF requests cannot be made + // but existing ones can still be fulfilled. + bool public s_disabled; + + // s_maxNumWords is the max number of words that can be requested in a single wrapped VRF request. + uint8 internal s_maxNumWords; + + // 9 bytes left /* Storage Slot 1: END */ /* Storage Slot 2: BEGIN */ - // solhint-disable-next-line chainlink-solidity/prefix-immutable-variables-with-i - uint256 public immutable SUBSCRIPTION_ID; + // s_keyHash is the key hash to use when requesting randomness. Fees are paid based on current gas + // fees, so this should be set to the highest gas lane on the network. + bytes32 internal s_keyHash; /* Storage Slot 2: END */ /* Storage Slot 3: BEGIN */ - // 5k is plenty for an EXTCODESIZE call (2600) + warm CALL (100) - // and some arithmetic operations. - uint256 private constant GAS_FOR_CALL_EXACT_CHECK = 5_000; - /* Storage Slot 3: END */ - - /* Storage Slot 4: BEGIN */ // lastRequestId is the request ID of the most recent VRF V2 request made by this wrapper. This // should only be relied on within the same transaction the request was made. uint256 public override lastRequestId; - /* Storage Slot 4: END */ + /* Storage Slot 3: END */ - /* Storage Slot 5: BEGIN */ + /* Storage Slot 4: BEGIN */ // s_fallbackWeiPerUnitLink is the backup LINK exchange rate used when the LINK/NATIVE feed is // stale. int256 private s_fallbackWeiPerUnitLink; - /* Storage Slot 5: END */ + /* Storage Slot 4: END */ - /* Storage Slot 6: BEGIN */ + /* Storage Slot 5: BEGIN */ // s_stalenessSeconds is the number of seconds before we consider the feed price to be stale and // fallback to fallbackWeiPerUnitLink. uint32 private s_stalenessSeconds; - // s_fulfillmentFlatFeeLinkPPM is the flat fee in millionths of LINK that VRFCoordinatorV2 - // charges. - uint32 private s_fulfillmentFlatFeeLinkPPM; - - // s_fulfillmentFlatFeeLinkPPM is the flat fee in millionths of LINK that VRFCoordinatorV2 - // charges. - uint32 private s_fulfillmentFlatFeeNativePPM; - - LinkTokenInterface public s_link; - /* Storage Slot 6: END */ - - /* Storage Slot 7: BEGIN */ // s_wrapperGasOverhead reflects the gas overhead of the wrapper's fulfillRandomWords // function. The cost for this gas is passed to the user. uint32 private s_wrapperGasOverhead; @@ -92,27 +101,26 @@ contract VRFV2PlusWrapper is ConfirmedOwner, TypeAndVersionInterface, VRFConsume // payment calculation in the coordinator. uint32 private s_coordinatorGasOverhead; - AggregatorV3Interface public s_linkNativeFeed; - /* Storage Slot 7: END */ - - /* Storage Slot 8: BEGIN */ - // s_configured tracks whether this contract has been configured. If not configured, randomness - // requests cannot be made. - bool public s_configured; + // s_fulfillmentFlatFeeLinkPPM is the flat fee in millionths of native that VRFCoordinatorV2 + // charges for native payment. + uint32 private s_fulfillmentFlatFeeNativePPM; - // s_disabled disables the contract when true. When disabled, new VRF requests cannot be made - // but existing ones can still be fulfilled. - bool public s_disabled; + // s_fulfillmentFlatFeeLinkDiscountPPM is the flat fee discount in millionths of native that VRFCoordinatorV2 + // charges for link payment. + uint32 private s_fulfillmentFlatFeeLinkDiscountPPM; - // s_wrapperPremiumPercentage is the premium ratio in percentage. For example, a value of 0 - // indicates no premium. A value of 15 indicates a 15 percent premium. - uint8 private s_wrapperPremiumPercentage; + // s_coordinatorNativePremiumPercentage is the coordinator's premium ratio in percentage for native payment. + // For example, a value of 0 indicates no premium. A value of 15 indicates a 15 percent premium. + // Wrapper has no premium. This premium is for VRFCoordinator. + uint8 private s_coordinatorNativePremiumPercentage; - // s_maxNumWords is the max number of words that can be requested in a single wrapped VRF request. - uint8 internal s_maxNumWords; + // s_coordinatorLinkPremiumPercentage is the premium ratio in percentage for link payment. For example, a + // value of 0 indicates no premium. A value of 15 indicates a 15 percent premium. + // Wrapper has no premium. This premium is for VRFCoordinator. + uint8 private s_coordinatorLinkPremiumPercentage; - uint16 private constant EXPECTED_MIN_LENGTH = 36; - /* Storage Slot 8: END */ + // 6 bytes left + /* Storage Slot 5: END */ struct Callback { address callbackAddress; @@ -123,40 +131,32 @@ contract VRFV2PlusWrapper is ConfirmedOwner, TypeAndVersionInterface, VRFConsume // GasPrice is unlikely to be more than 14 ETH on most chains uint64 requestGasPrice; } - /* Storage Slot 9: BEGIN */ + /* Storage Slot 6: BEGIN */ mapping(uint256 => Callback) /* requestID */ /* callback */ public s_callbacks; + /* Storage Slot 6: END */ - /* Storage Slot 9: END */ - - constructor(address _link, address _linkNativeFeed, address _coordinator) VRFConsumerBaseV2Plus(_coordinator) { - if (_link != address(0)) { - s_link = LinkTokenInterface(_link); - } - if (_linkNativeFeed != address(0)) { - s_linkNativeFeed = AggregatorV3Interface(_linkNativeFeed); - } - - // Create this wrapper's subscription and add itself as a consumer. - uint256 subId = s_vrfCoordinator.createSubscription(); - SUBSCRIPTION_ID = subId; - s_vrfCoordinator.addConsumer(subId, address(this)); - } - - /** - * @notice set the link token and link native feed to be used by this wrapper - * @param link address of the link token - * @param linkNativeFeed address of the link native feed - */ - function setLinkAndLinkNativeFeed(address link, address linkNativeFeed) external onlyOwner { - // Disallow re-setting link token because the logic wouldn't really make sense - if (address(s_link) != address(0)) { - revert LinkAlreadySet(); + constructor( + address _link, + address _linkNativeFeed, + address _coordinator, + uint256 _subId + ) VRFConsumerBaseV2Plus(_coordinator) { + i_link = LinkTokenInterface(_link); + i_link_native_feed = AggregatorV3Interface(_linkNativeFeed); + + if (_subId == 0) { + revert SubscriptionIdMissing(); } - s_link = LinkTokenInterface(link); - s_linkNativeFeed = AggregatorV3Interface(linkNativeFeed); + // Sanity check: should revert if the subscription does not exist + s_vrfCoordinator.getSubscription(_subId); - emit LinkAndLinkNativeFeedSet(link, linkNativeFeed); + // Subscription for the wrapper is created and managed by an external account. + // Expectation is that wrapper contract address will be added as a consumer + // to this subscription by the external account (owner of the subscription). + // Migration of the wrapper's subscription to the new coordinator has to be + // handled by the external account (owner of the subscription). + SUBSCRIPTION_ID = _subId; } /** @@ -181,7 +181,9 @@ contract VRFV2PlusWrapper is ConfirmedOwner, TypeAndVersionInterface, VRFConsume * @param _coordinatorGasOverhead reflects the gas overhead of the coordinator's * fulfillRandomWords function. * - * @param _wrapperPremiumPercentage is the premium ratio in percentage for wrapper requests. + * @param _coordinatorNativePremiumPercentage is the coordinator's premium ratio in percentage for requests paid in native. + * + * @param _coordinatorLinkPremiumPercentage is the coordinator's premium ratio in percentage for requests paid in link. * * @param _keyHash to use for requesting randomness. * @param _maxNumWords is the max number of words that can be requested in a single wrapped VRF request @@ -191,26 +193,38 @@ contract VRFV2PlusWrapper is ConfirmedOwner, TypeAndVersionInterface, VRFConsume * @param _fallbackWeiPerUnitLink is the backup LINK exchange rate used when the LINK/NATIVE feed * is stale. * - * @param _fulfillmentFlatFeeLinkPPM is the flat fee in millionths of LINK that VRFCoordinatorV2Plus - * charges. - * * @param _fulfillmentFlatFeeNativePPM is the flat fee in millionths of native that VRFCoordinatorV2Plus - * charges. + * charges for native payment. + * + * @param _fulfillmentFlatFeeLinkDiscountPPM is the flat fee discount in millionths of native that VRFCoordinatorV2Plus + * charges for link payment. */ function setConfig( uint32 _wrapperGasOverhead, uint32 _coordinatorGasOverhead, - uint8 _wrapperPremiumPercentage, + uint8 _coordinatorNativePremiumPercentage, + uint8 _coordinatorLinkPremiumPercentage, bytes32 _keyHash, uint8 _maxNumWords, uint32 _stalenessSeconds, int256 _fallbackWeiPerUnitLink, - uint32 _fulfillmentFlatFeeLinkPPM, - uint32 _fulfillmentFlatFeeNativePPM + uint32 _fulfillmentFlatFeeNativePPM, + uint32 _fulfillmentFlatFeeLinkDiscountPPM ) external onlyOwner { + if (_fulfillmentFlatFeeLinkDiscountPPM > _fulfillmentFlatFeeNativePPM) { + revert LinkDiscountTooHigh(_fulfillmentFlatFeeLinkDiscountPPM, _fulfillmentFlatFeeNativePPM); + } + if (_coordinatorNativePremiumPercentage > PREMIUM_PERCENTAGE_MAX) { + revert InvalidPremiumPercentage(_coordinatorNativePremiumPercentage, PREMIUM_PERCENTAGE_MAX); + } + if (_coordinatorLinkPremiumPercentage > PREMIUM_PERCENTAGE_MAX) { + revert InvalidPremiumPercentage(_coordinatorLinkPremiumPercentage, PREMIUM_PERCENTAGE_MAX); + } + s_wrapperGasOverhead = _wrapperGasOverhead; s_coordinatorGasOverhead = _coordinatorGasOverhead; - s_wrapperPremiumPercentage = _wrapperPremiumPercentage; + s_coordinatorNativePremiumPercentage = _coordinatorNativePremiumPercentage; + s_coordinatorLinkPremiumPercentage = _coordinatorLinkPremiumPercentage; s_keyHash = _keyHash; s_maxNumWords = _maxNumWords; s_configured = true; @@ -218,19 +232,20 @@ contract VRFV2PlusWrapper is ConfirmedOwner, TypeAndVersionInterface, VRFConsume // Get other configuration from coordinator s_stalenessSeconds = _stalenessSeconds; s_fallbackWeiPerUnitLink = _fallbackWeiPerUnitLink; - s_fulfillmentFlatFeeLinkPPM = _fulfillmentFlatFeeLinkPPM; s_fulfillmentFlatFeeNativePPM = _fulfillmentFlatFeeNativePPM; + s_fulfillmentFlatFeeLinkDiscountPPM = _fulfillmentFlatFeeLinkDiscountPPM; emit ConfigSet( _wrapperGasOverhead, _coordinatorGasOverhead, - _wrapperPremiumPercentage, + _coordinatorNativePremiumPercentage, + _coordinatorLinkPremiumPercentage, _keyHash, _maxNumWords, _stalenessSeconds, _fallbackWeiPerUnitLink, - _fulfillmentFlatFeeLinkPPM, - _fulfillmentFlatFeeNativePPM + _fulfillmentFlatFeeNativePPM, + s_fulfillmentFlatFeeLinkDiscountPPM ); } @@ -243,11 +258,11 @@ contract VRFV2PlusWrapper is ConfirmedOwner, TypeAndVersionInterface, VRFConsume * @return stalenessSeconds is the number of seconds before we consider the feed price to be stale * and fallback to fallbackWeiPerUnitLink. * - * @return fulfillmentFlatFeeLinkPPM is the flat fee in millionths of LINK that VRFCoordinatorV2Plus - * charges. - * * @return fulfillmentFlatFeeNativePPM is the flat fee in millionths of native that VRFCoordinatorV2Plus - * charges. + * charges for native payment. + * + * @return fulfillmentFlatFeeLinkDiscountPPM is the flat fee discount in millionths of native that VRFCoordinatorV2Plus + * charges for link payment. * * @return wrapperGasOverhead reflects the gas overhead of the wrapper's fulfillRandomWords * function. The cost for this gas is passed to the user. @@ -255,7 +270,10 @@ contract VRFV2PlusWrapper is ConfirmedOwner, TypeAndVersionInterface, VRFConsume * @return coordinatorGasOverhead reflects the gas overhead of the coordinator's * fulfillRandomWords function. * - * @return wrapperPremiumPercentage is the premium ratio in percentage. For example, a value of 0 + * @return wrapperNativePremiumPercentage is the premium ratio in percentage for native payment. For example, a value of 0 + * indicates no premium. A value of 15 indicates a 15 percent premium. + * + * @return wrapperLinkPremiumPercentage is the premium ratio in percentage for link payment. For example, a value of 0 * indicates no premium. A value of 15 indicates a 15 percent premium. * * @return keyHash is the key hash to use when requesting randomness. Fees are paid based on @@ -270,11 +288,12 @@ contract VRFV2PlusWrapper is ConfirmedOwner, TypeAndVersionInterface, VRFConsume returns ( int256 fallbackWeiPerUnitLink, uint32 stalenessSeconds, - uint32 fulfillmentFlatFeeLinkPPM, uint32 fulfillmentFlatFeeNativePPM, + uint32 fulfillmentFlatFeeLinkDiscountPPM, uint32 wrapperGasOverhead, uint32 coordinatorGasOverhead, - uint8 wrapperPremiumPercentage, + uint8 wrapperNativePremiumPercentage, + uint8 wrapperLinkPremiumPercentage, bytes32 keyHash, uint8 maxNumWords ) @@ -282,11 +301,12 @@ contract VRFV2PlusWrapper is ConfirmedOwner, TypeAndVersionInterface, VRFConsume return ( s_fallbackWeiPerUnitLink, s_stalenessSeconds, - s_fulfillmentFlatFeeLinkPPM, s_fulfillmentFlatFeeNativePPM, + s_fulfillmentFlatFeeLinkDiscountPPM, s_wrapperGasOverhead, s_coordinatorGasOverhead, - s_wrapperPremiumPercentage, + s_coordinatorNativePremiumPercentage, + s_coordinatorLinkPremiumPercentage, s_keyHash, s_maxNumWords ); @@ -340,20 +360,21 @@ contract VRFV2PlusWrapper is ConfirmedOwner, TypeAndVersionInterface, VRFConsume function _calculateRequestPriceNative(uint256 _gas, uint256 _requestGasPrice) internal view returns (uint256) { // costWei is the base fee denominated in wei (native) - // costWei takes into account the L1 posting costs of the VRF fulfillment - // transaction, if we are on an L2. - uint256 costWei = (_requestGasPrice * - (_gas + s_wrapperGasOverhead + s_coordinatorGasOverhead) + - ChainSpecificUtil._getL1CalldataGasCost(s_fulfillmentTxSizeBytes)); - // ((wei/gas * (gas)) + l1wei) - // baseFee is the base fee denominated in wei - uint256 baseFee = costWei; - // feeWithPremium is the fee after the percentage premium is applied - uint256 feeWithPremium = (baseFee * (s_wrapperPremiumPercentage + 100)) / 100; - // feeWithFlatFee is the fee after the flat fee is applied on top of the premium - uint256 feeWithFlatFee = feeWithPremium + (1e12 * uint256(s_fulfillmentFlatFeeNativePPM)); - - return feeWithFlatFee; + // (wei/gas) * gas + uint256 wrapperCostWei = _requestGasPrice * s_wrapperGasOverhead; + + // coordinatorCostWei takes into account the L1 posting costs of the VRF fulfillment transaction, if we are on an L2. + // (wei/gas) * gas + l1wei + uint256 coordinatorCostWei = _requestGasPrice * + (_gas + s_coordinatorGasOverhead) + + ChainSpecificUtil._getL1CalldataGasCost(s_fulfillmentTxSizeBytes); + + // coordinatorCostWithPremiumAndFlatFeeWei is the coordinator cost with the percentage premium and flat fee applied + // coordinator cost * premium multiplier + flat fee + uint256 coordinatorCostWithPremiumAndFlatFeeWei = ((coordinatorCostWei * + (s_coordinatorNativePremiumPercentage + 100)) / 100) + (1e12 * uint256(s_fulfillmentFlatFeeNativePPM)); + + return wrapperCostWei + coordinatorCostWithPremiumAndFlatFeeWei; } function _calculateRequestPrice( @@ -362,20 +383,24 @@ contract VRFV2PlusWrapper is ConfirmedOwner, TypeAndVersionInterface, VRFConsume int256 _weiPerUnitLink ) internal view returns (uint256) { // costWei is the base fee denominated in wei (native) - // costWei takes into account the L1 posting costs of the VRF fulfillment - // transaction, if we are on an L2. - uint256 costWei = (_requestGasPrice * - (_gas + s_wrapperGasOverhead + s_coordinatorGasOverhead) + - ChainSpecificUtil._getL1CalldataGasCost(s_fulfillmentTxSizeBytes)); - // (1e18 juels/link) * ((wei/gas * (gas)) + l1wei) / (wei/link) == 1e18 juels * wei/link / (wei/link) == 1e18 juels * wei/link * link/wei == juels - // baseFee is the base fee denominated in juels (link) - uint256 baseFee = (1e18 * costWei) / uint256(_weiPerUnitLink); - // feeWithPremium is the fee after the percentage premium is applied - uint256 feeWithPremium = (baseFee * (s_wrapperPremiumPercentage + 100)) / 100; - // feeWithFlatFee is the fee after the flat fee is applied on top of the premium - uint256 feeWithFlatFee = feeWithPremium + (1e12 * uint256(s_fulfillmentFlatFeeLinkPPM)); - - return feeWithFlatFee; + // (wei/gas) * gas + uint256 wrapperCostWei = _requestGasPrice * s_wrapperGasOverhead; + + // coordinatorCostWei takes into account the L1 posting costs of the VRF fulfillment transaction, if we are on an L2. + // (wei/gas) * gas + l1wei + uint256 coordinatorCostWei = _requestGasPrice * + (_gas + s_coordinatorGasOverhead) + + ChainSpecificUtil._getL1CalldataGasCost(s_fulfillmentTxSizeBytes); + + // coordinatorCostWithPremiumAndFlatFeeWei is the coordinator cost with the percentage premium and flat fee applied + // coordinator cost * premium multiplier + flat fee + uint256 coordinatorCostWithPremiumAndFlatFeeWei = ((coordinatorCostWei * + (s_coordinatorLinkPremiumPercentage + 100)) / 100) + + (1e12 * uint256(s_fulfillmentFlatFeeNativePPM - s_fulfillmentFlatFeeLinkDiscountPPM)); + + // requestPrice is denominated in juels (link) + // (1e18 juels/link) * wei / (wei/link) = juels + return (1e18 * (wrapperCostWei + coordinatorCostWithPremiumAndFlatFeeWei)) / uint256(_weiPerUnitLink); } /** @@ -392,8 +417,8 @@ contract VRFV2PlusWrapper is ConfirmedOwner, TypeAndVersionInterface, VRFConsume * uint16 requestConfirmations, and uint32 numWords. */ function onTokenTransfer(address _sender, uint256 _amount, bytes calldata _data) external onlyConfiguredNotDisabled { - // solhint-disable-next-line custom-errors - require(msg.sender == address(s_link), "only callable from LINK"); + // solhint-disable-next-line gas-custom-errors + require(msg.sender == address(i_link), "only callable from LINK"); (uint32 callbackGasLimit, uint16 requestConfirmations, uint32 numWords, bytes memory extraArgs) = abi.decode( _data, @@ -403,9 +428,9 @@ contract VRFV2PlusWrapper is ConfirmedOwner, TypeAndVersionInterface, VRFConsume uint32 eip150Overhead = _getEIP150Overhead(callbackGasLimit); (int256 weiPerUnitLink, bool isFeedStale) = _getFeedData(); uint256 price = _calculateRequestPrice(callbackGasLimit, tx.gasprice, weiPerUnitLink); - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(_amount >= price, "fee too low"); - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(numWords <= s_maxNumWords, "numWords too high"); VRFV2PlusClient.RandomWordsRequest memory req = VRFV2PlusClient.RandomWordsRequest({ keyHash: s_keyHash, @@ -457,14 +482,14 @@ contract VRFV2PlusWrapper is ConfirmedOwner, TypeAndVersionInterface, VRFConsume uint16 _requestConfirmations, uint32 _numWords, bytes calldata extraArgs - ) external payable override returns (uint256 requestId) { + ) external payable override onlyConfiguredNotDisabled returns (uint256 requestId) { checkPaymentMode(extraArgs, false); uint32 eip150Overhead = _getEIP150Overhead(_callbackGasLimit); uint256 price = _calculateRequestPriceNative(_callbackGasLimit, tx.gasprice); - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(msg.value >= price, "fee too low"); - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(_numWords <= s_maxNumWords, "numWords too high"); VRFV2PlusClient.RandomWordsRequest memory req = VRFV2PlusClient.RandomWordsRequest({ keyHash: s_keyHash, @@ -490,8 +515,8 @@ contract VRFV2PlusWrapper is ConfirmedOwner, TypeAndVersionInterface, VRFConsume * @param _recipient is the address that should receive the LINK funds. */ function withdraw(address _recipient) external onlyOwner { - uint256 amount = s_link.balanceOf(address(this)); - if (!s_link.transfer(_recipient, amount)) { + uint256 amount = i_link.balanceOf(address(this)); + if (!i_link.transfer(_recipient, amount)) { revert FailedToTransferLink(); } @@ -506,7 +531,7 @@ contract VRFV2PlusWrapper is ConfirmedOwner, TypeAndVersionInterface, VRFConsume function withdrawNative(address _recipient) external onlyOwner { uint256 amount = address(this).balance; (bool success, ) = payable(_recipient).call{value: amount}(""); - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(success, "failed to withdraw native"); emit NativeWithdrawn(_recipient, amount); @@ -535,28 +560,38 @@ contract VRFV2PlusWrapper is ConfirmedOwner, TypeAndVersionInterface, VRFConsume function fulfillRandomWords(uint256 _requestId, uint256[] memory _randomWords) internal override { Callback memory callback = s_callbacks[_requestId]; delete s_callbacks[_requestId]; - // solhint-disable-next-line custom-errors - require(callback.callbackAddress != address(0), "request not found"); // This should never happen + + address callbackAddress = callback.callbackAddress; + // solhint-disable-next-line gas-custom-errors + require(callbackAddress != address(0), "request not found"); // This should never happen VRFV2PlusWrapperConsumerBase c; bytes memory resp = abi.encodeWithSelector(c.rawFulfillRandomWords.selector, _requestId, _randomWords); - bool success = _callWithExactGas(callback.callbackGasLimit, callback.callbackAddress, resp); + bool success = _callWithExactGas(callback.callbackGasLimit, callbackAddress, resp); if (!success) { - emit WrapperFulfillmentFailed(_requestId, callback.callbackAddress); + emit WrapperFulfillmentFailed(_requestId, callbackAddress); } } + function link() external view override returns (address) { + return address(i_link); + } + + function linkNativeFeed() external view override returns (address) { + return address(i_link_native_feed); + } + function _getFeedData() private view returns (int256 weiPerUnitLink, bool isFeedStale) { uint32 stalenessSeconds = s_stalenessSeconds; uint256 timestamp; - (, weiPerUnitLink, , timestamp, ) = s_linkNativeFeed.latestRoundData(); + (, weiPerUnitLink, , timestamp, ) = i_link_native_feed.latestRoundData(); // solhint-disable-next-line not-rely-on-time isFeedStale = stalenessSeconds > 0 && stalenessSeconds < block.timestamp - timestamp; if (isFeedStale) { weiPerUnitLink = s_fallbackWeiPerUnitLink; } - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(weiPerUnitLink >= 0, "Invalid LINK wei price"); return (weiPerUnitLink, isFeedStale); } @@ -602,22 +637,14 @@ contract VRFV2PlusWrapper is ConfirmedOwner, TypeAndVersionInterface, VRFConsume } function typeAndVersion() external pure virtual override returns (string memory) { - return "VRFV2Wrapper 1.0.0"; + return "VRFV2PlusWrapper 1.0.0"; } modifier onlyConfiguredNotDisabled() { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(s_configured, "wrapper is not configured"); - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(!s_disabled, "wrapper is disabled"); _; } - - /*************************************************************************** - * Section: Migration of VRFV2PlusWrapper to latest VRFV2PlusCoordinator - ***************************************************************************/ - - function migrate(address newCoordinator) external onlyOwner { - IVRFV2PlusMigrate(address(s_vrfCoordinator)).migrate(SUBSCRIPTION_ID, newCoordinator); - } } diff --git a/contracts/src/v0.8/vrf/dev/VRFV2PlusWrapperConsumerBase.sol b/contracts/src/v0.8/vrf/dev/VRFV2PlusWrapperConsumerBase.sol index ff9e2a83..07a3292f 100644 --- a/contracts/src/v0.8/vrf/dev/VRFV2PlusWrapperConsumerBase.sol +++ b/contracts/src/v0.8/vrf/dev/VRFV2PlusWrapperConsumerBase.sol @@ -29,38 +29,19 @@ import {IVRFV2PlusWrapper} from "./interfaces/IVRFV2PlusWrapper.sol"; * @dev fulfillment with the randomness result. */ abstract contract VRFV2PlusWrapperConsumerBase { - event LinkTokenSet(address link); - - error LINKAlreadySet(); error OnlyVRFWrapperCanFulfill(address have, address want); - LinkTokenInterface internal s_linkToken; + LinkTokenInterface internal immutable i_linkToken; IVRFV2PlusWrapper public immutable i_vrfV2PlusWrapper; /** - * @param _link is the address of LinkToken * @param _vrfV2PlusWrapper is the address of the VRFV2Wrapper contract */ - constructor(address _link, address _vrfV2PlusWrapper) { - if (_link != address(0)) { - s_linkToken = LinkTokenInterface(_link); - } - - i_vrfV2PlusWrapper = IVRFV2PlusWrapper(_vrfV2PlusWrapper); - } - - /** - * @notice setLinkToken changes the LINK token address. - * @param _link is the address of the new LINK token contract - */ - function setLinkToken(address _link) external { - if (address(s_linkToken) != address(0)) { - revert LINKAlreadySet(); - } - - s_linkToken = LinkTokenInterface(_link); + constructor(address _vrfV2PlusWrapper) { + IVRFV2PlusWrapper vrfV2PlusWrapper = IVRFV2PlusWrapper(_vrfV2PlusWrapper); - emit LinkTokenSet(_link); + i_linkToken = LinkTokenInterface(vrfV2PlusWrapper.link()); + i_vrfV2PlusWrapper = vrfV2PlusWrapper; } /** @@ -83,7 +64,7 @@ abstract contract VRFV2PlusWrapperConsumerBase { bytes memory extraArgs ) internal returns (uint256 requestId, uint256 reqPrice) { reqPrice = i_vrfV2PlusWrapper.calculateRequestPrice(_callbackGasLimit); - s_linkToken.transferAndCall( + i_linkToken.transferAndCall( address(i_vrfV2PlusWrapper), reqPrice, abi.encode(_callbackGasLimit, _requestConfirmations, _numWords, extraArgs) @@ -135,6 +116,6 @@ abstract contract VRFV2PlusWrapperConsumerBase { /// @notice getLinkToken returns the link token contract function getLinkToken() public view returns (LinkTokenInterface) { - return s_linkToken; + return i_linkToken; } } diff --git a/contracts/src/v0.8/vrf/dev/interfaces/IVRFCoordinatorV2Plus.sol b/contracts/src/v0.8/vrf/dev/interfaces/IVRFCoordinatorV2Plus.sol index 846da0b1..b0d5a801 100644 --- a/contracts/src/v0.8/vrf/dev/interfaces/IVRFCoordinatorV2Plus.sol +++ b/contracts/src/v0.8/vrf/dev/interfaces/IVRFCoordinatorV2Plus.sol @@ -9,7 +9,7 @@ import {IVRFSubscriptionV2Plus} from "./IVRFSubscriptionV2Plus.sol"; interface IVRFCoordinatorV2Plus is IVRFSubscriptionV2Plus { /** * @notice Request a set of random words. - * @param req - a struct containing following fiels for randomness request: + * @param req - a struct containing following fields for randomness request: * keyHash - Corresponds to a particular oracle job which uses * that key for generating the VRF proof. Different keyHash's have different gas price * ceilings, so you can select a specific one to bound your maximum per request cost. diff --git a/contracts/src/v0.8/vrf/dev/interfaces/IVRFMigratableConsumerV2Plus.sol b/contracts/src/v0.8/vrf/dev/interfaces/IVRFMigratableConsumerV2Plus.sol index 103d1f17..67d12b88 100644 --- a/contracts/src/v0.8/vrf/dev/interfaces/IVRFMigratableConsumerV2Plus.sol +++ b/contracts/src/v0.8/vrf/dev/interfaces/IVRFMigratableConsumerV2Plus.sol @@ -8,6 +8,6 @@ interface IVRFMigratableConsumerV2Plus { event CoordinatorSet(address vrfCoordinator); /// @notice Sets the VRF Coordinator address - /// @notice This method is should only be callable by the coordinator or contract owner + /// @notice This method should only be callable by the coordinator or contract owner function setCoordinator(address vrfCoordinator) external; } diff --git a/contracts/src/v0.8/vrf/dev/interfaces/IVRFSubscriptionV2Plus.sol b/contracts/src/v0.8/vrf/dev/interfaces/IVRFSubscriptionV2Plus.sol index 49c13198..b178ffb9 100644 --- a/contracts/src/v0.8/vrf/dev/interfaces/IVRFSubscriptionV2Plus.sol +++ b/contracts/src/v0.8/vrf/dev/interfaces/IVRFSubscriptionV2Plus.sol @@ -26,7 +26,7 @@ interface IVRFSubscriptionV2Plus { function cancelSubscription(uint256 subId, address to) external; /** - * @notice Request subscription owner transfer. + * @notice Accept subscription owner transfer. * @param subId - ID of the subscription * @dev will revert if original owner of subId has * not requested that msg.sender become the new owner. @@ -92,7 +92,7 @@ interface IVRFSubscriptionV2Plus { /** * @notice Fund a subscription with native. * @param subId - ID of the subscription - * @notice This method expects msg.value to be greater than 0. + * @notice This method expects msg.value to be greater than or equal to 0. */ function fundSubscriptionWithNative(uint256 subId) external payable; } diff --git a/contracts/src/v0.8/vrf/dev/interfaces/IVRFV2PlusWrapper.sol b/contracts/src/v0.8/vrf/dev/interfaces/IVRFV2PlusWrapper.sol index a00327b5..93f6bf0e 100644 --- a/contracts/src/v0.8/vrf/dev/interfaces/IVRFV2PlusWrapper.sol +++ b/contracts/src/v0.8/vrf/dev/interfaces/IVRFV2PlusWrapper.sol @@ -2,18 +2,18 @@ pragma solidity ^0.8.0; interface IVRFV2PlusWrapper { - event LinkAndLinkNativeFeedSet(address link, address linkNativeFeed); event FulfillmentTxSizeSet(uint32 size); event ConfigSet( uint32 wrapperGasOverhead, uint32 coordinatorGasOverhead, - uint8 wrapperPremiumPercentage, + uint8 coordinatorNativePremiumPercentage, + uint8 coordinatorLinkPremiumPercentage, bytes32 keyHash, uint8 maxNumWords, uint32 stalenessSeconds, int256 fallbackWeiPerUnitLink, - uint32 fulfillmentFlatFeeLinkPPM, - uint32 fulfillmentFlatFeeNativePPM + uint32 fulfillmentFlatFeeNativePPM, + uint32 fulfillmentFlatFeeLinkDiscountPPM ); event FallbackWeiPerUnitLinkUsed(uint256 requestId, int256 fallbackWeiPerUnitLink); event Withdrawn(address indexed to, uint256 amount); @@ -87,4 +87,7 @@ interface IVRFV2PlusWrapper { uint32 _numWords, bytes memory extraArgs ) external payable returns (uint256 requestId); + + function link() external view returns (address); + function linkNativeFeed() external view returns (address); } diff --git a/contracts/src/v0.8/vrf/dev/testhelpers/ExposedVRFCoordinatorV2_5.sol b/contracts/src/v0.8/vrf/dev/testhelpers/ExposedVRFCoordinatorV2_5.sol index 0f945719..3f4e799f 100644 --- a/contracts/src/v0.8/vrf/dev/testhelpers/ExposedVRFCoordinatorV2_5.sol +++ b/contracts/src/v0.8/vrf/dev/testhelpers/ExposedVRFCoordinatorV2_5.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.4; import {VRFCoordinatorV2_5} from "../VRFCoordinatorV2_5.sol"; +import {VRFTypes} from "../../VRFTypes.sol"; import {EnumerableSet} from "../../../vendor/openzeppelin-solidity/v4.7.3/contracts/utils/structs/EnumerableSet.sol"; // solhint-disable-next-line contract-name-camelcase @@ -25,7 +26,7 @@ contract ExposedVRFCoordinatorV2_5 is VRFCoordinatorV2_5 { function getRandomnessFromProofExternal( Proof calldata proof, - RequestCommitment calldata rc + VRFTypes.RequestCommitmentV2Plus calldata rc ) external view returns (Output memory) { return _getRandomnessFromProof(proof, rc); } diff --git a/contracts/src/v0.8/vrf/dev/testhelpers/VRFConsumerV2PlusUpgradeableExample.sol b/contracts/src/v0.8/vrf/dev/testhelpers/VRFConsumerV2PlusUpgradeableExample.sol index 6d77a5d5..65a88df2 100644 --- a/contracts/src/v0.8/vrf/dev/testhelpers/VRFConsumerV2PlusUpgradeableExample.sol +++ b/contracts/src/v0.8/vrf/dev/testhelpers/VRFConsumerV2PlusUpgradeableExample.sol @@ -23,7 +23,7 @@ contract VRFConsumerV2PlusUpgradeableExample is Initializable, VRFConsumerBaseV2 // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(requestId == s_requestId, "request ID is incorrect"); s_gasAvailable = gasleft(); @@ -40,14 +40,14 @@ contract VRFConsumerV2PlusUpgradeableExample is Initializable, VRFConsumerBaseV2 } function topUpSubscription(uint96 amount) external { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(s_subId != 0, "sub not set"); // Approve the link transfer. LINKTOKEN.transferAndCall(address(COORDINATOR), amount, abi.encode(s_subId)); } function updateSubscription(address[] memory consumers) external { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(s_subId != 0, "subID not set"); for (uint256 i = 0; i < consumers.length; i++) { COORDINATOR.addConsumer(s_subId, consumers[i]); diff --git a/contracts/src/v0.8/vrf/dev/testhelpers/VRFCoordinatorV2PlusUpgradedVersion.sol b/contracts/src/v0.8/vrf/dev/testhelpers/VRFCoordinatorV2PlusUpgradedVersion.sol index 2e3aef59..6599a68a 100644 --- a/contracts/src/v0.8/vrf/dev/testhelpers/VRFCoordinatorV2PlusUpgradedVersion.sol +++ b/contracts/src/v0.8/vrf/dev/testhelpers/VRFCoordinatorV2PlusUpgradedVersion.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.4; +pragma solidity 0.8.19; import {BlockhashStoreInterface} from "../../interfaces/BlockhashStoreInterface.sol"; // solhint-disable-next-line no-unused-import @@ -648,9 +648,9 @@ contract VRFCoordinatorV2PlusUpgradedVersion is revert CoordinatorNotRegistered(newCoordinator); } (uint96 balance, uint96 nativeBalance, , address owner, address[] memory consumers) = getSubscription(subId); - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(owner == msg.sender, "Not subscription owner"); - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(!pendingRequestExists(subId), "Pending request exists"); V1MigrationData memory migrationData = V1MigrationData({ @@ -667,7 +667,7 @@ contract VRFCoordinatorV2PlusUpgradedVersion is // Only transfer LINK if the token is active and there is a balance. if (address(LINK) != address(0) && balance != 0) { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(LINK.transfer(address(newCoordinator), balance), "insufficient funds"); } @@ -713,7 +713,11 @@ contract VRFCoordinatorV2PlusUpgradedVersion is } for (uint256 i = 0; i < migrationData.consumers.length; i++) { - s_consumers[migrationData.consumers[i]][migrationData.subId] = ConsumerConfig({active: true, nonce: 0}); + s_consumers[migrationData.consumers[i]][migrationData.subId] = ConsumerConfig({ + active: true, + nonce: 0, + pendingReqCount: 0 + }); } s_subscriptions[migrationData.subId] = Subscription({ diff --git a/contracts/src/v0.8/vrf/dev/testhelpers/VRFMaliciousConsumerV2Plus.sol b/contracts/src/v0.8/vrf/dev/testhelpers/VRFMaliciousConsumerV2Plus.sol index 9bbb5692..cfc12102 100644 --- a/contracts/src/v0.8/vrf/dev/testhelpers/VRFMaliciousConsumerV2Plus.sol +++ b/contracts/src/v0.8/vrf/dev/testhelpers/VRFMaliciousConsumerV2Plus.sol @@ -45,7 +45,7 @@ contract VRFMaliciousConsumerV2Plus is VRFConsumerBaseV2Plus { } function updateSubscription(address[] memory consumers) external { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(s_subId != 0, "subID not set"); for (uint256 i = 0; i < consumers.length; i++) { s_vrfCoordinator.addConsumer(s_subId, consumers[i]); diff --git a/contracts/src/v0.8/vrf/dev/testhelpers/VRFV2PlusConsumerExample.sol b/contracts/src/v0.8/vrf/dev/testhelpers/VRFV2PlusConsumerExample.sol index 2ef4e5c0..8063d2ea 100644 --- a/contracts/src/v0.8/vrf/dev/testhelpers/VRFV2PlusConsumerExample.sol +++ b/contracts/src/v0.8/vrf/dev/testhelpers/VRFV2PlusConsumerExample.sol @@ -29,7 +29,7 @@ contract VRFV2PlusConsumerExample is ConfirmedOwner, VRFConsumerBaseV2Plus { function getRandomness(uint256 requestId, uint256 idx) public view returns (uint256 randomWord) { Response memory resp = s_requests[requestId]; - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(resp.requestId != 0, "request ID is incorrect"); return resp.randomWords[idx]; } @@ -54,20 +54,20 @@ contract VRFV2PlusConsumerExample is ConfirmedOwner, VRFConsumerBaseV2Plus { } function topUpSubscription(uint96 amount) external { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(s_subId != 0, "sub not set"); s_linkToken.transferAndCall(address(s_vrfCoordinator), amount, abi.encode(s_subId)); } function topUpSubscriptionNative() external payable { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(s_subId != 0, "sub not set"); s_vrfCoordinatorApiV1.fundSubscriptionWithNative{value: msg.value}(s_subId); } // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(requestId == s_recentRequestId, "request ID is incorrect"); s_requests[requestId].randomWords = randomWords; s_requests[requestId].fulfilled = true; @@ -100,7 +100,7 @@ contract VRFV2PlusConsumerExample is ConfirmedOwner, VRFConsumerBaseV2Plus { } function updateSubscription(address[] memory consumers) external { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(s_subId != 0, "subID not set"); for (uint256 i = 0; i < consumers.length; i++) { s_vrfCoordinatorApiV1.addConsumer(s_subId, consumers[i]); diff --git a/contracts/src/v0.8/vrf/dev/testhelpers/VRFV2PlusExternalSubOwnerExample.sol b/contracts/src/v0.8/vrf/dev/testhelpers/VRFV2PlusExternalSubOwnerExample.sol index ed12d156..6b5c9f4b 100644 --- a/contracts/src/v0.8/vrf/dev/testhelpers/VRFV2PlusExternalSubOwnerExample.sol +++ b/contracts/src/v0.8/vrf/dev/testhelpers/VRFV2PlusExternalSubOwnerExample.sol @@ -21,7 +21,7 @@ contract VRFV2PlusExternalSubOwnerExample is VRFConsumerBaseV2Plus { // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(requestId == s_requestId, "request ID is incorrect"); s_randomWords = randomWords; } diff --git a/contracts/src/v0.8/vrf/dev/testhelpers/VRFV2PlusLoadTestWithMetrics.sol b/contracts/src/v0.8/vrf/dev/testhelpers/VRFV2PlusLoadTestWithMetrics.sol index d937728a..85cb7727 100644 --- a/contracts/src/v0.8/vrf/dev/testhelpers/VRFV2PlusLoadTestWithMetrics.sol +++ b/contracts/src/v0.8/vrf/dev/testhelpers/VRFV2PlusLoadTestWithMetrics.sol @@ -21,6 +21,8 @@ contract VRFV2PlusLoadTestWithMetrics is VRFConsumerBaseV2Plus { uint256 public s_lastRequestId; + uint32[] public s_requestBlockTimes; + struct RequestStatus { bool fulfilled; uint256[] randomWords; @@ -70,6 +72,8 @@ contract VRFV2PlusLoadTestWithMetrics is VRFConsumerBaseV2Plus { ); s_responseCount++; + + s_requestBlockTimes.push(uint32(responseTimeInBlocks)); } function requestRandomWords( @@ -116,6 +120,7 @@ contract VRFV2PlusLoadTestWithMetrics is VRFConsumerBaseV2Plus { s_fastestResponseTimeInSeconds = 999; s_requestCount = 0; s_responseCount = 0; + delete s_requestBlockTimes; } function getRequestStatus( @@ -161,4 +166,18 @@ contract VRFV2PlusLoadTestWithMetrics is VRFConsumerBaseV2Plus { return (_slowestResponseTime, _fastestResponseTime, averageInMillions); } + + function getRequestBlockTimes(uint256 offset, uint256 quantity) external view returns (uint32[] memory) { + uint256 end = offset + quantity; + if (end > s_requestBlockTimes.length) { + end = s_requestBlockTimes.length; + } + + uint32[] memory blockTimes = new uint32[](end - offset); + for (uint256 i = offset; i < end; i++) { + blockTimes[i - offset] = s_requestBlockTimes[i]; + } + + return blockTimes; + } } diff --git a/contracts/src/v0.8/vrf/dev/testhelpers/VRFV2PlusRevertingExample.sol b/contracts/src/v0.8/vrf/dev/testhelpers/VRFV2PlusRevertingExample.sol index 4e38ae39..07f2e44d 100644 --- a/contracts/src/v0.8/vrf/dev/testhelpers/VRFV2PlusRevertingExample.sol +++ b/contracts/src/v0.8/vrf/dev/testhelpers/VRFV2PlusRevertingExample.sol @@ -20,7 +20,7 @@ contract VRFV2PlusRevertingExample is VRFConsumerBaseV2Plus { // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore function fulfillRandomWords(uint256, uint256[] memory) internal pure override { - // solhint-disable-next-line custom-errors, reason-string + // solhint-disable-next-line gas-custom-errors, reason-string revert(); } @@ -34,14 +34,14 @@ contract VRFV2PlusRevertingExample is VRFConsumerBaseV2Plus { } function topUpSubscription(uint96 amount) external { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(s_subId != 0, "sub not set"); // Approve the link transfer. LINKTOKEN.transferAndCall(address(s_vrfCoordinator), amount, abi.encode(s_subId)); } function updateSubscription(address[] memory consumers) external { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(s_subId != 0, "subID not set"); for (uint256 i = 0; i < consumers.length; i++) { s_vrfCoordinator.addConsumer(s_subId, consumers[i]); diff --git a/contracts/src/v0.8/vrf/dev/testhelpers/VRFV2PlusSingleConsumerExample.sol b/contracts/src/v0.8/vrf/dev/testhelpers/VRFV2PlusSingleConsumerExample.sol index f3bf41d4..b956ab00 100644 --- a/contracts/src/v0.8/vrf/dev/testhelpers/VRFV2PlusSingleConsumerExample.sol +++ b/contracts/src/v0.8/vrf/dev/testhelpers/VRFV2PlusSingleConsumerExample.sol @@ -48,7 +48,7 @@ contract VRFV2PlusSingleConsumerExample is VRFConsumerBaseV2Plus { // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(requestId == s_requestId, "request ID is incorrect"); s_randomWords = randomWords; } diff --git a/contracts/src/v0.8/vrf/dev/testhelpers/VRFV2PlusWrapperConsumerExample.sol b/contracts/src/v0.8/vrf/dev/testhelpers/VRFV2PlusWrapperConsumerExample.sol index bc0e6531..5025a300 100644 --- a/contracts/src/v0.8/vrf/dev/testhelpers/VRFV2PlusWrapperConsumerExample.sol +++ b/contracts/src/v0.8/vrf/dev/testhelpers/VRFV2PlusWrapperConsumerExample.sol @@ -18,10 +18,7 @@ contract VRFV2PlusWrapperConsumerExample is VRFV2PlusWrapperConsumerBase, Confir mapping(uint256 => RequestStatus) /* requestId */ /* requestStatus */ public s_requests; - constructor( - address _link, - address _vrfV2Wrapper - ) ConfirmedOwner(msg.sender) VRFV2PlusWrapperConsumerBase(_link, _vrfV2Wrapper) {} + constructor(address _vrfV2Wrapper) ConfirmedOwner(msg.sender) VRFV2PlusWrapperConsumerBase(_vrfV2Wrapper) {} function makeRequest( uint32 _callbackGasLimit, @@ -51,7 +48,7 @@ contract VRFV2PlusWrapperConsumerExample is VRFV2PlusWrapperConsumerBase, Confir // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore function fulfillRandomWords(uint256 _requestId, uint256[] memory _randomWords) internal override { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(s_requests[_requestId].paid > 0, "request not found"); s_requests[_requestId].fulfilled = true; s_requests[_requestId].randomWords = _randomWords; @@ -61,7 +58,7 @@ contract VRFV2PlusWrapperConsumerExample is VRFV2PlusWrapperConsumerBase, Confir function getRequestStatus( uint256 _requestId ) external view returns (uint256 paid, bool fulfilled, uint256[] memory randomWords) { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(s_requests[_requestId].paid > 0, "request not found"); RequestStatus memory request = s_requests[_requestId]; return (request.paid, request.fulfilled, request.randomWords); @@ -70,14 +67,14 @@ contract VRFV2PlusWrapperConsumerExample is VRFV2PlusWrapperConsumerBase, Confir /// @notice withdrawLink withdraws the amount specified in amount to the owner /// @param amount the amount to withdraw, in juels function withdrawLink(uint256 amount) external onlyOwner { - s_linkToken.transfer(owner(), amount); + i_linkToken.transfer(owner(), amount); } /// @notice withdrawNative withdraws the amount specified in amount to the owner /// @param amount the amount to withdraw, in wei function withdrawNative(uint256 amount) external onlyOwner { (bool success, ) = payable(owner()).call{value: amount}(""); - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(success, "withdrawNative failed"); } } diff --git a/contracts/src/v0.8/vrf/dev/testhelpers/VRFV2PlusWrapperLoadTestConsumer.sol b/contracts/src/v0.8/vrf/dev/testhelpers/VRFV2PlusWrapperLoadTestConsumer.sol index 5b75bc07..1389aee5 100644 --- a/contracts/src/v0.8/vrf/dev/testhelpers/VRFV2PlusWrapperLoadTestConsumer.sol +++ b/contracts/src/v0.8/vrf/dev/testhelpers/VRFV2PlusWrapperLoadTestConsumer.sol @@ -13,6 +13,7 @@ contract VRFV2PlusWrapperLoadTestConsumer is VRFV2PlusWrapperConsumerBase, Confi uint256 public s_slowestFulfillment = 0; uint256 public s_fastestFulfillment = 999; uint256 public s_lastRequestId; + uint32[] public s_requestBlockTimes; // solhint-disable-next-line chainlink-solidity/prefix-storage-variables-with-s-underscore mapping(uint256 => uint256) internal requestHeights; // requestIds to block number when rand request was made @@ -32,10 +33,7 @@ contract VRFV2PlusWrapperLoadTestConsumer is VRFV2PlusWrapperConsumerBase, Confi mapping(uint256 => RequestStatus) /* requestId */ /* requestStatus */ public s_requests; - constructor( - address _link, - address _vrfV2PlusWrapper - ) ConfirmedOwner(msg.sender) VRFV2PlusWrapperConsumerBase(_link, _vrfV2PlusWrapper) {} + constructor(address _vrfV2PlusWrapper) ConfirmedOwner(msg.sender) VRFV2PlusWrapperConsumerBase(_vrfV2PlusWrapper) {} function makeRequests( uint32 _callbackGasLimit, @@ -105,7 +103,7 @@ contract VRFV2PlusWrapperLoadTestConsumer is VRFV2PlusWrapperConsumerBase, Confi // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore function fulfillRandomWords(uint256 _requestId, uint256[] memory _randomWords) internal override { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(s_requests[_requestId].paid > 0, "request not found"); uint256 fulfilmentBlockNumber = ChainSpecificUtil._getBlockNumber(); uint256 requestDelay = fulfilmentBlockNumber - requestHeights[_requestId]; @@ -125,6 +123,8 @@ contract VRFV2PlusWrapperLoadTestConsumer is VRFV2PlusWrapperConsumerBase, Confi s_requests[_requestId].fulfilmentTimestamp = block.timestamp; s_requests[_requestId].fulfilmentBlockNumber = fulfilmentBlockNumber; + s_requestBlockTimes.push(uint32(requestDelay)); + emit WrappedRequestFulfilled(_requestId, _randomWords, s_requests[_requestId].paid); } @@ -143,7 +143,7 @@ contract VRFV2PlusWrapperLoadTestConsumer is VRFV2PlusWrapperConsumerBase, Confi uint256 fulfilmentBlockNumber ) { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(s_requests[_requestId].paid > 0, "request not found"); RequestStatus memory request = s_requests[_requestId]; return ( @@ -157,25 +157,40 @@ contract VRFV2PlusWrapperLoadTestConsumer is VRFV2PlusWrapperConsumerBase, Confi ); } + function getRequestBlockTimes(uint256 offset, uint256 quantity) external view returns (uint32[] memory) { + uint256 end = offset + quantity; + if (end > s_requestBlockTimes.length) { + end = s_requestBlockTimes.length; + } + + uint32[] memory blockTimes = new uint32[](end - offset); + for (uint256 i = offset; i < end; i++) { + blockTimes[i - offset] = s_requestBlockTimes[i]; + } + + return blockTimes; + } + function reset() external { s_averageFulfillmentInMillions = 0; // in millions for better precision s_slowestFulfillment = 0; s_fastestFulfillment = 999; s_requestCount = 0; s_responseCount = 0; + delete s_requestBlockTimes; } /// @notice withdrawLink withdraws the amount specified in amount to the owner /// @param amount the amount to withdraw, in juels function withdrawLink(uint256 amount) external onlyOwner { - s_linkToken.transfer(owner(), amount); + i_linkToken.transfer(owner(), amount); } /// @notice withdrawNative withdraws the amount specified in amount to the owner /// @param amount the amount to withdraw, in wei function withdrawNative(uint256 amount) external onlyOwner { (bool success, ) = payable(owner()).call{value: amount}(""); - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(success, "withdrawNative failed"); } diff --git a/contracts/src/v0.8/vrf/mocks/VRFCoordinatorMock.sol b/contracts/src/v0.8/vrf/mocks/VRFCoordinatorMock.sol index 6695e79b..e192f749 100644 --- a/contracts/src/v0.8/vrf/mocks/VRFCoordinatorMock.sol +++ b/contracts/src/v0.8/vrf/mocks/VRFCoordinatorMock.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.0; import {LinkTokenInterface} from "../../shared/interfaces/LinkTokenInterface.sol"; import {VRFConsumerBase} from "../../vrf/VRFConsumerBase.sol"; -// solhint-disable custom-errors +// solhint-disable gas-custom-errors contract VRFCoordinatorMock { LinkTokenInterface public LINK; diff --git a/contracts/src/v0.8/vrf/mocks/VRFCoordinatorV2Mock.sol b/contracts/src/v0.8/vrf/mocks/VRFCoordinatorV2Mock.sol index b605815f..9617b764 100644 --- a/contracts/src/v0.8/vrf/mocks/VRFCoordinatorV2Mock.sol +++ b/contracts/src/v0.8/vrf/mocks/VRFCoordinatorV2Mock.sol @@ -7,7 +7,7 @@ import {VRFConsumerBaseV2} from "../VRFConsumerBaseV2.sol"; import {ConfirmedOwner} from "../../shared/access/ConfirmedOwner.sol"; // solhint-disable chainlink-solidity/prefix-immutable-variables-with-i -// solhint-disable custom-errors +// solhint-disable gas-custom-errors // solhint-disable avoid-low-level-calls contract VRFCoordinatorV2Mock is VRFCoordinatorV2Interface, ConfirmedOwner { diff --git a/contracts/src/v0.8/vrf/test/BaseTest.t.sol b/contracts/src/v0.8/vrf/test/BaseTest.t.sol new file mode 100644 index 00000000..4da698d1 --- /dev/null +++ b/contracts/src/v0.8/vrf/test/BaseTest.t.sol @@ -0,0 +1,17 @@ +pragma solidity ^0.8.0; + +import {Test} from "forge-std/Test.sol"; + +contract BaseTest is Test { + bool private s_baseTestInitialized; + address internal constant OWNER = 0x00007e64E1fB0C487F25dd6D3601ff6aF8d32e4e; + + function setUp() public virtual { + // BaseTest.setUp is often called multiple times from tests' setUp due to inheritance. + if (s_baseTestInitialized) return; + s_baseTestInitialized = true; + + // Set msg.sender to OWNER until changePrank or stopPrank is called + vm.startPrank(OWNER); + } +} diff --git a/contracts/src/v0.8/vrf/test/ChainSpecificUtil.t.sol b/contracts/src/v0.8/vrf/test/ChainSpecificUtil.t.sol new file mode 100644 index 00000000..efeb9027 --- /dev/null +++ b/contracts/src/v0.8/vrf/test/ChainSpecificUtil.t.sol @@ -0,0 +1,196 @@ +pragma solidity 0.8.6; + +import "./BaseTest.t.sol"; +import {ChainSpecificUtil} from "../../ChainSpecificUtil_v0_8_6.sol"; + +import {ArbSys} from "../../vendor/@arbitrum/nitro-contracts/src/precompiles/ArbSys.sol"; +import {ArbGasInfo} from "../../vendor/@arbitrum/nitro-contracts/src/precompiles/ArbGasInfo.sol"; +import {OVM_GasPriceOracle} from "../../vendor/@eth-optimism/contracts/v0.8.6/contracts/L2/predeploys/OVM_GasPriceOracle.sol"; + +contract ChainSpecificUtilTest is BaseTest { + // ------------ Start Arbitrum Constants ------------ + + /// @dev ARBSYS_ADDR is the address of the ArbSys precompile on Arbitrum. + /// @dev reference: https://github.com/OffchainLabs/nitro/blob/v2.0.14/contracts/src/precompiles/ArbSys.sol#L10 + address private constant ARBSYS_ADDR = address(0x0000000000000000000000000000000000000064); + ArbSys private constant ARBSYS = ArbSys(ARBSYS_ADDR); + + /// @dev ARBGAS_ADDR is the address of the ArbGasInfo precompile on Arbitrum. + /// @dev reference: https://github.com/OffchainLabs/nitro/blob/v2.0.14/contracts/src/precompiles/ArbGasInfo.sol#L10 + address private constant ARBGAS_ADDR = address(0x000000000000000000000000000000000000006C); + ArbGasInfo private constant ARBGAS = ArbGasInfo(ARBGAS_ADDR); + + uint256 private constant ARB_MAINNET_CHAIN_ID = 42161; + uint256 private constant ARB_GOERLI_TESTNET_CHAIN_ID = 421613; + uint256 private constant ARB_SEPOLIA_TESTNET_CHAIN_ID = 421614; + + // ------------ End Arbitrum Constants ------------ + + // ------------ Start Optimism Constants ------------ + /// @dev L1_FEE_DATA_PADDING includes 35 bytes for L1 data padding for Optimism + bytes internal constant L1_FEE_DATA_PADDING = + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; + /// @dev OVM_GASPRICEORACLE_ADDR is the address of the OVM_GasPriceOracle precompile on Optimism. + /// @dev reference: https://community.optimism.io/docs/developers/build/transaction-fees/#estimating-the-l1-data-fee + address private constant OVM_GASPRICEORACLE_ADDR = address(0x420000000000000000000000000000000000000F); + OVM_GasPriceOracle private constant OVM_GASPRICEORACLE = OVM_GasPriceOracle(OVM_GASPRICEORACLE_ADDR); + + uint256 private constant OP_MAINNET_CHAIN_ID = 10; + uint256 private constant OP_GOERLI_CHAIN_ID = 420; + uint256 private constant OP_SEPOLIA_CHAIN_ID = 11155420; + + /// @dev Base is a OP stack based rollup and follows the same L1 pricing logic as Optimism. + uint256 private constant BASE_MAINNET_CHAIN_ID = 8453; + uint256 private constant BASE_GOERLI_CHAIN_ID = 84531; + + // ------------ End Optimism Constants ------------ + + function setUp() public override { + BaseTest.setUp(); + vm.clearMockedCalls(); + } + + function testGetBlockhashArbitrum() public { + uint256[3] memory chainIds = [ARB_MAINNET_CHAIN_ID, ARB_GOERLI_TESTNET_CHAIN_ID, ARB_SEPOLIA_TESTNET_CHAIN_ID]; + bytes32[3] memory expectedBlockHashes = [keccak256("mainnet"), keccak256("goerli"), keccak256("sepolia")]; + uint256[3] memory expectedBlockNumbers = [uint256(10), 11, 12]; + for (uint256 i = 0; i < chainIds.length; i++) { + vm.chainId(chainIds[i]); + bytes32 expectedBlockHash = expectedBlockHashes[i]; + uint256 expectedBlockNumber = expectedBlockNumbers[i]; + vm.mockCall( + ARBSYS_ADDR, + abi.encodeWithSelector(ArbSys.arbBlockNumber.selector), + abi.encode(expectedBlockNumber + 1) + ); + vm.mockCall( + ARBSYS_ADDR, + abi.encodeWithSelector(ArbSys.arbBlockHash.selector, expectedBlockNumber), + abi.encodePacked(expectedBlockHash) + ); + bytes32 actualBlockHash = ChainSpecificUtil._getBlockhash(uint64(expectedBlockNumber)); + assertEq(expectedBlockHash, actualBlockHash, "incorrect blockhash"); + } + } + + function testGetBlockhashOptimism() public { + // Optimism L2 block hash is simply blockhash() + bytes32 actualBlockhash = ChainSpecificUtil._getBlockhash(uint64(block.number - 1)); + assertEq(blockhash(block.number - 1), actualBlockhash); + } + + function testGetBlockNumberArbitrum() public { + uint256[2] memory chainIds = [ARB_MAINNET_CHAIN_ID, ARB_GOERLI_TESTNET_CHAIN_ID]; + uint256[3] memory expectedBlockNumbers = [uint256(10), 11, 12]; + for (uint256 i = 0; i < chainIds.length; i++) { + vm.chainId(chainIds[i]); + uint256 expectedBlockNumber = expectedBlockNumbers[i]; + vm.mockCall(ARBSYS_ADDR, abi.encodeWithSelector(ArbSys.arbBlockNumber.selector), abi.encode(expectedBlockNumber)); + uint256 actualBlockNumber = ChainSpecificUtil._getBlockNumber(); + assertEq(expectedBlockNumber, actualBlockNumber, "incorrect block number"); + } + } + + function testGetBlockNumberOptimism() public { + // Optimism L2 block number is simply block.number + uint256 actualBlockNumber = ChainSpecificUtil._getBlockNumber(); + assertEq(block.number, actualBlockNumber); + } + + function testGetCurrentTxL1GasFeesArbitrum() public { + uint256[3] memory chainIds = [ARB_MAINNET_CHAIN_ID, ARB_GOERLI_TESTNET_CHAIN_ID, ARB_SEPOLIA_TESTNET_CHAIN_ID]; + uint256[3] memory expectedGasFees = [uint256(10 gwei), 12 gwei, 14 gwei]; + for (uint256 i = 0; i < chainIds.length; i++) { + vm.chainId(chainIds[i]); + uint256 expectedGasFee = expectedGasFees[i]; + vm.mockCall( + ARBGAS_ADDR, + abi.encodeWithSelector(ArbGasInfo.getCurrentTxL1GasFees.selector), + abi.encode(expectedGasFee) + ); + uint256 actualGasFee = ChainSpecificUtil._getCurrentTxL1GasFees(""); + assertEq(expectedGasFee, actualGasFee, "incorrect gas fees"); + } + } + + function testGetCurrentTxL1GasFeesOptimism() public { + // set optimism chain id + uint256[5] memory chainIds = [ + OP_MAINNET_CHAIN_ID, + OP_GOERLI_CHAIN_ID, + OP_SEPOLIA_CHAIN_ID, + BASE_MAINNET_CHAIN_ID, + BASE_GOERLI_CHAIN_ID + ]; + uint256[5] memory expectedGasFees = [uint256(10 gwei), 12 gwei, 14 gwei, 16 gwei, 18 gwei]; + for (uint256 i = 0; i < chainIds.length; i++) { + vm.chainId(chainIds[i]); + uint256 expectedL1Fee = expectedGasFees[i]; + bytes memory someCalldata = abi.encode(address(0), "blah", uint256(1)); + vm.mockCall( + OVM_GASPRICEORACLE_ADDR, + abi.encodeWithSelector(OVM_GasPriceOracle.getL1Fee.selector, bytes.concat(someCalldata, L1_FEE_DATA_PADDING)), + abi.encode(expectedL1Fee) + ); + uint256 actualL1Fee = ChainSpecificUtil._getCurrentTxL1GasFees(someCalldata); + assertEq(expectedL1Fee, actualL1Fee, "incorrect gas fees"); + } + } + + function testGetL1CalldataGasCostArbitrum() public { + uint256[3] memory chainIds = [ARB_MAINNET_CHAIN_ID, ARB_GOERLI_TESTNET_CHAIN_ID, ARB_SEPOLIA_TESTNET_CHAIN_ID]; + for (uint256 i = 0; i < chainIds.length; i++) { + vm.chainId(chainIds[i]); + vm.mockCall( + ARBGAS_ADDR, + abi.encodeWithSelector(ArbGasInfo.getPricesInWei.selector), + abi.encode(0, 10, 0, 0, 0, 0) + ); + + // fee = l1PricePerByte * (calldataSizeBytes + 140) + // fee = 10 * (10 + 140) = 1500 + uint256 dataFee = ChainSpecificUtil._getL1CalldataGasCost(10); + assertEq(dataFee, 1500); + } + } + + function testGetL1CalldataGasCostOptimism() public { + uint256[5] memory chainIds = [ + OP_MAINNET_CHAIN_ID, + OP_GOERLI_CHAIN_ID, + OP_SEPOLIA_CHAIN_ID, + BASE_MAINNET_CHAIN_ID, + BASE_GOERLI_CHAIN_ID + ]; + for (uint256 i = 0; i < chainIds.length; i++) { + vm.chainId(chainIds[i]); + vm.mockCall( + OVM_GASPRICEORACLE_ADDR, + abi.encodeWithSelector(bytes4(hex"519b4bd3")), // l1BaseFee() + abi.encode(10) + ); + vm.mockCall( + OVM_GASPRICEORACLE_ADDR, + abi.encodeWithSelector(bytes4(hex"0c18c162")), // overhead() + abi.encode(160) + ); + vm.mockCall( + OVM_GASPRICEORACLE_ADDR, + abi.encodeWithSelector(bytes4(hex"f45e65d8")), // scalar() + abi.encode(500_000) + ); + vm.mockCall( + OVM_GASPRICEORACLE_ADDR, + abi.encodeWithSelector(bytes4(hex"313ce567")), // decimals() + abi.encode(6) + ); + + // tx_data_gas = count_zero_bytes(tx_data) * 4 + count_non_zero_bytes(tx_data) * 16 + // tx_data_gas = 0 * 4 + 10 * 16 = 160 + // l1_data_fee = l1_gas_price * (tx_data_gas + fixed_overhead) * dynamic_overhead + // l1_data_fee = 10 * (160 + 160) * 500_000 / 1_000_000 = 1600 + uint256 dataFee = ChainSpecificUtil._getL1CalldataGasCost(10); + assertEq(dataFee, 1600); + } + } +} diff --git a/contracts/src/v0.8/vrf/test/TrustedBlockhashStore.t.sol b/contracts/src/v0.8/vrf/test/TrustedBlockhashStore.t.sol new file mode 100644 index 00000000..ec47f481 --- /dev/null +++ b/contracts/src/v0.8/vrf/test/TrustedBlockhashStore.t.sol @@ -0,0 +1,89 @@ +pragma solidity 0.8.19; + +import "./BaseTest.t.sol"; +import {TrustedBlockhashStore} from "../dev/TrustedBlockhashStore.sol"; +import {console} from "forge-std/console.sol"; + +contract TrustedBlockhashStoreTest is BaseTest { + address internal constant LINK_WHALE = 0xD883a6A1C22fC4AbFE938a5aDF9B2Cc31b1BF18B; + address internal constant LINK_WHALE_2 = 0xe9b2C5A6D9bA93dD354783a9De0a265da7551a20; + TrustedBlockhashStore bhs; + uint256 unreachableBlockNumber = 5; + bytes32 unreachableBlockhash; + + function setUp() public override { + BaseTest.setUp(); + + // Get the blockhash for a block that later becomes unreachable in the EVM. + vm.roll(10); + unreachableBlockhash = blockhash(unreachableBlockNumber); + + // Fund our users. + vm.roll(1000); + vm.deal(LINK_WHALE, 10_000 ether); + changePrank(LINK_WHALE); + + address[] memory whitelist = new address[](1); + whitelist[0] = LINK_WHALE; + bhs = new TrustedBlockhashStore(whitelist); + } + + function testGenericBHSFunctions() public { + // Should store. + uint256 blockNumber = 999; + bhs.store(blockNumber); + assertEq(bhs.getBlockhash(blockNumber), blockhash(blockNumber)); + + // Should store earliest. + uint256 earliestBlockNumber = block.number - 256; + bhs.storeEarliest(); + assertEq(bhs.getBlockhash(earliestBlockNumber), blockhash(earliestBlockNumber)); + } + + function testTrustedBHSFunctions() public { + uint256 recentBlockNumber = 999; + + // Assume that the EVM cannot access the blockhash for block 5. + uint256 unreachableBlock = 5; + assertEq(blockhash(unreachableBlock), 0); + + // Store blockhash from whitelisted address; + uint256[] memory invalidBlockNums = new uint256[](0); + uint256[] memory blockNums = new uint256[](1); + blockNums[0] = unreachableBlock; + bytes32[] memory blockhashes = new bytes32[](1); + blockhashes[0] = unreachableBlockhash; + + // Should not be able to store with invalid recent blockhash + vm.expectRevert(TrustedBlockhashStore.InvalidRecentBlockhash.selector); + bhs.storeTrusted(blockNums, blockhashes, recentBlockNumber, blockhash(998)); + + // Should not be able to store or change whitelist for non-whitelisted address. + changePrank(LINK_WHALE_2); + vm.expectRevert(TrustedBlockhashStore.NotInWhitelist.selector); + bhs.storeTrusted(blockNums, blockhashes, recentBlockNumber, blockhash(recentBlockNumber)); + vm.expectRevert("Only callable by owner"); + bhs.setWhitelist(new address[](0)); + + // Should not store for a mismatched list of block numbers and hashes. + changePrank(LINK_WHALE); + vm.expectRevert(TrustedBlockhashStore.InvalidTrustedBlockhashes.selector); + bhs.storeTrusted(invalidBlockNums, blockhashes, recentBlockNumber, blockhash(recentBlockNumber)); + + // Should store unreachable blocks via whitelisted address. + bhs.storeTrusted(blockNums, blockhashes, recentBlockNumber, blockhash(recentBlockNumber)); + assertEq(bhs.getBlockhash(unreachableBlock), unreachableBlockhash); + + // Change whitelist. Assert that the old whitelisted address can no longer store, + // but the new one can. + address[] memory newWhitelist = new address[](1); + newWhitelist[0] = LINK_WHALE_2; + bhs.setWhitelist(newWhitelist); + + vm.expectRevert(TrustedBlockhashStore.NotInWhitelist.selector); + bhs.storeTrusted(blockNums, blockhashes, recentBlockNumber, blockhash(recentBlockNumber)); + + changePrank(LINK_WHALE_2); + bhs.storeTrusted(blockNums, blockhashes, recentBlockNumber, blockhash(recentBlockNumber)); + } +} diff --git a/contracts/src/v0.8/vrf/test/VRFCoordinatorV2Mock.t.sol b/contracts/src/v0.8/vrf/test/VRFCoordinatorV2Mock.t.sol new file mode 100644 index 00000000..1716118b --- /dev/null +++ b/contracts/src/v0.8/vrf/test/VRFCoordinatorV2Mock.t.sol @@ -0,0 +1,381 @@ +pragma solidity 0.8.6; + +import "./BaseTest.t.sol"; +import {VRF} from "../VRF.sol"; +import {MockLinkToken} from "../../mocks/MockLinkToken.sol"; +import {MockV3Aggregator} from "../../tests/MockV3Aggregator.sol"; +import {VRFCoordinatorV2Mock} from "../mocks/VRFCoordinatorV2Mock.sol"; +import {VRFConsumerV2} from "../testhelpers/VRFConsumerV2.sol"; + +contract VRFCoordinatorV2MockTest is BaseTest { + MockLinkToken internal s_linkToken; + MockV3Aggregator internal s_linkEthFeed; + VRFCoordinatorV2Mock internal s_vrfCoordinatorV2Mock; + VRFConsumerV2 internal s_vrfConsumerV2; + address internal s_subOwner = address(1234); + address internal s_randomOwner = address(4567); + + // VRF KeyV2 generated from a node; not sensitive information. + // The secret key used to generate this key is: 10. + bytes internal constant UNCOMPRESSED_PUBLIC_KEY = + hex"a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7"; + bytes internal constant COMPRESSED_PUBLIC_KEY = + hex"a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c701"; + bytes32 internal constant KEY_HASH = hex"9f2353bde94264dbc3d554a94cceba2d7d2b4fdce4304d3e09a1fea9fbeb1528"; + + uint32 internal constant DEFAULT_CALLBACK_GAS_LIMIT = 500_000; + uint16 internal constant DEFAULT_REQUEST_CONFIRMATIONS = 3; + uint32 internal constant DEFAULT_NUM_WORDS = 1; + + uint96 pointOneLink = 0.1 ether; + uint96 oneLink = 1 ether; + + event SubscriptionCreated(uint64 indexed subId, address owner); + event SubscriptionFunded(uint64 indexed subId, uint256 oldBalance, uint256 newBalance); + event SubscriptionCanceled(uint64 indexed subId, address to, uint256 amount); + event ConsumerAdded(uint64 indexed subId, address consumer); + event ConsumerRemoved(uint64 indexed subId, address consumer); + event RandomWordsRequested( + bytes32 indexed keyHash, + uint256 requestId, + uint256 preSeed, + uint64 indexed subId, + uint16 minimumRequestConfirmations, + uint32 callbackGasLimit, + uint32 numWords, + address indexed sender + ); + event RandomWordsFulfilled(uint256 indexed requestId, uint256 outputSeed, uint96 payment, bool success); + + function setUp() public override { + BaseTest.setUp(); + + // Fund our users. + vm.roll(1); + vm.deal(OWNER, 10_000 ether); + vm.deal(s_subOwner, 20 ether); + + // Deploy link token and link/eth feed. + s_linkToken = new MockLinkToken(); + s_linkEthFeed = new MockV3Aggregator(18, 500000000000000000); // .5 ETH (good for testing) + + // Deploy coordinator and consumer. + s_vrfCoordinatorV2Mock = new VRFCoordinatorV2Mock( + pointOneLink, + 1_000_000_000 // 0.000000001 LINK per gas + ); + address coordinatorAddr = address(s_vrfCoordinatorV2Mock); + s_vrfConsumerV2 = new VRFConsumerV2(coordinatorAddr, address(s_linkToken)); + + s_vrfCoordinatorV2Mock.setConfig(); + } + + function testCreateSubscription() public { + vm.startPrank(s_subOwner); + vm.expectEmit( + true, // no first indexed topic + false, // no second indexed topic + false, // no third indexed topic + true // check data (target coordinator address) + ); + emit SubscriptionCreated(1, s_subOwner); + uint64 subId = s_vrfCoordinatorV2Mock.createSubscription(); + assertEq(subId, 1); + + (uint96 balance, uint64 reqCount, address owner, address[] memory consumers) = s_vrfCoordinatorV2Mock + .getSubscription(subId); + assertEq(balance, 0); + assertEq(reqCount, 0); + assertEq(owner, s_subOwner); + assertEq(consumers.length, 0); + // s_testCoordinator.fundSubscriptionWithEth{value: 10 ether}(subId); + + // Test if subId increments + vm.expectEmit(true, false, false, true); + emit SubscriptionCreated(2, s_subOwner); + subId = s_vrfCoordinatorV2Mock.createSubscription(); + assertEq(subId, 2); + vm.stopPrank(); + } + + function testAddConsumer() public { + vm.startPrank(s_subOwner); + uint64 subId = s_vrfCoordinatorV2Mock.createSubscription(); + vm.expectEmit(true, false, false, true); + emit ConsumerAdded(subId, address(s_vrfConsumerV2)); + s_vrfCoordinatorV2Mock.addConsumer(subId, address(s_vrfConsumerV2)); + + (uint96 balance, uint64 reqCount, address owner, address[] memory consumers) = s_vrfCoordinatorV2Mock + .getSubscription(subId); + assertEq(balance, 0); + assertEq(reqCount, 0); + assertEq(owner, s_subOwner); + assertEq(consumers.length, 1); + assertEq(consumers[0], address(s_vrfConsumerV2)); + vm.stopPrank(); + } + + // cannot add a consumer to a nonexistent subscription + function testAddConsumerToInvalidSub() public { + vm.startPrank(s_subOwner); + bytes4 reason = bytes4(keccak256("InvalidSubscription()")); + vm.expectRevert(toBytes(reason)); + s_vrfCoordinatorV2Mock.addConsumer(1, address(s_vrfConsumerV2)); + vm.stopPrank(); + } + + // cannot add more than the consumer maximum + function testAddMaxConsumers() public { + vm.startPrank(s_subOwner); + uint64 subId = s_vrfCoordinatorV2Mock.createSubscription(); + // Add 100 consumers + for (uint64 i = 101; i <= 200; ++i) { + s_vrfCoordinatorV2Mock.addConsumer(subId, address(bytes20(keccak256(abi.encodePacked(i))))); + } + // Adding 101th consumer should revert + bytes4 reason = bytes4(keccak256("TooManyConsumers()")); + vm.expectRevert(toBytes(reason)); + s_vrfCoordinatorV2Mock.addConsumer(subId, address(s_vrfConsumerV2)); + vm.stopPrank(); + } + + // can remove a consumer from a subscription + function testRemoveConsumerFromSub() public { + vm.startPrank(s_subOwner); + uint64 subId = s_vrfCoordinatorV2Mock.createSubscription(); + + s_vrfCoordinatorV2Mock.addConsumer(subId, address(s_vrfConsumerV2)); + + (, , , address[] memory consumers) = s_vrfCoordinatorV2Mock.getSubscription(subId); + assertEq(consumers.length, 1); + assertEq(consumers[0], address(s_vrfConsumerV2)); + + vm.expectEmit(true, false, false, true); + emit ConsumerRemoved(subId, address(s_vrfConsumerV2)); + s_vrfCoordinatorV2Mock.removeConsumer(subId, address(s_vrfConsumerV2)); + + vm.stopPrank(); + } + + // cannot remove a consumer from a nonexistent subscription + function testRemoveConsumerFromInvalidSub() public { + vm.startPrank(s_subOwner); + bytes4 reason = bytes4(keccak256("InvalidSubscription()")); + vm.expectRevert(toBytes(reason)); + s_vrfCoordinatorV2Mock.removeConsumer(1, address(s_vrfConsumerV2)); + vm.stopPrank(); + } + + // cannot remove a consumer after it is already removed + function testRemoveConsumerAgain() public { + vm.startPrank(s_subOwner); + uint64 subId = s_vrfCoordinatorV2Mock.createSubscription(); + + s_vrfCoordinatorV2Mock.addConsumer(subId, address(s_vrfConsumerV2)); + + (, , , address[] memory consumers) = s_vrfCoordinatorV2Mock.getSubscription(subId); + assertEq(consumers.length, 1); + assertEq(consumers[0], address(s_vrfConsumerV2)); + + vm.expectEmit(true, false, false, true); + emit ConsumerRemoved(subId, address(s_vrfConsumerV2)); + s_vrfCoordinatorV2Mock.removeConsumer(subId, address(s_vrfConsumerV2)); + + // Removing consumer again should revert with InvalidConsumer + bytes4 reason = bytes4(keccak256("InvalidConsumer()")); + vm.expectRevert(toBytes(reason)); + s_vrfCoordinatorV2Mock.removeConsumer(subId, address(s_vrfConsumerV2)); + vm.stopPrank(); + } + + // can fund a subscription + function testFundSubscription() public { + vm.startPrank(s_subOwner); + uint64 subId = s_vrfCoordinatorV2Mock.createSubscription(); + + vm.expectEmit(true, false, false, true); + emit SubscriptionFunded(subId, 0, oneLink); + s_vrfCoordinatorV2Mock.fundSubscription(subId, oneLink); + + (uint96 balance, , , address[] memory consumers) = s_vrfCoordinatorV2Mock.getSubscription(subId); + assertEq(balance, oneLink); + assertEq(consumers.length, 0); + vm.stopPrank(); + } + + // cannot fund a nonexistent subscription + function testFundInvalidSubscription() public { + vm.startPrank(s_subOwner); + + // Removing consumer again should revert with InvalidConsumer + bytes4 reason = bytes4(keccak256("InvalidSubscription()")); + vm.expectRevert(toBytes(reason)); + s_vrfCoordinatorV2Mock.removeConsumer(1, address(s_vrfConsumerV2)); + + vm.stopPrank(); + } + + // can cancel a subscription + function testCancelSubscription() public { + vm.startPrank(s_subOwner); + uint64 subId = s_vrfCoordinatorV2Mock.createSubscription(); + + s_vrfCoordinatorV2Mock.fundSubscription(subId, oneLink); + + vm.expectEmit(true, false, false, true); + emit SubscriptionCanceled(subId, s_subOwner, oneLink); + s_vrfCoordinatorV2Mock.cancelSubscription(subId, s_subOwner); + + bytes4 reason = bytes4(keccak256("InvalidSubscription()")); + vm.expectRevert(toBytes(reason)); + s_vrfCoordinatorV2Mock.getSubscription(subId); + + vm.stopPrank(); + } + + // fails to fulfill without being a valid consumer + function testRequestRandomWordsInvalidConsumer() public { + vm.startPrank(s_subOwner); + uint64 subId = s_vrfCoordinatorV2Mock.createSubscription(); + + s_vrfCoordinatorV2Mock.fundSubscription(subId, oneLink); + + bytes4 reason = bytes4(keccak256("InvalidConsumer()")); + vm.expectRevert(toBytes(reason)); + s_vrfCoordinatorV2Mock.requestRandomWords( + KEY_HASH, + subId, + DEFAULT_REQUEST_CONFIRMATIONS, + DEFAULT_CALLBACK_GAS_LIMIT, + DEFAULT_NUM_WORDS + ); + vm.stopPrank(); + } + + // fails to fulfill with insufficient funds + function testRequestRandomWordsInsufficientFunds() public { + vm.startPrank(s_subOwner); + uint64 subId = s_vrfCoordinatorV2Mock.createSubscription(); + + address consumerAddr = address(s_vrfConsumerV2); + s_vrfCoordinatorV2Mock.addConsumer(subId, address(s_vrfConsumerV2)); + + vm.stopPrank(); + + vm.startPrank(consumerAddr); + + vm.expectEmit(true, false, false, true); + emit RandomWordsRequested( + KEY_HASH, + 1, + 100, + subId, + DEFAULT_REQUEST_CONFIRMATIONS, + DEFAULT_CALLBACK_GAS_LIMIT, + DEFAULT_NUM_WORDS, + address(s_subOwner) + ); + uint256 reqId = s_vrfCoordinatorV2Mock.requestRandomWords( + KEY_HASH, + subId, + DEFAULT_REQUEST_CONFIRMATIONS, + DEFAULT_CALLBACK_GAS_LIMIT, + DEFAULT_NUM_WORDS + ); + + bytes4 reason = bytes4(keccak256("InsufficientBalance()")); + vm.expectRevert(toBytes(reason)); + s_vrfCoordinatorV2Mock.fulfillRandomWords(reqId, consumerAddr); + + vm.stopPrank(); + } + + // can request and fulfill [ @skip-coverage ] + function testRequestRandomWordsHappyPath() public { + vm.startPrank(s_subOwner); + uint64 subId = s_vrfCoordinatorV2Mock.createSubscription(); + + s_vrfCoordinatorV2Mock.fundSubscription(subId, oneLink); + + address consumerAddr = address(s_vrfConsumerV2); + s_vrfCoordinatorV2Mock.addConsumer(subId, consumerAddr); + + vm.expectEmit(true, false, false, true); + emit RandomWordsRequested( + KEY_HASH, + 1, + 100, + subId, + DEFAULT_REQUEST_CONFIRMATIONS, + DEFAULT_CALLBACK_GAS_LIMIT, + DEFAULT_NUM_WORDS, + address(s_subOwner) + ); + uint256 reqId = s_vrfConsumerV2.requestRandomness( + KEY_HASH, + subId, + DEFAULT_REQUEST_CONFIRMATIONS, + DEFAULT_CALLBACK_GAS_LIMIT, + DEFAULT_NUM_WORDS + ); + + vm.expectEmit(true, false, false, true); + emit RandomWordsFulfilled(reqId, 1, 100090236000000000, true); + s_vrfCoordinatorV2Mock.fulfillRandomWords(reqId, consumerAddr); + + vm.stopPrank(); + } + + // Correctly allows for user override of fulfillRandomWords [ @skip-coverage ] + function testRequestRandomWordsUserOverride() public { + vm.startPrank(s_subOwner); + uint64 subId = s_vrfCoordinatorV2Mock.createSubscription(); + + s_vrfCoordinatorV2Mock.fundSubscription(subId, oneLink); + + address consumerAddr = address(s_vrfConsumerV2); + s_vrfCoordinatorV2Mock.addConsumer(subId, consumerAddr); + + vm.expectEmit(true, false, false, true); + emit RandomWordsRequested( + KEY_HASH, + 1, + 100, + subId, + DEFAULT_REQUEST_CONFIRMATIONS, + DEFAULT_CALLBACK_GAS_LIMIT, + 2, + address(s_subOwner) + ); + uint256 reqId = s_vrfConsumerV2.requestRandomness( + KEY_HASH, + subId, + DEFAULT_REQUEST_CONFIRMATIONS, + DEFAULT_CALLBACK_GAS_LIMIT, + 2 + ); + + bytes4 reason = bytes4(keccak256("InvalidRandomWords()")); + vm.expectRevert(toBytes(reason)); + uint256[] memory words1 = new uint256[](5); + words1[0] = 1; + words1[1] = 2; + words1[2] = 3; + words1[3] = 4; + words1[4] = 5; + s_vrfCoordinatorV2Mock.fulfillRandomWordsWithOverride(reqId, consumerAddr, uint256[](words1)); + + vm.expectEmit(true, false, false, true); + uint256[] memory words2 = new uint256[](2); + words1[0] = 2533; + words1[1] = 1768; + emit RandomWordsFulfilled(reqId, 1, 100072314000000000, true); + s_vrfCoordinatorV2Mock.fulfillRandomWordsWithOverride(reqId, consumerAddr, words2); + + vm.stopPrank(); + } + + function toBytes(bytes4 _data) public pure returns (bytes memory) { + return abi.encodePacked(_data); + } +} diff --git a/contracts/src/v0.8/vrf/test/VRFCoordinatorV2Plus_Migration.t.sol b/contracts/src/v0.8/vrf/test/VRFCoordinatorV2Plus_Migration.t.sol new file mode 100644 index 00000000..31585656 --- /dev/null +++ b/contracts/src/v0.8/vrf/test/VRFCoordinatorV2Plus_Migration.t.sol @@ -0,0 +1,352 @@ +pragma solidity 0.8.19; + +import "./BaseTest.t.sol"; +import {VRFCoordinatorV2Plus_V2Example} from "../dev/testhelpers/VRFCoordinatorV2Plus_V2Example.sol"; +import {ExposedVRFCoordinatorV2_5} from "../dev/testhelpers/ExposedVRFCoordinatorV2_5.sol"; +import {VRFCoordinatorV2_5} from "../dev/VRFCoordinatorV2_5.sol"; +import {SubscriptionAPI} from "../dev/SubscriptionAPI.sol"; +import {VRFV2PlusConsumerExample} from "../dev/testhelpers/VRFV2PlusConsumerExample.sol"; +import {MockLinkToken} from "../../mocks/MockLinkToken.sol"; +import {MockV3Aggregator} from "../../tests/MockV3Aggregator.sol"; +import {VRFV2PlusMaliciousMigrator} from "../dev/testhelpers/VRFV2PlusMaliciousMigrator.sol"; + +contract VRFCoordinatorV2Plus_Migration is BaseTest { + uint256 internal constant DEFAULT_LINK_FUNDING = 10 ether; // 10 LINK + uint256 internal constant DEFAULT_NATIVE_FUNDING = 50 ether; // 50 ETH + uint32 internal constant DEFAULT_CALLBACK_GAS_LIMIT = 50_000; + uint16 internal constant DEFAULT_REQUEST_CONFIRMATIONS = 3; + uint32 internal constant DEFAULT_NUM_WORDS = 1; + // VRF KeyV2 generated from a node; not sensitive information. + // The secret key used to generate this key is: 10. + bytes internal constant UNCOMPRESSED_PUBLIC_KEY = + hex"a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7"; + bytes internal constant COMPRESSED_PUBLIC_KEY = + hex"a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c701"; + bytes32 internal constant KEY_HASH = hex"9f2353bde94264dbc3d554a94cceba2d7d2b4fdce4304d3e09a1fea9fbeb1528"; + uint64 internal constant GAS_LANE_MAX_GAS = 5000 gwei; + + ExposedVRFCoordinatorV2_5 v1Coordinator; + VRFCoordinatorV2Plus_V2Example v2Coordinator; + ExposedVRFCoordinatorV2_5 v1Coordinator_noLink; + VRFCoordinatorV2Plus_V2Example v2Coordinator_noLink; + uint256 subId; + uint256 subId_noLink; + VRFV2PlusConsumerExample testConsumer; + VRFV2PlusConsumerExample testConsumer_noLink; + MockLinkToken linkToken; + address linkTokenAddr; + MockV3Aggregator linkNativeFeed; + address v1CoordinatorAddr; + address v2CoordinatorAddr; + address v1CoordinatorAddr_noLink; + address v2CoordinatorAddr_noLink; + + event CoordinatorRegistered(address coordinatorAddress); + event CoordinatorDeregistered(address coordinatorAddress); + event MigrationCompleted(address newCoordinator, uint256 subId); + + function setUp() public override { + BaseTest.setUp(); + vm.deal(OWNER, 100 ether); + address bhs = makeAddr("bhs"); + v1Coordinator = new ExposedVRFCoordinatorV2_5(bhs); + v1Coordinator_noLink = new ExposedVRFCoordinatorV2_5(bhs); + subId = v1Coordinator.createSubscription(); + subId_noLink = v1Coordinator_noLink.createSubscription(); + linkToken = new MockLinkToken(); + linkNativeFeed = new MockV3Aggregator(18, 500000000000000000); // .5 ETH (good for testing) + v1Coordinator.setLINKAndLINKNativeFeed(address(linkToken), address(linkNativeFeed)); + linkTokenAddr = address(linkToken); + v2Coordinator = new VRFCoordinatorV2Plus_V2Example(address(linkToken), address(v1Coordinator)); + v2Coordinator_noLink = new VRFCoordinatorV2Plus_V2Example(address(0), address(v1Coordinator_noLink)); + v1CoordinatorAddr = address(v1Coordinator); + v2CoordinatorAddr = address(v2Coordinator); + v1CoordinatorAddr_noLink = address(v1Coordinator_noLink); + v2CoordinatorAddr_noLink = address(v2Coordinator_noLink); + + vm.expectEmit( + false, // no first indexed topic + false, // no second indexed topic + false, // no third indexed topic + true // check data (target coordinator address) + ); + emit CoordinatorRegistered(v2CoordinatorAddr); + v1Coordinator.registerMigratableCoordinator(v2CoordinatorAddr); + assertTrue(v1Coordinator.isTargetRegisteredExternal(v2CoordinatorAddr)); + + vm.expectEmit( + false, // no first indexed topic + false, // no second indexed topic + false, // no third indexed topic + true // check data (target coordinator address) + ); + emit CoordinatorRegistered(v2CoordinatorAddr_noLink); + v1Coordinator_noLink.registerMigratableCoordinator(v2CoordinatorAddr_noLink); + assertTrue(v1Coordinator_noLink.isTargetRegisteredExternal(v2CoordinatorAddr_noLink)); + + testConsumer = new VRFV2PlusConsumerExample(address(v1Coordinator), address(linkToken)); + testConsumer_noLink = new VRFV2PlusConsumerExample(address(v1Coordinator_noLink), address(0)); + v1Coordinator.setConfig( + DEFAULT_REQUEST_CONFIRMATIONS, + DEFAULT_CALLBACK_GAS_LIMIT, + 600, + 10_000, + 20_000, + 500_000, // fulfillmentFlatFeeNativePPM + 100_000, // fulfillmentFlatFeeLinkDiscountPPM + 15, // nativePremiumPercentage + 10 // linkPremiumPercentage + ); + v1Coordinator_noLink.setConfig( + DEFAULT_REQUEST_CONFIRMATIONS, + DEFAULT_CALLBACK_GAS_LIMIT, + 600, + 10_000, + 20_000, + 500_000, // fulfillmentFlatFeeNativePPM + 100_000, // fulfillmentFlatFeeLinkDiscountPPM + 15, // nativePremiumPercentage + 10 // linkPremiumPercentage + ); + registerProvingKey(); + testConsumer.setCoordinator(v1CoordinatorAddr); + testConsumer_noLink.setCoordinator(v1CoordinatorAddr_noLink); + } + + function testDeregister() public { + vm.expectEmit( + false, // no first indexed topic + false, // no second indexed topic + false, // no third indexed topic + true // check data (target coordinator address) + ); + emit CoordinatorDeregistered(v2CoordinatorAddr); + v1Coordinator.deregisterMigratableCoordinator(v2CoordinatorAddr); + assertFalse(v1Coordinator.isTargetRegisteredExternal(v2CoordinatorAddr)); + + vm.expectRevert(abi.encodeWithSelector(VRFCoordinatorV2_5.CoordinatorNotRegistered.selector, v2CoordinatorAddr)); + v1Coordinator.migrate(subId, v2CoordinatorAddr); + + // test register/deregister multiple coordinators + address v3CoordinatorAddr = makeAddr("v3Coordinator"); + v1Coordinator.registerMigratableCoordinator(v2CoordinatorAddr); + v1Coordinator.registerMigratableCoordinator(v3CoordinatorAddr); + assertTrue(v1Coordinator.isTargetRegisteredExternal(v2CoordinatorAddr)); + assertTrue(v1Coordinator.isTargetRegisteredExternal(v3CoordinatorAddr)); + + v1Coordinator.deregisterMigratableCoordinator(v3CoordinatorAddr); + assertTrue(v1Coordinator.isTargetRegisteredExternal(v2CoordinatorAddr)); + assertFalse(v1Coordinator.isTargetRegisteredExternal(v3CoordinatorAddr)); + + v1Coordinator.registerMigratableCoordinator(v3CoordinatorAddr); + assertTrue(v1Coordinator.isTargetRegisteredExternal(v2CoordinatorAddr)); + assertTrue(v1Coordinator.isTargetRegisteredExternal(v3CoordinatorAddr)); + + v1Coordinator.deregisterMigratableCoordinator(v2CoordinatorAddr); + assertFalse(v1Coordinator.isTargetRegisteredExternal(v2CoordinatorAddr)); + assertTrue(v1Coordinator.isTargetRegisteredExternal(v3CoordinatorAddr)); + + v1Coordinator.deregisterMigratableCoordinator(v3CoordinatorAddr); + assertFalse(v1Coordinator.isTargetRegisteredExternal(v2CoordinatorAddr)); + assertFalse(v1Coordinator.isTargetRegisteredExternal(v3CoordinatorAddr)); + } + + function testMigration() public { + linkToken.transferAndCall(v1CoordinatorAddr, DEFAULT_LINK_FUNDING, abi.encode(subId)); + v1Coordinator.fundSubscriptionWithNative{value: DEFAULT_NATIVE_FUNDING}(subId); + v1Coordinator.addConsumer(subId, address(testConsumer)); + + // subscription exists in V1 coordinator before migration + (uint96 balance, uint96 nativeBalance, uint64 reqCount, address owner, address[] memory consumers) = v1Coordinator + .getSubscription(subId); + assertEq(balance, DEFAULT_LINK_FUNDING); + assertEq(nativeBalance, DEFAULT_NATIVE_FUNDING); + assertEq(owner, address(OWNER)); + assertEq(consumers.length, 1); + assertEq(consumers[0], address(testConsumer)); + + assertEq(v1Coordinator.s_totalBalance(), DEFAULT_LINK_FUNDING); + assertEq(v1Coordinator.s_totalNativeBalance(), DEFAULT_NATIVE_FUNDING); + + // Update consumer to point to the new coordinator + vm.expectEmit( + false, // no first indexed field + false, // no second indexed field + false, // no third indexed field + true // check data fields + ); + emit MigrationCompleted(v2CoordinatorAddr, subId); + v1Coordinator.migrate(subId, v2CoordinatorAddr); + + // subscription no longer exists in v1 coordinator after migration + vm.expectRevert(SubscriptionAPI.InvalidSubscription.selector); + v1Coordinator.getSubscription(subId); + assertEq(v1Coordinator.s_totalBalance(), 0); + assertEq(v1Coordinator.s_totalNativeBalance(), 0); + assertEq(linkToken.balanceOf(v1CoordinatorAddr), 0); + assertEq(v1CoordinatorAddr.balance, 0); + + // subscription exists in v2 coordinator + (balance, nativeBalance, reqCount, owner, consumers) = v2Coordinator.getSubscription(subId); + assertEq(owner, address(OWNER)); + assertEq(consumers.length, 1); + assertEq(consumers[0], address(testConsumer)); + assertEq(reqCount, 0); + assertEq(balance, DEFAULT_LINK_FUNDING); + assertEq(nativeBalance, DEFAULT_NATIVE_FUNDING); + assertEq(v2Coordinator.s_totalLinkBalance(), DEFAULT_LINK_FUNDING); + assertEq(v2Coordinator.s_totalNativeBalance(), DEFAULT_NATIVE_FUNDING); + assertEq(linkToken.balanceOf(v2CoordinatorAddr), DEFAULT_LINK_FUNDING); + assertEq(v2CoordinatorAddr.balance, DEFAULT_NATIVE_FUNDING); + + // calling migrate again on V1 coordinator should fail + vm.expectRevert(SubscriptionAPI.InvalidSubscription.selector); + v1Coordinator.migrate(subId, v2CoordinatorAddr); + + // test request still works after migration + testConsumer.requestRandomWords( + DEFAULT_CALLBACK_GAS_LIMIT, + DEFAULT_REQUEST_CONFIRMATIONS, + DEFAULT_NUM_WORDS, + KEY_HASH, + false + ); + assertEq(testConsumer.s_recentRequestId(), 1); + + v2Coordinator.fulfillRandomWords(testConsumer.s_recentRequestId()); + assertEq( + testConsumer.getRandomness(testConsumer.s_recentRequestId(), 0), + v2Coordinator.generateFakeRandomness(testConsumer.s_recentRequestId())[0] + ); + } + + function testMigrationNoLink() public { + v1Coordinator_noLink.fundSubscriptionWithNative{value: DEFAULT_NATIVE_FUNDING}(subId_noLink); + v1Coordinator_noLink.addConsumer(subId_noLink, address(testConsumer_noLink)); + + // subscription exists in V1 coordinator before migration + ( + uint96 balance, + uint96 nativeBalance, + uint64 reqCount, + address owner, + address[] memory consumers + ) = v1Coordinator_noLink.getSubscription(subId_noLink); + assertEq(balance, 0); + assertEq(nativeBalance, DEFAULT_NATIVE_FUNDING); + assertEq(owner, address(OWNER)); + assertEq(consumers.length, 1); + assertEq(consumers[0], address(testConsumer_noLink)); + + assertEq(v1Coordinator_noLink.s_totalBalance(), 0); + assertEq(v1Coordinator_noLink.s_totalNativeBalance(), DEFAULT_NATIVE_FUNDING); + + // Update consumer to point to the new coordinator + vm.expectEmit( + false, // no first indexed field + false, // no second indexed field + false, // no third indexed field + true // check data fields + ); + emit MigrationCompleted(v2CoordinatorAddr_noLink, subId_noLink); + v1Coordinator_noLink.migrate(subId_noLink, v2CoordinatorAddr_noLink); + + // subscription no longer exists in v1 coordinator after migration + vm.expectRevert(SubscriptionAPI.InvalidSubscription.selector); + v1Coordinator_noLink.getSubscription(subId); + assertEq(v1Coordinator_noLink.s_totalBalance(), 0); + assertEq(v1Coordinator_noLink.s_totalNativeBalance(), 0); + assertEq(linkToken.balanceOf(v1CoordinatorAddr_noLink), 0); + assertEq(v1CoordinatorAddr_noLink.balance, 0); + + // subscription exists in v2 coordinator + (balance, nativeBalance, reqCount, owner, consumers) = v2Coordinator_noLink.getSubscription(subId_noLink); + assertEq(owner, address(OWNER)); + assertEq(consumers.length, 1); + assertEq(consumers[0], address(testConsumer_noLink)); + assertEq(reqCount, 0); + assertEq(balance, 0); + assertEq(nativeBalance, DEFAULT_NATIVE_FUNDING); + assertEq(v2Coordinator_noLink.s_totalLinkBalance(), 0); + assertEq(v2Coordinator_noLink.s_totalNativeBalance(), DEFAULT_NATIVE_FUNDING); + assertEq(linkToken.balanceOf(v2CoordinatorAddr_noLink), 0); + assertEq(v2CoordinatorAddr_noLink.balance, DEFAULT_NATIVE_FUNDING); + + // calling migrate again on V1 coordinator should fail + vm.expectRevert(SubscriptionAPI.InvalidSubscription.selector); + v1Coordinator_noLink.migrate(subId_noLink, v2CoordinatorAddr_noLink); + + // test request still works after migration + testConsumer_noLink.requestRandomWords( + DEFAULT_CALLBACK_GAS_LIMIT, + DEFAULT_REQUEST_CONFIRMATIONS, + DEFAULT_NUM_WORDS, + KEY_HASH, + false + ); + assertEq(testConsumer_noLink.s_recentRequestId(), 1); + + v2Coordinator_noLink.fulfillRandomWords(testConsumer_noLink.s_recentRequestId()); + assertEq( + testConsumer_noLink.getRandomness(testConsumer_noLink.s_recentRequestId(), 0), + v2Coordinator_noLink.generateFakeRandomness(testConsumer_noLink.s_recentRequestId())[0] + ); + } + + function testMigrateRevertsWhenInvalidCoordinator() external { + address invalidCoordinator = makeAddr("invalidCoordinator"); + + vm.expectRevert( + abi.encodeWithSelector(VRFCoordinatorV2_5.CoordinatorNotRegistered.selector, address(invalidCoordinator)) + ); + v1Coordinator.migrate(subId, invalidCoordinator); + } + + function testMigrateRevertsWhenInvalidCaller() external { + changePrank(makeAddr("invalidCaller")); + vm.expectRevert(bytes("Not subscription owner")); + v1Coordinator.migrate(subId, v2CoordinatorAddr); + } + + function testMigrateRevertsWhenPendingFulfillment() external { + v1Coordinator.addConsumer(subId, address(testConsumer)); + testConsumer.setSubId(subId); + testConsumer.requestRandomWords( + DEFAULT_CALLBACK_GAS_LIMIT, + DEFAULT_REQUEST_CONFIRMATIONS, + DEFAULT_NUM_WORDS, + KEY_HASH, + false + ); + + vm.expectRevert(bytes("Pending request exists")); + v1Coordinator.migrate(subId, v2CoordinatorAddr); + } + + function testMigrateRevertsWhenReentrant() public { + // deploy malicious contracts, subscriptions + address maliciousUser = makeAddr("maliciousUser"); + changePrank(maliciousUser); + uint256 maliciousSubId = v1Coordinator.createSubscription(); + VRFV2PlusMaliciousMigrator prankster = new VRFV2PlusMaliciousMigrator(address(v1Coordinator)); + v1Coordinator.addConsumer(maliciousSubId, address(prankster)); + + // try to migrate malicious subscription, should fail + vm.expectRevert(abi.encodeWithSelector(SubscriptionAPI.Reentrant.selector)); + v1Coordinator.migrate(maliciousSubId, v2CoordinatorAddr); + } + + function registerProvingKey() public { + uint256[2] memory uncompressedKeyParts = this.getProvingKeyParts(UNCOMPRESSED_PUBLIC_KEY); + v1Coordinator.registerProvingKey(uncompressedKeyParts, GAS_LANE_MAX_GAS); + v1Coordinator_noLink.registerProvingKey(uncompressedKeyParts, GAS_LANE_MAX_GAS); + } + + // note: Call this function via this.getProvingKeyParts to be able to pass memory as calldata and + // index over the byte array. + function getProvingKeyParts(bytes calldata uncompressedKey) public pure returns (uint256[2] memory) { + uint256 keyPart1 = uint256(bytes32(uncompressedKey[0:32])); + uint256 keyPart2 = uint256(bytes32(uncompressedKey[32:64])); + return [keyPart1, keyPart2]; + } +} diff --git a/contracts/src/v0.8/vrf/test/VRFV2Plus.t.sol b/contracts/src/v0.8/vrf/test/VRFV2Plus.t.sol new file mode 100644 index 00000000..b7c2c1f8 --- /dev/null +++ b/contracts/src/v0.8/vrf/test/VRFV2Plus.t.sol @@ -0,0 +1,1099 @@ +pragma solidity 0.8.19; + +import "./BaseTest.t.sol"; +import {VRF} from "../VRF.sol"; +import {MockLinkToken} from "../../mocks/MockLinkToken.sol"; +import {MockV3Aggregator} from "../../tests/MockV3Aggregator.sol"; +import {ExposedVRFCoordinatorV2_5} from "../dev/testhelpers/ExposedVRFCoordinatorV2_5.sol"; +import {VRFCoordinatorV2_5} from "../dev/VRFCoordinatorV2_5.sol"; +import {SubscriptionAPI} from "../dev/SubscriptionAPI.sol"; +import {BlockhashStore} from "../dev/BlockhashStore.sol"; +import {VRFV2PlusConsumerExample} from "../dev/testhelpers/VRFV2PlusConsumerExample.sol"; +import {VRFV2PlusClient} from "../dev/libraries/VRFV2PlusClient.sol"; +import {VRFTypes} from "../VRFTypes.sol"; +import {console} from "forge-std/console.sol"; +import {VmSafe} from "forge-std/Vm.sol"; +import {VRFV2PlusLoadTestWithMetrics} from "../dev/testhelpers/VRFV2PlusLoadTestWithMetrics.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; // for Math.ceilDiv + +/* + * USAGE INSTRUCTIONS: + * To add new tests/proofs, uncomment the "console.sol" import from foundry, and gather key fields + * from your VRF request. + * Then, pass your request info into the generate-proof-v2-plus script command + * located in /core/scripts/vrfv2/testnet/proofs.go to generate a proof that can be tested on-chain. + **/ + +contract VRFV2Plus is BaseTest { + address internal constant LINK_WHALE = 0xD883a6A1C22fC4AbFE938a5aDF9B2Cc31b1BF18B; + uint64 internal constant GAS_LANE_MAX_GAS = 5000 gwei; + uint16 internal constant MIN_CONFIRMATIONS = 0; + uint32 internal constant CALLBACK_GAS_LIMIT = 1_000_000; + uint32 internal constant NUM_WORDS = 1; + + // Bytecode for a VRFV2PlusConsumerExample contract. + // to calculate: console.logBytes(type(VRFV2PlusConsumerExample).creationCode); + bytes constant initializeCode = + hex"60806040523480156200001157600080fd5b5060405162001377380380620013778339810160408190526200003491620001cc565b8133806000816200008c5760405162461bcd60e51b815260206004820152601860248201527f43616e6e6f7420736574206f776e657220746f207a65726f000000000000000060448201526064015b60405180910390fd5b600080546001600160a01b0319166001600160a01b0384811691909117909155811615620000bf57620000bf8162000103565b5050600280546001600160a01b03199081166001600160a01b0394851617909155600580548216958416959095179094555060038054909316911617905562000204565b6001600160a01b0381163314156200015e5760405162461bcd60e51b815260206004820152601760248201527f43616e6e6f74207472616e7366657220746f2073656c66000000000000000000604482015260640162000083565b600180546001600160a01b0319166001600160a01b0383811691821790925560008054604051929316917fed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae12789190a350565b80516001600160a01b0381168114620001c757600080fd5b919050565b60008060408385031215620001e057600080fd5b620001eb83620001af565b9150620001fb60208401620001af565b90509250929050565b61116380620002146000396000f3fe608060405234801561001057600080fd5b50600436106101005760003560e01c80638098004311610097578063cf62c8ab11610066578063cf62c8ab14610242578063de367c8e14610255578063eff2701714610268578063f2fde38b1461027b57600080fd5b806380980043146101ab5780638da5cb5b146101be5780638ea98117146101cf578063a168fa89146101e257600080fd5b80635d7d53e3116100d35780635d7d53e314610166578063706da1ca1461016f5780637725135b1461017857806379ba5097146101a357600080fd5b80631fe543e31461010557806329e5d8311461011a5780632fa4e4421461014057806336bfffed14610153575b600080fd5b610118610113366004610e4e565b61028e565b005b61012d610128366004610ef2565b6102fa565b6040519081526020015b60405180910390f35b61011861014e366004610f7f565b610410565b610118610161366004610d5b565b6104bc565b61012d60045481565b61012d60065481565b60035461018b906001600160a01b031681565b6040516001600160a01b039091168152602001610137565b6101186105c0565b6101186101b9366004610e1c565b600655565b6000546001600160a01b031661018b565b6101186101dd366004610d39565b61067e565b61021d6101f0366004610e1c565b6007602052600090815260409020805460019091015460ff82169161010090046001600160a01b03169083565b6040805193151584526001600160a01b03909216602084015290820152606001610137565b610118610250366004610f7f565b61073d565b60055461018b906001600160a01b031681565b610118610276366004610f14565b610880565b610118610289366004610d39565b610a51565b6002546001600160a01b031633146102ec576002546040517f1cf993f40000000000000000000000000000000000000000000000000000000081523360048201526001600160a01b0390911660248201526044015b60405180910390fd5b6102f68282610a65565b5050565b60008281526007602090815260408083208151608081018352815460ff81161515825261010090046001600160a01b0316818501526001820154818401526002820180548451818702810187019095528085528695929460608601939092919083018282801561038957602002820191906000526020600020905b815481526020019060010190808311610375575b50505050508152505090508060400151600014156103e95760405162461bcd60e51b815260206004820152601760248201527f7265717565737420494420697320696e636f727265637400000000000000000060448201526064016102e3565b806060015183815181106103ff576103ff61111c565b602002602001015191505092915050565b6003546002546006546040805160208101929092526001600160a01b0393841693634000aea09316918591015b6040516020818303038152906040526040518463ffffffff1660e01b815260040161046a93929190610ffa565b602060405180830381600087803b15801561048457600080fd5b505af1158015610498573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906102f69190610dff565b60065461050b5760405162461bcd60e51b815260206004820152600d60248201527f7375624944206e6f74207365740000000000000000000000000000000000000060448201526064016102e3565b60005b81518110156102f65760055460065483516001600160a01b039092169163bec4c08c91908590859081106105445761054461111c565b60200260200101516040518363ffffffff1660e01b815260040161057b9291909182526001600160a01b0316602082015260400190565b600060405180830381600087803b15801561059557600080fd5b505af11580156105a9573d6000803e3d6000fd5b5050505080806105b8906110f3565b91505061050e565b6001546001600160a01b0316331461061a5760405162461bcd60e51b815260206004820152601660248201527f4d7573742062652070726f706f736564206f776e65720000000000000000000060448201526064016102e3565b600080543373ffffffffffffffffffffffffffffffffffffffff19808316821784556001805490911690556040516001600160a01b0390921692909183917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e091a350565b6000546001600160a01b031633148015906106a457506002546001600160a01b03163314155b1561070e57336106bc6000546001600160a01b031690565b6002546040517f061db9c10000000000000000000000000000000000000000000000000000000081526001600160a01b03938416600482015291831660248301529190911660448201526064016102e3565b6002805473ffffffffffffffffffffffffffffffffffffffff19166001600160a01b0392909216919091179055565b60065461041057600560009054906101000a90046001600160a01b03166001600160a01b031663a21a23e46040518163ffffffff1660e01b8152600401602060405180830381600087803b15801561079457600080fd5b505af11580156107a8573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906107cc9190610e35565b60068190556005546040517fbec4c08c00000000000000000000000000000000000000000000000000000000815260048101929092523060248301526001600160a01b03169063bec4c08c90604401600060405180830381600087803b15801561083557600080fd5b505af1158015610849573d6000803e3d6000fd5b505050506003546002546006546040516001600160a01b0393841693634000aea0931691859161043d919060200190815260200190565b60006040518060c0016040528084815260200160065481526020018661ffff1681526020018763ffffffff1681526020018563ffffffff1681526020016108d66040518060200160405280861515815250610af8565b90526002546040517f9b1c385e0000000000000000000000000000000000000000000000000000000081529192506000916001600160a01b0390911690639b1c385e90610927908590600401611039565b602060405180830381600087803b15801561094157600080fd5b505af1158015610955573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109799190610e35565b604080516080810182526000808252336020808401918252838501868152855184815280830187526060860190815287855260078352959093208451815493517fffffffffffffffffffffff0000000000000000000000000000000000000000009094169015157fffffffffffffffffffffff0000000000000000000000000000000000000000ff16176101006001600160a01b039094169390930292909217825591516001820155925180519495509193849392610a3f926002850192910190610ca9565b50505060049190915550505050505050565b610a59610b96565b610a6281610bf2565b50565b6004548214610ab65760405162461bcd60e51b815260206004820152601760248201527f7265717565737420494420697320696e636f727265637400000000000000000060448201526064016102e3565b60008281526007602090815260409091208251610adb92600290920191840190610ca9565b50506000908152600760205260409020805460ff19166001179055565b60607f92fd13387c7fe7befbc38d303d6468778fb9731bc4583f17d92989c6fcfdeaaa82604051602401610b3191511515815260200190565b60408051601f198184030181529190526020810180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167fffffffff000000000000000000000000000000000000000000000000000000009093169290921790915292915050565b6000546001600160a01b03163314610bf05760405162461bcd60e51b815260206004820152601660248201527f4f6e6c792063616c6c61626c65206279206f776e65720000000000000000000060448201526064016102e3565b565b6001600160a01b038116331415610c4b5760405162461bcd60e51b815260206004820152601760248201527f43616e6e6f74207472616e7366657220746f2073656c6600000000000000000060448201526064016102e3565b6001805473ffffffffffffffffffffffffffffffffffffffff19166001600160a01b0383811691821790925560008054604051929316917fed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae12789190a350565b828054828255906000526020600020908101928215610ce4579160200282015b82811115610ce4578251825591602001919060010190610cc9565b50610cf0929150610cf4565b5090565b5b80821115610cf05760008155600101610cf5565b80356001600160a01b0381168114610d2057600080fd5b919050565b803563ffffffff81168114610d2057600080fd5b600060208284031215610d4b57600080fd5b610d5482610d09565b9392505050565b60006020808385031215610d6e57600080fd5b823567ffffffffffffffff811115610d8557600080fd5b8301601f81018513610d9657600080fd5b8035610da9610da4826110cf565b61109e565b80828252848201915084840188868560051b8701011115610dc957600080fd5b600094505b83851015610df357610ddf81610d09565b835260019490940193918501918501610dce565b50979650505050505050565b600060208284031215610e1157600080fd5b8151610d5481611148565b600060208284031215610e2e57600080fd5b5035919050565b600060208284031215610e4757600080fd5b5051919050565b60008060408385031215610e6157600080fd5b8235915060208084013567ffffffffffffffff811115610e8057600080fd5b8401601f81018613610e9157600080fd5b8035610e9f610da4826110cf565b80828252848201915084840189868560051b8701011115610ebf57600080fd5b600094505b83851015610ee2578035835260019490940193918501918501610ec4565b5080955050505050509250929050565b60008060408385031215610f0557600080fd5b50508035926020909101359150565b600080600080600060a08688031215610f2c57600080fd5b610f3586610d25565b9450602086013561ffff81168114610f4c57600080fd5b9350610f5a60408701610d25565b9250606086013591506080860135610f7181611148565b809150509295509295909350565b600060208284031215610f9157600080fd5b81356bffffffffffffffffffffffff81168114610d5457600080fd5b6000815180845260005b81811015610fd357602081850181015186830182015201610fb7565b81811115610fe5576000602083870101525b50601f01601f19169290920160200192915050565b6001600160a01b03841681526bffffffffffffffffffffffff831660208201526060604082015260006110306060830184610fad565b95945050505050565b60208152815160208201526020820151604082015261ffff60408301511660608201526000606083015163ffffffff80821660808501528060808601511660a0850152505060a083015160c08084015261109660e0840182610fad565b949350505050565b604051601f8201601f1916810167ffffffffffffffff811182821017156110c7576110c7611132565b604052919050565b600067ffffffffffffffff8211156110e9576110e9611132565b5060051b60200190565b600060001982141561111557634e487b7160e01b600052601160045260246000fd5b5060010190565b634e487b7160e01b600052603260045260246000fd5b634e487b7160e01b600052604160045260246000fd5b8015158114610a6257600080fdfea164736f6c6343000806000a"; + + BlockhashStore s_bhs; + ExposedVRFCoordinatorV2_5 s_testCoordinator; + ExposedVRFCoordinatorV2_5 s_testCoordinator_noLink; + VRFV2PlusConsumerExample s_testConsumer; + MockLinkToken s_linkToken; + MockV3Aggregator s_linkNativeFeed; + + // VRF KeyV2 generated from a node; not sensitive information. + // The secret key used to generate this key is: 10. + bytes vrfUncompressedPublicKey = + hex"a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7"; + bytes vrfCompressedPublicKey = hex"a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c701"; + bytes32 vrfKeyHash = hex"9f2353bde94264dbc3d554a94cceba2d7d2b4fdce4304d3e09a1fea9fbeb1528"; + + function setUp() public override { + BaseTest.setUp(); + + // Fund our users. + vm.roll(1); + vm.deal(LINK_WHALE, 10_000 ether); + changePrank(LINK_WHALE); + + vm.txGasPrice(100 gwei); + + // Instantiate BHS. + s_bhs = new BlockhashStore(); + + // Deploy coordinator and consumer. + // Note: adding contract deployments to this section will require the VRF proofs be regenerated. + s_testCoordinator = new ExposedVRFCoordinatorV2_5(address(s_bhs)); + s_linkToken = new MockLinkToken(); + s_linkNativeFeed = new MockV3Aggregator(18, 500000000000000000); // .5 ETH (good for testing) + + // Use create2 to deploy our consumer, so that its address is always the same + // and surrounding changes do not alter our generated proofs. + bytes memory consumerInitCode = bytes.concat( + initializeCode, + abi.encode(address(s_testCoordinator), address(s_linkToken)) + ); + bytes32 abiEncodedOwnerAddress = bytes32(uint256(uint160(LINK_WHALE)) << 96); + address consumerCreate2Address; + assembly { + consumerCreate2Address := create2( + 0, // value - left at zero here + add(0x20, consumerInitCode), // initialization bytecode (excluding first memory slot which contains its length) + mload(consumerInitCode), // length of initialization bytecode + abiEncodedOwnerAddress // user-defined nonce to ensure unique SCA addresses + ) + } + s_testConsumer = VRFV2PlusConsumerExample(consumerCreate2Address); + + s_testCoordinator_noLink = new ExposedVRFCoordinatorV2_5(address(s_bhs)); + + // Configure the coordinator. + s_testCoordinator.setLINKAndLINKNativeFeed(address(s_linkToken), address(s_linkNativeFeed)); + } + + function setConfig() internal { + s_testCoordinator.setConfig( + 0, // minRequestConfirmations + 2_500_000, // maxGasLimit + 1, // stalenessSeconds + 50_000, // gasAfterPaymentCalculation + 50000000000000000, // fallbackWeiPerUnitLink + 500_000, // fulfillmentFlatFeeNativePPM + 100_000, // fulfillmentFlatFeeLinkDiscountPPM + 15, // nativePremiumPercentage + 10 // linkPremiumPercentage + ); + } + + function testSetConfig() public { + // Should setConfig successfully. + setConfig(); + + // Test that setting requestConfirmations above MAX_REQUEST_CONFIRMATIONS reverts. + vm.expectRevert(abi.encodeWithSelector(VRFCoordinatorV2_5.InvalidRequestConfirmations.selector, 500, 500, 200)); + s_testCoordinator.setConfig( + 500, + 2_500_000, + 1, + 50_000, + 50000000000000000, + 500_000, // fulfillmentFlatFeeNativePPM + 100_000, // fulfillmentFlatFeeLinkDiscountPPM + 15, // nativePremiumPercentage + 10 // linkPremiumPercentage + ); + + // Test that setting fallbackWeiPerUnitLink to zero reverts. + vm.expectRevert(abi.encodeWithSelector(VRFCoordinatorV2_5.InvalidLinkWeiPrice.selector, 0)); + + s_testCoordinator.setConfig( + 0, + 2_500_000, + 1, + 50_000, + 0, + 500_000, // fulfillmentFlatFeeNativePPM + 100_000, // fulfillmentFlatFeeLinkDiscountPPM + 15, // nativePremiumPercentage + 10 // linkPremiumPercentage + ); + + // Test that setting link discount flat fee higher than native flat fee reverts + vm.expectRevert(abi.encodeWithSelector(VRFCoordinatorV2_5.LinkDiscountTooHigh.selector, uint32(501), uint32(500))); + + s_testCoordinator.setConfig( + 0, + 2_500_000, + 1, + 50_000, + 500, + 500, // fulfillmentFlatFeeNativePPM + 501, // fulfillmentFlatFeeLinkDiscountPPM + 15, // nativePremiumPercentage + 10 // linkPremiumPercentage + ); + + // // Test that setting link discount flat fee equal to native flat fee does not revert + s_testCoordinator.setConfig( + 0, + 2_500_000, + 1, + 50_000, + 500, + 450, // fulfillmentFlatFeeNativePPM + 450, // fulfillmentFlatFeeLinkDiscountPPM + 15, // nativePremiumPercentage + 10 // linkPremiumPercentage + ); + + // Test that setting native premium percentage higher than 155 will revert + vm.expectRevert( + abi.encodeWithSelector(VRFCoordinatorV2_5.InvalidPremiumPercentage.selector, uint8(156), uint8(155)) + ); + + s_testCoordinator.setConfig( + 0, + 2_500_000, + 1, + 50_000, + 500, + 500_000, // fulfillmentFlatFeeNativePPM + 100_000, // fulfillmentFlatFeeLinkDiscountPPM + 156, // nativePremiumPercentage + 10 // linkPremiumPercentage + ); + + // Test that setting LINK premium percentage higher than 155 will revert + vm.expectRevert( + abi.encodeWithSelector(VRFCoordinatorV2_5.InvalidPremiumPercentage.selector, uint8(202), uint8(155)) + ); + + s_testCoordinator.setConfig( + 0, + 2_500_000, + 1, + 50_000, + 500, + 500_000, // fulfillmentFlatFeeNativePPM + 100_000, // fulfillmentFlatFeeLinkDiscountPPM + 15, // nativePremiumPercentage + 202 // linkPremiumPercentage + ); + } + + function testRegisterProvingKey() public { + // Should set the proving key successfully. + registerProvingKey(); + + // Should revert when already registered. + uint256[2] memory uncompressedKeyParts = this.getProvingKeyParts(vrfUncompressedPublicKey); + vm.expectRevert(abi.encodeWithSelector(VRFCoordinatorV2_5.ProvingKeyAlreadyRegistered.selector, vrfKeyHash)); + s_testCoordinator.registerProvingKey(uncompressedKeyParts, GAS_LANE_MAX_GAS); + } + + event ProvingKeyRegistered(bytes32 keyHash, uint64 maxGas); + event ProvingKeyDeregistered(bytes32 keyHash, uint64 maxGas); + + function registerProvingKey() public { + uint256[2] memory uncompressedKeyParts = this.getProvingKeyParts(vrfUncompressedPublicKey); + bytes32 keyHash = keccak256(abi.encode(uncompressedKeyParts)); + vm.expectEmit( + false, // no indexed args to check for + false, // no indexed args to check for + false, // no indexed args to check for + true + ); // check data fields: keyHash and maxGas + emit ProvingKeyRegistered(keyHash, GAS_LANE_MAX_GAS); + s_testCoordinator.registerProvingKey(uncompressedKeyParts, GAS_LANE_MAX_GAS); + (bool exists, uint64 maxGas) = s_testCoordinator.s_provingKeys(keyHash); + assertTrue(exists); + assertEq(GAS_LANE_MAX_GAS, maxGas); + assertEq(s_testCoordinator.s_provingKeyHashes(0), keyHash); + assertEq(keyHash, vrfKeyHash); + } + + function testDeregisterProvingKey() public { + // Should set the proving key successfully. + registerProvingKey(); + + bytes + memory unregisteredPubKey = hex"6d919e4ed6add6c34b2af77eb6b2d2f5d27db11ba004e70734b23bd4321ea234ff8577a063314bead6d88c1b01849289a5542767a5138924f38fed551a7773db"; + + // Should revert when given pubkey is not registered + uint256[2] memory unregisteredKeyParts = this.getProvingKeyParts(unregisteredPubKey); + bytes32 unregisterdKeyHash = keccak256(abi.encode(unregisteredKeyParts)); + vm.expectRevert(abi.encodeWithSelector(VRFCoordinatorV2_5.NoSuchProvingKey.selector, unregisterdKeyHash)); + s_testCoordinator.deregisterProvingKey(unregisteredKeyParts); + + // correctly deregister pubkey + uint256[2] memory uncompressedKeyParts = this.getProvingKeyParts(vrfUncompressedPublicKey); + bytes32 keyHash = keccak256(abi.encode(uncompressedKeyParts)); + vm.expectEmit( + false, // no indexed args to check for + false, // no indexed args to check for + false, // no indexed args to check for + true + ); // check data fields: keyHash and maxGas + emit ProvingKeyDeregistered(keyHash, GAS_LANE_MAX_GAS); + s_testCoordinator.deregisterProvingKey(uncompressedKeyParts); + (bool exists, uint64 maxGas) = s_testCoordinator.s_provingKeys(keyHash); + assertFalse(exists); + assertEq(0, maxGas); + } + + // note: Call this function via this.getProvingKeyParts to be able to pass memory as calldata and + // index over the byte array. + function getProvingKeyParts(bytes calldata uncompressedKey) public pure returns (uint256[2] memory) { + uint256 keyPart1 = uint256(bytes32(uncompressedKey[0:32])); + uint256 keyPart2 = uint256(bytes32(uncompressedKey[32:64])); + return [keyPart1, keyPart2]; + } + + function testCreateSubscription() public { + uint256 subId = s_testCoordinator.createSubscription(); + s_testCoordinator.fundSubscriptionWithNative{value: 10 ether}(subId); + } + + function testCancelSubWithNoLink() public { + uint256 subId = s_testCoordinator_noLink.createSubscription(); + s_testCoordinator_noLink.fundSubscriptionWithNative{value: 1000 ether}(subId); + + assertEq(LINK_WHALE.balance, 9000 ether); + s_testCoordinator_noLink.cancelSubscription(subId, LINK_WHALE); + assertEq(LINK_WHALE.balance, 10_000 ether); + + vm.expectRevert(SubscriptionAPI.InvalidSubscription.selector); + s_testCoordinator_noLink.getSubscription(subId); + } + + function testGetActiveSubscriptionIds() public { + uint numSubs = 40; + for (uint i = 0; i < numSubs; i++) { + s_testCoordinator.createSubscription(); + } + // get all subscriptions, assert length is correct + uint256[] memory allSubs = s_testCoordinator.getActiveSubscriptionIds(0, 0); + assertEq(allSubs.length, s_testCoordinator.getActiveSubscriptionIdsLength()); + + // paginate through subscriptions, batching by 10. + // we should eventually get all the subscriptions this way. + uint256[][] memory subIds = paginateSubscriptions(s_testCoordinator, 10); + // check that all subscriptions were returned + uint actualNumSubs = 0; + for (uint batchIdx = 0; batchIdx < subIds.length; batchIdx++) { + for (uint subIdx = 0; subIdx < subIds[batchIdx].length; subIdx++) { + s_testCoordinator.getSubscription(subIds[batchIdx][subIdx]); + actualNumSubs++; + } + } + assertEq(actualNumSubs, s_testCoordinator.getActiveSubscriptionIdsLength()); + + // cancel a bunch of subscriptions, assert that they are not returned + uint256[] memory subsToCancel = new uint256[](3); + for (uint i = 0; i < 3; i++) { + subsToCancel[i] = subIds[0][i]; + } + for (uint i = 0; i < subsToCancel.length; i++) { + s_testCoordinator.cancelSubscription(subsToCancel[i], LINK_WHALE); + } + uint256[][] memory newSubIds = paginateSubscriptions(s_testCoordinator, 10); + // check that all subscriptions were returned + // and assert that none of the canceled subscriptions are returned + actualNumSubs = 0; + for (uint batchIdx = 0; batchIdx < newSubIds.length; batchIdx++) { + for (uint subIdx = 0; subIdx < newSubIds[batchIdx].length; subIdx++) { + for (uint i = 0; i < subsToCancel.length; i++) { + assertFalse(newSubIds[batchIdx][subIdx] == subsToCancel[i]); + } + s_testCoordinator.getSubscription(newSubIds[batchIdx][subIdx]); + actualNumSubs++; + } + } + assertEq(actualNumSubs, s_testCoordinator.getActiveSubscriptionIdsLength()); + } + + function paginateSubscriptions( + ExposedVRFCoordinatorV2_5 coordinator, + uint256 batchSize + ) internal view returns (uint256[][] memory) { + uint arrIndex = 0; + uint startIndex = 0; + uint256 numSubs = coordinator.getActiveSubscriptionIdsLength(); + uint256[][] memory subIds = new uint256[][](Math.ceilDiv(numSubs, batchSize)); + while (startIndex < numSubs) { + subIds[arrIndex] = coordinator.getActiveSubscriptionIds(startIndex, batchSize); + startIndex += batchSize; + arrIndex++; + } + return subIds; + } + + event RandomWordsRequested( + bytes32 indexed keyHash, + uint256 requestId, + uint256 preSeed, + uint256 indexed subId, + uint16 minimumRequestConfirmations, + uint32 callbackGasLimit, + uint32 numWords, + bytes extraArgs, + address indexed sender + ); + event RandomWordsFulfilled( + uint256 indexed requestId, + uint256 outputSeed, + uint256 indexed subID, + uint96 payment, + bytes extraArgs, + bool success + ); + event FallbackWeiPerUnitLinkUsed(uint256 requestId, int256 fallbackWeiPerUnitLink); + + function testRequestAndFulfillRandomWordsNative() public { + ( + VRF.Proof memory proof, + VRFTypes.RequestCommitmentV2Plus memory rc, + uint256 subId, + uint256 requestId + ) = setupSubAndRequestRandomnessNativePayment(); + (, uint96 nativeBalanceBefore, , , ) = s_testCoordinator.getSubscription(subId); + + uint256 outputSeed = s_testCoordinator.getRandomnessFromProofExternal(proof, rc).randomness; + vm.recordLogs(); + uint96 payment = s_testCoordinator.fulfillRandomWords(proof, rc, false); + VmSafe.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries[0].topics[1], bytes32(uint256(requestId))); + assertEq(entries[0].topics[2], bytes32(uint256(subId))); + (uint256 loggedOutputSeed, , , bool loggedSuccess) = abi.decode(entries[0].data, (uint256, uint256, bool, bool)); + assertEq(loggedOutputSeed, outputSeed); + assertEq(loggedSuccess, true); + + (bool fulfilled, , ) = s_testConsumer.s_requests(requestId); + assertEq(fulfilled, true); + + // The cost of fulfillRandomWords is approximately 70_000 gas. + // gasAfterPaymentCalculation is 50_000. + // + // The cost of the VRF fulfillment charged to the user is: + // baseFeeWei = weiPerUnitGas * (gasAfterPaymentCalculation + startGas - gasleft()) + // baseFeeWei = 1e11 * (50_000 + 70_000) + // baseFeeWei = 1.2e16 + // flatFeeWei = 1e12 * (fulfillmentFlatFeeNativePPM) + // flatFeeWei = 1e12 * 500_000 = 5e17 + // ... + // billed_fee = baseFeeWei * (100 + linkPremiumPercentage / 100) + 5e17 + // billed_fee = 1.2e16 * 1.15 + 5e17 + // billed_fee = 5.138e+17 + (, uint96 nativeBalanceAfter, , , ) = s_testCoordinator.getSubscription(subId); + // 1e15 is less than 1 percent discrepancy + assertApproxEqAbs(payment, 5.138 * 1e17, 1e15); + assertApproxEqAbs(nativeBalanceAfter, nativeBalanceBefore - 5.138 * 1e17, 1e15); + assertFalse(s_testCoordinator.pendingRequestExists(subId)); + } + + function testRequestAndFulfillRandomWordsLINK() public { + ( + VRF.Proof memory proof, + VRFTypes.RequestCommitmentV2Plus memory rc, + uint256 subId, + uint256 requestId + ) = setupSubAndRequestRandomnessLINKPayment(); + (uint96 linkBalanceBefore, , , , ) = s_testCoordinator.getSubscription(subId); + + uint256 outputSeed = s_testCoordinator.getRandomnessFromProofExternal(proof, rc).randomness; + vm.recordLogs(); + uint96 payment = s_testCoordinator.fulfillRandomWords(proof, rc, false); + + VmSafe.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries[0].topics[1], bytes32(uint256(requestId))); + assertEq(entries[0].topics[2], bytes32(uint256(subId))); + (uint256 loggedOutputSeed, , , bool loggedSuccess) = abi.decode(entries[0].data, (uint256, uint256, bool, bool)); + assertEq(loggedOutputSeed, outputSeed); + assertEq(loggedSuccess, true); + + (bool fulfilled, , ) = s_testConsumer.s_requests(requestId); + assertEq(fulfilled, true); + + // The cost of fulfillRandomWords is approximately 86_000 gas. + // gasAfterPaymentCalculation is 50_000. + // + // The cost of the VRF fulfillment charged to the user is: + // paymentNoFee = (weiPerUnitGas * (gasAfterPaymentCalculation + startGas - gasleft() + l1CostWei) / link_native_ratio) + // paymentNoFee = (1e11 * (50_000 + 86_000 + 0)) / .5 + // paymentNoFee = 2.72e16 + // flatFeeWei = 1e12 * (fulfillmentFlatFeeNativePPM - fulfillmentFlatFeeLinkDiscountPPM) + // flatFeeWei = 1e12 * (500_000 - 100_000) + // flatFeeJuels = 1e18 * flatFeeWei / link_native_ratio + // flatFeeJuels = 4e17 / 0.5 = 8e17 + // billed_fee = paymentNoFee * ((100 + 10) / 100) + 8e17 + // billed_fee = 2.72e16 * 1.1 + 8e17 + // billed_fee = 2.992e16 + 8e17 = 8.2992e17 + // note: delta is doubled from the native test to account for more variance due to the link/native ratio + (uint96 linkBalanceAfter, , , , ) = s_testCoordinator.getSubscription(subId); + // 1e15 is less than 1 percent discrepancy + assertApproxEqAbs(payment, 8.2992 * 1e17, 1e15); + assertApproxEqAbs(linkBalanceAfter, linkBalanceBefore - 8.2992 * 1e17, 1e15); + assertFalse(s_testCoordinator.pendingRequestExists(subId)); + } + + function testRequestAndFulfillRandomWordsLINK_FallbackWeiPerUnitLinkUsed() public { + ( + VRF.Proof memory proof, + VRFTypes.RequestCommitmentV2Plus memory rc, + , + uint256 requestId + ) = setupSubAndRequestRandomnessLINKPayment(); + + (, , , uint32 stalenessSeconds, , , , , ) = s_testCoordinator.s_config(); + int256 fallbackWeiPerUnitLink = s_testCoordinator.s_fallbackWeiPerUnitLink(); + + // Set the link feed to be stale. + (uint80 roundId, int256 answer, uint256 startedAt, , ) = s_linkNativeFeed.latestRoundData(); + uint256 timestamp = block.timestamp - stalenessSeconds - 1; + s_linkNativeFeed.updateRoundData(roundId, answer, timestamp, startedAt); + + vm.expectEmit(false, false, false, true, address(s_testCoordinator)); + emit FallbackWeiPerUnitLinkUsed(requestId, fallbackWeiPerUnitLink); + s_testCoordinator.fulfillRandomWords(proof, rc, false); + } + + function setupSubAndRequestRandomnessLINKPayment() + internal + returns (VRF.Proof memory proof, VRFTypes.RequestCommitmentV2Plus memory rc, uint256 subId, uint256 requestId) + { + uint32 requestBlock = 20; + vm.roll(requestBlock); + s_linkToken.transfer(address(s_testConsumer), 10 ether); + s_testConsumer.createSubscriptionAndFund(10 ether); + subId = s_testConsumer.s_subId(); + + // Apply basic configs to contract. + setConfig(); + registerProvingKey(); + + // Request random words. + vm.expectEmit(true, true, false, true); + uint256 preSeed; + (requestId, preSeed) = s_testCoordinator.computeRequestIdExternal(vrfKeyHash, address(s_testConsumer), subId, 1); + emit RandomWordsRequested( + vrfKeyHash, + requestId, + preSeed, + subId, + MIN_CONFIRMATIONS, + CALLBACK_GAS_LIMIT, + NUM_WORDS, + VRFV2PlusClient._argsToBytes(VRFV2PlusClient.ExtraArgsV1({nativePayment: false})), // nativePayment, // nativePayment + address(s_testConsumer) // requester + ); + s_testConsumer.requestRandomWords(CALLBACK_GAS_LIMIT, MIN_CONFIRMATIONS, NUM_WORDS, vrfKeyHash, false); + (bool fulfilled, , ) = s_testConsumer.s_requests(requestId); + assertEq(fulfilled, false); + assertTrue(s_testCoordinator.pendingRequestExists(subId)); + + // Uncomment these console logs to see info about the request: + // console.log("requestId: ", requestId); + // console.log("preSeed: ", preSeed); + // console.log("sender: ", address(s_testConsumer)); + + // Move on to the next block. + // Store the previous block's blockhash, and assert that it is as expected. + vm.roll(requestBlock + 1); + s_bhs.store(requestBlock); + assertEq(hex"0000000000000000000000000000000000000000000000000000000000000014", s_bhs.getBlockhash(requestBlock)); + + // Fulfill the request. + // Proof generated via the generate-proof-v2-plus script command. Example usage: + /* + go run . generate-proof-v2-plus \ + -key-hash 0x9f2353bde94264dbc3d554a94cceba2d7d2b4fdce4304d3e09a1fea9fbeb1528 \ + -pre-seed 58424872742560034068603954318478134981993109073728628043159461959392650534066 \ + -block-hash 0x0000000000000000000000000000000000000000000000000000000000000014 \ + -block-num 20 \ + -sender 0x90A8820424CC8a819d14cBdE54D12fD3fbFa9bb2 + */ + proof = VRF.Proof({ + pk: [ + 72488970228380509287422715226575535698893157273063074627791787432852706183111, + 62070622898698443831883535403436258712770888294397026493185421712108624767191 + ], + gamma: [ + 38041205470219573731614166317842050442610096576830191475863676359766283013831, + 31897503406364148988967447112698248795931483458172800286988696482435433838056 + ], + c: 114706080610174375269579192101772790158458728655229562781479812703475130740224, + s: 91869928024010088265014058436030407245056128545665425448353233998362687232253, + seed: 58424872742560034068603954318478134981993109073728628043159461959392650534066, + uWitness: 0x1514536B09a51E671d070312bcD3653386d5a82b, + cGammaWitness: [ + 90605489216274499662544489893800286859751132311034850249229378789467669572783, + 76568417372883461229305641415175605031997103681542349721251313705711146936024 + ], + sHashWitness: [ + 43417948503950579681520475434461454031791886587406480417092620950034789197994, + 100772571879140362396088596211082924128900752544164141322636815729889228000249 + ], + zInv: 82374292458278672300647114418593830323283909625362447038989596015264004164958 + }); + rc = VRFTypes.RequestCommitmentV2Plus({ + blockNum: requestBlock, + subId: subId, + callbackGasLimit: 1000000, + numWords: 1, + sender: address(s_testConsumer), + extraArgs: VRFV2PlusClient._argsToBytes(VRFV2PlusClient.ExtraArgsV1({nativePayment: false})) + }); + return (proof, rc, subId, requestId); + } + + function setupSubAndRequestRandomnessNativePayment() + internal + returns (VRF.Proof memory proof, VRFTypes.RequestCommitmentV2Plus memory rc, uint256 subId, uint256 requestId) + { + uint32 requestBlock = 10; + vm.roll(requestBlock); + s_testConsumer.createSubscriptionAndFund(0); + subId = s_testConsumer.s_subId(); + s_testCoordinator.fundSubscriptionWithNative{value: 10 ether}(subId); + + // Apply basic configs to contract. + setConfig(); + registerProvingKey(); + + // Request random words. + vm.expectEmit(true, true, true, true); + uint256 preSeed; + (requestId, preSeed) = s_testCoordinator.computeRequestIdExternal(vrfKeyHash, address(s_testConsumer), subId, 1); + emit RandomWordsRequested( + vrfKeyHash, + requestId, + preSeed, + subId, + MIN_CONFIRMATIONS, + CALLBACK_GAS_LIMIT, + NUM_WORDS, + VRFV2PlusClient._argsToBytes(VRFV2PlusClient.ExtraArgsV1({nativePayment: true})), // nativePayment + address(s_testConsumer) // requester + ); + s_testConsumer.requestRandomWords(CALLBACK_GAS_LIMIT, MIN_CONFIRMATIONS, NUM_WORDS, vrfKeyHash, true); + (bool fulfilled, , ) = s_testConsumer.s_requests(requestId); + assertEq(fulfilled, false); + assertTrue(s_testCoordinator.pendingRequestExists(subId)); + + // Uncomment these console logs to see info about the request: + // console.log("requestId: ", requestId); + // console.log("preSeed: ", preSeed); + // console.log("sender: ", address(s_testConsumer)); + + // Move on to the next block. + // Store the previous block's blockhash, and assert that it is as expected. + vm.roll(requestBlock + 1); + s_bhs.store(requestBlock); + assertEq(hex"000000000000000000000000000000000000000000000000000000000000000a", s_bhs.getBlockhash(requestBlock)); + + // Fulfill the request. + // Proof generated via the generate-proof-v2-plus script command. Example usage: + /* + go run . generate-proof-v2-plus \ + -key-hash 0x9f2353bde94264dbc3d554a94cceba2d7d2b4fdce4304d3e09a1fea9fbeb1528 \ + -pre-seed 83266692323404068105564931899467966321583332182309426611016082057597749986430 \ + -block-hash 0x000000000000000000000000000000000000000000000000000000000000000a \ + -block-num 10 \ + -sender 0x90A8820424CC8a819d14cBdE54D12fD3fbFa9bb2 \ + -native-payment true + */ + proof = VRF.Proof({ + pk: [ + 72488970228380509287422715226575535698893157273063074627791787432852706183111, + 62070622898698443831883535403436258712770888294397026493185421712108624767191 + ], + gamma: [ + 47144451677122876068574640250190132179872561942855874114516471019540736524783, + 63001220656590641645486673489302242739512599229187442248048295264418080499391 + ], + c: 42928477813589729783511577059394077774341588261592343937605454161333818133643, + s: 14447529458406454898597883219032514356523135029224613793880920230249515634875, + seed: 83266692323404068105564931899467966321583332182309426611016082057597749986430, + uWitness: 0x5Ed3bb2AA8EAFe168a23079644d5dfBf892B1038, + cGammaWitness: [ + 40742088032247467257043132769297935807697466810312051815364187117543257089153, + 110399474382135664619186049639190334359061769014381608543009407662815758204131 + ], + sHashWitness: [ + 26556776392895292893984393164594214244553035014769995354896600239759043777485, + 67126706735912782218279556535631175449291035782208850310724682668198932501077 + ], + zInv: 88742453392918610091640193378775723954629905126315835248392650970979000380325 + }); + rc = VRFTypes.RequestCommitmentV2Plus({ + blockNum: requestBlock, + subId: subId, + callbackGasLimit: CALLBACK_GAS_LIMIT, + numWords: 1, + sender: address(s_testConsumer), + extraArgs: VRFV2PlusClient._argsToBytes(VRFV2PlusClient.ExtraArgsV1({nativePayment: true})) + }); + + return (proof, rc, subId, requestId); + } + + function testRequestAndFulfillRandomWords_NetworkGasPriceExceedsGasLane() public { + ( + VRF.Proof memory proof, + VRFTypes.RequestCommitmentV2Plus memory rc, + , + + ) = setupSubAndRequestRandomnessNativePayment(); + + // network gas is higher than gas lane max gas + uint256 networkGasPrice = GAS_LANE_MAX_GAS + 1; + vm.txGasPrice(networkGasPrice); + vm.expectRevert( + abi.encodeWithSelector(VRFCoordinatorV2_5.GasPriceExceeded.selector, networkGasPrice, GAS_LANE_MAX_GAS) + ); + s_testCoordinator.fulfillRandomWords(proof, rc, false); + } + + function testRequestAndFulfillRandomWords_OnlyPremium_NativePayment() public { + ( + VRF.Proof memory proof, + VRFTypes.RequestCommitmentV2Plus memory rc, + uint256 subId, + uint256 requestId + ) = setupSubAndRequestRandomnessNativePayment(); + (, uint96 nativeBalanceBefore, , , ) = s_testCoordinator.getSubscription(subId); + + // network gas is twice the gas lane max gas + uint256 networkGasPrice = GAS_LANE_MAX_GAS * 2; + vm.txGasPrice(networkGasPrice); + + uint256 outputSeed = s_testCoordinator.getRandomnessFromProofExternal(proof, rc).randomness; + vm.recordLogs(); + uint96 payment = s_testCoordinator.fulfillRandomWords(proof, rc, true /* onlyPremium */); + VmSafe.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries[0].topics[1], bytes32(uint256(requestId))); + assertEq(entries[0].topics[2], bytes32(uint256(subId))); + (uint256 loggedOutputSeed, , , bool loggedSuccess) = abi.decode(entries[0].data, (uint256, uint256, bool, bool)); + assertEq(loggedOutputSeed, outputSeed); + assertEq(loggedSuccess, true); + + (bool fulfilled, , ) = s_testConsumer.s_requests(requestId); + assertEq(fulfilled, true); + + // The cost of fulfillRandomWords is approximately 70_000 gas. + // gasAfterPaymentCalculation is 50_000. + // + // The cost of the VRF fulfillment charged to the user is: + // baseFeeWei = weiPerUnitGas * (gasAfterPaymentCalculation + startGas - gasleft()) + // network gas price is capped at gas lane max gas (5000 gwei) + // baseFeeWei = 5e12 * (50_000 + 70_000) + // baseFeeWei = 6e17 + // flatFeeWei = 1e12 * (fulfillmentFlatFeeNativePPM) + // flatFeeWei = 1e12 * 500_000 = 5e17 + // ... + // billed_fee = baseFeeWei * (linkPremiumPercentage / 100) + 5e17 + // billed_fee = 6e17 * 0.15 + 5e17 + // billed_fee = 5.9e+17 + (, uint96 nativeBalanceAfter, , , ) = s_testCoordinator.getSubscription(subId); + // 1e15 is less than 1 percent discrepancy + assertApproxEqAbs(payment, 5.9 * 1e17, 1e15); + assertApproxEqAbs(nativeBalanceAfter, nativeBalanceBefore - 5.9 * 1e17, 1e15); + assertFalse(s_testCoordinator.pendingRequestExists(subId)); + } + + function testRequestAndFulfillRandomWords_OnlyPremium_LinkPayment() public { + ( + VRF.Proof memory proof, + VRFTypes.RequestCommitmentV2Plus memory rc, + uint256 subId, + uint256 requestId + ) = setupSubAndRequestRandomnessLINKPayment(); + (uint96 linkBalanceBefore, , , , ) = s_testCoordinator.getSubscription(subId); + + // network gas is twice the gas lane max gas + uint256 networkGasPrice = GAS_LANE_MAX_GAS * 5; + vm.txGasPrice(networkGasPrice); + + uint256 outputSeed = s_testCoordinator.getRandomnessFromProofExternal(proof, rc).randomness; + vm.recordLogs(); + uint96 payment = s_testCoordinator.fulfillRandomWords(proof, rc, true /* onlyPremium */); + + VmSafe.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries[0].topics[1], bytes32(uint256(requestId))); + assertEq(entries[0].topics[2], bytes32(uint256(subId))); + (uint256 loggedOutputSeed, , , bool loggedSuccess) = abi.decode(entries[0].data, (uint256, uint256, bool, bool)); + assertEq(loggedOutputSeed, outputSeed); + assertEq(loggedSuccess, true); + + (bool fulfilled, , ) = s_testConsumer.s_requests(requestId); + assertEq(fulfilled, true); + + // The cost of fulfillRandomWords is approximately 86_000 gas. + // gasAfterPaymentCalculation is 50_000. + // + // The cost of the VRF fulfillment charged to the user is: + // paymentNoFee = (weiPerUnitGas * (gasAfterPaymentCalculation + startGas - gasleft() + l1CostWei) / link_native_ratio) + // network gas price is capped at gas lane max gas (5000 gwei) + // paymentNoFee = (5e12 * (50_000 + 86_000 + 0)) / .5 + // paymentNoFee = 1.36e+18 + // flatFeeWei = 1e12 * (fulfillmentFlatFeeNativePPM - fulfillmentFlatFeeLinkDiscountPPM) + // flatFeeWei = 1e12 * (500_000 - 100_000) + // flatFeeJuels = 1e18 * flatFeeWei / link_native_ratio + // flatFeeJuels = 4e17 / 0.5 = 8e17 + // billed_fee = paymentNoFee * (10 / 100) + 8e17 + // billed_fee = 1.36e+18 * 0.1 + 8e17 + // billed_fee = 9.36e+17 + // note: delta is doubled from the native test to account for more variance due to the link/native ratio + (uint96 linkBalanceAfter, , , , ) = s_testCoordinator.getSubscription(subId); + // 1e15 is less than 1 percent discrepancy + assertApproxEqAbs(payment, 9.36 * 1e17, 1e15); + assertApproxEqAbs(linkBalanceAfter, linkBalanceBefore - 9.36 * 1e17, 1e15); + assertFalse(s_testCoordinator.pendingRequestExists(subId)); + } + + function testRequestRandomWords_InvalidConsumer() public { + address subOwner = makeAddr("subOwner"); + changePrank(subOwner); + uint256 subId = s_testCoordinator.createSubscription(); + VRFV2PlusLoadTestWithMetrics consumer = new VRFV2PlusLoadTestWithMetrics(address(s_testCoordinator)); + + // consumer is not added to the subscription + vm.expectRevert(abi.encodeWithSelector(SubscriptionAPI.InvalidConsumer.selector, subId, address(consumer))); + consumer.requestRandomWords( + subId, + MIN_CONFIRMATIONS, + vrfKeyHash, + CALLBACK_GAS_LIMIT, + true, + NUM_WORDS, + 1 /* requestCount */ + ); + assertFalse(s_testCoordinator.pendingRequestExists(subId)); + } + + function testRequestRandomWords_ReAddConsumer_AssertRequestID() public { + // 1. setup consumer and subscription + setConfig(); + registerProvingKey(); + address subOwner = makeAddr("subOwner"); + changePrank(subOwner); + uint256 subId = s_testCoordinator.createSubscription(); + VRFV2PlusLoadTestWithMetrics consumer = createAndAddLoadTestWithMetricsConsumer(subId); + uint32 requestBlock = 10; + vm.roll(requestBlock); + changePrank(LINK_WHALE); + s_testCoordinator.fundSubscriptionWithNative{value: 10 ether}(subId); + + // 2. Request random words. + changePrank(subOwner); + vm.expectEmit(true, true, false, true); + uint256 requestId; + uint256 preSeed; + (requestId, preSeed) = s_testCoordinator.computeRequestIdExternal(vrfKeyHash, address(consumer), subId, 1); + emit RandomWordsRequested( + vrfKeyHash, + requestId, + preSeed, + subId, + MIN_CONFIRMATIONS, + CALLBACK_GAS_LIMIT, + NUM_WORDS, + VRFV2PlusClient._argsToBytes(VRFV2PlusClient.ExtraArgsV1({nativePayment: true})), + address(consumer) // requester + ); + consumer.requestRandomWords( + subId, + MIN_CONFIRMATIONS, + vrfKeyHash, + CALLBACK_GAS_LIMIT, + true /* nativePayment */, + NUM_WORDS, + 1 /* requestCount */ + ); + assertTrue(s_testCoordinator.pendingRequestExists(subId)); + + // 3. Fulfill the request above + //console.log("requestId: ", requestId); + //console.log("preSeed: ", preSeed); + //console.log("sender: ", address(consumer)); + + // Move on to the next block. + // Store the previous block's blockhash, and assert that it is as expected. + vm.roll(requestBlock + 1); + s_bhs.store(requestBlock); + assertEq(hex"000000000000000000000000000000000000000000000000000000000000000a", s_bhs.getBlockhash(requestBlock)); + + // Fulfill the request. + // Proof generated via the generate-proof-v2-plus script command. Example usage: + /* + go run . generate-proof-v2-plus \ + -key-hash 0x9f2353bde94264dbc3d554a94cceba2d7d2b4fdce4304d3e09a1fea9fbeb1528 \ + -pre-seed 94043941380654896554739370173616551044559721638888689173752661912204412136884 \ + -block-hash 0x000000000000000000000000000000000000000000000000000000000000000a \ + -block-num 10 \ + -sender 0x44CAfC03154A0708F9DCf988681821f648dA74aF \ + -native-payment true + */ + VRF.Proof memory proof = VRF.Proof({ + pk: [ + 72488970228380509287422715226575535698893157273063074627791787432852706183111, + 62070622898698443831883535403436258712770888294397026493185421712108624767191 + ], + gamma: [ + 18593555375562408458806406536059989757338587469093035962641476877033456068708, + 55675218112764789548330682504442195066741636758414578491295297591596761905475 + ], + c: 56595337384472359782910435918403237878894172750128610188222417200315739516270, + s: 60666722370046279064490737533582002977678558769715798604164042022636022215663, + seed: 94043941380654896554739370173616551044559721638888689173752661912204412136884, + uWitness: 0xEdbE15fd105cfEFb9CCcbBD84403d1F62719E50d, + cGammaWitness: [ + 11752391553651713021860307604522059957920042356542944931263270793211985356642, + 14713353048309058367510422609936133400473710094544154206129568172815229277104 + ], + sHashWitness: [ + 109716108880570827107616596438987062129934448629902940427517663799192095060206, + 79378277044196229730810703755304140279837983575681427317104232794580059801930 + ], + zInv: 18898957977631212231148068121702167284572066246731769473720131179584458697812 + }); + VRFTypes.RequestCommitmentV2Plus memory rc = VRFTypes.RequestCommitmentV2Plus({ + blockNum: requestBlock, + subId: subId, + callbackGasLimit: CALLBACK_GAS_LIMIT, + numWords: NUM_WORDS, + sender: address(consumer), + extraArgs: VRFV2PlusClient._argsToBytes(VRFV2PlusClient.ExtraArgsV1({nativePayment: true})) + }); + s_testCoordinator.fulfillRandomWords(proof, rc, true /* onlyPremium */); + assertFalse(s_testCoordinator.pendingRequestExists(subId)); + + // 4. remove consumer and verify request random words doesn't work + s_testCoordinator.removeConsumer(subId, address(consumer)); + vm.expectRevert(abi.encodeWithSelector(SubscriptionAPI.InvalidConsumer.selector, subId, address(consumer))); + consumer.requestRandomWords( + subId, + MIN_CONFIRMATIONS, + vrfKeyHash, + CALLBACK_GAS_LIMIT, + false /* nativePayment */, + NUM_WORDS, + 1 /* requestCount */ + ); + + // 5. re-add consumer and assert requestID nonce starts from 2 (nonce 1 was used before consumer removal) + s_testCoordinator.addConsumer(subId, address(consumer)); + vm.expectEmit(true, true, false, true); + uint256 requestId2; + uint256 preSeed2; + (requestId2, preSeed2) = s_testCoordinator.computeRequestIdExternal(vrfKeyHash, address(consumer), subId, 2); + emit RandomWordsRequested( + vrfKeyHash, + requestId2, + preSeed2, + subId, + MIN_CONFIRMATIONS, + CALLBACK_GAS_LIMIT, + NUM_WORDS, + VRFV2PlusClient._argsToBytes(VRFV2PlusClient.ExtraArgsV1({nativePayment: false})), // nativePayment, // nativePayment + address(consumer) // requester + ); + consumer.requestRandomWords( + subId, + MIN_CONFIRMATIONS, + vrfKeyHash, + CALLBACK_GAS_LIMIT, + false /* nativePayment */, + NUM_WORDS, + 1 /* requestCount */ + ); + assertNotEq(requestId, requestId2); + assertNotEq(preSeed, preSeed2); + assertTrue(s_testCoordinator.pendingRequestExists(subId)); + } + + function testRequestRandomWords_MultipleConsumers_PendingRequestExists() public { + // 1. setup consumer and subscription + setConfig(); + registerProvingKey(); + address subOwner = makeAddr("subOwner"); + changePrank(subOwner); + uint256 subId = s_testCoordinator.createSubscription(); + VRFV2PlusLoadTestWithMetrics consumer1 = createAndAddLoadTestWithMetricsConsumer(subId); + VRFV2PlusLoadTestWithMetrics consumer2 = createAndAddLoadTestWithMetricsConsumer(subId); + uint32 requestBlock = 10; + vm.roll(requestBlock); + changePrank(LINK_WHALE); + s_testCoordinator.fundSubscriptionWithNative{value: 10 ether}(subId); + + // 2. Request random words. + changePrank(subOwner); + (uint256 requestId1, uint256 preSeed1) = s_testCoordinator.computeRequestIdExternal( + vrfKeyHash, + address(consumer1), + subId, + 1 + ); + (uint256 requestId2, uint256 preSeed2) = s_testCoordinator.computeRequestIdExternal( + vrfKeyHash, + address(consumer2), + subId, + 1 + ); + assertNotEq(requestId1, requestId2); + assertNotEq(preSeed1, preSeed2); + consumer1.requestRandomWords( + subId, + MIN_CONFIRMATIONS, + vrfKeyHash, + CALLBACK_GAS_LIMIT, + true /* nativePayment */, + NUM_WORDS, + 1 /* requestCount */ + ); + consumer2.requestRandomWords( + subId, + MIN_CONFIRMATIONS, + vrfKeyHash, + CALLBACK_GAS_LIMIT, + true /* nativePayment */, + NUM_WORDS, + 1 /* requestCount */ + ); + assertTrue(s_testCoordinator.pendingRequestExists(subId)); + + // Move on to the next block. + // Store the previous block's blockhash, and assert that it is as expected. + vm.roll(requestBlock + 1); + s_bhs.store(requestBlock); + assertEq(hex"000000000000000000000000000000000000000000000000000000000000000a", s_bhs.getBlockhash(requestBlock)); + + // 3. Fulfill the 1st request above + console.log("requestId: ", requestId1); + console.log("preSeed: ", preSeed1); + console.log("sender: ", address(consumer1)); + + // Fulfill the request. + // Proof generated via the generate-proof-v2-plus script command. Example usage: + /* + go run . generate-proof-v2-plus \ + -key-hash 0x9f2353bde94264dbc3d554a94cceba2d7d2b4fdce4304d3e09a1fea9fbeb1528 \ + -pre-seed 94043941380654896554739370173616551044559721638888689173752661912204412136884 \ + -block-hash 0x000000000000000000000000000000000000000000000000000000000000000a \ + -block-num 10 \ + -sender 0x44CAfC03154A0708F9DCf988681821f648dA74aF \ + -native-payment true + */ + VRF.Proof memory proof = VRF.Proof({ + pk: [ + 72488970228380509287422715226575535698893157273063074627791787432852706183111, + 62070622898698443831883535403436258712770888294397026493185421712108624767191 + ], + gamma: [ + 18593555375562408458806406536059989757338587469093035962641476877033456068708, + 55675218112764789548330682504442195066741636758414578491295297591596761905475 + ], + c: 56595337384472359782910435918403237878894172750128610188222417200315739516270, + s: 60666722370046279064490737533582002977678558769715798604164042022636022215663, + seed: 94043941380654896554739370173616551044559721638888689173752661912204412136884, + uWitness: 0xEdbE15fd105cfEFb9CCcbBD84403d1F62719E50d, + cGammaWitness: [ + 11752391553651713021860307604522059957920042356542944931263270793211985356642, + 14713353048309058367510422609936133400473710094544154206129568172815229277104 + ], + sHashWitness: [ + 109716108880570827107616596438987062129934448629902940427517663799192095060206, + 79378277044196229730810703755304140279837983575681427317104232794580059801930 + ], + zInv: 18898957977631212231148068121702167284572066246731769473720131179584458697812 + }); + VRFTypes.RequestCommitmentV2Plus memory rc = VRFTypes.RequestCommitmentV2Plus({ + blockNum: requestBlock, + subId: subId, + callbackGasLimit: CALLBACK_GAS_LIMIT, + numWords: NUM_WORDS, + sender: address(consumer1), + extraArgs: VRFV2PlusClient._argsToBytes(VRFV2PlusClient.ExtraArgsV1({nativePayment: true})) + }); + s_testCoordinator.fulfillRandomWords(proof, rc, true /* onlyPremium */); + assertTrue(s_testCoordinator.pendingRequestExists(subId)); + + //4. Fulfill the 2nd request + console.log("requestId: ", requestId2); + console.log("preSeed: ", preSeed2); + console.log("sender: ", address(consumer2)); + + // Fulfill the request. + // Proof generated via the generate-proof-v2-plus script command. Example usage: + /* + go run . generate-proof-v2-plus \ + -key-hash 0x9f2353bde94264dbc3d554a94cceba2d7d2b4fdce4304d3e09a1fea9fbeb1528 \ + -pre-seed 60086281972849674111646805013521068579710860774417505336898013292594859262126 \ + -block-hash 0x000000000000000000000000000000000000000000000000000000000000000a \ + -block-num 10 \ + -sender 0xf5a165378E120f93784395aDF1E08a437e902865 \ + -native-payment true + */ + proof = VRF.Proof({ + pk: [ + 72488970228380509287422715226575535698893157273063074627791787432852706183111, + 62070622898698443831883535403436258712770888294397026493185421712108624767191 + ], + gamma: [ + 8781676794493524976318989249067879326013864868749595045909181134740761572122, + 70144896394968351242907510966944756907625107566821127114847472296460405612124 + ], + c: 67847193668837615807355025316836592349514589069599294392546721746916067719949, + s: 114946531382736685625345450298146929067341928840493664822961336014597880904075, + seed: 60086281972849674111646805013521068579710860774417505336898013292594859262126, + uWitness: 0xe1de4fD69277D0C5516cAE4d760b1d08BC340A28, + cGammaWitness: [ + 90301582727701442026215692513959255065128476395727596945643431833363167168678, + 61501369717028493801369453424028509804064958915788808540582630993703331669978 + ], + sHashWitness: [ + 98738650825542176387169085844714248077697103572877410412808249468787326424906, + 85647963391545223707301702874240345890884970941786094239896961457539737216630 + ], + zInv: 29080001901010358083725892808339807464533563010468652346220922643802059192842 + }); + rc = VRFTypes.RequestCommitmentV2Plus({ + blockNum: requestBlock, + subId: subId, + callbackGasLimit: CALLBACK_GAS_LIMIT, + numWords: NUM_WORDS, + sender: address(consumer2), + extraArgs: VRFV2PlusClient._argsToBytes(VRFV2PlusClient.ExtraArgsV1({nativePayment: true})) + }); + s_testCoordinator.fulfillRandomWords(proof, rc, true /* onlyPremium */); + assertFalse(s_testCoordinator.pendingRequestExists(subId)); + } + + function createAndAddLoadTestWithMetricsConsumer(uint256 subId) internal returns (VRFV2PlusLoadTestWithMetrics) { + VRFV2PlusLoadTestWithMetrics consumer = new VRFV2PlusLoadTestWithMetrics(address(s_testCoordinator)); + s_testCoordinator.addConsumer(subId, address(consumer)); + return consumer; + } +} diff --git a/contracts/src/v0.8/vrf/test/VRFV2PlusSubscriptionAPI.t.sol b/contracts/src/v0.8/vrf/test/VRFV2PlusSubscriptionAPI.t.sol new file mode 100644 index 00000000..4fbb44ea --- /dev/null +++ b/contracts/src/v0.8/vrf/test/VRFV2PlusSubscriptionAPI.t.sol @@ -0,0 +1,649 @@ +pragma solidity 0.8.19; + +import "./BaseTest.t.sol"; +import {ExposedVRFCoordinatorV2_5} from "../dev/testhelpers/ExposedVRFCoordinatorV2_5.sol"; +import {VRFV2PlusLoadTestWithMetrics} from "../dev/testhelpers/VRFV2PlusLoadTestWithMetrics.sol"; +import {SubscriptionAPI} from "../dev/SubscriptionAPI.sol"; +import {MockLinkToken} from "../../mocks/MockLinkToken.sol"; +import {MockV3Aggregator} from "../../tests/MockV3Aggregator.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; // for Strings.toString +import {VmSafe} from "forge-std/Vm.sol"; + +contract VRFV2PlusSubscriptionAPITest is BaseTest { + event SubscriptionFunded(uint256 indexed subId, uint256 oldBalance, uint256 newBalance); + event SubscriptionFundedWithNative(uint256 indexed subId, uint256 oldNativeBalance, uint256 newNativeBalance); + event SubscriptionCanceled(uint256 indexed subId, address to, uint256 amountLink, uint256 amountNative); + event FundsRecovered(address to, uint256 amountLink); + event NativeFundsRecovered(address to, uint256 amountNative); + event SubscriptionOwnerTransferRequested(uint256 indexed subId, address from, address to); + event SubscriptionOwnerTransferred(uint256 indexed subId, address from, address to); + event SubscriptionConsumerAdded(uint256 indexed subId, address consumer); + event SubscriptionConsumerRemoved(uint256 indexed subId, address consumer); + + ExposedVRFCoordinatorV2_5 s_subscriptionAPI; + + function setUp() public override { + BaseTest.setUp(); + address bhs = makeAddr("bhs"); + s_subscriptionAPI = new ExposedVRFCoordinatorV2_5(bhs); + } + + function testDefaultState() public { + assertEq(address(s_subscriptionAPI.LINK()), address(0)); + assertEq(address(s_subscriptionAPI.LINK_NATIVE_FEED()), address(0)); + assertEq(s_subscriptionAPI.s_currentSubNonce(), 0); + assertEq(s_subscriptionAPI.getActiveSubscriptionIdsLength(), 0); + assertEq(s_subscriptionAPI.s_totalBalance(), 0); + assertEq(s_subscriptionAPI.s_totalNativeBalance(), 0); + } + + function testSetLINKAndLINKNativeFeed() public { + address link = makeAddr("link"); + address linkNativeFeed = makeAddr("linkNativeFeed"); + s_subscriptionAPI.setLINKAndLINKNativeFeed(link, linkNativeFeed); + assertEq(address(s_subscriptionAPI.LINK()), link); + assertEq(address(s_subscriptionAPI.LINK_NATIVE_FEED()), linkNativeFeed); + + // try setting it again, should revert + vm.expectRevert(SubscriptionAPI.LinkAlreadySet.selector); + s_subscriptionAPI.setLINKAndLINKNativeFeed(link, linkNativeFeed); + } + + function testOwnerCancelSubscriptionNoFunds() public { + // CASE: new subscription w/ no funds at all + // Should cancel trivially + + // Note that the link token is not set, but this should still + // not fail in that case. + + // Create the subscription from a separate address + address subOwner = makeAddr("subOwner"); + changePrank(subOwner); + uint64 nonceBefore = s_subscriptionAPI.s_currentSubNonce(); + uint256 subId = s_subscriptionAPI.createSubscription(); + assertEq(s_subscriptionAPI.s_currentSubNonce(), nonceBefore + 1); + + // change back to owner and cancel the subscription + changePrank(OWNER); + vm.expectEmit(true, false, false, true); + emit SubscriptionCanceled(subId, subOwner, 0, 0); + s_subscriptionAPI.ownerCancelSubscription(subId); + + // assert that the subscription no longer exists + assertEq(s_subscriptionAPI.getActiveSubscriptionIdsLength(), 0); + assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).owner, address(0)); + // no point in checking s_subscriptions because all fields are zeroed out + // due to no balance and no requests made + } + + function testOwnerCancelSubscriptionNativeFundsOnly() public { + // CASE: new subscription with native funds only + // no link funds. + // should cancel and return the native funds + + // Create the subscription from a separate address + address subOwner = makeAddr("subOwner"); + changePrank(subOwner); + uint64 nonceBefore = s_subscriptionAPI.s_currentSubNonce(); + uint256 subId = s_subscriptionAPI.createSubscription(); + assertEq(s_subscriptionAPI.s_currentSubNonce(), nonceBefore + 1); + + // fund the subscription with ether + vm.deal(subOwner, 10 ether); + vm.expectEmit(true, false, false, true); + emit SubscriptionFundedWithNative(subId, 0, 5 ether); + s_subscriptionAPI.fundSubscriptionWithNative{value: 5 ether}(subId); + + // change back to owner and cancel the subscription + changePrank(OWNER); + vm.expectEmit(true, false, false, true); + emit SubscriptionCanceled(subId, subOwner, 0 /* link balance */, 5 ether /* native balance */); + s_subscriptionAPI.ownerCancelSubscription(subId); + + // assert that the subscription no longer exists + assertEq(s_subscriptionAPI.getActiveSubscriptionIdsLength(), 0); + assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).owner, address(0)); + assertEq(s_subscriptionAPI.getSubscriptionStruct(subId).nativeBalance, 0); + + // check the native balance of the subOwner, should be 10 ether + assertEq(address(subOwner).balance, 10 ether); + } + + function testOwnerCancelSubscriptionLinkFundsOnly() public { + // CASE: new subscription with link funds only + // no native funds. + // should cancel and return the link funds + + // Create link token and set the link token on the subscription api object + MockLinkToken linkToken = new MockLinkToken(); + s_subscriptionAPI.setLINKAndLINKNativeFeed(address(linkToken), address(0)); + assertEq(address(s_subscriptionAPI.LINK()), address(linkToken)); + + // Create the subscription from a separate address + address subOwner = makeAddr("subOwner"); + changePrank(subOwner); + uint64 nonceBefore = s_subscriptionAPI.s_currentSubNonce(); + uint256 subId = s_subscriptionAPI.createSubscription(); + assertEq(s_subscriptionAPI.s_currentSubNonce(), nonceBefore + 1); + + // fund the subscription with link + // can do it from the owner acct because anyone can fund a subscription + changePrank(OWNER); + vm.expectEmit(true, false, false, true); + emit SubscriptionFunded(subId, 0, 5 ether); + bool success = linkToken.transferAndCall(address(s_subscriptionAPI), 5 ether, abi.encode(subId)); + assertTrue(success, "failed link transfer and call"); + + // change back to owner and cancel the subscription + vm.expectEmit(true, false, false, true); + emit SubscriptionCanceled(subId, subOwner, 5 ether /* link balance */, 0 /* native balance */); + s_subscriptionAPI.ownerCancelSubscription(subId); + + // assert that the subscription no longer exists + assertEq(s_subscriptionAPI.getActiveSubscriptionIdsLength(), 0); + assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).owner, address(0)); + assertEq(s_subscriptionAPI.getSubscriptionStruct(subId).balance, 0); + + // check the link balance of the sub owner, should be 5 LINK + assertEq(linkToken.balanceOf(subOwner), 5 ether); + } + + function testOwnerCancelSubscriptionNativeAndLinkFunds() public { + // CASE: new subscription with link and native funds + // should cancel and return both link and native funds + + // Create link token and set the link token on the subscription api object + MockLinkToken linkToken = new MockLinkToken(); + s_subscriptionAPI.setLINKAndLINKNativeFeed(address(linkToken), address(0)); + assertEq(address(s_subscriptionAPI.LINK()), address(linkToken)); + + // Create the subscription from a separate address + address subOwner = makeAddr("subOwner"); + changePrank(subOwner); + uint64 nonceBefore = s_subscriptionAPI.s_currentSubNonce(); + uint256 subId = s_subscriptionAPI.createSubscription(); + assertEq(s_subscriptionAPI.s_currentSubNonce(), nonceBefore + 1); + + // fund the subscription with link + changePrank(OWNER); + vm.expectEmit(true, false, false, true); + emit SubscriptionFunded(subId, 0, 5 ether); + bool success = linkToken.transferAndCall(address(s_subscriptionAPI), 5 ether, abi.encode(subId)); + assertTrue(success, "failed link transfer and call"); + + // fund the subscription with ether + vm.deal(subOwner, 10 ether); + changePrank(subOwner); + vm.expectEmit(true, false, false, true); + emit SubscriptionFundedWithNative(subId, 0, 5 ether); + s_subscriptionAPI.fundSubscriptionWithNative{value: 5 ether}(subId); + + // change back to owner and cancel the subscription + changePrank(OWNER); + vm.expectEmit(true, false, false, true); + emit SubscriptionCanceled(subId, subOwner, 5 ether /* link balance */, 5 ether /* native balance */); + s_subscriptionAPI.ownerCancelSubscription(subId); + + // assert that the subscription no longer exists + assertEq(s_subscriptionAPI.getActiveSubscriptionIdsLength(), 0); + assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).owner, address(0)); + assertEq(s_subscriptionAPI.getSubscriptionStruct(subId).balance, 0); + assertEq(s_subscriptionAPI.getSubscriptionStruct(subId).nativeBalance, 0); + + // check the link balance of the sub owner, should be 5 LINK + assertEq(linkToken.balanceOf(subOwner), 5 ether, "link balance incorrect"); + // check the ether balance of the sub owner, should be 10 ether + assertEq(address(subOwner).balance, 10 ether, "native balance incorrect"); + } + + function testRecoverFundsLINKNotSet() public { + // CASE: link token not set + // should revert with error LinkNotSet + + // call recoverFunds + vm.expectRevert(SubscriptionAPI.LinkNotSet.selector); + s_subscriptionAPI.recoverFunds(OWNER); + } + + function testRecoverFundsBalanceInvariantViolated() public { + // CASE: link token set + // and internal balance is greater than external balance + + // Create link token and set the link token on the subscription api object + MockLinkToken linkToken = new MockLinkToken(); + s_subscriptionAPI.setLINKAndLINKNativeFeed(address(linkToken), address(0)); + assertEq(address(s_subscriptionAPI.LINK()), address(linkToken)); + + // set the total balance to be greater than the external balance + // so that we trigger the invariant violation + // note that this field is not modifiable in the actual contracts + // other than through onTokenTransfer or similar functions + s_subscriptionAPI.setTotalBalanceTestingOnlyXXX(100 ether); + + // call recoverFunds + vm.expectRevert(abi.encodeWithSelector(SubscriptionAPI.BalanceInvariantViolated.selector, 100 ether, 0)); + s_subscriptionAPI.recoverFunds(OWNER); + } + + function testRecoverFundsAmountToTransfer() public { + // CASE: link token set + // and internal balance is less than external balance + // (i.e invariant is not violated) + // should recover funds successfully + + // Create link token and set the link token on the subscription api object + MockLinkToken linkToken = new MockLinkToken(); + s_subscriptionAPI.setLINKAndLINKNativeFeed(address(linkToken), address(0)); + assertEq(address(s_subscriptionAPI.LINK()), address(linkToken)); + + // transfer 10 LINK to the contract to recover + bool success = linkToken.transfer(address(s_subscriptionAPI), 10 ether); + assertTrue(success, "failed link transfer"); + + // call recoverFunds + vm.expectEmit(true, false, false, true); + emit FundsRecovered(OWNER, 10 ether); + s_subscriptionAPI.recoverFunds(OWNER); + } + + function testRecoverFundsNothingToTransfer() public { + // CASE: link token set + // and there is nothing to transfer + // should do nothing at all + + // Create link token and set the link token on the subscription api object + MockLinkToken linkToken = new MockLinkToken(); + s_subscriptionAPI.setLINKAndLINKNativeFeed(address(linkToken), address(0)); + assertEq(address(s_subscriptionAPI.LINK()), address(linkToken)); + + // create a subscription and fund it with 5 LINK + address subOwner = makeAddr("subOwner"); + changePrank(subOwner); + uint64 nonceBefore = s_subscriptionAPI.s_currentSubNonce(); + uint256 subId = s_subscriptionAPI.createSubscription(); + assertEq(s_subscriptionAPI.s_currentSubNonce(), nonceBefore + 1); + + // fund the subscription with link + changePrank(OWNER); + vm.expectEmit(true, false, false, true); + emit SubscriptionFunded(subId, 0, 5 ether); + bool success = linkToken.transferAndCall(address(s_subscriptionAPI), 5 ether, abi.encode(subId)); + assertTrue(success, "failed link transfer and call"); + + // call recoverFunds, nothing should happen because external balance == internal balance + s_subscriptionAPI.recoverFunds(OWNER); + assertEq(linkToken.balanceOf(address(s_subscriptionAPI)), s_subscriptionAPI.s_totalBalance()); + } + + function testRecoverNativeFundsBalanceInvariantViolated() public { + // set the total balance to be greater than the external balance + // so that we trigger the invariant violation + // note that this field is not modifiable in the actual contracts + // other than through onTokenTransfer or similar functions + s_subscriptionAPI.setTotalNativeBalanceTestingOnlyXXX(100 ether); + + // call recoverFunds + vm.expectRevert(abi.encodeWithSelector(SubscriptionAPI.BalanceInvariantViolated.selector, 100 ether, 0)); + s_subscriptionAPI.recoverNativeFunds(payable(OWNER)); + } + + function testRecoverNativeFundsAmountToTransfer() public { + // transfer 10 LINK to the contract to recover + vm.deal(address(s_subscriptionAPI), 10 ether); + + // call recoverFunds + vm.expectEmit(true, false, false, true); + emit NativeFundsRecovered(OWNER, 10 ether); + s_subscriptionAPI.recoverNativeFunds(payable(OWNER)); + } + + function testRecoverNativeFundsNothingToTransfer() public { + // create a subscription and fund it with 5 ether + address subOwner = makeAddr("subOwner"); + changePrank(subOwner); + uint64 nonceBefore = s_subscriptionAPI.s_currentSubNonce(); + uint256 subId = s_subscriptionAPI.createSubscription(); + assertEq(s_subscriptionAPI.s_currentSubNonce(), nonceBefore + 1); + + // fund the subscription with ether + vm.deal(subOwner, 5 ether); + changePrank(subOwner); + vm.expectEmit(true, false, false, true); + emit SubscriptionFundedWithNative(subId, 0, 5 ether); + s_subscriptionAPI.fundSubscriptionWithNative{value: 5 ether}(subId); + + // call recoverNativeFunds, nothing should happen because external balance == internal balance + changePrank(OWNER); + s_subscriptionAPI.recoverNativeFunds(payable(OWNER)); + assertEq(address(s_subscriptionAPI).balance, s_subscriptionAPI.s_totalNativeBalance()); + } + + function testWithdrawNoLink() public { + // CASE: no link token set + vm.expectRevert(SubscriptionAPI.LinkNotSet.selector); + s_subscriptionAPI.withdraw(OWNER); + } + + function testWithdrawInsufficientBalance() public { + // CASE: link token set, trying to withdraw + // more than balance + MockLinkToken linkToken = new MockLinkToken(); + s_subscriptionAPI.setLINKAndLINKNativeFeed(address(linkToken), address(0)); + assertEq(address(s_subscriptionAPI.LINK()), address(linkToken)); + + // call withdraw + vm.expectRevert(SubscriptionAPI.InsufficientBalance.selector); + s_subscriptionAPI.withdraw(OWNER); + } + + function testWithdrawSufficientBalanceLinkSet() public { + // CASE: link token set, trying to withdraw + // less than balance + MockLinkToken linkToken = new MockLinkToken(); + s_subscriptionAPI.setLINKAndLINKNativeFeed(address(linkToken), address(0)); + assertEq(address(s_subscriptionAPI.LINK()), address(linkToken)); + + // transfer 10 LINK to the contract to withdraw + bool success = linkToken.transfer(address(s_subscriptionAPI), 10 ether); + assertTrue(success, "failed link transfer"); + + // set the withdrawable tokens of the contract to be 1 ether + s_subscriptionAPI.setWithdrawableTokensTestingOnlyXXX(1 ether); + assertEq(s_subscriptionAPI.getWithdrawableTokensTestingOnlyXXX(), 1 ether); + + // set the total balance to be the same as the link balance for consistency + // (this is not necessary for the test, but just to be sane) + s_subscriptionAPI.setTotalBalanceTestingOnlyXXX(10 ether); + + // call Withdraw from owner address + uint256 ownerBalance = linkToken.balanceOf(OWNER); + changePrank(OWNER); + s_subscriptionAPI.withdraw(OWNER); + // assert link balance of owner + assertEq(linkToken.balanceOf(OWNER) - ownerBalance, 1 ether, "owner link balance incorrect"); + // assert state of subscription api + assertEq(s_subscriptionAPI.getWithdrawableTokensTestingOnlyXXX(), 0, "owner withdrawable tokens incorrect"); + // assert that total balance is changed by the withdrawn amount + assertEq(s_subscriptionAPI.s_totalBalance(), 9 ether, "total balance incorrect"); + } + + function testWithdrawNativeInsufficientBalance() public { + // CASE: trying to withdraw more than balance + // should revert with InsufficientBalance + + // call WithdrawNative + changePrank(OWNER); + vm.expectRevert(SubscriptionAPI.InsufficientBalance.selector); + s_subscriptionAPI.withdrawNative(payable(OWNER)); + } + + function testWithdrawLinkInvalidOwner() public { + address invalidAddress = makeAddr("invalidAddress"); + changePrank(invalidAddress); + vm.expectRevert("Only callable by owner"); + s_subscriptionAPI.withdraw(payable(OWNER)); + } + + function testWithdrawNativeInvalidOwner() public { + address invalidAddress = makeAddr("invalidAddress"); + changePrank(invalidAddress); + vm.expectRevert("Only callable by owner"); + s_subscriptionAPI.withdrawNative(payable(OWNER)); + } + + function testWithdrawNativeSufficientBalance() public { + // CASE: trying to withdraw less than balance + // should withdraw successfully + + // transfer 10 ether to the contract to withdraw + vm.deal(address(s_subscriptionAPI), 10 ether); + + // set the withdrawable eth of the contract to be 1 ether + s_subscriptionAPI.setWithdrawableNativeTestingOnlyXXX(1 ether); + assertEq(s_subscriptionAPI.getWithdrawableNativeTestingOnlyXXX(), 1 ether); + + // set the total balance to be the same as the eth balance for consistency + // (this is not necessary for the test, but just to be sane) + s_subscriptionAPI.setTotalNativeBalanceTestingOnlyXXX(10 ether); + + // call WithdrawNative from owner address + changePrank(OWNER); + s_subscriptionAPI.withdrawNative(payable(OWNER)); + // assert native balance + assertEq(address(OWNER).balance, 1 ether, "owner native balance incorrect"); + // assert state of subscription api + assertEq(s_subscriptionAPI.getWithdrawableNativeTestingOnlyXXX(), 0, "owner withdrawable native incorrect"); + // assert that total balance is changed by the withdrawn amount + assertEq(s_subscriptionAPI.s_totalNativeBalance(), 9 ether, "total native balance incorrect"); + } + + function testOnTokenTransferCallerNotLink() public { + vm.expectRevert(SubscriptionAPI.OnlyCallableFromLink.selector); + s_subscriptionAPI.onTokenTransfer(makeAddr("someaddress"), 1 ether, abi.encode(uint256(1))); + } + + function testOnTokenTransferInvalidCalldata() public { + // create and set link token on subscription api + MockLinkToken linkToken = new MockLinkToken(); + s_subscriptionAPI.setLINKAndLINKNativeFeed(address(linkToken), address(0)); + assertEq(address(s_subscriptionAPI.LINK()), address(linkToken)); + + // call link.transferAndCall with invalid calldata + vm.expectRevert(SubscriptionAPI.InvalidCalldata.selector); + linkToken.transferAndCall(address(s_subscriptionAPI), 1 ether, abi.encode(uint256(1), address(1))); + } + + function testOnTokenTransferInvalidSubscriptionId() public { + // create and set link token on subscription api + MockLinkToken linkToken = new MockLinkToken(); + s_subscriptionAPI.setLINKAndLINKNativeFeed(address(linkToken), address(0)); + assertEq(address(s_subscriptionAPI.LINK()), address(linkToken)); + + // generate bogus sub id + uint256 subId = uint256(keccak256("idontexist")); + + // try to fund bogus sub id + vm.expectRevert(SubscriptionAPI.InvalidSubscription.selector); + linkToken.transferAndCall(address(s_subscriptionAPI), 1 ether, abi.encode(subId)); + } + + function testOnTokenTransferSuccess() public { + // happy path link funding test + // create and set link token on subscription api + MockLinkToken linkToken = new MockLinkToken(); + s_subscriptionAPI.setLINKAndLINKNativeFeed(address(linkToken), address(0)); + assertEq(address(s_subscriptionAPI.LINK()), address(linkToken)); + + // create a subscription and fund it with 5 LINK + address subOwner = makeAddr("subOwner"); + changePrank(subOwner); + uint64 nonceBefore = s_subscriptionAPI.s_currentSubNonce(); + uint256 subId = s_subscriptionAPI.createSubscription(); + assertEq(s_subscriptionAPI.s_currentSubNonce(), nonceBefore + 1); + + // fund the subscription with link + changePrank(OWNER); + vm.expectEmit(true, false, false, true); + emit SubscriptionFunded(subId, 0, 5 ether); + bool success = linkToken.transferAndCall(address(s_subscriptionAPI), 5 ether, abi.encode(subId)); + assertTrue(success, "failed link transfer and call"); + + // assert that the subscription is funded + assertEq(s_subscriptionAPI.getSubscriptionStruct(subId).balance, 5 ether); + } + + function testFundSubscriptionWithNativeInvalidSubscriptionId() public { + // CASE: invalid subscription id + // should revert with InvalidSubscription + + uint256 subId = uint256(keccak256("idontexist")); + + // try to fund the subscription with native, should fail + address funder = makeAddr("funder"); + vm.deal(funder, 5 ether); + changePrank(funder); + vm.expectRevert(SubscriptionAPI.InvalidSubscription.selector); + s_subscriptionAPI.fundSubscriptionWithNative{value: 5 ether}(subId); + } + + function testFundSubscriptionWithNative() public { + // happy path test + // funding subscription with native + + // create a subscription and fund it with native + address subOwner = makeAddr("subOwner"); + changePrank(subOwner); + uint64 nonceBefore = s_subscriptionAPI.s_currentSubNonce(); + uint256 subId = s_subscriptionAPI.createSubscription(); + assertEq(s_subscriptionAPI.s_currentSubNonce(), nonceBefore + 1); + + // fund the subscription with native + vm.deal(subOwner, 5 ether); + changePrank(subOwner); + vm.expectEmit(true, false, false, true); + emit SubscriptionFundedWithNative(subId, 0, 5 ether); + s_subscriptionAPI.fundSubscriptionWithNative{value: 5 ether}(subId); + + // assert that the subscription is funded + assertEq(s_subscriptionAPI.getSubscriptionStruct(subId).nativeBalance, 5 ether); + } + + function testCreateSubscription() public { + // test that the subscription is created successfully + // and test the initial state of the subscription + address subOwner = makeAddr("subOwner"); + changePrank(subOwner); + uint64 nonceBefore = s_subscriptionAPI.s_currentSubNonce(); + uint256 subId = s_subscriptionAPI.createSubscription(); + assertEq(s_subscriptionAPI.s_currentSubNonce(), nonceBefore + 1); + assertEq(s_subscriptionAPI.getActiveSubscriptionIdsLength(), 1); + assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).owner, subOwner); + assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).consumers.length, 0); + assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).requestedOwner, address(0)); + } + + function testCreateSubscriptionRecreate() public { + // create two subscriptions from the same eoa + // they should never be the same due to nonce incrementation + address subOwner = makeAddr("subOwner"); + changePrank(subOwner); + uint64 nonceBefore = s_subscriptionAPI.s_currentSubNonce(); + uint256 subId1 = s_subscriptionAPI.createSubscription(); + assertEq(s_subscriptionAPI.s_currentSubNonce(), nonceBefore + 1); + uint256 subId2 = s_subscriptionAPI.createSubscription(); + assertEq(s_subscriptionAPI.s_currentSubNonce(), nonceBefore + 2); + assertTrue(subId1 != subId2); + } + + function testSubscriptionOwnershipTransfer() public { + // create two eoa's, and create a subscription from one of them + // and transfer ownership to the other + // assert that the subscription is now owned by the other eoa + address oldOwner = makeAddr("oldOwner"); + address newOwner = makeAddr("newOwner"); + + // create sub + changePrank(oldOwner); + uint256 subId = s_subscriptionAPI.createSubscription(); + assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).owner, oldOwner); + + // request ownership transfer + changePrank(oldOwner); + vm.expectEmit(true, false, false, true); + emit SubscriptionOwnerTransferRequested(subId, oldOwner, newOwner); + s_subscriptionAPI.requestSubscriptionOwnerTransfer(subId, newOwner); + + // accept ownership transfer from newOwner + changePrank(newOwner); + vm.expectEmit(true, false, false, true); + emit SubscriptionOwnerTransferred(subId, oldOwner, newOwner); + s_subscriptionAPI.acceptSubscriptionOwnerTransfer(subId); + assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).requestedOwner, address(0)); + } + + function testAddConsumerTooManyConsumers() public { + // add 100 consumers to a sub and then + // try adding one more and see the revert + address subOwner = makeAddr("subOwner"); + changePrank(subOwner); + uint256 subId = s_subscriptionAPI.createSubscription(); + for (uint256 i = 0; i < 100; i++) { + address consumer = makeAddr(Strings.toString(i)); + vm.expectEmit(true, false, false, true); + emit SubscriptionConsumerAdded(subId, consumer); + s_subscriptionAPI.addConsumer(subId, consumer); + } + + // try adding one more consumer, should revert + address lastConsumer = makeAddr("consumer"); + changePrank(subOwner); + vm.expectRevert(SubscriptionAPI.TooManyConsumers.selector); + s_subscriptionAPI.addConsumer(subId, lastConsumer); + } + + function testAddConsumerReaddSameConsumer() public { + // try adding the same consumer twice + // should be a no-op + // assert state is unchanged after the 2nd add + address subOwner = makeAddr("subOwner"); + address consumer = makeAddr("consumer"); + changePrank(subOwner); + uint256 subId = s_subscriptionAPI.createSubscription(); + assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).consumers.length, 0); + changePrank(subOwner); + vm.expectEmit(true, false, false, true); + emit SubscriptionConsumerAdded(subId, consumer); + s_subscriptionAPI.addConsumer(subId, consumer); + assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).consumers.length, 1); + assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).consumers[0], consumer); + + // add consumer again, should be no-op + changePrank(subOwner); + VmSafe.Log[] memory events = vm.getRecordedLogs(); + s_subscriptionAPI.addConsumer(subId, consumer); + assertEq(events.length, 0); + assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).consumers.length, 1); + assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).consumers[0], consumer); + + // remove consumer + vm.expectEmit(true, false, false, true); + emit SubscriptionConsumerRemoved(subId, consumer); + s_subscriptionAPI.removeConsumer(subId, consumer); + assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).consumers.length, 0); + + // removing consumer twice should revert + vm.expectRevert(abi.encodeWithSelector(SubscriptionAPI.InvalidConsumer.selector, subId, address(consumer))); + s_subscriptionAPI.removeConsumer(subId, consumer); + + //re-add consumer + vm.expectEmit(true, false, false, true); + emit SubscriptionConsumerAdded(subId, consumer); + s_subscriptionAPI.addConsumer(subId, consumer); + assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).consumers.length, 1); + assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).consumers[0], consumer); + } + + function testAddConsumer() public { + // create a subscription and add a consumer + // assert subscription state afterwards + address subOwner = makeAddr("subOwner"); + address consumer = makeAddr("consumer"); + changePrank(subOwner); + uint256 subId = s_subscriptionAPI.createSubscription(); + assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).consumers.length, 0); + + // only subscription owner can add a consumer + address notSubOwner = makeAddr("notSubOwner"); + changePrank(notSubOwner); + vm.expectRevert(abi.encodeWithSelector(SubscriptionAPI.MustBeSubOwner.selector, subOwner)); + s_subscriptionAPI.addConsumer(subId, consumer); + + // subscription owner is able to add a consumer + changePrank(subOwner); + vm.expectEmit(true, false, false, true); + emit SubscriptionConsumerAdded(subId, consumer); + s_subscriptionAPI.addConsumer(subId, consumer); + assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).consumers.length, 1); + assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).consumers[0], consumer); + } +} diff --git a/contracts/src/v0.8/vrf/test/VRFV2PlusWrapper.t.sol b/contracts/src/v0.8/vrf/test/VRFV2PlusWrapper.t.sol new file mode 100644 index 00000000..89232f07 --- /dev/null +++ b/contracts/src/v0.8/vrf/test/VRFV2PlusWrapper.t.sol @@ -0,0 +1,458 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {BaseTest} from "./BaseTest.t.sol"; +import {MockLinkToken} from "../../mocks/MockLinkToken.sol"; +import {MockV3Aggregator} from "../../tests/MockV3Aggregator.sol"; +import {ExposedVRFCoordinatorV2_5} from "../dev/testhelpers/ExposedVRFCoordinatorV2_5.sol"; +import {SubscriptionAPI} from "../dev/SubscriptionAPI.sol"; +import {VRFV2PlusWrapperConsumerExample} from "../dev/testhelpers/VRFV2PlusWrapperConsumerExample.sol"; +import {VRFCoordinatorV2_5} from "../dev/VRFCoordinatorV2_5.sol"; +import {VRFConsumerBaseV2Plus} from "../dev/VRFConsumerBaseV2Plus.sol"; +import {VRFV2PlusWrapper} from "../dev/VRFV2PlusWrapper.sol"; +import {VRFV2PlusClient} from "../dev/libraries/VRFV2PlusClient.sol"; + +contract VRFV2PlusWrapperTest is BaseTest { + address internal constant LINK_WHALE = 0xD883a6A1C22fC4AbFE938a5aDF9B2Cc31b1BF18B; + bytes32 private vrfKeyHash = hex"9f2353bde94264dbc3d554a94cceba2d7d2b4fdce4304d3e09a1fea9fbeb1528"; + uint32 private wrapperGasOverhead = 10_000; + uint32 private coordinatorGasOverhead = 20_000; + uint256 private s_wrapperSubscriptionId; + + ExposedVRFCoordinatorV2_5 private s_testCoordinator; + MockLinkToken private s_linkToken; + MockV3Aggregator private s_linkNativeFeed; + VRFV2PlusWrapper private s_wrapper; + VRFV2PlusWrapperConsumerExample private s_consumer; + + function setUp() public override { + BaseTest.setUp(); + + // Fund our users. + vm.roll(1); + vm.deal(LINK_WHALE, 10_000 ether); + vm.stopPrank(); + vm.startPrank(LINK_WHALE); + + // Deploy link token and link/native feed. + s_linkToken = new MockLinkToken(); + s_linkNativeFeed = new MockV3Aggregator(18, 500000000000000000); // .5 ETH (good for testing) + + // Deploy coordinator. + s_testCoordinator = new ExposedVRFCoordinatorV2_5(address(0)); + + // Create subscription for all future wrapper contracts. + s_wrapperSubscriptionId = s_testCoordinator.createSubscription(); + + // Deploy wrapper. + s_wrapper = new VRFV2PlusWrapper( + address(s_linkToken), + address(s_linkNativeFeed), + address(s_testCoordinator), + uint256(s_wrapperSubscriptionId) + ); + assertEq(address(s_linkToken), address(s_wrapper.link())); + assertEq(address(s_linkNativeFeed), address(s_wrapper.linkNativeFeed())); + + // Add wrapper as a consumer to the wrapper's subscription. + s_testCoordinator.addConsumer(uint256(s_wrapperSubscriptionId), address(s_wrapper)); + + // Deploy consumer. + s_consumer = new VRFV2PlusWrapperConsumerExample(address(s_wrapper)); + + // Configure the coordinator. + s_testCoordinator.setLINKAndLINKNativeFeed(address(s_linkToken), address(s_linkNativeFeed)); + setConfigCoordinator(); + setConfigWrapper(); + + s_testCoordinator.s_config(); + } + + function setConfigCoordinator() internal { + s_testCoordinator.setConfig( + 0, // minRequestConfirmations + 2_500_000, // maxGasLimit + 1, // stalenessSeconds + 50_000, // gasAfterPaymentCalculation + 50000000000000000, // fallbackWeiPerUnitLink + 500_000, // fulfillmentFlatFeeNativePPM + 100_000, // fulfillmentFlatFeeLinkDiscountPPM + 15, // nativePremiumPercentage + 10 // linkPremiumPercentage + ); + } + + function setConfigWrapper() internal { + vm.expectEmit(false, false, false, true, address(s_wrapper)); + emit ConfigSet(wrapperGasOverhead, coordinatorGasOverhead, 0, 0, vrfKeyHash, 10, 1, 50000000000000000, 0, 0); + s_wrapper.setConfig( + wrapperGasOverhead, // wrapper gas overhead + coordinatorGasOverhead, // coordinator gas overhead + 0, // native premium percentage, + 0, // link premium percentage + vrfKeyHash, // keyHash + 10, // max number of words, + 1, // stalenessSeconds + 50000000000000000, // fallbackWeiPerUnitLink + 0, // fulfillmentFlatFeeNativePPM + 0 // fulfillmentFlatFeeLinkDiscountPPM + ); + ( + , + , + , + , + uint32 _wrapperGasOverhead, + uint32 _coordinatorGasOverhead, + uint8 _coordinatorNativePremiumPercentage, + uint8 _coordinatorLinkPremiumPercentage, + bytes32 _keyHash, + uint8 _maxNumWords + ) = s_wrapper.getConfig(); + assertEq(_wrapperGasOverhead, wrapperGasOverhead); + assertEq(_coordinatorGasOverhead, coordinatorGasOverhead); + assertEq(0, _coordinatorNativePremiumPercentage); + assertEq(0, _coordinatorLinkPremiumPercentage); + assertEq(vrfKeyHash, _keyHash); + assertEq(10, _maxNumWords); + } + + event RandomWordsRequested( + bytes32 indexed keyHash, + uint256 requestId, + uint256 preSeed, + uint256 indexed subId, + uint16 minimumRequestConfirmations, + uint32 callbackGasLimit, + uint32 numWords, + bytes extraArgs, + address indexed sender + ); + + // IVRFV2PlusWrapper events + event LinkNativeFeedSet(address linkNativeFeed); + event FulfillmentTxSizeSet(uint32 size); + event ConfigSet( + uint32 wrapperGasOverhead, + uint32 coordinatorGasOverhead, + uint8 coordinatorNativePremiumPercentage, + uint8 coordinatorLinkPremiumPercentage, + bytes32 keyHash, + uint8 maxNumWords, + uint32 stalenessSeconds, + int256 fallbackWeiPerUnitLink, + uint32 fulfillmentFlatFeeNativePPM, + uint32 fulfillmentFlatFeeLinkDiscountPPM + ); + event FallbackWeiPerUnitLinkUsed(uint256 requestId, int256 fallbackWeiPerUnitLink); + event Withdrawn(address indexed to, uint256 amount); + event NativeWithdrawn(address indexed to, uint256 amount); + event Enabled(); + event Disabled(); + + // VRFV2PlusWrapperConsumerBase events + event LinkTokenSet(address link); + + // SubscriptionAPI events + event SubscriptionConsumerAdded(uint256 indexed subId, address consumer); + + function testVRFV2PlusWrapperZeroAddress() public { + vm.expectRevert(VRFConsumerBaseV2Plus.ZeroAddress.selector); + new VRFV2PlusWrapper(address(s_linkToken), address(s_linkNativeFeed), address(0), uint256(0)); + } + + function testCreationOfANewVRFV2PlusWrapper() public { + // second wrapper contract will simply add itself to the same subscription + VRFV2PlusWrapper nextWrapper = new VRFV2PlusWrapper( + address(s_linkToken), + address(s_linkNativeFeed), + address(s_testCoordinator), + s_wrapperSubscriptionId + ); + assertEq(s_wrapperSubscriptionId, nextWrapper.SUBSCRIPTION_ID()); + } + + function testVRFV2PlusWrapperWithZeroSubscriptionId() public { + vm.expectRevert(VRFV2PlusWrapper.SubscriptionIdMissing.selector); + new VRFV2PlusWrapper(address(s_linkToken), address(s_linkNativeFeed), address(s_testCoordinator), uint256(0)); + } + + function testVRFV2PlusWrapperWithInvalidSubscriptionId() public { + vm.expectRevert(SubscriptionAPI.InvalidSubscription.selector); + new VRFV2PlusWrapper(address(s_linkToken), address(s_linkNativeFeed), address(s_testCoordinator), uint256(123456)); + } + + function testSetFulfillmentTxSize() public { + uint32 fulfillmentTxSize = 100_000; + vm.expectEmit(false, false, false, true, address(s_wrapper)); + emit FulfillmentTxSizeSet(fulfillmentTxSize); + s_wrapper.setFulfillmentTxSize(fulfillmentTxSize); + assertEq(s_wrapper.s_fulfillmentTxSizeBytes(), fulfillmentTxSize); + } + + function testSetCoordinatorZeroAddress() public { + vm.expectRevert(VRFConsumerBaseV2Plus.ZeroAddress.selector); + s_wrapper.setCoordinator(address(0)); + } + + function testRequestAndFulfillRandomWordsNativeWrapper() public { + // Fund subscription. + s_testCoordinator.fundSubscriptionWithNative{value: 10 ether}(s_wrapper.SUBSCRIPTION_ID()); + vm.deal(address(s_consumer), 10 ether); + + // Get type and version. + assertEq(s_wrapper.typeAndVersion(), "VRFV2PlusWrapper 1.0.0"); + + // Cannot make request while disabled. + vm.expectEmit(false, false, false, true, address(s_wrapper)); + emit Disabled(); + s_wrapper.disable(); + vm.expectRevert("wrapper is disabled"); + s_consumer.makeRequestNative(500_000, 0, 1); + vm.expectEmit(false, false, false, true, address(s_wrapper)); + emit Enabled(); + s_wrapper.enable(); + + // Request randomness from wrapper. + uint32 callbackGasLimit = 1_000_000; + vm.expectEmit(true, true, true, true); + (uint256 requestId, uint256 preSeed) = s_testCoordinator.computeRequestIdExternal( + vrfKeyHash, + address(s_wrapper), + s_wrapper.SUBSCRIPTION_ID(), + 1 + ); + uint32 EIP150Overhead = callbackGasLimit / 63 + 1; + emit RandomWordsRequested( + vrfKeyHash, + requestId, + preSeed, + s_wrapper.SUBSCRIPTION_ID(), // subId + 0, // minConfirmations + callbackGasLimit + EIP150Overhead + wrapperGasOverhead, // callbackGasLimit - accounts for EIP 150 + 1, // numWords + VRFV2PlusClient._argsToBytes(VRFV2PlusClient.ExtraArgsV1({nativePayment: true})), // extraArgs + address(s_wrapper) // requester + ); + requestId = s_consumer.makeRequestNative(callbackGasLimit, 0, 1); + + (uint256 paid, bool fulfilled, bool native) = s_consumer.s_requests(requestId); + uint32 expectedPaid = callbackGasLimit + wrapperGasOverhead + coordinatorGasOverhead; + uint256 wrapperNativeCostEstimate = s_wrapper.estimateRequestPriceNative(callbackGasLimit, tx.gasprice); + uint256 wrapperCostCalculation = s_wrapper.calculateRequestPriceNative(callbackGasLimit); + assertEq(paid, expectedPaid); + assertEq(uint256(paid), wrapperNativeCostEstimate); + assertEq(wrapperNativeCostEstimate, wrapperCostCalculation); + assertEq(fulfilled, false); + assertEq(native, true); + assertEq(address(s_consumer).balance, 10 ether - expectedPaid); + + (, uint256 gasLimit, ) = s_wrapper.s_callbacks(requestId); + assertEq(gasLimit, callbackGasLimit); + + changePrank(address(s_testCoordinator)); + uint256[] memory words = new uint256[](1); + words[0] = 123; + s_wrapper.rawFulfillRandomWords(requestId, words); + (, bool nowFulfilled, uint256[] memory storedWords) = s_consumer.getRequestStatus(requestId); + assertEq(nowFulfilled, true); + assertEq(storedWords[0], 123); + + // Withdraw funds from wrapper. + changePrank(LINK_WHALE); + uint256 priorWhaleBalance = LINK_WHALE.balance; + vm.expectEmit(true, false, false, true, address(s_wrapper)); + emit NativeWithdrawn(LINK_WHALE, paid); + s_wrapper.withdrawNative(LINK_WHALE); + assertEq(LINK_WHALE.balance, priorWhaleBalance + paid); + assertEq(address(s_wrapper).balance, 0); + } + + function testSetConfigFulfillmentFlatFee_LinkDiscountTooHigh() public { + // Test that setting link discount flat fee higher than native flat fee reverts + vm.expectRevert(abi.encodeWithSelector(VRFV2PlusWrapper.LinkDiscountTooHigh.selector, uint32(501), uint32(500))); + s_wrapper.setConfig( + wrapperGasOverhead, // wrapper gas overhead + coordinatorGasOverhead, // coordinator gas overhead + 0, // native premium percentage, + 0, // link premium percentage + vrfKeyHash, // keyHash + 10, // max number of words, + 1, // stalenessSeconds + 50000000000000000, // fallbackWeiPerUnitLink + 500, // fulfillmentFlatFeeNativePPM + 501 // fulfillmentFlatFeeLinkDiscountPPM + ); + } + + function testSetConfigFulfillmentFlatFee_LinkDiscountEqualsNative() public { + // Test that setting link discount flat fee equal to native flat fee does not revert + s_wrapper.setConfig( + wrapperGasOverhead, // wrapper gas overhead + coordinatorGasOverhead, // coordinator gas overhead + 0, // native premium percentage, + 0, // link premium percentage + vrfKeyHash, // keyHash + 10, // max number of words, + 1, // stalenessSeconds + 50000000000000000, // fallbackWeiPerUnitLink + 450, // fulfillmentFlatFeeNativePPM + 450 // fulfillmentFlatFeeLinkDiscountPPM + ); + } + + function testSetConfigNativePremiumPercentageInvalidPremiumPercentage() public { + // Test that setting native premium percentage higher than 155 will revert + vm.expectRevert( + abi.encodeWithSelector(VRFCoordinatorV2_5.InvalidPremiumPercentage.selector, uint8(156), uint8(155)) + ); + s_wrapper.setConfig( + wrapperGasOverhead, // wrapper gas overhead + coordinatorGasOverhead, // coordinator gas overhead + 156, // native premium percentage, + 0, // link premium percentage + vrfKeyHash, // keyHash + 10, // max number of words, + 1, // stalenessSeconds + 50000000000000000, // fallbackWeiPerUnitLink + 0, // fulfillmentFlatFeeNativePPM + 0 // fulfillmentFlatFeeLinkDiscountPPM + ); + } + + function testSetConfigLinkPremiumPercentageInvalidPremiumPercentage() public { + // Test that setting LINK premium percentage higher than 155 will revert + vm.expectRevert( + abi.encodeWithSelector(VRFCoordinatorV2_5.InvalidPremiumPercentage.selector, uint8(202), uint8(155)) + ); + s_wrapper.setConfig( + wrapperGasOverhead, // wrapper gas overhead + coordinatorGasOverhead, // coordinator gas overhead + 15, // native premium percentage, + 202, // link premium percentage + vrfKeyHash, // keyHash + 10, // max number of words, + 1, // stalenessSeconds + 50000000000000000, // fallbackWeiPerUnitLink + 0, // fulfillmentFlatFeeNativePPM + 0 // fulfillmentFlatFeeLinkDiscountPPM + ); + } + + function testRequestAndFulfillRandomWordsLINKWrapper() public { + // Fund subscription. + s_linkToken.transferAndCall(address(s_testCoordinator), 10 ether, abi.encode(s_wrapper.SUBSCRIPTION_ID())); + s_linkToken.transfer(address(s_consumer), 10 ether); + + // Request randomness from wrapper. + uint32 callbackGasLimit = 1_000_000; + vm.expectEmit(true, true, true, true); + (uint256 requestId, uint256 preSeed) = s_testCoordinator.computeRequestIdExternal( + vrfKeyHash, + address(s_wrapper), + s_wrapper.SUBSCRIPTION_ID(), + 1 + ); + uint32 EIP150Overhead = callbackGasLimit / 63 + 1; + emit RandomWordsRequested( + vrfKeyHash, + requestId, + preSeed, + s_wrapper.SUBSCRIPTION_ID(), // subId + 0, // minConfirmations + callbackGasLimit + EIP150Overhead + wrapperGasOverhead, // callbackGasLimit - accounts for EIP 150 + 1, // numWords + VRFV2PlusClient._argsToBytes(VRFV2PlusClient.ExtraArgsV1({nativePayment: false})), // extraArgs + address(s_wrapper) // requester + ); + s_consumer.makeRequest(callbackGasLimit, 0, 1); + + // Assert that the request was made correctly. + (uint256 paid, bool fulfilled, bool native) = s_consumer.s_requests(requestId); + uint32 expectedPaid = (callbackGasLimit + wrapperGasOverhead + coordinatorGasOverhead) * 2; + uint256 wrapperCostEstimate = s_wrapper.estimateRequestPrice(callbackGasLimit, tx.gasprice); + uint256 wrapperCostCalculation = s_wrapper.calculateRequestPrice(callbackGasLimit); + assertEq(paid, expectedPaid); // 1_030_000 * 2 for link/native ratio + assertEq(uint256(paid), wrapperCostEstimate); + assertEq(wrapperCostEstimate, wrapperCostCalculation); + assertEq(fulfilled, false); + assertEq(native, false); + assertEq(s_linkToken.balanceOf(address(s_consumer)), 10 ether - expectedPaid); + (, uint256 gasLimit, ) = s_wrapper.s_callbacks(requestId); + assertEq(gasLimit, callbackGasLimit); + + // Fulfill the request. + changePrank(address(s_testCoordinator)); + uint256[] memory words = new uint256[](1); + words[0] = 456; + s_wrapper.rawFulfillRandomWords(requestId, words); + (, bool nowFulfilled, uint256[] memory storedWords) = s_consumer.getRequestStatus(requestId); + assertEq(nowFulfilled, true); + assertEq(storedWords[0], 456); + + // Withdraw funds from wrapper. + changePrank(LINK_WHALE); + uint256 priorWhaleBalance = s_linkToken.balanceOf(LINK_WHALE); + vm.expectEmit(true, false, false, true, address(s_wrapper)); + emit Withdrawn(LINK_WHALE, paid); + s_wrapper.withdraw(LINK_WHALE); + assertEq(s_linkToken.balanceOf(LINK_WHALE), priorWhaleBalance + paid); + assertEq(s_linkToken.balanceOf(address(s_wrapper)), 0); + } + + function testRequestRandomWordsLINKWrapperFallbackWeiPerUnitLinkUsed() public { + // Fund subscription. + s_linkToken.transferAndCall(address(s_testCoordinator), 10 ether, abi.encode(s_wrapper.SUBSCRIPTION_ID())); + s_linkToken.transfer(address(s_consumer), 10 ether); + + // Set the link feed to be stale. + (, , , uint32 stalenessSeconds, , , , , ) = s_testCoordinator.s_config(); + int256 fallbackWeiPerUnitLink = s_testCoordinator.s_fallbackWeiPerUnitLink(); + (uint80 roundId, int256 answer, uint256 startedAt, , ) = s_linkNativeFeed.latestRoundData(); + uint256 timestamp = block.timestamp - stalenessSeconds - 1; + s_linkNativeFeed.updateRoundData(roundId, answer, timestamp, startedAt); + + // Request randomness from wrapper. + uint32 callbackGasLimit = 1_000_000; + (uint256 requestId, uint256 preSeed) = s_testCoordinator.computeRequestIdExternal( + vrfKeyHash, + address(s_wrapper), + s_wrapper.SUBSCRIPTION_ID(), + 1 + ); + uint32 EIP150Overhead = callbackGasLimit / 63 + 1; + vm.expectEmit(true, true, true, true); + emit FallbackWeiPerUnitLinkUsed(requestId, fallbackWeiPerUnitLink); + emit RandomWordsRequested( + vrfKeyHash, + requestId, + preSeed, + s_wrapper.SUBSCRIPTION_ID(), // subId + 0, // minConfirmations + callbackGasLimit + EIP150Overhead + wrapperGasOverhead, // callbackGasLimit - accounts for EIP 150 + 1, // numWords + VRFV2PlusClient._argsToBytes(VRFV2PlusClient.ExtraArgsV1({nativePayment: false})), // extraArgs + address(s_wrapper) // requester + ); + s_consumer.makeRequest(callbackGasLimit, 0, 1); + } + + function testRequestRandomWordsInNativeNotConfigured() public { + VRFV2PlusWrapper wrapper = new VRFV2PlusWrapper( + address(s_linkToken), + address(s_linkNativeFeed), + address(s_testCoordinator), + uint256(s_wrapperSubscriptionId) + ); + + vm.expectRevert("wrapper is not configured"); + wrapper.requestRandomWordsInNative(500_000, 0, 1, ""); + } + + function testRequestRandomWordsInNativeDisabled() public { + s_wrapper.disable(); + + vm.expectRevert("wrapper is disabled"); + s_wrapper.requestRandomWordsInNative(500_000, 0, 1, ""); + } +} diff --git a/contracts/src/v0.8/vrf/test/VRFV2PlusWrapper_Migration.t.sol b/contracts/src/v0.8/vrf/test/VRFV2PlusWrapper_Migration.t.sol new file mode 100644 index 00000000..deaef4ba --- /dev/null +++ b/contracts/src/v0.8/vrf/test/VRFV2PlusWrapper_Migration.t.sol @@ -0,0 +1,380 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {BaseTest} from "./BaseTest.t.sol"; +import {MockLinkToken} from "../../mocks/MockLinkToken.sol"; +import {MockV3Aggregator} from "../../tests/MockV3Aggregator.sol"; +import {ExposedVRFCoordinatorV2_5} from "../dev/testhelpers/ExposedVRFCoordinatorV2_5.sol"; +import {VRFCoordinatorV2Plus_V2Example} from "../dev/testhelpers/VRFCoordinatorV2Plus_V2Example.sol"; +import {VRFV2PlusWrapperConsumerExample} from "../dev/testhelpers/VRFV2PlusWrapperConsumerExample.sol"; +import {SubscriptionAPI} from "../dev/SubscriptionAPI.sol"; +import {VRFV2PlusWrapper} from "../dev/VRFV2PlusWrapper.sol"; + +contract VRFV2PlusWrapper_MigrationTest is BaseTest { + address internal constant LINK_WHALE = 0xD883a6A1C22fC4AbFE938a5aDF9B2Cc31b1BF18B; + uint256 internal constant DEFAULT_NATIVE_FUNDING = 7 ether; // 7 ETH + uint256 internal constant DEFAULT_LINK_FUNDING = 10 ether; // 10 ETH + bytes32 private vrfKeyHash = hex"9f2353bde94264dbc3d554a94cceba2d7d2b4fdce4304d3e09a1fea9fbeb1528"; + uint32 private wrapperGasOverhead = 10_000; + uint32 private coordinatorGasOverhead = 20_000; + uint256 private s_wrapperSubscriptionId; + + ExposedVRFCoordinatorV2_5 private s_testCoordinator; + MockLinkToken private s_linkToken; + MockV3Aggregator private s_linkNativeFeed; + VRFV2PlusWrapper private s_wrapper; + VRFV2PlusWrapperConsumerExample private s_consumer; + + VRFCoordinatorV2Plus_V2Example private s_newCoordinator; + + event CoordinatorRegistered(address coordinatorAddress); + event MigrationCompleted(address newCoordinator, uint256 subId); + event WrapperRequestMade(uint256 indexed requestId, uint256 paid); + + function setUp() public override { + BaseTest.setUp(); + + // Fund our users. + vm.roll(1); + vm.deal(LINK_WHALE, 10_000 ether); + changePrank(LINK_WHALE); + + // Deploy link token and link/native feed. + s_linkToken = new MockLinkToken(); + s_linkNativeFeed = new MockV3Aggregator(18, 500000000000000000); // .5 ETH (good for testing) + + // Deploy coordinator. + s_testCoordinator = new ExposedVRFCoordinatorV2_5(address(0)); + + // Create subscription for all future wrapper contracts. + s_wrapperSubscriptionId = s_testCoordinator.createSubscription(); + + // Deploy wrapper. + s_wrapper = new VRFV2PlusWrapper( + address(s_linkToken), + address(s_linkNativeFeed), + address(s_testCoordinator), + uint256(s_wrapperSubscriptionId) + ); + + // Add wrapper as a consumer to the wrapper's subscription. + s_testCoordinator.addConsumer(uint256(s_wrapperSubscriptionId), address(s_wrapper)); + + // Deploy consumer. + s_consumer = new VRFV2PlusWrapperConsumerExample(address(s_wrapper)); + + // Configure the coordinator. + s_testCoordinator.setLINKAndLINKNativeFeed(address(s_linkToken), address(s_linkNativeFeed)); + setConfigCoordinator(); + setConfigWrapper(); + + s_testCoordinator.s_config(); + + // Data structures for Migrateable Wrapper + s_newCoordinator = new VRFCoordinatorV2Plus_V2Example(address(0), address(s_testCoordinator)); + vm.expectEmit( + false, // no first indexed topic + false, // no second indexed topic + false, // no third indexed topic + true // check data (target coordinator address) + ); + address newCoordinatorAddr = address(s_newCoordinator); + emit CoordinatorRegistered(newCoordinatorAddr); + s_testCoordinator.registerMigratableCoordinator(newCoordinatorAddr); + assertTrue(s_testCoordinator.isTargetRegisteredExternal(newCoordinatorAddr)); + } + + function setConfigCoordinator() internal { + s_testCoordinator.setConfig( + 0, // minRequestConfirmations + 2_500_000, // maxGasLimit + 1, // stalenessSeconds + 50_000, // gasAfterPaymentCalculation + 50000000000000000, // fallbackWeiPerUnitLink + 500_000, // fulfillmentFlatFeeNativePPM + 100_000, // fulfillmentFlatFeeLinkDiscountPPM + 15, // nativePremiumPercentage + 10 // linkPremiumPercentage + ); + } + + function setConfigWrapper() internal { + s_wrapper.setConfig( + wrapperGasOverhead, // wrapper gas overhead + coordinatorGasOverhead, // coordinator gas overhead + 0, // native premium percentage, + 0, // link premium percentage + vrfKeyHash, // keyHash + 10, // max number of words, + 1, // stalenessSeconds + 50000000000000000, // fallbackWeiPerUnitLink + 0, // fulfillmentFlatFeeNativePPM + 0 // fulfillmentFlatFeeLinkDiscountPPM + ); + ( + , + , + , + , + uint32 _wrapperGasOverhead, + uint32 _coordinatorGasOverhead, + uint8 _coordinatorNativePremiumPercentage, + uint8 _coordinatorLinkPremiumPercentage, + bytes32 _keyHash, + uint8 _maxNumWords + ) = s_wrapper.getConfig(); + assertEq(_wrapperGasOverhead, wrapperGasOverhead); + assertEq(_coordinatorGasOverhead, coordinatorGasOverhead); + assertEq(0, _coordinatorNativePremiumPercentage); + assertEq(0, _coordinatorLinkPremiumPercentage); + assertEq(vrfKeyHash, _keyHash); + assertEq(10, _maxNumWords); + } + + event RandomWordsRequested( + bytes32 indexed keyHash, + uint256 requestId, + uint256 preSeed, + uint256 indexed subId, + uint16 minimumRequestConfirmations, + uint32 callbackGasLimit, + uint32 numWords, + bytes extraArgs, + address indexed sender + ); + + // IVRFV2PlusWrapper events + event Withdrawn(address indexed to, uint256 amount); + event NativeWithdrawn(address indexed to, uint256 amount); + + // IVRFMigratableConsumerV2Plus events + event CoordinatorSet(address vrfCoordinator); + + function testMigrateWrapperLINKPayment() public { + s_linkToken.transfer(address(s_consumer), DEFAULT_LINK_FUNDING); + + assertEq(uint256(s_wrapperSubscriptionId), uint256(s_wrapper.SUBSCRIPTION_ID())); + address oldCoordinatorAddr = address(s_testCoordinator); + assertEq(address(oldCoordinatorAddr), address(s_wrapper.s_vrfCoordinator())); + + // Fund subscription with native and LINK payment to check + // if funds are transferred to new subscription after call + // migration to new coordinator + s_linkToken.transferAndCall(oldCoordinatorAddr, DEFAULT_LINK_FUNDING, abi.encode(s_wrapperSubscriptionId)); + s_testCoordinator.fundSubscriptionWithNative{value: DEFAULT_NATIVE_FUNDING}(s_wrapperSubscriptionId); + + // subscription exists in V1 coordinator before migration + ( + uint96 balance, + uint96 nativeBalance, + uint64 reqCount, + address owner, + address[] memory consumers + ) = s_testCoordinator.getSubscription(s_wrapperSubscriptionId); + assertEq(reqCount, 0); + assertEq(balance, DEFAULT_LINK_FUNDING); + assertEq(nativeBalance, DEFAULT_NATIVE_FUNDING); + assertEq(owner, address(LINK_WHALE)); + assertEq(consumers.length, 1); + assertEq(consumers[0], address(s_wrapper)); + + // Update wrapper to point to the new coordinator + vm.expectEmit( + false, // no first indexed field + false, // no second indexed field + false, // no third indexed field + true // check data fields + ); + address newCoordinatorAddr = address(s_newCoordinator); + emit MigrationCompleted(newCoordinatorAddr, s_wrapperSubscriptionId); + + // old coordinator has to migrate wrapper's subscription to the new coordinator + s_testCoordinator.migrate(s_wrapperSubscriptionId, newCoordinatorAddr); + assertEq(address(newCoordinatorAddr), address(s_wrapper.s_vrfCoordinator())); + + // subscription no longer exists in v1 coordinator after migration + vm.expectRevert(SubscriptionAPI.InvalidSubscription.selector); + s_testCoordinator.getSubscription(s_wrapperSubscriptionId); + assertEq(s_testCoordinator.s_totalBalance(), 0); + assertEq(s_testCoordinator.s_totalNativeBalance(), 0); + assertEq(s_linkToken.balanceOf(oldCoordinatorAddr), 0); + assertEq(oldCoordinatorAddr.balance, 0); + + // subscription exists in v2 coordinator + (balance, nativeBalance, reqCount, owner, consumers) = s_newCoordinator.getSubscription(s_wrapperSubscriptionId); + assertEq(owner, address(LINK_WHALE)); + assertEq(consumers.length, 1); + assertEq(consumers[0], address(s_wrapper)); + assertEq(reqCount, 0); + assertEq(balance, DEFAULT_LINK_FUNDING); + assertEq(nativeBalance, DEFAULT_NATIVE_FUNDING); + assertEq(s_newCoordinator.s_totalLinkBalance(), DEFAULT_LINK_FUNDING); + assertEq(s_newCoordinator.s_totalNativeBalance(), DEFAULT_NATIVE_FUNDING); + assertEq(s_linkToken.balanceOf(newCoordinatorAddr), DEFAULT_LINK_FUNDING); + assertEq(newCoordinatorAddr.balance, DEFAULT_NATIVE_FUNDING); + + // calling migrate again on V1 coordinator should fail + vm.expectRevert(); + s_testCoordinator.migrate(s_wrapperSubscriptionId, newCoordinatorAddr); + + // Request randomness from wrapper. + uint32 callbackGasLimit = 1_000_000; + uint256 wrapperCost = s_wrapper.calculateRequestPrice(callbackGasLimit); + vm.expectEmit(true, true, true, true); + emit WrapperRequestMade(1, wrapperCost); + uint256 requestId = s_consumer.makeRequest(callbackGasLimit, 0, 1); + assertEq(requestId, 1); + + (uint256 paid, bool fulfilled, bool native) = s_consumer.s_requests(requestId); + uint32 expectedPaid = (callbackGasLimit + wrapperGasOverhead + coordinatorGasOverhead) * 2; + uint256 wrapperCostEstimate = s_wrapper.estimateRequestPrice(callbackGasLimit, tx.gasprice); + uint256 wrapperCostCalculation = s_wrapper.calculateRequestPrice(callbackGasLimit); + assertEq(paid, expectedPaid); // 1_030_000 * 2 for link/native ratio + assertEq(uint256(paid), wrapperCostEstimate); + assertEq(wrapperCostEstimate, wrapperCostCalculation); + assertEq(fulfilled, false); + assertEq(native, false); + assertEq(s_linkToken.balanceOf(address(s_consumer)), DEFAULT_LINK_FUNDING - expectedPaid); + + (, uint256 gasLimit, ) = s_wrapper.s_callbacks(requestId); + assertEq(gasLimit, callbackGasLimit); + + vm.stopPrank(); + + vm.startPrank(newCoordinatorAddr); + + uint256[] memory words = new uint256[](1); + words[0] = 123; + s_wrapper.rawFulfillRandomWords(requestId, words); + (, bool nowFulfilled, uint256[] memory storedWords) = s_consumer.getRequestStatus(requestId); + assertEq(nowFulfilled, true); + assertEq(storedWords[0], 123); + + vm.stopPrank(); + + /// Withdraw funds from wrapper. + vm.startPrank(LINK_WHALE); + uint256 priorWhaleBalance = s_linkToken.balanceOf(LINK_WHALE); + vm.expectEmit(true, false, false, true, address(s_wrapper)); + emit Withdrawn(LINK_WHALE, paid); + s_wrapper.withdraw(LINK_WHALE); + assertEq(s_linkToken.balanceOf(LINK_WHALE), priorWhaleBalance + paid); + assertEq(s_linkToken.balanceOf(address(s_wrapper)), 0); + + vm.stopPrank(); + } + + function testMigrateWrapperNativePayment() public { + vm.deal(address(s_consumer), DEFAULT_NATIVE_FUNDING); + + assertEq(uint256(s_wrapperSubscriptionId), uint256(s_wrapper.SUBSCRIPTION_ID())); + address oldCoordinatorAddr = address(s_testCoordinator); + assertEq(address(oldCoordinatorAddr), address(s_wrapper.s_vrfCoordinator())); + + // Fund subscription with native and LINK payment to check + // if funds are transferred to new subscription after call + // migration to new coordinator + s_linkToken.transferAndCall(oldCoordinatorAddr, DEFAULT_LINK_FUNDING, abi.encode(s_wrapperSubscriptionId)); + s_testCoordinator.fundSubscriptionWithNative{value: DEFAULT_NATIVE_FUNDING}(s_wrapperSubscriptionId); + + // subscription exists in V1 coordinator before migration + ( + uint96 balance, + uint96 nativeBalance, + uint64 reqCount, + address owner, + address[] memory consumers + ) = s_testCoordinator.getSubscription(s_wrapperSubscriptionId); + assertEq(reqCount, 0); + assertEq(balance, DEFAULT_LINK_FUNDING); + assertEq(nativeBalance, DEFAULT_NATIVE_FUNDING); + assertEq(owner, address(LINK_WHALE)); + assertEq(consumers.length, 1); + assertEq(consumers[0], address(s_wrapper)); + + // Update wrapper to point to the new coordinator + vm.expectEmit( + false, // no first indexed field + false, // no second indexed field + false, // no third indexed field + true // check data fields + ); + address newCoordinatorAddr = address(s_newCoordinator); + emit MigrationCompleted(newCoordinatorAddr, s_wrapperSubscriptionId); + + // old coordinator has to migrate wrapper's subscription to the new coordinator + s_testCoordinator.migrate(s_wrapperSubscriptionId, newCoordinatorAddr); + assertEq(address(newCoordinatorAddr), address(s_wrapper.s_vrfCoordinator())); + + // subscription no longer exists in v1 coordinator after migration + vm.expectRevert(SubscriptionAPI.InvalidSubscription.selector); + s_testCoordinator.getSubscription(s_wrapperSubscriptionId); + assertEq(s_testCoordinator.s_totalBalance(), 0); + assertEq(s_testCoordinator.s_totalNativeBalance(), 0); + assertEq(s_linkToken.balanceOf(oldCoordinatorAddr), 0); + assertEq(oldCoordinatorAddr.balance, 0); + + // subscription exists in v2 coordinator + (balance, nativeBalance, reqCount, owner, consumers) = s_newCoordinator.getSubscription(s_wrapperSubscriptionId); + assertEq(owner, address(LINK_WHALE)); + assertEq(consumers.length, 1); + assertEq(consumers[0], address(s_wrapper)); + assertEq(reqCount, 0); + assertEq(balance, DEFAULT_LINK_FUNDING); + assertEq(nativeBalance, DEFAULT_NATIVE_FUNDING); + assertEq(s_newCoordinator.s_totalLinkBalance(), DEFAULT_LINK_FUNDING); + assertEq(s_newCoordinator.s_totalNativeBalance(), DEFAULT_NATIVE_FUNDING); + assertEq(s_linkToken.balanceOf(newCoordinatorAddr), DEFAULT_LINK_FUNDING); + assertEq(newCoordinatorAddr.balance, DEFAULT_NATIVE_FUNDING); + + // calling migrate again on V1 coordinator should fail + vm.expectRevert(); + s_testCoordinator.migrate(s_wrapperSubscriptionId, newCoordinatorAddr); + + // Request randomness from wrapper. + uint32 callbackGasLimit = 1_000_000; + vm.expectEmit(true, true, true, true); + uint256 wrapperCost = s_wrapper.calculateRequestPriceNative(callbackGasLimit); + emit WrapperRequestMade(1, wrapperCost); + uint256 requestId = s_consumer.makeRequestNative(callbackGasLimit, 0, 1); + assertEq(requestId, 1); + + (uint256 paid, bool fulfilled, bool native) = s_consumer.s_requests(requestId); + uint32 expectedPaid = callbackGasLimit + wrapperGasOverhead + coordinatorGasOverhead; + uint256 wrapperNativeCostEstimate = s_wrapper.estimateRequestPriceNative(callbackGasLimit, tx.gasprice); + uint256 wrapperCostCalculation = s_wrapper.calculateRequestPriceNative(callbackGasLimit); + assertEq(paid, expectedPaid); + assertEq(uint256(paid), wrapperNativeCostEstimate); + assertEq(wrapperNativeCostEstimate, wrapperCostCalculation); + assertEq(fulfilled, false); + assertEq(native, true); + assertEq(address(s_consumer).balance, DEFAULT_NATIVE_FUNDING - expectedPaid); + + (, uint256 gasLimit, ) = s_wrapper.s_callbacks(requestId); + assertEq(gasLimit, callbackGasLimit); + + vm.stopPrank(); + + vm.startPrank(newCoordinatorAddr); + + uint256[] memory words = new uint256[](1); + words[0] = 123; + s_wrapper.rawFulfillRandomWords(requestId, words); + (, bool nowFulfilled, uint256[] memory storedWords) = s_consumer.getRequestStatus(requestId); + assertEq(nowFulfilled, true); + assertEq(storedWords[0], 123); + + vm.stopPrank(); + + // Withdraw funds from wrapper. + vm.startPrank(LINK_WHALE); + uint256 priorWhaleBalance = LINK_WHALE.balance; + vm.expectEmit(true, false, false, true, address(s_wrapper)); + emit NativeWithdrawn(LINK_WHALE, paid); + s_wrapper.withdrawNative(LINK_WHALE); + assertEq(LINK_WHALE.balance, priorWhaleBalance + paid); + assertEq(address(s_wrapper).balance, 0); + + vm.stopPrank(); + } +} diff --git a/contracts/src/v0.8/vrf/testhelpers/ChainSpecificUtilHelper.sol b/contracts/src/v0.8/vrf/testhelpers/ChainSpecificUtilHelper.sol index a594e026..16a157e3 100644 --- a/contracts/src/v0.8/vrf/testhelpers/ChainSpecificUtilHelper.sol +++ b/contracts/src/v0.8/vrf/testhelpers/ChainSpecificUtilHelper.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import "../../ChainSpecificUtil.sol"; +import {ChainSpecificUtil} from "../../ChainSpecificUtil_v0_8_6.sol"; /// @dev A helper contract that exposes ChainSpecificUtil methods for testing contract ChainSpecificUtilHelper { diff --git a/contracts/src/v0.8/vrf/testhelpers/VRFV2LoadTestWithMetrics.sol b/contracts/src/v0.8/vrf/testhelpers/VRFV2LoadTestWithMetrics.sol index fa44b3ee..b4d0104a 100644 --- a/contracts/src/v0.8/vrf/testhelpers/VRFV2LoadTestWithMetrics.sol +++ b/contracts/src/v0.8/vrf/testhelpers/VRFV2LoadTestWithMetrics.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.0; import {VRFCoordinatorV2Interface} from "../interfaces/VRFCoordinatorV2Interface.sol"; import {VRFConsumerBaseV2} from "../VRFConsumerBaseV2.sol"; -import {ChainSpecificUtil} from "../../ChainSpecificUtil.sol"; +import {ChainSpecificUtil} from "../../ChainSpecificUtil_v0_8_6.sol"; import {LinkTokenInterface} from "../../shared/interfaces/LinkTokenInterface.sol"; /** diff --git a/contracts/src/v0.8/vrf/testhelpers/VRFV2OwnerTestConsumer.sol b/contracts/src/v0.8/vrf/testhelpers/VRFV2OwnerTestConsumer.sol index 5961f4e5..8f1b2753 100644 --- a/contracts/src/v0.8/vrf/testhelpers/VRFV2OwnerTestConsumer.sol +++ b/contracts/src/v0.8/vrf/testhelpers/VRFV2OwnerTestConsumer.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.0; import {VRFCoordinatorV2Interface} from "../interfaces/VRFCoordinatorV2Interface.sol"; import {VRFConsumerBaseV2} from "../VRFConsumerBaseV2.sol"; import {ConfirmedOwner} from "../../shared/access/ConfirmedOwner.sol"; -import {ChainSpecificUtil} from "../../ChainSpecificUtil.sol"; +import {ChainSpecificUtil} from "../../ChainSpecificUtil_v0_8_6.sol"; import {LinkTokenInterface} from "../../shared/interfaces/LinkTokenInterface.sol"; contract VRFV2OwnerTestConsumer is VRFConsumerBaseV2, ConfirmedOwner { diff --git a/contracts/src/v0.8/vrf/testhelpers/VRFV2RevertingExample.sol b/contracts/src/v0.8/vrf/testhelpers/VRFV2RevertingExample.sol index 4eccafa3..3d9cf30e 100644 --- a/contracts/src/v0.8/vrf/testhelpers/VRFV2RevertingExample.sol +++ b/contracts/src/v0.8/vrf/testhelpers/VRFV2RevertingExample.sol @@ -20,7 +20,7 @@ contract VRFV2RevertingExample is VRFConsumerBaseV2 { } function fulfillRandomWords(uint256, uint256[] memory) internal pure override { - // solhint-disable-next-line custom-errors, reason-string + // solhint-disable-next-line gas-custom-errors, reason-string revert(); } @@ -34,14 +34,14 @@ contract VRFV2RevertingExample is VRFConsumerBaseV2 { } function topUpSubscription(uint96 amount) external { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(s_subId != 0, "sub not set"); // Approve the link transfer. LINKTOKEN.transferAndCall(address(COORDINATOR), amount, abi.encode(s_subId)); } function updateSubscription(address[] memory consumers) external { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(s_subId != 0, "subID not set"); for (uint256 i = 0; i < consumers.length; i++) { COORDINATOR.addConsumer(s_subId, consumers[i]); diff --git a/contracts/src/v0.8/vrf/testhelpers/VRFV2WrapperConsumerExample.sol b/contracts/src/v0.8/vrf/testhelpers/VRFV2WrapperConsumerExample.sol index 563a5b09..924e3e45 100644 --- a/contracts/src/v0.8/vrf/testhelpers/VRFV2WrapperConsumerExample.sol +++ b/contracts/src/v0.8/vrf/testhelpers/VRFV2WrapperConsumerExample.sol @@ -33,7 +33,7 @@ contract VRFV2WrapperConsumerExample is VRFV2WrapperConsumerBase, ConfirmedOwner } function fulfillRandomWords(uint256 _requestId, uint256[] memory _randomWords) internal override { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(s_requests[_requestId].paid > 0, "request not found"); s_requests[_requestId].fulfilled = true; s_requests[_requestId].randomWords = _randomWords; @@ -43,7 +43,7 @@ contract VRFV2WrapperConsumerExample is VRFV2WrapperConsumerBase, ConfirmedOwner function getRequestStatus( uint256 _requestId ) external view returns (uint256 paid, bool fulfilled, uint256[] memory randomWords) { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(s_requests[_requestId].paid > 0, "request not found"); RequestStatus memory request = s_requests[_requestId]; return (request.paid, request.fulfilled, request.randomWords); diff --git a/contracts/src/v0.8/vrf/testhelpers/VRFV2WrapperLoadTestConsumer.sol b/contracts/src/v0.8/vrf/testhelpers/VRFV2WrapperLoadTestConsumer.sol index 5a82d4b0..202e3a09 100644 --- a/contracts/src/v0.8/vrf/testhelpers/VRFV2WrapperLoadTestConsumer.sol +++ b/contracts/src/v0.8/vrf/testhelpers/VRFV2WrapperLoadTestConsumer.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.6; import {VRFV2WrapperConsumerBase} from "../VRFV2WrapperConsumerBase.sol"; import {ConfirmedOwner} from "../../shared/access/ConfirmedOwner.sol"; -import {ChainSpecificUtil} from "../../ChainSpecificUtil.sol"; +import {ChainSpecificUtil} from "../../ChainSpecificUtil_v0_8_6.sol"; import {VRFV2WrapperInterface} from "../interfaces/VRFV2WrapperInterface.sol"; contract VRFV2WrapperLoadTestConsumer is VRFV2WrapperConsumerBase, ConfirmedOwner { @@ -65,7 +65,7 @@ contract VRFV2WrapperLoadTestConsumer is VRFV2WrapperConsumerBase, ConfirmedOwne } function fulfillRandomWords(uint256 _requestId, uint256[] memory _randomWords) internal override { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(s_requests[_requestId].paid > 0, "request not found"); uint256 fulfilmentBlockNumber = ChainSpecificUtil._getBlockNumber(); uint256 requestDelay = fulfilmentBlockNumber - requestHeights[_requestId]; @@ -105,7 +105,7 @@ contract VRFV2WrapperLoadTestConsumer is VRFV2WrapperConsumerBase, ConfirmedOwne uint256 fulfilmentBlockNumber ) { - // solhint-disable-next-line custom-errors + // solhint-disable-next-line gas-custom-errors require(s_requests[_requestId].paid > 0, "request not found"); RequestStatus memory request = s_requests[_requestId]; return ( diff --git a/package.json b/package.json index 500126a8..71217f61 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { "dependencies": { - "@chainlink/contracts": "^1.0.0" + "@chainlink/contracts": "^1.1.0" } } diff --git a/version.txt b/version.txt index 3eefcb9d..9084fa2f 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.0.0 +1.1.0 diff --git a/yarn.lock b/yarn.lock index 647aa3bf..f9670dc8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26,16 +26,16 @@ picocolors "^1.0.0" "@babel/runtime@^7.20.1", "@babel/runtime@^7.5.5": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.1.tgz#431f9a794d173b53720e69a6464abc6f0e2a5c57" - integrity sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ== + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.4.tgz#de795accd698007a66ba44add6cc86542aff1edd" + integrity sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA== dependencies: regenerator-runtime "^0.14.0" -"@chainlink/contracts@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@chainlink/contracts/-/contracts-1.0.0.tgz#60ee2efc8a1f0d995bf2001c6645289523cf5d33" - integrity sha512-d7VEOewoZRR0U7SQq7837g77b0tLwNjIJo5FZ5ZFi9yJp03x73tqXpvdFZvb8sTod7OqV5wC72CDts+uHKKrYg== +"@chainlink/contracts@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@chainlink/contracts/-/contracts-1.1.0.tgz#7158db5771b9504d32d2235a5378ef56601a3e6c" + integrity sha512-J+gDUCnEOJ2ofCvy5L2VLrQ7DVs0NXK31w8MQrW6U1GpjzU1j+it7FOJHZMxZKGg7wDdWI06aWmCgFeiD1H+bA== dependencies: "@changesets/changelog-github" "^0.4.8" "@changesets/cli" "~2.26.2" @@ -43,7 +43,7 @@ "@openzeppelin/contracts" "4.9.3" "@openzeppelin/contracts-upgradeable" "4.9.3" "@scroll-tech/contracts" "0.1.0" - semver "^7.5.4" + semver "^7.6.0" "@changesets/apply-release-plan@^6.1.4": version "6.1.4" @@ -1034,9 +1034,9 @@ error-ex@^1.3.1: is-arrayish "^0.2.1" es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0: - version "1.23.2" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.2.tgz#693312f3940f967b8dd3eebacb590b01712622e0" - integrity sha512-60s3Xv2T2p1ICykc7c+DNDPLDMm9t4QxCOUU0K9JxiLjM3C1zB9YVdN7tjxrFd4+AkZ8CdX1ovUga4P2+1e+/w== + version "1.23.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" + integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== dependencies: array-buffer-byte-length "^1.0.1" arraybuffer.prototype.slice "^1.0.3" @@ -1077,11 +1077,11 @@ es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0: safe-regex-test "^1.0.3" string.prototype.trim "^1.2.9" string.prototype.trimend "^1.0.8" - string.prototype.trimstart "^1.0.7" + string.prototype.trimstart "^1.0.8" typed-array-buffer "^1.0.2" typed-array-byte-length "^1.0.1" typed-array-byte-offset "^1.0.2" - typed-array-length "^1.0.5" + typed-array-length "^1.0.6" unbox-primitive "^1.0.2" which-typed-array "^1.1.15" @@ -2060,7 +2060,7 @@ safe-regex-test@^1.0.3: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== -semver@^7.5.3, semver@^7.5.4: +semver@^7.5.3, semver@^7.6.0: version "7.6.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== @@ -2212,7 +2212,7 @@ string.prototype.trimend@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" -string.prototype.trimstart@^1.0.7: +string.prototype.trimstart@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== @@ -2353,7 +2353,7 @@ typed-array-byte-offset@^1.0.2: has-proto "^1.0.3" is-typed-array "^1.1.13" -typed-array-length@^1.0.5: +typed-array-length@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3" integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==