From 5dbcfa7bc101ebb2dbd4a31f9069a4761e69fbf6 Mon Sep 17 00:00:00 2001
From: Ryan Hall <hall.ryan.r@gmail.com>
Date: Thu, 3 Aug 2023 10:18:02 -0400
Subject: [PATCH 01/33] write UpkeepBalanceMonWithBuffer

---
 .../upkeeps/UpkeepBalanceMonWithBuffer.sol    | 334 ++++++++++++++++++
 1 file changed, 334 insertions(+)
 create mode 100644 contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol

diff --git a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol
new file mode 100644
index 00000000000..19d042f621b
--- /dev/null
+++ b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol
@@ -0,0 +1,334 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity 0.8.6;
+
+import "@chainlink/contracts/src/v0.8/ConfirmedOwner.sol";
+//import {AutomationRegistryInterface, State, Config} from "@chainlink/contracts/src/v0.8/interfaces/AutomationRegistryInterface1_2.sol";
+import {AutomationRegistryInterface, State, UpkeepInfo} from "@chainlink/contracts/src/v0.8/interfaces/AutomationRegistryInterface2_0.sol";
+import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol";
+import "@openzeppelin/contracts/security/Pausable.sol";
+import "@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol";
+
+/**
+ * @title The UpkeepBalanceMonitor contract.
+ * @notice A keeper-compatible contract that monitors and funds Chainlink Automation upkeeps.
+ */
+contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
+    LinkTokenInterface public LINKTOKEN;
+    AutomationRegistryInterface public REGISTRY;
+
+    uint256 private constant MIN_GAS_FOR_TRANSFER = 55_000;
+
+    bytes4 fundSig = REGISTRY.addFunds.selector;
+
+    event FundsAdded(uint256 amountAdded, uint256 newBalance, address sender);
+    event FundsWithdrawn(uint256 amountWithdrawn, address payee);
+    event TopUpSucceeded(uint256 indexed upkeepId);
+    event TopUpFailed(uint256 indexed upkeepId);
+    event KeeperRegistryAddressUpdated(address oldAddress, address newAddress);
+    event LinkTokenAddressUpdated(address oldAddress, address newAddress);
+    event MinWaitPeriodUpdated(
+        uint256 oldMinWaitPeriod,
+        uint256 newMinWaitPeriod
+    );
+    event OutOfGas(uint256 lastId);
+
+    error InvalidWatchList();
+    error OnlyKeeperRegistry();
+    error DuplicateSubcriptionId(uint256 duplicate);
+
+    struct Target {
+        bool isActive;
+        uint96 minBalanceJuels;
+        uint96 topUpAmountJuels;
+        uint56 lastTopUpTimestamp;
+    }
+
+    address public s_keeperRegistryAddress; // the address of the keeper registry
+    uint256 public s_minWaitPeriodSeconds; // minimum time to wait between top-ups
+    uint256[] public s_watchList; // the watchlist on which subscriptions are stored
+    mapping(uint256 => Target) internal s_targets;
+
+    /**
+     * @param linkTokenAddress the Link token address
+     * @param keeperRegistryAddress the address of the keeper registry contract
+     * @param minWaitPeriodSeconds the minimum wait period for addresses between funding
+     */
+    constructor(
+        address linkTokenAddress,
+        address keeperRegistryAddress,
+        uint256 minWaitPeriodSeconds
+    ) ConfirmedOwner(msg.sender) {
+        setLinkTokenAddress(linkTokenAddress); //	0x326C977E6efc84E512bB9C30f76E30c160eD06FB
+        setKeeperRegistryAddress(keeperRegistryAddress); // 0xE16Df59B887e3Caa439E0b29B42bA2e7976FD8b2
+        setMinWaitPeriodSeconds(minWaitPeriodSeconds); //0
+        TransferHelper.safeApprove(
+            linkTokenAddress,
+            address(keeperRegistryAddress),
+            type(uint256).max
+        );
+    }
+
+    /**
+     * @notice Sets the list of upkeeps to watch and their funding parameters.
+     * @param upkeepIDs the list of subscription ids to watch
+     * @param minBalancesJuels the minimum balances for each upkeep
+     * @param topUpAmountsJuels the amount to top up each upkeep
+     */
+    function setWatchList(
+        uint256[] calldata upkeepIDs,
+        uint96[] calldata minBalancesJuels,
+        uint96[] calldata topUpAmountsJuels
+    ) external onlyOwner {
+        if (
+            upkeepIDs.length != minBalancesJuels.length ||
+            upkeepIDs.length != topUpAmountsJuels.length
+        ) {
+            revert InvalidWatchList();
+        }
+        uint256[] memory oldWatchList = s_watchList;
+        for (uint256 idx = 0; idx < oldWatchList.length; idx++) {
+            s_targets[oldWatchList[idx]].isActive = false;
+        }
+        for (uint256 idx = 0; idx < upkeepIDs.length; idx++) {
+            if (s_targets[upkeepIDs[idx]].isActive) {
+                revert DuplicateSubcriptionId(upkeepIDs[idx]);
+            }
+            if (upkeepIDs[idx] == 0) {
+                revert InvalidWatchList();
+            }
+            if (topUpAmountsJuels[idx] <= minBalancesJuels[idx]) {
+                revert InvalidWatchList();
+            }
+            s_targets[upkeepIDs[idx]] = Target({
+                isActive: true,
+                minBalanceJuels: minBalancesJuels[idx],
+                topUpAmountJuels: topUpAmountsJuels[idx],
+                lastTopUpTimestamp: 0
+            });
+        }
+        s_watchList = upkeepIDs;
+    }
+
+    /**
+     * @notice Gets a list of upkeeps that are underfunded.
+     * @return list of upkeeps that are underfunded
+     */
+    function getUnderfundedUpkeeps() public view returns (uint256[] memory) {
+        uint256[] memory watchList = s_watchList;
+        uint256[] memory needsFunding = new uint256[](watchList.length);
+        uint256 count = 0;
+        uint256 minWaitPeriod = s_minWaitPeriodSeconds;
+        uint256 contractBalance = LINKTOKEN.balanceOf(address(this));
+        Target memory target;
+        for (uint256 idx = 0; idx < watchList.length; idx++) {
+            target = s_targets[watchList[idx]];
+            //( , , , uint96 upkeepBalance, , , ,) = REGISTRY.getUpkeep(watchList[idx]); <- for 1.2
+            UpkeepInfo memory upkeepInfo; //2.0
+            upkeepInfo = REGISTRY.getUpkeep(watchList[idx]); //2.0
+            uint96 upkeepBalance = upkeepInfo.balance; //2.0
+            uint96 minUpkeepBalance = REGISTRY.getMinBalanceForUpkeep(
+                watchList[idx]
+            );
+            uint96 minBalanceWithBuffer = getBalanceWithBuffer(
+                minUpkeepBalance
+            );
+            if (
+                target.lastTopUpTimestamp + minWaitPeriod <= block.timestamp &&
+                contractBalance >= target.topUpAmountJuels &&
+                (upkeepBalance < target.minBalanceJuels ||
+                    //upkeepBalance < minUpkeepBalance)
+                    upkeepBalance < minBalanceWithBuffer)
+            ) {
+                needsFunding[count] = watchList[idx];
+                count++;
+                contractBalance -= target.topUpAmountJuels;
+            }
+        }
+        if (count < watchList.length) {
+            assembly {
+                mstore(needsFunding, count)
+            }
+        }
+        return needsFunding;
+    }
+
+    /**
+     * @notice Send funds to the upkeeps provided.
+     * @param needsFunding the list of upkeeps to fund
+     */
+    function topUp(uint256[] memory needsFunding) public whenNotPaused {
+        uint256 minWaitPeriodSeconds = s_minWaitPeriodSeconds;
+        uint256 contractBalance = LINKTOKEN.balanceOf(address(this));
+        Target memory target;
+        for (uint256 idx = 0; idx < needsFunding.length; idx++) {
+            target = s_targets[needsFunding[idx]];
+            //( , , , uint96 upkeepBalance, , , ,) = REGISTRY.getUpkeep(needsFunding[idx]); <- for 1.2
+            UpkeepInfo memory upkeepInfo; //2.0
+            upkeepInfo = REGISTRY.getUpkeep(needsFunding[idx]); //2.0
+            uint96 upkeepBalance = upkeepInfo.balance; //2.0
+            uint96 minUpkeepBalance = REGISTRY.getMinBalanceForUpkeep(
+                needsFunding[idx]
+            );
+            uint96 minBalanceWithBuffer = getBalanceWithBuffer(
+                minUpkeepBalance
+            );
+            if (
+                target.isActive &&
+                target.lastTopUpTimestamp + minWaitPeriodSeconds <=
+                block.timestamp &&
+                (upkeepBalance < target.minBalanceJuels ||
+                    //upkeepBalance < minUpkeepBalance) &&
+                    upkeepBalance < minBalanceWithBuffer) &&
+                contractBalance >= target.topUpAmountJuels
+            ) {
+                REGISTRY.addFunds(needsFunding[idx], target.topUpAmountJuels);
+                s_targets[needsFunding[idx]].lastTopUpTimestamp = uint56(
+                    block.timestamp
+                );
+                contractBalance -= target.topUpAmountJuels;
+                emit TopUpSucceeded(needsFunding[idx]);
+            }
+            if (gasleft() < MIN_GAS_FOR_TRANSFER) {
+                emit OutOfGas(idx);
+                return;
+            }
+        }
+    }
+
+    /**
+     * @notice Gets list of upkeeps ids that are underfunded and returns a keeper-compatible payload.
+     * @return upkeepNeeded signals if upkeep is needed, performData is an abi encoded list of subscription ids that need funds
+     */
+    function checkUpkeep(bytes calldata)
+        external
+        view
+        whenNotPaused
+        returns (bool upkeepNeeded, bytes memory performData)
+    {
+        uint256[] memory needsFunding = getUnderfundedUpkeeps();
+        upkeepNeeded = needsFunding.length > 0;
+        performData = abi.encode(needsFunding);
+        return (upkeepNeeded, performData);
+    }
+
+    /**
+     * @notice Called by the keeper to send funds to underfunded addresses.
+     * @param performData the abi encoded list of addresses to fund
+     */
+    function performUpkeep(bytes calldata performData)
+        external
+        onlyKeeperRegistry
+        whenNotPaused
+    {
+        uint256[] memory needsFunding = abi.decode(performData, (uint256[]));
+        topUp(needsFunding);
+    }
+
+    /**
+     * @notice Withdraws the contract balance in LINK.
+     * @param amount the amount of LINK (in juels) to withdraw
+     * @param payee the address to pay
+     */
+    function withdraw(uint256 amount, address payable payee)
+        external
+        onlyOwner
+    {
+        require(payee != address(0));
+        emit FundsWithdrawn(amount, payee);
+        LINKTOKEN.transfer(payee, amount);
+    }
+
+    /**
+     * @notice Sets the LINK token address.
+     */
+    function setLinkTokenAddress(address linkTokenAddress) public onlyOwner {
+        require(linkTokenAddress != address(0));
+        emit LinkTokenAddressUpdated(address(LINKTOKEN), linkTokenAddress);
+        LINKTOKEN = LinkTokenInterface(linkTokenAddress);
+    }
+
+    /**
+     * @notice Sets the keeper registry address.
+     */
+    function setKeeperRegistryAddress(address keeperRegistryAddress)
+        public
+        onlyOwner
+    {
+        require(keeperRegistryAddress != address(0));
+        emit KeeperRegistryAddressUpdated(
+            s_keeperRegistryAddress,
+            keeperRegistryAddress
+        );
+        s_keeperRegistryAddress = keeperRegistryAddress;
+        REGISTRY = AutomationRegistryInterface(keeperRegistryAddress);
+    }
+
+    /**
+     * @notice Sets the minimum wait period (in seconds) for upkeep ids between funding.
+     */
+    function setMinWaitPeriodSeconds(uint256 period) public onlyOwner {
+        emit MinWaitPeriodUpdated(s_minWaitPeriodSeconds, period);
+        s_minWaitPeriodSeconds = period;
+    }
+
+    /**
+     * @notice Gets configuration information for a upkeep on the watchlist.
+     */
+    function getUpkeepInfo(uint256 upkeepId)
+        external
+        view
+        returns (
+            bool isActive,
+            uint96 minBalanceJuels,
+            uint96 topUpAmountJuels,
+            uint56 lastTopUpTimestamp
+        )
+    {
+        Target memory target = s_targets[upkeepId];
+        return (
+            target.isActive,
+            target.minBalanceJuels,
+            target.topUpAmountJuels,
+            target.lastTopUpTimestamp
+        );
+    }
+
+    /**
+     * @notice Gets the list of upkeeps ids being watched.
+     */
+    function getWatchList() external view returns (uint256[] memory) {
+        return s_watchList;
+    }
+
+    /**
+     * @notice Pause the contract, which prevents executing performUpkeep.
+     */
+    function pause() external onlyOwner {
+        _pause();
+    }
+
+    /**
+     * @notice Unpause the contract.
+     */
+    function unpause() external onlyOwner {
+        _unpause();
+    }
+
+    /**
+     * @notice Called to add buffer to minimum balance of upkeeps
+     * @param num the current minimum balance 
+     */
+    function getBalanceWithBuffer(uint96 num) internal pure returns (uint96) {
+        uint96 buffer = 20;
+        uint96 result = uint96((uint256(num) * (100 + buffer)) / 100); // convert to uint256 to prevent overflow
+        return result;
+    }
+
+    modifier onlyKeeperRegistry() {
+        if (msg.sender != s_keeperRegistryAddress) {
+            revert OnlyKeeperRegistry();
+        }
+        _;
+    }
+}

From 032c03a1941fe999a62a8ffbf8a472b6013a90c9 Mon Sep 17 00:00:00 2001
From: De Clercq Wentzel <10665586+wentzeld@users.noreply.github.com>
Date: Fri, 4 Aug 2023 05:50:54 -0700
Subject: [PATCH 02/33] Update AutomationRegistryInterface2_0.sol

Support getMinBalanceForUpkeep
---
 .../interfaces/v2_0/AutomationRegistryInterface2_0.sol       | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/contracts/src/v0.8/automation/interfaces/v2_0/AutomationRegistryInterface2_0.sol b/contracts/src/v0.8/automation/interfaces/v2_0/AutomationRegistryInterface2_0.sol
index cfc9ab80c71..c0d77ca59fb 100644
--- a/contracts/src/v0.8/automation/interfaces/v2_0/AutomationRegistryInterface2_0.sol
+++ b/contracts/src/v0.8/automation/interfaces/v2_0/AutomationRegistryInterface2_0.sol
@@ -146,6 +146,11 @@ interface AutomationRegistryBaseInterface {
       address[] memory transmitters,
       uint8 f
     );
+
+    function getMinBalanceForUpkeep(uint256 id)
+        external
+        view
+        returns (uint96 minBalance);
 }
 
 /**

From a6f2ac081dbc35bc994c8dd1f5f7f4339e18000f Mon Sep 17 00:00:00 2001
From: Ryan Hall <hall.ryan.r@gmail.com>
Date: Wed, 1 Nov 2023 14:54:19 -0400
Subject: [PATCH 03/33] reformat

---
 .../upkeeps/UpkeepBalanceMonWithBuffer.sol    | 533 ++++++++----------
 1 file changed, 237 insertions(+), 296 deletions(-)

diff --git a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol
index 19d042f621b..bcb3b0e78fd 100644
--- a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol
+++ b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol
@@ -2,333 +2,274 @@
 
 pragma solidity 0.8.6;
 
-import "@chainlink/contracts/src/v0.8/ConfirmedOwner.sol";
-//import {AutomationRegistryInterface, State, Config} from "@chainlink/contracts/src/v0.8/interfaces/AutomationRegistryInterface1_2.sol";
-import {AutomationRegistryInterface, State, UpkeepInfo} from "@chainlink/contracts/src/v0.8/interfaces/AutomationRegistryInterface2_0.sol";
-import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol";
+import "../../../ConfirmedOwner.sol";
+import {IKeeperRegistryMaster} from "../2_1/interfaces/IKeeperRegistryMaster.sol";
+import {LinkTokenInterface} from "../../../interfaces/LinkTokenInterface.sol";
 import "@openzeppelin/contracts/security/Pausable.sol";
-import "@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol";
 
 /**
  * @title The UpkeepBalanceMonitor contract.
  * @notice A keeper-compatible contract that monitors and funds Chainlink Automation upkeeps.
  */
 contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
-    LinkTokenInterface public LINKTOKEN;
-    AutomationRegistryInterface public REGISTRY;
+  LinkTokenInterface public LINKTOKEN;
+  IKeeperRegistryMaster public REGISTRY;
 
-    uint256 private constant MIN_GAS_FOR_TRANSFER = 55_000;
+  uint256 private constant MIN_GAS_FOR_TRANSFER = 55_000;
 
-    bytes4 fundSig = REGISTRY.addFunds.selector;
+  bytes4 fundSig = REGISTRY.addFunds.selector;
 
-    event FundsAdded(uint256 amountAdded, uint256 newBalance, address sender);
-    event FundsWithdrawn(uint256 amountWithdrawn, address payee);
-    event TopUpSucceeded(uint256 indexed upkeepId);
-    event TopUpFailed(uint256 indexed upkeepId);
-    event KeeperRegistryAddressUpdated(address oldAddress, address newAddress);
-    event LinkTokenAddressUpdated(address oldAddress, address newAddress);
-    event MinWaitPeriodUpdated(
-        uint256 oldMinWaitPeriod,
-        uint256 newMinWaitPeriod
-    );
-    event OutOfGas(uint256 lastId);
+  event FundsAdded(uint256 amountAdded, uint256 newBalance, address sender);
+  event FundsWithdrawn(uint256 amountWithdrawn, address payee);
+  event TopUpSucceeded(uint256 indexed upkeepId);
+  event TopUpFailed(uint256 indexed upkeepId);
+  event KeeperRegistryAddressUpdated(address oldAddress, address newAddress);
+  event LinkTokenAddressUpdated(address oldAddress, address newAddress);
+  event MinWaitPeriodUpdated(uint256 oldMinWaitPeriod, uint256 newMinWaitPeriod);
+  event OutOfGas(uint256 lastId);
 
-    error InvalidWatchList();
-    error OnlyKeeperRegistry();
-    error DuplicateSubcriptionId(uint256 duplicate);
+  error InvalidWatchList();
+  error OnlyKeeperRegistry();
+  error DuplicateSubcriptionId(uint256 duplicate);
 
-    struct Target {
-        bool isActive;
-        uint96 minBalanceJuels;
-        uint96 topUpAmountJuels;
-        uint56 lastTopUpTimestamp;
-    }
+  struct Target {
+    bool isActive;
+    uint96 minBalanceJuels;
+    uint96 topUpAmountJuels;
+    uint56 lastTopUpTimestamp;
+  }
 
-    address public s_keeperRegistryAddress; // the address of the keeper registry
-    uint256 public s_minWaitPeriodSeconds; // minimum time to wait between top-ups
-    uint256[] public s_watchList; // the watchlist on which subscriptions are stored
-    mapping(uint256 => Target) internal s_targets;
+  address public s_keeperRegistryAddress; // the address of the keeper registry
+  uint256 public s_minWaitPeriodSeconds; // minimum time to wait between top-ups
+  uint256[] public s_watchList; // the watchlist on which subscriptions are stored
+  mapping(uint256 => Target) internal s_targets;
 
-    /**
-     * @param linkTokenAddress the Link token address
-     * @param keeperRegistryAddress the address of the keeper registry contract
-     * @param minWaitPeriodSeconds the minimum wait period for addresses between funding
-     */
-    constructor(
-        address linkTokenAddress,
-        address keeperRegistryAddress,
-        uint256 minWaitPeriodSeconds
-    ) ConfirmedOwner(msg.sender) {
-        setLinkTokenAddress(linkTokenAddress); //	0x326C977E6efc84E512bB9C30f76E30c160eD06FB
-        setKeeperRegistryAddress(keeperRegistryAddress); // 0xE16Df59B887e3Caa439E0b29B42bA2e7976FD8b2
-        setMinWaitPeriodSeconds(minWaitPeriodSeconds); //0
-        TransferHelper.safeApprove(
-            linkTokenAddress,
-            address(keeperRegistryAddress),
-            type(uint256).max
-        );
-    }
+  /**
+   * @param linkTokenAddress the Link token address
+   * @param keeperRegistryAddress the address of the keeper registry contract
+   * @param minWaitPeriodSeconds the minimum wait period for addresses between funding
+   */
+  constructor(
+    address linkTokenAddress,
+    address keeperRegistryAddress,
+    uint256 minWaitPeriodSeconds
+  ) ConfirmedOwner(msg.sender) {
+    require(linkTokenAddress != address(0));
+    LINKTOKEN = LinkTokenInterface(linkTokenAddress);
+    setKeeperRegistryAddress(keeperRegistryAddress); // 0xE16Df59B887e3Caa439E0b29B42bA2e7976FD8b2
+    setMinWaitPeriodSeconds(minWaitPeriodSeconds); //0
+    LINKTOKEN.approve(keeperRegistryAddress, type(uint256).max);
+  }
 
