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).
Background
Perp.rates()(and other accrual-dependent state: EMAs, fair/mark price) returns the last-accrued value — the state as of the market's lasttouch()/state-changing interaction (Rates.lastTouch). It is not recomputed on read. SoPerpClient::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 singleMulticall3.aggregate3, invoked viaeth_call(simulation only — no transaction, no gas, nothing written on-chain). Multicall3 runs its sub-calls sequentially in one EVM call frame, sotouch()'s state writes are visible to the subsequentrates()read.Suggested API: add
get_funding_rate_live()(and possibly a live snapshot variant) alongside the existing cheapget_funding_rate(), so callers opt into the extra simulation cost deliberately.Why it works / constraints
.call()(eth_call), never.send()— this is a simulation; state changes are discarded. A.send()would actually executetouch()on-chain (harmless but wasteful).touch()is permissionless, so Multicall3 calling it (asmsg.sender) is fine.touch()reads the live AMM price and calls the beacon'sindex()(itself state-mutating, also simulates fine undereth_call), then refreshess.rates— so the read-back reflects current inputs.allowFailure: false, atouch()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 sameeth_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 frozenPerpexposes no on-chainquoteSwap. Consider implementing this alongside that quoting/simulation stage.Related: SDK refactor for frozen contracts (#55).