Safety

Remote equity and attestation

On the Solana leg nobody can lie about equity, because the chain reads the balance itself. On the bridged legs somebody can, bounded by staleness, deviation and a pause. This is a genuine downgrade and it is stated as one.

Two kinds of venue account#

LocalRemote
What it isAn SPL token account on SolanaSomewhere this program cannot see
Equitythe token account's balance, read directlyVenueAccount.remote_equity, an attested number with a timestamp
Who has to be trustedNobodyThe reporter, bounded by three rails, and the venue's own key model
Goes staleNoYes, at Config.max_remote_staleness
Used forSolana spot, and anything settling into a token accountHyperliquid, 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.

The attestation loop. A venue's public API reports equity, notional and the venue's own timestamp. The attestor, a key separate from the agent and the authority, posts them through report remote equity, which writes the attested equity onto the venue account. Anyone can fetch the same public reading and compare it. Three rails then apply, and they behave differently. A report whose notional exceeds five times equity is rejected outright and nothing is written. A report that moves further than the deviation band is recorded and also pauses the policy, so a real loss is never hidden. Staleness is checked later, when a money path reads the figure, where one stale venue fails the whole call. None of this proves the number is true: the reporter can lie within these bounds.Venue APIequity, notional,its own clockAttestorits own key, notthe agent'sreport_remote_equitywrites the figureanyone can fetch the same reading and compareTHREE RAILS, AND THEY DO NOT BEHAVE ALIKENotional over 5x equityREJECTEDnothing is written at allthe 5 is a literal in the program, not a config fieldMoves beyond the bandRECORDED, THEN PAUSEDa real loss is never hiddenonly the authority can restart itToo old to useCHECKED LATERnot when it is written, but when it is readone stale venue fails the whole callsweep_profit, agent_fund_venue, passive_deployeverything that has to know what equity isNone of this proves the number is truethe reporter can lie, bounded by these three railsthe timestamp compared is the venue's, not the reporter's

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.

CheckOutcome if it fails
signer equals Config.reporterNotReporter
the venue account's kind is RemoteNotRemoteVenue
asof <= now + 120 secondsFutureAsof
notional <= 5 * equityNotionalTooHigh
the move from the previous equity is within Config.max_deviation_bpsrecorded 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:

ArgumentSource fieldNote
venue accountthe 20-byte addresscanonical lowercase
equityclearinghouseState.marginSummary.accountValueat scale 1e6
notionalmarginSummary.totalNtlPosalready venue-aggregated. Do not re-sum the individual positions.
leveragenot sentcomputed on chain from equity and notional, so the reporter cannot assert it
asofthe response's own time fieldthe venue's timestamp, not the reporter's clock

Anybody can reproduce the number

clearinghouseState is public and unauthenticated. A third party can fetch it with one curl, compare it to what was posted on chain, and shout if they differ. That is not the same as the chain enforcing it, but it does mean a lie is detectable by anyone who cares to look.

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 same window applies when a money path reads it

A timestamp in the past is fine at report time and only becomes a problem when a money path reads it. sum_venue_accounts requires age >= -MAX_ASOF_SKEW_SECS and age <= max_remote_staleness, failing with FutureAsof or StaleRemoteEquity.

The two windows used to disagree, and it was a real bug

report_remote_equity accepted an asof up to 120 seconds ahead of ledger time while sum_venue_accounts demanded age >= 0, a zero tolerance. So an attestation the program had just accepted and stored made agent_fund_venue and sweep_profit fail with FutureAsof, for every venue in the set, until the ledger clock caught up. Venues stamp from their own clock and a validator routinely trails wall time, so this was routine rather than exotic, and one second of skew was enough to hit it.

Both windows are now MAX_ASOF_SKEW_SECS, with a regression test named for the behaviour.

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

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

Frequency is part of the setting

A 20% band is a band between consecutive reports, not per day. An attestor that posts rarely will trip the guard on ordinary market moves; one that posts often will let a large real move through as a series of small ones. The guard bounds a single step, and how much that is worth depends entirely on the reporting interval.

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.

InstructionAffected by stale remote equity?
sweep_profitYes. It cannot measure equity without every venue.
agent_fund_venueYes. The risk cap is a share of equity.
passive_deployYes, same reason.
claim_feesNo. It does not read equity.
routeNo.
compound_lpNo.
buyback_and_burnNo.

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:

GET /coins/:mint, a Remote venue account
{
  "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#

On the bridged legs, the reporter can lie

Bounded by staleness, by the deviation guard and by the pause, but it can lie. There is no cryptographic link between Hyperliquid's state and the number written on Solana. Anyone describing the remote leg as trustless is wrong.

The rails were provoked, not assumed

The Hyperliquid leg has been exercised against the real exchange and a local validator: real readings posted to chain, including $3,097,182.04 read from a public HLP address, with a later cycle posting a different figure so it is live rather than cached. Eight of eight rail tests passed, including that a 30% move is recorded and also pauses the policy, so a deviating report never hides the loss. Funding an unattested Remote venue correctly failed with StaleRemoteEquity: an attestor that stops freezes the funding path rather than letting money leave against a number nobody checked.

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_out is 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.

On a Remote venue, custody is the venue's rule, not this program's

agent_fund_venue can only pay a venue account the authority registered, so this program never lets the agent name a destination. Once the value is at the venue, though, what the trading key may do is decided there.

On Hyperliquid this is unverified. An agent key has been approved and has filled a real order on mainnet, and the probe that would establish whether the same key can withdraw has never been run. On Derive the question does not arise yet, because no Stags options vault exists to hold anything. Neither is covered by the rails on this page, which bound the reported number and nothing else. The full statement is on the safety model.

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.

GET /status, attestor
"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.