S Price: $0.552486 (+0.60%)

Contract Diff Checker

Contract Name:
ChainlinkEMA

Contract Source Code:

// SPDX-License-Identifier: MIT

pragma solidity 0.8.25;

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

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

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

interface IPriceOracle {
    /**
        @notice Returns the current oracle price, normalized to 1e18 precision
        @dev Called by all state-changing market / amm operations with the exception
             of `MainController.close_loan`
     */
    function price_w() external returns (uint256);

    /**
        @notice Returns the current oracle price, normalized to 1e18 precision
        @dev Read-only version used within view methods. Should always return
             the same value as `price_w`
     */
    function price() external view returns (uint256);
}


/**
    @title Chainlink EMA Oracle
    @author defidotmoney
    @notice Calculates an exponential moving average from a Chainlink feed
    @dev This contract is designed for use in L2/sidechain environments where
         gas costs are negligible. It is not recommended for use on Ethereum mainnet.
 */
contract ChainlinkEMA {
    IChainlinkAggregator public immutable chainlinkFeed;

    /// @notice The number of observations used in calculating the EMA.
    uint256 public immutable OBSERVATIONS;
    /// @notice The number of seconds between price observations when calculating the EMA.
    uint256 public immutable INTERVAL;
    /// @dev `2 / (OBSERVATIONS - 1)` stored with 1e18 precision
    uint256 public immutable SMOOTHING_FACTOR;

    uint256 private immutable MAX_LOOKBACK;
    uint256 private immutable PRECISION_MUL;

    uint256 public storedPrice;
    uint256 public storedObservationTimestamp;
    ChainlinkResponse public storedResponse;

    struct ChainlinkResponse {
        uint80 roundId;
        uint128 updatedAt;
        uint256 answer; // normalized to 1e18
    }

    constructor(IChainlinkAggregator _chainlink, uint256 _observations, uint256 _interval) {
        chainlinkFeed = _chainlink;
        OBSERVATIONS = _observations;
        INTERVAL = _interval;
        SMOOTHING_FACTOR = 2e18 / (_observations + 1);
        MAX_LOOKBACK = _observations * 2;
        PRECISION_MUL = 10 ** (18 - _chainlink.decimals());

        uint256 currentObservation = _getCurrentObservationTimestamp();
        (storedPrice, storedResponse) = _calculateNewEMA(currentObservation);
        storedObservationTimestamp = currentObservation;
    }

    /**
        @notice Returns the current oracle price, normalized to 1e18 precision.
        @dev Read-only version used in view methods. Returns the same value as `price_w`.
     */
    function price() external view returns (uint256 currentPrice) {
        uint256 currentObservation = _getCurrentObservationTimestamp();
        uint256 storedObservation = storedObservationTimestamp;
        if (currentObservation == storedObservation) return storedPrice;

        if (storedObservation + MAX_LOOKBACK * INTERVAL > currentObservation) {
            (currentPrice, , ) = _calculateLatestEMA(currentObservation, storedObservation);
        } else {
            (currentPrice, ) = _calculateNewEMA(currentObservation);
        }
        return currentPrice;
    }

    /**
        @notice Returns the current oracle price, normalized to 1e18 precision.
        @dev It is preferred to call this method during on-chain interactions if possible.
     */
    function price_w() external returns (uint256 currentPrice) {
        uint256 currentObservation = _getCurrentObservationTimestamp();
        uint256 storedObservation = storedObservationTimestamp;
        if (currentObservation == storedObservation) return storedPrice;

        if (storedObservation + MAX_LOOKBACK * INTERVAL > currentObservation) {
            bool isNewResponse;
            ChainlinkResponse memory response;
            (currentPrice, response, isNewResponse) = _calculateLatestEMA(currentObservation, storedObservation);
            if (isNewResponse) storedResponse = response;
        } else {
            (currentPrice, storedResponse) = _calculateNewEMA(currentObservation);
        }
        storedObservationTimestamp = currentObservation;
        storedPrice = currentPrice;
        return currentPrice;
    }

    /**
        @dev Calculates the latest EMA price by performing observations at all observation
             intervals since the last stored one. Used when the number of new observations
             required is less than `2 * OBSERVATIONS`.
     */
    function _calculateLatestEMA(
        uint256 currentObservation,
        uint256 storedObservation
    ) internal view returns (uint256 currentPrice, ChainlinkResponse memory latestResponse, bool isNewResponse) {
        currentPrice = storedPrice;
        latestResponse = _getLatestRoundData();
        ChainlinkResponse memory response = storedResponse;

        // special case, latest round is the same as stored round
        if (latestResponse.roundId == response.roundId) {
            uint256 answer = response.answer;
            while (storedObservation < currentObservation) {
                storedObservation += INTERVAL;
                currentPrice = _getNextEMA(answer, currentPrice);
            }
            return (currentPrice, latestResponse, false);
        }

        bool isLatestResponse;
        ChainlinkResponse memory nextResponse;
        if (latestResponse.roundId > response.roundId + 1) {
            nextResponse = _getNextRoundData(response.roundId);
        } else {
            nextResponse = latestResponse;
        }

        while (storedObservation < currentObservation) {
            storedObservation += INTERVAL;
            while (!isLatestResponse && nextResponse.updatedAt < storedObservation) {
                response = nextResponse;
                if (nextResponse.roundId == latestResponse.roundId) {
                    isLatestResponse = true;
                } else {
                    nextResponse = _getNextRoundData(nextResponse.roundId);
                }
            }
            currentPrice = _getNextEMA(response.answer, currentPrice);
        }

        return (currentPrice, latestResponse, true);
    }

    /**
        @dev Calculates an EMA price without relying on the last stored observation.
             Used when the number of new observations required is at least `2 * OBSERVATIONS`.
     */
    function _calculateNewEMA(
        uint256 observationTimestamp
    ) internal view returns (uint256 currentPrice, ChainlinkResponse memory latestResponse) {
        latestResponse = _getLatestRoundData();
        ChainlinkResponse memory response = latestResponse;

        uint256[] memory oracleResponses = new uint256[](MAX_LOOKBACK);

        // in the following while loops, we manually decrement and then increment
        // idx so we know where the first non-zero value is within oracleResponses
        uint256 idx = MAX_LOOKBACK;

        // iterate backward to get oracle responses for each observation time
        while (true) {
            while (response.updatedAt >= observationTimestamp) {
                if (response.roundId & type(uint64).max == 1) {
                    // first roundId for this aggregator, cannot look back further
                    break;
                }
                response = _getRoundData(response.roundId - 1);
            }
            if (response.updatedAt >= observationTimestamp) {
                if (idx == MAX_LOOKBACK) {
                    // edge case, if the first round is more recent than our latest
                    // observation time we can only return the first round's response
                    return (response.answer, latestResponse);
                }
                break;
            }
            idx--;
            oracleResponses[idx] = response.answer;
            if (idx == 0) break;
            observationTimestamp -= INTERVAL;
        }

        // now iterate forward to calculate EMA based on the observed oracle responses
        currentPrice = oracleResponses[idx];
        idx++;
        while (idx < MAX_LOOKBACK) {
            currentPrice = _getNextEMA(oracleResponses[idx], currentPrice);
            idx++;
        }

        return (currentPrice, latestResponse);
    }

    /** @dev Given the latest price and the last EMA, returns the new EMA */
    function _getNextEMA(uint256 newPrice, uint256 lastEMA) internal view returns (uint256) {
        return ((newPrice * SMOOTHING_FACTOR) + (lastEMA * (1e18 - SMOOTHING_FACTOR))) / 1e18;
    }

    /** @dev The timestamp of the latest oracle observation */
    function _getCurrentObservationTimestamp() internal view returns (uint256) {
        return (block.timestamp / INTERVAL) * INTERVAL;
    }

    function _getLatestRoundData() internal view returns (ChainlinkResponse memory) {
        (uint80 roundId, int256 answer, , uint256 updatedAt, ) = chainlinkFeed.latestRoundData();
        return _validateAndFormatResponse(roundId, answer, updatedAt);
    }

    function _getRoundData(uint80 roundId) internal view returns (ChainlinkResponse memory) {
        (uint80 roundId, int256 answer, , uint256 updatedAt, ) = chainlinkFeed.getRoundData(roundId);
        return _validateAndFormatResponse(roundId, answer, updatedAt);
    }

    /**
        @dev Given a `roundId`, gets the response data for the next round. This method is preferred
             over calling `_getRoundData(roundId + 1)` because it handles a case where the oracle
             phase has increased: https://docs.chain.link/data-feeds/historical-data#roundid-in-proxy
     */
    function _getNextRoundData(uint80 roundId) internal view returns (ChainlinkResponse memory) {
        try chainlinkFeed.getRoundData(roundId + 1) returns (uint80 round, int answer, uint, uint updatedAt, uint80) {
            // depending on the direction the wind blows, an invalid roundId can revert or return zeros
            if (updatedAt > 0) return _validateAndFormatResponse(round, answer, updatedAt);
        } catch {}
        uint80 nextRoundId = (((roundId >> 64) + 1) << 64) + 1;
        return _getRoundData(nextRoundId);
    }

    function _validateAndFormatResponse(
        uint80 roundId,
        int256 answer,
        uint256 updatedAt
    ) internal view returns (ChainlinkResponse memory) {
        require(answer > 0, "DFM: Chainlink answer too low");
        return
            ChainlinkResponse({
                roundId: roundId,
                updatedAt: uint128(updatedAt),
                answer: uint256(answer) * PRECISION_MUL
            });
    }
}

Please enter a contract address above to load the contract details and source code.

Context size (optional):