Echo.t.sol 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191
  1. // SPDX-License-Identifier: Apache 2
  2. pragma solidity ^0.8.0;
  3. import "forge-std/Test.sol";
  4. import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
  5. import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
  6. import "../contracts/echo/EchoUpgradeable.sol";
  7. import "../contracts/echo/IEcho.sol";
  8. import "../contracts/echo/EchoState.sol";
  9. import "../contracts/echo/EchoEvents.sol";
  10. import "../contracts/echo/EchoErrors.sol";
  11. import "./utils/EchoTestUtils.sol";
  12. contract MockEchoConsumer is IEchoConsumer {
  13. address private _echo;
  14. uint64 public lastSequenceNumber;
  15. PythStructs.PriceFeed[] private _lastPriceFeeds;
  16. constructor(address echo) {
  17. _echo = echo;
  18. }
  19. function getEcho() internal view override returns (address) {
  20. return _echo;
  21. }
  22. function echoCallback(
  23. uint64 sequenceNumber,
  24. PythStructs.PriceFeed[] memory priceFeeds
  25. ) internal override {
  26. lastSequenceNumber = sequenceNumber;
  27. for (uint i = 0; i < priceFeeds.length; i++) {
  28. _lastPriceFeeds.push(priceFeeds[i]);
  29. }
  30. }
  31. function lastPriceFeeds()
  32. external
  33. view
  34. returns (PythStructs.PriceFeed[] memory)
  35. {
  36. return _lastPriceFeeds;
  37. }
  38. }
  39. contract FailingEchoConsumer is IEchoConsumer {
  40. address private _echo;
  41. constructor(address echo) {
  42. _echo = echo;
  43. }
  44. function getEcho() internal view override returns (address) {
  45. return _echo;
  46. }
  47. function echoCallback(
  48. uint64,
  49. PythStructs.PriceFeed[] memory
  50. ) internal pure override {
  51. revert("callback failed");
  52. }
  53. }
  54. contract CustomErrorEchoConsumer is IEchoConsumer {
  55. error CustomError(string message);
  56. address private _echo;
  57. constructor(address echo) {
  58. _echo = echo;
  59. }
  60. function getEcho() internal view override returns (address) {
  61. return _echo;
  62. }
  63. function echoCallback(
  64. uint64,
  65. PythStructs.PriceFeed[] memory
  66. ) internal pure override {
  67. revert CustomError("callback failed");
  68. }
  69. }
  70. // FIXME: this shouldn't be IPulseConsumer.
  71. contract EchoTest is Test, EchoEvents, IEchoConsumer, EchoTestUtils {
  72. ERC1967Proxy public proxy;
  73. EchoUpgradeable public echo;
  74. MockEchoConsumer public consumer;
  75. address public owner;
  76. address public admin;
  77. address public pyth;
  78. address public defaultProvider;
  79. // Constants
  80. uint96 constant PYTH_FEE = 1 wei;
  81. uint96 constant DEFAULT_PROVIDER_FEE_PER_GAS = 1 wei;
  82. uint96 constant DEFAULT_PROVIDER_BASE_FEE = 1 wei;
  83. uint96 constant DEFAULT_PROVIDER_FEE_PER_FEED = 10 wei;
  84. function setUp() public {
  85. owner = address(1);
  86. admin = address(2);
  87. pyth = address(3);
  88. defaultProvider = address(4);
  89. EchoUpgradeable _echo = new EchoUpgradeable();
  90. proxy = new ERC1967Proxy(address(_echo), "");
  91. echo = EchoUpgradeable(address(proxy));
  92. echo.initialize(
  93. owner,
  94. admin,
  95. PYTH_FEE,
  96. pyth,
  97. defaultProvider,
  98. false,
  99. 15
  100. );
  101. vm.prank(defaultProvider);
  102. echo.registerProvider(
  103. DEFAULT_PROVIDER_BASE_FEE,
  104. DEFAULT_PROVIDER_FEE_PER_FEED,
  105. DEFAULT_PROVIDER_FEE_PER_GAS
  106. );
  107. consumer = new MockEchoConsumer(address(proxy));
  108. }
  109. // Helper function to calculate total fee
  110. // FIXME: I think this helper probably needs to take some arguments.
  111. function calculateTotalFee() internal view returns (uint96) {
  112. return
  113. echo.getFee(defaultProvider, CALLBACK_GAS_LIMIT, createPriceIds());
  114. }
  115. function testRequestPriceUpdate() public {
  116. // Set a realistic gas price
  117. vm.txGasPrice(30 gwei);
  118. bytes32[] memory priceIds = createPriceIds();
  119. uint64 publishTime = SafeCast.toUint64(block.timestamp);
  120. // Fund the consumer contract with enough ETH for higher gas price
  121. vm.deal(address(consumer), 1 ether);
  122. uint96 totalFee = calculateTotalFee();
  123. // Create the event data we expect to see
  124. bytes8[] memory expectedPriceIdPrefixes = new bytes8[](2);
  125. {
  126. bytes32 priceId0 = priceIds[0];
  127. bytes32 priceId1 = priceIds[1];
  128. bytes8 prefix0;
  129. bytes8 prefix1;
  130. assembly {
  131. prefix0 := priceId0
  132. prefix1 := priceId1
  133. }
  134. expectedPriceIdPrefixes[0] = prefix0;
  135. expectedPriceIdPrefixes[1] = prefix1;
  136. }
  137. EchoState.Request memory expectedRequest = EchoState.Request({
  138. sequenceNumber: 1,
  139. publishTime: publishTime,
  140. priceIdPrefixes: expectedPriceIdPrefixes,
  141. callbackGasLimit: uint32(CALLBACK_GAS_LIMIT),
  142. requester: address(consumer),
  143. provider: defaultProvider,
  144. fee: totalFee - PYTH_FEE
  145. });
  146. vm.expectEmit();
  147. emit PriceUpdateRequested(expectedRequest, priceIds);
  148. vm.prank(address(consumer));
  149. echo.requestPriceUpdatesWithCallback{value: totalFee}(
  150. defaultProvider,
  151. publishTime,
  152. priceIds,
  153. CALLBACK_GAS_LIMIT
  154. );
  155. // Additional assertions to verify event data was stored correctly
  156. EchoState.Request memory lastRequest = echo.getRequest(1);
  157. assertEq(lastRequest.sequenceNumber, expectedRequest.sequenceNumber);
  158. assertEq(lastRequest.publishTime, expectedRequest.publishTime);
  159. assertEq(
  160. lastRequest.priceIdPrefixes.length,
  161. expectedRequest.priceIdPrefixes.length
  162. );
  163. for (uint8 i = 0; i < lastRequest.priceIdPrefixes.length; i++) {
  164. assertEq(
  165. lastRequest.priceIdPrefixes[i],
  166. expectedRequest.priceIdPrefixes[i]
  167. );
  168. }
  169. assertEq(
  170. lastRequest.callbackGasLimit,
  171. expectedRequest.callbackGasLimit
  172. );
  173. assertEq(
  174. lastRequest.requester,
  175. expectedRequest.requester,
  176. "Requester mismatch"
  177. );
  178. }
  179. function testRequestWithInsufficientFee() public {
  180. // Set a realistic gas price
  181. vm.txGasPrice(30 gwei);
  182. bytes32[] memory priceIds = createPriceIds();
  183. vm.deal(address(consumer), 1 ether);
  184. vm.prank(address(consumer));
  185. vm.expectRevert(InsufficientFee.selector);
  186. echo.requestPriceUpdatesWithCallback{value: PYTH_FEE}( // Intentionally low fee
  187. defaultProvider,
  188. SafeCast.toUint64(block.timestamp),
  189. priceIds,
  190. CALLBACK_GAS_LIMIT
  191. );
  192. }
  193. function testExecuteCallback() public {
  194. bytes32[] memory priceIds = createPriceIds();
  195. uint64 publishTime = SafeCast.toUint64(block.timestamp);
  196. // Fund the consumer contract
  197. vm.deal(address(consumer), 1 gwei);
  198. uint96 totalFee = calculateTotalFee();
  199. // Step 1: Make the request as consumer
  200. vm.prank(address(consumer));
  201. uint64 sequenceNumber = echo.requestPriceUpdatesWithCallback{
  202. value: totalFee
  203. }(defaultProvider, publishTime, priceIds, CALLBACK_GAS_LIMIT);
  204. // Step 2: Create mock price feeds and setup Pyth response
  205. PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
  206. publishTime
  207. );
  208. // FIXME: this test doesn't ensure the Pyth fee is paid.
  209. mockParsePriceFeedUpdates(pyth, priceFeeds);
  210. // Create arrays for expected event data
  211. int64[] memory expectedPrices = new int64[](2);
  212. expectedPrices[0] = MOCK_BTC_PRICE;
  213. expectedPrices[1] = MOCK_ETH_PRICE;
  214. uint64[] memory expectedConf = new uint64[](2);
  215. expectedConf[0] = MOCK_BTC_CONF;
  216. expectedConf[1] = MOCK_ETH_CONF;
  217. int32[] memory expectedExpos = new int32[](2);
  218. expectedExpos[0] = MOCK_PRICE_FEED_EXPO;
  219. expectedExpos[1] = MOCK_PRICE_FEED_EXPO;
  220. uint64[] memory expectedPublishTimes = new uint64[](2);
  221. expectedPublishTimes[0] = publishTime;
  222. expectedPublishTimes[1] = publishTime;
  223. // Expect the PriceUpdateExecuted event with all price data
  224. vm.expectEmit();
  225. emit PriceUpdateExecuted(
  226. sequenceNumber,
  227. defaultProvider,
  228. priceIds,
  229. expectedPrices,
  230. expectedConf,
  231. expectedExpos,
  232. expectedPublishTimes
  233. );
  234. // Create mock update data and execute callback
  235. bytes[] memory updateData = createMockUpdateData(priceFeeds);
  236. vm.prank(defaultProvider);
  237. echo.executeCallback(
  238. defaultProvider,
  239. sequenceNumber,
  240. updateData,
  241. priceIds
  242. );
  243. // Verify callback was executed
  244. assertEq(consumer.lastSequenceNumber(), sequenceNumber);
  245. // Compare price feeds array length
  246. PythStructs.PriceFeed[] memory lastFeeds = consumer.lastPriceFeeds();
  247. assertEq(lastFeeds.length, priceFeeds.length);
  248. // Compare each price feed
  249. for (uint i = 0; i < priceFeeds.length; i++) {
  250. assertEq(lastFeeds[i].id, priceFeeds[i].id);
  251. assertEq(lastFeeds[i].price.price, priceFeeds[i].price.price);
  252. assertEq(lastFeeds[i].price.conf, priceFeeds[i].price.conf);
  253. assertEq(lastFeeds[i].price.expo, priceFeeds[i].price.expo);
  254. assertEq(
  255. lastFeeds[i].price.publishTime,
  256. priceFeeds[i].price.publishTime
  257. );
  258. }
  259. }
  260. function testExecuteCallbackFailure() public {
  261. FailingEchoConsumer failingConsumer = new FailingEchoConsumer(
  262. address(proxy)
  263. );
  264. (
  265. uint64 sequenceNumber,
  266. bytes32[] memory priceIds,
  267. uint256 publishTime
  268. ) = setupConsumerRequest(
  269. echo,
  270. defaultProvider,
  271. address(failingConsumer)
  272. );
  273. PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
  274. publishTime
  275. );
  276. mockParsePriceFeedUpdates(pyth, priceFeeds);
  277. bytes[] memory updateData = createMockUpdateData(priceFeeds);
  278. vm.expectEmit();
  279. emit PriceUpdateCallbackFailed(
  280. sequenceNumber,
  281. defaultProvider,
  282. priceIds,
  283. address(failingConsumer),
  284. "callback failed"
  285. );
  286. vm.prank(defaultProvider);
  287. echo.executeCallback(
  288. defaultProvider,
  289. sequenceNumber,
  290. updateData,
  291. priceIds
  292. );
  293. }
  294. function testExecuteCallbackCustomErrorFailure() public {
  295. CustomErrorEchoConsumer failingConsumer = new CustomErrorEchoConsumer(
  296. address(proxy)
  297. );
  298. (
  299. uint64 sequenceNumber,
  300. bytes32[] memory priceIds,
  301. uint256 publishTime
  302. ) = setupConsumerRequest(
  303. echo,
  304. defaultProvider,
  305. address(failingConsumer)
  306. );
  307. PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
  308. publishTime
  309. );
  310. mockParsePriceFeedUpdates(pyth, priceFeeds);
  311. bytes[] memory updateData = createMockUpdateData(priceFeeds);
  312. vm.expectEmit();
  313. emit PriceUpdateCallbackFailed(
  314. sequenceNumber,
  315. defaultProvider,
  316. priceIds,
  317. address(failingConsumer),
  318. "low-level error (possibly out of gas)"
  319. );
  320. vm.prank(defaultProvider);
  321. echo.executeCallback(
  322. defaultProvider,
  323. sequenceNumber,
  324. updateData,
  325. priceIds
  326. );
  327. }
  328. function testExecuteCallbackWithInsufficientGas() public {
  329. // Setup request with 1M gas limit
  330. (
  331. uint64 sequenceNumber,
  332. bytes32[] memory priceIds,
  333. uint256 publishTime
  334. ) = setupConsumerRequest(echo, defaultProvider, address(consumer));
  335. // Setup mock data
  336. PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
  337. publishTime
  338. );
  339. mockParsePriceFeedUpdates(pyth, priceFeeds);
  340. bytes[] memory updateData = createMockUpdateData(priceFeeds);
  341. // Try executing with only 100K gas when 1M is required
  342. vm.prank(defaultProvider);
  343. vm.expectRevert(); // Just expect any revert since it will be an out-of-gas error
  344. echo.executeCallback{gas: 100000}(
  345. defaultProvider,
  346. sequenceNumber,
  347. updateData,
  348. priceIds
  349. ); // Will fail because gasleft() < callbackGasLimit
  350. }
  351. function testExecuteCallbackWithFutureTimestamp() public {
  352. // Setup request with future timestamp
  353. bytes32[] memory priceIds = createPriceIds();
  354. uint64 futureTime = SafeCast.toUint64(block.timestamp + 10); // 10 seconds in future
  355. vm.deal(address(consumer), 1 gwei);
  356. uint96 totalFee = calculateTotalFee();
  357. vm.prank(address(consumer));
  358. uint64 sequenceNumber = echo.requestPriceUpdatesWithCallback{
  359. value: totalFee
  360. }(defaultProvider, futureTime, priceIds, CALLBACK_GAS_LIMIT);
  361. // Try to execute callback before the requested timestamp
  362. PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
  363. futureTime // Mock price feeds with future timestamp
  364. );
  365. mockParsePriceFeedUpdates(pyth, priceFeeds); // This will make parsePriceFeedUpdates return future-dated prices
  366. bytes[] memory updateData = createMockUpdateData(priceFeeds);
  367. vm.prank(defaultProvider);
  368. // Should succeed because we're simulating receiving future-dated price updates
  369. echo.executeCallback(
  370. defaultProvider,
  371. sequenceNumber,
  372. updateData,
  373. priceIds
  374. );
  375. // Compare price feeds array length
  376. PythStructs.PriceFeed[] memory lastFeeds = consumer.lastPriceFeeds();
  377. assertEq(lastFeeds.length, priceFeeds.length);
  378. // Compare each price feed publish time
  379. for (uint i = 0; i < priceFeeds.length; i++) {
  380. assertEq(
  381. lastFeeds[i].price.publishTime,
  382. priceFeeds[i].price.publishTime
  383. );
  384. }
  385. }
  386. function testRevertOnTooFarFutureTimestamp() public {
  387. bytes32[] memory priceIds = createPriceIds();
  388. uint64 farFutureTime = SafeCast.toUint64(block.timestamp + 61); // Just over 1 minute
  389. vm.deal(address(consumer), 1 gwei);
  390. uint96 totalFee = calculateTotalFee();
  391. vm.prank(address(consumer));
  392. vm.expectRevert("Too far in future");
  393. echo.requestPriceUpdatesWithCallback{value: totalFee}(
  394. defaultProvider,
  395. farFutureTime,
  396. priceIds,
  397. CALLBACK_GAS_LIMIT
  398. );
  399. }
  400. function testDoubleExecuteCallback() public {
  401. (
  402. uint64 sequenceNumber,
  403. bytes32[] memory priceIds,
  404. uint256 publishTime
  405. ) = setupConsumerRequest(echo, defaultProvider, address(consumer));
  406. PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
  407. publishTime
  408. );
  409. mockParsePriceFeedUpdates(pyth, priceFeeds);
  410. bytes[] memory updateData = createMockUpdateData(priceFeeds);
  411. // First execution
  412. vm.prank(defaultProvider);
  413. echo.executeCallback(
  414. defaultProvider,
  415. sequenceNumber,
  416. updateData,
  417. priceIds
  418. );
  419. // Second execution should fail
  420. vm.prank(defaultProvider);
  421. vm.expectRevert(NoSuchRequest.selector);
  422. echo.executeCallback(
  423. defaultProvider,
  424. sequenceNumber,
  425. updateData,
  426. priceIds
  427. );
  428. }
  429. function testGetFee() public {
  430. // Test with different gas limits to verify fee calculation
  431. uint32[] memory gasLimits = new uint32[](3);
  432. gasLimits[0] = 100_000;
  433. gasLimits[1] = 500_000;
  434. gasLimits[2] = 1_000_000;
  435. bytes32[] memory priceIds = createPriceIds();
  436. for (uint256 i = 0; i < gasLimits.length; i++) {
  437. uint32 gasLimit = gasLimits[i];
  438. uint96 expectedFee = SafeCast.toUint96(
  439. DEFAULT_PROVIDER_BASE_FEE +
  440. DEFAULT_PROVIDER_FEE_PER_FEED *
  441. priceIds.length +
  442. DEFAULT_PROVIDER_FEE_PER_GAS *
  443. gasLimit
  444. ) + PYTH_FEE;
  445. uint96 actualFee = echo.getFee(defaultProvider, gasLimit, priceIds);
  446. assertEq(
  447. actualFee,
  448. expectedFee,
  449. "Fee calculation incorrect for gas limit"
  450. );
  451. }
  452. // Test with zero gas limit
  453. uint96 expectedMinFee = SafeCast.toUint96(
  454. PYTH_FEE +
  455. DEFAULT_PROVIDER_BASE_FEE +
  456. DEFAULT_PROVIDER_FEE_PER_FEED *
  457. priceIds.length
  458. );
  459. uint96 actualMinFee = echo.getFee(defaultProvider, 0, priceIds);
  460. assertEq(
  461. actualMinFee,
  462. expectedMinFee,
  463. "Minimum fee calculation incorrect"
  464. );
  465. }
  466. function testWithdrawFees() public {
  467. // Setup: Request price update to accrue some fees
  468. bytes32[] memory priceIds = createPriceIds();
  469. vm.deal(address(consumer), 1 gwei);
  470. vm.prank(address(consumer));
  471. echo.requestPriceUpdatesWithCallback{value: calculateTotalFee()}(
  472. defaultProvider,
  473. SafeCast.toUint64(block.timestamp),
  474. priceIds,
  475. CALLBACK_GAS_LIMIT
  476. );
  477. // Get admin's balance before withdrawal
  478. uint256 adminBalanceBefore = admin.balance;
  479. uint128 accruedFees = echo.getAccruedPythFees();
  480. // Withdraw fees as admin
  481. vm.prank(admin);
  482. echo.withdrawFees(accruedFees);
  483. // Verify balances
  484. assertEq(
  485. admin.balance,
  486. adminBalanceBefore + accruedFees,
  487. "Admin balance should increase by withdrawn amount"
  488. );
  489. assertEq(
  490. echo.getAccruedPythFees(),
  491. 0,
  492. "Contract should have no fees after withdrawal"
  493. );
  494. }
  495. function testWithdrawFeesUnauthorized() public {
  496. vm.prank(address(0xdead));
  497. vm.expectRevert("Only admin can withdraw fees");
  498. echo.withdrawFees(1 ether);
  499. }
  500. function testWithdrawFeesInsufficientBalance() public {
  501. vm.prank(admin);
  502. vm.expectRevert("Insufficient balance");
  503. echo.withdrawFees(1 ether);
  504. }
  505. function testSetAndWithdrawAsFeeManager() public {
  506. address feeManager = address(0x789);
  507. vm.prank(defaultProvider);
  508. echo.setFeeManager(feeManager);
  509. // Setup: Request price update to accrue some fees
  510. bytes32[] memory priceIds = createPriceIds();
  511. vm.deal(address(consumer), 1 gwei);
  512. vm.prank(address(consumer));
  513. echo.requestPriceUpdatesWithCallback{value: calculateTotalFee()}(
  514. defaultProvider,
  515. SafeCast.toUint64(block.timestamp),
  516. priceIds,
  517. CALLBACK_GAS_LIMIT
  518. );
  519. // Get provider's accrued fees instead of total fees
  520. EchoState.ProviderInfo memory providerInfo = echo.getProviderInfo(
  521. defaultProvider
  522. );
  523. uint128 providerAccruedFees = providerInfo.accruedFeesInWei;
  524. uint256 managerBalanceBefore = feeManager.balance;
  525. vm.prank(feeManager);
  526. echo.withdrawAsFeeManager(defaultProvider, uint96(providerAccruedFees));
  527. assertEq(
  528. feeManager.balance,
  529. managerBalanceBefore + providerAccruedFees,
  530. "Fee manager balance should increase by withdrawn amount"
  531. );
  532. providerInfo = echo.getProviderInfo(defaultProvider);
  533. assertEq(
  534. providerInfo.accruedFeesInWei,
  535. 0,
  536. "Provider should have no fees after withdrawal"
  537. );
  538. }
  539. function testSetFeeManagerUnauthorized() public {
  540. address feeManager = address(0x789);
  541. vm.prank(address(0xdead));
  542. vm.expectRevert("Provider not registered");
  543. echo.setFeeManager(feeManager);
  544. }
  545. function testWithdrawAsFeeManagerUnauthorized() public {
  546. vm.prank(address(0xdead));
  547. vm.expectRevert("Only fee manager");
  548. echo.withdrawAsFeeManager(defaultProvider, 1 ether);
  549. }
  550. function testWithdrawAsFeeManagerInsufficientBalance() public {
  551. // Set up fee manager first
  552. address feeManager = address(0x789);
  553. vm.prank(defaultProvider);
  554. echo.setFeeManager(feeManager);
  555. vm.prank(feeManager);
  556. vm.expectRevert("Insufficient balance");
  557. echo.withdrawAsFeeManager(defaultProvider, 1 ether);
  558. }
  559. // Add new test for invalid priceIds
  560. function testExecuteCallbackWithInvalidPriceIds() public {
  561. bytes32[] memory priceIds = createPriceIds();
  562. uint256 publishTime = block.timestamp;
  563. // Setup request
  564. (uint64 sequenceNumber, , ) = setupConsumerRequest(
  565. echo,
  566. defaultProvider,
  567. address(consumer)
  568. );
  569. // Create different priceIds
  570. bytes32[] memory wrongPriceIds = new bytes32[](2);
  571. wrongPriceIds[0] = bytes32(uint256(1)); // Different price IDs
  572. wrongPriceIds[1] = bytes32(uint256(2));
  573. PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
  574. publishTime
  575. );
  576. mockParsePriceFeedUpdates(pyth, priceFeeds);
  577. bytes[] memory updateData = createMockUpdateData(priceFeeds);
  578. // Should revert when trying to execute with wrong priceIds
  579. vm.prank(defaultProvider);
  580. // Extract first 8 bytes of the price ID for the error expectation
  581. bytes8 storedPriceIdPrefix;
  582. assembly {
  583. storedPriceIdPrefix := mload(add(priceIds, 32))
  584. }
  585. vm.expectRevert(
  586. abi.encodeWithSelector(
  587. InvalidPriceIds.selector,
  588. wrongPriceIds[0],
  589. storedPriceIdPrefix
  590. )
  591. );
  592. echo.executeCallback(
  593. defaultProvider,
  594. sequenceNumber,
  595. updateData,
  596. wrongPriceIds
  597. );
  598. }
  599. function testRevertOnTooManyPriceIds() public {
  600. uint256 maxPriceIds = uint256(echo.MAX_PRICE_IDS());
  601. // Create array with MAX_PRICE_IDS + 1 price IDs
  602. bytes32[] memory priceIds = new bytes32[](maxPriceIds + 1);
  603. for (uint i = 0; i < priceIds.length; i++) {
  604. priceIds[i] = bytes32(uint256(i + 1));
  605. }
  606. vm.deal(address(consumer), 1 gwei);
  607. uint96 totalFee = calculateTotalFee();
  608. vm.prank(address(consumer));
  609. vm.expectRevert(
  610. abi.encodeWithSelector(
  611. TooManyPriceIds.selector,
  612. maxPriceIds + 1,
  613. maxPriceIds
  614. )
  615. );
  616. echo.requestPriceUpdatesWithCallback{value: totalFee}(
  617. defaultProvider,
  618. SafeCast.toUint64(block.timestamp),
  619. priceIds,
  620. CALLBACK_GAS_LIMIT
  621. );
  622. }
  623. function testProviderRegistration() public {
  624. address provider = address(0x123);
  625. uint96 providerFee = 1000;
  626. vm.prank(provider);
  627. echo.registerProvider(providerFee, providerFee, providerFee);
  628. EchoState.ProviderInfo memory info = echo.getProviderInfo(provider);
  629. assertEq(info.feePerGasInWei, providerFee);
  630. assertTrue(info.isRegistered);
  631. }
  632. function testSetProviderFee() public {
  633. address provider = address(0x123);
  634. uint96 initialBaseFee = 1000;
  635. uint96 initialFeePerFeed = 2000;
  636. uint96 initialFeePerGas = 3000;
  637. uint96 newFeePerFeed = 4000;
  638. uint96 newBaseFee = 5000;
  639. uint96 newFeePerGas = 6000;
  640. vm.prank(provider);
  641. echo.registerProvider(
  642. initialBaseFee,
  643. initialFeePerFeed,
  644. initialFeePerGas
  645. );
  646. vm.prank(provider);
  647. echo.setProviderFee(provider, newBaseFee, newFeePerFeed, newFeePerGas);
  648. EchoState.ProviderInfo memory info = echo.getProviderInfo(provider);
  649. assertEq(info.baseFeeInWei, newBaseFee);
  650. assertEq(info.feePerFeedInWei, newFeePerFeed);
  651. assertEq(info.feePerGasInWei, newFeePerGas);
  652. }
  653. function testDefaultProvider() public {
  654. address provider = address(0x123);
  655. uint96 providerFee = 1000;
  656. vm.prank(provider);
  657. echo.registerProvider(providerFee, providerFee, providerFee);
  658. vm.prank(admin);
  659. echo.setDefaultProvider(provider);
  660. assertEq(echo.getDefaultProvider(), provider);
  661. }
  662. function testRequestWithProvider() public {
  663. address provider = address(0x123);
  664. uint96 providerFee = 1000;
  665. vm.prank(provider);
  666. echo.registerProvider(providerFee, providerFee, providerFee);
  667. bytes32[] memory priceIds = new bytes32[](1);
  668. priceIds[0] = bytes32(uint256(1));
  669. uint128 totalFee = echo.getFee(provider, CALLBACK_GAS_LIMIT, priceIds);
  670. vm.deal(address(consumer), totalFee);
  671. vm.prank(address(consumer));
  672. uint64 sequenceNumber = echo.requestPriceUpdatesWithCallback{
  673. value: totalFee
  674. }(
  675. provider,
  676. SafeCast.toUint64(block.timestamp),
  677. priceIds,
  678. CALLBACK_GAS_LIMIT
  679. );
  680. EchoState.Request memory req = echo.getRequest(sequenceNumber);
  681. assertEq(req.provider, provider);
  682. }
  683. function testExclusivityPeriod() public {
  684. // Test initial value
  685. assertEq(
  686. echo.getExclusivityPeriod(),
  687. 15,
  688. "Initial exclusivity period should be 15 seconds"
  689. );
  690. // Test setting new value
  691. vm.prank(admin);
  692. vm.expectEmit();
  693. emit ExclusivityPeriodUpdated(15, 30);
  694. echo.setExclusivityPeriod(30);
  695. assertEq(
  696. echo.getExclusivityPeriod(),
  697. 30,
  698. "Exclusivity period should be updated"
  699. );
  700. }
  701. function testSetExclusivityPeriodUnauthorized() public {
  702. vm.prank(address(0xdead));
  703. vm.expectRevert("Only admin can set exclusivity period");
  704. echo.setExclusivityPeriod(30);
  705. }
  706. function testExecuteCallbackDuringExclusivity() public {
  707. // Register a second provider
  708. address secondProvider = address(0x456);
  709. vm.prank(secondProvider);
  710. echo.registerProvider(
  711. DEFAULT_PROVIDER_BASE_FEE,
  712. DEFAULT_PROVIDER_FEE_PER_FEED,
  713. DEFAULT_PROVIDER_FEE_PER_GAS
  714. );
  715. // Setup request
  716. (
  717. uint64 sequenceNumber,
  718. bytes32[] memory priceIds,
  719. uint256 publishTime
  720. ) = setupConsumerRequest(echo, defaultProvider, address(consumer));
  721. // Setup mock data
  722. PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
  723. publishTime
  724. );
  725. mockParsePriceFeedUpdates(pyth, priceFeeds);
  726. bytes[] memory updateData = createMockUpdateData(priceFeeds);
  727. // Try to execute with second provider during exclusivity period
  728. vm.expectRevert("Only assigned provider during exclusivity period");
  729. echo.executeCallback(
  730. secondProvider,
  731. sequenceNumber,
  732. updateData,
  733. priceIds
  734. );
  735. // Original provider should succeed
  736. echo.executeCallback(
  737. defaultProvider,
  738. sequenceNumber,
  739. updateData,
  740. priceIds
  741. );
  742. }
  743. function testExecuteCallbackAfterExclusivity() public {
  744. // Register a second provider
  745. address secondProvider = address(0x456);
  746. vm.prank(secondProvider);
  747. echo.registerProvider(
  748. DEFAULT_PROVIDER_BASE_FEE,
  749. DEFAULT_PROVIDER_FEE_PER_FEED,
  750. DEFAULT_PROVIDER_FEE_PER_GAS
  751. );
  752. // Setup request
  753. (
  754. uint64 sequenceNumber,
  755. bytes32[] memory priceIds,
  756. uint256 publishTime
  757. ) = setupConsumerRequest(echo, defaultProvider, address(consumer));
  758. // Setup mock data
  759. PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
  760. publishTime
  761. );
  762. mockParsePriceFeedUpdates(pyth, priceFeeds);
  763. bytes[] memory updateData = createMockUpdateData(priceFeeds);
  764. // Wait for exclusivity period to end
  765. vm.warp(block.timestamp + echo.getExclusivityPeriod() + 1);
  766. // Second provider should now succeed
  767. vm.prank(secondProvider);
  768. echo.executeCallback(
  769. defaultProvider,
  770. sequenceNumber,
  771. updateData,
  772. priceIds
  773. );
  774. }
  775. function testExecuteCallbackWithCustomExclusivityPeriod() public {
  776. // Register a second provider
  777. address secondProvider = address(0x456);
  778. vm.prank(secondProvider);
  779. echo.registerProvider(
  780. DEFAULT_PROVIDER_BASE_FEE,
  781. DEFAULT_PROVIDER_FEE_PER_FEED,
  782. DEFAULT_PROVIDER_FEE_PER_GAS
  783. );
  784. // Set custom exclusivity period
  785. vm.prank(admin);
  786. echo.setExclusivityPeriod(30);
  787. // Setup request
  788. (
  789. uint64 sequenceNumber,
  790. bytes32[] memory priceIds,
  791. uint256 publishTime
  792. ) = setupConsumerRequest(echo, defaultProvider, address(consumer));
  793. // Setup mock data
  794. PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
  795. publishTime
  796. );
  797. mockParsePriceFeedUpdates(pyth, priceFeeds);
  798. bytes[] memory updateData = createMockUpdateData(priceFeeds);
  799. // Try at 29 seconds (should fail for second provider)
  800. vm.warp(block.timestamp + 29);
  801. vm.expectRevert("Only assigned provider during exclusivity period");
  802. echo.executeCallback(
  803. secondProvider,
  804. sequenceNumber,
  805. updateData,
  806. priceIds
  807. );
  808. // Try at 31 seconds (should succeed for second provider)
  809. vm.warp(block.timestamp + 2);
  810. echo.executeCallback(
  811. secondProvider,
  812. sequenceNumber,
  813. updateData,
  814. priceIds
  815. );
  816. }
  817. function testGetFirstActiveRequests() public {
  818. // Setup test data
  819. (
  820. bytes32[] memory priceIds,
  821. bytes[] memory updateData
  822. ) = setupTestData();
  823. createTestRequests(priceIds);
  824. completeRequests(updateData, priceIds);
  825. testRequestScenarios(priceIds, updateData);
  826. }
  827. function setupTestData()
  828. private
  829. pure
  830. returns (bytes32[] memory, bytes[] memory)
  831. {
  832. bytes32[] memory priceIds = new bytes32[](1);
  833. priceIds[0] = bytes32(uint256(1));
  834. bytes[] memory updateData = new bytes[](1);
  835. return (priceIds, updateData);
  836. }
  837. function createTestRequests(bytes32[] memory priceIds) private {
  838. uint64 publishTime = SafeCast.toUint64(block.timestamp);
  839. for (uint i = 0; i < 5; i++) {
  840. vm.deal(address(this), 1 ether);
  841. echo.requestPriceUpdatesWithCallback{value: 1 ether}(
  842. defaultProvider,
  843. publishTime,
  844. priceIds,
  845. 1000000
  846. );
  847. }
  848. }
  849. function completeRequests(
  850. bytes[] memory updateData,
  851. bytes32[] memory priceIds
  852. ) private {
  853. // Create mock price feeds and setup Pyth response
  854. PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
  855. SafeCast.toUint64(block.timestamp)
  856. );
  857. mockParsePriceFeedUpdates(pyth, priceFeeds);
  858. updateData = createMockUpdateData(priceFeeds);
  859. vm.deal(defaultProvider, 2 ether); // Increase ETH allocation to prevent OutOfFunds
  860. vm.startPrank(defaultProvider);
  861. echo.executeCallback{value: 1 ether}(
  862. defaultProvider,
  863. 2,
  864. updateData,
  865. priceIds
  866. );
  867. echo.executeCallback{value: 1 ether}(
  868. defaultProvider,
  869. 4,
  870. updateData,
  871. priceIds
  872. );
  873. vm.stopPrank();
  874. }
  875. function testRequestScenarios(
  876. bytes32[] memory priceIds,
  877. bytes[] memory updateData
  878. ) private {
  879. // Test 1: Request more than available
  880. checkMoreThanAvailable();
  881. // Test 2: Request exact number
  882. checkExactNumber();
  883. // Test 3: Request fewer than available
  884. checkFewerThanAvailable();
  885. // Test 4: Request zero
  886. checkZeroRequest();
  887. // Test 5: Clear all and check empty
  888. clearAllRequests(updateData, priceIds);
  889. checkEmptyState();
  890. }
  891. // Split test scenarios into separate functions
  892. function checkMoreThanAvailable() private {
  893. (EchoState.Request[] memory requests, uint256 count) = echo
  894. .getFirstActiveRequests(10);
  895. assertEq(count, 3, "Should find 3 active requests");
  896. assertEq(requests.length, 3, "Array should be resized to 3");
  897. assertEq(
  898. requests[0].sequenceNumber,
  899. 1,
  900. "First request should be oldest"
  901. );
  902. assertEq(requests[1].sequenceNumber, 3, "Second request should be #3");
  903. assertEq(requests[2].sequenceNumber, 5, "Third request should be #5");
  904. }
  905. function checkExactNumber() private {
  906. (EchoState.Request[] memory requests, uint256 count) = echo
  907. .getFirstActiveRequests(3);
  908. assertEq(count, 3, "Should find 3 active requests");
  909. assertEq(requests.length, 3, "Array should match requested size");
  910. }
  911. function checkFewerThanAvailable() private {
  912. (EchoState.Request[] memory requests, uint256 count) = echo
  913. .getFirstActiveRequests(2);
  914. assertEq(count, 2, "Should find 2 active requests");
  915. assertEq(requests.length, 2, "Array should match requested size");
  916. assertEq(
  917. requests[0].sequenceNumber,
  918. 1,
  919. "First request should be oldest"
  920. );
  921. assertEq(requests[1].sequenceNumber, 3, "Second request should be #3");
  922. }
  923. function checkZeroRequest() private {
  924. (EchoState.Request[] memory requests, uint256 count) = echo
  925. .getFirstActiveRequests(0);
  926. assertEq(count, 0, "Should find 0 active requests");
  927. assertEq(requests.length, 0, "Array should be empty");
  928. }
  929. function clearAllRequests(
  930. bytes[] memory updateData,
  931. bytes32[] memory priceIds
  932. ) private {
  933. vm.deal(defaultProvider, 3 ether); // Increase ETH allocation
  934. vm.startPrank(defaultProvider);
  935. echo.executeCallback{value: 1 ether}(
  936. defaultProvider,
  937. 1,
  938. updateData,
  939. priceIds
  940. );
  941. echo.executeCallback{value: 1 ether}(
  942. defaultProvider,
  943. 3,
  944. updateData,
  945. priceIds
  946. );
  947. echo.executeCallback{value: 1 ether}(
  948. defaultProvider,
  949. 5,
  950. updateData,
  951. priceIds
  952. );
  953. vm.stopPrank();
  954. }
  955. function checkEmptyState() private {
  956. (EchoState.Request[] memory requests, uint256 count) = echo
  957. .getFirstActiveRequests(10);
  958. assertEq(count, 0, "Should find 0 active requests");
  959. assertEq(requests.length, 0, "Array should be empty");
  960. }
  961. function testGetFirstActiveRequestsGasUsage() public {
  962. // Setup test data
  963. bytes32[] memory priceIds = new bytes32[](1);
  964. priceIds[0] = bytes32(uint256(1));
  965. uint64 publishTime = SafeCast.toUint64(block.timestamp);
  966. uint256 callbackGasLimit = 1000000;
  967. // Create mock price feeds and setup Pyth response
  968. PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
  969. publishTime
  970. );
  971. mockParsePriceFeedUpdates(pyth, priceFeeds);
  972. bytes[] memory updateData = createMockUpdateData(priceFeeds);
  973. // Create 20 requests with some gaps
  974. for (uint i = 0; i < 20; i++) {
  975. vm.deal(address(this), 1 ether);
  976. echo.requestPriceUpdatesWithCallback{value: 1 ether}(
  977. defaultProvider,
  978. publishTime,
  979. priceIds,
  980. uint32(callbackGasLimit)
  981. );
  982. // Complete every third request to create gaps
  983. if (i % 3 == 0) {
  984. vm.deal(defaultProvider, 1 ether);
  985. vm.prank(defaultProvider);
  986. echo.executeCallback{value: 1 ether}(
  987. defaultProvider,
  988. uint64(i + 1),
  989. updateData,
  990. priceIds
  991. );
  992. }
  993. }
  994. // Measure gas for different request counts
  995. uint256 gas1 = gasleft();
  996. echo.getFirstActiveRequests(5);
  997. uint256 gas1Used = gas1 - gasleft();
  998. uint256 gas2 = gasleft();
  999. echo.getFirstActiveRequests(10);
  1000. uint256 gas2Used = gas2 - gasleft();
  1001. // Log gas usage for analysis
  1002. emit log_named_uint("Gas used for 5 requests", gas1Used);
  1003. emit log_named_uint("Gas used for 10 requests", gas2Used);
  1004. // Verify gas usage scales roughly linearly
  1005. // Allow 10% margin for other factors
  1006. assertApproxEqRel(
  1007. gas2Used,
  1008. gas1Used * 2,
  1009. 0.2e18, // 20% tolerance
  1010. "Gas usage should scale roughly linearly"
  1011. );
  1012. }
  1013. function getEcho() internal view override returns (address) {
  1014. return address(echo);
  1015. }
  1016. // Mock implementation of echoCallback
  1017. function echoCallback(
  1018. uint64 sequenceNumber,
  1019. PythStructs.PriceFeed[] memory priceFeeds
  1020. ) internal override {
  1021. // Just accept the callback, no need to do anything with the data
  1022. // This prevents the revert we're seeing
  1023. }
  1024. }