diff --git a/target_chains/ethereum/contracts/contracts/pulse/scheduler/Scheduler.sol b/target_chains/ethereum/contracts/contracts/pulse/scheduler/Scheduler.sol index c5a2c83aca..300bf0c2e5 100644 --- a/target_chains/ethereum/contracts/contracts/pulse/scheduler/Scheduler.sol +++ b/target_chains/ethereum/contracts/contracts/pulse/scheduler/Scheduler.sol @@ -671,8 +671,9 @@ abstract contract Scheduler is IScheduler, SchedulerState { function getMinimumBalance( uint8 numPriceFeeds ) external pure override returns (uint256 minimumBalanceInWei) { - // Simple implementation - minimum balance is 0.01 ETH per price feed - return numPriceFeeds * 0.01 ether; + // Placeholder implementation + // TODO: make this governable + return uint256(numPriceFeeds) * 0.01 ether; } // ACCESS CONTROL MODIFIERS diff --git a/target_chains/ethereum/contracts/forge-test/PulseScheduler.t.sol b/target_chains/ethereum/contracts/forge-test/PulseScheduler.t.sol index d51d429812..e30e8c6d73 100644 --- a/target_chains/ethereum/contracts/forge-test/PulseScheduler.t.sol +++ b/target_chains/ethereum/contracts/forge-test/PulseScheduler.t.sol @@ -6,7 +6,7 @@ import "forge-std/Test.sol"; import "forge-std/console.sol"; import "@pythnetwork/pyth-sdk-solidity/IPyth.sol"; import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import "./utils/PulseTestUtils.t.sol"; +import "./utils/PulseSchedulerTestUtils.t.sol"; import "../contracts/pulse/scheduler/SchedulerUpgradeable.sol"; import "../contracts/pulse/scheduler/IScheduler.sol"; import "../contracts/pulse/scheduler/SchedulerState.sol"; @@ -62,7 +62,7 @@ contract MockReader { } } -contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils { +contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { ERC1967Proxy public proxy; SchedulerUpgradeable public scheduler; MockReader public reader; @@ -71,9 +71,6 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils { address public pyth; address public pusher; - // Constants - uint96 constant PYTH_FEE = 1 wei; - function setUp() public { owner = address(1); admin = address(2); @@ -97,9 +94,8 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils { } function testCreateSubscription() public { - uint256 numFeeds = 2; // Corresponds to default createPriceIds() SchedulerState.SubscriptionParams - memory params = _createDefaultSubscriptionParams(numFeeds); + memory params = createDefaultSubscriptionParams(2, address(reader)); bytes32[] memory priceIds = params.priceIds; // Get the generated price IDs // Calculate minimum balance @@ -163,7 +159,10 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils { function testUpdateSubscription() public { // First add a subscription - uint256 subscriptionId = addTestSubscription(); + uint256 subscriptionId = addTestSubscription( + scheduler, + address(reader) + ); // Create updated parameters bytes32[] memory newPriceIds = createPriceIds(3); // Add one more price ID @@ -240,8 +239,12 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils { function testUpdateSubscriptionClearsRemovedPriceFeeds() public { // 1. Setup: Add subscription with 3 price feeds, update prices - uint256 numInitialFeeds = 3; - uint256 subscriptionId = addTestSubscriptionWithFeeds(numInitialFeeds); + uint8 numInitialFeeds = 3; + uint256 subscriptionId = addTestSubscriptionWithFeeds( + scheduler, + numInitialFeeds, + address(reader) + ); uint256 fundAmount = 1 ether; scheduler.addFunds{value: fundAmount}(subscriptionId); @@ -327,9 +330,12 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils { } function testcreateSubscriptionWithInsufficientFundsReverts() public { - uint256 numFeeds = 2; + uint8 numFeeds = 2; SchedulerState.SubscriptionParams - memory params = _createDefaultSubscriptionParams(numFeeds); + memory params = createDefaultSubscriptionParams( + numFeeds, + address(reader) + ); // Calculate minimum balance uint256 minimumBalance = scheduler.getMinimumBalance( @@ -342,7 +348,10 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils { } function testActivateDeactivateSubscription() public { - uint256 subscriptionId = addTestSubscription(); + uint256 subscriptionId = addTestSubscription( + scheduler, + address(reader) + ); // Get current params (SchedulerState.SubscriptionParams memory params, ) = scheduler @@ -388,7 +397,10 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils { function testAddFunds() public { // First add a subscription - uint256 subscriptionId = addTestSubscription(); + uint256 subscriptionId = addTestSubscription( + scheduler, + address(reader) + ); // Get initial balance (which includes minimum balance) (, SchedulerState.SubscriptionStatus memory initialStatus) = scheduler @@ -412,7 +424,10 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils { function testWithdrawFunds() public { // Add a subscription and get the parameters - uint256 subscriptionId = addTestSubscription(); + uint256 subscriptionId = addTestSubscription( + scheduler, + address(reader) + ); (SchedulerState.SubscriptionParams memory params, ) = scheduler .getSubscription(subscriptionId); uint256 minimumBalance = scheduler.getMinimumBalance( @@ -466,7 +481,10 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils { } function testPermanentSubscription() public { - uint256 subscriptionId = addTestSubscription(); + uint256 subscriptionId = addTestSubscription( + scheduler, + address(reader) + ); // Verify subscription was created as non-permanent initially (SchedulerState.SubscriptionParams memory params, ) = scheduler @@ -555,7 +573,10 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils { function testMakeExistingSubscriptionPermanent() public { // First create a non-permanent subscription - uint256 subscriptionId = addTestSubscription(); + uint256 subscriptionId = addTestSubscription( + scheduler, + address(reader) + ); // Verify it's not permanent (SchedulerState.SubscriptionParams memory params, ) = scheduler @@ -586,7 +607,10 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils { function testAnyoneCanAddFunds() public { // Create a subscription - uint256 subscriptionId = addTestSubscription(); + uint256 subscriptionId = addTestSubscription( + scheduler, + address(reader) + ); // Get initial balance (, SchedulerState.SubscriptionStatus memory initialStatus) = scheduler @@ -615,7 +639,10 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils { function testUpdatePriceFeedsWorks() public { // --- First Update --- // Add a subscription and funds - uint256 subscriptionId = addTestSubscription(); // Uses heartbeat 60s, deviation 100bps + uint256 subscriptionId = addTestSubscription( + scheduler, + address(reader) + ); // Uses heartbeat 60s, deviation 100bps uint256 fundAmount = 2 ether; // Add enough for two updates scheduler.addFunds{value: fundAmount}(subscriptionId); @@ -733,7 +760,9 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils { deviationThresholdBps: 0 }); uint256 subscriptionId = addTestSubscriptionWithUpdateCriteria( - criteria + scheduler, + criteria, + address(reader) ); uint256 fundAmount = 1 ether; scheduler.addFunds{value: fundAmount}(subscriptionId); @@ -778,7 +807,9 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils { deviationThresholdBps: deviationBps }); uint256 subscriptionId = addTestSubscriptionWithUpdateCriteria( - criteria + scheduler, + criteria, + address(reader) ); uint256 fundAmount = 1 ether; scheduler.addFunds{value: fundAmount}(subscriptionId); @@ -828,7 +859,10 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils { function testUpdatePriceFeedsRevertsOnOlderTimestamp() public { // Add a subscription and funds - uint256 subscriptionId = addTestSubscription(); + uint256 subscriptionId = addTestSubscription( + scheduler, + address(reader) + ); uint256 fundAmount = 1 ether; scheduler.addFunds{value: fundAmount}(subscriptionId); @@ -869,7 +903,10 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils { function testUpdatePriceFeedsRevertsOnMismatchedSlots() public { // First add a subscription and funds - uint256 subscriptionId = addTestSubscription(); + uint256 subscriptionId = addTestSubscription( + scheduler, + address(reader) + ); uint256 fundAmount = 1 ether; scheduler.addFunds{value: fundAmount}(subscriptionId); @@ -901,7 +938,10 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils { function testGetPricesUnsafeAllFeeds() public { // First add a subscription, funds, and update price feeds - uint256 subscriptionId = addTestSubscription(); + uint256 subscriptionId = addTestSubscription( + scheduler, + address(reader) + ); uint256 fundAmount = 1 ether; scheduler.addFunds{value: fundAmount}(subscriptionId); @@ -939,7 +979,11 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils { function testGetPricesUnsafeSelectiveFeeds() public { // First add a subscription with 3 price feeds, funds, and update price feeds - uint256 subscriptionId = addTestSubscriptionWithFeeds(3); + uint256 subscriptionId = addTestSubscriptionWithFeeds( + scheduler, + 3, + address(reader) + ); uint256 fundAmount = 1 ether; scheduler.addFunds{value: fundAmount}(subscriptionId); @@ -983,7 +1027,10 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils { } function testDisabledWhitelistAllowsUnrestrictedReads() public { - uint256 subscriptionId = addTestSubscription(); + uint256 subscriptionId = addTestSubscription( + scheduler, + address(reader) + ); // Get params and modify them (SchedulerState.SubscriptionParams memory params, ) = scheduler @@ -1024,7 +1071,10 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils { } function testEnabledWhitelistEnforcesOnlyAuthorizedReads() public { - uint256 subscriptionId = addTestSubscription(); + uint256 subscriptionId = addTestSubscription( + scheduler, + address(reader) + ); // Fund the subscription with enough to update it scheduler.addFunds{value: 1 ether}(subscriptionId); @@ -1088,7 +1138,10 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils { function testGetEmaPriceUnsafe() public { // First add a subscription, funds, and update price feeds - uint256 subscriptionId = addTestSubscription(); + uint256 subscriptionId = addTestSubscription( + scheduler, + address(reader) + ); uint256 fundAmount = 1 ether; scheduler.addFunds{value: fundAmount}(subscriptionId); @@ -1143,8 +1196,8 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils { function testGetActiveSubscriptions() public { // Add two subscriptions with the test contract as manager - addTestSubscription(); - addTestSubscription(); + addTestSubscription(scheduler, address(reader)); + addTestSubscription(scheduler, address(reader)); // Create a subscription with pusher as manager vm.startPrank(pusher); @@ -1256,110 +1309,29 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils { assertEq(emptyPageTotal, 3, "Total count should still be 3"); } - /// Helper function to add a test subscription with 2 price IDs - function addTestSubscription() internal returns (uint256) { - SchedulerState.SubscriptionParams - memory params = _createDefaultSubscriptionParams(2); - uint256 minimumBalance = scheduler.getMinimumBalance( - uint8(params.priceIds.length) - ); - return scheduler.createSubscription{value: minimumBalance}(params); - } - - /// Helper function to add a test subscription with variable number of feeds - function addTestSubscriptionWithFeeds( - uint256 numFeeds - ) internal returns (uint256) { - SchedulerState.SubscriptionParams - memory params = _createDefaultSubscriptionParams(numFeeds); - uint256 minimumBalance = scheduler.getMinimumBalance( - uint8(params.priceIds.length) - ); - return scheduler.createSubscription{value: minimumBalance}(params); - } - - /// Helper function to add a test subscription with specific update criteria - function addTestSubscriptionWithUpdateCriteria( - SchedulerState.UpdateCriteria memory updateCriteria - ) internal returns (uint256) { - bytes32[] memory priceIds = createPriceIds(); - address[] memory readerWhitelist = new address[](1); - readerWhitelist[0] = address(reader); - - SchedulerState.GasConfig memory gasConfig = SchedulerState.GasConfig({ - maxBaseFeeMultiplierCapPct: 10_000, - maxPriorityFeeMultiplierCapPct: 10_000 - }); - - SchedulerState.SubscriptionParams memory params = SchedulerState - .SubscriptionParams({ - priceIds: priceIds, - readerWhitelist: readerWhitelist, - whitelistEnabled: true, - isActive: true, - isPermanent: false, - updateCriteria: updateCriteria, // Use provided criteria - gasConfig: gasConfig - }); - - uint256 minimumBalance = scheduler.getMinimumBalance( - uint8(priceIds.length) - ); - return scheduler.createSubscription{value: minimumBalance}(params); - } - - // Helper function to create default subscription parameters - function _createDefaultSubscriptionParams( - uint256 numFeeds - ) internal view returns (SchedulerState.SubscriptionParams memory) { - bytes32[] memory priceIds = createPriceIds(numFeeds); - address[] memory readerWhitelist = new address[](1); - readerWhitelist[0] = address(reader); - - SchedulerState.UpdateCriteria memory updateCriteria = SchedulerState - .UpdateCriteria({ - updateOnHeartbeat: true, - heartbeatSeconds: 60, - updateOnDeviation: true, - deviationThresholdBps: 100 - }); - - SchedulerState.GasConfig memory gasConfig = SchedulerState.GasConfig({ - maxBaseFeeMultiplierCapPct: 10_000, - maxPriorityFeeMultiplierCapPct: 10_000 - }); - - return - SchedulerState.SubscriptionParams({ - priceIds: priceIds, - readerWhitelist: readerWhitelist, - whitelistEnabled: true, - isActive: true, - isPermanent: false, - updateCriteria: updateCriteria, - gasConfig: gasConfig - }); - } - function testSubscriptionParamValidations() public { uint256 initialSubId = 0; // For update tests // === Empty Price IDs === SchedulerState.SubscriptionParams - memory emptyPriceIdsParams = _createDefaultSubscriptionParams(1); + memory emptyPriceIdsParams = createDefaultSubscriptionParams( + 1, + address(reader) + ); emptyPriceIdsParams.priceIds = new bytes32[](0); vm.expectRevert(abi.encodeWithSelector(EmptyPriceIds.selector)); scheduler.createSubscription{value: 1 ether}(emptyPriceIdsParams); - initialSubId = addTestSubscription(); // Create a valid one for update test + initialSubId = addTestSubscription(scheduler, address(reader)); // Create a valid one for update test vm.expectRevert(abi.encodeWithSelector(EmptyPriceIds.selector)); scheduler.updateSubscription(initialSubId, emptyPriceIdsParams); // === Duplicate Price IDs === SchedulerState.SubscriptionParams - memory duplicatePriceIdsParams = _createDefaultSubscriptionParams( - 2 + memory duplicatePriceIdsParams = createDefaultSubscriptionParams( + 2, + address(reader) ); bytes32 duplicateId = duplicatePriceIdsParams.priceIds[0]; duplicatePriceIdsParams.priceIds[1] = duplicateId; @@ -1369,7 +1341,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils { ); scheduler.createSubscription{value: 1 ether}(duplicatePriceIdsParams); - initialSubId = addTestSubscription(); + initialSubId = addTestSubscription(scheduler, address(reader)); vm.expectRevert( abi.encodeWithSelector(DuplicatePriceId.selector, duplicateId) ); @@ -1377,7 +1349,10 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils { // === Too Many Whitelist Readers === SchedulerState.SubscriptionParams - memory largeWhitelistParams = _createDefaultSubscriptionParams(1); + memory largeWhitelistParams = createDefaultSubscriptionParams( + 1, + address(reader) + ); uint whitelistLength = uint(scheduler.MAX_READER_WHITELIST_SIZE()) + 1; address[] memory largeWhitelist = new address[](whitelistLength); for (uint i = 0; i < whitelistLength; i++) { @@ -1394,7 +1369,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils { ); scheduler.createSubscription{value: 1 ether}(largeWhitelistParams); - initialSubId = addTestSubscription(); + initialSubId = addTestSubscription(scheduler, address(reader)); vm.expectRevert( abi.encodeWithSelector( TooManyWhitelistedReaders.selector, @@ -1406,8 +1381,9 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils { // === Duplicate Whitelist Address === SchedulerState.SubscriptionParams - memory duplicateWhitelistParams = _createDefaultSubscriptionParams( - 1 + memory duplicateWhitelistParams = createDefaultSubscriptionParams( + 1, + address(reader) ); address[] memory duplicateWhitelist = new address[](2); duplicateWhitelist[0] = address(reader); @@ -1422,7 +1398,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils { ); scheduler.createSubscription{value: 1 ether}(duplicateWhitelistParams); - initialSubId = addTestSubscription(); + initialSubId = addTestSubscription(scheduler, address(reader)); vm.expectRevert( abi.encodeWithSelector( DuplicateWhitelistAddress.selector, @@ -1433,27 +1409,33 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils { // === Invalid Heartbeat (Zero Seconds) === SchedulerState.SubscriptionParams - memory invalidHeartbeatParams = _createDefaultSubscriptionParams(1); + memory invalidHeartbeatParams = createDefaultSubscriptionParams( + 1, + address(reader) + ); invalidHeartbeatParams.updateCriteria.updateOnHeartbeat = true; invalidHeartbeatParams.updateCriteria.heartbeatSeconds = 0; // Invalid vm.expectRevert(abi.encodeWithSelector(InvalidUpdateCriteria.selector)); scheduler.createSubscription{value: 1 ether}(invalidHeartbeatParams); - initialSubId = addTestSubscription(); + initialSubId = addTestSubscription(scheduler, address(reader)); vm.expectRevert(abi.encodeWithSelector(InvalidUpdateCriteria.selector)); scheduler.updateSubscription(initialSubId, invalidHeartbeatParams); // === Invalid Deviation (Zero Bps) === SchedulerState.SubscriptionParams - memory invalidDeviationParams = _createDefaultSubscriptionParams(1); + memory invalidDeviationParams = createDefaultSubscriptionParams( + 1, + address(reader) + ); invalidDeviationParams.updateCriteria.updateOnDeviation = true; invalidDeviationParams.updateCriteria.deviationThresholdBps = 0; // Invalid vm.expectRevert(abi.encodeWithSelector(InvalidUpdateCriteria.selector)); scheduler.createSubscription{value: 1 ether}(invalidDeviationParams); - initialSubId = addTestSubscription(); + initialSubId = addTestSubscription(scheduler, address(reader)); vm.expectRevert(abi.encodeWithSelector(InvalidUpdateCriteria.selector)); scheduler.updateSubscription(initialSubId, invalidDeviationParams); } diff --git a/target_chains/ethereum/contracts/forge-test/PulseSchedulerGasBenchmark.t.sol b/target_chains/ethereum/contracts/forge-test/PulseSchedulerGasBenchmark.t.sol new file mode 100644 index 0000000000..fd2536c02d --- /dev/null +++ b/target_chains/ethereum/contracts/forge-test/PulseSchedulerGasBenchmark.t.sol @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; +import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import "@pythnetwork/pyth-sdk-solidity/IPyth.sol"; +import "../contracts/pulse/scheduler/SchedulerUpgradeable.sol"; +import "../contracts/pulse/scheduler/IScheduler.sol"; +import "../contracts/pulse/scheduler/SchedulerState.sol"; +import "../contracts/pulse/scheduler/SchedulerEvents.sol"; +import "../contracts/pulse/scheduler/SchedulerErrors.sol"; +import "./utils/PulseSchedulerTestUtils.t.sol"; + +contract PulseSchedulerGasBenchmark is Test, PulseSchedulerTestUtils { + ERC1967Proxy public proxy; + SchedulerUpgradeable public scheduler; + address public manager; + address public admin; + address public pyth; + + function setUp() public { + manager = address(1); + admin = address(2); + pyth = address(3); + + SchedulerUpgradeable _scheduler = new SchedulerUpgradeable(); + proxy = new ERC1967Proxy(address(_scheduler), ""); + scheduler = SchedulerUpgradeable(address(proxy)); + + scheduler.initialize(manager, admin, pyth); + + // Start tests at a high timestamp to avoid underflow when we set + // `minPublishTime = timestamp - 1 hour` in updatePriceFeeds + vm.warp(100000); + + // Give manager 1000 ETH for testing + vm.deal(manager, 1000 ether); + } + + // Helper function to run the price feed update benchmark with a specified number of feeds + function _runUpdateAndQueryPriceFeedsBenchmark(uint8 numFeeds) internal { + // Setup: Create subscription and perform initial update + vm.prank(manager); + uint256 subscriptionId = _setupSubscriptionWithInitialUpdate(numFeeds); + (SchedulerState.SubscriptionParams memory params, ) = scheduler + .getSubscription(subscriptionId); + + // Advance time to meet heartbeat criteria + vm.warp(block.timestamp + 100); + + // Create new price feed updates with updated timestamp + uint64 newPublishTime = SafeCast.toUint64(block.timestamp); + PythStructs.PriceFeed[] memory newPriceFeeds; + uint64[] memory newSlots; + + (newPriceFeeds, newSlots) = createMockPriceFeedsWithSlots( + newPublishTime, + numFeeds + ); + + // Mock Pyth response for the benchmark + mockParsePriceFeedUpdatesWithSlots(pyth, newPriceFeeds, newSlots); + + // Actual benchmark: Measure gas for updating price feeds + uint256 startGas = gasleft(); + scheduler.updatePriceFeeds( + subscriptionId, + createMockUpdateData(newPriceFeeds), + params.priceIds + ); + uint256 updateGasUsed = startGas - gasleft(); + + console.log( + "Gas used for updating %s feeds: %s", + vm.toString(numFeeds), + vm.toString(updateGasUsed) + ); + + // Benchmark querying the price feeds after updating + uint256 queryStartGas = gasleft(); + scheduler.getPricesUnsafe(subscriptionId, params.priceIds); + uint256 queryGasUsed = queryStartGas - gasleft(); + + console.log( + "Gas used for querying %s feeds: %s", + vm.toString(numFeeds), + vm.toString(queryGasUsed) + ); + console.log( + "Total gas used for updating and querying %s feeds: %s", + vm.toString(numFeeds), + vm.toString(updateGasUsed + queryGasUsed) + ); + } + + // Helper function to set up a subscription with initial price update + function _setupSubscriptionWithInitialUpdate( + uint8 numFeeds + ) internal returns (uint256) { + uint256 subscriptionId = addTestSubscriptionWithFeeds( + scheduler, + numFeeds, + address(manager) + ); + + // Fetch the price IDs + (SchedulerState.SubscriptionParams memory params, ) = scheduler + .getSubscription(subscriptionId); + + // Create initial price feed updates + uint64 publishTime = SafeCast.toUint64(block.timestamp); + PythStructs.PriceFeed[] memory priceFeeds; + uint64[] memory slots; + + (priceFeeds, slots) = createMockPriceFeedsWithSlots( + publishTime, + numFeeds + ); + + mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds, slots); + bytes[] memory updateData = createMockUpdateData(priceFeeds); + + // Update the price feeds. We should have enough balance to cover the update + // because we funded the subscription with the minimum balance during creation. + scheduler.updatePriceFeeds(subscriptionId, updateData, params.priceIds); + return subscriptionId; + } + + // Helper function to create updated price feeds for benchmark + function _createUpdatedPriceFeeds( + uint8 numFeeds + ) internal returns (PythStructs.PriceFeed[] memory, uint64[] memory) {} + + /// Helper function for benchmarking querying active subscriptions with a specified number of total subscriptions. + /// Half of them will be inactive to simulate gaps in the subscriptions list. + /// Keepers will poll this function to get the list of active subscriptions. + function _runGetActiveSubscriptionsBenchmark( + uint256 numSubscriptions + ) internal { + // Setup: As manager, create subscriptions and then deactivate every other one. + vm.startPrank(manager); + + // Array to store subscription IDs + uint256[] memory subscriptionIds = new uint256[](numSubscriptions); + + // First create all subscriptions as active (with default 2 price feeds) + for (uint256 i = 0; i < numSubscriptions; i++) { + subscriptionIds[i] = addTestSubscription( + scheduler, + address(manager) + ); + } + + // Deactivate every other subscription + for (uint256 i = 0; i < numSubscriptions; i++) { + if (i % 2 == 1) { + (SchedulerState.SubscriptionParams memory params, ) = scheduler + .getSubscription(subscriptionIds[i]); + params.isActive = false; + scheduler.updateSubscription(subscriptionIds[i], params); + } + } + vm.stopPrank(); + + // Actual benchmark: Measure gas for fetching active subscriptions + uint256 startGas = gasleft(); + scheduler.getActiveSubscriptions(0, numSubscriptions); + uint256 gasUsed = startGas - gasleft(); + + console.log( + "Gas used for fetching %s active subscriptions out of %s total: %s", + vm.toString((numSubscriptions + 1) / 2), + vm.toString(numSubscriptions), + vm.toString(gasUsed) + ); + } + + // Benchmark tests for the basic flow: updating and reading price feeds with different feed counts + // NOTE: run these tests with -vv to see the gas usage for the operations under test, without setup costs + + function testUpdateAndQueryPriceFeeds01Feed() public { + _runUpdateAndQueryPriceFeedsBenchmark(1); + } + + function testUpdateAndQueryPriceFeeds02Feeds() public { + _runUpdateAndQueryPriceFeedsBenchmark(2); + } + + function testUpdateAndQueryPriceFeeds04Feeds() public { + _runUpdateAndQueryPriceFeedsBenchmark(4); + } + + function testUpdateAndQueryPriceFeeds08Feeds() public { + _runUpdateAndQueryPriceFeedsBenchmark(8); + } + + function testUpdateAndQueryPriceFeeds10Feeds() public { + _runUpdateAndQueryPriceFeedsBenchmark(10); + } + + function testUpdateAndQueryPriceFeeds20Feeds() public { + _runUpdateAndQueryPriceFeedsBenchmark(20); + } + + // Benchmark tests for fetching active subscriptions with different counts + // NOTE: run these tests with -vv to see the gas usage for the operations under test, without setup costs + + function testGetActiveSubscriptions010() public { + _runGetActiveSubscriptionsBenchmark(10); + } + + function testGetActiveSubscriptions100() public { + _runGetActiveSubscriptionsBenchmark(100); + } + + function testGetActiveSubscriptions1000() public { + _runGetActiveSubscriptionsBenchmark(1000); + } +} diff --git a/target_chains/ethereum/contracts/forge-test/utils/PulseSchedulerTestUtils.t.sol b/target_chains/ethereum/contracts/forge-test/utils/PulseSchedulerTestUtils.t.sol new file mode 100644 index 0000000000..b1806535d8 --- /dev/null +++ b/target_chains/ethereum/contracts/forge-test/utils/PulseSchedulerTestUtils.t.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import "@pythnetwork/pyth-sdk-solidity/IPyth.sol"; +import "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import "../../contracts/pulse/IPulse.sol"; +import "../../contracts/pulse/scheduler/SchedulerState.sol"; +import "./PulseTestUtils.t.sol"; +import "../../contracts/pulse/scheduler/SchedulerUpgradeable.sol"; + +abstract contract PulseSchedulerTestUtils is Test, PulseTestUtils { + /// Helper function to add a test subscription with 2 price IDs + function addTestSubscription( + SchedulerUpgradeable scheduler, + address whitelistedReader + ) internal returns (uint256) { + SchedulerState.SubscriptionParams + memory params = createDefaultSubscriptionParams( + 2, + whitelistedReader + ); + uint256 minimumBalance = scheduler.getMinimumBalance( + uint8(params.priceIds.length) + ); + return scheduler.createSubscription{value: minimumBalance}(params); + } + + /// Helper function to add a test subscription with variable number of feeds + function addTestSubscriptionWithFeeds( + SchedulerUpgradeable scheduler, + uint8 numFeeds, + address whitelistedReader + ) internal returns (uint256) { + SchedulerState.SubscriptionParams + memory params = createDefaultSubscriptionParams( + numFeeds, + whitelistedReader + ); + uint256 minimumBalance = scheduler.getMinimumBalance( + uint8(params.priceIds.length) + ); + return scheduler.createSubscription{value: minimumBalance}(params); + } + + /// Helper function to add a test subscription with specific update criteria + function addTestSubscriptionWithUpdateCriteria( + SchedulerUpgradeable scheduler, + SchedulerState.UpdateCriteria memory updateCriteria, + address whitelistedReader + ) internal returns (uint256) { + bytes32[] memory priceIds = createPriceIds(); + address[] memory readerWhitelist = new address[](1); + readerWhitelist[0] = whitelistedReader; + + SchedulerState.GasConfig memory gasConfig = SchedulerState.GasConfig({ + maxBaseFeeMultiplierCapPct: 10_000, + maxPriorityFeeMultiplierCapPct: 10_000 + }); + + SchedulerState.SubscriptionParams memory params = SchedulerState + .SubscriptionParams({ + priceIds: priceIds, + readerWhitelist: readerWhitelist, + whitelistEnabled: true, + isActive: true, + isPermanent: false, + updateCriteria: updateCriteria, + gasConfig: gasConfig + }); + + uint256 minimumBalance = scheduler.getMinimumBalance( + uint8(priceIds.length) + ); + return scheduler.createSubscription{value: minimumBalance}(params); + } + + // Helper function to create default subscription parameters + function createDefaultSubscriptionParams( + uint8 numFeeds, + address whitelistedReader + ) internal pure returns (SchedulerState.SubscriptionParams memory) { + bytes32[] memory priceIds = createPriceIds(numFeeds); + address[] memory readerWhitelist = new address[](1); + readerWhitelist[0] = whitelistedReader; + + SchedulerState.UpdateCriteria memory updateCriteria = SchedulerState + .UpdateCriteria({ + updateOnHeartbeat: true, + heartbeatSeconds: 60, + updateOnDeviation: true, + deviationThresholdBps: 100 + }); + + SchedulerState.GasConfig memory gasConfig = SchedulerState.GasConfig({ + maxBaseFeeMultiplierCapPct: 10_000, + maxPriorityFeeMultiplierCapPct: 10_000 + }); + + return + SchedulerState.SubscriptionParams({ + priceIds: priceIds, + readerWhitelist: readerWhitelist, + whitelistEnabled: true, + isActive: true, + isPermanent: false, + updateCriteria: updateCriteria, + gasConfig: gasConfig + }); + } +} diff --git a/target_chains/ethereum/contracts/forge-test/utils/PulseTestUtils.t.sol b/target_chains/ethereum/contracts/forge-test/utils/PulseTestUtils.t.sol index f5aa3f22c7..8c0fccf4c9 100644 --- a/target_chains/ethereum/contracts/forge-test/utils/PulseTestUtils.t.sol +++ b/target_chains/ethereum/contracts/forge-test/utils/PulseTestUtils.t.sol @@ -50,19 +50,30 @@ abstract contract PulseTestUtils is Test { function createPriceIds( uint256 numFeeds ) internal pure returns (bytes32[] memory) { - require(numFeeds <= 10, "Too many price feeds requested"); bytes32[] memory priceIds = new bytes32[](numFeeds); - if (numFeeds > 0) priceIds[0] = BTC_PRICE_FEED_ID; - if (numFeeds > 1) priceIds[1] = ETH_PRICE_FEED_ID; - if (numFeeds > 2) priceIds[2] = SOL_PRICE_FEED_ID; - if (numFeeds > 3) priceIds[3] = AVAX_PRICE_FEED_ID; - if (numFeeds > 4) priceIds[4] = MELANIA_PRICE_FEED_ID; - if (numFeeds > 5) priceIds[5] = PYTH_PRICE_FEED_ID; - if (numFeeds > 6) priceIds[6] = UNI_PRICE_FEED_ID; - if (numFeeds > 7) priceIds[7] = AAVE_PRICE_FEED_ID; - if (numFeeds > 8) priceIds[8] = DOGE_PRICE_FEED_ID; - if (numFeeds > 9) priceIds[9] = ADA_PRICE_FEED_ID; + // First assign our predefined price feed IDs + uint256 predefinedCount = 10; + uint256 assignCount = numFeeds < predefinedCount + ? numFeeds + : predefinedCount; + + if (assignCount > 0) priceIds[0] = BTC_PRICE_FEED_ID; + if (assignCount > 1) priceIds[1] = ETH_PRICE_FEED_ID; + if (assignCount > 2) priceIds[2] = SOL_PRICE_FEED_ID; + if (assignCount > 3) priceIds[3] = AVAX_PRICE_FEED_ID; + if (assignCount > 4) priceIds[4] = MELANIA_PRICE_FEED_ID; + if (assignCount > 5) priceIds[5] = PYTH_PRICE_FEED_ID; + if (assignCount > 6) priceIds[6] = UNI_PRICE_FEED_ID; + if (assignCount > 7) priceIds[7] = AAVE_PRICE_FEED_ID; + if (assignCount > 8) priceIds[8] = DOGE_PRICE_FEED_ID; + if (assignCount > 9) priceIds[9] = ADA_PRICE_FEED_ID; + + // For any additional feeds beyond our predefined ones, generate derived IDs + for (uint256 i = predefinedCount; i < numFeeds; i++) { + // Derive new price IDs by incrementing the last predefined price ID + priceIds[i] = bytes32(uint256(ADA_PRICE_FEED_ID) + (i - 9)); + } return priceIds; } @@ -109,7 +120,6 @@ abstract contract PulseTestUtils is Test { uint256 publishTime, uint256 numFeeds ) internal pure returns (PythStructs.PriceFeed[] memory) { - require(numFeeds <= 10, "Too many price feeds requested"); PythStructs.PriceFeed[] memory priceFeeds = new PythStructs.PriceFeed[]( numFeeds );