vaultsDocumentation

Source accounts

Implemented

A read-only signal with no permissions, and the split that keeps identity out of the trust path.

#The two questions

Two different things get called "the source", and conflating them is how identity ends up in a custody path. They are separate interfaces:

SourceIdentityProviderSourcePositionProvider
AnswersWho is this?What do they hold?
DataHandle, name, avatar, linksBalances, swaps, activity
AuthorityAdvisoryAuthoritative
SourceOff-chain (FomoScan is one implementation)Robinhood Chain only
May be absentYes, entirelyNever
DecidesWhich address to observe — once, at curationEverything the vault actually does

The module that defines these interfaces imports nothing. That is the enforcement: a SourcePositionProvider implementation has nowhere to obtain an off-chain API response from, so identity cannot leak into the position path even by mistake.

#What a source account cannot do

Every use of the source's address inside the vault contract is a balance read. There is no code path in which it is a caller, a role holder, an approval target, or a transfer recipient.

  • It cannot call any vault function.
  • It cannot move or approve vault assets.
  • It cannot pause, unpause, or configure anything.
  • It cannot receive vault shares or depositor funds.
  • It cannot be granted any of the above later — no function exists that would.

Security. An invariant test asserts across randomised action sequences — arbitrary interleavings of deposits, redemptions, source trades, epoch settlements and syncs — that the source account never holds vault shares. The property is checked by the test suite, not asserted in prose.

#Eligibility

A source becomes eligible only when both halves hold:

flowchart TB
  H["handle"] --> ID["Resolve identity"]
  ID --> EVM{"Verified EVM<br/>address returned?"}
  EVM -- "null" --> S1["evm_unverified<br/>(the provider holds no verified wallet —<br/>NOT proof the user has none)"]
  EVM -- "non-EVM only" --> S2["structural mismatch<br/>refused, not retried"]
  EVM -- yes --> ACT{"Observable Robinhood<br/>Chain activity?"}
  ACT -- no --> S3["no_robinhood_activity"]
  ACT -- yes --> OK["eligible"]

Two details matter here.

A 200 with evmAddress: null means the provider holds no verified wallet for that user. It does not mean the user has none. The distinction is preserved rather than flattened into "not found".

An identity that resolves only to a non-EVM chain is refused as a structural mismatch, not a transient failure, and is not retried. A Solana address cannot become an Ethereum-compatible one by trying again.

#Activity, and smart accounts

Activity is measured from chain, not from the identity provider. For an EOA, the account nonce is a reasonable signal. For a smart account it is meaningless — a contract account executing swaps through its own entrypoint can show a nonce of 1 while having made dozens of trades.

So activity counts outgoing transfers for contract accounts. An early version read the nonce and reported 1 for an account with 25 swaps; that address counted 585 outgoing transfers.

#Versioning

Which address a vault observes is recorded in the SourceRegistry against a source version and is immutable for the life of that vault. A curator can propose and activate a source for future deposits and future deployments; that power cannot touch an existing depositor's funds, cannot rewrite a snapshot, and cannot block a withdrawal, because those paths do not exist in the contracts.