Skip to content

fix(pulse): ensure subscription balance is greater than minimum balance after adding funds #2680

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

Merged
Merged
Show file tree
Hide file tree
Changes from all 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
10 changes: 10 additions & 0 deletions target_chains/ethereum/contracts/contracts/pulse/Scheduler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,16 @@ abstract contract Scheduler is IScheduler, SchedulerState {
}

status.balanceInWei += msg.value;

// If subscription is active, ensure minimum balance is maintained
if (params.isActive) {
uint256 minimumBalance = this.getMinimumBalance(
uint8(params.priceIds.length)
);
if (status.balanceInWei < minimumBalance) {
revert InsufficientBalance();
}
}
}

function withdrawFunds(
Expand Down
114 changes: 114 additions & 0 deletions target_chains/ethereum/contracts/forge-test/PulseScheduler.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,120 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
);
}

function testAddFundsWithInactiveSubscriptionReverts() public {
// Create a subscription with minimum balance
uint256 subscriptionId = addTestSubscription(
scheduler,
address(reader)
);

// Get subscription parameters and calculate minimum balance
(SchedulerState.SubscriptionParams memory params, ) = scheduler
.getSubscription(subscriptionId);
uint256 minimumBalance = scheduler.getMinimumBalance(
uint8(params.priceIds.length)
);

// Deactivate the subscription
SchedulerState.SubscriptionParams memory testParams = params;
testParams.isActive = false;
scheduler.updateSubscription(subscriptionId, testParams);

// Withdraw funds to get below minimum
uint256 withdrawAmount = minimumBalance - 1 wei;
scheduler.withdrawFunds(subscriptionId, withdrawAmount);

// Verify balance is now below minimum
(
SchedulerState.SubscriptionParams memory testUpdatedParams,
SchedulerState.SubscriptionStatus memory testUpdatedStatus
) = scheduler.getSubscription(subscriptionId);
assertEq(
testUpdatedStatus.balanceInWei,
1 wei,
"Balance should be 1 wei after withdrawal"
);

// Try to add funds to inactive subscription (should fail with InactiveSubscription)
vm.expectRevert(abi.encodeWithSelector(InactiveSubscription.selector));
scheduler.addFunds{value: 1 wei}(subscriptionId);

// Try to reactivate with insufficient balance (should fail)
testUpdatedParams.isActive = true;
vm.expectRevert(abi.encodeWithSelector(InsufficientBalance.selector));
scheduler.updateSubscription(subscriptionId, testUpdatedParams);
}

function testAddFundsEnforcesMinimumBalance() public {
uint256 subscriptionId = addTestSubscriptionWithFeeds(
scheduler,
2,
address(reader)
);
(SchedulerState.SubscriptionParams memory params, ) = scheduler
.getSubscription(subscriptionId);
uint256 minimumBalance = scheduler.getMinimumBalance(
uint8(params.priceIds.length)
);

// Send multiple price updates to drain the balance below minimum
for (uint i = 0; i < 5; i++) {
// Advance time to satisfy heartbeat criteria
vm.warp(block.timestamp + 60);

// Create price feeds with current timestamp
uint64 publishTime = SafeCast.toUint64(block.timestamp);
PythStructs.PriceFeed[] memory priceFeeds;
uint64[] memory slots;
(priceFeeds, slots) = createMockPriceFeedsWithSlots(
publishTime,
params.priceIds.length
);

// Mock Pyth response
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
bytes[] memory updateData = createMockUpdateData(priceFeeds);

// Perform update
vm.prank(pusher);
scheduler.updatePriceFeeds(subscriptionId, updateData);
}

// Verify balance is now below minimum
(
,
SchedulerState.SubscriptionStatus memory statusAfterUpdates
) = scheduler.getSubscription(subscriptionId);
assertTrue(
statusAfterUpdates.balanceInWei < minimumBalance,
"Balance should be below minimum after updates"
);

// Try to add funds that would still leave balance below minimum
// Expect a revert with InsufficientBalance
uint256 insufficientFunds = minimumBalance -
statusAfterUpdates.balanceInWei -
1;
vm.expectRevert(abi.encodeWithSelector(InsufficientBalance.selector));
scheduler.addFunds{value: insufficientFunds}(subscriptionId);

// Add sufficient funds to get back above minimum
uint256 sufficientFunds = minimumBalance -
statusAfterUpdates.balanceInWei +
1;
scheduler.addFunds{value: sufficientFunds}(subscriptionId);

// Verify balance is now above minimum
(
,
SchedulerState.SubscriptionStatus memory statusAfterAddingFunds
) = scheduler.getSubscription(subscriptionId);
assertTrue(
statusAfterAddingFunds.balanceInWei >= minimumBalance,
"Balance should be at or above minimum after adding sufficient funds"
);
}

function testWithdrawFunds() public {
// Add a subscription and get the parameters
uint256 subscriptionId = addTestSubscription(
Expand Down
Loading