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
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