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
}
}}
Submitted on: 2025-09-26 11:17:15
Comments
Log in to comment.
No comments yet.