Documentation

The Oracle

Levant has no order book and no third-party price feed. Every open, close, take-profit, stop-loss and liquidation settles against a price that a whitelisted signer has cryptographically signed — and the Diamond verifies that signature on-chain before a single token moves.

A self-hosted, signed-price oracle

Levant prices trades with the SignedPriceOracleFacet — a self-hosted, EIP-712 m-of-n signed-price oracle that lives inside the Diamond. There is no external price feed to trust: a whitelisted signer puts its ECDSA signature on a compact price report, and the contract verifies that signature on-chain at the moment it settles your trade. This is not a decentralized oracle network (DON) or a Chainlink data feed — Robinhood Chain mainnet has no production feeds, so Levant runs its own price layer and authenticates it with signatures instead of a third party.

The facet exposes the exact same getVerifiedPrice(bytes32,bytes) selector that a Chainlink Data Streams adapter would, so it is a drop-in swap via `diamondCut`: the TradingFacet, the keeper and the frontend never change. Trust moves from a DON to the signer set; everything else is identical. If a production oracle ever becomes available on this chain, swapping to it is a single upgrade that preserves the vault, LP shares and every open position.

Why prices must be unforgeable

Liquidation on Levant is permissionlessliquidatePosition accepts a price report from *any* caller. That is only safe if a price cannot be fabricated. An unauthenticated oracle would let anyone liquidate anyone at an invented price. So every price the engine acts on is an ECDSA signature over an EIP-712 digest, produced by a signer the owner has explicitly whitelisted. A report from an unknown key is rejected before it can touch a position.

The price report

Each report is a tiny EIP-712 typed struct — PriceReport — that binds three fields. The signature covers all three *and* the EIP-712 domain, so a signed report is valid for exactly one market, one price, one moment in time, on one chain, at one contract.

FieldTypeMeaning
marketIdbytes32Which market the price is for. Checked against the market the caller asked to price — a BTC report cannot settle an ETH trade.
price1e18uint256The price, scaled by 1e18 (18-decimal fixed point). Must be non-zero.
publishTimeuint64The chain-time second the price was signed. Governs staleness and future-skew rejection.

The digest the signer signs — and the Diamond re-derives — is built from the EIP-712 domain and the struct. The domain binds the chainId and the verifyingContract (the Diamond itself), which is what makes a report un-replayable across chains or contracts:

DOMAIN_SEPARATOR = keccak256(abi.encode(
    keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
    keccak256("Levant"),                              // name
    keccak256("1"),                                   // version
    4663,                                            // chainId  — Robinhood Chain mainnet
    0xE07098bc29Ebe50E6203606bc2eA0e1E189cb7e2        // verifyingContract — the Diamond
))

structHash = keccak256(abi.encode(
    keccak256("PriceReport(bytes32 marketId,uint256 price1e18,uint64 publishTime)"),
    marketId, price1e18, publishTime
))

digest = keccak256(0x1901 ++ DOMAIN_SEPARATOR ++ structHash)   // EIP-712 typed-data hash

The price rides *inside* the report bytes the keeper hands the contract, so an open, close or liquidation is always a single atomic transaction — the price and the action commit together:

signedReport = abi.encode(
    marketId,          // bytes32
    price1e18,          // uint256, price x 1e18
    publishTime,        // uint64,  chain-time seconds
    [sig0, sig1, ...]   // bytes[],  one signature per signer, sorted ascending by signer address
)

What the contract checks

Before any price is returned to the trading engine, getVerifiedPrice runs every check below. If any fails, the whole transaction reverts — the trade does not fill and no fee is taken.

CheckWhat it prevents
ECDSA over the EIP-712 digest; signer whitelisted by the ownerForged prices — a report from anyone but an authorized signer is rejected (NotSigner)
m-of-n signature thresholdA single rogue key settling a trade on its own — a report carrying fewer than the threshold's worth of signatures is rejected (ThresholdNotMet)
Recovered signers must strictly ascendOne key signing m times to fake a quorum — strict ordering dedupes without a memory set (SignersOutOfOrder)
marketId is inside the signed struct and checked against the callerCross-market replay — pricing one market with another market's report (MarketMismatch)
chainId + verifyingContract in the EIP-712 domainCross-chain / cross-contract replay of a valid report
publishTime + maxPriceAge >= block.timestamp (60s)Settling against a stale price (StalePrice)
publishTime <= block.timestamp + 5sFuture-dated / clock-skew tricks (FutureTimestamp)
OpenZeppelin ECDSA rejects high-s and bad vSignature malleability

