diff --git a/contracts/.changeset/thirty-lamps-reply.md b/contracts/.changeset/thirty-lamps-reply.md
new file mode 100644
index 00000000000..d8bcf8d4e83
--- /dev/null
+++ b/contracts/.changeset/thirty-lamps-reply.md
@@ -0,0 +1,5 @@
+---
+'@chainlink/contracts': patch
+---
+
+implement an auto registry for zksync with no forwarder interface change
diff --git a/contracts/.solhintignore b/contracts/.solhintignore
index 55d195c3059..bad1935442b 100644
--- a/contracts/.solhintignore
+++ b/contracts/.solhintignore
@@ -18,7 +18,6 @@
 ./src/v0.8/automation/libraries/internal/Cron.sol
 ./src/v0.8/automation/AutomationForwarder.sol
 ./src/v0.8/automation/AutomationForwarderLogic.sol
-./src/v0.8/automation/ZKSyncAutomationForwarder.sol
 ./src/v0.8/automation/interfaces/v2_2/IAutomationRegistryMaster.sol
 ./src/v0.8/automation/interfaces/v2_3/IAutomationRegistryMaster2_3.sol
 
diff --git a/contracts/scripts/generate-zksync-automation-master-interface-v2_3.ts b/contracts/scripts/generate-zksync-automation-master-interface-v2_3.ts
new file mode 100644
index 00000000000..1b91fd36361
--- /dev/null
+++ b/contracts/scripts/generate-zksync-automation-master-interface-v2_3.ts
@@ -0,0 +1,58 @@
+/**
+ * @description this script generates a master interface for interacting with the automation registry
+ * @notice run this script with pnpm ts-node ./scripts/generate-zksync-automation-master-interface-v2_3.ts
+ */
+import { ZKSyncAutomationRegistry2_3__factory as Registry } from '../typechain/factories/ZKSyncAutomationRegistry2_3__factory'
+import { ZKSyncAutomationRegistryLogicA2_3__factory as RegistryLogicA } from '../typechain/factories/ZKSyncAutomationRegistryLogicA2_3__factory'
+import { ZKSyncAutomationRegistryLogicB2_3__factory as RegistryLogicB } from '../typechain/factories/ZKSyncAutomationRegistryLogicB2_3__factory'
+import { ZKSyncAutomationRegistryLogicC2_3__factory as RegistryLogicC } from '../typechain/factories/ZKSyncAutomationRegistryLogicC2_3__factory'
+import { utils } from 'ethers'
+import fs from 'fs'
+import { exec } from 'child_process'
+
+const dest = 'src/v0.8/automation/interfaces/zksync'
+const srcDest = `${dest}/IZKSyncAutomationRegistryMaster2_3.sol`
+const tmpDest = `${dest}/tmp.txt`
+
+const combinedABI = []
+const abiSet = new Set()
+const abis = [
+  Registry.abi,
+  RegistryLogicA.abi,
+  RegistryLogicB.abi,
+  RegistryLogicC.abi,
+]
+
+for (const abi of abis) {
+  for (const entry of abi) {
+    const id = utils.id(JSON.stringify(entry))
+    if (!abiSet.has(id)) {
+      abiSet.add(id)
+      if (
+        entry.type === 'function' &&
+        (entry.name === 'checkUpkeep' ||
+          entry.name === 'checkCallback' ||
+          entry.name === 'simulatePerformUpkeep')
+      ) {
+        entry.stateMutability = 'view' // override stateMutability for check / callback / simulate functions
+      }
+      combinedABI.push(entry)
+    }
+  }
+}
+
+const checksum = utils.id(abis.join(''))
+
+fs.writeFileSync(`${tmpDest}`, JSON.stringify(combinedABI))
+
+const cmd = `
+cat ${tmpDest} | pnpm abi-to-sol --solidity-version ^0.8.4 --license MIT > ${srcDest} IZKSyncAutomationRegistryMaster2_3;
+echo "// solhint-disable \n// abi-checksum: ${checksum}" | cat - ${srcDest} > ${tmpDest} && mv ${tmpDest} ${srcDest};
+pnpm prettier --write ${srcDest};
+`
+
+exec(cmd)
+
+console.log(
+  'generated new master interface for zksync automation registry v2_3',
+)
diff --git a/contracts/src/v0.8/automation/ZKSyncAutomationForwarder.sol b/contracts/src/v0.8/automation/ZKSyncAutomationForwarder.sol
index cfbff1365e1..fd6eb3dee99 100644
--- a/contracts/src/v0.8/automation/ZKSyncAutomationForwarder.sol
+++ b/contracts/src/v0.8/automation/ZKSyncAutomationForwarder.sol
@@ -2,16 +2,19 @@
 pragma solidity ^0.8.16;
 
 import {IAutomationRegistryConsumer} from "./interfaces/IAutomationRegistryConsumer.sol";
+import {GAS_BOUND_CALLER, IGasBoundCaller} from "./interfaces/zksync/IGasBoundCaller.sol";
 