-    /**
-     * @notice Sets the list of upkeeps to watch and their funding parameters.
-     * @param upkeepIDs the list of subscription ids to watch
-     * @param minBalancesJuels the minimum balances for each upkeep
-     * @param topUpAmountsJuels the amount to top up each upkeep
-     */
-    function setWatchList(
-        uint256[] calldata upkeepIDs,
-        uint96[] calldata minBalancesJuels,
-        uint96[] calldata topUpAmountsJuels
-    ) external onlyOwner {
-        if (
-            upkeepIDs.length != minBalancesJuels.length ||
-            upkeepIDs.length != topUpAmountsJuels.length
-        ) {
-            revert InvalidWatchList();
-        }
-        uint256[] memory oldWatchList = s_watchList;
-        for (uint256 idx = 0; idx < oldWatchList.length; idx++) {
-            s_targets[oldWatchList[idx]].isActive = false;
-        }
-        for (uint256 idx = 0; idx < upkeepIDs.length; idx++) {
-            if (s_targets[upkeepIDs[idx]].isActive) {
-                revert DuplicateSubcriptionId(upkeepIDs[idx]);
-            }
-            if (upkeepIDs[idx] == 0) {
-                revert InvalidWatchList();
-            }
-            if (topUpAmountsJuels[idx] <= minBalancesJuels[idx]) {
-                revert InvalidWatchList();
-            }
-            s_targets[upkeepIDs[idx]] = Target({
-                isActive: true,
-                minBalanceJuels: minBalancesJuels[idx],
-                topUpAmountJuels: topUpAmountsJuels[idx],
-                lastTopUpTimestamp: 0
-            });
-        }
-        s_watchList = upkeepIDs;
+  /**
+   * @notice Sets the list of upkeeps to watch and their funding parameters.
+   * @param upkeepIDs the list of subscription ids to watch
+   * @param minBalancesJuels the minimum balances for each upkeep
+   * @param topUpAmountsJuels the amount to top up each upkeep
+   */
+  function setWatchList(
+    uint256[] calldata upkeepIDs,
+    uint96[] calldata minBalancesJuels,
+    uint96[] calldata topUpAmountsJuels
+  ) external onlyOwner {
+    if (upkeepIDs.length != minBalancesJuels.length || upkeepIDs.length != topUpAmountsJuels.length) {
+      revert InvalidWatchList();
     }
-
-    /**
-     * @notice Gets a list of upkeeps that are underfunded.
-     * @return list of upkeeps that are underfunded
-     */
-    function getUnderfundedUpkeeps() public view returns (uint256[] memory) {
-        uint256[] memory watchList = s_watchList;
-        uint256[] memory needsFunding = new uint256[](watchList.length);
-        uint256 count = 0;
-        uint256 minWaitPeriod = s_minWaitPeriodSeconds;
-        uint256 contractBalance = LINKTOKEN.balanceOf(address(this));
-        Target memory target;
-        for (uint256 idx = 0; idx < watchList.length; idx++) {
-            target = s_targets[watchList[idx]];
-            //( , , , uint96 upkeepBalance, , , ,) = REGISTRY.getUpkeep(watchList[idx]); <- for 1.2
-            UpkeepInfo memory upkeepInfo; //2.0
-            upkeepInfo = REGISTRY.getUpkeep(watchList[idx]); //2.0
-            uint96 upkeepBalance = upkeepInfo.balance; //2.0
-            uint96 minUpkeepBalance = REGISTRY.getMinBalanceForUpkeep(
-                watchList[idx]
-            );
-            uint96 minBalanceWithBuffer = getBalanceWithBuffer(
-                minUpkeepBalance
-            );
-            if (
-                target.lastTopUpTimestamp + minWaitPeriod <= block.timestamp &&
-                contractBalance >= target.topUpAmountJuels &&
-                (upkeepBalance < target.minBalanceJuels ||
-                    //upkeepBalance < minUpkeepBalance)
-                    upkeepBalance < minBalanceWithBuffer)
-            ) {
-                needsFunding[count] = watchList[idx];
-                count++;
-                contractBalance -= target.topUpAmountJuels;
-            }
-        }
-        if (count < watchList.length) {
-            assembly {
-                mstore(needsFunding, count)
-            }
-        }
-        return needsFunding;
+    uint256[] memory oldWatchList = s_watchList;
+    for (uint256 idx = 0; idx < oldWatchList.length; idx++) {
+      s_targets[oldWatchList[idx]].isActive = false;
     }
-
-    /**
-     * @notice Send funds to the upkeeps provided.
-     * @param needsFunding the list of upkeeps to fund
-     */
-    function topUp(uint256[] memory needsFunding) public whenNotPaused {
-        uint256 minWaitPeriodSeconds = s_minWaitPeriodSeconds;
-        uint256 contractBalance = LINKTOKEN.balanceOf(address(this));
-        Target memory target;
-        for (uint256 idx = 0; idx < needsFunding.length; idx++) {
-            target = s_targets[needsFunding[idx]];
-            //( , , , uint96 upkeepBalance, , , ,) = REGISTRY.getUpkeep(needsFunding[idx]); <- for 1.2
-            UpkeepInfo memory upkeepInfo; //2.0
-            upkeepInfo = REGISTRY.getUpkeep(needsFunding[idx]); //2.0
-            uint96 upkeepBalance = upkeepInfo.balance; //2.0
-            uint96 minUpkeepBalance = REGISTRY.getMinBalanceForUpkeep(
-                needsFunding[idx]
-            );
-            uint96 minBalanceWithBuffer = getBalanceWithBuffer(
-                minUpkeepBalance
-            );
-            if (
-                target.isActive &&
-                target.lastTopUpTimestamp + minWaitPeriodSeconds <=
-                block.timestamp &&
-                (upkeepBalance < target.minBalanceJuels ||
-                    //upkeepBalance < minUpkeepBalance) &&
-                    upkeepBalance < minBalanceWithBuffer) &&
-                contractBalance >= target.topUpAmountJuels
-            ) {
-                REGISTRY.addFunds(needsFunding[idx], target.topUpAmountJuels);
-                s_targets[needsFunding[idx]].lastTopUpTimestamp = uint56(
-                    block.timestamp
-                );
-                contractBalance -= target.topUpAmountJuels;
-                emit TopUpSucceeded(needsFunding[idx]);
-            }
-            if (gasleft() < MIN_GAS_FOR_TRANSFER) {
-                emit OutOfGas(idx);
-                return;
-            }
-        }
+    for (uint256 idx = 0; idx < upkeepIDs.length; idx++) {
+      if (s_targets[upkeepIDs[idx]].isActive) {
+        revert DuplicateSubcriptionId(upkeepIDs[idx]);
+      }
+      if (upkeepIDs[idx] == 0) {
+        revert InvalidWatchList();
+      }
+      if (topUpAmountsJuels[idx] <= minBalancesJuels[idx]) {
+        revert InvalidWatchList();
+      }
+      s_targets[upkeepIDs[idx]] = Target({
+        isActive: true,
+        minBalanceJuels: minBalancesJuels[idx],
+        topUpAmountJuels: topUpAmountsJuels[idx],
+        lastTopUpTimestamp: 0
+      });
     }
+    s_watchList = upkeepIDs;
+  }
 
-    /**
-     * @notice Gets list of upkeeps ids that are underfunded and returns a keeper-compatible payload.
-     * @return upkeepNeeded signals if upkeep is needed, performData is an abi encoded list of subscription ids that need funds
-     */
-    function checkUpkeep(bytes calldata)
-        external
-        view
-        whenNotPaused
-        returns (bool upkeepNeeded, bytes memory performData)
-    {
-        uint256[] memory needsFunding = getUnderfundedUpkeeps();
-        upkeepNeeded = needsFunding.length > 0;
-        performData = abi.encode(needsFunding);
-        return (upkeepNeeded, performData);
+  /**
+   * @notice Gets a list of upkeeps that are underfunded.
+   * @return list of upkeeps that are underfunded
+   */
+  function getUnderfundedUpkeeps() public view returns (uint256[] memory) {
+    uint256[] memory watchList = s_watchList;
+    uint256[] memory needsFunding = new uint256[](watchList.length);
+    uint256 count = 0;
+    uint256 minWaitPeriod = s_minWaitPeriodSeconds;
+    uint256 contractBalance = LINKTOKEN.balanceOf(address(this));
+    Target memory target;
+    for (uint256 idx = 0; idx < watchList.length; idx++) {
+      target = s_targets[watchList[idx]];
+      //( , , , uint96 upkeepBalance, , , ,) = REGISTRY.getUpkeep(watchList[idx]); <- for 1.2
+      UpkeepInfo memory upkeepInfo; //2.0
+      upkeepInfo = REGISTRY.getUpkeep(watchList[idx]); //2.0
+      uint96 upkeepBalance = upkeepInfo.balance; //2.0
+      uint96 minUpkeepBalance = REGISTRY.getMinBalanceForUpkeep(watchList[idx]);
+      uint96 minBalanceWithBuffer = getBalanceWithBuffer(minUpkeepBalance);
+      if (
+        target.lastTopUpTimestamp + minWaitPeriod <= block.timestamp &&
+        contractBalance >= target.topUpAmountJuels &&
+        (upkeepBalance < target.minBalanceJuels ||
+          //upkeepBalance < minUpkeepBalance)
+          upkeepBalance < minBalanceWithBuffer)
+      ) {
+        needsFunding[count] = watchList[idx];
+        count++;
+        contractBalance -= target.topUpAmountJuels;
+      }
     }
-
-    /**
-     * @notice Called by the keeper to send funds to underfunded addresses.
-     * @param performData the abi encoded list of addresses to fund
-     */
-    function performUpkeep(bytes calldata performData)
-        external
-        onlyKeeperRegistry
-        whenNotPaused
-    {
-        uint256[] memory needsFunding = abi.decode(performData, (uint256[]));
-        topUp(needsFunding);
+    if (count < watchList.length) {
+      assembly {
+        mstore(needsFunding, count)
+      }
     }
+    return needsFunding;
+  }
 
-    /**
-     * @notice Withdraws the contract balance in LINK.
-     * @param amount the amount of LINK (in juels) to withdraw
-     * @param payee the address to pay
-     */
-    function withdraw(uint256 amount, address payable payee)
-        external
-        onlyOwner
-    {
-        require(payee != address(0));
-        emit FundsWithdrawn(amount, payee);
-        LINKTOKEN.transfer(payee, amount);
+  /**
+   * @notice Send funds to the upkeeps provided.
+   * @param needsFunding the list of upkeeps to fund
+   */
+  function topUp(uint256[] memory needsFunding) public whenNotPaused {
+    uint256 minWaitPeriodSeconds = s_minWaitPeriodSeconds;
+    uint256 contractBalance = LINKTOKEN.balanceOf(address(this));
+    Target memory target;
+    for (uint256 idx = 0; idx < needsFunding.length; idx++) {
+      target = s_targets[needsFunding[idx]];
+      //( , , , uint96 upkeepBalance, , , ,) = REGISTRY.getUpkeep(needsFunding[idx]); <- for 1.2
+      UpkeepInfo memory upkeepInfo; //2.0
+      upkeepInfo = REGISTRY.getUpkeep(needsFunding[idx]); //2.0
+      uint96 upkeepBalance = upkeepInfo.balance; //2.0
+      uint96 minUpkeepBalance = REGISTRY.getMinBalanceForUpkeep(needsFunding[idx]);
+      uint96 minBalanceWithBuffer = getBalanceWithBuffer(minUpkeepBalance);
+      if (
+        target.isActive &&
+        target.lastTopUpTimestamp + minWaitPeriodSeconds <= block.timestamp &&
+        (upkeepBalance < target.minBalanceJuels ||
+          //upkeepBalance < minUpkeepBalance) &&
+          upkeepBalance < minBalanceWithBuffer) &&
+        contractBalance >= target.topUpAmountJuels
+      ) {
+        REGISTRY.addFunds(needsFunding[idx], target.topUpAmountJuels);
+        s_targets[needsFunding[idx]].lastTopUpTimestamp = uint56(block.timestamp);
+        contractBalance -= target.topUpAmountJuels;
+        emit TopUpSucceeded(needsFunding[idx]);
+      }
+      if (gasleft() < MIN_GAS_FOR_TRANSFER) {
+        emit OutOfGas(idx);
+        return;
+      }
     }
+  }
 
-    /**
-     * @notice Sets the LINK token address.
-     */
-    function setLinkTokenAddress(address linkTokenAddress) public onlyOwner {
-        require(linkTokenAddress != address(0));
-        emit LinkTokenAddressUpdated(address(LINKTOKEN), linkTokenAddress);
-        LINKTOKEN = LinkTokenInterface(linkTokenAddress);
-    }
+  /**
+   * @notice Gets list of upkeeps ids that are underfunded and returns a keeper-compatible payload.
+   * @return upkeepNeeded signals if upkeep is needed, performData is an abi encoded list of subscription ids that need funds
+   */
+  function checkUpkeep(
+    bytes calldata
+  ) external view whenNotPaused returns (bool upkeepNeeded, bytes memory performData) {
+    uint256[] memory needsFunding = getUnderfundedUpkeeps();
+    upkeepNeeded = needsFunding.length > 0;
+    performData = abi.encode(needsFunding);
+    return (upkeepNeeded, performData);
+  }
 
-    /**
-     * @notice Sets the keeper registry address.
-     */
-    function setKeeperRegistryAddress(address keeperRegistryAddress)
-        public
-        onlyOwner
-    {
-        require(keeperRegistryAddress != address(0));
-        emit KeeperRegistryAddressUpdated(
-            s_keeperRegistryAddress,
-            keeperRegistryAddress
-        );
-        s_keeperRegistryAddress = keeperRegistryAddress;
-        REGISTRY = AutomationRegistryInterface(keeperRegistryAddress);
-    }
+  /**
+   * @notice Called by the keeper to send funds to underfunded addresses.
+   * @param performData the abi encoded list of addresses to fund
+   */
+  function performUpkeep(bytes calldata performData) external onlyKeeperRegistry whenNotPaused {
+    uint256[] memory needsFunding = abi.decode(performData, (uint256[]));
+    topUp(needsFunding);
+  }
 
-    /**
-     * @notice Sets the minimum wait period (in seconds) for upkeep ids between funding.
-     */
-    function setMinWaitPeriodSeconds(uint256 period) public onlyOwner {
-        emit MinWaitPeriodUpdated(s_minWaitPeriodSeconds, period);
-        s_minWaitPeriodSeconds = period;
-    }
+  /**
+   * @notice Withdraws the contract balance in LINK.
+   * @param amount the amount of LINK (in juels) to withdraw
+   * @param payee the address to pay
+   */
+  function withdraw(uint256 amount, address payable payee) external onlyOwner {
+    require(payee != address(0));
+    emit FundsWithdrawn(amount, payee);
+    LINKTOKEN.transfer(payee, amount);
+  }
 
-    /**
-     * @notice Gets configuration information for a upkeep on the watchlist.
-     */
-    function getUpkeepInfo(uint256 upkeepId)
-        external
-        view
-        returns (
-            bool isActive,
-            uint96 minBalanceJuels,
-            uint96 topUpAmountJuels,
-            uint56 lastTopUpTimestamp
-        )
-    {
-        Target memory target = s_targets[upkeepId];
-        return (
-            target.isActive,
-            target.minBalanceJuels,
-            target.topUpAmountJuels,
-            target.lastTopUpTimestamp
-        );
-    }
+  /**
+   * @notice Sets the keeper registry address.
+   */
+  function setKeeperRegistryAddress(address keeperRegistryAddress) public onlyOwner {
+    require(keeperRegistryAddress != address(0));
+    emit KeeperRegistryAddressUpdated(s_keeperRegistryAddress, keeperRegistryAddress);
+    s_keeperRegistryAddress = keeperRegistryAddress;
+    REGISTRY = IKeeperRegistryMaster(keeperRegistryAddress);
+  }
 
-    /**
-     * @notice Gets the list of upkeeps ids being watched.
-     */
-    function getWatchList() external view returns (uint256[] memory) {
-        return s_watchList;
-    }
+  /**
+   * @notice Sets the minimum wait period (in seconds) for upkeep ids between funding.
+   */
+  function setMinWaitPeriodSeconds(uint256 period) public onlyOwner {
+    emit MinWaitPeriodUpdated(s_minWaitPeriodSeconds, period);
+    s_minWaitPeriodSeconds = period;
+  }
 
-    /**
-     * @notice Pause the contract, which prevents executing performUpkeep.
-     */
-    function pause() external onlyOwner {
-        _pause();
-    }
+  /**
+   * @notice Gets configuration information for a upkeep on the watchlist.
+   */
+  function getUpkeepInfo(
+    uint256 upkeepId
+  ) external view returns (bool isActive, uint96 minBalanceJuels, uint96 topUpAmountJuels, uint56 lastTopUpTimestamp) {
+    Target memory target = s_targets[upkeepId];
+    return (target.isActive, target.minBalanceJuels, target.topUpAmountJuels, target.lastTopUpTimestamp);
+  }
 
-    /**
-     * @notice Unpause the contract.
-     */
-    function unpause() external onlyOwner {
-        _unpause();
-    }
+  /**
+   * @notice Gets the list of upkeeps ids being watched.
+   */
+  function getWatchList() external view returns (uint256[] memory) {
+    return s_watchList;
+  }
 
-    /**
-     * @notice Called to add buffer to minimum balance of upkeeps
-     * @param num the current minimum balance 
-     */
-    function getBalanceWithBuffer(uint96 num) internal pure returns (uint96) {
-        uint96 buffer = 20;
-        uint96 result = uint96((uint256(num) * (100 + buffer)) / 100); // convert to uint256 to prevent overflow
-        return result;
-    }
+  /**
+   * @notice Pause the contract, which prevents executing performUpkeep.
+   */
+  function pause() external onlyOwner {
+    _pause();
+  }
+
+  /**
+   * @notice Unpause the contract.
+   */
+  function unpause() external onlyOwner {
+    _unpause();
+  }
+
+  /**
+   * @notice Called to add buffer to minimum balance of upkeeps
+   * @param num the current minimum balance
+   */
+  function getBalanceWithBuffer(uint96 num) internal pure returns (uint96) {
+    uint96 buffer = 20;
+    uint96 result = uint96((uint256(num) * (100 + buffer)) / 100); // convert to uint256 to prevent overflow
+    return result;
+  }
 
-    modifier onlyKeeperRegistry() {
-        if (msg.sender != s_keeperRegistryAddress) {
-            revert OnlyKeeperRegistry();
-        }
-        _;
+  modifier onlyKeeperRegistry() {
+    if (msg.sender != s_keeperRegistryAddress) {
+      revert OnlyKeeperRegistry();
     }
+    _;
+  }
 }

From 5c64517df0bd6d6e081c4d3adda59c61b3ff83a8 Mon Sep 17 00:00:00 2001
From: Ryan Hall <hall.ryan.r@gmail.com>
Date: Thu, 2 Nov 2023 13:31:16 -0400
Subject: [PATCH 04/33] remove modifier

---
 .../upkeeps/UpkeepBalanceMonWithBuffer.sol    | 27 ++++++-------------
 1 file changed, 8 insertions(+), 19 deletions(-)

diff --git a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol
index bcb3b0e78fd..72c37ddf5ff 100644
--- a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol
+++ b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol
@@ -83,10 +83,7 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
       if (s_targets[upkeepIDs[idx]].isActive) {
         revert DuplicateSubcriptionId(upkeepIDs[idx]);
       }
-      if (upkeepIDs[idx] == 0) {
-        revert InvalidWatchList();
-      }
-      if (topUpAmountsJuels[idx] <= minBalancesJuels[idx]) {
+      if (upkeepIDs[idx] == 0 || topUpAmountsJuels[idx] == 0) {
         revert InvalidWatchList();
       }
       s_targets[upkeepIDs[idx]] = Target({
@@ -110,13 +107,11 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
     uint256 minWaitPeriod = s_minWaitPeriodSeconds;
     uint256 contractBalance = LINKTOKEN.balanceOf(address(this));
     Target memory target;
+
     for (uint256 idx = 0; idx < watchList.length; idx++) {
       target = s_targets[watchList[idx]];
-      //( , , , uint96 upkeepBalance, , , ,) = REGISTRY.getUpkeep(watchList[idx]); <- for 1.2
-      UpkeepInfo memory upkeepInfo; //2.0
-      upkeepInfo = REGISTRY.getUpkeep(watchList[idx]); //2.0
-      uint96 upkeepBalance = upkeepInfo.balance; //2.0
-      uint96 minUpkeepBalance = REGISTRY.getMinBalanceForUpkeep(watchList[idx]);
+      uint96 balance = REGISTRY.getBalance(watchList[idx]);
+      uint96 minUpkeepBalance = REGISTRY.getMinBalance(watchList[idx]);
       uint96 minBalanceWithBuffer = getBalanceWithBuffer(minUpkeepBalance);
       if (
         target.lastTopUpTimestamp + minWaitPeriod <= block.timestamp &&
@@ -149,9 +144,9 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
     for (uint256 idx = 0; idx < needsFunding.length; idx++) {
       target = s_targets[needsFunding[idx]];
       //( , , , uint96 upkeepBalance, , , ,) = REGISTRY.getUpkeep(needsFunding[idx]); <- for 1.2
-      UpkeepInfo memory upkeepInfo; //2.0
-      upkeepInfo = REGISTRY.getUpkeep(needsFunding[idx]); //2.0
-      uint96 upkeepBalance = upkeepInfo.balance; //2.0
+      UpkeepInfo memory upkeepInfo;
+      upkeepInfo = REGISTRY.getUpkeep(needsFunding[idx]);
+      uint96 upkeepBalance = upkeepInfo.balance;
       uint96 minUpkeepBalance = REGISTRY.getMinBalanceForUpkeep(needsFunding[idx]);
       uint96 minBalanceWithBuffer = getBalanceWithBuffer(minUpkeepBalance);
       if (
@@ -192,6 +187,7 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
    * @param performData the abi encoded list of addresses to fund
    */
   function performUpkeep(bytes calldata performData) external onlyKeeperRegistry whenNotPaused {
+    if (msg.sender != s_keeperRegistryAddress) revert OnlyKeeperRegistry();
     uint256[] memory needsFunding = abi.decode(performData, (uint256[]));
     topUp(needsFunding);
   }
@@ -265,11 +261,4 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
     uint96 result = uint96((uint256(num) * (100 + buffer)) / 100); // convert to uint256 to prevent overflow
     return result;
   }
-
-  modifier onlyKeeperRegistry() {
-    if (msg.sender != s_keeperRegistryAddress) {
-      revert OnlyKeeperRegistry();
-    }
-    _;
-  }
 }

From 13e990c5a7469fff4fe680c621465a161bfc5101 Mon Sep 17 00:00:00 2001
From: Ryan Hall <hall.ryan.r@gmail.com>
Date: Thu, 2 Nov 2023 13:48:08 -0400
Subject: [PATCH 05/33] add typeAndVersion check in constructor

---
 .../upkeeps/UpkeepBalanceMonWithBuffer.sol    | 19 ++++++++++++-------
 1 file changed, 12 insertions(+), 7 deletions(-)

diff --git a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol
index 72c37ddf5ff..93e603affc4 100644
--- a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol
+++ b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol
@@ -5,6 +5,7 @@ pragma solidity 0.8.6;
 import "../../../ConfirmedOwner.sol";
 import {IKeeperRegistryMaster} from "../2_1/interfaces/IKeeperRegistryMaster.sol";
 import {LinkTokenInterface} from "../../../interfaces/LinkTokenInterface.sol";
+import {ITypeAndVersion} from "../../../interfaces/ITypeAndVersion.sol";
 import "@openzeppelin/contracts/security/Pausable.sol";
 
 /**
@@ -28,9 +29,10 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   event MinWaitPeriodUpdated(uint256 oldMinWaitPeriod, uint256 newMinWaitPeriod);
   event OutOfGas(uint256 lastId);
 
+  error DuplicateSubcriptionId(uint256 duplicate);
+  error InvalidKeeperRegistryVersion();
   error InvalidWatchList();
   error OnlyKeeperRegistry();
-  error DuplicateSubcriptionId(uint256 duplicate);
 
   struct Target {
     bool isActive;
@@ -59,6 +61,12 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
     setKeeperRegistryAddress(keeperRegistryAddress); // 0xE16Df59B887e3Caa439E0b29B42bA2e7976FD8b2
     setMinWaitPeriodSeconds(minWaitPeriodSeconds); //0
     LINKTOKEN.approve(keeperRegistryAddress, type(uint256).max);
+    if (
+      keccak256(bytes(ITypeAndVersion(keeperRegistryAddress).typeAndVersion())) !=
+      keccak256(bytes("KeeperRegistry 2.1.0"))
+    ) {
+      revert InvalidKeeperRegistryVersion();
+    }
   }
 
   /**
@@ -110,7 +118,7 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
 
     for (uint256 idx = 0; idx < watchList.length; idx++) {
       target = s_targets[watchList[idx]];
-      uint96 balance = REGISTRY.getBalance(watchList[idx]);
+      uint96 upkeepBalance = REGISTRY.getBalance(watchList[idx]);
       uint96 minUpkeepBalance = REGISTRY.getMinBalance(watchList[idx]);
       uint96 minBalanceWithBuffer = getBalanceWithBuffer(minUpkeepBalance);
       if (
@@ -143,10 +151,7 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
     Target memory target;
     for (uint256 idx = 0; idx < needsFunding.length; idx++) {
       target = s_targets[needsFunding[idx]];
-      //( , , , uint96 upkeepBalance, , , ,) = REGISTRY.getUpkeep(needsFunding[idx]); <- for 1.2
-      UpkeepInfo memory upkeepInfo;
-      upkeepInfo = REGISTRY.getUpkeep(needsFunding[idx]);
-      uint96 upkeepBalance = upkeepInfo.balance;
+      uint96 upkeepBalance = REGISTRY.getBalance(needsFunding[idx]);
       uint96 minUpkeepBalance = REGISTRY.getMinBalanceForUpkeep(needsFunding[idx]);
       uint96 minBalanceWithBuffer = getBalanceWithBuffer(minUpkeepBalance);
       if (
@@ -186,7 +191,7 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
    * @notice Called by the keeper to send funds to underfunded addresses.
    * @param performData the abi encoded list of addresses to fund
    */
-  function performUpkeep(bytes calldata performData) external onlyKeeperRegistry whenNotPaused {
+  function performUpkeep(bytes calldata performData) external whenNotPaused {
     if (msg.sender != s_keeperRegistryAddress) revert OnlyKeeperRegistry();
     uint256[] memory needsFunding = abi.decode(performData, (uint256[]));
     topUp(needsFunding);

From 10956d0fcb8dbf8a9862ef3c9aadc41e6c3420c7 Mon Sep 17 00:00:00 2001
From: Ryan Hall <hall.ryan.r@gmail.com>
Date: Thu, 2 Nov 2023 13:50:22 -0400
Subject: [PATCH 06/33] restore AutomationRegistryInterface2_0.sol function

---
 .../interfaces/v2_0/AutomationRegistryInterface2_0.sol       | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/contracts/src/v0.8/automation/interfaces/v2_0/AutomationRegistryInterface2_0.sol b/contracts/src/v0.8/automation/interfaces/v2_0/AutomationRegistryInterface2_0.sol
index c0d77ca59fb..cfc9ab80c71 100644
--- a/contracts/src/v0.8/automation/interfaces/v2_0/AutomationRegistryInterface2_0.sol
+++ b/contracts/src/v0.8/automation/interfaces/v2_0/AutomationRegistryInterface2_0.sol
@@ -146,11 +146,6 @@ interface AutomationRegistryBaseInterface {
       address[] memory transmitters,
       uint8 f
     );
-
-    function getMinBalanceForUpkeep(uint256 id)
-        external
-        view
-        returns (uint96 minBalance);
 }
 
 /**

From 16a7fec9b384cf40f70484a3c758d2972cc35587 Mon Sep 17 00:00:00 2001
From: Ryan Hall <hall.ryan.r@gmail.com>
Date: Thu, 2 Nov 2023 13:57:47 -0400
Subject: [PATCH 07/33] update import paths

---
 .../automation/upkeeps/UpkeepBalanceMonWithBuffer.sol     | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol
index 93e603affc4..6c5aa155e1a 100644
--- a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol
+++ b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol
@@ -2,10 +2,10 @@
 
 pragma solidity 0.8.6;
 
-import "../../../ConfirmedOwner.sol";
-import {IKeeperRegistryMaster} from "../2_1/interfaces/IKeeperRegistryMaster.sol";
-import {LinkTokenInterface} from "../../../interfaces/LinkTokenInterface.sol";
-import {ITypeAndVersion} from "../../../interfaces/ITypeAndVersion.sol";
+import "../../shared/access/ConfirmedOwner.sol";
+import {IKeeperRegistryMaster} from "../interfaces/v2_1/IKeeperRegistryMaster.sol";
+import {LinkTokenInterface} from "../../shared/interfaces/LinkTokenInterface.sol";
+import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol";
 import "@openzeppelin/contracts/security/Pausable.sol";
 
 /**

From ddefd43eccde50f7f6b548af33fcaecc9d21f8d4 Mon Sep 17 00:00:00 2001
From: Ryan Hall <hall.ryan.r@gmail.com>
Date: Thu, 2 Nov 2023 16:37:05 -0400
Subject: [PATCH 08/33] update comments

---
 .../upkeeps/UpkeepBalanceMonWithBuffer.sol    | 94 +++++++------------
 1 file changed, 34 insertions(+), 60 deletions(-)

diff --git a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol
index 6c5aa155e1a..80b916614d3 100644
--- a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol
+++ b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol
@@ -8,17 +8,14 @@ import {LinkTokenInterface} from "../../shared/interfaces/LinkTokenInterface.sol
 import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol";
 import "@openzeppelin/contracts/security/Pausable.sol";
 
-/**
- * @title The UpkeepBalanceMonitor contract.
- * @notice A keeper-compatible contract that monitors and funds Chainlink Automation upkeeps.
- */
+/// @title The UpkeepBalanceMonitor contract.
+/// @notice A keeper-compatible contract that monitors and funds Chainlink Automation upkeeps.
 contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   LinkTokenInterface public LINKTOKEN;
   IKeeperRegistryMaster public REGISTRY;
 
   uint256 private constant MIN_GAS_FOR_TRANSFER = 55_000;
-
-  bytes4 fundSig = REGISTRY.addFunds.selector;
+  bytes4 private fundSig = REGISTRY.addFunds.selector;
 
   event FundsAdded(uint256 amountAdded, uint256 newBalance, address sender);
   event FundsWithdrawn(uint256 amountWithdrawn, address payee);
@@ -46,11 +43,9 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   uint256[] public s_watchList; // the watchlist on which subscriptions are stored
   mapping(uint256 => Target) internal s_targets;
 
-  /**
-   * @param linkTokenAddress the Link token address
-   * @param keeperRegistryAddress the address of the keeper registry contract
-   * @param minWaitPeriodSeconds the minimum wait period for addresses between funding
-   */
+  /// @param linkTokenAddress the Link token address
+  /// @param keeperRegistryAddress the address of the keeper registry contract
+  /// @param minWaitPeriodSeconds the minimum wait period for addresses between funding
   constructor(
     address linkTokenAddress,
     address keeperRegistryAddress,
@@ -69,12 +64,10 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
     }
   }
 
-  /**
-   * @notice Sets the list of upkeeps to watch and their funding parameters.
-   * @param upkeepIDs the list of subscription ids to watch
-   * @param minBalancesJuels the minimum balances for each upkeep
-   * @param topUpAmountsJuels the amount to top up each upkeep
-   */
+  /// @notice Sets the list of upkeeps to watch and their funding parameters.
+  /// @param upkeepIDs the list of subscription ids to watch
+  /// @param minBalancesJuels the minimum balances for each upkeep
+  /// @param topUpAmountsJuels the amount to top up each upkeep
   function setWatchList(
     uint256[] calldata upkeepIDs,
     uint96[] calldata minBalancesJuels,
@@ -104,10 +97,8 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
     s_watchList = upkeepIDs;
   }
 
-  /**
-   * @notice Gets a list of upkeeps that are underfunded.
-   * @return list of upkeeps that are underfunded
-   */
+  /// @notice Gets a list of upkeeps that are underfunded.
+  /// @return list of upkeeps that are underfunded
   function getUnderfundedUpkeeps() public view returns (uint256[] memory) {
     uint256[] memory watchList = s_watchList;
     uint256[] memory needsFunding = new uint256[](watchList.length);
@@ -141,10 +132,8 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
     return needsFunding;
   }
 
-  /**
-   * @notice Send funds to the upkeeps provided.
-   * @param needsFunding the list of upkeeps to fund
-   */
+  /// @notice Send funds to the upkeeps provided.
+  /// @param needsFunding the list of upkeeps to fund
   function topUp(uint256[] memory needsFunding) public whenNotPaused {
     uint256 minWaitPeriodSeconds = s_minWaitPeriodSeconds;
     uint256 contractBalance = LINKTOKEN.balanceOf(address(this));
@@ -174,10 +163,8 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
     }
   }
 
-  /**
-   * @notice Gets list of upkeeps ids that are underfunded and returns a keeper-compatible payload.
-   * @return upkeepNeeded signals if upkeep is needed, performData is an abi encoded list of subscription ids that need funds
-   */
+  /// @notice Gets list of upkeeps ids that are underfunded and returns a keeper-compatible payload.
+  /// @return upkeepNeeded signals if upkeep is needed, performData is an abi encoded list of subscription ids that need funds
   function checkUpkeep(
     bytes calldata
   ) external view whenNotPaused returns (bool upkeepNeeded, bytes memory performData) {
@@ -187,30 +174,24 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
     return (upkeepNeeded, performData);
   }
 
-  /**
-   * @notice Called by the keeper to send funds to underfunded addresses.
-   * @param performData the abi encoded list of addresses to fund
-   */
+  /// @notice Called by the keeper to send funds to underfunded addresses.
+  /// @param performData the abi encoded list of addresses to fund
   function performUpkeep(bytes calldata performData) external whenNotPaused {
     if (msg.sender != s_keeperRegistryAddress) revert OnlyKeeperRegistry();
     uint256[] memory needsFunding = abi.decode(performData, (uint256[]));
     topUp(needsFunding);
   }
 
-  /**
-   * @notice Withdraws the contract balance in LINK.
-   * @param amount the amount of LINK (in juels) to withdraw
-   * @param payee the address to pay
-   */
+  /// @notice Withdraws the contract balance in LINK.
+  /// @param amount the amount of LINK (in juels) to withdraw
+  /// @param payee the address to pay
   function withdraw(uint256 amount, address payable payee) external onlyOwner {
     require(payee != address(0));
     emit FundsWithdrawn(amount, payee);
     LINKTOKEN.transfer(payee, amount);
   }
 
-  /**
-   * @notice Sets the keeper registry address.
-   */
+  /// @notice Sets the keeper registry address.
   function setKeeperRegistryAddress(address keeperRegistryAddress) public onlyOwner {
     require(keeperRegistryAddress != address(0));
     emit KeeperRegistryAddressUpdated(s_keeperRegistryAddress, keeperRegistryAddress);
@@ -218,17 +199,13 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
     REGISTRY = IKeeperRegistryMaster(keeperRegistryAddress);
   }
 
-  /**
-   * @notice Sets the minimum wait period (in seconds) for upkeep ids between funding.
-   */
+  /// @notice Sets the minimum wait period (in seconds) for upkeep ids between funding.
   function setMinWaitPeriodSeconds(uint256 period) public onlyOwner {
     emit MinWaitPeriodUpdated(s_minWaitPeriodSeconds, period);
     s_minWaitPeriodSeconds = period;
   }
 
-  /**
-   * @notice Gets configuration information for a upkeep on the watchlist.
-   */
+  /// @notice Gets configuration information for a upkeep on the watchlist.
   function getUpkeepInfo(
     uint256 upkeepId
   ) external view returns (bool isActive, uint96 minBalanceJuels, uint96 topUpAmountJuels, uint56 lastTopUpTimestamp) {
@@ -236,31 +213,28 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
     return (target.isActive, target.minBalanceJuels, target.topUpAmountJuels, target.lastTopUpTimestamp);
   }
 
-  /**
-   * @notice Gets the list of upkeeps ids being watched.
-   */
+  /// @notice Gets the keeper registry address
+  function getKeeperRegistryAddress() external view returns (address) {
+    return s_keeperRegistryAddress;
+  }
+
+  /// @notice Gets the list of upkeeps ids being watched.
   function getWatchList() external view returns (uint256[] memory) {
     return s_watchList;
   }
 
-  /**
-   * @notice Pause the contract, which prevents executing performUpkeep.
-   */
+  /// @notice Pause the contract, which prevents executing performUpkeep.
   function pause() external onlyOwner {
     _pause();
   }
 
-  /**
-   * @notice Unpause the contract.
-   */
+  /// @notice Unpause the contract.
   function unpause() external onlyOwner {
     _unpause();
   }
 
-  /**
-   * @notice Called to add buffer to minimum balance of upkeeps
-   * @param num the current minimum balance
-   */
+  /// @notice Called to add buffer to minimum balance of upkeeps
+  /// @param num the current minimum balance
   function getBalanceWithBuffer(uint96 num) internal pure returns (uint96) {
     uint96 buffer = 20;
     uint96 result = uint96((uint256(num) * (100 + buffer)) / 100); // convert to uint256 to prevent overflow

From 91d61fa67a2aaf0968bdccdd9ed5c1cc2bd22bf8 Mon Sep 17 00:00:00 2001
From: Ryan Hall <hall.ryan.r@gmail.com>
Date: Thu, 2 Nov 2023 16:39:15 -0400
Subject: [PATCH 09/33] cleanup

---
 .../upkeeps/UpkeepBalanceMonWithBuffer.sol    | 27 +++++++++++--------
 1 file changed, 16 insertions(+), 11 deletions(-)

diff --git a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol
index 80b916614d3..71a7dddb367 100644
--- a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol
+++ b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol
@@ -11,7 +11,7 @@ import "@openzeppelin/contracts/security/Pausable.sol";
 /// @title The UpkeepBalanceMonitor contract.
 /// @notice A keeper-compatible contract that monitors and funds Chainlink Automation upkeeps.
 contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
-  LinkTokenInterface public LINKTOKEN;
+  LinkTokenInterface public LINK_TOKEN;
   IKeeperRegistryMaster public REGISTRY;
 
   uint256 private constant MIN_GAS_FOR_TRANSFER = 55_000;
@@ -38,10 +38,10 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
     uint56 lastTopUpTimestamp;
   }
 
-  address public s_keeperRegistryAddress; // the address of the keeper registry
-  uint256 public s_minWaitPeriodSeconds; // minimum time to wait between top-ups
-  uint256[] public s_watchList; // the watchlist on which subscriptions are stored
-  mapping(uint256 => Target) internal s_targets;
+  address private s_keeperRegistryAddress; // the address of the keeper registry
+  uint256 private s_minWaitPeriodSeconds; // minimum time to wait between top-ups
+  uint256[] private s_watchList; // the watchlist on which subscriptions are stored
+  mapping(uint256 => Target) private s_targets;
 
   /// @param linkTokenAddress the Link token address
   /// @param keeperRegistryAddress the address of the keeper registry contract
@@ -52,10 +52,10 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
     uint256 minWaitPeriodSeconds
   ) ConfirmedOwner(msg.sender) {
     require(linkTokenAddress != address(0));
-    LINKTOKEN = LinkTokenInterface(linkTokenAddress);
+    LINK_TOKEN = LinkTokenInterface(linkTokenAddress);
     setKeeperRegistryAddress(keeperRegistryAddress); // 0xE16Df59B887e3Caa439E0b29B42bA2e7976FD8b2
     setMinWaitPeriodSeconds(minWaitPeriodSeconds); //0
-    LINKTOKEN.approve(keeperRegistryAddress, type(uint256).max);
+    LINK_TOKEN.approve(keeperRegistryAddress, type(uint256).max);
     if (
       keccak256(bytes(ITypeAndVersion(keeperRegistryAddress).typeAndVersion())) !=
       keccak256(bytes("KeeperRegistry 2.1.0"))
@@ -104,7 +104,7 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
     uint256[] memory needsFunding = new uint256[](watchList.length);
     uint256 count = 0;
     uint256 minWaitPeriod = s_minWaitPeriodSeconds;
-    uint256 contractBalance = LINKTOKEN.balanceOf(address(this));
+    uint256 contractBalance = LINK_TOKEN.balanceOf(address(this));
     Target memory target;
 
     for (uint256 idx = 0; idx < watchList.length; idx++) {
@@ -136,7 +136,7 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   /// @param needsFunding the list of upkeeps to fund
   function topUp(uint256[] memory needsFunding) public whenNotPaused {
     uint256 minWaitPeriodSeconds = s_minWaitPeriodSeconds;
-    uint256 contractBalance = LINKTOKEN.balanceOf(address(this));
+    uint256 contractBalance = LINK_TOKEN.balanceOf(address(this));
     Target memory target;
     for (uint256 idx = 0; idx < needsFunding.length; idx++) {
       target = s_targets[needsFunding[idx]];
@@ -188,7 +188,7 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   function withdraw(uint256 amount, address payable payee) external onlyOwner {
     require(payee != address(0));
     emit FundsWithdrawn(amount, payee);
-    LINKTOKEN.transfer(payee, amount);
+    LINK_TOKEN.transfer(payee, amount);
   }
 
   /// @notice Sets the keeper registry address.
@@ -218,6 +218,11 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
     return s_keeperRegistryAddress;
   }
 
+  /// @notice Gets the minimum wait period (in seconds) for upkeep ids between funding.
+  function getMinWaitPeriodSeconds() external view returns (uint256) {
+    return s_minWaitPeriodSeconds;
+  }
+
   /// @notice Gets the list of upkeeps ids being watched.
   function getWatchList() external view returns (uint256[] memory) {
     return s_watchList;
@@ -235,7 +240,7 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
 
   /// @notice Called to add buffer to minimum balance of upkeeps
   /// @param num the current minimum balance
-  function getBalanceWithBuffer(uint96 num) internal pure returns (uint96) {
+  function getBalanceWithBuffer(uint96 num) private pure returns (uint96) {
     uint96 buffer = 20;
     uint96 result = uint96((uint256(num) * (100 + buffer)) / 100); // convert to uint256 to prevent overflow
     return result;

From c9a122235dd021eefb344dd238d9e72409f9203d Mon Sep 17 00:00:00 2001
From: Ryan Hall <hall.ryan.r@gmail.com>
Date: Thu, 2 Nov 2023 16:50:23 -0400
Subject: [PATCH 10/33] get rid of redundant REGISTRY variable

---
 .../upkeeps/UpkeepBalanceMonWithBuffer.sol    | 58 +++++++++----------
 1 file changed, 27 insertions(+), 31 deletions(-)

diff --git a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol
index 71a7dddb367..9e9c5accd9a 100644
--- a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol
+++ b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol
@@ -11,20 +11,19 @@ import "@openzeppelin/contracts/security/Pausable.sol";
 /// @title The UpkeepBalanceMonitor contract.
 /// @notice A keeper-compatible contract that monitors and funds Chainlink Automation upkeeps.
 contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
-  LinkTokenInterface public LINK_TOKEN;
-  IKeeperRegistryMaster public REGISTRY;
+  LinkTokenInterface public immutable LINK_TOKEN;
 
   uint256 private constant MIN_GAS_FOR_TRANSFER = 55_000;
-  bytes4 private fundSig = REGISTRY.addFunds.selector;
+  bytes4 private fundSig = s_registry.addFunds.selector;
 
   event FundsAdded(uint256 amountAdded, uint256 newBalance, address sender);
   event FundsWithdrawn(uint256 amountWithdrawn, address payee);
-  event TopUpSucceeded(uint256 indexed upkeepId);
-  event TopUpFailed(uint256 indexed upkeepId);
-  event KeeperRegistryAddressUpdated(address oldAddress, address newAddress);
+  event KeeperRegistryAddressUpdated(IKeeperRegistryMaster oldAddress, IKeeperRegistryMaster newAddress);
   event LinkTokenAddressUpdated(address oldAddress, address newAddress);
   event MinWaitPeriodUpdated(uint256 oldMinWaitPeriod, uint256 newMinWaitPeriod);
   event OutOfGas(uint256 lastId);
+  event TopUpFailed(uint256 indexed upkeepId);
+  event TopUpSucceeded(uint256 indexed upkeepId);
 
   error DuplicateSubcriptionId(uint256 duplicate);
   error InvalidKeeperRegistryVersion();
@@ -38,7 +37,7 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
     uint56 lastTopUpTimestamp;
   }
 
-  address private s_keeperRegistryAddress; // the address of the keeper registry
+  IKeeperRegistryMaster private s_registry;
   uint256 private s_minWaitPeriodSeconds; // minimum time to wait between top-ups
   uint256[] private s_watchList; // the watchlist on which subscriptions are stored
   mapping(uint256 => Target) private s_targets;
@@ -48,20 +47,17 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   /// @param minWaitPeriodSeconds the minimum wait period for addresses between funding
   constructor(
     address linkTokenAddress,
-    address keeperRegistryAddress,
+    IKeeperRegistryMaster keeperRegistryAddress,
     uint256 minWaitPeriodSeconds
   ) ConfirmedOwner(msg.sender) {
     require(linkTokenAddress != address(0));
-    LINK_TOKEN = LinkTokenInterface(linkTokenAddress);
-    setKeeperRegistryAddress(keeperRegistryAddress); // 0xE16Df59B887e3Caa439E0b29B42bA2e7976FD8b2
-    setMinWaitPeriodSeconds(minWaitPeriodSeconds); //0
-    LINK_TOKEN.approve(keeperRegistryAddress, type(uint256).max);
-    if (
-      keccak256(bytes(ITypeAndVersion(keeperRegistryAddress).typeAndVersion())) !=
-      keccak256(bytes("KeeperRegistry 2.1.0"))
-    ) {
+    if (keccak256(bytes(keeperRegistryAddress.typeAndVersion())) != keccak256(bytes("KeeperRegistry 2.1.0"))) {
       revert InvalidKeeperRegistryVersion();
     }
+    LINK_TOKEN = LinkTokenInterface(linkTokenAddress);
+    setKeeperRegistryAddress(keeperRegistryAddress);
+    setMinWaitPeriodSeconds(minWaitPeriodSeconds);
+    LinkTokenInterface(linkTokenAddress).approve(address(keeperRegistryAddress), type(uint256).max);
   }
 
   /// @notice Sets the list of upkeeps to watch and their funding parameters.
@@ -109,8 +105,8 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
 
     for (uint256 idx = 0; idx < watchList.length; idx++) {
       target = s_targets[watchList[idx]];
-      uint96 upkeepBalance = REGISTRY.getBalance(watchList[idx]);
-      uint96 minUpkeepBalance = REGISTRY.getMinBalance(watchList[idx]);
+      uint96 upkeepBalance = s_registry.getBalance(watchList[idx]);
+      uint96 minUpkeepBalance = s_registry.getMinBalance(watchList[idx]);
       uint96 minBalanceWithBuffer = getBalanceWithBuffer(minUpkeepBalance);
       if (
         target.lastTopUpTimestamp + minWaitPeriod <= block.timestamp &&
@@ -140,8 +136,8 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
     Target memory target;
     for (uint256 idx = 0; idx < needsFunding.length; idx++) {
       target = s_targets[needsFunding[idx]];
-      uint96 upkeepBalance = REGISTRY.getBalance(needsFunding[idx]);
-      uint96 minUpkeepBalance = REGISTRY.getMinBalanceForUpkeep(needsFunding[idx]);
+      uint96 upkeepBalance = s_registry.getBalance(needsFunding[idx]);
+      uint96 minUpkeepBalance = s_registry.getMinBalanceForUpkeep(needsFunding[idx]);
       uint96 minBalanceWithBuffer = getBalanceWithBuffer(minUpkeepBalance);
       if (
         target.isActive &&
@@ -151,7 +147,7 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
           upkeepBalance < minBalanceWithBuffer) &&
         contractBalance >= target.topUpAmountJuels
       ) {
-        REGISTRY.addFunds(needsFunding[idx], target.topUpAmountJuels);
+        s_registry.addFunds(needsFunding[idx], target.topUpAmountJuels);
         s_targets[needsFunding[idx]].lastTopUpTimestamp = uint56(block.timestamp);
         contractBalance -= target.topUpAmountJuels;
         emit TopUpSucceeded(needsFunding[idx]);
@@ -177,7 +173,8 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   /// @notice Called by the keeper to send funds to underfunded addresses.
   /// @param performData the abi encoded list of addresses to fund
   function performUpkeep(bytes calldata performData) external whenNotPaused {
-    if (msg.sender != s_keeperRegistryAddress) revert OnlyKeeperRegistry();
+    // if (msg.sender != address(s_registry)) revert OnlyKeeperRegistry();
+    // TODO - forwarder contract
     uint256[] memory needsFunding = abi.decode(performData, (uint256[]));
     topUp(needsFunding);
   }
@@ -185,24 +182,23 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   /// @notice Withdraws the contract balance in LINK.
   /// @param amount the amount of LINK (in juels) to withdraw
   /// @param payee the address to pay
-  function withdraw(uint256 amount, address payable payee) external onlyOwner {
+  function withdraw(uint256 amount, address payee) external onlyOwner {
     require(payee != address(0));
-    emit FundsWithdrawn(amount, payee);
     LINK_TOKEN.transfer(payee, amount);
+    emit FundsWithdrawn(amount, payee);
   }
 
   /// @notice Sets the keeper registry address.
-  function setKeeperRegistryAddress(address keeperRegistryAddress) public onlyOwner {
-    require(keeperRegistryAddress != address(0));
-    emit KeeperRegistryAddressUpdated(s_keeperRegistryAddress, keeperRegistryAddress);
-    s_keeperRegistryAddress = keeperRegistryAddress;
-    REGISTRY = IKeeperRegistryMaster(keeperRegistryAddress);
+  function setKeeperRegistryAddress(IKeeperRegistryMaster keeperRegistryAddress) public onlyOwner {
+    require(address(keeperRegistryAddress) != address(0));
+    s_registry = keeperRegistryAddress;
+    emit KeeperRegistryAddressUpdated(s_registry, keeperRegistryAddress);
   }
 
   /// @notice Sets the minimum wait period (in seconds) for upkeep ids between funding.
   function setMinWaitPeriodSeconds(uint256 period) public onlyOwner {
-    emit MinWaitPeriodUpdated(s_minWaitPeriodSeconds, period);
     s_minWaitPeriodSeconds = period;
+    emit MinWaitPeriodUpdated(s_minWaitPeriodSeconds, period);
   }
 
   /// @notice Gets configuration information for a upkeep on the watchlist.
@@ -215,7 +211,7 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
 
   /// @notice Gets the keeper registry address
   function getKeeperRegistryAddress() external view returns (address) {
-    return s_keeperRegistryAddress;
+    return address(s_registry);
   }
 
   /// @notice Gets the minimum wait period (in seconds) for upkeep ids between funding.

From 9550f7e7d243001d9b6db82c46b569221281d448 Mon Sep 17 00:00:00 2001
From: Ryan Hall <hall.ryan.r@gmail.com>
Date: Thu, 2 Nov 2023 17:01:07 -0400
Subject: [PATCH 11/33] cleanup

---
 .../upkeeps/UpkeepBalanceMonWithBuffer.sol    | 23 +++++++++----------
 1 file changed, 11 insertions(+), 12 deletions(-)

diff --git a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol
index 9e9c5accd9a..968dd960683 100644
--- a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol
+++ b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol
@@ -11,10 +11,8 @@ import "@openzeppelin/contracts/security/Pausable.sol";
 /// @title The UpkeepBalanceMonitor contract.
 /// @notice A keeper-compatible contract that monitors and funds Chainlink Automation upkeeps.
 contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
-  LinkTokenInterface public immutable LINK_TOKEN;
-
   uint256 private constant MIN_GAS_FOR_TRANSFER = 55_000;
-  bytes4 private fundSig = s_registry.addFunds.selector;
+  LinkTokenInterface public immutable LINK_TOKEN;
 
   event FundsAdded(uint256 amountAdded, uint256 newBalance, address sender);
   event FundsWithdrawn(uint256 amountWithdrawn, address payee);
@@ -96,17 +94,18 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   /// @notice Gets a list of upkeeps that are underfunded.
   /// @return list of upkeeps that are underfunded
   function getUnderfundedUpkeeps() public view returns (uint256[] memory) {
-    uint256[] memory watchList = s_watchList;
-    uint256[] memory needsFunding = new uint256[](watchList.length);
+    uint256 numUpkeeps = s_watchList.length;
+    uint256[] memory needsFunding = new uint256[](numUpkeeps);
     uint256 count = 0;
     uint256 minWaitPeriod = s_minWaitPeriodSeconds;
     uint256 contractBalance = LINK_TOKEN.balanceOf(address(this));
     Target memory target;
-
-    for (uint256 idx = 0; idx < watchList.length; idx++) {
-      target = s_targets[watchList[idx]];
-      uint96 upkeepBalance = s_registry.getBalance(watchList[idx]);
-      uint96 minUpkeepBalance = s_registry.getMinBalance(watchList[idx]);
+    uint256 upkeepID;
+    for (uint256 idx = 0; idx < numUpkeeps; idx++) {
+      upkeepID = s_watchList[idx];
+      target = s_targets[upkeepID];
+      uint96 upkeepBalance = s_registry.getBalance(upkeepID);
+      uint96 minUpkeepBalance = s_registry.getMinBalance(upkeepID);
       uint96 minBalanceWithBuffer = getBalanceWithBuffer(minUpkeepBalance);
       if (
         target.lastTopUpTimestamp + minWaitPeriod <= block.timestamp &&
@@ -115,12 +114,12 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
           //upkeepBalance < minUpkeepBalance)
           upkeepBalance < minBalanceWithBuffer)
       ) {
-        needsFunding[count] = watchList[idx];
+        needsFunding[count] = upkeepID;
         count++;
         contractBalance -= target.topUpAmountJuels;
       }
     }
-    if (count < watchList.length) {
+    if (count < numUpkeeps) {
       assembly {
         mstore(needsFunding, count)
       }

From 4988a67346f345e659aa2e98bcb1aa0356932e0d Mon Sep 17 00:00:00 2001
From: Ryan Hall <hall.ryan.r@gmail.com>
Date: Fri, 3 Nov 2023 10:42:51 -0400
Subject: [PATCH 12/33] rename

---
 .../{UpkeepBalanceMonWithBuffer.sol => UpkeepBalanceMon.sol}      | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename contracts/src/v0.8/automation/upkeeps/{UpkeepBalanceMonWithBuffer.sol => UpkeepBalanceMon.sol} (100%)

diff --git a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol
similarity index 100%
rename from contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonWithBuffer.sol
rename to contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol

From 34eae70385adfcc0daae16ebe1d9ecc86377be2c Mon Sep 17 00:00:00 2001
From: Ryan Hall <hall.ryan.r@gmail.com>
Date: Fri, 3 Nov 2023 10:43:14 -0400
Subject: [PATCH 13/33] cleanup

---
 .../automation/upkeeps/UpkeepBalanceMon.sol   | 40 +++++++++----------
 1 file changed, 20 insertions(+), 20 deletions(-)

diff --git a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol
index 968dd960683..3f94bca3ccf 100644
--- a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol
+++ b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol
@@ -71,20 +71,20 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
       revert InvalidWatchList();
     }
     uint256[] memory oldWatchList = s_watchList;
-    for (uint256 idx = 0; idx < oldWatchList.length; idx++) {
-      s_targets[oldWatchList[idx]].isActive = false;
+    for (uint256 i = 0; i < oldWatchList.length; i++) {
+      s_targets[oldWatchList[i]].isActive = false;
     }
-    for (uint256 idx = 0; idx < upkeepIDs.length; idx++) {
-      if (s_targets[upkeepIDs[idx]].isActive) {
-        revert DuplicateSubcriptionId(upkeepIDs[idx]);
+    for (uint256 i = 0; i < upkeepIDs.length; i++) {
+      if (s_targets[upkeepIDs[i]].isActive) {
+        revert DuplicateSubcriptionId(upkeepIDs[i]);
       }
-      if (upkeepIDs[idx] == 0 || topUpAmountsJuels[idx] == 0) {
+      if (upkeepIDs[i] == 0 || topUpAmountsJuels[i] == 0) {
         revert InvalidWatchList();
       }
-      s_targets[upkeepIDs[idx]] = Target({
+      s_targets[upkeepIDs[i]] = Target({
         isActive: true,
-        minBalanceJuels: minBalancesJuels[idx],
-        topUpAmountJuels: topUpAmountsJuels[idx],
+        minBalanceJuels: minBalancesJuels[i],
+        topUpAmountJuels: topUpAmountsJuels[i],
         lastTopUpTimestamp: 0
       });
     }
@@ -96,13 +96,13 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   function getUnderfundedUpkeeps() public view returns (uint256[] memory) {
     uint256 numUpkeeps = s_watchList.length;
     uint256[] memory needsFunding = new uint256[](numUpkeeps);
-    uint256 count = 0;
     uint256 minWaitPeriod = s_minWaitPeriodSeconds;
     uint256 contractBalance = LINK_TOKEN.balanceOf(address(this));
+    uint256 count;
     Target memory target;
     uint256 upkeepID;
-    for (uint256 idx = 0; idx < numUpkeeps; idx++) {
-      upkeepID = s_watchList[idx];
+    for (uint256 i = 0; i < numUpkeeps; i++) {
+      upkeepID = s_watchList[i];
       target = s_targets[upkeepID];
       uint96 upkeepBalance = s_registry.getBalance(upkeepID);
       uint96 minUpkeepBalance = s_registry.getMinBalance(upkeepID);
@@ -133,10 +133,10 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
     uint256 minWaitPeriodSeconds = s_minWaitPeriodSeconds;
     uint256 contractBalance = LINK_TOKEN.balanceOf(address(this));
     Target memory target;
-    for (uint256 idx = 0; idx < needsFunding.length; idx++) {
-      target = s_targets[needsFunding[idx]];
-      uint96 upkeepBalance = s_registry.getBalance(needsFunding[idx]);
-      uint96 minUpkeepBalance = s_registry.getMinBalanceForUpkeep(needsFunding[idx]);
+    for (uint256 i = 0; i < needsFunding.length; i++) {
+      target = s_targets[needsFunding[i]];
+      uint96 upkeepBalance = s_registry.getBalance(needsFunding[i]);
+      uint96 minUpkeepBalance = s_registry.getMinBalanceForUpkeep(needsFunding[i]);
       uint96 minBalanceWithBuffer = getBalanceWithBuffer(minUpkeepBalance);
       if (
         target.isActive &&
@@ -146,13 +146,13 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
           upkeepBalance < minBalanceWithBuffer) &&
         contractBalance >= target.topUpAmountJuels
       ) {
-        s_registry.addFunds(needsFunding[idx], target.topUpAmountJuels);
-        s_targets[needsFunding[idx]].lastTopUpTimestamp = uint56(block.timestamp);
+        s_registry.addFunds(needsFunding[i], target.topUpAmountJuels);
+        s_targets[needsFunding[i]].lastTopUpTimestamp = uint56(block.timestamp);
         contractBalance -= target.topUpAmountJuels;
-        emit TopUpSucceeded(needsFunding[idx]);
+        emit TopUpSucceeded(needsFunding[i]);
       }
       if (gasleft() < MIN_GAS_FOR_TRANSFER) {
-        emit OutOfGas(idx);
+        emit OutOfGas(i);
         return;
       }
     }

From 4a60015cf90bebef63649bdc562fa4f2faddb43f Mon Sep 17 00:00:00 2001
From: Ryan Hall <hall.ryan.r@gmail.com>
Date: Fri, 3 Nov 2023 11:55:50 -0400
Subject: [PATCH 14/33] remove target, min wait period, switch to min/target
 percentages

---
 .../automation/upkeeps/UpkeepBalanceMon.sol   | 160 ++++++------------
 1 file changed, 55 insertions(+), 105 deletions(-)

diff --git a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol
index 3f94bca3ccf..999f710f8ab 100644
--- a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol
+++ b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol
@@ -18,7 +18,7 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   event FundsWithdrawn(uint256 amountWithdrawn, address payee);
   event KeeperRegistryAddressUpdated(IKeeperRegistryMaster oldAddress, IKeeperRegistryMaster newAddress);
   event LinkTokenAddressUpdated(address oldAddress, address newAddress);
-  event MinWaitPeriodUpdated(uint256 oldMinWaitPeriod, uint256 newMinWaitPeriod);
+  event ConfigSet(uint96 minPercentage, uint96 targetPercentage);
   event OutOfGas(uint256 lastId);
   event TopUpFailed(uint256 indexed upkeepId);
   event TopUpSucceeded(uint256 indexed upkeepId);
@@ -26,27 +26,27 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   error DuplicateSubcriptionId(uint256 duplicate);
   error InvalidKeeperRegistryVersion();
   error InvalidWatchList();
+  error MinPercentageTooLow();
   error OnlyKeeperRegistry();
 
-  struct Target {
-    bool isActive;
-    uint96 minBalanceJuels;
-    uint96 topUpAmountJuels;
-    uint56 lastTopUpTimestamp;
+  struct Config {
+    uint96 minPercentage;
+    uint96 targetPercentage;
   }
 
   IKeeperRegistryMaster private s_registry;
-  uint256 private s_minWaitPeriodSeconds; // minimum time to wait between top-ups
   uint256[] private s_watchList; // the watchlist on which subscriptions are stored
-  mapping(uint256 => Target) private s_targets;
+  Config private s_config;
 
   /// @param linkTokenAddress the Link token address
   /// @param keeperRegistryAddress the address of the keeper registry contract
-  /// @param minWaitPeriodSeconds the minimum wait period for addresses between funding
+  /// @param minPercentage the percentage of the min balance at which to trigger top ups
+  /// @param targetPercentage the percentage of the min balance to target during top ups
   constructor(
     address linkTokenAddress,
     IKeeperRegistryMaster keeperRegistryAddress,
-    uint256 minWaitPeriodSeconds
+    uint96 minPercentage,
+    uint96 targetPercentage
   ) ConfirmedOwner(msg.sender) {
     require(linkTokenAddress != address(0));
     if (keccak256(bytes(keeperRegistryAddress.typeAndVersion())) != keccak256(bytes("KeeperRegistry 2.1.0"))) {
@@ -54,41 +54,14 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
     }
     LINK_TOKEN = LinkTokenInterface(linkTokenAddress);
     setKeeperRegistryAddress(keeperRegistryAddress);
-    setMinWaitPeriodSeconds(minWaitPeriodSeconds);
+    setConfig(minPercentage, targetPercentage);
     LinkTokenInterface(linkTokenAddress).approve(address(keeperRegistryAddress), type(uint256).max);
   }
 
   /// @notice Sets the list of upkeeps to watch and their funding parameters.
-  /// @param upkeepIDs the list of subscription ids to watch
-  /// @param minBalancesJuels the minimum balances for each upkeep
-  /// @param topUpAmountsJuels the amount to top up each upkeep
-  function setWatchList(
-    uint256[] calldata upkeepIDs,
-    uint96[] calldata minBalancesJuels,
-    uint96[] calldata topUpAmountsJuels
-  ) external onlyOwner {
-    if (upkeepIDs.length != minBalancesJuels.length || upkeepIDs.length != topUpAmountsJuels.length) {
-      revert InvalidWatchList();
-    }
-    uint256[] memory oldWatchList = s_watchList;
-    for (uint256 i = 0; i < oldWatchList.length; i++) {
-      s_targets[oldWatchList[i]].isActive = false;
-    }
-    for (uint256 i = 0; i < upkeepIDs.length; i++) {
-      if (s_targets[upkeepIDs[i]].isActive) {
-        revert DuplicateSubcriptionId(upkeepIDs[i]);
-      }
-      if (upkeepIDs[i] == 0 || topUpAmountsJuels[i] == 0) {
-        revert InvalidWatchList();
-      }
-      s_targets[upkeepIDs[i]] = Target({
-        isActive: true,
-        minBalanceJuels: minBalancesJuels[i],
-        topUpAmountJuels: topUpAmountsJuels[i],
-        lastTopUpTimestamp: 0
-      });
-    }
-    s_watchList = upkeepIDs;
+  /// @param watchlist the list of subscription ids to watch
+  function setWatchList(uint256[] calldata watchlist) external onlyOwner {
+    s_watchList = watchlist;
   }
 
   /// @notice Gets a list of upkeeps that are underfunded.
@@ -96,27 +69,20 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   function getUnderfundedUpkeeps() public view returns (uint256[] memory) {
     uint256 numUpkeeps = s_watchList.length;
     uint256[] memory needsFunding = new uint256[](numUpkeeps);
-    uint256 minWaitPeriod = s_minWaitPeriodSeconds;
-    uint256 contractBalance = LINK_TOKEN.balanceOf(address(this));
+    Config memory config = s_config;
+    uint256 availableFunds = LINK_TOKEN.balanceOf(address(this));
     uint256 count;
-    Target memory target;
     uint256 upkeepID;
     for (uint256 i = 0; i < numUpkeeps; i++) {
       upkeepID = s_watchList[i];
-      target = s_targets[upkeepID];
       uint96 upkeepBalance = s_registry.getBalance(upkeepID);
-      uint96 minUpkeepBalance = s_registry.getMinBalance(upkeepID);
-      uint96 minBalanceWithBuffer = getBalanceWithBuffer(minUpkeepBalance);
-      if (
-        target.lastTopUpTimestamp + minWaitPeriod <= block.timestamp &&
-        contractBalance >= target.topUpAmountJuels &&
-        (upkeepBalance < target.minBalanceJuels ||
-          //upkeepBalance < minUpkeepBalance)
-          upkeepBalance < minBalanceWithBuffer)
-      ) {
+      uint96 minBalance = s_registry.getMinBalance(upkeepID);
+      uint96 topUpThreshold = (minBalance * config.minPercentage) / 100; // TODO - uint96?
+      uint96 topUpAmount = (minBalance * config.targetPercentage) / 100;
+      if (upkeepBalance <= topUpThreshold && availableFunds >= topUpAmount) {
         needsFunding[count] = upkeepID;
         count++;
-        contractBalance -= target.topUpAmountJuels;
+        availableFunds -= topUpAmount;
       }
     }
     if (count < numUpkeeps) {
@@ -130,32 +96,31 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   /// @notice Send funds to the upkeeps provided.
   /// @param needsFunding the list of upkeeps to fund
   function topUp(uint256[] memory needsFunding) public whenNotPaused {
-    uint256 minWaitPeriodSeconds = s_minWaitPeriodSeconds;
     uint256 contractBalance = LINK_TOKEN.balanceOf(address(this));
-    Target memory target;
-    for (uint256 i = 0; i < needsFunding.length; i++) {
-      target = s_targets[needsFunding[i]];
-      uint96 upkeepBalance = s_registry.getBalance(needsFunding[i]);
-      uint96 minUpkeepBalance = s_registry.getMinBalanceForUpkeep(needsFunding[i]);
-      uint96 minBalanceWithBuffer = getBalanceWithBuffer(minUpkeepBalance);
-      if (
-        target.isActive &&
-        target.lastTopUpTimestamp + minWaitPeriodSeconds <= block.timestamp &&
-        (upkeepBalance < target.minBalanceJuels ||
-          //upkeepBalance < minUpkeepBalance) &&
-          upkeepBalance < minBalanceWithBuffer) &&
-        contractBalance >= target.topUpAmountJuels
-      ) {
-        s_registry.addFunds(needsFunding[i], target.topUpAmountJuels);
-        s_targets[needsFunding[i]].lastTopUpTimestamp = uint56(block.timestamp);
-        contractBalance -= target.topUpAmountJuels;
-        emit TopUpSucceeded(needsFunding[i]);
-      }
-      if (gasleft() < MIN_GAS_FOR_TRANSFER) {
-        emit OutOfGas(i);
-        return;
-      }
-    }
+    // Target memory target;
+    // for (uint256 i = 0; i < needsFunding.length; i++) {
+    //   target = s_targets[needsFunding[i]];
+    //   uint96 upkeepBalance = s_registry.getBalance(needsFunding[i]);
+    //   uint96 minUpkeepBalance = s_registry.getMinBalanceForUpkeep(needsFunding[i]);
+    //   uint96 minBalanceWithBuffer = addBuffer(minUpkeepBalance, buffer);
+    //   if (
+    //     target.isActive &&
+    //     target.lastTopUpTimestamp + minWaitPeriodSeconds <= block.timestamp &&
+    //     (upkeepBalance < target.minBalanceJuels ||
+    //       //upkeepBalance < minUpkeepBalance) &&
+    //       upkeepBalance < minBalanceWithBuffer) &&
+    //     contractBalance >= target.topUpAmountJuels
+    //   ) {
+    //     s_registry.addFunds(needsFunding[i], target.topUpAmountJuels);
+    //     s_targets[needsFunding[i]].lastTopUpTimestamp = uint56(block.timestamp);
+    //     contractBalance -= target.topUpAmountJuels;
+    //     emit TopUpSucceeded(needsFunding[i]);
+    //   }
+    //   if (gasleft() < MIN_GAS_FOR_TRANSFER) {
+    //     emit OutOfGas(i);
+    //     return;
+    //   }
+    // }
   }
 
   /// @notice Gets list of upkeeps ids that are underfunded and returns a keeper-compatible payload.
@@ -194,18 +159,11 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
     emit KeeperRegistryAddressUpdated(s_registry, keeperRegistryAddress);
   }
 
-  /// @notice Sets the minimum wait period (in seconds) for upkeep ids between funding.
-  function setMinWaitPeriodSeconds(uint256 period) public onlyOwner {
-    s_minWaitPeriodSeconds = period;
-    emit MinWaitPeriodUpdated(s_minWaitPeriodSeconds, period);
-  }
-
-  /// @notice Gets configuration information for a upkeep on the watchlist.
-  function getUpkeepInfo(
-    uint256 upkeepId
-  ) external view returns (bool isActive, uint96 minBalanceJuels, uint96 topUpAmountJuels, uint56 lastTopUpTimestamp) {
-    Target memory target = s_targets[upkeepId];
-    return (target.isActive, target.minBalanceJuels, target.topUpAmountJuels, target.lastTopUpTimestamp);
+  /// @notice Sets the contract config
+  function setConfig(uint96 minPercentage, uint96 targetPercentage) public onlyOwner {
+    if (minPercentage < 100) revert MinPercentageTooLow();
+    s_config = Config({minPercentage: minPercentage, targetPercentage: targetPercentage});
+    emit ConfigSet(minPercentage, targetPercentage);
   }
 
   /// @notice Gets the keeper registry address
@@ -213,16 +171,16 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
     return address(s_registry);
   }
 
-  /// @notice Gets the minimum wait period (in seconds) for upkeep ids between funding.
-  function getMinWaitPeriodSeconds() external view returns (uint256) {
-    return s_minWaitPeriodSeconds;
-  }
-
   /// @notice Gets the list of upkeeps ids being watched.
   function getWatchList() external view returns (uint256[] memory) {
     return s_watchList;
   }
 
+  /// @notice Gets the contract config
+  function getConfig() external view returns (Config memory) {
+    return s_config;
+  }
+
   /// @notice Pause the contract, which prevents executing performUpkeep.
   function pause() external onlyOwner {
     _pause();
@@ -232,12 +190,4 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   function unpause() external onlyOwner {
     _unpause();
   }
-
-  /// @notice Called to add buffer to minimum balance of upkeeps
-  /// @param num the current minimum balance
-  function getBalanceWithBuffer(uint96 num) private pure returns (uint96) {
-    uint96 buffer = 20;
-    uint96 result = uint96((uint256(num) * (100 + buffer)) / 100); // convert to uint256 to prevent overflow
-    return result;
-  }
 }

From 2093d56ac22ed40cd9e3fb1170c95386448ca955 Mon Sep 17 00:00:00 2001
From: Ryan Hall <hall.ryan.r@gmail.com>
Date: Fri, 3 Nov 2023 12:43:05 -0400
Subject: [PATCH 15/33] refactor getUnderfundedUpkeeps() to return top up
 amounts

---
 .../automation/upkeeps/UpkeepBalanceMon.sol     | 17 ++++++++++-------
 1 file changed, 10 insertions(+), 7 deletions(-)

diff --git a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol
index 999f710f8ab..e34fb1176db 100644
--- a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol
+++ b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol
@@ -66,9 +66,10 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
 
   /// @notice Gets a list of upkeeps that are underfunded.
   /// @return list of upkeeps that are underfunded
-  function getUnderfundedUpkeeps() public view returns (uint256[] memory) {
+  function getUnderfundedUpkeeps() public view returns (uint256[] memory, uint256[] memory) {
     uint256 numUpkeeps = s_watchList.length;
     uint256[] memory needsFunding = new uint256[](numUpkeeps);
+    uint256[] memory topUpAmounts = new uint256[](numUpkeeps);
     Config memory config = s_config;
     uint256 availableFunds = LINK_TOKEN.balanceOf(address(this));
     uint256 count;
@@ -76,11 +77,12 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
     for (uint256 i = 0; i < numUpkeeps; i++) {
       upkeepID = s_watchList[i];
       uint96 upkeepBalance = s_registry.getBalance(upkeepID);
-      uint96 minBalance = s_registry.getMinBalance(upkeepID);
-      uint96 topUpThreshold = (minBalance * config.minPercentage) / 100; // TODO - uint96?
-      uint96 topUpAmount = (minBalance * config.targetPercentage) / 100;
+      uint256 minBalance = uint256(s_registry.getMinBalance(upkeepID));
+      uint256 topUpThreshold = (minBalance * config.minPercentage) / 100;
+      uint256 topUpAmount = (minBalance * config.targetPercentage) / 100;
       if (upkeepBalance <= topUpThreshold && availableFunds >= topUpAmount) {
         needsFunding[count] = upkeepID;
+        topUpAmounts[count] = topUpAmount;
         count++;
         availableFunds -= topUpAmount;
       }
@@ -88,9 +90,10 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
     if (count < numUpkeeps) {
       assembly {
         mstore(needsFunding, count)
+        mstore(topUpAmounts, count)
       }
     }
-    return needsFunding;
+    return (needsFunding, topUpAmounts);
   }
 
   /// @notice Send funds to the upkeeps provided.
@@ -128,9 +131,9 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   function checkUpkeep(
     bytes calldata
   ) external view whenNotPaused returns (bool upkeepNeeded, bytes memory performData) {
-    uint256[] memory needsFunding = getUnderfundedUpkeeps();
+    (uint256[] memory needsFunding, uint256[] memory topUpAmounts) = getUnderfundedUpkeeps();
     upkeepNeeded = needsFunding.length > 0;
-    performData = abi.encode(needsFunding);
+    performData = abi.encode(needsFunding, topUpAmounts);
     return (upkeepNeeded, performData);
   }
 

From 301e2c7b9ef2943c2967456216e67256fdc6e155 Mon Sep 17 00:00:00 2001
From: Ryan Hall <hall.ryan.r@gmail.com>
Date: Fri, 3 Nov 2023 13:44:36 -0400
Subject: [PATCH 16/33] refactor topUp() function

---
 .../automation/upkeeps/UpkeepBalanceMon.sol   | 175 +++++++++---------
 1 file changed, 83 insertions(+), 92 deletions(-)

diff --git a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol
index e34fb1176db..6c8ca8f3954 100644
--- a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol
+++ b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol
@@ -2,11 +2,13 @@
 
 pragma solidity 0.8.6;
 
-import "../../shared/access/ConfirmedOwner.sol";
+import {ConfirmedOwner} from "../../shared/access/ConfirmedOwner.sol";
 import {IKeeperRegistryMaster} from "../interfaces/v2_1/IKeeperRegistryMaster.sol";
+import {IAutomationForwarder} from "../interfaces/IAutomationForwarder.sol";
+import {IAutomationRegistryConsumer} from "../interfaces/IAutomationRegistryConsumer.sol";
 import {LinkTokenInterface} from "../../shared/interfaces/LinkTokenInterface.sol";
 import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol";
-import "@openzeppelin/contracts/security/Pausable.sol";
+import {Pausable} from "@openzeppelin/contracts/security/Pausable.sol";
 
 /// @title The UpkeepBalanceMonitor contract.
 /// @notice A keeper-compatible contract that monitors and funds Chainlink Automation upkeeps.
@@ -21,12 +23,12 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   event ConfigSet(uint96 minPercentage, uint96 targetPercentage);
   event OutOfGas(uint256 lastId);
   event TopUpFailed(uint256 indexed upkeepId);
-  event TopUpSucceeded(uint256 indexed upkeepId);
+  event TopUpSucceeded(uint256 indexed upkeepId, uint96 amount);
 
   error DuplicateSubcriptionId(uint256 duplicate);
   error InvalidKeeperRegistryVersion();
-  error InvalidWatchList();
   error MinPercentageTooLow();
+  error LengthMismatch();
   error OnlyKeeperRegistry();
 
   struct Config {
@@ -53,78 +55,13 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
       revert InvalidKeeperRegistryVersion();
     }
     LINK_TOKEN = LinkTokenInterface(linkTokenAddress);
-    setKeeperRegistryAddress(keeperRegistryAddress);
     setConfig(minPercentage, targetPercentage);
     LinkTokenInterface(linkTokenAddress).approve(address(keeperRegistryAddress), type(uint256).max);
   }
 
-  /// @notice Sets the list of upkeeps to watch and their funding parameters.
-  /// @param watchlist the list of subscription ids to watch
-  function setWatchList(uint256[] calldata watchlist) external onlyOwner {
-    s_watchList = watchlist;
-  }
-
-  /// @notice Gets a list of upkeeps that are underfunded.
-  /// @return list of upkeeps that are underfunded
-  function getUnderfundedUpkeeps() public view returns (uint256[] memory, uint256[] memory) {
-    uint256 numUpkeeps = s_watchList.length;
-    uint256[] memory needsFunding = new uint256[](numUpkeeps);
-    uint256[] memory topUpAmounts = new uint256[](numUpkeeps);
-    Config memory config = s_config;
-    uint256 availableFunds = LINK_TOKEN.balanceOf(address(this));
-    uint256 count;
-    uint256 upkeepID;
-    for (uint256 i = 0; i < numUpkeeps; i++) {
-      upkeepID = s_watchList[i];
-      uint96 upkeepBalance = s_registry.getBalance(upkeepID);
-      uint256 minBalance = uint256(s_registry.getMinBalance(upkeepID));
-      uint256 topUpThreshold = (minBalance * config.minPercentage) / 100;
-      uint256 topUpAmount = (minBalance * config.targetPercentage) / 100;
-      if (upkeepBalance <= topUpThreshold && availableFunds >= topUpAmount) {
-        needsFunding[count] = upkeepID;
-        topUpAmounts[count] = topUpAmount;
-        count++;
-        availableFunds -= topUpAmount;
-      }
-    }
-    if (count < numUpkeeps) {
-      assembly {
-        mstore(needsFunding, count)
-        mstore(topUpAmounts, count)
-      }
-    }
-    return (needsFunding, topUpAmounts);
-  }
-
-  /// @notice Send funds to the upkeeps provided.
-  /// @param needsFunding the list of upkeeps to fund
-  function topUp(uint256[] memory needsFunding) public whenNotPaused {
-    uint256 contractBalance = LINK_TOKEN.balanceOf(address(this));
-    // Target memory target;
-    // for (uint256 i = 0; i < needsFunding.length; i++) {
-    //   target = s_targets[needsFunding[i]];
-    //   uint96 upkeepBalance = s_registry.getBalance(needsFunding[i]);
-    //   uint96 minUpkeepBalance = s_registry.getMinBalanceForUpkeep(needsFunding[i]);
-    //   uint96 minBalanceWithBuffer = addBuffer(minUpkeepBalance, buffer);
-    //   if (
-    //     target.isActive &&
-    //     target.lastTopUpTimestamp + minWaitPeriodSeconds <= block.timestamp &&
-    //     (upkeepBalance < target.minBalanceJuels ||
-    //       //upkeepBalance < minUpkeepBalance) &&
-    //       upkeepBalance < minBalanceWithBuffer) &&
-    //     contractBalance >= target.topUpAmountJuels
-    //   ) {
-    //     s_registry.addFunds(needsFunding[i], target.topUpAmountJuels);
-    //     s_targets[needsFunding[i]].lastTopUpTimestamp = uint56(block.timestamp);
-    //     contractBalance -= target.topUpAmountJuels;
-    //     emit TopUpSucceeded(needsFunding[i]);
-    //   }
-    //   if (gasleft() < MIN_GAS_FOR_TRANSFER) {
-    //     emit OutOfGas(i);
-    //     return;
-    //   }
-    // }
-  }
+  // ================================================================
+  // |                    AUTOMATION COMPATIBLE                     |
+  // ================================================================
 
   /// @notice Gets list of upkeeps ids that are underfunded and returns a keeper-compatible payload.
   /// @return upkeepNeeded signals if upkeep is needed, performData is an abi encoded list of subscription ids that need funds
@@ -142,10 +79,29 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   function performUpkeep(bytes calldata performData) external whenNotPaused {
     // if (msg.sender != address(s_registry)) revert OnlyKeeperRegistry();
     // TODO - forwarder contract
-    uint256[] memory needsFunding = abi.decode(performData, (uint256[]));
-    topUp(needsFunding);
+    (uint256[] memory needsFunding, uint96[] memory topUpAmounts) = abi.decode(performData, (uint256[], uint96[]));
+    if (needsFunding.length != topUpAmounts.length) revert LengthMismatch();
+    IAutomationForwarder forwarder = IAutomationForwarder(msg.sender);
+    IAutomationRegistryConsumer registry = forwarder.getRegistry();
+    uint256 contractBalance = LINK_TOKEN.balanceOf(address(this));
+    for (uint256 i = 0; i < needsFunding.length; i++) {
+      try registry.addFunds(needsFunding[i], topUpAmounts[i]) {
+        emit TopUpSucceeded(needsFunding[i], topUpAmounts[i]);
+      } catch {
+        emit TopUpFailed(needsFunding[i]);
+      }
+      if (gasleft() < MIN_GAS_FOR_TRANSFER) {
+        // TODO - test
+        emit OutOfGas(i);
+        return;
+      }
+    }
   }
 
+  // ================================================================
+  // |                            ADMIN                             |
+  // ================================================================
+
   /// @notice Withdraws the contract balance in LINK.
   /// @param amount the amount of LINK (in juels) to withdraw
   /// @param payee the address to pay
@@ -155,11 +111,24 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
     emit FundsWithdrawn(amount, payee);
   }
 
-  /// @notice Sets the keeper registry address.
-  function setKeeperRegistryAddress(IKeeperRegistryMaster keeperRegistryAddress) public onlyOwner {
-    require(address(keeperRegistryAddress) != address(0));
-    s_registry = keeperRegistryAddress;
-    emit KeeperRegistryAddressUpdated(s_registry, keeperRegistryAddress);
+  /// @notice Pause the contract, which prevents executing performUpkeep.
+  function pause() external onlyOwner {
+    _pause();
+  }
+
+  /// @notice Unpause the contract.
+  function unpause() external onlyOwner {
+    _unpause();
+  }
+
+  // ================================================================
+  // |                           SETTERS                            |
+  // ================================================================
+
+  /// @notice Sets the list of upkeeps to watch and their funding parameters.
+  /// @param watchlist the list of subscription ids to watch
+  function setWatchList(uint256[] calldata watchlist) external onlyOwner {
+    s_watchList = watchlist;
   }
 
   /// @notice Sets the contract config
@@ -169,9 +138,41 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
     emit ConfigSet(minPercentage, targetPercentage);
   }
 
-  /// @notice Gets the keeper registry address
-  function getKeeperRegistryAddress() external view returns (address) {
-    return address(s_registry);
+  // ================================================================
+  // |                           GETTERS                            |
+  // ================================================================
+
+  /// @notice Gets a list of upkeeps that are underfunded.
+  /// @return needsFunding list of underfunded upkeepIDs
+  /// @return topUpAmounts amount to top up each upkeep
+  function getUnderfundedUpkeeps() public view returns (uint256[] memory, uint256[] memory) {
+    uint256 numUpkeeps = s_watchList.length;
+    uint256[] memory needsFunding = new uint256[](numUpkeeps);
+    uint256[] memory topUpAmounts = new uint256[](numUpkeeps);
+    Config memory config = s_config;
+    uint256 availableFunds = LINK_TOKEN.balanceOf(address(this));
+    uint256 count;
+    uint256 upkeepID;
+    for (uint256 i = 0; i < numUpkeeps; i++) {
+      upkeepID = s_watchList[i];
+      uint96 upkeepBalance = s_registry.getBalance(upkeepID);
+      uint256 minBalance = uint256(s_registry.getMinBalance(upkeepID));
+      uint256 topUpThreshold = (minBalance * config.minPercentage) / 100;
+      uint256 topUpAmount = (minBalance * config.targetPercentage) / 100;
+      if (upkeepBalance <= topUpThreshold && availableFunds >= topUpAmount) {
+        needsFunding[count] = upkeepID;
+        topUpAmounts[count] = topUpAmount;
+        count++;
+        availableFunds -= topUpAmount;
+      }
+    }
+    if (count < numUpkeeps) {
+      assembly {
+        mstore(needsFunding, count)
+        mstore(topUpAmounts, count)
+      }
+    }
+    return (needsFunding, topUpAmounts);
   }
 
   /// @notice Gets the list of upkeeps ids being watched.
@@ -183,14 +184,4 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   function getConfig() external view returns (Config memory) {
     return s_config;
   }
-
-  /// @notice Pause the contract, which prevents executing performUpkeep.
-  function pause() external onlyOwner {
-    _pause();
-  }
-
-  /// @notice Unpause the contract.
-  function unpause() external onlyOwner {
-    _unpause();
-  }
 }

From 431a2f1a4243d162f61cb7e12eb9d88bc3ea2514 Mon Sep 17 00:00:00 2001
From: Ryan Hall <hall.ryan.r@gmail.com>
Date: Fri, 3 Nov 2023 13:48:49 -0400
Subject: [PATCH 17/33] switch to max batch size

---
 .../automation/upkeeps/UpkeepBalanceMon.sol    | 18 +++++-------------
 1 file changed, 5 insertions(+), 13 deletions(-)

diff --git a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol
index 6c8ca8f3954..098342bb13b 100644
--- a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol
+++ b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol
@@ -13,15 +13,10 @@ import {Pausable} from "@openzeppelin/contracts/security/Pausable.sol";
 /// @title The UpkeepBalanceMonitor contract.
 /// @notice A keeper-compatible contract that monitors and funds Chainlink Automation upkeeps.
 contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
-  uint256 private constant MIN_GAS_FOR_TRANSFER = 55_000;
   LinkTokenInterface public immutable LINK_TOKEN;
 
-  event FundsAdded(uint256 amountAdded, uint256 newBalance, address sender);
   event FundsWithdrawn(uint256 amountWithdrawn, address payee);
-  event KeeperRegistryAddressUpdated(IKeeperRegistryMaster oldAddress, IKeeperRegistryMaster newAddress);
-  event LinkTokenAddressUpdated(address oldAddress, address newAddress);
   event ConfigSet(uint96 minPercentage, uint96 targetPercentage);
-  event OutOfGas(uint256 lastId);
   event TopUpFailed(uint256 indexed upkeepId);
   event TopUpSucceeded(uint256 indexed upkeepId, uint96 amount);
 
@@ -32,6 +27,7 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   error OnlyKeeperRegistry();
 
   struct Config {
+    uint8 maxBatchSize;
     uint96 minPercentage;
     uint96 targetPercentage;
   }
@@ -42,6 +38,7 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
 
   /// @param linkTokenAddress the Link token address
   /// @param keeperRegistryAddress the address of the keeper registry contract
+  /// @param maxBatchSize the maximum number of upkeeps to fund in a single transaction
   /// @param minPercentage the percentage of the min balance at which to trigger top ups
   /// @param targetPercentage the percentage of the min balance to target during top ups
   constructor(
@@ -55,7 +52,7 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
       revert InvalidKeeperRegistryVersion();
     }
     LINK_TOKEN = LinkTokenInterface(linkTokenAddress);
-    setConfig(minPercentage, targetPercentage);
+    setConfig(maxBatchSize, minPercentage, targetPercentage);
     LinkTokenInterface(linkTokenAddress).approve(address(keeperRegistryAddress), type(uint256).max);
   }
 
@@ -90,11 +87,6 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
       } catch {
         emit TopUpFailed(needsFunding[i]);
       }
-      if (gasleft() < MIN_GAS_FOR_TRANSFER) {
-        // TODO - test
-        emit OutOfGas(i);
-        return;
-      }
     }
   }
 
@@ -132,9 +124,9 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   }
 
   /// @notice Sets the contract config
-  function setConfig(uint96 minPercentage, uint96 targetPercentage) public onlyOwner {
+  function setConfig(uint8 maxBatchSize, uint96 minPercentage, uint96 targetPercentage) public onlyOwner {
     if (minPercentage < 100) revert MinPercentageTooLow();
-    s_config = Config({minPercentage: minPercentage, targetPercentage: targetPercentage});
+    s_config = Config({maxBatchSize: maxBatchSize, minPercentage: minPercentage, targetPercentage: targetPercentage});
     emit ConfigSet(minPercentage, targetPercentage);
   }
 

From 502242e415be8a15c61fc5aea47af21c6579afa0 Mon Sep 17 00:00:00 2001
From: Ryan Hall <hall.ryan.r@gmail.com>
Date: Fri, 3 Nov 2023 13:53:57 -0400
Subject: [PATCH 18/33] add max top up amount

---
 .../automation/upkeeps/UpkeepBalanceMon.sol   | 32 +++++++++++++++----
 1 file changed, 25 insertions(+), 7 deletions(-)

diff --git a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol
index 098342bb13b..b53e4d50baf 100644
--- a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol
+++ b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol
@@ -28,8 +28,9 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
 
   struct Config {
     uint8 maxBatchSize;
-    uint96 minPercentage;
-    uint96 targetPercentage;
+    uint24 minPercentage;
+    uint24 targetPercentage; // max target is 160K times the min balance
+    uint96 maxTopUpAmount;
   }
 
   IKeeperRegistryMaster private s_registry;
@@ -41,18 +42,21 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   /// @param maxBatchSize the maximum number of upkeeps to fund in a single transaction
   /// @param minPercentage the percentage of the min balance at which to trigger top ups
   /// @param targetPercentage the percentage of the min balance to target during top ups
+  /// @param maxTopUpAmount the maximum amount to top up an upkeep
   constructor(
     address linkTokenAddress,
     IKeeperRegistryMaster keeperRegistryAddress,
-    uint96 minPercentage,
-    uint96 targetPercentage
+    uint8 maxBatchSize,
+    uint24 minPercentage,
+    uint24 targetPercentage,
+    uint96 maxTopUpAmount
   ) ConfirmedOwner(msg.sender) {
     require(linkTokenAddress != address(0));
     if (keccak256(bytes(keeperRegistryAddress.typeAndVersion())) != keccak256(bytes("KeeperRegistry 2.1.0"))) {
       revert InvalidKeeperRegistryVersion();
     }
     LINK_TOKEN = LinkTokenInterface(linkTokenAddress);
-    setConfig(maxBatchSize, minPercentage, targetPercentage);
+    setConfig(maxBatchSize, minPercentage, targetPercentage, maxTopUpAmount);
     LinkTokenInterface(linkTokenAddress).approve(address(keeperRegistryAddress), type(uint256).max);
   }
 
@@ -124,9 +128,23 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   }
 
   /// @notice Sets the contract config
-  function setConfig(uint8 maxBatchSize, uint96 minPercentage, uint96 targetPercentage) public onlyOwner {
+  /// @param maxBatchSize the maximum number of upkeeps to fund in a single transaction
+  /// @param minPercentage the percentage of the min balance at which to trigger top ups
+  /// @param targetPercentage the percentage of the min balance to target during top ups
+  /// @param maxTopUpAmount the maximum amount to top up an upkeep
+  function setConfig(
+    uint8 maxBatchSize,
+    uint24 minPercentage,
+    uint24 targetPercentage,
+    uint96 maxTopUpAmount
+  ) public onlyOwner {
     if (minPercentage < 100) revert MinPercentageTooLow();
-    s_config = Config({maxBatchSize: maxBatchSize, minPercentage: minPercentage, targetPercentage: targetPercentage});
+    s_config = Config({
+      maxBatchSize: maxBatchSize,
+      minPercentage: minPercentage,
+      targetPercentage: targetPercentage,
+      maxTopUpAmount: maxTopUpAmount
+    });
     emit ConfigSet(minPercentage, targetPercentage);
   }
 

From c9837bf3fcade20ab0d1d6808010cc7d1e82043f Mon Sep 17 00:00:00 2001
From: Ryan Hall <hall.ryan.r@gmail.com>
Date: Fri, 3 Nov 2023 13:58:36 -0400
Subject: [PATCH 19/33] add maxTopUpAmount

---
 .../src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol
index b53e4d50baf..cb4f09836a0 100644
--- a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol
+++ b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol
@@ -21,8 +21,7 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   event TopUpSucceeded(uint256 indexed upkeepId, uint96 amount);
 
   error DuplicateSubcriptionId(uint256 duplicate);
-  error InvalidKeeperRegistryVersion();
-  error MinPercentageTooLow();
+  error InvalidConfig();
   error LengthMismatch();
   error OnlyKeeperRegistry();
 
@@ -52,9 +51,6 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
     uint96 maxTopUpAmount
   ) ConfirmedOwner(msg.sender) {
     require(linkTokenAddress != address(0));
-    if (keccak256(bytes(keeperRegistryAddress.typeAndVersion())) != keccak256(bytes("KeeperRegistry 2.1.0"))) {
-      revert InvalidKeeperRegistryVersion();
-    }
     LINK_TOKEN = LinkTokenInterface(linkTokenAddress);
     setConfig(maxBatchSize, minPercentage, targetPercentage, maxTopUpAmount);
     LinkTokenInterface(linkTokenAddress).approve(address(keeperRegistryAddress), type(uint256).max);
@@ -138,7 +134,8 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
     uint24 targetPercentage,
     uint96 maxTopUpAmount
   ) public onlyOwner {
-    if (minPercentage < 100) revert MinPercentageTooLow();
+    if (maxBatchSize == 0 || minPercentage < 100 || targetPercentage <= minPercentage || maxTopUpAmount == 0)
+      revert InvalidConfig();
     s_config = Config({
       maxBatchSize: maxBatchSize,
       minPercentage: minPercentage,
@@ -169,6 +166,9 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
       uint256 minBalance = uint256(s_registry.getMinBalance(upkeepID));
       uint256 topUpThreshold = (minBalance * config.minPercentage) / 100;
       uint256 topUpAmount = (minBalance * config.targetPercentage) / 100;
+      if (topUpAmount > config.maxTopUpAmount) {
+        topUpAmount = config.maxTopUpAmount;
+      }
       if (upkeepBalance <= topUpThreshold && availableFunds >= topUpAmount) {
         needsFunding[count] = upkeepID;
         topUpAmounts[count] = topUpAmount;

From 4a195af933b82fc73dd1cd2df6648e6c95fb278b Mon Sep 17 00:00:00 2001
From: Ryan Hall <hall.ryan.r@gmail.com>
Date: Fri, 3 Nov 2023 14:44:07 -0400
Subject: [PATCH 20/33] whitelist performUpkeep to forwarder

---
 .../automation/upkeeps/UpkeepBalanceMon.sol   | 97 ++++++++++---------
 1 file changed, 52 insertions(+), 45 deletions(-)

diff --git a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol
index cb4f09836a0..ba9dad16fc1 100644
--- a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol
+++ b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol
@@ -15,8 +15,9 @@ import {Pausable} from "@openzeppelin/contracts/security/Pausable.sol";
 contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   LinkTokenInterface public immutable LINK_TOKEN;
 
+  event ConfigSet(Config config);
+  event ForwarderSet(IAutomationForwarder forwarder);
   event FundsWithdrawn(uint256 amountWithdrawn, address payee);
-  event ConfigSet(uint96 minPercentage, uint96 targetPercentage);
   event TopUpFailed(uint256 indexed upkeepId);
   event TopUpSucceeded(uint256 indexed upkeepId, uint96 amount);
 
@@ -32,28 +33,16 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
     uint96 maxTopUpAmount;
   }
 
-  IKeeperRegistryMaster private s_registry;
   uint256[] private s_watchList; // the watchlist on which subscriptions are stored
   Config private s_config;
-
-  /// @param linkTokenAddress the Link token address
-  /// @param keeperRegistryAddress the address of the keeper registry contract
-  /// @param maxBatchSize the maximum number of upkeeps to fund in a single transaction
-  /// @param minPercentage the percentage of the min balance at which to trigger top ups
-  /// @param targetPercentage the percentage of the min balance to target during top ups
-  /// @param maxTopUpAmount the maximum amount to top up an upkeep
-  constructor(
-    address linkTokenAddress,
-    IKeeperRegistryMaster keeperRegistryAddress,
-    uint8 maxBatchSize,
-    uint24 minPercentage,
-    uint24 targetPercentage,
-    uint96 maxTopUpAmount
-  ) ConfirmedOwner(msg.sender) {
-    require(linkTokenAddress != address(0));
-    LINK_TOKEN = LinkTokenInterface(linkTokenAddress);
-    setConfig(maxBatchSize, minPercentage, targetPercentage, maxTopUpAmount);
-    LinkTokenInterface(linkTokenAddress).approve(address(keeperRegistryAddress), type(uint256).max);
+  IAutomationForwarder s_forwarder;
+
+  /// @param linkToken the Link token address
+  /// @param config the initial config for the contract
+  constructor(LinkTokenInterface linkToken, Config memory config) ConfirmedOwner(msg.sender) {
+    require(address(linkToken) != address(0));
+    LINK_TOKEN = linkToken;
+    setConfig(config);
   }
 
   // ================================================================
@@ -65,21 +54,28 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   function checkUpkeep(
     bytes calldata
   ) external view whenNotPaused returns (bool upkeepNeeded, bytes memory performData) {
+    IAutomationRegistryConsumer registry = getRegistry();
+    bool needsApproval = LINK_TOKEN.allowance(address(this), address(registry)) == 0;
     (uint256[] memory needsFunding, uint256[] memory topUpAmounts) = getUnderfundedUpkeeps();
     upkeepNeeded = needsFunding.length > 0;
-    performData = abi.encode(needsFunding, topUpAmounts);
+    performData = abi.encode(needsApproval, needsFunding, topUpAmounts);
     return (upkeepNeeded, performData);
   }
 
   /// @notice Called by the keeper to send funds to underfunded addresses.
   /// @param performData the abi encoded list of addresses to fund
   function performUpkeep(bytes calldata performData) external whenNotPaused {
-    // if (msg.sender != address(s_registry)) revert OnlyKeeperRegistry();
-    // TODO - forwarder contract
-    (uint256[] memory needsFunding, uint96[] memory topUpAmounts) = abi.decode(performData, (uint256[], uint96[]));
+    if (msg.sender != address(s_forwarder)) revert OnlyKeeperRegistry();
+    (bool needsApproval, uint256[] memory needsFunding, uint96[] memory topUpAmounts) = abi.decode(
+      performData,
+      (bool, uint256[], uint96[])
+    );
     if (needsFunding.length != topUpAmounts.length) revert LengthMismatch();
     IAutomationForwarder forwarder = IAutomationForwarder(msg.sender);
     IAutomationRegistryConsumer registry = forwarder.getRegistry();
+    if (needsApproval) {
+      LINK_TOKEN.approve(address(registry), type(uint256).max);
+    }
     uint256 contractBalance = LINK_TOKEN.balanceOf(address(this));
     for (uint256 i = 0; i < needsFunding.length; i++) {
       try registry.addFunds(needsFunding[i], topUpAmounts[i]) {
@@ -124,25 +120,25 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   }
 
   /// @notice Sets the contract config
-  /// @param maxBatchSize the maximum number of upkeeps to fund in a single transaction
-  /// @param minPercentage the percentage of the min balance at which to trigger top ups
-  /// @param targetPercentage the percentage of the min balance to target during top ups
-  /// @param maxTopUpAmount the maximum amount to top up an upkeep
-  function setConfig(
-    uint8 maxBatchSize,
-    uint24 minPercentage,
-    uint24 targetPercentage,
-    uint96 maxTopUpAmount
-  ) public onlyOwner {
-    if (maxBatchSize == 0 || minPercentage < 100 || targetPercentage <= minPercentage || maxTopUpAmount == 0)
+  /// @param config the new config
+  function setConfig(Config memory config) public onlyOwner {
+    if (
+      config.maxBatchSize == 0 ||
+      config.minPercentage < 100 ||
+      config.targetPercentage <= config.minPercentage ||
+      config.maxTopUpAmount == 0
+    ) {
       revert InvalidConfig();
-    s_config = Config({
-      maxBatchSize: maxBatchSize,
-      minPercentage: minPercentage,
-      targetPercentage: targetPercentage,
-      maxTopUpAmount: maxTopUpAmount
-    });
-    emit ConfigSet(minPercentage, targetPercentage);
+    }
+    s_config = config;
+    emit ConfigSet(config);
+  }
+
+  /// @notice Sets the upkeep's forwarder contract
+  /// @param forwarder the new forwarder
+  function setForwarder(IAutomationForwarder forwarder) external onlyOwner {
+    s_forwarder = forwarder;
+    emit ForwarderSet(forwarder);
   }
 
   // ================================================================
@@ -157,13 +153,14 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
     uint256[] memory needsFunding = new uint256[](numUpkeeps);
     uint256[] memory topUpAmounts = new uint256[](numUpkeeps);
     Config memory config = s_config;
+    IAutomationRegistryConsumer registry = getRegistry();
     uint256 availableFunds = LINK_TOKEN.balanceOf(address(this));
     uint256 count;
     uint256 upkeepID;
     for (uint256 i = 0; i < numUpkeeps; i++) {
       upkeepID = s_watchList[i];
-      uint96 upkeepBalance = s_registry.getBalance(upkeepID);
-      uint256 minBalance = uint256(s_registry.getMinBalance(upkeepID));
+      uint96 upkeepBalance = registry.getBalance(upkeepID);
+      uint256 minBalance = uint256(registry.getMinBalance(upkeepID));
       uint256 topUpThreshold = (minBalance * config.minPercentage) / 100;
       uint256 topUpAmount = (minBalance * config.targetPercentage) / 100;
       if (topUpAmount > config.maxTopUpAmount) {
@@ -194,4 +191,14 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   function getConfig() external view returns (Config memory) {
     return s_config;
   }
+
+  /// @notice Gets the upkeep's forwarder contract
+  function getForwarder() external view returns (IAutomationForwarder) {
+    return s_forwarder;
+  }
+
+  /// @notice Gets the registry contract
+  function getRegistry() public view returns (IAutomationRegistryConsumer) {
+    return s_forwarder.getRegistry();
+  }
 }

From 5cd20baae51708aebd7b780dc34926ef0648d9c7 Mon Sep 17 00:00:00 2001
From: Ryan Hall <hall.ryan.r@gmail.com>
Date: Fri, 3 Nov 2023 14:57:37 -0400
Subject: [PATCH 21/33] cleanup

---
 .../automation/upkeeps/UpkeepBalanceMon.sol   | 35 +++++++++++++------
 1 file changed, 24 insertions(+), 11 deletions(-)

diff --git a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol
index ba9dad16fc1..49155d2ceab 100644
--- a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol
+++ b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol
@@ -10,32 +10,44 @@ import {LinkTokenInterface} from "../../shared/interfaces/LinkTokenInterface.sol
 import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol";
 import {Pausable} from "@openzeppelin/contracts/security/Pausable.sol";
 
-/// @title The UpkeepBalanceMonitor contract.
+/// @title The UpkeepBalanceMonitor contract
 /// @notice A keeper-compatible contract that monitors and funds Chainlink Automation upkeeps.
 contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
-  LinkTokenInterface public immutable LINK_TOKEN;
-
   event ConfigSet(Config config);
   event ForwarderSet(IAutomationForwarder forwarder);
   event FundsWithdrawn(uint256 amountWithdrawn, address payee);
   event TopUpFailed(uint256 indexed upkeepId);
   event TopUpSucceeded(uint256 indexed upkeepId, uint96 amount);
+  event WatchListSet();
 
-  error DuplicateSubcriptionId(uint256 duplicate);
   error InvalidConfig();
-  error LengthMismatch();
+  error InvalidPerformData();
   error OnlyKeeperRegistry();
 
+  /// @member maxBatchSize is the maximum number of upkeeps to fund in a single transaction
+  /// @member minPercentage is the percentage of the upkeep's minBalance at which top-up occurs
+  /// @member targetPercentage is the percentage of the upkeep's minBalance to top-up to
+  /// @member maxTopUpAmount is the maximum amount of LINK to top-up an upkeep with
   struct Config {
     uint8 maxBatchSize;
     uint24 minPercentage;
-    uint24 targetPercentage; // max target is 160K times the min balance
+    uint24 targetPercentage;
     uint96 maxTopUpAmount;
   }
 
-  uint256[] private s_watchList; // the watchlist on which subscriptions are stored
+  // ================================================================
+  // |                           STORAGE                            |
+  // ================================================================
+
+  LinkTokenInterface private immutable LINK_TOKEN;
+
+  uint256[] private s_watchList;
   Config private s_config;
-  IAutomationForwarder s_forwarder;
+  IAutomationForwarder private s_forwarder;
+
+  // ================================================================
+  // |                         CONSTRUCTOR                          |
+  // ================================================================
 
   /// @param linkToken the Link token address
   /// @param config the initial config for the contract
@@ -70,13 +82,12 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
       performData,
       (bool, uint256[], uint96[])
     );
-    if (needsFunding.length != topUpAmounts.length) revert LengthMismatch();
+    if (needsFunding.length != topUpAmounts.length) revert InvalidPerformData();
     IAutomationForwarder forwarder = IAutomationForwarder(msg.sender);
     IAutomationRegistryConsumer registry = forwarder.getRegistry();
     if (needsApproval) {
       LINK_TOKEN.approve(address(registry), type(uint256).max);
     }
-    uint256 contractBalance = LINK_TOKEN.balanceOf(address(this));
     for (uint256 i = 0; i < needsFunding.length; i++) {
       try registry.addFunds(needsFunding[i], topUpAmounts[i]) {
         emit TopUpSucceeded(needsFunding[i], topUpAmounts[i]);
@@ -117,6 +128,7 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   /// @param watchlist the list of subscription ids to watch
   function setWatchList(uint256[] calldata watchlist) external onlyOwner {
     s_watchList = watchlist;
+    emit WatchListSet();
   }
 
   /// @notice Sets the contract config
@@ -136,6 +148,7 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
 
   /// @notice Sets the upkeep's forwarder contract
   /// @param forwarder the new forwarder
+  /// @dev this should only need to be called once, after registering the contract with the registry
   function setForwarder(IAutomationForwarder forwarder) external onlyOwner {
     s_forwarder = forwarder;
     emit ForwarderSet(forwarder);
@@ -182,7 +195,7 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
     return (needsFunding, topUpAmounts);
   }
 
-  /// @notice Gets the list of upkeeps ids being watched.
+  /// @notice Gets the list of upkeeps ids being monitored
   function getWatchList() external view returns (uint256[] memory) {
     return s_watchList;
   }

From cfe5f698f87b644b925797813b9162dcafab60dc Mon Sep 17 00:00:00 2001
From: Ryan Hall <hall.ryan.r@gmail.com>
Date: Fri, 3 Nov 2023 14:59:36 -0400
Subject: [PATCH 22/33] rename

---
 .../upkeeps/{UpkeepBalanceMon.sol => UpkeepBalanceMonitor.sol}    | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename contracts/src/v0.8/automation/upkeeps/{UpkeepBalanceMon.sol => UpkeepBalanceMonitor.sol} (100%)

diff --git a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol
similarity index 100%
rename from contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMon.sol
rename to contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol

From f79ad5fa59c905173c447eb4ff99d9418ee1f342 Mon Sep 17 00:00:00 2001
From: Ryan Hall <hall.ryan.r@gmail.com>
Date: Fri, 3 Nov 2023 15:57:17 -0400
Subject: [PATCH 23/33] bring topUp() back

---
 .../upkeeps/UpkeepBalanceMonitor.sol          | 38 +++++++++----------
 1 file changed, 18 insertions(+), 20 deletions(-)

diff --git a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol
index 49155d2ceab..36e0b46abcf 100644
--- a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol
+++ b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol
@@ -21,8 +21,8 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   event WatchListSet();
 
   error InvalidConfig();
-  error InvalidPerformData();
-  error OnlyKeeperRegistry();
+  error InvalidTopUpData();
+  error OnlyForwarderOrOwner();
 
   /// @member maxBatchSize is the maximum number of upkeeps to fund in a single transaction
   /// @member minPercentage is the percentage of the upkeep's minBalance at which top-up occurs
@@ -66,33 +66,31 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   function checkUpkeep(
     bytes calldata
   ) external view whenNotPaused returns (bool upkeepNeeded, bytes memory performData) {
-    IAutomationRegistryConsumer registry = getRegistry();
-    bool needsApproval = LINK_TOKEN.allowance(address(this), address(registry)) == 0;
     (uint256[] memory needsFunding, uint256[] memory topUpAmounts) = getUnderfundedUpkeeps();
     upkeepNeeded = needsFunding.length > 0;
-    performData = abi.encode(needsApproval, needsFunding, topUpAmounts);
+    performData = abi.encode(needsFunding, topUpAmounts);
     return (upkeepNeeded, performData);
   }
 
   /// @notice Called by the keeper to send funds to underfunded addresses.
   /// @param performData the abi encoded list of addresses to fund
   function performUpkeep(bytes calldata performData) external whenNotPaused {
-    if (msg.sender != address(s_forwarder)) revert OnlyKeeperRegistry();
-    (bool needsApproval, uint256[] memory needsFunding, uint96[] memory topUpAmounts) = abi.decode(
-      performData,
-      (bool, uint256[], uint96[])
-    );
-    if (needsFunding.length != topUpAmounts.length) revert InvalidPerformData();
-    IAutomationForwarder forwarder = IAutomationForwarder(msg.sender);
-    IAutomationRegistryConsumer registry = forwarder.getRegistry();
-    if (needsApproval) {
-      LINK_TOKEN.approve(address(registry), type(uint256).max);
-    }
-    for (uint256 i = 0; i < needsFunding.length; i++) {
-      try registry.addFunds(needsFunding[i], topUpAmounts[i]) {
-        emit TopUpSucceeded(needsFunding[i], topUpAmounts[i]);
+    (uint256[] memory needsFunding, uint96[] memory topUpAmounts) = abi.decode(performData, (uint256[], uint96[]));
+  }
+
+  /// @notice Called by the keeper/owner to send funds to underfunded upkeeps
+  /// @param upkeepIDs the list of upkeep ids to fund
+  /// @param topUpAmounts the list of amounts to fund each upkeep with
+  function topUp(uint256[] memory upkeepIDs, uint96[] memory topUpAmounts) public {
+    IAutomationForwarder forwarder = s_forwarder;
+    if (msg.sender != address(s_forwarder) && msg.sender != owner()) revert OnlyForwarderOrOwner();
+    if (upkeepIDs.length != topUpAmounts.length) revert InvalidTopUpData();
+    address registryAddress = address(forwarder.getRegistry());
+    for (uint256 i = 0; i < upkeepIDs.length; i++) {
+      try LINK_TOKEN.transferAndCall(registryAddress, topUpAmounts[i], abi.encode(upkeepIDs[i])) {
+        emit TopUpSucceeded(upkeepIDs[i], topUpAmounts[i]);
       } catch {
-        emit TopUpFailed(needsFunding[i]);
+        emit TopUpFailed(upkeepIDs[i]);
       }
     }
   }

From b076491f550b7bf4573d6321121a5876e169144e Mon Sep 17 00:00:00 2001
From: Ryan Hall <hall.ryan.r@gmail.com>
Date: Fri, 3 Nov 2023 17:00:32 -0400
Subject: [PATCH 24/33] write initial test suite

---
 .../upkeeps/UpkeepBalanceMonitor.sol          |   8 +-
 .../automation/UpkeepBalanceMonitor.test.ts   | 163 ++++++++++++++++++
 2 files changed, 169 insertions(+), 2 deletions(-)
 create mode 100644 contracts/test/v0.8/automation/UpkeepBalanceMonitor.test.ts

diff --git a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol
index 36e0b46abcf..16433fa4b99 100644
--- a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol
+++ b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol
@@ -75,7 +75,8 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   /// @notice Called by the keeper to send funds to underfunded addresses.
   /// @param performData the abi encoded list of addresses to fund
   function performUpkeep(bytes calldata performData) external whenNotPaused {
-    (uint256[] memory needsFunding, uint96[] memory topUpAmounts) = abi.decode(performData, (uint256[], uint96[]));
+    (uint256[] memory upkeepIDs, uint96[] memory topUpAmounts) = abi.decode(performData, (uint256[], uint96[]));
+    topUp(upkeepIDs, topUpAmounts);
   }
 
   /// @notice Called by the keeper/owner to send funds to underfunded upkeeps
@@ -173,7 +174,7 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
       uint96 upkeepBalance = registry.getBalance(upkeepID);
       uint256 minBalance = uint256(registry.getMinBalance(upkeepID));
       uint256 topUpThreshold = (minBalance * config.minPercentage) / 100;
-      uint256 topUpAmount = (minBalance * config.targetPercentage) / 100;
+      uint256 topUpAmount = ((minBalance * config.targetPercentage) / 100) - upkeepBalance;
       if (topUpAmount > config.maxTopUpAmount) {
         topUpAmount = config.maxTopUpAmount;
       }
@@ -183,6 +184,9 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
         count++;
         availableFunds -= topUpAmount;
       }
+      if (count == config.maxBatchSize) {
+        break;
+      }
     }
     if (count < numUpkeeps) {
       assembly {
diff --git a/contracts/test/v0.8/automation/UpkeepBalanceMonitor.test.ts b/contracts/test/v0.8/automation/UpkeepBalanceMonitor.test.ts
new file mode 100644
index 00000000000..183309698b6
--- /dev/null
+++ b/contracts/test/v0.8/automation/UpkeepBalanceMonitor.test.ts
@@ -0,0 +1,163 @@
+import { ethers } from 'hardhat'
+import chai, { assert, expect } from 'chai'
+import type { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'
+import { randomAddress } from '../../test-helpers/helpers'
+import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'
+import { IKeeperRegistryMaster__factory as RegistryFactory } from '../../../typechain/factories/IKeeperRegistryMaster__factory'
+import { IAutomationForwarder__factory as ForwarderFactory } from '../../../typechain/factories/IAutomationForwarder__factory'
+import { UpkeepBalanceMonitor } from '../../../typechain/UpkeepBalanceMonitor'
+import { LinkToken } from '../../../typechain/LinkToken'
+import { BigNumber } from 'ethers'
+import {
+  deployMockContract,
+  MockContract,
+} from '@ethereum-waffle/mock-contract'
+
+let owner: SignerWithAddress
+let stranger: SignerWithAddress
+let registry: MockContract
+let forwarder: MockContract
+let linkToken: LinkToken
+let upkeepBalanceMonitor: UpkeepBalanceMonitor
+
+const setup = async () => {
+  const accounts = await ethers.getSigners()
+  owner = accounts[0]
+  stranger = accounts[1]
+
+  const ltFactory = await ethers.getContractFactory(
+    'src/v0.4/LinkToken.sol:LinkToken',
+    owner,
+  )
+  linkToken = (await ltFactory.deploy()) as LinkToken
+  const bmFactory = await ethers.getContractFactory(
+    'UpkeepBalanceMonitor',
+    owner,
+  )
+  upkeepBalanceMonitor = await bmFactory.deploy(linkToken.address, {
+    maxBatchSize: 10,
+    minPercentage: 120,
+    targetPercentage: 300,
+    maxTopUpAmount: ethers.utils.parseEther('100'),
+  })
+  registry = await deployMockContract(owner, RegistryFactory.abi)
+  forwarder = await deployMockContract(owner, ForwarderFactory.abi)
+  await forwarder.mock.getRegistry.returns(registry.address)
+  await upkeepBalanceMonitor.setForwarder(forwarder.address)
+  await linkToken
+    .connect(owner)
+    .transfer(upkeepBalanceMonitor.address, ethers.utils.parseEther('10000'))
+  await upkeepBalanceMonitor
+    .connect(owner)
+    .setWatchList([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
+  for (let i = 1; i < 13; i++) {
+    await registry.mock.getMinBalance.withArgs(i).returns(100)
+    await registry.mock.getBalance.withArgs(i).returns(121) // all upkeeps are sufficiently funded
+  }
+}
+
+describe('UpkeepBalanceMonitor', () => {
+  beforeEach(async () => {
+    await loadFixture(setup)
+  })
+
+  describe('constructor', () => {
+    it('should set the initial values correctly', async () => {
+      const config = await upkeepBalanceMonitor.getConfig()
+      expect(config.maxBatchSize).to.equal(10)
+      expect(config.minPercentage).to.equal(120)
+      expect(config.targetPercentage).to.equal(300)
+      expect(config.maxTopUpAmount).to.equal(ethers.utils.parseEther('100'))
+    })
+  })
+
+  describe('setConfig', () => {
+    it('should set config correctly', async () => {
+      const newConfig = {
+        maxBatchSize: 100,
+        minPercentage: 150,
+        targetPercentage: 500,
+        maxTopUpAmount: 1,
+      }
+      await upkeepBalanceMonitor.connect(owner).setConfig(newConfig)
+      const config = await upkeepBalanceMonitor.getConfig()
+      expect(config.maxBatchSize).to.equal(newConfig.maxBatchSize)
+      expect(config.minPercentage).to.equal(newConfig.minPercentage)
+      expect(config.targetPercentage).to.equal(newConfig.targetPercentage)
+      expect(config.maxTopUpAmount).to.equal(newConfig.maxTopUpAmount)
+    })
+  })
+
+  describe('setForwarder', () => {
+    it('should set the forwarder correctly', async () => {
+      const expected = randomAddress()
+      await upkeepBalanceMonitor.connect(owner).setForwarder(expected)
+      const forwarderAddress = await upkeepBalanceMonitor.getForwarder()
+      expect(forwarderAddress).to.equal(expected)
+    })
+  })
+
+  describe('setWatchList', () => {
+    it('should add addresses to the watchlist', async () => {
+      const expected = [
+        BigNumber.from(1),
+        BigNumber.from(2),
+        BigNumber.from(10),
+      ]
+      await upkeepBalanceMonitor.connect(owner).setWatchList(expected)
+      const watchList = await upkeepBalanceMonitor.getWatchList()
+      expect(watchList).to.deep.equal(expected)
+    })
+  })
+
+  describe('withdraw', () => {
+    it('should withdraw funds to a payee', async () => {
+      const payee = randomAddress()
+      const initialBalance = await linkToken.balanceOf(
+        upkeepBalanceMonitor.address,
+      )
+      const withdrawAmount = 100
+      await upkeepBalanceMonitor.connect(owner).withdraw(withdrawAmount, payee)
+      const finalBalance = await linkToken.balanceOf(
+        upkeepBalanceMonitor.address,
+      )
+      const payeeBalance = await linkToken.balanceOf(payee)
+      expect(finalBalance).to.equal(initialBalance.sub(withdrawAmount))
+      expect(payeeBalance).to.equal(withdrawAmount)
+    })
+  })
+
+  describe('pause and unpause', () => {
+    it('should pause and unpause the contract', async () => {
+      await upkeepBalanceMonitor.connect(owner).pause()
+      expect(await upkeepBalanceMonitor.paused()).to.be.true
+      await upkeepBalanceMonitor.connect(owner).unpause()
+    })
+  })
+
+  describe('getUnderfundedUpkeeps', () => {
+    it('should find the underfunded upkeeps', async () => {
+      let [upkeepIDs, topUpAmounts] =
+        await upkeepBalanceMonitor.getUnderfundedUpkeeps()
+      expect(upkeepIDs.length).to.equal(0)
+      expect(topUpAmounts.length).to.equal(0)
+      // update the balance for some upkeeps
+      await registry.mock.getBalance.withArgs(2).returns(120)
+      await registry.mock.getBalance.withArgs(4).returns(15)
+      await registry.mock.getBalance.withArgs(5).returns(0)
+      ;[upkeepIDs, topUpAmounts] =
+        await upkeepBalanceMonitor.getUnderfundedUpkeeps()
+      expect(upkeepIDs).to.deep.equal([2, 4, 5].map(BigNumber.from))
+      expect(topUpAmounts).to.deep.equal([180, 285, 300].map(BigNumber.from))
+      // update all to need funding
+      for (let i = 1; i < 13; i++) {
+        await registry.mock.getBalance.withArgs(i).returns(0)
+      }
+      // test that only up to max batch size are included in the list
+      ;[upkeepIDs, topUpAmounts] =
+        await upkeepBalanceMonitor.getUnderfundedUpkeeps()
+      expect(upkeepIDs.length).to.equal(10)
+      expect(topUpAmounts.length).to.equal(10)
+    })
+  })
+})

From f73ab09a91af61f395e275adc17d7261daa8618e Mon Sep 17 00:00:00 2001
From: Ryan Hall <hall.ryan.r@gmail.com>
Date: Mon, 6 Nov 2023 10:42:30 -0500
Subject: [PATCH 25/33] fix solhint errors

---
 contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol | 2 --
 1 file changed, 2 deletions(-)

diff --git a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol
index 16433fa4b99..0f78120008a 100644
--- a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol
+++ b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol
@@ -3,11 +3,9 @@
 pragma solidity 0.8.6;
 
 import {ConfirmedOwner} from "../../shared/access/ConfirmedOwner.sol";
-import {IKeeperRegistryMaster} from "../interfaces/v2_1/IKeeperRegistryMaster.sol";
 import {IAutomationForwarder} from "../interfaces/IAutomationForwarder.sol";
 import {IAutomationRegistryConsumer} from "../interfaces/IAutomationRegistryConsumer.sol";
 import {LinkTokenInterface} from "../../shared/interfaces/LinkTokenInterface.sol";
-import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol";
 import {Pausable} from "@openzeppelin/contracts/security/Pausable.sol";
 
 /// @title The UpkeepBalanceMonitor contract

From 975f0156714ba81f4ac3f560f4350e4f7385ccb9 Mon Sep 17 00:00:00 2001
From: Ryan Hall <hall.ryan.r@gmail.com>
Date: Mon, 6 Nov 2023 11:14:46 -0500
Subject: [PATCH 26/33] update test for getUnderfundedUpkeeps()

---
 .../upkeeps/UpkeepBalanceMonitor.sol          |  4 +-
 .../automation/UpkeepBalanceMonitor.test.ts   | 51 ++++++++++++++++---
 2 files changed, 47 insertions(+), 8 deletions(-)

diff --git a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol
index 0f78120008a..5f053a38ab2 100644
--- a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol
+++ b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol
@@ -66,7 +66,9 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   ) external view whenNotPaused returns (bool upkeepNeeded, bytes memory performData) {
     (uint256[] memory needsFunding, uint256[] memory topUpAmounts) = getUnderfundedUpkeeps();
     upkeepNeeded = needsFunding.length > 0;
-    performData = abi.encode(needsFunding, topUpAmounts);
+    if (upkeepNeeded) {
+      performData = abi.encode(needsFunding, topUpAmounts);
+    }
     return (upkeepNeeded, performData);
   }
 
diff --git a/contracts/test/v0.8/automation/UpkeepBalanceMonitor.test.ts b/contracts/test/v0.8/automation/UpkeepBalanceMonitor.test.ts
index 183309698b6..861042fa7a5 100644
--- a/contracts/test/v0.8/automation/UpkeepBalanceMonitor.test.ts
+++ b/contracts/test/v0.8/automation/UpkeepBalanceMonitor.test.ts
@@ -49,8 +49,8 @@ const setup = async () => {
     .transfer(upkeepBalanceMonitor.address, ethers.utils.parseEther('10000'))
   await upkeepBalanceMonitor
     .connect(owner)
-    .setWatchList([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
-  for (let i = 1; i < 13; i++) {
+    .setWatchList([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
+  for (let i = 0; i < 12; i++) {
     await registry.mock.getMinBalance.withArgs(i).returns(100)
     await registry.mock.getBalance.withArgs(i).returns(121) // all upkeeps are sufficiently funded
   }
@@ -135,29 +135,66 @@ describe('UpkeepBalanceMonitor', () => {
     })
   })
 
-  describe('getUnderfundedUpkeeps', () => {
+  describe('checkUpkeep / getUnderfundedUpkeeps', () => {
     it('should find the underfunded upkeeps', async () => {
       let [upkeepIDs, topUpAmounts] =
         await upkeepBalanceMonitor.getUnderfundedUpkeeps()
       expect(upkeepIDs.length).to.equal(0)
       expect(topUpAmounts.length).to.equal(0)
+      let [upkeepNeeded, performData] =
+        await upkeepBalanceMonitor.checkUpkeep('0x')
+      expect(upkeepNeeded).to.be.false
+      expect(performData).to.equal('0x')
       // update the balance for some upkeeps
       await registry.mock.getBalance.withArgs(2).returns(120)
       await registry.mock.getBalance.withArgs(4).returns(15)
       await registry.mock.getBalance.withArgs(5).returns(0)
       ;[upkeepIDs, topUpAmounts] =
         await upkeepBalanceMonitor.getUnderfundedUpkeeps()
-      expect(upkeepIDs).to.deep.equal([2, 4, 5].map(BigNumber.from))
-      expect(topUpAmounts).to.deep.equal([180, 285, 300].map(BigNumber.from))
+      expect(upkeepIDs.map((v) => v.toNumber())).to.deep.equal([2, 4, 5])
+      expect(topUpAmounts.map((v) => v.toNumber())).to.deep.equal([
+        180, 285, 300,
+      ])
+      ;[upkeepNeeded, performData] =
+        await upkeepBalanceMonitor.checkUpkeep('0x')
+      expect(upkeepNeeded).to.be.true
+      expect(performData).to.equal(
+        ethers.utils.defaultAbiCoder.encode(
+          ['uint256[]', 'uint256[]'],
+          [
+            [2, 4, 5],
+            [180, 285, 300],
+          ],
+        ),
+      )
       // update all to need funding
-      for (let i = 1; i < 13; i++) {
+      for (let i = 0; i < 12; i++) {
         await registry.mock.getBalance.withArgs(i).returns(0)
       }
-      // test that only up to max batch size are included in the list
+      // only the max batch size are included in the list
+      ;[upkeepIDs, topUpAmounts] =
+        await upkeepBalanceMonitor.getUnderfundedUpkeeps()
+      expect(upkeepIDs.length).to.equal(10)
+      expect(topUpAmounts.length).to.equal(10)
+      expect(upkeepIDs.map((v) => v.toNumber())).to.deep.equal([
+        0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
+      ]) // 0-9
+      expect(topUpAmounts.map((v) => v.toNumber())).to.deep.equal([
+        ...Array(10).fill(300),
+      ])
+      // update the balance for some upkeeps
+      await registry.mock.getBalance.withArgs(0).returns(300)
+      await registry.mock.getBalance.withArgs(5).returns(300)
       ;[upkeepIDs, topUpAmounts] =
         await upkeepBalanceMonitor.getUnderfundedUpkeeps()
       expect(upkeepIDs.length).to.equal(10)
       expect(topUpAmounts.length).to.equal(10)
+      expect(upkeepIDs.map((v) => v.toNumber())).to.deep.equal([
+        1, 2, 3, 4, 6, 7, 8, 9, 10, 11,
+      ])
+      expect(topUpAmounts.map((v) => v.toNumber())).to.deep.equal([
+        ...Array(10).fill(300),
+      ])
     })
   })
 })

From 264715c6c6868344393373323535717205c58547 Mon Sep 17 00:00:00 2001
From: Ryan Hall <hall.ryan.r@gmail.com>
Date: Mon, 6 Nov 2023 12:17:24 -0500
Subject: [PATCH 27/33] add tests for owner only functions and events

---
 .../automation/UpkeepBalanceMonitor.test.ts   | 111 +++++++++++++++---
 1 file changed, 93 insertions(+), 18 deletions(-)

diff --git a/contracts/test/v0.8/automation/UpkeepBalanceMonitor.test.ts b/contracts/test/v0.8/automation/UpkeepBalanceMonitor.test.ts
index 861042fa7a5..afabbd06312 100644
--- a/contracts/test/v0.8/automation/UpkeepBalanceMonitor.test.ts
+++ b/contracts/test/v0.8/automation/UpkeepBalanceMonitor.test.ts
@@ -72,13 +72,14 @@ describe('UpkeepBalanceMonitor', () => {
   })
 
   describe('setConfig', () => {
+    const newConfig = {
+      maxBatchSize: 100,
+      minPercentage: 150,
+      targetPercentage: 500,
+      maxTopUpAmount: 1,
+    }
+
     it('should set config correctly', async () => {
-      const newConfig = {
-        maxBatchSize: 100,
-        minPercentage: 150,
-        targetPercentage: 500,
-        maxTopUpAmount: 1,
-      }
       await upkeepBalanceMonitor.connect(owner).setConfig(newConfig)
       const config = await upkeepBalanceMonitor.getConfig()
       expect(config.maxBatchSize).to.equal(newConfig.maxBatchSize)
@@ -86,37 +87,78 @@ describe('UpkeepBalanceMonitor', () => {
       expect(config.targetPercentage).to.equal(newConfig.targetPercentage)
       expect(config.maxTopUpAmount).to.equal(newConfig.maxTopUpAmount)
     })
+
+    it('cannot be called by a non-owner', async () => {
+      await expect(
+        upkeepBalanceMonitor.connect(stranger).setConfig(newConfig),
+      ).to.be.revertedWith('Only callable by owner')
+    })
+
+    it('should emit an event', async () => {
+      await expect(
+        upkeepBalanceMonitor.connect(owner).setConfig(newConfig),
+      ).to.emit(upkeepBalanceMonitor, 'ConfigSet')
+    })
   })
 
   describe('setForwarder', () => {
+    const newForwarder = randomAddress()
+
     it('should set the forwarder correctly', async () => {
-      const expected = randomAddress()
-      await upkeepBalanceMonitor.connect(owner).setForwarder(expected)
+      await upkeepBalanceMonitor.connect(owner).setForwarder(newForwarder)
       const forwarderAddress = await upkeepBalanceMonitor.getForwarder()
-      expect(forwarderAddress).to.equal(expected)
+      expect(forwarderAddress).to.equal(newForwarder)
+    })
+
+    it('cannot be called by a non-owner', async () => {
+      await expect(
+        upkeepBalanceMonitor.connect(stranger).setForwarder(randomAddress()),
+      ).to.be.revertedWith('Only callable by owner')
+    })
+
+    it('should emit an event', async () => {
+      await expect(
+        upkeepBalanceMonitor.connect(owner).setForwarder(newForwarder),
+      )
+        .to.emit(upkeepBalanceMonitor, 'ForwarderSet')
+        .withArgs(newForwarder)
     })
   })
 
   describe('setWatchList', () => {
+    const newWatchList = [
+      BigNumber.from(1),
+      BigNumber.from(2),
+      BigNumber.from(10),
+    ]
+
     it('should add addresses to the watchlist', async () => {
-      const expected = [
-        BigNumber.from(1),
-        BigNumber.from(2),
-        BigNumber.from(10),
-      ]
-      await upkeepBalanceMonitor.connect(owner).setWatchList(expected)
+      await upkeepBalanceMonitor.connect(owner).setWatchList(newWatchList)
       const watchList = await upkeepBalanceMonitor.getWatchList()
-      expect(watchList).to.deep.equal(expected)
+      expect(watchList).to.deep.equal(newWatchList)
+    })
+
+    it('cannot be called by a non-owner', async () => {
+      await expect(
+        upkeepBalanceMonitor.connect(stranger).setWatchList([1, 2, 3]),
+      ).to.be.revertedWith('Only callable by owner')
+    })
+
+    it('should emit an event', async () => {
+      await expect(
+        upkeepBalanceMonitor.connect(owner).setWatchList(newWatchList),
+      ).to.emit(upkeepBalanceMonitor, 'WatchListSet')
     })
   })
 
   describe('withdraw', () => {
+    const payee = randomAddress()
+    const withdrawAmount = 100
+
     it('should withdraw funds to a payee', async () => {
-      const payee = randomAddress()
       const initialBalance = await linkToken.balanceOf(
         upkeepBalanceMonitor.address,
       )
-      const withdrawAmount = 100
       await upkeepBalanceMonitor.connect(owner).withdraw(withdrawAmount, payee)
       const finalBalance = await linkToken.balanceOf(
         upkeepBalanceMonitor.address,
@@ -125,6 +167,20 @@ describe('UpkeepBalanceMonitor', () => {
       expect(finalBalance).to.equal(initialBalance.sub(withdrawAmount))
       expect(payeeBalance).to.equal(withdrawAmount)
     })
+
+    it('cannot be called by a non-owner', async () => {
+      await expect(
+        upkeepBalanceMonitor.connect(stranger).withdraw(withdrawAmount, payee),
+      ).to.be.revertedWith('Only callable by owner')
+    })
+
+    it('should emit an event', async () => {
+      await expect(
+        upkeepBalanceMonitor.connect(owner).withdraw(withdrawAmount, payee),
+      )
+        .to.emit(upkeepBalanceMonitor, 'FundsWithdrawn')
+        .withArgs(100, payee)
+    })
   })
 
   describe('pause and unpause', () => {
@@ -133,6 +189,25 @@ describe('UpkeepBalanceMonitor', () => {
       expect(await upkeepBalanceMonitor.paused()).to.be.true
       await upkeepBalanceMonitor.connect(owner).unpause()
     })
+
+    it('cannot be called by a non-owner', async () => {
+      await expect(
+        upkeepBalanceMonitor.connect(stranger).pause(),
+      ).to.be.revertedWith('Only callable by owner')
+      await upkeepBalanceMonitor.connect(owner).pause()
+      await expect(
+        upkeepBalanceMonitor.connect(stranger).unpause(),
+      ).to.be.revertedWith('Only callable by owner')
+    })
+
+    it('should emit an event', async () => {
+      await expect(upkeepBalanceMonitor.connect(owner).pause())
+        .to.emit(upkeepBalanceMonitor, 'Paused')
+        .withArgs(owner.address)
+      await expect(upkeepBalanceMonitor.connect(owner).unpause())
+        .to.emit(upkeepBalanceMonitor, 'Unpaused')
+        .withArgs(owner.address)
+    })
   })
 
   describe('checkUpkeep / getUnderfundedUpkeeps', () => {

From bf5ce994cb7f3a5de8c2631a069ac7c50bb1722b Mon Sep 17 00:00:00 2001
From: Ryan Hall <hall.ryan.r@gmail.com>
Date: Mon, 6 Nov 2023 13:40:48 -0500
Subject: [PATCH 28/33] add topUp and performUpkeep tests

---
 .../upkeeps/UpkeepBalanceMonitor.sol          |  19 ++-
 .../automation/UpkeepBalanceMonitor.test.ts   | 119 +++++++++++++++---
 2 files changed, 117 insertions(+), 21 deletions(-)

diff --git a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol
index 5f053a38ab2..baeff911880 100644
--- a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol
+++ b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol
@@ -82,17 +82,26 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   /// @notice Called by the keeper/owner to send funds to underfunded upkeeps
   /// @param upkeepIDs the list of upkeep ids to fund
   /// @param topUpAmounts the list of amounts to fund each upkeep with
+  /// @dev We explicitly choose not to verify that input upkeepIDs are included in the watchlist. We also
+  /// explicity permit any amount to be sent via topUpAmounts; it does not have to meet the criteria
+  /// specified in getUnderfundedUpkeeps(). Here, we are relying on the security of automation's OCR to
+  /// secure the output of getUnderfundedUpkeeps() as the input to topUp(), and we are treating the owner
+  /// as a privileged user that can perform arbitrary top-ups to any upkeepID.
   function topUp(uint256[] memory upkeepIDs, uint96[] memory topUpAmounts) public {
     IAutomationForwarder forwarder = s_forwarder;
     if (msg.sender != address(s_forwarder) && msg.sender != owner()) revert OnlyForwarderOrOwner();
     if (upkeepIDs.length != topUpAmounts.length) revert InvalidTopUpData();
     address registryAddress = address(forwarder.getRegistry());
     for (uint256 i = 0; i < upkeepIDs.length; i++) {
-      try LINK_TOKEN.transferAndCall(registryAddress, topUpAmounts[i], abi.encode(upkeepIDs[i])) {
-        emit TopUpSucceeded(upkeepIDs[i], topUpAmounts[i]);
-      } catch {
-        emit TopUpFailed(upkeepIDs[i]);
-      }
+      try LINK_TOKEN.transferAndCall(registryAddress, topUpAmounts[i], abi.encode(upkeepIDs[i])) returns (
+        bool success
+      ) {
+        if (success) {
+          emit TopUpSucceeded(upkeepIDs[i], topUpAmounts[i]);
+          continue;
+        }
+      } catch {}
+      emit TopUpFailed(upkeepIDs[i]);
     }
   }
 
diff --git a/contracts/test/v0.8/automation/UpkeepBalanceMonitor.test.ts b/contracts/test/v0.8/automation/UpkeepBalanceMonitor.test.ts
index afabbd06312..4c8ee70c301 100644
--- a/contracts/test/v0.8/automation/UpkeepBalanceMonitor.test.ts
+++ b/contracts/test/v0.8/automation/UpkeepBalanceMonitor.test.ts
@@ -61,7 +61,7 @@ describe('UpkeepBalanceMonitor', () => {
     await loadFixture(setup)
   })
 
-  describe('constructor', () => {
+  describe('constructor()', () => {
     it('should set the initial values correctly', async () => {
       const config = await upkeepBalanceMonitor.getConfig()
       expect(config.maxBatchSize).to.equal(10)
@@ -71,7 +71,7 @@ describe('UpkeepBalanceMonitor', () => {
     })
   })
 
-  describe('setConfig', () => {
+  describe('setConfig()', () => {
     const newConfig = {
       maxBatchSize: 100,
       minPercentage: 150,
@@ -101,7 +101,7 @@ describe('UpkeepBalanceMonitor', () => {
     })
   })
 
-  describe('setForwarder', () => {
+  describe('setForwarder()', () => {
     const newForwarder = randomAddress()
 
     it('should set the forwarder correctly', async () => {
@@ -125,7 +125,7 @@ describe('UpkeepBalanceMonitor', () => {
     })
   })
 
-  describe('setWatchList', () => {
+  describe('setWatchList()', () => {
     const newWatchList = [
       BigNumber.from(1),
       BigNumber.from(2),
@@ -151,7 +151,7 @@ describe('UpkeepBalanceMonitor', () => {
     })
   })
 
-  describe('withdraw', () => {
+  describe('withdraw()', () => {
     const payee = randomAddress()
     const withdrawAmount = 100
 
@@ -183,11 +183,12 @@ describe('UpkeepBalanceMonitor', () => {
     })
   })
 
-  describe('pause and unpause', () => {
+  describe('pause() and unpause()', () => {
     it('should pause and unpause the contract', async () => {
       await upkeepBalanceMonitor.connect(owner).pause()
       expect(await upkeepBalanceMonitor.paused()).to.be.true
       await upkeepBalanceMonitor.connect(owner).unpause()
+      expect(await upkeepBalanceMonitor.paused()).to.be.false
     })
 
     it('cannot be called by a non-owner', async () => {
@@ -199,18 +200,9 @@ describe('UpkeepBalanceMonitor', () => {
         upkeepBalanceMonitor.connect(stranger).unpause(),
       ).to.be.revertedWith('Only callable by owner')
     })
-
-    it('should emit an event', async () => {
-      await expect(upkeepBalanceMonitor.connect(owner).pause())
-        .to.emit(upkeepBalanceMonitor, 'Paused')
-        .withArgs(owner.address)
-      await expect(upkeepBalanceMonitor.connect(owner).unpause())
-        .to.emit(upkeepBalanceMonitor, 'Unpaused')
-        .withArgs(owner.address)
-    })
   })
 
-  describe('checkUpkeep / getUnderfundedUpkeeps', () => {
+  describe('checkUpkeep() / getUnderfundedUpkeeps()', () => {
     it('should find the underfunded upkeeps', async () => {
       let [upkeepIDs, topUpAmounts] =
         await upkeepBalanceMonitor.getUnderfundedUpkeeps()
@@ -272,4 +264,99 @@ describe('UpkeepBalanceMonitor', () => {
       ])
     })
   })
+
+  describe('topUp()', () => {
+    beforeEach(async () => {
+      await registry.mock.onTokenTransfer
+        .withArgs(
+          upkeepBalanceMonitor.address,
+          100,
+          ethers.utils.defaultAbiCoder.encode(['uint256'], [1]),
+        )
+        .returns()
+      await registry.mock.onTokenTransfer
+        .withArgs(
+          upkeepBalanceMonitor.address,
+          50,
+          ethers.utils.defaultAbiCoder.encode(['uint256'], [7]),
+        )
+        .returns()
+    })
+
+    it('cannot be called by a non-owner', async () => {
+      await expect(
+        upkeepBalanceMonitor.connect(stranger).topUp([], []),
+      ).to.be.revertedWith('OnlyForwarderOrOwner()')
+    })
+
+    it('tops up the upkeeps by the amounts provided', async () => {
+      const initialBalance = await linkToken.balanceOf(registry.address)
+      const tx = await upkeepBalanceMonitor
+        .connect(owner)
+        .topUp([1, 7], [100, 50])
+      const finalBalance = await linkToken.balanceOf(registry.address)
+      expect(finalBalance).to.equal(initialBalance.add(150))
+      await expect(tx)
+        .to.emit(upkeepBalanceMonitor, 'TopUpSucceeded')
+        .withArgs(1, 100)
+      await expect(tx)
+        .to.emit(upkeepBalanceMonitor, 'TopUpSucceeded')
+        .withArgs(7, 50)
+    })
+
+    it('does not abort if one top-up fails', async () => {
+      const initialBalance = await linkToken.balanceOf(registry.address)
+      const tx = await upkeepBalanceMonitor
+        .connect(owner)
+        .topUp([1, 7, 100], [100, 50, 100])
+      const finalBalance = await linkToken.balanceOf(registry.address)
+      expect(finalBalance).to.equal(initialBalance.add(150))
+      await expect(tx)
+        .to.emit(upkeepBalanceMonitor, 'TopUpSucceeded')
+        .withArgs(1, 100)
+      await expect(tx)
+        .to.emit(upkeepBalanceMonitor, 'TopUpSucceeded')
+        .withArgs(7, 50)
+      await expect(tx)
+        .to.emit(upkeepBalanceMonitor, 'TopUpFailed')
+        .withArgs(100)
+    })
+  })
+
+  describe('performUpkeep()', () => {
+    it('should revert if the contract is paused', async () => {
+      await upkeepBalanceMonitor.connect(owner).pause()
+      await expect(
+        upkeepBalanceMonitor.connect(owner).performUpkeep('0x'),
+      ).to.be.revertedWith('Pausable: paused')
+    })
+  })
+
+  describe('checkUpkeep() / performUpkeep()', () => {
+    it('works round-trip', async () => {
+      await registry.mock.getBalance.withArgs(1).returns(100) // needs 200
+      await registry.mock.getBalance.withArgs(7).returns(0) // needs 300
+      await registry.mock.onTokenTransfer
+        .withArgs(
+          upkeepBalanceMonitor.address,
+          200,
+          ethers.utils.defaultAbiCoder.encode(['uint256'], [1]),
+        )
+        .returns()
+      await registry.mock.onTokenTransfer
+        .withArgs(
+          upkeepBalanceMonitor.address,
+          300,
+          ethers.utils.defaultAbiCoder.encode(['uint256'], [7]),
+        )
+        .returns()
+      const [upkeepNeeded, performData] =
+        await upkeepBalanceMonitor.checkUpkeep('0x')
+      expect(upkeepNeeded).to.be.true
+      const initialBalance = await linkToken.balanceOf(registry.address)
+      await upkeepBalanceMonitor.connect(owner).performUpkeep(performData)
+      const finalBalance = await linkToken.balanceOf(registry.address)
+      expect(finalBalance).to.equal(initialBalance.add(500))
+    })
+  })
 })

From cf32810bcd1dab812efffa0e5dce4d6957ec5395 Mon Sep 17 00:00:00 2001
From: Ryan Hall <hall.ryan.r@gmail.com>
Date: Mon, 6 Nov 2023 14:51:55 -0500
Subject: [PATCH 29/33] rearrange functions on contract

---
 .../upkeeps/UpkeepBalanceMonitor.sol          | 120 +++++++++---------
 1 file changed, 62 insertions(+), 58 deletions(-)

diff --git a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol
index baeff911880..fa7842fc938 100644
--- a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol
+++ b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol
@@ -56,27 +56,47 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   }
 
   // ================================================================
-  // |                    AUTOMATION COMPATIBLE                     |
+  // |                      CORE FUNCTIONALITY                      |
   // ================================================================
 
-  /// @notice Gets list of upkeeps ids that are underfunded and returns a keeper-compatible payload.
-  /// @return upkeepNeeded signals if upkeep is needed, performData is an abi encoded list of subscription ids that need funds
-  function checkUpkeep(
-    bytes calldata
-  ) external view whenNotPaused returns (bool upkeepNeeded, bytes memory performData) {
-    (uint256[] memory needsFunding, uint256[] memory topUpAmounts) = getUnderfundedUpkeeps();
-    upkeepNeeded = needsFunding.length > 0;
-    if (upkeepNeeded) {
-      performData = abi.encode(needsFunding, topUpAmounts);
+  /// @notice Gets a list of upkeeps that are underfunded
+  /// @return needsFunding list of underfunded upkeepIDs
+  /// @return topUpAmounts amount to top up each upkeep
+  function getUnderfundedUpkeeps() public view returns (uint256[] memory, uint256[] memory) {
+    uint256 numUpkeeps = s_watchList.length;
+    uint256[] memory needsFunding = new uint256[](numUpkeeps);
+    uint256[] memory topUpAmounts = new uint256[](numUpkeeps);
+    Config memory config = s_config;
+    IAutomationRegistryConsumer registry = getRegistry();
+    uint256 availableFunds = LINK_TOKEN.balanceOf(address(this));
+    uint256 count;
+    uint256 upkeepID;
+    for (uint256 i = 0; i < numUpkeeps; i++) {
+      upkeepID = s_watchList[i];
+      uint96 upkeepBalance = registry.getBalance(upkeepID);
+      uint256 minBalance = uint256(registry.getMinBalance(upkeepID));
+      uint256 topUpThreshold = (minBalance * config.minPercentage) / 100;
+      uint256 topUpAmount = ((minBalance * config.targetPercentage) / 100) - upkeepBalance;
+      if (topUpAmount > config.maxTopUpAmount) {
+        topUpAmount = config.maxTopUpAmount;
+      }
+      if (upkeepBalance <= topUpThreshold && availableFunds >= topUpAmount) {
+        needsFunding[count] = upkeepID;
+        topUpAmounts[count] = topUpAmount;
+        count++;
+        availableFunds -= topUpAmount;
+      }
+      if (count == config.maxBatchSize) {
+        break;
+      }
     }
-    return (upkeepNeeded, performData);
-  }
-
-  /// @notice Called by the keeper to send funds to underfunded addresses.
-  /// @param performData the abi encoded list of addresses to fund
-  function performUpkeep(bytes calldata performData) external whenNotPaused {
-    (uint256[] memory upkeepIDs, uint96[] memory topUpAmounts) = abi.decode(performData, (uint256[], uint96[]));
-    topUp(upkeepIDs, topUpAmounts);
+    if (count < numUpkeeps) {
+      assembly {
+        mstore(needsFunding, count)
+        mstore(topUpAmounts, count)
+      }
+    }
+    return (needsFunding, topUpAmounts);
   }
 
   /// @notice Called by the keeper/owner to send funds to underfunded upkeeps
@@ -105,6 +125,30 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
     }
   }
 
+  // ================================================================
+  // |                    AUTOMATION COMPATIBLE                     |
+  // ================================================================
+
+  /// @notice Gets list of upkeeps ids that are underfunded and returns a keeper-compatible payload.
+  /// @return upkeepNeeded signals if upkeep is needed, performData is an abi encoded list of subscription ids that need funds
+  function checkUpkeep(
+    bytes calldata
+  ) external view whenNotPaused returns (bool upkeepNeeded, bytes memory performData) {
+    (uint256[] memory needsFunding, uint256[] memory topUpAmounts) = getUnderfundedUpkeeps();
+    upkeepNeeded = needsFunding.length > 0;
+    if (upkeepNeeded) {
+      performData = abi.encode(needsFunding, topUpAmounts);
+    }
+    return (upkeepNeeded, performData);
+  }
+
+  /// @notice Called by the keeper to send funds to underfunded addresses.
+  /// @param performData the abi encoded list of addresses to fund
+  function performUpkeep(bytes calldata performData) external whenNotPaused {
+    (uint256[] memory upkeepIDs, uint96[] memory topUpAmounts) = abi.decode(performData, (uint256[], uint96[]));
+    topUp(upkeepIDs, topUpAmounts);
+  }
+
   // ================================================================
   // |                            ADMIN                             |
   // ================================================================
@@ -166,46 +210,6 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   // |                           GETTERS                            |
   // ================================================================
 
-  /// @notice Gets a list of upkeeps that are underfunded.
-  /// @return needsFunding list of underfunded upkeepIDs
-  /// @return topUpAmounts amount to top up each upkeep
-  function getUnderfundedUpkeeps() public view returns (uint256[] memory, uint256[] memory) {
-    uint256 numUpkeeps = s_watchList.length;
-    uint256[] memory needsFunding = new uint256[](numUpkeeps);
-    uint256[] memory topUpAmounts = new uint256[](numUpkeeps);
-    Config memory config = s_config;
-    IAutomationRegistryConsumer registry = getRegistry();
-    uint256 availableFunds = LINK_TOKEN.balanceOf(address(this));
-    uint256 count;
-    uint256 upkeepID;
-    for (uint256 i = 0; i < numUpkeeps; i++) {
-      upkeepID = s_watchList[i];
-      uint96 upkeepBalance = registry.getBalance(upkeepID);
-      uint256 minBalance = uint256(registry.getMinBalance(upkeepID));
-      uint256 topUpThreshold = (minBalance * config.minPercentage) / 100;
-      uint256 topUpAmount = ((minBalance * config.targetPercentage) / 100) - upkeepBalance;
-      if (topUpAmount > config.maxTopUpAmount) {
-        topUpAmount = config.maxTopUpAmount;
-      }
-      if (upkeepBalance <= topUpThreshold && availableFunds >= topUpAmount) {
-        needsFunding[count] = upkeepID;
-        topUpAmounts[count] = topUpAmount;
-        count++;
-        availableFunds -= topUpAmount;
-      }
-      if (count == config.maxBatchSize) {
-        break;
-      }
-    }
-    if (count < numUpkeeps) {
-      assembly {
-        mstore(needsFunding, count)
-        mstore(topUpAmounts, count)
-      }
-    }
-    return (needsFunding, topUpAmounts);
-  }
-
   /// @notice Gets the list of upkeeps ids being monitored
   function getWatchList() external view returns (uint256[] memory) {
     return s_watchList;

From c45948447c6dc60168acdd557bb678d0eca017ca Mon Sep 17 00:00:00 2001
From: Ryan Hall <hall.ryan.r@gmail.com>
Date: Wed, 8 Nov 2023 10:47:06 -0500
Subject: [PATCH 30/33] add support for multiple registries

---
 .../upkeeps/UpkeepBalanceMonitor.sol          | 128 ++++++++++--------
 .../automation/UpkeepBalanceMonitor.test.ts   |  77 ++++++++---
 2 files changed, 133 insertions(+), 72 deletions(-)

diff --git a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol
index fa7842fc938..5f21029c1a6 100644
--- a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol
+++ b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol
@@ -1,22 +1,24 @@
 // SPDX-License-Identifier: MIT
 
-pragma solidity 0.8.6;
+pragma solidity 0.8.19;
 
 import {ConfirmedOwner} from "../../shared/access/ConfirmedOwner.sol";
-import {IAutomationForwarder} from "../interfaces/IAutomationForwarder.sol";
 import {IAutomationRegistryConsumer} from "../interfaces/IAutomationRegistryConsumer.sol";
 import {LinkTokenInterface} from "../../shared/interfaces/LinkTokenInterface.sol";
 import {Pausable} from "@openzeppelin/contracts/security/Pausable.sol";
+import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
 
 /// @title The UpkeepBalanceMonitor contract
 /// @notice A keeper-compatible contract that monitors and funds Chainlink Automation upkeeps.
 contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
+  using EnumerableSet for EnumerableSet.AddressSet;
+
   event ConfigSet(Config config);
-  event ForwarderSet(IAutomationForwarder forwarder);
+  event ForwarderSet(address forwarderAddress);
   event FundsWithdrawn(uint256 amountWithdrawn, address payee);
   event TopUpFailed(uint256 indexed upkeepId);
   event TopUpSucceeded(uint256 indexed upkeepId, uint96 amount);
-  event WatchListSet();
+  event WatchListSet(address registryAddress);
 
   error InvalidConfig();
   error InvalidTopUpData();
@@ -39,9 +41,10 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
 
   LinkTokenInterface private immutable LINK_TOKEN;
 
-  uint256[] private s_watchList;
+  mapping(address => uint256[]) s_registryWatchLists;
+  EnumerableSet.AddressSet s_registries;
   Config private s_config;
-  IAutomationForwarder private s_forwarder;
+  address private s_forwarderAddress;
 
   // ================================================================
   // |                         CONSTRUCTOR                          |
@@ -61,59 +64,65 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
 
   /// @notice Gets a list of upkeeps that are underfunded
   /// @return needsFunding list of underfunded upkeepIDs
+  /// @return registryAddresses list of registries that the upkeepIDs belong to
   /// @return topUpAmounts amount to top up each upkeep
-  function getUnderfundedUpkeeps() public view returns (uint256[] memory, uint256[] memory) {
-    uint256 numUpkeeps = s_watchList.length;
-    uint256[] memory needsFunding = new uint256[](numUpkeeps);
-    uint256[] memory topUpAmounts = new uint256[](numUpkeeps);
+  function getUnderfundedUpkeeps() public view returns (uint256[] memory, address[] memory, uint256[] memory) {
     Config memory config = s_config;
-    IAutomationRegistryConsumer registry = getRegistry();
+    uint256[] memory needsFunding = new uint256[](config.maxBatchSize);
+    address[] memory registryAddresses = new address[](config.maxBatchSize);
+    uint256[] memory topUpAmounts = new uint256[](config.maxBatchSize);
     uint256 availableFunds = LINK_TOKEN.balanceOf(address(this));
     uint256 count;
-    uint256 upkeepID;
-    for (uint256 i = 0; i < numUpkeeps; i++) {
-      upkeepID = s_watchList[i];
-      uint96 upkeepBalance = registry.getBalance(upkeepID);
-      uint256 minBalance = uint256(registry.getMinBalance(upkeepID));
-      uint256 topUpThreshold = (minBalance * config.minPercentage) / 100;
-      uint256 topUpAmount = ((minBalance * config.targetPercentage) / 100) - upkeepBalance;
-      if (topUpAmount > config.maxTopUpAmount) {
-        topUpAmount = config.maxTopUpAmount;
-      }
-      if (upkeepBalance <= topUpThreshold && availableFunds >= topUpAmount) {
-        needsFunding[count] = upkeepID;
-        topUpAmounts[count] = topUpAmount;
-        count++;
-        availableFunds -= topUpAmount;
+    for (uint256 i = 0; i < s_registries.length(); i++) {
+      IAutomationRegistryConsumer registry = IAutomationRegistryConsumer(s_registries.at(i));
+      for (uint256 j = 0; j < s_registryWatchLists[address(registry)].length; j++) {
+        uint256 upkeepID = s_registryWatchLists[address(registry)][j];
+        uint96 upkeepBalance = registry.getBalance(upkeepID);
+        uint256 minBalance = uint256(registry.getMinBalance(upkeepID));
+        uint256 topUpThreshold = (minBalance * config.minPercentage) / 100;
+        uint256 topUpAmount = ((minBalance * config.targetPercentage) / 100) - upkeepBalance;
+        if (topUpAmount > config.maxTopUpAmount) {
+          topUpAmount = config.maxTopUpAmount;
+        }
+        if (upkeepBalance <= topUpThreshold && availableFunds >= topUpAmount) {
+          needsFunding[count] = upkeepID;
+          topUpAmounts[count] = topUpAmount;
+          registryAddresses[count] = address(registry);
+          count++;
+          availableFunds -= topUpAmount;
+        }
+        if (count == config.maxBatchSize) {
+          break;
+        }
       }
       if (count == config.maxBatchSize) {
         break;
       }
     }
-    if (count < numUpkeeps) {
+    if (count < config.maxBatchSize) {
       assembly {
         mstore(needsFunding, count)
+        mstore(registryAddresses, count)
         mstore(topUpAmounts, count)
       }
     }
-    return (needsFunding, topUpAmounts);
+    return (needsFunding, registryAddresses, topUpAmounts);
   }
 
   /// @notice Called by the keeper/owner to send funds to underfunded upkeeps
   /// @param upkeepIDs the list of upkeep ids to fund
+  /// @param registryAddresses the list of registries that the upkeepIDs belong to
   /// @param topUpAmounts the list of amounts to fund each upkeep with
   /// @dev We explicitly choose not to verify that input upkeepIDs are included in the watchlist. We also
   /// explicity permit any amount to be sent via topUpAmounts; it does not have to meet the criteria
   /// specified in getUnderfundedUpkeeps(). Here, we are relying on the security of automation's OCR to
   /// secure the output of getUnderfundedUpkeeps() as the input to topUp(), and we are treating the owner
   /// as a privileged user that can perform arbitrary top-ups to any upkeepID.
-  function topUp(uint256[] memory upkeepIDs, uint96[] memory topUpAmounts) public {
-    IAutomationForwarder forwarder = s_forwarder;
-    if (msg.sender != address(s_forwarder) && msg.sender != owner()) revert OnlyForwarderOrOwner();
+  function topUp(uint256[] memory upkeepIDs, address[] memory registryAddresses, uint96[] memory topUpAmounts) public {
+    if (msg.sender != address(s_forwarderAddress) && msg.sender != owner()) revert OnlyForwarderOrOwner();
     if (upkeepIDs.length != topUpAmounts.length) revert InvalidTopUpData();
-    address registryAddress = address(forwarder.getRegistry());
     for (uint256 i = 0; i < upkeepIDs.length; i++) {
-      try LINK_TOKEN.transferAndCall(registryAddress, topUpAmounts[i], abi.encode(upkeepIDs[i])) returns (
+      try LINK_TOKEN.transferAndCall(registryAddresses[i], topUpAmounts[i], abi.encode(upkeepIDs[i])) returns (
         bool success
       ) {
         if (success) {
@@ -134,10 +143,14 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   function checkUpkeep(
     bytes calldata
   ) external view whenNotPaused returns (bool upkeepNeeded, bytes memory performData) {
-    (uint256[] memory needsFunding, uint256[] memory topUpAmounts) = getUnderfundedUpkeeps();
+    (
+      uint256[] memory needsFunding,
+      address[] memory registryAddresses,
+      uint256[] memory topUpAmounts
+    ) = getUnderfundedUpkeeps();
     upkeepNeeded = needsFunding.length > 0;
     if (upkeepNeeded) {
-      performData = abi.encode(needsFunding, topUpAmounts);
+      performData = abi.encode(needsFunding, registryAddresses, topUpAmounts);
     }
     return (upkeepNeeded, performData);
   }
@@ -145,8 +158,11 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   /// @notice Called by the keeper to send funds to underfunded addresses.
   /// @param performData the abi encoded list of addresses to fund
   function performUpkeep(bytes calldata performData) external whenNotPaused {
-    (uint256[] memory upkeepIDs, uint96[] memory topUpAmounts) = abi.decode(performData, (uint256[], uint96[]));
-    topUp(upkeepIDs, topUpAmounts);
+    (uint256[] memory upkeepIDs, address[] memory registryAddresses, uint96[] memory topUpAmounts) = abi.decode(
+      performData,
+      (uint256[], address[], uint96[])
+    );
+    topUp(upkeepIDs, registryAddresses, topUpAmounts);
   }
 
   // ================================================================
@@ -178,9 +194,15 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
 
   /// @notice Sets the list of upkeeps to watch and their funding parameters.
   /// @param watchlist the list of subscription ids to watch
-  function setWatchList(uint256[] calldata watchlist) external onlyOwner {
-    s_watchList = watchlist;
-    emit WatchListSet();
+  function setWatchList(address registryAddress, uint256[] calldata watchlist) external onlyOwner {
+    if (watchlist.length == 0) {
+      s_registries.remove(registryAddress);
+      delete s_registryWatchLists[registryAddress];
+    } else {
+      s_registries.add(registryAddress);
+      s_registryWatchLists[registryAddress] = watchlist;
+    }
+    emit WatchListSet(registryAddress);
   }
 
   /// @notice Sets the contract config
@@ -199,11 +221,11 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   }
 
   /// @notice Sets the upkeep's forwarder contract
-  /// @param forwarder the new forwarder
+  /// @param forwarderAddress the new forwarder
   /// @dev this should only need to be called once, after registering the contract with the registry
-  function setForwarder(IAutomationForwarder forwarder) external onlyOwner {
-    s_forwarder = forwarder;
-    emit ForwarderSet(forwarder);
+  function setForwarder(address forwarderAddress) external onlyOwner {
+    s_forwarderAddress = forwarderAddress;
+    emit ForwarderSet(forwarderAddress);
   }
 
   // ================================================================
@@ -211,8 +233,13 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   // ================================================================
 
   /// @notice Gets the list of upkeeps ids being monitored
-  function getWatchList() external view returns (uint256[] memory) {
-    return s_watchList;
+  function getWatchList() external view returns (address[] memory, uint256[][] memory) {
+    address[] memory registryAddresses = s_registries.values();
+    uint256[][] memory upkeepIDs = new uint256[][](registryAddresses.length);
+    for (uint256 i = 0; i < registryAddresses.length; i++) {
+      upkeepIDs[i] = s_registryWatchLists[registryAddresses[i]];
+    }
+    return (registryAddresses, upkeepIDs);
   }
 
   /// @notice Gets the contract config
@@ -221,12 +248,7 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   }
 
   /// @notice Gets the upkeep's forwarder contract
-  function getForwarder() external view returns (IAutomationForwarder) {
-    return s_forwarder;
-  }
-
-  /// @notice Gets the registry contract
-  function getRegistry() public view returns (IAutomationRegistryConsumer) {
-    return s_forwarder.getRegistry();
+  function getForwarder() external view returns (address) {
+    return s_forwarderAddress;
   }
 }
diff --git a/contracts/test/v0.8/automation/UpkeepBalanceMonitor.test.ts b/contracts/test/v0.8/automation/UpkeepBalanceMonitor.test.ts
index 4c8ee70c301..fa2880b8c37 100644
--- a/contracts/test/v0.8/automation/UpkeepBalanceMonitor.test.ts
+++ b/contracts/test/v0.8/automation/UpkeepBalanceMonitor.test.ts
@@ -1,5 +1,5 @@
 import { ethers } from 'hardhat'
-import chai, { assert, expect } from 'chai'
+import { expect } from 'chai'
 import type { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'
 import { randomAddress } from '../../test-helpers/helpers'
 import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'
@@ -16,6 +16,7 @@ import {
 let owner: SignerWithAddress
 let stranger: SignerWithAddress
 let registry: MockContract
+let registry2: MockContract
 let forwarder: MockContract
 let linkToken: LinkToken
 let upkeepBalanceMonitor: UpkeepBalanceMonitor
@@ -41,6 +42,7 @@ const setup = async () => {
     maxTopUpAmount: ethers.utils.parseEther('100'),
   })
   registry = await deployMockContract(owner, RegistryFactory.abi)
+  registry2 = await deployMockContract(owner, RegistryFactory.abi)
   forwarder = await deployMockContract(owner, ForwarderFactory.abi)
   await forwarder.mock.getRegistry.returns(registry.address)
   await upkeepBalanceMonitor.setForwarder(forwarder.address)
@@ -49,11 +51,18 @@ const setup = async () => {
     .transfer(upkeepBalanceMonitor.address, ethers.utils.parseEther('10000'))
   await upkeepBalanceMonitor
     .connect(owner)
-    .setWatchList([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
-  for (let i = 0; i < 12; i++) {
+    .setWatchList(registry.address, [0, 1, 2, 3, 4, 5, 6, 7, 8])
+  await upkeepBalanceMonitor
+    .connect(owner)
+    .setWatchList(registry2.address, [9, 10, 11])
+  for (let i = 0; i < 9; i++) {
     await registry.mock.getMinBalance.withArgs(i).returns(100)
     await registry.mock.getBalance.withArgs(i).returns(121) // all upkeeps are sufficiently funded
   }
+  for (let i = 9; i < 12; i++) {
+    await registry2.mock.getMinBalance.withArgs(i).returns(100)
+    await registry2.mock.getBalance.withArgs(i).returns(121) // all upkeeps are sufficiently funded
+  }
 }
 
 describe('UpkeepBalanceMonitor', () => {
@@ -133,21 +142,29 @@ describe('UpkeepBalanceMonitor', () => {
     ]
 
     it('should add addresses to the watchlist', async () => {
-      await upkeepBalanceMonitor.connect(owner).setWatchList(newWatchList)
-      const watchList = await upkeepBalanceMonitor.getWatchList()
-      expect(watchList).to.deep.equal(newWatchList)
+      await upkeepBalanceMonitor
+        .connect(owner)
+        .setWatchList(registry.address, newWatchList)
+      const [_, upkeepIDs] = await upkeepBalanceMonitor.getWatchList()
+      expect(upkeepIDs[0]).to.deep.equal(newWatchList)
     })
 
     it('cannot be called by a non-owner', async () => {
       await expect(
-        upkeepBalanceMonitor.connect(stranger).setWatchList([1, 2, 3]),
+        upkeepBalanceMonitor
+          .connect(stranger)
+          .setWatchList(registry.address, [1, 2, 3]),
       ).to.be.revertedWith('Only callable by owner')
     })
 
     it('should emit an event', async () => {
       await expect(
-        upkeepBalanceMonitor.connect(owner).setWatchList(newWatchList),
-      ).to.emit(upkeepBalanceMonitor, 'WatchListSet')
+        upkeepBalanceMonitor
+          .connect(owner)
+          .setWatchList(registry.address, newWatchList),
+      )
+        .to.emit(upkeepBalanceMonitor, 'WatchListSet')
+        .withArgs(registry.address)
     })
   })
 
@@ -204,9 +221,10 @@ describe('UpkeepBalanceMonitor', () => {
 
   describe('checkUpkeep() / getUnderfundedUpkeeps()', () => {
     it('should find the underfunded upkeeps', async () => {
-      let [upkeepIDs, topUpAmounts] =
+      let [upkeepIDs, registries, topUpAmounts] =
         await upkeepBalanceMonitor.getUnderfundedUpkeeps()
       expect(upkeepIDs.length).to.equal(0)
+      expect(registries.length).to.equal(0)
       expect(topUpAmounts.length).to.equal(0)
       let [upkeepNeeded, performData] =
         await upkeepBalanceMonitor.checkUpkeep('0x')
@@ -216,9 +234,14 @@ describe('UpkeepBalanceMonitor', () => {
       await registry.mock.getBalance.withArgs(2).returns(120)
       await registry.mock.getBalance.withArgs(4).returns(15)
       await registry.mock.getBalance.withArgs(5).returns(0)
-      ;[upkeepIDs, topUpAmounts] =
+      ;[upkeepIDs, registries, topUpAmounts] =
         await upkeepBalanceMonitor.getUnderfundedUpkeeps()
       expect(upkeepIDs.map((v) => v.toNumber())).to.deep.equal([2, 4, 5])
+      expect(registries).to.deep.equal([
+        registry.address,
+        registry.address,
+        registry.address,
+      ])
       expect(topUpAmounts.map((v) => v.toNumber())).to.deep.equal([
         180, 285, 300,
       ])
@@ -227,38 +250,50 @@ describe('UpkeepBalanceMonitor', () => {
       expect(upkeepNeeded).to.be.true
       expect(performData).to.equal(
         ethers.utils.defaultAbiCoder.encode(
-          ['uint256[]', 'uint256[]'],
+          ['uint256[]', 'address[]', 'uint256[]'],
           [
             [2, 4, 5],
+            [registry.address, registry.address, registry.address],
             [180, 285, 300],
           ],
         ),
       )
       // update all to need funding
-      for (let i = 0; i < 12; i++) {
+      for (let i = 0; i < 9; i++) {
         await registry.mock.getBalance.withArgs(i).returns(0)
       }
+      for (let i = 9; i < 12; i++) {
+        await registry2.mock.getBalance.withArgs(i).returns(0)
+      }
       // only the max batch size are included in the list
-      ;[upkeepIDs, topUpAmounts] =
+      ;[upkeepIDs, registries, topUpAmounts] =
         await upkeepBalanceMonitor.getUnderfundedUpkeeps()
       expect(upkeepIDs.length).to.equal(10)
       expect(topUpAmounts.length).to.equal(10)
       expect(upkeepIDs.map((v) => v.toNumber())).to.deep.equal([
         0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
-      ]) // 0-9
+      ])
+      expect(registries).to.deep.equal([
+        ...Array(9).fill(registry.address),
+        registry2.address,
+      ])
       expect(topUpAmounts.map((v) => v.toNumber())).to.deep.equal([
         ...Array(10).fill(300),
       ])
       // update the balance for some upkeeps
       await registry.mock.getBalance.withArgs(0).returns(300)
       await registry.mock.getBalance.withArgs(5).returns(300)
-      ;[upkeepIDs, topUpAmounts] =
+      ;[upkeepIDs, registries, topUpAmounts] =
         await upkeepBalanceMonitor.getUnderfundedUpkeeps()
       expect(upkeepIDs.length).to.equal(10)
       expect(topUpAmounts.length).to.equal(10)
       expect(upkeepIDs.map((v) => v.toNumber())).to.deep.equal([
         1, 2, 3, 4, 6, 7, 8, 9, 10, 11,
       ])
+      expect(registries).to.deep.equal([
+        ...Array(7).fill(registry.address),
+        ...Array(3).fill(registry2.address),
+      ])
       expect(topUpAmounts.map((v) => v.toNumber())).to.deep.equal([
         ...Array(10).fill(300),
       ])
@@ -285,7 +320,7 @@ describe('UpkeepBalanceMonitor', () => {
 
     it('cannot be called by a non-owner', async () => {
       await expect(
-        upkeepBalanceMonitor.connect(stranger).topUp([], []),
+        upkeepBalanceMonitor.connect(stranger).topUp([], [], []),
       ).to.be.revertedWith('OnlyForwarderOrOwner()')
     })
 
@@ -293,7 +328,7 @@ describe('UpkeepBalanceMonitor', () => {
       const initialBalance = await linkToken.balanceOf(registry.address)
       const tx = await upkeepBalanceMonitor
         .connect(owner)
-        .topUp([1, 7], [100, 50])
+        .topUp([1, 7], [registry.address, registry.address], [100, 50])
       const finalBalance = await linkToken.balanceOf(registry.address)
       expect(finalBalance).to.equal(initialBalance.add(150))
       await expect(tx)
@@ -308,7 +343,11 @@ describe('UpkeepBalanceMonitor', () => {
       const initialBalance = await linkToken.balanceOf(registry.address)
       const tx = await upkeepBalanceMonitor
         .connect(owner)
-        .topUp([1, 7, 100], [100, 50, 100])
+        .topUp(
+          [1, 7, 100],
+          [registry.address, registry.address, registry.address],
+          [100, 50, 100],
+        )
       const finalBalance = await linkToken.balanceOf(registry.address)
       expect(finalBalance).to.equal(initialBalance.add(150))
       await expect(tx)

From 280e9029eb6b6b1f3907636e5fca584a417ee95a Mon Sep 17 00:00:00 2001
From: Ryan Hall <hall.ryan.r@gmail.com>
Date: Wed, 8 Nov 2023 10:53:40 -0500
Subject: [PATCH 31/33] cleanup

---
 .../src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol     | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol
index 5f21029c1a6..963eb9cbd44 100644
--- a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol
+++ b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol
@@ -192,8 +192,9 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   // |                           SETTERS                            |
   // ================================================================
 
-  /// @notice Sets the list of upkeeps to watch and their funding parameters.
-  /// @param watchlist the list of subscription ids to watch
+  /// @notice Sets the list of upkeeps to watch
+  /// @param registryAddress the registry that this watchlist applies to
+  /// @param watchlist the list of UpkeepIDs to watch
   function setWatchList(address registryAddress, uint256[] calldata watchlist) external onlyOwner {
     if (watchlist.length == 0) {
       s_registries.remove(registryAddress);

From 2d30ed34807ba81f8919cc22ef2eacc4ffc21fec Mon Sep 17 00:00:00 2001
From: Ryan Hall <hall.ryan.r@gmail.com>
Date: Wed, 8 Nov 2023 15:16:36 -0500
Subject: [PATCH 32/33] change topUpAmounts to uint96[]

---
 .../v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol
index 963eb9cbd44..f074717985e 100644
--- a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol
+++ b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol
@@ -66,11 +66,11 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   /// @return needsFunding list of underfunded upkeepIDs
   /// @return registryAddresses list of registries that the upkeepIDs belong to
   /// @return topUpAmounts amount to top up each upkeep
-  function getUnderfundedUpkeeps() public view returns (uint256[] memory, address[] memory, uint256[] memory) {
+  function getUnderfundedUpkeeps() public view returns (uint256[] memory, address[] memory, uint96[] memory) {
     Config memory config = s_config;
     uint256[] memory needsFunding = new uint256[](config.maxBatchSize);
     address[] memory registryAddresses = new address[](config.maxBatchSize);
-    uint256[] memory topUpAmounts = new uint256[](config.maxBatchSize);
+    uint96[] memory topUpAmounts = new uint96[](config.maxBatchSize);
     uint256 availableFunds = LINK_TOKEN.balanceOf(address(this));
     uint256 count;
     for (uint256 i = 0; i < s_registries.length(); i++) {
@@ -78,9 +78,9 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
       for (uint256 j = 0; j < s_registryWatchLists[address(registry)].length; j++) {
         uint256 upkeepID = s_registryWatchLists[address(registry)][j];
         uint96 upkeepBalance = registry.getBalance(upkeepID);
-        uint256 minBalance = uint256(registry.getMinBalance(upkeepID));
-        uint256 topUpThreshold = (minBalance * config.minPercentage) / 100;
-        uint256 topUpAmount = ((minBalance * config.targetPercentage) / 100) - upkeepBalance;
+        uint96 minBalance = registry.getMinBalance(upkeepID);
+        uint96 topUpThreshold = (minBalance * config.minPercentage) / 100;
+        uint96 topUpAmount = ((minBalance * config.targetPercentage) / 100) - upkeepBalance;
         if (topUpAmount > config.maxTopUpAmount) {
           topUpAmount = config.maxTopUpAmount;
         }
@@ -146,7 +146,7 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
     (
       uint256[] memory needsFunding,
       address[] memory registryAddresses,
-      uint256[] memory topUpAmounts
+      uint96[] memory topUpAmounts
     ) = getUnderfundedUpkeeps();
     upkeepNeeded = needsFunding.length > 0;
     if (upkeepNeeded) {

From 06dfba231c75726ac75db1f70f33dad3c8de1f10 Mon Sep 17 00:00:00 2001
From: Ryan Hall <hall.ryan.r@gmail.com>
Date: Wed, 15 Nov 2023 14:50:20 -0500
Subject: [PATCH 33/33] move pausable to topUp(); add length check to topUp()

---
 .../automation/upkeeps/UpkeepBalanceMonitor.sol  | 15 +++++++++------
 .../v0.8/automation/UpkeepBalanceMonitor.test.ts | 16 +++++++---------
 2 files changed, 16 insertions(+), 15 deletions(-)

diff --git a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol
index f074717985e..dae17da7293 100644
--- a/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol
+++ b/contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol
@@ -118,9 +118,14 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
   /// specified in getUnderfundedUpkeeps(). Here, we are relying on the security of automation's OCR to
   /// secure the output of getUnderfundedUpkeeps() as the input to topUp(), and we are treating the owner
   /// as a privileged user that can perform arbitrary top-ups to any upkeepID.
-  function topUp(uint256[] memory upkeepIDs, address[] memory registryAddresses, uint96[] memory topUpAmounts) public {
+  function topUp(
+    uint256[] memory upkeepIDs,
+    address[] memory registryAddresses,
+    uint96[] memory topUpAmounts
+  ) public whenNotPaused {
     if (msg.sender != address(s_forwarderAddress) && msg.sender != owner()) revert OnlyForwarderOrOwner();
-    if (upkeepIDs.length != topUpAmounts.length) revert InvalidTopUpData();
+    if (upkeepIDs.length != registryAddresses.length || upkeepIDs.length != topUpAmounts.length)
+      revert InvalidTopUpData();
     for (uint256 i = 0; i < upkeepIDs.length; i++) {
       try LINK_TOKEN.transferAndCall(registryAddresses[i], topUpAmounts[i], abi.encode(upkeepIDs[i])) returns (
         bool success
@@ -140,9 +145,7 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
 
   /// @notice Gets list of upkeeps ids that are underfunded and returns a keeper-compatible payload.
   /// @return upkeepNeeded signals if upkeep is needed, performData is an abi encoded list of subscription ids that need funds
-  function checkUpkeep(
-    bytes calldata
-  ) external view whenNotPaused returns (bool upkeepNeeded, bytes memory performData) {
+  function checkUpkeep(bytes calldata) external view returns (bool upkeepNeeded, bytes memory performData) {
     (
       uint256[] memory needsFunding,
       address[] memory registryAddresses,
@@ -157,7 +160,7 @@ contract UpkeepBalanceMonitor is ConfirmedOwner, Pausable {
 
   /// @notice Called by the keeper to send funds to underfunded addresses.
   /// @param performData the abi encoded list of addresses to fund
-  function performUpkeep(bytes calldata performData) external whenNotPaused {
+  function performUpkeep(bytes calldata performData) external {
     (uint256[] memory upkeepIDs, address[] memory registryAddresses, uint96[] memory topUpAmounts) = abi.decode(
       performData,
       (uint256[], address[], uint96[])
diff --git a/contracts/test/v0.8/automation/UpkeepBalanceMonitor.test.ts b/contracts/test/v0.8/automation/UpkeepBalanceMonitor.test.ts
index fa2880b8c37..259a9c3b9f8 100644
--- a/contracts/test/v0.8/automation/UpkeepBalanceMonitor.test.ts
+++ b/contracts/test/v0.8/automation/UpkeepBalanceMonitor.test.ts
@@ -324,6 +324,13 @@ describe('UpkeepBalanceMonitor', () => {
       ).to.be.revertedWith('OnlyForwarderOrOwner()')
     })
 
+    it('should revert if the contract is paused', async () => {
+      await upkeepBalanceMonitor.connect(owner).pause()
+      await expect(
+        upkeepBalanceMonitor.connect(owner).topUp([], [], []),
+      ).to.be.revertedWith('Pausable: paused')
+    })
+
     it('tops up the upkeeps by the amounts provided', async () => {
       const initialBalance = await linkToken.balanceOf(registry.address)
       const tx = await upkeepBalanceMonitor
@@ -362,15 +369,6 @@ describe('UpkeepBalanceMonitor', () => {
     })
   })
 
-  describe('performUpkeep()', () => {
-    it('should revert if the contract is paused', async () => {
-      await upkeepBalanceMonitor.connect(owner).pause()
-      await expect(
-        upkeepBalanceMonitor.connect(owner).performUpkeep('0x'),
-      ).to.be.revertedWith('Pausable: paused')
-    })
-  })
-
   describe('checkUpkeep() / performUpkeep()', () => {
     it('works round-trip', async () => {
       await registry.mock.getBalance.withArgs(1).returns(100) // needs 200