Skip to content

release: promote paused v2.3 testnet suite - #39

Merged
moscowchill merged 70 commits into
mainfrom
dev
Aug 3, 2026
Merged

release: promote paused v2.3 testnet suite#39
moscowchill merged 70 commits into
mainfrom
dev

Conversation

@moscowchill

Copy link
Copy Markdown
Contributor

Summary

  • promote the accumulated reviewed dev changes, including QuantaPool v2.3 accounting hardening and deployment safety controls
  • record the verified v2.3 testnet contract suite and update frontend defaults while keeping both v2.3 and legacy v2.2 paused
  • include the staking frontend, wallet-connect improvements, dependency updates, and operational documentation already merged through feature PRs

Validation

  • Foundry formatting, build, and all 226 contract tests pass in CI
  • deployment safety tests, Solidity artifact compilation, frontend lint, and frontend build pass in CI
  • independent live read-back confirmed chain ID, runtime hashes, ownership, one-shot links, paused state, and zero v2.3 supply/accounting after more than 500 confirmations
  • legacy v2.2 pause transactions and effective deposit/transfer reverts were independently confirmed

Release risk

  • v2.3 remains paused pending an atomic or independently verifiable beacon exit and reward-settlement procedure
  • legacy v2.2 remains paused and migration-bound because its historical off-contract principal is not represented in its accounting
  • this PR promotes source only; it does not deploy the static frontend or change chain state

moscowchill and others added 30 commits April 15, 2026 21:17
Mirrors the working manual deployment documented in docs/NODE-SETUP.md.
Changes not yet validated against a live host run.

- gqrl role: fetch testnetv2 config.yml + genesis.ssz with sha256 check,
  pass --networkid 1337, drop broken --{mainnet,testnet} flags
- qrysm-beacon role: --genesis-state + --chain-config-file + --config-file,
  two hardcoded bootstrap ENRs extracted from qrysm mainnet_config.go:40-41
- qrysm-validator role: same chain-config wiring, keystore import step
  deferred to manual follow-up
- group_vars/all.yml: drop rewards_oracle/operator_registry vars, add
  validator_manager_address; v2.2 addresses now authoritative

Terraform + ansible not wired to a live stack; PR covers the static diff
only. Next pass: re-apply terraform, re-run playbook, iterate on residual
failures.
Three fixes found running the playbook against new terraform-provisioned
hosts:
- beacon health probe: /eth/v1/node/health -> /qrl/v1/node/health (QRL fork
  renamed the REST prefix; old path 404s)
- qrysm-validator: guard 'Create qrysm user' with getent check; previously
  usermod ran on every play and failed when qrysm-beacon was already using
  the user (process 47822)
- drop ansible_date_time.iso8601 from the backup-node lock-file template;
  it's undefined when the play is resumed via --start-at-task (no facts
  gathered) and the timestamp added nothing actionable anyway

Verified: playbook runs clean on both primary + backup from fresh VPS;
gqrl + qrysm-beacon up on both; primary beacon at slot 3007 / backup 2111
climbing toward clock head 21307.
Found when scenario 2 ran end-to-end on a freshly-provisioned Hetzner
stack (see docs/V2-DEPLOYMENT-STATUS.md validator #2 notes).

- fee_recipient_address: empty string made qrysm-validator exit on boot
  ("default fileConfig fee recipient is not a valid qrl address"). Set
  default to the QuantaPool deployer in group_vars; comment explains why
  it cannot be empty. Override per-inventory for other operators.
- qrysm user primary group: Ansible's default user-module behavior on
  Ubuntu 24 assigned `users` (GID 100) as the primary group instead of
  the intended `qrysm` group. Result: 0640 files under /etc/quantapool
  became unreadable by qrysm uid and the validator failed to open its
  wallet password. Pin `group: qrysm` explicitly and create the group
  before the user.
- staking-deposit-cli was never built by the roles; operators had to
  `go build` it by hand on the new host. Added a build task in the
  qrysm-beacon role (qrysm source tree is already cloned there) and a
  path variable.

Syntax-checked via ansible-playbook --syntax-check. Full re-run deferred
to the next fresh provision (validator #2's host is already patched in
place).
Backend PR DigitalGuards/myqrlwallet-backend#36 landed the cache whitelist
fix: only network-invariant RPC methods (qrl_chainId, net_version) are
cached now; state reads always hit upstream. Verified in-session with
scripts/probe-cache-race.js — a 100 QRL deposit immediately shows in
bufferedQRL() with no wait.

Removes the 30-iteration getBlockNumber poll that tx() used to sit in
after each sendTransaction. Status phase re-run end-to-end, unchanged.
…2 moderate)

