MultiVenueOracle
0x065ec88e0f00af22349e5f3905ba556ed5c312cf
Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
6,218 bytes
ABI entries
31
9 read · 9 write
License
none
Contract information
- Address
- 0x065ec88e0f00af22349e5f3905ba556ed5c312cf
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.24+commit.e11b9ed9
- Optimization
- Enabled
- Creator
- 0x0d45254857…2E1FA4aF74
- Creation tx
- 0xc7105ac6ca…8a3642da4d
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (9)
emaPricePer1e18(address) → uint256
emaWindow() → uint256
isKeeper(address) → bool
owner() → address
pendingOwner() → address
sourceOf(address) → uint8, address, bool, tuple
stateView() → address
valueInWeth(address, uint256) → uint256
weth() → address
Events (5)
KeeperSetOwnershipTransferStartedOwnershipTransferredParamsSourceSet
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "initialOwner",
"type": "address"
},
{
"internalType": "address",
"name": "weth_",
"type": "address"
},
{
"internalType": "address",
"name": "stateView_",
"type": "address"
},
{
"internalType": "uint256",
"name": "emaWindow_",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "BadPool",
"type": "error"
},
{
"inputs": [],
"name": "BadWindow",
"type": "error"
},
{
"inputs": [],
"name": "NoSource",
"type": "error"
},
{
"inputs": [],
"name": "NotKeeper",
"type": "error"
},
{
"inputs": [],
"name": "NotReady",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "OwnableInvalidOwner",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "OwnableUnauthorizedAccount",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "keeper",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "enabled",
"type": "bool"
}
],
"name": "KeeperSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferStarted",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "stateView",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "emaWindow",
"type": "uint256"
}
],
"name": "Params",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": false,
"internalType": "enum MultiVenueOracle.Venue",
"name": "venue",
"type": "uint8"
},
{
"indexed": false,
"internalType": "address",
"name": "pool",
"type": "address"
}
],
"name": "SourceSet",
"type": "event"
},
{
"inputs": [],
"name": "acceptOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "emaPricePer1e18",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "emaWindow",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "isKeeper",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pendingOwner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "keeper",
"type": "address"
},
{
"internalType": "bool",
"name": "enabled",
"type": "bool"
}
],
"name": "setKeeper",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "stateView_",
"type": "address"
},
{
"internalType": "uint256",
"name": "emaWindow_",
"type": "uint256"
}
],
"name": "setParams",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "pair",
"type": "address"
}
],
"name": "setV2Source",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "pool",
"type": "address"
}
],
"name": "setV3Source",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"components": [
{
"internalType": "address",
"name": "currency0",
"type": "address"
},
{
"internalType": "address",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "address",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
}
],
"name": "setV4Source",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "sourceOf",
"outputs": [
{
"internalType": "enum MultiVenueOracle.Venue",
"name": "venue",
"type": "uint8"
},
{
"internalType": "address",
"name": "pool",
"type": "address"
},
{
"internalType": "bool",
"name": "quoteIsToken0",
"type": "bool"
},
{
"components": [
{
"internalType": "address",
"name": "currency0",
"type": "address"
},
{
"internalType": "address",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "address",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "v4Key",
"type": "tuple"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "stateView",
"outputs": [
{
"internalType": "contract IStateView",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "update",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "valueInWeth",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "weth",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {Governed} from "../access/Governed.sol";
import {IPriceOracle} from "../interfaces/IPriceOracle.sol";
import {IStateView, PoolKey} from "../interfaces/IUniswapV4.sol";
interface IV3PoolMinimal {
function slot0() external view returns (uint160 sqrtPriceX96, int24, uint16, uint16, uint16, uint8, bool);
function token0() external view returns (address);
}
interface IV2PairMinimal {
function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32);
function token0() external view returns (address);
}
/// @title MultiVenueOracle
/// @notice Prices each constituent in WETH for the IndexManager's market-cap weighting, reading from whichever
/// Uniswap venue that token actually trades on (v2 reserves / v3 slot0 / v4 StateView) — because the
/// RH memes are split across v2/v3/v4. Keeps a keeper-sampled EMA of the price (per 1e18 token, in
/// WETH) for manipulation resistance. Native ETH is treated as WETH (≈1:1) for pricing.
///
/// @dev Price math validated on-chain against the real CashCat v3 pool (matches the v3 Quoter). Set each
/// token's source (owner). `update` folds the current spot into the EMA; `valueInWeth` uses the EMA.
contract MultiVenueOracle is IPriceOracle, Governed {
uint256 private constant Q96 = 2 ** 96;
address private constant NATIVE = address(0);
enum Venue {
NONE,
V2,
V3,
V4
}
struct Source {
Venue venue;
address pool; // v2 pair / v3 pool
bool quoteIsToken0; // is WETH/ETH currency0 in the pool?
PoolKey v4Key; // v4 only
}
address public immutable weth;
IStateView public stateView; // v4
uint256 public emaWindow;
mapping(address => Source) public sourceOf;
mapping(address => uint256) public emaPricePer1e18; // WETH per 1e18 token (EMA)
event SourceSet(address indexed token, Venue venue, address pool);
event Params(address stateView, uint256 emaWindow);
error NoSource();
error NotReady();
error BadWindow();
error BadPool();
constructor(address initialOwner, address weth_, address stateView_, uint256 emaWindow_)
Governed(initialOwner)
{
if (emaWindow_ == 0) revert BadWindow();
weth = weth_;
stateView = IStateView(stateView_);
emaWindow = emaWindow_;
}
function setParams(address stateView_, uint256 emaWindow_) external onlyOwner {
if (emaWindow_ == 0) revert BadWindow();
stateView = IStateView(stateView_);
emaWindow = emaWindow_;
emit Params(stateView_, emaWindow_);
}
function setV2Source(address token, address pair) external onlyOwner {
bool q0 = IV2PairMinimal(pair).token0() == weth;
if (!q0 && IV2PairMinimal(pair).token0() == token) {
// ok, weth is token1
}
sourceOf[token] = Source(Venue.V2, pair, q0, _emptyKey());
emit SourceSet(token, Venue.V2, pair);
}
function setV3Source(address token, address pool) external onlyOwner {
bool q0 = IV3PoolMinimal(pool).token0() == weth;
sourceOf[token] = Source(Venue.V3, pool, q0, _emptyKey());
emit SourceSet(token, Venue.V3, pool);
}
function setV4Source(address token, PoolKey calldata key) external onlyOwner {
bool quoteIs0 = (key.currency0 == weth || key.currency0 == NATIVE);
bool quoteIs1 = (key.currency1 == weth || key.currency1 == NATIVE);
if (!quoteIs0 && !quoteIs1) revert BadPool();
sourceOf[token] = Source(Venue.V4, address(0), quoteIs0, key);
emit SourceSet(token, Venue.V4, address(0));
}
// --- IPriceOracle -------------------------------------------------------
function update(address token) external {
uint256 spot = _spotPricePer1e18(token);
if (spot == 0) return;
uint256 ema = emaPricePer1e18[token];
emaPricePer1e18[token] = ema == 0 ? spot : (ema * (emaWindow - 1) + spot) / emaWindow;
}
function valueInWeth(address token, uint256 amount) external view returns (uint256) {
if (token == weth) return amount;
uint256 px = emaPricePer1e18[token];
if (px == 0) revert NotReady();
return Math.mulDiv(amount, px, 1e18);
}
/// @notice Current spot WETH value of 1e18 units of `token`, read from its venue.
function _spotPricePer1e18(address token) internal view returns (uint256) {
Source memory s = sourceOf[token];
if (s.venue == Venue.NONE) revert NoSource();
if (s.venue == Venue.V2) {
(uint112 r0, uint112 r1,) = IV2PairMinimal(s.pool).getReserves();
(uint256 rQuote, uint256 rToken) = s.quoteIsToken0 ? (uint256(r0), uint256(r1)) : (uint256(r1), uint256(r0));
if (rToken == 0) return 0;
return Math.mulDiv(1e18, rQuote, rToken);
}
uint160 sqrtP;
if (s.venue == Venue.V3) {
(sqrtP,,,,,,) = IV3PoolMinimal(s.pool).slot0();
} else {
(sqrtP,,,) = stateView.getSlot0(keccak256(abi.encode(s.v4Key)));
}
if (sqrtP == 0) return 0;
// price(token1 per token0) = (sqrtP/2^96)^2. value of 1e18 token in WETH:
if (s.quoteIsToken0) {
// quote(WETH/ETH)=token0, meme=token1 → weth per meme = 1/price → value = 1e18 * (2^96/sqrtP)^2
return Math.mulDiv(Math.mulDiv(1e18, Q96, sqrtP), Q96, sqrtP);
} else {
// meme=token0, quote=token1 → weth per meme = price → value = 1e18 * (sqrtP/2^96)^2
return Math.mulDiv(Math.mulDiv(1e18, sqrtP, Q96), sqrtP, Q96);
}
}
function _emptyKey() private pure returns (PoolKey memory) {
return PoolKey(address(0), address(0), 0, 0, address(0));
}
}
Chain explorer3112msChain node94ms