PythOracleAdapter

Description:

Multi-signature wallet contract requiring multiple confirmations for transaction execution.

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.20;

// src/core/interfaces/IAggregatorV3Interface.sol

interface IAggregatorV3Interface {
  function decimals() external view returns (uint8);

  function description() external view returns (string memory);

  function version() external view returns (uint256);

  function getRoundData(
    uint80 _roundId
  ) external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound);

  function latestRoundData()
    external
    view
    returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound);
}

// src/oracles/base/IPythMinimumOracle.sol

interface IPythMinimumOracle {
    // A price with a degree of uncertainty, represented as a price +- a confidence interval.
    //
    // The confidence interval roughly corresponds to the standard error of a normal distribution.
    // Both the price and confidence are stored in a fixed-point numeric representation,
    // `x * (10^expo)`, where `expo` is the exponent.
    //
    // Please refer to the documentation at https://docs.pyth.network/documentation/pythnet-price-feeds/best-practices for how
    // to how this price safely.
    struct Price {
        // Price
        int64 price;
        // Confidence interval around the price
        uint64 conf;
        // Price exponent
        int32 expo;
        // Unix timestamp describing when the price was published
        uint publishTime;
    }

    // PriceFeed represents a current aggregate price from pyth publisher feeds.
    struct PriceFeed {
        // The price ID.
        bytes32 id;
        // Latest available price
        Price price;
        // Latest available exponentially-weighted moving average price
        Price emaPrice;
    }

    /// @notice Returns the price feed with the ID specified.
    /// @dev Reverts if the price does not exist.
    /// @param id The Pyth Price Feed ID of which to fetch the PriceFeed.
    function queryPriceFeed(bytes32 id) external view returns (PriceFeed memory priceFeed);

    /// @notice Returns the price feed with the ID specified.
    /// @dev Reverts if the price does not exist.
    /// @param id The Pyth Price Feed ID of which to fetch the PriceFeed.
    function getPrice(bytes32 id) external view returns (PriceFeed memory priceFeed);

    function getPriceNoOlderThan(bytes32 id, uint256 age) external view returns (PriceFeed memory priceFeed);

    function priceFeedExists(bytes32 id) external view returns (bool);

    function getValidTimePeriod() external view returns (uint256);
}

// src/core/OwnableGuarded.sol

abstract contract OwnableGuarded {
    // ----------------------------------------------------------------------------------------------------
    // Constants
    // ----------------------------------------------------------------------------------------------------
    // Booleans are more expensive than uint256 or any type that takes up a full
    // word because each write operation emits an extra SLOAD to first read the
    // slot's contents, replace the bits taken up by the boolean, and then write
    // back. This is the compiler's defense against contract upgrades and
    // pointer aliasing, and it cannot be disabled.

    // The values being non-zero value makes deployment a bit more expensive,
    // but in exchange the refund on every call to nonReentrant will be lower in
    // amount. Since refunds are capped to a percentage of the total
    // transaction's gas, it is best to keep them low in cases like this one, to
    // increase the likelihood of the full refund coming into effect.
    uint256 private constant NOT_ENTERED = 1;
    uint256 private constant ENTERED = 2;

    // ----------------------------------------------------------------------------------------------------
    // Errors
    // ----------------------------------------------------------------------------------------------------
    error OwnerOnly();
    error OwnerAddressRequired();
    error ReentrancyGuardReentrantCall();

    // ----------------------------------------------------------------------------------------------------
    // Storage layout
    // ----------------------------------------------------------------------------------------------------
    uint256 private _status;
    address internal _owner;

    // ----------------------------------------------------------------------------------------------------
    // Events
    // ----------------------------------------------------------------------------------------------------
    /**
     * @notice Triggers when contract ownership changes.
     * @param previousOwner The previous owner of the contract.
     * @param newOwner The new owner of the contract.
     */
    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    // ----------------------------------------------------------------------------------------------------
    // Modifiers
    // ----------------------------------------------------------------------------------------------------
    /**
     * @dev Prevents a contract from calling itself, directly or indirectly.
     * Calling a `nonReentrant` function from another `nonReentrant`
     * function is not supported. It is possible to prevent this from happening
     * by making the `nonReentrant` function external, and making it call a
     * `private` function that does the actual work.
     */
    modifier nonReentrant() {
        // On the first call to nonReentrant, _status will be NOT_ENTERED
        if (_status == ENTERED) {
            revert ReentrancyGuardReentrantCall();
        }

        // Any calls to nonReentrant after this point will fail
        _status = ENTERED;

        _;

        // By storing the original value once again, a refund is triggered (see https://eips.ethereum.org/EIPS/eip-2200)
        _status = NOT_ENTERED;
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        if (msg.sender != _owner) revert OwnerOnly();
        _;
    }

    // ----------------------------------------------------------------------------------------------------
    // Functions
    // ----------------------------------------------------------------------------------------------------
    /**
     * @notice Transfers ownership of the contract to the account specified.
     * @param newOwner The address of the new owner.
     */
    function transferOwnership(address newOwner) external virtual nonReentrant onlyOwner {
        _transferOwnership(newOwner);
    }

    function _transferOwnership(address newOwner) internal virtual {
        if (newOwner == address(0)) revert OwnerAddressRequired();

        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }

    /**
     * @notice Gets the owner of the contract.
     * @return address The address who owns the contract.
     */
    function owner() external view virtual returns (address) {
        return _owner;
    }
}

