Chains, wallets and validators
The six screens behind on-chain staking — activating a chain, the staking key, the validator policy, the batch ledger, the incident desk and the compliance record — and the one rule they all share, that nothing here ever holds a customer's exit.
On-chain staking takes a customer's coins into a wallet this platform controls and delegates them on a network. Before that can happen for a chain, four records must exist, and this page is where they are made. It is also where the product is paused, and where what the engines find is shown to a person.
Six screens, all under Staking in the admin sidebar:
| Screen | Path | Answers |
|---|---|---|
| Chains | /admin/staking/chain |
Is this chain switched on, and what is still missing? |
| Wallets | /admin/staking/wallet |
Which key holds the coins, what does it hold, is it frozen? |
| Validators | /admin/staking/validator-set |
Who do we delegate to, and do they still pass the policy? |
| Batches | /admin/staking/batch |
What was signed, what was broadcast, what is stuck? |
| Incidents | /admin/staking/incident |
What needs a human? |
| Compliance | /admin/staking/compliance |
What was declared, who consented, what was stated? |
None of these screens can move a customer's coins out of the product. Every exit — unstake, claim, return, refund — runs from the scheduled tasks regardless of anything set here. The switches on this page stop new intake only.
Chains — the activation wizard
- The staking wallet — generated once per chain and encrypted with the ecosystem vault. Its address, its state and its balance are printed here because this is the only screen that proves the key exists.
- The validator set — named, with the word that matters beside it. A set reading anything but passes stops new delegation on this chain.
- The declaration — the commission, the notice period before a rise, the slashing policy and its cap, the jurisdictions served and the regulator. This is the record a customer is shown, so it is also the record somebody will one day ask you to justify.
- A chain nobody has activated — no wallet, no set, no declaration, and no way for a customer to reach it. A chain stays here until all four records exist.
One card per chain the product supports: SOL (native delegation from
platform-owned stake accounts) and ETH (Lido stETH, held as shares). The
network is read from the same environment variable the Ecosystem addon uses for
that chain's deposits (SOL_NETWORK, ETH_NETWORK), so an install cannot
gather real coins and delegate on a test network.
Four steps, each a row in the database:
- Staking wallet. A dedicated key, generated once per chain, encrypted with the ecosystem vault. It is never a second master-wallet row: the master wallet pays customer withdrawals and every reader of it keys by chain alone, so a second row could sign a withdrawal. Key material is never returned by any endpoint or shown on any screen.
- Validator set (Solana only). A named list of vote accounts, chosen from the network screened against the policy below. A set that fails the policy cannot be attached to an activation.
- Declaration. The default commission on observed rewards, the notice period before a commission rise, the slashing policy (pass through to holders, or reimburse up to a cap), whether the operator is licensed and by whom, the jurisdictions served, the validator due-diligence note, and two acknowledgements: customer coins are ring-fenced, and no reward is fixed, guaranteed or topped up.
- Operator statement. Five sentences a Super Admin accepts by version. The acceptance is stamped with who, when, from which address and which words.
The card lists every outstanding requirement by name and enables Activate only when the list is empty.
What the declaration implies
The jurisdiction rules are derived from the declaration, never typed, and the card shows them as the declaration changes:
| Declared | Rule |
|---|---|
| Anything | Singapore is blocked and the block cannot be removed |
| Serves AE | The activation is refused: Dubai requires a per-client wallet and a pooled staking wallet cannot serve it |
| Serves HK | The SFC attestation is required, and the Lido venue is refused (third-party custody) |
| Serves GB | The prescribed UK risk warning is shown, and affiliate rewards on staking are forced off |
| Serves US | The contested-state warning is shown |
| Not licensed | US, GB, HK, AE, JP and KR are blocked, non-removably |
The vendor of this software provides tooling and defaults, not a licence.
Pause, resume, retire
Pause stops new stakes on every pool of the chain and records a reason that is shown at the stake door. Observation, unbonding, claims, returns and refunds continue. Resume re-checks every requirement first. Retire is terminal and refused while any position under the activation still holds coins.
An ACTIVE declaration cannot be edited: pause first. A change to the commission or the validator list is a change to the terms every open pool publishes, and the pool's disclosure version moves with it.
Wallets
The table shows chain, network, address, status, the last observed balance and the gas reserve floor. Three actions:
- Freeze, with a reason. A frozen wallet refuses to sign delegations and sweeps and keeps signing exits, claims, returns and refunds. Super Admin only.
- Unfreeze. Super Admin only.
- Gas reserve floor. Below this native balance the reconciler opens a
LOW_GASincident. A staking wallet that cannot pay a fee cannot process an exit, so the floor should cover a week of exits.
Create staking wallet lists the supported chains, whether each has a wallet, and whether its network variable is set. One wallet per chain; a second request is refused.
A floor of zero is not a low floor. It is no check at all.
The reconciler's low-gas test is gasReserveFloor > 0 && balance < floor. The
comparison is only made when the floor is above zero, so a floor of 0 does not
mean "alert at any balance" — it means the LOW_GAS incident can never open for
that wallet, whatever the balance falls to.
The column shipped with defaultValue: 0. A wallet created before the default
landed therefore has the alert switched off, and the first sign of trouble is an
exit that stops moving rather than a critical incident on the desk. A wallet
created by the current version starts at 0.05 SOL or 0.05 ETH —
about ten thousand Solana signatures, and enough Ethereum gas for the three
transactions a Lido exit costs.
Open /admin/staking/wallet and read the gas reserve floor column. Any wallet
showing 0 is not being watched. Type a number into it; nothing else about the
wallet has to change.
The same figure is also the reconciler's tolerance on the second wallet check,
the one that compares the balance against what the book says the wallet owes. A
floor of 0 gives that check no slack for fees spent between an exit settling
and the transfer landing, so it can open a DRIFT incident for an amount the
wallet legitimately paid in gas.
Validators
A validator set is held to the policy in code, not in a setting:
| Rule | Threshold |
|---|---|
| Members | At least 8 |
| Superminority | None of the validators whose cumulative stake reaches a third of the network |
| Inflation commission | At most 5% |
| MEV commission | At most 10%, and an unrecorded commission is itself a breach | | Delinquent | Refused | | Vote credits | Some in the last two epochs | | Skip rate | At most 30%, once the validator has been assigned 20 leader slots this epoch; an unreadable rate is itself a breach | | Pool share | At most 15% of a pool's principal per validator | | Operators | At least two distinct ASNs, and a member with no ASN recorded is itself a breach |
Every rule fails closed. An input the engine cannot read is a breach, not a skipped rule — a screen that drops the checks it has no data for reports a healthy set while grading a subset of the policy the disclosure describes. The breach names itself as unknown ("MEV commission is unknown; record it on the validator so the policy can be applied") rather than as a failure, because the fix is to enter the fact, not to argue with a number.
Two of the facts are not on the network. A validator's MEV commission is set on its relayer, not in its vote account, and the ASN is a fact about its hosting. Both are columns on the validator row, and both are typed in on the set's edit screen below. Until they are, the set is unhealthy and delegates nothing.
Because a live candidate is by definition not yet a member, the MEV rule is deferred for the picker: the candidate screen applies the 10% cap to any figure that exists but does not breach a candidate for having none. The set-level evaluation applies the full rule the moment the validator becomes a member.
New validator set reads the network live, screens every validator, and lists them sorted by stake with the rule each one failed. A failing validator cannot be ticked. Weights decide how a delegation is split; each piece stays at or above the chain's minimum delegation.
Every set is re-screened every six hours. A member that starts failing is suspended (no new delegation; its existing stake keeps running until an exit or a rebalance moves it), the set is marked as failing, and intake on its pools pauses. Evaluate now re-reads the network on demand.
The set's edit screen
/admin/staking/validator-set/new builds a set; /admin/staking/validator-set/{id}
maintains one. The edit screen's header carries three actions: All sets,
Re-screen now — the same evaluation the six-hourly job runs and the same one
the table's Evaluate now fires, under a different label — and Save.
Saving tells you which way it went: either that the set passes the policy, or
how many breaches are still outstanding.
The top card is the verdict: pass or fail, the chain, the network, the set's status and the epoch the evaluation read, then every outstanding breach as a line of its own. The set's name is edited in the same card.
Under it, one block per member that is not removed, showing the vote account,
its inflation commission from the network, its status badge (ACTIVE,
SUSPENDED) and its current breach. Four fields:
| Field | What it is |
|---|---|
| Label | An optional name for your own use. Nothing reads it but this screen. |
| Weight | How the delegation is split. At least 1; anything lower is raised to 1 on save. |
| MEV commission % | Required. Blank is a breach, not a zero. |
| ASN | The operator's network, e.g. AS24940. Blank is a breach. |
Save sends every member at once, so a member you have not touched is written back with the values already on screen.
The last card is Retire set. A retired set takes no new delegation; existing stake accounts stay where they are until an exit or a rebalance moves them, and the route refuses to retire while an active pool still delegates through the set. The button is disabled once the set is already retired.
frontend/middlewares/permissions.json lists /admin/staking/validator-set as
access.staking.validator. Neither /admin/staking/validator-set/new nor
/admin/staking/validator-set/{id} is listed, and neither directory carries a
permission.ts of its own — the root one does, but the matcher reads the
manifest, matching a path exactly or through its [id] segments, with no
prefix rule. An admin path it does not recognise falls through to the base
access.admin, so any role holding access.admin can open the set builder and
the set editor.
Every request those screens make is still checked server-side, route by route:
the candidate list and the set read need view.staking.validator, creating a
set needs create.staking.validator, and saving, re-screening or retiring one
needs edit.staking.validator. A role holding only access.admin therefore
reaches an empty shell — "The network could not be read", "This validator set
could not be read" — and writes nothing. Treat it as a grading inaccuracy when
you audit who can reach what, not as an access hole, and do not read the
manifest as the answer to "who can open this".
Batches
Every on-chain operation is a row: gathers, delegations, exits, claims, returns, refunds, commission exits and sweeps. Each carries the intent the transaction firewall approved — the decoded instruction list and its SHA-256 — and the hash that was broadcast.
The table shows kind (with the chain and pool beneath it), status (with the attempt count on anything not yet confirmed), amount, the transaction hash or "Not broadcast", and when it was created. Kind, status and chain are the filterable columns; the hash and the chain are searchable. Confirmed-at, chain and the last error open with the row.
Click a row for the rest. The Approved intent digest and the decoded instruction list — every instruction the firewall passed, in order, exactly as it was approved before anything was signed — are in that panel and nowhere else. The digest above the list is that list's SHA-256. A row whose panel says "No approved intent recorded yet" has not reached the signer.
The firewall decodes every instruction a staking key is asked to sign and
refuses by name anything outside the allow-list for the batch kind. On Solana
that is the System program's create and transfer, and the Stake program's
initialize, delegate, deactivate, withdraw, split and merge; authorize,
authorizeWithSeed, setLockup and every other program are refused. On
Ethereum the contract address and the four-byte selector are allow-listed per
network. The approved digest is stored on the batch before anything is signed,
and the sender re-derives it from the exact bytes it broadcasts.
A batch that reached the chain is never marked failed; it retries with backoff until it lands. Only a batch that was never broadcast can fail, and a failed gather is what makes a position refundable. Retry now makes an errored batch due on the runner's next tick; it does not sign anything itself.
Incidents
- Severity — what it costs you to ignore this. A critical incident holds new stakes on the chain; a warning does not.
- Status — and the trap on this screen. Acknowledged records that a person saw it and lifts nothing. Only Resolved releases the hold.
- Reconcile now — re-runs the check that opens these rather than the one that closes them. An incident whose cause is fixed closes on the next run; one whose cause is not comes straight back.
- Last seen — the reconciler runs hourly, so a row that has not been seen for many hours is describing a problem that has probably already stopped.
The reconciler runs hourly and opens an incident for every discrepancy it cannot explain:
| Kind | Opened when | Severity |
|---|---|---|
LOW_GAS |
A wallet's balance is below its gas reserve floor | Critical |
DRIFT |
Holders' shares plus treasury shares differ from the pool total, or the book value differs from the tranche value | Critical / Warning |
BATCH_STUCK |
A batch has been in flight longer than the chain's finality window | Warning |
OBSERVER_LAG |
A pool with shares has not been observed for twice its cadence | Warning |
VALIDATOR_BREACH |
A set no longer passes the policy | Warning |
SLASHING |
The observer recorded a negative reward | Critical |
A condition that persists is one incident with a rising count, and a condition
that clears resolves its own incident. Every new incident notifies everyone
holding access.staking. A critical incident holds new intake on its pool
and nothing else. Reconcile now, above the table, runs the check on demand
and reports how many incidents it opened and closed; it moves no coins.
The row actions, and the one that looks like a fix
A row that is not already resolved carries these in its menu:
- Acknowledge — on an
OPENrow only. It takes no words and fires the moment you choose it; there is no dialog and no confirmation. The row moves toACKNOWLEDGED. - Resolve… — opens a dialog with one required field, "What was done (kept as the record)". The button stays disabled until you have written something, and the text is stored on the incident.
- Reimburse… — only on a
SLASHINGrow with a recorded loss. See below.
The kill-switch asks for any CRITICAL incident whose status is not
RESOLVED. ACKNOWLEDGED is not RESOLVED, so acknowledging changes nothing
about the pool: it keeps refusing new stakes exactly as before.
What it does change is the desk. An operator filtering for open incidents no
longer sees the row, and the pool goes on turning customers away with nothing
on screen that explains why. A LOW_GAS incident acknowledged and left is the
clearest case: the wallet is still short, the pool is still closed, and the
line that said so has moved out of view.
Acknowledge to say "I have seen this and I am working on it". Resolve when it is actually dealt with — and a condition the reconciler can still see will reopen the incident on its next pass anyway, which is the check on resolving too early.
A SLASHING incident carries Reimburse… when the pool's activation
declared REIMBURSE_CAPPED: it pays up to the cap per incident, pro rata to
the holders, from the Super Admin's ECO wallet, and refuses with the
shortfall named when that wallet cannot fund it. Leave the amount blank to pay
the whole remaining allowance; the result names what was paid, to how many
holders, and either that the incident is now resolved or how much of the cap is
left. Nothing on-chain moves.
Three further kinds are raised by the ten-minute alerts job:
UNBONDING_OVERDUE, DELEGATION_STALE and COMMISSION. See
Operating on-chain staking.
Compliance records and exports
The Settings screen's Compliance tab reads /api/admin/staking/compliance/records:
the fixed-rate territory gate, every activation with its acceptance and derived
rules, consent counts by disclosure version, the statements written and the
incidents open. Three CSV exports hang off it, at
/api/admin/staking/compliance/export?kind= consents, activations or
observations.
The Compliance console (Staking → Compliance) shows the same records as tabs — activations with their derived rules, consents, monthly statements and the exports — and carries the one write in it: writing a period's missing statements ahead of the nightly run.
Permissions
| Key | Opens |
|---|---|
access.staking.chain · view.staking.chain · create.staking.chain · edit.staking.chain |
The Chains screen, starting and editing a declaration, pause and resume. Activate, retire and wallet creation additionally need the Super Admin role. |
access.staking.wallet · view.staking.wallet · create.staking.wallet · edit.staking.wallet |
The Wallets screen, the gas reserve floor. Create, freeze and unfreeze additionally need the Super Admin role. |
access.staking.validator · view.staking.validator · create.staking.validator · edit.staking.validator |
Validator sets, the screened candidate list, evaluation |
access.staking.batch · view.staking.batch · edit.staking.batch |
The batch ledger and retry |
access.staking.incident · view.staking.incident · edit.staking.incident |
Incidents, acknowledge, resolve, and running the reconciler |
access.staking.settings · view.staking.settings · edit.staking.settings |
The Compliance console, its records and its three exports; writing a period's missing statements needs the edit key |
The Compliance console has no permission key of its own. It reuses the settings
keys, which means view.staking.settings opens both the Settings screen and
every activation declaration, every consent with the customer's name, every
monthly statement and all three CSV exports. Grade it as a customer-records
permission, not as a settings permission.