Staking 6.2.4

Latest

15 September 2026

This release has upgrade notes. Read them before updating — they describe behaviour changes that need your attention.

ON-CHAIN STAKINGCRONRELIABILITYFEESSETTLEMENTTREASURY

Staking v6.2.4

Release Date: September 15, 2026 Tags: ON-CHAIN STAKING, CRON, RELIABILITY, FEES, SETTLEMENT, TREASURY

Overview

The six on-chain scheduled tasks could be dead while the cron console reported them completed at a 100% success rate. Both engines imported a helper module that ships with the Ecosystem extension rather than with this one, so on an install that does not carry that file the on-chain cron module never loaded — and a module that fails to import was recorded as a run that succeeded. Nothing was gathered, delegated, exited, claimed, returned or refunded, no reward was observed, and the reconciler and the alerting that would have noticed were inside the same dead module.

A fixed-rate early-withdrawal fee the platform could not collect was still taken out of the principal. The fee collector declines a charge when the payer is the Super Admin and when no Super Admin is configured. The deduction ran regardless, so the money left the staker and reached nobody.

Backend only; no schema change. Update Core first — the modules the engines now import live there.

Update Instructions

pnpm updator

Everything in this release is server-side and needs the backend restart. There is no new table, no seeder, no setting and no browser bundle to rebuild.

  • Update Core first, in the same window. The two helper modules the engines import — utils/network-fee-tracker.ts and utils/withdrawal-outcome.ts — move into Core in the release that follows v6.7.8, and this release is what points the engines at their new path. On an older Core they are not at that path, the engine module will not load, and all six on-chain tasks refuse. Core also carries the refusal machinery itself, which is the half that makes the failure visible at all.
  • Then open Admin → System → Cron and read the six rows whose titles begin On-chain staking:. Whatever they said before this release is not evidence that anything ran. See Upgrade Notes.
  • If you run the fixed-rate product, look for exits that were charged a fee nobody received before you reconcile this month's figures. What to look for is in Upgrade Notes. This release stops it happening again and restores nothing.

Upgrade Notes

Your on-chain cron rows were not evidence that anything ran

getStakingRealCronUtils() returned the same null for "this customer never bought the addon" and for "the addon is installed and its module threw on import". The six wrappers then returned undefined without throwing, the run was recorded with no error, and the admin cron table showed six green rows with a 100% success rate and a recent last-run time. A week of that table is indistinguishable from a week of the engine working.

The one place the truth was written down is the backend log, once per import attempt:

CRON [IMPORT] Failed to load module @b/api/(ext)/staking/utils/real/cron:
  Cannot find module '.../backend/dist/src/blockchains/network-fee-tracker'

From this release a module that is installed and did not load sets a standing refusal on each of the six jobs. The rows read REFUSED, they stay refused between ticks because it is a state and not a run result, and one URGENT alert goes out per broken module — not per job. A module that is absent stays silent, because a customer who never bought the addon is not suffering a fault; six green rows on an install without the addon still mean nothing ran. The rest is under "The on-chain cron rows read REFUSED" in Troubleshooting.

Work that was skipped is not lost, and it is not replayed by this release either. The batch runner takes gathers, delegations, exits, claims, returns and refunds oldest first, so a queue that built up while the module was dead drains on its own once it loads. The Solana observer reads every epoch since the pool was last observed and then squares the book against the chain, writing any residual the epoch figures do not explain as its own adjustment window. What you should not assume is that the position states you are looking at are current: run the reconciler by hand once the rows are green (POST /api/admin/staking/reconcile, permission edit.staking.incident) and read the incidents it opens rather than waiting for the hourly pass.

An exit may have been charged a fee that reached nobody

On a live install a Super Admin exited a 1,000,000 PBX position early and was returned 900,000. There is no EARLY_WITHDRAWAL_FEE transaction for the missing 100,000 and no stakingAdminEarning row against that position, because the fee was never collected — it was deducted from the principal and credited nowhere. A second position the same minute lost a further 100.

That is the fingerprint to look for: a settled position whose principal was reduced by an exit fee, with no fee transaction and no admin-earning row carrying its id. It happens in exactly the two cases the collector refuses — the staker is the Super Admin, and no Super Admin is configured at all. The first is the one that was observed; the second would apply to every staker on an install in that state.

The reward path carried the mirror of the same mistake, against the treasury rather than the staker: it funded the yield as though the fee had been collected when it had not. Neither is reversed here. Both are corrected going forward only, so any adminProfit figure you have already published for an affected period is out by the fee slice, and the wallet balances behind it are what they are.

The 6.2.3 note was wrong about switching the addon off

The v6.2.3 notes said that if you acknowledged the warning and switched the addon off, exits really would stop until you switched it back on. They do not. processRealStakingBatches is flagged returnsCustomerMoney, and a job with that flag is scheduled on the disabled branch as well as the enabled one, so it keeps claiming unbonded coins and sending them home while the addon's screens are shut. That was already true when 6.2.3 published; the note has been corrected, and the in-app dialog says it plainly.

Two of this addon's seven jobs carry the flag — the batch runner and the fixed-rate processStakingPositions. The observer, the reconciler, the validator refresh, the alerting and the monthly statements all still stop, so a disabled addon completes exits and tells you nothing about them. What does bring the batch runner down is stopEverything on the addon toggle, which is a deliberate hard stop and strands anything in UNBONDING until you lift it. The eleven jobs across the platform that survive a disable are listed in Core v6.7.6.

Fixed

All six on-chain tasks were dead and the cron console reported success

Both engines carried a top-level import { recordBurnedNetworkFee } from "@b/blockchains/network-fee-tracker". That file is not core: it arrives with the Ecosystem extension, which shipped it in 6.5.0. This addon declares a dependency on the core product only, so on an install without Ecosystem — or with a copy of it older than 6.5.0 — the file is not on disk at all. The import chain has no slack in it: cron/index.ts safe-imports @b/api/(ext)/staking/utils/real/cron, which imports ./register, which imports both engines. One missing file took all six jobs down, and the collapse of "not installed" into "failed to load" at the safe-import boundary is what let them report success anyway.

  • Fixed by moving both shared helpers into Core, beside the five chain helpers already there, and pointing the Solana and Lido engines at @b/utils/network-fee-tracker. Nothing in this addon now imports a path that belongs to another product. A re-export is left behind at the old location for one release cycle, so an install that takes a rebuilt Core alongside an un-rebuilt chain zip still resolves.
  • Fixed, in Core, the reporting: a job whose module is installed and failed to load is now announced as a refusal instead of recorded as a completed run.
  • The static imports in real/register.ts are deliberate and are documented as such in the file. A per-engine try/catch would let a broken Lido leave Solana's exits running while all six rows reported success again — the exact failure this release removes. Per-engine isolation is only correct together with a degraded notice naming the missing engine, and that does not exist yet.

An early-withdrawal fee the platform could not collect was still deducted

Settlement credited principal - fee and only then asked the collector to take the fee. The collector refuses two cases outright — the payer is the Super Admin, which is a circular charge, and there is no Super Admin to pay — and returned without taking anything, while the deduction had already happened. The fee did not stay with the house; it was destroyed.

  • Fixed so settlement asks decideFeeCollection first, with the same inputs the collector uses, and waives the fee when the answer is no. The full principal is returned instead, and a line naming the position and the reason is written to the log.
  • Both sides ask the one shared decision function, so they cannot drift apart and decide differently.
  • Fixed-rate only. An on-chain exit has no early-withdrawal fee; it settles at min(request price, final price) and its commission is minted as treasury shares, not charged here.

An uncollected fee was booked as money the treasury had paid

The reward path has two legs: the fee the platform keeps, and the platform loss that funds the yield. The loss leg added the fee back into the gross unconditionally, so when the collector declined the fee the treasury was debited for money it never received — on every accrual, for as long as the position lived. What the platform actually parts with is the net paid to the staker plus only a fee it really kept.

  • Fixed bookStakingPlatformFee to return whether the fee actually reached the Super Admin wallet, and both callers — the per-period accrual and the auto-compound maturity booking — to add the fee into the loss leg only when it did.
  • The fee leg was always right. It is the loss leg that assumed a fee it had not checked, which is why the two figures could disagree.