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