-uint256 constant PERFORM_GAS_CUSHION = 5_000;
+uint256 constant PERFORM_GAS_CUSHION = 50_000;
 
 /**
- * @title AutomationForwarder is a relayer that sits between the registry and the customer's target contract
+ * @title ZKSyncAutomationForwarder is a relayer that sits between the registry and the customer's target contract
  * @dev The purpose of the forwarder is to give customers a consistent address to authorize against,
  * which stays consistent between migrations. The Forwarder also exposes the registry address, so that users who
  * want to programmatically interact with the registry (ie top up funds) can do so.
  */
 contract ZKSyncAutomationForwarder {
+  error InvalidCaller(address);
+
   /// @notice the user's target contract address
   address private immutable i_target;
 
@@ -31,11 +34,14 @@ contract ZKSyncAutomationForwarder {
    * @param gasAmount is the amount of gas to use in the call
    * @param data is the 4 bytes function selector + arbitrary function data
    * @return success indicating whether the target call succeeded or failed
+   * @return gasUsed the total gas used from this forwarding call
    */
   function forward(uint256 gasAmount, bytes memory data) external returns (bool success, uint256 gasUsed) {
-    if (msg.sender != address(s_registry)) revert();
+    if (msg.sender != address(s_registry)) revert InvalidCaller(msg.sender);
+
+    uint256 g1 = gasleft();
     address target = i_target;
-    gasUsed = gasleft();
+
     assembly {
       let g := gas()
       // Compute g -= PERFORM_GAS_CUSHION and check for underflow
@@ -52,10 +58,18 @@ contract ZKSyncAutomationForwarder {
       if iszero(extcodesize(target)) {
         revert(0, 0)
       }
-      // call with exact gas
-      success := call(gasAmount, target, 0, add(data, 0x20), mload(data), 0, 0)
     }
-    gasUsed = gasUsed - gasleft();
+
+    bytes memory returnData;
+    // solhint-disable-next-line avoid-low-level-calls
+    (success, returnData) = GAS_BOUND_CALLER.delegatecall{gas: gasAmount}(
+      abi.encodeWithSelector(IGasBoundCaller.gasBoundCall.selector, target, gasAmount, data)
+    );
+    uint256 pubdataGasSpent;
+    if (success) {
+      (, pubdataGasSpent) = abi.decode(returnData, (bytes, uint256));
+    }
+    gasUsed = g1 - gasleft() + pubdataGasSpent;
     return (success, gasUsed);
   }
 
@@ -63,7 +77,8 @@ contract ZKSyncAutomationForwarder {
     return i_target;
   }
 
-  fallback() external {
+  // solhint-disable-next-line no-complex-fallback
+  fallback() external payable {
     // copy to memory for assembly access
     address logic = i_logic;
     // copied directly from OZ's Proxy contract
diff --git a/contracts/src/v0.8/automation/interfaces/zksync/IGasBoundCaller.sol b/contracts/src/v0.8/automation/interfaces/zksync/IGasBoundCaller.sol
new file mode 100644
index 00000000000..9edb541f9aa
--- /dev/null
+++ b/contracts/src/v0.8/automation/interfaces/zksync/IGasBoundCaller.sol
@@ -0,0 +1,8 @@
+// SPDX-License-Identifier: BUSL-1.1
+pragma solidity 0.8.19;
+
+address constant GAS_BOUND_CALLER = address(0xc706EC7dfA5D4Dc87f29f859094165E8290530f5);
+
+interface IGasBoundCaller {
+  function gasBoundCall(address _to, uint256 _maxTotalGas, bytes calldata _data) external payable;
+}
diff --git a/contracts/src/v0.8/automation/interfaces/zksync/ISystemContext.sol b/contracts/src/v0.8/automation/interfaces/zksync/ISystemContext.sol
new file mode 100644
index 00000000000..c8f480065c4
--- /dev/null
+++ b/contracts/src/v0.8/automation/interfaces/zksync/ISystemContext.sol
@@ -0,0 +1,10 @@
+// SPDX-License-Identifier: BUSL-1.1
+pragma solidity 0.8.19;
+
+ISystemContext constant SYSTEM_CONTEXT_CONTRACT = ISystemContext(address(0x800b));
+
+interface ISystemContext {
+  function gasPrice() external view returns (uint256);
+  function gasPerPubdataByte() external view returns (uint256 gasPerPubdataByte);
+  function getCurrentPubdataSpent() external view returns (uint256 currentPubdataSpent);
+}
diff --git a/contracts/src/v0.8/automation/interfaces/zksync/IZKSyncAutomationRegistryMaster2_3.sol b/contracts/src/v0.8/automation/interfaces/zksync/IZKSyncAutomationRegistryMaster2_3.sol
new file mode 100644
index 00000000000..26b8a7d5c55
--- /dev/null
+++ b/contracts/src/v0.8/automation/interfaces/zksync/IZKSyncAutomationRegistryMaster2_3.sol
@@ -0,0 +1,441 @@
+// solhint-disable
+// abi-checksum: 0x5857a77a981fcb60dbdac0700e68420cbe544249b20d9326d51c5ef8584c5dd7
+// SPDX-License-Identifier: MIT
+// !! THIS FILE WAS AUTOGENERATED BY abi-to-sol v0.6.6. SEE SOURCE BELOW. !!
+pragma solidity ^0.8.4;
+
+interface IZKSyncAutomationRegistryMaster2_3 {
+  error ArrayHasNoEntries();
+  error CannotCancel();
+  error CheckDataExceedsLimit();
+  error ConfigDigestMismatch();
+  error DuplicateEntry();
+  error DuplicateSigners();
+  error GasLimitCanOnlyIncrease();
+  error GasLimitOutsideRange();
+  error IncorrectNumberOfFaultyOracles();
+  error IncorrectNumberOfSignatures();
+  error IncorrectNumberOfSigners();
+  error IndexOutOfRange();
+  error InsufficientBalance(uint256 available, uint256 requested);
+  error InsufficientLinkLiquidity();
+  error InvalidDataLength();
+  error InvalidFeed();
+  error InvalidPayee();
+  error InvalidRecipient();
+  error InvalidReport();
+  error InvalidSigner();
+  error InvalidToken();
+  error InvalidTransmitter();
+  error InvalidTrigger();
+  error InvalidTriggerType();
+  error MigrationNotPermitted();
+  error MustSettleOffchain();
+  error MustSettleOnchain();
+  error NotAContract();
+  error OnlyActiveSigners();
+  error OnlyActiveTransmitters();
+  error OnlyCallableByAdmin();
+  error OnlyCallableByLINKToken();
+  error OnlyCallableByOwnerOrAdmin();
+  error OnlyCallableByOwnerOrRegistrar();
+  error OnlyCallableByPayee();
+  error OnlyCallableByProposedAdmin();
+  error OnlyCallableByProposedPayee();
+  error OnlyCallableByUpkeepPrivilegeManager();
+  error OnlyFinanceAdmin();
+  error OnlyPausedUpkeep();
+  error OnlySimulatedBackend();
+  error OnlyUnpausedUpkeep();
+  error ParameterLengthError();
+  error ReentrantCall();
+  error RegistryPaused();
+  error RepeatedSigner();
+  error RepeatedTransmitter();
+  error TargetCheckReverted(bytes reason);
+  error TooManyOracles();
+  error TranscoderNotSet();
+  error TransferFailed();
+  error UpkeepAlreadyExists();
+  error UpkeepCancelled();
+  error UpkeepNotCanceled();
+  error UpkeepNotNeeded();
+  error ValueNotChanged();
+  error ZeroAddressNotAllowed();
+  event AdminPrivilegeConfigSet(address indexed admin, bytes privilegeConfig);
+  event BillingConfigOverridden(uint256 indexed id, ZKSyncAutomationRegistryBase2_3.BillingOverrides overrides);
+  event BillingConfigOverrideRemoved(uint256 indexed id);
+  event BillingConfigSet(address indexed token, ZKSyncAutomationRegistryBase2_3.BillingConfig config);
+  event CancelledUpkeepReport(uint256 indexed id, bytes trigger);
+  event ChainSpecificModuleUpdated(address newModule);
+  event ConfigSet(
+    uint32 previousConfigBlockNumber,
+    bytes32 configDigest,
+    uint64 configCount,
+    address[] signers,
+    address[] transmitters,
+    uint8 f,
+    bytes onchainConfig,
+    uint64 offchainConfigVersion,
+    bytes offchainConfig
+  );
+  event DedupKeyAdded(bytes32 indexed dedupKey);
+  event 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);
+  event PayeesUpdated(address[] transmitters, address[] payees);
+  event PayeeshipTransferRequested(address indexed transmitter, address indexed from, address indexed to);
+  event PayeeshipTransferred(address indexed transmitter, address indexed from, address indexed to);
+  event PaymentWithdrawn(address indexed transmitter, uint256 indexed amount, address indexed to, address payee);
+  event ReorgedUpkeepReport(uint256 indexed id, bytes trigger);
+  event StaleUpkeepReport(uint256 indexed id, bytes trigger);
+  event Transmitted(bytes32 configDigest, uint32 epoch);
+  event Unpaused(address account);
+  event UpkeepAdminTransferRequested(uint256 indexed id, address indexed from, address indexed to);
+  event UpkeepAdminTransferred(uint256 indexed id, address indexed from, address indexed to);
+  event UpkeepCanceled(uint256 indexed id, uint64 indexed atBlockHeight);
+  event UpkeepCharged(uint256 indexed id, ZKSyncAutomationRegistryBase2_3.PaymentReceipt receipt);
+  event UpkeepCheckDataSet(uint256 indexed id, bytes newCheckData);
+  event UpkeepGasLimitSet(uint256 indexed id, uint96 gasLimit);
+  event UpkeepMigrated(uint256 indexed id, uint256 remainingBalance, address destination);
+  event UpkeepOffchainConfigSet(uint256 indexed id, bytes offchainConfig);
+  event UpkeepPaused(uint256 indexed id);
+  event UpkeepPerformed(
+    uint256 indexed id,
+    bool indexed success,
+    uint96 totalPayment,
+    uint256 gasUsed,
+    uint256 gasOverhead,
+    bytes trigger
+  );
+  event UpkeepPrivilegeConfigSet(uint256 indexed id, bytes privilegeConfig);
+  event UpkeepReceived(uint256 indexed id, uint256 startingBalance, address importedFrom);
+  event UpkeepRegistered(uint256 indexed id, uint32 performGas, address admin);
+  event UpkeepTriggerConfigSet(uint256 indexed id, bytes triggerConfig);
+  event UpkeepUnpaused(uint256 indexed id);
+  fallback() external payable;
+  function acceptOwnership() external;
+  function fallbackTo() external view returns (address);
+  function latestConfigDetails() external view returns (uint32 configCount, uint32 blockNumber, bytes32 configDigest);
+  function latestConfigDigestAndEpoch() external view returns (bool scanLogs, bytes32 configDigest, uint32 epoch);
+  function owner() external view returns (address);
+  function setConfig(
+    address[] memory signers,
+    address[] memory transmitters,
+    uint8 f,
+    bytes memory onchainConfigBytes,
+    uint64 offchainConfigVersion,
+    bytes memory offchainConfig
+  ) external;
+  function setConfigTypeSafe(
+    address[] memory signers,
+    address[] memory transmitters,
+    uint8 f,
+    ZKSyncAutomationRegistryBase2_3.OnchainConfig memory onchainConfig,
+    uint64 offchainConfigVersion,
+    bytes memory offchainConfig,
+    address[] memory billingTokens,
+    ZKSyncAutomationRegistryBase2_3.BillingConfig[] memory billingConfigs
+  ) external;
+  function transferOwnership(address to) external;
+  function transmit(
+    bytes32[3] memory reportContext,
+    bytes memory rawReport,
+    bytes32[] memory rs,
+    bytes32[] memory ss,
+    bytes32 rawVs
+  ) external;
+  function typeAndVersion() external view returns (string memory);
+
+  function cancelUpkeep(uint256 id) external;
+  function migrateUpkeeps(uint256[] memory ids, address destination) external;
+  function onTokenTransfer(address sender, uint256 amount, bytes memory data) 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 addFunds(uint256 id, uint96 amount) external payable;
+  function checkCallback(
+    uint256 id,
+    bytes[] memory values,
+    bytes memory extraData
+  ) external view returns (bool upkeepNeeded, bytes memory performData, uint8 upkeepFailureReason, uint256 gasUsed);
+  function checkUpkeep(
+    uint256 id,
+    bytes memory triggerData
+  )
+    external
+    view
+    returns (
+      bool upkeepNeeded,
+      bytes memory performData,
+      uint8 upkeepFailureReason,
+      uint256 gasUsed,
+      uint256 gasLimit,
+      uint256 fastGasWei,
+      uint256 linkUSD
+    );
+  function checkUpkeep(
+    uint256 id
+  )
+    external
+    view
+    returns (
+      bool upkeepNeeded,
+      bytes memory performData,
+      uint8 upkeepFailureReason,
+      uint256 gasUsed,
+      uint256 gasLimit,
+      uint256 fastGasWei,
+      uint256 linkUSD
+    );
+  function executeCallback(
+    uint256 id,
+    bytes memory payload
+  ) external returns (bool upkeepNeeded, bytes memory performData, uint8 upkeepFailureReason, uint256 gasUsed);
+  function pauseUpkeep(uint256 id) external;
+  function removeBillingOverrides(uint256 id) external;
+  function setBillingOverrides(
+    uint256 id,
+    ZKSyncAutomationRegistryBase2_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 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 getAvailableERC20ForPayment(address billingToken) external view returns (uint256);
+  function getBalance(uint256 id) external view returns (uint96 balance);
+  function getBillingConfig(
+    address billingToken
+  ) external view returns (ZKSyncAutomationRegistryBase2_3.BillingConfig memory);
+  function getBillingOverrides(
+    uint256 upkeepID
+  ) external view returns (ZKSyncAutomationRegistryBase2_3.BillingOverrides memory);
+  function getBillingOverridesEnabled(uint256 upkeepID) external view returns (bool);
+  function getBillingToken(uint256 upkeepID) external view returns (address);
+  function getBillingTokenConfig(
+    address token
+  ) external view returns (ZKSyncAutomationRegistryBase2_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 (ZKSyncAutomationRegistryBase2_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 (ZKSyncAutomationRegistryBase2_3.HotVars memory);
+  function getLinkAddress() external view returns (address);
+  function getLinkUSDFeedAddress() external view returns (address);
+  function getLogGasOverhead() external pure returns (uint256);
+  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 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 (
+      IAutomationV21PlusCommon.StateLegacy memory state,
+      IAutomationV21PlusCommon.OnchainConfigLegacy memory config,
+      address[] memory signers,
+      address[] memory transmitters,
+      uint8 f
+    );
+  function getStorage() external view returns (ZKSyncAutomationRegistryBase2_3.Storage memory);
+  function getTransmitterInfo(
+    address query
+  ) external view returns (bool active, uint8 index, uint96 balance, uint96 lastCollected, address payee);
+  function getTransmittersWithPayees()
+    external
+    view
+    returns (ZKSyncAutomationRegistryBase2_3.TransmitterPayeeInfo[] memory);
+  function getTriggerType(uint256 upkeepId) external pure returns (uint8);
+  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 (int256);
+  function pause() external;
+  function setAdminPrivilegeConfig(address admin, bytes memory newPrivilegeConfig) external;
+  function setPayees(address[] memory payees) external;
+  function setPeerRegistryMigrationPermission(address peer, uint8 permission) 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 unpause() external;
+  function upkeepVersion() external pure returns (uint8);
+  function withdrawPayment(address from, address to) external;
+}
+
+interface ZKSyncAutomationRegistryBase2_3 {
+  struct BillingOverrides {
+    uint32 gasFeePPB;
+    uint24 flatFeeMilliCents;
+  }
+
+  struct BillingConfig {
+    uint32 gasFeePPB;
+    uint24 flatFeeMilliCents;
+    address priceFeed;
+    uint8 decimals;
+    uint256 fallbackPrice;
+    uint96 minSpend;
+  }
+
+  struct PaymentReceipt {
+    uint96 gasChargeInBillingToken;
+    uint96 premiumInBillingToken;
+    uint96 gasReimbursementInJuels;
+    uint96 premiumInJuels;
+    address billingToken;
+    uint96 linkUSD;
+    uint96 nativeUSD;
+    uint96 billingUSD;
+  }
+
+  struct OnchainConfig {
+    uint32 checkGasLimit;
+    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[] registrars;
+    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 TransmitterPayeeInfo {
+    address transmitterAddress;
+    address payeeAddress;
+  }
+}
+
+interface IAutomationV21PlusCommon {
+  struct StateLegacy {
+    uint32 nonce;
+    uint96 ownerLinkBalance;
+    uint256 expectedLinkBalance;
+    uint96 totalPremium;
+    uint256 numUpkeeps;
+    uint32 configCount;
+    uint32 latestConfigBlockNumber;
+    bytes32 latestConfigDigest;
+    uint32 latestEpoch;
+    bool paused;
+  }
+
+  struct OnchainConfigLegacy {
+    uint32 paymentPremiumPPB;
+    uint32 flatFeeMicroLink;
+    uint32 checkGasLimit;
+    uint24 stalenessSeconds;
+    uint16 gasCeilingMultiplier;
+    uint96 minUpkeepSpend;
+    uint32 maxPerformGas;
+    uint32 maxCheckDataSize;
+    uint32 maxPerformDataSize;
+    uint32 maxRevertDataSize;
+    uint256 fallbackGasPrice;
+    uint256 fallbackLinkPrice;
+    address transcoder;
+    address[] registrars;
+    address upkeepPrivilegeManager;
+  }
+
+  struct UpkeepInfoLegacy {
+    address target;
+    uint32 performGas;
+    bytes checkData;
+    uint96 balance;
+    address admin;
+    uint64 maxValidBlocknumber;
+    uint32 lastPerformedBlockNumber;
+    uint96 amountSpent;
+    bool paused;
+    bytes offchainConfig;
+  }
+}
+
+// THIS FILE WAS AUTOGENERATED FROM THE FOLLOWING ABI JSON:
+/*
+[{"inputs":[{"internalType":"contract ZKSyncAutomationRegistryLogicA2_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 ZKSyncAutomationRegistryBase2_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 IERC20Metadata","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":"uint8","name":"decimals","type":"uint8"},{"internalType":"uint256","name":"fallbackPrice","type":"uint256"},{"internalType":"uint96","name":"minSpend","type":"uint96"}],"indexed":false,"internalType":"struct ZKSyncAutomationRegistryBase2_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"},{"components":[{"internalType":"uint96","name":"gasChargeInBillingToken","type":"uint96"},{"internalType":"uint96","name":"premiumInBillingToken","type":"uint96"},{"internalType":"uint96","name":"gasReimbursementInJuels","type":"uint96"},{"internalType":"uint96","name":"premiumInJuels","type":"uint96"},{"internalType":"contract IERC20Metadata","name":"billingToken","type":"address"},{"internalType":"uint96","name":"linkUSD","type":"uint96"},{"internalType":"uint96","name":"nativeUSD","type":"uint96"},{"internalType":"uint96","name":"billingUSD","type":"uint96"}],"indexed":false,"internalType":"struct ZKSyncAutomationRegistryBase2_3.PaymentReceipt","name":"receipt","type":"tuple"}],"name":"UpkeepCharged","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":[],"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":[],"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 ZKSyncAutomationRegistryBase2_3.OnchainConfig","name":"onchainConfig","type":"tuple"},{"internalType":"uint64","name":"offchainConfigVersion","type":"uint64"},{"internalType":"bytes","name":"offchainConfig","type":"bytes"},{"internalType":"contract IERC20Metadata[]","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":"uint8","name":"decimals","type":"uint8"},{"internalType":"uint256","name":"fallbackPrice","type":"uint256"},{"internalType":"uint96","name":"minSpend","type":"uint96"}],"internalType":"struct ZKSyncAutomationRegistryBase2_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 ZKSyncAutomationRegistryLogicB2_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":"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":[{"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 ZKSyncAutomationRegistryBase2_3.Trigger","name":"triggerType","type":"uint8"},{"internalType":"contract IERC20Metadata","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 ZKSyncAutomationRegistryLogicC2_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":"uint96","name":"amount","type":"uint96"}],"name":"addFunds","outputs":[],"stateMutability":"payable","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 ZKSyncAutomationRegistryBase2_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 ZKSyncAutomationRegistryBase2_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 ZKSyncAutomationRegistryBase2_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 ZKSyncAutomationRegistryBase2_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 ZKSyncAutomationRegistryBase2_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 IERC20Metadata","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 ZKSyncAutomationRegistryBase2_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":"contract IERC20Metadata","name":"billingToken","type":"address"}],"name":"getAvailableERC20ForPayment","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"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 IERC20Metadata","name":"billingToken","type":"address"}],"name":"getBillingConfig","outputs":[{"components":[{"internalType":"uint32","name":"gasFeePPB","type":"uint32"},{"internalType":"uint24","name":"flatFeeMilliCents","type":"uint24"},{"internalType":"contract AggregatorV3Interface","name":"priceFeed","type":"address"},{"internalType":"uint8","name":"decimals","type":"uint8"},{"internalType":"uint256","name":"fallbackPrice","type":"uint256"},{"internalType":"uint96","name":"minSpend","type":"uint96"}],"internalType":"struct ZKSyncAutomationRegistryBase2_3.BillingConfig","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"upkeepID","type":"uint256"}],"name":"getBillingOverrides","outputs":[{"components":[{"internalType":"uint32","name":"gasFeePPB","type":"uint32"},{"internalType":"uint24","name":"flatFeeMilliCents","type":"uint24"}],"internalType":"struct ZKSyncAutomationRegistryBase2_3.BillingOverrides","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"upkeepID","type":"uint256"}],"name":"getBillingOverridesEnabled","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"upkeepID","type":"uint256"}],"name":"getBillingToken","outputs":[{"internalType":"contract IERC20Metadata","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"contract IERC20Metadata","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":"uint8","name":"decimals","type":"uint8"},{"internalType":"uint256","name":"fallbackPrice","type":"uint256"},{"internalType":"uint96","name":"minSpend","type":"uint96"}],"internalType":"struct ZKSyncAutomationRegistryBase2_3.BillingConfig","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getBillingTokens","outputs":[{"internalType":"contract IERC20Metadata[]","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 ZKSyncAutomationRegistryBase2_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 ZKSyncAutomationRegistryBase2_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 ZKSyncAutomationRegistryBase2_3.Trigger","name":"triggerType","type":"uint8"},{"internalType":"uint32","name":"gasLimit","type":"uint32"},{"internalType":"contract IERC20Metadata","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 ZKSyncAutomationRegistryBase2_3.PayoutMode","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"peer","type":"address"}],"name":"getPeerRegistryMigrationPermission","outputs":[{"internalType":"enum ZKSyncAutomationRegistryBase2_3.MigrationPermission","name":"","type":"uint8"}],"stateMutability":"view","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 IERC20Metadata","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 ZKSyncAutomationRegistryBase2_3.Storage","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"query","type":"address"}],"name":"getTransmitterInfo","outputs":[{"internalType":"bool","name":"active","type":"bool"},{"internalType":"uint8","name":"index","type":"uint8"},{"internalType":"uint96","name":"balance","type":"uint96"},{"internalType":"uint96","name":"lastCollected","type":"uint96"},{"internalType":"address","name":"payee","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getTransmittersWithPayees","outputs":[{"components":[{"internalType":"address","name":"transmitterAddress","type":"address"},{"internalType":"address","name":"payeeAddress","type":"address"}],"internalType":"struct ZKSyncAutomationRegistryBase2_3.TransmitterPayeeInfo[]","name":"","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"upkeepId","type":"uint256"}],"name":"getTriggerType","outputs":[{"internalType":"enum ZKSyncAutomationRegistryBase2_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 ZKSyncAutomationRegistryBase2_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 IERC20Metadata","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/test/v2_3_zksync/BaseTest.t.sol b/contracts/src/v0.8/automation/test/v2_3_zksync/BaseTest.t.sol
new file mode 100644
index 00000000000..cde05ab3a22
--- /dev/null
+++ b/contracts/src/v0.8/automation/test/v2_3_zksync/BaseTest.t.sol
@@ -0,0 +1,500 @@
+// SPDX-License-Identifier: MIT
+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 {ERC20Mock6Decimals} from "../../mocks/ERC20Mock6Decimals.sol";
+import {MockV3Aggregator} from "../../../tests/MockV3Aggregator.sol";
+import {AutomationForwarderLogic} from "../../AutomationForwarderLogic.sol";
+import {UpkeepTranscoder5_0 as Transcoder} from "../../v2_3/UpkeepTranscoder5_0.sol";
+import {ZKSyncAutomationRegistry2_3} from "../../v2_3_zksync/ZKSyncAutomationRegistry2_3.sol";
+import {ZKSyncAutomationRegistryLogicA2_3} from "../../v2_3_zksync/ZKSyncAutomationRegistryLogicA2_3.sol";
+import {ZKSyncAutomationRegistryLogicB2_3} from "../../v2_3_zksync/ZKSyncAutomationRegistryLogicB2_3.sol";
+import {ZKSyncAutomationRegistryLogicC2_3} from "../../v2_3_zksync/ZKSyncAutomationRegistryLogicC2_3.sol";
+import {ZKSyncAutomationRegistryBase2_3 as ZKSyncAutoBase} from "../../v2_3_zksync/ZKSyncAutomationRegistryBase2_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 {IERC20Metadata as IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/extensions/IERC20Metadata.sol";
+import {MockUpkeep} from "../../mocks/MockUpkeep.sol";
+import {IWrappedNative} from "../../interfaces/v2_3/IWrappedNative.sol";
+import {WETH9} from "../WETH9.sol";
+import {MockGasBoundCaller} from "../../../tests/MockGasBoundCaller.sol";
+import {MockZKSyncSystemContext} from "../../../tests/MockZKSyncSystemContext.sol";
+
+/**
+ * @title BaseTest provides basic test setup procedures and dependencies for use by other
+ * unit tests
+ */
+contract BaseTest is Test {
+  // test state (not exposed to derived 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;
+  ERC20Mock6Decimals internal usdToken6;
+  ERC20Mock internal usdToken18;
+  ERC20Mock internal usdToken18_2;
+  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;
+  MockGasBoundCaller internal GAS_BOUND_CALLER;
+  MockZKSyncSystemContext internal SYSTEM_CONTEXT;
+
+  // 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);
+    linkToken = new LinkToken();
+    linkToken.grantMintRole(OWNER);
+    usdToken18 = new ERC20Mock("MOCK_ERC20_18Decimals", "MOCK_ERC20_18Decimals", OWNER, 0);
+    usdToken18_2 = new ERC20Mock("Second_MOCK_ERC20_18Decimals", "Second_MOCK_ERC20_18Decimals", OWNER, 0);
+    usdToken6 = new ERC20Mock6Decimals("MOCK_ERC20_6Decimals", "MOCK_ERC20_6Decimals", 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();
+    GAS_BOUND_CALLER = new MockGasBoundCaller();
+    SYSTEM_CONTEXT = new MockZKSyncSystemContext();
+
+    bytes memory callerCode = address(GAS_BOUND_CALLER).code;
+    vm.etch(0xc706EC7dfA5D4Dc87f29f859094165E8290530f5, callerCode);
+
+    bytes memory contextCode = address(SYSTEM_CONTEXT).code;
+    vm.etch(0x000000000000000000000000000000000000800B, contextCode);
+
+    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);
+
+    usdToken18.mint(OWNER, 1000e18);
+    usdToken18.mint(UPKEEP_ADMIN, 1000e18);
+    usdToken18.mint(FINANCE_ADMIN, 1000e18);
+    usdToken18.mint(STRANGER, 1000e18);
+
+    usdToken18_2.mint(UPKEEP_ADMIN, 1000e18);
+
+    usdToken6.mint(OWNER, 1000e6);
+    usdToken6.mint(UPKEEP_ADMIN, 1000e6);
+    usdToken6.mint(FINANCE_ADMIN, 1000e6);
+    usdToken6.mint(STRANGER, 1000e6);
+
+    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 deployZKSyncRegistry(ZKSyncAutoBase.PayoutMode payoutMode) internal returns (Registry) {
+    AutomationForwarderLogic forwarderLogic = new AutomationForwarderLogic();
+    ZKSyncAutomationRegistryLogicC2_3 logicC2_3 = new ZKSyncAutomationRegistryLogicC2_3(
+      address(linkToken),
+      address(LINK_USD_FEED),
+      address(NATIVE_USD_FEED),
+      address(FAST_GAS_FEED),
+      address(forwarderLogic),
+      ZERO_ADDRESS,
+      payoutMode,
+      address(weth)
+    );
+    ZKSyncAutomationRegistryLogicB2_3 logicB2_3 = new ZKSyncAutomationRegistryLogicB2_3(logicC2_3);
+    ZKSyncAutomationRegistryLogicA2_3 logicA2_3 = new ZKSyncAutomationRegistryLogicA2_3(logicB2_3);
+    return Registry(payable(address(new ZKSyncAutomationRegistry2_3(logicA2_3))));
+  }
+
+  /// @notice deploys and configures a registry, registrar, and everything needed for most tests
+  function deployAndConfigureZKSyncRegistryAndRegistrar(
+    ZKSyncAutoBase.PayoutMode payoutMode
+  ) internal returns (Registry, AutomationRegistrar2_3) {
+    Registry registry = deployZKSyncRegistry(payoutMode);
+
+    IERC20[] memory billingTokens = new IERC20[](4);
+    billingTokens[0] = IERC20(address(usdToken18));
+    billingTokens[1] = IERC20(address(weth));
+    billingTokens[2] = IERC20(address(linkToken));
+    billingTokens[3] = IERC20(address(usdToken6));
+    uint256[] memory minRegistrationFees = new uint256[](billingTokens.length);
+    minRegistrationFees[0] = 100e18; // 100 USD
+    minRegistrationFees[1] = 5e18; // 5 Native
+    minRegistrationFees[2] = 5e18; // 5 LINK
+    minRegistrationFees[3] = 100e6; // 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: DEFAULT_GAS_FEE_PPB, // 15%
+      flatFeeMilliCents: DEFAULT_FLAT_FEE_MILLI_CENTS, // 2 cents
+      priceFeed: address(USDTOKEN_USD_FEED),
+      fallbackPrice: 100_000_000, // $1
+      minSpend: 1e18, // 1 USD
+      decimals: 18
+    });
+    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: 5e18, // 5 Native
+      decimals: 18
+    });
+    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: 1e18, // 1 LINK
+      decimals: 18
+    });
+    billingTokenConfigs[3] = AutomationRegistryBase2_3.BillingConfig({
+      gasFeePPB: DEFAULT_GAS_FEE_PPB, // 15%
+      flatFeeMilliCents: DEFAULT_FLAT_FEE_MILLI_CENTS, // 2 cents
+      priceFeed: address(USDTOKEN_USD_FEED),
+      fallbackPrice: 1e8, // $1
+      minSpend: 1e6, // 1 USD
+      decimals: 6
+    });
+
+    if (payoutMode == ZKSyncAutoBase.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
+    );
+    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, bytes4 selector) internal {
+    uint256[] memory ids = new uint256[](1);
+    ids[0] = id;
+    _transmit(ids, registry, selector);
+  }
+
+  function _transmit(uint256[] memory ids, Registry registry, bytes4 selector) internal {
+    bytes memory reportBytes;
+    {
+      uint256[] memory upkeepIds = new uint256[](ids.length);
+      uint256[] memory gasLimits = new uint256[](ids.length);
+      bytes[] memory performDatas = new bytes[](ids.length);
+      bytes[] memory triggers = new bytes[](ids.length);
+      for (uint256 i = 0; i < ids.length; i++) {
+        upkeepIds[i] = ids[i];
+        gasLimits[i] = registry.getUpkeep(ids[i]).performGas;
+        performDatas[i] = new bytes(0);
+        uint8 triggerType = registry.getTriggerType(ids[i]);
+        if (triggerType == 0) {
+          triggers[i] = _encodeConditionalTrigger(
+            ZKSyncAutoBase.ConditionalTrigger(uint32(block.number - 1), blockhash(block.number - 1))
+          );
+        } else {
+          revert("not implemented");
+        }
+      }
+      ZKSyncAutoBase.Report memory report = ZKSyncAutoBase.Report(
+        uint256(1000000000),
+        uint256(2000000000),
+        upkeepIds,
+        gasLimits,
+        triggers,
+        performDatas
+      );
+
+      reportBytes = _encodeReport(report);
+    }
+    (, , bytes32 configDigest) = registry.latestConfigDetails();
+    bytes32[3] memory reportContext = [configDigest, configDigest, configDigest];
+    uint256[] memory signerPKs = new uint256[](2);
+    signerPKs[0] = SIGNING_KEY0;
+    signerPKs[1] = SIGNING_KEY1;
+    (bytes32[] memory rs, bytes32[] memory ss, bytes32 vs) = _signReport(reportBytes, reportContext, signerPKs);
+
+    vm.startPrank(TRANSMITTERS[0]);
+    if (selector != bytes4(0)) {
+      vm.expectRevert(selector);
+    }
+    registry.transmit(reportContext, reportBytes, rs, ss, vs);
+    vm.stopPrank();
+  }
+
+  /// @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(ZKSyncAutoBase.Report memory report) internal pure returns (bytes memory reportBytes) {
+    return abi.encode(report);
+  }
+
+  function _encodeConditionalTrigger(
+    ZKSyncAutoBase.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 with 18 decimals to the recipient
+  function _mintERC20_18Decimals(address recipient, uint256 amount) internal {
+    vm.prank(OWNER);
+    usdToken18.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/test/v2_3_zksync/ZKSyncAutomationRegistry2_3.t.sol b/contracts/src/v0.8/automation/test/v2_3_zksync/ZKSyncAutomationRegistry2_3.t.sol
new file mode 100644
index 00000000000..7098d9f38fa
--- /dev/null
+++ b/contracts/src/v0.8/automation/test/v2_3_zksync/ZKSyncAutomationRegistry2_3.t.sol
@@ -0,0 +1,2772 @@
+// SPDX-License-Identifier: BUSL-1.1
+pragma solidity 0.8.19;
+
+import {Vm} from "forge-std/Test.sol";
+import {BaseTest} from "./BaseTest.t.sol";
+import {ZKSyncAutomationRegistryBase2_3 as AutoBase} from "../../v2_3_zksync/ZKSyncAutomationRegistryBase2_3.sol";
+import {AutomationRegistrar2_3 as Registrar} from "../../v2_3/AutomationRegistrar2_3.sol";
+import {IAutomationRegistryMaster2_3 as Registry, AutomationRegistryBase2_3, IAutomationV21PlusCommon} from "../../interfaces/v2_3/IAutomationRegistryMaster2_3.sol";
+import {ChainModuleBase} from "../../chains/ChainModuleBase.sol";
+import {IERC20Metadata as IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/extensions/IERC20Metadata.sol";
+import {IWrappedNative} from "../../interfaces/v2_3/IWrappedNative.sol";
+
+// forge test --match-path src/v0.8/automation/test/v2_3_zksync/ZKSyncAutomationRegistry2_3.t.sol --match-test
+
+enum Trigger {
+  CONDITION,
+  LOG
+}
+
+contract SetUp is BaseTest {
+  Registry internal registry;
+  AutomationRegistryBase2_3.OnchainConfig internal config;
+  bytes internal constant offchainConfigBytes = abi.encode(1234, ZERO_ADDRESS);
+
+  uint256 linkUpkeepID;
+  uint256 linkUpkeepID2; // 2 upkeeps use the same billing token (LINK) to test migration scenario
+  uint256 usdUpkeepID18; // 1 upkeep uses ERC20 token with 18 decimals
+  uint256 usdUpkeepID6; // 1 upkeep uses ERC20 token with 6 decimals
+  uint256 nativeUpkeepID;
+
+  function setUp() public virtual override {
+    super.setUp();
+    (registry, ) = deployAndConfigureZKSyncRegistryAndRegistrar(AutoBase.PayoutMode.ON_CHAIN);
+    config = registry.getConfig();
+
+    vm.startPrank(OWNER);
+    linkToken.approve(address(registry), type(uint256).max);
+    usdToken6.approve(address(registry), type(uint256).max);
+    usdToken18.approve(address(registry), type(uint256).max);
+    weth.approve(address(registry), type(uint256).max);
+    vm.startPrank(UPKEEP_ADMIN);
+    linkToken.approve(address(registry), type(uint256).max);
+    usdToken6.approve(address(registry), type(uint256).max);
+    usdToken18.approve(address(registry), type(uint256).max);
+    weth.approve(address(registry), type(uint256).max);
+    vm.startPrank(STRANGER);
+    linkToken.approve(address(registry), type(uint256).max);
+    usdToken6.approve(address(registry), type(uint256).max);
+    usdToken18.approve(address(registry), type(uint256).max);
+    weth.approve(address(registry), type(uint256).max);
+    vm.stopPrank();
+
+    linkUpkeepID = registry.registerUpkeep(
+      address(TARGET1),
+      config.maxPerformGas,
+      UPKEEP_ADMIN,
+      uint8(Trigger.CONDITION),
+      address(linkToken),
+      "",
+      "",
+      ""
+    );
+
+    linkUpkeepID2 = registry.registerUpkeep(
+      address(TARGET1),
+      config.maxPerformGas,
+      UPKEEP_ADMIN,
+      uint8(Trigger.CONDITION),
+      address(linkToken),
+      "",
+      "",
+      ""
+    );
+
+    usdUpkeepID18 = registry.registerUpkeep(
+      address(TARGET1),
+      config.maxPerformGas,
+      UPKEEP_ADMIN,
+      uint8(Trigger.CONDITION),
+      address(usdToken18),
+      "",
+      "",
+      ""
+    );
+
+    usdUpkeepID6 = registry.registerUpkeep(
+      address(TARGET1),
+      config.maxPerformGas,
+      UPKEEP_ADMIN,
+      uint8(Trigger.CONDITION),
+      address(usdToken6),
+      "",
+      "",
+      ""
+    );
+
+    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(linkUpkeepID2, registry.getMinBalanceForUpkeep(linkUpkeepID2));
+    registry.addFunds(usdUpkeepID18, registry.getMinBalanceForUpkeep(usdUpkeepID18));
+    registry.addFunds(usdUpkeepID6, registry.getMinBalanceForUpkeep(usdUpkeepID6));
+    registry.addFunds(nativeUpkeepID, registry.getMinBalanceForUpkeep(nativeUpkeepID));
+    vm.stopPrank();
+  }
+}
+
+contract LatestConfigDetails is SetUp {
+  function testGet() public {
+    (uint32 configCount, uint32 blockNumber, bytes32 configDigest) = registry.latestConfigDetails();
+    assertEq(configCount, 1);
+    assertTrue(blockNumber > 0);
+    assertNotEq(configDigest, "");
+  }
+}
+
+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(Registry.OnlySimulatedBackend.selector));
+    registry.checkUpkeep(id, triggerData);
+  }
+}
+
+contract WithdrawFunds is SetUp {
+  event FundsWithdrawn(uint256 indexed id, uint256 amount, address to);
+
+  function test_RevertsWhen_CalledByNonAdmin() external {
+    vm.expectRevert(Registry.OnlyCallableByAdmin.selector);
+    vm.prank(STRANGER);
+    registry.withdrawFunds(linkUpkeepID, STRANGER);
+  }
+
+  function test_RevertsWhen_InvalidRecipient() external {
+    vm.expectRevert(Registry.InvalidRecipient.selector);
+    vm.prank(UPKEEP_ADMIN);
+    registry.withdrawFunds(linkUpkeepID, ZERO_ADDRESS);
+  }
+
+  function test_RevertsWhen_UpkeepNotCanceled() external {
+    vm.expectRevert(Registry.UpkeepNotCanceled.selector);
+    vm.prank(UPKEEP_ADMIN);
+    registry.withdrawFunds(linkUpkeepID, UPKEEP_ADMIN);
+  }
+
+  function test_Happy_Link() external {
+    vm.startPrank(UPKEEP_ADMIN);
+    registry.cancelUpkeep(linkUpkeepID);
+    vm.roll(100 + block.number);
+
+    uint256 startUpkeepAdminBalance = linkToken.balanceOf(UPKEEP_ADMIN);
+    uint256 startLinkReserveAmountBalance = registry.getReserveAmount(address(linkToken));
+
+    uint256 upkeepBalance = registry.getBalance(linkUpkeepID);
+    vm.expectEmit();
+    emit FundsWithdrawn(linkUpkeepID, upkeepBalance, address(UPKEEP_ADMIN));
+    registry.withdrawFunds(linkUpkeepID, UPKEEP_ADMIN);
+
+    assertEq(registry.getBalance(linkUpkeepID), 0);
+    assertEq(linkToken.balanceOf(UPKEEP_ADMIN), startUpkeepAdminBalance + upkeepBalance);
+    assertEq(registry.getReserveAmount(address(linkToken)), startLinkReserveAmountBalance - upkeepBalance);
+  }
+
+  function test_Happy_USDToken() external {
+    vm.startPrank(UPKEEP_ADMIN);
+    registry.cancelUpkeep(usdUpkeepID6);
+    vm.roll(100 + block.number);
+
+    uint256 startUpkeepAdminBalance = usdToken6.balanceOf(UPKEEP_ADMIN);
+    uint256 startUSDToken6ReserveAmountBalance = registry.getReserveAmount(address(usdToken6));
+
+    uint256 upkeepBalance = registry.getBalance(usdUpkeepID6);
+    vm.expectEmit();
+    emit FundsWithdrawn(usdUpkeepID6, upkeepBalance, address(UPKEEP_ADMIN));
+    registry.withdrawFunds(usdUpkeepID6, UPKEEP_ADMIN);
+
+    assertEq(registry.getBalance(usdUpkeepID6), 0);
+    assertEq(usdToken6.balanceOf(UPKEEP_ADMIN), startUpkeepAdminBalance + upkeepBalance);
+    assertEq(registry.getReserveAmount(address(usdToken6)), startUSDToken6ReserveAmountBalance - upkeepBalance);
+  }
+}
+
+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 test_HappyWhen_NativeUpkeep_WithMsgValue0() 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);
+    assertEq(registry.getAvailableERC20ForPayment(address(weth)), 0);
+  }
+
+  // when msg.value is not 0, it uses the native payment path
+  function test_HappyWhen_NativeUpkeep_WithMsgValueNot0() 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);
+    assertEq(registry.getAvailableERC20ForPayment(address(weth)), 0);
+  }
+
+  // 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 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);
+    registry.addFunds(linkUpkeepID, 1);
+    assertEq(registry.getBalance(linkUpkeepID), startAmount + 1);
+    vm.prank(STRANGER);
+    registry.addFunds(linkUpkeepID, 1);
+    assertEq(registry.getBalance(linkUpkeepID), startAmount + 2);
+  }
+
+  function test_movesFundFromCorrectToken() public {
+    vm.startPrank(UPKEEP_ADMIN);
+
+    uint256 startLINKRegistryBalance = linkToken.balanceOf(address(registry));
+    uint256 startUSDRegistryBalance = usdToken18.balanceOf(address(registry));
+    uint256 startLinkUpkeepBalance = registry.getBalance(linkUpkeepID);
+    uint256 startUSDUpkeepBalance = registry.getBalance(usdUpkeepID18);
+
+    registry.addFunds(linkUpkeepID, 1);
+    assertEq(registry.getBalance(linkUpkeepID), startLinkUpkeepBalance + 1);
+    assertEq(registry.getBalance(usdUpkeepID18), startUSDRegistryBalance);
+    assertEq(linkToken.balanceOf(address(registry)), startLINKRegistryBalance + 1);
+    assertEq(usdToken18.balanceOf(address(registry)), startUSDUpkeepBalance);
+
+    registry.addFunds(usdUpkeepID18, 2);
+    assertEq(registry.getBalance(linkUpkeepID), startLinkUpkeepBalance + 1);
+    assertEq(registry.getBalance(usdUpkeepID18), startUSDRegistryBalance + 2);
+    assertEq(linkToken.balanceOf(address(registry)), startLINKRegistryBalance + 1);
+    assertEq(usdToken18.balanceOf(address(registry)), startUSDUpkeepBalance + 2);
+  }
+
+  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(registry), 1e10);
+
+    //check there's a balance
+    assertEq(linkToken.balanceOf(address(registry)), startBalance + 1e10);
+
+    //check the link available has increased by the same amount
+    assertEq(uint256(registry.linkAvailableForPayment()), uint256(startLinkAvailable) + 1e10);
+  }
+
+  function testWithdrawLinkRevertsBecauseOnlyFinanceAdminAllowed() public {
+    vm.expectRevert(abi.encodeWithSelector(Registry.OnlyFinanceAdmin.selector));
+    registry.withdrawLink(aMockAddress, 1);
+  }
+
+  function testWithdrawLinkRevertsBecauseOfInsufficientBalance() public {
+    vm.startPrank(FINANCE_ADMIN);
+
+    // try to withdraw 1 link while there is 0 balance
+    vm.expectRevert(abi.encodeWithSelector(Registry.InsufficientBalance.selector, 0, 1));
+    registry.withdrawLink(aMockAddress, 1);
+
+    vm.stopPrank();
+  }
+
+  function testWithdrawLinkRevertsBecauseOfInvalidRecipient() public {
+    vm.startPrank(FINANCE_ADMIN);
+
+    // try to withdraw 1 link while there is 0 balance
+    vm.expectRevert(abi.encodeWithSelector(Registry.InvalidRecipient.selector));
+    registry.withdrawLink(ZERO_ADDRESS, 1);
+
+    vm.stopPrank();
+  }
+
+  function testWithdrawLinkSuccess() public {
+    //simulate a deposit of link to the liquidity pool
+    _mintLink(address(registry), 1e10);
+    uint256 startBalance = linkToken.balanceOf(address(registry));
+
+    vm.startPrank(FINANCE_ADMIN);
+
+    // try to withdraw 1 link while there is a ton of link available
+    registry.withdrawLink(aMockAddress, 1);
+
+    vm.stopPrank();
+
+    assertEq(linkToken.balanceOf(address(aMockAddress)), 1);
+    assertEq(linkToken.balanceOf(address(registry)), startBalance - 1);
+  }
+
+  function test_WithdrawERC20Fees_RespectsReserveAmount() public {
+    assertEq(registry.getBalance(usdUpkeepID18), registry.getReserveAmount(address(usdToken18)));
+    vm.startPrank(FINANCE_ADMIN);
+    vm.expectRevert(abi.encodeWithSelector(Registry.InsufficientBalance.selector, 0, 1));
+    registry.withdrawERC20Fees(address(usdToken18), FINANCE_ADMIN, 1);
+  }
+
+  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
+  }
+
+  // default is ON_CHAIN mode
+  function test_WithdrawERC20Fees_RevertsWhen_LinkAvailableForPaymentIsNegative() public {
+    _transmit(usdUpkeepID18, registry, bytes4(0)); // adds USD token to finance withdrawable, and gives NOPs a LINK balance
+    require(registry.linkAvailableForPayment() < 0, "linkAvailableForPayment should be negative");
+    require(
+      registry.getAvailableERC20ForPayment(address(usdToken18)) > 0,
+      "ERC20AvailableForPayment should be positive"
+    );
+    vm.expectRevert(Registry.InsufficientLinkLiquidity.selector);
+    vm.prank(FINANCE_ADMIN);
+    registry.withdrawERC20Fees(address(usdToken18), FINANCE_ADMIN, 1); // should revert
+    _mintLink(address(registry), uint256(registry.linkAvailableForPayment() * -10)); // top up LINK liquidity pool
+    vm.prank(FINANCE_ADMIN);
+    registry.withdrawERC20Fees(address(usdToken18), FINANCE_ADMIN, 1); // now finance can withdraw
+  }
+
+  function test_WithdrawERC20Fees_InOffChainMode_Happy() public {
+    // deploy and configure a registry with OFF_CHAIN payout
+    (Registry registry, ) = deployAndConfigureZKSyncRegistryAndRegistrar(AutoBase.PayoutMode.OFF_CHAIN);
+
+    // register an upkeep and add funds
+    uint256 id = registry.registerUpkeep(address(TARGET1), 1000000, UPKEEP_ADMIN, 0, address(usdToken18), "", "", "");
+    _mintERC20_18Decimals(UPKEEP_ADMIN, 1e20);
+    vm.startPrank(UPKEEP_ADMIN);
+    usdToken18.approve(address(registry), 1e20);
+    registry.addFunds(id, 1e20);
+
+    // manually create a transmit so transmitters earn some rewards
+    _transmit(id, registry, bytes4(0));
+    require(registry.linkAvailableForPayment() < 0, "linkAvailableForPayment should be negative");
+    vm.prank(FINANCE_ADMIN);
+    registry.withdrawERC20Fees(address(usdToken18), aMockAddress, 1); // finance can withdraw
+
+    // recipient should get the funds
+    assertEq(usdToken18.balanceOf(address(aMockAddress)), 1);
+  }
+
+  function testWithdrawERC20FeeSuccess() public {
+    // deposit excess USDToken to the registry (this goes to the "finance withdrawable" pool be default)
+    uint256 startReserveAmount = registry.getReserveAmount(address(usdToken18));
+    uint256 startAmount = usdToken18.balanceOf(address(registry));
+    _mintERC20_18Decimals(address(registry), 1e10);
+
+    // depositing shouldn't change reserve amount
+    assertEq(registry.getReserveAmount(address(usdToken18)), startReserveAmount);
+
+    vm.startPrank(FINANCE_ADMIN);
+
+    // try to withdraw 1 USDToken
+    registry.withdrawERC20Fees(address(usdToken18), aMockAddress, 1);
+
+    vm.stopPrank();
+
+    assertEq(usdToken18.balanceOf(address(aMockAddress)), 1);
+    assertEq(usdToken18.balanceOf(address(registry)), startAmount + 1e10 - 1);
+    assertEq(registry.getReserveAmount(address(usdToken18)), startReserveAmount);
+  }
+}
+
+contract SetConfig is SetUp {
+  event ConfigSet(
+    uint32 previousConfigBlockNumber,
+    bytes32 configDigest,
+    uint64 configCount,
+    address[] signers,
+    address[] transmitters,
+    uint8 f,
+    bytes onchainConfig,
+    uint64 offchainConfigVersion,
+    bytes offchainConfig
+  );
+
+  address module = address(new ChainModuleBase());
+
+  AutomationRegistryBase2_3.OnchainConfig cfg =
+    AutomationRegistryBase2_3.OnchainConfig({
+      checkGasLimit: 5_000_000,
+      stalenessSeconds: 90_000,
+      gasCeilingMultiplier: 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: _getRegistrars(),
+      upkeepPrivilegeManager: PRIVILEGE_MANAGER,
+      chainModule: module,
+      reorgProtectionEnabled: true,
+      financeAdmin: FINANCE_ADMIN
+    });
+
+  function testSetConfigSuccess() public {
+    (uint32 configCount, uint32 blockNumber, ) = registry.latestConfigDetails();
+    assertEq(configCount, 1);
+
+    address billingTokenAddress = address(usdToken18);
+    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: address(USDTOKEN_USD_FEED),
+      fallbackPrice: 2_000_000_000, // $20
+      minSpend: 100_000,
+      decimals: 18
+    });
+
+    bytes memory onchainConfigBytes = abi.encode(cfg);
+    bytes memory onchainConfigBytesWithBilling = abi.encode(cfg, billingTokens, billingConfigs);
+
+    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.setConfig(
+      SIGNERS,
+      TRANSMITTERS,
+      F,
+      onchainConfigBytesWithBilling,
+      OFFCHAIN_CONFIG_VERSION,
+      offchainConfigBytes
+    );
+
+    (, , address[] memory signers, address[] memory transmitters, uint8 f) = registry.getState();
+
+    assertEq(signers, SIGNERS);
+    assertEq(transmitters, TRANSMITTERS);
+    assertEq(f, F);
+
+    AutomationRegistryBase2_3.BillingConfig memory config = registry.getBillingTokenConfig(billingTokenAddress);
+    assertEq(config.gasFeePPB, 5_000);
+    assertEq(config.flatFeeMilliCents, 20_000);
+    assertEq(config.priceFeed, address(USDTOKEN_USD_FEED));
+    assertEq(config.minSpend, 100_000);
+
+    address[] memory tokens = registry.getBillingTokens();
+    assertEq(tokens.length, 1);
+  }
+
+  function testSetConfigMultipleBillingConfigsSuccess() public {
+    (uint32 configCount, , ) = registry.latestConfigDetails();
+    assertEq(configCount, 1);
+
+    address billingTokenAddress1 = address(linkToken);
+    address billingTokenAddress2 = address(usdToken18);
+    address[] memory billingTokens = new address[](2);
+    billingTokens[0] = billingTokenAddress1;
+    billingTokens[1] = billingTokenAddress2;
+
+    AutomationRegistryBase2_3.BillingConfig[] memory billingConfigs = new AutomationRegistryBase2_3.BillingConfig[](2);
+    billingConfigs[0] = AutomationRegistryBase2_3.BillingConfig({
+      gasFeePPB: 5_001,
+      flatFeeMilliCents: 20_001,
+      priceFeed: address(USDTOKEN_USD_FEED),
+      fallbackPrice: 100,
+      minSpend: 100,
+      decimals: 18
+    });
+    billingConfigs[1] = AutomationRegistryBase2_3.BillingConfig({
+      gasFeePPB: 5_002,
+      flatFeeMilliCents: 20_002,
+      priceFeed: address(USDTOKEN_USD_FEED),
+      fallbackPrice: 200,
+      minSpend: 200,
+      decimals: 18
+    });
+
+    bytes memory onchainConfigBytesWithBilling = abi.encode(cfg, billingTokens, billingConfigs);
+
+    registry.setConfig(
+      SIGNERS,
+      TRANSMITTERS,
+      F,
+      onchainConfigBytesWithBilling,
+      OFFCHAIN_CONFIG_VERSION,
+      offchainConfigBytes
+    );
+
+    (, , address[] memory signers, address[] memory transmitters, uint8 f) = registry.getState();
+
+    assertEq(signers, SIGNERS);
+    assertEq(transmitters, TRANSMITTERS);
+    assertEq(f, F);
+
+    AutomationRegistryBase2_3.BillingConfig memory config1 = registry.getBillingTokenConfig(billingTokenAddress1);
+    assertEq(config1.gasFeePPB, 5_001);
+    assertEq(config1.flatFeeMilliCents, 20_001);
+    assertEq(config1.priceFeed, address(USDTOKEN_USD_FEED));
+    assertEq(config1.fallbackPrice, 100);
+    assertEq(config1.minSpend, 100);
+
+    AutomationRegistryBase2_3.BillingConfig memory config2 = registry.getBillingTokenConfig(billingTokenAddress2);
+    assertEq(config2.gasFeePPB, 5_002);
+    assertEq(config2.flatFeeMilliCents, 20_002);
+    assertEq(config2.priceFeed, address(USDTOKEN_USD_FEED));
+    assertEq(config2.fallbackPrice, 200);
+    assertEq(config2.minSpend, 200);
+
+    address[] memory tokens = registry.getBillingTokens();
+    assertEq(tokens.length, 2);
+  }
+
+  function testSetConfigTwiceAndLastSetOverwrites() public {
+    (uint32 configCount, , ) = registry.latestConfigDetails();
+    assertEq(configCount, 1);
+
+    // BillingConfig1
+    address billingTokenAddress1 = address(usdToken18);
+    address[] memory billingTokens1 = new address[](1);
+    billingTokens1[0] = billingTokenAddress1;
+
+    AutomationRegistryBase2_3.BillingConfig[] memory billingConfigs1 = new AutomationRegistryBase2_3.BillingConfig[](1);
+    billingConfigs1[0] = AutomationRegistryBase2_3.BillingConfig({
+      gasFeePPB: 5_001,
+      flatFeeMilliCents: 20_001,
+      priceFeed: address(USDTOKEN_USD_FEED),
+      fallbackPrice: 100,
+      minSpend: 100,
+      decimals: 18
+    });
+
+    // the first time uses the default onchain config with 2 registrars
+    bytes memory onchainConfigBytesWithBilling1 = abi.encode(cfg, billingTokens1, billingConfigs1);
+
+    // set config once
+    registry.setConfig(
+      SIGNERS,
+      TRANSMITTERS,
+      F,
+      onchainConfigBytesWithBilling1,
+      OFFCHAIN_CONFIG_VERSION,
+      offchainConfigBytes
+    );
+
+    (, IAutomationV21PlusCommon.OnchainConfigLegacy memory onchainConfig1, , , ) = registry.getState();
+    assertEq(onchainConfig1.registrars.length, 2);
+
+    // BillingConfig2
+    address billingTokenAddress2 = address(usdToken18);
+    address[] memory billingTokens2 = new address[](1);
+    billingTokens2[0] = billingTokenAddress2;
+
+    AutomationRegistryBase2_3.BillingConfig[] memory billingConfigs2 = new AutomationRegistryBase2_3.BillingConfig[](1);
+    billingConfigs2[0] = AutomationRegistryBase2_3.BillingConfig({
+      gasFeePPB: 5_002,
+      flatFeeMilliCents: 20_002,
+      priceFeed: address(USDTOKEN_USD_FEED),
+      fallbackPrice: 200,
+      minSpend: 200,
+      decimals: 18
+    });
+
+    address[] memory newRegistrars = new address[](3);
+    newRegistrars[0] = address(uint160(uint256(keccak256("newRegistrar1"))));
+    newRegistrars[1] = address(uint160(uint256(keccak256("newRegistrar2"))));
+    newRegistrars[2] = address(uint160(uint256(keccak256("newRegistrar3"))));
+
+    // new onchain config with 3 new registrars, all other fields stay the same as the default
+    AutomationRegistryBase2_3.OnchainConfig memory cfg2 = AutomationRegistryBase2_3.OnchainConfig({
+      checkGasLimit: 5_000_000,
+      stalenessSeconds: 90_000,
+      gasCeilingMultiplier: 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: newRegistrars,
+      upkeepPrivilegeManager: PRIVILEGE_MANAGER,
+      chainModule: module,
+      reorgProtectionEnabled: true,
+      financeAdmin: FINANCE_ADMIN
+    });
+
+    // the second time uses the new onchain config with 3 new registrars and also new billing tokens/configs
+    bytes memory onchainConfigBytesWithBilling2 = abi.encode(cfg2, billingTokens2, billingConfigs2);
+
+    // set config twice
+    registry.setConfig(
+      SIGNERS,
+      TRANSMITTERS,
+      F,
+      onchainConfigBytesWithBilling2,
+      OFFCHAIN_CONFIG_VERSION,
+      offchainConfigBytes
+    );
+
+    (
+      ,
+      IAutomationV21PlusCommon.OnchainConfigLegacy memory onchainConfig2,
+      address[] memory signers,
+      address[] memory transmitters,
+      uint8 f
+    ) = registry.getState();
+
+    assertEq(onchainConfig2.registrars.length, 3);
+    for (uint256 i = 0; i < newRegistrars.length; i++) {
+      assertEq(newRegistrars[i], onchainConfig2.registrars[i]);
+    }
+    assertEq(signers, SIGNERS);
+    assertEq(transmitters, TRANSMITTERS);
+    assertEq(f, F);
+
+    AutomationRegistryBase2_3.BillingConfig memory config2 = registry.getBillingTokenConfig(billingTokenAddress2);
+    assertEq(config2.gasFeePPB, 5_002);
+    assertEq(config2.flatFeeMilliCents, 20_002);
+    assertEq(config2.priceFeed, address(USDTOKEN_USD_FEED));
+    assertEq(config2.fallbackPrice, 200);
+    assertEq(config2.minSpend, 200);
+
+    address[] memory tokens = registry.getBillingTokens();
+    assertEq(tokens.length, 1);
+  }
+
+  function testSetConfigDuplicateBillingConfigFailure() public {
+    (uint32 configCount, , ) = registry.latestConfigDetails();
+    assertEq(configCount, 1);
+
+    address billingTokenAddress1 = address(linkToken);
+    address billingTokenAddress2 = address(linkToken);
+    address[] memory billingTokens = new address[](2);
+    billingTokens[0] = billingTokenAddress1;
+    billingTokens[1] = billingTokenAddress2;
+
+    AutomationRegistryBase2_3.BillingConfig[] memory billingConfigs = new AutomationRegistryBase2_3.BillingConfig[](2);
+    billingConfigs[0] = AutomationRegistryBase2_3.BillingConfig({
+      gasFeePPB: 5_001,
+      flatFeeMilliCents: 20_001,
+      priceFeed: address(USDTOKEN_USD_FEED),
+      fallbackPrice: 100,
+      minSpend: 100,
+      decimals: 18
+    });
+    billingConfigs[1] = AutomationRegistryBase2_3.BillingConfig({
+      gasFeePPB: 5_002,
+      flatFeeMilliCents: 20_002,
+      priceFeed: address(USDTOKEN_USD_FEED),
+      fallbackPrice: 200,
+      minSpend: 200,
+      decimals: 18
+    });
+
+    bytes memory onchainConfigBytesWithBilling = abi.encode(cfg, billingTokens, billingConfigs);
+
+    // expect revert because of duplicate tokens
+    vm.expectRevert(abi.encodeWithSelector(Registry.DuplicateEntry.selector));
+    registry.setConfig(
+      SIGNERS,
+      TRANSMITTERS,
+      F,
+      onchainConfigBytesWithBilling,
+      OFFCHAIN_CONFIG_VERSION,
+      offchainConfigBytes
+    );
+  }
+
+  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: address(USDTOKEN_USD_FEED),
+      fallbackPrice: 2_000_000_000, // $20
+      minSpend: 100_000,
+      decimals: 18
+    });
+
+    // deploy registry with OFF_CHAIN payout mode
+    registry = deployZKSyncRegistry(AutoBase.PayoutMode.OFF_CHAIN);
+
+    vm.expectRevert(abi.encodeWithSelector(Registry.InvalidToken.selector));
+    registry.setConfigTypeSafe(
+      SIGNERS,
+      TRANSMITTERS,
+      F,
+      cfg,
+      OFFCHAIN_CONFIG_VERSION,
+      offchainConfigBytes,
+      billingTokens,
+      billingConfigs
+    );
+  }
+
+  function testSetConfigRevertDueToInvalidDecimals() 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: address(USDTOKEN_USD_FEED),
+      fallbackPrice: 2_000_000_000, // $20
+      minSpend: 100_000,
+      decimals: 6 // link token should have 18 decimals
+    });
+
+    vm.expectRevert(abi.encodeWithSelector(Registry.InvalidToken.selector));
+    registry.setConfigTypeSafe(
+      SIGNERS,
+      TRANSMITTERS,
+      F,
+      cfg,
+      OFFCHAIN_CONFIG_VERSION,
+      offchainConfigBytes,
+      billingTokens,
+      billingConfigs
+    );
+  }
+
+  function testSetConfigOnTransmittersAndPayees() public {
+    registry.setPayees(PAYEES);
+    AutomationRegistryBase2_3.TransmitterPayeeInfo[] memory transmitterPayeeInfos = registry
+      .getTransmittersWithPayees();
+    assertEq(transmitterPayeeInfos.length, TRANSMITTERS.length);
+
+    for (uint256 i = 0; i < transmitterPayeeInfos.length; i++) {
+      address transmitterAddress = transmitterPayeeInfos[i].transmitterAddress;
+      address payeeAddress = transmitterPayeeInfos[i].payeeAddress;
+
+      address expectedTransmitter = TRANSMITTERS[i];
+      address expectedPayee = PAYEES[i];
+
+      assertEq(transmitterAddress, expectedTransmitter);
+      assertEq(payeeAddress, expectedPayee);
+    }
+  }
+
+  function testSetConfigWithNewTransmittersSuccess() public {
+    registry = deployZKSyncRegistry(AutoBase.PayoutMode.OFF_CHAIN);
+
+    (uint32 configCount, uint32 blockNumber, ) = registry.latestConfigDetails();
+    assertEq(configCount, 0);
+
+    address billingTokenAddress = address(usdToken18);
+    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: address(USDTOKEN_USD_FEED),
+      fallbackPrice: 2_000_000_000, // $20
+      minSpend: 100_000,
+      decimals: 18
+    });
+
+    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 _getRegistrars() private pure returns (address[] memory) {
+    address[] memory registrars = new address[](2);
+    registrars[0] = address(uint160(uint256(keccak256("registrar1"))));
+    registrars[1] = address(uint160(uint256(keccak256("registrar2"))));
+    return registrars;
+  }
+
+  function _configDigestFromConfigData(
+    uint256 chainId,
+    address contractAddress,
+    uint64 configCount,
+    address[] memory signers,
+    address[] memory transmitters,
+    uint8 f,
+    bytes memory onchainConfig,
+    uint64 offchainConfigVersion,
+    bytes memory offchainConfig
+  ) internal pure returns (bytes32) {
+    uint256 h = uint256(
+      keccak256(
+        abi.encode(
+          chainId,
+          contractAddress,
+          configCount,
+          signers,
+          transmitters,
+          f,
+          onchainConfig,
+          offchainConfigVersion,
+          offchainConfig
+        )
+      )
+    );
+    uint256 prefixMask = type(uint256).max << (256 - 16); // 0xFFFF00..00
+    uint256 prefix = 0x0001 << (256 - 16); // 0x000100..00
+    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, ) = deployAndConfigureZKSyncRegistryAndRegistrar(AutoBase.PayoutMode.ON_CHAIN);
+
+    vm.expectRevert(abi.encodeWithSelector(Registry.OnlyFinanceAdmin.selector));
+    registry.settleNOPsOffchain();
+  }
+
+  function testSettleNOPsOffchainRevertDueToOffchainSettlementDisabled() public {
+    (Registry registry, ) = deployAndConfigureZKSyncRegistryAndRegistrar(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, ) = deployAndConfigureZKSyncRegistryAndRegistrar(AutoBase.PayoutMode.OFF_CHAIN);
+    registry.setPayees(PAYEES);
+
+    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();
+  }
+
+  // 1. transmitter balance zeroed after settlement, 2. admin can withdraw ERC20, 3. switch to onchain mode, 4. link amount owed to NOPs stays the same
+  function testSettleNOPsOffchainSuccessWithERC20MultiSteps() public {
+    // deploy and configure a registry with OFF_CHAIN payout
+    (Registry registry, ) = deployAndConfigureZKSyncRegistryAndRegistrar(AutoBase.PayoutMode.OFF_CHAIN);
+    registry.setPayees(PAYEES);
+
+    // register an upkeep and add funds
+    uint256 id = registry.registerUpkeep(address(TARGET1), 1000000, UPKEEP_ADMIN, 0, address(usdToken18), "", "", "");
+    _mintERC20_18Decimals(UPKEEP_ADMIN, 1e20);
+    vm.startPrank(UPKEEP_ADMIN);
+    usdToken18.approve(address(registry), 1e20);
+    registry.addFunds(id, 1e20);
+
+    // manually create a transmit so transmitters earn some rewards
+    _transmit(id, registry, bytes4(0));
+
+    // 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);
+    }
+
+    // after the offchain settlement, the total reserve amount of LINK should be 0
+    assertEq(registry.getReserveAmount(address(linkToken)), 0);
+    // should have some ERC20s in registry after transmit
+    uint256 erc20ForPayment1 = registry.getAvailableERC20ForPayment(address(usdToken18));
+    require(erc20ForPayment1 > 0, "ERC20AvailableForPayment should be positive");
+
+    vm.startPrank(UPKEEP_ADMIN);
+    vm.roll(100 + block.number);
+    // manually create a transmit so transmitters earn some rewards
+    _transmit(id, registry, bytes4(0));
+
+    uint256 erc20ForPayment2 = registry.getAvailableERC20ForPayment(address(usdToken18));
+    require(erc20ForPayment2 > erc20ForPayment1, "ERC20AvailableForPayment should be greater after another transmit");
+
+    // finance admin comes to withdraw all available ERC20s
+    vm.startPrank(FINANCE_ADMIN);
+    registry.withdrawERC20Fees(address(usdToken18), FINANCE_ADMIN, erc20ForPayment2);
+
+    uint256 erc20ForPayment3 = registry.getAvailableERC20ForPayment(address(usdToken18));
+    require(erc20ForPayment3 == 0, "ERC20AvailableForPayment should be 0 now after withdrawal");
+
+    uint256 reservedLink = registry.getReserveAmount(address(linkToken));
+    require(reservedLink > 0, "Reserve amount of LINK should be positive since there was another transmit");
+
+    // owner comes to disable offchain mode
+    vm.startPrank(registry.owner());
+    registry.disableOffchainPayments();
+
+    // finance admin comes to withdraw all available ERC20s, should revert bc of insufficient link liquidity
+    vm.startPrank(FINANCE_ADMIN);
+    uint256 erc20ForPayment4 = registry.getAvailableERC20ForPayment(address(usdToken18));
+    vm.expectRevert(abi.encodeWithSelector(Registry.InsufficientLinkLiquidity.selector));
+    registry.withdrawERC20Fees(address(usdToken18), FINANCE_ADMIN, erc20ForPayment4);
+
+    // reserved link amount to NOPs should stay the same after switching to onchain mode
+    assertEq(registry.getReserveAmount(address(linkToken)), reservedLink);
+    // available ERC20 for payment should be 0 since finance admin withdrew all already
+    assertEq(erc20ForPayment4, 0);
+  }
+
+  function testSettleNOPsOffchainForDeactivatedTransmittersSuccess() public {
+    // deploy and configure a registry with OFF_CHAIN payout
+    (Registry registry, Registrar registrar) = deployAndConfigureZKSyncRegistryAndRegistrar(
+      AutoBase.PayoutMode.OFF_CHAIN
+    );
+
+    // register an upkeep and add funds
+    uint256 id = registry.registerUpkeep(address(TARGET1), 1000000, UPKEEP_ADMIN, 0, address(usdToken18), "", "", "");
+    _mintERC20_18Decimals(UPKEEP_ADMIN, 1e20);
+    vm.startPrank(UPKEEP_ADMIN);
+    usdToken18.approve(address(registry), 1e20);
+    registry.addFunds(id, 1e20);
+
+    // manually create a transmit so TRANSMITTERS earn some rewards
+    _transmit(id, registry, bytes4(0));
+
+    // TRANSMITTERS have positive balance now
+    // configure the registry to use NEW_TRANSMITTERS
+    _configureWithNewTransmitters(registry, registrar);
+
+    _transmit(id, registry, bytes4(0));
+
+    // 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);
+    }
+
+    // after the offchain settlement, the total reserve amount of LINK should be 0
+    assertEq(registry.getReserveAmount(address(linkToken)), 0);
+  }
+
+  function testDisableOffchainPaymentsRevertDueToUnauthorizedCaller() public {
+    (Registry registry, ) = deployAndConfigureZKSyncRegistryAndRegistrar(AutoBase.PayoutMode.OFF_CHAIN);
+
+    vm.startPrank(FINANCE_ADMIN);
+    vm.expectRevert(bytes("Only callable by owner"));
+    registry.disableOffchainPayments();
+  }
+
+  function testDisableOffchainPaymentsSuccess() public {
+    (Registry registry, ) = deployAndConfigureZKSyncRegistryAndRegistrar(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, ) = deployAndConfigureZKSyncRegistryAndRegistrar(AutoBase.PayoutMode.OFF_CHAIN);
+    registry.setPayees(PAYEES);
+
+    // register an upkeep and add funds
+    uint256 id = registry.registerUpkeep(address(TARGET1), 1000000, UPKEEP_ADMIN, 0, address(usdToken18), "", "", "");
+    _mintERC20_18Decimals(UPKEEP_ADMIN, 1e20);
+    vm.startPrank(UPKEEP_ADMIN);
+    usdToken18.approve(address(registry), 1e20);
+    registry.addFunds(id, 1e20);
+
+    // manually create a transmit so transmitters earn some rewards
+    _transmit(id, registry, bytes4(0));
+
+    // 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, ) = deployAndConfigureZKSyncRegistryAndRegistrar(AutoBase.PayoutMode.OFF_CHAIN);
+    registry.setPayees(PAYEES);
+
+    // register an upkeep and add funds
+    uint256 id = registry.registerUpkeep(address(TARGET1), 1000000, UPKEEP_ADMIN, 0, address(usdToken18), "", "", "");
+    _mintERC20_18Decimals(UPKEEP_ADMIN, 1e20);
+    vm.startPrank(UPKEEP_ADMIN);
+    usdToken18.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, bytes4(0));
+    }
+
+    // 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, bytes4(0));
+    }
+
+    // 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(usdToken18));
+
+    uint256[] memory minRegistrationFees = new uint256[](billingTokens.length);
+    minRegistrationFees[0] = 100e18; // 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: 1e8, // $1
+      minSpend: 1e18, // 1 USD
+      decimals: 18
+    });
+
+    address[] memory 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 = deployZKSyncRegistry(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(usdUpkeepID18));
+  }
+
+  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_with18Decimals() public {
+    // set fee to 0
+    AutomationRegistryBase2_3.BillingConfig memory usdTokenConfig = registry.getBillingTokenConfig(address(usdToken18));
+    usdTokenConfig.flatFeeMilliCents = 0;
+    _updateBillingTokenConfig(registry, address(usdToken18), usdTokenConfig);
+
+    uint256 minBalanceBefore = registry.getMinBalanceForUpkeep(usdUpkeepID18);
+
+    // set fee to non-zero
+    usdTokenConfig.flatFeeMilliCents = 100;
+    _updateBillingTokenConfig(registry, address(usdToken18), usdTokenConfig);
+
+    uint256 minBalanceAfter = registry.getMinBalanceForUpkeep(usdUpkeepID18);
+    assertEq(
+      minBalanceAfter,
+      minBalanceBefore + ((uint256(usdTokenConfig.flatFeeMilliCents) * 1e13) / 10 ** (18 - usdTokenConfig.decimals))
+    );
+  }
+
+  function test_accountsForFlatFee_with6Decimals() public {
+    // set fee to 0
+    AutomationRegistryBase2_3.BillingConfig memory usdTokenConfig = registry.getBillingTokenConfig(address(usdToken6));
+    usdTokenConfig.flatFeeMilliCents = 0;
+    _updateBillingTokenConfig(registry, address(usdToken6), usdTokenConfig);
+
+    uint256 minBalanceBefore = registry.getMinBalanceForUpkeep(usdUpkeepID6);
+
+    // set fee to non-zero
+    usdTokenConfig.flatFeeMilliCents = 100;
+    _updateBillingTokenConfig(registry, address(usdToken6), usdTokenConfig);
+
+    uint256 minBalanceAfter = registry.getMinBalanceForUpkeep(usdUpkeepID6);
+    assertEq(
+      minBalanceAfter,
+      minBalanceBefore + ((uint256(usdTokenConfig.flatFeeMilliCents) * 1e13) / 10 ** (18 - usdTokenConfig.decimals))
+    );
+  }
+}
+
+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_transmitRevertWithExtraBytes() external {
+    bytes32[3] memory exampleReportContext = [
+      bytes32(0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef),
+      bytes32(0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890),
+      bytes32(0x7890abcdef1234567890abcdef1234567890abcdef1234567890abcdef123456)
+    ];
+
+    bytes memory exampleRawReport = hex"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+
+    bytes32[] memory exampleRs = new bytes32[](3);
+    exampleRs[0] = bytes32(0x1234561234561234561234561234561234561234561234561234561234561234);
+    exampleRs[1] = bytes32(0x1234561234561234561234561234561234561234561234561234561234561234);
+    exampleRs[2] = bytes32(0x7890789078907890789078907890789078907890789078907890789078907890);
+
+    bytes32[] memory exampleSs = new bytes32[](3);
+    exampleSs[0] = bytes32(0x1234561234561234561234561234561234561234561234561234561234561234);
+    exampleSs[1] = bytes32(0x1234561234561234561234561234561234561234561234561234561234561234);
+    exampleSs[2] = bytes32(0x1234561234561234561234561234561234561234561234561234561234561234);
+
+    bytes32 exampleRawVs = bytes32(0x1234561234561234561234561234561234561234561234561234561234561234);
+
+    bytes memory transmitData = abi.encodeWithSelector(
+      registry.transmit.selector,
+      exampleReportContext,
+      exampleRawReport,
+      exampleRs,
+      exampleSs,
+      exampleRawVs
+    );
+    bytes memory badTransmitData = bytes.concat(transmitData, bytes1(0x00)); // add extra data
+    vm.startPrank(TRANSMITTERS[0]);
+    (bool success, bytes memory returnData) = address(registry).call(badTransmitData); // send the bogus transmit
+    assertFalse(success, "Call did not revert as expected");
+    assertEq(returnData, abi.encodePacked(Registry.InvalidDataLength.selector));
+    vm.stopPrank();
+  }
+
+  function test_handlesMixedBatchOfBillingTokens() external {
+    uint256[] memory prevUpkeepBalances = new uint256[](3);
+    prevUpkeepBalances[0] = registry.getBalance(linkUpkeepID);
+    prevUpkeepBalances[1] = registry.getBalance(usdUpkeepID18);
+    prevUpkeepBalances[2] = registry.getBalance(nativeUpkeepID);
+    uint256[] memory prevTokenBalances = new uint256[](3);
+    prevTokenBalances[0] = linkToken.balanceOf(address(registry));
+    prevTokenBalances[1] = usdToken18.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(usdToken18));
+    prevReserveBalances[2] = registry.getReserveAmount(address(weth));
+    uint256[] memory upkeepIDs = new uint256[](3);
+    upkeepIDs[0] = linkUpkeepID;
+    upkeepIDs[1] = usdUpkeepID18;
+    upkeepIDs[2] = nativeUpkeepID;
+
+    // withdraw-able by finance team should be 0
+    require(registry.getAvailableERC20ForPayment(address(usdToken18)) == 0, "ERC20AvailableForPayment should be 0");
+    require(registry.getAvailableERC20ForPayment(address(weth)) == 0, "ERC20AvailableForPayment should be 0");
+
+    // do the thing
+    _transmit(upkeepIDs, registry, bytes4(0));
+
+    // withdraw-able by the finance team should be positive
+    require(
+      registry.getAvailableERC20ForPayment(address(usdToken18)) > 0,
+      "ERC20AvailableForPayment should be positive"
+    );
+    require(registry.getAvailableERC20ForPayment(address(weth)) > 0, "ERC20AvailableForPayment should be positive");
+
+    // assert upkeep balances have decreased
+    require(prevUpkeepBalances[0] > registry.getBalance(linkUpkeepID), "link upkeep balance should have decreased");
+    require(prevUpkeepBalances[1] > registry.getBalance(usdUpkeepID18), "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], usdToken18.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(usdToken18)),
+      "usd reserve amount should have decreased"
+    );
+    require(
+      prevReserveBalances[2] > registry.getReserveAmount(address(weth)),
+      "native reserve amount should have decreased"
+    );
+  }
+
+  function test_handlesInsufficientBalanceWithUSDToken18() external {
+    // deploy and configure a registry with ON_CHAIN payout
+    (Registry registry, ) = deployAndConfigureZKSyncRegistryAndRegistrar(AutoBase.PayoutMode.ON_CHAIN);
+
+    // register an upkeep and add funds
+    uint256 upkeepID = registry.registerUpkeep(
+      address(TARGET1),
+      1000000,
+      UPKEEP_ADMIN,
+      0,
+      address(usdToken18),
+      "",
+      "",
+      ""
+    );
+    _mintERC20_18Decimals(UPKEEP_ADMIN, 1e20);
+    vm.startPrank(UPKEEP_ADMIN);
+    usdToken18.approve(address(registry), 1e20);
+    registry.addFunds(upkeepID, 1); // smaller than gasCharge
+    uint256 balance = registry.getBalance(upkeepID);
+
+    // manually create a transmit
+    vm.recordLogs();
+    _transmit(upkeepID, registry, bytes4(0));
+    Vm.Log[] memory entries = vm.getRecordedLogs();
+
+    assertEq(entries.length, 3);
+    Vm.Log memory l1 = entries[1];
+    assertEq(
+      l1.topics[0],
+      keccak256("UpkeepCharged(uint256,(uint96,uint96,uint96,uint96,address,uint96,uint96,uint96))")
+    );
+    (
+      uint96 gasChargeInBillingToken,
+      uint96 premiumInBillingToken,
+      uint96 gasReimbursementInJuels,
+      uint96 premiumInJuels,
+      address billingToken,
+      uint96 linkUSD,
+      uint96 nativeUSD,
+      uint96 billingUSD
+    ) = abi.decode(l1.data, (uint96, uint96, uint96, uint96, address, uint96, uint96, uint96));
+
+    assertEq(gasChargeInBillingToken, balance);
+    assertEq(gasReimbursementInJuels, (balance * billingUSD) / linkUSD);
+    assertEq(premiumInJuels, 0);
+    assertEq(premiumInBillingToken, 0);
+  }
+
+  function test_handlesInsufficientBalanceWithUSDToken6() external {
+    // deploy and configure a registry with ON_CHAIN payout
+    (Registry registry, ) = deployAndConfigureZKSyncRegistryAndRegistrar(AutoBase.PayoutMode.ON_CHAIN);
+
+    // register an upkeep and add funds
+    uint256 upkeepID = registry.registerUpkeep(
+      address(TARGET1),
+      1000000,
+      UPKEEP_ADMIN,
+      0,
+      address(usdToken6),
+      "",
+      "",
+      ""
+    );
+    vm.prank(OWNER);
+    usdToken6.mint(UPKEEP_ADMIN, 1e20);
+
+    vm.startPrank(UPKEEP_ADMIN);
+    usdToken6.approve(address(registry), 1e20);
+    registry.addFunds(upkeepID, 100); // this is greater than gasCharge but less than (gasCharge + premium)
+    uint256 balance = registry.getBalance(upkeepID);
+
+    // manually create a transmit
+    vm.recordLogs();
+    _transmit(upkeepID, registry, bytes4(0));
+    Vm.Log[] memory entries = vm.getRecordedLogs();
+
+    assertEq(entries.length, 3);
+    Vm.Log memory l1 = entries[1];
+    assertEq(
+      l1.topics[0],
+      keccak256("UpkeepCharged(uint256,(uint96,uint96,uint96,uint96,address,uint96,uint96,uint96))")
+    );
+    (
+      uint96 gasChargeInBillingToken,
+      uint96 premiumInBillingToken,
+      uint96 gasReimbursementInJuels,
+      uint96 premiumInJuels,
+      address billingToken,
+      uint96 linkUSD,
+      uint96 nativeUSD,
+      uint96 billingUSD
+    ) = abi.decode(l1.data, (uint96, uint96, uint96, uint96, address, uint96, uint96, uint96));
+
+    assertEq(premiumInJuels, (balance * billingUSD * 1e12) / linkUSD - gasReimbursementInJuels); // scale to 18 decimals
+    assertEq(premiumInBillingToken, (premiumInJuels * linkUSD + (billingUSD * 1e12 - 1)) / (billingUSD * 1e12));
+  }
+}
+
+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, ) = deployAndConfigureZKSyncRegistryAndRegistrar(AutoBase.PayoutMode.ON_CHAIN);
+    idsToMigrate.push(linkUpkeepID);
+    idsToMigrate.push(linkUpkeepID2);
+    idsToMigrate.push(usdUpkeepID18);
+    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(usdUpkeepID18);
+    registry.setUpkeepTriggerConfig(linkUpkeepID, randomBytes(100));
+    registry.setUpkeepCheckData(nativeUpkeepID, randomBytes(25));
+
+    // record previous state
+    uint256[] memory prevUpkeepBalances = new uint256[](4);
+    prevUpkeepBalances[0] = registry.getBalance(linkUpkeepID);
+    prevUpkeepBalances[1] = registry.getBalance(linkUpkeepID2);
+    prevUpkeepBalances[2] = registry.getBalance(usdUpkeepID18);
+    prevUpkeepBalances[3] = registry.getBalance(nativeUpkeepID);
+    uint256[] memory prevReserveBalances = new uint256[](3);
+    prevReserveBalances[0] = registry.getReserveAmount(address(linkToken));
+    prevReserveBalances[1] = registry.getReserveAmount(address(usdToken18));
+    prevReserveBalances[2] = registry.getReserveAmount(address(weth));
+    uint256[] memory prevTokenBalances = new uint256[](3);
+    prevTokenBalances[0] = linkToken.balanceOf(address(registry));
+    prevTokenBalances[1] = usdToken18.balanceOf(address(registry));
+    prevTokenBalances[2] = weth.balanceOf(address(registry));
+    bytes[] memory prevUpkeepData = new bytes[](4);
+    prevUpkeepData[0] = abi.encode(registry.getUpkeep(linkUpkeepID));
+    prevUpkeepData[1] = abi.encode(registry.getUpkeep(linkUpkeepID2));
+    prevUpkeepData[2] = abi.encode(registry.getUpkeep(usdUpkeepID18));
+    prevUpkeepData[3] = abi.encode(registry.getUpkeep(nativeUpkeepID));
+    bytes[] memory prevUpkeepTriggerData = new bytes[](4);
+    prevUpkeepTriggerData[0] = registry.getUpkeepTriggerConfig(linkUpkeepID);
+    prevUpkeepTriggerData[1] = registry.getUpkeepTriggerConfig(linkUpkeepID2);
+    prevUpkeepTriggerData[2] = registry.getUpkeepTriggerConfig(usdUpkeepID18);
+    prevUpkeepTriggerData[3] = registry.getUpkeepTriggerConfig(nativeUpkeepID);
+
+    // event expectations
+    vm.expectEmit(address(registry));
+    emit UpkeepMigrated(linkUpkeepID, prevUpkeepBalances[0], address(newRegistry));
+    vm.expectEmit(address(registry));
+    emit UpkeepMigrated(linkUpkeepID2, prevUpkeepBalances[1], address(newRegistry));
+    vm.expectEmit(address(registry));
+    emit UpkeepMigrated(usdUpkeepID18, prevUpkeepBalances[2], address(newRegistry));
+    vm.expectEmit(address(registry));
+    emit UpkeepMigrated(nativeUpkeepID, prevUpkeepBalances[3], address(newRegistry));
+    vm.expectEmit(address(newRegistry));
+    emit UpkeepReceived(linkUpkeepID, prevUpkeepBalances[0], address(registry));
+    vm.expectEmit(address(newRegistry));
+    emit UpkeepReceived(linkUpkeepID2, prevUpkeepBalances[1], address(registry));
+    vm.expectEmit(address(newRegistry));
+    emit UpkeepReceived(usdUpkeepID18, prevUpkeepBalances[2], address(registry));
+    vm.expectEmit(address(newRegistry));
+    emit UpkeepReceived(nativeUpkeepID, prevUpkeepBalances[3], address(registry));
+
+    // do the thing
+    registry.migrateUpkeeps(idsToMigrate, address(newRegistry));
+
+    // assert upkeep balances have been migrated
+    assertEq(registry.getBalance(linkUpkeepID), 0);
+    assertEq(registry.getBalance(linkUpkeepID2), 0);
+    assertEq(registry.getBalance(usdUpkeepID18), 0);
+    assertEq(registry.getBalance(nativeUpkeepID), 0);
+    assertEq(newRegistry.getBalance(linkUpkeepID), prevUpkeepBalances[0]);
+    assertEq(newRegistry.getBalance(linkUpkeepID2), prevUpkeepBalances[1]);
+    assertEq(newRegistry.getBalance(usdUpkeepID18), prevUpkeepBalances[2]);
+    assertEq(newRegistry.getBalance(nativeUpkeepID), prevUpkeepBalances[3]);
+
+    // assert reserve balances have been adjusted
+    assertEq(
+      newRegistry.getReserveAmount(address(linkToken)),
+      newRegistry.getBalance(linkUpkeepID) + newRegistry.getBalance(linkUpkeepID2)
+    );
+    assertEq(newRegistry.getReserveAmount(address(usdToken18)), newRegistry.getBalance(usdUpkeepID18));
+    assertEq(newRegistry.getReserveAmount(address(weth)), newRegistry.getBalance(nativeUpkeepID));
+    assertEq(
+      newRegistry.getReserveAmount(address(linkToken)),
+      prevReserveBalances[0] - registry.getReserveAmount(address(linkToken))
+    );
+    assertEq(
+      newRegistry.getReserveAmount(address(usdToken18)),
+      prevReserveBalances[1] - registry.getReserveAmount(address(usdToken18))
+    );
+    assertEq(
+      newRegistry.getReserveAmount(address(weth)),
+      prevReserveBalances[2] - registry.getReserveAmount(address(weth))
+    );
+
+    // assert token have been transferred
+    assertEq(
+      linkToken.balanceOf(address(newRegistry)),
+      newRegistry.getBalance(linkUpkeepID) + newRegistry.getBalance(linkUpkeepID2)
+    );
+    assertEq(usdToken18.balanceOf(address(newRegistry)), newRegistry.getBalance(usdUpkeepID18));
+    assertEq(weth.balanceOf(address(newRegistry)), newRegistry.getBalance(nativeUpkeepID));
+    assertEq(linkToken.balanceOf(address(registry)), prevTokenBalances[0] - linkToken.balanceOf(address(newRegistry)));
+    assertEq(
+      usdToken18.balanceOf(address(registry)),
+      prevTokenBalances[1] - usdToken18.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(linkUpkeepID2)));
+    assertEq(prevUpkeepData[2], abi.encode(newRegistry.getUpkeep(usdUpkeepID18)));
+    assertEq(prevUpkeepData[3], abi.encode(newRegistry.getUpkeep(nativeUpkeepID)));
+    assertEq(prevUpkeepTriggerData[0], newRegistry.getUpkeepTriggerConfig(linkUpkeepID));
+    assertEq(prevUpkeepTriggerData[1], newRegistry.getUpkeepTriggerConfig(linkUpkeepID2));
+    assertEq(prevUpkeepTriggerData[2], newRegistry.getUpkeepTriggerConfig(usdUpkeepID18));
+    assertEq(prevUpkeepTriggerData[3], newRegistry.getUpkeepTriggerConfig(nativeUpkeepID));
+
+    vm.stopPrank();
+  }
+}
+
+contract Pause is SetUp {
+  function test_RevertsWhen_CalledByNonOwner() external {
+    vm.expectRevert(bytes("Only callable by owner"));
+    vm.prank(STRANGER);
+    registry.pause();
+  }
+
+  function test_CalledByOwner_success() external {
+    vm.startPrank(registry.owner());
+    registry.pause();
+
+    (IAutomationV21PlusCommon.StateLegacy memory state, , , , ) = registry.getState();
+    assertTrue(state.paused);
+  }
+
+  function test_revertsWhen_registerUpkeepInPausedRegistry() external {
+    vm.startPrank(registry.owner());
+    registry.pause();
+
+    vm.expectRevert(Registry.RegistryPaused.selector);
+    uint256 id = registry.registerUpkeep(
+      address(TARGET1),
+      config.maxPerformGas,
+      UPKEEP_ADMIN,
+      uint8(Trigger.CONDITION),
+      address(linkToken),
+      "",
+      "",
+      ""
+    );
+  }
+
+  function test_revertsWhen_transmitInPausedRegistry() external {
+    vm.startPrank(registry.owner());
+    registry.pause();
+
+    _transmit(usdUpkeepID18, registry, Registry.RegistryPaused.selector);
+  }
+}
+
+contract Unpause is SetUp {
+  function test_RevertsWhen_CalledByNonOwner() external {
+    vm.startPrank(registry.owner());
+    registry.pause();
+
+    vm.expectRevert(bytes("Only callable by owner"));
+    vm.startPrank(STRANGER);
+    registry.unpause();
+  }
+
+  function test_CalledByOwner_success() external {
+    vm.startPrank(registry.owner());
+    registry.pause();
+    (IAutomationV21PlusCommon.StateLegacy memory state1, , , , ) = registry.getState();
+    assertTrue(state1.paused);
+
+    registry.unpause();
+    (IAutomationV21PlusCommon.StateLegacy memory state2, , , , ) = registry.getState();
+    assertFalse(state2.paused);
+  }
+}
+
+contract CancelUpkeep is SetUp {
+  event UpkeepCanceled(uint256 indexed id, uint64 indexed atBlockHeight);
+
+  function test_RevertsWhen_IdIsInvalid_CalledByAdmin() external {
+    vm.startPrank(UPKEEP_ADMIN);
+    vm.expectRevert(Registry.CannotCancel.selector);
+    registry.cancelUpkeep(1111111);
+  }
+
+  function test_RevertsWhen_IdIsInvalid_CalledByOwner() external {
+    vm.startPrank(registry.owner());
+    vm.expectRevert(Registry.CannotCancel.selector);
+    registry.cancelUpkeep(1111111);
+  }
+
+  function test_RevertsWhen_NotCalledByOwnerOrAdmin() external {
+    vm.startPrank(STRANGER);
+    vm.expectRevert(Registry.OnlyCallableByOwnerOrAdmin.selector);
+    registry.cancelUpkeep(linkUpkeepID);
+  }
+
+  function test_RevertsWhen_UpkeepAlreadyCanceledByAdmin_CalledByOwner() external {
+    uint256 bn = block.number;
+    vm.startPrank(UPKEEP_ADMIN);
+    registry.cancelUpkeep(linkUpkeepID);
+
+    vm.startPrank(registry.owner());
+    vm.expectRevert(Registry.UpkeepCancelled.selector);
+    registry.cancelUpkeep(linkUpkeepID);
+  }
+
+  function test_RevertsWhen_UpkeepAlreadyCanceledByOwner_CalledByAdmin() external {
+    uint256 bn = block.number;
+    vm.startPrank(registry.owner());
+    registry.cancelUpkeep(linkUpkeepID);
+
+    vm.startPrank(UPKEEP_ADMIN);
+    vm.expectRevert(Registry.UpkeepCancelled.selector);
+    registry.cancelUpkeep(linkUpkeepID);
+  }
+
+  function test_RevertsWhen_UpkeepAlreadyCanceledByAdmin_CalledByAdmin() external {
+    uint256 bn = block.number;
+    vm.startPrank(UPKEEP_ADMIN);
+    registry.cancelUpkeep(linkUpkeepID);
+
+    vm.expectRevert(Registry.UpkeepCancelled.selector);
+    registry.cancelUpkeep(linkUpkeepID);
+  }
+
+  function test_RevertsWhen_UpkeepAlreadyCanceledByOwner_CalledByOwner() external {
+    uint256 bn = block.number;
+    vm.startPrank(registry.owner());
+    registry.cancelUpkeep(linkUpkeepID);
+
+    vm.expectRevert(Registry.UpkeepCancelled.selector);
+    registry.cancelUpkeep(linkUpkeepID);
+  }
+
+  function test_CancelUpkeep_SetMaxValidBlockNumber_CalledByAdmin() external {
+    uint256 bn = block.number;
+    vm.startPrank(UPKEEP_ADMIN);
+    registry.cancelUpkeep(linkUpkeepID);
+
+    uint256 maxValidBlocknumber = uint256(registry.getUpkeep(linkUpkeepID).maxValidBlocknumber);
+
+    // 50 is the cancellation delay
+    assertEq(bn + 50, maxValidBlocknumber);
+  }
+
+  function test_CancelUpkeep_SetMaxValidBlockNumber_CalledByOwner() external {
+    uint256 bn = block.number;
+    vm.startPrank(registry.owner());
+    registry.cancelUpkeep(linkUpkeepID);
+
+    uint256 maxValidBlocknumber = uint256(registry.getUpkeep(linkUpkeepID).maxValidBlocknumber);
+
+    // cancellation by registry owner is immediate and no cancellation delay is applied
+    assertEq(bn, maxValidBlocknumber);
+  }
+
+  function test_CancelUpkeep_EmitEvent_CalledByAdmin() external {
+    uint256 bn = block.number;
+    vm.startPrank(UPKEEP_ADMIN);
+
+    vm.expectEmit();
+    emit UpkeepCanceled(linkUpkeepID, uint64(bn + 50));
+    registry.cancelUpkeep(linkUpkeepID);
+  }
+
+  function test_CancelUpkeep_EmitEvent_CalledByOwner() external {
+    uint256 bn = block.number;
+    vm.startPrank(registry.owner());
+
+    vm.expectEmit();
+    emit UpkeepCanceled(linkUpkeepID, uint64(bn));
+    registry.cancelUpkeep(linkUpkeepID);
+  }
+}
+
+contract SetPeerRegistryMigrationPermission is SetUp {
+  function test_SetPeerRegistryMigrationPermission_CalledByOwner() external {
+    address peer = randomAddress();
+    vm.startPrank(registry.owner());
+
+    uint8 permission = registry.getPeerRegistryMigrationPermission(peer);
+    assertEq(0, permission);
+
+    registry.setPeerRegistryMigrationPermission(peer, 1);
+    permission = registry.getPeerRegistryMigrationPermission(peer);
+    assertEq(1, permission);
+
+    registry.setPeerRegistryMigrationPermission(peer, 2);
+    permission = registry.getPeerRegistryMigrationPermission(peer);
+    assertEq(2, permission);
+
+    registry.setPeerRegistryMigrationPermission(peer, 0);
+    permission = registry.getPeerRegistryMigrationPermission(peer);
+    assertEq(0, permission);
+  }
+
+  function test_RevertsWhen_InvalidPermission_CalledByOwner() external {
+    address peer = randomAddress();
+    vm.startPrank(registry.owner());
+
+    vm.expectRevert();
+    registry.setPeerRegistryMigrationPermission(peer, 100);
+  }
+
+  function test_RevertsWhen_CalledByNonOwner() external {
+    address peer = randomAddress();
+    vm.startPrank(STRANGER);
+
+    vm.expectRevert(bytes("Only callable by owner"));
+    registry.setPeerRegistryMigrationPermission(peer, 1);
+  }
+}
+
+contract SetUpkeepPrivilegeConfig is SetUp {
+  function test_RevertsWhen_CalledByNonManager() external {
+    vm.startPrank(STRANGER);
+
+    vm.expectRevert(Registry.OnlyCallableByUpkeepPrivilegeManager.selector);
+    registry.setUpkeepPrivilegeConfig(linkUpkeepID, hex"1233");
+  }
+
+  function test_UpkeepHasEmptyConfig() external {
+    bytes memory cfg = registry.getUpkeepPrivilegeConfig(linkUpkeepID);
+    assertEq(cfg, bytes(""));
+  }
+
+  function test_SetUpkeepPrivilegeConfig_CalledByManager() external {
+    vm.startPrank(PRIVILEGE_MANAGER);
+
+    registry.setUpkeepPrivilegeConfig(linkUpkeepID, hex"1233");
+
+    bytes memory cfg = registry.getUpkeepPrivilegeConfig(linkUpkeepID);
+    assertEq(cfg, hex"1233");
+  }
+}
+
+contract SetAdminPrivilegeConfig is SetUp {
+  function test_RevertsWhen_CalledByNonManager() external {
+    vm.startPrank(STRANGER);
+
+    vm.expectRevert(Registry.OnlyCallableByUpkeepPrivilegeManager.selector);
+    registry.setAdminPrivilegeConfig(randomAddress(), hex"1233");
+  }
+
+  function test_UpkeepHasEmptyConfig() external {
+    bytes memory cfg = registry.getAdminPrivilegeConfig(randomAddress());
+    assertEq(cfg, bytes(""));
+  }
+
+  function test_SetAdminPrivilegeConfig_CalledByManager() external {
+    vm.startPrank(PRIVILEGE_MANAGER);
+    address admin = randomAddress();
+
+    registry.setAdminPrivilegeConfig(admin, hex"1233");
+
+    bytes memory cfg = registry.getAdminPrivilegeConfig(admin);
+    assertEq(cfg, hex"1233");
+  }
+}
+
+contract TransferUpkeepAdmin is SetUp {
+  event UpkeepAdminTransferRequested(uint256 indexed id, address indexed from, address indexed to);
+
+  function test_RevertsWhen_NotCalledByAdmin() external {
+    vm.startPrank(STRANGER);
+
+    vm.expectRevert(Registry.OnlyCallableByAdmin.selector);
+    registry.transferUpkeepAdmin(linkUpkeepID, randomAddress());
+  }
+
+  function test_RevertsWhen_TransferToSelf() external {
+    vm.startPrank(UPKEEP_ADMIN);
+
+    vm.expectRevert(Registry.ValueNotChanged.selector);
+    registry.transferUpkeepAdmin(linkUpkeepID, UPKEEP_ADMIN);
+  }
+
+  function test_RevertsWhen_UpkeepCanceled() external {
+    vm.startPrank(UPKEEP_ADMIN);
+
+    registry.cancelUpkeep(linkUpkeepID);
+
+    vm.expectRevert(Registry.UpkeepCancelled.selector);
+    registry.transferUpkeepAdmin(linkUpkeepID, randomAddress());
+  }
+
+  function test_DoesNotChangeUpkeepAdmin() external {
+    vm.startPrank(UPKEEP_ADMIN);
+    registry.transferUpkeepAdmin(linkUpkeepID, randomAddress());
+
+    assertEq(registry.getUpkeep(linkUpkeepID).admin, UPKEEP_ADMIN);
+  }
+
+  function test_EmitEvent_CalledByAdmin() external {
+    vm.startPrank(UPKEEP_ADMIN);
+    address newAdmin = randomAddress();
+
+    vm.expectEmit();
+    emit UpkeepAdminTransferRequested(linkUpkeepID, UPKEEP_ADMIN, newAdmin);
+    registry.transferUpkeepAdmin(linkUpkeepID, newAdmin);
+
+    // transferring to the same propose admin won't yield another event
+    vm.recordLogs();
+    registry.transferUpkeepAdmin(linkUpkeepID, newAdmin);
+    Vm.Log[] memory entries = vm.getRecordedLogs();
+    assertEq(0, entries.length);
+  }
+
+  function test_CancelTransfer_ByTransferToEmptyAddress() external {
+    vm.startPrank(UPKEEP_ADMIN);
+    address newAdmin = randomAddress();
+
+    vm.expectEmit();
+    emit UpkeepAdminTransferRequested(linkUpkeepID, UPKEEP_ADMIN, newAdmin);
+    registry.transferUpkeepAdmin(linkUpkeepID, newAdmin);
+
+    vm.expectEmit();
+    emit UpkeepAdminTransferRequested(linkUpkeepID, UPKEEP_ADMIN, address(0));
+    registry.transferUpkeepAdmin(linkUpkeepID, address(0));
+  }
+}
+
+contract AcceptUpkeepAdmin is SetUp {
+  event UpkeepAdminTransferred(uint256 indexed id, address indexed from, address indexed to);
+
+  function test_RevertsWhen_NotCalledByProposedAdmin() external {
+    vm.startPrank(UPKEEP_ADMIN);
+    address newAdmin = randomAddress();
+    registry.transferUpkeepAdmin(linkUpkeepID, newAdmin);
+
+    vm.startPrank(STRANGER);
+    vm.expectRevert(Registry.OnlyCallableByProposedAdmin.selector);
+    registry.acceptUpkeepAdmin(linkUpkeepID);
+  }
+
+  function test_RevertsWhen_UpkeepCanceled() external {
+    vm.startPrank(UPKEEP_ADMIN);
+    address newAdmin = randomAddress();
+    registry.transferUpkeepAdmin(linkUpkeepID, newAdmin);
+
+    registry.cancelUpkeep(linkUpkeepID);
+
+    vm.startPrank(newAdmin);
+    vm.expectRevert(Registry.UpkeepCancelled.selector);
+    registry.acceptUpkeepAdmin(linkUpkeepID);
+  }
+
+  function test_UpkeepAdminChanged() external {
+    vm.startPrank(UPKEEP_ADMIN);
+    address newAdmin = randomAddress();
+    registry.transferUpkeepAdmin(linkUpkeepID, newAdmin);
+
+    vm.startPrank(newAdmin);
+    vm.expectEmit();
+    emit UpkeepAdminTransferred(linkUpkeepID, UPKEEP_ADMIN, newAdmin);
+    registry.acceptUpkeepAdmin(linkUpkeepID);
+
+    assertEq(newAdmin, registry.getUpkeep(linkUpkeepID).admin);
+  }
+}
+
+contract PauseUpkeep is SetUp {
+  event UpkeepPaused(uint256 indexed id);
+
+  function test_RevertsWhen_NotCalledByUpkeepAdmin() external {
+    vm.startPrank(STRANGER);
+
+    vm.expectRevert(Registry.OnlyCallableByAdmin.selector);
+    registry.pauseUpkeep(linkUpkeepID);
+  }
+
+  function test_RevertsWhen_InvalidUpkeepId() external {
+    vm.startPrank(UPKEEP_ADMIN);
+
+    vm.expectRevert(Registry.OnlyCallableByAdmin.selector);
+    registry.pauseUpkeep(linkUpkeepID + 1);
+  }
+
+  function test_RevertsWhen_UpkeepAlreadyCanceled() external {
+    vm.startPrank(UPKEEP_ADMIN);
+    registry.cancelUpkeep(linkUpkeepID);
+
+    vm.expectRevert(Registry.UpkeepCancelled.selector);
+    registry.pauseUpkeep(linkUpkeepID);
+  }
+
+  function test_RevertsWhen_UpkeepAlreadyPaused() external {
+    vm.startPrank(UPKEEP_ADMIN);
+    registry.pauseUpkeep(linkUpkeepID);
+
+    vm.expectRevert(Registry.OnlyUnpausedUpkeep.selector);
+    registry.pauseUpkeep(linkUpkeepID);
+  }
+
+  function test_EmitEvent_CalledByAdmin() external {
+    vm.startPrank(UPKEEP_ADMIN);
+
+    vm.expectEmit();
+    emit UpkeepPaused(linkUpkeepID);
+    registry.pauseUpkeep(linkUpkeepID);
+  }
+}
+
+contract UnpauseUpkeep is SetUp {
+  event UpkeepUnpaused(uint256 indexed id);
+
+  function test_RevertsWhen_InvalidUpkeepId() external {
+    vm.startPrank(UPKEEP_ADMIN);
+
+    vm.expectRevert(Registry.OnlyCallableByAdmin.selector);
+    registry.unpauseUpkeep(linkUpkeepID + 1);
+  }
+
+  function test_RevertsWhen_UpkeepIsNotPaused() external {
+    vm.startPrank(UPKEEP_ADMIN);
+
+    vm.expectRevert(Registry.OnlyPausedUpkeep.selector);
+    registry.unpauseUpkeep(linkUpkeepID);
+  }
+
+  function test_RevertsWhen_UpkeepAlreadyCanceled() external {
+    vm.startPrank(UPKEEP_ADMIN);
+    registry.pauseUpkeep(linkUpkeepID);
+
+    registry.cancelUpkeep(linkUpkeepID);
+
+    vm.expectRevert(Registry.UpkeepCancelled.selector);
+    registry.unpauseUpkeep(linkUpkeepID);
+  }
+
+  function test_RevertsWhen_NotCalledByUpkeepAdmin() external {
+    vm.startPrank(UPKEEP_ADMIN);
+    registry.pauseUpkeep(linkUpkeepID);
+
+    vm.startPrank(STRANGER);
+    vm.expectRevert(Registry.OnlyCallableByAdmin.selector);
+    registry.unpauseUpkeep(linkUpkeepID);
+  }
+
+  function test_UnpauseUpkeep_CalledByUpkeepAdmin() external {
+    vm.startPrank(UPKEEP_ADMIN);
+    registry.pauseUpkeep(linkUpkeepID);
+
+    uint256[] memory ids1 = registry.getActiveUpkeepIDs(0, 0);
+
+    vm.expectEmit();
+    emit UpkeepUnpaused(linkUpkeepID);
+    registry.unpauseUpkeep(linkUpkeepID);
+
+    uint256[] memory ids2 = registry.getActiveUpkeepIDs(0, 0);
+    assertEq(ids1.length + 1, ids2.length);
+  }
+}
+
+contract SetUpkeepCheckData is SetUp {
+  event UpkeepCheckDataSet(uint256 indexed id, bytes newCheckData);
+
+  function test_RevertsWhen_InvalidUpkeepId() external {
+    vm.startPrank(UPKEEP_ADMIN);
+
+    vm.expectRevert(Registry.OnlyCallableByAdmin.selector);
+    registry.setUpkeepCheckData(linkUpkeepID + 1, hex"1234");
+  }
+
+  function test_RevertsWhen_UpkeepAlreadyCanceled() external {
+    vm.startPrank(UPKEEP_ADMIN);
+    registry.cancelUpkeep(linkUpkeepID);
+
+    vm.expectRevert(Registry.UpkeepCancelled.selector);
+    registry.setUpkeepCheckData(linkUpkeepID, hex"1234");
+  }
+
+  function test_RevertsWhen_NewCheckDataTooLarge() external {
+    vm.startPrank(UPKEEP_ADMIN);
+
+    vm.expectRevert(Registry.CheckDataExceedsLimit.selector);
+    registry.setUpkeepCheckData(linkUpkeepID, new bytes(10_000));
+  }
+
+  function test_RevertsWhen_NotCalledByAdmin() external {
+    vm.startPrank(STRANGER);
+
+    vm.expectRevert(Registry.OnlyCallableByAdmin.selector);
+    registry.setUpkeepCheckData(linkUpkeepID, hex"1234");
+  }
+
+  function test_UpdateOffchainConfig_CalledByAdmin() external {
+    vm.startPrank(UPKEEP_ADMIN);
+
+    vm.expectEmit();
+    emit UpkeepCheckDataSet(linkUpkeepID, hex"1234");
+    registry.setUpkeepCheckData(linkUpkeepID, hex"1234");
+
+    assertEq(registry.getUpkeep(linkUpkeepID).checkData, hex"1234");
+  }
+
+  function test_UpdateOffchainConfigOnPausedUpkeep_CalledByAdmin() external {
+    vm.startPrank(UPKEEP_ADMIN);
+
+    registry.pauseUpkeep(linkUpkeepID);
+
+    vm.expectEmit();
+    emit UpkeepCheckDataSet(linkUpkeepID, hex"1234");
+    registry.setUpkeepCheckData(linkUpkeepID, hex"1234");
+
+    assertTrue(registry.getUpkeep(linkUpkeepID).paused);
+    assertEq(registry.getUpkeep(linkUpkeepID).checkData, hex"1234");
+  }
+}
+
+contract SetUpkeepGasLimit is SetUp {
+  event UpkeepGasLimitSet(uint256 indexed id, uint96 gasLimit);
+
+  function test_RevertsWhen_InvalidUpkeepId() external {
+    vm.startPrank(UPKEEP_ADMIN);
+
+    vm.expectRevert(Registry.OnlyCallableByAdmin.selector);
+    registry.setUpkeepGasLimit(linkUpkeepID + 1, 1230000);
+  }
+
+  function test_RevertsWhen_UpkeepAlreadyCanceled() external {
+    vm.startPrank(UPKEEP_ADMIN);
+    registry.cancelUpkeep(linkUpkeepID);
+
+    vm.expectRevert(Registry.UpkeepCancelled.selector);
+    registry.setUpkeepGasLimit(linkUpkeepID, 1230000);
+  }
+
+  function test_RevertsWhen_NewGasLimitOutOfRange() external {
+    vm.startPrank(UPKEEP_ADMIN);
+
+    vm.expectRevert(Registry.GasLimitOutsideRange.selector);
+    registry.setUpkeepGasLimit(linkUpkeepID, 300);
+
+    vm.expectRevert(Registry.GasLimitOutsideRange.selector);
+    registry.setUpkeepGasLimit(linkUpkeepID, 3000000000);
+  }
+
+  function test_RevertsWhen_NotCalledByAdmin() external {
+    vm.startPrank(STRANGER);
+
+    vm.expectRevert(Registry.OnlyCallableByAdmin.selector);
+    registry.setUpkeepGasLimit(linkUpkeepID, 1230000);
+  }
+
+  function test_UpdateGasLimit_CalledByAdmin() external {
+    vm.startPrank(UPKEEP_ADMIN);
+
+    vm.expectEmit();
+    emit UpkeepGasLimitSet(linkUpkeepID, 1230000);
+    registry.setUpkeepGasLimit(linkUpkeepID, 1230000);
+
+    assertEq(registry.getUpkeep(linkUpkeepID).performGas, 1230000);
+  }
+}
+
+contract SetUpkeepOffchainConfig is SetUp {
+  event UpkeepOffchainConfigSet(uint256 indexed id, bytes offchainConfig);
+
+  function test_RevertsWhen_InvalidUpkeepId() external {
+    vm.startPrank(UPKEEP_ADMIN);
+
+    vm.expectRevert(Registry.OnlyCallableByAdmin.selector);
+    registry.setUpkeepOffchainConfig(linkUpkeepID + 1, hex"1233");
+  }
+
+  function test_RevertsWhen_UpkeepAlreadyCanceled() external {
+    vm.startPrank(UPKEEP_ADMIN);
+    registry.cancelUpkeep(linkUpkeepID);
+
+    vm.expectRevert(Registry.UpkeepCancelled.selector);
+    registry.setUpkeepOffchainConfig(linkUpkeepID, hex"1234");
+  }
+
+  function test_RevertsWhen_NotCalledByAdmin() external {
+    vm.startPrank(STRANGER);
+
+    vm.expectRevert(Registry.OnlyCallableByAdmin.selector);
+    registry.setUpkeepOffchainConfig(linkUpkeepID, hex"1234");
+  }
+
+  function test_UpdateOffchainConfig_CalledByAdmin() external {
+    vm.startPrank(UPKEEP_ADMIN);
+
+    vm.expectEmit();
+    emit UpkeepOffchainConfigSet(linkUpkeepID, hex"1234");
+    registry.setUpkeepOffchainConfig(linkUpkeepID, hex"1234");
+
+    assertEq(registry.getUpkeep(linkUpkeepID).offchainConfig, hex"1234");
+  }
+}
+
+contract SetUpkeepTriggerConfig is SetUp {
+  event UpkeepTriggerConfigSet(uint256 indexed id, bytes triggerConfig);
+
+  function test_RevertsWhen_InvalidUpkeepId() external {
+    vm.startPrank(UPKEEP_ADMIN);
+
+    vm.expectRevert(Registry.OnlyCallableByAdmin.selector);
+    registry.setUpkeepTriggerConfig(linkUpkeepID + 1, hex"1233");
+  }
+
+  function test_RevertsWhen_UpkeepAlreadyCanceled() external {
+    vm.startPrank(UPKEEP_ADMIN);
+    registry.cancelUpkeep(linkUpkeepID);
+
+    vm.expectRevert(Registry.UpkeepCancelled.selector);
+    registry.setUpkeepTriggerConfig(linkUpkeepID, hex"1234");
+  }
+
+  function test_RevertsWhen_NotCalledByAdmin() external {
+    vm.startPrank(STRANGER);
+
+    vm.expectRevert(Registry.OnlyCallableByAdmin.selector);
+    registry.setUpkeepTriggerConfig(linkUpkeepID, hex"1234");
+  }
+
+  function test_UpdateTriggerConfig_CalledByAdmin() external {
+    vm.startPrank(UPKEEP_ADMIN);
+
+    vm.expectEmit();
+    emit UpkeepTriggerConfigSet(linkUpkeepID, hex"1234");
+    registry.setUpkeepTriggerConfig(linkUpkeepID, hex"1234");
+
+    assertEq(registry.getUpkeepTriggerConfig(linkUpkeepID), hex"1234");
+  }
+}
+
+contract TransferPayeeship is SetUp {
+  event PayeeshipTransferRequested(address indexed transmitter, address indexed from, address indexed to);
+
+  function test_RevertsWhen_NotCalledByPayee() external {
+    vm.startPrank(STRANGER);
+
+    vm.expectRevert(Registry.OnlyCallableByPayee.selector);
+    registry.transferPayeeship(TRANSMITTERS[0], randomAddress());
+  }
+
+  function test_RevertsWhen_TransferToSelf() external {
+    registry.setPayees(PAYEES);
+    vm.startPrank(PAYEES[0]);
+
+    vm.expectRevert(Registry.ValueNotChanged.selector);
+    registry.transferPayeeship(TRANSMITTERS[0], PAYEES[0]);
+  }
+
+  function test_Transfer_DoesNotChangePayee() external {
+    registry.setPayees(PAYEES);
+
+    vm.startPrank(PAYEES[0]);
+
+    registry.transferPayeeship(TRANSMITTERS[0], randomAddress());
+
+    (, , , , address payee) = registry.getTransmitterInfo(TRANSMITTERS[0]);
+    assertEq(PAYEES[0], payee);
+  }
+
+  function test_EmitEvent_CalledByPayee() external {
+    registry.setPayees(PAYEES);
+
+    vm.startPrank(PAYEES[0]);
+    address newPayee = randomAddress();
+
+    vm.expectEmit();
+    emit PayeeshipTransferRequested(TRANSMITTERS[0], PAYEES[0], newPayee);
+    registry.transferPayeeship(TRANSMITTERS[0], newPayee);
+
+    // transferring to the same propose payee won't yield another event
+    vm.recordLogs();
+    registry.transferPayeeship(TRANSMITTERS[0], newPayee);
+    Vm.Log[] memory entries = vm.getRecordedLogs();
+    assertEq(0, entries.length);
+  }
+}
+
+contract AcceptPayeeship is SetUp {
+  event PayeeshipTransferred(address indexed transmitter, address indexed from, address indexed to);
+
+  function test_RevertsWhen_NotCalledByProposedPayee() external {
+    registry.setPayees(PAYEES);
+
+    vm.startPrank(PAYEES[0]);
+    address newPayee = randomAddress();
+    registry.transferPayeeship(TRANSMITTERS[0], newPayee);
+
+    vm.startPrank(STRANGER);
+    vm.expectRevert(Registry.OnlyCallableByProposedPayee.selector);
+    registry.acceptPayeeship(TRANSMITTERS[0]);
+  }
+
+  function test_PayeeChanged() external {
+    registry.setPayees(PAYEES);
+
+    vm.startPrank(PAYEES[0]);
+    address newPayee = randomAddress();
+    registry.transferPayeeship(TRANSMITTERS[0], newPayee);
+
+    vm.startPrank(newPayee);
+    vm.expectEmit();
+    emit PayeeshipTransferred(TRANSMITTERS[0], PAYEES[0], newPayee);
+    registry.acceptPayeeship(TRANSMITTERS[0]);
+
+    (, , , , address payee) = registry.getTransmitterInfo(TRANSMITTERS[0]);
+    assertEq(newPayee, payee);
+  }
+}
+
+contract SetPayees is SetUp {
+  event PayeesUpdated(address[] transmitters, address[] payees);
+
+  function test_RevertsWhen_NotCalledByOwner() external {
+    vm.startPrank(STRANGER);
+
+    vm.expectRevert(bytes("Only callable by owner"));
+    registry.setPayees(NEW_PAYEES);
+  }
+
+  function test_RevertsWhen_PayeesLengthError() external {
+    vm.startPrank(registry.owner());
+
+    address[] memory payees = new address[](5);
+    vm.expectRevert(Registry.ParameterLengthError.selector);
+    registry.setPayees(payees);
+  }
+
+  function test_RevertsWhen_InvalidPayee() external {
+    vm.startPrank(registry.owner());
+
+    NEW_PAYEES[0] = address(0);
+    vm.expectRevert(Registry.InvalidPayee.selector);
+    registry.setPayees(NEW_PAYEES);
+  }
+
+  function test_SetPayees_WhenExistingPayeesAreEmpty() external {
+    (registry, ) = deployAndConfigureZKSyncRegistryAndRegistrar(AutoBase.PayoutMode.ON_CHAIN);
+
+    for (uint256 i = 0; i < TRANSMITTERS.length; i++) {
+      (, , , , address payee) = registry.getTransmitterInfo(TRANSMITTERS[i]);
+      assertEq(address(0), payee);
+    }
+
+    vm.startPrank(registry.owner());
+
+    vm.expectEmit();
+    emit PayeesUpdated(TRANSMITTERS, PAYEES);
+    registry.setPayees(PAYEES);
+    for (uint256 i = 0; i < TRANSMITTERS.length; i++) {
+      (bool active, , , , address payee) = registry.getTransmitterInfo(TRANSMITTERS[i]);
+      assertTrue(active);
+      assertEq(PAYEES[i], payee);
+    }
+  }
+
+  function test_DotNotSetPayeesToIgnoredAddress() external {
+    address IGNORE_ADDRESS = 0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF;
+    (registry, ) = deployAndConfigureZKSyncRegistryAndRegistrar(AutoBase.PayoutMode.ON_CHAIN);
+    PAYEES[0] = IGNORE_ADDRESS;
+
+    registry.setPayees(PAYEES);
+    (bool active, , , , address payee) = registry.getTransmitterInfo(TRANSMITTERS[0]);
+    assertTrue(active);
+    assertEq(address(0), payee);
+
+    (active, , , , payee) = registry.getTransmitterInfo(TRANSMITTERS[1]);
+    assertTrue(active);
+    assertEq(PAYEES[1], payee);
+  }
+}
+
+contract GetActiveUpkeepIDs is SetUp {
+  function test_RevertsWhen_IndexOutOfRange() external {
+    vm.expectRevert(Registry.IndexOutOfRange.selector);
+    registry.getActiveUpkeepIDs(5, 0);
+
+    vm.expectRevert(Registry.IndexOutOfRange.selector);
+    registry.getActiveUpkeepIDs(6, 0);
+  }
+
+  function test_ReturnsAllUpkeeps_WhenMaxCountIsZero() external {
+    uint256[] memory uids = registry.getActiveUpkeepIDs(0, 0);
+    assertEq(5, uids.length);
+
+    uids = registry.getActiveUpkeepIDs(2, 0);
+    assertEq(3, uids.length);
+  }
+
+  function test_ReturnsAllRemainingUpkeeps_WhenMaxCountIsTooLarge() external {
+    uint256[] memory uids = registry.getActiveUpkeepIDs(2, 20);
+    assertEq(3, uids.length);
+  }
+
+  function test_ReturnsUpkeeps_BoundByMaxCount() external {
+    uint256[] memory uids = registry.getActiveUpkeepIDs(1, 2);
+    assertEq(2, uids.length);
+    assertEq(uids[0], linkUpkeepID2);
+    assertEq(uids[1], usdUpkeepID18);
+  }
+}
diff --git a/contracts/src/v0.8/automation/testhelpers/UpkeepCounterNew.sol b/contracts/src/v0.8/automation/testhelpers/UpkeepCounterNew.sol
new file mode 100644
index 00000000000..76b38776893
--- /dev/null
+++ b/contracts/src/v0.8/automation/testhelpers/UpkeepCounterNew.sol
@@ -0,0 +1,126 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.16;
+
+contract UpkeepCounterNew {
+  event PerformingUpkeep(
+    address indexed from,
+    uint256 initialTimestamp,
+    uint256 lastTimestamp,
+    uint256 previousBlock,
+    uint256 counter
+  );
+  error InvalidCaller(address caller, address forwarder);
+
+  uint256 public testRange;
+  uint256 public interval;
+  uint256 public lastTimestamp;
+  uint256 public previousPerformBlock;
+  uint256 public initialTimestamp;
+  uint256 public counter;
+  bool public useMoreCheckGas;
+  bool public useMorePerformGas;
+  bool public useMorePerformData;
+  uint256 public checkGasToBurn;
+  uint256 public performGasToBurn;
+  bytes public data;
+  bytes public dataCopy;
+  bool public trickSimulation = false;
+  address public forwarder;
+
+  constructor() {
+    testRange = 1000000;
+    interval = 40;
+    previousPerformBlock = 0;
+    lastTimestamp = block.timestamp;
+    initialTimestamp = 0;
+    counter = 0;
+    useMoreCheckGas = false;
+    useMorePerformData = false;
+    useMorePerformGas = false;
+    checkGasToBurn = 9700000;
+    performGasToBurn = 7700000;
+  }
+
+  function setPerformGasToBurn(uint256 _performGasToBurn) external {
+    performGasToBurn = _performGasToBurn;
+  }
+
+  function setCheckGasToBurn(uint256 _checkGasToBurn) external {
+    checkGasToBurn = _checkGasToBurn;
+  }
+
+  function setUseMoreCheckGas(bool _useMoreCheckGas) external {
+    useMoreCheckGas = _useMoreCheckGas;
+  }
+
+  function setUseMorePerformGas(bool _useMorePerformGas) external {
+    useMorePerformGas = _useMorePerformGas;
+  }
+
+  function setUseMorePerformData(bool _useMorePerformData) external {
+    useMorePerformData = _useMorePerformData;
+  }
+
+  function setData(bytes calldata _data) external {
+    data = _data;
+  }
+
+  function checkUpkeep(bytes calldata) external view returns (bool, bytes memory) {
+    if (useMoreCheckGas) {
+      uint256 startGas = gasleft();
+      while (startGas - gasleft() < checkGasToBurn) {} // burn gas
+    }
+
+    if (useMorePerformData) {
+      return (eligible(), data);
+    }
+    return (eligible(), "");
+  }
+
+  function setTrickSimulation(bool _trickSimulation) external {
+    trickSimulation = _trickSimulation;
+  }
+
+  function performUpkeep(bytes calldata performData) external {
+    if (trickSimulation && tx.origin == address(0)) {
+      return;
+    }
+
+    if (msg.sender != forwarder) {
+      revert InvalidCaller(msg.sender, forwarder);
+    }
+
+    if (useMorePerformGas) {
+      uint256 startGas = gasleft();
+      while (startGas - gasleft() < performGasToBurn) {} // burn gas
+    }
+
+    if (initialTimestamp == 0) {
+      initialTimestamp = block.timestamp;
+    }
+    lastTimestamp = block.timestamp;
+    counter = counter + 1;
+    dataCopy = performData;
+    emit PerformingUpkeep(tx.origin, initialTimestamp, lastTimestamp, previousPerformBlock, counter);
+    previousPerformBlock = lastTimestamp;
+  }
+
+  function eligible() public view returns (bool) {
+    if (initialTimestamp == 0) {
+      return true;
+    }
+
+    return (block.timestamp - initialTimestamp) < testRange && (block.timestamp - lastTimestamp) >= interval;
+  }
+
+  function setSpread(uint256 _testRange, uint256 _interval) external {
+    testRange = _testRange;
+    interval = _interval;
+    initialTimestamp = 0;
+    counter = 0;
+  }
+
+  function setForwarder(address _forwarder) external {
+    forwarder = _forwarder;
+  }
+}
diff --git a/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistry2_3.sol b/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistry2_3.sol
index 00858085e3e..67a9a56b3d5 100644
--- a/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistry2_3.sol
+++ b/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistry2_3.sol
@@ -72,7 +72,6 @@ contract ZKSyncAutomationRegistry2_3 is ZKSyncAutomationRegistryBase2_3, OCR2Abs
     uint16 numUpkeepsPassedChecks;
     uint96 totalReimbursement;
     uint96 totalPremium;