overrides block (no @theQRL change, no --force): tmp 0.0.33->0.2.7 (HIGH
GHSA-52f5-9888-hmc6 + GHSA-ph9p-34f9-6g65, via solc@0.8.34), follow-redirects
1.15.11->1.16.0 (moderate GHSA-r4q5-vmmm-2653, via solc), ws 8.20.0->8.21.0
(moderate GHSA-58qx-3vcg-4xpx, via @theqrl/web3-providers-ws). Audit 16 -> 12.

Gate: forge build (exit 0) + forge test (187 passing) + npm compile (byte-identical
solc 0.8.34 output). @theqrl/web3 1.0 + wallet.js 6 DEFERRED: forge tests Solidity
only; deploy/integration JS scripts need a live RPC + funded wallet to validate.
…ts-ws

fix(deps): pin tmp/follow-redirects/ws via overrides (audit 16->12)
The real fundValidator() path forwards 40k QRL to the beacon deposit
contract, dropping the pool balance by the stake. _syncRewards() compared
balance - withdrawalReserve against totalPooledQRL, so funding a validator
read as a 40k slashing event: the next (permissionless) syncRewards would
collapse the exchange rate, after which a dust deposit could mint a
near-unbounded share count and drain the pool when stake/rewards returned.

- add stakedQRL accumulator, incremented by fundValidator() when principal
  leaves for the beacon contract
- _syncRewards() now reconciles balance + stakedQRL - withdrawalReserve, so
  funding is balance-neutral (fundValidatorMVP keeps stakedQRL at zero)
- add owner-only recordValidatorExit(amount) to settle returned principal so
  it is not double-counted as rewards on the next sync
- emergencyWithdraw() recoverable calc excludes off-contract stakedQRL
- fix stale 0x01 -> 0x00 withdrawal-credentials doc comment
- 8 new regression tests (195 pass total); refresh docs + test counts

Note: live v2.2 pool carries the pre-fix bytecode and has already run a real
fundValidator() — do not call syncRewards() on it until redeployed as v2.3.

https://claude.ai/code/session_017KpTnYNbCJgqAffCktJ6Wc
test_EmergencyWithdraw_ExcludesStakedFromProtocolFunds sent the recovered
QRL to owner (the Test contract), which has no receive() so the transfer
reverted with TransferFailed. The stakedQRL carve-out under test had already
worked (the call was reached). Send to a fresh EOA instead. 195/195 pass.

https://claude.ai/code/session_017KpTnYNbCJgqAffCktJ6Wc
Minimal Lido-style web app for the liquid staking protocol, built on the
MyQRLWallet design system (Vite 7, React 19, TypeScript, MobX, Tailwind 4,
Radix primitives, dark theme + QRL orange).

- Stake page: deposit QRL -> stQRL with live preview, position card, FAQ
- Withdrawals page: request (128-block delay), claim (FIFO), cancel
- Stats page: pool, validator funding progress, rewards, contract links
- How-it-works page: plain-language user documentation
- MobX poolStore: read-only RPC + EIP-6963 QRL Wallet extension signing
  (qrl_requestAccounts / qrl_sendTransaction), receipt polling for 60s blocks
- Testnet contract addresses default from config/testnet-hyperion.json
Correctness (from 7-angle review):
- Show the exact snapshot payout (withdrawalRequests.qrlAmount) for pending
  withdrawals instead of the drifting current share value — claimWithdrawal
  pays the snapshot, not the current rate
- Disable withdrawal requests while the contract is paused (requestWithdrawal
  is whenNotPaused; previously only deposits were gated)
- Guard refreshAccount against resurrecting state after disconnect/account
  switch mid-fetch
- Move parseUnits inside the tx pipeline so invalid amounts surface as a
  failed tx instead of an unhandled rejection
- Serialize transactions at the store level (single pending tx slot)
- Handle provider accountsChanged events (switch account or disconnect)

Efficiency / cleanup:
- Cache contract instances; cache immutable (claimed/cancelled) withdrawal
  requests so the 30s poll stops refetching history
- Skip background refresh while the tab is hidden; receipt polling 10s
- Guard init() against StrictMode double-invoke
- Replace hardcoded #4aafff with the blue-accent theme token everywhere
- stakeableBalance (gas reserve) moved from page into the store
- dismissConnectError store action instead of component-side mutation

