Skip to content

Add live (fresh-accrual) reads via touch()+view Multicall3 eth_call simulation #56

Description

@Praz314159

Background

Perp.rates() (and other accrual-dependent state: EMAs, fair/mark price) returns the last-accrued value — the state as of the market's last touch()/state-changing interaction (Rates.lastTouch). It is not recomputed on read. So PerpClient::get_funding_rate() can be stale between interactions: it reflects the funding rate at last accrual, not a fresh evaluation of the funding module against the current AMM price / beacon index / EMAs.

Proposal

Expose a "live" read that prepends a touch() to the view read inside a single Multicall3.aggregate3, invoked via eth_call (simulation only — no transaction, no gas, nothing written on-chain). Multicall3 runs its sub-calls sequentially in one EVM call frame, so touch()'s state writes are visible to the subsequent rates() read.

let perp = self.deployments.perp;
let calls = vec![
    IMulticall3::Call3 { target: perp, allowFailure: false,
        callData: Perp::touchCall {}.abi_encode().into() },   // accrues funding/util/EMA
    IMulticall3::Call3 { target: perp, allowFailure: false,
        callData: Perp::ratesCall {}.abi_encode().into() },   // reads the just-updated state
];
let results = IMulticall3::new(MULTICALL3, &self.provider)
    .aggregate3(calls).call().await?;                          // .call() = eth_call (simulate)
let rates = Perp::ratesCall::abi_decode_returns(&results[1].returnData)?;
let daily = /* funding_per_day_to_f64 */ ...;

Suggested API: add get_funding_rate_live() (and possibly a live snapshot variant) alongside the existing cheap get_funding_rate(), so callers opt into the extra simulation cost deliberately.

Why it works / constraints

  • Must use .call() (eth_call), never .send() — this is a simulation; state changes are discarded. A .send() would actually execute touch() on-chain (harmless but wasteful).
  • touch() is permissionless, so Multicall3 calling it (as msg.sender) is fine.
  • touch() reads the live AMM price and calls the beacon's index() (itself state-mutating, also simulates fine under eth_call), then refreshes s.rates — so the read-back reflects current inputs.
  • State visibility across sub-calls (shared execution frame) is the key property. With allowFailure: false, a touch() revert fails the whole simulated call.

Generalizes

The same touch()-then-view pattern gives fresh EMAs and a fresh fair/mark price in one RPC. This is the same eth_call-simulation primitive needed to bring back pre-trade quoting / price-impact (quote_price_impact_curve, max_safe_notional), which were removed in the contract-architecture refactor because the frozen Perp exposes no on-chain quoteSwap. Consider implementing this alongside that quoting/simulation stage.

Related: SDK refactor for frozen contracts (#55).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions