@hyperlane-xyz/core
Version:
Core solidity contracts for Hyperlane
1,509 lines (1,400 loc) • 204 kB
text/typescript
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],