The two time checks are the freshness envelope. A report is valid only inside a narrow window around the current chain time:

reject FutureTimestamp  if  publishTime  >  block.timestamp + 5      // 5s proposer-skew allowance
reject StalePrice       if  block.timestamp  >  publishTime + 60     // maxPriceAge = 60s on the live deployment

// A price older than 60 seconds cannot settle a trade.
// A price dated more than 5 seconds in the future cannot either.
Note
The 60-second max price age means a stuck or lagging keeper can never settle an old price — its reports simply expire. The max price age is owner-configurable but can never be set to zero (a zero window could never be satisfied), which would freeze every open, close and liquidation.

m-of-n signatures and the signer set

The oracle supports an m-of-n threshold: a report must carry at least threshold signatures, each from a distinct whitelisted signer, sorted in strictly ascending address order. The strict ordering is what dedupes them — one key cannot satisfy a 2-of-3 threshold by signing twice. The owner manages the set on-chain: addPriceSigner / removePriceSigner adjust the roster and setSignerThreshold moves the bar. A signer can never be removed if doing so would drop the count below the threshold, and the threshold is always constrained to 1..signerCount — both guards exist so pricing can never be accidentally frozen.

Warning
Today the oracle is 1-of-1. The keeper is the only whitelisted signer (0x5040b3387F450Af658f8E7C93ca3F0D44b07E7e4), so the price that settles your trade is whatever that single key signs — its own multi-source quote, authenticated. The on-chain checks constrain freshness, timing, market and chain binding, and signer identity, but they cannot detect a plausible-but-wrong price from the authorized signer. You are trusting the keeper's price sourcing and the security of its key. m-of-n across independent machines is the roadmap, and it is what removes this single point of trust. Ownership still sits on the deployer key, the code is unaudited, and testnet tokens have no monetary value.

Where the price comes from

The number the keeper signs is drawn from a multi-source spot feed with ordered fallback, so a single upstream outage cannot stall fills, take-profit/stop-loss or liquidations. For each market it tries Gate.io first, then KuCoin, then CoinGecko as a last resort — the first source to return a valid quote wins, and each source only fills the markets the earlier ones missed. Every request has an 8-second timeout. If *every* provider fails and nothing is collected, the keeper keeps its last known prices rather than signing a bad one. (Pyth is deliberately not used: from 2026-07-31 its price-feed API requires a paid plan.)

Tip
The price feed lives entirely inside the off-chain keeper on a server — it is never exposed to the browser. The client never queries Gate.io, KuCoin or CoinGecko and never holds a signing key, so no user or webpage can inject a price into the system.

There is an important distinction from a DON-style feed. In the live signed-oracle mode, the keeper's own price is both the trigger (it decides *when* to fill an order, hit a stop, or liquidate) and the settlement price it signs into the report. The oracle authenticates who produced the number and that it is fresh and correctly bound — it does not independently source the number itself.

Chain-time signing

The only clock a contract can read is block.timestamp, and it is not wall-clock time. Robinhood Chain mainnet runs its block timestamps roughly 52 seconds behind the host clock. A report stamped with ordinary Date.now() would therefore look future-dated to the contract and be rejected (FutureTimestamp), reverting every fill, close and liquidation. So the keeper signs in chain time: it tracks the chain's offset from its own clock, resyncs about every 10 seconds by reading the latest block, and stamps each publishTime in the chain's frame of reference. A failed resync keeps the last known offset rather than stalling the keeper, so a transient RPC hiccup never halts pricing.

The oracle on-chain

Everything the oracle does is inspectable on-chain. priceDomainSeparator() returns the exact domain the contract will verify against; isPriceSigner, priceSignerCount, signerThreshold and maxPriceAge expose the full policy. The relevant addresses on Robinhood Chain mainnet (chainId 4663):

ComponentAddress
Diamond (EIP-712 verifyingContract — the address you interact with)0xE07098bc29Ebe50E6203606bc2eA0e1E189cb7e2
SignedPriceOracleFacet (verification logic)0x7BCa681C11ac4dc822095457662dCcD3004349F5
Whitelisted signer (the keeper — sole signer today)0x5040b3387F450Af658f8E7C93ca3F0D44b07E7e4
Owner (manages the signer set and threshold)0x520F136b72Cc7A24FDB1C073189704442e9945F6

The facet is part of the same EIP-2535 Diamond as the rest of Levant, built with Foundry (solc 0.8.28) and covered by the protocol's 171 passing tests. This is unaudited testnet software: prices are authenticated, not guaranteed, the oracle is 1-of-1 today, and nothing here is investment advice.