EchoGasBenchmark.t.sol 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. // SPDX-License-Identifier: Apache 2
  2. pragma solidity ^0.8.0;
  3. import "forge-std/Test.sol";
  4. import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
  5. import "../contracts/echo/EchoUpgradeable.sol";
  6. import "../contracts/echo/IEcho.sol";
  7. import "../contracts/echo/EchoState.sol";
  8. import "../contracts/echo/EchoEvents.sol";
  9. import "../contracts/echo/EchoErrors.sol";
  10. import "./utils/EchoTestUtils.sol";
  11. import {console} from "forge-std/console.sol";
  12. contract EchoGasBenchmark is Test, EchoTestUtils {
  13. ERC1967Proxy public proxy;
  14. EchoUpgradeable public echo;
  15. IEchoConsumer public consumer;
  16. address public owner;
  17. address public admin;
  18. address public pyth;
  19. address public defaultProvider;
  20. uint96 constant PYTH_FEE = 1 wei;
  21. uint96 constant DEFAULT_PROVIDER_FEE_PER_GAS = 1 wei;
  22. uint96 constant DEFAULT_PROVIDER_BASE_FEE = 1 wei;
  23. uint96 constant DEFAULT_PROVIDER_FEE_PER_FEED = 10 wei;
  24. function setUp() public {
  25. owner = address(1);
  26. admin = address(2);
  27. pyth = address(3);
  28. defaultProvider = address(4);
  29. EchoUpgradeable _echo = new EchoUpgradeable();
  30. proxy = new ERC1967Proxy(address(_echo), "");
  31. echo = EchoUpgradeable(address(proxy));
  32. echo.initialize(
  33. owner,
  34. admin,
  35. PYTH_FEE,
  36. pyth,
  37. defaultProvider,
  38. true,
  39. 15
  40. );
  41. vm.prank(defaultProvider);
  42. echo.registerProvider(
  43. DEFAULT_PROVIDER_BASE_FEE,
  44. DEFAULT_PROVIDER_FEE_PER_FEED,
  45. DEFAULT_PROVIDER_FEE_PER_GAS
  46. );
  47. consumer = new VoidEchoConsumer(address(proxy));
  48. }
  49. // Estimate how much gas is used by all of the data mocking functionality in the other gas benchmarks.
  50. // Subtract this amount from the gas benchmarks to estimate the true usage of the echo flow.
  51. function testDataMocking() public {
  52. uint64 timestamp = SafeCast.toUint64(block.timestamp);
  53. createPriceIds();
  54. PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
  55. timestamp
  56. );
  57. mockParsePriceFeedUpdates(pyth, priceFeeds);
  58. createMockUpdateData(priceFeeds);
  59. }
  60. // Helper function to run the basic request + fulfill flow with a specified number of feeds
  61. function _runBenchmarkWithFeeds(uint256 numFeeds) internal {
  62. uint64 timestamp = SafeCast.toUint64(block.timestamp);
  63. bytes32[] memory priceIds = createPriceIds(numFeeds);
  64. uint32 callbackGasLimit = 100000;
  65. uint96 totalFee = echo.getFee(
  66. defaultProvider,
  67. callbackGasLimit,
  68. priceIds
  69. );
  70. vm.deal(address(consumer), 1 ether);
  71. vm.prank(address(consumer));
  72. uint64 sequenceNumber = echo.requestPriceUpdatesWithCallback{
  73. value: totalFee
  74. }(defaultProvider, timestamp, priceIds, callbackGasLimit);
  75. PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
  76. timestamp,
  77. numFeeds
  78. );
  79. mockParsePriceFeedUpdates(pyth, priceFeeds);
  80. bytes[] memory updateData = createMockUpdateData(priceFeeds);
  81. echo.executeCallback(
  82. defaultProvider,
  83. sequenceNumber,
  84. updateData,
  85. priceIds
  86. );
  87. }
  88. function testBasicFlowWith01Feeds() public {
  89. _runBenchmarkWithFeeds(1);
  90. }
  91. function testBasicFlowWith02Feeds() public {
  92. _runBenchmarkWithFeeds(2);
  93. }
  94. function testBasicFlowWith04Feeds() public {
  95. _runBenchmarkWithFeeds(4);
  96. }
  97. function testBasicFlowWith08Feeds() public {
  98. _runBenchmarkWithFeeds(8);
  99. }
  100. function testBasicFlowWith10Feeds() public {
  101. _runBenchmarkWithFeeds(10);
  102. }
  103. // This test checks the gas usage for worst-case out-of-order fulfillment.
  104. // It creates 10 requests, and then fulfills them in reverse order.
  105. //
  106. // The last fulfillment will be the most expensive since it needs
  107. // to linearly scan through all the fulfilled requests in storage
  108. // in order to update _state.lastUnfulfilledReq
  109. //
  110. // NOTE: Run test with `forge test --gas-report --match-test testMultipleRequestsOutOfOrderFulfillment`
  111. // and observe the `max` value for `executeCallback` to see the cost of the most expensive request.
  112. function testMultipleRequestsOutOfOrderFulfillment() public {
  113. uint64 timestamp = SafeCast.toUint64(block.timestamp);
  114. bytes32[] memory priceIds = createPriceIds(2);
  115. uint32 callbackGasLimit = 100000;
  116. uint128 totalFee = echo.getFee(
  117. defaultProvider,
  118. callbackGasLimit,
  119. priceIds
  120. );
  121. // Create 10 requests
  122. uint64[] memory sequenceNumbers = new uint64[](10);
  123. vm.deal(address(consumer), 10 ether);
  124. for (uint i = 0; i < 10; i++) {
  125. vm.prank(address(consumer));
  126. sequenceNumbers[i] = echo.requestPriceUpdatesWithCallback{
  127. value: totalFee
  128. }(
  129. defaultProvider,
  130. timestamp + uint64(i),
  131. priceIds,
  132. callbackGasLimit
  133. );
  134. }
  135. PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
  136. timestamp
  137. );
  138. mockParsePriceFeedUpdates(pyth, priceFeeds);
  139. bytes[] memory updateData = createMockUpdateData(priceFeeds);
  140. // Execute callbacks in reverse
  141. uint startGas = gasleft();
  142. for (uint i = 9; i > 0; i--) {
  143. echo.executeCallback(
  144. defaultProvider,
  145. sequenceNumbers[i],
  146. updateData,
  147. priceIds
  148. );
  149. }
  150. uint midGas = gasleft();
  151. // Execute the first request last - this would be the most expensive
  152. // in the original implementation as it would need to loop through
  153. // all sequence numbers
  154. echo.executeCallback(
  155. defaultProvider,
  156. sequenceNumbers[0],
  157. updateData,
  158. priceIds
  159. );
  160. uint endGas = gasleft();
  161. // Log gas usage for the last callback which would be the most expensive
  162. // in the original implementation (need to run test with -vv)
  163. console.log(
  164. "Gas used for last callback (seq 1): %s",
  165. vm.toString(midGas - endGas)
  166. );
  167. console.log(
  168. "Gas used for all other callbacks: %s",
  169. vm.toString(startGas - midGas)
  170. );
  171. }
  172. // Helper function to run the overflow mapping benchmark with a specified number of feeds
  173. function _runOverflowBenchmarkWithFeeds(uint256 numFeeds) internal {
  174. uint64 timestamp = SafeCast.toUint64(block.timestamp);
  175. bytes32[] memory priceIds = createPriceIds(numFeeds);
  176. uint32 callbackGasLimit = 100000;
  177. uint128 totalFee = echo.getFee(
  178. defaultProvider,
  179. callbackGasLimit,
  180. priceIds
  181. );
  182. // Create NUM_REQUESTS requests to fill up the main array
  183. // The constant is defined in EchoState.sol as 32
  184. uint64[] memory sequenceNumbers = new uint64[](32);
  185. vm.deal(address(consumer), 50 ether);
  186. // Use the same timestamp for all requests to avoid "Too far in future" error
  187. for (uint i = 0; i < 32; i++) {
  188. vm.prank(address(consumer));
  189. sequenceNumbers[i] = echo.requestPriceUpdatesWithCallback{
  190. value: totalFee
  191. }(defaultProvider, timestamp, priceIds, callbackGasLimit);
  192. }
  193. // Create one more request that will go to the overflow mapping
  194. // (This could potentially happen earlier if a shortKey collides,
  195. // but this guarantees it.)
  196. vm.prank(address(consumer));
  197. echo.requestPriceUpdatesWithCallback{value: totalFee}(
  198. defaultProvider,
  199. timestamp,
  200. priceIds,
  201. callbackGasLimit
  202. );
  203. }
  204. // These tests benchmark the gas usage when a new request overflows the fixed-size
  205. // request array and gets stored in the overflow mapping.
  206. //
  207. // NOTE: Run test with `forge test --gas-report --match-test testOverflowMappingGasUsageWithXXFeeds`
  208. // and observe the `max` value for `executeCallback` to see the cost of the overflowing request.
  209. function testOverflowMappingGasUsageWith01Feeds() public {
  210. _runOverflowBenchmarkWithFeeds(1);
  211. }
  212. function testOverflowMappingGasUsageWith02Feeds() public {
  213. _runOverflowBenchmarkWithFeeds(2);
  214. }
  215. function testOverflowMappingGasUsageWith04Feeds() public {
  216. _runOverflowBenchmarkWithFeeds(4);
  217. }
  218. function testOverflowMappingGasUsageWith08Feeds() public {
  219. _runOverflowBenchmarkWithFeeds(8);
  220. }
  221. function testOverflowMappingGasUsageWith10Feeds() public {
  222. _runOverflowBenchmarkWithFeeds(10);
  223. }
  224. }
  225. // A simple consumer that does nothing with the price updates.
  226. // Used to estimate the gas usage of the echo flow.
  227. contract VoidEchoConsumer is IEchoConsumer {
  228. address private _echo;
  229. constructor(address echo) {
  230. _echo = echo;
  231. }
  232. function getEcho() internal view override returns (address) {
  233. return _echo;
  234. }
  235. function echoCallback(
  236. uint64 sequenceNumber,
  237. PythStructs.PriceFeed[] memory priceFeeds
  238. ) internal override {}
  239. }