Explorer integration:
- Fetch QRL/USD from zondscan /api/overview (same endpoint as myqrlwallet)
  and show USD values for TVL and the user's position
Stakers can now pull up their staking history in-app: Deposited,
WithdrawalRequested, WithdrawalClaimed and WithdrawalCancelled events
(all indexed by user) are merged into a newest-first activity card on
the Stake page, each entry linking to the transaction on zondscan, with
a direct 'View on Zondscan' link for the address (also added to the
header address chip). Degrades gracefully when the RPC proxy doesn't
expose log queries.
Addresses the phantom-reward front-running window flagged in review of the
stakedQRL change. Validator exit proceeds arrive via EIP-4895 and land in
address(this).balance a block before the owner can settle them with
recordValidatorExit(). In that window balance + stakedQRL double-counts the
principal, so an unrestricted _syncRewards() would book it as a large phantom
reward and spike the exchange rate. Because requestWithdrawal() snapshots the
QRL value at request time, a front-runner could lock that inflated rate into a
withdrawal and drain the pool when the rate corrects.

Fix: reward sync is permissionless only while all principal is on-contract
(stakedQRL == 0). Once principal is staked off-contract (stakedQRL > 0),
syncRewards() and the implicit sync inside requestWithdrawal/claimWithdrawal
are owner-only, so exit settlement and reward recognition are sequenced by the
operator and cannot be front-run. The MVP path (fundValidatorMVP keeps QRL
in-contract, stakedQRL == 0) is unaffected and stays fully permissionless.

- add _permissionlessSyncAllowed() (stakedQRL == 0) with rationale
- syncRewards() reverts NotOwner for non-owner callers while stakedQRL > 0
- requestWithdrawal/claimWithdrawal only inline-sync when permissionless is safe
  (payout already uses the request-time snapshot, so behavior is unchanged)
- 5 regression tests (PHANTOM-REWARD FRONT-RUN PROTECTION block): permissionless
  when unstaked, owner-only while staked, front-run blocked during exit,
  permissionless resumes after settlement, owner still recognizes genuine rewards
- regenerate hyperion mirrors; refresh docs + test counts (200 pass)

Note: compiles clean with solc 0.8.34; forge binaries are not reachable in the
dev sandbox so the full Foundry suite was not run here.
…unding

- refreshAccount now fetches only the live withdrawal tail [nextIndex, total)
  instead of every historical request. nextIndex = total - pending equals the
  contract's nextWithdrawalIndex, and indices below it are claimed/cancelled and
  immutable, so the per-poll RPC fan-out is bounded by pending requests rather
  than a user's entire withdrawal history (avoids hundreds of concurrent calls /
  rate-limiting for active accounts).
- track completedWithdrawalsCount (= nextIndex) on AccountState and use it for
  the 'N completed withdrawals' line, instead of filtering the (no-longer-fully-
  fetched) request array for claimed entries.
- blocksToTime: round to whole minutes before splitting hours/minutes so it can
  no longer render '≈ 60m' or '1h 60m'.
…-review-t3g5wj

fix(DepositPool-v2): track off-contract staked principal in reward sync
…design-4xh69j

Add contract ABIs for QuantaPool frontend
Uncommitted drift from the scenario-2 Hetzner deploy (PR #19 covered the
ansible side; this is the terraform half):

- contract vars: drop rewards_oracle/operator_registry (retired in v2),
  add validator_manager; defaults now the live Q-prefixed v2.2 addresses
  from config/testnet-hyperion.json
- SSH key: look up pre-existing project key by fingerprint instead of
  creating one
- server types: cpx31/21/11 retired by Hetzner; move to cpx32/22
- monitoring node off by default (primary-IP quota: 2); reuse node #1
- per-module hcloud required_providers pin (~> 1.45); datacenter ->
  location on hcloud_server; terraform fmt across the tree
- gitignore terraform state/tfvars, .env.*, and the generated
  ansible inventory.ini (real host IPs; template stays tracked)
Referenced by docs/V2-DEPLOYMENT-STATUS.md (scenario 2) and the 433953a
commit message but never committed:

- fanout-test-wallets.js: seed N fresh ML-DSA-87 wallets from a funder
  mnemonic into .env.scenario2 (gitignored)
