PulseScheduler.t.sol 95 KB

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