Pulse.sol 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. // SPDX-License-Identifier: Apache 2
  2. pragma solidity ^0.8.0;
  3. import "@openzeppelin/contracts/utils/math/SafeCast.sol";
  4. import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
  5. import "./IPulse.sol";
  6. import "./PulseState.sol";
  7. import "./PulseErrors.sol";
  8. abstract contract Pulse is IPulse, PulseState {
  9. function _initialize(
  10. address admin,
  11. uint128 pythFeeInWei,
  12. address pythAddress,
  13. address defaultProvider,
  14. bool prefillRequestStorage,
  15. uint256 exclusivityPeriodSeconds
  16. ) internal {
  17. require(admin != address(0), "admin is zero address");
  18. require(pythAddress != address(0), "pyth is zero address");
  19. require(
  20. defaultProvider != address(0),
  21. "defaultProvider is zero address"
  22. );
  23. _state.admin = admin;
  24. _state.accruedFeesInWei = 0;
  25. _state.pythFeeInWei = pythFeeInWei;
  26. _state.pyth = pythAddress;
  27. _state.currentSequenceNumber = 1;
  28. // Two-step initialization process:
  29. // 1. Set the default provider address here
  30. // 2. Provider must call registerProvider() in a separate transaction to set their fee
  31. // This ensures the provider maintains control over their own fee settings
  32. _state.defaultProvider = defaultProvider;
  33. _state.exclusivityPeriodSeconds = exclusivityPeriodSeconds;
  34. if (prefillRequestStorage) {
  35. for (uint8 i = 0; i < NUM_REQUESTS; i++) {
  36. Request storage req = _state.requests[i];
  37. req.sequenceNumber = 0;
  38. req.publishTime = 1;
  39. req.callbackGasLimit = 1;
  40. req.requester = address(1);
  41. req.priceIdsHash = bytes32(uint256(1));
  42. req.fee = 1;
  43. req.provider = address(1);
  44. }
  45. }
  46. }
  47. // TODO: there can be a separate wrapper function that defaults the provider (or uses the cheapest or something).
  48. function requestPriceUpdatesWithCallback(
  49. address provider,
  50. uint64 publishTime,
  51. bytes32[] calldata priceIds,
  52. uint256 callbackGasLimit
  53. ) external payable override returns (uint64 requestSequenceNumber) {
  54. require(
  55. _state.providers[provider].isRegistered,
  56. "Provider not registered"
  57. );
  58. // FIXME: this comment is wrong. (we're not using tx.gasprice)
  59. // NOTE: The 60-second future limit on publishTime prevents a DoS vector where
  60. // attackers could submit many low-fee requests for far-future updates when gas prices
  61. // are low, forcing executors to fulfill them later when gas prices might be much higher.
  62. // Since tx.gasprice is used to calculate fees, allowing far-future requests would make
  63. // the fee estimation unreliable.
  64. require(publishTime <= block.timestamp + 60, "Too far in future");
  65. requestSequenceNumber = _state.currentSequenceNumber++;
  66. uint128 requiredFee = getFee(provider, callbackGasLimit, priceIds);
  67. if (msg.value < requiredFee) revert InsufficientFee();
  68. Request storage req = allocRequest(requestSequenceNumber);
  69. req.sequenceNumber = requestSequenceNumber;
  70. req.publishTime = publishTime;
  71. req.callbackGasLimit = SafeCast.toUint128(callbackGasLimit);
  72. req.requester = msg.sender;
  73. req.provider = provider;
  74. req.fee = SafeCast.toUint128(msg.value - _state.pythFeeInWei);
  75. req.priceIdsHash = keccak256(abi.encode(priceIds));
  76. _state.accruedFeesInWei += _state.pythFeeInWei;
  77. emit PriceUpdateRequested(req, priceIds);
  78. }
  79. // TODO: does this need to be payable? Any cost paid to Pyth could be taken out of the provider's accrued fees.
  80. function executeCallback(
  81. address providerToCredit,
  82. uint64 sequenceNumber,
  83. bytes[] calldata updateData,
  84. bytes32[] calldata priceIds
  85. ) external payable override {
  86. Request storage req = findActiveRequest(sequenceNumber);
  87. // Check provider exclusivity using configurable period
  88. if (
  89. block.timestamp < req.publishTime + _state.exclusivityPeriodSeconds
  90. ) {
  91. require(
  92. providerToCredit == req.provider,
  93. "Only assigned provider during exclusivity period"
  94. );
  95. }
  96. // Verify priceIds match
  97. if (req.priceIdsHash != keccak256(abi.encode(priceIds))) {
  98. revert InvalidPriceIds(
  99. keccak256(abi.encode(priceIds)),
  100. req.priceIdsHash
  101. );
  102. }
  103. // TODO: should this use parsePriceFeedUpdatesUnique? also, do we need to add 1 to maxPublishTime?
  104. IPyth pyth = IPyth(_state.pyth);
  105. uint256 pythFee = pyth.getUpdateFee(updateData);
  106. PythStructs.PriceFeed[] memory priceFeeds = pyth.parsePriceFeedUpdates{
  107. value: pythFee
  108. }(
  109. updateData,
  110. priceIds,
  111. SafeCast.toUint64(req.publishTime),
  112. SafeCast.toUint64(req.publishTime)
  113. );
  114. // TODO: if this effect occurs here, we need to guarantee that executeCallback can never revert.
  115. // If executeCallback can revert, then funds can be permanently locked in the contract.
  116. // TODO: there also needs to be some penalty mechanism in case the expected provider doesn't execute the callback.
  117. // This should take funds from the expected provider and give to providerToCredit. The penalty should probably scale
  118. // with time in order to ensure that the callback eventually gets executed.
  119. // (There may be exploits with ^ though if the consumer contract is malicious ?)
  120. _state.providers[providerToCredit].accruedFeesInWei += SafeCast
  121. .toUint128((req.fee + msg.value) - pythFee);
  122. clearRequest(sequenceNumber);
  123. try
  124. IPulseConsumer(req.requester)._pulseCallback{
  125. gas: req.callbackGasLimit
  126. }(sequenceNumber, priceFeeds)
  127. {
  128. // Callback succeeded
  129. emitPriceUpdate(sequenceNumber, priceIds, priceFeeds);
  130. } catch Error(string memory reason) {
  131. // Explicit revert/require
  132. emit PriceUpdateCallbackFailed(
  133. sequenceNumber,
  134. providerToCredit,
  135. priceIds,
  136. req.requester,
  137. reason
  138. );
  139. } catch {
  140. // Out of gas or other low-level errors
  141. emit PriceUpdateCallbackFailed(
  142. sequenceNumber,
  143. providerToCredit,
  144. priceIds,
  145. req.requester,
  146. "low-level error (possibly out of gas)"
  147. );
  148. }
  149. }
  150. function emitPriceUpdate(
  151. uint64 sequenceNumber,
  152. bytes32[] memory priceIds,
  153. PythStructs.PriceFeed[] memory priceFeeds
  154. ) internal {
  155. int64[] memory prices = new int64[](priceFeeds.length);
  156. uint64[] memory conf = new uint64[](priceFeeds.length);
  157. int32[] memory expos = new int32[](priceFeeds.length);
  158. uint64[] memory publishTimes = new uint64[](priceFeeds.length);
  159. for (uint i = 0; i < priceFeeds.length; i++) {
  160. prices[i] = priceFeeds[i].price.price;
  161. conf[i] = priceFeeds[i].price.conf;
  162. expos[i] = priceFeeds[i].price.expo;
  163. // Safe cast because this is a unix timestamp in seconds.
  164. publishTimes[i] = SafeCast.toUint64(
  165. priceFeeds[i].price.publishTime
  166. );
  167. }
  168. emit PriceUpdateExecuted(
  169. sequenceNumber,
  170. msg.sender,
  171. priceIds,
  172. prices,
  173. conf,
  174. expos,
  175. publishTimes
  176. );
  177. }
  178. function getFee(
  179. address provider,
  180. uint256 callbackGasLimit,
  181. bytes32[] calldata priceIds
  182. ) public view override returns (uint128 feeAmount) {
  183. uint128 baseFee = _state.pythFeeInWei; // Fixed fee to Pyth
  184. // Note: The provider needs to set its fees to include the fee charged by the Pyth contract.
  185. // Ideally, we would be able to automatically compute the pyth fees from the priceIds, but the
  186. // fee computation on IPyth assumes it has the full updated data.
  187. uint128 providerBaseFee = _state.providers[provider].baseFeeInWei;
  188. uint128 providerFeedFee = SafeCast.toUint128(
  189. priceIds.length * _state.providers[provider].feePerFeedInWei
  190. );
  191. uint128 providerFeeInWei = _state.providers[provider].feePerGasInWei; // Provider's per-gas rate
  192. uint256 gasFee = callbackGasLimit * providerFeeInWei; // Total provider fee based on gas
  193. feeAmount =
  194. baseFee +
  195. providerBaseFee +
  196. providerFeedFee +
  197. SafeCast.toUint128(gasFee); // Total fee user needs to pay
  198. }
  199. function getPythFeeInWei()
  200. public
  201. view
  202. override
  203. returns (uint128 pythFeeInWei)
  204. {
  205. pythFeeInWei = _state.pythFeeInWei;
  206. }
  207. function getAccruedPythFees()
  208. public
  209. view
  210. override
  211. returns (uint128 accruedFeesInWei)
  212. {
  213. accruedFeesInWei = _state.accruedFeesInWei;
  214. }
  215. function getRequest(
  216. uint64 sequenceNumber
  217. ) public view override returns (Request memory req) {
  218. req = findRequest(sequenceNumber);
  219. }
  220. function requestKey(
  221. uint64 sequenceNumber
  222. ) internal pure returns (bytes32 hash, uint8 shortHash) {
  223. hash = keccak256(abi.encodePacked(sequenceNumber));
  224. shortHash = uint8(hash[0] & NUM_REQUESTS_MASK);
  225. }
  226. // TODO: move out governance functions into a separate PulseGovernance contract
  227. function withdrawFees(uint128 amount) external override {
  228. require(msg.sender == _state.admin, "Only admin can withdraw fees");
  229. require(_state.accruedFeesInWei >= amount, "Insufficient balance");
  230. _state.accruedFeesInWei -= amount;
  231. (bool sent, ) = msg.sender.call{value: amount}("");
  232. require(sent, "Failed to send fees");
  233. emit FeesWithdrawn(msg.sender, amount);
  234. }
  235. function findActiveRequest(
  236. uint64 sequenceNumber
  237. ) internal view returns (Request storage req) {
  238. req = findRequest(sequenceNumber);
  239. if (!isActive(req) || req.sequenceNumber != sequenceNumber)
  240. revert NoSuchRequest();
  241. }
  242. function findRequest(
  243. uint64 sequenceNumber
  244. ) internal view returns (Request storage req) {
  245. (bytes32 key, uint8 shortKey) = requestKey(sequenceNumber);
  246. req = _state.requests[shortKey];
  247. if (req.sequenceNumber == sequenceNumber) {
  248. return req;
  249. } else {
  250. req = _state.requestsOverflow[key];
  251. }
  252. }
  253. function clearRequest(uint64 sequenceNumber) internal {
  254. (bytes32 key, uint8 shortKey) = requestKey(sequenceNumber);
  255. Request storage req = _state.requests[shortKey];
  256. if (req.sequenceNumber == sequenceNumber) {
  257. req.sequenceNumber = 0;
  258. } else {
  259. delete _state.requestsOverflow[key];
  260. }
  261. }
  262. function allocRequest(
  263. uint64 sequenceNumber
  264. ) internal returns (Request storage req) {
  265. (, uint8 shortKey) = requestKey(sequenceNumber);
  266. req = _state.requests[shortKey];
  267. if (isActive(req)) {
  268. (bytes32 reqKey, ) = requestKey(req.sequenceNumber);
  269. _state.requestsOverflow[reqKey] = req;
  270. }
  271. }
  272. function isActive(Request memory req) internal pure returns (bool) {
  273. return req.sequenceNumber != 0;
  274. }
  275. function setFeeManager(address manager) external override {
  276. require(
  277. _state.providers[msg.sender].isRegistered,
  278. "Provider not registered"
  279. );
  280. address oldFeeManager = _state.providers[msg.sender].feeManager;
  281. _state.providers[msg.sender].feeManager = manager;
  282. emit FeeManagerUpdated(msg.sender, oldFeeManager, manager);
  283. }
  284. function withdrawAsFeeManager(
  285. address provider,
  286. uint128 amount
  287. ) external override {
  288. require(
  289. msg.sender == _state.providers[provider].feeManager,
  290. "Only fee manager"
  291. );
  292. require(
  293. _state.providers[provider].accruedFeesInWei >= amount,
  294. "Insufficient balance"
  295. );
  296. _state.providers[provider].accruedFeesInWei -= amount;
  297. (bool sent, ) = msg.sender.call{value: amount}("");
  298. require(sent, "Failed to send fees");
  299. emit FeesWithdrawn(msg.sender, amount);
  300. }
  301. function registerProvider(
  302. uint128 baseFeeInWei,
  303. uint128 feePerFeedInWei,
  304. uint128 feePerGasInWei
  305. ) external override {
  306. ProviderInfo storage provider = _state.providers[msg.sender];
  307. require(!provider.isRegistered, "Provider already registered");
  308. provider.baseFeeInWei = baseFeeInWei;
  309. provider.feePerFeedInWei = feePerFeedInWei;
  310. provider.feePerGasInWei = feePerGasInWei;
  311. provider.isRegistered = true;
  312. emit ProviderRegistered(msg.sender, feePerGasInWei);
  313. }
  314. function setProviderFee(
  315. address provider,
  316. uint128 newBaseFeeInWei,
  317. uint128 newFeePerFeedInWei,
  318. uint128 newFeePerGasInWei
  319. ) external override {
  320. require(
  321. _state.providers[provider].isRegistered,
  322. "Provider not registered"
  323. );
  324. require(
  325. msg.sender == provider ||
  326. msg.sender == _state.providers[provider].feeManager,
  327. "Only provider or fee manager can invoke this method"
  328. );
  329. uint128 oldBaseFee = _state.providers[provider].baseFeeInWei;
  330. uint128 oldFeePerFeed = _state.providers[provider].feePerFeedInWei;
  331. uint128 oldFeePerGas = _state.providers[provider].feePerGasInWei;
  332. _state.providers[provider].baseFeeInWei = newBaseFeeInWei;
  333. _state.providers[provider].feePerFeedInWei = newFeePerFeedInWei;
  334. _state.providers[provider].feePerGasInWei = newFeePerGasInWei;
  335. emit ProviderFeeUpdated(
  336. provider,
  337. oldBaseFee,
  338. oldFeePerFeed,
  339. oldFeePerGas,
  340. newBaseFeeInWei,
  341. newFeePerFeedInWei,
  342. newFeePerGasInWei
  343. );
  344. }
  345. function getProviderInfo(
  346. address provider
  347. ) external view override returns (ProviderInfo memory) {
  348. return _state.providers[provider];
  349. }
  350. function getDefaultProvider() external view override returns (address) {
  351. return _state.defaultProvider;
  352. }
  353. function setDefaultProvider(address provider) external override {
  354. require(
  355. msg.sender == _state.admin,
  356. "Only admin can set default provider"
  357. );
  358. require(
  359. _state.providers[provider].isRegistered,
  360. "Provider not registered"
  361. );
  362. address oldProvider = _state.defaultProvider;
  363. _state.defaultProvider = provider;
  364. emit DefaultProviderUpdated(oldProvider, provider);
  365. }
  366. function setExclusivityPeriod(uint256 periodSeconds) external override {
  367. require(
  368. msg.sender == _state.admin,
  369. "Only admin can set exclusivity period"
  370. );
  371. uint256 oldPeriod = _state.exclusivityPeriodSeconds;
  372. _state.exclusivityPeriodSeconds = periodSeconds;
  373. emit ExclusivityPeriodUpdated(oldPeriod, periodSeconds);
  374. }
  375. function getExclusivityPeriod() external view override returns (uint256) {
  376. return _state.exclusivityPeriodSeconds;
  377. }
  378. }