| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431 |
- // SPDX-License-Identifier: Apache 2
- pragma solidity ^0.8.0;
- import "@openzeppelin/contracts/utils/math/SafeCast.sol";
- import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
- import "./IPulse.sol";
- import "./PulseState.sol";
- import "./PulseErrors.sol";
- abstract contract Pulse is IPulse, PulseState {
- function _initialize(
- address admin,
- uint128 pythFeeInWei,
- address pythAddress,
- address defaultProvider,
- bool prefillRequestStorage,
- uint256 exclusivityPeriodSeconds
- ) internal {
- require(admin != address(0), "admin is zero address");
- require(pythAddress != address(0), "pyth is zero address");
- require(
- defaultProvider != address(0),
- "defaultProvider is zero address"
- );
- _state.admin = admin;
- _state.accruedFeesInWei = 0;
- _state.pythFeeInWei = pythFeeInWei;
- _state.pyth = pythAddress;
- _state.currentSequenceNumber = 1;
- // Two-step initialization process:
- // 1. Set the default provider address here
- // 2. Provider must call registerProvider() in a separate transaction to set their fee
- // This ensures the provider maintains control over their own fee settings
- _state.defaultProvider = defaultProvider;
- _state.exclusivityPeriodSeconds = exclusivityPeriodSeconds;
- if (prefillRequestStorage) {
- for (uint8 i = 0; i < NUM_REQUESTS; i++) {
- Request storage req = _state.requests[i];
- req.sequenceNumber = 0;
- req.publishTime = 1;
- req.callbackGasLimit = 1;
- req.requester = address(1);
- req.priceIdsHash = bytes32(uint256(1));
- req.fee = 1;
- req.provider = address(1);
- }
- }
- }
- // TODO: there can be a separate wrapper function that defaults the provider (or uses the cheapest or something).
- function requestPriceUpdatesWithCallback(
- address provider,
- uint64 publishTime,
- bytes32[] calldata priceIds,
- uint256 callbackGasLimit
- ) external payable override returns (uint64 requestSequenceNumber) {
- require(
- _state.providers[provider].isRegistered,
- "Provider not registered"
- );
- // FIXME: this comment is wrong. (we're not using tx.gasprice)
- // NOTE: The 60-second future limit on publishTime prevents a DoS vector where
- // attackers could submit many low-fee requests for far-future updates when gas prices
- // are low, forcing executors to fulfill them later when gas prices might be much higher.
- // Since tx.gasprice is used to calculate fees, allowing far-future requests would make
- // the fee estimation unreliable.
- require(publishTime <= block.timestamp + 60, "Too far in future");
- requestSequenceNumber = _state.currentSequenceNumber++;
- uint128 requiredFee = getFee(provider, callbackGasLimit, priceIds);
- if (msg.value < requiredFee) revert InsufficientFee();
- Request storage req = allocRequest(requestSequenceNumber);
- req.sequenceNumber = requestSequenceNumber;
- req.publishTime = publishTime;
- req.callbackGasLimit = SafeCast.toUint128(callbackGasLimit);
- req.requester = msg.sender;
- req.provider = provider;
- req.fee = SafeCast.toUint128(msg.value - _state.pythFeeInWei);
- req.priceIdsHash = keccak256(abi.encode(priceIds));
- _state.accruedFeesInWei += _state.pythFeeInWei;
- emit PriceUpdateRequested(req, priceIds);
- }
- // TODO: does this need to be payable? Any cost paid to Pyth could be taken out of the provider's accrued fees.
- function executeCallback(
- address providerToCredit,
- uint64 sequenceNumber,
- bytes[] calldata updateData,
- bytes32[] calldata priceIds
- ) external payable override {
- Request storage req = findActiveRequest(sequenceNumber);
- // Check provider exclusivity using configurable period
- if (
- block.timestamp < req.publishTime + _state.exclusivityPeriodSeconds
- ) {
- require(
- providerToCredit == req.provider,
- "Only assigned provider during exclusivity period"
- );
- }
- // Verify priceIds match
- if (req.priceIdsHash != keccak256(abi.encode(priceIds))) {
- revert InvalidPriceIds(
- keccak256(abi.encode(priceIds)),
- req.priceIdsHash
- );
- }
- // TODO: should this use parsePriceFeedUpdatesUnique? also, do we need to add 1 to maxPublishTime?
- IPyth pyth = IPyth(_state.pyth);
- uint256 pythFee = pyth.getUpdateFee(updateData);
- PythStructs.PriceFeed[] memory priceFeeds = pyth.parsePriceFeedUpdates{
- value: pythFee
- }(
- updateData,
- priceIds,
- SafeCast.toUint64(req.publishTime),
- SafeCast.toUint64(req.publishTime)
- );
- // TODO: if this effect occurs here, we need to guarantee that executeCallback can never revert.
- // If executeCallback can revert, then funds can be permanently locked in the contract.
- // TODO: there also needs to be some penalty mechanism in case the expected provider doesn't execute the callback.
- // This should take funds from the expected provider and give to providerToCredit. The penalty should probably scale
- // with time in order to ensure that the callback eventually gets executed.
- // (There may be exploits with ^ though if the consumer contract is malicious ?)
- _state.providers[providerToCredit].accruedFeesInWei += SafeCast
- .toUint128((req.fee + msg.value) - pythFee);
- clearRequest(sequenceNumber);
- try
- IPulseConsumer(req.requester)._pulseCallback{
- gas: req.callbackGasLimit
- }(sequenceNumber, priceFeeds)
- {
- // Callback succeeded
- emitPriceUpdate(sequenceNumber, priceIds, priceFeeds);
- } catch Error(string memory reason) {
- // Explicit revert/require
- emit PriceUpdateCallbackFailed(
- sequenceNumber,
- providerToCredit,
- priceIds,
- req.requester,
- reason
- );
- } catch {
- // Out of gas or other low-level errors
- emit PriceUpdateCallbackFailed(
- sequenceNumber,
- providerToCredit,
- priceIds,
- req.requester,
- "low-level error (possibly out of gas)"
- );
- }
- }
- function emitPriceUpdate(
- uint64 sequenceNumber,
- bytes32[] memory priceIds,
- PythStructs.PriceFeed[] memory priceFeeds
- ) internal {
- int64[] memory prices = new int64[](priceFeeds.length);
- uint64[] memory conf = new uint64[](priceFeeds.length);
- int32[] memory expos = new int32[](priceFeeds.length);
- uint64[] memory publishTimes = new uint64[](priceFeeds.length);
- for (uint i = 0; i < priceFeeds.length; i++) {
- prices[i] = priceFeeds[i].price.price;
- conf[i] = priceFeeds[i].price.conf;
- expos[i] = priceFeeds[i].price.expo;
- // Safe cast because this is a unix timestamp in seconds.
- publishTimes[i] = SafeCast.toUint64(
- priceFeeds[i].price.publishTime
- );
- }
- emit PriceUpdateExecuted(
- sequenceNumber,
- msg.sender,
- priceIds,
- prices,
- conf,
- expos,
- publishTimes
- );
- }
- function getFee(
- address provider,
- uint256 callbackGasLimit,
- bytes32[] calldata priceIds
- ) public view override returns (uint128 feeAmount) {
- uint128 baseFee = _state.pythFeeInWei; // Fixed fee to Pyth
- // Note: The provider needs to set its fees to include the fee charged by the Pyth contract.
- // Ideally, we would be able to automatically compute the pyth fees from the priceIds, but the
- // fee computation on IPyth assumes it has the full updated data.
- uint128 providerBaseFee = _state.providers[provider].baseFeeInWei;
- uint128 providerFeedFee = SafeCast.toUint128(
- priceIds.length * _state.providers[provider].feePerFeedInWei
- );
- uint128 providerFeeInWei = _state.providers[provider].feePerGasInWei; // Provider's per-gas rate
- uint256 gasFee = callbackGasLimit * providerFeeInWei; // Total provider fee based on gas
- feeAmount =
- baseFee +
- providerBaseFee +
- providerFeedFee +
- SafeCast.toUint128(gasFee); // Total fee user needs to pay
- }
- function getPythFeeInWei()
- public
- view
- override
- returns (uint128 pythFeeInWei)
- {
- pythFeeInWei = _state.pythFeeInWei;
- }
- function getAccruedPythFees()
- public
- view
- override
- returns (uint128 accruedFeesInWei)
- {
- accruedFeesInWei = _state.accruedFeesInWei;
- }
- function getRequest(
- uint64 sequenceNumber
- ) public view override returns (Request memory req) {
- req = findRequest(sequenceNumber);
- }
- function requestKey(
- uint64 sequenceNumber
- ) internal pure returns (bytes32 hash, uint8 shortHash) {
- hash = keccak256(abi.encodePacked(sequenceNumber));
- shortHash = uint8(hash[0] & NUM_REQUESTS_MASK);
- }
- // TODO: move out governance functions into a separate PulseGovernance contract
- function withdrawFees(uint128 amount) external override {
- require(msg.sender == _state.admin, "Only admin can withdraw fees");
- require(_state.accruedFeesInWei >= amount, "Insufficient balance");
- _state.accruedFeesInWei -= amount;
- (bool sent, ) = msg.sender.call{value: amount}("");
- require(sent, "Failed to send fees");
- emit FeesWithdrawn(msg.sender, amount);
- }
- function findActiveRequest(
- uint64 sequenceNumber
- ) internal view returns (Request storage req) {
- req = findRequest(sequenceNumber);
- if (!isActive(req) || req.sequenceNumber != sequenceNumber)
- revert NoSuchRequest();
- }
- function findRequest(
- uint64 sequenceNumber
- ) internal view returns (Request storage req) {
- (bytes32 key, uint8 shortKey) = requestKey(sequenceNumber);
- req = _state.requests[shortKey];
- if (req.sequenceNumber == sequenceNumber) {
- return req;
- } else {
- req = _state.requestsOverflow[key];
- }
- }
- function clearRequest(uint64 sequenceNumber) internal {
- (bytes32 key, uint8 shortKey) = requestKey(sequenceNumber);
- Request storage req = _state.requests[shortKey];
- if (req.sequenceNumber == sequenceNumber) {
- req.sequenceNumber = 0;
- } else {
- delete _state.requestsOverflow[key];
- }
- }
- function allocRequest(
- uint64 sequenceNumber
- ) internal returns (Request storage req) {
- (, uint8 shortKey) = requestKey(sequenceNumber);
- req = _state.requests[shortKey];
- if (isActive(req)) {
- (bytes32 reqKey, ) = requestKey(req.sequenceNumber);
- _state.requestsOverflow[reqKey] = req;
- }
- }
- function isActive(Request memory req) internal pure returns (bool) {
- return req.sequenceNumber != 0;
- }
- function setFeeManager(address manager) external override {
- require(
- _state.providers[msg.sender].isRegistered,
- "Provider not registered"
- );
- address oldFeeManager = _state.providers[msg.sender].feeManager;
- _state.providers[msg.sender].feeManager = manager;
- emit FeeManagerUpdated(msg.sender, oldFeeManager, manager);
- }
- function withdrawAsFeeManager(
- address provider,
- uint128 amount
- ) external override {
- require(
- msg.sender == _state.providers[provider].feeManager,
- "Only fee manager"
- );
- require(
- _state.providers[provider].accruedFeesInWei >= amount,
- "Insufficient balance"
- );
- _state.providers[provider].accruedFeesInWei -= amount;
- (bool sent, ) = msg.sender.call{value: amount}("");
- require(sent, "Failed to send fees");
- emit FeesWithdrawn(msg.sender, amount);
- }
- function registerProvider(
- uint128 baseFeeInWei,
- uint128 feePerFeedInWei,
- uint128 feePerGasInWei
- ) external override {
- ProviderInfo storage provider = _state.providers[msg.sender];
- require(!provider.isRegistered, "Provider already registered");
- provider.baseFeeInWei = baseFeeInWei;
- provider.feePerFeedInWei = feePerFeedInWei;
- provider.feePerGasInWei = feePerGasInWei;
- provider.isRegistered = true;
- emit ProviderRegistered(msg.sender, feePerGasInWei);
- }
- function setProviderFee(
- address provider,
- uint128 newBaseFeeInWei,
- uint128 newFeePerFeedInWei,
- uint128 newFeePerGasInWei
- ) external override {
- require(
- _state.providers[provider].isRegistered,
- "Provider not registered"
- );
- require(
- msg.sender == provider ||
- msg.sender == _state.providers[provider].feeManager,
- "Only provider or fee manager can invoke this method"
- );
- uint128 oldBaseFee = _state.providers[provider].baseFeeInWei;
- uint128 oldFeePerFeed = _state.providers[provider].feePerFeedInWei;
- uint128 oldFeePerGas = _state.providers[provider].feePerGasInWei;
- _state.providers[provider].baseFeeInWei = newBaseFeeInWei;
- _state.providers[provider].feePerFeedInWei = newFeePerFeedInWei;
- _state.providers[provider].feePerGasInWei = newFeePerGasInWei;
- emit ProviderFeeUpdated(
- provider,
- oldBaseFee,
- oldFeePerFeed,
- oldFeePerGas,
- newBaseFeeInWei,
- newFeePerFeedInWei,
- newFeePerGasInWei
- );
- }
- function getProviderInfo(
- address provider
- ) external view override returns (ProviderInfo memory) {
- return _state.providers[provider];
- }
- function getDefaultProvider() external view override returns (address) {
- return _state.defaultProvider;
- }
- function setDefaultProvider(address provider) external override {
- require(
- msg.sender == _state.admin,
- "Only admin can set default provider"
- );
- require(
- _state.providers[provider].isRegistered,
- "Provider not registered"
- );
- address oldProvider = _state.defaultProvider;
- _state.defaultProvider = provider;
- emit DefaultProviderUpdated(oldProvider, provider);
- }
- function setExclusivityPeriod(uint256 periodSeconds) external override {
- require(
- msg.sender == _state.admin,
- "Only admin can set exclusivity period"
- );
- uint256 oldPeriod = _state.exclusivityPeriodSeconds;
- _state.exclusivityPeriodSeconds = periodSeconds;
- emit ExclusivityPeriodUpdated(oldPeriod, periodSeconds);
- }
- function getExclusivityPeriod() external view override returns (uint256) {
- return _state.exclusivityPeriodSeconds;
- }
- }
|