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

AUTO-10716: fix a funding bug in LinkAvailableBalanceMonitor #13364

Merged
merged 8 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
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
6 changes: 6 additions & 0 deletions .changeset/eleven-terms-explain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"chainlink": patch
---

#bugfix
fix a funding bug in LinkAvailableBalanceMonitor
6 changes: 6 additions & 0 deletions contracts/.changeset/tricky-meals-decide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@chainlink/contracts": patch
---

#bugfix
fix a funding bug in LinkAvailableBalanceMonitor
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ interface ILinkAvailable {
/// at your own risk!!!
/// @dev some areas for improvement / acknowledgement of limitations:
/// validate that all addresses conform to interface when adding them to the watchlist
/// this is a "trusless" upkeep, meaning it does not trust the caller of performUpkeep;
/// this is a "trustless" upkeep, meaning it does not trust the caller of performUpkeep;
/// we could save a fair amount of gas and re-write this upkeep for use with Automation v2.0+,
/// which has significantly different trust assumptions
contract LinkAvailableBalanceMonitor is AccessControl, AutomationCompatibleInterface, Pausable {
Expand All @@ -47,7 +47,7 @@ contract LinkAvailableBalanceMonitor is AccessControl, AutomationCompatibleInter
event MinWaitPeriodSet(uint256 s_minWaitPeriodSeconds, uint256 minWaitPeriodSeconds);
event TopUpBlocked(address indexed topUpAddress);
event TopUpFailed(address indexed recipient);
event TopUpSucceeded(address indexed topUpAddress);
event TopUpSucceeded(address indexed topUpAddress, uint256 amount);
event TopUpUpdated(address indexed addr, uint256 oldTopUpAmount, uint256 newTopUpAmount);
event WatchlistUpdated();

Expand Down Expand Up @@ -170,9 +170,9 @@ contract LinkAvailableBalanceMonitor is AccessControl, AutomationCompatibleInter
/// @param targetAddress the address to be added to the watchlist
/// @param dstChainSelector carries a non-zero value in case the targetAddress is an onRamp, otherwise it carries a 0
/// @dev this function has to be compatible with the event onRampSet(address, dstChainSelector) emitted by
/// the CCIP router. Important detail to know is this event is also emitted when an onRamp is decomissioned,
/// the CCIP router. Important detail to know is this event is also emitted when an onRamp is decommissioned,
/// in which case it will carry the proper dstChainSelector along with the 0x0 address
function addToWatchListOrDecomission(address targetAddress, uint64 dstChainSelector) public onlyAdminOrExecutor {
function addToWatchListOrDecommission(address targetAddress, uint64 dstChainSelector) public onlyAdminOrExecutor {
if (s_targets[targetAddress].isActive) revert DuplicateAddress(targetAddress);
if (targetAddress == address(0) && dstChainSelector == 0) revert InvalidAddress(targetAddress);
bool onRampExists = s_onRampAddresses.contains(dstChainSelector);
Expand All @@ -195,7 +195,7 @@ contract LinkAvailableBalanceMonitor is AccessControl, AutomationCompatibleInter
if (dstChainSelector > 0) {
s_onRampAddresses.set(dstChainSelector, targetAddress);
}
// else if is refundant as this is the only corner case left, maintaining it for legibility
// else if is redundant as this is the only corner case left, maintaining it for legibility
} else if (targetAddress == address(0) && dstChainSelector > 0) {
s_onRampAddresses.remove(dstChainSelector);
}
Expand Down Expand Up @@ -227,21 +227,21 @@ contract LinkAvailableBalanceMonitor is AccessControl, AutomationCompatibleInter
uint256 minWaitPeriod = s_minWaitPeriodSeconds;
address[] memory targetsToFund = new address[](maxPerform);
MonitoredAddress memory contractToFund;
address targetAddress;
for (
uint256 numChecked = 0;
numChecked < numToCheck;
(idx, numChecked) = ((idx + 1) % numTargets, numChecked + 1)
) {
address targetAddress = s_watchList.at(idx);
targetAddress = s_watchList.at(idx);
contractToFund = s_targets[targetAddress];
if (
_needsFunding(
targetAddress,
contractToFund.lastTopUpTimestamp + minWaitPeriod,
contractToFund.minBalance,
contractToFund.isActive
)
) {
(bool fundingNeeded, ) = _needsFunding(
targetAddress,
contractToFund.lastTopUpTimestamp + minWaitPeriod,
contractToFund.minBalance,
contractToFund.isActive
);
if (fundingNeeded) {
targetsToFund[numFound] = targetAddress;
numFound++;
if (numFound == maxPerform) {
Expand All @@ -267,64 +267,65 @@ contract LinkAvailableBalanceMonitor is AccessControl, AutomationCompatibleInter
address targetAddress = targetAddresses[idx];
contractToFund = s_targets[targetAddress];
s_targets[targetAddress].lastTopUpTimestamp = uint56(block.timestamp);
if (
localBalance >= contractToFund.topUpAmount &&
_needsFunding(
targetAddress,
contractToFund.lastTopUpTimestamp + minWaitPeriod,
contractToFund.minBalance,
contractToFund.isActive
)
) {
bool success = i_linkToken.transfer(targetAddress, contractToFund.topUpAmount);

(bool fundingNeeded, address target) = _needsFunding(
targetAddress,
contractToFund.lastTopUpTimestamp + minWaitPeriod,
contractToFund.minBalance,
contractToFund.isActive
);
if (localBalance >= contractToFund.topUpAmount && fundingNeeded) {
bool success = i_linkToken.transfer(target, contractToFund.topUpAmount);
if (success) {
localBalance -= contractToFund.topUpAmount;
emit TopUpSucceeded(targetAddress);
emit TopUpSucceeded(target, contractToFund.topUpAmount);
} else {
s_targets[targetAddress].lastTopUpTimestamp = contractToFund.lastTopUpTimestamp;
s_targets[targetAddress].lastTopUpTimestamp = contractToFund.lastTopUpTimestamp; // should we update this?
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@RyanRHall what do you think of this?

Copy link
Contributor

Choose a reason for hiding this comment

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

this is super confusing and unnecessarily complicated lol. From what I can tell, the lastTopUpTimestamp is always updated on line 269, and then on line 283 and 287 we set the time stamp back to what it was (meaning net no change) - is that how you interpret this code too?

I think we should take a second and think about what makes most sense from a product perspective. The min wait period is supposed to act as a rate limiter. And I think the question we're asking here is "should the rate limit apply to failed transfers?". So imagine a transfer fails for some reason, do we want the system to retry immediately or only retry after some wait period? I would think the latter, but we might want to double check with finance team.

emit TopUpFailed(targetAddress);
}
} else {
s_targets[targetAddress].lastTopUpTimestamp = contractToFund.lastTopUpTimestamp;
s_targets[targetAddress].lastTopUpTimestamp = contractToFund.lastTopUpTimestamp; // should we update this?
emit TopUpBlocked(targetAddress);
}
}
}

/// @notice checks the target (could be direct target or IAggregatorProxy), and determines
/// if it is elligible for funding
/// if it is eligible for funding
/// @param targetAddress the target to check
/// @param minBalance minimum balance required for the target
/// @param minWaitPeriodPassed the minimum wait period (target lastTopUpTimestamp + minWaitPeriod)
/// @return bool whether the target needs funding or not
/// @return address the address to fund. for DF, this is the aggregator address behind the proxy address.
/// for other products, it's the original target address
function _needsFunding(
address targetAddress,
uint256 minWaitPeriodPassed,
uint256 minBalance,
bool contractIsActive
) private view returns (bool) {
) private view returns (bool, address) {
// Explicitly check if the targetAddress is the zero address
// or if it's not a contract. In both cases return with false,
// to prevent target.linkAvailableForPayment from running,
// which would revert the operation.
if (targetAddress == address(0) || targetAddress.code.length == 0) {
return false;
return (false, address(0));
}
ILinkAvailable target;
IAggregatorProxy proxy = IAggregatorProxy(targetAddress);
try proxy.aggregator() returns (address aggregatorAddress) {
// proxy.aggregator() can return a 0 address if the address is not an aggregator
if (aggregatorAddress == address(0)) return false;
if (aggregatorAddress == address(0)) return (false, address(0));
target = ILinkAvailable(aggregatorAddress);
} catch {
target = ILinkAvailable(targetAddress);
}
try target.linkAvailableForPayment() returns (int256 balance) {
if (balance < int256(minBalance) && minWaitPeriodPassed <= block.timestamp && contractIsActive) {
return true;
return (true, address(target));
}
} catch {}
return false;
return (false, address(0));
}

/// @notice Gets list of subscription ids that are underfunded and returns a keeper-compatible payload.
Expand All @@ -334,9 +335,18 @@ contract LinkAvailableBalanceMonitor is AccessControl, AutomationCompatibleInter
bytes calldata
) external view override whenNotPaused returns (bool upkeepNeeded, bytes memory performData) {
address[] memory needsFunding = sampleUnderfundedAddresses();
upkeepNeeded = needsFunding.length > 0;
performData = abi.encode(needsFunding);
return (upkeepNeeded, performData);
if (needsFunding.length == 0) {
return (false, "");
}
uint96 total_batch_balance;
for (uint256 idx = 0; idx < needsFunding.length; idx++) {
address targetAddress = needsFunding[idx];
total_batch_balance = total_batch_balance + s_targets[targetAddress].topUpAmount;
}
if (i_linkToken.balanceOf(address(this)) >= total_batch_balance) {
return (true, abi.encode(needsFunding));
}
return (false, "");
}

/// @notice Called by the keeper to send funds to underfunded addresses.
Expand Down
Loading
Loading