Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upkeep Balance Monitor #11180

Merged
merged 34 commits into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
5dbcfa7
write UpkeepBalanceMonWithBuffer
RyanRHall Aug 3, 2023
032c03a
Update AutomationRegistryInterface2_0.sol
wentzeld Aug 4, 2023
a6f2ac0
reformat
RyanRHall Nov 1, 2023
5c64517
remove modifier
RyanRHall Nov 2, 2023
13e990c
add typeAndVersion check in constructor
RyanRHall Nov 2, 2023
10956d0
restore AutomationRegistryInterface2_0.sol function
RyanRHall Nov 2, 2023
16a7fec
update import paths
RyanRHall Nov 2, 2023
ddefd43
update comments
RyanRHall Nov 2, 2023
91d61fa
cleanup
RyanRHall Nov 2, 2023
c9a1222
get rid of redundant REGISTRY variable
RyanRHall Nov 2, 2023
9550f7e
cleanup
RyanRHall Nov 2, 2023
4988a67
rename
RyanRHall Nov 3, 2023
34eae70
cleanup
RyanRHall Nov 3, 2023
4a60015
remove target, min wait period, switch to min/target percentages
RyanRHall Nov 3, 2023
2093d56
refactor getUnderfundedUpkeeps() to return top up amounts
RyanRHall Nov 3, 2023
301e2c7
refactor topUp() function
RyanRHall Nov 3, 2023
431a2f1
switch to max batch size
RyanRHall Nov 3, 2023
502242e
add max top up amount
RyanRHall Nov 3, 2023
c9837bf
add maxTopUpAmount
RyanRHall Nov 3, 2023
4a195af
whitelist performUpkeep to forwarder
RyanRHall Nov 3, 2023
5cd20ba
cleanup
RyanRHall Nov 3, 2023
cfe5f69
rename
RyanRHall Nov 3, 2023
f79ad5f
bring topUp() back
RyanRHall Nov 3, 2023
b076491
write initial test suite
RyanRHall Nov 3, 2023
f73ab09
fix solhint errors
RyanRHall Nov 6, 2023
975f015
update test for getUnderfundedUpkeeps()
RyanRHall Nov 6, 2023
264715c
add tests for owner only functions and events
RyanRHall Nov 6, 2023
bf5ce99
add topUp and performUpkeep tests
RyanRHall Nov 6, 2023
cf32810
rearrange functions on contract
RyanRHall Nov 6, 2023
c459484
add support for multiple registries
RyanRHall Nov 8, 2023
280e902
cleanup
RyanRHall Nov 8, 2023
2d30ed3
change topUpAmounts to uint96[]
RyanRHall Nov 8, 2023
06dfba2
move pausable to topUp(); add length check to topUp()
RyanRHall Nov 15, 2023
e9162f0
Merge branch 'develop' into upkeepbalancemonitor
RyanRHall Nov 27, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
258 changes: 258 additions & 0 deletions contracts/src/v0.8/automation/upkeeps/UpkeepBalanceMonitor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
// SPDX-License-Identifier: MIT

pragma solidity 0.8.19;

import {ConfirmedOwner} from "../../shared/access/ConfirmedOwner.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(address forwarderAddress);
event FundsWithdrawn(uint256 amountWithdrawn, address payee);
event TopUpFailed(uint256 indexed upkeepId);
event TopUpSucceeded(uint256 indexed upkeepId, uint96 amount);
event WatchListSet(address registryAddress);

error InvalidConfig();
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
/// @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;
uint96 maxTopUpAmount;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, any specific reason of using those int types? I guess uint8 should be enough for percentage?

}

// ================================================================
// | STORAGE |
// ================================================================

LinkTokenInterface private immutable LINK_TOKEN;

mapping(address => uint256[]) s_registryWatchLists;
EnumerableSet.AddressSet s_registries;
Config private s_config;
address private s_forwarderAddress;

