diff --git a/target_chains/ethereum/contracts/contracts/pulse/Scheduler.sol b/target_chains/ethereum/contracts/contracts/pulse/Scheduler.sol index 4aa5e17f97..38484d0458 100644 --- a/target_chains/ethereum/contracts/contracts/pulse/Scheduler.sol +++ b/target_chains/ethereum/contracts/contracts/pulse/Scheduler.sol @@ -42,6 +42,11 @@ abstract contract Scheduler is IScheduler, SchedulerState { revert InsufficientBalance(); } + // Check deposit limit for permanent subscriptions + if (subscriptionParams.isPermanent && msg.value > MAX_DEPOSIT_LIMIT) { + revert MaxDepositLimitExceeded(); + } + // Set subscription to active subscriptionParams.isActive = true; @@ -519,11 +524,23 @@ abstract contract Scheduler is IScheduler, SchedulerState { /// BALANCE MANAGEMENT function addFunds(uint256 subscriptionId) external payable override { - if (!_state.subscriptionParams[subscriptionId].isActive) { + SubscriptionParams storage params = _state.subscriptionParams[ + subscriptionId + ]; + SubscriptionStatus storage status = _state.subscriptionStatuses[ + subscriptionId + ]; + + if (!params.isActive) { revert InactiveSubscription(); } - _state.subscriptionStatuses[subscriptionId].balanceInWei += msg.value; + // Check deposit limit for permanent subscriptions + if (params.isPermanent && msg.value > MAX_DEPOSIT_LIMIT) { + revert MaxDepositLimitExceeded(); + } + + status.balanceInWei += msg.value; } function withdrawFunds( diff --git a/target_chains/ethereum/contracts/contracts/pulse/SchedulerErrors.sol b/target_chains/ethereum/contracts/contracts/pulse/SchedulerErrors.sol index 899af3a840..0aaf0d0d6c 100644 --- a/target_chains/ethereum/contracts/contracts/pulse/SchedulerErrors.sol +++ b/target_chains/ethereum/contracts/contracts/pulse/SchedulerErrors.sol @@ -36,3 +36,4 @@ error DuplicateWhitelistAddress(address addr); // Payment errors error KeeperPaymentFailed(); +error MaxDepositLimitExceeded(); diff --git a/target_chains/ethereum/contracts/contracts/pulse/SchedulerState.sol b/target_chains/ethereum/contracts/contracts/pulse/SchedulerState.sol index e4c086348f..f4557cf297 100644 --- a/target_chains/ethereum/contracts/contracts/pulse/SchedulerState.sol +++ b/target_chains/ethereum/contracts/contracts/pulse/SchedulerState.sol @@ -9,6 +9,8 @@ contract SchedulerState { uint8 public constant MAX_PRICE_IDS_PER_SUBSCRIPTION = 255; /// Maximum number of addresses in the reader whitelist uint8 public constant MAX_READER_WHITELIST_SIZE = 255; + /// Maximum deposit limit for permanent subscriptions in wei + uint256 public constant MAX_DEPOSIT_LIMIT = 100 ether; /// Maximum time in the past (relative to current block timestamp) /// for which a price update timestamp is considered valid diff --git a/target_chains/ethereum/contracts/forge-test/PulseScheduler.t.sol b/target_chains/ethereum/contracts/forge-test/PulseScheduler.t.sol index ee06ff171f..10352ccda9 100644 --- a/target_chains/ethereum/contracts/forge-test/PulseScheduler.t.sol +++ b/target_chains/ethereum/contracts/forge-test/PulseScheduler.t.sol @@ -779,6 +779,112 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { scheduler.updateSubscription(subscriptionId, params); } + function testPermanentSubscriptionDepositLimit() public { + // Test 1: Creating a permanent subscription with deposit exceeding MAX_DEPOSIT_LIMIT should fail + SchedulerState.SubscriptionParams + memory params = createDefaultSubscriptionParams(2, address(reader)); + params.isPermanent = true; + + uint256 maxDepositLimit = 100 ether; // Same as MAX_DEPOSIT_LIMIT in SchedulerState + uint256 excessiveDeposit = maxDepositLimit + 1 ether; + vm.deal(address(this), excessiveDeposit); + + vm.expectRevert( + abi.encodeWithSelector(MaxDepositLimitExceeded.selector) + ); + scheduler.createSubscription{value: excessiveDeposit}(params); + + // Test 2: Creating a permanent subscription with deposit within MAX_DEPOSIT_LIMIT should succeed + uint256 validDeposit = maxDepositLimit; + vm.deal(address(this), validDeposit); + + uint256 subscriptionId = scheduler.createSubscription{ + value: validDeposit + }(params); + + // Verify subscription was created correctly + ( + SchedulerState.SubscriptionParams memory storedParams, + SchedulerState.SubscriptionStatus memory status + ) = scheduler.getSubscription(subscriptionId); + + assertTrue( + storedParams.isPermanent, + "Subscription should be permanent" + ); + assertEq( + status.balanceInWei, + validDeposit, + "Balance should match deposit amount" + ); + + // Test 3: Adding funds to a permanent subscription with deposit exceeding MAX_DEPOSIT_LIMIT should fail + uint256 largeAdditionalFunds = maxDepositLimit + 1; + vm.deal(address(this), largeAdditionalFunds); + + vm.expectRevert( + abi.encodeWithSelector(MaxDepositLimitExceeded.selector) + ); + scheduler.addFunds{value: largeAdditionalFunds}(subscriptionId); + + // Test 4: Adding funds to a permanent subscription within MAX_DEPOSIT_LIMIT should succeed + // Create a non-permanent subscription to test partial funding + SchedulerState.SubscriptionParams + memory nonPermanentParams = createDefaultSubscriptionParams( + 2, + address(reader) + ); + uint256 minimumBalance = scheduler.getMinimumBalance( + uint8(nonPermanentParams.priceIds.length) + ); + vm.deal(address(this), minimumBalance); + + uint256 nonPermanentSubId = scheduler.createSubscription{ + value: minimumBalance + }(nonPermanentParams); + + // Add funds to the non-permanent subscription (should be within limit) + uint256 validAdditionalFunds = 5 ether; + vm.deal(address(this), validAdditionalFunds); + + scheduler.addFunds{value: validAdditionalFunds}(nonPermanentSubId); + + // Verify funds were added correctly + ( + , + SchedulerState.SubscriptionStatus memory nonPermanentStatus + ) = scheduler.getSubscription(nonPermanentSubId); + + assertEq( + nonPermanentStatus.balanceInWei, + minimumBalance + validAdditionalFunds, + "Balance should be increased by the funded amount" + ); + + // Test 5: Non-permanent subscriptions should not be subject to the deposit limit + uint256 largeDeposit = maxDepositLimit * 2; + vm.deal(address(this), largeDeposit); + + SchedulerState.SubscriptionParams + memory unlimitedParams = createDefaultSubscriptionParams( + 2, + address(reader) + ); + uint256 unlimitedSubId = scheduler.createSubscription{ + value: largeDeposit + }(unlimitedParams); + + // Verify subscription was created with the large deposit + (, SchedulerState.SubscriptionStatus memory unlimitedStatus) = scheduler + .getSubscription(unlimitedSubId); + + assertEq( + unlimitedStatus.balanceInWei, + largeDeposit, + "Non-permanent subscription should accept large deposits" + ); + } + function testAnyoneCanAddFunds() public { // Create a subscription uint256 subscriptionId = addTestSubscription(