- scenario2-deposit.js: deposit each wallet's balance into DepositPoolV2
- probe-cache-race.js: verify the backend cache whitelist fix (deposit
  then immediate bufferedQRL read); funder mnemonic moved out of the
  source into .env.scenario2 as FUNDER_MNEMONIC
Replace with plain punctuation (colons, commas, periods, hyphens).
Empty-value placeholders now render "-" instead of an em dash.
Deployed to quantapool.com and quantapool.io.
- README: status now reflects live v2.2 testnet deploy + frontend at
  quantapool.com/.io; roadmap updated (v2.3 redeploy pending); frontend/
  added to project structure
- V2-DEPLOYMENT-STATUS: PR #20 merge noted under section 7 (redeploy
  still required); new Frontend section with hosting, deploy, and CORS
  coupling (qrlwallet backend ALLOWED_ORIGINS + zondscan
  CORS_ALLOW_ORIGINS); last-updated bumped
- infrastructure/docs: ARCHITECTURE rewards flow updated to v2 trustless
  sync; validator-integration marked deprecated (v1 doc)
- all tracked markdown swept free of em dashes
Sweeps .gitignore, ansible, monitoring (incl. grafana dashboard title),
and scripts. Audited contracts/ sources excluded on purpose: comment
changes there require Hyperion mirror regen + forge gate, not worth it
without an explicit ask. YAML/JSON/JS all re-validated after the sweep.
Deposit/withdraw yo-yo cycles could force the operator to bridge
liquidity or exit validators at no cost to the attacker. Fresh deposits
now mature for minStakeBlocks (default 1536, ~1 day) before they can be
transferred or queued for withdrawal.

- two-bucket lazy maturity in stQRLv2 (immatureSharesOf /
  matureAtBlockOf), mirroring the _lockedShares pattern; top-ups fold
  remaining immature shares into a new bucket and reset its maturity
- immature shares are non-transferable: closes the fresh-address
  bypass; transfers never touch the recipient bucket, so no dust grief
- owner deposits exempt: operator bridge capital enters and exits
  without the wait
- setMinStakeBlocks owner-only, capped at MAX_MIN_STAKE_BLOCKS (46500,
  ~30 days), 0 disables
- requestWithdrawal subtracts immature shares from spendable balance
- 16 new tests incl. locked+immature<=balance invariant fuzz; legacy
  setUps pin minStakeBlocks=0 to stay same-block. Suite: 216 pass.
- Hyperion mirrors regenerated, hypc compile clean

Activates on-chain with the v2.3 redeploy; live v2.2 is unchanged.
- ABI: immatureSharesOf, matureAtBlockOf, minStakeBlocks
- poolStore: immature shares + maturity block in the account snapshot,
  each call individually guarded so the UI degrades gracefully against
  the live v2.2 contracts (missing views read as 0); tracks
  currentBlock for countdowns
- Withdrawals page: available balance excludes immature shares; shows
  a maturing notice with approximate wall-time remaining
- Stake FAQ + How It Works copy updated; stale 178-test claim fixed
- gates: eslint zero warnings, vite build clean
Answers the recurring community questions: two-step request/claim flow
with snapshot pricing and share burn, where claim QRL comes from
(deposit buffer first, full validator exit if needed, no partial
pulls), and the cap question (deposits uncapped, claims gated on
reserve liquidity; min stake lock noted).
feat(footer): link the MyQRLWallet ecosystem site
Add a wallet selector so users can connect either the QRL browser
extension or MyQRLWallet (mobile/desktop/web) over the connect relay,
matching the reference dApp example.

- poolStore constructs the @qrlwallet/connect SDK (which announces over
  EIP-6963), discovers QRL-capable wallets, and drives both connect
  paths through one provider.request() surface. Full relay lifecycle:
  getConnectionURI first-connect, newConnection reset, mobile deep link,
  stored-session auto-reconnect, and QR regen on a wallet-side drop.
- runTx branches the tx shape by transport: the QRL extension needs an
  explicit gas limit (it does not estimate for a dApp send; verified
  against the extension source) sent under both gas and gasLimit; the
  relay wallet self-estimates so it gets the minimal shape.
- New WalletPickerModal + QrPairModal, both mounted at app root.
- The Connect button opens the picker; the 3 existing call sites are
  unchanged.
moscowchill and others added 29 commits July 9, 2026 13:42
…ension-rdns

Accept the MyQRLWallet Extension rdns
- newConnection on mobile still used the bare location.href redirect
  and could dead-end without the app; route it through
  attemptWalletRedirect with the same fallback as connectViaRelay
