PulseScheduler.t.sol 93 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470
  1. // SPDX-License-Identifier: Apache 2
  2. pragma solidity ^0.8.0;
  3. import "forge-std/Test.sol";
  4. import "forge-std/console.sol";
  5. import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
  6. import "../contracts/pulse/SchedulerUpgradeable.sol";
  7. import "../contracts/pulse/IScheduler.sol";
  8. import "../contracts/pulse/SchedulerState.sol";
  9. import "../contracts/pulse/SchedulerEvents.sol";
  10. import "../contracts/pulse/SchedulerErrors.sol";
  11. import "./utils/PulseSchedulerTestUtils.t.sol";
  12. import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
  13. contract MockReader {
  14. address private _scheduler;
  15. constructor(address scheduler) {
  16. _scheduler = scheduler;
  17. }
  18. function getPricesUnsafe(
  19. uint256 subscriptionId,
  20. bytes32[] memory priceIds
  21. ) external view returns (PythStructs.Price[] memory) {
  22. return IScheduler(_scheduler).getPricesUnsafe(subscriptionId, priceIds);
  23. }
  24. function getEmaPriceUnsafe(
  25. uint256 subscriptionId,
  26. bytes32[] memory priceIds
  27. ) external view returns (PythStructs.Price[] memory) {
  28. return
  29. IScheduler(_scheduler).getEmaPriceUnsafe(subscriptionId, priceIds);
  30. }
  31. function verifyPriceFeeds(
  32. uint256 subscriptionId,
  33. bytes32[] memory priceIds,
  34. PythStructs.PriceFeed[] memory expectedFeeds
  35. ) external view returns (bool) {
  36. PythStructs.Price[] memory actualPrices = IScheduler(_scheduler)
  37. .getPricesUnsafe(subscriptionId, priceIds);
  38. if (actualPrices.length != expectedFeeds.length) {
  39. return false;
  40. }
  41. for (uint i = 0; i < actualPrices.length; i++) {
  42. if (
  43. actualPrices[i].price != expectedFeeds[i].price.price ||
  44. actualPrices[i].conf != expectedFeeds[i].price.conf ||
  45. actualPrices[i].publishTime !=
  46. expectedFeeds[i].price.publishTime
  47. ) {
  48. return false;
  49. }
  50. }
  51. return true;
  52. }
  53. }
  54. contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
  55. ERC1967Proxy public proxy;
  56. SchedulerUpgradeable public scheduler;
  57. MockReader public reader;
  58. address public owner;
  59. address public admin;
  60. address public pyth;
  61. address public pusher;
  62. function setUp() public {
  63. owner = address(1);
  64. admin = address(2);
  65. pyth = address(3);
  66. pusher = address(4);
  67. uint128 minBalancePerFeed = 10 ** 16; // 0.01 ether
  68. uint128 keeperFee = 10 ** 14; // 0.0001 ether
  69. SchedulerUpgradeable _scheduler = new SchedulerUpgradeable();
  70. proxy = new ERC1967Proxy(
  71. address(_scheduler),
  72. abi.encodeWithSelector(
  73. SchedulerUpgradeable.initialize.selector,
  74. owner,
  75. admin,
  76. pyth,
  77. minBalancePerFeed,
  78. keeperFee
  79. )
  80. );
  81. scheduler = SchedulerUpgradeable(address(proxy));
  82. reader = new MockReader(address(proxy));
  83. // Start tests at a high timestamp to avoid underflow when we set
  84. // `minPublishTime = timestamp - 1 hour` in updatePriceFeeds
  85. vm.warp(100000);
  86. // Give pusher 100 ETH for testing
  87. vm.deal(pusher, 100 ether);
  88. }
  89. function testCreateSubscription() public {
  90. SchedulerState.SubscriptionParams
  91. memory params = createDefaultSubscriptionParams(2, address(reader));
  92. bytes32[] memory priceIds = params.priceIds; // Get the generated price IDs
  93. // Calculate minimum balance
  94. uint256 minimumBalance = scheduler.getMinimumBalance(
  95. uint8(priceIds.length)
  96. );
  97. // Add subscription with minimum balance
  98. vm.expectEmit();
  99. emit SubscriptionCreated(1, address(this));
  100. uint256 subscriptionId = scheduler.createSubscription{
  101. value: minimumBalance
  102. }(params);
  103. assertEq(subscriptionId, 1, "Subscription ID should be 1");
  104. // Verify subscription was added correctly
  105. (
  106. SchedulerState.SubscriptionParams memory storedParams,
  107. SchedulerState.SubscriptionStatus memory status
  108. ) = scheduler.getSubscription(subscriptionId);
  109. assertEq(
  110. storedParams.priceIds.length,
  111. priceIds.length,
  112. "Price IDs length mismatch"
  113. );
  114. assertEq(
  115. storedParams.readerWhitelist.length,
  116. params.readerWhitelist.length,
  117. "Whitelist length mismatch"
  118. );
  119. assertEq(
  120. storedParams.whitelistEnabled,
  121. params.whitelistEnabled,
  122. "whitelistEnabled should match"
  123. );
  124. assertTrue(storedParams.isActive, "Subscription should be active");
  125. assertEq(
  126. storedParams.updateCriteria.heartbeatSeconds,
  127. params.updateCriteria.heartbeatSeconds,
  128. "Heartbeat seconds mismatch"
  129. );
  130. assertEq(
  131. storedParams.updateCriteria.deviationThresholdBps,
  132. params.updateCriteria.deviationThresholdBps,
  133. "Deviation threshold mismatch"
  134. );
  135. assertEq(
  136. status.balanceInWei,
  137. minimumBalance,
  138. "Initial balance should match minimum balance"
  139. );
  140. }
  141. function testUpdateSubscription() public {
  142. // First add a subscription
  143. uint256 subscriptionId = addTestSubscription(
  144. scheduler,
  145. address(reader)
  146. );
  147. // Create updated parameters
  148. bytes32[] memory newPriceIds = createPriceIds(3); // Add one more price ID
  149. address[] memory newReaderWhitelist = new address[](2);
  150. newReaderWhitelist[0] = address(reader);
  151. newReaderWhitelist[1] = address(0x123);
  152. SchedulerState.UpdateCriteria memory newUpdateCriteria = SchedulerState
  153. .UpdateCriteria({
  154. updateOnHeartbeat: true,
  155. heartbeatSeconds: 120, // Changed from 60
  156. updateOnDeviation: true,
  157. deviationThresholdBps: 200 // Changed from 100
  158. });
  159. SchedulerState.SubscriptionParams memory newParams = SchedulerState
  160. .SubscriptionParams({
  161. priceIds: newPriceIds,
  162. readerWhitelist: newReaderWhitelist,
  163. whitelistEnabled: false, // Changed from true
  164. isActive: true,
  165. isPermanent: false,
  166. updateCriteria: newUpdateCriteria
  167. });
  168. // Add the required funds to cover the new minimum balance
  169. scheduler.addFunds{
  170. value: scheduler.getMinimumBalance(uint8(newPriceIds.length))
  171. }(subscriptionId);
  172. // Update subscription
  173. vm.expectEmit();
  174. emit SubscriptionUpdated(subscriptionId);
  175. scheduler.updateSubscription(subscriptionId, newParams);
  176. // Verify subscription was updated correctly
  177. (SchedulerState.SubscriptionParams memory storedParams, ) = scheduler
  178. .getSubscription(subscriptionId);
  179. assertEq(
  180. storedParams.priceIds.length,
  181. newPriceIds.length,
  182. "Price IDs length mismatch"
  183. );
  184. assertEq(
  185. storedParams.readerWhitelist.length,
  186. newReaderWhitelist.length,
  187. "Whitelist length mismatch"
  188. );
  189. assertEq(
  190. storedParams.whitelistEnabled,
  191. false,
  192. "whitelistEnabled should be false"
  193. );
  194. assertEq(
  195. storedParams.updateCriteria.heartbeatSeconds,
  196. 120,
  197. "Heartbeat seconds mismatch"
  198. );
  199. assertEq(
  200. storedParams.updateCriteria.deviationThresholdBps,
  201. 200,
  202. "Deviation threshold mismatch"
  203. );
  204. }
  205. function testUpdateSubscriptionClearsRemovedPriceFeeds() public {
  206. // 1. Setup: Add subscription with 3 price feeds, update prices
  207. uint8 numInitialFeeds = 3;
  208. uint256 subscriptionId = addTestSubscriptionWithFeeds(
  209. scheduler,
  210. numInitialFeeds,
  211. address(reader)
  212. );
  213. uint256 fundAmount = 1 ether;
  214. scheduler.addFunds{value: fundAmount}(subscriptionId);
  215. bytes32[] memory initialPriceIds = createPriceIds(numInitialFeeds);
  216. uint64 publishTime = SafeCast.toUint64(block.timestamp);
  217. PythStructs.PriceFeed[] memory initialPriceFeeds;
  218. uint64[] memory slots;
  219. (initialPriceFeeds, slots) = createMockPriceFeedsWithSlots(
  220. publishTime,
  221. numInitialFeeds
  222. );
  223. mockParsePriceFeedUpdatesWithSlotsStrict(
  224. pyth,
  225. initialPriceFeeds,
  226. slots
  227. );
  228. bytes[] memory updateData = createMockUpdateData(initialPriceFeeds);
  229. vm.prank(pusher);
  230. scheduler.updatePriceFeeds(subscriptionId, updateData);
  231. // Verify initial state: All 3 feeds should be readable
  232. assertTrue(
  233. reader.verifyPriceFeeds(
  234. subscriptionId,
  235. initialPriceIds,
  236. initialPriceFeeds
  237. ),
  238. "Initial price feeds verification failed"
  239. );
  240. // 2. Action: Update subscription to remove the last price feed
  241. bytes32[] memory newPriceIds = new bytes32[](numInitialFeeds - 1);
  242. for (uint i = 0; i < newPriceIds.length; i++) {
  243. newPriceIds[i] = initialPriceIds[i];
  244. }
  245. bytes32 removedPriceId = initialPriceIds[numInitialFeeds - 1]; // The ID we removed
  246. (SchedulerState.SubscriptionParams memory currentParams, ) = scheduler
  247. .getSubscription(subscriptionId);
  248. SchedulerState.SubscriptionParams memory newParams = currentParams; // Copy existing params
  249. newParams.priceIds = newPriceIds; // Update price IDs
  250. vm.expectEmit(); // Expect SubscriptionUpdated
  251. emit SubscriptionUpdated(subscriptionId);
  252. scheduler.updateSubscription(subscriptionId, newParams);
  253. // 3. Verification:
  254. // - Querying the removed price ID should revert
  255. bytes32[] memory removedIdArray = new bytes32[](1);
  256. removedIdArray[0] = removedPriceId;
  257. vm.expectRevert(
  258. abi.encodeWithSelector(
  259. InvalidPriceId.selector,
  260. removedPriceId,
  261. bytes32(0)
  262. )
  263. );
  264. scheduler.getPricesUnsafe(subscriptionId, removedIdArray);
  265. // - Querying the remaining price IDs should still work
  266. PythStructs.PriceFeed[]
  267. memory expectedRemainingFeeds = new PythStructs.PriceFeed[](
  268. newPriceIds.length
  269. );
  270. for (uint i = 0; i < newPriceIds.length; i++) {
  271. expectedRemainingFeeds[i] = initialPriceFeeds[i]; // Prices remain from the initial update
  272. }
  273. assertTrue(
  274. reader.verifyPriceFeeds(
  275. subscriptionId,
  276. newPriceIds,
  277. expectedRemainingFeeds
  278. ),
  279. "Remaining price feeds verification failed after update"
  280. );
  281. // - Querying all feeds (empty array) should return only the remaining feeds
  282. PythStructs.Price[] memory allPricesAfterUpdate = scheduler
  283. .getPricesUnsafe(subscriptionId, new bytes32[](0));
  284. assertEq(
  285. allPricesAfterUpdate.length,
  286. newPriceIds.length,
  287. "Querying all should only return remaining feeds"
  288. );
  289. }
  290. // Helper function to reduce stack depth in testUpdateSubscriptionResetsPriceLastUpdatedAt
  291. function _setupSubscriptionAndFirstUpdate()
  292. private
  293. returns (uint256 subscriptionId, uint64 publishTime)
  294. {
  295. // Setup subscription with heartbeat criteria
  296. uint32 heartbeatSeconds = 60; // 60 second heartbeat
  297. SchedulerState.UpdateCriteria memory criteria = SchedulerState
  298. .UpdateCriteria({
  299. updateOnHeartbeat: true,
  300. heartbeatSeconds: heartbeatSeconds,
  301. updateOnDeviation: false,
  302. deviationThresholdBps: 0
  303. });
  304. subscriptionId = addTestSubscriptionWithUpdateCriteria(
  305. scheduler,
  306. criteria,
  307. address(reader)
  308. );
  309. scheduler.addFunds{value: 1 ether}(subscriptionId);
  310. // Update prices to set priceLastUpdatedAt to a non-zero value
  311. publishTime = SafeCast.toUint64(block.timestamp);
  312. PythStructs.PriceFeed[] memory priceFeeds;
  313. uint64[] memory slots;
  314. (priceFeeds, slots) = createMockPriceFeedsWithSlots(publishTime, 2);
  315. mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
  316. bytes[] memory updateData = createMockUpdateData(priceFeeds);
  317. vm.prank(pusher);
  318. scheduler.updatePriceFeeds(subscriptionId, updateData);
  319. return (subscriptionId, publishTime);
  320. }
  321. function testUpdateSubscriptionResetsPriceLastUpdatedAt() public {
  322. // 1. Setup subscription and perform first update
  323. (
  324. uint256 subscriptionId,
  325. uint64 publishTime1
  326. ) = _setupSubscriptionAndFirstUpdate();
  327. // Verify priceLastUpdatedAt is set
  328. (, SchedulerState.SubscriptionStatus memory status) = scheduler
  329. .getSubscription(subscriptionId);
  330. assertEq(
  331. status.priceLastUpdatedAt,
  332. publishTime1,
  333. "priceLastUpdatedAt should be set to the first update timestamp"
  334. );
  335. // 2. Update subscription to add price IDs
  336. (SchedulerState.SubscriptionParams memory currentParams, ) = scheduler
  337. .getSubscription(subscriptionId);
  338. bytes32[] memory newPriceIds = createPriceIds(3);
  339. SchedulerState.SubscriptionParams memory newParams = currentParams;
  340. newParams.priceIds = newPriceIds;
  341. // Update the subscription
  342. scheduler.updateSubscription(subscriptionId, newParams);
  343. // 3. Verify priceLastUpdatedAt is reset to 0
  344. (, status) = scheduler.getSubscription(subscriptionId);
  345. assertEq(
  346. status.priceLastUpdatedAt,
  347. 0,
  348. "priceLastUpdatedAt should be reset to 0 after adding new price IDs"
  349. );
  350. // 4. Verify immediate update is possible
  351. _verifyImmediateUpdatePossible(subscriptionId);
  352. }
  353. function _verifyImmediateUpdatePossible(uint256 subscriptionId) private {
  354. // Create new price feeds for the new price IDs
  355. uint64 publishTime2 = SafeCast.toUint64(block.timestamp + 1); // Just 1 second later
  356. PythStructs.PriceFeed[] memory priceFeeds;
  357. uint64[] memory slots;
  358. (priceFeeds, slots) = createMockPriceFeedsWithSlots(publishTime2, 3); // 3 feeds for new price IDs
  359. mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
  360. bytes[] memory updateData = createMockUpdateData(priceFeeds);
  361. // This should succeed even though we haven't waited for heartbeatSeconds
  362. // because priceLastUpdatedAt was reset to 0
  363. vm.prank(pusher);
  364. scheduler.updatePriceFeeds(subscriptionId, updateData);
  365. // Verify the update was processed
  366. (, SchedulerState.SubscriptionStatus memory status) = scheduler
  367. .getSubscription(subscriptionId);
  368. assertEq(
  369. status.priceLastUpdatedAt,
  370. publishTime2,
  371. "Second update should be processed with new timestamp"
  372. );
  373. // Verify that normal heartbeat criteria apply again for subsequent updates
  374. uint64 publishTime3 = SafeCast.toUint64(block.timestamp + 10); // Only 10 seconds later
  375. (priceFeeds, slots) = createMockPriceFeedsWithSlots(publishTime3, 3);
  376. mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
  377. updateData = createMockUpdateData(priceFeeds);
  378. // This should fail because we haven't waited for heartbeatSeconds since the last update
  379. vm.expectRevert(
  380. abi.encodeWithSelector(UpdateConditionsNotMet.selector)
  381. );
  382. vm.prank(pusher);
  383. scheduler.updatePriceFeeds(subscriptionId, updateData);
  384. }
  385. function testcreateSubscriptionWithInsufficientFundsReverts() public {
  386. uint8 numFeeds = 2;
  387. SchedulerState.SubscriptionParams
  388. memory params = createDefaultSubscriptionParams(
  389. numFeeds,
  390. address(reader)
  391. );
  392. // Calculate minimum balance
  393. uint256 minimumBalance = scheduler.getMinimumBalance(
  394. uint8(params.priceIds.length)
  395. );
  396. // Try to add subscription with insufficient funds
  397. vm.expectRevert(abi.encodeWithSelector(InsufficientBalance.selector));
  398. scheduler.createSubscription{value: minimumBalance - 1 wei}(params);
  399. }
  400. function testActivateDeactivateSubscription() public {
  401. // Add multiple subscriptions
  402. uint256 subId1 = addTestSubscription(scheduler, address(reader)); // ID 1
  403. uint256 subId2 = addTestSubscription(scheduler, address(reader)); // ID 2
  404. uint256 subId3 = addTestSubscription(scheduler, address(reader)); // ID 3
  405. // --- Verify initial state ---
  406. (uint256[] memory activeIds, , uint256 totalCount) = scheduler
  407. .getActiveSubscriptions(0, 10);
  408. assertEq(totalCount, 3, "Initial: Total count should be 3");
  409. assertEq(activeIds.length, 3, "Initial: Active IDs length should be 3");
  410. assertEq(activeIds[0], subId1, "Initial: ID 1 should be active");
  411. assertEq(activeIds[1], subId2, "Initial: ID 2 should be active");
  412. assertEq(activeIds[2], subId3, "Initial: ID 3 should be active");
  413. // --- Deactivate the middle subscription (ID 2) ---
  414. (SchedulerState.SubscriptionParams memory params2, ) = scheduler
  415. .getSubscription(subId2);
  416. params2.isActive = false;
  417. vm.expectEmit();
  418. emit SubscriptionDeactivated(subId2);
  419. vm.expectEmit();
  420. emit SubscriptionUpdated(subId2);
  421. scheduler.updateSubscription(subId2, params2);
  422. // Verify state after deactivating ID 2
  423. (activeIds, , totalCount) = scheduler.getActiveSubscriptions(0, 10);
  424. assertEq(totalCount, 2, "After Deact 2: Total count should be 2");
  425. assertEq(
  426. activeIds.length,
  427. 2,
  428. "After Deact 2: Active IDs length should be 2"
  429. );
  430. assertEq(activeIds[0], subId1, "After Deact 2: ID 1 should be active");
  431. assertEq(
  432. activeIds[1],
  433. subId3,
  434. "After Deact 2: ID 3 should be active (moved)"
  435. ); // ID 3 takes the place of ID 2
  436. // --- Deactivate the last subscription (ID 3, now at index 1) ---
  437. (SchedulerState.SubscriptionParams memory params3, ) = scheduler
  438. .getSubscription(subId3);
  439. params3.isActive = false;
  440. vm.expectEmit();
  441. emit SubscriptionDeactivated(subId3);
  442. vm.expectEmit();
  443. emit SubscriptionUpdated(subId3);
  444. scheduler.updateSubscription(subId3, params3);
  445. // Verify state after deactivating ID 3
  446. (activeIds, , totalCount) = scheduler.getActiveSubscriptions(0, 10);
  447. assertEq(totalCount, 1, "After Deact 3: Total count should be 1");
  448. assertEq(
  449. activeIds.length,
  450. 1,
  451. "After Deact 3: Active IDs length should be 1"
  452. );
  453. assertEq(
  454. activeIds[0],
  455. subId1,
  456. "After Deact 3: Only ID 1 should be active"
  457. );
  458. // --- Reactivate the middle subscription (ID 2) ---
  459. params2.isActive = true; // Use the params struct from earlier
  460. vm.expectEmit();
  461. emit SubscriptionActivated(subId2);
  462. vm.expectEmit();
  463. emit SubscriptionUpdated(subId2);
  464. scheduler.updateSubscription(subId2, params2);
  465. // Verify state after reactivating ID 2
  466. (activeIds, , totalCount) = scheduler.getActiveSubscriptions(0, 10);
  467. assertEq(totalCount, 2, "After React 2: Total count should be 2");
  468. assertEq(
  469. activeIds.length,
  470. 2,
  471. "After React 2: Active IDs length should be 2"
  472. );
  473. assertEq(activeIds[0], subId1, "After React 2: ID 1 should be active");
  474. assertEq(activeIds[1], subId2, "After React 2: ID 2 should be active"); // ID 2 is added back to the end
  475. // --- Reactivate the last subscription (ID 3) ---
  476. params3.isActive = true; // Use the params struct from earlier
  477. vm.expectEmit();
  478. emit SubscriptionActivated(subId3);
  479. vm.expectEmit();
  480. emit SubscriptionUpdated(subId3);
  481. scheduler.updateSubscription(subId3, params3);
  482. // Verify final state (all active)
  483. (activeIds, , totalCount) = scheduler.getActiveSubscriptions(0, 10);
  484. assertEq(totalCount, 3, "Final: Total count should be 3");
  485. assertEq(activeIds.length, 3, "Final: Active IDs length should be 3");
  486. assertEq(activeIds[0], subId1, "Final: ID 1 should be active");
  487. assertEq(activeIds[1], subId2, "Final: ID 2 should be active");
  488. assertEq(activeIds[2], subId3, "Final: ID 3 should be active"); // ID 3 is added back to the end
  489. // --- Deactivate all remaining subscriptions ---
  490. // Deactivate ID 1 (first element)
  491. (SchedulerState.SubscriptionParams memory params1, ) = scheduler
  492. .getSubscription(subId1);
  493. params1.isActive = false;
  494. vm.expectEmit();
  495. emit SubscriptionDeactivated(subId1);
  496. vm.expectEmit();
  497. emit SubscriptionUpdated(subId1);
  498. scheduler.updateSubscription(subId1, params1);
  499. // Verify state after deactivating ID 1
  500. (activeIds, , totalCount) = scheduler.getActiveSubscriptions(0, 10);
  501. assertEq(totalCount, 2, "After Deact 1: Total count should be 2");
  502. assertEq(
  503. activeIds.length,
  504. 2,
  505. "After Deact 1: Active IDs length should be 2"
  506. );
  507. assertEq(
  508. activeIds[0],
  509. subId3,
  510. "After Deact 1: ID 3 should be at index 0"
  511. ); // ID 3 moved to front
  512. assertEq(
  513. activeIds[1],
  514. subId2,
  515. "After Deact 1: ID 2 should be at index 1"
  516. );
  517. // Deactivate ID 2 (now last element)
  518. params2.isActive = false; // Use existing params struct
  519. vm.expectEmit();
  520. emit SubscriptionDeactivated(subId2);
  521. vm.expectEmit();
  522. emit SubscriptionUpdated(subId2);
  523. scheduler.updateSubscription(subId2, params2);
  524. // Verify state after deactivating ID 2
  525. (activeIds, , totalCount) = scheduler.getActiveSubscriptions(0, 10);
  526. assertEq(
  527. totalCount,
  528. 1,
  529. "After Deact 2 (again): Total count should be 1"
  530. );
  531. assertEq(
  532. activeIds.length,
  533. 1,
  534. "After Deact 2 (again): Active IDs length should be 1"
  535. );
  536. assertEq(
  537. activeIds[0],
  538. subId3,
  539. "After Deact 2 (again): Only ID 3 should be active"
  540. );
  541. // Deactivate ID 3 (last remaining element)
  542. params3.isActive = false; // Use existing params struct
  543. vm.expectEmit();
  544. emit SubscriptionDeactivated(subId3);
  545. vm.expectEmit();
  546. emit SubscriptionUpdated(subId3);
  547. scheduler.updateSubscription(subId3, params3);
  548. // Verify final empty state
  549. (activeIds, , totalCount) = scheduler.getActiveSubscriptions(0, 10);
  550. assertEq(totalCount, 0, "Empty: Total count should be 0");
  551. assertEq(activeIds.length, 0, "Empty: Active IDs length should be 0");
  552. }
  553. function testAddFunds() public {
  554. // First add a subscription
  555. uint256 subscriptionId = addTestSubscription(
  556. scheduler,
  557. address(reader)
  558. );
  559. // Get initial balance (which includes minimum balance)
  560. (, SchedulerState.SubscriptionStatus memory initialStatus) = scheduler
  561. .getSubscription(subscriptionId);
  562. uint256 initialBalance = initialStatus.balanceInWei;
  563. // Add funds
  564. uint256 fundAmount = 1 ether;
  565. scheduler.addFunds{value: fundAmount}(subscriptionId);
  566. // Verify funds were added
  567. (, SchedulerState.SubscriptionStatus memory status) = scheduler
  568. .getSubscription(subscriptionId);
  569. assertEq(
  570. status.balanceInWei,
  571. initialBalance + fundAmount,
  572. "Balance should match initial balance plus added funds"
  573. );
  574. }
  575. function testAddFundsWithInactiveSubscriptionReverts() public {
  576. // Create a subscription with minimum balance
  577. uint256 subscriptionId = addTestSubscription(
  578. scheduler,
  579. address(reader)
  580. );
  581. // Get subscription parameters and calculate minimum balance
  582. (SchedulerState.SubscriptionParams memory params, ) = scheduler
  583. .getSubscription(subscriptionId);
  584. uint256 minimumBalance = scheduler.getMinimumBalance(
  585. uint8(params.priceIds.length)
  586. );
  587. // Deactivate the subscription
  588. SchedulerState.SubscriptionParams memory testParams = params;
  589. testParams.isActive = false;
  590. scheduler.updateSubscription(subscriptionId, testParams);
  591. // Withdraw funds to get below minimum
  592. uint256 withdrawAmount = minimumBalance - 1 wei;
  593. scheduler.withdrawFunds(subscriptionId, withdrawAmount);
  594. // Verify balance is now below minimum
  595. (
  596. SchedulerState.SubscriptionParams memory testUpdatedParams,
  597. SchedulerState.SubscriptionStatus memory testUpdatedStatus
  598. ) = scheduler.getSubscription(subscriptionId);
  599. assertEq(
  600. testUpdatedStatus.balanceInWei,
  601. 1 wei,
  602. "Balance should be 1 wei after withdrawal"
  603. );
  604. // Try to add funds to inactive subscription (should fail with InactiveSubscription)
  605. vm.expectRevert(abi.encodeWithSelector(InactiveSubscription.selector));
  606. scheduler.addFunds{value: 1 wei}(subscriptionId);
  607. // Try to reactivate with insufficient balance (should fail)
  608. testUpdatedParams.isActive = true;
  609. vm.expectRevert(abi.encodeWithSelector(InsufficientBalance.selector));
  610. scheduler.updateSubscription(subscriptionId, testUpdatedParams);
  611. }
  612. function testAddFundsEnforcesMinimumBalance() public {
  613. uint256 subscriptionId = addTestSubscriptionWithFeeds(
  614. scheduler,
  615. 2,
  616. address(reader)
  617. );
  618. (SchedulerState.SubscriptionParams memory params, ) = scheduler
  619. .getSubscription(subscriptionId);
  620. uint256 minimumBalance = scheduler.getMinimumBalance(
  621. uint8(params.priceIds.length)
  622. );
  623. // Send multiple price updates to drain the balance below minimum
  624. for (uint i = 0; i < 5; i++) {
  625. // Advance time to satisfy heartbeat criteria
  626. vm.warp(block.timestamp + 60);
  627. // Create price feeds with current timestamp
  628. uint64 publishTime = SafeCast.toUint64(block.timestamp);
  629. PythStructs.PriceFeed[] memory priceFeeds;
  630. uint64[] memory slots;
  631. (priceFeeds, slots) = createMockPriceFeedsWithSlots(
  632. publishTime,
  633. params.priceIds.length
  634. );
  635. // Mock Pyth response
  636. mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
  637. bytes[] memory updateData = createMockUpdateData(priceFeeds);
  638. // Perform update
  639. vm.prank(pusher);
  640. scheduler.updatePriceFeeds(subscriptionId, updateData);
  641. }
  642. // Verify balance is now below minimum
  643. (
  644. ,
  645. SchedulerState.SubscriptionStatus memory statusAfterUpdates
  646. ) = scheduler.getSubscription(subscriptionId);
  647. assertTrue(
  648. statusAfterUpdates.balanceInWei < minimumBalance,
  649. "Balance should be below minimum after updates"
  650. );
  651. // Try to add funds that would still leave balance below minimum
  652. // Expect a revert with InsufficientBalance
  653. uint256 insufficientFunds = minimumBalance -
  654. statusAfterUpdates.balanceInWei -
  655. 1;
  656. vm.expectRevert(abi.encodeWithSelector(InsufficientBalance.selector));
  657. scheduler.addFunds{value: insufficientFunds}(subscriptionId);
  658. // Add sufficient funds to get back above minimum
  659. uint256 sufficientFunds = minimumBalance -
  660. statusAfterUpdates.balanceInWei +
  661. 1;
  662. scheduler.addFunds{value: sufficientFunds}(subscriptionId);
  663. // Verify balance is now above minimum
  664. (
  665. ,
  666. SchedulerState.SubscriptionStatus memory statusAfterAddingFunds
  667. ) = scheduler.getSubscription(subscriptionId);
  668. assertTrue(
  669. statusAfterAddingFunds.balanceInWei >= minimumBalance,
  670. "Balance should be at or above minimum after adding sufficient funds"
  671. );
  672. }
  673. function testWithdrawFunds() public {
  674. // Add a subscription and get the parameters
  675. uint256 subscriptionId = addTestSubscription(
  676. scheduler,
  677. address(reader)
  678. );
  679. (SchedulerState.SubscriptionParams memory params, ) = scheduler
  680. .getSubscription(subscriptionId);
  681. uint256 minimumBalance = scheduler.getMinimumBalance(
  682. uint8(params.priceIds.length)
  683. );
  684. // Add extra funds
  685. uint256 extraFunds = 1 ether;
  686. scheduler.addFunds{value: extraFunds}(subscriptionId);
  687. // Get initial balance
  688. uint256 initialBalance = address(this).balance;
  689. // Withdraw extra funds
  690. scheduler.withdrawFunds(subscriptionId, extraFunds);
  691. // Verify funds were withdrawn
  692. (, SchedulerState.SubscriptionStatus memory status) = scheduler
  693. .getSubscription(subscriptionId);
  694. assertEq(
  695. status.balanceInWei,
  696. minimumBalance,
  697. "Remaining balance should be minimum balance"
  698. );
  699. assertEq(
  700. address(this).balance,
  701. initialBalance + extraFunds,
  702. "Withdrawn amount not received"
  703. );
  704. // Try to withdraw below minimum balance
  705. vm.expectRevert(abi.encodeWithSelector(InsufficientBalance.selector));
  706. scheduler.withdrawFunds(subscriptionId, 1 wei);
  707. // Deactivate subscription
  708. params.isActive = false;
  709. scheduler.updateSubscription(subscriptionId, params);
  710. // Now we should be able to withdraw all funds
  711. scheduler.withdrawFunds(subscriptionId, minimumBalance);
  712. // Verify all funds were withdrawn
  713. (, status) = scheduler.getSubscription(subscriptionId);
  714. assertEq(
  715. status.balanceInWei,
  716. 0,
  717. "Balance should be 0 after withdrawing all funds"
  718. );
  719. }
  720. function testPermanentSubscription() public {
  721. uint256 subscriptionId = addTestSubscription(
  722. scheduler,
  723. address(reader)
  724. );
  725. // Verify subscription was created as non-permanent initially
  726. (SchedulerState.SubscriptionParams memory params, ) = scheduler
  727. .getSubscription(subscriptionId);
  728. assertFalse(params.isPermanent, "Should not be permanent initially");
  729. // Make it permanent
  730. params.isPermanent = true;
  731. scheduler.updateSubscription(subscriptionId, params);
  732. // Verify subscription is now permanent
  733. (SchedulerState.SubscriptionParams memory storedParams, ) = scheduler
  734. .getSubscription(subscriptionId);
  735. assertTrue(
  736. storedParams.isPermanent,
  737. "Subscription should be permanent"
  738. );
  739. // Test 1: Cannot disable isPermanent flag
  740. SchedulerState.SubscriptionParams memory updatedParams = storedParams;
  741. updatedParams.isPermanent = false;
  742. vm.expectRevert(
  743. abi.encodeWithSelector(CannotUpdatePermanentSubscription.selector)
  744. );
  745. scheduler.updateSubscription(subscriptionId, updatedParams);
  746. // Test 2: Cannot remove price feeds
  747. updatedParams = storedParams;
  748. bytes32[] memory reducedPriceIds = new bytes32[](
  749. params.priceIds.length - 1
  750. );
  751. for (uint i = 0; i < reducedPriceIds.length; i++) {
  752. reducedPriceIds[i] = params.priceIds[i];
  753. }
  754. updatedParams.priceIds = reducedPriceIds;
  755. vm.expectRevert(
  756. abi.encodeWithSelector(CannotUpdatePermanentSubscription.selector)
  757. );
  758. scheduler.updateSubscription(subscriptionId, updatedParams);
  759. // Test 3: Cannot withdraw funds
  760. uint256 extraFunds = 1 ether;
  761. vm.deal(address(0x123), extraFunds);
  762. // Anyone can add funds (not just manager)
  763. vm.prank(address(0x123));
  764. scheduler.addFunds{value: extraFunds}(subscriptionId);
  765. vm.expectRevert(
  766. abi.encodeWithSelector(CannotUpdatePermanentSubscription.selector)
  767. );
  768. scheduler.withdrawFunds(subscriptionId, 0.1 ether);
  769. // Test 4: Cannot add more price feeds
  770. updatedParams = storedParams;
  771. bytes32[] memory expandedPriceIds = new bytes32[](
  772. params.priceIds.length + 1
  773. );
  774. for (uint i = 0; i < params.priceIds.length; i++) {
  775. expandedPriceIds[i] = params.priceIds[i];
  776. }
  777. expandedPriceIds[params.priceIds.length] = bytes32(
  778. uint256(keccak256(abi.encodePacked("additional-price-id")))
  779. );
  780. updatedParams.priceIds = expandedPriceIds;
  781. vm.expectRevert(
  782. abi.encodeWithSelector(CannotUpdatePermanentSubscription.selector)
  783. );
  784. scheduler.updateSubscription(subscriptionId, updatedParams);
  785. // Verify price feeds were not added (length should remain the same)
  786. (storedParams, ) = scheduler.getSubscription(subscriptionId);
  787. assertEq(
  788. storedParams.priceIds.length,
  789. params.priceIds.length, // Verify length hasn't changed
  790. "Should not be able to add price feeds to permanent subscription"
  791. );
  792. // Test 6: Cannot change updateCriteria
  793. updatedParams = storedParams;
  794. updatedParams.updateCriteria.heartbeatSeconds =
  795. storedParams.updateCriteria.heartbeatSeconds +
  796. 60;
  797. vm.expectRevert(
  798. abi.encodeWithSelector(CannotUpdatePermanentSubscription.selector)
  799. );
  800. scheduler.updateSubscription(subscriptionId, updatedParams);
  801. // Test 7: Cannot change whitelistEnabled
  802. updatedParams = storedParams;
  803. updatedParams.whitelistEnabled = !storedParams.whitelistEnabled;
  804. vm.expectRevert(
  805. abi.encodeWithSelector(CannotUpdatePermanentSubscription.selector)
  806. );
  807. scheduler.updateSubscription(subscriptionId, updatedParams);
  808. // Test 8: Cannot change the set of readers in the whitelist (add one)
  809. updatedParams = storedParams;
  810. address[] memory expandedWhitelist = new address[](
  811. storedParams.readerWhitelist.length + 1
  812. );
  813. for (uint i = 0; i < storedParams.readerWhitelist.length; i++) {
  814. expandedWhitelist[i] = storedParams.readerWhitelist[i];
  815. }
  816. expandedWhitelist[storedParams.readerWhitelist.length] = address(0x456);
  817. updatedParams.readerWhitelist = expandedWhitelist;
  818. vm.expectRevert(
  819. abi.encodeWithSelector(CannotUpdatePermanentSubscription.selector)
  820. );
  821. scheduler.updateSubscription(subscriptionId, updatedParams);
  822. // Test 9: Cannot change the set of readers in the whitelist (remove one)
  823. // Requires at least one reader in the initial setup
  824. if (storedParams.readerWhitelist.length > 0) {
  825. updatedParams = storedParams;
  826. address[] memory reducedWhitelist = new address[](
  827. storedParams.readerWhitelist.length - 1
  828. );
  829. for (uint i = 0; i < reducedWhitelist.length; i++) {
  830. reducedWhitelist[i] = storedParams.readerWhitelist[i];
  831. }
  832. updatedParams.readerWhitelist = reducedWhitelist;
  833. vm.expectRevert(
  834. abi.encodeWithSelector(
  835. CannotUpdatePermanentSubscription.selector
  836. )
  837. );
  838. scheduler.updateSubscription(subscriptionId, updatedParams);
  839. }
  840. // Test 10: Cannot deactivate a permanent subscription
  841. updatedParams = storedParams;
  842. updatedParams.isActive = false;
  843. vm.expectRevert(
  844. abi.encodeWithSelector(CannotUpdatePermanentSubscription.selector)
  845. );
  846. scheduler.updateSubscription(subscriptionId, updatedParams);
  847. }
  848. function testMakeExistingSubscriptionPermanent() public {
  849. // First create a non-permanent subscription
  850. uint256 subscriptionId = addTestSubscription(
  851. scheduler,
  852. address(reader)
  853. );
  854. // Verify it's not permanent
  855. (SchedulerState.SubscriptionParams memory params, ) = scheduler
  856. .getSubscription(subscriptionId);
  857. assertFalse(
  858. params.isPermanent,
  859. "Subscription should not be permanent initially"
  860. );
  861. // Make it permanent
  862. params.isPermanent = true;
  863. scheduler.updateSubscription(subscriptionId, params);
  864. // Verify it's now permanent
  865. (params, ) = scheduler.getSubscription(subscriptionId);
  866. assertTrue(params.isPermanent, "Subscription should now be permanent");
  867. // Verify we can't make it non-permanent again
  868. params.isPermanent = false;
  869. vm.expectRevert(
  870. abi.encodeWithSelector(CannotUpdatePermanentSubscription.selector)
  871. );
  872. scheduler.updateSubscription(subscriptionId, params);
  873. }
  874. function testPermanentSubscriptionDepositLimit() public {
  875. // Test 1: Creating a permanent subscription with deposit exceeding MAX_DEPOSIT_LIMIT should fail
  876. SchedulerState.SubscriptionParams
  877. memory params = createDefaultSubscriptionParams(2, address(reader));
  878. params.isPermanent = true;
  879. uint256 maxDepositLimit = 100 ether; // Same as MAX_DEPOSIT_LIMIT in SchedulerState
  880. uint256 excessiveDeposit = maxDepositLimit + 1 ether;
  881. vm.deal(address(this), excessiveDeposit);
  882. vm.expectRevert(
  883. abi.encodeWithSelector(MaxDepositLimitExceeded.selector)
  884. );
  885. scheduler.createSubscription{value: excessiveDeposit}(params);
  886. // Test 2: Creating a permanent subscription with deposit within MAX_DEPOSIT_LIMIT should succeed
  887. uint256 validDeposit = maxDepositLimit;
  888. vm.deal(address(this), validDeposit);
  889. uint256 subscriptionId = scheduler.createSubscription{
  890. value: validDeposit
  891. }(params);
  892. // Verify subscription was created correctly
  893. (
  894. SchedulerState.SubscriptionParams memory storedParams,
  895. SchedulerState.SubscriptionStatus memory status
  896. ) = scheduler.getSubscription(subscriptionId);
  897. assertTrue(
  898. storedParams.isPermanent,
  899. "Subscription should be permanent"
  900. );
  901. assertEq(
  902. status.balanceInWei,
  903. validDeposit,
  904. "Balance should match deposit amount"
  905. );
  906. // Test 3: Adding funds to a permanent subscription with deposit exceeding MAX_DEPOSIT_LIMIT should fail
  907. uint256 largeAdditionalFunds = maxDepositLimit + 1;
  908. vm.deal(address(this), largeAdditionalFunds);
  909. vm.expectRevert(
  910. abi.encodeWithSelector(MaxDepositLimitExceeded.selector)
  911. );
  912. scheduler.addFunds{value: largeAdditionalFunds}(subscriptionId);
  913. // Test 4: Adding funds to a permanent subscription within MAX_DEPOSIT_LIMIT should succeed
  914. // Create a non-permanent subscription to test partial funding
  915. SchedulerState.SubscriptionParams
  916. memory nonPermanentParams = createDefaultSubscriptionParams(
  917. 2,
  918. address(reader)
  919. );
  920. uint256 minimumBalance = scheduler.getMinimumBalance(
  921. uint8(nonPermanentParams.priceIds.length)
  922. );
  923. vm.deal(address(this), minimumBalance);
  924. uint256 nonPermanentSubId = scheduler.createSubscription{
  925. value: minimumBalance
  926. }(nonPermanentParams);
  927. // Add funds to the non-permanent subscription (should be within limit)
  928. uint256 validAdditionalFunds = 5 ether;
  929. vm.deal(address(this), validAdditionalFunds);
  930. scheduler.addFunds{value: validAdditionalFunds}(nonPermanentSubId);
  931. // Verify funds were added correctly
  932. (
  933. ,
  934. SchedulerState.SubscriptionStatus memory nonPermanentStatus
  935. ) = scheduler.getSubscription(nonPermanentSubId);
  936. assertEq(
  937. nonPermanentStatus.balanceInWei,
  938. minimumBalance + validAdditionalFunds,
  939. "Balance should be increased by the funded amount"
  940. );
  941. // Test 5: Non-permanent subscriptions should not be subject to the deposit limit
  942. uint256 largeDeposit = maxDepositLimit * 2;
  943. vm.deal(address(this), largeDeposit);
  944. SchedulerState.SubscriptionParams
  945. memory unlimitedParams = createDefaultSubscriptionParams(
  946. 2,
  947. address(reader)
  948. );
  949. uint256 unlimitedSubId = scheduler.createSubscription{
  950. value: largeDeposit
  951. }(unlimitedParams);
  952. // Verify subscription was created with the large deposit
  953. (, SchedulerState.SubscriptionStatus memory unlimitedStatus) = scheduler
  954. .getSubscription(unlimitedSubId);
  955. assertEq(
  956. unlimitedStatus.balanceInWei,
  957. largeDeposit,
  958. "Non-permanent subscription should accept large deposits"
  959. );
  960. }
  961. function testAnyoneCanAddFunds() public {
  962. // Create a subscription
  963. uint256 subscriptionId = addTestSubscription(
  964. scheduler,
  965. address(reader)
  966. );
  967. // Get initial balance
  968. (, SchedulerState.SubscriptionStatus memory initialStatus) = scheduler
  969. .getSubscription(subscriptionId);
  970. uint256 initialBalance = initialStatus.balanceInWei;
  971. // Have a different address add funds
  972. address funder = address(0x123);
  973. uint256 fundAmount = 1 ether;
  974. vm.deal(funder, fundAmount);
  975. vm.prank(funder);
  976. scheduler.addFunds{value: fundAmount}(subscriptionId);
  977. // Verify funds were added
  978. (, SchedulerState.SubscriptionStatus memory status) = scheduler
  979. .getSubscription(subscriptionId);
  980. assertEq(
  981. status.balanceInWei,
  982. initialBalance + fundAmount,
  983. "Balance should be increased by the funded amount"
  984. );
  985. }
  986. function testUpdatePriceFeedsUpdatesPricesCorrectly() public {
  987. // --- First Update ---
  988. // Add a subscription and funds
  989. uint256 subscriptionId = addTestSubscription(
  990. scheduler,
  991. address(reader)
  992. ); // Uses heartbeat 60s, deviation 100bps
  993. uint256 fundAmount = 2 ether; // Add enough for two updates
  994. scheduler.addFunds{value: fundAmount}(subscriptionId);
  995. // Create price feeds and mock Pyth response for first update
  996. bytes32[] memory priceIds = createPriceIds();
  997. uint64 publishTime1 = SafeCast.toUint64(block.timestamp);
  998. PythStructs.PriceFeed[] memory priceFeeds1;
  999. uint64[] memory slots;
  1000. (priceFeeds1, slots) = createMockPriceFeedsWithSlots(
  1001. publishTime1,
  1002. priceIds.length
  1003. );
  1004. mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds1, slots);
  1005. bytes[] memory updateData1 = createMockUpdateData(priceFeeds1);
  1006. // Perform first update
  1007. vm.expectEmit();
  1008. emit PricesUpdated(subscriptionId, publishTime1);
  1009. vm.prank(pusher);
  1010. scheduler.updatePriceFeeds(subscriptionId, updateData1);
  1011. // Verify first update
  1012. (, SchedulerState.SubscriptionStatus memory status1) = scheduler
  1013. .getSubscription(subscriptionId);
  1014. assertEq(
  1015. status1.priceLastUpdatedAt,
  1016. publishTime1,
  1017. "First update timestamp incorrect"
  1018. );
  1019. assertEq(
  1020. status1.totalUpdates,
  1021. priceIds.length,
  1022. "Total updates should be equal to the number of price feeds"
  1023. );
  1024. assertTrue(
  1025. status1.totalSpent > 0,
  1026. "Total spent should be > 0 after first update"
  1027. );
  1028. uint256 spentAfterFirst = status1.totalSpent; // Store spent amount
  1029. // --- Second Update ---
  1030. // Advance time beyond heartbeat interval (e.g., 100 seconds)
  1031. vm.warp(block.timestamp + 100);
  1032. // Create price feeds for second update by cloning first update and modifying
  1033. uint64 publishTime2 = SafeCast.toUint64(block.timestamp);
  1034. PythStructs.PriceFeed[]
  1035. memory priceFeeds2 = new PythStructs.PriceFeed[](
  1036. priceFeeds1.length
  1037. );
  1038. for (uint i = 0; i < priceFeeds1.length; i++) {
  1039. priceFeeds2[i] = priceFeeds1[i]; // Clone the feed struct
  1040. priceFeeds2[i].price.publishTime = publishTime2; // Update timestamp
  1041. // Apply a 100 bps price increase (satisfies update criteria)
  1042. int64 priceDiff = int64(
  1043. (uint64(priceFeeds1[i].price.price) * 100) / 10_000
  1044. );
  1045. priceFeeds2[i].price.price = priceFeeds1[i].price.price + priceDiff;
  1046. priceFeeds2[i].emaPrice.publishTime = publishTime2;
  1047. }
  1048. mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds2, slots); // Mock for the second call
  1049. bytes[] memory updateData2 = createMockUpdateData(priceFeeds2);
  1050. // Perform second update
  1051. vm.expectEmit();
  1052. emit PricesUpdated(subscriptionId, publishTime2);
  1053. vm.prank(pusher);
  1054. scheduler.updatePriceFeeds(subscriptionId, updateData2);
  1055. // Verify second update
  1056. (, SchedulerState.SubscriptionStatus memory status2) = scheduler
  1057. .getSubscription(subscriptionId);
  1058. assertEq(
  1059. status2.priceLastUpdatedAt,
  1060. publishTime2,
  1061. "Second update timestamp incorrect"
  1062. );
  1063. assertEq(
  1064. status2.totalUpdates,
  1065. priceIds.length * 2,
  1066. "Total updates should be equal to the number of price feeds * 2 (first + second update)"
  1067. );
  1068. assertTrue(
  1069. status2.totalSpent > spentAfterFirst,
  1070. "Total spent should increase after second update"
  1071. );
  1072. // Verify price feed data using the reader contract for the second update
  1073. assertTrue(
  1074. reader.verifyPriceFeeds(
  1075. subscriptionId,
  1076. new bytes32[](0),
  1077. priceFeeds2
  1078. ),
  1079. "Price feeds verification failed after second update"
  1080. );
  1081. }
  1082. function testUpdatePriceFeedsPaysKeeperCorrectly() public {
  1083. // Set gas price
  1084. uint256 gasPrice = 0.1 gwei;
  1085. vm.txGasPrice(gasPrice);
  1086. // Add subscription and funds
  1087. uint256 subscriptionId = addTestSubscription(
  1088. scheduler,
  1089. address(reader)
  1090. );
  1091. // Prepare update data
  1092. (SchedulerState.SubscriptionParams memory params, ) = scheduler
  1093. .getSubscription(subscriptionId);
  1094. (
  1095. PythStructs.PriceFeed[] memory priceFeeds,
  1096. uint64[] memory slots
  1097. ) = createMockPriceFeedsWithSlots(
  1098. SafeCast.toUint64(block.timestamp),
  1099. params.priceIds.length
  1100. );
  1101. uint256 mockPythFee = MOCK_PYTH_FEE_PER_FEED * params.priceIds.length;
  1102. mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
  1103. bytes[] memory updateData = createMockUpdateData(priceFeeds);
  1104. // Get state before
  1105. uint256 pusherBalanceBefore = pusher.balance;
  1106. (, SchedulerState.SubscriptionStatus memory statusBefore) = scheduler
  1107. .getSubscription(subscriptionId);
  1108. console.log(
  1109. "Subscription balance before update:",
  1110. vm.toString(statusBefore.balanceInWei)
  1111. );
  1112. // Perform update
  1113. vm.prank(pusher);
  1114. scheduler.updatePriceFeeds(subscriptionId, updateData);
  1115. // Get state after
  1116. (, SchedulerState.SubscriptionStatus memory statusAfter) = scheduler
  1117. .getSubscription(subscriptionId);
  1118. // Calculate total fee deducted from subscription
  1119. uint256 totalFeeDeducted = statusBefore.balanceInWei -
  1120. statusAfter.balanceInWei;
  1121. // Calculate minimum keeper fee (overhead + feed-specific fee)
  1122. // The real cost is more because of the gas used in the updatePriceFeeds function
  1123. uint256 minKeeperFee = (scheduler.GAS_OVERHEAD() * gasPrice) +
  1124. (uint256(scheduler.getSingleUpdateKeeperFeeInWei()) *
  1125. params.priceIds.length);
  1126. assertGt(
  1127. totalFeeDeducted,
  1128. minKeeperFee + mockPythFee,
  1129. "Total fee deducted should be greater than the sum of keeper fee and Pyth fee (since gas usage of updatePriceFeeds is not accounted for)"
  1130. );
  1131. assertEq(
  1132. statusAfter.totalSpent,
  1133. statusBefore.totalSpent + totalFeeDeducted,
  1134. "Total spent should increase by the total fee deducted"
  1135. );
  1136. assertEq(
  1137. pusher.balance,
  1138. pusherBalanceBefore + totalFeeDeducted - mockPythFee,
  1139. "Pusher balance should increase by the keeper fee"
  1140. );
  1141. // This assertion is self-evident based on the calculations above, but keeping it for clarity
  1142. assertEq(
  1143. statusAfter.balanceInWei,
  1144. statusBefore.balanceInWei - totalFeeDeducted,
  1145. "Subscription balance should decrease by the total fee deducted"
  1146. );
  1147. }
  1148. function testUpdatePriceFeedsRevertsInsufficientBalanceForKeeperFee()
  1149. public
  1150. {
  1151. // Set gas price
  1152. uint256 gasPrice = 0.5 gwei;
  1153. vm.txGasPrice(gasPrice);
  1154. // Mock the minimum balance for the subscription to be
  1155. // zero so that we can test the keeper fee
  1156. vm.mockCall(
  1157. address(scheduler),
  1158. abi.encodeWithSelector(Scheduler.getMinimumBalance.selector),
  1159. abi.encode(0)
  1160. );
  1161. // Add subscription
  1162. uint256 subscriptionId = addTestSubscription(
  1163. scheduler,
  1164. address(reader)
  1165. );
  1166. bytes32[] memory priceIds = createPriceIds();
  1167. // Prepare update data and get Pyth fee
  1168. uint64 publishTime = SafeCast.toUint64(block.timestamp);
  1169. PythStructs.PriceFeed[] memory priceFeeds;
  1170. uint64[] memory slots;
  1171. (priceFeeds, slots) = createMockPriceFeedsWithSlots(
  1172. publishTime,
  1173. priceIds.length
  1174. );
  1175. uint256 mockPythFee = MOCK_PYTH_FEE_PER_FEED * priceIds.length;
  1176. mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
  1177. bytes[] memory updateData = createMockUpdateData(priceFeeds);
  1178. // Calculate minimum keeper fee (overhead + feed-specific fee)
  1179. // The real cost is more because of the gas used in the updatePriceFeeds function
  1180. uint256 minKeeperFee = (scheduler.GAS_OVERHEAD() * gasPrice) +
  1181. (uint256(scheduler.getSingleUpdateKeeperFeeInWei()) *
  1182. priceIds.length);
  1183. // Fund subscription without enough for Pyth fee + keeper fee
  1184. // It won't be enough because of the gas cost of updatePriceFeeds
  1185. uint256 fundAmount = mockPythFee + minKeeperFee;
  1186. scheduler.addFunds{value: fundAmount}(subscriptionId);
  1187. // Get and print the subscription balance before attempting the update
  1188. (, SchedulerState.SubscriptionStatus memory status) = scheduler
  1189. .getSubscription(subscriptionId);
  1190. console.log(
  1191. "Subscription balance before update:",
  1192. vm.toString(status.balanceInWei)
  1193. );
  1194. console.log("Required Pyth fee:", vm.toString(mockPythFee));
  1195. console.log("Minimum keeper fee:", vm.toString(minKeeperFee));
  1196. console.log(
  1197. "Total minimum required:",
  1198. vm.toString(mockPythFee + minKeeperFee)
  1199. );
  1200. // Expect revert due to insufficient balance for total fee
  1201. vm.expectRevert(abi.encodeWithSelector(InsufficientBalance.selector));
  1202. vm.prank(pusher);
  1203. scheduler.updatePriceFeeds(subscriptionId, updateData);
  1204. }
  1205. function testUpdatePriceFeedsRevertsOnHeartbeatUpdateConditionNotMet()
  1206. public
  1207. {
  1208. // Add a subscription with only heartbeat criteria (60 seconds)
  1209. uint32 heartbeat = 60;
  1210. SchedulerState.UpdateCriteria memory criteria = SchedulerState
  1211. .UpdateCriteria({
  1212. updateOnHeartbeat: true,
  1213. heartbeatSeconds: heartbeat,
  1214. updateOnDeviation: false,
  1215. deviationThresholdBps: 0
  1216. });
  1217. uint256 subscriptionId = addTestSubscriptionWithUpdateCriteria(
  1218. scheduler,
  1219. criteria,
  1220. address(reader)
  1221. );
  1222. uint256 fundAmount = 1 ether;
  1223. scheduler.addFunds{value: fundAmount}(subscriptionId);
  1224. // First update to set initial timestamp
  1225. uint64 publishTime1 = SafeCast.toUint64(block.timestamp);
  1226. PythStructs.PriceFeed[] memory priceFeeds1;
  1227. uint64[] memory slots1;
  1228. (priceFeeds1, slots1) = createMockPriceFeedsWithSlots(publishTime1, 2);
  1229. mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds1, slots1);
  1230. bytes[] memory updateData1 = createMockUpdateData(priceFeeds1);
  1231. vm.prank(pusher);
  1232. scheduler.updatePriceFeeds(subscriptionId, updateData1);
  1233. // Prepare second update within heartbeat interval
  1234. vm.warp(block.timestamp + 30); // Advance time by 30 seconds (less than 60)
  1235. uint64 publishTime2 = SafeCast.toUint64(block.timestamp);
  1236. PythStructs.PriceFeed[] memory priceFeeds2;
  1237. uint64[] memory slots2;
  1238. (priceFeeds2, slots2) = createMockPriceFeedsWithSlots(publishTime2, 2);
  1239. mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds2, slots2);
  1240. bytes[] memory updateData2 = createMockUpdateData(priceFeeds2);
  1241. // Expect revert because heartbeat condition is not met
  1242. vm.expectRevert(
  1243. abi.encodeWithSelector(UpdateConditionsNotMet.selector)
  1244. );
  1245. vm.prank(pusher);
  1246. scheduler.updatePriceFeeds(subscriptionId, updateData2);
  1247. }
  1248. function testUpdatePriceFeedsRevertsOnDeviationUpdateConditionNotMet()
  1249. public
  1250. {
  1251. // Add a subscription with only deviation criteria (100 bps / 1%)
  1252. uint16 deviationBps = 100;
  1253. SchedulerState.UpdateCriteria memory criteria = SchedulerState
  1254. .UpdateCriteria({
  1255. updateOnHeartbeat: false,
  1256. heartbeatSeconds: 0,
  1257. updateOnDeviation: true,
  1258. deviationThresholdBps: deviationBps
  1259. });
  1260. uint256 subscriptionId = addTestSubscriptionWithUpdateCriteria(
  1261. scheduler,
  1262. criteria,
  1263. address(reader)
  1264. );
  1265. uint256 fundAmount = 1 ether;
  1266. scheduler.addFunds{value: fundAmount}(subscriptionId);
  1267. // First update to set initial price
  1268. uint64 publishTime1 = SafeCast.toUint64(block.timestamp);
  1269. PythStructs.PriceFeed[] memory priceFeeds1;
  1270. uint64[] memory slots;
  1271. (priceFeeds1, slots) = createMockPriceFeedsWithSlots(publishTime1, 2);
  1272. mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds1, slots);
  1273. bytes[] memory updateData1 = createMockUpdateData(priceFeeds1);
  1274. vm.prank(pusher);
  1275. scheduler.updatePriceFeeds(subscriptionId, updateData1);
  1276. // Prepare second update with price deviation less than threshold (e.g., 50 bps)
  1277. vm.warp(block.timestamp + 1000); // Advance time significantly (doesn't matter for deviation)
  1278. uint64 publishTime2 = SafeCast.toUint64(block.timestamp);
  1279. // Clone priceFeeds1 and apply a 50 bps deviation to its prices
  1280. PythStructs.PriceFeed[]
  1281. memory priceFeeds2 = new PythStructs.PriceFeed[](
  1282. priceFeeds1.length
  1283. );
  1284. for (uint i = 0; i < priceFeeds1.length; i++) {
  1285. priceFeeds2[i].id = priceFeeds1[i].id;
  1286. // Apply 50 bps deviation to the price
  1287. int64 priceDiff = int64(
  1288. (uint64(priceFeeds1[i].price.price) * 50) / 10_000
  1289. );
  1290. priceFeeds2[i].price.price = priceFeeds1[i].price.price + priceDiff;
  1291. priceFeeds2[i].price.conf = priceFeeds1[i].price.conf;
  1292. priceFeeds2[i].price.expo = priceFeeds1[i].price.expo;
  1293. priceFeeds2[i].price.publishTime = publishTime2;
  1294. }
  1295. mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds2, slots);
  1296. bytes[] memory updateData2 = createMockUpdateData(priceFeeds2);
  1297. // Expect revert because deviation condition is not met
  1298. vm.expectRevert(
  1299. abi.encodeWithSelector(UpdateConditionsNotMet.selector)
  1300. );
  1301. vm.prank(pusher);
  1302. scheduler.updatePriceFeeds(subscriptionId, updateData2);
  1303. }
  1304. function testUpdatePriceFeedsRevertsOnOlderTimestamp() public {
  1305. // Add a subscription and funds
  1306. uint256 subscriptionId = addTestSubscription(
  1307. scheduler,
  1308. address(reader)
  1309. );
  1310. uint256 fundAmount = 1 ether;
  1311. scheduler.addFunds{value: fundAmount}(subscriptionId);
  1312. // First update to establish last updated timestamp
  1313. uint64 publishTime1 = SafeCast.toUint64(block.timestamp);
  1314. PythStructs.PriceFeed[] memory priceFeeds1;
  1315. uint64[] memory slots1;
  1316. (priceFeeds1, slots1) = createMockPriceFeedsWithSlots(publishTime1, 2);
  1317. mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds1, slots1);
  1318. bytes[] memory updateData1 = createMockUpdateData(priceFeeds1);
  1319. vm.prank(pusher);
  1320. scheduler.updatePriceFeeds(subscriptionId, updateData1);
  1321. // Prepare second update with an older timestamp
  1322. uint64 publishTime2 = publishTime1 - 10; // Timestamp older than the first update
  1323. PythStructs.PriceFeed[] memory priceFeeds2;
  1324. uint64[] memory slots2;
  1325. (priceFeeds2, slots2) = createMockPriceFeedsWithSlots(publishTime2, 2);
  1326. // Mock Pyth response to return feeds with the older timestamp
  1327. mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds2, slots2);
  1328. bytes[] memory updateData2 = createMockUpdateData(priceFeeds2);
  1329. // Expect revert with TimestampOlderThanLastUpdate (checked in _validateShouldUpdatePrices)
  1330. vm.expectRevert(
  1331. abi.encodeWithSelector(
  1332. TimestampOlderThanLastUpdate.selector,
  1333. publishTime2,
  1334. publishTime1
  1335. )
  1336. );
  1337. // Attempt to update price feeds
  1338. vm.prank(pusher);
  1339. scheduler.updatePriceFeeds(subscriptionId, updateData2);
  1340. }
  1341. function testUpdatePriceFeedsRevertsOnMismatchedSlots() public {
  1342. // First add a subscription and funds
  1343. uint256 subscriptionId = addTestSubscription(
  1344. scheduler,
  1345. address(reader)
  1346. );
  1347. uint256 fundAmount = 1 ether;
  1348. scheduler.addFunds{value: fundAmount}(subscriptionId);
  1349. // Create two price feeds with same timestamp but different slots
  1350. uint64 publishTime = SafeCast.toUint64(block.timestamp);
  1351. PythStructs.PriceFeed[] memory priceFeeds = new PythStructs.PriceFeed[](
  1352. 2
  1353. );
  1354. priceFeeds[0] = createSingleMockPriceFeed(publishTime);
  1355. priceFeeds[1] = createSingleMockPriceFeed(publishTime);
  1356. // Create slots array with different slot values
  1357. uint64[] memory slots = new uint64[](2);
  1358. slots[0] = 100;
  1359. slots[1] = 200; // Different slot
  1360. // Mock Pyth response to return these feeds with mismatched slots
  1361. mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
  1362. bytes[] memory updateData = createMockUpdateData(priceFeeds);
  1363. // Expect revert with PriceSlotMismatch error
  1364. vm.expectRevert(abi.encodeWithSelector(PriceSlotMismatch.selector));
  1365. // Attempt to update price feeds
  1366. vm.prank(pusher);
  1367. scheduler.updatePriceFeeds(subscriptionId, updateData);
  1368. }
  1369. function testUpdateSubscriptionEnforcesMinimumBalance() public {
  1370. // Setup: Create subscription with 2 feeds, funded exactly to minimum
  1371. uint8 initialNumFeeds = 2;
  1372. uint256 subscriptionId = addTestSubscriptionWithFeeds(
  1373. scheduler,
  1374. initialNumFeeds,
  1375. address(reader)
  1376. );
  1377. (
  1378. SchedulerState.SubscriptionParams memory currentParams,
  1379. SchedulerState.SubscriptionStatus memory initialStatus
  1380. ) = scheduler.getSubscription(subscriptionId);
  1381. uint256 initialMinimumBalance = scheduler.getMinimumBalance(
  1382. initialNumFeeds
  1383. );
  1384. assertEq(
  1385. initialStatus.balanceInWei,
  1386. initialMinimumBalance,
  1387. "Initial balance should be the minimum"
  1388. );
  1389. // Prepare new params with more feeds (4)
  1390. uint8 newNumFeeds = 4;
  1391. SchedulerState.SubscriptionParams memory newParams = currentParams;
  1392. newParams.priceIds = createPriceIds(newNumFeeds); // Increase feeds
  1393. newParams.isActive = true; // Keep it active
  1394. // Action 1: Try to update with insufficient funds
  1395. vm.expectRevert(abi.encodeWithSelector(InsufficientBalance.selector));
  1396. scheduler.updateSubscription(subscriptionId, newParams);
  1397. // Action 2: Supply enough funds to the updateSubscription call to meet the new minimum balance
  1398. uint256 newMinimumBalance = scheduler.getMinimumBalance(newNumFeeds);
  1399. uint256 requiredFunds = newMinimumBalance - initialMinimumBalance;
  1400. scheduler.updateSubscription{value: requiredFunds}(
  1401. subscriptionId,
  1402. newParams
  1403. );
  1404. // Verification 2: Update should now succeed
  1405. (SchedulerState.SubscriptionParams memory updatedParams, ) = scheduler
  1406. .getSubscription(subscriptionId);
  1407. assertEq(
  1408. updatedParams.priceIds.length,
  1409. newNumFeeds,
  1410. "Number of price feeds should be updated"
  1411. );
  1412. // Scenario 3: Deactivating while adding feeds - should NOT check min balance
  1413. // Reset state: create another subscription funded to minimum
  1414. uint8 initialNumFeeds_deact = 2;
  1415. uint256 subId_deact = addTestSubscriptionWithFeeds(
  1416. scheduler,
  1417. initialNumFeeds_deact,
  1418. address(reader)
  1419. );
  1420. // Prepare params to add feeds (4) but also deactivate
  1421. uint8 newNumFeeds_deact = 4;
  1422. (
  1423. SchedulerState.SubscriptionParams memory currentParams_deact,
  1424. ) = scheduler.getSubscription(subId_deact);
  1425. SchedulerState.SubscriptionParams
  1426. memory newParams_deact = currentParams_deact;
  1427. newParams_deact.priceIds = createPriceIds(newNumFeeds_deact);
  1428. newParams_deact.isActive = false; // Deactivate
  1429. // Action 3: Update (should succeed even with insufficient min balance for 4 feeds)
  1430. scheduler.updateSubscription(subId_deact, newParams_deact);
  1431. // Verification 3: Subscription should be inactive and have 4 feeds
  1432. (
  1433. SchedulerState.SubscriptionParams memory updatedParams_deact,
  1434. ) = scheduler.getSubscription(subId_deact);
  1435. assertFalse(
  1436. updatedParams_deact.isActive,
  1437. "Subscription should be inactive"
  1438. );
  1439. assertEq(
  1440. updatedParams_deact.priceIds.length,
  1441. newNumFeeds_deact,
  1442. "Number of price feeds should be updated even when deactivating"
  1443. );
  1444. // Scenario 4: Reducing number of feeds still checks minimum balance
  1445. // Create a subscription with 2 feeds funded to minimum
  1446. uint8 initialNumFeeds_reduce = 2;
  1447. uint256 subId_reduce = addTestSubscriptionWithFeeds(
  1448. scheduler,
  1449. initialNumFeeds_reduce,
  1450. address(reader)
  1451. );
  1452. // Deplete the balance by updating price feeds multiple times
  1453. uint64 publishTime = SafeCast.toUint64(block.timestamp);
  1454. for (uint i = 0; i < 50; i++) {
  1455. // Advance publish time by 60s for each update to satisfy update criteria
  1456. (
  1457. PythStructs.PriceFeed[] memory priceFeeds_reduce,
  1458. uint64[] memory slots_reduce
  1459. ) = createMockPriceFeedsWithSlots(publishTime + (i * 60), 2);
  1460. mockParsePriceFeedUpdatesWithSlotsStrict(
  1461. pyth,
  1462. priceFeeds_reduce,
  1463. slots_reduce
  1464. );
  1465. bytes[] memory updateData_reduce = createMockUpdateData(
  1466. priceFeeds_reduce
  1467. );
  1468. vm.prank(pusher);
  1469. scheduler.updatePriceFeeds(subId_reduce, updateData_reduce);
  1470. }
  1471. // Check that balance is now below minimum for 1 feed
  1472. (, SchedulerState.SubscriptionStatus memory status_reduce) = scheduler
  1473. .getSubscription(subId_reduce);
  1474. uint256 minBalanceForOneFeed = scheduler.getMinimumBalance(1);
  1475. assertTrue(
  1476. status_reduce.balanceInWei < minBalanceForOneFeed,
  1477. "Balance should be below minimum for 1 feed"
  1478. );
  1479. // Prepare params to reduce feeds from 2 to 1
  1480. (
  1481. SchedulerState.SubscriptionParams memory currentParams_reduce,
  1482. ) = scheduler.getSubscription(subId_reduce);
  1483. SchedulerState.SubscriptionParams
  1484. memory newParams_reduce = currentParams_reduce;
  1485. newParams_reduce.priceIds = new bytes32[](1);
  1486. newParams_reduce.priceIds[0] = currentParams_reduce.priceIds[0];
  1487. // Action 4: Update should fail due to insufficient balance
  1488. vm.expectRevert(abi.encodeWithSelector(InsufficientBalance.selector));
  1489. scheduler.updateSubscription(subId_reduce, newParams_reduce);
  1490. // Add funds to cover minimum balance for 1 feed
  1491. uint256 additionalFunds = minBalanceForOneFeed -
  1492. status_reduce.balanceInWei +
  1493. 0.01 ether;
  1494. // Now the update should succeed
  1495. scheduler.updateSubscription{value: additionalFunds}(
  1496. subId_reduce,
  1497. newParams_reduce
  1498. );
  1499. // Verify the subscription now has 1 feed
  1500. (
  1501. SchedulerState.SubscriptionParams memory updatedParams_reduce,
  1502. ) = scheduler.getSubscription(subId_reduce);
  1503. assertEq(
  1504. updatedParams_reduce.priceIds.length,
  1505. 1,
  1506. "Number of price feeds should be reduced to 1"
  1507. );
  1508. }
  1509. function testGetPricesUnsafeAllFeeds() public {
  1510. // First add a subscription, funds, and update price feeds
  1511. uint256 subscriptionId = addTestSubscription(
  1512. scheduler,
  1513. address(reader)
  1514. );
  1515. uint256 fundAmount = 1 ether;
  1516. scheduler.addFunds{value: fundAmount}(subscriptionId);
  1517. bytes32[] memory priceIds = createPriceIds();
  1518. uint64 publishTime = SafeCast.toUint64(block.timestamp);
  1519. PythStructs.PriceFeed[] memory priceFeeds;
  1520. uint64[] memory slots;
  1521. (priceFeeds, slots) = createMockPriceFeedsWithSlots(publishTime, 2);
  1522. mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
  1523. bytes[] memory updateData = createMockUpdateData(priceFeeds);
  1524. vm.prank(pusher);
  1525. scheduler.updatePriceFeeds(subscriptionId, updateData);
  1526. // Get all latest prices (empty priceIds array)
  1527. bytes32[] memory emptyPriceIds = new bytes32[](0);
  1528. PythStructs.Price[] memory latestPrices = scheduler.getPricesUnsafe(
  1529. subscriptionId,
  1530. emptyPriceIds
  1531. );
  1532. // Verify all price feeds were returned
  1533. assertEq(
  1534. latestPrices.length,
  1535. priceIds.length,
  1536. "Should return all price feeds"
  1537. );
  1538. // Verify price feed data using the reader contract
  1539. assertTrue(
  1540. reader.verifyPriceFeeds(subscriptionId, emptyPriceIds, priceFeeds),
  1541. "Price feeds verification failed"
  1542. );
  1543. }
  1544. function testGetPricesUnsafeSelectiveFeeds() public {
  1545. // First add a subscription with 3 price feeds, funds, and update price feeds
  1546. uint256 subscriptionId = addTestSubscriptionWithFeeds(
  1547. scheduler,
  1548. 3,
  1549. address(reader)
  1550. );
  1551. uint256 fundAmount = 1 ether;
  1552. scheduler.addFunds{value: fundAmount}(subscriptionId);
  1553. bytes32[] memory priceIds = createPriceIds(3);
  1554. uint64 publishTime = SafeCast.toUint64(block.timestamp);
  1555. PythStructs.PriceFeed[] memory priceFeeds;
  1556. uint64[] memory slots;
  1557. (priceFeeds, slots) = createMockPriceFeedsWithSlots(publishTime, 3);
  1558. mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
  1559. bytes[] memory updateData = createMockUpdateData(priceFeeds);
  1560. vm.prank(pusher);
  1561. scheduler.updatePriceFeeds(subscriptionId, updateData);
  1562. // Get only the first price feed
  1563. bytes32[] memory selectedPriceIds = new bytes32[](1);
  1564. selectedPriceIds[0] = priceIds[0];
  1565. PythStructs.Price[] memory latestPrices = scheduler.getPricesUnsafe(
  1566. subscriptionId,
  1567. selectedPriceIds
  1568. );
  1569. // Verify only one price feed was returned
  1570. assertEq(latestPrices.length, 1, "Should return only one price feed");
  1571. // Create expected price feed array with just the first feed
  1572. PythStructs.PriceFeed[]
  1573. memory expectedFeeds = new PythStructs.PriceFeed[](1);
  1574. expectedFeeds[0] = priceFeeds[0];
  1575. // Verify price feed data using the reader contract
  1576. assertTrue(
  1577. reader.verifyPriceFeeds(
  1578. subscriptionId,
  1579. selectedPriceIds,
  1580. expectedFeeds
  1581. ),
  1582. "Price feeds verification failed"
  1583. );
  1584. }
  1585. function testDisabledWhitelistAllowsUnrestrictedReads() public {
  1586. uint256 subscriptionId = addTestSubscription(
  1587. scheduler,
  1588. address(reader)
  1589. );
  1590. // Get params and modify them
  1591. (SchedulerState.SubscriptionParams memory params, ) = scheduler
  1592. .getSubscription(subscriptionId);
  1593. params.whitelistEnabled = false;
  1594. params.readerWhitelist = new address[](0);
  1595. scheduler.updateSubscription(subscriptionId, params);
  1596. // Fund the subscription with enough to update it
  1597. scheduler.addFunds{value: 1 ether}(subscriptionId);
  1598. // Update price feeds for the subscription
  1599. uint64 publishTime = SafeCast.toUint64(block.timestamp);
  1600. PythStructs.PriceFeed[] memory priceFeeds;
  1601. uint64[] memory slots;
  1602. (priceFeeds, slots) = createMockPriceFeedsWithSlots(publishTime, 2);
  1603. mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
  1604. bytes[] memory updateData = createMockUpdateData(priceFeeds);
  1605. vm.prank(pusher);
  1606. scheduler.updatePriceFeeds(subscriptionId, updateData);
  1607. // Try to access from a non-whitelisted address (should succeed)
  1608. address randomUser = address(0xdead);
  1609. vm.startPrank(randomUser);
  1610. bytes32[] memory emptyPriceIds = new bytes32[](0);
  1611. // Should not revert since whitelist is disabled
  1612. scheduler.getPricesUnsafe(subscriptionId, emptyPriceIds);
  1613. vm.stopPrank();
  1614. // Verify the data is correct using the test's reader
  1615. assertTrue(
  1616. reader.verifyPriceFeeds(subscriptionId, emptyPriceIds, priceFeeds),
  1617. "Whitelist Disabled: Price feeds verification failed"
  1618. );
  1619. }
  1620. function testEnabledWhitelistEnforcesOnlyAuthorizedReads() public {
  1621. uint256 subscriptionId = addTestSubscription(
  1622. scheduler,
  1623. address(reader)
  1624. );
  1625. // Fund the subscription with enough to update it
  1626. scheduler.addFunds{value: 1 ether}(subscriptionId);
  1627. // Get the price IDs from the created subscription
  1628. (SchedulerState.SubscriptionParams memory params, ) = scheduler
  1629. .getSubscription(subscriptionId);
  1630. bytes32[] memory priceIds = params.priceIds;
  1631. // Update price feeds for the subscription
  1632. uint64 publishTime = SafeCast.toUint64(block.timestamp + 10); // Slightly different time
  1633. PythStructs.PriceFeed[] memory priceFeeds;
  1634. uint64[] memory slots;
  1635. (priceFeeds, slots) = createMockPriceFeedsWithSlots(
  1636. publishTime,
  1637. priceIds.length
  1638. );
  1639. mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
  1640. bytes[] memory updateData = createMockUpdateData(priceFeeds);
  1641. vm.prank(pusher);
  1642. scheduler.updatePriceFeeds(subscriptionId, updateData);
  1643. // Try to access from a non-whitelisted address (should fail)
  1644. vm.startPrank(address(0xdead));
  1645. bytes32[] memory emptyPriceIds = new bytes32[](0);
  1646. vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector));
  1647. scheduler.getPricesUnsafe(subscriptionId, emptyPriceIds);
  1648. vm.stopPrank();
  1649. // Try to access from the whitelisted reader address (should succeed)
  1650. // Note: We call via the reader contract instance itself
  1651. PythStructs.Price[] memory pricesFromReader = reader.getPricesUnsafe(
  1652. subscriptionId,
  1653. emptyPriceIds
  1654. );
  1655. assertEq(
  1656. pricesFromReader.length,
  1657. priceIds.length,
  1658. "Whitelist Enabled: Reader should get correct number of prices"
  1659. );
  1660. // Verify the data obtained by the whitelisted reader is correct
  1661. assertTrue(
  1662. reader.verifyPriceFeeds(subscriptionId, emptyPriceIds, priceFeeds),
  1663. "Whitelist Enabled: Price feeds verification failed via reader"
  1664. );
  1665. // Try to access from the manager address (should succeed)
  1666. // Test contract is the manager
  1667. vm.startPrank(address(this));
  1668. PythStructs.Price[] memory pricesFromManager = scheduler
  1669. .getPricesUnsafe(subscriptionId, emptyPriceIds);
  1670. assertEq(
  1671. pricesFromManager.length,
  1672. priceIds.length,
  1673. "Whitelist Enabled: Manager should get correct number of prices"
  1674. );
  1675. vm.stopPrank();
  1676. }
  1677. function testGetEmaPriceUnsafe() public {
  1678. // First add a subscription, funds, and update price feeds
  1679. uint256 subscriptionId = addTestSubscription(
  1680. scheduler,
  1681. address(reader)
  1682. );
  1683. uint256 fundAmount = 1 ether;
  1684. scheduler.addFunds{value: fundAmount}(subscriptionId);
  1685. bytes32[] memory priceIds = createPriceIds();
  1686. uint64 publishTime = SafeCast.toUint64(block.timestamp);
  1687. PythStructs.PriceFeed[] memory priceFeeds;
  1688. uint64[] memory slots;
  1689. (priceFeeds, slots) = createMockPriceFeedsWithSlots(publishTime, 2);
  1690. // Ensure EMA prices are set in the mock price feeds
  1691. for (uint i = 0; i < priceFeeds.length; i++) {
  1692. priceFeeds[i].emaPrice.price = priceFeeds[i].price.price * 2; // Make EMA price different for testing
  1693. priceFeeds[i].emaPrice.conf = priceFeeds[i].price.conf;
  1694. priceFeeds[i].emaPrice.publishTime = publishTime;
  1695. priceFeeds[i].emaPrice.expo = priceFeeds[i].price.expo;
  1696. }
  1697. mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
  1698. bytes[] memory updateData = createMockUpdateData(priceFeeds);
  1699. vm.prank(pusher);
  1700. scheduler.updatePriceFeeds(subscriptionId, updateData);
  1701. // Get EMA prices
  1702. bytes32[] memory emptyPriceIds = new bytes32[](0);
  1703. PythStructs.Price[] memory emaPrices = scheduler.getEmaPriceUnsafe(
  1704. subscriptionId,
  1705. emptyPriceIds
  1706. );
  1707. // Verify all EMA prices were returned
  1708. assertEq(
  1709. emaPrices.length,
  1710. priceIds.length,
  1711. "Should return all EMA prices"
  1712. );
  1713. // Verify EMA price values
  1714. for (uint i = 0; i < emaPrices.length; i++) {
  1715. assertEq(
  1716. emaPrices[i].price,
  1717. priceFeeds[i].emaPrice.price,
  1718. "EMA price value mismatch"
  1719. );
  1720. assertEq(
  1721. emaPrices[i].publishTime,
  1722. priceFeeds[i].emaPrice.publishTime,
  1723. "EMA price publish time mismatch"
  1724. );
  1725. }
  1726. }
  1727. function testGetActiveSubscriptions() public {
  1728. // Add two subscriptions with the test contract as manager
  1729. addTestSubscription(scheduler, address(reader));
  1730. addTestSubscription(scheduler, address(reader));
  1731. // Create a subscription with pusher as manager
  1732. vm.startPrank(pusher);
  1733. bytes32[] memory priceIds = createPriceIds();
  1734. address[] memory emptyWhitelist = new address[](0);
  1735. SchedulerState.UpdateCriteria memory updateCriteria = SchedulerState
  1736. .UpdateCriteria({
  1737. updateOnHeartbeat: true,
  1738. heartbeatSeconds: 60,
  1739. updateOnDeviation: true,
  1740. deviationThresholdBps: 100
  1741. });
  1742. SchedulerState.SubscriptionParams memory pusherParams = SchedulerState
  1743. .SubscriptionParams({
  1744. priceIds: priceIds,
  1745. readerWhitelist: emptyWhitelist,
  1746. whitelistEnabled: false,
  1747. isActive: true,
  1748. isPermanent: false,
  1749. updateCriteria: updateCriteria
  1750. });
  1751. uint256 minimumBalance = scheduler.getMinimumBalance(
  1752. uint8(priceIds.length)
  1753. );
  1754. vm.deal(pusher, minimumBalance);
  1755. scheduler.createSubscription{value: minimumBalance}(pusherParams);
  1756. vm.stopPrank();
  1757. // Get active subscriptions directly - should work without any special permissions
  1758. uint256[] memory activeIds;
  1759. SchedulerState.SubscriptionParams[] memory activeParams;
  1760. uint256 totalCount;
  1761. (activeIds, activeParams, totalCount) = scheduler
  1762. .getActiveSubscriptions(0, 10);
  1763. // We added 3 subscriptions and all should be active
  1764. assertEq(activeIds.length, 3, "Should have 3 active subscriptions");
  1765. assertEq(
  1766. activeParams.length,
  1767. 3,
  1768. "Should have 3 active subscription params"
  1769. );
  1770. assertEq(totalCount, 3, "Total count should be 3");
  1771. // Verify subscription params
  1772. for (uint i = 0; i < activeIds.length; i++) {
  1773. (
  1774. SchedulerState.SubscriptionParams memory storedParams,
  1775. ) = scheduler.getSubscription(activeIds[i]);
  1776. assertEq(
  1777. activeParams[i].priceIds.length,
  1778. storedParams.priceIds.length,
  1779. "Price IDs length mismatch"
  1780. );
  1781. assertEq(
  1782. activeParams[i].updateCriteria.heartbeatSeconds,
  1783. storedParams.updateCriteria.heartbeatSeconds,
  1784. "Heartbeat seconds mismatch"
  1785. );
  1786. }
  1787. // Test pagination - get only the first subscription
  1788. vm.prank(owner);
  1789. (uint256[] memory firstPageIds, , uint256 firstPageTotal) = scheduler
  1790. .getActiveSubscriptions(0, 1);
  1791. assertEq(
  1792. firstPageIds.length,
  1793. 1,
  1794. "Should have 1 subscription in first page"
  1795. );
  1796. assertEq(firstPageTotal, 3, "Total count should still be 3");
  1797. // Test pagination - get the second page
  1798. vm.prank(owner);
  1799. (uint256[] memory secondPageIds, , uint256 secondPageTotal) = scheduler
  1800. .getActiveSubscriptions(1, 2);
  1801. assertEq(
  1802. secondPageIds.length,
  1803. 2,
  1804. "Should have 2 subscriptions in second page"
  1805. );
  1806. assertEq(secondPageTotal, 3, "Total count should still be 3");
  1807. // Test pagination - start index beyond total count
  1808. vm.prank(owner);
  1809. (uint256[] memory emptyPageIds, , uint256 emptyPageTotal) = scheduler
  1810. .getActiveSubscriptions(10, 10);
  1811. assertEq(
  1812. emptyPageIds.length,
  1813. 0,
  1814. "Should have 0 subscriptions when start index is beyond total"
  1815. );
  1816. assertEq(emptyPageTotal, 3, "Total count should still be 3");
  1817. }
  1818. function testSubscriptionParamValidations() public {
  1819. uint256 initialSubId = 0; // For update tests
  1820. // === Empty Price IDs ===
  1821. SchedulerState.SubscriptionParams
  1822. memory emptyPriceIdsParams = createDefaultSubscriptionParams(
  1823. 1,
  1824. address(reader)
  1825. );
  1826. emptyPriceIdsParams.priceIds = new bytes32[](0);
  1827. vm.expectRevert(abi.encodeWithSelector(EmptyPriceIds.selector));
  1828. scheduler.createSubscription{value: 1 ether}(emptyPriceIdsParams);
  1829. initialSubId = addTestSubscription(scheduler, address(reader)); // Create a valid one for update test
  1830. vm.expectRevert(abi.encodeWithSelector(EmptyPriceIds.selector));
  1831. scheduler.updateSubscription(initialSubId, emptyPriceIdsParams);
  1832. // === Duplicate Price IDs ===
  1833. SchedulerState.SubscriptionParams
  1834. memory duplicatePriceIdsParams = createDefaultSubscriptionParams(
  1835. 2,
  1836. address(reader)
  1837. );
  1838. bytes32 duplicateId = duplicatePriceIdsParams.priceIds[0];
  1839. duplicatePriceIdsParams.priceIds[1] = duplicateId;
  1840. vm.expectRevert(
  1841. abi.encodeWithSelector(DuplicatePriceId.selector, duplicateId)
  1842. );
  1843. scheduler.createSubscription{value: 1 ether}(duplicatePriceIdsParams);
  1844. initialSubId = addTestSubscription(scheduler, address(reader));
  1845. vm.expectRevert(
  1846. abi.encodeWithSelector(DuplicatePriceId.selector, duplicateId)
  1847. );
  1848. scheduler.updateSubscription(initialSubId, duplicatePriceIdsParams);
  1849. // === Too Many Whitelist Readers ===
  1850. SchedulerState.SubscriptionParams
  1851. memory largeWhitelistParams = createDefaultSubscriptionParams(
  1852. 1,
  1853. address(reader)
  1854. );
  1855. uint whitelistLength = uint(scheduler.MAX_READER_WHITELIST_SIZE()) + 1;
  1856. address[] memory largeWhitelist = new address[](whitelistLength);
  1857. for (uint i = 0; i < whitelistLength; i++) {
  1858. largeWhitelist[i] = address(uint160(i + 1)); // Unique addresses
  1859. }
  1860. largeWhitelistParams.readerWhitelist = largeWhitelist;
  1861. vm.expectRevert(
  1862. abi.encodeWithSelector(
  1863. TooManyWhitelistedReaders.selector,
  1864. largeWhitelist.length,
  1865. scheduler.MAX_READER_WHITELIST_SIZE()
  1866. )
  1867. );
  1868. scheduler.createSubscription{value: 1 ether}(largeWhitelistParams);
  1869. initialSubId = addTestSubscription(scheduler, address(reader));
  1870. vm.expectRevert(
  1871. abi.encodeWithSelector(
  1872. TooManyWhitelistedReaders.selector,
  1873. largeWhitelist.length,
  1874. scheduler.MAX_READER_WHITELIST_SIZE()
  1875. )
  1876. );
  1877. scheduler.updateSubscription(initialSubId, largeWhitelistParams);
  1878. // === Duplicate Whitelist Address ===
  1879. SchedulerState.SubscriptionParams
  1880. memory duplicateWhitelistParams = createDefaultSubscriptionParams(
  1881. 1,
  1882. address(reader)
  1883. );
  1884. address[] memory duplicateWhitelist = new address[](2);
  1885. duplicateWhitelist[0] = address(reader);
  1886. duplicateWhitelist[1] = address(reader); // Duplicate
  1887. duplicateWhitelistParams.readerWhitelist = duplicateWhitelist;
  1888. vm.expectRevert(
  1889. abi.encodeWithSelector(
  1890. DuplicateWhitelistAddress.selector,
  1891. address(reader)
  1892. )
  1893. );
  1894. scheduler.createSubscription{value: 1 ether}(duplicateWhitelistParams);
  1895. initialSubId = addTestSubscription(scheduler, address(reader));
  1896. vm.expectRevert(
  1897. abi.encodeWithSelector(
  1898. DuplicateWhitelistAddress.selector,
  1899. address(reader)
  1900. )
  1901. );
  1902. scheduler.updateSubscription(initialSubId, duplicateWhitelistParams);
  1903. // === Invalid Heartbeat (Zero Seconds) ===
  1904. SchedulerState.SubscriptionParams
  1905. memory invalidHeartbeatParams = createDefaultSubscriptionParams(
  1906. 1,
  1907. address(reader)
  1908. );
  1909. invalidHeartbeatParams.updateCriteria.updateOnHeartbeat = true;
  1910. invalidHeartbeatParams.updateCriteria.heartbeatSeconds = 0; // Invalid
  1911. vm.expectRevert(abi.encodeWithSelector(InvalidUpdateCriteria.selector));
  1912. scheduler.createSubscription{value: 1 ether}(invalidHeartbeatParams);
  1913. initialSubId = addTestSubscription(scheduler, address(reader));
  1914. vm.expectRevert(abi.encodeWithSelector(InvalidUpdateCriteria.selector));
  1915. scheduler.updateSubscription(initialSubId, invalidHeartbeatParams);
  1916. // === Invalid Deviation (Zero Bps) ===
  1917. SchedulerState.SubscriptionParams
  1918. memory invalidDeviationParams = createDefaultSubscriptionParams(
  1919. 1,
  1920. address(reader)
  1921. );
  1922. invalidDeviationParams.updateCriteria.updateOnDeviation = true;
  1923. invalidDeviationParams.updateCriteria.deviationThresholdBps = 0; // Invalid
  1924. vm.expectRevert(abi.encodeWithSelector(InvalidUpdateCriteria.selector));
  1925. scheduler.createSubscription{value: 1 ether}(invalidDeviationParams);
  1926. initialSubId = addTestSubscription(scheduler, address(reader));
  1927. vm.expectRevert(abi.encodeWithSelector(InvalidUpdateCriteria.selector));
  1928. scheduler.updateSubscription(initialSubId, invalidDeviationParams);
  1929. }
  1930. function testUpdatePriceFeedsSucceedsWithStaleFeedIfLatestIsValid() public {
  1931. // Add a subscription and funds
  1932. uint256 subscriptionId = addTestSubscription(
  1933. scheduler,
  1934. address(reader)
  1935. );
  1936. // Advance time past the validity period
  1937. vm.warp(
  1938. block.timestamp +
  1939. scheduler.PAST_TIMESTAMP_MAX_VALIDITY_PERIOD() +
  1940. 600
  1941. ); // Warp 1 hour 10 mins
  1942. uint64 currentTime = SafeCast.toUint64(block.timestamp);
  1943. uint64 validPublishTime = currentTime - 1800; // 30 mins ago (within 1 hour validity)
  1944. uint64 stalePublishTime = currentTime -
  1945. (scheduler.PAST_TIMESTAMP_MAX_VALIDITY_PERIOD() + 300); // 1 hour 5 mins ago (outside validity)
  1946. PythStructs.PriceFeed[] memory priceFeeds = new PythStructs.PriceFeed[](
  1947. 2
  1948. );
  1949. priceFeeds[0] = createSingleMockPriceFeed(stalePublishTime);
  1950. priceFeeds[1] = createSingleMockPriceFeed(validPublishTime);
  1951. uint64[] memory slots = new uint64[](2);
  1952. slots[0] = 100;
  1953. slots[1] = 100; // Same slot
  1954. // Mock Pyth response (should succeed in the real world as minValidTime is 0)
  1955. mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
  1956. bytes[] memory updateData = createMockUpdateData(priceFeeds);
  1957. // Expect PricesUpdated event with the latest valid timestamp
  1958. vm.expectEmit();
  1959. emit PricesUpdated(subscriptionId, validPublishTime);
  1960. // Perform update - should succeed because the latest timestamp in the update data is valid
  1961. vm.prank(pusher);
  1962. scheduler.updatePriceFeeds(subscriptionId, updateData);
  1963. // Verify last updated timestamp
  1964. (, SchedulerState.SubscriptionStatus memory status) = scheduler
  1965. .getSubscription(subscriptionId);
  1966. assertEq(
  1967. status.priceLastUpdatedAt,
  1968. validPublishTime,
  1969. "Last updated timestamp should be the latest valid one"
  1970. );
  1971. }
  1972. function testUpdatePriceFeedsRevertsIfLatestTimestampIsTooOld() public {
  1973. // Add a subscription and funds
  1974. uint256 subscriptionId = addTestSubscription(
  1975. scheduler,
  1976. address(reader)
  1977. );
  1978. // Advance time past the validity period
  1979. vm.warp(
  1980. block.timestamp +
  1981. scheduler.PAST_TIMESTAMP_MAX_VALIDITY_PERIOD() +
  1982. 600
  1983. ); // Warp 1 hour 10 mins
  1984. uint64 currentTime = SafeCast.toUint64(block.timestamp);
  1985. // Make the *latest* timestamp too old
  1986. uint64 stalePublishTime1 = currentTime -
  1987. (scheduler.PAST_TIMESTAMP_MAX_VALIDITY_PERIOD() + 300); // 1 hour 5 mins ago
  1988. uint64 stalePublishTime2 = currentTime -
  1989. (scheduler.PAST_TIMESTAMP_MAX_VALIDITY_PERIOD() + 600); // 1 hour 10 mins ago
  1990. PythStructs.PriceFeed[] memory priceFeeds = new PythStructs.PriceFeed[](
  1991. 2
  1992. );
  1993. priceFeeds[0] = createSingleMockPriceFeed(stalePublishTime2); // Oldest
  1994. priceFeeds[1] = createSingleMockPriceFeed(stalePublishTime1); // Latest, but still too old
  1995. uint64[] memory slots = new uint64[](2);
  1996. slots[0] = 100;
  1997. slots[1] = 100; // Same slot
  1998. // Mock Pyth response (should succeed in the real world as minValidTime is 0)
  1999. mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
  2000. bytes[] memory updateData = createMockUpdateData(priceFeeds);
  2001. // Expect revert with TimestampTooOld (checked in _validateShouldUpdatePrices)
  2002. vm.expectRevert(
  2003. abi.encodeWithSelector(
  2004. TimestampTooOld.selector,
  2005. stalePublishTime1, // The latest timestamp from the update
  2006. currentTime
  2007. )
  2008. );
  2009. // Attempt to update price feeds
  2010. vm.prank(pusher);
  2011. scheduler.updatePriceFeeds(subscriptionId, updateData);
  2012. }
  2013. // Required to receive ETH when withdrawing funds
  2014. receive() external payable {}
  2015. function testUpdateSubscriptionRemovesPriceUpdatesForRemovedPriceIds()
  2016. public
  2017. {
  2018. // 1. Setup: Add subscription with 3 price feeds, update prices
  2019. uint8 numInitialFeeds = 3;
  2020. uint256 subscriptionId = addTestSubscriptionWithFeeds(
  2021. scheduler,
  2022. numInitialFeeds,
  2023. address(reader)
  2024. );
  2025. scheduler.addFunds{value: 1 ether}(subscriptionId);
  2026. // Get initial price IDs and create mock price feeds
  2027. bytes32[] memory initialPriceIds = createPriceIds(numInitialFeeds);
  2028. uint64 publishTime = SafeCast.toUint64(block.timestamp);
  2029. // Setup and perform initial price update
  2030. (
  2031. PythStructs.PriceFeed[] memory priceFeeds,
  2032. uint64[] memory slots
  2033. ) = createMockPriceFeedsWithSlots(publishTime, numInitialFeeds);
  2034. mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
  2035. vm.prank(pusher);
  2036. scheduler.updatePriceFeeds(
  2037. subscriptionId,
  2038. createMockUpdateData(priceFeeds)
  2039. );
  2040. // Store the removed price ID for later use
  2041. bytes32 removedPriceId = initialPriceIds[numInitialFeeds - 1];
  2042. // 2. Action: Update subscription to remove the last price feed
  2043. (SchedulerState.SubscriptionParams memory params, ) = scheduler
  2044. .getSubscription(subscriptionId);
  2045. // Create new price IDs array without the last ID
  2046. bytes32[] memory newPriceIds = new bytes32[](numInitialFeeds - 1);
  2047. for (uint i = 0; i < newPriceIds.length; i++) {
  2048. newPriceIds[i] = initialPriceIds[i];
  2049. }
  2050. params.priceIds = newPriceIds;
  2051. vm.expectEmit();
  2052. emit SubscriptionUpdated(subscriptionId);
  2053. scheduler.updateSubscription(subscriptionId, params);
  2054. // 3. Verification:
  2055. // - Verify that the removed price ID is no longer part of the subscription's price IDs
  2056. (SchedulerState.SubscriptionParams memory updatedParams, ) = scheduler
  2057. .getSubscription(subscriptionId);
  2058. assertEq(
  2059. updatedParams.priceIds.length,
  2060. numInitialFeeds - 1,
  2061. "Subscription should have one less price ID"
  2062. );
  2063. bool removedPriceIdFound = false;
  2064. for (uint i = 0; i < updatedParams.priceIds.length; i++) {
  2065. if (updatedParams.priceIds[i] == removedPriceId) {
  2066. removedPriceIdFound = true;
  2067. break;
  2068. }
  2069. }
  2070. assertFalse(
  2071. removedPriceIdFound,
  2072. "Removed price ID should not be in the subscription's price IDs"
  2073. );
  2074. // - Querying all feeds should return only the remaining feeds
  2075. PythStructs.Price[] memory allPricesAfterUpdate = scheduler
  2076. .getPricesUnsafe(subscriptionId, new bytes32[](0));
  2077. assertEq(
  2078. allPricesAfterUpdate.length,
  2079. newPriceIds.length,
  2080. "Querying all should only return remaining feeds"
  2081. );
  2082. // - Verify that trying to get the price of the removed feed directly reverts
  2083. bytes32[] memory removedIdArray = new bytes32[](1);
  2084. removedIdArray[0] = removedPriceId;
  2085. vm.expectRevert(
  2086. abi.encodeWithSelector(
  2087. InvalidPriceId.selector,
  2088. removedPriceId,
  2089. bytes32(0)
  2090. )
  2091. );
  2092. scheduler.getPricesUnsafe(subscriptionId, removedIdArray);
  2093. }
  2094. function testUpdateSubscriptionRevertsWithTooManyPriceIds() public {
  2095. // 1. Setup: Create a subscription with a valid number of price IDs
  2096. uint8 initialNumFeeds = 2;
  2097. uint256 subscriptionId = addTestSubscriptionWithFeeds(
  2098. scheduler,
  2099. initialNumFeeds,
  2100. address(reader)
  2101. );
  2102. // 2. Prepare params with too many price IDs (MAX_PRICE_IDS_PER_SUBSCRIPTION + 1)
  2103. (SchedulerState.SubscriptionParams memory currentParams, ) = scheduler
  2104. .getSubscription(subscriptionId);
  2105. uint16 tooManyFeeds = uint16(
  2106. scheduler.MAX_PRICE_IDS_PER_SUBSCRIPTION()
  2107. ) + 1;
  2108. bytes32[] memory tooManyPriceIds = createPriceIds(tooManyFeeds);
  2109. SchedulerState.SubscriptionParams memory newParams = currentParams;
  2110. newParams.priceIds = tooManyPriceIds;
  2111. // 3. Expect revert when trying to update with too many price IDs
  2112. vm.expectRevert(
  2113. abi.encodeWithSelector(
  2114. TooManyPriceIds.selector,
  2115. tooManyFeeds,
  2116. scheduler.MAX_PRICE_IDS_PER_SUBSCRIPTION()
  2117. )
  2118. );
  2119. scheduler.updateSubscription(subscriptionId, newParams);
  2120. }
  2121. }