Skip to content

Instantly share code, notes, and snippets.

@cNoveron
Created September 3, 2025 06:04
Show Gist options
  • Select an option

  • Save cNoveron/e2a47beea640caed202393e8e957eb77 to your computer and use it in GitHub Desktop.

Select an option

Save cNoveron/e2a47beea640caed202393e8e957eb77 to your computer and use it in GitHub Desktop.
// 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