// ================================================================
// | CONSTRUCTOR |
// ================================================================

/// @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);
}

// ================================================================
// | CORE FUNCTIONALITY |
// ================================================================

/// @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, address[] memory, uint96[] memory) {
Config memory config = s_config;
uint256[] memory needsFunding = new uint256[](config.maxBatchSize);
address[] memory registryAddresses = new address[](config.maxBatchSize);
uint96[] memory topUpAmounts = new uint96[](config.maxBatchSize);
uint256 availableFunds = LINK_TOKEN.balanceOf(address(this));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this means this current contract holds LINK which will be used to auto fund the upkeeps. Is it possible to give a warning when this availableFunds is lower than topUpAmount as in L83?

uint256 count;
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);
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;
}
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 < config.maxBatchSize) {
assembly {
mstore(needsFunding, count)
mstore(registryAddresses, count)
mstore(topUpAmounts, count)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

n00b, what does this mean? we are setting the value of count to the address of topUpAmounts? isn't topUpAmounts of type uint256[]?

Copy link
Contributor

@FelixFan1992 FelixFan1992 Nov 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these arrays are created by the size of config.maxBatchSize
however, we only have count number of underfunded upkeeps
this basically cuts the array size to count and saves the gas using inline assembly.

see https://docs.soliditylang.org/en/v0.8.16/internals/layout_in_storage.html#mappings-and-dynamic-arrays

}
}
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,
address[] memory registryAddresses,
uint96[] memory topUpAmounts
) public whenNotPaused {
if (msg.sender != address(s_forwarderAddress) && msg.sender != owner()) revert OnlyForwarderOrOwner();
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
) {
if (success) {
emit TopUpSucceeded(upkeepIDs[i], topUpAmounts[i]);
continue;
}
} catch {}
emit TopUpFailed(upkeepIDs[i]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so, if there is any upkeep fails to topUp, we log this event and carry on.

Nit: is it more readable to have this emit TopUpFailed(upkeepIDs[i]); in the catch clause?

}
}

// ================================================================
// | 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 returns (bool upkeepNeeded, bytes memory performData) {
(
uint256[] memory needsFunding,
address[] memory registryAddresses,
uint96[] memory topUpAmounts
) = getUnderfundedUpkeeps();
upkeepNeeded = needsFunding.length > 0;
if (upkeepNeeded) {
performData = abi.encode(needsFunding, registryAddresses, 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 {
(uint256[] memory upkeepIDs, address[] memory registryAddresses, uint96[] memory topUpAmounts) = abi.decode(
performData,
(uint256[], address[], uint96[])
);
topUp(upkeepIDs, registryAddresses, topUpAmounts);
}

// ================================================================
// | ADMIN |
// ================================================================

/// @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 payee) external onlyOwner {
require(payee != address(0));
LINK_TOKEN.transfer(payee, amount);
emit FundsWithdrawn(amount, payee);
}

/// @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
/// @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);
delete s_registryWatchLists[registryAddress];
} else {
s_registries.add(registryAddress);
s_registryWatchLists[registryAddress] = watchlist;
}
emit WatchListSet(registryAddress);
}

/// @notice Sets the contract config
/// @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;
emit ConfigSet(config);
}

/// @notice Sets the upkeep's forwarder contract
/// @param forwarderAddress the new forwarder
/// @dev this should only need to be called once, after registering the contract with the registry
function setForwarder(address forwarderAddress) external onlyOwner {
s_forwarderAddress = forwarderAddress;
emit ForwarderSet(forwarderAddress);
}

// ================================================================
// | GETTERS |
// ================================================================

/// @notice Gets the list of upkeeps ids being monitored
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
function getConfig() external view returns (Config memory) {
return s_config;
}

/// @notice Gets the upkeep's forwarder contract
function getForwarder() external view returns (address) {
return s_forwarderAddress;
}
}
Loading
Loading