// src/oracles/PythOracleAdapter.sol

/**
 * @title Oracle adapter for Pyth/ChainLink.
 * @dev The contract validates the staleness of the Pyth feed by default.
 */
contract PythOracleAdapter is IAggregatorV3Interface, OwnableGuarded {
    // ----------------------------------------------------------------------------
    // Constants
    // ----------------------------------------------------------------------------
    /// @notice Strict mode. Check for staleness.
    uint8 constant public FETCH_STRATEGY_STRICT = 0;

    /// @notice Relaxed mode. No staleness.
    uint8 constant public FETCH_STRATEGY_RELAXED = 1;

    /// @notice Medium mode. Check for staleness within a limit expressed in seconds.
    uint8 constant public FETCH_STRATEGY_WITH_LIMIT = 2;

    // ----------------------------------------------------------------------------
    // Errors
    // ----------------------------------------------------------------------------
    error NegativePrice();
    error DecimalPlacesNotSupported();
    error InvalidFetchStrategy();
    error OracleDecimalsMismatch();
    error InputRequired();
    error InvalidThreshold();

    // ----------------------------------------------------------------------------
    // Tightly-packed storage layout
    // ----------------------------------------------------------------------------
    /// @notice The ID of the price feed reported by Pyth.
    bytes32 public priceFeedId;

    /// @notice The staleness threshold (in seconds) to apply on Pyth.
    uint256 public stalenessThreshold;

    /// @notice The network-wide Pyth contract address applicable to this network.
    address public priceFeedContractAddress;

    /// @notice The fetch strategy to use when getting price feeds from Pyth.
    uint8 public fetchStrategy;

    /// @dev The number of decimals reported by the Pyth oracle.
    uint8 internal _pythDecimals;

    uint8 internal _oracleDecimals;
    uint256 internal _oracleVersion;
    string internal _oracleDescription;

    // ----------------------------------------------------------------------------
    // Events
    // ----------------------------------------------------------------------------
    event FetchStrategyUpdated();
    event StalenessThresholdUpdated();

    // ----------------------------------------------------------------------------
    // Constructor
    // ----------------------------------------------------------------------------
    constructor(
        uint8 newOracleDecimals,
        uint256 newOracleVersion,
        string memory newOracleDescription,
        bytes32 newPriceFeedId, 
        address priceFeedContractAddr
    ) {
        _owner = msg.sender;
        _initMe(newOracleDecimals, newOracleVersion, newOracleDescription, newPriceFeedId, priceFeedContractAddr);
    }

    // ----------------------------------------------------------------------------
    // Functions
    // ----------------------------------------------------------------------------
    /**
     * @notice Updates the fetch strategy for getting prices from Pyth.
     * @dev Valid values are: 0 = strict, 1 = Relaxed, 2 = With a limit
     * @param newFetchStrategy The new strategy.
     */
    function updateFetchStrategy(uint8 newFetchStrategy) external nonReentrant onlyOwner {
        if (
            (newFetchStrategy != FETCH_STRATEGY_STRICT) && 
            (newFetchStrategy != FETCH_STRATEGY_RELAXED) &&
            (newFetchStrategy != FETCH_STRATEGY_WITH_LIMIT)
        ) revert InvalidFetchStrategy();

        fetchStrategy = newFetchStrategy;
        emit FetchStrategyUpdated();
    }

    /**
     * @notice Updates the staleness threshold accepted by this oracle.
     * @param newStalenessThreshold The new threshold, expressed in seconds.
     */
    function updateStalenessThreshold(uint256 newStalenessThreshold) external nonReentrant onlyOwner {
        if (newStalenessThreshold < 1) revert InvalidThreshold();

        stalenessThreshold = newStalenessThreshold;
        emit StalenessThresholdUpdated();
    }

    /// @dev Initializes the oracle
    function _initMe(
        uint8 newOracleDecimals,
        uint256 newOracleVersion,
        string memory newOracleDescription,
        bytes32 newPriceFeedId, 
        address priceFeedContractAddr
    ) private {
        if (newPriceFeedId == bytes32(0)) revert InputRequired();
        if (newOracleDecimals < 6) revert DecimalPlacesNotSupported();

        _oracleDecimals = newOracleDecimals;
        _oracleVersion = newOracleVersion;
        _oracleDescription = newOracleDescription;

        // The network-wide Pyth contract address applicable to this chain, as reported at https://docs.pyth.network/price-feeds/core/contract-addresses/evm
        // In Pyth, the contract address below tracks all of the price feeds for the entire network.
        priceFeedContractAddress = priceFeedContractAddr;

        // The ID of the price feed, as reported by Pyth at https://docs.pyth.network/price-feeds/core/price-feeds/price-feed-ids
        priceFeedId = newPriceFeedId;

        stalenessThreshold = IPythMinimumOracle(priceFeedContractAddress).getValidTimePeriod();

        // Validate the Pyth contract address and Feed identifier. This call will revert in case of error.
        IPythMinimumOracle.PriceFeed memory priceFeed = IPythMinimumOracle(priceFeedContractAddress).queryPriceFeed(priceFeedId);

        // Notice that the exponent reported by the Pyth oracle can be negative.
        uint256 decsAbsolute = (priceFeed.price.expo < 0) ? uint256(-1 * int256(priceFeed.price.expo)) : uint256(int256(priceFeed.price.expo));

        // Cache the number of decimals reported by the Pyth oracle. The oracle should never change their decimal places.
        _pythDecimals = uint8(decsAbsolute);
    }

    function decimals() external view override returns (uint8) {
        return _oracleDecimals;
    }

    function description() external view override returns (string memory) {
        return _oracleDescription;
    }

    function version() external view override returns (uint256) {
        return _oracleVersion;
    }

    function latestRoundData() external view override returns (
        uint80 roundId,
        int256 answer,
        uint256 startedAt,
        uint256 updatedAt,
        uint80 answeredInRound
    ) {
        return _queryLatestRound();
    }

    function getRoundData(uint80) external view override returns (
        uint80 roundId,
        int256 answer,
        uint256 startedAt,
        uint256 updatedAt,
        uint80 answeredInRound
    ) {
        return _queryLatestRound();
    }

    /// @dev Reads a feed from Pyth using the strategy defined by the owner of this contract.
    function _readFeed() private view returns (IPythMinimumOracle.PriceFeed memory priceFeed) {
        // Price lookup
        if (fetchStrategy == FETCH_STRATEGY_STRICT) {
            // Strict. Checks for staleness.
            priceFeed = IPythMinimumOracle(priceFeedContractAddress).getPrice(priceFeedId);
        } else if (fetchStrategy == FETCH_STRATEGY_RELAXED) {
            // Relaxed. Does not check for staleness
            priceFeed = IPythMinimumOracle(priceFeedContractAddress).queryPriceFeed(priceFeedId);
        } else if (fetchStrategy == FETCH_STRATEGY_WITH_LIMIT) {
            // Get the feed. Throw if the feed is older than the staleness threshold.
            priceFeed = IPythMinimumOracle(priceFeedContractAddress).getPriceNoOlderThan(priceFeedId, stalenessThreshold);
        }

        // The price reported by the Pyth oracle cannot be negative or zero, no matter what.
        if (priceFeed.price.price < 1) revert NegativePrice();

        // The exponent reported by the Pyth oracle should not change.
        uint256 decs = (priceFeed.price.expo < 0) ? uint256(-1 * int256(priceFeed.price.expo)) : uint256(int256(priceFeed.price.expo));
        if (uint8(decs) != _pythDecimals) revert OracleDecimalsMismatch();
    }

    /// @dev Grabs the price from Pyth and converts the result into a ChainLink-compatible output 
    function _queryLatestRound() private view returns (
        uint80 roundId,
        int256 answer,
        uint256 startedAt,
        uint256 updatedAt,
        uint80 answeredInRound
    ) {
        // Read the feed from Pyth
        IPythMinimumOracle.PriceFeed memory priceFeed = _readFeed();

        // Get the rough price reported by the Pyth oracle.
        int256 price = int256(priceFeed.price.price);

        // Scale down the price reported by Pyth, if applicable.
        answer = (_oracleDecimals < _pythDecimals) ? int256(price) / int256(10 ** (_pythDecimals - _oracleDecimals)) : price;

        // Pyth does not work with ChainLink rounds, so we always return "1" as the round ID
        roundId = uint80(1);
        answeredInRound = uint80(1);
        startedAt = priceFeed.price.publishTime;
        updatedAt = priceFeed.price.publishTime;
    }
}

Tags:
Multisig, Multi-Signature, Factory, Oracle|addr:0xde79b8b8cc000b1602eedcbc548f113f531ed18d|verified:true|block:23747895|tx:0xcd96e088f45a39e67a1da6d49401840aed6337e51f8044b084e56cf79feb9e40|first_check:1762525267

Submitted on: 2025-11-07 15:21:08

Comments

Log in to comment.

No comments yet.