OracleAggregatorV2

Description:

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

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

{{
  "language": "Solidity",
  "sources": {
    "contracts/v2/oracle/OracleAggregatorV2.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

/**
 * @title OracleAggregatorV2
 * @author Term Structure Labs
 * @notice Enhanced oracle aggregator for TermMax V2 protocol with improved price feed management
 * @dev This contract references design concepts from AAVE's oracle system
 * Implements price feed aggregation with primary and backup oracles,
 * staleness checks via heartbeats, and governance-controlled updates with timelocks
 * similar to AAVE's oracle architecture
 *
 * Key V2 improvements over V1:
 * - Independent heartbeat configuration for backup oracles
 * - Price capping mechanism with maxPrice parameter
 * - Oracle revocation capability for enhanced security
 */
import {Ownable2Step, Ownable} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {AggregatorV3Interface, IOracleV2} from "./IOracleV2.sol";
import {VersionV2} from "../VersionV2.sol";

contract OracleAggregatorV2 is IOracleV2, Ownable2Step, VersionV2 {
    /// @notice The timelock period in seconds that must elapse before pending oracles can be accepted
    /// @dev Immutable value set during contract construction for security
    uint256 internal immutable _timeLock;

    /**
     * @notice Structure for storing pending oracle updates with timelock protection
     * @param oracle The oracle configuration waiting to be activated
     * @param validAt The timestamp when this pending oracle can be accepted
     */
    struct PendingOracle {
        Oracle oracle;
        uint64 validAt;
    }

    /// @notice Error thrown when the asset or oracle address is invalid
    error InvalidAssetOrOracle();

    /**
     * @notice Error thrown when trying to accept a change that has no pending value
     */
    error NoPendingValue();

    /**
     * @notice Error thrown when trying to accept a change before the timelock period has elapsed
     */
    error TimelockNotElapsed();

    /**
     * @notice Event emitted when an oracle configuration is updated
     * @param asset The address of the asset whose oracle was updated
     * @param aggregator The address of the primary aggregator
     * @param backupAggregator The address of the backup aggregator
     * @param maxPrice The maximum price cap for this asset (0 = no cap)
     * @param minPrice The minimum price floor for this asset (0 = no floor)
     * @param heartbeat The staleness threshold for the primary aggregator
     * @param backupHeartbeat The staleness threshold for the backup aggregator
     */
    event UpdateOracle(
        address indexed asset,
        AggregatorV3Interface indexed aggregator,
        AggregatorV3Interface indexed backupAggregator,
        int256 maxPrice,
        int256 minPrice,
        uint32 heartbeat,
        uint32 backupHeartbeat
    );

    /**
     * @notice Event emitted when a pending oracle is submitted
     * @param asset The address of the asset for the pending oracle
     * @param aggregator The address of the primary aggregator
     * @param backupAggregator The address of the backup aggregator
     * @param maxPrice The maximum price cap for this asset
     * @param minPrice The minimum price floor for this asset (0 = no floor)
     * @param heartbeat The staleness threshold for the primary aggregator
     * @param backupHeartbeat The staleness threshold for the backup aggregator
     * @param validAt The timestamp when this pending oracle can be accepted
     */
    event SubmitPendingOracle(
        address indexed asset,
        AggregatorV3Interface indexed aggregator,
        AggregatorV3Interface indexed backupAggregator,
        int256 maxPrice,
        int256 minPrice,
        uint32 heartbeat,
        uint32 backupHeartbeat,
        uint64 validAt
    );

    /**
     * @notice Event emitted when a pending oracle is revoked
     * @param asset The address of the asset whose pending oracle was revoked
     */
    event RevokePendingOracle(address indexed asset);

    /// @notice Mapping of asset addresses to their active oracle configurations
    /// @dev Contains the currently live oracle settings for each asset
    mapping(address => Oracle) public oracles;

    /// @notice Mapping of asset addresses to their pending oracle configurations
    /// @dev Contains oracle updates waiting for timelock expiration
    mapping(address => PendingOracle) public pendingOracles;

    constructor(address _owner, uint256 timeLock) Ownable(_owner) {
        _timeLock = timeLock;
    }

    /**
     * @inheritdoc IOracleV2
     */
    function submitPendingOracle(address asset, Oracle memory oracle) external onlyOwner {
        // Handle oracle removal case
        if (address(oracle.aggregator) == address(0) && address(oracle.backupAggregator) == address(0)) {
            delete oracles[asset];
            emit UpdateOracle(asset, AggregatorV3Interface(address(0)), AggregatorV3Interface(address(0)), 0, 0, 0, 0);
            return;
        }

        // Validate inputs for oracle addition/update
        if (asset == address(0) || oracle.aggregator == AggregatorV3Interface(address(0))) {
            revert InvalidAssetOrOracle();
        }

        // Update immediately if the aggregator and backupAggregator are not changed
        Oracle memory oldOracle = oracles[asset];
        if (oldOracle.aggregator == oracle.aggregator && oldOracle.backupAggregator == oracle.backupAggregator) {
            _updateOracle(asset, oracle);
            return;
        }

        // Ensure backup aggregator has same decimals as primary if both are present
        if (address(oracle.backupAggregator) != address(0)) {
            if (oracle.aggregator.decimals() != oracle.backupAggregator.decimals()) {
                revert InvalidAssetOrOracle();
            }
        }

        // Store pending oracle with timelock
        uint64 validAt = uint64(block.timestamp + _timeLock);
        pendingOracles[asset] = PendingOracle({oracle: oracle, validAt: validAt});

        emit SubmitPendingOracle(
            asset,
            oracle.aggregator,
            oracle.backupAggregator,
            oracle.maxPrice,
            oracle.minPrice,
            oracle.heartbeat,
            oracle.backupHeartbeat,
            validAt
        );
    }

    /**
     * @inheritdoc IOracleV2
     */
    function acceptPendingOracle(address asset) external {
        if (pendingOracles[asset].validAt == 0) {
            revert NoPendingValue();
        }
        if (block.timestamp < pendingOracles[asset].validAt) {
            revert TimelockNotElapsed();
        }

        // Activate the pending oracle
        _updateOracle(asset, pendingOracles[asset].oracle);
        delete pendingOracles[asset];
    }

    function _updateOracle(address asset, Oracle memory oracle) internal {
        oracles[asset] = oracle;
        emit UpdateOracle(
            asset,
            oracle.aggregator,
            oracle.backupAggregator,
            oracle.maxPrice,
            oracle.minPrice,
            oracle.heartbeat,
            oracle.backupHeartbeat
        );
    }

    /**
     * @inheritdoc IOracleV2
     */
    function revokePendingOracle(address asset) external onlyOwner {
        if (pendingOracles[asset].validAt == 0) {
            revert NoPendingValue();
        }
        delete pendingOracles[asset];
        emit RevokePendingOracle(asset);
    }

    /**
     * @inheritdoc IOracleV2
     */
    function getPrice(address asset) public view virtual override returns (uint256, uint8) {
        Oracle memory oracle = oracles[asset];

        if (oracle.aggregator == AggregatorV3Interface(address(0))) {
            revert InvalidAssetOrOracle();
        }

        // Try primary oracle first
        {
            (, int256 answer,, uint256 updatedAt,) = oracle.aggregator.latestRoundData();
            // Check if primary oracle is fresh and has positive price
            if (oracle.heartbeat == 0 || oracle.heartbeat + updatedAt >= block.timestamp) {
                if (_checkAnswer(answer, oracle)) {
                    return (uint256(answer), oracle.aggregator.decimals());
                }
            }
        }

        // Try backup oracle if available
        if (address(oracle.backupAggregator) != address(0)) {
            (, int256 answer,, uint256 updatedAt,) = oracle.backupAggregator.latestRoundData();
            // Check if backup oracle is fresh and has positive price
            if (oracle.backupHeartbeat == 0 || oracle.backupHeartbeat + updatedAt >= block.timestamp) {
                if (_checkAnswer(answer, oracle)) {
                    return (uint256(answer), oracle.backupAggregator.decimals());
                }
            }
        }

        // Both oracles failed or are stale
        revert OracleIsNotWorking(asset);
    }

    function _checkAnswer(int256 answer, Oracle memory oracle) internal pure returns (bool) {
        if (answer < 0) {
            return false; // Negative prices are invalid
        } else if (oracle.maxPrice != 0 && answer > oracle.maxPrice) {
            return false; // Price exceeds maximum cap
        } else if (oracle.minPrice != 0 && answer < oracle.minPrice) {
            return false; // Price is below minimum floor
        }
        return true; // Price is valid
    }
}
"
    },
    "dependencies/@openzeppelin-contracts-5.2.0/access/Ownable2Step.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (access/Ownable2Step.sol)

pragma solidity ^0.8.20;

import {Ownable} from "./Ownable.sol";

/**
 * @dev Contract module which provides access control mechanism, where
 * there is an account (an owner) that can be granted exclusive access to
 * specific functions.
 *
 * This extension of the {Ownable} contract includes a two-step mechanism to transfer
 * ownership, where the new owner must call {acceptOwnership} in order to replace the
 * old one. This can help prevent common mistakes, such as transfers of ownership to
 * incorrect accounts, or to contracts that are unable to interact with the
 * permission system.
 *
 * The initial owner is specified at deployment time in the constructor for `Ownable`. This
 * can later be changed with {transferOwnership} and {acceptOwnership}.
 *
 * This module is used through inheritance. It will make available all functions
 * from parent (Ownable).
 */
abstract contract Ownable2Step is Ownable {
    address private _pendingOwner;

    event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner);

    /**
     * @dev Returns the address of the pending owner.
     */
    function pendingOwner() public view virtual returns (address) {
        return _pendingOwner;
    }

    /**
     * @dev Starts the ownership transfer of the contract to a new account. Replaces the pending transfer if there is one.
     * Can only be called by the current owner.
     *
     * Setting `newOwner` to the zero address is allowed; this can be used to cancel an initiated ownership transfer.
     */
    function transferOwnership(address newOwner) public virtual override onlyOwner {
        _pendingOwner = newOwner;
        emit OwnershipTransferStarted(owner(), newOwner);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`) and deletes any pending owner.
     * Internal function without access restriction.
     */
    function _transferOwnership(address newOwner) internal virtual override {
        delete _pendingOwner;
        super._transferOwnership(newOwner);
    }

    /**
     * @dev The new owner accepts the ownership transfer.
     */
    function acceptOwnership() public virtual {
        address sender = _msgSender();
        if (pendingOwner() != sender) {
            revert OwnableUnauthorizedAccount(sender);
        }
        _transferOwnership(sender);
    }
}
"
    },
    "contracts/v2/oracle/IOracleV2.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";

/**
 * @title IOracleV2
 * @author Term Structure Labs
 * @notice Enhanced oracle interface for TermMax V2 protocol with improved price feed management
 * @dev Extends the V1 oracle interface with additional features including price caps, separate backup heartbeats,
 * and oracle revocation capabilities for enhanced security and flexibility
 */
interface IOracleV2 {
    /**
     * @notice Oracle configuration structure for price feed management
     * @dev Contains primary and backup aggregators with independent heartbeat configurations
     * @param aggregator Primary price feed aggregator (required)
     * @param backupAggregator Secondary price feed aggregator for fallback (optional)
     * @param maxPrice Maximum allowed price value for this asset (0 = no limit)
     * @param minPrice Minimum allowed price value for this asset (0 = no limit)
     * @param heartbeat Maximum allowed staleness for primary aggregator in seconds (0 = no staleness check)
     * @param backupHeartbeat Maximum allowed staleness for backup aggregator in seconds (0 = no staleness check)
     */
    struct Oracle {
        AggregatorV3Interface aggregator;
        AggregatorV3Interface backupAggregator;
        int256 maxPrice;
        int256 minPrice;
        uint32 heartbeat;
        uint32 backupHeartbeat;
    }

    /**
     * @notice Error thrown when the oracle system cannot provide a reliable price
     * @dev Occurs when both primary and backup oracles are stale, returning invalid data, or when no oracle is configured
     * @param asset The address of the asset for which the oracle is not working
     */
    error OracleIsNotWorking(address asset);

    /**
     * @notice Retrieves the current price of an asset in USD from the oracle system
     * @dev Uses primary oracle first, falls back to backup if primary is stale or invalid
     * Applies maxPrice cap if configured. Returns price with the aggregator's native decimals
     * @param asset The address of the asset to get the price for
     * @return price The current price of the asset (may be capped by maxPrice)
     * @return decimals The number of decimal places in the returned price
     * @custom:reverts OracleIsNotWorking if no valid price can be obtained
     */
    function getPrice(address asset) external view returns (uint256 price, uint8 decimals);

    /**
     * @notice Submits a new oracle configuration for an asset with timelock protection
     * @dev Creates a pending oracle update that must wait for the timelock period before activation
     * Used for adding new oracles or updating existing ones with enhanced security
     * @param asset The address of the asset to configure the oracle for
     * @param oracle The oracle configuration structure with primary/backup feeds and settings
     * @custom:access Typically restricted to oracle managers or governance
     * @custom:security Subject to timelock delay for security
     */
    function submitPendingOracle(address asset, Oracle memory oracle) external;

    /**
     * @notice Activates a previously submitted pending oracle configuration
     * @dev Can only be called after the timelock period has elapsed since submission
     * Replaces the current oracle configuration with the pending one
     * @param asset The address of the asset to accept the pending oracle for
     * @custom:access Usually callable by anyone after timelock expires
     * @custom:validation Requires valid pending oracle and elapsed timelock
     */
    function acceptPendingOracle(address asset) external;

    /**
     * @notice Cancels a pending oracle configuration before it can be accepted
     * @dev Allows oracle managers to revoke pending updates if errors are discovered
     * Can only revoke pending oracles that haven't been accepted yet
     * @param asset The address of the asset to revoke the pending oracle for
     * @custom:access Typically restricted to oracle managers or governance
     * @custom:security Provides emergency mechanism to cancel erroneous oracle updates
     */
    function revokePendingOracle(address asset) external;
}
"
    },
    "contracts/v2/VersionV2.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

abstract contract VersionV2 {
    // Function to get the version number
    function getVersion() public pure virtual returns (string memory) {
        return "2.0.0";
    }
}
"
    },
    "dependencies/@openzeppelin-contracts-5.2.0/access/Ownable.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol)

pragma solidity ^0.8.20;

import {Context} from "../utils/Context.sol";

/**
 * @dev Contract module which provides a basic access control mechanism, where
 * there is an account (an owner) that can be granted exclusive access to
 * specific functions.
 *
 * The initial owner is set to the address provided by the deployer. This can
 * later be changed with {transferOwnership}.
 *
 * This module is used through inheritance. It will make available the modifier
 * `onlyOwner`, which can be applied to your functions to restrict their use to
 * the owner.
 */
abstract contract Ownable is Context {
    address private _owner;

    /**
     * @dev The caller account is not authorized to perform an operation.
     */
    error OwnableUnauthorizedAccount(address account);

    /**
     * @dev The owner is not a valid owner account. (eg. `address(0)`)
     */
    error OwnableInvalidOwner(address owner);

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /**
     * @dev Initializes the contract setting the address provided by the deployer as the initial owner.
     */
    constructor(address initialOwner) {
        if (initialOwner == address(0)) {
            revert OwnableInvalidOwner(address(0));
        }
        _transferOwnership(initialOwner);
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        _checkOwner();
        _;
    }

    /**
     * @dev Returns the address of the current owner.
     */
    function owner() public view virtual returns (address) {
        return _owner;
    }

    /**
     * @dev Throws if the sender is not the owner.
     */
    function _checkOwner() internal view virtual {
        if (owner() != _msgSender()) {
            revert OwnableUnauthorizedAccount(_msgSender());
        }
    }

    /**
     * @dev Leaves the contract without owner. It will not be possible to call
     * `onlyOwner` functions. Can only be called by the current owner.
     *
     * NOTE: Renouncing ownership will leave the contract without an owner,
     * thereby disabling any functionality that is only available to the owner.
     */
    function renounceOwnership() public virtual onlyOwner {
        _transferOwnership(address(0));
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Can only be called by the current owner.
     */
    function transferOwnership(address newOwner) public virtual onlyOwner {
        if (newOwner == address(0)) {
            revert OwnableInvalidOwner(address(0));
        }
        _transferOwnership(newOwner);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Internal function without access restriction.
     */
    function _transferOwnership(address newOwner) internal virtual {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}
"
    },
    "dependencies/@chainlink-contracts-1.2.0/src/v0.8/shared/interfaces/AggregatorV3Interface.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// solhint-disable-next-line interface-starts-with-i
interface AggregatorV3Interface {
  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);
}
"
    },
    "dependencies/@openzeppelin-contracts-5.2.0/utils/Context.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)

pragma solidity ^0.8.20;

/**
 * @dev Provides information about the current execution context, including the
 * sender of the transaction and its data. While these are generally available
 * via msg.sender and msg.data, they should not be accessed in such a direct
 * manner, since when dealing with meta-transactions the account sending and
 * paying for execution may not be the actual sender (as far as an application
 * is concerned).
 *
 * This contract is only required for intermediate, library-like contracts.
 */
abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

    function _msgData() internal view virtual returns (bytes calldata) {
        return msg.data;
    }

    function _contextSuffixLength() internal view virtual returns (uint256) {
        return 0;
    }
}
"
    }
  },
  "settings": {
    "remappings": [
      "@chainlink/contracts/=dependencies/@chainlink-contracts-1.2.0/",
      "@morpho/=dependencies/metamorpho-1.0.0/",
      "@openzeppelin/contracts-upgradeable/=dependencies/@openzeppelin-contracts-upgradeable-5.2.0/",
      "@openzeppelin/contracts/=dependencies/@openzeppelin-contracts-5.2.0/",
      "@pendle/core-v2/=dependencies/pendle-core-v2-1.0.0/",
      "@uniswap/v3-core/=dependencies/@uniswap-v3-core-1.0.2-solc-0.8-simulate/",
      "@uniswap/v3-periphery/=dependencies/@uniswap-v3-periphery-1.4.4/",
      "forge-std/=dependencies/forge-std-1.9.6/src/",
      "@chainlink-contracts-1.2.0/=dependencies/@chainlink-contracts-1.2.0/src/",
      "@openzeppelin-contracts-5.2.0/=dependencies/@openzeppelin-contracts-5.2.0/",
      "@openzeppelin-contracts-upgradeable-5.2.0/=dependencies/@openzeppelin-contracts-upgradeable-5.2.0/",
      "@uniswap-v3-core-1.0.2-solc-0.8-simulate/=dependencies/@uniswap-v3-core-1.0.2-solc-0.8-simulate/contracts/",
      "@uniswap-v3-periphery-1.4.4/=dependencies/@uniswap-v3-periphery-1.4.4/contracts/",
      "forge-std-1.9.6/=dependencies/forge-std-1.9.6/src/",
      "metamorpho-1.0.0/=dependencies/metamorpho-1.0.0/src/",
      "pendle-core-v2-1.0.0/=dependencies/pendle-core-v2-1.0.0/contracts/"
    ],
    "optimizer": {
      "enabled": true,
      "runs": 200
    },
    "metadata": {
      "useLiteralContent": false,
      "bytecodeHash": "ipfs",
      "appendCBOR": true
    },
    "outputSelection": {
      "*": {
        "*": [
          "evm.bytecode",
          "evm.deployedBytecode",
          "devdoc",
          "userdoc",
          "metadata",
          "abi"
        ]
      }
    },
    "evmVersion": "cancun",
    "viaIR": true
  }
}}

Tags:
Multisig, Multi-Signature, Factory, Oracle|addr:0xedb5dfb6393551faf499cf55494b1f6e44c2c612|verified:true|block:23445706|tx:0x0ebd870aadfa83be21730c896b509252e447e24400ae65d7eab0c2f28a3ff0c3|first_check:1758878232

Submitted on: 2025-09-26 11:17:15

Comments

Log in to comment.

No comments yet.