-    uint256 totalCalldataWeight;
   }
 
   // ================================================================
@@ -89,7 +88,6 @@ contract ZKSyncAutomationRegistry2_3 is ZKSyncAutomationRegistryBase2_3, OCR2Abs
     bytes32[] calldata ss,
     bytes32 rawVs
   ) external override {
-    uint256 gasOverhead = gasleft();
     // use this msg.data length check to ensure no extra data is included in the call
     // 4 is first 4 bytes of the keccak-256 hash of the function signature. ss.length == rs.length so use one of them
     // 4 + (32 * 3) + (rawReport.length + 32 + 32) + (32 * rs.length + 32 + 32) + (32 * ss.length + 32 + 32) + 32
@@ -110,7 +108,7 @@ contract ZKSyncAutomationRegistry2_3 is ZKSyncAutomationRegistryBase2_3, OCR2Abs
     uint40 epochAndRound = uint40(uint256(reportContext[1]));
     uint32 epoch = uint32(epochAndRound >> 8);
 
-    _handleReport(hotVars, report, gasOverhead);
+    _handleReport(hotVars, report);
 
     if (epoch > hotVars.latestEpoch) {
       s_hotVars.latestEpoch = epoch;
@@ -121,22 +119,20 @@ contract ZKSyncAutomationRegistry2_3 is ZKSyncAutomationRegistryBase2_3, OCR2Abs
    * @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 {
+  function _handleReport(HotVars memory hotVars, Report memory report) private {
     UpkeepTransmitInfo[] memory upkeepTransmitInfo = new UpkeepTransmitInfo[](report.upkeepIds.length);
     TransmitVars memory transmitVars = TransmitVars({
       numUpkeepsPassedChecks: 0,
-      totalCalldataWeight: 0,
       totalReimbursement: 0,
       totalPremium: 0
     });
 
     uint256 blocknumber = hotVars.chainModule.blockNumber();
-    uint256 l1Fee = hotVars.chainModule.getCurrentL1Fee(msg.data.length); // this will be updated
+    uint256 gasOverhead;
 
     for (uint256 i = 0; i < report.upkeepIds.length; i++) {
       upkeepTransmitInfo[i].upkeep = s_upkeep[report.upkeepIds[i]];
@@ -163,28 +159,25 @@ contract ZKSyncAutomationRegistry2_3 is ZKSyncAutomationRegistryBase2_3, OCR2Abs
         report.performDatas[i]
       );
 
-      // To split L1 fee across the upkeeps, assign a weight to this upkeep based on the length
-      // of the perform data and calldata overhead
-      upkeepTransmitInfo[i].calldataWeight =
-        report.performDatas[i].length +
-        TRANSMIT_CALLDATA_FIXED_BYTES_OVERHEAD +
-        (TRANSMIT_CALLDATA_PER_SIGNER_BYTES_OVERHEAD * (hotVars.f + 1));
-      transmitVars.totalCalldataWeight += upkeepTransmitInfo[i].calldataWeight;
-
-      // Deduct the gasUsed by upkeep from the overhead tally - upkeeps pay for their own gas individually
-      gasOverhead -= upkeepTransmitInfo[i].gasUsed;
-
       // Store last perform block number / deduping key for upkeep
       _updateTriggerMarker(report.upkeepIds[i], blocknumber, upkeepTransmitInfo[i]);
+
+      if (upkeepTransmitInfo[i].triggerType == Trigger.CONDITION) {
+        gasOverhead += REGISTRY_CONDITIONAL_OVERHEAD;
+      } else {
+        gasOverhead += REGISTRY_LOG_OVERHEAD;
+      }
     }
     // No upkeeps to be performed in this report
     if (transmitVars.numUpkeepsPassedChecks == 0) {
       return;
     }
 
-    // This is the overall gas overhead that will be split across performed upkeeps
-    // Take upper bound of 16 gas per callData bytes
-    gasOverhead = (gasOverhead - gasleft()) + (16 * msg.data.length) + ACCOUNTING_FIXED_GAS_OVERHEAD;
+    gasOverhead +=
+      16 *
+      msg.data.length +
+      ACCOUNTING_FIXED_GAS_OVERHEAD +
+      (REGISTRY_PER_SIGNER_GAS_OVERHEAD * (hotVars.f + 1));
     gasOverhead = gasOverhead / transmitVars.numUpkeepsPassedChecks + ACCOUNTING_PER_UPKEEP_GAS_OVERHEAD;
 
     {
@@ -200,7 +193,7 @@ contract ZKSyncAutomationRegistry2_3 is ZKSyncAutomationRegistryBase2_3, OCR2Abs
             PaymentParams({
               gasLimit: upkeepTransmitInfo[i].gasUsed,
               gasOverhead: gasOverhead,
-              l1CostWei: (l1Fee * upkeepTransmitInfo[i].calldataWeight) / transmitVars.totalCalldataWeight,
+              l1CostWei: 0,
               fastGasWei: report.fastGasWei,
               linkUSD: report.linkUSD,
               nativeUSD: nativeUSD,
diff --git a/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryBase2_3.sol b/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryBase2_3.sol
index 524ecacc826..41097af7f26 100644
--- a/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryBase2_3.sol
+++ b/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryBase2_3.sol
@@ -34,8 +34,6 @@ abstract contract ZKSyncAutomationRegistryBase2_3 is ConfirmedOwner {
   bytes4 internal constant CHECK_LOG_SELECTOR = ILogAutomation.checkLog.selector;
   uint256 internal constant PERFORM_GAS_MIN = 2_300;
   uint256 internal constant CANCELLATION_DELAY = 50;
-  uint256 internal constant PERFORM_GAS_CUSHION = 5_000;
-  uint256 internal constant PPB_BASE = 1_000_000_000;
   uint32 internal constant UINT32_MAX = type(uint32).max;
   // The first byte of the mask can be 0, because we only ever have 31 oracles
   uint256 internal constant ORACLE_MASK = 0x0001010101010101010101010101010101010101010101010101010101010101;
@@ -47,20 +45,13 @@ abstract contract ZKSyncAutomationRegistryBase2_3 is ConfirmedOwner {
   uint256 internal constant REGISTRY_CONDITIONAL_OVERHEAD = 98_200; // Fixed gas overhead for conditional upkeeps
   uint256 internal constant REGISTRY_LOG_OVERHEAD = 122_500; // Fixed gas overhead for log upkeeps
   uint256 internal constant REGISTRY_PER_SIGNER_GAS_OVERHEAD = 5_600; // Value scales with f
-  uint256 internal constant REGISTRY_PER_PERFORM_BYTE_GAS_OVERHEAD = 24; // Per perform data byte overhead
-
-  // The overhead (in bytes) in addition to perform data for upkeep sent in calldata
-  // This includes overhead for all struct encoding as well as report signatures
-  // There is a fixed component and a per signer component. This is calculated exactly by doing abi encoding
-  uint256 internal constant TRANSMIT_CALLDATA_FIXED_BYTES_OVERHEAD = 932;
-  uint256 internal constant TRANSMIT_CALLDATA_PER_SIGNER_BYTES_OVERHEAD = 64;
 
   // Next block of constants are used in actual payment calculation. We calculate the exact gas used within the
   // tx itself, but since payment processing itself takes gas, and it needs the overhead as input, we use fixed constants
   // to account for gas used in payment processing. These values are calibrated using hardhat tests which simulates various cases and verifies that
   // the variables result in accurate estimation
-  uint256 internal constant ACCOUNTING_FIXED_GAS_OVERHEAD = 51_200; // Fixed overhead per tx
-  uint256 internal constant ACCOUNTING_PER_UPKEEP_GAS_OVERHEAD = 14_200; // Overhead per upkeep performed in batch
+  uint256 internal constant ACCOUNTING_FIXED_GAS_OVERHEAD = 51_000; // Fixed overhead per tx
+  uint256 internal constant ACCOUNTING_PER_UPKEEP_GAS_OVERHEAD = 20_000; // Overhead per upkeep performed in batch
 
   LinkTokenInterface internal immutable i_link;
   AggregatorV3Interface internal immutable i_linkUSDFeed;
@@ -313,7 +304,6 @@ abstract contract ZKSyncAutomationRegistryBase2_3 is ConfirmedOwner {
    * @member performSuccess whether the perform was successful
    * @member triggerType the type of trigger
    * @member gasUsed gasUsed by this upkeep in perform
-   * @member calldataWeight weight assigned to this upkeep for its contribution to calldata. It is used to split L1 fee
    * @member dedupID unique ID used to dedup an upkeep/trigger combo
    */
   struct UpkeepTransmitInfo {
@@ -322,7 +312,6 @@ abstract contract ZKSyncAutomationRegistryBase2_3 is ConfirmedOwner {
     bool performSuccess;
     Trigger triggerType;
     uint256 gasUsed;
-    uint256 calldataWeight;
     bytes32 dedupID;
   }
 
@@ -739,7 +728,6 @@ abstract contract ZKSyncAutomationRegistryBase2_3 is ConfirmedOwner {
     uint256 nativeUSD,
     IERC20 billingToken
   ) internal view returns (uint96) {
-    uint256 maxL1Fee;
     uint256 maxGasOverhead;
 
     {
@@ -750,15 +738,8 @@ abstract contract ZKSyncAutomationRegistryBase2_3 is ConfirmedOwner {
       } 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 chainModuleFixedOverhead, ) = s_hotVars.chainModule.getGasOverhead();
+      maxGasOverhead += (REGISTRY_PER_SIGNER_GAS_OVERHEAD * (hotVars.f + 1)) + chainModuleFixedOverhead;
     }
 
     BillingTokenPaymentParams memory paymentParams = _getBillingTokenPaymentParams(hotVars, billingToken);
@@ -774,7 +755,7 @@ abstract contract ZKSyncAutomationRegistryBase2_3 is ConfirmedOwner {
       PaymentParams({
         gasLimit: performGas,
         gasOverhead: maxGasOverhead,
-        l1CostWei: maxL1Fee,
+        l1CostWei: 0,
         fastGasWei: fastGasWei,
         linkUSD: linkUSD,
         nativeUSD: nativeUSD,
diff --git a/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryLogicC2_3.sol b/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryLogicC2_3.sol
index 61d0eecfbaf..3b4b023c7a2 100644
--- a/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryLogicC2_3.sol
+++ b/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryLogicC2_3.sol
@@ -222,22 +222,10 @@ contract ZKSyncAutomationRegistryLogicC2_3 is ZKSyncAutomationRegistryBase2_3 {
     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;
   }
diff --git a/contracts/src/v0.8/tests/MockGasBoundCaller.sol b/contracts/src/v0.8/tests/MockGasBoundCaller.sol
new file mode 100644
index 00000000000..3184f9dba38
--- /dev/null
+++ b/contracts/src/v0.8/tests/MockGasBoundCaller.sol
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: BUSL-1.1
+pragma solidity 0.8.19;
+
+contract MockGasBoundCaller {
+  error TransactionFailed(address target);
+
+  function gasBoundCall(address target, uint256 gasAmount, bytes memory data) external payable {
+    bool success;
+    assembly {
+      success := call(gasAmount, target, 0, add(data, 0x20), mload(data), 0, 0)
+    }
+
+    // gas bound caller will propagate the revert
+    if (!success) {
+      revert TransactionFailed(target);
+    }
+
+    uint256 pubdataGas = 500000;
+    bytes memory returnData = abi.encode(address(0), pubdataGas);
+
+    uint256 paddedReturndataLen = returnData.length + 96;
+    if (paddedReturndataLen % 32 != 0) {
+      paddedReturndataLen += 32 - (paddedReturndataLen % 32);
+    }
+
+    assembly {
+      mstore(sub(returnData, 0x40), 0x40)
+      mstore(sub(returnData, 0x20), pubdataGas)
+      return(sub(returnData, 0x40), paddedReturndataLen)
+    }
+  }
+}
diff --git a/contracts/src/v0.8/tests/MockZKSyncSystemContext.sol b/contracts/src/v0.8/tests/MockZKSyncSystemContext.sol
new file mode 100644
index 00000000000..265d4b678a5
--- /dev/null
+++ b/contracts/src/v0.8/tests/MockZKSyncSystemContext.sol
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: BUSL-1.1
+pragma solidity 0.8.19;
+
+contract MockZKSyncSystemContext {
+  function gasPrice() external pure returns (uint256) {
+    return 250000000; // 0.25 gwei
+  }
+
+  function gasPerPubdataByte() external pure returns (uint256) {
+    return 500;
+  }
+
+  function getCurrentPubdataSpent() external pure returns (uint256 currentPubdataSpent) {
+    return 1000;
+  }
+}
diff --git a/contracts/test/v0.8/automation/ZKSyncAutomationRegistry2_3.test.ts b/contracts/test/v0.8/automation/ZKSyncAutomationRegistry2_3.test.ts
new file mode 100644
index 00000000000..95210cf6444
--- /dev/null
+++ b/contracts/test/v0.8/automation/ZKSyncAutomationRegistry2_3.test.ts
@@ -0,0 +1,4403 @@
+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 { MockZKSyncSystemContext__factory as MockZKSyncSystemContextFactory } from '../../../typechain/factories/MockZKSyncSystemContext__factory'
+import { ChainModuleBase__factory as ChainModuleBaseFactory } from '../../../typechain/factories/ChainModuleBase__factory'
+import { MockGasBoundCaller__factory as MockGasBoundCallerFactory } from '../../../typechain/factories/MockGasBoundCaller__factory'
+import { ILogAutomation__factory as ILogAutomationactory } from '../../../typechain/factories/ILogAutomation__factory'
+import { AutomationCompatibleUtils } from '../../../typechain/AutomationCompatibleUtils'
+import { StreamsLookupUpkeep } from '../../../typechain/StreamsLookupUpkeep'
+import { MockV3Aggregator } from '../../../typechain/MockV3Aggregator'
+import { MockGasBoundCaller } from '../../../typechain/MockGasBoundCaller'
+import { UpkeepMock } from '../../../typechain/UpkeepMock'
+import { ChainModuleBase } from '../../../typechain/ChainModuleBase'
+import { UpkeepTranscoder } from '../../../typechain/UpkeepTranscoder'
+import { MockZKSyncSystemContext } from '../../../typechain/MockZKSyncSystemContext'
+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 { deployZKSyncRegistry23 } 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(50_000)
+// 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(50_000)
+
+// 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 pubdataGas = BigNumber.from(500000)
+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 mockV3AggregatorFactory: MockV3AggregatorFactory
+let mockGasBoundCallerFactory: MockGasBoundCallerFactory
+let upkeepMockFactory: UpkeepMockFactory
+let upkeepAutoFunderFactory: UpkeepAutoFunderFactory
+let moduleBaseFactory: ChainModuleBaseFactory
+let mockZKSyncSystemContextFactory: MockZKSyncSystemContextFactory
+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 mgRegistry: IAutomationRegistry // "migrate registry" used in migration tests
+let mock: UpkeepMock
+let autoFunderUpkeep: UpkeepAutoFunder
+let ltUpkeep: MockContract
+let transcoder: UpkeepTranscoder
+let moduleBase: ChainModuleBase
+let mockGasBoundCaller: MockGasBoundCaller
+let mockZKSyncSystemContext: MockZKSyncSystemContext
+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('ZKSyncAutomationRegistry2_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 baseConfig: 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
+    mockZKSyncSystemContextFactory = await ethers.getContractFactory(
+      'MockZKSyncSystemContext',
+    )
+    mockGasBoundCallerFactory =
+      await ethers.getContractFactory('MockGasBoundCaller')
+    upkeepMockFactory = await ethers.getContractFactory('UpkeepMock')
+    upkeepAutoFunderFactory =
+      await ethers.getContractFactory('UpkeepAutoFunder')
+    moduleBaseFactory = await ethers.getContractFactory('ChainModuleBase')
+    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
+  ) => {
+    const gasSpent = gasOverhead.add(BigNumber.from(upkeepGasSpent))
+    const gasPayment = gasWei
+      .mul(gasMultiplier)
+      .mul(gasSpent)
+      .mul(nativeUSD)
+      .div(linkUSD)
+
+    const premium = gasWei
+      .mul(gasMultiplier)
+      .mul(upkeepGasSpent)
+      .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,
+  ) => {
+    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(chainModuleOverheads.chainModuleFixedOverhead)
+
+    const totalLogOverhead = registryLogOverhead
+      .add(registryPerSignerGasOverhead.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),
+        ).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),
+        ).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()
+    mockZKSyncSystemContext = await mockZKSyncSystemContextFactory
+      .connect(owner)
+      .deploy()
+    mockGasBoundCaller = await mockGasBoundCallerFactory.connect(owner).deploy()
+    moduleBase = await moduleBaseFactory.connect(owner).deploy()
+    streamsLookupUpkeep = await streamsLookupUpkeepFactory
+      .connect(owner)
+      .deploy(
+        BigNumber.from('10000'),
+        BigNumber.from('100'),
+        false /* useArbBlock */,
+        true /* staging */,
+        false /* verify mercury response */,
+      )
+
+    const zksyncSystemContextCode = await ethers.provider.send('eth_getCode', [
+      mockZKSyncSystemContext.address,
+    ])
+    await ethers.provider.send('hardhat_setCode', [
+      '0x000000000000000000000000000000000000800B',
+      zksyncSystemContextCode,
+    ])
+
+    const gasBoundCallerCode = await ethers.provider.send('eth_getCode', [
+      mockGasBoundCaller.address,
+    ])
+    await ethers.provider.send('hardhat_setCode', [
+      '0xc706EC7dfA5D4Dc87f29f859094165E8290530f5',
+      gasBoundCallerCode,
+    ])
+
+    const financeAdminAddress = await financeAdmin.getAddress()
+
+    config = {
+      checkGasLimit,
+      stalenessSeconds,
+      gasCeilingMultiplier,
+      maxCheckDataSize,
+      maxPerformDataSize,
+      maxRevertDataSize,
+      maxPerformGas,
+      fallbackGasPrice,
+      fallbackLinkPrice,
+      fallbackNativePrice,
+      transcoder: transcoder.address,
+      registrars: [],
+      upkeepPrivilegeManager: upkeepManager,
+      chainModule: moduleBase.address,
+      reorgProtectionEnabled: true,
+      financeAdmin: financeAdminAddress,
+    }
+
+    baseConfig = [
+      signerAddresses,
+      keeperAddresses,
+      f,
+      config,
+      offchainVersion,
+      offchainBytes,
+      [linkToken.address],
+      [
+        {
+          gasFeePPB: paymentPremiumPPB,
+          flatFeeMilliCents,
+          priceFeed: linkUSDFeed.address,
+          fallbackPrice: fallbackLinkPrice,
+          minSpend: minUpkeepSpend,
+          decimals: 18,
+        },
+      ],
+    ]
+
+    const registryParams: Parameters<typeof deployZKSyncRegistry23> = [
+      owner,
+      linkToken.address,
+      linkUSDFeed.address,
+      nativeUSDFeed.address,
+      gasPriceFeed.address,
+      zeroAddress,
+      0, // onchain payout mode
+      wrappedNativeTokenAddress,
+    ]
+
+    registry = await deployZKSyncRegistry23(...registryParams)
+    mgRegistry = await deployZKSyncRegistry23(...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)
+    for (const reg of [registry, 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], {
+            checkBlockNum: 1,
+            checkBlockHash: emptyBytes32,
+          })
+          const receipt = await tx.wait()
+          const upkeepPerformedLogs = parseUpkeepPerformedLogs(receipt)
+          assert.equal(
+            upkeepPerformedLogs.length,
+            1,
+            `wrong log count for ${type} upkeep`,
+          )
+        }
+      })
+
+      it('returns early when future block number is provided as trigger, irrespective of blockhash being present', async () => {
+        const tests: [string, BigNumber][] = [
+          ['conditional', upkeepId],
+          ['log-trigger', logUpkeepId],
+        ]
+        for (const [type, id] of tests) {
+          const latestBlock = await ethers.provider.getBlock('latest')
+
+          // Should fail when blockhash is empty
+          let tx = await getTransmitTx(registry, keeper1, [id], {
+            checkBlockNum: latestBlock.number + 100,
+            checkBlockHash: emptyBytes32,
+          })
+          let receipt = await tx.wait()
+          let reorgedUpkeepReportLogs = parseReorgedUpkeepReportLogs(receipt)
+          // exactly 1 ReorgedUpkeepReportLogs log should be emitted
+          assert.equal(
+            reorgedUpkeepReportLogs.length,
+            1,
+            `wrong log count for ${type} upkeep`,
+          )
+
+          // Should also fail when blockhash is not empty
+          tx = await getTransmitTx(registry, keeper1, [id], {
+            checkBlockNum: latestBlock.number + 100,
+            checkBlockHash: latestBlock.hash,
+          })
+          receipt = await tx.wait()
+          reorgedUpkeepReportLogs = parseReorgedUpkeepReportLogs(receipt)
+          // exactly 1 ReorgedUpkeepReportLogs log should be emitted
+          assert.equal(
+            reorgedUpkeepReportLogs.length,
+            1,
+            `wrong log count for ${type} upkeep`,
+          )
+        }
+      })
+
+      it('returns early when future block number is provided as trigger, irrespective of 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],
+          )
+        const tests: [string, BigNumber][] = [
+          ['conditional', upkeepId],
+          ['log-trigger', logUpkeepId],
+        ]
+        for (const [type, id] of tests) {
+          const latestBlock = await ethers.provider.getBlock('latest')
+
+          // Should fail when blockhash is empty
+          let tx = await getTransmitTx(registry, keeper1, [id], {
+            checkBlockNum: latestBlock.number + 100,
+            checkBlockHash: emptyBytes32,
+          })
+          let receipt = await tx.wait()
+          let reorgedUpkeepReportLogs = parseReorgedUpkeepReportLogs(receipt)
+          // exactly 1 ReorgedUpkeepReportLogs log should be emitted
+          assert.equal(
+            reorgedUpkeepReportLogs.length,
+            1,
+            `wrong log count for ${type} upkeep`,
+          )
+
+          // Should also fail when blockhash is not empty
+          tx = await getTransmitTx(registry, keeper1, [id], {
+            checkBlockNum: latestBlock.number + 100,
+            checkBlockHash: latestBlock.hash,
+          })
+          receipt = await tx.wait()
+          reorgedUpkeepReportLogs = parseReorgedUpkeepReportLogs(receipt)
+          // exactly 1 ReorgedUpkeepReportLogs log should be emitted
+          assert.equal(
+            reorgedUpkeepReportLogs.length,
+            1,
+            `wrong log count for ${type} upkeep`,
+          )
+        }
+      })
+
+      it('returns early when upkeep is cancelled and cancellation delay has gone', async () => {
+        const latestBlockReport = await makeLatestBlockReport([upkeepId])
+        await registry.connect(admin).cancelUpkeep(upkeepId)
+
+        for (let i = 0; i < cancellationDelay; i++) {
+          await ethers.provider.send('evm_mine', [])
+        }
+
+        const tx = await getTransmitTxWithReport(
+          registry,
+          keeper1,
+          latestBlockReport,
+        )
+
+        const receipt = await tx.wait()
+        const cancelledUpkeepReportLogs =
+          parseCancelledUpkeepReportLogs(receipt)
+        // exactly 1 CancelledUpkeepReport log should be emitted
+        assert.equal(cancelledUpkeepReportLogs.length, 1)
+      })
+
+      it('does not revert if the target cannot execute', async () => {
+        await mock.setCanPerform(false)
+        const tx = await getTransmitTx(registry, keeper1, [upkeepId])
+
+        const receipt = await tx.wait()
+        const upkeepPerformedLogs = parseUpkeepPerformedLogs(receipt)
+        // exactly 1 Upkeep Performed should be emitted
+        assert.equal(upkeepPerformedLogs.length, 1)
+        const upkeepPerformedLog = upkeepPerformedLogs[0]
+
+        const success = upkeepPerformedLog.args.success
+        assert.equal(success, false)
+      })
+
+      it('does not revert if the target runs out of gas', async () => {
+        await mock.setCanPerform(false)
+
+        const tx = await getTransmitTx(registry, keeper1, [upkeepId], {
+          performGas: 10, // too little gas
+        })
+
+        const receipt = await tx.wait()
+        const upkeepPerformedLogs = parseUpkeepPerformedLogs(receipt)
+        // exactly 1 Upkeep Performed should be emitted
+        assert.equal(upkeepPerformedLogs.length, 1)
+        const upkeepPerformedLog = upkeepPerformedLogs[0]
+
+        const success = upkeepPerformedLog.args.success
+        assert.equal(success, false)
+      })
+
+      it('reverts if not enough gas supplied', async () => {
+        await mock.setCanPerform(true)
+        await evmRevert(
+          getTransmitTx(registry, keeper1, [upkeepId], {
+            gasLimit: BigNumber.from(150000),
+          }),
+        )
+      })
+
+      it('executes the data passed to the registry', async () => {
+        await mock.setCanPerform(true)
+
+        const tx = await getTransmitTx(registry, keeper1, [upkeepId], {
+          performDatas: [randomBytes],
+        })
+        const receipt = await tx.wait()
+
+        const upkeepPerformedWithABI = [
+          'event UpkeepPerformedWith(bytes upkeepData)',
+        ]
+        const iface = new ethers.utils.Interface(upkeepPerformedWithABI)
+        const parsedLogs = []
+        for (let i = 0; i < receipt.logs.length; i++) {
+          const log = receipt.logs[i]
+          try {
+            parsedLogs.push(iface.parseLog(log))
+          } catch (e) {
+            // ignore log
+          }
+        }
+        assert.equal(parsedLogs.length, 1)
+        assert.equal(parsedLogs[0].args.upkeepData, randomBytes)
+      })
+
+      it('uses actual execution price for payment and premium calculation', async () => {
+        // Actual multiplier is 2, but we set gasPrice to be == gasWei
+        const gasPrice = gasWei
+        await mock.setCanPerform(true)
+        const registryPremiumBefore = (await registry.getState()).state
+          .totalPremium
+        const tx = await getTransmitTx(registry, keeper1, [upkeepId], {
+          gasPrice,
+        })
+        const receipt = await tx.wait()
+        const registryPremiumAfter = (await registry.getState()).state
+          .totalPremium
+        const premium = registryPremiumAfter.sub(registryPremiumBefore)
+
+        const upkeepPerformedLogs = parseUpkeepPerformedLogs(receipt)
+        // exactly 1 Upkeep Performed should be emitted
+        assert.equal(upkeepPerformedLogs.length, 1)
+        const upkeepPerformedLog = upkeepPerformedLogs[0]
+
+        const gasUsed = upkeepPerformedLog.args.gasUsed // 14657 gasUsed
+        const gasOverhead = upkeepPerformedLog.args.gasOverhead // 137230 gasOverhead
+        const totalPayment = upkeepPerformedLog.args.totalPayment
+
+        assert.equal(
+          linkForGas(
+            gasUsed,
+            gasOverhead,
+            BigNumber.from('1'), // Not the config multiplier, but the actual gas used
+            paymentPremiumPPB,
+            flatFeeMilliCents,
+            // pubdataGas.mul(gasPrice),
+          ).total.toString(),
+          totalPayment.toString(),
+        )
+
+        assert.equal(
+          linkForGas(
+            gasUsed,
+            gasOverhead,
+            BigNumber.from('1'), // Not the config multiplier, but the actual gas used
+            paymentPremiumPPB,
+            flatFeeMilliCents,
+            // pubdataGas.mul(gasPrice),
+          ).premium.toString(),
+          premium.toString(),
+        )
+      })
+
+      it('only pays at a rate up to the gas ceiling [ @skip-coverage ]', async () => {
+        // Actual multiplier is 2, but we set gasPrice to be 10x
+        const gasPrice = gasWei.mul(BigNumber.from('10'))
+        await mock.setCanPerform(true)
+
+        const tx = await getTransmitTx(registry, keeper1, [upkeepId], {
+          gasPrice,
+        })
+        const receipt = await tx.wait()
+        const upkeepPerformedLogs = parseUpkeepPerformedLogs(receipt)
+        // exactly 1 Upkeep Performed should be emitted
+        assert.equal(upkeepPerformedLogs.length, 1)
+        const upkeepPerformedLog = upkeepPerformedLogs[0]
+
+        const gasUsed = upkeepPerformedLog.args.gasUsed
+        const gasOverhead = upkeepPerformedLog.args.gasOverhead
+        const totalPayment = upkeepPerformedLog.args.totalPayment
+
+        assert.equal(
+          linkForGas(
+            gasUsed,
+            gasOverhead,
+            gasCeilingMultiplier, // Should be same with exisitng multiplier
+            paymentPremiumPPB,
+            flatFeeMilliCents,
+            // pubdataGas.mul(gasPrice),
+          ).total.toString(),
+          totalPayment.toString(),
+        )
+      })
+
+      itMaybe('can self fund', async () => {
+        const maxPayment = await registry.getMaxPaymentForGas(
+          upkeepId,
+          Trigger.CONDITION,
+          performGas,
+          linkToken.address,
+        )
+
+        // First set auto funding amount to 0 and verify that balance is deducted upon performUpkeep
+        let initialBalance = toWei('100')
+        await registry.connect(owner).addFunds(afUpkeepId, initialBalance)
+        await autoFunderUpkeep.setAutoFundLink(0)
+        await autoFunderUpkeep.setIsEligible(true)
+        await getTransmitTx(registry, keeper1, [afUpkeepId])
+
+        let postUpkeepBalance = (await registry.getUpkeep(afUpkeepId)).balance
+        assert.isTrue(postUpkeepBalance.lt(initialBalance)) // Balance should be deducted
+        assert.isTrue(postUpkeepBalance.gte(initialBalance.sub(maxPayment))) // Balance should not be deducted more than maxPayment
+
+        // Now set auto funding amount to 100 wei and verify that the balance increases
+        initialBalance = postUpkeepBalance
+        const autoTopupAmount = toWei('100')
+        await autoFunderUpkeep.setAutoFundLink(autoTopupAmount)
+        await autoFunderUpkeep.setIsEligible(true)
+        await getTransmitTx(registry, keeper1, [afUpkeepId])
+
+        postUpkeepBalance = (await registry.getUpkeep(afUpkeepId)).balance
+        // Balance should increase by autoTopupAmount and decrease by max maxPayment
+        assert.isTrue(
+          postUpkeepBalance.gte(
+            initialBalance.add(autoTopupAmount).sub(maxPayment),
+          ),
+        )
+      })
+
+      it('can self cancel', async () => {
+        await registry.connect(owner).addFunds(afUpkeepId, toWei('100'))
+
+        await autoFunderUpkeep.setIsEligible(true)
+        await autoFunderUpkeep.setShouldCancel(true)
+
+        let registration = await registry.getUpkeep(afUpkeepId)
+        const oldExpiration = registration.maxValidBlocknumber
+
+        // Do the thing
+        await getTransmitTx(registry, keeper1, [afUpkeepId])
+
+        // Verify upkeep gets cancelled
+        registration = await registry.getUpkeep(afUpkeepId)
+        const newExpiration = registration.maxValidBlocknumber
+        assert.isTrue(newExpiration.lt(oldExpiration))
+      })
+
+      it('reverts when configDigest mismatches', async () => {
+        const report = await makeLatestBlockReport([upkeepId])
+        const reportContext = [emptyBytes32, epochAndRound5_1, emptyBytes32] // wrong config digest
+        const sigs = signReport(reportContext, report, signers.slice(0, f + 1))
+        await evmRevertCustomError(
+          registry
+            .connect(keeper1)
+            .transmit(
+              [reportContext[0], reportContext[1], reportContext[2]],
+              report,
+              sigs.rs,
+              sigs.ss,
+              sigs.vs,
+            ),
+          registry,
+          'ConfigDigestMismatch',
+        )
+      })
+
+      it('reverts with incorrect number of signatures', async () => {
+        const configDigest = (await registry.getState()).state
+          .latestConfigDigest
+        const report = await makeLatestBlockReport([upkeepId])
+        const reportContext = [configDigest, epochAndRound5_1, emptyBytes32] // wrong config digest
+        const sigs = signReport(reportContext, report, signers.slice(0, f + 2))
+        await evmRevertCustomError(
+          registry
+            .connect(keeper1)
+            .transmit(
+              [reportContext[0], reportContext[1], reportContext[2]],
+              report,
+              sigs.rs,
+              sigs.ss,
+              sigs.vs,
+            ),
+          registry,
+          'IncorrectNumberOfSignatures',
+        )
+      })
+
+      it('reverts with invalid signature for inactive signers', async () => {
+        const configDigest = (await registry.getState()).state
+          .latestConfigDigest
+        const report = await makeLatestBlockReport([upkeepId])
+        const reportContext = [configDigest, epochAndRound5_1, emptyBytes32] // wrong config digest
+        const sigs = signReport(reportContext, report, [
+          new ethers.Wallet(ethers.Wallet.createRandom()),
+          new ethers.Wallet(ethers.Wallet.createRandom()),
+        ])
+        await evmRevertCustomError(
+          registry
+            .connect(keeper1)
+            .transmit(
+              [reportContext[0], reportContext[1], reportContext[2]],
+              report,
+              sigs.rs,
+              sigs.ss,
+              sigs.vs,
+            ),
+          registry,
+          'OnlyActiveSigners',
+        )
+      })
+
+      it('reverts with invalid signature for duplicated signers', async () => {
+        const configDigest = (await registry.getState()).state
+          .latestConfigDigest
+        const report = await makeLatestBlockReport([upkeepId])
+        const reportContext = [configDigest, epochAndRound5_1, emptyBytes32] // wrong config digest
+        const sigs = signReport(reportContext, report, [signer1, signer1])
+        await evmRevertCustomError(
+          registry
+            .connect(keeper1)
+            .transmit(
+              [reportContext[0], reportContext[1], reportContext[2]],
+              report,
+              sigs.rs,
+              sigs.ss,
+              sigs.vs,
+            ),
+          registry,
+          'DuplicateSigners',
+        )
+      })
+
+      itMaybe(
+        'has a large enough gas overhead to cover upkeep that use all its gas [ @skip-coverage ]',
+        async () => {
+          await registry.connect(owner).setConfigTypeSafe(
+            signerAddresses,
+            keeperAddresses,
+            10, // maximise f to maximise overhead
+            config,
+            offchainVersion,
+            offchainBytes,
+            baseConfig[6],
+            baseConfig[7],
+          )
+          const tx = await registry.connect(owner).registerUpkeep(
+            mock.address,
+            maxPerformGas, // max allowed gas
+            await admin.getAddress(),
+            Trigger.CONDITION,
+            linkToken.address,
+            '0x',
+            '0x',
+            '0x',
+          )
+          const testUpkeepId = await getUpkeepID(tx)
+          await registry.connect(admin).addFunds(testUpkeepId, toWei('100'))
+
+          let performData = '0x'
+          for (let i = 0; i < maxPerformDataSize.toNumber(); i++) {
+            performData += '11'
+          } // max allowed performData
+
+          await mock.setCanPerform(true)
+          await mock.setPerformGasToBurn(maxPerformGas)
+
+          await getTransmitTx(registry, keeper1, [testUpkeepId], {
+            gasLimit: maxPerformGas.add(transmitGasOverhead),
+            numSigners: 11,
+            performDatas: [performData],
+          }) // Should not revert
+        },
+      )
+
+      itMaybe(
+        'performs upkeep, deducts payment, updates lastPerformed and emits events',
+        async () => {
+          await mock.setCanPerform(true)
+
+          for (const i in fArray) {
+            const newF = fArray[i]
+            await registry
+              .connect(owner)
+              .setConfigTypeSafe(
+                signerAddresses,
+                keeperAddresses,
+                newF,
+                config,
+                offchainVersion,
+                offchainBytes,
+                baseConfig[6],
+                baseConfig[7],
+              )
+            const checkBlock = await ethers.provider.getBlock('latest')
+
+            const keeperBefore = await registry.getTransmitterInfo(
+              await keeper1.getAddress(),
+            )
+            const registrationBefore = await registry.getUpkeep(upkeepId)
+            const registryPremiumBefore = (await registry.getState()).state
+              .totalPremium
+            const keeperLinkBefore = await linkToken.balanceOf(
+              await keeper1.getAddress(),
+            )
+            const registryLinkBefore = await linkToken.balanceOf(
+              registry.address,
+            )
+
+            // Do the thing
+            const tx = await getTransmitTx(registry, keeper1, [upkeepId], {
+              checkBlockNum: checkBlock.number,
+              checkBlockHash: checkBlock.hash,
+              numSigners: newF + 1,
+            })
+
+            const receipt = await tx.wait()
+
+            const upkeepPerformedLogs = parseUpkeepPerformedLogs(receipt)
+            // exactly 1 Upkeep Performed should be emitted
+            assert.equal(upkeepPerformedLogs.length, 1)
+            const upkeepPerformedLog = upkeepPerformedLogs[0]
+
+            const id = upkeepPerformedLog.args.id
+            const success = upkeepPerformedLog.args.success
+            const trigger = upkeepPerformedLog.args.trigger
+            const gasUsed = upkeepPerformedLog.args.gasUsed
+            const gasOverhead = upkeepPerformedLog.args.gasOverhead
+            const totalPayment = upkeepPerformedLog.args.totalPayment
+            assert.equal(id.toString(), upkeepId.toString())
+            assert.equal(success, true)
+            assert.equal(
+              trigger,
+              encodeBlockTrigger({
+                blockNum: checkBlock.number,
+                blockHash: checkBlock.hash,
+              }),
+            )
+            assert.isTrue(gasUsed.gt(BigNumber.from('0')))
+            assert.isTrue(gasOverhead.gt(BigNumber.from('0')))
+            assert.isTrue(totalPayment.gt(BigNumber.from('0')))
+
+            const keeperAfter = await registry.getTransmitterInfo(
+              await keeper1.getAddress(),
+            )
+            const registrationAfter = await registry.getUpkeep(upkeepId)
+            const keeperLinkAfter = await linkToken.balanceOf(
+              await keeper1.getAddress(),
+            )
+            const registryLinkAfter = await linkToken.balanceOf(
+              registry.address,
+            )
+            const registryPremiumAfter = (await registry.getState()).state
+              .totalPremium
+            const premium = registryPremiumAfter.sub(registryPremiumBefore)
+            // Keeper payment is gasPayment + premium / num keepers
+            const keeperPayment = totalPayment
+              .sub(premium)
+              .add(premium.div(BigNumber.from(keeperAddresses.length)))
+
+            assert.equal(
+              keeperAfter.balance.sub(keeperPayment).toString(),
+              keeperBefore.balance.toString(),
+            )
+            assert.equal(
+              registrationBefore.balance.sub(totalPayment).toString(),
+              registrationAfter.balance.toString(),
+            )
+            assert.isTrue(keeperLinkAfter.eq(keeperLinkBefore))
+            assert.isTrue(registryLinkBefore.eq(registryLinkAfter))
+
+            // Amount spent should be updated correctly
+            assert.equal(
+              registrationAfter.amountSpent.sub(totalPayment).toString(),
+              registrationBefore.amountSpent.toString(),
+            )
+            assert.isTrue(
+              registrationAfter.amountSpent
+                .sub(registrationBefore.amountSpent)
+                .eq(registrationBefore.balance.sub(registrationAfter.balance)),
+            )
+            // Last perform block number should be updated
+            assert.equal(
+              registrationAfter.lastPerformedBlockNumber.toString(),
+              tx.blockNumber?.toString(),
+            )
+
+            // Latest epoch should be 5
+            assert.equal((await registry.getState()).state.latestEpoch, 5)
+          }
+        },
+      )
+
+      // describe.only('Gas benchmarking conditional upkeeps [ @skip-coverage ]', function () {
+      //   const fs = [1]
+      //   fs.forEach(function (newF) {
+      //     it(
+      //       'When f=' +
+      //         newF +
+      //         ' calculates gas overhead appropriately within a margin for different scenarios',
+      //       async () => {
+      //         // Perform the upkeep once to remove non-zero storage slots and have predictable gas measurement
+      //         let tx = await getTransmitTx(registry, keeper1, [upkeepId])
+      //         await tx.wait()
+      //
+      //         await registry
+      //           .connect(admin)
+      //           .setUpkeepGasLimit(upkeepId, performGas.mul(3))
+      //
+      //         // Different test scenarios
+      //         let longBytes = '0x'
+      //         for (let i = 0; i < maxPerformDataSize.toNumber(); i++) {
+      //           longBytes += '11'
+      //         }
+      //         const upkeepSuccessArray = [true, false]
+      //         const performGasArray = [5000, performGas]
+      //         const performDataArray = ['0x', longBytes]
+      //         const chainModuleOverheads = await moduleBase.getGasOverhead()
+      //
+      //         for (const i in upkeepSuccessArray) {
+      //           for (const j in performGasArray) {
+      //             for (const k in performDataArray) {
+      //               const upkeepSuccess = upkeepSuccessArray[i]
+      //               const performGas = performGasArray[j]
+      //               const performData = performDataArray[k]
+      //
+      //               await mock.setCanPerform(upkeepSuccess)
+      //               await mock.setPerformGasToBurn(performGas)
+      //               await registry
+      //                 .connect(owner)
+      //                 .setConfigTypeSafe(
+      //                   signerAddresses,
+      //                   keeperAddresses,
+      //                   newF,
+      //                   config,
+      //                   offchainVersion,
+      //                   offchainBytes,
+      //                   baseConfig[6],
+      //                   baseConfig[7],
+      //                 )
+      //               tx = await getTransmitTx(registry, keeper1, [upkeepId], {
+      //                 numSigners: newF + 1,
+      //                 performDatas: [performData],
+      //               })
+      //               const receipt = await tx.wait()
+      //               const upkeepPerformedLogs =
+      //                 parseUpkeepPerformedLogs(receipt)
+      //               // exactly 1 Upkeep Performed should be emitted
+      //               assert.equal(upkeepPerformedLogs.length, 1)
+      //               const upkeepPerformedLog = upkeepPerformedLogs[0]
+      //
+      //               const upkeepGasUsed = upkeepPerformedLog.args.gasUsed
+      //               const chargedGasOverhead =
+      //                 upkeepPerformedLog.args.gasOverhead
+      //               const actualGasOverhead = receipt.gasUsed
+      //                 .sub(upkeepGasUsed)
+      //                 .add(500000) // the amount of pubdataGas used returned by mock gas bound caller
+      //               const estimatedGasOverhead = registryConditionalOverhead
+      //                 .add(
+      //                   registryPerSignerGasOverhead.mul(
+      //                     BigNumber.from(newF + 1),
+      //                   ),
+      //                 )
+      //                 .add(chainModuleOverheads.chainModuleFixedOverhead)
+      //                 .add(65_400)
+      //
+      //               assert.isTrue(upkeepGasUsed.gt(BigNumber.from('0')))
+      //               assert.isTrue(chargedGasOverhead.gt(BigNumber.from('0')))
+      //               assert.isTrue(actualGasOverhead.gt(BigNumber.from('0')))
+      //
+      //               console.log(
+      //                 'Gas Benchmarking conditional upkeeps:',
+      //                 'upkeepSuccess=',
+      //                 upkeepSuccess,
+      //                 'performGas=',
+      //                 performGas.toString(),
+      //                 'performData length=',
+      //                 performData.length / 2 - 1,
+      //                 'sig verification ( f =',
+      //                 newF,
+      //                 '): estimated overhead: ',
+      //                 estimatedGasOverhead.toString(), // 179800
+      //                 ' charged overhead: ',
+      //                 chargedGasOverhead.toString(), // 180560
+      //                 ' actual overhead: ',
+      //                 actualGasOverhead.toString(), // 632949
+      //                 ' calculation margin over gasUsed: ',
+      //                 chargedGasOverhead.sub(actualGasOverhead).toString(), // 18456
+      //                 ' estimation margin over gasUsed: ',
+      //                 estimatedGasOverhead.sub(actualGasOverhead).toString(), // -27744
+      //                 ' upkeepGasUsed: ',
+      //                 upkeepGasUsed, // 988620
+      //                 ' receipt.gasUsed: ',
+      //                 receipt.gasUsed, // 1121569
+      //               )
+      //
+      //               // The actual gas overhead should be less than charged gas overhead, but not by a lot
+      //               // The charged gas overhead is controlled by ACCOUNTING_FIXED_GAS_OVERHEAD and
+      //               // ACCOUNTING_PER_UPKEEP_GAS_OVERHEAD, and their correct values should be set to
+      //               // satisfy constraints in multiple places
+      //               assert.isTrue(
+      //                 chargedGasOverhead.gt(actualGasOverhead),
+      //                 'Gas overhead calculated is too low, increase account gas variables (ACCOUNTING_FIXED_GAS_OVERHEAD/ACCOUNTING_PER_UPKEEP_GAS_OVERHEAD) by at least ' +
+      //                   actualGasOverhead.sub(chargedGasOverhead).toString(),
+      //               )
+      //               assert.isTrue(
+      //                 chargedGasOverhead // 180560
+      //                   .sub(actualGasOverhead) // 132940
+      //                   .lt(gasCalculationMargin),
+      //                 'Gas overhead calculated is too high, decrease account gas variables (ACCOUNTING_FIXED_GAS_OVERHEAD/ACCOUNTING_PER_SIGNER_GAS_OVERHEAD)  by at least ' +
+      //                   chargedGasOverhead
+      //                     .sub(actualGasOverhead)
+      //                     .sub(gasCalculationMargin)
+      //                     .toString(),
+      //               )
+      //
+      //               // The estimated overhead during checkUpkeep should be close to the actual overhead in transaction
+      //               // It should be greater than the actual overhead but not by a lot
+      //               // The estimated overhead is controlled by variables
+      //               // REGISTRY_CONDITIONAL_OVERHEAD, REGISTRY_LOG_OVERHEAD, REGISTRY_PER_SIGNER_GAS_OVERHEAD
+      //               // REGISTRY_PER_PERFORM_BYTE_GAS_OVERHEAD
+      //               assert.isTrue(
+      //                 estimatedGasOverhead.gt(actualGasOverhead),
+      //                 'Gas overhead estimated in check upkeep is too low, increase estimation gas variables (REGISTRY_CONDITIONAL_OVERHEAD/REGISTRY_LOG_OVERHEAD/REGISTRY_PER_SIGNER_GAS_OVERHEAD/REGISTRY_PER_PERFORM_BYTE_GAS_OVERHEAD) by at least ' +
+      //                   estimatedGasOverhead.sub(chargedGasOverhead).toString(),
+      //               )
+      //               assert.isTrue(
+      //                 estimatedGasOverhead
+      //                   .sub(actualGasOverhead)
+      //                   .lt(gasEstimationMargin),
+      //                 'Gas overhead estimated is too high, decrease estimation gas variables (REGISTRY_CONDITIONAL_OVERHEAD/REGISTRY_LOG_OVERHEAD/REGISTRY_PER_SIGNER_GAS_OVERHEAD/REGISTRY_PER_PERFORM_BYTE_GAS_OVERHEAD)  by at least ' +
+      //                   estimatedGasOverhead
+      //                     .sub(actualGasOverhead)
+      //                     .sub(gasEstimationMargin)
+      //                     .toString(),
+      //               )
+      //             }
+      //           }
+      //         }
+      //       },
+      //     )
+      //   })
+      // })
+
+      // describe.only('Gas benchmarking log upkeeps [ @skip-coverage ]', function () {
+      //   const fs = [1]
+      //   fs.forEach(function (newF) {
+      //     it(
+      //       'When f=' +
+      //         newF +
+      //         ' calculates gas overhead appropriately within a margin',
+      //       async () => {
+      //         // Perform the upkeep once to remove non-zero storage slots and have predictable gas measurement
+      //         let tx = await getTransmitTx(registry, keeper1, [logUpkeepId])
+      //         await tx.wait()
+      //         const performData = '0x'
+      //         await mock.setCanPerform(true)
+      //         await mock.setPerformGasToBurn(performGas)
+      //         await registry.setConfigTypeSafe(
+      //           signerAddresses,
+      //           keeperAddresses,
+      //           newF,
+      //           config,
+      //           offchainVersion,
+      //           offchainBytes,
+      //           baseConfig[6],
+      //           baseConfig[7],
+      //         )
+      //         tx = await getTransmitTx(registry, keeper1, [logUpkeepId], {
+      //           numSigners: newF + 1,
+      //           performDatas: [performData],
+      //         })
+      //         const receipt = await tx.wait()
+      //         const upkeepPerformedLogs = parseUpkeepPerformedLogs(receipt)
+      //         // exactly 1 Upkeep Performed should be emitted
+      //         assert.equal(upkeepPerformedLogs.length, 1)
+      //         const upkeepPerformedLog = upkeepPerformedLogs[0]
+      //         const chainModuleOverheads = await moduleBase.getGasOverhead()
+      //
+      //         const upkeepGasUsed = upkeepPerformedLog.args.gasUsed
+      //         const chargedGasOverhead = upkeepPerformedLog.args.gasOverhead
+      //         const actualGasOverhead = receipt.gasUsed
+      //           .sub(upkeepGasUsed)
+      //           .add(500000) // the amount of pubdataGas used returned by mock gas bound caller
+      //         const estimatedGasOverhead = registryLogOverhead
+      //           .add(registryPerSignerGasOverhead.mul(BigNumber.from(newF + 1)))
+      //           .add(chainModuleOverheads.chainModuleFixedOverhead)
+      //           .add(65_400)
+      //
+      //         assert.isTrue(upkeepGasUsed.gt(BigNumber.from('0')))
+      //         assert.isTrue(chargedGasOverhead.gt(BigNumber.from('0')))
+      //         assert.isTrue(actualGasOverhead.gt(BigNumber.from('0')))
+      //
+      //         console.log(
+      //           'Gas Benchmarking log upkeeps:',
+      //           'upkeepSuccess=',
+      //           true,
+      //           'performGas=',
+      //           performGas.toString(),
+      //           'performData length=',
+      //           performData.length / 2 - 1,
+      //           'sig verification ( f =',
+      //           newF,
+      //           '): estimated overhead: ',
+      //           estimatedGasOverhead.toString(),
+      //           ' charged overhead: ',
+      //           chargedGasOverhead.toString(),
+      //           ' actual overhead: ',
+      //           actualGasOverhead.toString(),
+      //           ' calculation margin over gasUsed: ',
+      //           chargedGasOverhead.sub(actualGasOverhead).toString(),
+      //           ' estimation margin over gasUsed: ',
+      //           estimatedGasOverhead.sub(actualGasOverhead).toString(),
+      //           ' upkeepGasUsed: ',
+      //           upkeepGasUsed,
+      //           ' receipt.gasUsed: ',
+      //           receipt.gasUsed,
+      //         )
+      //
+      //         assert.isTrue(
+      //           chargedGasOverhead.gt(actualGasOverhead),
+      //           'Gas overhead calculated is too low, increase account gas variables (ACCOUNTING_FIXED_GAS_OVERHEAD/ACCOUNTING_PER_UPKEEP_GAS_OVERHEAD) by at least ' +
+      //             actualGasOverhead.sub(chargedGasOverhead).toString(),
+      //         )
+      //         assert.isTrue(
+      //           chargedGasOverhead
+      //             .sub(actualGasOverhead)
+      //             .lt(gasCalculationMargin),
+      //           'Gas overhead calculated is too high, decrease account gas variables (ACCOUNTING_FIXED_GAS_OVERHEAD/ACCOUNTING_PER_SIGNER_GAS_OVERHEAD)  by at least ' +
+      //             chargedGasOverhead
+      //               .sub(actualGasOverhead)
+      //               .sub(gasCalculationMargin)
+      //               .toString(),
+      //         )
+      //
+      //         assert.isTrue(
+      //           estimatedGasOverhead.gt(actualGasOverhead),
+      //           'Gas overhead estimated in check upkeep is too low, increase estimation gas variables (REGISTRY_CONDITIONAL_OVERHEAD/REGISTRY_LOG_OVERHEAD/REGISTRY_PER_SIGNER_GAS_OVERHEAD/REGISTRY_PER_PERFORM_BYTE_GAS_OVERHEAD) by at least ' +
+      //             estimatedGasOverhead.sub(chargedGasOverhead).toString(),
+      //         )
+      //         assert.isTrue(
+      //           estimatedGasOverhead
+      //             .sub(actualGasOverhead)
+      //             .lt(gasEstimationMargin),
+      //           'Gas overhead estimated is too high, decrease estimation gas variables (REGISTRY_CONDITIONAL_OVERHEAD/REGISTRY_LOG_OVERHEAD/REGISTRY_PER_SIGNER_GAS_OVERHEAD/REGISTRY_PER_PERFORM_BYTE_GAS_OVERHEAD)  by at least ' +
+      //             estimatedGasOverhead
+      //               .sub(actualGasOverhead)
+      //               .sub(gasEstimationMargin)
+      //               .toString(),
+      //         )
+      //       },
+      //     )
+      //   })
+      // })
+    })
+  })
+
+  describeMaybe(
+    '#transmit with upkeep batches [ @skip-coverage ]',
+    function () {
+      const numPassingConditionalUpkeepsArray = [0, 1, 5]
+      const numPassingLogUpkeepsArray = [0, 1, 5]
+      const numFailingUpkeepsArray = [0, 3]
+
+      for (let idx = 0; idx < numPassingConditionalUpkeepsArray.length; idx++) {
+        for (let jdx = 0; jdx < numPassingLogUpkeepsArray.length; jdx++) {
+          for (let kdx = 0; kdx < numFailingUpkeepsArray.length; kdx++) {
+            const numPassingConditionalUpkeeps =
+              numPassingConditionalUpkeepsArray[idx]
+            const numPassingLogUpkeeps = numPassingLogUpkeepsArray[jdx]
+            const numFailingUpkeeps = numFailingUpkeepsArray[kdx]
+            if (
+              numPassingConditionalUpkeeps == 0 &&
+              numPassingLogUpkeeps == 0
+            ) {
+              continue
+            }
+            it(
+              '[Conditional:' +
+                numPassingConditionalUpkeeps +
+                ',Log:' +
+                numPassingLogUpkeeps +
+                ',Failures:' +
+                numFailingUpkeeps +
+                '] performs successful upkeeps and does not charge failing upkeeps',
+              async () => {
+                const allUpkeeps = await getMultipleUpkeepsDeployedAndFunded(
+                  numPassingConditionalUpkeeps,
+                  numPassingLogUpkeeps,
+                  numFailingUpkeeps,
+                )
+                const passingConditionalUpkeepIds =
+                  allUpkeeps.passingConditionalUpkeepIds
+                const passingLogUpkeepIds = allUpkeeps.passingLogUpkeepIds
+                const failingUpkeepIds = allUpkeeps.failingUpkeepIds
+
+                const keeperBefore = await registry.getTransmitterInfo(
+                  await keeper1.getAddress(),
+                )
+                const keeperLinkBefore = await linkToken.balanceOf(
+                  await keeper1.getAddress(),
+                )
+                const registryLinkBefore = await linkToken.balanceOf(
+                  registry.address,
+                )
+                const registryPremiumBefore = (await registry.getState()).state
+                  .totalPremium
+                const registrationConditionalPassingBefore = await Promise.all(
+                  passingConditionalUpkeepIds.map(async (id) => {
+                    const reg = await registry.getUpkeep(BigNumber.from(id))
+                    assert.equal(reg.lastPerformedBlockNumber.toString(), '0')
+                    return reg
+                  }),
+                )
+                const registrationLogPassingBefore = await Promise.all(
+                  passingLogUpkeepIds.map(async (id) => {
+                    const reg = await registry.getUpkeep(BigNumber.from(id))
+                    assert.equal(reg.lastPerformedBlockNumber.toString(), '0')
+                    return reg
+                  }),
+                )
+                const registrationFailingBefore = await Promise.all(
+                  failingUpkeepIds.map(async (id) => {
+                    const reg = await registry.getUpkeep(BigNumber.from(id))
+                    assert.equal(reg.lastPerformedBlockNumber.toString(), '0')
+                    return reg
+                  }),
+                )
+
+                // cancel upkeeps so they will fail in the transmit process
+                // must call the cancel upkeep as the owner to avoid the CANCELLATION_DELAY
+                for (let ldx = 0; ldx < failingUpkeepIds.length; ldx++) {
+                  await registry
+                    .connect(owner)
+                    .cancelUpkeep(failingUpkeepIds[ldx])
+                }
+
+                const tx = await getTransmitTx(
+                  registry,
+                  keeper1,
+                  passingConditionalUpkeepIds.concat(
+                    passingLogUpkeepIds.concat(failingUpkeepIds),
+                  ),
+                )
+
+                const receipt = await tx.wait()
+                const upkeepPerformedLogs = parseUpkeepPerformedLogs(receipt)
+                // exactly numPassingUpkeeps Upkeep Performed should be emitted
+                assert.equal(
+                  upkeepPerformedLogs.length,
+                  numPassingConditionalUpkeeps + numPassingLogUpkeeps,
+                )
+                const cancelledUpkeepReportLogs =
+                  parseCancelledUpkeepReportLogs(receipt)
+                // exactly numFailingUpkeeps Upkeep Performed should be emitted
+                assert.equal(
+                  cancelledUpkeepReportLogs.length,
+                  numFailingUpkeeps,
+                )
+
+                const keeperAfter = await registry.getTransmitterInfo(
+                  await keeper1.getAddress(),
+                )
+                const keeperLinkAfter = await linkToken.balanceOf(
+                  await keeper1.getAddress(),
+                )
+                const registryLinkAfter = await linkToken.balanceOf(
+                  registry.address,
+                )
+                const registrationConditionalPassingAfter = await Promise.all(
+                  passingConditionalUpkeepIds.map(async (id) => {
+                    return await registry.getUpkeep(BigNumber.from(id))
+                  }),
+                )
+                const registrationLogPassingAfter = await Promise.all(
+                  passingLogUpkeepIds.map(async (id) => {
+                    return await registry.getUpkeep(BigNumber.from(id))
+                  }),
+                )
+                const registrationFailingAfter = await Promise.all(
+                  failingUpkeepIds.map(async (id) => {
+                    return await registry.getUpkeep(BigNumber.from(id))
+                  }),
+                )
+                const registryPremiumAfter = (await registry.getState()).state
+                  .totalPremium
+                const premium = registryPremiumAfter.sub(registryPremiumBefore)
+
+                let netPayment = BigNumber.from('0')
+                for (let i = 0; i < numPassingConditionalUpkeeps; i++) {
+                  const id = upkeepPerformedLogs[i].args.id
+                  const gasUsed = upkeepPerformedLogs[i].args.gasUsed
+                  const gasOverhead = upkeepPerformedLogs[i].args.gasOverhead
+                  const totalPayment = upkeepPerformedLogs[i].args.totalPayment
+
+                  expect(id).to.equal(passingConditionalUpkeepIds[i])
+                  assert.isTrue(gasUsed.gt(BigNumber.from('0')))
+                  assert.isTrue(gasOverhead.gt(BigNumber.from('0')))
+                  assert.isTrue(totalPayment.gt(BigNumber.from('0')))
+
+                  // Balance should be deducted
+                  assert.equal(
+                    registrationConditionalPassingBefore[i].balance
+                      .sub(totalPayment)
+                      .toString(),
+                    registrationConditionalPassingAfter[i].balance.toString(),
+                  )
+
+                  // Amount spent should be updated correctly
+                  assert.equal(
+                    registrationConditionalPassingAfter[i].amountSpent
+                      .sub(totalPayment)
+                      .toString(),
+                    registrationConditionalPassingBefore[
+                      i
+                    ].amountSpent.toString(),
+                  )
+
+                  // Last perform block number should be updated
+                  assert.equal(
+                    registrationConditionalPassingAfter[
+                      i
+                    ].lastPerformedBlockNumber.toString(),
+                    tx.blockNumber?.toString(),
+                  )
+
+                  netPayment = netPayment.add(totalPayment)
+                }
+
+                for (let i = 0; i < numPassingLogUpkeeps; i++) {
+                  const id =
+                    upkeepPerformedLogs[numPassingConditionalUpkeeps + i].args
+                      .id
+                  const gasUsed =
+                    upkeepPerformedLogs[numPassingConditionalUpkeeps + i].args
+                      .gasUsed
+                  const gasOverhead =
+                    upkeepPerformedLogs[numPassingConditionalUpkeeps + i].args
+                      .gasOverhead
+                  const totalPayment =
+                    upkeepPerformedLogs[numPassingConditionalUpkeeps + i].args
+                      .totalPayment
+
+                  expect(id).to.equal(passingLogUpkeepIds[i])
+                  assert.isTrue(gasUsed.gt(BigNumber.from('0')))
+                  assert.isTrue(gasOverhead.gt(BigNumber.from('0')))
+                  assert.isTrue(totalPayment.gt(BigNumber.from('0')))
+
+                  // Balance should be deducted
+                  assert.equal(
+                    registrationLogPassingBefore[i].balance
+                      .sub(totalPayment)
+                      .toString(),
+                    registrationLogPassingAfter[i].balance.toString(),
+                  )
+
+                  // Amount spent should be updated correctly
+                  assert.equal(
+                    registrationLogPassingAfter[i].amountSpent
+                      .sub(totalPayment)
+                      .toString(),
+                    registrationLogPassingBefore[i].amountSpent.toString(),
+                  )
+
+                  // Last perform block number should not be updated for log triggers
+                  assert.equal(
+                    registrationLogPassingAfter[
+                      i
+                    ].lastPerformedBlockNumber.toString(),
+                    '0',
+                  )
+
+                  netPayment = netPayment.add(totalPayment)
+                }
+
+                for (let i = 0; i < numFailingUpkeeps; i++) {
+                  // CancelledUpkeep log should be emitted
+                  const id = cancelledUpkeepReportLogs[i].args.id
+                  expect(id).to.equal(failingUpkeepIds[i])
+
+                  // Balance and amount spent should be same
+                  assert.equal(
+                    registrationFailingBefore[i].balance.toString(),
+                    registrationFailingAfter[i].balance.toString(),
+                  )
+                  assert.equal(
+                    registrationFailingBefore[i].amountSpent.toString(),
+                    registrationFailingAfter[i].amountSpent.toString(),
+                  )
+
+                  // Last perform block number should not be updated
+                  assert.equal(
+                    registrationFailingAfter[
+                      i
+                    ].lastPerformedBlockNumber.toString(),
+                    '0',
+                  )
+                }
+
+                // Keeper payment is gasPayment + premium / num keepers
+                const keeperPayment = netPayment
+                  .sub(premium)
+                  .add(premium.div(BigNumber.from(keeperAddresses.length)))
+
+                // Keeper should be paid net payment for all passed upkeeps
+                assert.equal(
+                  keeperAfter.balance.sub(keeperPayment).toString(),
+                  keeperBefore.balance.toString(),
+                )
+
+                assert.isTrue(keeperLinkAfter.eq(keeperLinkBefore))
+                assert.isTrue(registryLinkBefore.eq(registryLinkAfter))
+              },
+            )
+
+            it(
+              '[Conditional:' +
+                numPassingConditionalUpkeeps +
+                ',Log' +
+                numPassingLogUpkeeps +
+                ',Failures:' +
+                numFailingUpkeeps +
+                '] splits gas overhead appropriately among performed upkeeps [ @skip-coverage ]',
+              async () => {
+                const allUpkeeps = await getMultipleUpkeepsDeployedAndFunded(
+                  numPassingConditionalUpkeeps,
+                  numPassingLogUpkeeps,
+                  numFailingUpkeeps,
+                )
+                const passingConditionalUpkeepIds =
+                  allUpkeeps.passingConditionalUpkeepIds
+                const passingLogUpkeepIds = allUpkeeps.passingLogUpkeepIds
+                const failingUpkeepIds = allUpkeeps.failingUpkeepIds
+
+                // Perform the upkeeps once to remove non-zero storage slots and have predictable gas measurement
+                let tx = await getTransmitTx(
+                  registry,
+                  keeper1,
+                  passingConditionalUpkeepIds.concat(
+                    passingLogUpkeepIds.concat(failingUpkeepIds),
+                  ),
+                )
+
+                await tx.wait()
+
+                // cancel upkeeps so they will fail in the transmit process
+                // must call the cancel upkeep as the owner to avoid the CANCELLATION_DELAY
+                for (let ldx = 0; ldx < failingUpkeepIds.length; ldx++) {
+                  await registry
+                    .connect(owner)
+                    .cancelUpkeep(failingUpkeepIds[ldx])
+                }
+
+                // Do the actual thing
+
+                tx = await getTransmitTx(
+                  registry,
+                  keeper1,
+                  passingConditionalUpkeepIds.concat(
+                    passingLogUpkeepIds.concat(failingUpkeepIds),
+                  ),
+                )
+
+                const receipt = await tx.wait()
+                const upkeepPerformedLogs = parseUpkeepPerformedLogs(receipt)
+                // exactly numPassingUpkeeps Upkeep Performed should be emitted
+                assert.equal(
+                  upkeepPerformedLogs.length,
+                  numPassingConditionalUpkeeps + numPassingLogUpkeeps,
+                )
+
+                let netGasUsedPlusChargedOverhead = BigNumber.from('0')
+                for (let i = 0; i < numPassingConditionalUpkeeps; i++) {
+                  const gasUsed = upkeepPerformedLogs[i].args.gasUsed
+                  const chargedGasOverhead =
+                    upkeepPerformedLogs[i].args.gasOverhead
+
+                  assert.isTrue(gasUsed.gt(BigNumber.from('0')))
+                  assert.isTrue(chargedGasOverhead.gt(BigNumber.from('0')))
+
+                  // Overhead should be same for every upkeep
+                  assert.isTrue(
+                    chargedGasOverhead.eq(
+                      upkeepPerformedLogs[0].args.gasOverhead,
+                    ),
+                  )
+                  netGasUsedPlusChargedOverhead = netGasUsedPlusChargedOverhead
+                    .add(gasUsed)
+                    .add(chargedGasOverhead)
+                }
+
+                for (let i = 0; i < numPassingLogUpkeeps; i++) {
+                  const gasUsed =
+                    upkeepPerformedLogs[numPassingConditionalUpkeeps + i].args
+                      .gasUsed
+                  const chargedGasOverhead =
+                    upkeepPerformedLogs[numPassingConditionalUpkeeps + i].args
+                      .gasOverhead
+
+                  assert.isTrue(gasUsed.gt(BigNumber.from('0')))
+                  assert.isTrue(chargedGasOverhead.gt(BigNumber.from('0')))
+
+                  // Overhead should be same for every upkeep
+                  assert.isTrue(
+                    chargedGasOverhead.eq(
+                      upkeepPerformedLogs[numPassingConditionalUpkeeps].args
+                        .gasOverhead,
+                    ),
+                  )
+                  netGasUsedPlusChargedOverhead = netGasUsedPlusChargedOverhead
+                    .add(gasUsed)
+                    .add(chargedGasOverhead)
+                }
+
+                console.log(
+                  'Gas Benchmarking - batching (passedConditionalUpkeeps: ',
+                  numPassingConditionalUpkeeps,
+                  'passedLogUpkeeps:',
+                  numPassingLogUpkeeps,
+                  'failedUpkeeps:',
+                  numFailingUpkeeps,
+                  '): ',
+                  numPassingConditionalUpkeeps > 0
+                    ? 'charged conditional overhead'
+                    : '',
+                  numPassingConditionalUpkeeps > 0
+                    ? upkeepPerformedLogs[0].args.gasOverhead.toString()
+                    : '',
+                  numPassingLogUpkeeps > 0 ? 'charged log overhead' : '',
+                  numPassingLogUpkeeps > 0
+                    ? upkeepPerformedLogs[
+                        numPassingConditionalUpkeeps
+                      ].args.gasOverhead.toString()
+                    : '',
+                  ' margin over gasUsed',
+                  netGasUsedPlusChargedOverhead.sub(receipt.gasUsed).toString(),
+                )
+
+                // The total gas charged should be greater than tx gas
+                assert.isTrue(
+                  netGasUsedPlusChargedOverhead.gt(receipt.gasUsed),
+                  'Charged gas overhead is too low for batch upkeeps, increase ACCOUNTING_PER_UPKEEP_GAS_OVERHEAD',
+                )
+              },
+            )
+          }
+        }
+      }
+
+      it('has enough perform gas overhead for large batches [ @skip-coverage ]', async () => {
+        const numUpkeeps = 20
+        const upkeepIds: BigNumber[] = []
+        let totalPerformGas = BigNumber.from('0')
+        for (let i = 0; i < numUpkeeps; i++) {
+          const mock = await upkeepMockFactory.deploy()
+          const tx = await registry
+            .connect(owner)
+            .registerUpkeep(
+              mock.address,
+              performGas,
+              await admin.getAddress(),
+              Trigger.CONDITION,
+              linkToken.address,
+              '0x',
+              '0x',
+              '0x',
+            )
+          const testUpkeepId = await getUpkeepID(tx)
+          upkeepIds.push(testUpkeepId)
+
+          // Add funds to passing upkeeps
+          await registry.connect(owner).addFunds(testUpkeepId, toWei('10'))
+
+          await mock.setCanPerform(true)
+          await mock.setPerformGasToBurn(performGas)
+
+          totalPerformGas = totalPerformGas.add(performGas)
+        }
+
+        // Should revert with no overhead added
+        await evmRevert(
+          getTransmitTx(registry, keeper1, upkeepIds, {
+            gasLimit: totalPerformGas,
+          }),
+        )
+        // Should not revert with overhead added
+        await getTransmitTx(registry, keeper1, upkeepIds, {
+          gasLimit: totalPerformGas.add(transmitGasOverhead),
+        })
+      })
+    },
+  )
+
+  describe('#recoverFunds', () => {
+    const sent = toWei('7')
+
+    beforeEach(async () => {
+      await linkToken.connect(admin).approve(registry.address, toWei('100'))
+      await linkToken
+        .connect(owner)
+        .transfer(await keeper1.getAddress(), toWei('1000'))
+
+      // add funds to upkeep 1 and perform and withdraw some payment
+      const tx = await registry
+        .connect(owner)
+        .registerUpkeep(
+          mock.address,
+          performGas,
+          await admin.getAddress(),
+          Trigger.CONDITION,
+          linkToken.address,
+          '0x',
+          '0x',
+          '0x',
+        )
+
+      const id1 = await getUpkeepID(tx)
+      await registry.connect(admin).addFunds(id1, toWei('5'))
+
+      await getTransmitTx(registry, keeper1, [id1])
+      await getTransmitTx(registry, keeper2, [id1])
+      await getTransmitTx(registry, keeper3, [id1])
+
+      await registry
+        .connect(payee1)
+        .withdrawPayment(
+          await keeper1.getAddress(),
+          await nonkeeper.getAddress(),
+        )
+
+      // transfer funds directly to the registry
+      await linkToken.connect(keeper1).transfer(registry.address, sent)
+
+      // add funds to upkeep 2 and perform and withdraw some payment
+      const tx2 = await registry
+        .connect(owner)
+        .registerUpkeep(
+          mock.address,
+          performGas,
+          await admin.getAddress(),
+          Trigger.CONDITION,
+          linkToken.address,
+          '0x',
+          '0x',
+          '0x',
+        )
+      const id2 = await getUpkeepID(tx2)
+      await registry.connect(admin).addFunds(id2, toWei('5'))
+
+      await getTransmitTx(registry, keeper1, [id2])
+      await getTransmitTx(registry, keeper2, [id2])
+      await getTransmitTx(registry, keeper3, [id2])
+
+      await registry
+        .connect(payee2)
+        .withdrawPayment(
+          await keeper2.getAddress(),
+          await nonkeeper.getAddress(),
+        )
+
+      // transfer funds using onTokenTransfer
+      const data = ethers.utils.defaultAbiCoder.encode(['uint256'], [id2])
+      await linkToken
+        .connect(owner)
+        .transferAndCall(registry.address, toWei('1'), data)
+
+      // withdraw some funds
+      await registry.connect(owner).cancelUpkeep(id1)
+      await registry
+        .connect(admin)
+        .withdrawFunds(id1, await nonkeeper.getAddress())
+    })
+  })
+
+  describe('#getMinBalanceForUpkeep / #checkUpkeep / #transmit', () => {
+    it('calculates the minimum balance appropriately', async () => {
+      await mock.setCanCheck(true)
+
+      const oneWei = BigNumber.from(1)
+      const minBalance = await registry.getMinBalanceForUpkeep(upkeepId)
+      const tooLow = minBalance.sub(oneWei)
+
+      await registry.connect(admin).addFunds(upkeepId, tooLow)
+      let checkUpkeepResult = await registry
+        .connect(zeroAddress)
+        .callStatic['checkUpkeep(uint256)'](upkeepId)
+
+      assert.equal(checkUpkeepResult.upkeepNeeded, false)
+      assert.equal(
+        checkUpkeepResult.upkeepFailureReason,
+        UpkeepFailureReason.INSUFFICIENT_BALANCE,
+      )
+
+      await registry.connect(admin).addFunds(upkeepId, oneWei)
+      checkUpkeepResult = await registry
+        .connect(zeroAddress)
+        .callStatic['checkUpkeep(uint256)'](upkeepId)
+      assert.equal(checkUpkeepResult.upkeepNeeded, true)
+    })
+
+    it('uses maxPerformData size in checkUpkeep but actual performDataSize in transmit', async () => {
+      const tx = await registry
+        .connect(owner)
+        .registerUpkeep(
+          mock.address,
+          performGas,
+          await admin.getAddress(),
+          Trigger.CONDITION,
+          linkToken.address,
+          '0x',
+          '0x',
+          '0x',
+        )
+      const upkeepID = await getUpkeepID(tx)
+      await mock.setCanCheck(true)
+      await mock.setCanPerform(true)
+
+      // upkeep is underfunded by 1 wei
+      const minBalance1 = (await registry.getMinBalanceForUpkeep(upkeepID)).sub(
+        1,
+      )
+      await registry.connect(owner).addFunds(upkeepID, minBalance1)
+
+      // upkeep check should return false, 2 should return true
+      const checkUpkeepResult = await registry
+        .connect(zeroAddress)
+        .callStatic['checkUpkeep(uint256)'](upkeepID)
+      assert.equal(checkUpkeepResult.upkeepNeeded, false)
+      assert.equal(
+        checkUpkeepResult.upkeepFailureReason,
+        UpkeepFailureReason.INSUFFICIENT_BALANCE,
+      )
+
+      // however upkeep should perform and pay all the remaining balance
+      let maxPerformData = '0x'
+      for (let i = 0; i < maxPerformDataSize.toNumber(); i++) {
+        maxPerformData += '11'
+      }
+
+      const tx2 = await getTransmitTx(registry, keeper1, [upkeepID], {
+        gasPrice: gasWei.mul(gasCeilingMultiplier),
+        performDatas: [maxPerformData],
+      })
+
+      const receipt = await tx2.wait()
+      const upkeepPerformedLogs = parseUpkeepPerformedLogs(receipt)
+      assert.equal(upkeepPerformedLogs.length, 1)
+    })
+  })
+
+  describe('#withdrawFunds', () => {
+    let upkeepId2: BigNumber
+
+    beforeEach(async () => {
+      const tx = await registry
+        .connect(owner)
+        .registerUpkeep(
+          mock.address,
+          performGas,
+          await admin.getAddress(),
+          Trigger.CONDITION,
+          linkToken.address,
+          '0x',
+          '0x',
+          '0x',
+        )
+      upkeepId2 = await getUpkeepID(tx)
+
+      await registry.connect(admin).addFunds(upkeepId, toWei('100'))
+      await registry.connect(admin).addFunds(upkeepId2, toWei('100'))
+
+      // Do a perform so that upkeep is charged some amount
+      await getTransmitTx(registry, keeper1, [upkeepId])
+      await getTransmitTx(registry, keeper1, [upkeepId2])
+    })
+
+    describe('after the registration is paused, then cancelled', () => {
+      it('allows the admin to withdraw', async () => {
+        const balance = await registry.getBalance(upkeepId)
+        const payee = await payee1.getAddress()
+        await registry.connect(admin).pauseUpkeep(upkeepId)
+        await registry.connect(owner).cancelUpkeep(upkeepId)
+        await expect(() =>
+          registry.connect(admin).withdrawFunds(upkeepId, payee),
+        ).to.changeTokenBalance(linkToken, payee1, balance)
+      })
+    })
+
+    describe('after the registration is cancelled', () => {
+      beforeEach(async () => {
+        await registry.connect(owner).cancelUpkeep(upkeepId)
+        await registry.connect(owner).cancelUpkeep(upkeepId2)
+      })
+
+      it('can be called successively on two upkeeps', async () => {
+        await registry
+          .connect(admin)
+          .withdrawFunds(upkeepId, await payee1.getAddress())
+        await registry
+          .connect(admin)
+          .withdrawFunds(upkeepId2, await payee1.getAddress())
+      })
+
+      it('moves the funds out and updates the balance and emits an event', async () => {
+        const payee1Before = await linkToken.balanceOf(
+          await payee1.getAddress(),
+        )
+        const registryBefore = await linkToken.balanceOf(registry.address)
+
+        let registration = await registry.getUpkeep(upkeepId)
+        const previousBalance = registration.balance
+
+        const tx = await registry
+          .connect(admin)
+          .withdrawFunds(upkeepId, await payee1.getAddress())
+        await expect(tx)
+          .to.emit(registry, 'FundsWithdrawn')
+          .withArgs(upkeepId, previousBalance, await payee1.getAddress())
+
+        const payee1After = await linkToken.balanceOf(await payee1.getAddress())
+        const registryAfter = await linkToken.balanceOf(registry.address)
+
+        assert.isTrue(payee1Before.add(previousBalance).eq(payee1After))
+        assert.isTrue(registryBefore.sub(previousBalance).eq(registryAfter))
+
+        registration = await registry.getUpkeep(upkeepId)
+        assert.equal(registration.balance.toNumber(), 0)
+      })
+    })
+  })
+
+  describe('#simulatePerformUpkeep', () => {
+    it('reverts if called by non zero address', async () => {
+      await evmRevertCustomError(
+        registry
+          .connect(await owner.getAddress())
+          .callStatic.simulatePerformUpkeep(upkeepId, '0x'),
+        registry,
+        'OnlySimulatedBackend',
+      )
+    })
+
+    it('reverts when registry is paused', async () => {
+      await registry.connect(owner).pause()
+      await evmRevertCustomError(
+        registry
+          .connect(zeroAddress)
+          .callStatic.simulatePerformUpkeep(upkeepId, '0x'),
+        registry,
+        'RegistryPaused',
+      )
+    })
+
+    it('returns false and gasUsed when perform fails', async () => {
+      await mock.setCanPerform(false)
+
+      const simulatePerformResult = await registry
+        .connect(zeroAddress)
+        .callStatic.simulatePerformUpkeep(upkeepId, '0x')
+
+      assert.equal(simulatePerformResult.success, false)
+      assert.isTrue(simulatePerformResult.gasUsed.gt(BigNumber.from('0'))) // Some gas should be used
+    })
+
+    it('returns true, gasUsed, and performGas when perform succeeds', async () => {
+      await mock.setCanPerform(true)
+
+      const simulatePerformResult = await registry
+        .connect(zeroAddress)
+        .callStatic.simulatePerformUpkeep(upkeepId, '0x')
+
+      assert.equal(simulatePerformResult.success, true)
+      assert.isTrue(simulatePerformResult.gasUsed.gt(BigNumber.from('0'))) // Some gas should be used
+    })
+
+    it('returns correct amount of gasUsed when perform succeeds', async () => {
+      await mock.setCanPerform(true)
+      await mock.setPerformGasToBurn(performGas) // 1,000,000
+
+      // increase upkeep gas limit because the mock gas bound caller will always return 500,000 as the L1 gas used
+      // that brings the total gas used to about 1M + 0.5M = 1.5M
+      await registry
+        .connect(admin)
+        .setUpkeepGasLimit(upkeepId, BigNumber.from(2000000))
+
+      const simulatePerformResult = await registry
+        .connect(zeroAddress)
+        .callStatic.simulatePerformUpkeep(upkeepId, '0x')
+
+      // Full execute gas should be used, with some performGasBuffer(1000)
+      assert.isTrue(
+        simulatePerformResult.gasUsed.gt(
+          performGas.add(pubdataGas).sub(BigNumber.from('1000')),
+        ),
+      )
+    })
+  })
+
+  describe('#checkUpkeep', () => {
+    it('reverts if called by non zero address', async () => {
+      await evmRevertCustomError(
+        registry
+          .connect(await owner.getAddress())
+          .callStatic['checkUpkeep(uint256)'](upkeepId),
+        registry,
+        'OnlySimulatedBackend',
+      )
+    })
+
+    it('returns false and error code if the upkeep is cancelled by admin', async () => {
+      await registry.connect(admin).cancelUpkeep(upkeepId)
+
+      const checkUpkeepResult = await registry
+        .connect(zeroAddress)
+        .callStatic['checkUpkeep(uint256)'](upkeepId)
+
+      assert.equal(checkUpkeepResult.upkeepNeeded, false)
+      assert.equal(checkUpkeepResult.performData, '0x')
+      assert.equal(
+        checkUpkeepResult.upkeepFailureReason,
+        UpkeepFailureReason.UPKEEP_CANCELLED,
+      )
+      expect(checkUpkeepResult.gasUsed).to.equal(0)
+      expect(checkUpkeepResult.gasLimit).to.equal(performGas)
+    })
+
+    it('returns false and error code if the upkeep is cancelled by owner', async () => {
+      await registry.connect(owner).cancelUpkeep(upkeepId)
+
+      const checkUpkeepResult = await registry
+        .connect(zeroAddress)
+        .callStatic['checkUpkeep(uint256)'](upkeepId)
+
+      assert.equal(checkUpkeepResult.upkeepNeeded, false)
+      assert.equal(checkUpkeepResult.performData, '0x')
+      assert.equal(
+        checkUpkeepResult.upkeepFailureReason,
+        UpkeepFailureReason.UPKEEP_CANCELLED,
+      )
+      expect(checkUpkeepResult.gasUsed).to.equal(0)
+      expect(checkUpkeepResult.gasLimit).to.equal(performGas)
+    })
+
+    it('returns false and error code if the registry is paused', async () => {
+      await registry.connect(owner).pause()
+
+      const checkUpkeepResult = await registry
+        .connect(zeroAddress)
+        .callStatic['checkUpkeep(uint256)'](upkeepId)
+
+      assert.equal(checkUpkeepResult.upkeepNeeded, false)
+      assert.equal(checkUpkeepResult.performData, '0x')
+      assert.equal(
+        checkUpkeepResult.upkeepFailureReason,
+        UpkeepFailureReason.REGISTRY_PAUSED,
+      )
+      expect(checkUpkeepResult.gasUsed).to.equal(0)
+      expect(checkUpkeepResult.gasLimit).to.equal(performGas)
+    })
+
+    it('returns false and error code if the upkeep is paused', async () => {
+      await registry.connect(admin).pauseUpkeep(upkeepId)
+
+      const checkUpkeepResult = await registry
+        .connect(zeroAddress)
+        .callStatic['checkUpkeep(uint256)'](upkeepId)
+
+      assert.equal(checkUpkeepResult.upkeepNeeded, false)
+      assert.equal(checkUpkeepResult.performData, '0x')
+      assert.equal(
+        checkUpkeepResult.upkeepFailureReason,
+        UpkeepFailureReason.UPKEEP_PAUSED,
+      )
+      expect(checkUpkeepResult.gasUsed).to.equal(0)
+      expect(checkUpkeepResult.gasLimit).to.equal(performGas)
+    })
+
+    it('returns false and error code if user is out of funds', async () => {
+      const checkUpkeepResult = await registry
+        .connect(zeroAddress)
+        .callStatic['checkUpkeep(uint256)'](upkeepId)
+
+      assert.equal(checkUpkeepResult.upkeepNeeded, false)
+      assert.equal(checkUpkeepResult.performData, '0x')
+      assert.equal(
+        checkUpkeepResult.upkeepFailureReason,
+        UpkeepFailureReason.INSUFFICIENT_BALANCE,
+      )
+      expect(checkUpkeepResult.gasUsed).to.equal(0)
+      expect(checkUpkeepResult.gasLimit).to.equal(performGas)
+    })
+
+    context('when the registration is funded', () => {
+      beforeEach(async () => {
+        await linkToken.connect(admin).approve(registry.address, toWei('200'))
+        await registry.connect(admin).addFunds(upkeepId, toWei('100'))
+        await registry.connect(admin).addFunds(logUpkeepId, toWei('100'))
+      })
+
+      it('returns false, error code, and revert data if the target check reverts', async () => {
+        await mock.setShouldRevertCheck(true)
+        await mock.setCheckRevertReason(
+          'custom revert error, clever way to insert offchain data',
+        )
+        const checkUpkeepResult = await registry
+          .connect(zeroAddress)
+          .callStatic['checkUpkeep(uint256)'](upkeepId)
+        assert.equal(checkUpkeepResult.upkeepNeeded, false)
+
+        const revertReasonBytes = `0x${checkUpkeepResult.performData.slice(10)}` // remove sighash
+        assert.equal(
+          ethers.utils.defaultAbiCoder.decode(['string'], revertReasonBytes)[0],
+          'custom revert error, clever way to insert offchain data',
+        )
+        assert.equal(
+          checkUpkeepResult.upkeepFailureReason,
+          UpkeepFailureReason.TARGET_CHECK_REVERTED,
+        )
+        assert.isTrue(checkUpkeepResult.gasUsed.gt(BigNumber.from('0'))) // Some gas should be used
+        expect(checkUpkeepResult.gasLimit).to.equal(performGas)
+        // Feed data should be returned here
+        assert.isTrue(checkUpkeepResult.fastGasWei.gt(BigNumber.from('0')))
+        assert.isTrue(checkUpkeepResult.linkUSD.gt(BigNumber.from('0')))
+      })
+
+      it('returns false, error code, and no revert data if the target check revert data exceeds maxRevertDataSize', async () => {
+        await mock.setShouldRevertCheck(true)
+        let longRevertReason = ''
+        for (let i = 0; i <= maxRevertDataSize.toNumber(); i++) {
+          longRevertReason += 'x'
+        }
+        await mock.setCheckRevertReason(longRevertReason)
+        const checkUpkeepResult = await registry
+          .connect(zeroAddress)
+          .callStatic['checkUpkeep(uint256)'](upkeepId)
+        assert.equal(checkUpkeepResult.upkeepNeeded, false)
+
+        assert.equal(checkUpkeepResult.performData, '0x')
+        assert.equal(
+          checkUpkeepResult.upkeepFailureReason,
+          UpkeepFailureReason.REVERT_DATA_EXCEEDS_LIMIT,
+        )
+        assert.isTrue(checkUpkeepResult.gasUsed.gt(BigNumber.from('0'))) // Some gas should be used
+        expect(checkUpkeepResult.gasLimit).to.equal(performGas)
+      })
+
+      it('returns false and error code if the upkeep is not needed', async () => {
+        await mock.setCanCheck(false)
+        const checkUpkeepResult = await registry
+          .connect(zeroAddress)
+          .callStatic['checkUpkeep(uint256)'](upkeepId)
+
+        assert.equal(checkUpkeepResult.upkeepNeeded, false)
+        assert.equal(checkUpkeepResult.performData, '0x')
+        assert.equal(
+          checkUpkeepResult.upkeepFailureReason,
+          UpkeepFailureReason.UPKEEP_NOT_NEEDED,
+        )
+        assert.isTrue(checkUpkeepResult.gasUsed.gt(BigNumber.from('0'))) // Some gas should be used
+        expect(checkUpkeepResult.gasLimit).to.equal(performGas)
+      })
+
+      it('returns false and error code if the performData exceeds limit', async () => {
+        let longBytes = '0x'
+        for (let i = 0; i < 5000; i++) {
+          longBytes += '1'
+        }
+        await mock.setCanCheck(true)
+        await mock.setPerformData(longBytes)
+
+        const checkUpkeepResult = await registry
+          .connect(zeroAddress)
+          .callStatic['checkUpkeep(uint256)'](upkeepId)
+
+        assert.equal(checkUpkeepResult.upkeepNeeded, false)
+        assert.equal(checkUpkeepResult.performData, '0x')
+        assert.equal(
+          checkUpkeepResult.upkeepFailureReason,
+          UpkeepFailureReason.PERFORM_DATA_EXCEEDS_LIMIT,
+        )
+        assert.isTrue(checkUpkeepResult.gasUsed.gt(BigNumber.from('0'))) // Some gas should be used
+        expect(checkUpkeepResult.gasLimit).to.equal(performGas)
+      })
+
+      it('returns true with gas used if the target can execute', async () => {
+        await mock.setCanCheck(true)
+        await mock.setPerformData(randomBytes)
+
+        const latestBlock = await ethers.provider.getBlock('latest')
+
+        const checkUpkeepResult = await registry
+          .connect(zeroAddress)
+          .callStatic['checkUpkeep(uint256)'](upkeepId, {
+            blockTag: latestBlock.number,
+          })
+
+        assert.equal(checkUpkeepResult.upkeepNeeded, true)
+        assert.equal(checkUpkeepResult.performData, randomBytes)
+        assert.equal(
+          checkUpkeepResult.upkeepFailureReason,
+          UpkeepFailureReason.NONE,
+        )
+        assert.isTrue(checkUpkeepResult.gasUsed.gt(BigNumber.from('0'))) // Some gas should be used
+        expect(checkUpkeepResult.gasLimit).to.equal(performGas)
+        assert.isTrue(checkUpkeepResult.fastGasWei.eq(gasWei))
+        assert.isTrue(checkUpkeepResult.linkUSD.eq(linkUSD))
+      })
+
+      it('calls checkLog for log-trigger upkeeps', async () => {
+        const log: Log = {
+          index: 0,
+          timestamp: 0,
+          txHash: ethers.utils.randomBytes(32),
+          blockNumber: 100,
+          blockHash: ethers.utils.randomBytes(32),
+          source: randomAddress(),
+          topics: [ethers.utils.randomBytes(32), ethers.utils.randomBytes(32)],
+          data: ethers.utils.randomBytes(1000),
+        }
+
+        await ltUpkeep.mock.checkLog.withArgs(log, '0x').returns(true, '0x1234')
+
+        const checkData = encodeLog(log)
+
+        const checkUpkeepResult = await registry
+          .connect(zeroAddress)
+          .callStatic['checkUpkeep(uint256,bytes)'](logUpkeepId, checkData)
+
+        expect(checkUpkeepResult.upkeepNeeded).to.be.true
+        expect(checkUpkeepResult.performData).to.equal('0x1234')
+      })
+
+      itMaybe(
+        'has a large enough gas overhead to cover upkeeps that use all their gas [ @skip-coverage ]',
+        async () => {
+          await mock.setCanCheck(true)
+          await mock.setCheckGasToBurn(checkGasLimit)
+          const gas = checkGasLimit.add(checkGasOverhead)
+          const checkUpkeepResult = await registry
+            .connect(zeroAddress)
+            .callStatic['checkUpkeep(uint256)'](upkeepId, {
+              gasLimit: gas,
+            })
+
+          assert.equal(checkUpkeepResult.upkeepNeeded, true)
+        },
+      )
+    })
+  })
+
+  describe('#getMaxPaymentForGas', () => {
+    itMaybe('calculates the max fee appropriately in ZKSync', async () => {
+      await verifyMaxPayment(registry, moduleBase)
+    })
+
+    it('uses the fallback gas price if the feed has issues in ZKSync', async () => {
+      const chainModuleOverheads = await moduleBase.getGasOverhead()
+      const expectedFallbackMaxPayment = linkForGas(
+        performGas,
+        registryConditionalOverhead
+          .add(registryPerSignerGasOverhead.mul(f + 1))
+          .add(chainModuleOverheads.chainModuleFixedOverhead),
+        gasCeilingMultiplier.mul('2'), // fallbackGasPrice is 2x gas price
+        paymentPremiumPPB,
+        flatFeeMilliCents,
+      ).total
+
+      // Stale feed
+      let roundId = 99
+      const answer = 100
+      let updatedAt = 946684800 // New Years 2000 🥳
+      let startedAt = 946684799
+      await gasPriceFeed
+        .connect(owner)
+        .updateRoundData(roundId, answer, updatedAt, startedAt)
+
+      assert.equal(
+        expectedFallbackMaxPayment.toString(),
+        (
+          await registry.getMaxPaymentForGas(
+            upkeepId,
+            Trigger.CONDITION,
+            performGas,
+            linkToken.address,
+          )
+        ).toString(),
+      )
+
+      // Negative feed price
+      roundId = 100
+      updatedAt = now()
+      startedAt = 946684799
+      await gasPriceFeed
+        .connect(owner)
+        .updateRoundData(roundId, -100, updatedAt, startedAt)
+
+      assert.equal(
+        expectedFallbackMaxPayment.toString(),
+        (
+          await registry.getMaxPaymentForGas(
+            upkeepId,
+            Trigger.CONDITION,
+            performGas,
+            linkToken.address,
+          )
+        ).toString(),
+      )
+
+      // Zero feed price
+      roundId = 101
+      updatedAt = now()
+      startedAt = 946684799
+      await gasPriceFeed
+        .connect(owner)
+        .updateRoundData(roundId, 0, updatedAt, startedAt)
+
+      assert.equal(
+        expectedFallbackMaxPayment.toString(),
+        (
+          await registry.getMaxPaymentForGas(
+            upkeepId,
+            Trigger.CONDITION,
+            performGas,
+            linkToken.address,
+          )
+        ).toString(),
+      )
+    })
+
+    it('uses the fallback link price if the feed has issues in ZKSync', async () => {
+      const chainModuleOverheads = await moduleBase.getGasOverhead()
+      const expectedFallbackMaxPayment = linkForGas(
+        performGas,
+        registryConditionalOverhead
+          .add(registryPerSignerGasOverhead.mul(f + 1))
+          .add(chainModuleOverheads.chainModuleFixedOverhead),
+        gasCeilingMultiplier.mul('2'), // fallbackLinkPrice is 1/2 link price, so multiply by 2
+        paymentPremiumPPB,
+        flatFeeMilliCents,
+      ).total
+
+      // Stale feed
+      let roundId = 99
+      const answer = 100
+      let updatedAt = 946684800 // New Years 2000 🥳
+      let startedAt = 946684799
+      await linkUSDFeed
+        .connect(owner)
+        .updateRoundData(roundId, answer, updatedAt, startedAt)
+
+      assert.equal(
+        expectedFallbackMaxPayment.toString(),
+        (
+          await registry.getMaxPaymentForGas(
+            upkeepId,
+            Trigger.CONDITION,
+            performGas,
+            linkToken.address,
+          )
+        ).toString(),
+      )
+
+      // Negative feed price
+      roundId = 100
+      updatedAt = now()
+      startedAt = 946684799
+      await linkUSDFeed
+        .connect(owner)
+        .updateRoundData(roundId, -100, updatedAt, startedAt)
+
+      assert.equal(
+        expectedFallbackMaxPayment.toString(),
+        (
+          await registry.getMaxPaymentForGas(
+            upkeepId,
+            Trigger.CONDITION,
+            performGas,
+            linkToken.address,
+          )
+        ).toString(),
+      )
+
+      // Zero feed price
+      roundId = 101
+      updatedAt = now()
+      startedAt = 946684799
+      await linkUSDFeed
+        .connect(owner)
+        .updateRoundData(roundId, 0, updatedAt, startedAt)
+
+      assert.equal(
+        expectedFallbackMaxPayment.toString(),
+        (
+          await registry.getMaxPaymentForGas(
+            upkeepId,
+            Trigger.CONDITION,
+            performGas,
+            linkToken.address,
+          )
+        ).toString(),
+      )
+    })
+  })
+
+  describe('#typeAndVersion', () => {
+    it('uses the correct type and version', async () => {
+      const typeAndVersion = await registry.typeAndVersion()
+      assert.equal(typeAndVersion, 'AutomationRegistry 2.3.0')
+    })
+  })
+
+  describeMaybe('#setConfig - onchain', async () => {
+    const maxGas = BigNumber.from(6)
+    const staleness = BigNumber.from(4)
+    const ceiling = BigNumber.from(5)
+    const newMaxCheckDataSize = BigNumber.from(10000)
+    const newMaxPerformDataSize = BigNumber.from(10000)
+    const newMaxRevertDataSize = BigNumber.from(10000)
+    const newMaxPerformGas = BigNumber.from(10000000)
+    const fbGasEth = BigNumber.from(7)
+    const fbLinkEth = BigNumber.from(8)
+    const fbNativeEth = BigNumber.from(100)
+    const newTranscoder = randomAddress()
+    const newRegistrars = [randomAddress(), randomAddress()]
+    const upkeepManager = randomAddress()
+    const financeAdminAddress = randomAddress()
+
+    const newConfig: OnChainConfig = {
+      checkGasLimit: maxGas,
+      stalenessSeconds: staleness,
+      gasCeilingMultiplier: ceiling,
+      maxCheckDataSize: newMaxCheckDataSize,
+      maxPerformDataSize: newMaxPerformDataSize,
+      maxRevertDataSize: newMaxRevertDataSize,
+      maxPerformGas: newMaxPerformGas,
+      fallbackGasPrice: fbGasEth,
+      fallbackLinkPrice: fbLinkEth,
+      fallbackNativePrice: fbNativeEth,
+      transcoder: newTranscoder,
+      registrars: newRegistrars,
+      upkeepPrivilegeManager: upkeepManager,
+      chainModule: moduleBase.address,
+      reorgProtectionEnabled: true,
+      financeAdmin: financeAdminAddress,
+    }
+
+    it('reverts when called by anyone but the proposed owner', async () => {
+      await evmRevert(
+        registry
+          .connect(payee1)
+          .setConfigTypeSafe(
+            signerAddresses,
+            keeperAddresses,
+            f,
+            newConfig,
+            offchainVersion,
+            offchainBytes,
+            baseConfig[6],
+            baseConfig[7],
+          ),
+        'Only callable by owner',
+      )
+    })
+
+    it('reverts if signers or transmitters are the zero address', async () => {
+      await evmRevertCustomError(
+        registry
+          .connect(owner)
+          .setConfigTypeSafe(
+            [randomAddress(), randomAddress(), randomAddress(), zeroAddress],
+            [
+              randomAddress(),
+              randomAddress(),
+              randomAddress(),
+              randomAddress(),
+            ],
+            f,
+            newConfig,
+            offchainVersion,
+            offchainBytes,
+            baseConfig[6],
+            baseConfig[7],
+          ),
+        registry,
+        'InvalidSigner',
+      )
+
+      await evmRevertCustomError(
+        registry
+          .connect(owner)
+          .setConfigTypeSafe(
+            [
+              randomAddress(),
+              randomAddress(),
+              randomAddress(),
+              randomAddress(),
+            ],
+            [randomAddress(), randomAddress(), randomAddress(), zeroAddress],
+            f,
+            newConfig,
+            offchainVersion,
+            offchainBytes,
+            baseConfig[6],
+            baseConfig[7],
+          ),
+        registry,
+        'InvalidTransmitter',
+      )
+    })
+
+    it('updates the onchainConfig and configDigest', async () => {
+      const old = await registry.getState()
+      const oldConfig = await registry.getConfig()
+      const oldState = old.state
+      assert.isTrue(stalenessSeconds.eq(oldConfig.stalenessSeconds))
+      assert.isTrue(gasCeilingMultiplier.eq(oldConfig.gasCeilingMultiplier))
+
+      await registry
+        .connect(owner)
+        .setConfigTypeSafe(
+          signerAddresses,
+          keeperAddresses,
+          f,
+          newConfig,
+          offchainVersion,
+          offchainBytes,
+          [],
+          [],
+        )
+
+      const updated = await registry.getState()
+      const updatedConfig = updated.config
+      const updatedState = updated.state
+      assert.equal(updatedConfig.stalenessSeconds, staleness.toNumber())
+      assert.equal(updatedConfig.gasCeilingMultiplier, ceiling.toNumber())
+      assert.equal(
+        updatedConfig.maxCheckDataSize,
+        newMaxCheckDataSize.toNumber(),
+      )
+      assert.equal(
+        updatedConfig.maxPerformDataSize,
+        newMaxPerformDataSize.toNumber(),
+      )
+      assert.equal(
+        updatedConfig.maxRevertDataSize,
+        newMaxRevertDataSize.toNumber(),
+      )
+      assert.equal(updatedConfig.maxPerformGas, newMaxPerformGas.toNumber())
+      assert.equal(updatedConfig.checkGasLimit, maxGas.toNumber())
+      assert.equal(
+        updatedConfig.fallbackGasPrice.toNumber(),
+        fbGasEth.toNumber(),
+      )
+      assert.equal(
+        updatedConfig.fallbackLinkPrice.toNumber(),
+        fbLinkEth.toNumber(),
+      )
+      assert.equal(updatedState.latestEpoch, 0)
+
+      assert(oldState.configCount + 1 == updatedState.configCount)
+      assert(
+        oldState.latestConfigBlockNumber !=
+          updatedState.latestConfigBlockNumber,
+      )
+      assert(oldState.latestConfigDigest != updatedState.latestConfigDigest)
+
+      assert.equal(updatedConfig.transcoder, newTranscoder)
+      assert.deepEqual(updatedConfig.registrars, newRegistrars)
+      assert.equal(updatedConfig.upkeepPrivilegeManager, upkeepManager)
+    })
+
+    it('maintains paused state when config is changed', async () => {
+      await registry.pause()
+      const old = await registry.getState()
+      assert.isTrue(old.state.paused)
+
+      await registry
+        .connect(owner)
+        .setConfigTypeSafe(
+          signerAddresses,
+          keeperAddresses,
+          f,
+          newConfig,
+          offchainVersion,
+          offchainBytes,
+          [],
+          [],
+        )
+
+      const updated = await registry.getState()
+      assert.isTrue(updated.state.paused)
+    })
+
+    it('emits an event', async () => {
+      const tx = await registry
+        .connect(owner)
+        .setConfigTypeSafe(
+          signerAddresses,
+          keeperAddresses,
+          f,
+          newConfig,
+          offchainVersion,
+          offchainBytes,
+          [],
+          [],
+        )
+      await expect(tx).to.emit(registry, 'ConfigSet')
+    })
+  })
+
+  describe('#setConfig - offchain', () => {
+    let newKeepers: string[]
+
+    beforeEach(async () => {
+      newKeepers = [
+        await personas.Eddy.getAddress(),
+        await personas.Nick.getAddress(),
+        await personas.Neil.getAddress(),
+        await personas.Carol.getAddress(),
+      ]
+    })
+
+    it('reverts when called by anyone but the owner', async () => {
+      await evmRevert(
+        registry
+          .connect(payee1)
+          .setConfigTypeSafe(
+            newKeepers,
+            newKeepers,
+            f,
+            config,
+            offchainVersion,
+            offchainBytes,
+            baseConfig[6],
+            baseConfig[7],
+          ),
+        'Only callable by owner',
+      )
+    })
+
+    it('reverts if too many keeperAddresses set', async () => {
+      for (let i = 0; i < 40; i++) {
+        newKeepers.push(randomAddress())
+      }
+      await evmRevertCustomError(
+        registry
+          .connect(owner)
+          .setConfigTypeSafe(
+            newKeepers,
+            newKeepers,
+            f,
+            config,
+            offchainVersion,
+            offchainBytes,
+            baseConfig[6],
+            baseConfig[7],
+          ),
+        registry,
+        'TooManyOracles',
+      )
+    })
+
+    it('reverts if f=0', async () => {
+      await evmRevertCustomError(
+        registry
+          .connect(owner)
+          .setConfigTypeSafe(
+            newKeepers,
+            newKeepers,
+            0,
+            config,
+            offchainVersion,
+            offchainBytes,
+            baseConfig[6],
+            baseConfig[7],
+          ),
+        registry,
+        'IncorrectNumberOfFaultyOracles',
+      )
+    })
+
+    it('reverts if signers != transmitters length', async () => {
+      const signers = [randomAddress()]
+      await evmRevertCustomError(
+        registry
+          .connect(owner)
+          .setConfigTypeSafe(
+            signers,
+            newKeepers,
+            f,
+            config,
+            offchainVersion,
+            offchainBytes,
+            baseConfig[6],
+            baseConfig[7],
+          ),
+        registry,
+        'IncorrectNumberOfSigners',
+      )
+    })
+
+    it('reverts if signers <= 3f', async () => {
+      newKeepers.pop()
+      await evmRevertCustomError(
+        registry
+          .connect(owner)
+          .setConfigTypeSafe(
+            newKeepers,
+            newKeepers,
+            f,
+            config,
+            offchainVersion,
+            offchainBytes,
+            baseConfig[6],
+            baseConfig[7],
+          ),
+        registry,
+        'IncorrectNumberOfSigners',
+      )
+    })
+
+    it('reverts on repeated signers', async () => {
+      const newSigners = [
+        await personas.Eddy.getAddress(),
+        await personas.Eddy.getAddress(),
+        await personas.Eddy.getAddress(),
+        await personas.Eddy.getAddress(),
+      ]
+      await evmRevertCustomError(
+        registry
+          .connect(owner)
+          .setConfigTypeSafe(
+            newSigners,
+            newKeepers,
+            f,
+            config,
+            offchainVersion,
+            offchainBytes,
+            baseConfig[6],
+            baseConfig[7],
+          ),
+        registry,
+        'RepeatedSigner',
+      )
+    })
+
+    it('reverts on repeated transmitters', async () => {
+      const newTransmitters = [
+        await personas.Eddy.getAddress(),
+        await personas.Eddy.getAddress(),
+        await personas.Eddy.getAddress(),
+        await personas.Eddy.getAddress(),
+      ]
+      await evmRevertCustomError(
+        registry
+          .connect(owner)
+          .setConfigTypeSafe(
+            newKeepers,
+            newTransmitters,
+            f,
+            config,
+            offchainVersion,
+            offchainBytes,
+            baseConfig[6],
+            baseConfig[7],
+          ),
+        registry,
+        'RepeatedTransmitter',
+      )
+    })
+
+    itMaybe('stores new config and emits event', async () => {
+      // Perform an upkeep so that totalPremium is updated
+      await registry.connect(admin).addFunds(upkeepId, toWei('100'))
+      let tx = await getTransmitTx(registry, keeper1, [upkeepId])
+      await tx.wait()
+
+      const newOffChainVersion = BigNumber.from('2')
+      const newOffChainConfig = '0x1122'
+
+      const old = await registry.getState()
+      const oldState = old.state
+      assert(oldState.totalPremium.gt(BigNumber.from('0')))
+
+      const newSigners = newKeepers
+      tx = await registry
+        .connect(owner)
+        .setConfigTypeSafe(
+          newSigners,
+          newKeepers,
+          f,
+          config,
+          newOffChainVersion,
+          newOffChainConfig,
+          [],
+          [],
+        )
+
+      const updated = await registry.getState()
+      const updatedState = updated.state
+      assert(oldState.totalPremium.eq(updatedState.totalPremium))
+
+      // Old signer addresses which are not in new signers should be non active
+      for (let i = 0; i < signerAddresses.length; i++) {
+        const signer = signerAddresses[i]
+        if (!newSigners.includes(signer)) {
+          assert(!(await registry.getSignerInfo(signer)).active)
+          assert((await registry.getSignerInfo(signer)).index == 0)
+        }
+      }
+      // New signer addresses should be active
+      for (let i = 0; i < newSigners.length; i++) {
+        const signer = newSigners[i]
+        assert((await registry.getSignerInfo(signer)).active)
+        assert((await registry.getSignerInfo(signer)).index == i)
+      }
+      // Old transmitter addresses which are not in new transmitter should be non active, update lastCollected but retain other info
+      for (let i = 0; i < keeperAddresses.length; i++) {
+        const transmitter = keeperAddresses[i]
+        if (!newKeepers.includes(transmitter)) {
+          assert(!(await registry.getTransmitterInfo(transmitter)).active)
+          assert((await registry.getTransmitterInfo(transmitter)).index == i)
+          assert(
+            (await registry.getTransmitterInfo(transmitter)).lastCollected.eq(
+              oldState.totalPremium.sub(
+                oldState.totalPremium.mod(keeperAddresses.length),
+              ),
+            ),
+          )
+        }
+      }
+      // New transmitter addresses should be active
+      for (let i = 0; i < newKeepers.length; i++) {
+        const transmitter = newKeepers[i]
+        assert((await registry.getTransmitterInfo(transmitter)).active)
+        assert((await registry.getTransmitterInfo(transmitter)).index == i)
+        assert(
+          (await registry.getTransmitterInfo(transmitter)).lastCollected.eq(
+            oldState.totalPremium,
+          ),
+        )
+      }
+
+      // config digest should be updated
+      assert(oldState.configCount + 1 == updatedState.configCount)
+      assert(
+        oldState.latestConfigBlockNumber !=
+          updatedState.latestConfigBlockNumber,
+      )
+      assert(oldState.latestConfigDigest != updatedState.latestConfigDigest)
+
+      //New config should be updated
+      assert.deepEqual(updated.signers, newKeepers)
+      assert.deepEqual(updated.transmitters, newKeepers)
+
+      // Event should have been emitted
+      await expect(tx).to.emit(registry, 'ConfigSet')
+    })
+  })
+
+  describe('#cancelUpkeep', () => {
+    describe('when called by the admin', async () => {
+      describeMaybe('when an upkeep has been performed', async () => {
+        beforeEach(async () => {
+          await linkToken.connect(owner).approve(registry.address, toWei('100'))
+          await registry.connect(owner).addFunds(upkeepId, toWei('100'))
+          await getTransmitTx(registry, keeper1, [upkeepId])
+        })
+
+        it('deducts a cancellation fee from the upkeep and adds to reserve', async () => {
+          const newMinUpkeepSpend = toWei('10')
+          const financeAdminAddress = await financeAdmin.getAddress()
+
+          await registry.connect(owner).setConfigTypeSafe(
+            signerAddresses,
+            keeperAddresses,
+            f,
+            {
+              checkGasLimit,
+              stalenessSeconds,
+              gasCeilingMultiplier,
+              maxCheckDataSize,
+              maxPerformDataSize,
+              maxRevertDataSize,
+              maxPerformGas,
+              fallbackGasPrice,
+              fallbackLinkPrice,
+              fallbackNativePrice,
+              transcoder: transcoder.address,
+              registrars: [],
+              upkeepPrivilegeManager: upkeepManager,
+              chainModule: moduleBase.address,
+              reorgProtectionEnabled: true,
+              financeAdmin: financeAdminAddress,
+            },
+            offchainVersion,
+            offchainBytes,
+            [linkToken.address],
+            [
+              {
+                gasFeePPB: paymentPremiumPPB,
+                flatFeeMilliCents,
+                priceFeed: linkUSDFeed.address,
+                fallbackPrice: fallbackLinkPrice,
+                minSpend: newMinUpkeepSpend,
+                decimals: 18,
+              },
+            ],
+          )
+
+          const payee1Before = await linkToken.balanceOf(
+            await payee1.getAddress(),
+          )
+          const upkeepBefore = (await registry.getUpkeep(upkeepId)).balance
+          const ownerBefore = await registry.linkAvailableForPayment()
+
+          const amountSpent = toWei('100').sub(upkeepBefore)
+          const cancellationFee = newMinUpkeepSpend.sub(amountSpent)
+
+          await registry.connect(admin).cancelUpkeep(upkeepId)
+
+          const payee1After = await linkToken.balanceOf(
+            await payee1.getAddress(),
+          )
+          const upkeepAfter = (await registry.getUpkeep(upkeepId)).balance
+          const ownerAfter = await registry.linkAvailableForPayment()
+
+          // post upkeep balance should be previous balance minus cancellation fee
+          assert.isTrue(upkeepBefore.sub(cancellationFee).eq(upkeepAfter))
+          // payee balance should not change
+          assert.isTrue(payee1Before.eq(payee1After))
+          // owner should receive the cancellation fee
+          assert.isTrue(ownerAfter.sub(ownerBefore).eq(cancellationFee))
+        })
+
+        it('deducts up to balance as cancellation fee', async () => {
+          // Very high min spend, should deduct whole balance as cancellation fees
+          const newMinUpkeepSpend = toWei('1000')
+          const financeAdminAddress = await financeAdmin.getAddress()
+
+          await registry.connect(owner).setConfigTypeSafe(
+            signerAddresses,
+            keeperAddresses,
+            f,
+            {
+              checkGasLimit,
+              stalenessSeconds,
+              gasCeilingMultiplier,
+              maxCheckDataSize,
+              maxPerformDataSize,
+              maxRevertDataSize,
+              maxPerformGas,
+              fallbackGasPrice,
+              fallbackLinkPrice,
+              fallbackNativePrice,
+              transcoder: transcoder.address,
+              registrars: [],
+              upkeepPrivilegeManager: upkeepManager,
+              chainModule: moduleBase.address,
+              reorgProtectionEnabled: true,
+              financeAdmin: financeAdminAddress,
+            },
+            offchainVersion,
+            offchainBytes,
+            [linkToken.address],
+            [
+              {
+                gasFeePPB: paymentPremiumPPB,
+                flatFeeMilliCents,
+                priceFeed: linkUSDFeed.address,
+                fallbackPrice: fallbackLinkPrice,
+                minSpend: newMinUpkeepSpend,
+                decimals: 18,
+              },
+            ],
+          )
+          const payee1Before = await linkToken.balanceOf(
+            await payee1.getAddress(),
+          )
+          const upkeepBefore = (await registry.getUpkeep(upkeepId)).balance
+          const ownerBefore = await registry.linkAvailableForPayment()
+
+          await registry.connect(admin).cancelUpkeep(upkeepId)
+          const payee1After = await linkToken.balanceOf(
+            await payee1.getAddress(),
+          )
+          const ownerAfter = await registry.linkAvailableForPayment()
+          const upkeepAfter = (await registry.getUpkeep(upkeepId)).balance
+
+          // all upkeep balance is deducted for cancellation fee
+          assert.equal(upkeepAfter.toNumber(), 0)
+          // payee balance should not change
+          assert.isTrue(payee1After.eq(payee1Before))
+          // all upkeep balance is transferred to the owner
+          assert.isTrue(ownerAfter.sub(ownerBefore).eq(upkeepBefore))
+        })
+
+        it('does not deduct cancellation fee if more than minUpkeepSpendDollars is spent', async () => {
+          // Very low min spend, already spent in one perform upkeep
+          const newMinUpkeepSpend = BigNumber.from(420)
+          const financeAdminAddress = await financeAdmin.getAddress()
+
+          await registry.connect(owner).setConfigTypeSafe(
+            signerAddresses,
+            keeperAddresses,
+            f,
+            {
+              checkGasLimit,
+              stalenessSeconds,
+              gasCeilingMultiplier,
+              maxCheckDataSize,
+              maxPerformDataSize,
+              maxRevertDataSize,
+              maxPerformGas,
+              fallbackGasPrice,
+              fallbackLinkPrice,
+              fallbackNativePrice,
+              transcoder: transcoder.address,
+              registrars: [],
+              upkeepPrivilegeManager: upkeepManager,
+              chainModule: moduleBase.address,
+              reorgProtectionEnabled: true,
+              financeAdmin: financeAdminAddress,
+            },
+            offchainVersion,
+            offchainBytes,
+            [linkToken.address],
+            [
+              {
+                gasFeePPB: paymentPremiumPPB,
+                flatFeeMilliCents,
+                priceFeed: linkUSDFeed.address,
+                fallbackPrice: fallbackLinkPrice,
+                minSpend: newMinUpkeepSpend,
+                decimals: 18,
+              },
+            ],
+          )
+          const payee1Before = await linkToken.balanceOf(
+            await payee1.getAddress(),
+          )
+          const upkeepBefore = (await registry.getUpkeep(upkeepId)).balance
+          const ownerBefore = await registry.linkAvailableForPayment()
+
+          await registry.connect(admin).cancelUpkeep(upkeepId)
+          const payee1After = await linkToken.balanceOf(
+            await payee1.getAddress(),
+          )
+          const ownerAfter = await registry.linkAvailableForPayment()
+          const upkeepAfter = (await registry.getUpkeep(upkeepId)).balance
+
+          // upkeep does not pay cancellation fee after cancellation because minimum upkeep spent is met
+          assert.isTrue(upkeepBefore.eq(upkeepAfter))
+          // owner balance does not change
+          assert.isTrue(ownerAfter.eq(ownerBefore))
+          // payee balance does not change
+          assert.isTrue(payee1Before.eq(payee1After))
+        })
+      })
+    })
+  })
+
+  describe('#withdrawPayment', () => {
+    beforeEach(async () => {
+      await linkToken.connect(owner).approve(registry.address, toWei('100'))
+      await registry.connect(owner).addFunds(upkeepId, toWei('100'))
+      await getTransmitTx(registry, keeper1, [upkeepId])
+    })
+
+    it('reverts if called by anyone but the payee', async () => {
+      await evmRevertCustomError(
+        registry
+          .connect(payee2)
+          .withdrawPayment(
+            await keeper1.getAddress(),
+            await nonkeeper.getAddress(),
+          ),
+        registry,
+        'OnlyCallableByPayee',
+      )
+    })
+
+    it('reverts if called with the 0 address', async () => {
+      await evmRevertCustomError(
+        registry
+          .connect(payee2)
+          .withdrawPayment(await keeper1.getAddress(), zeroAddress),
+        registry,
+        'InvalidRecipient',
+      )
+    })
+
+    it('updates the balances', async () => {
+      const to = await nonkeeper.getAddress()
+      const keeperBefore = await registry.getTransmitterInfo(
+        await keeper1.getAddress(),
+      )
+      const registrationBefore = (await registry.getUpkeep(upkeepId)).balance
+      const toLinkBefore = await linkToken.balanceOf(to)
+      const registryLinkBefore = await linkToken.balanceOf(registry.address)
+      const registryPremiumBefore = (await registry.getState()).state
+        .totalPremium
+      const ownerBefore = await registry.linkAvailableForPayment()
+
+      // Withdrawing for first time, last collected = 0
+      assert.equal(keeperBefore.lastCollected.toString(), '0')
+
+      //// Do the thing
+      await registry
+        .connect(payee1)
+        .withdrawPayment(await keeper1.getAddress(), to)
+
+      const keeperAfter = await registry.getTransmitterInfo(
+        await keeper1.getAddress(),
+      )
+      const registrationAfter = (await registry.getUpkeep(upkeepId)).balance
+      const toLinkAfter = await linkToken.balanceOf(to)
+      const registryLinkAfter = await linkToken.balanceOf(registry.address)
+      const registryPremiumAfter = (await registry.getState()).state
+        .totalPremium
+      const ownerAfter = await registry.linkAvailableForPayment()
+
+      // registry total premium should not change
+      assert.isTrue(registryPremiumBefore.eq(registryPremiumAfter))
+
+      // Last collected should be updated to premium-change
+      assert.isTrue(
+        keeperAfter.lastCollected.eq(
+          registryPremiumBefore.sub(
+            registryPremiumBefore.mod(keeperAddresses.length),
+          ),
+        ),
+      )
+
+      // owner balance should remain unchanged
+      assert.isTrue(ownerAfter.eq(ownerBefore))
+
+      assert.isTrue(keeperAfter.balance.eq(BigNumber.from(0)))
+      assert.isTrue(registrationBefore.eq(registrationAfter))
+      assert.isTrue(toLinkBefore.add(keeperBefore.balance).eq(toLinkAfter))
+      assert.isTrue(
+        registryLinkBefore.sub(keeperBefore.balance).eq(registryLinkAfter),
+      )
+    })
+
+    it('emits a log announcing the withdrawal', async () => {
+      const balance = (
+        await registry.getTransmitterInfo(await keeper1.getAddress())
+      ).balance
+      const tx = await registry
+        .connect(payee1)
+        .withdrawPayment(
+          await keeper1.getAddress(),
+          await nonkeeper.getAddress(),
+        )
+      await expect(tx)
+        .to.emit(registry, 'PaymentWithdrawn')
+        .withArgs(
+          await keeper1.getAddress(),
+          balance,
+          await nonkeeper.getAddress(),
+          await payee1.getAddress(),
+        )
+    })
+  })
+
+  describe('#checkCallback', () => {
+    it('returns false with appropriate failure reason when target callback reverts', async () => {
+      await streamsLookupUpkeep.setShouldRevertCallback(true)
+
+      const values: any[] = ['0x1234', '0xabcd']
+      const res = await registry
+        .connect(zeroAddress)
+        .callStatic.checkCallback(streamsLookupUpkeepId, values, '0x')
+
+      assert.isFalse(res.upkeepNeeded)
+      assert.equal(res.performData, '0x')
+      assert.equal(
+        res.upkeepFailureReason,
+        UpkeepFailureReason.CHECK_CALLBACK_REVERTED,
+      )
+      assert.isTrue(res.gasUsed.gt(BigNumber.from('0'))) // Some gas should be used
+    })
+
+    it('returns false with appropriate failure reason when target callback returns big performData', async () => {
+      let longBytes = '0x'
+      for (let i = 0; i <= maxPerformDataSize.toNumber(); i++) {
+        longBytes += '11'
+      }
+      const values: any[] = [longBytes, longBytes]
+      const res = await registry
+        .connect(zeroAddress)
+        .callStatic.checkCallback(streamsLookupUpkeepId, values, '0x')
+
+      assert.isFalse(res.upkeepNeeded)
+      assert.equal(res.performData, '0x')
+      assert.equal(
+        res.upkeepFailureReason,
+        UpkeepFailureReason.PERFORM_DATA_EXCEEDS_LIMIT,
+      )
+      assert.isTrue(res.gasUsed.gt(BigNumber.from('0'))) // Some gas should be used
+    })
+
+    it('returns false with appropriate failure reason when target callback returns false', async () => {
+      await streamsLookupUpkeep.setCallbackReturnBool(false)
+      const values: any[] = ['0x1234', '0xabcd']
+      const res = await registry
+        .connect(zeroAddress)
+        .callStatic.checkCallback(streamsLookupUpkeepId, values, '0x')
+
+      assert.isFalse(res.upkeepNeeded)
+      assert.equal(res.performData, '0x')
+      assert.equal(
+        res.upkeepFailureReason,
+        UpkeepFailureReason.UPKEEP_NOT_NEEDED,
+      )
+      assert.isTrue(res.gasUsed.gt(BigNumber.from('0'))) // Some gas should be used
+    })
+
+    it('succeeds with upkeep needed', async () => {
+      const values: any[] = ['0x1234', '0xabcd']
+
+      const res = await registry
+        .connect(zeroAddress)
+        .callStatic.checkCallback(streamsLookupUpkeepId, values, '0x')
+      const expectedPerformData = ethers.utils.defaultAbiCoder.encode(
+        ['bytes[]', 'bytes'],
+        [values, '0x'],
+      )
+
+      assert.isTrue(res.upkeepNeeded)
+      assert.equal(res.performData, expectedPerformData)
+      assert.equal(res.upkeepFailureReason, UpkeepFailureReason.NONE)
+      assert.isTrue(res.gasUsed.gt(BigNumber.from('0'))) // Some gas should be used
+    })
+  })
+
+  describe('transmitterPremiumSplit [ @skip-coverage ]', () => {
+    beforeEach(async () => {
+      await linkToken.connect(owner).approve(registry.address, toWei('100'))
+      await registry.connect(owner).addFunds(upkeepId, toWei('100'))
+    })
+
+    it('splits premium evenly across transmitters', async () => {
+      // Do a transmit from keeper1
+      await getTransmitTx(registry, keeper1, [upkeepId])
+
+      const registryPremium = (await registry.getState()).state.totalPremium
+      assert.isTrue(registryPremium.gt(BigNumber.from(0)))
+
+      const premiumPerTransmitter = registryPremium.div(
+        BigNumber.from(keeperAddresses.length),
+      )
+      const k1Balance = (
+        await registry.getTransmitterInfo(await keeper1.getAddress())
+      ).balance
+      // transmitter should be reimbursed for gas and get the premium
+      assert.isTrue(k1Balance.gt(premiumPerTransmitter))
+      const k1GasReimbursement = k1Balance.sub(premiumPerTransmitter)
+
+      const k2Balance = (
+        await registry.getTransmitterInfo(await keeper2.getAddress())
+      ).balance
+      // non transmitter should get its share of premium
+      assert.isTrue(k2Balance.eq(premiumPerTransmitter))
+
+      // Now do a transmit from keeper 2
+      await getTransmitTx(registry, keeper2, [upkeepId])
+      const registryPremiumNew = (await registry.getState()).state.totalPremium
+      assert.isTrue(registryPremiumNew.gt(registryPremium))
+      const premiumPerTransmitterNew = registryPremiumNew.div(
+        BigNumber.from(keeperAddresses.length),
+      )
+      const additionalPremium = premiumPerTransmitterNew.sub(
+        premiumPerTransmitter,
+      )
+
+      const k1BalanceNew = (
+        await registry.getTransmitterInfo(await keeper1.getAddress())
+      ).balance
+      // k1 should get the new premium
+      assert.isTrue(
+        k1BalanceNew.eq(k1GasReimbursement.add(premiumPerTransmitterNew)),
+      )
+
+      const k2BalanceNew = (
+        await registry.getTransmitterInfo(await keeper2.getAddress())
+      ).balance
+      // k2 should get gas reimbursement in addition to new premium
+      assert.isTrue(k2BalanceNew.gt(k2Balance.add(additionalPremium)))
+    })
+
+    it('updates last collected upon payment withdrawn', async () => {
+      // Do a transmit from keeper1
+      await getTransmitTx(registry, keeper1, [upkeepId])
+
+      const registryPremium = (await registry.getState()).state.totalPremium
+      const k1 = await registry.getTransmitterInfo(await keeper1.getAddress())
+      const k2 = await registry.getTransmitterInfo(await keeper2.getAddress())
+
+      // Withdrawing for first time, last collected = 0
+      assert.isTrue(k1.lastCollected.eq(BigNumber.from(0)))
+      assert.isTrue(k2.lastCollected.eq(BigNumber.from(0)))
+
+      //// Do the thing
+      await registry
+        .connect(payee1)
+        .withdrawPayment(
+          await keeper1.getAddress(),
+          await nonkeeper.getAddress(),
+        )
+
+      const k1New = await registry.getTransmitterInfo(
+        await keeper1.getAddress(),
+      )
+      const k2New = await registry.getTransmitterInfo(
+        await keeper2.getAddress(),
+      )
+
+      // transmitter info lastCollected should be updated for k1, not for k2
+      assert.isTrue(
+        k1New.lastCollected.eq(
+          registryPremium.sub(registryPremium.mod(keeperAddresses.length)),
+        ),
+      )
+      assert.isTrue(k2New.lastCollected.eq(BigNumber.from(0)))
+    })
+
+    // itMaybe(
+    it('maintains consistent balance information across all parties', async () => {
+      // throughout transmits, withdrawals, setConfigs total claim on balances should remain less than expected balance
+      // some spare change can get lost but it should be less than maxAllowedSpareChange
+
+      let maxAllowedSpareChange = BigNumber.from('0')
+      await verifyConsistentAccounting(maxAllowedSpareChange)
+
+      await getTransmitTx(registry, keeper1, [upkeepId])
+      maxAllowedSpareChange = maxAllowedSpareChange.add(BigNumber.from('31'))
+      await verifyConsistentAccounting(maxAllowedSpareChange)
+
+      await registry
+        .connect(payee1)
+        .withdrawPayment(
+          await keeper1.getAddress(),
+          await nonkeeper.getAddress(),
+        )
+      await verifyConsistentAccounting(maxAllowedSpareChange)
+
+      await registry
+        .connect(payee2)
+        .withdrawPayment(
+          await keeper2.getAddress(),
+          await nonkeeper.getAddress(),
+        )
+      await verifyConsistentAccounting(maxAllowedSpareChange)
+
+      await getTransmitTx(registry, keeper1, [upkeepId])
+      maxAllowedSpareChange = maxAllowedSpareChange.add(BigNumber.from('31'))
+      await verifyConsistentAccounting(maxAllowedSpareChange)
+
+      await registry.connect(owner).setConfigTypeSafe(
+        signerAddresses.slice(2, 15), // only use 2-14th index keepers
+        keeperAddresses.slice(2, 15),
+        f,
+        config,
+        offchainVersion,
+        offchainBytes,
+        baseConfig[6],
+        baseConfig[7],
+      )
+      await verifyConsistentAccounting(maxAllowedSpareChange)
+
+      await getTransmitTx(registry, keeper3, [upkeepId], {
+        startingSignerIndex: 2,
+      })
+      maxAllowedSpareChange = maxAllowedSpareChange.add(BigNumber.from('13'))
+      await verifyConsistentAccounting(maxAllowedSpareChange)
+
+      await registry
+        .connect(payee1)
+        .withdrawPayment(
+          await keeper1.getAddress(),
+          await nonkeeper.getAddress(),
+        )
+      await verifyConsistentAccounting(maxAllowedSpareChange)
+
+      await registry
+        .connect(payee3)
+        .withdrawPayment(
+          await keeper3.getAddress(),
+          await nonkeeper.getAddress(),
+        )
+      await verifyConsistentAccounting(maxAllowedSpareChange)
+
+      await registry.connect(owner).setConfigTypeSafe(
+        signerAddresses.slice(0, 4), // only use 0-3rd index keepers
+        keeperAddresses.slice(0, 4),
+        f,
+        config,
+        offchainVersion,
+        offchainBytes,
+        baseConfig[6],
+        baseConfig[7],
+      )
+      await verifyConsistentAccounting(maxAllowedSpareChange)
+      await getTransmitTx(registry, keeper1, [upkeepId])
+      maxAllowedSpareChange = maxAllowedSpareChange.add(BigNumber.from('4'))
+      await getTransmitTx(registry, keeper3, [upkeepId])
+      maxAllowedSpareChange = maxAllowedSpareChange.add(BigNumber.from('4'))
+
+      await verifyConsistentAccounting(maxAllowedSpareChange)
+      await registry
+        .connect(payee5)
+        .withdrawPayment(
+          await keeper5.getAddress(),
+          await nonkeeper.getAddress(),
+        )
+      await verifyConsistentAccounting(maxAllowedSpareChange)
+
+      await registry
+        .connect(payee1)
+        .withdrawPayment(
+          await keeper1.getAddress(),
+          await nonkeeper.getAddress(),
+        )
+      await verifyConsistentAccounting(maxAllowedSpareChange)
+    })
+  })
+})
diff --git a/contracts/test/v0.8/automation/helpers.ts b/contracts/test/v0.8/automation/helpers.ts
index b2cdfb4efd9..99f2cef9b87 100644
--- a/contracts/test/v0.8/automation/helpers.ts
+++ b/contracts/test/v0.8/automation/helpers.ts
@@ -9,6 +9,7 @@ import { IAutomationRegistryMaster__factory as IAutomationRegistryMasterFactory
 import { assert } from 'chai'
 import { FunctionFragment } from '@ethersproject/abi'
 import { AutomationRegistryLogicC2_3__factory as AutomationRegistryLogicC2_3Factory } from '../../../typechain/factories/AutomationRegistryLogicC2_3__factory'
+import { ZKSyncAutomationRegistryLogicC2_3__factory as ZKSyncAutomationRegistryLogicC2_3Factory } from '../../../typechain/factories/ZKSyncAutomationRegistryLogicC2_3__factory'
 import { IAutomationRegistryMaster2_3 as IAutomationRegistry2_3 } from '../../../typechain/IAutomationRegistryMaster2_3'
 import { IAutomationRegistryMaster2_3__factory as IAutomationRegistryMaster2_3Factory } from '../../../typechain/factories/IAutomationRegistryMaster2_3__factory'
 
@@ -212,3 +213,51 @@ export const deployRegistry23 = async (
   const master = await registryFactory.connect(from).deploy(logicA.address)
   return IAutomationRegistryMaster2_3Factory.connect(master.address, from)
 }
+
+export const deployZKSyncRegistry23 = async (
+  from: Signer,
+  link: Parameters<ZKSyncAutomationRegistryLogicC2_3Factory['deploy']>[0],
+  linkUSD: Parameters<ZKSyncAutomationRegistryLogicC2_3Factory['deploy']>[1],
+  nativeUSD: Parameters<ZKSyncAutomationRegistryLogicC2_3Factory['deploy']>[2],
+  fastgas: Parameters<ZKSyncAutomationRegistryLogicC2_3Factory['deploy']>[3],
+  allowedReadOnlyAddress: Parameters<
+    AutomationRegistryLogicC2_3Factory['deploy']
+  >[5],
+  payoutMode: Parameters<ZKSyncAutomationRegistryLogicC2_3Factory['deploy']>[6],
+  wrappedNativeTokenAddress: Parameters<
+    ZKSyncAutomationRegistryLogicC2_3Factory['deploy']
+  >[7],
+): Promise<IAutomationRegistry2_3> => {
+  const logicCFactory = await ethers.getContractFactory(
+    'ZKSyncAutomationRegistryLogicC2_3',
+  )
+  const logicBFactory = await ethers.getContractFactory(
+    'ZKSyncAutomationRegistryLogicB2_3',
+  )
+  const logicAFactory = await ethers.getContractFactory(
+    'ZKSyncAutomationRegistryLogicA2_3',
+  )
+  const registryFactory = await ethers.getContractFactory(
+    'ZKSyncAutomationRegistry2_3',
+  )
+  const forwarderLogicFactory = await ethers.getContractFactory(
+    'AutomationForwarderLogic',
+  )
+  const forwarderLogic = await forwarderLogicFactory.connect(from).deploy()
+  const logicC = await logicCFactory
+    .connect(from)
+    .deploy(
+      link,
+      linkUSD,
+      nativeUSD,
+      fastgas,
+      forwarderLogic.address,
+      allowedReadOnlyAddress,
+      payoutMode,
+      wrappedNativeTokenAddress,
+    )
+  const logicB = await logicBFactory.connect(from).deploy(logicC.address)
+  const logicA = await logicAFactory.connect(from).deploy(logicB.address)
+  const master = await registryFactory.connect(from).deploy(logicA.address)
+  return IAutomationRegistryMaster2_3Factory.connect(master.address, from)
+}