Created
September 3, 2025 06:04
-
-
Save cNoveron/e2a47beea640caed202393e8e957eb77 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // SPDX-License-Identifier: GPL-3.0 | |
| pragma solidity ^0.5.16; | |
| pragma experimental ABIEncoderV2; | |
| import "./Base/XTokenOracle.sol"; | |
| import "./Uniswap/UniswapAnchoredView.sol"; | |
| import "./Chainlink/ChainlinkPriceOracle.sol"; | |
| contract LXOracle is XTokenOracle { | |
| address public admin; | |
| address public pendingAdmin; | |
| enum PriceSource { | |
| INVALID, /// default value in mapping | |
| FIXED_USD, /// implies the fixedPrice is a constant multiple of the USD price (which is 1) | |
| REPORTED_XSD, | |
| REPORTED_LEXE, /// implies the price is set by the reporter | |
| REPORTED, | |
| CHAINLINK, | |
| UNISWAP | |
| } | |
| uint256 packedPrice; | |
| /// @notice The Open Oracle Reporter | |
| address public reporter; | |
| /// @notice Circuit breaker for using anchor price oracle directly, ignoring reporter | |
| bool public reporterInvalidated; | |
| bytes32 constant rotateHash = keccak256(abi.encodePacked("rotate")); | |
| /** | |
| * @notice Emitted when pendingAdmin is changed | |
| */ | |
| event NewPendingAdmin(address oldPendingAdmin, address newPendingAdmin); | |
| /** | |
| * @notice Emitted when pendingAdmin is accepted, which means admin is updated | |
| */ | |
| event NewAdmin(address oldAdmin, address newAdmin); | |
| event ReporterChanged(address oldReporter, address newReporter); | |
| /// @notice The event emitted when reporter invalidates itself | |
| event ReporterInvalidated(address reporter); | |
| mapping(address => PriceSource) public sources; | |
| enum UnitSystem { | |
| DEFAULT_NONE, | |
| USD18_ATOMS_PER_TOKEN18_1UNIT, | |
| // min USD18 atoms per 1e18 token18 atoms: 1 | |
| // min USD18 atoms per 1 token18 atom: 1 / 1e18 = 1e-18 | |
| // max token18 atoms per 1 USD18 atom: 1 / 1e-18 = 1e18 | |
| // max token18 atoms per 1 USD18 atom: 1e18 / 1e(18-18) = 1e18 | |
| // max token18 atoms per 1 USD18 unit: 1e18 * 1e18 = 1e36 | |
| // max token18 units per 1 USD18 unit: 1e36 / 1e18 = 1e18 | |
| USD18_ATOMS_PER_TOKEN8_1UNIT, | |
| // min USD18 atoms per 1e8 token8 atoms: 1 | |
| // min USD18 atoms per 1 token8 atom: 1 / 1e8 = 1e-8 | |
| // max token18 atoms per 1 USD18 atom: 1 / 1e-18 = 1e18 | |
| // max token8 atoms per 1 USD18 atom: 1e18 / 1e(18-8) = 1e8 | |
| // max token8 atoms per 1 USD18 unit: 1e8 * 1e18 = 1e26 | |
| // max token8 units per 1 USD18 unit: 1e26 / 1e8 = 1e18 | |
| USD18_ATOMS_PER_TOKEN6_1UNIT, | |
| // min USD18 atoms per 1e6 token6 atoms: 1 | |
| // min USD18 atoms per 1 token6 atom: 1 / 1e6 = 1e-6 | |
| // max token18 atoms per 1 USD18 atom: 1 / 1e-18 = 1e18 | |
| // max token6 atoms per 1 USD18 atom: 1e18 / 1e(18-6) = 1e6 | |
| // max token6 atoms per 1 USD18 unit: 1e6 * 1e18 = 1e24 | |
| // max token6 units per 1 USD18 unit: 1e24 / 1e6 = 1e18 | |
| USD18_ATOMS_PER_TOKEN18_1ATOM, | |
| // min USD18 atoms per 1 token18 atom: 1 / 1e(18-18) = 1 | |
| // max token18 atoms per 1 USD18 atom: 1 / 1 = 1 | |
| // max token18 atoms per 1 USD18 atom: 1 / 1 = 1 | |
| // max token18 atoms per 1 USD18 unit: 1 * 1e18 = 1e18 | |
| // max token18 units per 1 USD18 unit: 1e18 / 1e18 = 1 | |
| USD18_ATOMS_PER_TOKEN8_1ATOM, | |
| // min USD18 atoms per 1 token18 atom: 1 / 1e(18-8) = 1e-10 | |
| // max token18 atoms per 1 USD18 atom: 1e10 / 1 = 1e10 | |
| // max token8 atoms per 1 USD18 atom: 1e10 / 1e10 = 1 | |
| // max token8 atoms per 1 USD18 unit: 1 * 1e18 = 1e18 | |
| // max token8 units per 1 USD18 unit: 1e18 / 1e8 = 1e10 | |
| USD18_ATOMS_PER_TOKEN6_1ATOM, | |
| // min USD18 atoms per 1 token18 atom: 1 / 1e(18-6) = 1e-12 | |
| // max token18 atoms per 1 USD18 atom: 1e12 / 1 = 1e12 | |
| // max token6 atoms per 1 USD18 atom: 1e12 / 1e12 = 1 | |
| // max token6 atoms per 1 USD18 unit: 1 * 1e18 = 1e18 | |
| // max token6 units per 1 USD18 unit: 1e18 / 1e6 = 1e12 | |
| TOKEN18_ATOMS_PER_USD18_1UNIT, | |
| // min token18 atoms per 1e18 USD18 atoms: 1e(18-18) / 1e18 = 1e-18 | |
| // max USD18 atoms per 1 token18 atom: 1 / 1e-18 = 1e18 | |
| // max USD18 atoms per 1 token18 atom: 1e(18-18) * 1e18 = 1e18 | |
| // max USD18 atoms per 1 token18 unit: 1e18 * 1e18 = 1e36 | |
| // max USD18 units per 1 token18 unit: 1e36 / 1e18 = 1e18 | |
| TOKEN8_ATOMS_PER_USD18_1UNIT, | |
| // min token18 atoms per 1 USD18 atom: 1e(18-8) / 1e18 = 1e-8 | |
| // max USD18 atoms per 1 token18 atom: 1 / 1e-8 = 1e8 | |
| // max USD18 atoms per 1 token8 atom: 1e(18-8) * 1e8 = 1e18 | |
| // max USD18 atoms per 1 token8 unit: 1e8 * 1e18 = 1e26 | |
| // max USD18 units per 1 token8 unit: 1e26 / 1e18 = 1e8 | |
| TOKEN6_ATOMS_PER_USD18_1UNIT, | |
| // min token18 atoms per 1 USD18 unit: 1 | |
| // min token18 atoms per 1e18 USD18 atoms: 1e(18-6) / 1e18 = 1e-6 | |
| // max USD18 atoms per 1 token18 atom: 1 / 1e-6 = 1e6 | |
| // max USD18 atoms per 1 token6 atom: 1e(18-6) * 1e6 = 1e18 | |
| // max USD18 atoms per 1 token6 unit: 1e6 * 1e18 = 1e24 | |
| // max USD18 units per 1 token6 unit: 1e24 / 1e18 = 1e6 | |
| TOKEN18_ATOMS_PER_USD18_1ATOM, | |
| // min token18 atoms per 1 USD18 atom: 1e(18-18) / 1 = 1 | |
| // max USD18 atoms per 1 token18 atom: 1 / 1 = 1 | |
| // max USD18 atoms per 1 token18 atom: (18-18) * 1 = 1 | |
| // max USD18 atoms per 1 token18 unit: 1e18 * 1 = 1e18 | |
| // max USD18 units per 1 token18 unit: 1e18 / 1e18 = 1 | |
| TOKEN8_ATOMS_PER_USD18_1ATOM, | |
| // min token18 atoms per 1 USD18 atom: 1e(18-8) / 1 = 1e10 | |
| // max USD18 atoms per 1 token18 atom: 1 / 1e10 = 1e-10 | |
| // max USD18 atoms per 1 token8 atom: 1e-10 * 1e10 = 1 | |
| // max USD18 atoms per 1 token8 unit: 1e8 * 1 = 1e8 | |
| // max USD18 units per 1 token8 unit: 1e8 / 1e18 = 1e-10 | |
| TOKEN6_ATOMS_PER_USD18_1ATOM | |
| // min token18 atoms per 1 USD18 atom: 1e(18-6) / 1 = 1e12 | |
| // max USD18 atoms per 1 token18 atom: 1 / 1e12 = 1e-12 | |
| // max USD18 atoms per 1 token6 atom: 1e(18-6) * 1e-12 = 1 | |
| // max USD18 atoms per 1 token6 unit: 1e6 * 1 = 1e6 | |
| // max USD18 units per 1 token6 unit: 1e6 / 1e18 = 1e-12 | |
| } | |
| struct PriceInUnitSystem { | |
| uint256 price; | |
| UnitSystem unitSystem; | |
| } | |
| // mapping(address => PriceInUnitSystem) public reportedPrices; | |
| mapping(address => uint256) public reportedPrices; | |
| ChainlinkPriceOracle chainlinkOracle; | |
| UniswapAnchoredView uniswapAnchor; | |
| /// @notice The highest ratio of the new price to the anchor price that will still trigger the price to be updated | |
| uint256 public upperBoundAnchorRatio; | |
| /// @notice The lowest ratio of the new price to the anchor price that will still trigger the price to be updated | |
| uint256 public lowerBoundAnchorRatio; | |
| /** | |
| * @notice Construct a uniswap anchored view for a set of token configurations | |
| * @dev Note that to avoid immature TWAPs, the system must run for at least a single anchorPeriod before using. | |
| * @param anchorToleranceMantissa_ The percentage tolerance that the reporter may deviate from the uniswap anchor | |
| */ | |
| constructor( | |
| address _admin, | |
| address _reporter, | |
| uint256 anchorToleranceMantissa_, | |
| PriceSource[] memory _feedTypes, | |
| address[] memory tokenAddresses, | |
| string memory _xNativeTokenSymbol, | |
| address[] memory chainlinkXTokenAddresses | |
| ) XTokenOracle(_xNativeTokenSymbol) public { | |
| require( | |
| _feedTypes.length == tokenAddresses.length, | |
| "_feedTypes not the same length as tokenAddresses" | |
| ); | |
| admin = _admin; | |
| reporter = _reporter; | |
| chainlinkOracle = new ChainlinkPriceOracle( | |
| _xNativeTokenSymbol | |
| ); | |
| for (uint256 i = 0; i < chainlinkXTokenAddresses.length; i++) { | |
| // console.logString("chainlinkTokenAddresses[i]"); | |
| // console.logAddress(chainlinkTokenAddresses[i]); | |
| sources[chainlinkXTokenAddresses[i]] = PriceSource.CHAINLINK; | |
| } | |
| for (uint256 i = 0; i < _feedTypes.length; i++) { | |
| // console.logString("tokenAddresses[i]"); | |
| // console.logAddress(tokenAddresses[i]); | |
| // console.logString("_feedTypes[i]"); | |
| // console.log(uint(_feedTypes[i])); | |
| sources[tokenAddresses[i]] = _feedTypes[i]; | |
| } | |
| // Allow the tolerance to be whatever the deployer chooses, but prevent under/overflow (and prices from being 0) | |
| upperBoundAnchorRatio = anchorToleranceMantissa_ > uint256(-1) - 100e16 | |
| ? uint256(-1) | |
| : 100e16 + anchorToleranceMantissa_; | |
| lowerBoundAnchorRatio = anchorToleranceMantissa_ < 100e16 | |
| ? 100e16 - anchorToleranceMantissa_ | |
| : 1; | |
| } | |
| function getUnderlyingPrice(XToken xToken) external view returns (uint256) { | |
| address underlying = _getUnderlyingAddress(xToken); | |
| PriceSource source = sources[address(xToken)]; | |
| if (source == PriceSource.INVALID) { | |
| revert("This token is not supported by this oracle"); | |
| } else if (source == PriceSource.FIXED_USD) { | |
| return 1e18; | |
| } else if (source == PriceSource.CHAINLINK) { | |
| string memory symbol = xToken.symbol(); | |
| bool isWbtcUsd = compareStrings(symbol, 'xWBTC'); | |
| if (isWbtcUsd) return chainlinkOracle.wbtcUsd(); | |
| bool isUsdcUsd = compareStrings(symbol, 'xUSDC'); | |
| if (isUsdcUsd) return chainlinkOracle.usdcUsd(); | |
| bool isEthUsd = compareStrings(symbol, 'xETH'); | |
| if (isEthUsd) return chainlinkOracle.ethUsd(); | |
| } else if (source == PriceSource.REPORTED_XSD) { | |
| uint256 _packedPrice = packedPrice; | |
| require( | |
| !reporterInvalidated, | |
| "LXOracle: The reporter has been invalidated" | |
| ); | |
| uint256 price = (_packedPrice >> 128); | |
| return price; | |
| } else if (source == PriceSource.REPORTED_LEXE) { | |
| uint256 _packedPrice = packedPrice; | |
| require( | |
| !reporterInvalidated, | |
| "LXOracle: The reporter has been invalidated" | |
| ); | |
| uint256 price = uint256(uint128(_packedPrice)); | |
| return price; | |
| } else if (source == PriceSource.REPORTED) { | |
| /* this shall be refactored to return a struct */ | |
| return reportedPrices[underlying];//.price; | |
| } | |
| } | |
| modifier onlyReporter() { | |
| if (msg.sender != reporter) { | |
| return; | |
| } | |
| _; | |
| } | |
| function setXsdLexePackedPrice(uint128 xsdPrice, uint128 lexePrice) | |
| external | |
| onlyReporter | |
| { | |
| packedPrice = (uint256(xsdPrice) << 128) + uint256(lexePrice); | |
| } | |
| function reportPrice(XToken xToken, uint256 price/* , UnitSystem unitSystem */) | |
| public | |
| onlyReporter | |
| { | |
| reportedPrices[address(xToken)] = price; | |
| // reportedPrices[asset] = PriceInUnitSystem({ | |
| // price: price, | |
| // unitSystem: unitSystem | |
| // }); | |
| } | |
| function isWithinAnchor(uint256 reporterPrice, uint256 anchorPrice) | |
| internal | |
| view | |
| returns (bool) | |
| { | |
| if (reporterPrice > 0) { | |
| uint256 anchorRatio = mul(anchorPrice, 100e16) / reporterPrice; | |
| return | |
| anchorRatio <= upperBoundAnchorRatio && | |
| anchorRatio >= lowerBoundAnchorRatio; | |
| } | |
| return false; | |
| } | |
| function _setReporter(address _reporter) external { | |
| require(msg.sender == admin, "Only admin can set reporter!"); | |
| address oldReporter = reporter; | |
| reporter = _reporter; | |
| emit ReporterChanged(oldReporter, _reporter); | |
| } | |
| function _deactivateReporter() external { | |
| require( | |
| msg.sender == admin, | |
| "Only admin can deactivate reporter through this function!" | |
| ); | |
| reporterInvalidated = true; | |
| emit ReporterInvalidated(reporter); | |
| } | |
| /** | |
| * @notice Invalidate the reporter | |
| */ | |
| function invalidateReporter( | |
| bytes calldata message, | |
| bytes calldata signature | |
| ) external { | |
| (string memory decodedMessage, ) = abi.decode( | |
| message, | |
| (string, address) | |
| ); | |
| require( | |
| keccak256(abi.encodePacked(decodedMessage)) == rotateHash, | |
| "invalid message must be 'rotate'" | |
| ); | |
| require( | |
| source(message, signature) == reporter, | |
| "invalidation message must come from the reporter" | |
| ); | |
| reporterInvalidated = true; | |
| emit ReporterInvalidated(reporter); | |
| } | |
| /** | |
| * @notice Begins transfer of admin rights. The newPendingAdmin must call `_acceptAdmin` to finalize the transfer. | |
| * @dev Admin function to begin change of admin. The newPendingAdmin must call `_acceptAdmin` to finalize the transfer. | |
| * @param newPendingAdmin New pending admin. | |
| * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) | |
| */ | |
| function _setPendingAdmin(address newPendingAdmin) public { | |
| require( | |
| msg.sender == admin, | |
| "LXOracle: Only admin can set pending admin" | |
| ); | |
| // Save current value, if any, for inclusion in log | |
| address oldPendingAdmin = pendingAdmin; | |
| // Store pendingAdmin with value newPendingAdmin | |
| pendingAdmin = newPendingAdmin; | |
| // Emit NewPendingAdmin(oldPendingAdmin, newPendingAdmin) | |
| emit NewPendingAdmin(oldPendingAdmin, newPendingAdmin); | |
| } | |
| /** | |
| * @notice Accepts transfer of admin rights. msg.sender must be pendingAdmin | |
| * @dev Admin function for pending admin to accept role and update admin | |
| * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) | |
| */ | |
| function _acceptAdmin() public { | |
| // Check caller is pendingAdmin and pendingAdmin ≠ address(0) | |
| require( | |
| msg.sender == pendingAdmin && msg.sender != address(0), | |
| "LXOracle: Only pending admin can accept admin" | |
| ); | |
| // Save current values for inclusion in log | |
| address oldAdmin = admin; | |
| address oldPendingAdmin = pendingAdmin; | |
| // Store admin with value pendingAdmin | |
| admin = pendingAdmin; | |
| // Clear the pending value | |
| pendingAdmin = address(0); | |
| emit NewAdmin(oldAdmin, admin); | |
| emit NewPendingAdmin(oldPendingAdmin, pendingAdmin); | |
| } | |
| /** | |
| * @notice Recovers the source address which signed a message | |
| * @dev Comparing to a claimed address would add nothing, | |
| * as the caller could simply perform the recover and claim that address. | |
| * @param message The data that was presumably signed | |
| * @param signature The fingerprint of the data + private key | |
| * @return The source address which signed the message, presumably | |
| */ | |
| function source(bytes memory message, bytes memory signature) | |
| public | |
| pure | |
| returns (address) | |
| { | |
| (bytes32 r, bytes32 s, uint8 v) = abi.decode( | |
| signature, | |
| (bytes32, bytes32, uint8) | |
| ); | |
| bytes32 hash = keccak256( | |
| abi.encodePacked( | |
| "\x19Ethereum Signed Message:\n32", | |
| keccak256(message) | |
| ) | |
| ); | |
| return ecrecover(hash, v, r, s); | |
| } | |
| /// @dev Overflow proof multiplication | |
| function mul(uint256 a, uint256 b) internal pure returns (uint256) { | |
| if (a == 0) return 0; | |
| uint256 c = a * b; | |
| require(c / a == b, "multiplication overflow"); | |
| return c; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment