UNPKG

@hyperlane-xyz/core

Version:

Core solidity contracts for Hyperlane

1,509 lines (1,400 loc) 204 kB
import { ethers } from 'hardhat' import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' import { assert, expect } from 'chai' import { BigNumber, BigNumberish, BytesLike, Contract, ContractFactory, ContractReceipt, ContractTransaction, Signer, Wallet, } from 'ethers' import { evmRevert, evmRevertCustomError } from '../../test-helpers/matchers' import { getUsers, Personas } from '../../test-helpers/setup' import { randomAddress, toWei } from '../../test-helpers/helpers' import { StreamsLookupUpkeep__factory as StreamsLookupUpkeepFactory } from '../../../typechain/factories/StreamsLookupUpkeep__factory' import { MockV3Aggregator__factory as MockV3AggregatorFactory } from '../../../typechain/factories/MockV3Aggregator__factory' import { UpkeepMock__factory as UpkeepMockFactory } from '../../../typechain/factories/UpkeepMock__factory' import { UpkeepAutoFunder__factory as UpkeepAutoFunderFactory } from '../../../typechain/factories/UpkeepAutoFunder__factory' import { MockArbGasInfo__factory as MockArbGasInfoFactory } from '../../../typechain/factories/MockArbGasInfo__factory' import { MockOVMGasPriceOracle__factory as MockOVMGasPriceOracleFactory } from '../../../typechain/factories/MockOVMGasPriceOracle__factory' import { ChainModuleBase__factory as ChainModuleBaseFactory } from '../../../typechain/factories/ChainModuleBase__factory' import { ArbitrumModule__factory as ArbitrumModuleFactory } from '../../../typechain/factories/ArbitrumModule__factory' import { OptimismModule__factory as OptimismModuleFactory } from '../../../typechain/factories/OptimismModule__factory' import { ILogAutomation__factory as ILogAutomationactory } from '../../../typechain/factories/ILogAutomation__factory' import { IAutomationForwarder__factory as IAutomationForwarderFactory } from '../../../typechain/factories/IAutomationForwarder__factory' import { MockArbSys__factory as MockArbSysFactory } from '../../../typechain/factories/MockArbSys__factory' import { AutomationCompatibleUtils } from '../../../typechain/AutomationCompatibleUtils' import { MockArbGasInfo } from '../../../typechain/MockArbGasInfo' import { MockOVMGasPriceOracle } from '../../../typechain/MockOVMGasPriceOracle' import { StreamsLookupUpkeep } from '../../../typechain/StreamsLookupUpkeep' import { MockV3Aggregator } from '../../../typechain/MockV3Aggregator' import { UpkeepMock } from '../../../typechain/UpkeepMock' import { ChainModuleBase } from '../../../typechain/ChainModuleBase' import { ArbitrumModule } from '../../../typechain/ArbitrumModule' import { OptimismModule } from '../../../typechain/OptimismModule' import { UpkeepTranscoder } from '../../../typechain/UpkeepTranscoder' import { IChainModule, UpkeepAutoFunder } from '../../../typechain' import { CancelledUpkeepReportEvent, IAutomationRegistryMaster2_3 as IAutomationRegistry, ReorgedUpkeepReportEvent, StaleUpkeepReportEvent, UpkeepPerformedEvent, } from '../../../typechain/IAutomationRegistryMaster2_3' import { deployMockContract, MockContract, } from '@ethereum-waffle/mock-contract' import { deployRegistry23 } from './helpers' import { AutomationUtils2_3 } from '../../../typechain/AutomationUtils2_3' const describeMaybe = process.env.SKIP_SLOW ? describe.skip : describe const itMaybe = process.env.SKIP_SLOW ? it.skip : it // copied from AutomationRegistryInterface2_3.sol enum UpkeepFailureReason { NONE, UPKEEP_CANCELLED, UPKEEP_PAUSED, TARGET_CHECK_REVERTED, UPKEEP_NOT_NEEDED, PERFORM_DATA_EXCEEDS_LIMIT, INSUFFICIENT_BALANCE, CHECK_CALLBACK_REVERTED, REVERT_DATA_EXCEEDS_LIMIT, REGISTRY_PAUSED, } // copied from AutomationRegistryBase2_3.sol enum Trigger { CONDITION, LOG, } // un-exported types that must be extracted from the utils contract type Report = Parameters<AutomationUtils2_3['_report']>[0] type LogTrigger = Parameters<AutomationCompatibleUtils['_logTrigger']>[0] type ConditionalTrigger = Parameters< AutomationCompatibleUtils['_conditionalTrigger'] >[0] type Log = Parameters<AutomationCompatibleUtils['_log']>[0] type OnChainConfig = Parameters<IAutomationRegistry['setConfigTypeSafe']>[3] // ----------------------------------------------------------------------------------------------- // These values should match the constants declared in registry let registryConditionalOverhead: BigNumber let registryLogOverhead: BigNumber let registryPerSignerGasOverhead: BigNumber let registryPerPerformByteGasOverhead: BigNumber let registryTransmitCalldataFixedBytesOverhead: BigNumber let registryTransmitCalldataPerSignerBytesOverhead: BigNumber let cancellationDelay: number // This is the margin for gas that we test for. Gas charged should always be greater // than total gas used in tx but should not increase beyond this margin const gasCalculationMargin = BigNumber.from(5000) // This is the margin for gas overhead estimation in checkUpkeep. The estimated gas // overhead should be larger than actual gas overhead but should not increase beyond this margin const gasEstimationMargin = BigNumber.from(5000) // 1 Link = 0.005 Eth const linkUSD = BigNumber.from('2000000000') // 1 LINK = $20 const nativeUSD = BigNumber.from('400000000000') // 1 ETH = $4000 const gasWei = BigNumber.from(1000000000) // 1 gwei // ----------------------------------------------------------------------------------------------- // test-wide configs for upkeeps const performGas = BigNumber.from('1000000') const paymentPremiumBase = BigNumber.from('1000000000') const paymentPremiumPPB = BigNumber.from('250000000') const flatFeeMilliCents = BigNumber.from(0) const randomBytes = '0x1234abcd' const emptyBytes = '0x' const emptyBytes32 = '0x0000000000000000000000000000000000000000000000000000000000000000' const transmitGasOverhead = 1_040_000 const checkGasOverhead = 600_000 const stalenessSeconds = BigNumber.from(43820) const gasCeilingMultiplier = BigNumber.from(2) const checkGasLimit = BigNumber.from(10000000) const fallbackGasPrice = gasWei.mul(BigNumber.from('2')) const fallbackLinkPrice = linkUSD.div(BigNumber.from('2')) const fallbackNativePrice = nativeUSD.div(BigNumber.from('2')) const maxCheckDataSize = BigNumber.from(1000) const maxPerformDataSize = BigNumber.from(1000) const maxRevertDataSize = BigNumber.from(1000) const maxPerformGas = BigNumber.from(5000000) const minUpkeepSpend = BigNumber.from(0) const f = 1 const offchainVersion = 1 const offchainBytes = '0x' const zeroAddress = ethers.constants.AddressZero const wrappedNativeTokenAddress = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' const epochAndRound5_1 = '0x0000000000000000000000000000000000000000000000000000000000000501' let logTriggerConfig: string // ----------------------------------------------------------------------------------------------- // Smart contract factories let linkTokenFactory: ContractFactory let mockArbGasInfoFactory: MockArbGasInfoFactory let mockOVMGasPriceOracleFactory: MockOVMGasPriceOracleFactory let mockV3AggregatorFactory: MockV3AggregatorFactory let upkeepMockFactory: UpkeepMockFactory let upkeepAutoFunderFactory: UpkeepAutoFunderFactory let chainModuleBaseFactory: ChainModuleBaseFactory let arbitrumModuleFactory: ArbitrumModuleFactory let optimismModuleFactory: OptimismModuleFactory let streamsLookupUpkeepFactory: StreamsLookupUpkeepFactory let personas: Personas // contracts let linkToken: Contract let linkUSDFeed: MockV3Aggregator let nativeUSDFeed: MockV3Aggregator let gasPriceFeed: MockV3Aggregator let registry: IAutomationRegistry // default registry, used for most tests let arbRegistry: IAutomationRegistry // arbitrum registry let opRegistry: IAutomationRegistry // optimism registry let mgRegistry: IAutomationRegistry // "migrate registry" used in migration tests let blankRegistry: IAutomationRegistry // used to test initial configurations let mockArbGasInfo: MockArbGasInfo let mockOVMGasPriceOracle: MockOVMGasPriceOracle let mock: UpkeepMock let autoFunderUpkeep: UpkeepAutoFunder let ltUpkeep: MockContract let transcoder: UpkeepTranscoder let chainModuleBase: ChainModuleBase let arbitrumModule: ArbitrumModule let optimismModule: OptimismModule let streamsLookupUpkeep: StreamsLookupUpkeep let automationUtils: AutomationCompatibleUtils let automationUtils2_3: AutomationUtils2_3 function now() { return Math.floor(Date.now() / 1000) } async function getUpkeepID(tx: ContractTransaction): Promise<BigNumber> { const receipt = await tx.wait() for (const event of receipt.events || []) { if ( event.args && event.eventSignature == 'UpkeepRegistered(uint256,uint32,address)' ) { return event.args[0] } } throw new Error('could not find upkeep ID in tx event logs') } const getTriggerType = (upkeepId: BigNumber): Trigger => { const hexBytes = ethers.utils.defaultAbiCoder.encode(['uint256'], [upkeepId]) const bytes = ethers.utils.arrayify(hexBytes) for (let idx = 4; idx < 15; idx++) { if (bytes[idx] != 0) { return Trigger.CONDITION } } return bytes[15] as Trigger } const encodeBlockTrigger = (conditionalTrigger: ConditionalTrigger) => { return ( '0x' + automationUtils.interface .encodeFunctionData('_conditionalTrigger', [conditionalTrigger]) .slice(10) ) } const encodeLogTrigger = (logTrigger: LogTrigger) => { return ( '0x' + automationUtils.interface .encodeFunctionData('_logTrigger', [logTrigger]) .slice(10) ) } const encodeLog = (log: Log) => { return ( '0x' + automationUtils.interface.encodeFunctionData('_log', [log]).slice(10) ) } const encodeReport = (report: Report) => { return ( '0x' + automationUtils2_3.interface .encodeFunctionData('_report', [report]) .slice(10) ) } type UpkeepData = { Id: BigNumberish performGas: BigNumberish performData: BytesLike trigger: BytesLike } const makeReport = (upkeeps: UpkeepData[]) => { const upkeepIds = upkeeps.map((u) => u.Id) const performGases = upkeeps.map((u) => u.performGas) const triggers = upkeeps.map((u) => u.trigger) const performDatas = upkeeps.map((u) => u.performData) return encodeReport({ fastGasWei: gasWei, linkUSD, upkeepIds, gasLimits: performGases, triggers, performDatas, }) } const makeLatestBlockReport = async (upkeepsIDs: BigNumberish[]) => { const latestBlock = await ethers.provider.getBlock('latest') const upkeeps: UpkeepData[] = [] for (let i = 0; i < upkeepsIDs.length; i++) { upkeeps.push({ Id: upkeepsIDs[i], performGas, trigger: encodeBlockTrigger({ blockNum: latestBlock.number, blockHash: latestBlock.hash, }), performData: '0x', }) } return makeReport(upkeeps) } const signReport = ( reportContext: string[], report: any, signers: Wallet[], ) => { const reportDigest = ethers.utils.keccak256(report) const packedArgs = ethers.utils.solidityPack( ['bytes32', 'bytes32[3]'], [reportDigest, reportContext], ) const packedDigest = ethers.utils.keccak256(packedArgs) const signatures = [] for (const signer of signers) { signatures.push(signer._signingKey().signDigest(packedDigest)) } const vs = signatures.map((i) => '0' + (i.v - 27).toString(16)).join('') return { vs: '0x' + vs.padEnd(64, '0'), rs: signatures.map((i) => i.r), ss: signatures.map((i) => i.s), } } const parseUpkeepPerformedLogs = (receipt: ContractReceipt) => { const parsedLogs = [] for (const rawLog of receipt.logs) { try { const log = registry.interface.parseLog(rawLog) if ( log.name == registry.interface.events[ 'UpkeepPerformed(uint256,bool,uint96,uint256,uint256,bytes)' ].name ) { parsedLogs.push(log as unknown as UpkeepPerformedEvent) } } catch { continue } } return parsedLogs } const parseReorgedUpkeepReportLogs = (receipt: ContractReceipt) => { const parsedLogs = [] for (const rawLog of receipt.logs) { try { const log = registry.interface.parseLog(rawLog) if ( log.name == registry.interface.events['ReorgedUpkeepReport(uint256,bytes)'].name ) { parsedLogs.push(log as unknown as ReorgedUpkeepReportEvent) } } catch { continue } } return parsedLogs } const parseStaleUpkeepReportLogs = (receipt: ContractReceipt) => { const parsedLogs = [] for (const rawLog of receipt.logs) { try { const log = registry.interface.parseLog(rawLog) if ( log.name == registry.interface.events['StaleUpkeepReport(uint256,bytes)'].name ) { parsedLogs.push(log as unknown as StaleUpkeepReportEvent) } } catch { continue } } return parsedLogs } const parseCancelledUpkeepReportLogs = (receipt: ContractReceipt) => { const parsedLogs = [] for (const rawLog of receipt.logs) { try { const log = registry.interface.parseLog(rawLog) if ( log.name == registry.interface.events['CancelledUpkeepReport(uint256,bytes)'].name ) { parsedLogs.push(log as unknown as CancelledUpkeepReportEvent) } } catch { continue } } return parsedLogs } describe('AutomationRegistry2_3', () => { let owner: Signer let keeper1: Signer let keeper2: Signer let keeper3: Signer let keeper4: Signer let keeper5: Signer let nonkeeper: Signer let signer1: Wallet let signer2: Wallet let signer3: Wallet let signer4: Wallet let signer5: Wallet let admin: Signer let payee1: Signer let payee2: Signer let payee3: Signer let payee4: Signer let payee5: Signer let financeAdmin: Signer let upkeepId: BigNumber // conditional upkeep let afUpkeepId: BigNumber // auto funding upkeep let logUpkeepId: BigNumber // log trigger upkeepID let streamsLookupUpkeepId: BigNumber // streams lookup upkeep const numUpkeeps = 4 // see above let keeperAddresses: string[] let payees: string[] let signers: Wallet[] let signerAddresses: string[] let config: OnChainConfig let arbConfig: OnChainConfig let opConfig: OnChainConfig let baseConfig: Parameters<IAutomationRegistry['setConfigTypeSafe']> let arbConfigParams: Parameters<IAutomationRegistry['setConfigTypeSafe']> let opConfigParams: Parameters<IAutomationRegistry['setConfigTypeSafe']> let upkeepManager: string before(async () => { personas = (await getUsers()).personas const compatibleUtilsFactory = await ethers.getContractFactory( 'AutomationCompatibleUtils', ) automationUtils = await compatibleUtilsFactory.deploy() const utilsFactory = await ethers.getContractFactory('AutomationUtils2_3') automationUtils2_3 = await utilsFactory.deploy() linkTokenFactory = await ethers.getContractFactory( 'src/v0.8/shared/test/helpers/LinkTokenTestHelper.sol:LinkTokenTestHelper', ) // need full path because there are two contracts with name MockV3Aggregator mockV3AggregatorFactory = (await ethers.getContractFactory( 'src/v0.8/tests/MockV3Aggregator.sol:MockV3Aggregator', )) as unknown as MockV3AggregatorFactory mockArbGasInfoFactory = await ethers.getContractFactory('MockArbGasInfo') mockOVMGasPriceOracleFactory = await ethers.getContractFactory( 'MockOVMGasPriceOracle', ) upkeepMockFactory = await ethers.getContractFactory('UpkeepMock') upkeepAutoFunderFactory = await ethers.getContractFactory('UpkeepAutoFunder') chainModuleBaseFactory = await ethers.getContractFactory('ChainModuleBase') arbitrumModuleFactory = await ethers.getContractFactory('ArbitrumModule') optimismModuleFactory = await ethers.getContractFactory('OptimismModule') streamsLookupUpkeepFactory = await ethers.getContractFactory( 'StreamsLookupUpkeep', ) owner = personas.Default keeper1 = personas.Carol keeper2 = personas.Eddy keeper3 = personas.Nancy keeper4 = personas.Norbert keeper5 = personas.Nick nonkeeper = personas.Ned admin = personas.Neil payee1 = personas.Nelly payee2 = personas.Norbert payee3 = personas.Nick payee4 = personas.Eddy payee5 = personas.Carol upkeepManager = await personas.Norbert.getAddress() financeAdmin = personas.Nick // signers signer1 = new ethers.Wallet( '0x7777777000000000000000000000000000000000000000000000000000000001', ) signer2 = new ethers.Wallet( '0x7777777000000000000000000000000000000000000000000000000000000002', ) signer3 = new ethers.Wallet( '0x7777777000000000000000000000000000000000000000000000000000000003', ) signer4 = new ethers.Wallet( '0x7777777000000000000000000000000000000000000000000000000000000004', ) signer5 = new ethers.Wallet( '0x7777777000000000000000000000000000000000000000000000000000000005', ) keeperAddresses = [ await keeper1.getAddress(), await keeper2.getAddress(), await keeper3.getAddress(), await keeper4.getAddress(), await keeper5.getAddress(), ] payees = [ await payee1.getAddress(), await payee2.getAddress(), await payee3.getAddress(), await payee4.getAddress(), await payee5.getAddress(), ] signers = [signer1, signer2, signer3, signer4, signer5] // We append 26 random addresses to keepers, payees and signers to get a system of 31 oracles // This allows f value of 1 - 10 for (let i = 0; i < 26; i++) { keeperAddresses.push(randomAddress()) payees.push(randomAddress()) signers.push(ethers.Wallet.createRandom()) } signerAddresses = [] for (const signer of signers) { signerAddresses.push(await signer.getAddress()) } logTriggerConfig = '0x' + automationUtils.interface .encodeFunctionData('_logTriggerConfig', [ { contractAddress: randomAddress(), filterSelector: 0, topic0: ethers.utils.randomBytes(32), topic1: ethers.utils.randomBytes(32), topic2: ethers.utils.randomBytes(32), topic3: ethers.utils.randomBytes(32), }, ]) .slice(10) }) // This function is similar to registry's _calculatePaymentAmount // It uses global fastGasWei, linkEth, and assumes isExecution = false (gasFee = fastGasWei*multiplier) // rest of the parameters are the same const linkForGas = ( upkeepGasSpent: BigNumber, gasOverhead: BigNumber, gasMultiplier: BigNumber, premiumPPB: BigNumber, flatFee: BigNumber, // in millicents l1CostWei?: BigNumber, ) => { l1CostWei = l1CostWei === undefined ? BigNumber.from(0) : l1CostWei const gasSpent = gasOverhead.add(BigNumber.from(upkeepGasSpent)) const base = gasWei .mul(gasMultiplier) .mul(gasSpent) .mul(nativeUSD) .div(linkUSD) const l1Fee = l1CostWei.mul(nativeUSD).div(linkUSD) const gasPayment = base.add(l1Fee) const premium = gasWei .mul(gasMultiplier) .mul(upkeepGasSpent) .add(l1CostWei) .mul(premiumPPB) .mul(nativeUSD) .div(paymentPremiumBase) .add(flatFee.mul(BigNumber.from(10).pow(21))) .div(linkUSD) return { total: gasPayment.add(premium), gasPayment, premium, } } const verifyMaxPayment = async ( registry: IAutomationRegistry, chainModule: IChainModule, maxl1CostWeWithoutMultiplier?: BigNumber, ) => { type TestCase = { name: string multiplier: number gas: number premium: number flatFee: number } const tests: TestCase[] = [ { name: 'no fees', multiplier: 1, gas: 100000, premium: 0, flatFee: 0, }, { name: 'basic fees', multiplier: 1, gas: 100000, premium: 250000000, flatFee: 1000000, }, { name: 'max fees', multiplier: 3, gas: 10000000, premium: 250000000, flatFee: 1000000, }, ] const fPlusOne = BigNumber.from(f + 1) const chainModuleOverheads = await chainModule.getGasOverhead() const totalConditionalOverhead = registryConditionalOverhead .add(registryPerSignerGasOverhead.mul(fPlusOne)) .add( registryPerPerformByteGasOverhead .add(chainModuleOverheads.chainModulePerByteOverhead) .mul( maxPerformDataSize .add(registryTransmitCalldataFixedBytesOverhead) .add( registryTransmitCalldataPerSignerBytesOverhead.mul(fPlusOne), ), ), ) .add(chainModuleOverheads.chainModuleFixedOverhead) const totalLogOverhead = registryLogOverhead .add(registryPerSignerGasOverhead.mul(fPlusOne)) .add( registryPerPerformByteGasOverhead .add(chainModuleOverheads.chainModulePerByteOverhead) .mul( maxPerformDataSize .add(registryTransmitCalldataFixedBytesOverhead) .add( registryTransmitCalldataPerSignerBytesOverhead.mul(fPlusOne), ), ), ) .add(chainModuleOverheads.chainModuleFixedOverhead) const financeAdminAddress = await financeAdmin.getAddress() for (const test of tests) { await registry.connect(owner).setConfigTypeSafe( signerAddresses, keeperAddresses, f, { checkGasLimit, stalenessSeconds, gasCeilingMultiplier: test.multiplier, maxCheckDataSize, maxPerformDataSize, maxRevertDataSize, maxPerformGas, fallbackGasPrice, fallbackLinkPrice, fallbackNativePrice, transcoder: transcoder.address, registrars: [], upkeepPrivilegeManager: upkeepManager, chainModule: chainModule.address, reorgProtectionEnabled: true, financeAdmin: financeAdminAddress, }, offchainVersion, offchainBytes, [linkToken.address], [ { gasFeePPB: test.premium, flatFeeMilliCents: test.flatFee, priceFeed: linkUSDFeed.address, fallbackPrice: fallbackLinkPrice, minSpend: minUpkeepSpend, decimals: 18, }, ], ) const conditionalPrice = await registry.getMaxPaymentForGas( upkeepId, Trigger.CONDITION, test.gas, linkToken.address, ) expect(conditionalPrice).to.equal( linkForGas( BigNumber.from(test.gas), totalConditionalOverhead, BigNumber.from(test.multiplier), BigNumber.from(test.premium), BigNumber.from(test.flatFee), maxl1CostWeWithoutMultiplier?.mul(BigNumber.from(test.multiplier)), ).total, ) const logPrice = await registry.getMaxPaymentForGas( upkeepId, Trigger.LOG, test.gas, linkToken.address, ) expect(logPrice).to.equal( linkForGas( BigNumber.from(test.gas), totalLogOverhead, BigNumber.from(test.multiplier), BigNumber.from(test.premium), BigNumber.from(test.flatFee), maxl1CostWeWithoutMultiplier?.mul(BigNumber.from(test.multiplier)), ).total, ) } } const verifyConsistentAccounting = async ( maxAllowedSpareChange: BigNumber, ) => { const expectedLinkBalance = await registry.getReserveAmount( linkToken.address, ) const linkTokenBalance = await linkToken.balanceOf(registry.address) const upkeepIdBalance = (await registry.getUpkeep(upkeepId)).balance let totalKeeperBalance = BigNumber.from(0) for (let i = 0; i < keeperAddresses.length; i++) { totalKeeperBalance = totalKeeperBalance.add( (await registry.getTransmitterInfo(keeperAddresses[i])).balance, ) } const linkAvailableForPayment = await registry.linkAvailableForPayment() assert.isTrue(expectedLinkBalance.eq(linkTokenBalance)) assert.isTrue( upkeepIdBalance .add(totalKeeperBalance) .add(linkAvailableForPayment) .lte(expectedLinkBalance), ) assert.isTrue( expectedLinkBalance .sub(upkeepIdBalance) .sub(totalKeeperBalance) .sub(linkAvailableForPayment) .lte(maxAllowedSpareChange), ) } interface GetTransmitTXOptions { numSigners?: number startingSignerIndex?: number gasLimit?: BigNumberish gasPrice?: BigNumberish performGas?: BigNumberish performDatas?: string[] checkBlockNum?: number checkBlockHash?: string logBlockHash?: BytesLike txHash?: BytesLike logIndex?: number timestamp?: number } const getTransmitTx = async ( registry: IAutomationRegistry, transmitter: Signer, upkeepIds: BigNumber[], overrides: GetTransmitTXOptions = {}, ) => { const latestBlock = await ethers.provider.getBlock('latest') const configDigest = (await registry.getState()).state.latestConfigDigest const config = { numSigners: f + 1, startingSignerIndex: 0, performDatas: undefined, performGas, checkBlockNum: latestBlock.number, checkBlockHash: latestBlock.hash, logIndex: 0, txHash: undefined, // assigned uniquely below logBlockHash: undefined, // assigned uniquely below timestamp: now(), gasLimit: undefined, gasPrice: undefined, } Object.assign(config, overrides) const upkeeps: UpkeepData[] = [] for (let i = 0; i < upkeepIds.length; i++) { let trigger: string switch (getTriggerType(upkeepIds[i])) { case Trigger.CONDITION: trigger = encodeBlockTrigger({ blockNum: config.checkBlockNum, blockHash: config.checkBlockHash, }) break case Trigger.LOG: trigger = encodeLogTrigger({ logBlockHash: config.logBlockHash || ethers.utils.randomBytes(32), txHash: config.txHash || ethers.utils.randomBytes(32), logIndex: config.logIndex, blockNum: config.checkBlockNum, blockHash: config.checkBlockHash, }) break } upkeeps.push({ Id: upkeepIds[i], performGas: config.performGas, trigger, performData: config.performDatas ? config.performDatas[i] : '0x', }) } const report = makeReport(upkeeps) const reportContext = [configDigest, epochAndRound5_1, emptyBytes32] const sigs = signReport( reportContext, report, signers.slice( config.startingSignerIndex, config.startingSignerIndex + config.numSigners, ), ) type txOverride = { gasLimit?: BigNumberish | Promise<BigNumberish> gasPrice?: BigNumberish | Promise<BigNumberish> } const txOverrides: txOverride = {} if (config.gasLimit) { txOverrides.gasLimit = config.gasLimit } if (config.gasPrice) { txOverrides.gasPrice = config.gasPrice } return registry .connect(transmitter) .transmit( [configDigest, epochAndRound5_1, emptyBytes32], report, sigs.rs, sigs.ss, sigs.vs, txOverrides, ) } const getTransmitTxWithReport = async ( registry: IAutomationRegistry, transmitter: Signer, report: BytesLike, ) => { const configDigest = (await registry.getState()).state.latestConfigDigest const reportContext = [configDigest, epochAndRound5_1, emptyBytes32] const sigs = signReport(reportContext, report, signers.slice(0, f + 1)) return registry .connect(transmitter) .transmit( [configDigest, epochAndRound5_1, emptyBytes32], report, sigs.rs, sigs.ss, sigs.vs, ) } const setup = async () => { linkToken = await linkTokenFactory.connect(owner).deploy() gasPriceFeed = await mockV3AggregatorFactory .connect(owner) .deploy(0, gasWei) linkUSDFeed = await mockV3AggregatorFactory .connect(owner) .deploy(8, linkUSD) nativeUSDFeed = await mockV3AggregatorFactory .connect(owner) .deploy(8, nativeUSD) const upkeepTranscoderFactory = await ethers.getContractFactory( 'UpkeepTranscoder5_0', ) transcoder = await upkeepTranscoderFactory.connect(owner).deploy() mockArbGasInfo = await mockArbGasInfoFactory.connect(owner).deploy() mockOVMGasPriceOracle = await mockOVMGasPriceOracleFactory .connect(owner) .deploy() chainModuleBase = await chainModuleBaseFactory.connect(owner).deploy() arbitrumModule = await arbitrumModuleFactory.connect(owner).deploy() optimismModule = await optimismModuleFactory.connect(owner).deploy() streamsLookupUpkeep = await streamsLookupUpkeepFactory .connect(owner) .deploy( BigNumber.from('10000'), BigNumber.from('100'), false /* useArbBlock */, true /* staging */, false /* verify mercury response */, ) const arbOracleCode = await ethers.provider.send('eth_getCode', [ mockArbGasInfo.address, ]) await ethers.provider.send('hardhat_setCode', [ '0x000000000000000000000000000000000000006C', arbOracleCode, ]) const optOracleCode = await ethers.provider.send('eth_getCode', [ mockOVMGasPriceOracle.address, ]) await ethers.provider.send('hardhat_setCode', [ '0x420000000000000000000000000000000000000F', optOracleCode, ]) const mockArbSys = await new MockArbSysFactory(owner).deploy() const arbSysCode = await ethers.provider.send('eth_getCode', [ mockArbSys.address, ]) await ethers.provider.send('hardhat_setCode', [ '0x0000000000000000000000000000000000000064', arbSysCode, ]) const financeAdminAddress = await financeAdmin.getAddress() config = { checkGasLimit, stalenessSeconds, gasCeilingMultiplier, maxCheckDataSize, maxPerformDataSize, maxRevertDataSize, maxPerformGas, fallbackGasPrice, fallbackLinkPrice, fallbackNativePrice, transcoder: transcoder.address, registrars: [], upkeepPrivilegeManager: upkeepManager, chainModule: chainModuleBase.address, reorgProtectionEnabled: true, financeAdmin: financeAdminAddress, } arbConfig = { ...config } arbConfig.chainModule = arbitrumModule.address opConfig = { ...config } opConfig.chainModule = optimismModule.address baseConfig = [ signerAddresses, keeperAddresses, f, config, offchainVersion, offchainBytes, [linkToken.address], [ { gasFeePPB: paymentPremiumPPB, flatFeeMilliCents, priceFeed: linkUSDFeed.address, fallbackPrice: fallbackLinkPrice, minSpend: minUpkeepSpend, decimals: 18, }, ], ] arbConfigParams = [ signerAddresses, keeperAddresses, f, arbConfig, offchainVersion, offchainBytes, [linkToken.address], [ { gasFeePPB: paymentPremiumPPB, flatFeeMilliCents, priceFeed: linkUSDFeed.address, fallbackPrice: fallbackLinkPrice, minSpend: minUpkeepSpend, decimals: 18, }, ], ] opConfigParams = [ signerAddresses, keeperAddresses, f, opConfig, offchainVersion, offchainBytes, [linkToken.address], [ { gasFeePPB: paymentPremiumPPB, flatFeeMilliCents, priceFeed: linkUSDFeed.address, fallbackPrice: fallbackLinkPrice, minSpend: minUpkeepSpend, decimals: 18, }, ], ] const registryParams: Parameters<typeof deployRegistry23> = [ owner, linkToken.address, linkUSDFeed.address, nativeUSDFeed.address, gasPriceFeed.address, zeroAddress, 0, // onchain payout mode wrappedNativeTokenAddress, ] registry = await deployRegistry23(...registryParams) arbRegistry = await deployRegistry23(...registryParams) opRegistry = await deployRegistry23(...registryParams) mgRegistry = await deployRegistry23(...registryParams) blankRegistry = await deployRegistry23(...registryParams) registryConditionalOverhead = await registry.getConditionalGasOverhead() registryLogOverhead = await registry.getLogGasOverhead() registryPerSignerGasOverhead = await registry.getPerSignerGasOverhead() registryPerPerformByteGasOverhead = await registry.getPerPerformByteGasOverhead() registryTransmitCalldataFixedBytesOverhead = await registry.getTransmitCalldataFixedBytesOverhead() registryTransmitCalldataPerSignerBytesOverhead = await registry.getTransmitCalldataPerSignerBytesOverhead() cancellationDelay = (await registry.getCancellationDelay()).toNumber() await registry.connect(owner).setConfigTypeSafe(...baseConfig) await mgRegistry.connect(owner).setConfigTypeSafe(...baseConfig) await arbRegistry.connect(owner).setConfigTypeSafe(...arbConfigParams) await opRegistry.connect(owner).setConfigTypeSafe(...opConfigParams) for (const reg of [registry, arbRegistry, opRegistry, mgRegistry]) { await reg.connect(owner).setPayees(payees) await linkToken.connect(admin).approve(reg.address, toWei('1000')) await linkToken.connect(owner).approve(reg.address, toWei('1000')) } mock = await upkeepMockFactory.deploy() await linkToken .connect(owner) .transfer(await admin.getAddress(), toWei('1000')) let tx = await registry .connect(owner) .registerUpkeep( mock.address, performGas, await admin.getAddress(), Trigger.CONDITION, linkToken.address, randomBytes, '0x', '0x', ) upkeepId = await getUpkeepID(tx) autoFunderUpkeep = await upkeepAutoFunderFactory .connect(owner) .deploy(linkToken.address, registry.address) tx = await registry .connect(owner) .registerUpkeep( autoFunderUpkeep.address, performGas, autoFunderUpkeep.address, Trigger.CONDITION, linkToken.address, '0x', '0x', '0x', ) afUpkeepId = await getUpkeepID(tx) ltUpkeep = await deployMockContract(owner, ILogAutomationactory.abi) tx = await registry .connect(owner) .registerUpkeep( ltUpkeep.address, performGas, await admin.getAddress(), Trigger.LOG, linkToken.address, '0x', logTriggerConfig, emptyBytes, ) logUpkeepId = await getUpkeepID(tx) await autoFunderUpkeep.setUpkeepId(afUpkeepId) // Give enough funds for upkeep as well as to the upkeep contract await linkToken .connect(owner) .transfer(autoFunderUpkeep.address, toWei('1000')) tx = await registry .connect(owner) .registerUpkeep( streamsLookupUpkeep.address, performGas, await admin.getAddress(), Trigger.CONDITION, linkToken.address, '0x', '0x', '0x', ) streamsLookupUpkeepId = await getUpkeepID(tx) } const getMultipleUpkeepsDeployedAndFunded = async ( numPassingConditionalUpkeeps: number, numPassingLogUpkeeps: number, numFailingUpkeeps: number, ) => { const passingConditionalUpkeepIds = [] const passingLogUpkeepIds = [] const failingUpkeepIds = [] for (let i = 0; i < numPassingConditionalUpkeeps; i++) { const mock = await upkeepMockFactory.deploy() await mock.setCanPerform(true) await mock.setPerformGasToBurn(BigNumber.from('0')) const tx = await registry .connect(owner) .registerUpkeep( mock.address, performGas, await admin.getAddress(), Trigger.CONDITION, linkToken.address, '0x', '0x', '0x', ) const condUpkeepId = await getUpkeepID(tx) passingConditionalUpkeepIds.push(condUpkeepId) // Add funds to passing upkeeps await registry.connect(admin).addFunds(condUpkeepId, toWei('100')) } for (let i = 0; i < numPassingLogUpkeeps; i++) { const mock = await upkeepMockFactory.deploy() await mock.setCanPerform(true) await mock.setPerformGasToBurn(BigNumber.from('0')) const tx = await registry .connect(owner) .registerUpkeep( mock.address, performGas, await admin.getAddress(), Trigger.LOG, linkToken.address, '0x', logTriggerConfig, emptyBytes, ) const logUpkeepId = await getUpkeepID(tx) passingLogUpkeepIds.push(logUpkeepId) // Add funds to passing upkeeps await registry.connect(admin).addFunds(logUpkeepId, toWei('100')) } for (let i = 0; i < numFailingUpkeeps; i++) { const mock = await upkeepMockFactory.deploy() await mock.setCanPerform(true) await mock.setPerformGasToBurn(BigNumber.from('0')) const tx = await registry .connect(owner) .registerUpkeep( mock.address, performGas, await admin.getAddress(), Trigger.CONDITION, linkToken.address, '0x', '0x', '0x', ) const failingUpkeepId = await getUpkeepID(tx) failingUpkeepIds.push(failingUpkeepId) } return { passingConditionalUpkeepIds, passingLogUpkeepIds, failingUpkeepIds, } } beforeEach(async () => { await loadFixture(setup) }) describe('#transmit', () => { const fArray = [1, 5, 10] it('reverts when registry is paused', async () => { await registry.connect(owner).pause() await evmRevertCustomError( getTransmitTx(registry, keeper1, [upkeepId]), registry, 'RegistryPaused', ) }) it('reverts when called by non active transmitter', async () => { await evmRevertCustomError( getTransmitTx(registry, payee1, [upkeepId]), registry, 'OnlyActiveTransmitters', ) }) it('reverts when report data lengths mismatches', async () => { const upkeepIds = [] const gasLimits: BigNumber[] = [] const triggers: string[] = [] const performDatas = [] upkeepIds.push(upkeepId) gasLimits.push(performGas) triggers.push('0x') performDatas.push('0x') // Push an extra perform data performDatas.push('0x') const report = encodeReport({ fastGasWei: 0, linkUSD: 0, upkeepIds, gasLimits, triggers, performDatas, }) await evmRevertCustomError( getTransmitTxWithReport(registry, keeper1, report), registry, 'InvalidReport', ) }) it('returns early when invalid upkeepIds are included in report', async () => { const tx = await getTransmitTx(registry, keeper1, [ upkeepId.add(BigNumber.from('1')), ]) const receipt = await tx.wait() const cancelledUpkeepReportLogs = parseCancelledUpkeepReportLogs(receipt) // exactly 1 CancelledUpkeepReport log should be emitted assert.equal(cancelledUpkeepReportLogs.length, 1) }) it('performs even when the upkeep has insufficient funds and the upkeep pays out all the remaining balance', async () => { // add very little fund to this upkeep await registry.connect(admin).addFunds(upkeepId, BigNumber.from(10)) const tx = await getTransmitTx(registry, keeper1, [upkeepId]) const receipt = await tx.wait() // the upkeep is underfunded in transmit but still performed const upkeepPerformedLogs = parseUpkeepPerformedLogs(receipt) assert.equal(upkeepPerformedLogs.length, 1) const balance = (await registry.getUpkeep(upkeepId)).balance assert.equal(balance.toNumber(), 0) }) context('When the upkeep is funded', async () => { beforeEach(async () => { // Fund the upkeep await Promise.all([ registry.connect(admin).addFunds(upkeepId, toWei('100')), registry.connect(admin).addFunds(logUpkeepId, toWei('100')), ]) }) it('handles duplicate upkeepIDs', async () => { const tests: [string, BigNumber, number, number][] = [ // [name, upkeep, num stale, num performed] ['conditional', upkeepId, 1, 1], // checkBlocks must be sequential ['log-trigger', logUpkeepId, 0, 2], // logs are deduped based on the "trigger ID" ] for (const [type, id, nStale, nPerformed] of tests) { const tx = await getTransmitTx(registry, keeper1, [id, id]) const receipt = await tx.wait() const staleUpkeepReport = parseStaleUpkeepReportLogs(receipt) const upkeepPerformedLogs = parseUpkeepPerformedLogs(receipt) assert.equal( staleUpkeepReport.length, nStale, `wrong log count for ${type} upkeep`, ) assert.equal( upkeepPerformedLogs.length, nPerformed, `wrong log count for ${type} upkeep`, ) } }) it('handles duplicate log triggers', async () => { const logBlockHash = ethers.utils.randomBytes(32) const txHash = ethers.utils.randomBytes(32) const logIndex = 0 const expectedDedupKey = ethers.utils.solidityKeccak256( ['uint256', 'bytes32', 'bytes32', 'uint32'], [logUpkeepId, logBlockHash, txHash, logIndex], ) assert.isFalse(await registry.hasDedupKey(expectedDedupKey)) const tx = await getTransmitTx( registry, keeper1, [logUpkeepId, logUpkeepId], { logBlockHash, txHash, logIndex }, // will result in the same dedup key ) const receipt = await tx.wait() const staleUpkeepReport = parseStaleUpkeepReportLogs(receipt) const upkeepPerformedLogs = parseUpkeepPerformedLogs(receipt) assert.equal(staleUpkeepReport.length, 1) assert.equal(upkeepPerformedLogs.length, 1) assert.isTrue(await registry.hasDedupKey(expectedDedupKey)) await expect(tx) .to.emit(registry, 'DedupKeyAdded') .withArgs(expectedDedupKey) }) it('returns early when check block number is less than last perform (block)', async () => { // First perform an upkeep to put last perform block number on upkeep state const tx = await getTransmitTx(registry, keeper1, [upkeepId]) await tx.wait() const lastPerformed = (await registry.getUpkeep(upkeepId)) .lastPerformedBlockNumber const lastPerformBlock = await ethers.provider.getBlock(lastPerformed) assert.equal(lastPerformed.toString(), tx.blockNumber?.toString()) // Try to transmit a report which has checkBlockNumber = lastPerformed-1, should result in stale report const transmitTx = await getTransmitTx(registry, keeper1, [upkeepId], { checkBlockNum: lastPerformBlock.number - 1, checkBlockHash: lastPerformBlock.parentHash, }) const receipt = await transmitTx.wait() const staleUpkeepReportLogs = parseStaleUpkeepReportLogs(receipt) // exactly 1 StaleUpkeepReportLogs log should be emitted assert.equal(staleUpkeepReportLogs.length, 1) }) it('handles case when check block hash does not match', async () => { const tests: [string, BigNumber][] = [ ['conditional', upkeepId], ['log-trigger', logUpkeepId], ] for (const [type, id] of tests) { const latestBlock = await ethers.provider.getBlock('latest') // Try to transmit a report which has incorrect checkBlockHash const tx = await getTransmitTx(registry, keeper1, [id], { checkBlockNum: latestBlock.number - 1, checkBlockHash: latestBlock.hash, // should be latestBlock.parentHash }) const receipt = await tx.wait() const reorgedUpkeepReportLogs = parseReorgedUpkeepReportLogs(receipt) // exactly 1 ReorgedUpkeepReportLogs log should be emitted assert.equal( reorgedUpkeepReportLogs.length, 1, `wrong log count for ${type} upkeep`, ) } }) it('handles case when check block number is older than 256 blocks', async () => { for (let i = 0; i < 256; i++) { await ethers.provider.send('evm_mine', []) } const tests: [string, BigNumber][] = [ ['conditional', upkeepId], ['log-trigger', logUpkeepId], ] for (const [type, id] of tests) { const latestBlock = await ethers.provider.getBlock('latest') const old = await ethers.provider.getBlock(latestBlock.number - 256) // Try to transmit a report which has incorrect checkBlockHash const tx = await getTransmitTx(registry, keeper1, [id], { checkBlockNum: old.number, checkBlockHash: old.hash, }) const receipt = await tx.wait() const reorgedUpkeepReportLogs = parseReorgedUpkeepReportLogs(receipt) // exactly 1 ReorgedUpkeepReportLogs log should be emitted assert.equal( reorgedUpkeepReportLogs.length, 1, `wrong log count for ${type} upkeep`, ) } }) it('allows bypassing reorg protection with empty blockhash', async () => { const tests: [string, BigNumber][] = [ ['conditional', upkeepId], ['log-trigger', logUpkeepId], ] for (const [type, id] of tests) { const latestBlock = await ethers.provider.getBlock('latest') const tx = await getTransmitTx(registry, keeper1, [id], { checkBlockNum: latestBlock.number, checkBlockHash: emptyBytes32, }) const receipt = await tx.wait() const upkeepPerformedLogs = parseUpkeepPerformedLogs(receipt) assert.equal( upkeepPerformedLogs.length, 1, `wrong log count for ${type} upkeep`, ) } }) it('allows bypassing reorg protection with reorgProtectionEnabled false config', async () => { const tests: [string, BigNumber][] = [ ['conditional', upkeepId], ['log-trigger', logUpkeepId], ] const newConfig = config newConfig.reorgProtectionEnabled = false await registry // used to test initial configurations .connect(owner) .setConfigTypeSafe( signerAddresses, keeperAddresses, f, newConfig, offchainVersion, offchainBytes, baseConfig[6], baseConfig[7], ) for (const [type, id] of tests) { const latestBlock = await ethers.provider.getBlock('latest') // Try to transmit a report which has incorrect checkBlockHash const tx = await getTransmitTx(registry, keeper1, [id], { checkBlockNum: latestBlock.number - 1, checkBlockHash: latestBlock.hash, // should be latestBlock.parentHash }) const receipt = await tx.wait() const upkeepPerformedLogs = parseUpkeepPerformedLogs(receipt) assert.equal( upkeepPerformedLogs.length, 1, `wrong log count for ${type} upkeep`, ) } }) it('allows very old trigger block numbers when bypassing reorg protection with reorgProtectionEnabled config', async () => { const newConfig = config newConfig.reorgProtectionEnabled = false await registry // used to test initial configurations .connect(owner) .setConfigTypeSafe( signerAddresses, keeperAddresses, f, newConfig, offchainVersion, offchainBytes, baseConfig[6], baseConfig[7], ) for (let i = 0; i < 256; i++) { await ethers.provider.send('evm_mine', []) } const tests: [string, BigNumber][] = [ ['conditional', upkeepId], ['log-trigger', logUpkeepId], ] for (const [type, id] of tests) { const latestBlock = await ethers.provider.getBlock('latest') const old = await ethers.provider.getBlock(latestBlock.number - 256) // Try to transmit a report which has incorrect checkBlockHash const tx = await getTransmitTx(registry, keeper1, [id], { checkBlockNum: old.number, checkBlockHash: old.hash, }) const receipt = await tx.wait() const upkeepPerformedLogs = parseUpkeepPerformedLogs(receipt) assert.equal( upkeepPerformedLogs.length, 1, `wrong log count for ${type} upkeep`, ) } }) it('allows very old trigger block numbers when bypassing reorg protection with empty blockhash', async () => { // mine enough blocks so that blockhash(1) is unavailable for (let i = 0; i <= 256; i++) { await ethers.provider.send('evm_mine', []) } const tests: [string, BigNumber][] = [ ['conditional', upkeepId], ['log-trigger', logUpkeepId], ] for (const [type, id] of tests) { const tx = await getTransmitTx(registry, keeper1, [id],