08 · Security model Trust assumptions
What you trust,
and what you don't have to.
StockYield's production contracts have no owner, no pause switch and no upgrade path. The privileged roles that do exist are narrow, immutable in scope, and enumerated here in full, alongside the two things the protocol cannot control: the issuer of the Stock Token and the oracles it settles against. Every claim on this page maps to a public getter or a verified source file.
8.1 Trust boundaries
Five parties touch the system. Only the first, the user's wallet, ever holds an asset; only the last two, the issuer and the price oracles, can act on the protocol without going through a function it defines. The operators in the middle have exactly the powers listed on their boxes and no others.
8.2 Immutability by contract
The table is derived by reading each contract for Ownable, pausable, proxy or self-destruct patterns and for any function that writes a parameter after construction. "None" means the pattern does not appear in the source.
| Contract | Owner / role | Pause | Upgrade | Post-deploy setters | Status |
|---|---|---|---|---|---|
| DividendSeries | None. Twelve immutables set in the constructor. | None | None | None; state changes only through split, merge, finalize, redeem. | Live ×3 |
| ClaimToken (PT / YT) | None. mint/burn callable only by its series. | None | None | None | Live ×6 |
| AttestedDividendOracle | administrator, immutable, no transfer. | None | None | createEventSlot, createSignerSet — append-only; existing slots, signer sets and finalized results cannot be changed. | Live ×3 |
| ManagedEligibilityPolicy | Ownable2Step owner; renounceOwnership reverts. | None | None | setEligibility, setEligibilityBatch | Live ×3 |
| OpenEligibilityPolicy | None; stateless, returns true. | None | None | None | Not used by live series |
| BasketDesk (sDESK) | None | None | None | None. Fees, bounties, leverage and age limits are constant; the Chainlink feed and Pyth id allowlists are written once in the constructor. defineBasket is permissionless and only accepts allowlisted legs. | Live |
| BasketFactory · BasketToken | None. creator is recorded but has no powers. | None | None | None | Live |
| StockOptionSeries · StockOptionFactory · OptionQuoteBook · AtomicStockOptionRouter | None | None | None | None | Not deployed |
| ProtocolOptionTreasury | Ownable2Step owner | None | None | setDistributor; writeOption, buyStockToken, harvestToStakers are owner-only operations. | Not deployed |
| ClaimMarket | None | None | None | None | Not deployed |
No StockYield contract is deployed behind a proxy and none contains delegatecall. The Stock Tokens themselves are issuer contracts outside this table; see §8.4.
8.3 Enumerated admin powers
Three roles exist across the live deployment. For every live series stack the policy owner and the oracle administrator are the same account: the deploying account for that stack, readable as owner() on the policy and administrator() on the oracle. The oracle signers are three separate keys, none of which is the deployer; the deploy script reverts otherwise.
| Policy owner | Can mark any address eligible or ineligible (setEligibility, setEligibilityBatch) and hand ownership to a new account that must accept (Ownable2Step). Cannot renounce (overridden to revert), block a merge or redemption (never eligibility-gated), move or freeze claims or collateral, or change any series or oracle parameter. Marking a holder ineligible only stops that holder from sending claims to another address or approving a spender; exit to the underlying stays open. |
|---|---|
| Oracle administrator | Can call createEventSlot for a new (asset, periodStart, periodEnd) and createSignerSet to append a new signer-set version. Cannot modify or delete a slot, change the signer set a slot pins, submit an attestation without threshold signatures, shorten the immutable challengeDelay, or transfer the role. A new signer set only applies to slots created afterwards; the three live series each pin signer set 1. |
| Oracle signers | Can, with any two of three keys, attest a dividend factor in [1e18, 1.05e18] together with a non-zero evidence hash, and supersede it with a corrected revision while the challenge delay is open. Cannot attest outside the slot's asset, period or factor ceiling, reuse a digest or nonce, act after finalize(eventId), or act on a different chain (chain id is part of the EIP-712 domain). A single key can do nothing. |
| Everyone else | Anyone may submit a fully signed attestation, call finalize(eventId) once the delay has passed, call finalize() on a matured series, define a basket on the desk, settle or void a stranded desk position, or deploy a basket ETF. None of these actions requires a role. |
8.4 Issuer token controls
Every settlement-claim series, every basket ETF and the desk's price feeds reference Robinhood Stock Tokens, which are ERC-8056 tokens administered by their issuer. The protocol only sees them through IERC20 plus decimals() and uiMultiplier(); it cannot observe or influence the issuer's controls.
| Pause · block | A paused token or a blocked address makes transferFrom / transfer revert. StockYield delta-measures every transfer and reverts the whole call with TransferMismatch if fewer or more units move than requested. Nothing is partially applied and no fee is absorbed. |
|---|---|
| Burn · balance change | If the issuer reduces the balance held by a series, _requireFullyCollateralized fails with Insolvent and split, merge and finalize stop; after finalization _requireFinalizedSolvent stops redemption. The contract never pays out more than it holds and never socialises a shortfall silently. A BasketToken whose constituent balance falls short will likewise fail its redeem transfer rather than deliver an incomplete recipe. |
| Upgrade | An issuer upgrade can change any of the above behaviours, including decimals() and transfer semantics. The series pins the token address, not its code; there is no protocol-side mitigation beyond the checks above. |
| uiMultiplier() | Display-only. Series arithmetic uses raw units; equal PT and YT counts at split are an accounting invariant, not a claim about display value. The interface reads uiMultiplier(), newUiMultiplier() and uiMultiplierEffectiveAt() at a block-stamped snapshot to show pending corporate actions, and reports the mechanics as unavailable rather than guessing when those reads fail. |
| Deny-list, not allow-list | The Stock Token was verified onchain to screen a deny-list of blocked addresses rather than to enforce positive eligibility. ClaimToken therefore exempts burns from its own eligibility gate: allowing an ineligible holder to merge or redeem does not hand the underlying to anyone the issuer screens out. |
StockYield is not affiliated with the issuer. Issuer-side action can delay or prevent splits, merges, redemptions, basket mints and redeems, and desk settlement that depends on a Stock Token feed. Those outcomes are outside the protocol's control and are not insured.
8.5 Oracle failure modes and fail-closed behaviour
Two oracle families exist and fail differently. The settlement oracle is human-signed, slow and terminal; the price oracles are automated, fast and time-boxed. In both cases the response to bad or missing data is to refuse the action, and where a position could otherwise be trapped, to open a permissionless recovery path after a fixed delay.
Settlement oracle (AttestedDividendOracle)
| Condition | Contract behaviour | Recovery |
|---|---|---|
| No attestation submitted | getSettlement returns (false, 0, 0); DividendSeries.finalize() reverts OracleNotFinalized. | Signers may still attest at any time before a new attestation's own deadline. Until then merge() remains the only exit and requires equal PT and YT. |
| Wrong factor attested | Recorded but not final for challengeDelay (2 days on every live oracle). | A corrected attestation naming the current digest in supersedes with revision + 1 replaces it and restarts the delay. After finalize(eventId) the result is permanent. |
| Attestation deadline passes before finalization | finalize(eventId) reverts AttestationExpired. | Submit a superseding attestation with a fresh validity window (≤ 30 days). |
| Factor out of range | submitAttestation reverts DividendFactorOutOfRange; defensively, the series also reverts InvalidDividendFactor if a stored factor were ever outside [1e18, maxDividendFactor]. | Not applicable; the value never enters state. |
| One signer key compromised | No effect: threshold is 2 and a duplicate or unsorted signer list reverts. | The administrator may create a new signer set for future slots. Existing slots keep set 1. |
| Two signer keys compromised | An adversarial factor can be attested, bounded to [1e18, 1.05e18] by the slot ceiling. | Only the 2-day challenge window; the honest signer cannot veto alone. This is the residual trust in the signer set. |
| Wrong chain | Constructor reverts unless block.chainid == 4663; every call and view checks deploymentChainId. | Not applicable. |
Price oracles (BasketDesk via BasketIndex)
| Condition | Contract behaviour | Recovery |
|---|---|---|
Chainlink round older than MAX_FEED_AGE (26 hours) | Reverts StaleFeed; no barrier or settlement is enforced against it. | Wait for a fresh round. |
| Pyth confidence wider than 2% of price | Reverts WideConfidence. | Wait for a tighter update. |
No signed Pyth update inside the 10-second pricing window (PYTH_DELAY 5 s, PYTH_WINDOW 10 s) | The request cannot be priced; margin stays held. | After STRANDED_GRACE (24 hours) anyone may voidOpen a pending open, refunding margin less accrued funding, or reopen a pending close, returning the position to open. |
Chainlink feed dead for DEAD_FEED_AGE (30 days) | Position cannot settle. | Anyone may abandon; margin less funding is refunded and reserves released. |
Basket with a Pyth leg open for PYTH_STRANDED (180 days) with no dead Chainlink feed | Position cannot be force-abandoned by third parties. | The position owner may abandon; margin less funding is refunded. |
Parameter values and the pricing model itself are covered in Synthetic basket desk.
8.6 Front-end trust
The interface is a static application that talks to the chain only through the user's wallet and to market data only through its own origin. It holds no keys and asks for none.
- Wallet only. There is no credential or secret input and no account sign-in. Shipped wallet code does not request
personal_sign,eth_sign, typed-data signatures or key export. Connecting reveals a public address; every state change is a transaction the wallet displays for review. - No secrets in the client. Upstream market-data credentials are read from the server process environment and never shipped; the browser receives responses from the StockYield origin only.
- No tracker or account cookie. Wallet addresses and transaction journals stay in browser storage.
- Every number names its source. Series state is read from one block-stamped wallet-RPC snapshot; optional reads that time out or return malformed data are reported as unavailable, never interpolated.
Every response from server.mjs carries the following headers. connect-src 'self' means page scripts cannot contact any RPC, API or third party directly; frame-ancestors 'none' and X-Frame-Options: DENY prevent the app being embedded for click-jacking; script-src-attr 'none' and object-src 'none' rule out inline handlers and plugins. The only permitted frame source is the isolated TradingView chart frame.
content-security-policy: default-src 'self'; connect-src 'self'; font-src 'self' data:;
img-src 'self' data:; frame-src https://s.tradingview.com; object-src 'none';
script-src 'self'; script-src-elem 'self'; script-src-attr 'none';
style-src 'self' 'unsafe-inline'; base-uri 'self'; form-action 'self';
frame-ancestors 'none'
cross-origin-opener-policy: same-origin
cross-origin-resource-policy: same-origin
permissions-policy: camera=(), microphone=(), geolocation=(), payment=(), usb=()
referrer-policy: strict-origin-when-cross-origin
strict-transport-security: max-age=31536000
x-content-type-options: nosniff
x-frame-options: DENY
x-permitted-cross-domain-policies: none
The front end is a convenience, not a trust anchor. A compromised web host could present a wrong contract address; the wallet's transaction preview and the addresses on /trust and Deployed contracts are the reference. Always compare the destination shown by the wallet against the verified address before approving.
8.7 Verification procedure
Every production address is source-verified on Blockscout, so each claim above can be checked against the deployed bytecode and its constructor arguments rather than against this document.
- Open the address at
https://robinhoodchain.blockscout.com/address/<addr>and confirm the Contract tab shows verified source with the expected contract name. Compare the address character-for-character with /trust or Deployed contracts. - Confirm the absence of admin surface: the verified ABI of
DividendSeries,ClaimToken,BasketDesk,BasketFactoryandBasketTokenexposes noowner(),pause(),upgradeTo()or parameter setter. - For each series stack, read:
series.asset(),start(),splitClose(),maturity(),maxDividendFactor(),oracle(),eligibilityPolicy(),oracleEventId(),oracleSignerSetId();oracle.administrator(),challengeDelay(),signerSets(1),isSigner(1, <signer>),eventSlots(<eventId>),eventStates(<eventId>);policy.owner(),pendingOwner(),isEligible(<you>). - For the desk, read
allowedFeed(<feed>)/allowedPythId(<id>)and the public constants (OPEN_FEE_BPS,MAX_LEVERAGE_WAD,MAX_FEED_AGE, …). For a basket ETF, readfactory()and the recipe and confirm the constituent balances covertotalSupply(). - Before any transaction, confirm the wallet's destination address equals the verified series, desk or basket address and that the call data decodes to the function you intend.
The deployment scripts perform the same checks after every broadcast: each contract is interrogated through its own getters and the stack reverts if any wiring invariant (owner, administrator, chain id, challenge delay, signer set, event slot, series immutables) does not match the configuration.