- deeplink helper: qrlconnect: scheme guard, try/catch around the
  navigation (settle instead of leaking listeners on synchronous
  throws), timer declared before use, navigator guard in appStoreUrl
- extension discovery: defensive optional access on announce payloads
  (any page script can dispatch eip6963:announceProvider)
Gemini review followups: deep-link fallback hardening
Replaces the hand-copied QrPairModal internals with a thin MobX
observer wrapper around <qrl-pairing-modal> from
@qrlwallet/connect-ui@0.1.0 (same self-gating on poolStore.pairingUri).
Gains the shared UX: 'Open web wallet' fragment handoff, 'Open desktop
app' deep link, copy-code fallback, dialog a11y. Drops the qrcode
dependency (the modal was its only user).

Also fixes the pre-existing prefer-const lint error in
utils/deeplink.ts (timer assigned exactly once) that was failing lint
on dev.
refactor(frontend): adopt @qrlwallet/connect-ui for the pairing modal
…wallet/connect 3.2.0

Drops the inlined utils/deeplink.ts copy now that the SDK ships the
helper (announced in its 3.2.0 changelog); behavior identical.
chore(frontend): use the SDK deep-link helper from @qrlwallet/connect 3.2.0
…nects

The SDK emits 'disconnect' both for wallet-initiated terminates and for its reconnect-probe timeout, which fires whenever the wallet app is merely backgrounded (routine on mobile). The store treated every live-session disconnect as terminal: regenerateRelayQr() rotated the channel (orphaning the wallet side's session), wiped account/withdrawals/activity, and popped an unsolicited QR. With @qrlwallet/connect >= 3.3.0 the rotation can also strand an approval already in flight on the old channel.

Now a disconnect while hasStoredSession() is still true keeps the paired state: any request revives the channel (relay-buffered; deep-link wake on 3.3.0). A true terminate (stored session cleared) still falls through to the existing re-pair and reset paths. Safe on the deployed 3.2.0 too. Mirrors myqrlwallet-connect PR #32's example fix and QuantaSwap PR #25.
…ssion

Keep the revivable relay session on non-terminate disconnects
…obots, sitemap

- index.html: canonical to https://quantapool.com/, full Open Graph +
  Twitter summary_large_image tags, Organization (DigitalGuards) +
  WebApplication JSON-LD, apple-touch-icon, sharper title/description
  (QRL 2.0, post-quantum blockchain wording)
- new 1200x630 og-image.png and 180x180 apple-touch-icon.png rendered
  from the brand SVG
- public/robots.txt (allow all + sitemap ref) and sitemap.xml covering
  the four routes
- RouteSeo component keeps title/description/canonical/og:url in sync
  per route; canonical always points at quantapool.com so the .io
  mirror self-identifies as a duplicate
- footer: add QuantaSwap ecosystem link
feat(seo): full meta/JSON-LD/robots/sitemap + per-route SEO + brand OG card
feat(seo): logo-forward OG card v2
…ystem

Port the myqrlwallet-frontend design system: warm-ink token set (ember
primary, blue-accent identity, new success token, radius 0.75rem), body
atmosphere (grain + ember/cyan glows) adapted to the body-scroll model,
self-hosted Sora/Instrument Sans/JetBrains Mono via fontsource imported
from main.tsx, surface-glass cards with surface-ember hero forms,
font-data on all amounts/addresses/block numbers, page-enter route
staggers and glow-dot status indicators with reduced-motion guards.
Header network dot now reflects rpcError state. Orange acts, blue
identifies: all action buttons are ember, addresses and info chips blue.

Gates: eslint zero warnings, tsc + vite build green.
feat(frontend): Obsidian & Ember restyle to match the wallet design system
…mprint)

Legal notice page: testnet-only scope, no-services disclosure, provider imprint
* feat(frontend): display native amounts as Quanta

Native coin amounts now show the Quanta unit instead of QRL, via a shared
NATIVE_UNIT constant in config/networks.ts. The stQRL token symbol and all
network/project references (QRL network, QRL 2.0, QRL validator, QRL beacon
chain) are unchanged.

* refactor(frontend): route Quanta labels through NATIVE_UNIT (review)
…ing-20260803

fix(protocol): harden pool accounting and deployment
…t-20260803

chore(deploy): record paused v2.3 testnet suite
…260803

chore(release): synchronize main into dev
@moscowchill
moscowchill merged commit 01c151b into main Aug 3, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants