Remote equity and attestation
Two kinds of venue account#
| Local | Remote | |
|---|---|---|
| What it is | An SPL token account on Solana | Somewhere this program cannot see |
| Equity | the token account's balance, read directly | VenueAccount.remote_equity, an attested number with a timestamp |
| Who has to be trusted | Nobody | The reporter, bounded by three rails, and the venue's own key model |
| Goes stale | No | Yes, at Config.max_remote_staleness |
| Used for | Solana spot, and anything settling into a token account | Hyperliquid, Derive |
Everything the Local kind gets for free has to be paid for in trust on the Remote side. That is the whole subject of this page.
Two rails act when the report is written and one acts when it is read, which is why a policy can hold a perfectly valid attestation and still refuse to move money.
What report_remote_equity checks#
Reporter only. The signer must equal Config.reporter, which should not be the agent and should not be the authority. The instruction takes three arguments: equity, notional and asof.
| Check | Outcome if it fails |
|---|---|
signer equals Config.reporter | NotReporter |
the venue account's kind is Remote | NotRemoteVenue |
asof <= now + 120 seconds | FutureAsof |
notional <= 5 * equity | NotionalTooHigh |
the move from the previous equity is within Config.max_deviation_bps | recorded anyway, but the policy is paused |
Note the asymmetry in the last two rows. An over-leveraged report is rejected outright, so the 5x cap is enforced on the attestation itself. A large move is recorded and then freezes the money, so a real loss is always recordable while a suspicious one stops anything else happening.
The Hyperliquid payload, field by field#
The attestor reads Hyperliquid's public API and maps it onto the instruction's arguments. The mapping is pinned to real response fields rather than invented:
| Argument | Source field | Note |
|---|---|---|
| venue account | the 20-byte address | canonical lowercase |
equity | clearinghouseState.marginSummary.accountValue | at scale 1e6 |
notional | marginSummary.totalNtlPos | already venue-aggregated. Do not re-sum the individual positions. |
| leverage | not sent | computed on chain from equity and notional, so the reporter cannot assert it |
asof | the response's own time field | the venue's timestamp, not the reporter's clock |
Taking asof from the venue's own timestamp rather than the reporter's clock is the important detail: staleness is then gated against a clock the reporter does not control.
Clock skew, and why the tolerance is not zero#
require(asof <= now + MAX_ASOF_SKEW_SECS) // 120 seconds, a program constant
Two independent time sources are being compared: the venue's clock and Solana's. A zero tolerance would reject an attestation whenever the venue ran even a second ahead, which would freeze every money path for no reason at all. That was found the hard way, on a real devnet run.
The attack the check actually defends against is a far-future stamp used to defeat staleness, so the tolerance is bounded rather than the future being forbidden outright. 120 seconds is a constant in the program, not a config field, so no admin can widen it.
The deviation guard#
previous = VenueAccount.remote_equity
if previous > 0:
diff = |equity - previous|
deviated = diff > previous * Config.max_deviation_bps / 10000
write equity and asof regardless
if deviated: Policy.paused = trueThe guard is symmetric: an implausible gain trips it exactly as an implausible loss does. The first report on a venue account never trips it, because previous is zero and there is nothing to compare against.
On the live deployment max_deviation_bps is 2000, so a report moving equity more than 20% from the last one pauses the policy. Only the policy authority can unpause, which means a deviation always puts a human in the loop.
Staleness, and what it blocks#
Config.max_remote_staleness is 120 seconds on the live deployment. Any money path that has to know the vault's equity reads every registered venue account, and if any Remote attestation is older than that, the whole call reverts.
| Instruction | Affected by stale remote equity? |
|---|---|
sweep_profit | Yes. It cannot measure equity without every venue. |
agent_fund_venue | Yes. The risk cap is a share of equity. |
passive_deploy | Yes, same reason. |
claim_fees | No. It does not read equity. |
route | No. |
compound_lp | No. |
buyback_and_burn | No. |
There is no fallback to the last known figure and no skipping of the stale venue. This is deliberate: a partial equity number would understate how much is deployed, which would loosen the risk cap exactly when the system knows least.
bridged_out, the visible gap#
Every time bankroll funds move to a Remote venue account, whether through agent_fund_venue or passive_deploy, VenueAccount.bridged_out is incremented by the amount. It is incremented at the moment of the transfer, even though an off-chain relayer only moves the funds across later, because from that moment the funds have left local custody.
So the gap between what was sent out and what is claimed to be there is always visible on chain, without trusting anybody:
{
"tokenAccount": "3KZXskLQHPTu2Vtjb4MVBpF4VVFjiUouLMjGzZ7bAVdF",
"kind": "Remote",
"assetClass": 16,
"revoked": false,
"remote": { "value": "0", "source": "chain:report_remote_equity", "asOf": "2026-09-23T01:34:07.000Z" },
"bridgedOut": { "value": "50000000000", "source": "chain:VenueAccount.bridged_out" }
}Read that as: 50,000,000,000 base units of quote have left the bankroll for this venue, and the attested equity on the far side is zero. On a local validator, where nothing carries the value onward, that is exactly what is expected, and it is also precisely the shape a real problem would take on mainnet.
The honest downgrade#
Three mitigations, none of which is a proof:
- the reporter is a separate key from the agent and from the authority, so the party trading is not the party reporting;
- both venues expose public read APIs, so any third party can independently check a posted number;
bridged_outis tracked on chain, so the gap between sent and claimed is always visible.
And one thing that is not a mitigation at all: the bridge is operator-run. Value has crossed to Hyperliquid on mainnet, once, on 2026-09-23, and nothing about the crossing is trustless. A Solana program cannot verify another chain, so the relayer swaps and pays out of a float it holds on the EVM side, and what bounds it is not its honesty: if it takes funds and does not deliver, remote_equity never rises, the gap against bridged_out stays visible, and the deviation rail pauses the policy at the next attestation. Nothing has ever reached Derive. See venues.
The other downgrade: custody#
Attestation is about whether the reported number is true. There is a second thing the program cannot enforce across the same boundary, and it is about whether the money stays put.
Attestation freshness, live#
/status reports the attestor's last action and per-venue freshness. On the local deployment neither venue has a reading, and the response says why rather than returning a zero.
"attestor": {
"lastActionAt": "2026-09-23T01:34:08.000Z",
"perVenue": [
{
"venue": "hyperliquid",
"value": null,
"source": "venue poller",
"asOf": null,
"absent": true,
"reason": "no reading yet: no INDEXER_TARGETS_PATH target configured for this venue, or not polled since indexer start"
},
{
"venue": "derive",
"value": null,
"source": "venue poller",
"asOf": null,
"absent": true,
"reason": "no reading yet: no INDEXER_TARGETS_PATH target configured for this venue, or not polled since indexer start"
}
],
"moneyPathsFrozenNote": "a policy's money paths freeze when report_remote_equity trips its pause (a deviation beyond max_deviation_bps) or the protocol is paused; check the specific coin's `paused`/`protocolPaused` fields, this section is venue-level freshness only"
}Per-coin, the flag to read is moneyPathsFrozen#
The status endpoint's attestor section is venue-level freshness only. Whether a specific policy is actually frozen is a per-coin question, and /coins/:mint answers it directly with paused, protocolPaused and the derived moneyPathsFrozen.