-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
Upkeep Balance Monitor #11180
Changes from all commits
5dbcfa7
032c03a
a6f2ac0
5c64517
13e990c
10956d0
16a7fec
ddefd43
91d61fa
c9a1222
9550f7e
4988a67
34eae70
4a60015
2093d56
301e2c7
431a2f1
502242e
c9837bf
4a195af
5cd20ba
cfe5f69
f79ad5f
b076491
f73ab09
975f015
264715c
bf5ce99
cf32810
c459484
280e902
2d30ed3
06dfba2
e9162f0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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; | ||
} | ||
|
||
// ================================================================ | ||
// | 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)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. n00b, what does this mean? we are setting the value of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. these arrays are created by the size of |
||
} | ||
} | ||
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]); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
} | ||
} | ||
|
||
// ================================================================ | ||
// | 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; | ||
} | ||
} |
There was a problem hiding this comment.
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?