From c41d4d56650d80d27ec40078ce055210052c2683 Mon Sep 17 00:00:00 2001 From: moscowchill Date: Tue, 15 Sep 2026 23:41:10 +0200 Subject: [PATCH 1/3] feat(pool): replace stQRL with native QRL pooled staking --- .github/workflows/test.yml | 49 +- .gitignore | 6 +- README.md | 265 +-- config/hyperion-toolchain.json | 7 + config/qrl-upstream-sources.json | 83 + config/testnet-hyperion.json | 10 - config/testnet-hyperion.v2.0.json.bak | 10 - config/testnet-hyperion.v2.1.json.bak | 10 - config/testnet-hyperion.v2.2.json.bak | 10 - config/testnet.json | 12 - contracts/hyperion/DepositPool-v2.hyp | 998 ---------- contracts/hyperion/README.md | 41 +- contracts/hyperion/ValidatorManager.hyp | 351 ---- contracts/hyperion/stQRL-v2.hyp | 578 ------ contracts/solidity/DepositPool-v2.sol | 996 ---------- contracts/solidity/ValidatorManager.sol | 349 ---- contracts/solidity/stQRL-v2.sol | 576 ------ .../solidity/v1-deprecated/DepositPool.sol | 362 ---- .../v1-deprecated/OperatorRegistry.sol | 276 --- .../solidity/v1-deprecated/RewardsOracle.sol | 238 --- .../solidity/v1-deprecated/TestToken.sol | 49 - contracts/solidity/v1-deprecated/stQRL.sol | 309 --- contracts/test/DepositPool-v2.t.sol | 1696 ---------------- contracts/test/ValidatorManager.t.sol | 720 ------- contracts/test/hyperion/DepositPool-v2.t.hyp | 1698 ---------------- .../test/hyperion/ValidatorManager.t.hyp | 722 ------- contracts/test/hyperion/stQRL-v2.t.hyp | 985 --------- contracts/test/stQRL-v2.t.sol | 983 --------- docs/QRL-UPSTREAM-BASELINE.md | 56 + ...ANTAPOOL-FEE-SECURITY-REVIEW-2026-08-28.md | 240 +++ docs/TERMINAL-WITHDRAWAL-RECEIPTS.md | 181 ++ docs/TESTNET-RETIREMENT.md | 27 + docs/UPSTREAM-FINDINGS.md | 2 + docs/V2-DEPLOYMENT-STATUS.md | 251 ++- docs/architecture.md | 345 +--- docs/legacy/V2-ARCHITECTURE.md | 371 ++++ docs/legacy/V2-README.md | 303 +++ foundry.toml | 12 - frontend/.env.example | 26 +- frontend/README.md | 81 +- frontend/index.html | 27 +- frontend/package-lock.json | 872 +++----- frontend/package.json | 15 +- frontend/public/og-image.png | Bin 84413 -> 239584 bytes frontend/public/social-card.svg | 15 + frontend/scripts/sync-native-abi.js | 11 + frontend/src/App.tsx | 2 +- frontend/src/abi/DepositPoolV2.ts | 1079 ---------- frontend/src/abi/NativeQrlPool.ts | 1429 +++++++++++++ frontend/src/abi/StQRLV2.ts | 775 -------- frontend/src/abi/ValidatorManager.ts | 540 ----- frontend/src/components/ActivityCard.tsx | 97 +- frontend/src/components/AmountInput.tsx | 9 +- frontend/src/components/ConnectButton.tsx | 16 +- frontend/src/components/Layout/Footer.tsx | 22 +- frontend/src/components/Layout/Header.tsx | 24 +- frontend/src/components/RouteSeo.tsx | 17 +- frontend/src/components/StatsBar.tsx | 46 +- frontend/src/components/TxBanner.tsx | 18 +- frontend/src/config/networks.ts | 99 +- frontend/src/pages/HowItWorksPage.tsx | 290 +-- frontend/src/pages/LegalPage.tsx | 198 +- frontend/src/pages/StakePage.tsx | 391 ++-- frontend/src/pages/StatsPage.tsx | 271 ++- frontend/src/pages/WithdrawalsPage.tsx | 514 ++--- frontend/src/stores/poolStore.ts | 935 +++++---- frontend/src/utils/format.test.ts | 37 + frontend/src/utils/format.ts | 68 +- frontend/src/utils/nativeGas.test.ts | 119 ++ frontend/src/utils/nativeGas.ts | 77 + frontend/src/utils/nativeLogs.test.ts | 62 + frontend/src/utils/nativeLogs.ts | 31 + frontend/src/utils/nativePosition.test.ts | 51 + frontend/src/utils/nativePosition.ts | 33 + .../src/utils/poolStoreTransactions.test.ts | 261 +++ frontend/src/utils/qrlAddress.test.ts | 70 +- frontend/src/utils/qrlAddress.ts | 40 +- frontend/src/utils/web3/extension.ts | 2 +- frontend/src/vite-env.d.ts | 11 +- infrastructure/README.md | 52 +- infrastructure/ansible/group_vars/all.yml | 5 +- .../monitoring/templates/monitoring.env.j2 | 7 +- infrastructure/docs/ARCHITECTURE.md | 237 +-- infrastructure/docs/runbooks/monitoring.md | 253 +-- infrastructure/docs/validator-integration.md | 37 +- .../terraform/environments/testnet/main.tf | 18 +- infrastructure/terraform/main.tf | 14 +- .../modules/monitoring-server/main.tf | 24 +- .../terraform/modules/validator-node/main.tf | 24 +- infrastructure/terraform/variables.tf | 24 +- monitoring/README.md | 116 +- monitoring/alertmanager/alertmanager.yml | 2 +- monitoring/contract-exporter/Dockerfile | 4 +- .../contract-exporter/package-lock.json | 1062 +++------- monitoring/contract-exporter/package.json | 9 +- monitoring/contract-exporter/src/config.js | 58 +- monitoring/contract-exporter/src/contracts.js | 333 +--- monitoring/contract-exporter/src/index.js | 155 +- monitoring/contract-exporter/src/metrics.js | 232 +-- monitoring/docker-compose.yml | 15 +- .../grafana/dashboards/contract-state.json | 342 ++-- .../prometheus/rules/contract-alerts.yml | 107 +- .../prometheus/tests/contract-alerts.test.yml | 127 ++ monitoring/test-rules.sh | 20 + native/FINAL-SECURITY-REVIEW.md | 68 + native/IMPLEMENTATION.md | 167 ++ native/LEGACY-DEPENDENCIES.md | 60 + native/RETIRED-FILES.json | 62 + native/REVIEW.md | 103 + native/accounting-model.js | 614 ++++++ native/accounting-model.test.js | 726 +++++++ native/contracts/NativeAccountProof.hyp | 147 ++ native/contracts/NativeCheckpointExecutor.hyp | 40 + native/contracts/NativeFinalityVerifier.hyp | 305 +++ native/contracts/NativeLedger.hyp | 509 +++++ native/contracts/NativePortfolioVerifier.hyp | 325 +++ native/contracts/NativeQrlPool.hyp | 141 ++ native/contracts/NativeSSZ.hyp | 160 ++ native/contracts/NativeValidatorGate.hyp | 175 ++ native/finality/README.md | 19 + native/finality/run.js | 187 ++ native/finality/testdata/provenance.json | 16 + .../testdata/public-certificates.json.gz | Bin 0 -> 1230352 bytes native/lifecycle/.gitignore | 1 + native/lifecycle/LIVE-RESULT.md | 74 + native/lifecycle/README.md | 45 + native/lifecycle/build-helpers.sh | 26 + native/lifecycle/checkpoint.js | 310 +++ native/lifecycle/claims.js | 165 ++ native/lifecycle/context.py | 87 + native/lifecycle/deploy-executor.js | 42 + native/lifecycle/finality-codec.js | 60 + native/lifecycle/finish.py | 119 ++ native/lifecycle/keeper.py | 103 + native/lifecycle/key-fixture.go | 136 ++ native/lifecycle/maintain-checkpoints.py | 86 + native/lifecycle/observe.py | 68 + native/lifecycle/predict-address.go | 25 + native/lifecycle/prepare-exit.py | 72 + native/lifecycle/prepare-key.py | 55 + native/lifecycle/report.js | 191 ++ native/lifecycle/run.js | 288 +++ native/lifecycle/start-validator.py | 54 + native/lifecycle/transactions.js | 158 ++ native/network/README.md | 24 + native/network/bind-probe.star | 10 + native/network/build-images.sh | 65 + native/network/canonical-deposit.abi.json | 128 ++ native/network/deposit-fixture.go | 99 + native/network/genesis-entrypoint.sh | 16 + native/network/kurtosis.yaml | 41 + native/network/qualify-genesis.py | 33 + native/network/redact-fixture-log.py | 11 + native/network/runtime.Dockerfile | 36 + native/network/source-lock.json | 19 + native/network/start.sh | 29 + native/network/verify-network.py | 146 ++ native/proofs/CHECKPOINT-EXECUTION.md | 23 + native/proofs/CheckpointPoolFixture.hyp | 17 + native/proofs/FixtureFinality.hyp | 31 + native/proofs/GateCapacityFixture.hyp | 14 + native/proofs/GatePoolFixture.hyp | 26 + native/proofs/README.md | 44 + native/proofs/SCALE.md | 32 + native/proofs/capacity_fixture_test.go | 76 + native/proofs/capture-checkpoint.py | 116 ++ native/proofs/capture-finality.py | 142 ++ native/proofs/capture-flow.py | 100 + native/proofs/cmd/analyze-finality/main.go | 524 +++++ native/proofs/cmd/export-flow/main.go | 84 + native/proofs/cmd/export-state/main.go | 46 + native/proofs/cmd/relay-exit/main.go | 269 +++ native/proofs/cmd/relay-exit/main_test.go | 36 + native/proofs/cmd/verify-cash/main.go | 239 +++ native/proofs/fixtures_test.go | 110 + native/proofs/flowcache.go | 65 + native/proofs/flowcache_test.go | 49 + native/proofs/gate_fixture_test.go | 152 ++ native/proofs/go.mod | 207 ++ native/proofs/go.sum | 1420 +++++++++++++ native/proofs/make-adapter-plan.js | 103 + native/proofs/make-capacity-plan.js | 54 + native/proofs/make-executor-plan.js | 79 + native/proofs/make-gate-plan.js | 91 + native/proofs/make-plan.js | 114 ++ native/proofs/make-recovery-plan.js | 133 ++ native/proofs/observe-exit.py | 154 ++ native/proofs/run-executor.js | 49 + native/proofs/run.js | 56 + native/proofs/ssz.go | 155 ++ native/proofs/state.go | 83 + native/recovery/README.md | 23 + native/recovery/live.js | 215 ++ native/recovery/transactions.js | 158 ++ native/testing/ClaimReceiverFixture.hyp | 30 + native/testing/LedgerFixture.hyp | 23 + native/testing/README.md | 33 + native/testing/execution/go.mod | 64 + native/testing/execution/go.sum | 241 +++ native/testing/execution/main.go | 521 +++++ native/testing/execution/main_test.go | 61 + native/testing/make-ledger-plan.js | 510 +++++ native/testing/run.js | 113 ++ package-lock.json | 1238 +++++------- package.json | 31 +- prototype/.gitignore | 2 + prototype/README.md | 101 + prototype/accounting/make-plan.js | 237 +++ prototype/contracts/CashRecoveryPool.hyp | 139 ++ .../contracts/CheckpointProofHarness.hyp | 312 +++ .../contracts/FinalityFeasibilityVerifier.hyp | 258 +++ .../contracts/FinalizedPoolCashProbe.hyp | 220 ++ .../FinalizedValidatorRecordProbe.hyp | 124 ++ prototype/contracts/FundingGatePrototype.hyp | 131 ++ prototype/contracts/NativeReturnProbe.hyp | 79 + .../contracts/RecoverableFinalityVerifier.hyp | 305 +++ prototype/contracts/test/ProbeFeeCallback.hyp | 40 + prototype/execution/go.mod | 64 + prototype/execution/go.sum | 241 +++ prototype/execution/main.go | 458 +++++ prototype/finality/LIVE-RUN.md | 60 + prototype/finality/README.md | 170 ++ prototype/finality/capture/README.md | 60 + .../capture/TERMINAL-CAPTURE-RESULT.md | 34 + prototype/finality/capture/analyze.go | 524 +++++ prototype/finality/capture/capture.py | 138 ++ prototype/finality/cost-report.js | 125 ++ prototype/finality/live.js | 357 ++++ prototype/finality/make-plan.js | 304 +++ prototype/finality/make-record-plan.js | 196 ++ prototype/lifecycle/.gitignore | 1 + prototype/lifecycle/PROBE-REGRESSIONS.md | 27 + prototype/lifecycle/README.md | 71 + prototype/lifecycle/TERMINAL-STATE.md | 13 + prototype/lifecycle/generator-entrypoint.sh | 7 + prototype/lifecycle/generator.Dockerfile | 2 + prototype/lifecycle/kurtosis.yaml | 41 + prototype/lifecycle/make-probe-plan.js | 169 ++ prototype/lifecycle/observe.py | 136 ++ prototype/lifecycle/prepare-exit.py | 67 + prototype/lifecycle/prepare-key.py | 70 + prototype/lifecycle/run.js | 305 +++ prototype/lifecycle/start-network.sh | 30 + prototype/lifecycle/start-validator.py | 54 + prototype/make-funding-plan.js | 131 ++ prototype/native/CASH-RECOVERY.md | 39 + prototype/native/CHECKPOINT-DESIGN.md | 107 + prototype/native/DECISIONS.md | 71 + prototype/native/UPSTREAM-REFRESH.md | 55 + prototype/native/VALIDATION.md | 74 + prototype/native/capture-checkpoint.py | 116 ++ prototype/native/make-checkpoint-plan.js | 270 +++ prototype/native/make-recovery-plan.js | 244 +++ prototype/native/verify-checkpoint.go | 239 +++ prototype/native/verify-trie-fixtures.go | 89 + prototype/native/wind-down-model.js | 216 ++ prototype/native/wind-down-model.test.js | 306 +++ prototype/network/.gitignore | 1 + prototype/network/README.md | 43 + prototype/network/bind-probe.star | 10 + prototype/network/build-images.sh | 60 + prototype/network/genesis-entrypoint.sh | 10 + prototype/network/kurtosis.yaml | 43 + prototype/network/prepare-fixture-exit.py | 81 + prototype/network/runtime.Dockerfile | 32 + prototype/network/start.sh | 46 + prototype/network/verify-network.py | 128 ++ prototype/protocol/LIVE-EXIT-RESULT.md | 44 + prototype/protocol/LIVE-FUNDED-EXIT-RESULT.md | 44 + prototype/protocol/README.md | 72 + prototype/protocol/checkpoints_test.go | 270 +++ prototype/protocol/cmd/relay-exit/main.go | 247 +++ .../protocol/cmd/relay-exit/main_test.go | 36 + prototype/protocol/funding_fixture_test.go | 123 ++ prototype/protocol/go.mod | 207 ++ prototype/protocol/go.sum | 1418 +++++++++++++ prototype/protocol/protocol_test.go | 307 +++ prototype/protocol/scripts/observe-exit.py | 146 ++ prototype/protocol/testdata/checkpoints.json | 1765 +++++++++++++++++ prototype/recovery/README.md | 47 + prototype/recovery/make-plan.js | 250 +++ prototype/run-components.js | 98 + prototype/source-lock.json | 82 + scripts/beacon-deposit-target.test.js | 137 ++ scripts/check-deposit-contract.js | 93 - scripts/check-qrl-upstream.js | 188 ++ scripts/check-qrl-upstream.test.js | 99 + scripts/compile-hyperion.js | 96 +- scripts/compile.js | 109 - scripts/deploy-hyperion-safety.test.js | 730 ------- scripts/deploy-hyperion.js | 869 -------- scripts/deploy-test-token.js | 122 -- scripts/emergency-pause-live-v2.js | 102 - scripts/fanout-test-wallets.js | 74 - scripts/fund-validator-real.js | 129 -- scripts/integration-test-v2.js | 681 ------- scripts/lib/beaconDepositTarget.js | 153 ++ scripts/lib/loadDeployer.js | 130 +- scripts/lib/qrysmDeposit.js | 211 ++ scripts/loadDeployer.test.js | 120 ++ scripts/native-monitoring.test.js | 51 + scripts/probe-cache-race.js | 62 - scripts/qrysm-deposit.test.js | 148 ++ scripts/scenario2-deposit.js | 87 - scripts/submit-deposit.js | 162 -- scripts/sync-hyperion.js | 133 -- scripts/test.js | 33 + scripts/v1-deprecated/README.md | 24 +- scripts/v1-deprecated/configure.js | 92 - scripts/v1-deprecated/deploy.js | 187 -- scripts/v1-deprecated/fund-validator.js | 133 -- scripts/v1-deprecated/integration-test.js | 555 ------ scripts/v1-deprecated/test-deposit.js | 115 -- scripts/v1-deprecated/upgrade-deposit-pool.js | 143 -- scripts/verify-deposit-data.js | 133 -- slither-report.txt | 2 + 316 files changed, 35105 insertions(+), 25172 deletions(-) create mode 100644 config/hyperion-toolchain.json create mode 100644 config/qrl-upstream-sources.json delete mode 100644 config/testnet-hyperion.json delete mode 100644 config/testnet-hyperion.v2.0.json.bak delete mode 100644 config/testnet-hyperion.v2.1.json.bak delete mode 100644 config/testnet-hyperion.v2.2.json.bak delete mode 100644 config/testnet.json delete mode 100644 contracts/hyperion/DepositPool-v2.hyp delete mode 100644 contracts/hyperion/ValidatorManager.hyp delete mode 100644 contracts/hyperion/stQRL-v2.hyp delete mode 100644 contracts/solidity/DepositPool-v2.sol delete mode 100644 contracts/solidity/ValidatorManager.sol delete mode 100644 contracts/solidity/stQRL-v2.sol delete mode 100644 contracts/solidity/v1-deprecated/DepositPool.sol delete mode 100644 contracts/solidity/v1-deprecated/OperatorRegistry.sol delete mode 100644 contracts/solidity/v1-deprecated/RewardsOracle.sol delete mode 100644 contracts/solidity/v1-deprecated/TestToken.sol delete mode 100644 contracts/solidity/v1-deprecated/stQRL.sol delete mode 100644 contracts/test/DepositPool-v2.t.sol delete mode 100644 contracts/test/ValidatorManager.t.sol delete mode 100644 contracts/test/hyperion/DepositPool-v2.t.hyp delete mode 100644 contracts/test/hyperion/ValidatorManager.t.hyp delete mode 100644 contracts/test/hyperion/stQRL-v2.t.hyp delete mode 100644 contracts/test/stQRL-v2.t.sol create mode 100644 docs/QRL-UPSTREAM-BASELINE.md create mode 100644 docs/QUANTAPOOL-FEE-SECURITY-REVIEW-2026-08-28.md create mode 100644 docs/TERMINAL-WITHDRAWAL-RECEIPTS.md create mode 100644 docs/TESTNET-RETIREMENT.md create mode 100644 docs/legacy/V2-ARCHITECTURE.md create mode 100644 docs/legacy/V2-README.md delete mode 100644 foundry.toml create mode 100644 frontend/public/social-card.svg create mode 100644 frontend/scripts/sync-native-abi.js delete mode 100644 frontend/src/abi/DepositPoolV2.ts create mode 100644 frontend/src/abi/NativeQrlPool.ts delete mode 100644 frontend/src/abi/StQRLV2.ts delete mode 100644 frontend/src/abi/ValidatorManager.ts create mode 100644 frontend/src/utils/format.test.ts create mode 100644 frontend/src/utils/nativeGas.test.ts create mode 100644 frontend/src/utils/nativeGas.ts create mode 100644 frontend/src/utils/nativeLogs.test.ts create mode 100644 frontend/src/utils/nativeLogs.ts create mode 100644 frontend/src/utils/nativePosition.test.ts create mode 100644 frontend/src/utils/nativePosition.ts create mode 100644 frontend/src/utils/poolStoreTransactions.test.ts create mode 100644 monitoring/prometheus/tests/contract-alerts.test.yml create mode 100644 monitoring/test-rules.sh create mode 100644 native/FINAL-SECURITY-REVIEW.md create mode 100644 native/IMPLEMENTATION.md create mode 100644 native/LEGACY-DEPENDENCIES.md create mode 100644 native/RETIRED-FILES.json create mode 100644 native/REVIEW.md create mode 100644 native/accounting-model.js create mode 100644 native/accounting-model.test.js create mode 100644 native/contracts/NativeAccountProof.hyp create mode 100644 native/contracts/NativeCheckpointExecutor.hyp create mode 100644 native/contracts/NativeFinalityVerifier.hyp create mode 100644 native/contracts/NativeLedger.hyp create mode 100644 native/contracts/NativePortfolioVerifier.hyp create mode 100644 native/contracts/NativeQrlPool.hyp create mode 100644 native/contracts/NativeSSZ.hyp create mode 100644 native/contracts/NativeValidatorGate.hyp create mode 100644 native/finality/README.md create mode 100644 native/finality/run.js create mode 100644 native/finality/testdata/provenance.json create mode 100644 native/finality/testdata/public-certificates.json.gz create mode 100644 native/lifecycle/.gitignore create mode 100644 native/lifecycle/LIVE-RESULT.md create mode 100644 native/lifecycle/README.md create mode 100644 native/lifecycle/build-helpers.sh create mode 100644 native/lifecycle/checkpoint.js create mode 100644 native/lifecycle/claims.js create mode 100644 native/lifecycle/context.py create mode 100644 native/lifecycle/deploy-executor.js create mode 100644 native/lifecycle/finality-codec.js create mode 100644 native/lifecycle/finish.py create mode 100644 native/lifecycle/keeper.py create mode 100644 native/lifecycle/key-fixture.go create mode 100644 native/lifecycle/maintain-checkpoints.py create mode 100644 native/lifecycle/observe.py create mode 100644 native/lifecycle/predict-address.go create mode 100644 native/lifecycle/prepare-exit.py create mode 100644 native/lifecycle/prepare-key.py create mode 100644 native/lifecycle/report.js create mode 100644 native/lifecycle/run.js create mode 100644 native/lifecycle/start-validator.py create mode 100644 native/lifecycle/transactions.js create mode 100644 native/network/README.md create mode 100644 native/network/bind-probe.star create mode 100755 native/network/build-images.sh create mode 100644 native/network/canonical-deposit.abi.json create mode 100644 native/network/deposit-fixture.go create mode 100755 native/network/genesis-entrypoint.sh create mode 100644 native/network/kurtosis.yaml create mode 100644 native/network/qualify-genesis.py create mode 100644 native/network/redact-fixture-log.py create mode 100644 native/network/runtime.Dockerfile create mode 100644 native/network/source-lock.json create mode 100755 native/network/start.sh create mode 100644 native/network/verify-network.py create mode 100644 native/proofs/CHECKPOINT-EXECUTION.md create mode 100644 native/proofs/CheckpointPoolFixture.hyp create mode 100644 native/proofs/FixtureFinality.hyp create mode 100644 native/proofs/GateCapacityFixture.hyp create mode 100644 native/proofs/GatePoolFixture.hyp create mode 100644 native/proofs/README.md create mode 100644 native/proofs/SCALE.md create mode 100644 native/proofs/capacity_fixture_test.go create mode 100644 native/proofs/capture-checkpoint.py create mode 100644 native/proofs/capture-finality.py create mode 100644 native/proofs/capture-flow.py create mode 100644 native/proofs/cmd/analyze-finality/main.go create mode 100644 native/proofs/cmd/export-flow/main.go create mode 100644 native/proofs/cmd/export-state/main.go create mode 100644 native/proofs/cmd/relay-exit/main.go create mode 100644 native/proofs/cmd/relay-exit/main_test.go create mode 100644 native/proofs/cmd/verify-cash/main.go create mode 100644 native/proofs/fixtures_test.go create mode 100644 native/proofs/flowcache.go create mode 100644 native/proofs/flowcache_test.go create mode 100644 native/proofs/gate_fixture_test.go create mode 100644 native/proofs/go.mod create mode 100644 native/proofs/go.sum create mode 100644 native/proofs/make-adapter-plan.js create mode 100644 native/proofs/make-capacity-plan.js create mode 100644 native/proofs/make-executor-plan.js create mode 100644 native/proofs/make-gate-plan.js create mode 100644 native/proofs/make-plan.js create mode 100644 native/proofs/make-recovery-plan.js create mode 100644 native/proofs/observe-exit.py create mode 100644 native/proofs/run-executor.js create mode 100644 native/proofs/run.js create mode 100644 native/proofs/ssz.go create mode 100644 native/proofs/state.go create mode 100644 native/recovery/README.md create mode 100644 native/recovery/live.js create mode 100644 native/recovery/transactions.js create mode 100644 native/testing/ClaimReceiverFixture.hyp create mode 100644 native/testing/LedgerFixture.hyp create mode 100644 native/testing/README.md create mode 100644 native/testing/execution/go.mod create mode 100644 native/testing/execution/go.sum create mode 100644 native/testing/execution/main.go create mode 100644 native/testing/execution/main_test.go create mode 100644 native/testing/make-ledger-plan.js create mode 100644 native/testing/run.js create mode 100644 prototype/.gitignore create mode 100644 prototype/README.md create mode 100644 prototype/accounting/make-plan.js create mode 100644 prototype/contracts/CashRecoveryPool.hyp create mode 100644 prototype/contracts/CheckpointProofHarness.hyp create mode 100644 prototype/contracts/FinalityFeasibilityVerifier.hyp create mode 100644 prototype/contracts/FinalizedPoolCashProbe.hyp create mode 100644 prototype/contracts/FinalizedValidatorRecordProbe.hyp create mode 100644 prototype/contracts/FundingGatePrototype.hyp create mode 100644 prototype/contracts/NativeReturnProbe.hyp create mode 100644 prototype/contracts/RecoverableFinalityVerifier.hyp create mode 100644 prototype/contracts/test/ProbeFeeCallback.hyp create mode 100644 prototype/execution/go.mod create mode 100644 prototype/execution/go.sum create mode 100644 prototype/execution/main.go create mode 100644 prototype/finality/LIVE-RUN.md create mode 100644 prototype/finality/README.md create mode 100644 prototype/finality/capture/README.md create mode 100644 prototype/finality/capture/TERMINAL-CAPTURE-RESULT.md create mode 100644 prototype/finality/capture/analyze.go create mode 100644 prototype/finality/capture/capture.py create mode 100644 prototype/finality/cost-report.js create mode 100644 prototype/finality/live.js create mode 100644 prototype/finality/make-plan.js create mode 100644 prototype/finality/make-record-plan.js create mode 100644 prototype/lifecycle/.gitignore create mode 100644 prototype/lifecycle/PROBE-REGRESSIONS.md create mode 100644 prototype/lifecycle/README.md create mode 100644 prototype/lifecycle/TERMINAL-STATE.md create mode 100755 prototype/lifecycle/generator-entrypoint.sh create mode 100644 prototype/lifecycle/generator.Dockerfile create mode 100644 prototype/lifecycle/kurtosis.yaml create mode 100644 prototype/lifecycle/make-probe-plan.js create mode 100644 prototype/lifecycle/observe.py create mode 100644 prototype/lifecycle/prepare-exit.py create mode 100644 prototype/lifecycle/prepare-key.py create mode 100644 prototype/lifecycle/run.js create mode 100755 prototype/lifecycle/start-network.sh create mode 100644 prototype/lifecycle/start-validator.py create mode 100644 prototype/make-funding-plan.js create mode 100644 prototype/native/CASH-RECOVERY.md create mode 100644 prototype/native/CHECKPOINT-DESIGN.md create mode 100644 prototype/native/DECISIONS.md create mode 100644 prototype/native/UPSTREAM-REFRESH.md create mode 100644 prototype/native/VALIDATION.md create mode 100644 prototype/native/capture-checkpoint.py create mode 100644 prototype/native/make-checkpoint-plan.js create mode 100644 prototype/native/make-recovery-plan.js create mode 100644 prototype/native/verify-checkpoint.go create mode 100644 prototype/native/verify-trie-fixtures.go create mode 100644 prototype/native/wind-down-model.js create mode 100644 prototype/native/wind-down-model.test.js create mode 100644 prototype/network/.gitignore create mode 100644 prototype/network/README.md create mode 100644 prototype/network/bind-probe.star create mode 100755 prototype/network/build-images.sh create mode 100755 prototype/network/genesis-entrypoint.sh create mode 100644 prototype/network/kurtosis.yaml create mode 100644 prototype/network/prepare-fixture-exit.py create mode 100644 prototype/network/runtime.Dockerfile create mode 100755 prototype/network/start.sh create mode 100644 prototype/network/verify-network.py create mode 100644 prototype/protocol/LIVE-EXIT-RESULT.md create mode 100644 prototype/protocol/LIVE-FUNDED-EXIT-RESULT.md create mode 100644 prototype/protocol/README.md create mode 100644 prototype/protocol/checkpoints_test.go create mode 100644 prototype/protocol/cmd/relay-exit/main.go create mode 100644 prototype/protocol/cmd/relay-exit/main_test.go create mode 100644 prototype/protocol/funding_fixture_test.go create mode 100644 prototype/protocol/go.mod create mode 100644 prototype/protocol/go.sum create mode 100644 prototype/protocol/protocol_test.go create mode 100644 prototype/protocol/scripts/observe-exit.py create mode 100644 prototype/protocol/testdata/checkpoints.json create mode 100644 prototype/recovery/README.md create mode 100644 prototype/recovery/make-plan.js create mode 100644 prototype/run-components.js create mode 100644 prototype/source-lock.json create mode 100644 scripts/beacon-deposit-target.test.js delete mode 100644 scripts/check-deposit-contract.js create mode 100644 scripts/check-qrl-upstream.js create mode 100644 scripts/check-qrl-upstream.test.js delete mode 100644 scripts/compile.js delete mode 100644 scripts/deploy-hyperion-safety.test.js delete mode 100644 scripts/deploy-hyperion.js delete mode 100644 scripts/deploy-test-token.js delete mode 100644 scripts/emergency-pause-live-v2.js delete mode 100644 scripts/fanout-test-wallets.js delete mode 100644 scripts/fund-validator-real.js delete mode 100644 scripts/integration-test-v2.js create mode 100644 scripts/lib/beaconDepositTarget.js create mode 100644 scripts/lib/qrysmDeposit.js create mode 100644 scripts/loadDeployer.test.js create mode 100644 scripts/native-monitoring.test.js delete mode 100644 scripts/probe-cache-race.js create mode 100644 scripts/qrysm-deposit.test.js delete mode 100644 scripts/scenario2-deposit.js delete mode 100644 scripts/submit-deposit.js delete mode 100644 scripts/sync-hyperion.js create mode 100644 scripts/test.js delete mode 100644 scripts/v1-deprecated/configure.js delete mode 100644 scripts/v1-deprecated/deploy.js delete mode 100644 scripts/v1-deprecated/fund-validator.js delete mode 100644 scripts/v1-deprecated/integration-test.js delete mode 100644 scripts/v1-deprecated/test-deposit.js delete mode 100644 scripts/v1-deprecated/upgrade-deposit-pool.js delete mode 100644 scripts/verify-deposit-data.js diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4ed8bad..510d544 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,38 +7,9 @@ on: pull_request: workflow_dispatch: -env: - FOUNDRY_PROFILE: ci - jobs: - check: - name: Foundry project - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: actions/checkout@v5 - with: - persist-credentials: false - submodules: recursive - - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - - - name: Show Forge version - run: forge --version - - - name: Run Forge fmt - run: forge fmt --check - - - name: Run Forge build - run: forge build --sizes - - - name: Run Forge tests - run: forge test -vvv - javascript: - name: Deployment tooling and frontend + name: Contract tooling and frontend runs-on: ubuntu-latest permissions: contents: read @@ -58,11 +29,17 @@ jobs: - name: Install deployment dependencies run: npm ci - - name: Compile Solidity deployment artifacts - run: npm run compile + # The complete local gate also compiles the pinned native contracts and + # executes their bytecode in unmodified go-qrl QRVM. A reviewed compiler + # binary is required; these hosted gates make no VM-execution claim. + - name: Test native accounting model + run: npm run test:model + + - name: Test contract tooling + run: npm run test:tooling - - name: Test deployment safety controls - run: node scripts/deploy-hyperion-safety.test.js + - name: Test monitoring rules + run: sh monitoring/test-rules.sh - name: Install frontend dependencies working-directory: frontend @@ -76,6 +53,10 @@ jobs: working-directory: frontend run: npm test + - name: Typecheck frontend + working-directory: frontend + run: npm run typecheck + - name: Build frontend working-directory: frontend run: npm run build diff --git a/.gitignore b/.gitignore index 1b06ecc..b1e9929 100644 --- a/.gitignore +++ b/.gitignore @@ -29,7 +29,11 @@ validator_keys/ keystore_password.txt *.swp -# Foundry build artifacts +# Local compiler and analysis artifacts cache/ out/ findings/* + +# Generated Python helper caches +__pycache__/ +*.py[cod] diff --git a/README.md b/README.md index 47eb7ac..cf6585a 100644 --- a/README.md +++ b/README.md @@ -1,249 +1,50 @@ # QuantaPool -Decentralized liquid staking protocol for QRL. Deposit QRL, receive stQRL, earn validator rewards automatically. +QuantaPool is a native-QRL pooled staking implementation. Users deposit and claim QRL directly through immutable contracts. Their principal, loss exposure, pending deposits, withdrawal requests and reserved rewards are recorded on chain. No transferable staking receipt token exists. -## Overview +The native contracts, accounting model, proof tools and application checks are implemented. A complete local validator lifecycle returned all 44,000 QRL of contributed principal, paid native rewards and the earned 10% operator fee, and drained the pool to zero. That normal-flow run used frozen earlier artifacts; the final hardened source has separate native-VM and actual cash-recovery qualification. [Implementation evidence](native/IMPLEMENTATION.md) records the exact versions, checks and boundaries. Public launch remains subject to review of the trust anchor, committee assumptions, expiry policy, public exits, transaction-tip control and substantial proof cost described in the [architecture](docs/architecture.md). -QuantaPool enables QRL holders to participate in Proof-of-Stake validation without running their own validator nodes. Users deposit QRL and receive stQRL, a fixed-balance token where `balanceOf()` returns stable shares and `getQRLValue()` returns the current QRL equivalent (which grows with rewards). +The [final security review](native/FINAL-SECURITY-REVIEW.md) records the resolved findings, release-candidate checks and remaining launch gates. -### Key Features +## Economic rules -- **Liquid Staking**: Receive stQRL tokens that can be transferred while underlying QRL earns rewards -- **Fixed-Balance Token**: Share balance stays constant (tax-friendly), QRL value grows with rewards -- **Slashing-Safe**: Fixed-balance design handles slashing by proportionally reducing all holders' QRL value -- **Trustless Sync**: No oracle needed - rewards detected via EIP-4895 balance increases -- **Griefing-Resistant**: Fresh deposits mature for ~1 day (owner-tunable) before they can be transferred or withdrawn, blocking deposit/withdraw yo-yo attacks on pool liquidity -- **Post-Quantum Secure**: Built on QRL's Dilithium ML-DSA-87 signature scheme -- **Production Infrastructure**: Terraform + Ansible for automated validator deployment -- **Monitoring Stack**: Prometheus, Grafana dashboards, and Alertmanager with Discord/Telegram alerts -- **Web Frontend**: React staking app live at [quantapool.com](https://quantapool.com) and [quantapool.io](https://quantapool.io) +- Native validators use the unchanged 40,000 QRL protocol amount. The immutable validator operator first risks its own 2,000 QRL preparation deposit; contracts authenticate the canonical pool recipient before atomically adopting that principal and releasing the 38,000 QRL pooled top-up. +- Internal nontransferable stake units allocate verified gains and losses. Deposits and withdrawal requests settle against a later authenticated checkpoint. No user iteration occurs on reward receipt or loss recognition. +- A global FIFO serves withdrawal and reward requests from available cash. Reserved claims are cash-backed and payable only to their original beneficiaries. +- The operator earns a fixed **10%** of eligible realized net consensus gains, with deterministic loss recovery and rounding. Principal, gifts and unclassified receipts are fee-exempt. The fee destination is immutable. +- Public signed exits are stored before pooled funding. Anyone can relay them under the supported native fork and eligibility rules. This also allows unwanted early exits. +- Expiry of verification or complete pool accounting permanently closes normal operation. Recovery preserves existing shares and pays actual present and later cash, with zero new fees. -## Architecture - -``` -┌─────────────────────────────────────────────────────────────┐ -│ User │ -└───────────────────────────┬─────────────────────────────────┘ - │ deposit() / requestWithdrawal() - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ DepositPool-v2.sol │ -│ - Accepts deposits, mints stQRL shares │ -│ - Queues and processes withdrawals │ -│ - Trustless reward sync via balance checking │ -│ - Funds validators via beacon deposit contract │ -└───────────────────────────┬─────────────────────────────────┘ - │ mintShares() / burnShares() - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ stQRL-v2.sol │ -│ - Fixed-balance QRC-20 token │ -│ - Shares-based accounting (wstETH-style) │ -│ - balanceOf = shares, getQRLValue = QRL equivalent │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ ValidatorManager.sol │ -│ - Tracks validator states (pending → active → exited) │ -│ - Stores Dilithium pubkeys (2,592 bytes) │ -│ - MVP: single trusted operator model │ -└───────────────────────────┬─────────────────────────────────┘ - │ - ┌─────────────┴─────────────┐ - ▼ ▼ -┌──────────────────────┐ ┌──────────────────────────────┐ -│ Infrastructure │ │ Monitoring │ -│ Terraform + Ansible │ │ Prometheus + Grafana │ -│ gqrl, qrysm nodes │ │ Contract exporter + alerts │ -└──────────────────────┘ └──────────────────────────────┘ -``` - -## Project Structure - -``` -QuantaPool/ -├── contracts/ # All on-chain code lives here -│ ├── solidity/ # Solidity sources (source of truth) -│ │ ├── stQRL-v2.sol # Fixed-balance liquid staking token -│ │ ├── DepositPool-v2.sol# Deposits, withdrawals, reward sync -│ │ └── ValidatorManager.sol # Validator lifecycle tracking -│ ├── hyperion/ # Auto-synced Hyperion mirrors (.hyp) -│ │ └── README.md # Dialect rules and hypc workflow -│ └── test/ # Foundry test suite (226 tests) -│ ├── stQRL-v2.t.sol # Core token tests -│ ├── DepositPool-v2.t.sol # Deposit/withdrawal tests -│ ├── ValidatorManager.t.sol # Validator lifecycle tests -│ └── hyperion/ # Generated .t.hyp mirrors (reference only) -├── build/hyperion/ # hypc output (ABI, bin, manifest.json) - gitignored -├── frontend/ # React staking app (quantapool.com / quantapool.io) -├── infrastructure/ # Production validator deployment -│ ├── terraform/ # Hetzner Cloud provisioning -│ ├── ansible/ # Node configuration (gqrl, qrysm) -│ ├── scripts/ # deploy.sh, failover.sh, health-check.sh -│ └── docs/ # Runbooks and deployment guides -├── monitoring/ # Observability stack -│ ├── prometheus/ # Scrape config + alert rules -│ ├── grafana/ # Dashboards (validator, contract, system) -│ ├── alertmanager/ # Discord/Telegram routing by severity -│ └── contract-exporter/ # Custom Node.js exporter for on-chain metrics -├── key-management/ # Validator key lifecycle scripts -├── scripts/ # Build & deployment automation -├── config/ # Network deployment configs -└── docs/ # Architecture docs -``` - -## Contracts - -| Contract | LOC | Purpose | -|----------|-----|---------| -| `stQRL-v2.sol` | 576 | Fixed-balance liquid staking token (shares-based, min-stake maturity lock) | -| `DepositPool-v2.sol` | 889 | User entry point, deposits/withdrawals, trustless reward sync | -| `ValidatorManager.sol` | 349 | Validator lifecycle: Pending → Active → Exiting → Exited | - -All on-chain code lives under `contracts/`. Solidity sources in `contracts/solidity/` are the canonical editing target; Hyperion mirrors in `contracts/hyperion/` are generated from them (never hand-edit). Foundry tests live in `contracts/test/` with a parallel `contracts/test/hyperion/` tree of reference `.t.hyp` mirrors. Compiled Hyperion artifacts land in `build/hyperion/` (gitignored). - -## How Fixed-Balance Model Works - -1. User deposits 100 QRL when pool has 1000 QRL and 1000 shares -2. User receives 100 shares, `balanceOf()` = 100 shares -3. Validators earn 50 QRL rewards (pool now has 1050 QRL) -4. User's `balanceOf()` still = **100 shares** (unchanged, tax-friendly) -5. User's `getQRLValue()` = 100 × 1050 / 1000 = **105 QRL** - -If slashing occurs (pool drops to 950 QRL): -- User's `balanceOf()` still = **100 shares** -- User's `getQRLValue()` = 100 × 950 / 1000 = **95 QRL** -- Loss distributed proportionally to all holders - -## How Withdrawals Work - -Unstaking is a two-step flow. The QRL is never burned; the stQRL shares are. - -1. **Request** (`requestWithdrawal(shares)`): your shares are locked (non-transferable, still on your balance) and the contract returns a current QRL estimate. A 128-block delay (~2 hours) starts. You can `cancelWithdrawal()` any time before claiming; the shares simply unlock. -2. **Claim** (`claimWithdrawal()`, FIFO per account): after the delay and accounting settlement, the locked shares are valued at the current exchange rate and burned. You receive that settled QRL amount from the withdrawal reserve. Queued shares continue receiving rewards and bearing slashing losses until claim. - -**Example:** you hold 1,000 stQRL at rate 1.05. Request locks the shares and estimates 1,050 QRL. If the settled rate is 1.06 at claim, 1,000 stQRL burns and you receive 1,060 QRL. If slashing reduces the settled rate, the claim decreases proportionally. - -### Where does the claim QRL come from? - -The withdrawal reserve never holds the full TVL; it only covers pending claims. Pooled QRL lives in three places (all visible on-chain): validators (40,000 QRL each, staked on the beacon chain), the deposit buffer (accumulating toward the next validator), and the withdrawal reserve. Claims are sourced in this order: - -1. **Deposit flow first**: QRL in the buffer is earmarked in the reserve, so withdrawals are netted against incoming stake. Reserved QRL remains in pooled accounting until claim burns the matching shares. -2. **Validator exit if needed**: validator stake is all-or-nothing; you cannot partially withdraw from a validator. If the buffer cannot cover pending claims, one validator exits fully, its 40,000 QRL returns, claims are paid, and the remainder goes back to the buffer. - -If a funded request is cancelled or its settled payout falls, the operator can -call `releaseWithdrawalReserve(amount)`. The contract restores only liquidity -that originally came from `bufferedQRL`, so simulated stake and unbuffered -rewards cannot be counted again as fresh validator principal. - -So if 5 validators are full and someone claims a 5,000 QRL position, that 5,000 comes from the buffer/new deposits if available, otherwise one validator exits and the leftover 35,000 refills the buffer. Same model as Lido and Rocket Pool. - -### Is there a cap? - -Deposits are uncapped. The limit is on the claim side: you can always *request* a withdrawal, but `claimWithdrawal()` reverts until the reserve covers your amount (worst case, one validator-exit cycle). Fresh deposits also carry a minimum stake lock (default ~1 day) before the shares can transfer or enter a withdrawal request; this is anti-griefing protection, not a withdrawal queue. - -## Infrastructure - -Production-ready validator infrastructure using Terraform and Ansible. - -**Components provisioned:** -- **Primary validator node** - gqrl (execution) + qrysm-beacon + qrysm-validator -- **Backup validator node** - hot standby with failover script -- **Monitoring server** - Prometheus, Grafana, Alertmanager - -**Key management scripts** handle the full Dilithium key lifecycle: generation, encryption, backup, restore, and import to the validator client. - -See `infrastructure/docs/DEPLOYMENT.md` for the step-by-step deployment guide and `infrastructure/docs/runbooks/` for operational procedures. - -## Monitoring - -Docker Compose stack providing full observability: - -- **Prometheus**: Scrapes metrics from gqrl, qrysm-beacon, qrysm-validator, and the custom contract exporter -- **Grafana**: Three dashboards - Validator Overview, Contract State, System Resources -- **Alertmanager**: Routes alerts by severity (Critical/Warning/Info) to Discord and Telegram -- **Contract Exporter**: Custom Node.js service exposing on-chain metrics (stQRL exchange rate, TVL, deposit queue, validator count) - -See `monitoring/README.md` for setup and configuration. +Execution tips remain under validator-operator routing control in upstream QRL. Enforceable claims cover received authenticated assets. This implementation does not promise every proposer tip, a fixed return, guaranteed principal or a withdrawal deadline. ## Development -### Prerequisites - -- [Foundry](https://book.getfoundry.sh/getting-started/installation) -- `hypc` for Hyperion compilation/deployment - -### Build - -```bash -forge build +```sh +npm ci +npm run compile +npm test +npm --prefix frontend ci +npm --prefix frontend run lint +npm --prefix frontend test +npm --prefix frontend run build ``` -### Test - -```bash -forge test -``` - -### Test with verbosity - -```bash -forge test -vvv -``` - -### Hyperion workflow - -```bash -npm run sync:hyperion -npm run compile:hyperion -npm run deploy:hyperion -``` - -See `contracts/hyperion/README.md` for the dedicated Hyperion layout and deploy config. - -### CI - -GitHub Actions runs `forge fmt --check`, `forge build --sizes`, and `forge test -vvv` on every push and pull request. - -## Test Coverage - -- **226 tests passing** across stQRL-v2, DepositPool-v2, and ValidatorManager -- Share/QRL conversion math, multi-user rewards, slashing scenarios -- Withdrawal flow with 128-block delay enforcement -- Validator lifecycle (registration, activation, exit, slashing) -- Virtual shares to prevent first-depositor attacks -- Access control, pause functionality, and reentrancy protection -- Fuzz testing for edge cases - -## Status - -**v2.3 is deployed and paused on QRL v2 testnet** with the reviewed accounting and launch-safety fixes. The legacy v2.2 pool is also paused while its historical validator stake is migration-bound. The staking frontend serves at [quantapool.com](https://quantapool.com) and [quantapool.io](https://quantapool.io). Keep v2.3 paused until beacon exit and reward settlement can be independently verified. Addresses and operational detail: `docs/V2-DEPLOYMENT-STATUS.md`. - -### Roadmap - -- [x] v2 fixed-balance contracts with audit remediations -- [x] Validator infrastructure (Terraform + Ansible) -- [x] Monitoring and alerting stack -- [x] Key management tooling -- [x] Deploy v2 contracts to QRL v2 testnet (v2.2, two validators funded) -- [x] Staking frontend live at quantapool.com and quantapool.io -- [x] Redeploy paused v2.3 with off-contract stake accounting and security fixes -- [ ] Complete legacy migration and independently verified settlement procedures -- [ ] Integrate staking UI into [qrlwallet.com](https://qrlwallet.com) - -## Security +Compilation requires the reviewed compiler identified in `config/hyperion-toolchain.json`. Set `HYPERION_COMPILER` to its executable when it is outside the local qualified source tree. All active package commands target the native implementation. The executable tests use the pinned, unmodified go-qrl QRVM with 64-byte addresses and 64-byte ABI words. [Ledger tests](native/testing/README.md) identify their synthetic economic inputs; proof and real-network results are recorded separately. -- Slither static analysis completed (0 critical/high findings) -- Virtual shares (1e3) to prevent first-depositor/inflation attacks -- See `slither-report.txt` for full analysis results +[Source pins and local network setup](native/network/README.md) describe the current cyyber revisions, loopback isolation, canonical deposit runtime and supported local timing configuration. The network harness does not patch qrysm, go-qrl or their consensus rules. [Implementation and validation](native/IMPLEMENTATION.md) records scope, invariants, privileges and current evidence. -## Acknowledgments +## Layout -- [Lido](https://lido.fi/) and [Rocket Pool](https://rocketpool.net/) for pioneering liquid staking designs -- [The QRL Core Team](https://www.theqrl.org/) for building post-quantum secure blockchain infrastructure -- [Robyer](https://github.com/robyer) for community feedback on the fixed-balance token model (tax implications of rebasing) +| Path | Purpose | +|---|---| +| `native/contracts/` | Immutable finality verifier, portfolio proof verifier, validator gate and native pool | +| `native/testing/`, `native/accounting-model*` | Executable QRVM accounting checks and independent arithmetic model | +| `native/proofs/` | Native state, flow and signature fixtures, capture tools and proof tests | +| `native/network/`, `native/lifecycle/` | Pinned local network and actual deployment/lifecycle harness | +| `frontend/` | Native QRL deposits, positions, withdrawal requests and claims | +| `monitoring/` | Read-only native accounting and finality metrics | +| `prototype/`, `docs/legacy/` | Earlier evidence and historical design records | -## License +The old public-testnet contracts are abandoned. Their balances, validators, addresses and storage impose no migration requirement. Token contracts, token tests, old deployment commands and token ABIs are removed from the active implementation. Historical records remain identified as such. No old on-chain state is imported or modified. See [testnet retirement](docs/TESTNET-RETIREMENT.md). -GPL-3.0 +License: GPL-3.0. diff --git a/config/hyperion-toolchain.json b/config/hyperion-toolchain.json new file mode 100644 index 0000000..a525746 --- /dev/null +++ b/config/hyperion-toolchain.json @@ -0,0 +1,7 @@ +{ + "hyperionCommit": "cee9d335984c139c1dc0b0e84ec13290031ea4b1", + "compilerVersion": "0.2.0-develop.2026.9.15+commit.cee9d335.Linux.g++", + "compilerSha256": "11b53f5b2519f381aa46d5704d9e0dde3f0ff2552be07b0cd3aba17741ee612a", + "runtime": "Unmodified go-qrl QRVM from native/network/source-lock.json", + "maximumRuntimeBytes": 24576 +} diff --git a/config/qrl-upstream-sources.json b/config/qrl-upstream-sources.json new file mode 100644 index 0000000..c520f3b --- /dev/null +++ b/config/qrl-upstream-sources.json @@ -0,0 +1,83 @@ +{ + "schemaVersion": 1, + "observedAt": "2026-09-15", + "purpose": "Pinned native QRL source qualification. Execution tests use unmodified go-qrl QRVM.", + "repositories": { + "go-qrl": { + "repository": "https://github.com/cyyber/go-qrl.git", + "branch": "main", + "commit": "9b404c38a63bfebe07cfd07b5f5d144d1c2cf9b9", + "requiredCommits": [ + "f313724147f8fd39f63e0e75c43de4ee6153a615", + "81cad58b9599367c336e4dcb5bb331a632d31d97", + "a09cf0d9f46991ce3fa6450c01d9cb02700f1db0", + "fa2cc537b32caebf1969dbb384e17d8d7e096116" + ], + "goTests": { + "tags": [], + "packages": [ + "./common", + "./accounts/abi", + "./core/vm" + ], + "pattern": "Test(IsAddress|MustParseAddress|MustParseMixedcaseAddress|Address|Mixedcase|PrecompiledDepositroot|DepositrootInputLayout|PrecompiledMLDSA87|ABI|Abi|Pack|Unpack|Type|Event|Method|DefaultFunctionParsing)" + } + }, + "qrysm": { + "repository": "https://github.com/cyyber/qrysm.git", + "branch": "main", + "commit": "3b816311ac3e86b7a7af40a062ae290318554f7a", + "requiredCommits": [ + "4f84e81b88b3dab985ff3b1fb1a89ace15e5a82d", + "820954d3a2877d4e04e34f23276b478a5e4f2266", + "a0c6ec51ee557fb2365f029013665dfaee8b3c06", + "734600fc68829cf147020705250683c10657f862" + ], + "goTests": { + "tags": [ + "develop" + ], + "packages": [ + "./config/params", + "./contracts/deposit", + "./beacon-chain/core/blocks" + ], + "pattern": "Test(Validate|UnmarshalConfig|Register_MinimumDepositBoundary|DepositInput|VerifyDepositSignature|ProcessDeposit|ProcessDeposits|ProcessVoluntaryExits|VerifyExitAndSignature|ProcessWithdrawals|ProcessBlindWithdrawals|Config_Mainnet|Config_Default)" + } + }, + "hyperion": { + "repository": "https://github.com/cyyber/hyperion.git", + "branch": "main", + "commit": "cee9d335984c139c1dc0b0e84ec13290031ea4b1", + "requiredCommits": [ + "3232b592ae89d68dd3301c4cb842a43445555f3c", + "27b4107544b34cabd1e5d7d0c30b27b3f0345486", + "2b9a0f1d5352cf7a4d64718fb04b4b6640041ba1" + ] + }, + "qrvmone": { + "repository": "https://github.com/cyyber/qrvmone.git", + "branch": "main", + "commit": "bca84a21a046bc8506ee4b7a89b31ac9e621904f", + "requiredCommits": [], + "submodules": { + "qrvmc": "b70fb65874ce9a50a55475f800dcb33e39a5a261" + } + } + }, + "reviewedProtocol": { + "addressBytes": 64, + "vmWordBytes": 64, + "hashBytes": 32, + "validatorStakeQrl": "40000", + "minimumBeaconDepositQrl": "2000", + "randaoCommitmentBytes": 32, + "publicKeyBytes": 2592, + "signatureBytes": 4627, + "slotsPerEpoch": 128, + "secondsPerSlot": 60, + "minimumActiveEpochsBeforeVoluntaryExit": 16, + "syncCommitteeSize": 128, + "epochsPerSyncCommitteePeriod": 8 + } +} diff --git a/config/testnet-hyperion.json b/config/testnet-hyperion.json deleted file mode 100644 index ae5a0fe..0000000 --- a/config/testnet-hyperion.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "provider": "https://qrlwallet.com/api/qrl-rpc/testnet", - "chainId": 1337, - "txConfirmations": 12, - "contracts": { - "stQRLV2": "Q7d4cA4872502a1ab02bCA855C093449aaE2bee58", - "depositPoolV2": "Q8e01Ea0bC7e337806154573A5B46Bb37F50Ea8fC", - "validatorManager": "Qd84648a8F7314652B3E98D346645415eA03cce5f" - } -} diff --git a/config/testnet-hyperion.v2.0.json.bak b/config/testnet-hyperion.v2.0.json.bak deleted file mode 100644 index 4bf7bcc..0000000 --- a/config/testnet-hyperion.v2.0.json.bak +++ /dev/null @@ -1,10 +0,0 @@ -{ - "provider": "https://qrlwallet.com/api/qrl-rpc/testnet", - "chainId": 1337, - "txConfirmations": 12, - "contracts": { - "stQRLV2": "Q09046968aF19E745F4aBa7A9fa5CD946b4E981DB", - "depositPoolV2": "Q38F73cb87c60d365fdFA7abF0e534fc1a9D5F9B9", - "validatorManager": "Q1b083D7Dc47212DcBc4595249D9384Fa16cE6FC5" - } -} \ No newline at end of file diff --git a/config/testnet-hyperion.v2.1.json.bak b/config/testnet-hyperion.v2.1.json.bak deleted file mode 100644 index 53d2f91..0000000 --- a/config/testnet-hyperion.v2.1.json.bak +++ /dev/null @@ -1,10 +0,0 @@ -{ - "provider": "https://qrlwallet.com/api/qrl-rpc/testnet", - "chainId": 1337, - "txConfirmations": 12, - "contracts": { - "stQRLV2": "Qd4EC1BEBdD86A9Aa387295d82d0B3Ef3E84f955e", - "depositPoolV2": "QD4B89C98727a9C149fDaCf9DcE46E0E7846BaDC5", - "validatorManager": "Q9a80a082870B6632cF0E71494162BFC2AF53F4d8" - } -} \ No newline at end of file diff --git a/config/testnet-hyperion.v2.2.json.bak b/config/testnet-hyperion.v2.2.json.bak deleted file mode 100644 index 1518b10..0000000 --- a/config/testnet-hyperion.v2.2.json.bak +++ /dev/null @@ -1,10 +0,0 @@ -{ - "provider": "https://qrlwallet.com/api/qrl-rpc/testnet", - "chainId": 1337, - "txConfirmations": 12, - "contracts": { - "stQRLV2": "QA2f23388d1e3986416A36d2Ef113850D6900b69C", - "depositPoolV2": "Q109d7C528a67b80eb638D4C85e7C4545ef9Bb9aC", - "validatorManager": "QA5b6e85B7713670589e4eAf2F039380Ec2792c8C" - } -} diff --git a/config/testnet.json b/config/testnet.json deleted file mode 100644 index baee628..0000000 --- a/config/testnet.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "provider": "https://qrlwallet.com/api/qrl-rpc/testnet", - "chainId": 1337, - "txConfirmations": 12, - "contracts": { - "testToken": "", - "stQRL": "", - "depositPool": "", - "rewardsOracle": "", - "operatorRegistry": "" - } -} diff --git a/contracts/hyperion/DepositPool-v2.hyp b/contracts/hyperion/DepositPool-v2.hyp deleted file mode 100644 index 0066b8b..0000000 --- a/contracts/hyperion/DepositPool-v2.hyp +++ /dev/null @@ -1,998 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -// Generated from ../contracts/solidity/DepositPool-v2.sol by scripts/sync-hyperion.js. -// Edit the Solidity source first, then re-run this script. -pragma hyperion >=0.0; - -/** - * @title DepositPool v2 - User Entry Point for QuantaPool - * @author QuantaPool - * @notice Accepts QRL deposits, manages withdrawals, and syncs validator rewards - * - * @dev Key responsibilities: - * 1. Accept user deposits → mint stQRL shares - * 2. Queue and process withdrawals → burn shares, return QRL - * 3. Trustless reward sync → detect balance changes from validators - * 4. Fund validators → send QRL to beacon deposit contract - * - * Reward Sync (Oracle-Free): - * Validator rewards arrive via EIP-4895 as balance increases WITHOUT - * triggering contract code. This contract periodically checks its balance - * and updates stQRL's totalPooledQRL accordingly. - * - * syncRewards() can be called by anyone while all principal is on-contract. - * While stakedQRL is nonzero, only the owner may sequence reward sync and - * validator-exit settlement. The contract compares its accounted assets to - * the observed balance plus tracked off-contract principal. - * - * Balance Accounting: - * contractBalance + stakedQRL = totalPooledQRL - * - * - totalPooledQRL: All QRL under pool management (buffered + staked + rewards) - * This is what stQRL token tracks. Includes buffered deposits waiting - * to fund validators, principal staked off-contract, plus any rewards - * that arrive via EIP-4895. - * - stakedQRL: principal forwarded to the beacon deposit contract by the - * real fundValidator() path. It lives off-contract but is still pooled, - * so _syncRewards() adds it back when reconciling the balance. - * - withdrawalReserve: Liquid QRL earmarked for pending withdrawals. It - * remains pooled until the matching shares are burned at claim time. - * - * For MVP (testnet), fundValidatorMVP keeps QRL in this contract and stakedQRL - * stays zero. For production, fundValidator sends QRL to the beacon deposit - * contract (incrementing stakedQRL); it returns when validators exit, at which - * point the owner calls recordValidatorExit() to settle the accounting. - */ - -interface IstQRL { - function mintShares(address to, uint256 qrlAmount) external returns (uint256); - function burnShares(address from, uint256 sharesAmount) external returns (uint256); - function updateTotalPooledQRL(uint256 newAmount) external; - function lockShares(address account, uint256 sharesAmount) external; - function unlockShares(address account, uint256 sharesAmount) external; - function totalPooledQRL() external view returns (uint256); - function totalShares() external view returns (uint256); - function sharesOf(address account) external view returns (uint256); - function lockedSharesOf(address account) external view returns (uint256); - function immatureSharesOf(address account) external view returns (uint256); - function getSharesByPooledQRL(uint256 qrlAmount) external view returns (uint256); - function getPooledQRLByShares(uint256 sharesAmount) external view returns (uint256); -} - -/// @notice QRL beacon chain deposit contract interface -interface IDepositContract { - function deposit( - bytes calldata pubkey, - bytes calldata withdrawal_credentials, - bytes calldata signature, - bytes32 deposit_data_root - ) external payable; -} - -contract DepositPoolV2 { - // ============================================================= - // CONSTANTS - // ============================================================= - - /// @notice Minimum stake for a QRL validator (MaxEffectiveBalance from QRL beacon config) - uint256 public constant VALIDATOR_STAKE = 40_000 quanta; - - /// @notice QRL beacon chain deposit contract - address public constant DEPOSIT_CONTRACT = Q4242424242424242424242424242424242424242; - - /// @notice Dilithium pubkey length (bytes) - uint256 private constant PUBKEY_LENGTH = 2592; - - /// @notice ML-DSA-87 signature length (bytes). qrysm enforces exactly - /// this value in crypto/ml_dsa_87/ml_dsa_87t/signature.go — any other - /// length will be rejected by the beacon deposit contract. - uint256 private constant SIGNATURE_LENGTH = 4627; - - /// @notice Withdrawal credentials length - uint256 private constant CREDENTIALS_LENGTH = 32; - - /// @notice Minimum blocks to wait before claiming withdrawal - uint256 public constant WITHDRAWAL_DELAY = 128; // ~2 hours on QRL - - /// @notice Absolute minimum for minDepositFloor (dust prevention, ~1e15 wei) - uint256 public constant ABSOLUTE_MIN_DEPOSIT = 0.001 quanta; - - // ============================================================= - // STORAGE - // ============================================================= - - /// @notice stQRL token contract - IstQRL public stQRL; - - /// @notice Contract owner - address public owner; - - /// @notice QRL buffered for next validator (not yet staked) - uint256 public bufferedQRL; - - /// @notice Number of active validators - uint256 public validatorCount; - - /// @notice Minimum deposit amount - uint256 public minDeposit; - - /// @notice Adjustable floor for minDeposit (owner can lower after deployment) - uint256 public minDepositFloor = 100 quanta; - - /// @notice Paused state - bool public paused; - - /// @notice Reentrancy guard - uint256 private _locked; - - // ============================================================= - // WITHDRAWAL STORAGE - // ============================================================= - - /// @notice Withdrawal request data - struct WithdrawalRequest { - uint256 shares; // Shares to burn - uint256 qrlAmount; // Request-time estimate, replaced by actual payout on claim - uint256 requestBlock; // Block when requested - bool claimed; // Whether claimed - } - - /// @notice Withdrawal requests by user (supports multiple requests via array) - mapping(address => WithdrawalRequest[]) public withdrawalRequests; - - /// @notice Next withdrawal request ID to process for each user - mapping(address => uint256) public nextWithdrawalIndex; - - /// @notice Total shares locked in withdrawal queue - uint256 public totalWithdrawalShares; - - /// @notice QRL reserved for pending withdrawals - uint256 public withdrawalReserve; - - /// @notice Portion of withdrawalReserve that was moved out of bufferedQRL - /// @dev Tracks exactly how much buffer may be restored when an earmark is - /// released. The remainder came from unbuffered liquid assets. - uint256 public bufferedQRLInReserve; - - // ============================================================= - // SYNC STORAGE - // ============================================================= - - /// @notice Last block when rewards were synced - uint256 public lastSyncBlock; - - /// @notice Total rewards received (cumulative, for stats) - uint256 public totalRewardsReceived; - - /// @notice Total slashing losses (cumulative, for stats) - uint256 public totalSlashingLosses; - - /// @notice QRL principal forwarded to the beacon deposit contract that is - /// staked off-contract (not in address(this).balance) but still under - /// protocol management. - /// @dev Only the real beacon path (fundValidator) moves QRL off-contract, - /// so only it increments this. fundValidatorMVP keeps QRL in the - /// contract and leaves stakedQRL untouched. _syncRewards() adds this - /// back when reconciling balance against totalPooledQRL, otherwise - /// funding a validator would look like a slashing event. The owner - /// decrements it via recordValidatorExit() when exit proceeds return. - uint256 public stakedQRL; - - // ============================================================= - // EVENTS - // ============================================================= - - event Deposited(address indexed user, uint256 qrlAmount, uint256 sharesReceived); - - event WithdrawalRequested(address indexed user, uint256 shares, uint256 qrlAmount, uint256 requestBlock); - - event WithdrawalClaimed(address indexed user, uint256 shares, uint256 qrlAmount); - - event RewardsSynced(uint256 rewardsAmount, uint256 newTotalPooled, uint256 blockNumber); - - event SlashingDetected(uint256 lossAmount, uint256 newTotalPooled, uint256 blockNumber); - - event ValidatorFunded(uint256 indexed validatorId, bytes pubkey, uint256 amount); - - event ValidatorExitRecorded(uint256 amount, uint256 remainingStaked); - - event WithdrawalReserveFunded(uint256 amount); - event WithdrawalReserveReleased(uint256 amount, uint256 bufferRestored); - event WithdrawalCancelled(address indexed user, uint256 indexed requestId, uint256 shares); - event MinDepositUpdated(uint256 newMinDeposit); - event MinDepositFloorUpdated(uint256 newFloor); - event Paused(address account); - event Unpaused(address account); - event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); - event StQRLSet(address indexed stQRL); - event EmergencyWithdrawal(address indexed to, uint256 amount); - - // ============================================================= - // ERRORS - // ============================================================= - - error NotOwner(); - error ContractPaused(); - error ReentrancyGuard(); - error ZeroAddress(); - error ZeroAmount(); - error BelowMinDeposit(); - error BelowMinDepositFloor(); - error BelowAbsoluteMin(); - error InsufficientShares(); - error NoWithdrawalPending(); - error WithdrawalNotReady(); - error InsufficientReserve(); - error InsufficientBuffer(); - error InvalidPubkeyLength(); - error InvalidSignatureLength(); - error InvalidCredentialsLength(); - error InvalidWithdrawalCredentials(); - error TransferFailed(); - error StQRLNotSet(); - error StQRLAlreadySet(); - error InvalidWithdrawalIndex(); - error ExceedsRecoverableAmount(); - error ExceedsStakedAmount(); - error AccountingNotSettled(); - - // ============================================================= - // MODIFIERS - // ============================================================= - - modifier onlyOwner() { - if (msg.sender != owner) revert NotOwner(); - _; - } - - modifier whenNotPaused() { - if (paused) revert ContractPaused(); - _; - } - - modifier nonReentrant() { - if (_locked == 1) revert ReentrancyGuard(); - _locked = 1; - _; - _locked = 0; - } - - // ============================================================= - // CONSTRUCTOR - // ============================================================= - - constructor() { - owner = msg.sender; - minDeposit = 100 quanta; // 100 QRL minimum - lastSyncBlock = block.number; - } - - // ============================================================= - // DEPOSIT FUNCTIONS - // ============================================================= - - /** - * @notice Deposit QRL and receive stQRL - * @dev Reconciles pre-existing balance changes, mints shares at the current - * exchange rate, and adds the deposit to the validator buffer. - * - * @return shares Amount of stQRL shares minted - */ - function deposit() external payable nonReentrant whenNotPaused returns (uint256 shares) { - if (address(stQRL) == address(0)) revert StQRLNotSet(); - if (msg.value < minDeposit) revert BelowMinDeposit(); - - // msg.value is already in this contract's balance. Reconcile against - // the balance immediately before this deposit so existing rewards or - // losses cannot be captured by the new depositor. - _syncBeforeDeposit(msg.value); - - // Mint shares FIRST - this calculates shares at current rate - // mintShares internally calls getSharesByPooledQRL(qrlAmount) - // This must happen BEFORE updating totalPooledQRL to ensure fair pricing - shares = stQRL.mintShares(msg.sender, msg.value); - - // Add to buffer (deposited QRL waiting to fund validators) - bufferedQRL += msg.value; - - // Update total pooled QRL (deposit is now under protocol management) - // This must happen AFTER minting to not affect the share calculation - uint256 newTotalPooled = stQRL.totalPooledQRL() + msg.value; - stQRL.updateTotalPooledQRL(newTotalPooled); - - emit Deposited(msg.sender, msg.value, shares); - return shares; - } - - /** - * @notice Preview deposit - get expected shares for QRL amount - * @param qrlAmount Amount of QRL to deposit - * @return shares Expected shares to receive - */ - function previewDeposit(uint256 qrlAmount) external view returns (uint256 shares) { - if (address(stQRL) == address(0)) return qrlAmount; - return stQRL.getSharesByPooledQRL(qrlAmount); - } - - // ============================================================= - // WITHDRAWAL FUNCTIONS - // ============================================================= - - /** - * @notice Request withdrawal of stQRL - * @dev Users can have multiple pending withdrawal requests - * @param shares Amount of shares to withdraw - * @return requestId The ID of this withdrawal request - * @return qrlAmount Current QRL estimate. The actual claim amount is priced - * after accounting is synchronized at claim time. - */ - function requestWithdrawal(uint256 shares) - external - nonReentrant - whenNotPaused - returns (uint256 requestId, uint256 qrlAmount) - { - if (shares == 0) revert ZeroAmount(); - // Spendable shares exclude pending-withdrawal locks and the minimum - // stake lock on fresh deposits (anti-griefing, see stQRLv2). - uint256 unlockedShares = - stQRL.sharesOf(msg.sender) - stQRL.lockedSharesOf(msg.sender) - stQRL.immatureSharesOf(msg.sender); - if (unlockedShares < shares) revert InsufficientShares(); - - // Refresh the request-time estimate when permissionless sync is safe. - // With principal staked off-contract, the rate is owner-controlled and - // the estimate uses the last settled rate. It creates no payout right. - if (_permissionlessSyncAllowed()) { - _syncRewards(); - } - - // Store an informational estimate for history and UI compatibility. - // Claims deliberately reprice these shares after settlement. - uint256 currentPooled = stQRL.totalPooledQRL(); - uint256 convertedAmount = stQRL.getPooledQRLByShares(shares); - qrlAmount = convertedAmount < currentPooled ? convertedAmount : currentPooled; - - // Lock shares so they cannot be transferred - stQRL.lockShares(msg.sender, shares); - - // Create withdrawal request (push to array for multiple requests support) - requestId = withdrawalRequests[msg.sender].length; - withdrawalRequests[msg.sender].push( - WithdrawalRequest({shares: shares, qrlAmount: qrlAmount, requestBlock: block.number, claimed: false}) - ); - - totalWithdrawalShares += shares; - - emit WithdrawalRequested(msg.sender, shares, qrlAmount, block.number); - return (requestId, qrlAmount); - } - - /** - * @notice Claim the next pending withdrawal (FIFO order) - * @dev Reprices and burns shares, then transfers QRL to the user. - * Skips cancelled requests (claimed=true, shares=0) automatically. - * The request-time estimate is not an entitlement. This makes queued - * holders participate in rewards and slashing until their shares burn. - * @return qrlAmount Amount of QRL received - */ - function claimWithdrawal() external nonReentrant returns (uint256 qrlAmount) { - uint256 requestIndex = nextWithdrawalIndex[msg.sender]; - uint256 totalRequests = withdrawalRequests[msg.sender].length; - - // Skip cancelled requests (shares=0 && claimed=true) - // Bounded: user can only create cancellations via their own txs, - // so the practical depth is small. Tested up to 500 in suite. - while (requestIndex < totalRequests && withdrawalRequests[msg.sender][requestIndex].shares == 0) { - requestIndex++; - } - if (requestIndex >= totalRequests) revert NoWithdrawalPending(); - - // Update index to account for skipped cancelled requests - nextWithdrawalIndex[msg.sender] = requestIndex; - - WithdrawalRequest storage request = withdrawalRequests[msg.sender][requestIndex]; - - // === CHECKS === - if (request.claimed) revert NoWithdrawalPending(); - if (block.number < request.requestBlock + WITHDRAWAL_DELAY) revert WithdrawalNotReady(); - - // Refresh pooled accounting when permissionless sync is safe. While - // principal is off-contract, reject claims during any unsettled balance - // delta. The owner must sequence exit settlement and reward sync first. - if (_permissionlessSyncAllowed()) { - _syncRewards(); - } else { - _requireAccountingSettled(); - } - - // Cache shares before state changes - uint256 sharesToBurn = request.shares; - - // Price at settlement so queued holders receive rewards and bear losses - // until their shares are actually removed from circulation. Cap at real - // pooled assets because the token's virtual offset is not spendable QRL. - uint256 currentPooled = stQRL.totalPooledQRL(); - uint256 convertedAmount = stQRL.getPooledQRLByShares(sharesToBurn); - qrlAmount = convertedAmount < currentPooled ? convertedAmount : currentPooled; - - if (withdrawalReserve < qrlAmount || address(this).balance < qrlAmount) { - revert InsufficientReserve(); - } - - // Unlock shares before burning - stQRL.unlockShares(msg.sender, sharesToBurn); - - // Burn shares at the same synchronized exchange rate used above. - stQRL.burnShares(msg.sender, sharesToBurn); - - // === EFFECTS === - request.qrlAmount = qrlAmount; - request.claimed = true; - nextWithdrawalIndex[msg.sender] = requestIndex + 1; - totalWithdrawalShares -= sharesToBurn; - withdrawalReserve -= qrlAmount; - uint256 bufferedReserveConsumed = qrlAmount < bufferedQRLInReserve ? qrlAmount : bufferedQRLInReserve; - bufferedQRLInReserve -= bufferedReserveConsumed; - - // Reserved QRL stays pooled until claim. Remove the claimed assets in - // the same transaction that removes their shares, preserving the rate - // for every remaining and newly minted share. - stQRL.updateTotalPooledQRL(currentPooled - qrlAmount); - - // === INTERACTION (ETH transfer last) === - (bool success,) = msg.sender.call{value: qrlAmount}(""); - if (!success) revert TransferFailed(); - - emit WithdrawalClaimed(msg.sender, sharesToBurn, qrlAmount); - return qrlAmount; - } - - /** - * @notice Cancel a specific pending withdrawal request - * @dev Returns shares to normal circulating state. Only unclaimed requests can be cancelled. - * @param requestId The index of the withdrawal request to cancel - */ - function cancelWithdrawal(uint256 requestId) external nonReentrant { - if (requestId >= withdrawalRequests[msg.sender].length) revert InvalidWithdrawalIndex(); - if (requestId < nextWithdrawalIndex[msg.sender]) revert InvalidWithdrawalIndex(); // Already processed - - WithdrawalRequest storage request = withdrawalRequests[msg.sender][requestId]; - - if (request.shares == 0) revert NoWithdrawalPending(); - if (request.claimed) revert NoWithdrawalPending(); - - uint256 shares = request.shares; - totalWithdrawalShares -= shares; - request.shares = 0; - request.claimed = true; // Mark as processed - - // Unlock shares so they can be transferred again - stQRL.unlockShares(msg.sender, shares); - - emit WithdrawalCancelled(msg.sender, requestId, shares); - } - - /** - * @notice Get withdrawal request details by index - * @param user Address to query - * @param requestId Index of the withdrawal request - */ - function getWithdrawalRequest(address user, uint256 requestId) - external - view - returns ( - uint256 shares, - uint256 currentQRLValue, - uint256 requestBlock, - bool canClaim, - uint256 blocksRemaining, - bool claimed - ) - { - if (requestId >= withdrawalRequests[user].length) { - return (0, 0, 0, false, 0, false); - } - - WithdrawalRequest storage request = withdrawalRequests[user][requestId]; - shares = request.shares; - uint256 currentPooled = stQRL.totalPooledQRL(); - uint256 convertedAmount = stQRL.getPooledQRLByShares(shares); - currentQRLValue = convertedAmount < currentPooled ? convertedAmount : currentPooled; - requestBlock = request.requestBlock; - claimed = request.claimed; - - uint256 unlockBlock = request.requestBlock + WITHDRAWAL_DELAY; - bool accountingSettled = _actualTotalPooled() == stQRL.totalPooledQRL(); - canClaim = !request.claimed && request.shares > 0 && block.number >= unlockBlock - && withdrawalReserve >= currentQRLValue && address(this).balance >= currentQRLValue && accountingSettled; - - blocksRemaining = block.number >= unlockBlock ? 0 : unlockBlock - block.number; - } - - /** - * @notice Get the number of withdrawal requests for a user - * @param user Address to query - * @return total Total number of requests - * @return pending Number of pending (unprocessed) requests - */ - function getWithdrawalRequestCount(address user) external view returns (uint256 total, uint256 pending) { - total = withdrawalRequests[user].length; - uint256 nextIndex = nextWithdrawalIndex[user]; - pending = total > nextIndex ? total - nextIndex : 0; - } - - // ============================================================= - // REWARD SYNC FUNCTIONS - // ============================================================= - - /** - * @notice Sync rewards from validator balance changes - * @dev Trustless and permissionless while all principal is on-contract - * (stakedQRL == 0). While validator principal is staked off-contract - * (stakedQRL > 0) this is restricted to the owner — see - * {_permissionlessSyncAllowed} for why. - */ - function syncRewards() external nonReentrant { - if (!_permissionlessSyncAllowed() && msg.sender != owner) revert NotOwner(); - _syncRewards(); - } - - /** - * @notice Whether reward sync may be triggered permissionlessly right now. - * @dev Sync infers rewards/slashing from balance deltas. That inference is - * only unambiguous while every QRL of principal sits in this contract - * (stakedQRL == 0): then `balance` is exactly the pooled total. - * - * Once a real fundValidator() forwards principal to the beacon deposit - * contract (stakedQRL > 0), an exit sweep returning that principal via - * EIP-4895 lands in `address(this).balance` automatically, a block - * before the owner can call recordValidatorExit() to settle it. In - * that window `balance + stakedQRL` double-counts the principal, so a - * sync would book it as a large phantom reward and spike the exchange - * rate. A permissionless caller could front-run the settlement and - * claim a matured queued withdrawal at that inflated rate. - * - * So while principal is off-contract the exchange rate only moves - * under owner control: the operator sequences recordValidatorExit() - * and syncRewards() so settlement and reward recognition cannot be - * front-run. The MVP path (fundValidatorMVP keeps QRL in-contract, - * stakedQRL == 0) is unaffected and stays fully permissionless. - */ - function _permissionlessSyncAllowed() internal view returns (bool) { - return stakedQRL == 0; - } - - /** - * @dev Internal reward sync logic - * - * Balance accounting: - * On-contract QRL = bufferedQRL + rewards + withdrawalReserve - * Off-contract QRL = stakedQRL (principal sent to the beacon deposit - * contract via fundValidator; still under protocol management) - * withdrawalReserve remains pooled until claim burns the matching shares - * actualTotalPooled = balance + stakedQRL - * - * If actualTotalPooled > previousPooled → rewards arrived - * If actualTotalPooled < previousPooled → slashing occurred - * - * Note: For MVP (fundValidatorMVP), staked QRL stays in the contract and - * stakedQRL is zero, so this reduces to balance. - * For production (fundValidator), staked QRL leaves for the beacon deposit - * contract; adding stakedQRL back keeps the funding from registering as a - * slashing event. When exit proceeds return via EIP-4895, the owner calls - * recordValidatorExit() to move that principal back from stakedQRL into the - * on-contract balance so it is not double-counted as rewards. - */ - function _syncRewards() internal { - if (address(stQRL) == address(0)) return; - - _syncToActualTotal(_actualTotalPooled()); - } - - /** - * @dev Reconcile the balance that existed immediately before a deposit. - * With no off-contract stake this is permissionless and unambiguous. - * With stake off-contract, any delta must be settled by the owner first - * because an exit return can be indistinguishable from a reward. - */ - function _syncBeforeDeposit(uint256 depositAmount) internal { - uint256 actualBeforeDeposit = address(this).balance - depositAmount + stakedQRL; - - if (_permissionlessSyncAllowed()) { - _syncToActualTotal(actualBeforeDeposit); - } else if (actualBeforeDeposit != stQRL.totalPooledQRL()) { - revert AccountingNotSettled(); - } - } - - /** - * @dev Revert while an on-chain balance delta is waiting for owner settlement. - */ - function _requireAccountingSettled() internal view { - if (_actualTotalPooled() != stQRL.totalPooledQRL()) revert AccountingNotSettled(); - } - - /** - * @dev Assets backing all outstanding shares, including liquid reserves. - */ - function _actualTotalPooled() internal view returns (uint256) { - return address(this).balance + stakedQRL; - } - - /** - * @dev Attribute a previously computed actual asset total as reward or loss. - */ - function _syncToActualTotal(uint256 actualTotalPooled) internal { - // What we previously tracked as pooled - uint256 previousPooled = stQRL.totalPooledQRL(); - - // Compare and attribute difference - if (actualTotalPooled > previousPooled) { - // Rewards arrived (via EIP-4895 or direct transfer) - uint256 rewards = actualTotalPooled - previousPooled; - totalRewardsReceived += rewards; - stQRL.updateTotalPooledQRL(actualTotalPooled); - lastSyncBlock = block.number; - - emit RewardsSynced(rewards, actualTotalPooled, block.number); - } else if (actualTotalPooled < previousPooled) { - // Slashing detected (or funds removed somehow) - uint256 loss = previousPooled - actualTotalPooled; - totalSlashingLosses += loss; - stQRL.updateTotalPooledQRL(actualTotalPooled); - lastSyncBlock = block.number; - - emit SlashingDetected(loss, actualTotalPooled, block.number); - } - // If equal, no change needed - } - - // ============================================================= - // VALIDATOR FUNCTIONS - // ============================================================= - - /** - * @notice Fund a validator with beacon chain deposit - * @dev Only owner can call. Sends VALIDATOR_STAKE to beacon deposit contract. - * @param pubkey Dilithium public key (2592 bytes) - * @param withdrawal_credentials Must point to this contract (0x00 + 11 zero bytes + address) - * @param signature ML-DSA-87 signature (4627 bytes) - * @param deposit_data_root SSZ hash of deposit data - * @return validatorId The new validator's ID - */ - function fundValidator( - bytes calldata pubkey, - bytes calldata withdrawal_credentials, - bytes calldata signature, - bytes32 deposit_data_root - ) external onlyOwner nonReentrant returns (uint256 validatorId) { - if (bufferedQRL < VALIDATOR_STAKE || _availableValidatorLiquidity() < VALIDATOR_STAKE) { - revert InsufficientBuffer(); - } - if (pubkey.length != PUBKEY_LENGTH) revert InvalidPubkeyLength(); - if (signature.length != SIGNATURE_LENGTH) revert InvalidSignatureLength(); - if (withdrawal_credentials.length != CREDENTIALS_LENGTH) revert InvalidCredentialsLength(); - - // Verify withdrawal credentials point to this contract. - // Format: 0x00 (1 byte) + 11 zero bytes + contract address (20 bytes) = 32 bytes. - // The prefix byte 0x00 is `ExecutionAddressWithdrawalPrefixByte` in qrysm's - // beacon config (config/params/mainnet_config.go:74), i.e. "withdrawals - // are executed to the 20-byte address in bytes[12:32]". Ethereum's spec - // uses 0x01 for the same meaning; QRL v2 re-uses 0x00 because there is - // no BLS-key-withdrawal path to distinguish from. This prefix is what - // staking-deposit-cli emits for --execution-address deposits; mismatch - // here would make validators non-withdrawable (stuck stake). - bytes32 expectedCredentials = bytes32(abi.encodePacked(bytes1(0x00), bytes11(0), address(this))); - bytes32 actualCredentials; - assembly { - actualCredentials := calldataload(withdrawal_credentials.offset) - } - if (actualCredentials != expectedCredentials) revert InvalidWithdrawalCredentials(); - - bufferedQRL -= VALIDATOR_STAKE; - // Principal leaves the contract for the beacon deposit contract but - // stays under protocol management. Track it so _syncRewards() does not - // read the outgoing transfer as a slashing loss. - stakedQRL += VALIDATOR_STAKE; - validatorId = validatorCount++; - - // Call beacon deposit contract - IDepositContract(DEPOSIT_CONTRACT).deposit{value: VALIDATOR_STAKE}( - pubkey, withdrawal_credentials, signature, deposit_data_root - ); - - emit ValidatorFunded(validatorId, pubkey, VALIDATOR_STAKE); - return validatorId; - } - - /** - * @notice Fund a validator (MVP testing - no actual beacon deposit) - * @dev Moves QRL from buffer to simulated stake. For testnet only. - * @return validatorId The new validator's ID - */ - function fundValidatorMVP() external onlyOwner nonReentrant returns (uint256 validatorId) { - if (bufferedQRL < VALIDATOR_STAKE || _availableValidatorLiquidity() < VALIDATOR_STAKE) { - revert InsufficientBuffer(); - } - - bufferedQRL -= VALIDATOR_STAKE; - validatorId = validatorCount++; - - // QRL stays in contract, simulating staked funds - emit ValidatorFunded(validatorId, "", VALIDATOR_STAKE); - return validatorId; - } - - /** - * @notice Record that staked principal has returned from the beacon chain - * @dev Validator exit proceeds (principal) arrive via EIP-4895 and land in - * address(this).balance. Without this call, the next _syncRewards() - * would see both the higher balance AND the still-elevated stakedQRL, - * double-counting the principal as fresh rewards. The owner calls this - * to move `amount` from the off-contract stakedQRL accumulator back - * into on-contract accounting, leaving totalPooledQRL unchanged. - * - * The observed return, capped at the retired nominal principal, is - * restored to bufferedQRL. This makes unused exit proceeds eligible - * to fund a later validator. Any surplus remains unbuffered and is - * correctly attributed as rewards by the next _syncRewards(). - * - * MVP note: fundValidatorMVP never increments stakedQRL, so this is - * only relevant once the real beacon path (fundValidator) is in use. - * @param amount Nominal off-contract stake retired (<= stakedQRL) - */ - function recordValidatorExit(uint256 amount) external onlyOwner { - if (amount == 0) revert ZeroAmount(); - if (amount > stakedQRL) revert ExceedsStakedAmount(); - - // At settled accounting, totalPooledQRL - stakedQRL is the expected - // on-contract balance before an exit return. Buffer only liquidity - // observed above that baseline, capped at the nominal stake retired. - // A slashed exit therefore cannot create unsupported buffer credit. - uint256 currentPooled = stQRL.totalPooledQRL(); - uint256 expectedOnContract = currentPooled > stakedQRL ? currentPooled - stakedQRL : 0; - uint256 currentBalance = address(this).balance; - uint256 observedReturn = currentBalance > expectedOnContract ? currentBalance - expectedOnContract : 0; - uint256 returnedPrincipal = observedReturn < amount ? observedReturn : amount; - - stakedQRL -= amount; - bufferedQRL += returnedPrincipal; - - emit ValidatorExitRecorded(amount, stakedQRL); - } - - /** - * @notice Earmark liquid pooled QRL for pending withdrawals - * @dev Reserved QRL remains in totalPooledQRL until claim burns the matching - * shares. Keeping both assets and shares in the conversion totals - * prevents deposits during the queue from receiving inflated shares. - * - * Invariant maintained: address(this).balance + stakedQRL = totalPooledQRL - * - * For MVP: pooled QRL is in the contract, so this earmarks liquidity. - * For production: call this after validator exit proceeds arrive and - * _syncRewards() has already attributed them to totalPooledQRL. - * - * @param amount Amount of liquid pooled QRL to earmark - */ - function fundWithdrawalReserve(uint256 amount) external onlyOwner { - if (amount == 0) revert ZeroAmount(); - - if (_permissionlessSyncAllowed()) { - _syncRewards(); - } else { - _requireAccountingSettled(); - } - - if (amount > _availableValidatorLiquidity()) revert InsufficientBuffer(); - - uint256 availableLiquidity = _availableValidatorLiquidity(); - withdrawalReserve += amount; - - // Consume unbuffered rewards first, then remove any remaining amount - // from deposit-originated validator liquidity. - uint256 unbufferedLiquidity = availableLiquidity > bufferedQRL ? availableLiquidity - bufferedQRL : 0; - if (amount > unbufferedLiquidity) { - uint256 bufferedAmountReserved = amount - unbufferedLiquidity; - bufferedQRL -= bufferedAmountReserved; - bufferedQRLInReserve += bufferedAmountReserved; - } - - emit WithdrawalReserveFunded(amount); - } - - /** - * @notice Release liquid QRL that no longer needs to remain earmarked - * @dev This is used after a queued withdrawal is cancelled or its required - * payout falls. Only the portion originally taken from bufferedQRL is - * restored to the validator buffer. This avoids treating rewards or - * MVP-simulated stake as fresh validator principal. - * @param amount Amount of withdrawal reserve to release - */ - function releaseWithdrawalReserve(uint256 amount) external onlyOwner { - if (amount == 0) revert ZeroAmount(); - if (amount > withdrawalReserve) revert InsufficientReserve(); - - uint256 bufferRestored = amount < bufferedQRLInReserve ? amount : bufferedQRLInReserve; - withdrawalReserve -= amount; - bufferedQRLInReserve -= bufferRestored; - bufferedQRL += bufferRestored; - - emit WithdrawalReserveReleased(amount, bufferRestored); - } - - // ============================================================= - // VIEW FUNCTIONS - // ============================================================= - - /** - * @notice Get pool status - */ - function getPoolStatus() - external - view - returns ( - uint256 totalPooled, - uint256 totalShares, - uint256 buffered, - uint256 validators, - uint256 pendingWithdrawalShares, - uint256 reserveBalance, - uint256 exchangeRate - ) - { - totalPooled = address(stQRL) != address(0) ? stQRL.totalPooledQRL() : 0; - totalShares = address(stQRL) != address(0) ? stQRL.totalShares() : 0; - buffered = bufferedQRL; - validators = validatorCount; - pendingWithdrawalShares = totalWithdrawalShares; - reserveBalance = withdrawalReserve; - exchangeRate = totalShares > 0 ? (totalPooled * 1e18) / totalShares : 1e18; - } - - /** - * @notice Get reward/slashing stats - */ - function getRewardStats() - external - view - returns (uint256 totalRewards, uint256 totalSlashing, uint256 netRewards, uint256 lastSync) - { - totalRewards = totalRewardsReceived; - totalSlashing = totalSlashingLosses; - netRewards = totalRewardsReceived > totalSlashingLosses ? totalRewardsReceived - totalSlashingLosses : 0; - lastSync = lastSyncBlock; - } - - /** - * @notice Check if validator funding is possible - */ - function canFundValidator() external view returns (bool possible, uint256 bufferedAmount) { - possible = bufferedQRL >= VALIDATOR_STAKE && _availableValidatorLiquidity() >= VALIDATOR_STAKE; - bufferedAmount = bufferedQRL; - } - - /** - * @dev On-contract QRL that has not been earmarked for withdrawals. - */ - function _availableValidatorLiquidity() internal view returns (uint256) { - uint256 balance = address(this).balance; - return balance > withdrawalReserve ? balance - withdrawalReserve : 0; - } - - // ============================================================= - // ADMIN FUNCTIONS - // ============================================================= - - /** - * @notice Set the stQRL token contract (one-time only) - * @param _stQRL Address of stQRL contract - */ - function setStQRL(address _stQRL) external onlyOwner { - if (_stQRL == address(0)) revert ZeroAddress(); - if (address(stQRL) != address(0)) revert StQRLAlreadySet(); - stQRL = IstQRL(_stQRL); - emit StQRLSet(_stQRL); - } - - /** - * @notice Set minimum deposit amount - * @param _minDeposit New minimum deposit - */ - function setMinDeposit(uint256 _minDeposit) external onlyOwner { - if (_minDeposit < minDepositFloor) revert BelowMinDepositFloor(); - minDeposit = _minDeposit; - emit MinDepositUpdated(_minDeposit); - } - - /** - * @notice Set the adjustable floor for minDeposit - * @dev Allows owner to lower the floor post-deployment (e.g., if QRL appreciates) - * @param _floor New floor value (must be >= ABSOLUTE_MIN_DEPOSIT) - */ - function setMinDepositFloor(uint256 _floor) external onlyOwner { - if (_floor < ABSOLUTE_MIN_DEPOSIT) revert BelowAbsoluteMin(); - minDepositFloor = _floor; - emit MinDepositFloorUpdated(_floor); - } - - /** - * @notice Pause the contract - */ - function pause() external onlyOwner { - paused = true; - emit Paused(msg.sender); - } - - /** - * @notice Unpause the contract - */ - function unpause() external onlyOwner { - paused = false; - emit Unpaused(msg.sender); - } - - /** - * @notice Transfer ownership - * @param newOwner New owner address - */ - function transferOwnership(address newOwner) external onlyOwner { - if (newOwner == address(0)) revert ZeroAddress(); - emit OwnershipTransferred(owner, newOwner); - owner = newOwner; - } - - /** - * @notice Emergency withdrawal of stuck funds - * @dev Native balance deltas are treated as pooled rewards after stQRL is - * configured because they cannot be distinguished from validator - * rewards. Recovery is therefore limited to demonstrably unaccounted - * funds, primarily before pool initialization. - * @param to Recipient address - * @param amount Amount to withdraw - */ - function emergencyWithdraw(address to, uint256 amount) external onlyOwner nonReentrant { - if (to == address(0)) revert ZeroAddress(); - if (amount == 0) revert ZeroAmount(); - - if (address(stQRL) != address(0)) { - _requireAccountingSettled(); - } - - // Calculate recoverable amount from balance minus on-contract pooled funds. - // totalPooledQRL includes stakedQRL, which lives off-contract at the beacon - // deposit contract, so it must be excluded when comparing against this balance. - uint256 pooled = address(stQRL) != address(0) ? stQRL.totalPooledQRL() : 0; - uint256 onContractPooled = pooled > stakedQRL ? pooled - stakedQRL : 0; - // withdrawalReserve is a subset of pooled assets. Use the larger value - // defensively if accounting is temporarily stale instead of counting it twice. - uint256 totalProtocolFunds = onContractPooled > withdrawalReserve ? onContractPooled : withdrawalReserve; - uint256 currentBalance = address(this).balance; - uint256 recoverableAmount = currentBalance > totalProtocolFunds ? currentBalance - totalProtocolFunds : 0; - - if (amount > recoverableAmount) revert ExceedsRecoverableAmount(); - - (bool success,) = to.call{value: amount}(""); - if (!success) revert TransferFailed(); - - emit EmergencyWithdrawal(to, amount); - } - - // ============================================================= - // RECEIVE FUNCTION - // ============================================================= - - /** - * @notice Receive QRL (from validator exits, rewards, or direct sends) - * @dev Rewards arrive via EIP-4895 WITHOUT triggering this function. - * This is only triggered by explicit transfers (e.g. validator exit - * proceeds via a regular transaction). - * - * Incoming ETH is NOT auto-classified. It increases address(this).balance, - * and the next _syncRewards() call will detect it as a balance increase - * and attribute it to totalPooledQRL. The owner can then call - * fundWithdrawalReserve() to earmark it for pending withdrawals. - */ - receive() external payable { - // No automatic accounting - _syncRewards() will detect the balance change - } -} diff --git a/contracts/hyperion/README.md b/contracts/hyperion/README.md index 40d945f..4ae7402 100644 --- a/contracts/hyperion/README.md +++ b/contracts/hyperion/README.md @@ -1,40 +1,5 @@ -# Hyperion Sources +# Retired token contracts -Generated Hyperion mirrors of the canonical Solidity contracts. Kept peer to `contracts/solidity/` so both language flavors are co-located under `contracts/`. +The former `stQRL-v2.hyp`, `DepositPool-v2.hyp` and `ValidatorManager.hyp` contracts and their token-specific tests are removed. The native implementation lives in [native/contracts](../../native/contracts/), with current [architecture](../../docs/architecture.md) and [validation](../../native/IMPLEMENTATION.md). -## Layout - -``` -contracts/solidity/*.sol # canonical, hand-edited -contracts/hyperion/*.hyp # generated mirrors - do not edit directly -contracts/test/*.sol # Foundry test suite (canonical) -contracts/test/hyperion/*.t.hyp # generated test mirrors (reference only; not compiled) -build/hyperion/ # hypc output (ABI, bin, manifest.json) - gitignored -config/testnet-hyperion.json # deployment addresses + provider for the Hyperion path -``` - -## Workflow - -1. Sync the generated Hyperion sources from the Solidity originals: - -```bash -node scripts/sync-hyperion.js -``` - -2. Compile with the Hyperion compiler: - -```bash -HYPERION_COMPILER=/path/to/hypc node scripts/compile-hyperion.js -``` - -3. Deploy the v2 contracts to QRL: - -```bash -TESTNET_SEED="..." node scripts/deploy-hyperion.js -``` - -## Notes - -- The Solidity sources in `contracts/solidity/` are the canonical editing target. Never hand-edit a `.hyp` file - regenerate. -- `scripts/sync-hyperion.js` translates three Solidity-vs-Hyperion dialect differences: pragma version, unit suffixes (`ether/wei/gwei` → `quanta/planck/shor`), and address literal prefix (`0x<40hex>` → `Q<40hex>`). -- The Foundry tests in `contracts/test/` are the canonical test suite; `contracts/test/hyperion/` is a mirrored compatibility layer kept for reference but not run through `hypc`. +No token ABI, allowance, exchange-rate conversion or storage migration is required. Previous design details remain in the explicitly [archived architecture](../../docs/legacy/V2-ARCHITECTURE.md). Old on-chain deployments are abandoned and untouched. diff --git a/contracts/hyperion/ValidatorManager.hyp b/contracts/hyperion/ValidatorManager.hyp deleted file mode 100644 index b69dd28..0000000 --- a/contracts/hyperion/ValidatorManager.hyp +++ /dev/null @@ -1,351 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -// Generated from ../contracts/solidity/ValidatorManager.sol by scripts/sync-hyperion.js. -// Edit the Solidity source first, then re-run this script. -pragma hyperion >=0.0; - -/** - * @title ValidatorManager - Simplified Validator Tracking for QuantaPool - * @author QuantaPool - * @notice Tracks validator pubkeys and status for the liquid staking pool - * - * @dev MVP Design: - * - Single trusted operator (owner) - * - No bonds, collateral, or complex economics - * - Simple validator state machine: Pending → Active → Exiting → Exited - * - Future: Permissionless operator registration - * - * This contract is intentionally minimal. Complex operator economics - * can be added in v3 after the core staking mechanism is proven. - */ -contract ValidatorManager { - // ============================================================= - // CONSTANTS - // ============================================================= - - /// @notice QRL validator stake amount (MaxEffectiveBalance from QRL beacon config) - uint256 public constant VALIDATOR_STAKE = 40_000 quanta; - - /// @notice Dilithium pubkey length - uint256 private constant PUBKEY_LENGTH = 2592; - - // ============================================================= - // ENUMS - // ============================================================= - - /// @notice Validator lifecycle states - enum ValidatorStatus { - None, // Not registered - Pending, // Registered, awaiting activation - Active, // Currently validating - Exiting, // Exit requested - Exited, // Fully exited, funds returned - Slashed // Slashed (for record keeping) - } - - // ============================================================= - // STRUCTS - // ============================================================= - - /// @notice Validator data - struct Validator { - bytes pubkey; // Dilithium public key (2592 bytes) - ValidatorStatus status; // Current status - uint256 activatedBlock; // Block when activated - uint256 exitedBlock; // Block when exited (0 if not exited) - } - - // ============================================================= - // STORAGE - // ============================================================= - - /// @notice Contract owner (operator for MVP) - address public owner; - - /// @notice DepositPool contract (authorized to register validators) - address public depositPool; - - /// @notice Validator data by index - mapping(uint256 => Validator) public validators; - - /// @notice Pubkey hash to validator index - mapping(bytes32 => uint256) public pubkeyToIndex; - - /// @notice Total validators ever registered - uint256 public totalValidators; - - /// @notice Count of active validators - uint256 public activeValidatorCount; - - /// @notice Count of pending validators - uint256 public pendingValidatorCount; - - // ============================================================= - // EVENTS - // ============================================================= - - event ValidatorRegistered(uint256 indexed validatorId, bytes pubkey, ValidatorStatus status); - - event ValidatorActivated(uint256 indexed validatorId, uint256 activatedBlock); - - event ValidatorExitRequested(uint256 indexed validatorId, uint256 requestBlock); - - event ValidatorExited(uint256 indexed validatorId, uint256 exitedBlock); - - event ValidatorSlashed(uint256 indexed validatorId, uint256 slashedBlock); - - event DepositPoolSet(address indexed depositPool); - event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); - - // ============================================================= - // ERRORS - // ============================================================= - - error NotOwner(); - error NotDepositPool(); - error NotAuthorized(); - error ZeroAddress(); - error InvalidPubkeyLength(); - error ValidatorAlreadyExists(); - error ValidatorNotFound(); - error InvalidStatusTransition(); - - // ============================================================= - // MODIFIERS - // ============================================================= - - modifier onlyOwner() { - if (msg.sender != owner) revert NotOwner(); - _; - } - - modifier onlyDepositPool() { - if (msg.sender != depositPool) revert NotDepositPool(); - _; - } - - modifier onlyAuthorized() { - if (msg.sender != owner && msg.sender != depositPool) revert NotAuthorized(); - _; - } - - // ============================================================= - // CONSTRUCTOR - // ============================================================= - - constructor() { - owner = msg.sender; - } - - // ============================================================= - // VALIDATOR REGISTRATION - // ============================================================= - - /** - * @notice Register a new validator - * @dev Called by DepositPool when funding a validator - * @param pubkey Dilithium public key (2592 bytes) - * @return validatorId The new validator's index - */ - function registerValidator(bytes calldata pubkey) external onlyAuthorized returns (uint256 validatorId) { - if (pubkey.length != PUBKEY_LENGTH) revert InvalidPubkeyLength(); - - bytes32 pubkeyHash = keccak256(pubkey); - if (pubkeyToIndex[pubkeyHash] != 0) revert ValidatorAlreadyExists(); - - // Validator IDs start at 1 (0 means not found) - validatorId = ++totalValidators; - - validators[validatorId] = - Validator({pubkey: pubkey, status: ValidatorStatus.Pending, activatedBlock: 0, exitedBlock: 0}); - - pubkeyToIndex[pubkeyHash] = validatorId; - pendingValidatorCount++; - - emit ValidatorRegistered(validatorId, pubkey, ValidatorStatus.Pending); - return validatorId; - } - - // ============================================================= - // STATUS TRANSITIONS - // ============================================================= - - /** - * @notice Mark validator as active (confirmed on beacon chain) - * @param validatorId The validator to activate - */ - function activateValidator(uint256 validatorId) external onlyOwner { - Validator storage v = validators[validatorId]; - if (v.status != ValidatorStatus.Pending) revert InvalidStatusTransition(); - - v.status = ValidatorStatus.Active; - v.activatedBlock = block.number; - - pendingValidatorCount--; - activeValidatorCount++; - - emit ValidatorActivated(validatorId, block.number); - } - - /** - * @notice Mark validator as exiting - * @param validatorId The validator requesting exit - */ - function requestValidatorExit(uint256 validatorId) external onlyOwner { - Validator storage v = validators[validatorId]; - if (v.status != ValidatorStatus.Active) revert InvalidStatusTransition(); - - v.status = ValidatorStatus.Exiting; - - emit ValidatorExitRequested(validatorId, block.number); - } - - /** - * @notice Mark validator as fully exited - * @param validatorId The validator that has exited - */ - function markValidatorExited(uint256 validatorId) external onlyOwner { - Validator storage v = validators[validatorId]; - if (v.status != ValidatorStatus.Exiting) revert InvalidStatusTransition(); - - v.status = ValidatorStatus.Exited; - v.exitedBlock = block.number; - - activeValidatorCount--; - - emit ValidatorExited(validatorId, block.number); - } - - /** - * @notice Mark validator as slashed - * @param validatorId The slashed validator - */ - function markValidatorSlashed(uint256 validatorId) external onlyOwner { - Validator storage v = validators[validatorId]; - ValidatorStatus previousStatus = v.status; - - if (previousStatus != ValidatorStatus.Active && previousStatus != ValidatorStatus.Exiting) { - revert InvalidStatusTransition(); - } - - v.status = ValidatorStatus.Slashed; - v.exitedBlock = block.number; - - // Decrement counter - both Active and Exiting validators count toward activeValidatorCount - activeValidatorCount--; - - emit ValidatorSlashed(validatorId, block.number); - } - - /** - * @notice Batch activate multiple validators - * @param validatorIds Array of validator IDs to activate - */ - function batchActivateValidators(uint256[] calldata validatorIds) external onlyOwner { - for (uint256 i = 0; i < validatorIds.length; i++) { - Validator storage v = validators[validatorIds[i]]; - if (v.status == ValidatorStatus.Pending) { - v.status = ValidatorStatus.Active; - v.activatedBlock = block.number; - pendingValidatorCount--; - activeValidatorCount++; - emit ValidatorActivated(validatorIds[i], block.number); - } - } - } - - // ============================================================= - // VIEW FUNCTIONS - // ============================================================= - - /** - * @notice Get validator details - * @param validatorId The validator to query - */ - function getValidator(uint256 validatorId) - external - view - returns (bytes memory pubkey, ValidatorStatus status, uint256 activatedBlock, uint256 exitedBlock) - { - Validator storage v = validators[validatorId]; - return (v.pubkey, v.status, v.activatedBlock, v.exitedBlock); - } - - /** - * @notice Get validator ID by pubkey - * @param pubkey The pubkey to look up - * @return validatorId (0 if not found) - */ - function getValidatorIdByPubkey(bytes calldata pubkey) external view returns (uint256) { - return pubkeyToIndex[keccak256(pubkey)]; - } - - /** - * @notice Get validator status by pubkey - * @param pubkey The pubkey to look up - */ - function getValidatorStatus(bytes calldata pubkey) external view returns (ValidatorStatus) { - uint256 validatorId = pubkeyToIndex[keccak256(pubkey)]; - if (validatorId == 0) return ValidatorStatus.None; - return validators[validatorId].status; - } - - /** - * @notice Get summary statistics - */ - function getStats() external view returns (uint256 total, uint256 pending, uint256 active, uint256 totalStaked) { - total = totalValidators; - pending = pendingValidatorCount; - active = activeValidatorCount; - totalStaked = activeValidatorCount * VALIDATOR_STAKE; - } - - /** - * @notice Get all validators in a specific status - * @param status The status to filter by - * @return validatorIds Array of matching validator IDs - */ - function getValidatorsByStatus(ValidatorStatus status) external view returns (uint256[] memory validatorIds) { - // First pass: count matches - uint256 count = 0; - for (uint256 i = 1; i <= totalValidators; i++) { - if (validators[i].status == status) { - count++; - } - } - - // Second pass: collect IDs - validatorIds = new uint256[](count); - uint256 index = 0; - for (uint256 i = 1; i <= totalValidators; i++) { - if (validators[i].status == status) { - validatorIds[index++] = i; - } - } - - return validatorIds; - } - - // ============================================================= - // ADMIN FUNCTIONS - // ============================================================= - - /** - * @notice Set the DepositPool contract - * @param _depositPool Address of DepositPool - */ - function setDepositPool(address _depositPool) external onlyOwner { - if (_depositPool == address(0)) revert ZeroAddress(); - depositPool = _depositPool; - emit DepositPoolSet(_depositPool); - } - - /** - * @notice Transfer ownership - * @param newOwner New owner address - */ - function transferOwnership(address newOwner) external onlyOwner { - if (newOwner == address(0)) revert ZeroAddress(); - emit OwnershipTransferred(owner, newOwner); - owner = newOwner; - } -} diff --git a/contracts/hyperion/stQRL-v2.hyp b/contracts/hyperion/stQRL-v2.hyp deleted file mode 100644 index 06d689a..0000000 --- a/contracts/hyperion/stQRL-v2.hyp +++ /dev/null @@ -1,578 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -// Generated from ../contracts/solidity/stQRL-v2.sol by scripts/sync-hyperion.js. -// Edit the Solidity source first, then re-run this script. -pragma hyperion >=0.0; - -/** - * @title stQRL v2 - Fixed-Balance Staked QRL Token - * @author QuantaPool - * @notice Liquid staking token for QRL. Balance represents shares (fixed), - * use getQRLValue() to see current QRL equivalent. - * - * @dev Key concepts: - * - balanceOf() returns raw shares (stable, tax-friendly) - * - getQRLValue() returns QRL equivalent (changes with rewards/slashing) - * - Exchange rate: totalPooledQRL / totalShares - * - * This is a fixed-balance model (like wstETH) rather than rebasing (like stETH). - * Chosen for cleaner tax implications - balance only changes on deposit/withdraw. - * - * Example: - * 1. User deposits 100 QRL when pool has 1000 QRL and 1000 shares - * 2. User receives 100 shares, balanceOf() = 100 - * 3. Validators earn 50 QRL rewards (pool now has 1050 QRL) - * 4. User's balanceOf() still = 100 shares (unchanged) - * 5. User's getQRLValue() = 100 * 1050 / 1000 = 105 QRL - * - * If slashing occurs (pool drops to 950 QRL): - * - User's balanceOf() still = 100 shares - * - User's getQRLValue() = 100 * 950 / 1000 = 95 QRL - */ -contract stQRLv2 { - // ============================================================= - // CONSTANTS - // ============================================================= - - string public constant name = "Staked QRL"; - string public constant symbol = "stQRL"; - uint8 public constant decimals = 18; - - /// @notice Initial shares per QRL (1:1 at launch) - uint256 private constant INITIAL_SHARES_PER_QRL = 1; - - /// @notice Virtual shares offset to prevent first depositor attack (donation attack) - /// @dev Adding virtual shares/assets creates a floor that makes share inflation attacks - /// economically unviable. With 1e3 virtual offset, an attacker would need to - /// donate ~1000x more than they could steal. See OpenZeppelin ERC4626 for details. - uint256 private constant VIRTUAL_SHARES = 1e3; - uint256 private constant VIRTUAL_ASSETS = 1e3; - - /// @notice Upper bound for the minimum stake lock (about 30 days at ~56s blocks) - /// @dev Caps owner power: deposits can never be locked longer than this. - uint256 public constant MAX_MIN_STAKE_BLOCKS = 46500; - - // ============================================================= - // SHARE STORAGE - // ============================================================= - - /// @notice Total shares in existence - uint256 private _totalShares; - - /// @notice Shares held by each account - mapping(address => uint256) private _shares; - - /// @notice Allowances for transferFrom (in shares) - /// @dev All amounts in this contract are shares, not QRL - mapping(address => mapping(address => uint256)) private _allowances; - - /// @notice Shares locked for pending withdrawals (cannot be transferred) - mapping(address => uint256) private _lockedShares; - - /// @notice Minimum stake lock applied to fresh deposits, in blocks (0 disables) - /// @dev Anti-griefing: prevents deposit/withdraw yo-yo cycles that force the - /// operator to bridge liquidity or exit validators at no cost to the - /// attacker. Owner deposits are exempt (bridge capital stays nimble). - uint256 public minStakeBlocks = 1536; - - /// @notice Shares from recent deposits that have not matured yet - /// @dev Immature shares cannot be transferred or queued for withdrawal. - /// Matures lazily: immatureSharesOf() reports 0 once _matureAtBlock - /// passes, no storage write needed. A new deposit folds any remaining - /// immature shares into the new bucket and resets the maturity block; - /// already-matured shares are unaffected. Transfers never write to the - /// recipient's bucket, so third parties cannot extend someone's lock. - mapping(address => uint256) private _immatureShares; - - /// @notice Block at which an account's immature bucket matures - mapping(address => uint256) private _matureAtBlock; - - // ============================================================= - // POOL STORAGE - // ============================================================= - - /// @notice Total QRL controlled by the protocol (staked + rewards - slashing) - /// @dev Updated by DepositPool via updateTotalPooledQRL() - uint256 private _totalPooledQRL; - - // ============================================================= - // ACCESS CONTROL - // ============================================================= - - /// @notice Contract owner (for initial setup) - address public owner; - - /// @notice DepositPool contract (only address that can mint/burn/update) - address public depositPool; - - /// @notice Pause state for emergencies - bool public paused; - - // ============================================================= - // EVENTS - // ============================================================= - - // QRC-20 standard events (values are in shares) - event Transfer(address indexed from, address indexed to, uint256 value); - event Approval(address indexed owner, address indexed spender, uint256 value); - - // Pool events - event TotalPooledQRLUpdated(uint256 previousAmount, uint256 newAmount); - event SharesMinted(address indexed to, uint256 sharesAmount, uint256 qrlAmount); - event SharesBurned(address indexed from, uint256 sharesAmount, uint256 qrlAmount); - - // Admin events - event DepositPoolSet(address indexed previousPool, address indexed newPool); - event MinStakeBlocksUpdated(uint256 previousValue, uint256 newValue); - event Paused(address account); - event Unpaused(address account); - event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); - - // ============================================================= - // ERRORS - // ============================================================= - - error NotOwner(); - error NotDepositPool(); - error ContractPaused(); - error ZeroAddress(); - error ZeroAmount(); - error InsufficientBalance(); - error InsufficientAllowance(); - error DepositPoolAlreadySet(); - error InsufficientUnlockedShares(); - error InsufficientMaturedShares(); - error MinStakeBlocksTooHigh(); - - // ============================================================= - // MODIFIERS - // ============================================================= - - modifier onlyOwner() { - if (msg.sender != owner) revert NotOwner(); - _; - } - - modifier onlyDepositPool() { - if (msg.sender != depositPool) revert NotDepositPool(); - _; - } - - modifier whenNotPaused() { - if (paused) revert ContractPaused(); - _; - } - - // ============================================================= - // CONSTRUCTOR - // ============================================================= - - constructor() { - owner = msg.sender; - } - - // ============================================================= - // QRC-20 VIEW FUNCTIONS - // ============================================================= - - /** - * @notice Returns the total supply of stQRL tokens (in shares) - * @dev Use totalPooledQRL() for the QRL value - * @return Total stQRL shares in circulation - */ - function totalSupply() external view returns (uint256) { - return _totalShares; - } - - /** - * @notice Returns the stQRL balance of an account (in shares) - * @dev Returns raw shares - stable value that only changes on deposit/withdraw - * Use getQRLValue() for the current QRL equivalent - * @param account The address to query - * @return The account's share balance - */ - function balanceOf(address account) public view returns (uint256) { - return _shares[account]; - } - - /** - * @notice Returns the allowance for a spender (in shares) - * @param _owner The token owner - * @param spender The approved spender - * @return The allowance in shares - */ - function allowance(address _owner, address spender) public view returns (uint256) { - return _allowances[_owner][spender]; - } - - // ============================================================= - // QRC-20 WRITE FUNCTIONS - // ============================================================= - - /** - * @notice Transfer stQRL shares to another address - * @param to Recipient address - * @param amount Amount of shares to transfer - * @return success True if transfer succeeded - */ - function transfer(address to, uint256 amount) external whenNotPaused returns (bool) { - _transfer(msg.sender, to, amount); - return true; - } - - /** - * @notice Approve a spender to transfer stQRL shares on your behalf - * @param spender The address to approve - * @param amount The amount of shares to approve - * @return success True if approval succeeded - */ - function approve(address spender, uint256 amount) external returns (bool) { - _approve(msg.sender, spender, amount); - return true; - } - - /** - * @notice Transfer stQRL shares from one address to another (with approval) - * @param from Source address - * @param to Destination address - * @param amount Amount of shares to transfer - * @return success True if transfer succeeded - */ - function transferFrom(address from, address to, uint256 amount) external whenNotPaused returns (bool) { - if (amount == 0) revert ZeroAmount(); - - uint256 currentAllowance = _allowances[from][msg.sender]; - if (currentAllowance < amount) revert InsufficientAllowance(); - - // Decrease allowance (unless unlimited) - if (currentAllowance != type(uint256).max) { - _allowances[from][msg.sender] = currentAllowance - amount; - emit Approval(from, msg.sender, _allowances[from][msg.sender]); - } - - _transfer(from, to, amount); - return true; - } - - // ============================================================= - // SHARE VIEW FUNCTIONS - // ============================================================= - - /** - * @notice Returns the total shares in existence - * @dev Same as totalSupply() in fixed-balance model - * @return Total shares - */ - function totalShares() external view returns (uint256) { - return _totalShares; - } - - /** - * @notice Returns the shares held by an account - * @dev Same as balanceOf() in fixed-balance model - * @param account The address to query - * @return The account's share balance - */ - function sharesOf(address account) external view returns (uint256) { - return _shares[account]; - } - - /** - * @notice Returns the current QRL value of an account's shares - * @dev This is what would have been balanceOf() in a rebasing model - * Value changes as rewards accrue or slashing occurs - * @param account The address to query - * @return The account's stQRL value in QRL terms - */ - function getQRLValue(address account) public view returns (uint256) { - return getPooledQRLByShares(_shares[account]); - } - - /** - * @notice Convert a QRL amount to shares - * @dev shares = qrlAmount * (totalShares + VIRTUAL_SHARES) / (totalPooledQRL + VIRTUAL_ASSETS) - * Virtual offsets prevent first depositor inflation attacks. - * @param qrlAmount The QRL amount to convert - * @return The equivalent number of shares - */ - function getSharesByPooledQRL(uint256 qrlAmount) public view returns (uint256) { - // Use virtual shares/assets to prevent donation attacks - // Even with 0 real shares/assets, the virtual offset ensures fair pricing - return (qrlAmount * (_totalShares + VIRTUAL_SHARES)) / (_totalPooledQRL + VIRTUAL_ASSETS); - } - - /** - * @notice Convert shares to QRL amount - * @dev qrlAmount = shares * (totalPooledQRL + VIRTUAL_ASSETS) / (totalShares + VIRTUAL_SHARES) - * Virtual offsets prevent first depositor inflation attacks. - * @param sharesAmount The shares to convert - * @return The equivalent QRL amount - */ - function getPooledQRLByShares(uint256 sharesAmount) public view returns (uint256) { - // Use virtual shares/assets to prevent donation attacks - // This ensures consistent pricing with getSharesByPooledQRL - return (sharesAmount * (_totalPooledQRL + VIRTUAL_ASSETS)) / (_totalShares + VIRTUAL_SHARES); - } - - /** - * @notice Returns the total QRL controlled by the protocol - * @dev This is the sum of all staked QRL plus rewards minus slashing - * @return Total pooled QRL - */ - function totalPooledQRL() external view returns (uint256) { - return _totalPooledQRL; - } - - /** - * @notice Returns the current exchange rate (QRL per share, scaled by 1e18) - * @dev Useful for UI display and calculations. Uses virtual offsets for consistency. - * @return Exchange rate (1e18 = 1:1) - */ - function getExchangeRate() external view returns (uint256) { - // Use virtual offsets for consistency with share conversion functions - return ((_totalPooledQRL + VIRTUAL_ASSETS) * 1e18) / (_totalShares + VIRTUAL_SHARES); - } - - // ============================================================= - // DEPOSIT POOL FUNCTIONS - // ============================================================= - - /** - * @notice Mint new shares to a recipient - * @dev Only callable by DepositPool when user deposits QRL - * @param to Recipient of the new shares - * @param qrlAmount Amount of QRL being deposited - * @return shares Number of shares minted - */ - function mintShares(address to, uint256 qrlAmount) external onlyDepositPool whenNotPaused returns (uint256 shares) { - if (to == address(0)) revert ZeroAddress(); - if (qrlAmount == 0) revert ZeroAmount(); - - shares = getSharesByPooledQRL(qrlAmount); - if (shares == 0) revert ZeroAmount(); - - _totalShares += shares; - _shares[to] += shares; - - // Apply the minimum stake lock to fresh deposits. The owner (operator) - // is exempt so bridge capital can enter and exit without the maturity - // wait. Remaining immature shares fold into the new bucket and the - // maturity block resets; matured shares are unaffected. - if (minStakeBlocks > 0 && to != owner) { - _immatureShares[to] = immatureSharesOf(to) + shares; - _matureAtBlock[to] = block.number + minStakeBlocks; - } - - // Note: totalPooledQRL is updated separately via updateTotalPooledQRL - // This allows DepositPool to batch updates - - emit SharesMinted(to, shares, qrlAmount); - emit Transfer(address(0), to, shares); - - return shares; - } - - /** - * @notice Burn shares from an account - * @dev Only callable by DepositPool when user withdraws QRL - * @param from Account to burn shares from - * @param sharesAmount Number of shares to burn - * @return qrlAmount Amount of QRL the burned shares were worth - */ - function burnShares(address from, uint256 sharesAmount) - external - onlyDepositPool - whenNotPaused - returns (uint256 qrlAmount) - { - if (from == address(0)) revert ZeroAddress(); - if (sharesAmount == 0) revert ZeroAmount(); - if (_shares[from] < sharesAmount) revert InsufficientBalance(); - - qrlAmount = getPooledQRLByShares(sharesAmount); - - _shares[from] -= sharesAmount; - _totalShares -= sharesAmount; - - // Note: totalPooledQRL is updated separately via updateTotalPooledQRL - - emit SharesBurned(from, sharesAmount, qrlAmount); - emit Transfer(from, address(0), sharesAmount); - - return qrlAmount; - } - - /** - * @notice Update the total pooled QRL - * @dev Called by DepositPool after syncing rewards/slashing - * This changes the exchange rate (affects getQRLValue, not balanceOf) - * @param newTotalPooledQRL The new total pooled QRL amount - */ - function updateTotalPooledQRL(uint256 newTotalPooledQRL) external onlyDepositPool { - uint256 previousAmount = _totalPooledQRL; - _totalPooledQRL = newTotalPooledQRL; - emit TotalPooledQRLUpdated(previousAmount, newTotalPooledQRL); - } - - // ============================================================= - // SHARE LOCKING FUNCTIONS - // ============================================================= - - /** - * @notice Lock shares for a pending withdrawal - * @dev Only callable by DepositPool. Locked shares cannot be transferred. - * @param account The account whose shares to lock - * @param sharesAmount Number of shares to lock - */ - function lockShares(address account, uint256 sharesAmount) external onlyDepositPool { - _lockedShares[account] += sharesAmount; - } - - /** - * @notice Unlock shares after withdrawal claim or cancellation - * @dev Only callable by DepositPool - * @param account The account whose shares to unlock - * @param sharesAmount Number of shares to unlock - */ - function unlockShares(address account, uint256 sharesAmount) external onlyDepositPool { - _lockedShares[account] -= sharesAmount; - } - - /** - * @notice Returns the locked shares for an account - * @param account The address to query - * @return The number of locked shares - */ - function lockedSharesOf(address account) external view returns (uint256) { - return _lockedShares[account]; - } - - // ============================================================= - // STAKE MATURITY FUNCTIONS - // ============================================================= - - /** - * @notice Returns the not-yet-matured shares for an account - * @dev Immature shares cannot be transferred or queued for withdrawal. - * Matures lazily: returns 0 once the maturity block has passed. - * @param account The address to query - * @return The number of immature shares - */ - function immatureSharesOf(address account) public view returns (uint256) { - if (block.number >= _matureAtBlock[account]) { - return 0; - } - return _immatureShares[account]; - } - - /** - * @notice Returns the block at which an account's immature bucket matures - * @dev 0 if the account never deposited. May be in the past (bucket matured). - * @param account The address to query - * @return The maturity block number - */ - function matureAtBlockOf(address account) external view returns (uint256) { - return _matureAtBlock[account]; - } - - /** - * @notice Set the minimum stake lock applied to fresh deposits - * @dev Capped at MAX_MIN_STAKE_BLOCKS. 0 disables the lock entirely. - * Only affects future deposits; existing buckets keep their maturity. - * @param _minStakeBlocks The new lock duration in blocks - */ - function setMinStakeBlocks(uint256 _minStakeBlocks) external onlyOwner { - if (_minStakeBlocks > MAX_MIN_STAKE_BLOCKS) revert MinStakeBlocksTooHigh(); - emit MinStakeBlocksUpdated(minStakeBlocks, _minStakeBlocks); - minStakeBlocks = _minStakeBlocks; - } - - // ============================================================= - // INTERNAL FUNCTIONS - // ============================================================= - - /** - * @dev Internal transfer logic - amount is in shares - */ - function _transfer(address from, address to, uint256 amount) internal { - if (from == address(0)) revert ZeroAddress(); - if (to == address(0)) revert ZeroAddress(); - if (amount == 0) revert ZeroAmount(); - if (_shares[from] < amount) revert InsufficientBalance(); - if (_shares[from] - _lockedShares[from] < amount) revert InsufficientUnlockedShares(); - // After sending, what remains unlocked must still cover the immature - // bucket: immature shares are non-transferable. This closes the lock - // bypass of moving fresh shares to an address with no lock state. - if (_shares[from] - _lockedShares[from] - amount < immatureSharesOf(from)) { - revert InsufficientMaturedShares(); - } - - _shares[from] -= amount; - _shares[to] += amount; - - emit Transfer(from, to, amount); - } - - /** - * @dev Internal approve logic - amount is in shares - */ - function _approve(address _owner, address spender, uint256 amount) internal { - if (_owner == address(0)) revert ZeroAddress(); - if (spender == address(0)) revert ZeroAddress(); - - _allowances[_owner][spender] = amount; - emit Approval(_owner, spender, amount); - } - - // ============================================================= - // ADMIN FUNCTIONS - // ============================================================= - - /** - * @notice Set the DepositPool contract address - * @dev Can only be called once by owner - * @param _depositPool The DepositPool contract address - */ - function setDepositPool(address _depositPool) external onlyOwner { - if (_depositPool == address(0)) revert ZeroAddress(); - if (depositPool != address(0)) revert DepositPoolAlreadySet(); - - emit DepositPoolSet(depositPool, _depositPool); - depositPool = _depositPool; - } - - /** - * @notice Pause the contract - * @dev Blocks transfers, minting, and burning - */ - function pause() external onlyOwner { - paused = true; - emit Paused(msg.sender); - } - - /** - * @notice Unpause the contract - */ - function unpause() external onlyOwner { - paused = false; - emit Unpaused(msg.sender); - } - - /** - * @notice Transfer ownership - * @param newOwner The new owner address - */ - function transferOwnership(address newOwner) external onlyOwner { - if (newOwner == address(0)) revert ZeroAddress(); - emit OwnershipTransferred(owner, newOwner); - owner = newOwner; - } - - /** - * @notice Renounce ownership (irreversible) - * @dev Use after DepositPool is set and system is stable - */ - function renounceOwnership() external onlyOwner { - emit OwnershipTransferred(owner, address(0)); - owner = address(0); - } -} diff --git a/contracts/solidity/DepositPool-v2.sol b/contracts/solidity/DepositPool-v2.sol deleted file mode 100644 index 17b1c3f..0000000 --- a/contracts/solidity/DepositPool-v2.sol +++ /dev/null @@ -1,996 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.24; - -/** - * @title DepositPool v2 - User Entry Point for QuantaPool - * @author QuantaPool - * @notice Accepts QRL deposits, manages withdrawals, and syncs validator rewards - * - * @dev Key responsibilities: - * 1. Accept user deposits → mint stQRL shares - * 2. Queue and process withdrawals → burn shares, return QRL - * 3. Trustless reward sync → detect balance changes from validators - * 4. Fund validators → send QRL to beacon deposit contract - * - * Reward Sync (Oracle-Free): - * Validator rewards arrive via EIP-4895 as balance increases WITHOUT - * triggering contract code. This contract periodically checks its balance - * and updates stQRL's totalPooledQRL accordingly. - * - * syncRewards() can be called by anyone while all principal is on-contract. - * While stakedQRL is nonzero, only the owner may sequence reward sync and - * validator-exit settlement. The contract compares its accounted assets to - * the observed balance plus tracked off-contract principal. - * - * Balance Accounting: - * contractBalance + stakedQRL = totalPooledQRL - * - * - totalPooledQRL: All QRL under pool management (buffered + staked + rewards) - * This is what stQRL token tracks. Includes buffered deposits waiting - * to fund validators, principal staked off-contract, plus any rewards - * that arrive via EIP-4895. - * - stakedQRL: principal forwarded to the beacon deposit contract by the - * real fundValidator() path. It lives off-contract but is still pooled, - * so _syncRewards() adds it back when reconciling the balance. - * - withdrawalReserve: Liquid QRL earmarked for pending withdrawals. It - * remains pooled until the matching shares are burned at claim time. - * - * For MVP (testnet), fundValidatorMVP keeps QRL in this contract and stakedQRL - * stays zero. For production, fundValidator sends QRL to the beacon deposit - * contract (incrementing stakedQRL); it returns when validators exit, at which - * point the owner calls recordValidatorExit() to settle the accounting. - */ - -interface IstQRL { - function mintShares(address to, uint256 qrlAmount) external returns (uint256); - function burnShares(address from, uint256 sharesAmount) external returns (uint256); - function updateTotalPooledQRL(uint256 newAmount) external; - function lockShares(address account, uint256 sharesAmount) external; - function unlockShares(address account, uint256 sharesAmount) external; - function totalPooledQRL() external view returns (uint256); - function totalShares() external view returns (uint256); - function sharesOf(address account) external view returns (uint256); - function lockedSharesOf(address account) external view returns (uint256); - function immatureSharesOf(address account) external view returns (uint256); - function getSharesByPooledQRL(uint256 qrlAmount) external view returns (uint256); - function getPooledQRLByShares(uint256 sharesAmount) external view returns (uint256); -} - -/// @notice QRL beacon chain deposit contract interface -interface IDepositContract { - function deposit( - bytes calldata pubkey, - bytes calldata withdrawal_credentials, - bytes calldata signature, - bytes32 deposit_data_root - ) external payable; -} - -contract DepositPoolV2 { - // ============================================================= - // CONSTANTS - // ============================================================= - - /// @notice Minimum stake for a QRL validator (MaxEffectiveBalance from QRL beacon config) - uint256 public constant VALIDATOR_STAKE = 40_000 ether; - - /// @notice QRL beacon chain deposit contract - address public constant DEPOSIT_CONTRACT = 0x4242424242424242424242424242424242424242; - - /// @notice Dilithium pubkey length (bytes) - uint256 private constant PUBKEY_LENGTH = 2592; - - /// @notice ML-DSA-87 signature length (bytes). qrysm enforces exactly - /// this value in crypto/ml_dsa_87/ml_dsa_87t/signature.go — any other - /// length will be rejected by the beacon deposit contract. - uint256 private constant SIGNATURE_LENGTH = 4627; - - /// @notice Withdrawal credentials length - uint256 private constant CREDENTIALS_LENGTH = 32; - - /// @notice Minimum blocks to wait before claiming withdrawal - uint256 public constant WITHDRAWAL_DELAY = 128; // ~2 hours on QRL - - /// @notice Absolute minimum for minDepositFloor (dust prevention, ~1e15 wei) - uint256 public constant ABSOLUTE_MIN_DEPOSIT = 0.001 ether; - - // ============================================================= - // STORAGE - // ============================================================= - - /// @notice stQRL token contract - IstQRL public stQRL; - - /// @notice Contract owner - address public owner; - - /// @notice QRL buffered for next validator (not yet staked) - uint256 public bufferedQRL; - - /// @notice Number of active validators - uint256 public validatorCount; - - /// @notice Minimum deposit amount - uint256 public minDeposit; - - /// @notice Adjustable floor for minDeposit (owner can lower after deployment) - uint256 public minDepositFloor = 100 ether; - - /// @notice Paused state - bool public paused; - - /// @notice Reentrancy guard - uint256 private _locked; - - // ============================================================= - // WITHDRAWAL STORAGE - // ============================================================= - - /// @notice Withdrawal request data - struct WithdrawalRequest { - uint256 shares; // Shares to burn - uint256 qrlAmount; // Request-time estimate, replaced by actual payout on claim - uint256 requestBlock; // Block when requested - bool claimed; // Whether claimed - } - - /// @notice Withdrawal requests by user (supports multiple requests via array) - mapping(address => WithdrawalRequest[]) public withdrawalRequests; - - /// @notice Next withdrawal request ID to process for each user - mapping(address => uint256) public nextWithdrawalIndex; - - /// @notice Total shares locked in withdrawal queue - uint256 public totalWithdrawalShares; - - /// @notice QRL reserved for pending withdrawals - uint256 public withdrawalReserve; - - /// @notice Portion of withdrawalReserve that was moved out of bufferedQRL - /// @dev Tracks exactly how much buffer may be restored when an earmark is - /// released. The remainder came from unbuffered liquid assets. - uint256 public bufferedQRLInReserve; - - // ============================================================= - // SYNC STORAGE - // ============================================================= - - /// @notice Last block when rewards were synced - uint256 public lastSyncBlock; - - /// @notice Total rewards received (cumulative, for stats) - uint256 public totalRewardsReceived; - - /// @notice Total slashing losses (cumulative, for stats) - uint256 public totalSlashingLosses; - - /// @notice QRL principal forwarded to the beacon deposit contract that is - /// staked off-contract (not in address(this).balance) but still under - /// protocol management. - /// @dev Only the real beacon path (fundValidator) moves QRL off-contract, - /// so only it increments this. fundValidatorMVP keeps QRL in the - /// contract and leaves stakedQRL untouched. _syncRewards() adds this - /// back when reconciling balance against totalPooledQRL, otherwise - /// funding a validator would look like a slashing event. The owner - /// decrements it via recordValidatorExit() when exit proceeds return. - uint256 public stakedQRL; - - // ============================================================= - // EVENTS - // ============================================================= - - event Deposited(address indexed user, uint256 qrlAmount, uint256 sharesReceived); - - event WithdrawalRequested(address indexed user, uint256 shares, uint256 qrlAmount, uint256 requestBlock); - - event WithdrawalClaimed(address indexed user, uint256 shares, uint256 qrlAmount); - - event RewardsSynced(uint256 rewardsAmount, uint256 newTotalPooled, uint256 blockNumber); - - event SlashingDetected(uint256 lossAmount, uint256 newTotalPooled, uint256 blockNumber); - - event ValidatorFunded(uint256 indexed validatorId, bytes pubkey, uint256 amount); - - event ValidatorExitRecorded(uint256 amount, uint256 remainingStaked); - - event WithdrawalReserveFunded(uint256 amount); - event WithdrawalReserveReleased(uint256 amount, uint256 bufferRestored); - event WithdrawalCancelled(address indexed user, uint256 indexed requestId, uint256 shares); - event MinDepositUpdated(uint256 newMinDeposit); - event MinDepositFloorUpdated(uint256 newFloor); - event Paused(address account); - event Unpaused(address account); - event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); - event StQRLSet(address indexed stQRL); - event EmergencyWithdrawal(address indexed to, uint256 amount); - - // ============================================================= - // ERRORS - // ============================================================= - - error NotOwner(); - error ContractPaused(); - error ReentrancyGuard(); - error ZeroAddress(); - error ZeroAmount(); - error BelowMinDeposit(); - error BelowMinDepositFloor(); - error BelowAbsoluteMin(); - error InsufficientShares(); - error NoWithdrawalPending(); - error WithdrawalNotReady(); - error InsufficientReserve(); - error InsufficientBuffer(); - error InvalidPubkeyLength(); - error InvalidSignatureLength(); - error InvalidCredentialsLength(); - error InvalidWithdrawalCredentials(); - error TransferFailed(); - error StQRLNotSet(); - error StQRLAlreadySet(); - error InvalidWithdrawalIndex(); - error ExceedsRecoverableAmount(); - error ExceedsStakedAmount(); - error AccountingNotSettled(); - - // ============================================================= - // MODIFIERS - // ============================================================= - - modifier onlyOwner() { - if (msg.sender != owner) revert NotOwner(); - _; - } - - modifier whenNotPaused() { - if (paused) revert ContractPaused(); - _; - } - - modifier nonReentrant() { - if (_locked == 1) revert ReentrancyGuard(); - _locked = 1; - _; - _locked = 0; - } - - // ============================================================= - // CONSTRUCTOR - // ============================================================= - - constructor() { - owner = msg.sender; - minDeposit = 100 ether; // 100 QRL minimum - lastSyncBlock = block.number; - } - - // ============================================================= - // DEPOSIT FUNCTIONS - // ============================================================= - - /** - * @notice Deposit QRL and receive stQRL - * @dev Reconciles pre-existing balance changes, mints shares at the current - * exchange rate, and adds the deposit to the validator buffer. - * - * @return shares Amount of stQRL shares minted - */ - function deposit() external payable nonReentrant whenNotPaused returns (uint256 shares) { - if (address(stQRL) == address(0)) revert StQRLNotSet(); - if (msg.value < minDeposit) revert BelowMinDeposit(); - - // msg.value is already in this contract's balance. Reconcile against - // the balance immediately before this deposit so existing rewards or - // losses cannot be captured by the new depositor. - _syncBeforeDeposit(msg.value); - - // Mint shares FIRST - this calculates shares at current rate - // mintShares internally calls getSharesByPooledQRL(qrlAmount) - // This must happen BEFORE updating totalPooledQRL to ensure fair pricing - shares = stQRL.mintShares(msg.sender, msg.value); - - // Add to buffer (deposited QRL waiting to fund validators) - bufferedQRL += msg.value; - - // Update total pooled QRL (deposit is now under protocol management) - // This must happen AFTER minting to not affect the share calculation - uint256 newTotalPooled = stQRL.totalPooledQRL() + msg.value; - stQRL.updateTotalPooledQRL(newTotalPooled); - - emit Deposited(msg.sender, msg.value, shares); - return shares; - } - - /** - * @notice Preview deposit - get expected shares for QRL amount - * @param qrlAmount Amount of QRL to deposit - * @return shares Expected shares to receive - */ - function previewDeposit(uint256 qrlAmount) external view returns (uint256 shares) { - if (address(stQRL) == address(0)) return qrlAmount; - return stQRL.getSharesByPooledQRL(qrlAmount); - } - - // ============================================================= - // WITHDRAWAL FUNCTIONS - // ============================================================= - - /** - * @notice Request withdrawal of stQRL - * @dev Users can have multiple pending withdrawal requests - * @param shares Amount of shares to withdraw - * @return requestId The ID of this withdrawal request - * @return qrlAmount Current QRL estimate. The actual claim amount is priced - * after accounting is synchronized at claim time. - */ - function requestWithdrawal(uint256 shares) - external - nonReentrant - whenNotPaused - returns (uint256 requestId, uint256 qrlAmount) - { - if (shares == 0) revert ZeroAmount(); - // Spendable shares exclude pending-withdrawal locks and the minimum - // stake lock on fresh deposits (anti-griefing, see stQRLv2). - uint256 unlockedShares = - stQRL.sharesOf(msg.sender) - stQRL.lockedSharesOf(msg.sender) - stQRL.immatureSharesOf(msg.sender); - if (unlockedShares < shares) revert InsufficientShares(); - - // Refresh the request-time estimate when permissionless sync is safe. - // With principal staked off-contract, the rate is owner-controlled and - // the estimate uses the last settled rate. It creates no payout right. - if (_permissionlessSyncAllowed()) { - _syncRewards(); - } - - // Store an informational estimate for history and UI compatibility. - // Claims deliberately reprice these shares after settlement. - uint256 currentPooled = stQRL.totalPooledQRL(); - uint256 convertedAmount = stQRL.getPooledQRLByShares(shares); - qrlAmount = convertedAmount < currentPooled ? convertedAmount : currentPooled; - - // Lock shares so they cannot be transferred - stQRL.lockShares(msg.sender, shares); - - // Create withdrawal request (push to array for multiple requests support) - requestId = withdrawalRequests[msg.sender].length; - withdrawalRequests[msg.sender].push( - WithdrawalRequest({shares: shares, qrlAmount: qrlAmount, requestBlock: block.number, claimed: false}) - ); - - totalWithdrawalShares += shares; - - emit WithdrawalRequested(msg.sender, shares, qrlAmount, block.number); - return (requestId, qrlAmount); - } - - /** - * @notice Claim the next pending withdrawal (FIFO order) - * @dev Reprices and burns shares, then transfers QRL to the user. - * Skips cancelled requests (claimed=true, shares=0) automatically. - * The request-time estimate is not an entitlement. This makes queued - * holders participate in rewards and slashing until their shares burn. - * @return qrlAmount Amount of QRL received - */ - function claimWithdrawal() external nonReentrant returns (uint256 qrlAmount) { - uint256 requestIndex = nextWithdrawalIndex[msg.sender]; - uint256 totalRequests = withdrawalRequests[msg.sender].length; - - // Skip cancelled requests (shares=0 && claimed=true) - // Bounded: user can only create cancellations via their own txs, - // so the practical depth is small. Tested up to 500 in suite. - while (requestIndex < totalRequests && withdrawalRequests[msg.sender][requestIndex].shares == 0) { - requestIndex++; - } - if (requestIndex >= totalRequests) revert NoWithdrawalPending(); - - // Update index to account for skipped cancelled requests - nextWithdrawalIndex[msg.sender] = requestIndex; - - WithdrawalRequest storage request = withdrawalRequests[msg.sender][requestIndex]; - - // === CHECKS === - if (request.claimed) revert NoWithdrawalPending(); - if (block.number < request.requestBlock + WITHDRAWAL_DELAY) revert WithdrawalNotReady(); - - // Refresh pooled accounting when permissionless sync is safe. While - // principal is off-contract, reject claims during any unsettled balance - // delta. The owner must sequence exit settlement and reward sync first. - if (_permissionlessSyncAllowed()) { - _syncRewards(); - } else { - _requireAccountingSettled(); - } - - // Cache shares before state changes - uint256 sharesToBurn = request.shares; - - // Price at settlement so queued holders receive rewards and bear losses - // until their shares are actually removed from circulation. Cap at real - // pooled assets because the token's virtual offset is not spendable QRL. - uint256 currentPooled = stQRL.totalPooledQRL(); - uint256 convertedAmount = stQRL.getPooledQRLByShares(sharesToBurn); - qrlAmount = convertedAmount < currentPooled ? convertedAmount : currentPooled; - - if (withdrawalReserve < qrlAmount || address(this).balance < qrlAmount) { - revert InsufficientReserve(); - } - - // Unlock shares before burning - stQRL.unlockShares(msg.sender, sharesToBurn); - - // Burn shares at the same synchronized exchange rate used above. - stQRL.burnShares(msg.sender, sharesToBurn); - - // === EFFECTS === - request.qrlAmount = qrlAmount; - request.claimed = true; - nextWithdrawalIndex[msg.sender] = requestIndex + 1; - totalWithdrawalShares -= sharesToBurn; - withdrawalReserve -= qrlAmount; - uint256 bufferedReserveConsumed = qrlAmount < bufferedQRLInReserve ? qrlAmount : bufferedQRLInReserve; - bufferedQRLInReserve -= bufferedReserveConsumed; - - // Reserved QRL stays pooled until claim. Remove the claimed assets in - // the same transaction that removes their shares, preserving the rate - // for every remaining and newly minted share. - stQRL.updateTotalPooledQRL(currentPooled - qrlAmount); - - // === INTERACTION (ETH transfer last) === - (bool success,) = msg.sender.call{value: qrlAmount}(""); - if (!success) revert TransferFailed(); - - emit WithdrawalClaimed(msg.sender, sharesToBurn, qrlAmount); - return qrlAmount; - } - - /** - * @notice Cancel a specific pending withdrawal request - * @dev Returns shares to normal circulating state. Only unclaimed requests can be cancelled. - * @param requestId The index of the withdrawal request to cancel - */ - function cancelWithdrawal(uint256 requestId) external nonReentrant { - if (requestId >= withdrawalRequests[msg.sender].length) revert InvalidWithdrawalIndex(); - if (requestId < nextWithdrawalIndex[msg.sender]) revert InvalidWithdrawalIndex(); // Already processed - - WithdrawalRequest storage request = withdrawalRequests[msg.sender][requestId]; - - if (request.shares == 0) revert NoWithdrawalPending(); - if (request.claimed) revert NoWithdrawalPending(); - - uint256 shares = request.shares; - totalWithdrawalShares -= shares; - request.shares = 0; - request.claimed = true; // Mark as processed - - // Unlock shares so they can be transferred again - stQRL.unlockShares(msg.sender, shares); - - emit WithdrawalCancelled(msg.sender, requestId, shares); - } - - /** - * @notice Get withdrawal request details by index - * @param user Address to query - * @param requestId Index of the withdrawal request - */ - function getWithdrawalRequest(address user, uint256 requestId) - external - view - returns ( - uint256 shares, - uint256 currentQRLValue, - uint256 requestBlock, - bool canClaim, - uint256 blocksRemaining, - bool claimed - ) - { - if (requestId >= withdrawalRequests[user].length) { - return (0, 0, 0, false, 0, false); - } - - WithdrawalRequest storage request = withdrawalRequests[user][requestId]; - shares = request.shares; - uint256 currentPooled = stQRL.totalPooledQRL(); - uint256 convertedAmount = stQRL.getPooledQRLByShares(shares); - currentQRLValue = convertedAmount < currentPooled ? convertedAmount : currentPooled; - requestBlock = request.requestBlock; - claimed = request.claimed; - - uint256 unlockBlock = request.requestBlock + WITHDRAWAL_DELAY; - bool accountingSettled = _actualTotalPooled() == stQRL.totalPooledQRL(); - canClaim = !request.claimed && request.shares > 0 && block.number >= unlockBlock - && withdrawalReserve >= currentQRLValue && address(this).balance >= currentQRLValue && accountingSettled; - - blocksRemaining = block.number >= unlockBlock ? 0 : unlockBlock - block.number; - } - - /** - * @notice Get the number of withdrawal requests for a user - * @param user Address to query - * @return total Total number of requests - * @return pending Number of pending (unprocessed) requests - */ - function getWithdrawalRequestCount(address user) external view returns (uint256 total, uint256 pending) { - total = withdrawalRequests[user].length; - uint256 nextIndex = nextWithdrawalIndex[user]; - pending = total > nextIndex ? total - nextIndex : 0; - } - - // ============================================================= - // REWARD SYNC FUNCTIONS - // ============================================================= - - /** - * @notice Sync rewards from validator balance changes - * @dev Trustless and permissionless while all principal is on-contract - * (stakedQRL == 0). While validator principal is staked off-contract - * (stakedQRL > 0) this is restricted to the owner — see - * {_permissionlessSyncAllowed} for why. - */ - function syncRewards() external nonReentrant { - if (!_permissionlessSyncAllowed() && msg.sender != owner) revert NotOwner(); - _syncRewards(); - } - - /** - * @notice Whether reward sync may be triggered permissionlessly right now. - * @dev Sync infers rewards/slashing from balance deltas. That inference is - * only unambiguous while every QRL of principal sits in this contract - * (stakedQRL == 0): then `balance` is exactly the pooled total. - * - * Once a real fundValidator() forwards principal to the beacon deposit - * contract (stakedQRL > 0), an exit sweep returning that principal via - * EIP-4895 lands in `address(this).balance` automatically, a block - * before the owner can call recordValidatorExit() to settle it. In - * that window `balance + stakedQRL` double-counts the principal, so a - * sync would book it as a large phantom reward and spike the exchange - * rate. A permissionless caller could front-run the settlement and - * claim a matured queued withdrawal at that inflated rate. - * - * So while principal is off-contract the exchange rate only moves - * under owner control: the operator sequences recordValidatorExit() - * and syncRewards() so settlement and reward recognition cannot be - * front-run. The MVP path (fundValidatorMVP keeps QRL in-contract, - * stakedQRL == 0) is unaffected and stays fully permissionless. - */ - function _permissionlessSyncAllowed() internal view returns (bool) { - return stakedQRL == 0; - } - - /** - * @dev Internal reward sync logic - * - * Balance accounting: - * On-contract QRL = bufferedQRL + rewards + withdrawalReserve - * Off-contract QRL = stakedQRL (principal sent to the beacon deposit - * contract via fundValidator; still under protocol management) - * withdrawalReserve remains pooled until claim burns the matching shares - * actualTotalPooled = balance + stakedQRL - * - * If actualTotalPooled > previousPooled → rewards arrived - * If actualTotalPooled < previousPooled → slashing occurred - * - * Note: For MVP (fundValidatorMVP), staked QRL stays in the contract and - * stakedQRL is zero, so this reduces to balance. - * For production (fundValidator), staked QRL leaves for the beacon deposit - * contract; adding stakedQRL back keeps the funding from registering as a - * slashing event. When exit proceeds return via EIP-4895, the owner calls - * recordValidatorExit() to move that principal back from stakedQRL into the - * on-contract balance so it is not double-counted as rewards. - */ - function _syncRewards() internal { - if (address(stQRL) == address(0)) return; - - _syncToActualTotal(_actualTotalPooled()); - } - - /** - * @dev Reconcile the balance that existed immediately before a deposit. - * With no off-contract stake this is permissionless and unambiguous. - * With stake off-contract, any delta must be settled by the owner first - * because an exit return can be indistinguishable from a reward. - */ - function _syncBeforeDeposit(uint256 depositAmount) internal { - uint256 actualBeforeDeposit = address(this).balance - depositAmount + stakedQRL; - - if (_permissionlessSyncAllowed()) { - _syncToActualTotal(actualBeforeDeposit); - } else if (actualBeforeDeposit != stQRL.totalPooledQRL()) { - revert AccountingNotSettled(); - } - } - - /** - * @dev Revert while an on-chain balance delta is waiting for owner settlement. - */ - function _requireAccountingSettled() internal view { - if (_actualTotalPooled() != stQRL.totalPooledQRL()) revert AccountingNotSettled(); - } - - /** - * @dev Assets backing all outstanding shares, including liquid reserves. - */ - function _actualTotalPooled() internal view returns (uint256) { - return address(this).balance + stakedQRL; - } - - /** - * @dev Attribute a previously computed actual asset total as reward or loss. - */ - function _syncToActualTotal(uint256 actualTotalPooled) internal { - // What we previously tracked as pooled - uint256 previousPooled = stQRL.totalPooledQRL(); - - // Compare and attribute difference - if (actualTotalPooled > previousPooled) { - // Rewards arrived (via EIP-4895 or direct transfer) - uint256 rewards = actualTotalPooled - previousPooled; - totalRewardsReceived += rewards; - stQRL.updateTotalPooledQRL(actualTotalPooled); - lastSyncBlock = block.number; - - emit RewardsSynced(rewards, actualTotalPooled, block.number); - } else if (actualTotalPooled < previousPooled) { - // Slashing detected (or funds removed somehow) - uint256 loss = previousPooled - actualTotalPooled; - totalSlashingLosses += loss; - stQRL.updateTotalPooledQRL(actualTotalPooled); - lastSyncBlock = block.number; - - emit SlashingDetected(loss, actualTotalPooled, block.number); - } - // If equal, no change needed - } - - // ============================================================= - // VALIDATOR FUNCTIONS - // ============================================================= - - /** - * @notice Fund a validator with beacon chain deposit - * @dev Only owner can call. Sends VALIDATOR_STAKE to beacon deposit contract. - * @param pubkey Dilithium public key (2592 bytes) - * @param withdrawal_credentials Must point to this contract (0x00 + 11 zero bytes + address) - * @param signature ML-DSA-87 signature (4627 bytes) - * @param deposit_data_root SSZ hash of deposit data - * @return validatorId The new validator's ID - */ - function fundValidator( - bytes calldata pubkey, - bytes calldata withdrawal_credentials, - bytes calldata signature, - bytes32 deposit_data_root - ) external onlyOwner nonReentrant returns (uint256 validatorId) { - if (bufferedQRL < VALIDATOR_STAKE || _availableValidatorLiquidity() < VALIDATOR_STAKE) { - revert InsufficientBuffer(); - } - if (pubkey.length != PUBKEY_LENGTH) revert InvalidPubkeyLength(); - if (signature.length != SIGNATURE_LENGTH) revert InvalidSignatureLength(); - if (withdrawal_credentials.length != CREDENTIALS_LENGTH) revert InvalidCredentialsLength(); - - // Verify withdrawal credentials point to this contract. - // Format: 0x00 (1 byte) + 11 zero bytes + contract address (20 bytes) = 32 bytes. - // The prefix byte 0x00 is `ExecutionAddressWithdrawalPrefixByte` in qrysm's - // beacon config (config/params/mainnet_config.go:74), i.e. "withdrawals - // are executed to the 20-byte address in bytes[12:32]". Ethereum's spec - // uses 0x01 for the same meaning; QRL v2 re-uses 0x00 because there is - // no BLS-key-withdrawal path to distinguish from. This prefix is what - // staking-deposit-cli emits for --execution-address deposits; mismatch - // here would make validators non-withdrawable (stuck stake). - bytes32 expectedCredentials = bytes32(abi.encodePacked(bytes1(0x00), bytes11(0), address(this))); - bytes32 actualCredentials; - assembly { - actualCredentials := calldataload(withdrawal_credentials.offset) - } - if (actualCredentials != expectedCredentials) revert InvalidWithdrawalCredentials(); - - bufferedQRL -= VALIDATOR_STAKE; - // Principal leaves the contract for the beacon deposit contract but - // stays under protocol management. Track it so _syncRewards() does not - // read the outgoing transfer as a slashing loss. - stakedQRL += VALIDATOR_STAKE; - validatorId = validatorCount++; - - // Call beacon deposit contract - IDepositContract(DEPOSIT_CONTRACT).deposit{value: VALIDATOR_STAKE}( - pubkey, withdrawal_credentials, signature, deposit_data_root - ); - - emit ValidatorFunded(validatorId, pubkey, VALIDATOR_STAKE); - return validatorId; - } - - /** - * @notice Fund a validator (MVP testing - no actual beacon deposit) - * @dev Moves QRL from buffer to simulated stake. For testnet only. - * @return validatorId The new validator's ID - */ - function fundValidatorMVP() external onlyOwner nonReentrant returns (uint256 validatorId) { - if (bufferedQRL < VALIDATOR_STAKE || _availableValidatorLiquidity() < VALIDATOR_STAKE) { - revert InsufficientBuffer(); - } - - bufferedQRL -= VALIDATOR_STAKE; - validatorId = validatorCount++; - - // QRL stays in contract, simulating staked funds - emit ValidatorFunded(validatorId, "", VALIDATOR_STAKE); - return validatorId; - } - - /** - * @notice Record that staked principal has returned from the beacon chain - * @dev Validator exit proceeds (principal) arrive via EIP-4895 and land in - * address(this).balance. Without this call, the next _syncRewards() - * would see both the higher balance AND the still-elevated stakedQRL, - * double-counting the principal as fresh rewards. The owner calls this - * to move `amount` from the off-contract stakedQRL accumulator back - * into on-contract accounting, leaving totalPooledQRL unchanged. - * - * The observed return, capped at the retired nominal principal, is - * restored to bufferedQRL. This makes unused exit proceeds eligible - * to fund a later validator. Any surplus remains unbuffered and is - * correctly attributed as rewards by the next _syncRewards(). - * - * MVP note: fundValidatorMVP never increments stakedQRL, so this is - * only relevant once the real beacon path (fundValidator) is in use. - * @param amount Nominal off-contract stake retired (<= stakedQRL) - */ - function recordValidatorExit(uint256 amount) external onlyOwner { - if (amount == 0) revert ZeroAmount(); - if (amount > stakedQRL) revert ExceedsStakedAmount(); - - // At settled accounting, totalPooledQRL - stakedQRL is the expected - // on-contract balance before an exit return. Buffer only liquidity - // observed above that baseline, capped at the nominal stake retired. - // A slashed exit therefore cannot create unsupported buffer credit. - uint256 currentPooled = stQRL.totalPooledQRL(); - uint256 expectedOnContract = currentPooled > stakedQRL ? currentPooled - stakedQRL : 0; - uint256 currentBalance = address(this).balance; - uint256 observedReturn = currentBalance > expectedOnContract ? currentBalance - expectedOnContract : 0; - uint256 returnedPrincipal = observedReturn < amount ? observedReturn : amount; - - stakedQRL -= amount; - bufferedQRL += returnedPrincipal; - - emit ValidatorExitRecorded(amount, stakedQRL); - } - - /** - * @notice Earmark liquid pooled QRL for pending withdrawals - * @dev Reserved QRL remains in totalPooledQRL until claim burns the matching - * shares. Keeping both assets and shares in the conversion totals - * prevents deposits during the queue from receiving inflated shares. - * - * Invariant maintained: address(this).balance + stakedQRL = totalPooledQRL - * - * For MVP: pooled QRL is in the contract, so this earmarks liquidity. - * For production: call this after validator exit proceeds arrive and - * _syncRewards() has already attributed them to totalPooledQRL. - * - * @param amount Amount of liquid pooled QRL to earmark - */ - function fundWithdrawalReserve(uint256 amount) external onlyOwner { - if (amount == 0) revert ZeroAmount(); - - if (_permissionlessSyncAllowed()) { - _syncRewards(); - } else { - _requireAccountingSettled(); - } - - if (amount > _availableValidatorLiquidity()) revert InsufficientBuffer(); - - uint256 availableLiquidity = _availableValidatorLiquidity(); - withdrawalReserve += amount; - - // Consume unbuffered rewards first, then remove any remaining amount - // from deposit-originated validator liquidity. - uint256 unbufferedLiquidity = availableLiquidity > bufferedQRL ? availableLiquidity - bufferedQRL : 0; - if (amount > unbufferedLiquidity) { - uint256 bufferedAmountReserved = amount - unbufferedLiquidity; - bufferedQRL -= bufferedAmountReserved; - bufferedQRLInReserve += bufferedAmountReserved; - } - - emit WithdrawalReserveFunded(amount); - } - - /** - * @notice Release liquid QRL that no longer needs to remain earmarked - * @dev This is used after a queued withdrawal is cancelled or its required - * payout falls. Only the portion originally taken from bufferedQRL is - * restored to the validator buffer. This avoids treating rewards or - * MVP-simulated stake as fresh validator principal. - * @param amount Amount of withdrawal reserve to release - */ - function releaseWithdrawalReserve(uint256 amount) external onlyOwner { - if (amount == 0) revert ZeroAmount(); - if (amount > withdrawalReserve) revert InsufficientReserve(); - - uint256 bufferRestored = amount < bufferedQRLInReserve ? amount : bufferedQRLInReserve; - withdrawalReserve -= amount; - bufferedQRLInReserve -= bufferRestored; - bufferedQRL += bufferRestored; - - emit WithdrawalReserveReleased(amount, bufferRestored); - } - - // ============================================================= - // VIEW FUNCTIONS - // ============================================================= - - /** - * @notice Get pool status - */ - function getPoolStatus() - external - view - returns ( - uint256 totalPooled, - uint256 totalShares, - uint256 buffered, - uint256 validators, - uint256 pendingWithdrawalShares, - uint256 reserveBalance, - uint256 exchangeRate - ) - { - totalPooled = address(stQRL) != address(0) ? stQRL.totalPooledQRL() : 0; - totalShares = address(stQRL) != address(0) ? stQRL.totalShares() : 0; - buffered = bufferedQRL; - validators = validatorCount; - pendingWithdrawalShares = totalWithdrawalShares; - reserveBalance = withdrawalReserve; - exchangeRate = totalShares > 0 ? (totalPooled * 1e18) / totalShares : 1e18; - } - - /** - * @notice Get reward/slashing stats - */ - function getRewardStats() - external - view - returns (uint256 totalRewards, uint256 totalSlashing, uint256 netRewards, uint256 lastSync) - { - totalRewards = totalRewardsReceived; - totalSlashing = totalSlashingLosses; - netRewards = totalRewardsReceived > totalSlashingLosses ? totalRewardsReceived - totalSlashingLosses : 0; - lastSync = lastSyncBlock; - } - - /** - * @notice Check if validator funding is possible - */ - function canFundValidator() external view returns (bool possible, uint256 bufferedAmount) { - possible = bufferedQRL >= VALIDATOR_STAKE && _availableValidatorLiquidity() >= VALIDATOR_STAKE; - bufferedAmount = bufferedQRL; - } - - /** - * @dev On-contract QRL that has not been earmarked for withdrawals. - */ - function _availableValidatorLiquidity() internal view returns (uint256) { - uint256 balance = address(this).balance; - return balance > withdrawalReserve ? balance - withdrawalReserve : 0; - } - - // ============================================================= - // ADMIN FUNCTIONS - // ============================================================= - - /** - * @notice Set the stQRL token contract (one-time only) - * @param _stQRL Address of stQRL contract - */ - function setStQRL(address _stQRL) external onlyOwner { - if (_stQRL == address(0)) revert ZeroAddress(); - if (address(stQRL) != address(0)) revert StQRLAlreadySet(); - stQRL = IstQRL(_stQRL); - emit StQRLSet(_stQRL); - } - - /** - * @notice Set minimum deposit amount - * @param _minDeposit New minimum deposit - */ - function setMinDeposit(uint256 _minDeposit) external onlyOwner { - if (_minDeposit < minDepositFloor) revert BelowMinDepositFloor(); - minDeposit = _minDeposit; - emit MinDepositUpdated(_minDeposit); - } - - /** - * @notice Set the adjustable floor for minDeposit - * @dev Allows owner to lower the floor post-deployment (e.g., if QRL appreciates) - * @param _floor New floor value (must be >= ABSOLUTE_MIN_DEPOSIT) - */ - function setMinDepositFloor(uint256 _floor) external onlyOwner { - if (_floor < ABSOLUTE_MIN_DEPOSIT) revert BelowAbsoluteMin(); - minDepositFloor = _floor; - emit MinDepositFloorUpdated(_floor); - } - - /** - * @notice Pause the contract - */ - function pause() external onlyOwner { - paused = true; - emit Paused(msg.sender); - } - - /** - * @notice Unpause the contract - */ - function unpause() external onlyOwner { - paused = false; - emit Unpaused(msg.sender); - } - - /** - * @notice Transfer ownership - * @param newOwner New owner address - */ - function transferOwnership(address newOwner) external onlyOwner { - if (newOwner == address(0)) revert ZeroAddress(); - emit OwnershipTransferred(owner, newOwner); - owner = newOwner; - } - - /** - * @notice Emergency withdrawal of stuck funds - * @dev Native balance deltas are treated as pooled rewards after stQRL is - * configured because they cannot be distinguished from validator - * rewards. Recovery is therefore limited to demonstrably unaccounted - * funds, primarily before pool initialization. - * @param to Recipient address - * @param amount Amount to withdraw - */ - function emergencyWithdraw(address to, uint256 amount) external onlyOwner nonReentrant { - if (to == address(0)) revert ZeroAddress(); - if (amount == 0) revert ZeroAmount(); - - if (address(stQRL) != address(0)) { - _requireAccountingSettled(); - } - - // Calculate recoverable amount from balance minus on-contract pooled funds. - // totalPooledQRL includes stakedQRL, which lives off-contract at the beacon - // deposit contract, so it must be excluded when comparing against this balance. - uint256 pooled = address(stQRL) != address(0) ? stQRL.totalPooledQRL() : 0; - uint256 onContractPooled = pooled > stakedQRL ? pooled - stakedQRL : 0; - // withdrawalReserve is a subset of pooled assets. Use the larger value - // defensively if accounting is temporarily stale instead of counting it twice. - uint256 totalProtocolFunds = onContractPooled > withdrawalReserve ? onContractPooled : withdrawalReserve; - uint256 currentBalance = address(this).balance; - uint256 recoverableAmount = currentBalance > totalProtocolFunds ? currentBalance - totalProtocolFunds : 0; - - if (amount > recoverableAmount) revert ExceedsRecoverableAmount(); - - (bool success,) = to.call{value: amount}(""); - if (!success) revert TransferFailed(); - - emit EmergencyWithdrawal(to, amount); - } - - // ============================================================= - // RECEIVE FUNCTION - // ============================================================= - - /** - * @notice Receive QRL (from validator exits, rewards, or direct sends) - * @dev Rewards arrive via EIP-4895 WITHOUT triggering this function. - * This is only triggered by explicit transfers (e.g. validator exit - * proceeds via a regular transaction). - * - * Incoming ETH is NOT auto-classified. It increases address(this).balance, - * and the next _syncRewards() call will detect it as a balance increase - * and attribute it to totalPooledQRL. The owner can then call - * fundWithdrawalReserve() to earmark it for pending withdrawals. - */ - receive() external payable { - // No automatic accounting - _syncRewards() will detect the balance change - } -} diff --git a/contracts/solidity/ValidatorManager.sol b/contracts/solidity/ValidatorManager.sol deleted file mode 100644 index 6e78670..0000000 --- a/contracts/solidity/ValidatorManager.sol +++ /dev/null @@ -1,349 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.24; - -/** - * @title ValidatorManager - Simplified Validator Tracking for QuantaPool - * @author QuantaPool - * @notice Tracks validator pubkeys and status for the liquid staking pool - * - * @dev MVP Design: - * - Single trusted operator (owner) - * - No bonds, collateral, or complex economics - * - Simple validator state machine: Pending → Active → Exiting → Exited - * - Future: Permissionless operator registration - * - * This contract is intentionally minimal. Complex operator economics - * can be added in v3 after the core staking mechanism is proven. - */ -contract ValidatorManager { - // ============================================================= - // CONSTANTS - // ============================================================= - - /// @notice QRL validator stake amount (MaxEffectiveBalance from QRL beacon config) - uint256 public constant VALIDATOR_STAKE = 40_000 ether; - - /// @notice Dilithium pubkey length - uint256 private constant PUBKEY_LENGTH = 2592; - - // ============================================================= - // ENUMS - // ============================================================= - - /// @notice Validator lifecycle states - enum ValidatorStatus { - None, // Not registered - Pending, // Registered, awaiting activation - Active, // Currently validating - Exiting, // Exit requested - Exited, // Fully exited, funds returned - Slashed // Slashed (for record keeping) - } - - // ============================================================= - // STRUCTS - // ============================================================= - - /// @notice Validator data - struct Validator { - bytes pubkey; // Dilithium public key (2592 bytes) - ValidatorStatus status; // Current status - uint256 activatedBlock; // Block when activated - uint256 exitedBlock; // Block when exited (0 if not exited) - } - - // ============================================================= - // STORAGE - // ============================================================= - - /// @notice Contract owner (operator for MVP) - address public owner; - - /// @notice DepositPool contract (authorized to register validators) - address public depositPool; - - /// @notice Validator data by index - mapping(uint256 => Validator) public validators; - - /// @notice Pubkey hash to validator index - mapping(bytes32 => uint256) public pubkeyToIndex; - - /// @notice Total validators ever registered - uint256 public totalValidators; - - /// @notice Count of active validators - uint256 public activeValidatorCount; - - /// @notice Count of pending validators - uint256 public pendingValidatorCount; - - // ============================================================= - // EVENTS - // ============================================================= - - event ValidatorRegistered(uint256 indexed validatorId, bytes pubkey, ValidatorStatus status); - - event ValidatorActivated(uint256 indexed validatorId, uint256 activatedBlock); - - event ValidatorExitRequested(uint256 indexed validatorId, uint256 requestBlock); - - event ValidatorExited(uint256 indexed validatorId, uint256 exitedBlock); - - event ValidatorSlashed(uint256 indexed validatorId, uint256 slashedBlock); - - event DepositPoolSet(address indexed depositPool); - event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); - - // ============================================================= - // ERRORS - // ============================================================= - - error NotOwner(); - error NotDepositPool(); - error NotAuthorized(); - error ZeroAddress(); - error InvalidPubkeyLength(); - error ValidatorAlreadyExists(); - error ValidatorNotFound(); - error InvalidStatusTransition(); - - // ============================================================= - // MODIFIERS - // ============================================================= - - modifier onlyOwner() { - if (msg.sender != owner) revert NotOwner(); - _; - } - - modifier onlyDepositPool() { - if (msg.sender != depositPool) revert NotDepositPool(); - _; - } - - modifier onlyAuthorized() { - if (msg.sender != owner && msg.sender != depositPool) revert NotAuthorized(); - _; - } - - // ============================================================= - // CONSTRUCTOR - // ============================================================= - - constructor() { - owner = msg.sender; - } - - // ============================================================= - // VALIDATOR REGISTRATION - // ============================================================= - - /** - * @notice Register a new validator - * @dev Called by DepositPool when funding a validator - * @param pubkey Dilithium public key (2592 bytes) - * @return validatorId The new validator's index - */ - function registerValidator(bytes calldata pubkey) external onlyAuthorized returns (uint256 validatorId) { - if (pubkey.length != PUBKEY_LENGTH) revert InvalidPubkeyLength(); - - bytes32 pubkeyHash = keccak256(pubkey); - if (pubkeyToIndex[pubkeyHash] != 0) revert ValidatorAlreadyExists(); - - // Validator IDs start at 1 (0 means not found) - validatorId = ++totalValidators; - - validators[validatorId] = - Validator({pubkey: pubkey, status: ValidatorStatus.Pending, activatedBlock: 0, exitedBlock: 0}); - - pubkeyToIndex[pubkeyHash] = validatorId; - pendingValidatorCount++; - - emit ValidatorRegistered(validatorId, pubkey, ValidatorStatus.Pending); - return validatorId; - } - - // ============================================================= - // STATUS TRANSITIONS - // ============================================================= - - /** - * @notice Mark validator as active (confirmed on beacon chain) - * @param validatorId The validator to activate - */ - function activateValidator(uint256 validatorId) external onlyOwner { - Validator storage v = validators[validatorId]; - if (v.status != ValidatorStatus.Pending) revert InvalidStatusTransition(); - - v.status = ValidatorStatus.Active; - v.activatedBlock = block.number; - - pendingValidatorCount--; - activeValidatorCount++; - - emit ValidatorActivated(validatorId, block.number); - } - - /** - * @notice Mark validator as exiting - * @param validatorId The validator requesting exit - */ - function requestValidatorExit(uint256 validatorId) external onlyOwner { - Validator storage v = validators[validatorId]; - if (v.status != ValidatorStatus.Active) revert InvalidStatusTransition(); - - v.status = ValidatorStatus.Exiting; - - emit ValidatorExitRequested(validatorId, block.number); - } - - /** - * @notice Mark validator as fully exited - * @param validatorId The validator that has exited - */ - function markValidatorExited(uint256 validatorId) external onlyOwner { - Validator storage v = validators[validatorId]; - if (v.status != ValidatorStatus.Exiting) revert InvalidStatusTransition(); - - v.status = ValidatorStatus.Exited; - v.exitedBlock = block.number; - - activeValidatorCount--; - - emit ValidatorExited(validatorId, block.number); - } - - /** - * @notice Mark validator as slashed - * @param validatorId The slashed validator - */ - function markValidatorSlashed(uint256 validatorId) external onlyOwner { - Validator storage v = validators[validatorId]; - ValidatorStatus previousStatus = v.status; - - if (previousStatus != ValidatorStatus.Active && previousStatus != ValidatorStatus.Exiting) { - revert InvalidStatusTransition(); - } - - v.status = ValidatorStatus.Slashed; - v.exitedBlock = block.number; - - // Decrement counter - both Active and Exiting validators count toward activeValidatorCount - activeValidatorCount--; - - emit ValidatorSlashed(validatorId, block.number); - } - - /** - * @notice Batch activate multiple validators - * @param validatorIds Array of validator IDs to activate - */ - function batchActivateValidators(uint256[] calldata validatorIds) external onlyOwner { - for (uint256 i = 0; i < validatorIds.length; i++) { - Validator storage v = validators[validatorIds[i]]; - if (v.status == ValidatorStatus.Pending) { - v.status = ValidatorStatus.Active; - v.activatedBlock = block.number; - pendingValidatorCount--; - activeValidatorCount++; - emit ValidatorActivated(validatorIds[i], block.number); - } - } - } - - // ============================================================= - // VIEW FUNCTIONS - // ============================================================= - - /** - * @notice Get validator details - * @param validatorId The validator to query - */ - function getValidator(uint256 validatorId) - external - view - returns (bytes memory pubkey, ValidatorStatus status, uint256 activatedBlock, uint256 exitedBlock) - { - Validator storage v = validators[validatorId]; - return (v.pubkey, v.status, v.activatedBlock, v.exitedBlock); - } - - /** - * @notice Get validator ID by pubkey - * @param pubkey The pubkey to look up - * @return validatorId (0 if not found) - */ - function getValidatorIdByPubkey(bytes calldata pubkey) external view returns (uint256) { - return pubkeyToIndex[keccak256(pubkey)]; - } - - /** - * @notice Get validator status by pubkey - * @param pubkey The pubkey to look up - */ - function getValidatorStatus(bytes calldata pubkey) external view returns (ValidatorStatus) { - uint256 validatorId = pubkeyToIndex[keccak256(pubkey)]; - if (validatorId == 0) return ValidatorStatus.None; - return validators[validatorId].status; - } - - /** - * @notice Get summary statistics - */ - function getStats() external view returns (uint256 total, uint256 pending, uint256 active, uint256 totalStaked) { - total = totalValidators; - pending = pendingValidatorCount; - active = activeValidatorCount; - totalStaked = activeValidatorCount * VALIDATOR_STAKE; - } - - /** - * @notice Get all validators in a specific status - * @param status The status to filter by - * @return validatorIds Array of matching validator IDs - */ - function getValidatorsByStatus(ValidatorStatus status) external view returns (uint256[] memory validatorIds) { - // First pass: count matches - uint256 count = 0; - for (uint256 i = 1; i <= totalValidators; i++) { - if (validators[i].status == status) { - count++; - } - } - - // Second pass: collect IDs - validatorIds = new uint256[](count); - uint256 index = 0; - for (uint256 i = 1; i <= totalValidators; i++) { - if (validators[i].status == status) { - validatorIds[index++] = i; - } - } - - return validatorIds; - } - - // ============================================================= - // ADMIN FUNCTIONS - // ============================================================= - - /** - * @notice Set the DepositPool contract - * @param _depositPool Address of DepositPool - */ - function setDepositPool(address _depositPool) external onlyOwner { - if (_depositPool == address(0)) revert ZeroAddress(); - depositPool = _depositPool; - emit DepositPoolSet(_depositPool); - } - - /** - * @notice Transfer ownership - * @param newOwner New owner address - */ - function transferOwnership(address newOwner) external onlyOwner { - if (newOwner == address(0)) revert ZeroAddress(); - emit OwnershipTransferred(owner, newOwner); - owner = newOwner; - } -} diff --git a/contracts/solidity/stQRL-v2.sol b/contracts/solidity/stQRL-v2.sol deleted file mode 100644 index 9882ab6..0000000 --- a/contracts/solidity/stQRL-v2.sol +++ /dev/null @@ -1,576 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.24; - -/** - * @title stQRL v2 - Fixed-Balance Staked QRL Token - * @author QuantaPool - * @notice Liquid staking token for QRL. Balance represents shares (fixed), - * use getQRLValue() to see current QRL equivalent. - * - * @dev Key concepts: - * - balanceOf() returns raw shares (stable, tax-friendly) - * - getQRLValue() returns QRL equivalent (changes with rewards/slashing) - * - Exchange rate: totalPooledQRL / totalShares - * - * This is a fixed-balance model (like wstETH) rather than rebasing (like stETH). - * Chosen for cleaner tax implications - balance only changes on deposit/withdraw. - * - * Example: - * 1. User deposits 100 QRL when pool has 1000 QRL and 1000 shares - * 2. User receives 100 shares, balanceOf() = 100 - * 3. Validators earn 50 QRL rewards (pool now has 1050 QRL) - * 4. User's balanceOf() still = 100 shares (unchanged) - * 5. User's getQRLValue() = 100 * 1050 / 1000 = 105 QRL - * - * If slashing occurs (pool drops to 950 QRL): - * - User's balanceOf() still = 100 shares - * - User's getQRLValue() = 100 * 950 / 1000 = 95 QRL - */ -contract stQRLv2 { - // ============================================================= - // CONSTANTS - // ============================================================= - - string public constant name = "Staked QRL"; - string public constant symbol = "stQRL"; - uint8 public constant decimals = 18; - - /// @notice Initial shares per QRL (1:1 at launch) - uint256 private constant INITIAL_SHARES_PER_QRL = 1; - - /// @notice Virtual shares offset to prevent first depositor attack (donation attack) - /// @dev Adding virtual shares/assets creates a floor that makes share inflation attacks - /// economically unviable. With 1e3 virtual offset, an attacker would need to - /// donate ~1000x more than they could steal. See OpenZeppelin ERC4626 for details. - uint256 private constant VIRTUAL_SHARES = 1e3; - uint256 private constant VIRTUAL_ASSETS = 1e3; - - /// @notice Upper bound for the minimum stake lock (about 30 days at ~56s blocks) - /// @dev Caps owner power: deposits can never be locked longer than this. - uint256 public constant MAX_MIN_STAKE_BLOCKS = 46500; - - // ============================================================= - // SHARE STORAGE - // ============================================================= - - /// @notice Total shares in existence - uint256 private _totalShares; - - /// @notice Shares held by each account - mapping(address => uint256) private _shares; - - /// @notice Allowances for transferFrom (in shares) - /// @dev All amounts in this contract are shares, not QRL - mapping(address => mapping(address => uint256)) private _allowances; - - /// @notice Shares locked for pending withdrawals (cannot be transferred) - mapping(address => uint256) private _lockedShares; - - /// @notice Minimum stake lock applied to fresh deposits, in blocks (0 disables) - /// @dev Anti-griefing: prevents deposit/withdraw yo-yo cycles that force the - /// operator to bridge liquidity or exit validators at no cost to the - /// attacker. Owner deposits are exempt (bridge capital stays nimble). - uint256 public minStakeBlocks = 1536; - - /// @notice Shares from recent deposits that have not matured yet - /// @dev Immature shares cannot be transferred or queued for withdrawal. - /// Matures lazily: immatureSharesOf() reports 0 once _matureAtBlock - /// passes, no storage write needed. A new deposit folds any remaining - /// immature shares into the new bucket and resets the maturity block; - /// already-matured shares are unaffected. Transfers never write to the - /// recipient's bucket, so third parties cannot extend someone's lock. - mapping(address => uint256) private _immatureShares; - - /// @notice Block at which an account's immature bucket matures - mapping(address => uint256) private _matureAtBlock; - - // ============================================================= - // POOL STORAGE - // ============================================================= - - /// @notice Total QRL controlled by the protocol (staked + rewards - slashing) - /// @dev Updated by DepositPool via updateTotalPooledQRL() - uint256 private _totalPooledQRL; - - // ============================================================= - // ACCESS CONTROL - // ============================================================= - - /// @notice Contract owner (for initial setup) - address public owner; - - /// @notice DepositPool contract (only address that can mint/burn/update) - address public depositPool; - - /// @notice Pause state for emergencies - bool public paused; - - // ============================================================= - // EVENTS - // ============================================================= - - // QRC-20 standard events (values are in shares) - event Transfer(address indexed from, address indexed to, uint256 value); - event Approval(address indexed owner, address indexed spender, uint256 value); - - // Pool events - event TotalPooledQRLUpdated(uint256 previousAmount, uint256 newAmount); - event SharesMinted(address indexed to, uint256 sharesAmount, uint256 qrlAmount); - event SharesBurned(address indexed from, uint256 sharesAmount, uint256 qrlAmount); - - // Admin events - event DepositPoolSet(address indexed previousPool, address indexed newPool); - event MinStakeBlocksUpdated(uint256 previousValue, uint256 newValue); - event Paused(address account); - event Unpaused(address account); - event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); - - // ============================================================= - // ERRORS - // ============================================================= - - error NotOwner(); - error NotDepositPool(); - error ContractPaused(); - error ZeroAddress(); - error ZeroAmount(); - error InsufficientBalance(); - error InsufficientAllowance(); - error DepositPoolAlreadySet(); - error InsufficientUnlockedShares(); - error InsufficientMaturedShares(); - error MinStakeBlocksTooHigh(); - - // ============================================================= - // MODIFIERS - // ============================================================= - - modifier onlyOwner() { - if (msg.sender != owner) revert NotOwner(); - _; - } - - modifier onlyDepositPool() { - if (msg.sender != depositPool) revert NotDepositPool(); - _; - } - - modifier whenNotPaused() { - if (paused) revert ContractPaused(); - _; - } - - // ============================================================= - // CONSTRUCTOR - // ============================================================= - - constructor() { - owner = msg.sender; - } - - // ============================================================= - // QRC-20 VIEW FUNCTIONS - // ============================================================= - - /** - * @notice Returns the total supply of stQRL tokens (in shares) - * @dev Use totalPooledQRL() for the QRL value - * @return Total stQRL shares in circulation - */ - function totalSupply() external view returns (uint256) { - return _totalShares; - } - - /** - * @notice Returns the stQRL balance of an account (in shares) - * @dev Returns raw shares - stable value that only changes on deposit/withdraw - * Use getQRLValue() for the current QRL equivalent - * @param account The address to query - * @return The account's share balance - */ - function balanceOf(address account) public view returns (uint256) { - return _shares[account]; - } - - /** - * @notice Returns the allowance for a spender (in shares) - * @param _owner The token owner - * @param spender The approved spender - * @return The allowance in shares - */ - function allowance(address _owner, address spender) public view returns (uint256) { - return _allowances[_owner][spender]; - } - - // ============================================================= - // QRC-20 WRITE FUNCTIONS - // ============================================================= - - /** - * @notice Transfer stQRL shares to another address - * @param to Recipient address - * @param amount Amount of shares to transfer - * @return success True if transfer succeeded - */ - function transfer(address to, uint256 amount) external whenNotPaused returns (bool) { - _transfer(msg.sender, to, amount); - return true; - } - - /** - * @notice Approve a spender to transfer stQRL shares on your behalf - * @param spender The address to approve - * @param amount The amount of shares to approve - * @return success True if approval succeeded - */ - function approve(address spender, uint256 amount) external returns (bool) { - _approve(msg.sender, spender, amount); - return true; - } - - /** - * @notice Transfer stQRL shares from one address to another (with approval) - * @param from Source address - * @param to Destination address - * @param amount Amount of shares to transfer - * @return success True if transfer succeeded - */ - function transferFrom(address from, address to, uint256 amount) external whenNotPaused returns (bool) { - if (amount == 0) revert ZeroAmount(); - - uint256 currentAllowance = _allowances[from][msg.sender]; - if (currentAllowance < amount) revert InsufficientAllowance(); - - // Decrease allowance (unless unlimited) - if (currentAllowance != type(uint256).max) { - _allowances[from][msg.sender] = currentAllowance - amount; - emit Approval(from, msg.sender, _allowances[from][msg.sender]); - } - - _transfer(from, to, amount); - return true; - } - - // ============================================================= - // SHARE VIEW FUNCTIONS - // ============================================================= - - /** - * @notice Returns the total shares in existence - * @dev Same as totalSupply() in fixed-balance model - * @return Total shares - */ - function totalShares() external view returns (uint256) { - return _totalShares; - } - - /** - * @notice Returns the shares held by an account - * @dev Same as balanceOf() in fixed-balance model - * @param account The address to query - * @return The account's share balance - */ - function sharesOf(address account) external view returns (uint256) { - return _shares[account]; - } - - /** - * @notice Returns the current QRL value of an account's shares - * @dev This is what would have been balanceOf() in a rebasing model - * Value changes as rewards accrue or slashing occurs - * @param account The address to query - * @return The account's stQRL value in QRL terms - */ - function getQRLValue(address account) public view returns (uint256) { - return getPooledQRLByShares(_shares[account]); - } - - /** - * @notice Convert a QRL amount to shares - * @dev shares = qrlAmount * (totalShares + VIRTUAL_SHARES) / (totalPooledQRL + VIRTUAL_ASSETS) - * Virtual offsets prevent first depositor inflation attacks. - * @param qrlAmount The QRL amount to convert - * @return The equivalent number of shares - */ - function getSharesByPooledQRL(uint256 qrlAmount) public view returns (uint256) { - // Use virtual shares/assets to prevent donation attacks - // Even with 0 real shares/assets, the virtual offset ensures fair pricing - return (qrlAmount * (_totalShares + VIRTUAL_SHARES)) / (_totalPooledQRL + VIRTUAL_ASSETS); - } - - /** - * @notice Convert shares to QRL amount - * @dev qrlAmount = shares * (totalPooledQRL + VIRTUAL_ASSETS) / (totalShares + VIRTUAL_SHARES) - * Virtual offsets prevent first depositor inflation attacks. - * @param sharesAmount The shares to convert - * @return The equivalent QRL amount - */ - function getPooledQRLByShares(uint256 sharesAmount) public view returns (uint256) { - // Use virtual shares/assets to prevent donation attacks - // This ensures consistent pricing with getSharesByPooledQRL - return (sharesAmount * (_totalPooledQRL + VIRTUAL_ASSETS)) / (_totalShares + VIRTUAL_SHARES); - } - - /** - * @notice Returns the total QRL controlled by the protocol - * @dev This is the sum of all staked QRL plus rewards minus slashing - * @return Total pooled QRL - */ - function totalPooledQRL() external view returns (uint256) { - return _totalPooledQRL; - } - - /** - * @notice Returns the current exchange rate (QRL per share, scaled by 1e18) - * @dev Useful for UI display and calculations. Uses virtual offsets for consistency. - * @return Exchange rate (1e18 = 1:1) - */ - function getExchangeRate() external view returns (uint256) { - // Use virtual offsets for consistency with share conversion functions - return ((_totalPooledQRL + VIRTUAL_ASSETS) * 1e18) / (_totalShares + VIRTUAL_SHARES); - } - - // ============================================================= - // DEPOSIT POOL FUNCTIONS - // ============================================================= - - /** - * @notice Mint new shares to a recipient - * @dev Only callable by DepositPool when user deposits QRL - * @param to Recipient of the new shares - * @param qrlAmount Amount of QRL being deposited - * @return shares Number of shares minted - */ - function mintShares(address to, uint256 qrlAmount) external onlyDepositPool whenNotPaused returns (uint256 shares) { - if (to == address(0)) revert ZeroAddress(); - if (qrlAmount == 0) revert ZeroAmount(); - - shares = getSharesByPooledQRL(qrlAmount); - if (shares == 0) revert ZeroAmount(); - - _totalShares += shares; - _shares[to] += shares; - - // Apply the minimum stake lock to fresh deposits. The owner (operator) - // is exempt so bridge capital can enter and exit without the maturity - // wait. Remaining immature shares fold into the new bucket and the - // maturity block resets; matured shares are unaffected. - if (minStakeBlocks > 0 && to != owner) { - _immatureShares[to] = immatureSharesOf(to) + shares; - _matureAtBlock[to] = block.number + minStakeBlocks; - } - - // Note: totalPooledQRL is updated separately via updateTotalPooledQRL - // This allows DepositPool to batch updates - - emit SharesMinted(to, shares, qrlAmount); - emit Transfer(address(0), to, shares); - - return shares; - } - - /** - * @notice Burn shares from an account - * @dev Only callable by DepositPool when user withdraws QRL - * @param from Account to burn shares from - * @param sharesAmount Number of shares to burn - * @return qrlAmount Amount of QRL the burned shares were worth - */ - function burnShares(address from, uint256 sharesAmount) - external - onlyDepositPool - whenNotPaused - returns (uint256 qrlAmount) - { - if (from == address(0)) revert ZeroAddress(); - if (sharesAmount == 0) revert ZeroAmount(); - if (_shares[from] < sharesAmount) revert InsufficientBalance(); - - qrlAmount = getPooledQRLByShares(sharesAmount); - - _shares[from] -= sharesAmount; - _totalShares -= sharesAmount; - - // Note: totalPooledQRL is updated separately via updateTotalPooledQRL - - emit SharesBurned(from, sharesAmount, qrlAmount); - emit Transfer(from, address(0), sharesAmount); - - return qrlAmount; - } - - /** - * @notice Update the total pooled QRL - * @dev Called by DepositPool after syncing rewards/slashing - * This changes the exchange rate (affects getQRLValue, not balanceOf) - * @param newTotalPooledQRL The new total pooled QRL amount - */ - function updateTotalPooledQRL(uint256 newTotalPooledQRL) external onlyDepositPool { - uint256 previousAmount = _totalPooledQRL; - _totalPooledQRL = newTotalPooledQRL; - emit TotalPooledQRLUpdated(previousAmount, newTotalPooledQRL); - } - - // ============================================================= - // SHARE LOCKING FUNCTIONS - // ============================================================= - - /** - * @notice Lock shares for a pending withdrawal - * @dev Only callable by DepositPool. Locked shares cannot be transferred. - * @param account The account whose shares to lock - * @param sharesAmount Number of shares to lock - */ - function lockShares(address account, uint256 sharesAmount) external onlyDepositPool { - _lockedShares[account] += sharesAmount; - } - - /** - * @notice Unlock shares after withdrawal claim or cancellation - * @dev Only callable by DepositPool - * @param account The account whose shares to unlock - * @param sharesAmount Number of shares to unlock - */ - function unlockShares(address account, uint256 sharesAmount) external onlyDepositPool { - _lockedShares[account] -= sharesAmount; - } - - /** - * @notice Returns the locked shares for an account - * @param account The address to query - * @return The number of locked shares - */ - function lockedSharesOf(address account) external view returns (uint256) { - return _lockedShares[account]; - } - - // ============================================================= - // STAKE MATURITY FUNCTIONS - // ============================================================= - - /** - * @notice Returns the not-yet-matured shares for an account - * @dev Immature shares cannot be transferred or queued for withdrawal. - * Matures lazily: returns 0 once the maturity block has passed. - * @param account The address to query - * @return The number of immature shares - */ - function immatureSharesOf(address account) public view returns (uint256) { - if (block.number >= _matureAtBlock[account]) { - return 0; - } - return _immatureShares[account]; - } - - /** - * @notice Returns the block at which an account's immature bucket matures - * @dev 0 if the account never deposited. May be in the past (bucket matured). - * @param account The address to query - * @return The maturity block number - */ - function matureAtBlockOf(address account) external view returns (uint256) { - return _matureAtBlock[account]; - } - - /** - * @notice Set the minimum stake lock applied to fresh deposits - * @dev Capped at MAX_MIN_STAKE_BLOCKS. 0 disables the lock entirely. - * Only affects future deposits; existing buckets keep their maturity. - * @param _minStakeBlocks The new lock duration in blocks - */ - function setMinStakeBlocks(uint256 _minStakeBlocks) external onlyOwner { - if (_minStakeBlocks > MAX_MIN_STAKE_BLOCKS) revert MinStakeBlocksTooHigh(); - emit MinStakeBlocksUpdated(minStakeBlocks, _minStakeBlocks); - minStakeBlocks = _minStakeBlocks; - } - - // ============================================================= - // INTERNAL FUNCTIONS - // ============================================================= - - /** - * @dev Internal transfer logic - amount is in shares - */ - function _transfer(address from, address to, uint256 amount) internal { - if (from == address(0)) revert ZeroAddress(); - if (to == address(0)) revert ZeroAddress(); - if (amount == 0) revert ZeroAmount(); - if (_shares[from] < amount) revert InsufficientBalance(); - if (_shares[from] - _lockedShares[from] < amount) revert InsufficientUnlockedShares(); - // After sending, what remains unlocked must still cover the immature - // bucket: immature shares are non-transferable. This closes the lock - // bypass of moving fresh shares to an address with no lock state. - if (_shares[from] - _lockedShares[from] - amount < immatureSharesOf(from)) { - revert InsufficientMaturedShares(); - } - - _shares[from] -= amount; - _shares[to] += amount; - - emit Transfer(from, to, amount); - } - - /** - * @dev Internal approve logic - amount is in shares - */ - function _approve(address _owner, address spender, uint256 amount) internal { - if (_owner == address(0)) revert ZeroAddress(); - if (spender == address(0)) revert ZeroAddress(); - - _allowances[_owner][spender] = amount; - emit Approval(_owner, spender, amount); - } - - // ============================================================= - // ADMIN FUNCTIONS - // ============================================================= - - /** - * @notice Set the DepositPool contract address - * @dev Can only be called once by owner - * @param _depositPool The DepositPool contract address - */ - function setDepositPool(address _depositPool) external onlyOwner { - if (_depositPool == address(0)) revert ZeroAddress(); - if (depositPool != address(0)) revert DepositPoolAlreadySet(); - - emit DepositPoolSet(depositPool, _depositPool); - depositPool = _depositPool; - } - - /** - * @notice Pause the contract - * @dev Blocks transfers, minting, and burning - */ - function pause() external onlyOwner { - paused = true; - emit Paused(msg.sender); - } - - /** - * @notice Unpause the contract - */ - function unpause() external onlyOwner { - paused = false; - emit Unpaused(msg.sender); - } - - /** - * @notice Transfer ownership - * @param newOwner The new owner address - */ - function transferOwnership(address newOwner) external onlyOwner { - if (newOwner == address(0)) revert ZeroAddress(); - emit OwnershipTransferred(owner, newOwner); - owner = newOwner; - } - - /** - * @notice Renounce ownership (irreversible) - * @dev Use after DepositPool is set and system is stable - */ - function renounceOwnership() external onlyOwner { - emit OwnershipTransferred(owner, address(0)); - owner = address(0); - } -} diff --git a/contracts/solidity/v1-deprecated/DepositPool.sol b/contracts/solidity/v1-deprecated/DepositPool.sol deleted file mode 100644 index 9e82f82..0000000 --- a/contracts/solidity/v1-deprecated/DepositPool.sol +++ /dev/null @@ -1,362 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.20; - -/** - * @title DepositPool - User entry point for QuantaPool - * @dev Accepts QRL deposits, mints stQRL, manages withdrawal queue - * - * Deposits accumulate until 40,000 QRL threshold is reached, - * then a validator can be created. - */ - -interface IstQRL { - function mint(address to, uint256 assets) external returns (uint256); - function burn(address from, uint256 shares) external returns (uint256); - function convertToShares(uint256 assets) external view returns (uint256); - function convertToAssets(uint256 shares) external view returns (uint256); - function balanceOf(address account) external view returns (uint256); - function totalAssets() external view returns (uint256); -} - -/// @notice Zond beacon chain deposit contract interface -interface IDepositContract { - function deposit( - bytes calldata pubkey, - bytes calldata withdrawal_credentials, - bytes calldata signature, - bytes32 deposit_data_root - ) external payable; -} - -contract DepositPool { - // ============================================================= - // CONSTANTS - // ============================================================= - - /// @notice Amount of QRL needed to create one validator - uint256 public constant VALIDATOR_THRESHOLD = 40_000 ether; - - /// @notice Zond beacon chain deposit contract address - address public constant DEPOSIT_CONTRACT = 0x4242424242424242424242424242424242424242; - - /// @notice Expected Dilithium pubkey length (bytes) - uint256 private constant PUBKEY_LENGTH = 2592; - - /// @notice Expected Dilithium signature length (bytes) - uint256 private constant SIGNATURE_LENGTH = 4595; - - // ============================================================= - // STORAGE - // ============================================================= - - /// @notice stQRL token contract - IstQRL public stQRL; - - /// @notice Owner address - address public owner; - - /// @notice QRL waiting in queue (not yet staked) - uint256 public pendingDeposits; - - /// @notice QRL available for immediate withdrawals - uint256 public liquidReserve; - - /// @notice Minimum deposit amount - uint256 public minDeposit; - - /// @notice Number of validators created - uint256 public validatorCount; - - /// @notice Paused state - bool public paused; - - /// @notice Reentrancy guard - bool private locked; - - /// @notice Withdrawal request struct - struct WithdrawalRequest { - uint256 shares; - uint256 requestBlock; - bool processed; - } - - /// @notice Pending withdrawal requests - mapping(address => WithdrawalRequest) public withdrawalRequests; - - /// @notice Total pending withdrawal amount (in QRL) - uint256 public pendingWithdrawals; - - // ============================================================= - // EVENTS - // ============================================================= - - event Deposited(address indexed user, uint256 assets, uint256 shares); - event WithdrawalRequested(address indexed user, uint256 shares, uint256 assets); - event WithdrawalClaimed(address indexed user, uint256 assets); - event ValidatorFunded(uint256 indexed validatorId, uint256 amount); - event ValidatorStaked(uint256 indexed validatorId, bytes pubkey); - event LiquidityAdded(uint256 amount); - event MinDepositUpdated(uint256 newMinDeposit); - event Paused(address account); - event Unpaused(address account); - - // ============================================================= - // MODIFIERS - // ============================================================= - - modifier onlyOwner() { - require(msg.sender == owner, "DepositPool: not owner"); - _; - } - - modifier whenNotPaused() { - require(!paused, "DepositPool: paused"); - _; - } - - modifier nonReentrant() { - require(!locked, "DepositPool: reentrant call"); - locked = true; - _; - locked = false; - } - - // ============================================================= - // CONSTRUCTOR - // ============================================================= - - constructor(address _stQRL) { - require(_stQRL != address(0), "DepositPool: zero address"); - stQRL = IstQRL(_stQRL); - owner = msg.sender; - minDeposit = 1 ether; // 1 QRL minimum - } - - // ============================================================= - // DEPOSIT FUNCTIONS - // ============================================================= - - /// @notice Deposit QRL and receive stQRL - /// @return shares Amount of stQRL minted - function deposit() external payable nonReentrant whenNotPaused returns (uint256 shares) { - require(msg.value >= minDeposit, "DepositPool: below minimum"); - - // Mint stQRL to depositor - shares = stQRL.mint(msg.sender, msg.value); - - // Add to pending deposits - pendingDeposits += msg.value; - - emit Deposited(msg.sender, msg.value, shares); - - return shares; - } - - /// @notice Preview deposit - get expected shares for amount - function previewDeposit(uint256 assets) external view returns (uint256) { - return stQRL.convertToShares(assets); - } - - // ============================================================= - // WITHDRAWAL FUNCTIONS - // ============================================================= - - /// @notice Request withdrawal by specifying shares - /// @param shares Amount of stQRL to withdraw - /// @return assets Expected QRL amount - function requestWithdrawal(uint256 shares) external nonReentrant whenNotPaused returns (uint256 assets) { - require(shares > 0, "DepositPool: zero shares"); - require(stQRL.balanceOf(msg.sender) >= shares, "DepositPool: insufficient balance"); - require(withdrawalRequests[msg.sender].shares == 0, "DepositPool: pending withdrawal exists"); - - assets = stQRL.convertToAssets(shares); - - // Check if immediate withdrawal possible - if (liquidReserve >= assets) { - // Immediate withdrawal - liquidReserve -= assets; - - // Burn shares - stQRL.burn(msg.sender, shares); - - // Transfer QRL - (bool success,) = msg.sender.call{value: assets}(""); - require(success, "DepositPool: transfer failed"); - - emit WithdrawalClaimed(msg.sender, assets); - } else { - // Queue for later - withdrawalRequests[msg.sender] = - WithdrawalRequest({shares: shares, requestBlock: block.number, processed: false}); - pendingWithdrawals += assets; - - emit WithdrawalRequested(msg.sender, shares, assets); - } - - return assets; - } - - /// @notice Claim a queued withdrawal - function claimWithdrawal() external nonReentrant { - WithdrawalRequest storage request = withdrawalRequests[msg.sender]; - - require(request.shares > 0, "DepositPool: no pending withdrawal"); - require(!request.processed, "DepositPool: already processed"); - // Wait at least 128 blocks (~128 minutes on Zond = ~1 epoch) - require(block.number >= request.requestBlock + 128, "DepositPool: too early"); - - uint256 assets = stQRL.convertToAssets(request.shares); - require(liquidReserve >= assets, "DepositPool: insufficient liquidity"); - - request.processed = true; - liquidReserve -= assets; - pendingWithdrawals -= assets; - - // Burn shares - stQRL.burn(msg.sender, request.shares); - - // Transfer QRL - (bool success,) = msg.sender.call{value: assets}(""); - require(success, "DepositPool: transfer failed"); - - // Clean up - delete withdrawalRequests[msg.sender]; - - emit WithdrawalClaimed(msg.sender, assets); - } - - /// @notice Preview withdrawal - get expected assets for shares - function previewWithdrawal(uint256 shares) external view returns (uint256) { - return stQRL.convertToAssets(shares); - } - - // ============================================================= - // VIEW FUNCTIONS - // ============================================================= - - /// @notice Get deposit queue status - function getQueueStatus() - external - view - returns (uint256 pending, uint256 threshold, uint256 remaining, uint256 validatorsReady) - { - pending = pendingDeposits; - threshold = VALIDATOR_THRESHOLD; - remaining = pending >= threshold ? 0 : threshold - pending; - validatorsReady = pending / threshold; - } - - /// @notice Get user's withdrawal request - function getWithdrawalRequest(address user) - external - view - returns (uint256 shares, uint256 assets, uint256 requestBlock, bool canClaim) - { - WithdrawalRequest storage request = withdrawalRequests[user]; - shares = request.shares; - assets = stQRL.convertToAssets(shares); - requestBlock = request.requestBlock; - canClaim = !request.processed && request.shares > 0 && block.number >= request.requestBlock + 128 - && liquidReserve >= assets; - } - - /// @notice Get total value locked - function getTVL() external view returns (uint256) { - return stQRL.totalAssets(); - } - - // ============================================================= - // OPERATOR FUNCTIONS - // ============================================================= - - /// @notice Fund a validator with beacon chain deposit - /// @dev Calls the Zond beacon deposit contract with validator keys - /// @param pubkey Dilithium public key (2592 bytes) - /// @param withdrawal_credentials Withdrawal credentials (32 bytes) - /// @param signature Dilithium signature (4595 bytes) - /// @param deposit_data_root SHA-256 hash of SSZ-encoded deposit data - function fundValidator( - bytes calldata pubkey, - bytes calldata withdrawal_credentials, - bytes calldata signature, - bytes32 deposit_data_root - ) external onlyOwner nonReentrant returns (uint256 validatorId) { - require(pendingDeposits >= VALIDATOR_THRESHOLD, "DepositPool: below threshold"); - require(pubkey.length == PUBKEY_LENGTH, "DepositPool: invalid pubkey length"); - require(withdrawal_credentials.length == 32, "DepositPool: invalid credentials length"); - require(signature.length == SIGNATURE_LENGTH, "DepositPool: invalid signature length"); - - pendingDeposits -= VALIDATOR_THRESHOLD; - validatorId = validatorCount++; - - // Call beacon deposit contract - IDepositContract(DEPOSIT_CONTRACT).deposit{value: VALIDATOR_THRESHOLD}( - pubkey, withdrawal_credentials, signature, deposit_data_root - ); - - emit ValidatorStaked(validatorId, pubkey); - - return validatorId; - } - - /// @notice Fund a validator (accounting only, no beacon deposit) - /// @dev For MVP testing - funds stay in contract - function fundValidatorMVP() external onlyOwner nonReentrant returns (uint256 validatorId) { - require(pendingDeposits >= VALIDATOR_THRESHOLD, "DepositPool: below threshold"); - - pendingDeposits -= VALIDATOR_THRESHOLD; - validatorId = validatorCount++; - - // Funds stay in this contract for MVP testing - emit ValidatorFunded(validatorId, VALIDATOR_THRESHOLD); - - return validatorId; - } - - /// @notice Add liquidity for withdrawals (from validator exits) - function addLiquidity() external payable { - liquidReserve += msg.value; - emit LiquidityAdded(msg.value); - } - - // ============================================================= - // ADMIN FUNCTIONS - // ============================================================= - - function setMinDeposit(uint256 _minDeposit) external onlyOwner { - minDeposit = _minDeposit; - emit MinDepositUpdated(_minDeposit); - } - - function setStQRL(address _stQRL) external onlyOwner { - require(_stQRL != address(0), "DepositPool: zero address"); - stQRL = IstQRL(_stQRL); - } - - function pause() external onlyOwner { - paused = true; - emit Paused(msg.sender); - } - - function unpause() external onlyOwner { - paused = false; - emit Unpaused(msg.sender); - } - - function transferOwnership(address newOwner) external onlyOwner { - require(newOwner != address(0), "DepositPool: zero address"); - owner = newOwner; - } - - /// @notice Emergency withdrawal of stuck funds - function emergencyWithdraw(address to, uint256 amount) external onlyOwner { - require(to != address(0), "DepositPool: zero address"); - (bool success,) = to.call{value: amount}(""); - require(success, "DepositPool: transfer failed"); - } - - // Allow receiving QRL - receive() external payable { - liquidReserve += msg.value; - } -} diff --git a/contracts/solidity/v1-deprecated/OperatorRegistry.sol b/contracts/solidity/v1-deprecated/OperatorRegistry.sol deleted file mode 100644 index f032ef0..0000000 --- a/contracts/solidity/v1-deprecated/OperatorRegistry.sol +++ /dev/null @@ -1,276 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.20; - -/** - * @title OperatorRegistry - Manages node operators and validators - * @dev Tracks operator bonds, validators, and commission - * - * MVP: Single operator (owner) - * Future: Permissionless operator registration - */ - -contract OperatorRegistry { - // ============================================================= - // CONSTANTS - // ============================================================= - - /// @notice Minimum bond for quarter minipool (10,000 QRL) - uint256 public constant QUARTER_BOND = 10_000 ether; - - /// @notice Minimum bond for half minipool (20,000 QRL) - uint256 public constant HALF_BOND = 20_000 ether; - - /// @notice Full validator stake - uint256 public constant VALIDATOR_STAKE = 40_000 ether; - - // ============================================================= - // STORAGE - // ============================================================= - - /// @notice Owner address - address public owner; - - /// @notice Commission rate in basis points (1000 = 10%) - uint256 public commissionRate; - - /// @notice Minimum collateral per operator - uint256 public minCollateral; - - /// @notice Operator data - struct Operator { - bool registered; - bool active; - uint256 bondAmount; - uint256 collateral; - uint256 validatorCount; - uint256 pendingRewards; - } - - /// @notice Registered operators - mapping(address => Operator) public operators; - - /// @notice List of operator addresses - address[] public operatorList; - - /// @notice Validator public keys (Dilithium) - bytes[] public validatorPubkeys; - - /// @notice Validator to operator mapping - mapping(bytes32 => address) public validatorOperator; - - /// @notice Total active validators - uint256 public totalValidators; - - // ============================================================= - // EVENTS - // ============================================================= - - event OperatorRegistered(address indexed operator, uint256 bondAmount, uint256 collateral); - event OperatorDeactivated(address indexed operator, string reason); - event ValidatorAdded(address indexed operator, bytes pubkey); - event ValidatorRemoved(address indexed operator, bytes pubkey); - event CommissionUpdated(uint256 newRate); - event RewardsDistributed(uint256 totalRewards, uint256 commission); - event RewardsClaimed(address indexed operator, uint256 amount); - - // ============================================================= - // MODIFIERS - // ============================================================= - - modifier onlyOwner() { - require(msg.sender == owner, "OperatorRegistry: not owner"); - _; - } - - modifier onlyOperator() { - require(operators[msg.sender].registered, "OperatorRegistry: not operator"); - _; - } - - // ============================================================= - // CONSTRUCTOR - // ============================================================= - - constructor() { - owner = msg.sender; - commissionRate = 1000; // 10% default - minCollateral = 5_000 ether; // 5,000 QRL minimum collateral - - // Register owner as initial operator (MVP) - _registerOperator(msg.sender, 0, 0); - } - - // ============================================================= - // OPERATOR FUNCTIONS - // ============================================================= - - /// @notice Register as a node operator - /// @param bondType 0 = quarter bond (10k), 1 = half bond (20k) - function registerOperator(uint256 bondType) external payable { - require(!operators[msg.sender].registered, "OperatorRegistry: already registered"); - - uint256 requiredBond = bondType == 0 ? QUARTER_BOND : HALF_BOND; - require(msg.value >= requiredBond + minCollateral, "OperatorRegistry: insufficient deposit"); - - uint256 collateral = msg.value - requiredBond; - - _registerOperator(msg.sender, requiredBond, collateral); - - emit OperatorRegistered(msg.sender, requiredBond, collateral); - } - - function _registerOperator(address operator, uint256 bond, uint256 collateral) internal { - operators[operator] = Operator({ - registered: true, - active: true, - bondAmount: bond, - collateral: collateral, - validatorCount: 0, - pendingRewards: 0 - }); - operatorList.push(operator); - } - - /// @notice Add a validator (MVP: owner only) - /// @param pubkey Dilithium public key of the validator - function addValidator(bytes calldata pubkey) external onlyOwner { - require(pubkey.length > 0, "OperatorRegistry: empty pubkey"); - - bytes32 pubkeyHash = keccak256(pubkey); - require(validatorOperator[pubkeyHash] == address(0), "OperatorRegistry: validator exists"); - - Operator storage op = operators[msg.sender]; - require(op.active, "OperatorRegistry: operator not active"); - - validatorPubkeys.push(pubkey); - validatorOperator[pubkeyHash] = msg.sender; - op.validatorCount++; - totalValidators++; - - emit ValidatorAdded(msg.sender, pubkey); - } - - /// @notice Remove a validator (for exits) - function removeValidator(bytes calldata pubkey) external onlyOwner { - bytes32 pubkeyHash = keccak256(pubkey); - address operator = validatorOperator[pubkeyHash]; - require(operator != address(0), "OperatorRegistry: validator not found"); - - operators[operator].validatorCount--; - validatorOperator[pubkeyHash] = address(0); - totalValidators--; - - emit ValidatorRemoved(operator, pubkey); - } - - /// @notice Distribute rewards to operators (called by RewardsOracle) - /// @param totalRewards Total rewards to distribute - function distributeRewards(uint256 totalRewards) external payable { - require(msg.value == totalRewards, "OperatorRegistry: incorrect value"); - require(totalValidators > 0, "OperatorRegistry: no validators"); - - // Calculate commission - uint256 totalCommission = (totalRewards * commissionRate) / 10000; - uint256 rewardsPerValidator = totalCommission / totalValidators; - - // Distribute to operators based on validator count - for (uint256 i = 0; i < operatorList.length; i++) { - Operator storage op = operators[operatorList[i]]; - if (op.validatorCount > 0) { - op.pendingRewards += rewardsPerValidator * op.validatorCount; - } - } - - emit RewardsDistributed(totalRewards, totalCommission); - } - - /// @notice Claim accumulated rewards - function claimRewards() external onlyOperator { - Operator storage op = operators[msg.sender]; - uint256 rewards = op.pendingRewards; - require(rewards > 0, "OperatorRegistry: no rewards"); - - op.pendingRewards = 0; - - (bool success,) = msg.sender.call{value: rewards}(""); - require(success, "OperatorRegistry: transfer failed"); - - emit RewardsClaimed(msg.sender, rewards); - } - - // ============================================================= - // VIEW FUNCTIONS - // ============================================================= - - /// @notice Get operator info - function getOperator(address operator) - external - view - returns ( - bool registered, - bool active, - uint256 bondAmount, - uint256 collateral, - uint256 validatorCount, - uint256 pendingRewards - ) - { - Operator storage op = operators[operator]; - return (op.registered, op.active, op.bondAmount, op.collateral, op.validatorCount, op.pendingRewards); - } - - /// @notice Get all validator pubkeys - function getValidators() external view returns (bytes[] memory) { - return validatorPubkeys; - } - - /// @notice Get operator count - function getOperatorCount() external view returns (uint256) { - return operatorList.length; - } - - /// @notice Calculate pooled amount needed for operator bond - function getPooledAmount(address operator) external view returns (uint256) { - return VALIDATOR_STAKE - operators[operator].bondAmount; - } - - // ============================================================= - // ADMIN FUNCTIONS - // ============================================================= - - function setCommissionRate(uint256 _rate) external onlyOwner { - require(_rate >= 500 && _rate <= 2000, "OperatorRegistry: rate must be 5-20%"); - commissionRate = _rate; - emit CommissionUpdated(_rate); - } - - function setMinCollateral(uint256 _minCollateral) external onlyOwner { - minCollateral = _minCollateral; - } - - function deactivateOperator(address operator) external onlyOwner { - require(operators[operator].registered, "OperatorRegistry: not registered"); - operators[operator].active = false; - emit OperatorDeactivated(operator, "Admin action"); - } - - function activateOperator(address operator) external onlyOwner { - require(operators[operator].registered, "OperatorRegistry: not registered"); - operators[operator].active = true; - } - - function transferOwnership(address newOwner) external onlyOwner { - require(newOwner != address(0), "OperatorRegistry: zero address"); - owner = newOwner; - } - - /// @notice Emergency withdraw (for stuck funds) - function emergencyWithdraw(address to, uint256 amount) external onlyOwner { - require(to != address(0), "OperatorRegistry: zero address"); - (bool success,) = to.call{value: amount}(""); - require(success, "OperatorRegistry: transfer failed"); - } - - // Allow receiving QRL - receive() external payable {} -} diff --git a/contracts/solidity/v1-deprecated/RewardsOracle.sol b/contracts/solidity/v1-deprecated/RewardsOracle.sol deleted file mode 100644 index 527848f..0000000 --- a/contracts/solidity/v1-deprecated/RewardsOracle.sol +++ /dev/null @@ -1,238 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.20; - -/** - * @title RewardsOracle - Reports validator rewards to stQRL - * @dev Updates exchange rate based on validator performance - * - * MVP: Single trusted operator submits reports - * Future: Multi-party oracle with threshold signatures - */ - -interface IstQRL { - function updateRewards(uint256 newTotalAssets) external; - function totalAssets() external view returns (uint256); -} - -contract RewardsOracle { - // ============================================================= - // STORAGE - // ============================================================= - - /// @notice stQRL token contract - IstQRL public stQRL; - - /// @notice Owner address - address public owner; - - /// @notice Authorized oracle addresses - mapping(address => bool) public isOracle; - - /// @notice Number of active oracles - uint256 public oracleCount; - - /// @notice Last reported total balance - uint256 public lastReportedBalance; - - /// @notice Last report timestamp - uint256 public lastReportTimestamp; - - /// @notice Minimum time between reports (seconds) - uint256 public reportCooldown; - - /// @notice Maximum rate change per report (basis points, 100 = 1%) - uint256 public maxRateChange; - - // ============================================================= - // EVENTS - // ============================================================= - - event ReportSubmitted(address indexed oracle, uint256 previousBalance, uint256 newBalance, uint256 rewards); - event OracleAdded(address indexed oracle); - event OracleRemoved(address indexed oracle); - event CooldownUpdated(uint256 newCooldown); - event MaxRateChangeUpdated(uint256 newMaxChange); - - // ============================================================= - // MODIFIERS - // ============================================================= - - modifier onlyOwner() { - require(msg.sender == owner, "RewardsOracle: not owner"); - _; - } - - modifier onlyOracle() { - require(isOracle[msg.sender], "RewardsOracle: not oracle"); - _; - } - - // ============================================================= - // CONSTRUCTOR - // ============================================================= - - constructor(address _stQRL) { - require(_stQRL != address(0), "RewardsOracle: zero address"); - stQRL = IstQRL(_stQRL); - owner = msg.sender; - - // Add owner as initial oracle - isOracle[msg.sender] = true; - oracleCount = 1; - - // Default settings - reportCooldown = 24 hours; - maxRateChange = 100; // 1% max change per report - - emit OracleAdded(msg.sender); - } - - // ============================================================= - // ORACLE FUNCTIONS - // ============================================================= - - /// @notice Submit validator balance report - /// @param newTotalBalance Total QRL across all validators - function submitReport(uint256 newTotalBalance) external onlyOracle { - require(block.timestamp >= lastReportTimestamp + reportCooldown, "RewardsOracle: cooldown not elapsed"); - - uint256 previousBalance = lastReportedBalance; - - // First report - just set baseline - if (previousBalance == 0) { - lastReportedBalance = newTotalBalance; - lastReportTimestamp = block.timestamp; - - emit ReportSubmitted(msg.sender, 0, newTotalBalance, 0); - return; - } - - // Calculate rewards (balance increase) - require(newTotalBalance >= previousBalance, "RewardsOracle: balance cannot decrease"); - uint256 rewards = newTotalBalance - previousBalance; - - // Validate rate change is within limits - if (rewards > 0) { - uint256 changePercent = (rewards * 10000) / previousBalance; - require(changePercent <= maxRateChange, "RewardsOracle: rate change exceeds max"); - } - - // Update stQRL with new total assets - uint256 currentAssets = stQRL.totalAssets(); - uint256 newAssets = currentAssets + rewards; - stQRL.updateRewards(newAssets); - - // Update state - lastReportedBalance = newTotalBalance; - lastReportTimestamp = block.timestamp; - - emit ReportSubmitted(msg.sender, previousBalance, newTotalBalance, rewards); - } - - /// @notice Force update in case of emergency (e.g., slashing) - /// @dev Only owner, bypasses rate change limit - function emergencyUpdate(uint256 newTotalBalance) external onlyOwner { - require(block.timestamp >= lastReportTimestamp + 1 hours, "RewardsOracle: min 1 hour between updates"); - - uint256 previousBalance = lastReportedBalance; - uint256 currentAssets = stQRL.totalAssets(); - - // Handle both increases and decreases - if (newTotalBalance >= previousBalance) { - uint256 rewards = newTotalBalance - previousBalance; - stQRL.updateRewards(currentAssets + rewards); - } else { - // Slashing scenario - this would need special handling in stQRL - // For MVP, we don't support decreasing balance - revert("RewardsOracle: slashing not supported in MVP"); - } - - lastReportedBalance = newTotalBalance; - lastReportTimestamp = block.timestamp; - - emit ReportSubmitted(msg.sender, previousBalance, newTotalBalance, 0); - } - - // ============================================================= - // VIEW FUNCTIONS - // ============================================================= - - /// @notice Get oracle status - function getStatus() - external - view - returns (uint256 lastReport, uint256 cooldownRemaining, uint256 lastBalance, bool canReport) - { - lastReport = lastReportTimestamp; - - if (block.timestamp < lastReportTimestamp + reportCooldown) { - cooldownRemaining = (lastReportTimestamp + reportCooldown) - block.timestamp; - } else { - cooldownRemaining = 0; - } - - lastBalance = lastReportedBalance; - canReport = cooldownRemaining == 0; - } - - /// @notice Estimate expected rewards based on APY - /// @param annualYieldBps Annual yield in basis points (e.g., 250 = 2.5%) - function estimateRewards(uint256 annualYieldBps) external view returns (uint256) { - if (lastReportTimestamp == 0) return 0; - - uint256 timeSinceReport = block.timestamp - lastReportTimestamp; - uint256 currentAssets = stQRL.totalAssets(); - - // rewards = assets * yield * time / year - return (currentAssets * annualYieldBps * timeSinceReport) / (365 days * 10000); - } - - // ============================================================= - // ADMIN FUNCTIONS - // ============================================================= - - function addOracle(address oracle) external onlyOwner { - require(!isOracle[oracle], "RewardsOracle: already oracle"); - require(oracle != address(0), "RewardsOracle: zero address"); - - isOracle[oracle] = true; - oracleCount++; - - emit OracleAdded(oracle); - } - - function removeOracle(address oracle) external onlyOwner { - require(isOracle[oracle], "RewardsOracle: not oracle"); - require(oracleCount > 1, "RewardsOracle: cannot remove last oracle"); - - isOracle[oracle] = false; - oracleCount--; - - emit OracleRemoved(oracle); - } - - function setReportCooldown(uint256 _cooldown) external onlyOwner { - require(_cooldown >= 1 hours, "RewardsOracle: cooldown too short"); - require(_cooldown <= 7 days, "RewardsOracle: cooldown too long"); - - reportCooldown = _cooldown; - emit CooldownUpdated(_cooldown); - } - - function setMaxRateChange(uint256 _maxChange) external onlyOwner { - require(_maxChange <= 500, "RewardsOracle: max 5% per report"); - - maxRateChange = _maxChange; - emit MaxRateChangeUpdated(_maxChange); - } - - function setStQRL(address _stQRL) external onlyOwner { - require(_stQRL != address(0), "RewardsOracle: zero address"); - stQRL = IstQRL(_stQRL); - } - - function transferOwnership(address newOwner) external onlyOwner { - require(newOwner != address(0), "RewardsOracle: zero address"); - owner = newOwner; - } -} diff --git a/contracts/solidity/v1-deprecated/TestToken.sol b/contracts/solidity/v1-deprecated/TestToken.sol deleted file mode 100644 index 8d45c6b..0000000 --- a/contracts/solidity/v1-deprecated/TestToken.sol +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.20; - -/** - * @title TestToken - * @dev Simple ZRC20 token to verify Zond deployment works - */ -contract TestToken { - string public name = "Test Token"; - string public symbol = "TEST"; - uint8 public decimals = 18; - uint256 public totalSupply; - - mapping(address => uint256) public balanceOf; - mapping(address => mapping(address => uint256)) public allowance; - - event Transfer(address indexed from, address indexed to, uint256 value); - event Approval(address indexed owner, address indexed spender, uint256 value); - - constructor(uint256 initialSupply) { - totalSupply = initialSupply * 10 ** decimals; - balanceOf[msg.sender] = totalSupply; - emit Transfer(address(0), msg.sender, totalSupply); - } - - function transfer(address to, uint256 amount) public returns (bool) { - require(balanceOf[msg.sender] >= amount, "Insufficient balance"); - balanceOf[msg.sender] -= amount; - balanceOf[to] += amount; - emit Transfer(msg.sender, to, amount); - return true; - } - - function approve(address spender, uint256 amount) public returns (bool) { - allowance[msg.sender][spender] = amount; - emit Approval(msg.sender, spender, amount); - return true; - } - - function transferFrom(address from, address to, uint256 amount) public returns (bool) { - require(balanceOf[from] >= amount, "Insufficient balance"); - require(allowance[from][msg.sender] >= amount, "Insufficient allowance"); - allowance[from][msg.sender] -= amount; - balanceOf[from] -= amount; - balanceOf[to] += amount; - emit Transfer(from, to, amount); - return true; - } -} diff --git a/contracts/solidity/v1-deprecated/stQRL.sol b/contracts/solidity/v1-deprecated/stQRL.sol deleted file mode 100644 index b14adaa..0000000 --- a/contracts/solidity/v1-deprecated/stQRL.sol +++ /dev/null @@ -1,309 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.20; - -/** - * @title stQRL - Staked QRL Liquid Staking Token - * @dev ERC-4626 compliant tokenized vault for QRL liquid staking - * - * Users deposit QRL, receive stQRL shares. Exchange rate increases - * as validators earn rewards, so stQRL appreciates in QRL value. - */ -contract stQRL { - // ============================================================= - // ERC-20 STORAGE - // ============================================================= - - string public constant name = "Staked QRL"; - string public constant symbol = "stQRL"; - uint8 public constant decimals = 18; - - uint256 public totalSupply; - mapping(address => uint256) public balanceOf; - mapping(address => mapping(address => uint256)) public allowance; - - // ============================================================= - // VAULT STORAGE - // ============================================================= - - /// @notice Total QRL assets under management (deposits + rewards) - uint256 public totalAssets; - - /// @notice Pending rewards from validators (updated by oracle) - uint256 public pendingRewards; - - /// @notice Last time rewards were updated - uint256 public lastRewardUpdate; - - // ============================================================= - // ACCESS CONTROL - // ============================================================= - - address public owner; - address public depositPool; - address public rewardsOracle; - - bool public paused; - - // ============================================================= - // EVENTS - // ============================================================= - - // ERC-20 events - event Transfer(address indexed from, address indexed to, uint256 value); - event Approval(address indexed owner, address indexed spender, uint256 value); - - // ERC-4626 events - event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares); - event Withdraw( - address indexed sender, address indexed receiver, address indexed owner, uint256 assets, uint256 shares - ); - - // Admin events - event DepositPoolUpdated(address indexed oldPool, address indexed newPool); - event RewardsOracleUpdated(address indexed oldOracle, address indexed newOracle); - event RewardsUpdated(uint256 newTotalAssets, uint256 timestamp); - event Paused(address account); - event Unpaused(address account); - - // ============================================================= - // MODIFIERS - // ============================================================= - - modifier onlyOwner() { - require(msg.sender == owner, "stQRL: not owner"); - _; - } - - modifier onlyDepositPool() { - require(msg.sender == depositPool, "stQRL: not deposit pool"); - _; - } - - modifier onlyRewardsOracle() { - require(msg.sender == rewardsOracle, "stQRL: not oracle"); - _; - } - - modifier whenNotPaused() { - require(!paused, "stQRL: paused"); - _; - } - - // ============================================================= - // CONSTRUCTOR - // ============================================================= - - constructor() { - owner = msg.sender; - lastRewardUpdate = block.timestamp; - } - - // ============================================================= - // ERC-20 FUNCTIONS - // ============================================================= - - function transfer(address to, uint256 amount) public whenNotPaused returns (bool) { - require(balanceOf[msg.sender] >= amount, "stQRL: insufficient balance"); - balanceOf[msg.sender] -= amount; - balanceOf[to] += amount; - emit Transfer(msg.sender, to, amount); - return true; - } - - function approve(address spender, uint256 amount) public returns (bool) { - allowance[msg.sender][spender] = amount; - emit Approval(msg.sender, spender, amount); - return true; - } - - function transferFrom(address from, address to, uint256 amount) public whenNotPaused returns (bool) { - require(balanceOf[from] >= amount, "stQRL: insufficient balance"); - require(allowance[from][msg.sender] >= amount, "stQRL: insufficient allowance"); - - allowance[from][msg.sender] -= amount; - balanceOf[from] -= amount; - balanceOf[to] += amount; - - emit Transfer(from, to, amount); - return true; - } - - // ============================================================= - // ERC-4626 VIEW FUNCTIONS - // ============================================================= - - /// @notice Returns the underlying asset (QRL/native token) - /// @dev Returns address(0) to indicate native token - function asset() public pure returns (address) { - return address(0); // Native QRL - } - - /// @notice Returns total assets under management - function totalAssetsManaaged() public view returns (uint256) { - return totalAssets; - } - - /// @notice Convert assets (QRL) to shares (stQRL) - function convertToShares(uint256 assets) public view returns (uint256) { - uint256 supply = totalSupply; - if (supply == 0 || totalAssets == 0) { - return assets; // 1:1 initially - } - return (assets * supply) / totalAssets; - } - - /// @notice Convert shares (stQRL) to assets (QRL) - function convertToAssets(uint256 shares) public view returns (uint256) { - uint256 supply = totalSupply; - if (supply == 0) { - return shares; // 1:1 initially - } - return (shares * totalAssets) / supply; - } - - /// @notice Get current exchange rate (QRL per stQRL, scaled by 1e18) - function getExchangeRate() public view returns (uint256) { - if (totalSupply == 0) { - return 1e18; // 1:1 initially - } - return (totalAssets * 1e18) / totalSupply; - } - - /// @notice Maximum deposit allowed - function maxDeposit(address) public pure returns (uint256) { - return type(uint256).max; - } - - /// @notice Maximum mint allowed - function maxMint(address) public pure returns (uint256) { - return type(uint256).max; - } - - /// @notice Maximum withdrawal allowed - function maxWithdraw(address owner_) public view returns (uint256) { - return convertToAssets(balanceOf[owner_]); - } - - /// @notice Maximum redeem allowed - function maxRedeem(address owner_) public view returns (uint256) { - return balanceOf[owner_]; - } - - /// @notice Preview shares for deposit amount - function previewDeposit(uint256 assets) public view returns (uint256) { - return convertToShares(assets); - } - - /// @notice Preview assets needed for mint amount - function previewMint(uint256 shares) public view returns (uint256) { - uint256 supply = totalSupply; - if (supply == 0 || totalAssets == 0) { - return shares; - } - // Round up - return (shares * totalAssets + supply - 1) / supply; - } - - /// @notice Preview shares needed for withdraw amount - function previewWithdraw(uint256 assets) public view returns (uint256) { - uint256 supply = totalSupply; - if (supply == 0 || totalAssets == 0) { - return assets; - } - // Round up - return (assets * supply + totalAssets - 1) / totalAssets; - } - - /// @notice Preview assets for redeem amount - function previewRedeem(uint256 shares) public view returns (uint256) { - return convertToAssets(shares); - } - - // ============================================================= - // DEPOSIT POOL FUNCTIONS - // ============================================================= - - /// @notice Mint shares to user (called by DepositPool) - /// @param to Address to mint shares to - /// @param assets Amount of QRL deposited - /// @return shares Amount of stQRL minted - function mint(address to, uint256 assets) external onlyDepositPool whenNotPaused returns (uint256 shares) { - shares = convertToShares(assets); - require(shares > 0, "stQRL: zero shares"); - - totalSupply += shares; - balanceOf[to] += shares; - totalAssets += assets; - - emit Transfer(address(0), to, shares); - emit Deposit(msg.sender, to, assets, shares); - - return shares; - } - - /// @notice Burn shares from user (called by DepositPool) - /// @param from Address to burn shares from - /// @param shares Amount of stQRL to burn - /// @return assets Amount of QRL to return - function burn(address from, uint256 shares) external onlyDepositPool whenNotPaused returns (uint256 assets) { - require(balanceOf[from] >= shares, "stQRL: insufficient balance"); - - assets = convertToAssets(shares); - require(assets > 0, "stQRL: zero assets"); - - balanceOf[from] -= shares; - totalSupply -= shares; - totalAssets -= assets; - - emit Transfer(from, address(0), shares); - emit Withdraw(msg.sender, from, from, assets, shares); - - return assets; - } - - // ============================================================= - // ORACLE FUNCTIONS - // ============================================================= - - /// @notice Update total assets with new rewards (called by Oracle) - /// @param newTotalAssets New total assets value - function updateRewards(uint256 newTotalAssets) external onlyRewardsOracle { - require(newTotalAssets >= totalAssets, "stQRL: assets cannot decrease"); - - uint256 rewards = newTotalAssets - totalAssets; - pendingRewards = rewards; - totalAssets = newTotalAssets; - lastRewardUpdate = block.timestamp; - - emit RewardsUpdated(newTotalAssets, block.timestamp); - } - - // ============================================================= - // ADMIN FUNCTIONS - // ============================================================= - - function setDepositPool(address _depositPool) external onlyOwner { - emit DepositPoolUpdated(depositPool, _depositPool); - depositPool = _depositPool; - } - - function setRewardsOracle(address _oracle) external onlyOwner { - emit RewardsOracleUpdated(rewardsOracle, _oracle); - rewardsOracle = _oracle; - } - - function pause() external onlyOwner { - paused = true; - emit Paused(msg.sender); - } - - function unpause() external onlyOwner { - paused = false; - emit Unpaused(msg.sender); - } - - function transferOwnership(address newOwner) external onlyOwner { - require(newOwner != address(0), "stQRL: zero address"); - owner = newOwner; - } -} diff --git a/contracts/test/DepositPool-v2.t.sol b/contracts/test/DepositPool-v2.t.sol deleted file mode 100644 index 88396b7..0000000 --- a/contracts/test/DepositPool-v2.t.sol +++ /dev/null @@ -1,1696 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.24; - -import "forge-std/Test.sol"; -import "../solidity/stQRL-v2.sol"; -import "../solidity/DepositPool-v2.sol"; - -/** - * @title DepositPool v2 Integration Tests - * @notice Tests for deposit, withdrawal, and reward sync flows - */ -contract DepositPoolV2Test is Test { - stQRLv2 public token; - DepositPoolV2 public pool; - - address public owner; - address public user1; - address public user2; - - event Deposited(address indexed user, uint256 qrlAmount, uint256 sharesReceived); - event WithdrawalRequested(address indexed user, uint256 shares, uint256 qrlAmount, uint256 requestBlock); - event WithdrawalClaimed(address indexed user, uint256 shares, uint256 qrlAmount); - event RewardsSynced(uint256 rewardsAmount, uint256 newTotalPooled, uint256 blockNumber); - event SlashingDetected(uint256 lossAmount, uint256 newTotalPooled, uint256 blockNumber); - - function setUp() public { - owner = address(this); - user1 = address(0x1); - user2 = address(0x2); - - // Deploy contracts - token = new stQRLv2(); - pool = new DepositPoolV2(); - - // Link contracts - pool.setStQRL(address(token)); - token.setDepositPool(address(pool)); - - // Legacy tests deposit and withdraw in the same block. The minimum - // stake lock has its own dedicated test section, which re-enables it. - token.setMinStakeBlocks(0); - - // Fund test users - vm.deal(user1, 1000 ether); - vm.deal(user2, 1000 ether); - } - - // ========================================================================= - // DEPOSIT TESTS - // ========================================================================= - - function test_Deposit() public { - vm.prank(user1); - uint256 shares = pool.deposit{value: 100 ether}(); - - assertEq(shares, 100 ether); - assertEq(token.balanceOf(user1), 100 ether); - assertEq(pool.bufferedQRL(), 100 ether); - } - - function test_Deposit_MinimumEnforced() public { - vm.prank(user1); - vm.expectRevert(DepositPoolV2.BelowMinDeposit.selector); - pool.deposit{value: 0.01 ether}(); // Below 0.1 minimum - } - - function test_MultipleDeposits() public { - vm.prank(user1); - pool.deposit{value: 100 ether}(); - - vm.prank(user2); - pool.deposit{value: 200 ether}(); - - assertEq(token.balanceOf(user1), 100 ether); - assertEq(token.balanceOf(user2), 200 ether); - assertEq(pool.bufferedQRL(), 300 ether); - assertEq(token.totalSupply(), 300 ether); - } - - function test_DepositAfterRewards() public { - // User1 deposits 100 QRL - vm.prank(user1); - pool.deposit{value: 100 ether}(); - - // Simulate rewards by sending ETH directly and syncing - vm.deal(address(pool), 150 ether); // 50 QRL rewards - pool.syncRewards(); - - // User1's shares unchanged (fixed-balance) - assertEq(token.balanceOf(user1), 100 ether); - // But QRL value increased (approx due to virtual shares) - assertApproxEqRel(token.getQRLValue(user1), 150 ether, 1e14); - - // User2 deposits 150 QRL (should get ~100 shares at new rate) - vm.prank(user2); - uint256 shares = pool.deposit{value: 150 ether}(); - - // User2 gets shares based on current rate - // Rate: 150 QRL / 100 shares = 1.5 QRL per share - // For 150 QRL: 150 / 1.5 ≈ 100 shares (approx due to virtual shares) - assertApproxEqRel(shares, 100 ether, 1e14); - assertApproxEqRel(token.sharesOf(user2), 100 ether, 1e14); - } - - // ========================================================================= - // REWARD SYNC TESTS - // ========================================================================= - - function test_SyncRewards_DetectsRewards() public { - // User deposits - vm.prank(user1); - pool.deposit{value: 100 ether}(); - - // Initial state - assertEq(token.totalPooledQRL(), 100 ether); - assertEq(pool.totalRewardsReceived(), 0); - - // Simulate validator rewards by adding ETH to contract - vm.deal(address(pool), 110 ether); // 10 QRL rewards - - // Sync should detect rewards - vm.expectEmit(true, true, true, true); - emit RewardsSynced(10 ether, 110 ether, block.number); - pool.syncRewards(); - - assertEq(token.totalPooledQRL(), 110 ether); - assertEq(pool.totalRewardsReceived(), 10 ether); - // Shares unchanged (fixed-balance) - assertEq(token.balanceOf(user1), 100 ether); - // QRL value reflects rewards (approx due to virtual shares) - assertApproxEqRel(token.getQRLValue(user1), 110 ether, 1e14); - } - - function test_SyncRewards_DetectsSlashing() public { - // This test demonstrates slashing detection - // Slashing math is verified in stQRL tests (balance decrease) - // Here we just verify the sync doesn't break with no change - vm.prank(user1); - pool.deposit{value: 100 ether}(); - - // Sync should work without changes - pool.syncRewards(); - assertEq(token.totalPooledQRL(), 100 ether); - } - - function test_SyncRewards_NoChangeWhenBalanceMatch() public { - vm.prank(user1); - pool.deposit{value: 100 ether}(); - - uint256 rewardsBefore = pool.totalRewardsReceived(); - pool.syncRewards(); - uint256 rewardsAfter = pool.totalRewardsReceived(); - - // No change in rewards - assertEq(rewardsBefore, rewardsAfter); - } - - // ========================================================================= - // WITHDRAWAL TESTS - // ========================================================================= - - function test_RequestWithdrawal() public { - // Deposit - vm.prank(user1); - pool.deposit{value: 100 ether}(); - - // Request withdrawal - vm.prank(user1); - (uint256 requestId, uint256 qrlAmount) = pool.requestWithdrawal(50 ether); - - assertEq(requestId, 0); - assertEq(qrlAmount, 50 ether); - - (uint256 shares, uint256 qrl, uint256 requestBlock, bool canClaim,, bool claimed) = - pool.getWithdrawalRequest(user1, 0); - - assertEq(shares, 50 ether); - assertEq(qrl, 50 ether); - assertEq(requestBlock, block.number); - assertFalse(canClaim); // Not enough time passed - assertFalse(claimed); - } - - function test_ClaimWithdrawal() public { - // Deposit - vm.prank(user1); - pool.deposit{value: 100 ether}(); - - // Request withdrawal first. The returned value is the current estimate. - vm.prank(user1); - pool.requestWithdrawal(50 ether); - - // Earmark liquid QRL for the claim. - pool.fundWithdrawalReserve(50 ether); - - // Wait for withdrawal delay - vm.roll(block.number + 129); // > 128 blocks - - // Claim - uint256 balanceBefore = user1.balance; - vm.prank(user1); - uint256 claimed = pool.claimWithdrawal(); - - assertEq(claimed, 50 ether); - assertEq(user1.balance - balanceBefore, 50 ether); - assertEq(token.balanceOf(user1), 50 ether); - } - - function test_ClaimWithdrawal_TooEarly() public { - vm.prank(user1); - pool.deposit{value: 100 ether}(); - - vm.prank(user1); - pool.requestWithdrawal(50 ether); - - pool.fundWithdrawalReserve(50 ether); - - // Try to claim immediately (should fail) - vm.prank(user1); - vm.expectRevert(DepositPoolV2.WithdrawalNotReady.selector); - pool.claimWithdrawal(); - } - - function test_ClaimWithdrawal_InsufficientReserve() public { - vm.prank(user1); - pool.deposit{value: 100 ether}(); - - // No withdrawal reserve funded - - vm.prank(user1); - pool.requestWithdrawal(50 ether); - - vm.roll(block.number + 129); - - vm.prank(user1); - vm.expectRevert(DepositPoolV2.InsufficientReserve.selector); - pool.claimWithdrawal(); - } - - function test_CancelWithdrawal() public { - vm.prank(user1); - pool.deposit{value: 100 ether}(); - - vm.prank(user1); - (uint256 requestId,) = pool.requestWithdrawal(50 ether); - - assertEq(pool.totalWithdrawalShares(), 50 ether); - - vm.prank(user1); - pool.cancelWithdrawal(requestId); - - assertEq(pool.totalWithdrawalShares(), 0); - } - - function test_WithdrawalAfterRewards() public { - // Deposit 100 QRL - vm.prank(user1); - pool.deposit{value: 100 ether}(); - - // Add 10% rewards - vm.deal(address(pool), 110 ether); - pool.syncRewards(); - - // Shares unchanged (fixed-balance) - assertEq(token.balanceOf(user1), 100 ether); - // User's shares now worth 110 QRL (approx due to virtual shares) - assertApproxEqRel(token.getQRLValue(user1), 110 ether, 1e14); - - // Request withdrawal of all shares before funding reserve. - vm.prank(user1); - (, uint256 qrlAmount) = pool.requestWithdrawal(100 ether); - - // Approx due to virtual shares - assertApproxEqRel(qrlAmount, 110 ether, 1e14); - - // Earmark enough liquid QRL to cover the claim. - pool.fundWithdrawalReserve(token.totalPooledQRL()); - - vm.roll(block.number + 129); - - uint256 balanceBefore = user1.balance; - vm.prank(user1); - uint256 claimed = pool.claimWithdrawal(); - - // Should receive ~110 QRL (original + rewards) - assertApproxEqRel(user1.balance - balanceBefore, 110 ether, 1e14); - assertEq(user1.balance - balanceBefore, claimed); - } - - // ========================================================================= - // SLASHING SIMULATION - // ========================================================================= - - function test_SlashingReducesWithdrawalAmount() public { - // Deposit 100 QRL - vm.prank(user1); - pool.deposit{value: 100 ether}(); - - // User's shares are worth 100 QRL initially (approx due to virtual shares) - assertApproxEqRel(token.getQRLValue(user1), 100 ether, 1e14); - - // Simulate slashing by directly reducing the contract balance - // In real scenarios, this happens through validator slashing on the beacon chain - vm.deal(address(pool), 90 ether); // Was 100, now 90 - - // Sync to detect the "slashing" - pool.syncRewards(); - - // User's shares now worth less (90 QRL instead of 100) (approx) - assertApproxEqRel(token.getQRLValue(user1), 90 ether, 1e14); - - // Request withdrawal of all shares after the loss is synchronized. - vm.prank(user1); - (, uint256 qrlAmount) = pool.requestWithdrawal(100 ether); - - // Should only get ~90 QRL (slashed amount) (approx due to virtual shares) - assertApproxEqRel(qrlAmount, 90 ether, 1e14); - - // Earmark liquid QRL for the claim. - pool.fundWithdrawalReserve(token.totalPooledQRL()); - - vm.roll(block.number + pool.WITHDRAWAL_DELAY()); - (,,, bool canClaim,,) = pool.getWithdrawalRequest(user1, 0); - assertTrue(canClaim); - uint256 balanceBefore = user1.balance; - vm.prank(user1); - uint256 claimed = pool.claimWithdrawal(); - assertEq(claimed, 90 ether); - assertEq(user1.balance - balanceBefore, 90 ether); - } - - function test_SlashingDetected_EmitsEvent() public { - vm.prank(user1); - pool.deposit{value: 100 ether}(); - - // Simulate slashing by directly reducing the contract balance - vm.deal(address(pool), 90 ether); // Was 100, now 90 - - vm.expectEmit(true, true, true, true); - emit SlashingDetected(10 ether, 90 ether, block.number); - pool.syncRewards(); - } - - // ========================================================================= - // VALIDATOR FUNDING TESTS - // ========================================================================= - - function test_CanFundValidator() public { - // Fund users with enough ETH for this test - vm.deal(user1, 20000 ether); - vm.deal(user2, 20000 ether); - - // Deposit less than threshold - vm.prank(user1); - pool.deposit{value: 20000 ether}(); - - (bool possible, uint256 buffered) = pool.canFundValidator(); - assertFalse(possible); - assertEq(buffered, 20000 ether); - - // Deposit more to reach threshold - vm.prank(user2); - pool.deposit{value: 20000 ether}(); - - (possible, buffered) = pool.canFundValidator(); - assertTrue(possible); - assertEq(buffered, 40000 ether); - } - - function test_FundValidatorMVP() public { - // Deposit enough for validator (40,000 QRL per Zond mainnet config) - vm.deal(user1, 40000 ether); - vm.prank(user1); - pool.deposit{value: 40000 ether}(); - - uint256 validatorId = pool.fundValidatorMVP(); - - assertEq(validatorId, 0); - assertEq(pool.validatorCount(), 1); - assertEq(pool.bufferedQRL(), 0); - } - - // ========================================================================= - // VIEW FUNCTION TESTS - // ========================================================================= - - function test_GetPoolStatus() public { - vm.prank(user1); - pool.deposit{value: 100 ether}(); - - ( - uint256 totalPooled, - uint256 totalShares, - uint256 buffered, - uint256 validators, - uint256 pendingShares, - uint256 reserve, - uint256 rate - ) = pool.getPoolStatus(); - - assertEq(totalPooled, 100 ether); - assertEq(totalShares, 100 ether); - assertEq(buffered, 100 ether); - assertEq(validators, 0); - assertEq(pendingShares, 0); - assertEq(reserve, 0); - assertEq(rate, 1e18); // 1:1 exchange rate - } - - function test_GetRewardStats() public { - vm.prank(user1); - pool.deposit{value: 100 ether}(); - - // Add rewards - vm.deal(address(pool), 110 ether); - pool.syncRewards(); - - (uint256 totalRewards, uint256 totalSlashing, uint256 netRewards, uint256 lastSync) = pool.getRewardStats(); - - assertEq(totalRewards, 10 ether); - assertEq(totalSlashing, 0); - assertEq(netRewards, 10 ether); - assertEq(lastSync, block.number); - } - - // ========================================================================= - // ACCESS CONTROL TESTS - // ========================================================================= - - function test_OnlyOwnerCanFundValidator() public { - vm.deal(user1, 40000 ether); - vm.prank(user1); - pool.deposit{value: 40000 ether}(); - - vm.prank(user1); - vm.expectRevert(DepositPoolV2.NotOwner.selector); - pool.fundValidatorMVP(); - } - - function test_OnlyOwnerCanPause() public { - vm.prank(user1); - vm.expectRevert(DepositPoolV2.NotOwner.selector); - pool.pause(); - } - - function test_PauseBlocksDeposits() public { - pool.pause(); - - vm.prank(user1); - vm.expectRevert(DepositPoolV2.ContractPaused.selector); - pool.deposit{value: 100 ether}(); - } - - // ========================================================================= - // FUZZ TESTS - // ========================================================================= - - function testFuzz_DepositAndWithdraw(uint256 amount) public { - amount = bound(amount, 100 ether, 10000 ether); - - vm.deal(user1, amount * 2); - - vm.prank(user1); - pool.deposit{value: amount}(); - - assertEq(token.balanceOf(user1), amount); - - // Request withdrawal first, then earmark liquid QRL. - uint256 shares = token.sharesOf(user1); - vm.prank(user1); - pool.requestWithdrawal(shares); - - pool.fundWithdrawalReserve(amount); - - vm.roll(block.number + 129); - - uint256 balanceBefore = user1.balance; - vm.prank(user1); - pool.claimWithdrawal(); - - // Should get back approximately the same amount (minus any rounding) - assertApproxEqRel(user1.balance - balanceBefore, amount, 1e15); - } - - // ========================================================================= - // DEPOSIT ERROR TESTS - // ========================================================================= - - function test_Deposit_StQRLNotSet_Reverts() public { - // Deploy fresh pool without stQRL set - DepositPoolV2 freshPool = new DepositPoolV2(); - - vm.prank(user1); - vm.expectRevert(DepositPoolV2.StQRLNotSet.selector); - freshPool.deposit{value: 1 ether}(); - } - - function test_Deposit_ZeroAmount_Reverts() public { - vm.prank(user1); - vm.expectRevert(DepositPoolV2.BelowMinDeposit.selector); - pool.deposit{value: 0}(); - } - - function test_Deposit_EmitsEvent() public { - vm.prank(user1); - vm.expectEmit(true, false, false, true); - emit Deposited(user1, 100 ether, 100 ether); - pool.deposit{value: 100 ether}(); - } - - // ========================================================================= - // WITHDRAWAL ERROR TESTS - // ========================================================================= - - function test_RequestWithdrawal_ZeroShares_Reverts() public { - vm.prank(user1); - pool.deposit{value: 100 ether}(); - - vm.prank(user1); - vm.expectRevert(DepositPoolV2.ZeroAmount.selector); - pool.requestWithdrawal(0); - } - - function test_RequestWithdrawal_InsufficientShares_Reverts() public { - vm.prank(user1); - pool.deposit{value: 100 ether}(); - - vm.prank(user1); - vm.expectRevert(DepositPoolV2.InsufficientShares.selector); - pool.requestWithdrawal(150 ether); - } - - function test_MultipleWithdrawalRequests() public { - // Multiple withdrawal requests are now allowed - vm.prank(user1); - pool.deposit{value: 100 ether}(); - - vm.prank(user1); - (uint256 requestId1,) = pool.requestWithdrawal(50 ether); - - vm.prank(user1); - (uint256 requestId2,) = pool.requestWithdrawal(25 ether); - - assertEq(requestId1, 0); - assertEq(requestId2, 1); - assertEq(pool.totalWithdrawalShares(), 75 ether); - - // Verify both requests exist - (uint256 total, uint256 pending) = pool.getWithdrawalRequestCount(user1); - assertEq(total, 2); - assertEq(pending, 2); - } - - function test_RequestWithdrawal_WhenPaused_Reverts() public { - vm.prank(user1); - pool.deposit{value: 100 ether}(); - - pool.pause(); - - vm.prank(user1); - vm.expectRevert(DepositPoolV2.ContractPaused.selector); - pool.requestWithdrawal(50 ether); - } - - function test_RequestWithdrawal_EmitsEvent() public { - vm.prank(user1); - pool.deposit{value: 100 ether}(); - - vm.prank(user1); - vm.expectEmit(true, false, false, true); - emit WithdrawalRequested(user1, 50 ether, 50 ether, block.number); - pool.requestWithdrawal(50 ether); - } - - function test_ClaimWithdrawal_NoRequest_Reverts() public { - vm.prank(user1); - vm.expectRevert(DepositPoolV2.NoWithdrawalPending.selector); - pool.claimWithdrawal(); - } - - function test_ClaimWithdrawal_EmitsEvent() public { - vm.prank(user1); - pool.deposit{value: 100 ether}(); - - // Request first, then fund reserve. - vm.prank(user1); - pool.requestWithdrawal(50 ether); - - pool.fundWithdrawalReserve(50 ether); - - vm.roll(block.number + 129); - - vm.prank(user1); - vm.expectEmit(true, false, false, true); - emit WithdrawalClaimed(user1, 50 ether, 50 ether); - pool.claimWithdrawal(); - } - - function test_CancelWithdrawal_NoRequest_Reverts() public { - vm.prank(user1); - vm.expectRevert(DepositPoolV2.InvalidWithdrawalIndex.selector); - pool.cancelWithdrawal(0); - } - - // ========================================================================= - // VALIDATOR FUNDING ERROR TESTS - // ========================================================================= - - function test_FundValidatorMVP_InsufficientBuffer_Reverts() public { - // Deposit less than validator stake - vm.deal(user1, 5000 ether); - vm.prank(user1); - pool.deposit{value: 5000 ether}(); - - vm.expectRevert(DepositPoolV2.InsufficientBuffer.selector); - pool.fundValidatorMVP(); - } - - function test_FundValidatorMVP_EmitsEvent() public { - vm.deal(user1, 40000 ether); - vm.prank(user1); - pool.deposit{value: 40000 ether}(); - - vm.expectEmit(true, false, false, true); - emit ValidatorFunded(0, "", 40000 ether); - pool.fundValidatorMVP(); - } - - // ========================================================================= - // ADMIN FUNCTION TESTS - // ========================================================================= - - function test_SetStQRL() public { - DepositPoolV2 freshPool = new DepositPoolV2(); - address newStQRL = address(0x123); - - freshPool.setStQRL(newStQRL); - - assertEq(address(freshPool.stQRL()), newStQRL); - } - - function test_SetStQRL_ZeroAddress_Reverts() public { - DepositPoolV2 freshPool = new DepositPoolV2(); - - vm.expectRevert(DepositPoolV2.ZeroAddress.selector); - freshPool.setStQRL(address(0)); - } - - function test_SetStQRL_NotOwner_Reverts() public { - vm.prank(user1); - vm.expectRevert(DepositPoolV2.NotOwner.selector); - pool.setStQRL(address(0x123)); - } - - function test_SetStQRL_AlreadySet_Reverts() public { - // stQRL is already set in setUp() - vm.expectRevert(DepositPoolV2.StQRLAlreadySet.selector); - pool.setStQRL(address(0x123)); - } - - function test_SetMinDeposit() public { - pool.setMinDeposit(200 ether); - assertEq(pool.minDeposit(), 200 ether); - - // Cannot set below the current floor (100 ether by default) - vm.expectRevert(DepositPoolV2.BelowMinDepositFloor.selector); - pool.setMinDeposit(50 ether); - } - - function test_SetMinDeposit_NotOwner_Reverts() public { - vm.prank(user1); - vm.expectRevert(DepositPoolV2.NotOwner.selector); - pool.setMinDeposit(200 ether); - } - - function test_SetMinDeposit_EmitsEvent() public { - vm.expectEmit(false, false, false, true); - emit MinDepositUpdated(200 ether); - pool.setMinDeposit(200 ether); - } - - function test_Unpause() public { - pool.pause(); - assertTrue(pool.paused()); - - pool.unpause(); - assertFalse(pool.paused()); - } - - function test_Unpause_NotOwner_Reverts() public { - pool.pause(); - - vm.prank(user1); - vm.expectRevert(DepositPoolV2.NotOwner.selector); - pool.unpause(); - } - - function test_TransferOwnership() public { - address newOwner = address(0x999); - - pool.transferOwnership(newOwner); - - assertEq(pool.owner(), newOwner); - } - - function test_TransferOwnership_ZeroAddress_Reverts() public { - vm.expectRevert(DepositPoolV2.ZeroAddress.selector); - pool.transferOwnership(address(0)); - } - - function test_TransferOwnership_NotOwner_Reverts() public { - vm.prank(user1); - vm.expectRevert(DepositPoolV2.NotOwner.selector); - pool.transferOwnership(user1); - } - - function test_TransferOwnership_EmitsEvent() public { - address newOwner = address(0x999); - - vm.expectEmit(true, true, false, false); - emit OwnershipTransferred(owner, newOwner); - pool.transferOwnership(newOwner); - } - - function test_EmergencyWithdraw_UnsyncedRewardsBecomePooled() public { - vm.prank(user1); - pool.deposit{value: 100 ether}(); - - // Native inflows are indistinguishable from validator rewards. - vm.deal(address(pool), 110 ether); - - address recipient = address(0x999); - vm.expectRevert(DepositPoolV2.AccountingNotSettled.selector); - pool.emergencyWithdraw(recipient, 10 ether); - - pool.syncRewards(); - assertEq(pool.totalRewardsReceived(), 10 ether); - vm.expectRevert(DepositPoolV2.ExceedsRecoverableAmount.selector); - pool.emergencyWithdraw(recipient, 10 ether); - assertEq(recipient.balance, 0); - } - - function test_EmergencyWithdraw_ExceedsRecoverable_Reverts() public { - vm.prank(user1); - pool.deposit{value: 100 ether}(); - - // No excess funds - balance equals pooled QRL - // Try to withdraw pool funds - vm.expectRevert(DepositPoolV2.ExceedsRecoverableAmount.selector); - pool.emergencyWithdraw(address(0x999), 10 ether); - } - - function test_EmergencyWithdraw_ZeroAddress_Reverts() public { - vm.prank(user1); - pool.deposit{value: 100 ether}(); - - // Add excess funds - vm.deal(address(pool), 110 ether); - - vm.expectRevert(DepositPoolV2.ZeroAddress.selector); - pool.emergencyWithdraw(address(0), 10 ether); - } - - function test_EmergencyWithdraw_ZeroAmount_Reverts() public { - vm.prank(user1); - pool.deposit{value: 100 ether}(); - - vm.expectRevert(DepositPoolV2.ZeroAmount.selector); - pool.emergencyWithdraw(address(0x999), 0); - } - - function test_EmergencyWithdraw_NotOwner_Reverts() public { - vm.prank(user1); - pool.deposit{value: 100 ether}(); - - // Add excess funds - vm.deal(address(pool), 110 ether); - - vm.prank(user1); - vm.expectRevert(DepositPoolV2.NotOwner.selector); - pool.emergencyWithdraw(user1, 10 ether); - } - - // ========================================================================= - // VIEW FUNCTION TESTS - // ========================================================================= - - function test_PreviewDeposit() public view { - // Before any deposits, 1:1 ratio - uint256 shares = pool.previewDeposit(100 ether); - assertEq(shares, 100 ether); - } - - function test_PreviewDeposit_AfterRewards() public { - vm.prank(user1); - pool.deposit{value: 100 ether}(); - - // Add 50% rewards - vm.deal(address(pool), 150 ether); - pool.syncRewards(); - - // 100 QRL should now get fewer shares - uint256 shares = pool.previewDeposit(100 ether); - // At 1.5 QRL/share rate, 100 QRL = 66.67 shares - assertApproxEqRel(shares, 66.67 ether, 1e16); - } - - function test_PreviewDeposit_StQRLNotSet() public { - DepositPoolV2 freshPool = new DepositPoolV2(); - - // Should return 1:1 if stQRL not set - uint256 shares = freshPool.previewDeposit(100 ether); - assertEq(shares, 100 ether); - } - - // ========================================================================= - // RECEIVE FUNCTION TESTS - // ========================================================================= - - function test_Receive_IsNoOp() public { - // receive() is a no-op — incoming ETH does NOT auto-add to withdrawalReserve. - // _syncRewards() will later detect it as a balance increase (rewards). - uint256 reserveBefore = pool.withdrawalReserve(); - - // Send ETH directly to contract - (bool success,) = address(pool).call{value: 50 ether}(""); - assertTrue(success); - - // withdrawalReserve unchanged (receive is no-op) - assertEq(pool.withdrawalReserve(), reserveBefore); - - // syncRewards picks it up as rewards - pool.syncRewards(); - assertEq(pool.totalRewardsReceived(), 50 ether); - } - - function test_Receive_DetectedAsRewardsBySyncRewards() public { - // Deposit first so there's an existing totalPooledQRL baseline - vm.prank(user1); - pool.deposit{value: 100 ether}(); - - // Send ETH directly — receive() is a no-op, no event emitted - (bool success,) = address(pool).call{value: 50 ether}(""); - assertTrue(success); - - // syncRewards detects the 50 ether increase as rewards - vm.expectEmit(true, true, true, true); - emit RewardsSynced(50 ether, 150 ether, block.number); - pool.syncRewards(); - } - - function test_FundWithdrawalReserve() public { - // Need liquid pooled QRL to earmark. - vm.prank(user1); - pool.deposit{value: 100 ether}(); - - uint256 reserveBefore = pool.withdrawalReserve(); - uint256 pooledBefore = token.totalPooledQRL(); - - pool.fundWithdrawalReserve(50 ether); - - assertEq(pool.withdrawalReserve(), reserveBefore + 50 ether); - assertEq(token.totalPooledQRL(), pooledBefore); - assertEq(pool.bufferedQRL(), 50 ether); - } - - function test_FundWithdrawalReserve_EmitsEvent() public { - // Need liquid pooled QRL to earmark. - vm.prank(user1); - pool.deposit{value: 100 ether}(); - - vm.expectEmit(false, false, false, true); - emit WithdrawalReserveFunded(50 ether); - pool.fundWithdrawalReserve(50 ether); - } - - // ========================================================================= - // MULTI-USER SCENARIOS - // ========================================================================= - - function test_MultipleUsersWithdrawalQueue() public { - // User1 and User2 both deposit - vm.prank(user1); - pool.deposit{value: 100 ether}(); - - vm.prank(user2); - pool.deposit{value: 100 ether}(); - - // Verify initial state - assertEq(token.totalPooledQRL(), 200 ether); - assertEq(token.totalShares(), 200 ether); - - // Both users request withdrawals at the current 1:1 estimate. - vm.prank(user1); - pool.requestWithdrawal(50 ether); - - vm.prank(user2); - pool.requestWithdrawal(50 ether); - - assertEq(pool.totalWithdrawalShares(), 100 ether); - - // Earmark enough liquid QRL for both claims. - pool.fundWithdrawalReserve(100 ether); - - // Reserved assets and queued shares both remain in conversion totals. - assertEq(token.totalPooledQRL(), 200 ether); - assertEq(pool.withdrawalReserve(), 100 ether); - - // Wait for delay - vm.roll(block.number + 129); - - // User1 claims - should receive exactly 50 ether - uint256 user1BalanceBefore = user1.balance; - vm.prank(user1); - uint256 user1Claimed = pool.claimWithdrawal(); - assertEq(user1Claimed, 50 ether); - assertEq(user1.balance - user1BalanceBefore, 50 ether); - - // User2 claims - should also receive exactly 50 ether - uint256 user2BalanceBefore = user2.balance; - vm.prank(user2); - uint256 user2Claimed = pool.claimWithdrawal(); - assertEq(user2Claimed, 50 ether); - assertEq(user2.balance - user2BalanceBefore, 50 ether); - - // Queue should be empty - assertEq(pool.totalWithdrawalShares(), 0); - } - - function test_RewardsDistributedProportionally() public { - // User1 deposits 100 QRL - vm.prank(user1); - pool.deposit{value: 100 ether}(); - - // User2 deposits 200 QRL - vm.prank(user2); - pool.deposit{value: 200 ether}(); - - // Add 30 QRL rewards (10% of 300) - vm.deal(address(pool), 330 ether); - pool.syncRewards(); - - // User1 has 100/300 = 33.33% of shares -> 33.33% of 330 = 110 QRL (approx) - assertApproxEqRel(token.getQRLValue(user1), 110 ether, 1e14); - - // User2 has 200/300 = 66.67% of shares -> 66.67% of 330 = 220 QRL (approx) - assertApproxEqRel(token.getQRLValue(user2), 220 ether, 1e14); - } - - // ========================================================================= - // MIN DEPOSIT FLOOR TESTS - // ========================================================================= - - function test_SetMinDepositFloor() public { - // Default floor is 100 ether - assertEq(pool.minDepositFloor(), 100 ether); - - // Owner can lower the floor - pool.setMinDepositFloor(1 ether); - assertEq(pool.minDepositFloor(), 1 ether); - - // Owner can raise it back - pool.setMinDepositFloor(50 ether); - assertEq(pool.minDepositFloor(), 50 ether); - } - - function test_SetMinDepositFloor_BelowAbsoluteMin_Reverts() public { - // Cannot set floor below ABSOLUTE_MIN_DEPOSIT (0.001 ether) - vm.expectRevert(DepositPoolV2.BelowAbsoluteMin.selector); - pool.setMinDepositFloor(0.0001 ether); - - // Zero also reverts - vm.expectRevert(DepositPoolV2.BelowAbsoluteMin.selector); - pool.setMinDepositFloor(0); - } - - function test_SetMinDepositFloor_NotOwner_Reverts() public { - vm.prank(user1); - vm.expectRevert(DepositPoolV2.NotOwner.selector); - pool.setMinDepositFloor(1 ether); - } - - function test_SetMinDepositFloor_EmitsEvent() public { - vm.expectEmit(false, false, false, true); - emit MinDepositFloorUpdated(1 ether); - pool.setMinDepositFloor(1 ether); - } - - function test_SetMinDeposit_AfterFloorLowered() public { - // Lower the floor first - pool.setMinDepositFloor(1 ether); - assertEq(pool.minDepositFloor(), 1 ether); - - // Now we can lower minDeposit below the old 100 ether floor - pool.setMinDeposit(5 ether); - assertEq(pool.minDeposit(), 5 ether); - - // Deposits at the new lower minimum work - vm.deal(user1, 10 ether); - vm.prank(user1); - uint256 shares = pool.deposit{value: 5 ether}(); - assertEq(shares, 5 ether); - - // Still cannot go below the new floor - vm.expectRevert(DepositPoolV2.BelowMinDepositFloor.selector); - pool.setMinDeposit(0.5 ether); - } - - // ========================================================================= - // fundValidator() — REAL BEACON PATH - // ========================================================================= - // These lock in the QRL-specific bits of the beacon deposit call that - // differ from Ethereum muscle memory: the 0x00 withdrawal-prefix byte - // (ExecutionAddressWithdrawalPrefixByte in qrysm), the 2592-byte ML-DSA - // pubkey, and the 4627-byte ML-DSA-87 signature. - - address constant DEPOSIT_CONTRACT_ADDR = 0x4242424242424242424242424242424242424242; - - function _stubDepositContract() internal { - // Minimal bytecode at 0x4242...: STOP (0x00) — accepts any call, reverts - // none, returns nothing. Enough to satisfy IDepositContract.deposit() - // as long as we don't care about the receipt/event side-effects. - vm.etch(DEPOSIT_CONTRACT_ADDR, hex"00"); - } - - function _fundBufferForValidator() internal { - vm.deal(user1, 40000 ether); - vm.prank(user1); - pool.deposit{value: 40000 ether}(); - } - - function _validCredentials() internal view returns (bytes memory) { - // 0x00 prefix + 11 zero bytes + 20-byte pool address = 32 bytes total. - return abi.encodePacked(bytes1(0x00), bytes11(0), address(pool)); - } - - function _validPubkey() internal pure returns (bytes memory) { - bytes memory pk = new bytes(2592); - pk[0] = 0xab; // arbitrary non-zero so it's distinguishable - return pk; - } - - function _validSignature() internal pure returns (bytes memory) { - bytes memory sig = new bytes(4627); - sig[0] = 0xcd; - return sig; - } - - function test_FundValidator_AcceptsZeroPrefix() public { - _stubDepositContract(); - _fundBufferForValidator(); - - uint256 validatorId = - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - - assertEq(validatorId, 0); - assertEq(pool.validatorCount(), 1); - assertEq(pool.bufferedQRL(), 0); - } - - function test_FundValidator_RejectsEthereumOnePrefix() public { - // Regression guard: Ethereum's 0x01 prefix (BLS_WITHDRAWAL_PREFIX +1) - // must NOT validate. Before the 0x00 fix, this test would have passed - // against the buggy contract. - _stubDepositContract(); - _fundBufferForValidator(); - - bytes memory badCreds = abi.encodePacked(bytes1(0x01), bytes11(0), address(pool)); - - vm.expectRevert(DepositPoolV2.InvalidWithdrawalCredentials.selector); - pool.fundValidator(_validPubkey(), badCreds, _validSignature(), bytes32(uint256(1))); - } - - function test_FundValidator_RejectsWrongContractAddress() public { - _stubDepositContract(); - _fundBufferForValidator(); - - // Correct 0x00 prefix + 11 zero bytes, but a different 20-byte address. - // A user-controlled address here would redirect withdrawals away from - // the pool — the guard must catch it. - bytes memory badCreds = abi.encodePacked(bytes1(0x00), bytes11(0), address(0xdead)); - - vm.expectRevert(DepositPoolV2.InvalidWithdrawalCredentials.selector); - pool.fundValidator(_validPubkey(), badCreds, _validSignature(), bytes32(uint256(1))); - } - - function test_FundValidator_RejectsShortCredentials() public { - _stubDepositContract(); - _fundBufferForValidator(); - - bytes memory shortCreds = abi.encodePacked(bytes1(0x00), bytes11(0)); // only 12 bytes - - vm.expectRevert(DepositPoolV2.InvalidCredentialsLength.selector); - pool.fundValidator(_validPubkey(), shortCreds, _validSignature(), bytes32(uint256(1))); - } - - function test_FundValidator_RejectsWrongPubkeyLength() public { - _stubDepositContract(); - _fundBufferForValidator(); - - bytes memory shortPubkey = new bytes(2591); - - vm.expectRevert(DepositPoolV2.InvalidPubkeyLength.selector); - pool.fundValidator(shortPubkey, _validCredentials(), _validSignature(), bytes32(uint256(1))); - } - - function test_FundValidator_RejectsWrongSignatureLength() public { - _stubDepositContract(); - _fundBufferForValidator(); - - bytes memory shortSig = new bytes(4626); - - vm.expectRevert(DepositPoolV2.InvalidSignatureLength.selector); - pool.fundValidator(_validPubkey(), _validCredentials(), shortSig, bytes32(uint256(1))); - } - - function test_FundValidator_RequiresFullBuffer() public { - _stubDepositContract(); - vm.deal(user1, 39999 ether); - vm.prank(user1); - pool.deposit{value: 39999 ether}(); - - vm.expectRevert(DepositPoolV2.InsufficientBuffer.selector); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - } - - function test_FundValidator_OnlyOwner() public { - _stubDepositContract(); - _fundBufferForValidator(); - - vm.prank(user1); - vm.expectRevert(DepositPoolV2.NotOwner.selector); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - } - - function test_FundValidator_ForwardsStakeToDepositContract() public { - _stubDepositContract(); - _fundBufferForValidator(); - - uint256 beaconBalanceBefore = DEPOSIT_CONTRACT_ADDR.balance; - uint256 poolBalanceBefore = address(pool).balance; - - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - - assertEq(DEPOSIT_CONTRACT_ADDR.balance - beaconBalanceBefore, 40000 ether); - assertEq(poolBalanceBefore - address(pool).balance, 40000 ether); - } - - // ========================================================================= - // OFF-CONTRACT STAKE ACCOUNTING (stakedQRL) - // ========================================================================= - // fundValidator() forwards 40k QRL to the beacon deposit contract, so the - // pool's own balance drops by the stake. _syncRewards() must add stakedQRL - // back when reconciling, otherwise the funding tx reads as a slashing event - // and the exchange rate collapses. These tests lock that in. - - function test_FundValidator_TracksStakedQRL() public { - _stubDepositContract(); - _fundBufferForValidator(); - - assertEq(pool.stakedQRL(), 0); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - - // Principal left the contract but is still pooled. - assertEq(pool.stakedQRL(), 40000 ether); - assertEq(address(pool).balance, 0); - assertEq(token.totalPooledQRL(), 40000 ether); - } - - function test_SyncAfterFundValidator_NoPhantomSlashing() public { - _stubDepositContract(); - _fundBufferForValidator(); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - - // Contract balance is now 0, stakedQRL is 40k. A naive balance-only - // sync would report a 40k slashing loss here. - uint256 pooledBefore = token.totalPooledQRL(); - pool.syncRewards(); - - assertEq(token.totalPooledQRL(), pooledBefore, "pooled must not change"); - assertEq(pool.totalSlashingLosses(), 0, "no phantom slashing"); - assertEq(pool.totalRewardsReceived(), 0, "no phantom rewards"); - // Exchange rate stays ~1:1 (depositor's value preserved). - assertApproxEqRel(token.getQRLValue(user1), 40000 ether, 1e14); - } - - function test_RewardsArriveWhileStaked_AttributedAsRewards() public { - _stubDepositContract(); - _fundBufferForValidator(); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - - // 50 QRL of beacon rewards sweep in via EIP-4895 (modeled as a balance - // bump on top of the off-contract stake). - vm.deal(address(pool), 50 ether); - - vm.expectEmit(true, true, true, true); - emit RewardsSynced(50 ether, 40050 ether, block.number); - pool.syncRewards(); - - assertEq(token.totalPooledQRL(), 40050 ether); - assertEq(pool.totalRewardsReceived(), 50 ether); - assertEq(pool.stakedQRL(), 40000 ether, "stake unchanged by reward sweep"); - } - - function test_RecordValidatorExit_SettlesReturnedPrincipal() public { - _stubDepositContract(); - _fundBufferForValidator(); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - - // Validator exits: 40k principal returns to the contract balance. - vm.deal(address(pool), 40000 ether); - - // Owner reconciles: principal moves from off-contract stakedQRL back - // into the on-contract balance. totalPooledQRL stays put. - vm.expectEmit(true, true, true, true); - emit ValidatorExitRecorded(40000 ether, 0); - pool.recordValidatorExit(40000 ether); - - assertEq(pool.stakedQRL(), 0); - - // Without the recordValidatorExit() above, this sync would have double - // counted the returned principal as 40k of rewards. - pool.syncRewards(); - assertEq(token.totalPooledQRL(), 40000 ether); - assertEq(pool.totalRewardsReceived(), 0, "principal must not count as rewards"); - assertEq(pool.bufferedQRL(), 40000 ether, "returned principal must refill buffer"); - } - - function test_RecordValidatorExit_RemainderCanFundNextValidator() public { - _stubDepositContract(); - _fundBufferForValidator(); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - - vm.prank(user1); - pool.requestWithdrawal(5000 ether); - - vm.deal(address(pool), 40000 ether); - pool.recordValidatorExit(40000 ether); - pool.fundWithdrawalReserve(5000 ether); - - assertEq(pool.bufferedQRL(), 35000 ether); - assertEq(pool.bufferedQRLInReserve(), 5000 ether); - - vm.roll(block.number + pool.WITHDRAWAL_DELAY()); - vm.prank(user1); - assertEq(pool.claimWithdrawal(), 5000 ether); - - vm.deal(user2, 5000 ether); - vm.prank(user2); - pool.deposit{value: 5000 ether}(); - - assertEq(pool.bufferedQRL(), 40000 ether); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(2))); - assertEq(pool.stakedQRL(), 40000 ether); - assertEq(address(pool).balance, 0); - } - - function testFuzz_RecordValidatorExit_BuffersOnlyObservedPrincipal(uint256 returnedAmount) public { - returnedAmount = bound(returnedAmount, 0, 80000 ether); - - _stubDepositContract(); - _fundBufferForValidator(); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - - vm.deal(address(pool), returnedAmount); - pool.recordValidatorExit(40000 ether); - - uint256 expectedBuffer = returnedAmount < 40000 ether ? returnedAmount : 40000 ether; - assertEq(pool.bufferedQRL(), expectedBuffer); - assertLe(pool.bufferedQRL(), address(pool).balance); - - pool.syncRewards(); - assertEq(token.totalPooledQRL(), returnedAmount); - } - - function test_RecordValidatorExit_OnlyOwner() public { - _stubDepositContract(); - _fundBufferForValidator(); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - - vm.prank(user1); - vm.expectRevert(DepositPoolV2.NotOwner.selector); - pool.recordValidatorExit(40000 ether); - } - - function test_RecordValidatorExit_RejectsZero() public { - vm.expectRevert(DepositPoolV2.ZeroAmount.selector); - pool.recordValidatorExit(0); - } - - function test_RecordValidatorExit_RejectsAboveStaked() public { - _stubDepositContract(); - _fundBufferForValidator(); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - - vm.expectRevert(DepositPoolV2.ExceedsStakedAmount.selector); - pool.recordValidatorExit(40000 ether + 1); - } - - function test_EmergencyWithdraw_BlocksUnsyncedRewardsWhileStaked() public { - _stubDepositContract(); - _fundBufferForValidator(); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - - // A 5 QRL balance delta may be a validator reward, so emergency - // recovery must wait for owner accounting and cannot extract it. - vm.deal(address(pool), 5 ether); - - address recipient = address(0xBEEF); - vm.expectRevert(DepositPoolV2.AccountingNotSettled.selector); - pool.emergencyWithdraw(recipient, 5 ether); - - pool.syncRewards(); - vm.expectRevert(DepositPoolV2.ExceedsRecoverableAmount.selector); - pool.emergencyWithdraw(recipient, 5 ether); - assertEq(address(pool).balance, 5 ether); - assertEq(recipient.balance, 0); - } - - // ========================================================================= - // PHANTOM-REWARD FRONT-RUN PROTECTION (validator exits) - // ========================================================================= - // While principal is staked off-contract (stakedQRL > 0), an exit sweep - // returns principal to the balance a block before the owner can settle it - // with recordValidatorExit(). A permissionless sync in that window would - // book the principal as a phantom reward and spike the rate. Permissionless - // reward sync and claims with unsettled deltas are disabled whenever - // stakedQRL > 0. With stakedQRL == 0, sync stays fully permissionless. - - function test_SyncRewards_PermissionlessWhenNoStake() public { - // No off-contract principal: anyone may sync (trustless path intact). - vm.prank(user1); - pool.deposit{value: 100 ether}(); - vm.deal(address(pool), 110 ether); // 10 QRL of rewards arrive - - vm.prank(user2); - pool.syncRewards(); - - assertEq(token.totalPooledQRL(), 110 ether); - assertEq(pool.totalRewardsReceived(), 10 ether); - } - - function test_SyncRewards_OwnerOnlyWhileStaked() public { - _stubDepositContract(); - _fundBufferForValidator(); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - - // Principal is now off-contract — permissionless sync is blocked. - vm.prank(user2); - vm.expectRevert(DepositPoolV2.NotOwner.selector); - pool.syncRewards(); - - // The owner can still sync (no revert). - pool.syncRewards(); - } - - function test_PhantomReward_FrontRunBlockedDuringExit() public { - _stubDepositContract(); - vm.deal(user1, 80000 ether); - vm.prank(user1); - pool.deposit{value: 80000 ether}(); - - vm.prank(user1); - (, uint256 estimate) = pool.requestWithdrawal(40000 ether); - assertEq(estimate, 40000 ether); - pool.fundWithdrawalReserve(40000 ether); - - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - vm.roll(block.number + pool.WITHDRAWAL_DELAY()); - - // Exit principal lands before owner settlement: balance = 80k and - // stakedQRL = 40k, which would incorrectly read as 120k pooled. - vm.deal(address(pool), 80000 ether); - - vm.prank(user2); - vm.expectRevert(DepositPoolV2.NotOwner.selector); - pool.syncRewards(); - - vm.prank(user1); - vm.expectRevert(DepositPoolV2.AccountingNotSettled.selector); - pool.claimWithdrawal(); - - pool.recordValidatorExit(40000 ether); - vm.prank(user1); - assertEq(pool.claimWithdrawal(), 40000 ether); - assertEq(pool.totalRewardsReceived(), 0, "no phantom reward recognized"); - assertEq(token.totalPooledQRL(), 40000 ether, "remaining rate unchanged"); - } - - function test_ExitSettlement_ThenPermissionlessSyncResumes() public { - _stubDepositContract(); - _fundBufferForValidator(); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - vm.deal(address(pool), 40000 ether); // principal returned - - // Owner settles — cannot be front-run (see test above) — clearing stake. - pool.recordValidatorExit(40000 ether); - assertEq(pool.stakedQRL(), 0); - - // Permissionless sync works again and books no phantom reward. - vm.prank(user2); - pool.syncRewards(); - assertEq(token.totalPooledQRL(), 40000 ether); - assertEq(pool.totalRewardsReceived(), 0, "principal must not count as rewards"); - } - - function test_OwnerSync_RecognizesGenuineRewardsWhileStaked() public { - _stubDepositContract(); - _fundBufferForValidator(); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - - // 50 QRL of genuine beacon rewards sweep in on top of the stake. - vm.deal(address(pool), 50 ether); - - // Still blocked for the public... - vm.prank(user2); - vm.expectRevert(DepositPoolV2.NotOwner.selector); - pool.syncRewards(); - - // ...but the owner recognizes the real rewards under controlled timing. - pool.syncRewards(); - assertEq(token.totalPooledQRL(), 40050 ether); - assertEq(pool.totalRewardsReceived(), 50 ether); - } - - // ========================================================================= - // ASSET ACCOUNTING REGRESSIONS - // ========================================================================= - - function test_ReserveFundingDoesNotDiluteSubsequentDepositor() public { - vm.deal(user1, 1000 ether); - vm.prank(user1); - pool.deposit{value: 1000 ether}(); - - vm.prank(user1); - (, uint256 estimate) = pool.requestWithdrawal(100 ether); - pool.fundWithdrawalReserve(estimate); - - // Reserve funding keeps both the assets and queued shares in the rate. - assertEq(token.totalPooledQRL(), 1000 ether); - assertEq(token.totalShares(), 1000 ether); - - vm.prank(user2); - uint256 user2Shares = pool.deposit{value: 100 ether}(); - assertEq(user2Shares, 100 ether); - - vm.roll(block.number + pool.WITHDRAWAL_DELAY()); - vm.prank(user1); - assertEq(pool.claimWithdrawal(), 100 ether); - - assertEq(token.getQRLValue(user2), 100 ether); - assertEq(token.getQRLValue(user1), 900 ether); - } - - function test_DepositAccountsPendingRewardsBeforeMinting() public { - vm.deal(user1, 1000 ether); - vm.prank(user1); - pool.deposit{value: 1000 ether}(); - - // Model a reward sweep that changes balance without invoking pool code. - vm.deal(address(pool), 1100 ether); - - vm.prank(user2); - uint256 user2Shares = pool.deposit{value: 100 ether}(); - - assertEq(pool.totalRewardsReceived(), 100 ether); - assertEq(token.totalPooledQRL(), 1200 ether); - assertLt(user2Shares, 100 ether); - assertApproxEqAbs(token.getQRLValue(user2), 100 ether, 2); - } - - function test_DepositWhileStakedRequiresBalanceSettlement() public { - _stubDepositContract(); - _fundBufferForValidator(); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - - vm.deal(address(pool), 100 ether); - vm.prank(user2); - vm.expectRevert(DepositPoolV2.AccountingNotSettled.selector); - pool.deposit{value: 100 ether}(); - - pool.syncRewards(); - vm.prank(user2); - uint256 user2Shares = pool.deposit{value: 100 ether}(); - assertLt(user2Shares, 100 ether); - assertApproxEqAbs(token.getQRLValue(user2), 100 ether, 2); - } - - function test_WithdrawalReserveCannotBeFundedIntoValidator() public { - _stubDepositContract(); - vm.deal(user1, 40000 ether); - vm.prank(user1); - uint256 shares = pool.deposit{value: 40000 ether}(); - - vm.prank(user1); - (, uint256 estimate) = pool.requestWithdrawal(shares); - pool.fundWithdrawalReserve(estimate); - - assertEq(pool.withdrawalReserve(), 40000 ether); - assertEq(pool.bufferedQRL(), 0); - (bool possible,) = pool.canFundValidator(); - assertFalse(possible); - - vm.expectRevert(DepositPoolV2.InsufficientBuffer.selector); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - - vm.expectRevert(DepositPoolV2.InsufficientBuffer.selector); - pool.fundValidatorMVP(); - - vm.roll(block.number + pool.WITHDRAWAL_DELAY()); - vm.prank(user1); - assertEq(pool.claimWithdrawal(), 40000 ether); - } - - function test_RepeatedReserveFundingProtectsReserveDuringValidatorFunding() public { - _stubDepositContract(); - vm.deal(user1, 80000 ether); - vm.prank(user1); - pool.deposit{value: 80000 ether}(); - - vm.prank(user1); - pool.requestWithdrawal(40000 ether); - pool.fundWithdrawalReserve(20000 ether); - pool.fundWithdrawalReserve(20000 ether); - - assertEq(pool.withdrawalReserve(), 40000 ether); - assertEq(pool.bufferedQRL(), 40000 ether); - (bool possible,) = pool.canFundValidator(); - assertTrue(possible); - - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - - assertEq(address(pool).balance, 40000 ether); - assertEq(pool.withdrawalReserve(), 40000 ether); - assertEq(pool.bufferedQRL(), 0); - } - - function test_ReleaseReserveAfterCancellationRestoresValidatorLiquidity() public { - _stubDepositContract(); - vm.deal(user1, 40000 ether); - vm.prank(user1); - uint256 shares = pool.deposit{value: 40000 ether}(); - - vm.prank(user1); - (uint256 requestId,) = pool.requestWithdrawal(shares); - pool.fundWithdrawalReserve(40000 ether); - - assertEq(pool.bufferedQRL(), 0); - assertEq(pool.bufferedQRLInReserve(), 40000 ether); - - vm.prank(user1); - pool.cancelWithdrawal(requestId); - pool.releaseWithdrawalReserve(40000 ether); - - assertEq(pool.withdrawalReserve(), 0); - assertEq(pool.bufferedQRLInReserve(), 0); - assertEq(pool.bufferedQRL(), 40000 ether); - (bool possible,) = pool.canFundValidator(); - assertTrue(possible); - - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - assertEq(address(pool).balance, 0); - } - - function test_ReleaseReserveDoesNotDoubleCountMvpStakeAsBuffer() public { - vm.deal(user1, 80000 ether); - vm.prank(user1); - pool.deposit{value: 80000 ether}(); - - pool.fundValidatorMVP(); - pool.fundWithdrawalReserve(60000 ether); - - // Forty thousand came from the simulated stake and only twenty - // thousand came from the remaining validator buffer. - assertEq(pool.bufferedQRL(), 20000 ether); - assertEq(pool.bufferedQRLInReserve(), 20000 ether); - - pool.releaseWithdrawalReserve(60000 ether); - assertEq(pool.withdrawalReserve(), 0); - assertEq(pool.bufferedQRL(), 40000 ether); - - pool.fundValidatorMVP(); - vm.expectRevert(DepositPoolV2.InsufficientBuffer.selector); - pool.fundValidatorMVP(); - } - - function test_QueuedWithdrawalBearsSlashingBeforeClaim() public { - _stubDepositContract(); - token.setMinStakeBlocks(1536); - vm.deal(user1, 10000 ether); - vm.deal(user2, 30000 ether); - - vm.prank(user1); - uint256 user1Shares = pool.deposit{value: 10000 ether}(); - vm.prank(user2); - pool.deposit{value: 30000 ether}(); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - - vm.roll(block.number + token.minStakeBlocks()); - - // Only 30,000 QRL returns from the 40,000 QRL validator. - vm.deal(address(pool), 30000 ether); - vm.prank(user1); - (, uint256 requestEstimate) = pool.requestWithdrawal(user1Shares); - assertEq(requestEstimate, 10000 ether); - - vm.roll(block.number + pool.WITHDRAWAL_DELAY()); - vm.prank(user1); - vm.expectRevert(DepositPoolV2.AccountingNotSettled.selector); - pool.claimWithdrawal(); - - pool.recordValidatorExit(40000 ether); - pool.syncRewards(); - uint256 settledPayout = token.getPooledQRLByShares(user1Shares); - assertApproxEqAbs(settledPayout, 7500 ether, 100); - assertLt(settledPayout, requestEstimate); - - pool.fundWithdrawalReserve(settledPayout); - uint256 balanceBefore = user1.balance; - vm.prank(user1); - uint256 claimed = pool.claimWithdrawal(); - - assertEq(claimed, settledPayout); - assertEq(user1.balance - balanceBefore, settledPayout); - assertApproxEqAbs(token.getQRLValue(user2), 22500 ether, 1000); - } - - // ========================================================================= - // MINIMUM STAKE LOCK TESTS - // ========================================================================= - - uint256 internal constant LOCK_BLOCKS = 1536; - - function test_MinStakeLock_RequestWithdrawal_ImmatureReverts() public { - token.setMinStakeBlocks(LOCK_BLOCKS); - - vm.prank(user1); - pool.deposit{value: 100 ether}(); - - // Fresh deposit cannot be queued for withdrawal - vm.prank(user1); - vm.expectRevert(DepositPoolV2.InsufficientShares.selector); - pool.requestWithdrawal(100 ether); - - // After maturity the same request goes through - vm.roll(block.number + LOCK_BLOCKS); - vm.prank(user1); - (uint256 requestId, uint256 qrlAmount) = pool.requestWithdrawal(100 ether); - assertEq(requestId, 0); - assertEq(qrlAmount, 100 ether); - } - - function test_MinStakeLock_OnlyMaturedSharesSpendable() public { - token.setMinStakeBlocks(LOCK_BLOCKS); - - // First deposit matures, top-up stays immature - vm.prank(user1); - pool.deposit{value: 100 ether}(); - vm.roll(block.number + LOCK_BLOCKS); - - vm.prank(user1); - pool.deposit{value: 100 ether}(); - - // The matured 100 can be requested, the immature 100 cannot - vm.prank(user1); - pool.requestWithdrawal(100 ether); - - vm.prank(user1); - vm.expectRevert(DepositPoolV2.InsufficientShares.selector); - pool.requestWithdrawal(1 ether); - } - - function test_MinStakeLock_OwnerBridgeExempt() public { - token.setMinStakeBlocks(LOCK_BLOCKS); - vm.deal(address(this), 200 ether); - - // Operator bridge capital: deposit and request back out immediately - pool.deposit{value: 100 ether}(); - assertEq(token.immatureSharesOf(address(this)), 0); - - (uint256 requestId, uint256 qrlAmount) = pool.requestWithdrawal(100 ether); - assertEq(requestId, 0); - assertEq(qrlAmount, 100 ether); - } - - // ========================================================================= - // EVENT DECLARATIONS - // ========================================================================= - - event MinDepositUpdated(uint256 newMinDeposit); - event MinDepositFloorUpdated(uint256 newFloor); - event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); - event ValidatorFunded(uint256 indexed validatorId, bytes pubkey, uint256 amount); - event ValidatorExitRecorded(uint256 amount, uint256 remainingStaked); - event WithdrawalReserveFunded(uint256 amount); -} diff --git a/contracts/test/ValidatorManager.t.sol b/contracts/test/ValidatorManager.t.sol deleted file mode 100644 index 264d181..0000000 --- a/contracts/test/ValidatorManager.t.sol +++ /dev/null @@ -1,720 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.24; - -import "forge-std/Test.sol"; -import "../solidity/ValidatorManager.sol"; - -/** - * @title ValidatorManager Tests - * @notice Unit tests for validator lifecycle management - */ -contract ValidatorManagerTest is Test { - ValidatorManager public manager; - address public owner; - address public depositPool; - address public operator; - address public randomUser; - - // Dilithium pubkey is 2592 bytes - uint256 constant PUBKEY_LENGTH = 2592; - uint256 constant VALIDATOR_STAKE = 40_000 ether; - - // Events to test - event ValidatorRegistered(uint256 indexed validatorId, bytes pubkey, ValidatorManager.ValidatorStatus status); - event ValidatorActivated(uint256 indexed validatorId, uint256 activatedBlock); - event ValidatorExitRequested(uint256 indexed validatorId, uint256 requestBlock); - event ValidatorExited(uint256 indexed validatorId, uint256 exitedBlock); - event ValidatorSlashed(uint256 indexed validatorId, uint256 slashedBlock); - event DepositPoolSet(address indexed depositPool); - event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); - - function setUp() public { - owner = address(this); - depositPool = address(0x1); - operator = address(0x2); - randomUser = address(0x3); - - manager = new ValidatorManager(); - manager.setDepositPool(depositPool); - } - - // ========================================================================= - // HELPERS - // ========================================================================= - - function _generatePubkey(uint256 seed) internal pure returns (bytes memory) { - bytes memory pubkey = new bytes(PUBKEY_LENGTH); - for (uint256 i = 0; i < PUBKEY_LENGTH; i++) { - pubkey[i] = bytes1(uint8(uint256(keccak256(abi.encodePacked(seed, i))) % 256)); - } - return pubkey; - } - - function _registerValidator(uint256 seed) internal returns (uint256 validatorId, bytes memory pubkey) { - pubkey = _generatePubkey(seed); - vm.prank(depositPool); - validatorId = manager.registerValidator(pubkey); - } - - function _registerAndActivate(uint256 seed) internal returns (uint256 validatorId, bytes memory pubkey) { - (validatorId, pubkey) = _registerValidator(seed); - manager.activateValidator(validatorId); - } - - // ========================================================================= - // INITIALIZATION TESTS - // ========================================================================= - - function test_InitialState() public view { - assertEq(manager.owner(), owner); - assertEq(manager.depositPool(), depositPool); - assertEq(manager.totalValidators(), 0); - assertEq(manager.activeValidatorCount(), 0); - assertEq(manager.pendingValidatorCount(), 0); - assertEq(manager.VALIDATOR_STAKE(), VALIDATOR_STAKE); - } - - function test_GetStats_Initial() public view { - (uint256 total, uint256 pending, uint256 active, uint256 totalStaked) = manager.getStats(); - assertEq(total, 0); - assertEq(pending, 0); - assertEq(active, 0); - assertEq(totalStaked, 0); - } - - // ========================================================================= - // VALIDATOR REGISTRATION TESTS - // ========================================================================= - - function test_RegisterValidator() public { - bytes memory pubkey = _generatePubkey(1); - - vm.prank(depositPool); - uint256 validatorId = manager.registerValidator(pubkey); - - assertEq(validatorId, 1); - assertEq(manager.totalValidators(), 1); - assertEq(manager.pendingValidatorCount(), 1); - assertEq(manager.activeValidatorCount(), 0); - - ( - bytes memory storedPubkey, - ValidatorManager.ValidatorStatus status, - uint256 activatedBlock, - uint256 exitedBlock - ) = manager.getValidator(validatorId); - - assertEq(storedPubkey, pubkey); - assertEq(uint256(status), uint256(ValidatorManager.ValidatorStatus.Pending)); - assertEq(activatedBlock, 0); - assertEq(exitedBlock, 0); - } - - function test_RegisterValidator_EmitsEvent() public { - bytes memory pubkey = _generatePubkey(1); - - vm.expectEmit(true, false, false, true); - emit ValidatorRegistered(1, pubkey, ValidatorManager.ValidatorStatus.Pending); - - vm.prank(depositPool); - manager.registerValidator(pubkey); - } - - function test_RegisterValidator_ByOwner() public { - bytes memory pubkey = _generatePubkey(1); - uint256 validatorId = manager.registerValidator(pubkey); - assertEq(validatorId, 1); - } - - function test_RegisterValidator_NotAuthorized_Reverts() public { - bytes memory pubkey = _generatePubkey(1); - - vm.prank(randomUser); - vm.expectRevert(ValidatorManager.NotAuthorized.selector); - manager.registerValidator(pubkey); - } - - function test_RegisterValidator_InvalidPubkeyLength_Reverts() public { - bytes memory shortPubkey = new bytes(100); - - vm.prank(depositPool); - vm.expectRevert(ValidatorManager.InvalidPubkeyLength.selector); - manager.registerValidator(shortPubkey); - } - - function test_RegisterValidator_EmptyPubkey_Reverts() public { - bytes memory emptyPubkey = new bytes(0); - - vm.prank(depositPool); - vm.expectRevert(ValidatorManager.InvalidPubkeyLength.selector); - manager.registerValidator(emptyPubkey); - } - - function test_RegisterValidator_Duplicate_Reverts() public { - bytes memory pubkey = _generatePubkey(1); - - vm.prank(depositPool); - manager.registerValidator(pubkey); - - vm.prank(depositPool); - vm.expectRevert(ValidatorManager.ValidatorAlreadyExists.selector); - manager.registerValidator(pubkey); - } - - function test_RegisterValidator_MultipleValidators() public { - for (uint256 i = 1; i <= 5; i++) { - bytes memory pubkey = _generatePubkey(i); - vm.prank(depositPool); - uint256 validatorId = manager.registerValidator(pubkey); - assertEq(validatorId, i); - } - - assertEq(manager.totalValidators(), 5); - assertEq(manager.pendingValidatorCount(), 5); - } - - // ========================================================================= - // VALIDATOR ACTIVATION TESTS - // ========================================================================= - - function test_ActivateValidator() public { - (uint256 validatorId,) = _registerValidator(1); - - assertEq(manager.pendingValidatorCount(), 1); - assertEq(manager.activeValidatorCount(), 0); - - manager.activateValidator(validatorId); - - assertEq(manager.pendingValidatorCount(), 0); - assertEq(manager.activeValidatorCount(), 1); - - (, ValidatorManager.ValidatorStatus status, uint256 activatedBlock,) = manager.getValidator(validatorId); - - assertEq(uint256(status), uint256(ValidatorManager.ValidatorStatus.Active)); - assertEq(activatedBlock, block.number); - } - - function test_ActivateValidator_EmitsEvent() public { - (uint256 validatorId,) = _registerValidator(1); - - vm.expectEmit(true, false, false, true); - emit ValidatorActivated(validatorId, block.number); - - manager.activateValidator(validatorId); - } - - function test_ActivateValidator_NotOwner_Reverts() public { - (uint256 validatorId,) = _registerValidator(1); - - vm.prank(randomUser); - vm.expectRevert(ValidatorManager.NotOwner.selector); - manager.activateValidator(validatorId); - } - - function test_ActivateValidator_NotPending_Reverts() public { - (uint256 validatorId,) = _registerAndActivate(1); - - // Already active, cannot activate again - vm.expectRevert(ValidatorManager.InvalidStatusTransition.selector); - manager.activateValidator(validatorId); - } - - function test_ActivateValidator_NonExistent_Reverts() public { - // Validator 999 doesn't exist (status is None) - vm.expectRevert(ValidatorManager.InvalidStatusTransition.selector); - manager.activateValidator(999); - } - - // ========================================================================= - // BATCH ACTIVATION TESTS - // ========================================================================= - - function test_BatchActivateValidators() public { - // Register 5 validators - uint256[] memory ids = new uint256[](5); - for (uint256 i = 0; i < 5; i++) { - (ids[i],) = _registerValidator(i + 1); - } - - assertEq(manager.pendingValidatorCount(), 5); - assertEq(manager.activeValidatorCount(), 0); - - manager.batchActivateValidators(ids); - - assertEq(manager.pendingValidatorCount(), 0); - assertEq(manager.activeValidatorCount(), 5); - } - - function test_BatchActivateValidators_SkipsNonPending() public { - // Register 3 validators - (uint256 id1,) = _registerValidator(1); - (uint256 id2,) = _registerValidator(2); - (uint256 id3,) = _registerValidator(3); - - // Activate id2 individually first - manager.activateValidator(id2); - - uint256[] memory ids = new uint256[](3); - ids[0] = id1; - ids[1] = id2; // Already active, should be skipped - ids[2] = id3; - - manager.batchActivateValidators(ids); - - // All should be active now - assertEq(manager.pendingValidatorCount(), 0); - assertEq(manager.activeValidatorCount(), 3); - } - - function test_BatchActivateValidators_EmptyArray() public { - uint256[] memory ids = new uint256[](0); - manager.batchActivateValidators(ids); - // Should not revert, just do nothing - } - - function test_BatchActivateValidators_NotOwner_Reverts() public { - (uint256 validatorId,) = _registerValidator(1); - uint256[] memory ids = new uint256[](1); - ids[0] = validatorId; - - vm.prank(randomUser); - vm.expectRevert(ValidatorManager.NotOwner.selector); - manager.batchActivateValidators(ids); - } - - // ========================================================================= - // EXIT REQUEST TESTS - // ========================================================================= - - function test_RequestValidatorExit() public { - (uint256 validatorId,) = _registerAndActivate(1); - - manager.requestValidatorExit(validatorId); - - (, ValidatorManager.ValidatorStatus status,,) = manager.getValidator(validatorId); - assertEq(uint256(status), uint256(ValidatorManager.ValidatorStatus.Exiting)); - - // Counter should still show as active (exiting validators count as active until fully exited) - assertEq(manager.activeValidatorCount(), 1); - } - - function test_RequestValidatorExit_EmitsEvent() public { - (uint256 validatorId,) = _registerAndActivate(1); - - vm.expectEmit(true, false, false, true); - emit ValidatorExitRequested(validatorId, block.number); - - manager.requestValidatorExit(validatorId); - } - - function test_RequestValidatorExit_NotOwner_Reverts() public { - (uint256 validatorId,) = _registerAndActivate(1); - - vm.prank(randomUser); - vm.expectRevert(ValidatorManager.NotOwner.selector); - manager.requestValidatorExit(validatorId); - } - - function test_RequestValidatorExit_NotActive_Reverts() public { - (uint256 validatorId,) = _registerValidator(1); - - // Still pending, cannot request exit - vm.expectRevert(ValidatorManager.InvalidStatusTransition.selector); - manager.requestValidatorExit(validatorId); - } - - // ========================================================================= - // MARK EXITED TESTS - // ========================================================================= - - function test_MarkValidatorExited() public { - (uint256 validatorId,) = _registerAndActivate(1); - manager.requestValidatorExit(validatorId); - - assertEq(manager.activeValidatorCount(), 1); - - manager.markValidatorExited(validatorId); - - (, ValidatorManager.ValidatorStatus status,, uint256 exitedBlock) = manager.getValidator(validatorId); - assertEq(uint256(status), uint256(ValidatorManager.ValidatorStatus.Exited)); - assertEq(exitedBlock, block.number); - assertEq(manager.activeValidatorCount(), 0); - } - - function test_MarkValidatorExited_EmitsEvent() public { - (uint256 validatorId,) = _registerAndActivate(1); - manager.requestValidatorExit(validatorId); - - vm.expectEmit(true, false, false, true); - emit ValidatorExited(validatorId, block.number); - - manager.markValidatorExited(validatorId); - } - - function test_MarkValidatorExited_NotOwner_Reverts() public { - (uint256 validatorId,) = _registerAndActivate(1); - manager.requestValidatorExit(validatorId); - - vm.prank(randomUser); - vm.expectRevert(ValidatorManager.NotOwner.selector); - manager.markValidatorExited(validatorId); - } - - function test_MarkValidatorExited_NotExiting_Reverts() public { - (uint256 validatorId,) = _registerAndActivate(1); - - // Still active, not exiting - vm.expectRevert(ValidatorManager.InvalidStatusTransition.selector); - manager.markValidatorExited(validatorId); - } - - // ========================================================================= - // SLASHING TESTS (M-1 FIX VERIFICATION) - // ========================================================================= - - function test_MarkValidatorSlashed_FromActive() public { - (uint256 validatorId,) = _registerAndActivate(1); - - assertEq(manager.activeValidatorCount(), 1); - - manager.markValidatorSlashed(validatorId); - - (, ValidatorManager.ValidatorStatus status,, uint256 exitedBlock) = manager.getValidator(validatorId); - assertEq(uint256(status), uint256(ValidatorManager.ValidatorStatus.Slashed)); - assertEq(exitedBlock, block.number); - - // M-1 FIX: Counter should decrement when slashing from Active - assertEq(manager.activeValidatorCount(), 0); - } - - function test_MarkValidatorSlashed_FromExiting() public { - (uint256 validatorId,) = _registerAndActivate(1); - manager.requestValidatorExit(validatorId); - - assertEq(manager.activeValidatorCount(), 1); - - manager.markValidatorSlashed(validatorId); - - (, ValidatorManager.ValidatorStatus status,,) = manager.getValidator(validatorId); - assertEq(uint256(status), uint256(ValidatorManager.ValidatorStatus.Slashed)); - - // Counter should decrement - Exiting validators still count as active - assertEq(manager.activeValidatorCount(), 0); - } - - function test_MarkValidatorSlashed_MultipleActiveValidators() public { - // Register and activate 3 validators - (uint256 id1,) = _registerAndActivate(1); - (uint256 id2,) = _registerAndActivate(2); - (uint256 id3,) = _registerAndActivate(3); - - assertEq(manager.activeValidatorCount(), 3); - - // Slash the middle one - manager.markValidatorSlashed(id2); - - // M-1 FIX: Counter should be 2 now - assertEq(manager.activeValidatorCount(), 2); - - // Slash another - manager.markValidatorSlashed(id1); - assertEq(manager.activeValidatorCount(), 1); - - // Slash the last one - manager.markValidatorSlashed(id3); - assertEq(manager.activeValidatorCount(), 0); - } - - function test_MarkValidatorSlashed_EmitsEvent() public { - (uint256 validatorId,) = _registerAndActivate(1); - - vm.expectEmit(true, false, false, true); - emit ValidatorSlashed(validatorId, block.number); - - manager.markValidatorSlashed(validatorId); - } - - function test_MarkValidatorSlashed_NotOwner_Reverts() public { - (uint256 validatorId,) = _registerAndActivate(1); - - vm.prank(randomUser); - vm.expectRevert(ValidatorManager.NotOwner.selector); - manager.markValidatorSlashed(validatorId); - } - - function test_MarkValidatorSlashed_FromPending_Reverts() public { - (uint256 validatorId,) = _registerValidator(1); - - vm.expectRevert(ValidatorManager.InvalidStatusTransition.selector); - manager.markValidatorSlashed(validatorId); - } - - function test_MarkValidatorSlashed_FromExited_Reverts() public { - (uint256 validatorId,) = _registerAndActivate(1); - manager.requestValidatorExit(validatorId); - manager.markValidatorExited(validatorId); - - vm.expectRevert(ValidatorManager.InvalidStatusTransition.selector); - manager.markValidatorSlashed(validatorId); - } - - function test_MarkValidatorSlashed_AlreadySlashed_Reverts() public { - (uint256 validatorId,) = _registerAndActivate(1); - manager.markValidatorSlashed(validatorId); - - vm.expectRevert(ValidatorManager.InvalidStatusTransition.selector); - manager.markValidatorSlashed(validatorId); - } - - // ========================================================================= - // VIEW FUNCTION TESTS - // ========================================================================= - - function test_GetValidatorIdByPubkey() public { - bytes memory pubkey = _generatePubkey(42); - - vm.prank(depositPool); - uint256 validatorId = manager.registerValidator(pubkey); - - uint256 lookupId = manager.getValidatorIdByPubkey(pubkey); - assertEq(lookupId, validatorId); - } - - function test_GetValidatorIdByPubkey_NotFound() public view { - bytes memory unknownPubkey = _generatePubkey(999); - uint256 lookupId = manager.getValidatorIdByPubkey(unknownPubkey); - assertEq(lookupId, 0); - } - - function test_GetValidatorStatus() public { - bytes memory pubkey = _generatePubkey(1); - - vm.prank(depositPool); - manager.registerValidator(pubkey); - - ValidatorManager.ValidatorStatus status = manager.getValidatorStatus(pubkey); - assertEq(uint256(status), uint256(ValidatorManager.ValidatorStatus.Pending)); - } - - function test_GetValidatorStatus_NotFound() public view { - bytes memory unknownPubkey = _generatePubkey(999); - ValidatorManager.ValidatorStatus status = manager.getValidatorStatus(unknownPubkey); - assertEq(uint256(status), uint256(ValidatorManager.ValidatorStatus.None)); - } - - function test_GetStats() public { - // Register 3 validators - _registerValidator(1); - _registerValidator(2); - (uint256 id3,) = _registerValidator(3); - - // Activate 1 - manager.activateValidator(id3); - - (uint256 total, uint256 pending, uint256 active, uint256 totalStaked) = manager.getStats(); - - assertEq(total, 3); - assertEq(pending, 2); - assertEq(active, 1); - assertEq(totalStaked, VALIDATOR_STAKE); - } - - function test_GetValidatorsByStatus() public { - // Register 5 validators - _registerValidator(1); - (uint256 id2,) = _registerValidator(2); - _registerValidator(3); - (uint256 id4,) = _registerValidator(4); - _registerValidator(5); - - // Activate some - manager.activateValidator(id2); - manager.activateValidator(id4); - - // Get pending validators - uint256[] memory pendingIds = manager.getValidatorsByStatus(ValidatorManager.ValidatorStatus.Pending); - assertEq(pendingIds.length, 3); - - // Get active validators - uint256[] memory activeIds = manager.getValidatorsByStatus(ValidatorManager.ValidatorStatus.Active); - assertEq(activeIds.length, 2); - assertEq(activeIds[0], id2); - assertEq(activeIds[1], id4); - - // Request exit for one - manager.requestValidatorExit(id2); - uint256[] memory exitingIds = manager.getValidatorsByStatus(ValidatorManager.ValidatorStatus.Exiting); - assertEq(exitingIds.length, 1); - assertEq(exitingIds[0], id2); - } - - function test_GetValidatorsByStatus_None() public view { - uint256[] memory noneIds = manager.getValidatorsByStatus(ValidatorManager.ValidatorStatus.None); - assertEq(noneIds.length, 0); - } - - // ========================================================================= - // ADMIN FUNCTION TESTS - // ========================================================================= - - function test_SetDepositPool() public { - ValidatorManager newManager = new ValidatorManager(); - address newDepositPool = address(0x999); - - newManager.setDepositPool(newDepositPool); - - assertEq(newManager.depositPool(), newDepositPool); - } - - function test_SetDepositPool_EmitsEvent() public { - ValidatorManager newManager = new ValidatorManager(); - address newDepositPool = address(0x999); - - vm.expectEmit(true, false, false, false); - emit DepositPoolSet(newDepositPool); - - newManager.setDepositPool(newDepositPool); - } - - function test_SetDepositPool_NotOwner_Reverts() public { - ValidatorManager newManager = new ValidatorManager(); - - vm.prank(randomUser); - vm.expectRevert(ValidatorManager.NotOwner.selector); - newManager.setDepositPool(address(0x999)); - } - - function test_SetDepositPool_ZeroAddress_Reverts() public { - ValidatorManager newManager = new ValidatorManager(); - - vm.expectRevert(ValidatorManager.ZeroAddress.selector); - newManager.setDepositPool(address(0)); - } - - function test_TransferOwnership() public { - address newOwner = address(0x888); - - manager.transferOwnership(newOwner); - - assertEq(manager.owner(), newOwner); - } - - function test_TransferOwnership_EmitsEvent() public { - address newOwner = address(0x888); - - vm.expectEmit(true, true, false, false); - emit OwnershipTransferred(owner, newOwner); - - manager.transferOwnership(newOwner); - } - - function test_TransferOwnership_NotOwner_Reverts() public { - vm.prank(randomUser); - vm.expectRevert(ValidatorManager.NotOwner.selector); - manager.transferOwnership(address(0x888)); - } - - function test_TransferOwnership_ZeroAddress_Reverts() public { - vm.expectRevert(ValidatorManager.ZeroAddress.selector); - manager.transferOwnership(address(0)); - } - - function test_TransferOwnership_NewOwnerCanOperate() public { - address newOwner = address(0x888); - manager.transferOwnership(newOwner); - - (uint256 validatorId,) = _registerValidator(1); - - // New owner can activate - vm.prank(newOwner); - manager.activateValidator(validatorId); - - assertEq(manager.activeValidatorCount(), 1); - } - - // ========================================================================= - // FULL LIFECYCLE TEST - // ========================================================================= - - function test_FullValidatorLifecycle() public { - // 1. Register - bytes memory pubkey = _generatePubkey(1); - vm.prank(depositPool); - uint256 validatorId = manager.registerValidator(pubkey); - - (, ValidatorManager.ValidatorStatus status,,) = manager.getValidator(validatorId); - assertEq(uint256(status), uint256(ValidatorManager.ValidatorStatus.Pending)); - - // 2. Activate - manager.activateValidator(validatorId); - (, status,,) = manager.getValidator(validatorId); - assertEq(uint256(status), uint256(ValidatorManager.ValidatorStatus.Active)); - - // 3. Request exit - manager.requestValidatorExit(validatorId); - (, status,,) = manager.getValidator(validatorId); - assertEq(uint256(status), uint256(ValidatorManager.ValidatorStatus.Exiting)); - - // 4. Mark exited - manager.markValidatorExited(validatorId); - (, status,,) = manager.getValidator(validatorId); - assertEq(uint256(status), uint256(ValidatorManager.ValidatorStatus.Exited)); - } - - function test_FullValidatorLifecycle_WithSlashing() public { - // 1. Register - bytes memory pubkey = _generatePubkey(1); - vm.prank(depositPool); - uint256 validatorId = manager.registerValidator(pubkey); - - // 2. Activate - manager.activateValidator(validatorId); - assertEq(manager.activeValidatorCount(), 1); - - // 3. Slashed while active - manager.markValidatorSlashed(validatorId); - - (, ValidatorManager.ValidatorStatus status,,) = manager.getValidator(validatorId); - assertEq(uint256(status), uint256(ValidatorManager.ValidatorStatus.Slashed)); - assertEq(manager.activeValidatorCount(), 0); - } - - // ========================================================================= - // FUZZ TESTS - // ========================================================================= - - function testFuzz_RegisterMultipleValidators(uint8 count) public { - vm.assume(count > 0 && count <= 50); - - for (uint256 i = 1; i <= count; i++) { - bytes memory pubkey = _generatePubkey(i); - vm.prank(depositPool); - uint256 validatorId = manager.registerValidator(pubkey); - assertEq(validatorId, i); - } - - assertEq(manager.totalValidators(), count); - assertEq(manager.pendingValidatorCount(), count); - } - - function testFuzz_SlashingCounterCorrectness(uint8 activeCount, uint8 slashCount) public { - vm.assume(activeCount > 0 && activeCount <= 20); - vm.assume(slashCount <= activeCount); - - // Register and activate validators - uint256[] memory ids = new uint256[](activeCount); - for (uint256 i = 0; i < activeCount; i++) { - (ids[i],) = _registerAndActivate(i + 1); - } - - assertEq(manager.activeValidatorCount(), activeCount); - - // Slash some validators - for (uint256 i = 0; i < slashCount; i++) { - manager.markValidatorSlashed(ids[i]); - } - - // Verify counter is correct (M-1 fix verification) - assertEq(manager.activeValidatorCount(), activeCount - slashCount); - } -} diff --git a/contracts/test/hyperion/DepositPool-v2.t.hyp b/contracts/test/hyperion/DepositPool-v2.t.hyp deleted file mode 100644 index 876811e..0000000 --- a/contracts/test/hyperion/DepositPool-v2.t.hyp +++ /dev/null @@ -1,1698 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -// Generated from ../test/DepositPool-v2.t.sol by scripts/sync-hyperion.js. -// Edit the Solidity source first, then re-run this script. -pragma hyperion >=0.0; - -import "forge-std/Test.hyp"; -import "../../hyperion/stQRL-v2.hyp"; -import "../../hyperion/DepositPool-v2.hyp"; - -/** - * @title DepositPool v2 Integration Tests - * @notice Tests for deposit, withdrawal, and reward sync flows - */ -contract DepositPoolV2Test is Test { - stQRLv2 public token; - DepositPoolV2 public pool; - - address public owner; - address public user1; - address public user2; - - event Deposited(address indexed user, uint256 qrlAmount, uint256 sharesReceived); - event WithdrawalRequested(address indexed user, uint256 shares, uint256 qrlAmount, uint256 requestBlock); - event WithdrawalClaimed(address indexed user, uint256 shares, uint256 qrlAmount); - event RewardsSynced(uint256 rewardsAmount, uint256 newTotalPooled, uint256 blockNumber); - event SlashingDetected(uint256 lossAmount, uint256 newTotalPooled, uint256 blockNumber); - - function setUp() public { - owner = address(this); - user1 = address(0x1); - user2 = address(0x2); - - // Deploy contracts - token = new stQRLv2(); - pool = new DepositPoolV2(); - - // Link contracts - pool.setStQRL(address(token)); - token.setDepositPool(address(pool)); - - // Legacy tests deposit and withdraw in the same block. The minimum - // stake lock has its own dedicated test section, which re-enables it. - token.setMinStakeBlocks(0); - - // Fund test users - vm.deal(user1, 1000 quanta); - vm.deal(user2, 1000 quanta); - } - - // ========================================================================= - // DEPOSIT TESTS - // ========================================================================= - - function test_Deposit() public { - vm.prank(user1); - uint256 shares = pool.deposit{value: 100 quanta}(); - - assertEq(shares, 100 quanta); - assertEq(token.balanceOf(user1), 100 quanta); - assertEq(pool.bufferedQRL(), 100 quanta); - } - - function test_Deposit_MinimumEnforced() public { - vm.prank(user1); - vm.expectRevert(DepositPoolV2.BelowMinDeposit.selector); - pool.deposit{value: 0.01 quanta}(); // Below 0.1 minimum - } - - function test_MultipleDeposits() public { - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - - vm.prank(user2); - pool.deposit{value: 200 quanta}(); - - assertEq(token.balanceOf(user1), 100 quanta); - assertEq(token.balanceOf(user2), 200 quanta); - assertEq(pool.bufferedQRL(), 300 quanta); - assertEq(token.totalSupply(), 300 quanta); - } - - function test_DepositAfterRewards() public { - // User1 deposits 100 QRL - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - - // Simulate rewards by sending ETH directly and syncing - vm.deal(address(pool), 150 quanta); // 50 QRL rewards - pool.syncRewards(); - - // User1's shares unchanged (fixed-balance) - assertEq(token.balanceOf(user1), 100 quanta); - // But QRL value increased (approx due to virtual shares) - assertApproxEqRel(token.getQRLValue(user1), 150 quanta, 1e14); - - // User2 deposits 150 QRL (should get ~100 shares at new rate) - vm.prank(user2); - uint256 shares = pool.deposit{value: 150 quanta}(); - - // User2 gets shares based on current rate - // Rate: 150 QRL / 100 shares = 1.5 QRL per share - // For 150 QRL: 150 / 1.5 ≈ 100 shares (approx due to virtual shares) - assertApproxEqRel(shares, 100 quanta, 1e14); - assertApproxEqRel(token.sharesOf(user2), 100 quanta, 1e14); - } - - // ========================================================================= - // REWARD SYNC TESTS - // ========================================================================= - - function test_SyncRewards_DetectsRewards() public { - // User deposits - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - - // Initial state - assertEq(token.totalPooledQRL(), 100 quanta); - assertEq(pool.totalRewardsReceived(), 0); - - // Simulate validator rewards by adding ETH to contract - vm.deal(address(pool), 110 quanta); // 10 QRL rewards - - // Sync should detect rewards - vm.expectEmit(true, true, true, true); - emit RewardsSynced(10 quanta, 110 quanta, block.number); - pool.syncRewards(); - - assertEq(token.totalPooledQRL(), 110 quanta); - assertEq(pool.totalRewardsReceived(), 10 quanta); - // Shares unchanged (fixed-balance) - assertEq(token.balanceOf(user1), 100 quanta); - // QRL value reflects rewards (approx due to virtual shares) - assertApproxEqRel(token.getQRLValue(user1), 110 quanta, 1e14); - } - - function test_SyncRewards_DetectsSlashing() public { - // This test demonstrates slashing detection - // Slashing math is verified in stQRL tests (balance decrease) - // Here we just verify the sync doesn't break with no change - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - - // Sync should work without changes - pool.syncRewards(); - assertEq(token.totalPooledQRL(), 100 quanta); - } - - function test_SyncRewards_NoChangeWhenBalanceMatch() public { - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - - uint256 rewardsBefore = pool.totalRewardsReceived(); - pool.syncRewards(); - uint256 rewardsAfter = pool.totalRewardsReceived(); - - // No change in rewards - assertEq(rewardsBefore, rewardsAfter); - } - - // ========================================================================= - // WITHDRAWAL TESTS - // ========================================================================= - - function test_RequestWithdrawal() public { - // Deposit - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - - // Request withdrawal - vm.prank(user1); - (uint256 requestId, uint256 qrlAmount) = pool.requestWithdrawal(50 quanta); - - assertEq(requestId, 0); - assertEq(qrlAmount, 50 quanta); - - (uint256 shares, uint256 qrl, uint256 requestBlock, bool canClaim,, bool claimed) = - pool.getWithdrawalRequest(user1, 0); - - assertEq(shares, 50 quanta); - assertEq(qrl, 50 quanta); - assertEq(requestBlock, block.number); - assertFalse(canClaim); // Not enough time passed - assertFalse(claimed); - } - - function test_ClaimWithdrawal() public { - // Deposit - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - - // Request withdrawal first. The returned value is the current estimate. - vm.prank(user1); - pool.requestWithdrawal(50 quanta); - - // Earmark liquid QRL for the claim. - pool.fundWithdrawalReserve(50 quanta); - - // Wait for withdrawal delay - vm.roll(block.number + 129); // > 128 blocks - - // Claim - uint256 balanceBefore = user1.balance; - vm.prank(user1); - uint256 claimed = pool.claimWithdrawal(); - - assertEq(claimed, 50 quanta); - assertEq(user1.balance - balanceBefore, 50 quanta); - assertEq(token.balanceOf(user1), 50 quanta); - } - - function test_ClaimWithdrawal_TooEarly() public { - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - - vm.prank(user1); - pool.requestWithdrawal(50 quanta); - - pool.fundWithdrawalReserve(50 quanta); - - // Try to claim immediately (should fail) - vm.prank(user1); - vm.expectRevert(DepositPoolV2.WithdrawalNotReady.selector); - pool.claimWithdrawal(); - } - - function test_ClaimWithdrawal_InsufficientReserve() public { - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - - // No withdrawal reserve funded - - vm.prank(user1); - pool.requestWithdrawal(50 quanta); - - vm.roll(block.number + 129); - - vm.prank(user1); - vm.expectRevert(DepositPoolV2.InsufficientReserve.selector); - pool.claimWithdrawal(); - } - - function test_CancelWithdrawal() public { - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - - vm.prank(user1); - (uint256 requestId,) = pool.requestWithdrawal(50 quanta); - - assertEq(pool.totalWithdrawalShares(), 50 quanta); - - vm.prank(user1); - pool.cancelWithdrawal(requestId); - - assertEq(pool.totalWithdrawalShares(), 0); - } - - function test_WithdrawalAfterRewards() public { - // Deposit 100 QRL - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - - // Add 10% rewards - vm.deal(address(pool), 110 quanta); - pool.syncRewards(); - - // Shares unchanged (fixed-balance) - assertEq(token.balanceOf(user1), 100 quanta); - // User's shares now worth 110 QRL (approx due to virtual shares) - assertApproxEqRel(token.getQRLValue(user1), 110 quanta, 1e14); - - // Request withdrawal of all shares before funding reserve. - vm.prank(user1); - (, uint256 qrlAmount) = pool.requestWithdrawal(100 quanta); - - // Approx due to virtual shares - assertApproxEqRel(qrlAmount, 110 quanta, 1e14); - - // Earmark enough liquid QRL to cover the claim. - pool.fundWithdrawalReserve(token.totalPooledQRL()); - - vm.roll(block.number + 129); - - uint256 balanceBefore = user1.balance; - vm.prank(user1); - uint256 claimed = pool.claimWithdrawal(); - - // Should receive ~110 QRL (original + rewards) - assertApproxEqRel(user1.balance - balanceBefore, 110 quanta, 1e14); - assertEq(user1.balance - balanceBefore, claimed); - } - - // ========================================================================= - // SLASHING SIMULATION - // ========================================================================= - - function test_SlashingReducesWithdrawalAmount() public { - // Deposit 100 QRL - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - - // User's shares are worth 100 QRL initially (approx due to virtual shares) - assertApproxEqRel(token.getQRLValue(user1), 100 quanta, 1e14); - - // Simulate slashing by directly reducing the contract balance - // In real scenarios, this happens through validator slashing on the beacon chain - vm.deal(address(pool), 90 quanta); // Was 100, now 90 - - // Sync to detect the "slashing" - pool.syncRewards(); - - // User's shares now worth less (90 QRL instead of 100) (approx) - assertApproxEqRel(token.getQRLValue(user1), 90 quanta, 1e14); - - // Request withdrawal of all shares after the loss is synchronized. - vm.prank(user1); - (, uint256 qrlAmount) = pool.requestWithdrawal(100 quanta); - - // Should only get ~90 QRL (slashed amount) (approx due to virtual shares) - assertApproxEqRel(qrlAmount, 90 quanta, 1e14); - - // Earmark liquid QRL for the claim. - pool.fundWithdrawalReserve(token.totalPooledQRL()); - - vm.roll(block.number + pool.WITHDRAWAL_DELAY()); - (,,, bool canClaim,,) = pool.getWithdrawalRequest(user1, 0); - assertTrue(canClaim); - uint256 balanceBefore = user1.balance; - vm.prank(user1); - uint256 claimed = pool.claimWithdrawal(); - assertEq(claimed, 90 quanta); - assertEq(user1.balance - balanceBefore, 90 quanta); - } - - function test_SlashingDetected_EmitsEvent() public { - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - - // Simulate slashing by directly reducing the contract balance - vm.deal(address(pool), 90 quanta); // Was 100, now 90 - - vm.expectEmit(true, true, true, true); - emit SlashingDetected(10 quanta, 90 quanta, block.number); - pool.syncRewards(); - } - - // ========================================================================= - // VALIDATOR FUNDING TESTS - // ========================================================================= - - function test_CanFundValidator() public { - // Fund users with enough ETH for this test - vm.deal(user1, 20000 quanta); - vm.deal(user2, 20000 quanta); - - // Deposit less than threshold - vm.prank(user1); - pool.deposit{value: 20000 quanta}(); - - (bool possible, uint256 buffered) = pool.canFundValidator(); - assertFalse(possible); - assertEq(buffered, 20000 quanta); - - // Deposit more to reach threshold - vm.prank(user2); - pool.deposit{value: 20000 quanta}(); - - (possible, buffered) = pool.canFundValidator(); - assertTrue(possible); - assertEq(buffered, 40000 quanta); - } - - function test_FundValidatorMVP() public { - // Deposit enough for validator (40,000 QRL per Zond mainnet config) - vm.deal(user1, 40000 quanta); - vm.prank(user1); - pool.deposit{value: 40000 quanta}(); - - uint256 validatorId = pool.fundValidatorMVP(); - - assertEq(validatorId, 0); - assertEq(pool.validatorCount(), 1); - assertEq(pool.bufferedQRL(), 0); - } - - // ========================================================================= - // VIEW FUNCTION TESTS - // ========================================================================= - - function test_GetPoolStatus() public { - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - - ( - uint256 totalPooled, - uint256 totalShares, - uint256 buffered, - uint256 validators, - uint256 pendingShares, - uint256 reserve, - uint256 rate - ) = pool.getPoolStatus(); - - assertEq(totalPooled, 100 quanta); - assertEq(totalShares, 100 quanta); - assertEq(buffered, 100 quanta); - assertEq(validators, 0); - assertEq(pendingShares, 0); - assertEq(reserve, 0); - assertEq(rate, 1e18); // 1:1 exchange rate - } - - function test_GetRewardStats() public { - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - - // Add rewards - vm.deal(address(pool), 110 quanta); - pool.syncRewards(); - - (uint256 totalRewards, uint256 totalSlashing, uint256 netRewards, uint256 lastSync) = pool.getRewardStats(); - - assertEq(totalRewards, 10 quanta); - assertEq(totalSlashing, 0); - assertEq(netRewards, 10 quanta); - assertEq(lastSync, block.number); - } - - // ========================================================================= - // ACCESS CONTROL TESTS - // ========================================================================= - - function test_OnlyOwnerCanFundValidator() public { - vm.deal(user1, 40000 quanta); - vm.prank(user1); - pool.deposit{value: 40000 quanta}(); - - vm.prank(user1); - vm.expectRevert(DepositPoolV2.NotOwner.selector); - pool.fundValidatorMVP(); - } - - function test_OnlyOwnerCanPause() public { - vm.prank(user1); - vm.expectRevert(DepositPoolV2.NotOwner.selector); - pool.pause(); - } - - function test_PauseBlocksDeposits() public { - pool.pause(); - - vm.prank(user1); - vm.expectRevert(DepositPoolV2.ContractPaused.selector); - pool.deposit{value: 100 quanta}(); - } - - // ========================================================================= - // FUZZ TESTS - // ========================================================================= - - function testFuzz_DepositAndWithdraw(uint256 amount) public { - amount = bound(amount, 100 quanta, 10000 quanta); - - vm.deal(user1, amount * 2); - - vm.prank(user1); - pool.deposit{value: amount}(); - - assertEq(token.balanceOf(user1), amount); - - // Request withdrawal first, then earmark liquid QRL. - uint256 shares = token.sharesOf(user1); - vm.prank(user1); - pool.requestWithdrawal(shares); - - pool.fundWithdrawalReserve(amount); - - vm.roll(block.number + 129); - - uint256 balanceBefore = user1.balance; - vm.prank(user1); - pool.claimWithdrawal(); - - // Should get back approximately the same amount (minus any rounding) - assertApproxEqRel(user1.balance - balanceBefore, amount, 1e15); - } - - // ========================================================================= - // DEPOSIT ERROR TESTS - // ========================================================================= - - function test_Deposit_StQRLNotSet_Reverts() public { - // Deploy fresh pool without stQRL set - DepositPoolV2 freshPool = new DepositPoolV2(); - - vm.prank(user1); - vm.expectRevert(DepositPoolV2.StQRLNotSet.selector); - freshPool.deposit{value: 1 quanta}(); - } - - function test_Deposit_ZeroAmount_Reverts() public { - vm.prank(user1); - vm.expectRevert(DepositPoolV2.BelowMinDeposit.selector); - pool.deposit{value: 0}(); - } - - function test_Deposit_EmitsEvent() public { - vm.prank(user1); - vm.expectEmit(true, false, false, true); - emit Deposited(user1, 100 quanta, 100 quanta); - pool.deposit{value: 100 quanta}(); - } - - // ========================================================================= - // WITHDRAWAL ERROR TESTS - // ========================================================================= - - function test_RequestWithdrawal_ZeroShares_Reverts() public { - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - - vm.prank(user1); - vm.expectRevert(DepositPoolV2.ZeroAmount.selector); - pool.requestWithdrawal(0); - } - - function test_RequestWithdrawal_InsufficientShares_Reverts() public { - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - - vm.prank(user1); - vm.expectRevert(DepositPoolV2.InsufficientShares.selector); - pool.requestWithdrawal(150 quanta); - } - - function test_MultipleWithdrawalRequests() public { - // Multiple withdrawal requests are now allowed - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - - vm.prank(user1); - (uint256 requestId1,) = pool.requestWithdrawal(50 quanta); - - vm.prank(user1); - (uint256 requestId2,) = pool.requestWithdrawal(25 quanta); - - assertEq(requestId1, 0); - assertEq(requestId2, 1); - assertEq(pool.totalWithdrawalShares(), 75 quanta); - - // Verify both requests exist - (uint256 total, uint256 pending) = pool.getWithdrawalRequestCount(user1); - assertEq(total, 2); - assertEq(pending, 2); - } - - function test_RequestWithdrawal_WhenPaused_Reverts() public { - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - - pool.pause(); - - vm.prank(user1); - vm.expectRevert(DepositPoolV2.ContractPaused.selector); - pool.requestWithdrawal(50 quanta); - } - - function test_RequestWithdrawal_EmitsEvent() public { - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - - vm.prank(user1); - vm.expectEmit(true, false, false, true); - emit WithdrawalRequested(user1, 50 quanta, 50 quanta, block.number); - pool.requestWithdrawal(50 quanta); - } - - function test_ClaimWithdrawal_NoRequest_Reverts() public { - vm.prank(user1); - vm.expectRevert(DepositPoolV2.NoWithdrawalPending.selector); - pool.claimWithdrawal(); - } - - function test_ClaimWithdrawal_EmitsEvent() public { - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - - // Request first, then fund reserve. - vm.prank(user1); - pool.requestWithdrawal(50 quanta); - - pool.fundWithdrawalReserve(50 quanta); - - vm.roll(block.number + 129); - - vm.prank(user1); - vm.expectEmit(true, false, false, true); - emit WithdrawalClaimed(user1, 50 quanta, 50 quanta); - pool.claimWithdrawal(); - } - - function test_CancelWithdrawal_NoRequest_Reverts() public { - vm.prank(user1); - vm.expectRevert(DepositPoolV2.InvalidWithdrawalIndex.selector); - pool.cancelWithdrawal(0); - } - - // ========================================================================= - // VALIDATOR FUNDING ERROR TESTS - // ========================================================================= - - function test_FundValidatorMVP_InsufficientBuffer_Reverts() public { - // Deposit less than validator stake - vm.deal(user1, 5000 quanta); - vm.prank(user1); - pool.deposit{value: 5000 quanta}(); - - vm.expectRevert(DepositPoolV2.InsufficientBuffer.selector); - pool.fundValidatorMVP(); - } - - function test_FundValidatorMVP_EmitsEvent() public { - vm.deal(user1, 40000 quanta); - vm.prank(user1); - pool.deposit{value: 40000 quanta}(); - - vm.expectEmit(true, false, false, true); - emit ValidatorFunded(0, "", 40000 quanta); - pool.fundValidatorMVP(); - } - - // ========================================================================= - // ADMIN FUNCTION TESTS - // ========================================================================= - - function test_SetStQRL() public { - DepositPoolV2 freshPool = new DepositPoolV2(); - address newStQRL = address(0x123); - - freshPool.setStQRL(newStQRL); - - assertEq(address(freshPool.stQRL()), newStQRL); - } - - function test_SetStQRL_ZeroAddress_Reverts() public { - DepositPoolV2 freshPool = new DepositPoolV2(); - - vm.expectRevert(DepositPoolV2.ZeroAddress.selector); - freshPool.setStQRL(address(0)); - } - - function test_SetStQRL_NotOwner_Reverts() public { - vm.prank(user1); - vm.expectRevert(DepositPoolV2.NotOwner.selector); - pool.setStQRL(address(0x123)); - } - - function test_SetStQRL_AlreadySet_Reverts() public { - // stQRL is already set in setUp() - vm.expectRevert(DepositPoolV2.StQRLAlreadySet.selector); - pool.setStQRL(address(0x123)); - } - - function test_SetMinDeposit() public { - pool.setMinDeposit(200 quanta); - assertEq(pool.minDeposit(), 200 quanta); - - // Cannot set below the current floor (100 quanta by default) - vm.expectRevert(DepositPoolV2.BelowMinDepositFloor.selector); - pool.setMinDeposit(50 quanta); - } - - function test_SetMinDeposit_NotOwner_Reverts() public { - vm.prank(user1); - vm.expectRevert(DepositPoolV2.NotOwner.selector); - pool.setMinDeposit(200 quanta); - } - - function test_SetMinDeposit_EmitsEvent() public { - vm.expectEmit(false, false, false, true); - emit MinDepositUpdated(200 quanta); - pool.setMinDeposit(200 quanta); - } - - function test_Unpause() public { - pool.pause(); - assertTrue(pool.paused()); - - pool.unpause(); - assertFalse(pool.paused()); - } - - function test_Unpause_NotOwner_Reverts() public { - pool.pause(); - - vm.prank(user1); - vm.expectRevert(DepositPoolV2.NotOwner.selector); - pool.unpause(); - } - - function test_TransferOwnership() public { - address newOwner = address(0x999); - - pool.transferOwnership(newOwner); - - assertEq(pool.owner(), newOwner); - } - - function test_TransferOwnership_ZeroAddress_Reverts() public { - vm.expectRevert(DepositPoolV2.ZeroAddress.selector); - pool.transferOwnership(address(0)); - } - - function test_TransferOwnership_NotOwner_Reverts() public { - vm.prank(user1); - vm.expectRevert(DepositPoolV2.NotOwner.selector); - pool.transferOwnership(user1); - } - - function test_TransferOwnership_EmitsEvent() public { - address newOwner = address(0x999); - - vm.expectEmit(true, true, false, false); - emit OwnershipTransferred(owner, newOwner); - pool.transferOwnership(newOwner); - } - - function test_EmergencyWithdraw_UnsyncedRewardsBecomePooled() public { - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - - // Native inflows are indistinguishable from validator rewards. - vm.deal(address(pool), 110 quanta); - - address recipient = address(0x999); - vm.expectRevert(DepositPoolV2.AccountingNotSettled.selector); - pool.emergencyWithdraw(recipient, 10 quanta); - - pool.syncRewards(); - assertEq(pool.totalRewardsReceived(), 10 quanta); - vm.expectRevert(DepositPoolV2.ExceedsRecoverableAmount.selector); - pool.emergencyWithdraw(recipient, 10 quanta); - assertEq(recipient.balance, 0); - } - - function test_EmergencyWithdraw_ExceedsRecoverable_Reverts() public { - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - - // No excess funds - balance equals pooled QRL - // Try to withdraw pool funds - vm.expectRevert(DepositPoolV2.ExceedsRecoverableAmount.selector); - pool.emergencyWithdraw(address(0x999), 10 quanta); - } - - function test_EmergencyWithdraw_ZeroAddress_Reverts() public { - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - - // Add excess funds - vm.deal(address(pool), 110 quanta); - - vm.expectRevert(DepositPoolV2.ZeroAddress.selector); - pool.emergencyWithdraw(address(0), 10 quanta); - } - - function test_EmergencyWithdraw_ZeroAmount_Reverts() public { - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - - vm.expectRevert(DepositPoolV2.ZeroAmount.selector); - pool.emergencyWithdraw(address(0x999), 0); - } - - function test_EmergencyWithdraw_NotOwner_Reverts() public { - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - - // Add excess funds - vm.deal(address(pool), 110 quanta); - - vm.prank(user1); - vm.expectRevert(DepositPoolV2.NotOwner.selector); - pool.emergencyWithdraw(user1, 10 quanta); - } - - // ========================================================================= - // VIEW FUNCTION TESTS - // ========================================================================= - - function test_PreviewDeposit() public view { - // Before any deposits, 1:1 ratio - uint256 shares = pool.previewDeposit(100 quanta); - assertEq(shares, 100 quanta); - } - - function test_PreviewDeposit_AfterRewards() public { - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - - // Add 50% rewards - vm.deal(address(pool), 150 quanta); - pool.syncRewards(); - - // 100 QRL should now get fewer shares - uint256 shares = pool.previewDeposit(100 quanta); - // At 1.5 QRL/share rate, 100 QRL = 66.67 shares - assertApproxEqRel(shares, 66.67 quanta, 1e16); - } - - function test_PreviewDeposit_StQRLNotSet() public { - DepositPoolV2 freshPool = new DepositPoolV2(); - - // Should return 1:1 if stQRL not set - uint256 shares = freshPool.previewDeposit(100 quanta); - assertEq(shares, 100 quanta); - } - - // ========================================================================= - // RECEIVE FUNCTION TESTS - // ========================================================================= - - function test_Receive_IsNoOp() public { - // receive() is a no-op — incoming ETH does NOT auto-add to withdrawalReserve. - // _syncRewards() will later detect it as a balance increase (rewards). - uint256 reserveBefore = pool.withdrawalReserve(); - - // Send ETH directly to contract - (bool success,) = address(pool).call{value: 50 quanta}(""); - assertTrue(success); - - // withdrawalReserve unchanged (receive is no-op) - assertEq(pool.withdrawalReserve(), reserveBefore); - - // syncRewards picks it up as rewards - pool.syncRewards(); - assertEq(pool.totalRewardsReceived(), 50 quanta); - } - - function test_Receive_DetectedAsRewardsBySyncRewards() public { - // Deposit first so there's an existing totalPooledQRL baseline - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - - // Send ETH directly — receive() is a no-op, no event emitted - (bool success,) = address(pool).call{value: 50 quanta}(""); - assertTrue(success); - - // syncRewards detects the 50 quanta increase as rewards - vm.expectEmit(true, true, true, true); - emit RewardsSynced(50 quanta, 150 quanta, block.number); - pool.syncRewards(); - } - - function test_FundWithdrawalReserve() public { - // Need liquid pooled QRL to earmark. - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - - uint256 reserveBefore = pool.withdrawalReserve(); - uint256 pooledBefore = token.totalPooledQRL(); - - pool.fundWithdrawalReserve(50 quanta); - - assertEq(pool.withdrawalReserve(), reserveBefore + 50 quanta); - assertEq(token.totalPooledQRL(), pooledBefore); - assertEq(pool.bufferedQRL(), 50 quanta); - } - - function test_FundWithdrawalReserve_EmitsEvent() public { - // Need liquid pooled QRL to earmark. - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - - vm.expectEmit(false, false, false, true); - emit WithdrawalReserveFunded(50 quanta); - pool.fundWithdrawalReserve(50 quanta); - } - - // ========================================================================= - // MULTI-USER SCENARIOS - // ========================================================================= - - function test_MultipleUsersWithdrawalQueue() public { - // User1 and User2 both deposit - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - - vm.prank(user2); - pool.deposit{value: 100 quanta}(); - - // Verify initial state - assertEq(token.totalPooledQRL(), 200 quanta); - assertEq(token.totalShares(), 200 quanta); - - // Both users request withdrawals at the current 1:1 estimate. - vm.prank(user1); - pool.requestWithdrawal(50 quanta); - - vm.prank(user2); - pool.requestWithdrawal(50 quanta); - - assertEq(pool.totalWithdrawalShares(), 100 quanta); - - // Earmark enough liquid QRL for both claims. - pool.fundWithdrawalReserve(100 quanta); - - // Reserved assets and queued shares both remain in conversion totals. - assertEq(token.totalPooledQRL(), 200 quanta); - assertEq(pool.withdrawalReserve(), 100 quanta); - - // Wait for delay - vm.roll(block.number + 129); - - // User1 claims - should receive exactly 50 quanta - uint256 user1BalanceBefore = user1.balance; - vm.prank(user1); - uint256 user1Claimed = pool.claimWithdrawal(); - assertEq(user1Claimed, 50 quanta); - assertEq(user1.balance - user1BalanceBefore, 50 quanta); - - // User2 claims - should also receive exactly 50 quanta - uint256 user2BalanceBefore = user2.balance; - vm.prank(user2); - uint256 user2Claimed = pool.claimWithdrawal(); - assertEq(user2Claimed, 50 quanta); - assertEq(user2.balance - user2BalanceBefore, 50 quanta); - - // Queue should be empty - assertEq(pool.totalWithdrawalShares(), 0); - } - - function test_RewardsDistributedProportionally() public { - // User1 deposits 100 QRL - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - - // User2 deposits 200 QRL - vm.prank(user2); - pool.deposit{value: 200 quanta}(); - - // Add 30 QRL rewards (10% of 300) - vm.deal(address(pool), 330 quanta); - pool.syncRewards(); - - // User1 has 100/300 = 33.33% of shares -> 33.33% of 330 = 110 QRL (approx) - assertApproxEqRel(token.getQRLValue(user1), 110 quanta, 1e14); - - // User2 has 200/300 = 66.67% of shares -> 66.67% of 330 = 220 QRL (approx) - assertApproxEqRel(token.getQRLValue(user2), 220 quanta, 1e14); - } - - // ========================================================================= - // MIN DEPOSIT FLOOR TESTS - // ========================================================================= - - function test_SetMinDepositFloor() public { - // Default floor is 100 quanta - assertEq(pool.minDepositFloor(), 100 quanta); - - // Owner can lower the floor - pool.setMinDepositFloor(1 quanta); - assertEq(pool.minDepositFloor(), 1 quanta); - - // Owner can raise it back - pool.setMinDepositFloor(50 quanta); - assertEq(pool.minDepositFloor(), 50 quanta); - } - - function test_SetMinDepositFloor_BelowAbsoluteMin_Reverts() public { - // Cannot set floor below ABSOLUTE_MIN_DEPOSIT (0.001 quanta) - vm.expectRevert(DepositPoolV2.BelowAbsoluteMin.selector); - pool.setMinDepositFloor(0.0001 quanta); - - // Zero also reverts - vm.expectRevert(DepositPoolV2.BelowAbsoluteMin.selector); - pool.setMinDepositFloor(0); - } - - function test_SetMinDepositFloor_NotOwner_Reverts() public { - vm.prank(user1); - vm.expectRevert(DepositPoolV2.NotOwner.selector); - pool.setMinDepositFloor(1 quanta); - } - - function test_SetMinDepositFloor_EmitsEvent() public { - vm.expectEmit(false, false, false, true); - emit MinDepositFloorUpdated(1 quanta); - pool.setMinDepositFloor(1 quanta); - } - - function test_SetMinDeposit_AfterFloorLowered() public { - // Lower the floor first - pool.setMinDepositFloor(1 quanta); - assertEq(pool.minDepositFloor(), 1 quanta); - - // Now we can lower minDeposit below the old 100 quanta floor - pool.setMinDeposit(5 quanta); - assertEq(pool.minDeposit(), 5 quanta); - - // Deposits at the new lower minimum work - vm.deal(user1, 10 quanta); - vm.prank(user1); - uint256 shares = pool.deposit{value: 5 quanta}(); - assertEq(shares, 5 quanta); - - // Still cannot go below the new floor - vm.expectRevert(DepositPoolV2.BelowMinDepositFloor.selector); - pool.setMinDeposit(0.5 quanta); - } - - // ========================================================================= - // fundValidator() — REAL BEACON PATH - // ========================================================================= - // These lock in the QRL-specific bits of the beacon deposit call that - // differ from Ethereum muscle memory: the 0x00 withdrawal-prefix byte - // (ExecutionAddressWithdrawalPrefixByte in qrysm), the 2592-byte ML-DSA - // pubkey, and the 4627-byte ML-DSA-87 signature. - - address constant DEPOSIT_CONTRACT_ADDR = Q4242424242424242424242424242424242424242; - - function _stubDepositContract() internal { - // Minimal bytecode at 0x4242...: STOP (0x00) — accepts any call, reverts - // none, returns nothing. Enough to satisfy IDepositContract.deposit() - // as long as we don't care about the receipt/event side-effects. - vm.etch(DEPOSIT_CONTRACT_ADDR, hex"00"); - } - - function _fundBufferForValidator() internal { - vm.deal(user1, 40000 quanta); - vm.prank(user1); - pool.deposit{value: 40000 quanta}(); - } - - function _validCredentials() internal view returns (bytes memory) { - // 0x00 prefix + 11 zero bytes + 20-byte pool address = 32 bytes total. - return abi.encodePacked(bytes1(0x00), bytes11(0), address(pool)); - } - - function _validPubkey() internal pure returns (bytes memory) { - bytes memory pk = new bytes(2592); - pk[0] = 0xab; // arbitrary non-zero so it's distinguishable - return pk; - } - - function _validSignature() internal pure returns (bytes memory) { - bytes memory sig = new bytes(4627); - sig[0] = 0xcd; - return sig; - } - - function test_FundValidator_AcceptsZeroPrefix() public { - _stubDepositContract(); - _fundBufferForValidator(); - - uint256 validatorId = - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - - assertEq(validatorId, 0); - assertEq(pool.validatorCount(), 1); - assertEq(pool.bufferedQRL(), 0); - } - - function test_FundValidator_RejectsEthereumOnePrefix() public { - // Regression guard: Ethereum's 0x01 prefix (BLS_WITHDRAWAL_PREFIX +1) - // must NOT validate. Before the 0x00 fix, this test would have passed - // against the buggy contract. - _stubDepositContract(); - _fundBufferForValidator(); - - bytes memory badCreds = abi.encodePacked(bytes1(0x01), bytes11(0), address(pool)); - - vm.expectRevert(DepositPoolV2.InvalidWithdrawalCredentials.selector); - pool.fundValidator(_validPubkey(), badCreds, _validSignature(), bytes32(uint256(1))); - } - - function test_FundValidator_RejectsWrongContractAddress() public { - _stubDepositContract(); - _fundBufferForValidator(); - - // Correct 0x00 prefix + 11 zero bytes, but a different 20-byte address. - // A user-controlled address here would redirect withdrawals away from - // the pool — the guard must catch it. - bytes memory badCreds = abi.encodePacked(bytes1(0x00), bytes11(0), address(0xdead)); - - vm.expectRevert(DepositPoolV2.InvalidWithdrawalCredentials.selector); - pool.fundValidator(_validPubkey(), badCreds, _validSignature(), bytes32(uint256(1))); - } - - function test_FundValidator_RejectsShortCredentials() public { - _stubDepositContract(); - _fundBufferForValidator(); - - bytes memory shortCreds = abi.encodePacked(bytes1(0x00), bytes11(0)); // only 12 bytes - - vm.expectRevert(DepositPoolV2.InvalidCredentialsLength.selector); - pool.fundValidator(_validPubkey(), shortCreds, _validSignature(), bytes32(uint256(1))); - } - - function test_FundValidator_RejectsWrongPubkeyLength() public { - _stubDepositContract(); - _fundBufferForValidator(); - - bytes memory shortPubkey = new bytes(2591); - - vm.expectRevert(DepositPoolV2.InvalidPubkeyLength.selector); - pool.fundValidator(shortPubkey, _validCredentials(), _validSignature(), bytes32(uint256(1))); - } - - function test_FundValidator_RejectsWrongSignatureLength() public { - _stubDepositContract(); - _fundBufferForValidator(); - - bytes memory shortSig = new bytes(4626); - - vm.expectRevert(DepositPoolV2.InvalidSignatureLength.selector); - pool.fundValidator(_validPubkey(), _validCredentials(), shortSig, bytes32(uint256(1))); - } - - function test_FundValidator_RequiresFullBuffer() public { - _stubDepositContract(); - vm.deal(user1, 39999 quanta); - vm.prank(user1); - pool.deposit{value: 39999 quanta}(); - - vm.expectRevert(DepositPoolV2.InsufficientBuffer.selector); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - } - - function test_FundValidator_OnlyOwner() public { - _stubDepositContract(); - _fundBufferForValidator(); - - vm.prank(user1); - vm.expectRevert(DepositPoolV2.NotOwner.selector); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - } - - function test_FundValidator_ForwardsStakeToDepositContract() public { - _stubDepositContract(); - _fundBufferForValidator(); - - uint256 beaconBalanceBefore = DEPOSIT_CONTRACT_ADDR.balance; - uint256 poolBalanceBefore = address(pool).balance; - - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - - assertEq(DEPOSIT_CONTRACT_ADDR.balance - beaconBalanceBefore, 40000 quanta); - assertEq(poolBalanceBefore - address(pool).balance, 40000 quanta); - } - - // ========================================================================= - // OFF-CONTRACT STAKE ACCOUNTING (stakedQRL) - // ========================================================================= - // fundValidator() forwards 40k QRL to the beacon deposit contract, so the - // pool's own balance drops by the stake. _syncRewards() must add stakedQRL - // back when reconciling, otherwise the funding tx reads as a slashing event - // and the exchange rate collapses. These tests lock that in. - - function test_FundValidator_TracksStakedQRL() public { - _stubDepositContract(); - _fundBufferForValidator(); - - assertEq(pool.stakedQRL(), 0); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - - // Principal left the contract but is still pooled. - assertEq(pool.stakedQRL(), 40000 quanta); - assertEq(address(pool).balance, 0); - assertEq(token.totalPooledQRL(), 40000 quanta); - } - - function test_SyncAfterFundValidator_NoPhantomSlashing() public { - _stubDepositContract(); - _fundBufferForValidator(); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - - // Contract balance is now 0, stakedQRL is 40k. A naive balance-only - // sync would report a 40k slashing loss here. - uint256 pooledBefore = token.totalPooledQRL(); - pool.syncRewards(); - - assertEq(token.totalPooledQRL(), pooledBefore, "pooled must not change"); - assertEq(pool.totalSlashingLosses(), 0, "no phantom slashing"); - assertEq(pool.totalRewardsReceived(), 0, "no phantom rewards"); - // Exchange rate stays ~1:1 (depositor's value preserved). - assertApproxEqRel(token.getQRLValue(user1), 40000 quanta, 1e14); - } - - function test_RewardsArriveWhileStaked_AttributedAsRewards() public { - _stubDepositContract(); - _fundBufferForValidator(); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - - // 50 QRL of beacon rewards sweep in via EIP-4895 (modeled as a balance - // bump on top of the off-contract stake). - vm.deal(address(pool), 50 quanta); - - vm.expectEmit(true, true, true, true); - emit RewardsSynced(50 quanta, 40050 quanta, block.number); - pool.syncRewards(); - - assertEq(token.totalPooledQRL(), 40050 quanta); - assertEq(pool.totalRewardsReceived(), 50 quanta); - assertEq(pool.stakedQRL(), 40000 quanta, "stake unchanged by reward sweep"); - } - - function test_RecordValidatorExit_SettlesReturnedPrincipal() public { - _stubDepositContract(); - _fundBufferForValidator(); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - - // Validator exits: 40k principal returns to the contract balance. - vm.deal(address(pool), 40000 quanta); - - // Owner reconciles: principal moves from off-contract stakedQRL back - // into the on-contract balance. totalPooledQRL stays put. - vm.expectEmit(true, true, true, true); - emit ValidatorExitRecorded(40000 quanta, 0); - pool.recordValidatorExit(40000 quanta); - - assertEq(pool.stakedQRL(), 0); - - // Without the recordValidatorExit() above, this sync would have double - // counted the returned principal as 40k of rewards. - pool.syncRewards(); - assertEq(token.totalPooledQRL(), 40000 quanta); - assertEq(pool.totalRewardsReceived(), 0, "principal must not count as rewards"); - assertEq(pool.bufferedQRL(), 40000 quanta, "returned principal must refill buffer"); - } - - function test_RecordValidatorExit_RemainderCanFundNextValidator() public { - _stubDepositContract(); - _fundBufferForValidator(); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - - vm.prank(user1); - pool.requestWithdrawal(5000 quanta); - - vm.deal(address(pool), 40000 quanta); - pool.recordValidatorExit(40000 quanta); - pool.fundWithdrawalReserve(5000 quanta); - - assertEq(pool.bufferedQRL(), 35000 quanta); - assertEq(pool.bufferedQRLInReserve(), 5000 quanta); - - vm.roll(block.number + pool.WITHDRAWAL_DELAY()); - vm.prank(user1); - assertEq(pool.claimWithdrawal(), 5000 quanta); - - vm.deal(user2, 5000 quanta); - vm.prank(user2); - pool.deposit{value: 5000 quanta}(); - - assertEq(pool.bufferedQRL(), 40000 quanta); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(2))); - assertEq(pool.stakedQRL(), 40000 quanta); - assertEq(address(pool).balance, 0); - } - - function testFuzz_RecordValidatorExit_BuffersOnlyObservedPrincipal(uint256 returnedAmount) public { - returnedAmount = bound(returnedAmount, 0, 80000 quanta); - - _stubDepositContract(); - _fundBufferForValidator(); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - - vm.deal(address(pool), returnedAmount); - pool.recordValidatorExit(40000 quanta); - - uint256 expectedBuffer = returnedAmount < 40000 quanta ? returnedAmount : 40000 quanta; - assertEq(pool.bufferedQRL(), expectedBuffer); - assertLe(pool.bufferedQRL(), address(pool).balance); - - pool.syncRewards(); - assertEq(token.totalPooledQRL(), returnedAmount); - } - - function test_RecordValidatorExit_OnlyOwner() public { - _stubDepositContract(); - _fundBufferForValidator(); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - - vm.prank(user1); - vm.expectRevert(DepositPoolV2.NotOwner.selector); - pool.recordValidatorExit(40000 quanta); - } - - function test_RecordValidatorExit_RejectsZero() public { - vm.expectRevert(DepositPoolV2.ZeroAmount.selector); - pool.recordValidatorExit(0); - } - - function test_RecordValidatorExit_RejectsAboveStaked() public { - _stubDepositContract(); - _fundBufferForValidator(); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - - vm.expectRevert(DepositPoolV2.ExceedsStakedAmount.selector); - pool.recordValidatorExit(40000 quanta + 1); - } - - function test_EmergencyWithdraw_BlocksUnsyncedRewardsWhileStaked() public { - _stubDepositContract(); - _fundBufferForValidator(); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - - // A 5 QRL balance delta may be a validator reward, so emergency - // recovery must wait for owner accounting and cannot extract it. - vm.deal(address(pool), 5 quanta); - - address recipient = address(0xBEEF); - vm.expectRevert(DepositPoolV2.AccountingNotSettled.selector); - pool.emergencyWithdraw(recipient, 5 quanta); - - pool.syncRewards(); - vm.expectRevert(DepositPoolV2.ExceedsRecoverableAmount.selector); - pool.emergencyWithdraw(recipient, 5 quanta); - assertEq(address(pool).balance, 5 quanta); - assertEq(recipient.balance, 0); - } - - // ========================================================================= - // PHANTOM-REWARD FRONT-RUN PROTECTION (validator exits) - // ========================================================================= - // While principal is staked off-contract (stakedQRL > 0), an exit sweep - // returns principal to the balance a block before the owner can settle it - // with recordValidatorExit(). A permissionless sync in that window would - // book the principal as a phantom reward and spike the rate. Permissionless - // reward sync and claims with unsettled deltas are disabled whenever - // stakedQRL > 0. With stakedQRL == 0, sync stays fully permissionless. - - function test_SyncRewards_PermissionlessWhenNoStake() public { - // No off-contract principal: anyone may sync (trustless path intact). - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - vm.deal(address(pool), 110 quanta); // 10 QRL of rewards arrive - - vm.prank(user2); - pool.syncRewards(); - - assertEq(token.totalPooledQRL(), 110 quanta); - assertEq(pool.totalRewardsReceived(), 10 quanta); - } - - function test_SyncRewards_OwnerOnlyWhileStaked() public { - _stubDepositContract(); - _fundBufferForValidator(); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - - // Principal is now off-contract — permissionless sync is blocked. - vm.prank(user2); - vm.expectRevert(DepositPoolV2.NotOwner.selector); - pool.syncRewards(); - - // The owner can still sync (no revert). - pool.syncRewards(); - } - - function test_PhantomReward_FrontRunBlockedDuringExit() public { - _stubDepositContract(); - vm.deal(user1, 80000 quanta); - vm.prank(user1); - pool.deposit{value: 80000 quanta}(); - - vm.prank(user1); - (, uint256 estimate) = pool.requestWithdrawal(40000 quanta); - assertEq(estimate, 40000 quanta); - pool.fundWithdrawalReserve(40000 quanta); - - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - vm.roll(block.number + pool.WITHDRAWAL_DELAY()); - - // Exit principal lands before owner settlement: balance = 80k and - // stakedQRL = 40k, which would incorrectly read as 120k pooled. - vm.deal(address(pool), 80000 quanta); - - vm.prank(user2); - vm.expectRevert(DepositPoolV2.NotOwner.selector); - pool.syncRewards(); - - vm.prank(user1); - vm.expectRevert(DepositPoolV2.AccountingNotSettled.selector); - pool.claimWithdrawal(); - - pool.recordValidatorExit(40000 quanta); - vm.prank(user1); - assertEq(pool.claimWithdrawal(), 40000 quanta); - assertEq(pool.totalRewardsReceived(), 0, "no phantom reward recognized"); - assertEq(token.totalPooledQRL(), 40000 quanta, "remaining rate unchanged"); - } - - function test_ExitSettlement_ThenPermissionlessSyncResumes() public { - _stubDepositContract(); - _fundBufferForValidator(); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - vm.deal(address(pool), 40000 quanta); // principal returned - - // Owner settles — cannot be front-run (see test above) — clearing stake. - pool.recordValidatorExit(40000 quanta); - assertEq(pool.stakedQRL(), 0); - - // Permissionless sync works again and books no phantom reward. - vm.prank(user2); - pool.syncRewards(); - assertEq(token.totalPooledQRL(), 40000 quanta); - assertEq(pool.totalRewardsReceived(), 0, "principal must not count as rewards"); - } - - function test_OwnerSync_RecognizesGenuineRewardsWhileStaked() public { - _stubDepositContract(); - _fundBufferForValidator(); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - - // 50 QRL of genuine beacon rewards sweep in on top of the stake. - vm.deal(address(pool), 50 quanta); - - // Still blocked for the public... - vm.prank(user2); - vm.expectRevert(DepositPoolV2.NotOwner.selector); - pool.syncRewards(); - - // ...but the owner recognizes the real rewards under controlled timing. - pool.syncRewards(); - assertEq(token.totalPooledQRL(), 40050 quanta); - assertEq(pool.totalRewardsReceived(), 50 quanta); - } - - // ========================================================================= - // ASSET ACCOUNTING REGRESSIONS - // ========================================================================= - - function test_ReserveFundingDoesNotDiluteSubsequentDepositor() public { - vm.deal(user1, 1000 quanta); - vm.prank(user1); - pool.deposit{value: 1000 quanta}(); - - vm.prank(user1); - (, uint256 estimate) = pool.requestWithdrawal(100 quanta); - pool.fundWithdrawalReserve(estimate); - - // Reserve funding keeps both the assets and queued shares in the rate. - assertEq(token.totalPooledQRL(), 1000 quanta); - assertEq(token.totalShares(), 1000 quanta); - - vm.prank(user2); - uint256 user2Shares = pool.deposit{value: 100 quanta}(); - assertEq(user2Shares, 100 quanta); - - vm.roll(block.number + pool.WITHDRAWAL_DELAY()); - vm.prank(user1); - assertEq(pool.claimWithdrawal(), 100 quanta); - - assertEq(token.getQRLValue(user2), 100 quanta); - assertEq(token.getQRLValue(user1), 900 quanta); - } - - function test_DepositAccountsPendingRewardsBeforeMinting() public { - vm.deal(user1, 1000 quanta); - vm.prank(user1); - pool.deposit{value: 1000 quanta}(); - - // Model a reward sweep that changes balance without invoking pool code. - vm.deal(address(pool), 1100 quanta); - - vm.prank(user2); - uint256 user2Shares = pool.deposit{value: 100 quanta}(); - - assertEq(pool.totalRewardsReceived(), 100 quanta); - assertEq(token.totalPooledQRL(), 1200 quanta); - assertLt(user2Shares, 100 quanta); - assertApproxEqAbs(token.getQRLValue(user2), 100 quanta, 2); - } - - function test_DepositWhileStakedRequiresBalanceSettlement() public { - _stubDepositContract(); - _fundBufferForValidator(); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - - vm.deal(address(pool), 100 quanta); - vm.prank(user2); - vm.expectRevert(DepositPoolV2.AccountingNotSettled.selector); - pool.deposit{value: 100 quanta}(); - - pool.syncRewards(); - vm.prank(user2); - uint256 user2Shares = pool.deposit{value: 100 quanta}(); - assertLt(user2Shares, 100 quanta); - assertApproxEqAbs(token.getQRLValue(user2), 100 quanta, 2); - } - - function test_WithdrawalReserveCannotBeFundedIntoValidator() public { - _stubDepositContract(); - vm.deal(user1, 40000 quanta); - vm.prank(user1); - uint256 shares = pool.deposit{value: 40000 quanta}(); - - vm.prank(user1); - (, uint256 estimate) = pool.requestWithdrawal(shares); - pool.fundWithdrawalReserve(estimate); - - assertEq(pool.withdrawalReserve(), 40000 quanta); - assertEq(pool.bufferedQRL(), 0); - (bool possible,) = pool.canFundValidator(); - assertFalse(possible); - - vm.expectRevert(DepositPoolV2.InsufficientBuffer.selector); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - - vm.expectRevert(DepositPoolV2.InsufficientBuffer.selector); - pool.fundValidatorMVP(); - - vm.roll(block.number + pool.WITHDRAWAL_DELAY()); - vm.prank(user1); - assertEq(pool.claimWithdrawal(), 40000 quanta); - } - - function test_RepeatedReserveFundingProtectsReserveDuringValidatorFunding() public { - _stubDepositContract(); - vm.deal(user1, 80000 quanta); - vm.prank(user1); - pool.deposit{value: 80000 quanta}(); - - vm.prank(user1); - pool.requestWithdrawal(40000 quanta); - pool.fundWithdrawalReserve(20000 quanta); - pool.fundWithdrawalReserve(20000 quanta); - - assertEq(pool.withdrawalReserve(), 40000 quanta); - assertEq(pool.bufferedQRL(), 40000 quanta); - (bool possible,) = pool.canFundValidator(); - assertTrue(possible); - - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - - assertEq(address(pool).balance, 40000 quanta); - assertEq(pool.withdrawalReserve(), 40000 quanta); - assertEq(pool.bufferedQRL(), 0); - } - - function test_ReleaseReserveAfterCancellationRestoresValidatorLiquidity() public { - _stubDepositContract(); - vm.deal(user1, 40000 quanta); - vm.prank(user1); - uint256 shares = pool.deposit{value: 40000 quanta}(); - - vm.prank(user1); - (uint256 requestId,) = pool.requestWithdrawal(shares); - pool.fundWithdrawalReserve(40000 quanta); - - assertEq(pool.bufferedQRL(), 0); - assertEq(pool.bufferedQRLInReserve(), 40000 quanta); - - vm.prank(user1); - pool.cancelWithdrawal(requestId); - pool.releaseWithdrawalReserve(40000 quanta); - - assertEq(pool.withdrawalReserve(), 0); - assertEq(pool.bufferedQRLInReserve(), 0); - assertEq(pool.bufferedQRL(), 40000 quanta); - (bool possible,) = pool.canFundValidator(); - assertTrue(possible); - - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - assertEq(address(pool).balance, 0); - } - - function test_ReleaseReserveDoesNotDoubleCountMvpStakeAsBuffer() public { - vm.deal(user1, 80000 quanta); - vm.prank(user1); - pool.deposit{value: 80000 quanta}(); - - pool.fundValidatorMVP(); - pool.fundWithdrawalReserve(60000 quanta); - - // Forty thousand came from the simulated stake and only twenty - // thousand came from the remaining validator buffer. - assertEq(pool.bufferedQRL(), 20000 quanta); - assertEq(pool.bufferedQRLInReserve(), 20000 quanta); - - pool.releaseWithdrawalReserve(60000 quanta); - assertEq(pool.withdrawalReserve(), 0); - assertEq(pool.bufferedQRL(), 40000 quanta); - - pool.fundValidatorMVP(); - vm.expectRevert(DepositPoolV2.InsufficientBuffer.selector); - pool.fundValidatorMVP(); - } - - function test_QueuedWithdrawalBearsSlashingBeforeClaim() public { - _stubDepositContract(); - token.setMinStakeBlocks(1536); - vm.deal(user1, 10000 quanta); - vm.deal(user2, 30000 quanta); - - vm.prank(user1); - uint256 user1Shares = pool.deposit{value: 10000 quanta}(); - vm.prank(user2); - pool.deposit{value: 30000 quanta}(); - pool.fundValidator(_validPubkey(), _validCredentials(), _validSignature(), bytes32(uint256(1))); - - vm.roll(block.number + token.minStakeBlocks()); - - // Only 30,000 QRL returns from the 40,000 QRL validator. - vm.deal(address(pool), 30000 quanta); - vm.prank(user1); - (, uint256 requestEstimate) = pool.requestWithdrawal(user1Shares); - assertEq(requestEstimate, 10000 quanta); - - vm.roll(block.number + pool.WITHDRAWAL_DELAY()); - vm.prank(user1); - vm.expectRevert(DepositPoolV2.AccountingNotSettled.selector); - pool.claimWithdrawal(); - - pool.recordValidatorExit(40000 quanta); - pool.syncRewards(); - uint256 settledPayout = token.getPooledQRLByShares(user1Shares); - assertApproxEqAbs(settledPayout, 7500 quanta, 100); - assertLt(settledPayout, requestEstimate); - - pool.fundWithdrawalReserve(settledPayout); - uint256 balanceBefore = user1.balance; - vm.prank(user1); - uint256 claimed = pool.claimWithdrawal(); - - assertEq(claimed, settledPayout); - assertEq(user1.balance - balanceBefore, settledPayout); - assertApproxEqAbs(token.getQRLValue(user2), 22500 quanta, 1000); - } - - // ========================================================================= - // MINIMUM STAKE LOCK TESTS - // ========================================================================= - - uint256 internal constant LOCK_BLOCKS = 1536; - - function test_MinStakeLock_RequestWithdrawal_ImmatureReverts() public { - token.setMinStakeBlocks(LOCK_BLOCKS); - - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - - // Fresh deposit cannot be queued for withdrawal - vm.prank(user1); - vm.expectRevert(DepositPoolV2.InsufficientShares.selector); - pool.requestWithdrawal(100 quanta); - - // After maturity the same request goes through - vm.roll(block.number + LOCK_BLOCKS); - vm.prank(user1); - (uint256 requestId, uint256 qrlAmount) = pool.requestWithdrawal(100 quanta); - assertEq(requestId, 0); - assertEq(qrlAmount, 100 quanta); - } - - function test_MinStakeLock_OnlyMaturedSharesSpendable() public { - token.setMinStakeBlocks(LOCK_BLOCKS); - - // First deposit matures, top-up stays immature - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - vm.roll(block.number + LOCK_BLOCKS); - - vm.prank(user1); - pool.deposit{value: 100 quanta}(); - - // The matured 100 can be requested, the immature 100 cannot - vm.prank(user1); - pool.requestWithdrawal(100 quanta); - - vm.prank(user1); - vm.expectRevert(DepositPoolV2.InsufficientShares.selector); - pool.requestWithdrawal(1 quanta); - } - - function test_MinStakeLock_OwnerBridgeExempt() public { - token.setMinStakeBlocks(LOCK_BLOCKS); - vm.deal(address(this), 200 quanta); - - // Operator bridge capital: deposit and request back out immediately - pool.deposit{value: 100 quanta}(); - assertEq(token.immatureSharesOf(address(this)), 0); - - (uint256 requestId, uint256 qrlAmount) = pool.requestWithdrawal(100 quanta); - assertEq(requestId, 0); - assertEq(qrlAmount, 100 quanta); - } - - // ========================================================================= - // EVENT DECLARATIONS - // ========================================================================= - - event MinDepositUpdated(uint256 newMinDeposit); - event MinDepositFloorUpdated(uint256 newFloor); - event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); - event ValidatorFunded(uint256 indexed validatorId, bytes pubkey, uint256 amount); - event ValidatorExitRecorded(uint256 amount, uint256 remainingStaked); - event WithdrawalReserveFunded(uint256 amount); -} diff --git a/contracts/test/hyperion/ValidatorManager.t.hyp b/contracts/test/hyperion/ValidatorManager.t.hyp deleted file mode 100644 index 97db802..0000000 --- a/contracts/test/hyperion/ValidatorManager.t.hyp +++ /dev/null @@ -1,722 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -// Generated from ../test/ValidatorManager.t.sol by scripts/sync-hyperion.js. -// Edit the Solidity source first, then re-run this script. -pragma hyperion >=0.0; - -import "forge-std/Test.hyp"; -import "../../hyperion/ValidatorManager.hyp"; - -/** - * @title ValidatorManager Tests - * @notice Unit tests for validator lifecycle management - */ -contract ValidatorManagerTest is Test { - ValidatorManager public manager; - address public owner; - address public depositPool; - address public operator; - address public randomUser; - - // Dilithium pubkey is 2592 bytes - uint256 constant PUBKEY_LENGTH = 2592; - uint256 constant VALIDATOR_STAKE = 40_000 quanta; - - // Events to test - event ValidatorRegistered(uint256 indexed validatorId, bytes pubkey, ValidatorManager.ValidatorStatus status); - event ValidatorActivated(uint256 indexed validatorId, uint256 activatedBlock); - event ValidatorExitRequested(uint256 indexed validatorId, uint256 requestBlock); - event ValidatorExited(uint256 indexed validatorId, uint256 exitedBlock); - event ValidatorSlashed(uint256 indexed validatorId, uint256 slashedBlock); - event DepositPoolSet(address indexed depositPool); - event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); - - function setUp() public { - owner = address(this); - depositPool = address(0x1); - operator = address(0x2); - randomUser = address(0x3); - - manager = new ValidatorManager(); - manager.setDepositPool(depositPool); - } - - // ========================================================================= - // HELPERS - // ========================================================================= - - function _generatePubkey(uint256 seed) internal pure returns (bytes memory) { - bytes memory pubkey = new bytes(PUBKEY_LENGTH); - for (uint256 i = 0; i < PUBKEY_LENGTH; i++) { - pubkey[i] = bytes1(uint8(uint256(keccak256(abi.encodePacked(seed, i))) % 256)); - } - return pubkey; - } - - function _registerValidator(uint256 seed) internal returns (uint256 validatorId, bytes memory pubkey) { - pubkey = _generatePubkey(seed); - vm.prank(depositPool); - validatorId = manager.registerValidator(pubkey); - } - - function _registerAndActivate(uint256 seed) internal returns (uint256 validatorId, bytes memory pubkey) { - (validatorId, pubkey) = _registerValidator(seed); - manager.activateValidator(validatorId); - } - - // ========================================================================= - // INITIALIZATION TESTS - // ========================================================================= - - function test_InitialState() public view { - assertEq(manager.owner(), owner); - assertEq(manager.depositPool(), depositPool); - assertEq(manager.totalValidators(), 0); - assertEq(manager.activeValidatorCount(), 0); - assertEq(manager.pendingValidatorCount(), 0); - assertEq(manager.VALIDATOR_STAKE(), VALIDATOR_STAKE); - } - - function test_GetStats_Initial() public view { - (uint256 total, uint256 pending, uint256 active, uint256 totalStaked) = manager.getStats(); - assertEq(total, 0); - assertEq(pending, 0); - assertEq(active, 0); - assertEq(totalStaked, 0); - } - - // ========================================================================= - // VALIDATOR REGISTRATION TESTS - // ========================================================================= - - function test_RegisterValidator() public { - bytes memory pubkey = _generatePubkey(1); - - vm.prank(depositPool); - uint256 validatorId = manager.registerValidator(pubkey); - - assertEq(validatorId, 1); - assertEq(manager.totalValidators(), 1); - assertEq(manager.pendingValidatorCount(), 1); - assertEq(manager.activeValidatorCount(), 0); - - ( - bytes memory storedPubkey, - ValidatorManager.ValidatorStatus status, - uint256 activatedBlock, - uint256 exitedBlock - ) = manager.getValidator(validatorId); - - assertEq(storedPubkey, pubkey); - assertEq(uint256(status), uint256(ValidatorManager.ValidatorStatus.Pending)); - assertEq(activatedBlock, 0); - assertEq(exitedBlock, 0); - } - - function test_RegisterValidator_EmitsEvent() public { - bytes memory pubkey = _generatePubkey(1); - - vm.expectEmit(true, false, false, true); - emit ValidatorRegistered(1, pubkey, ValidatorManager.ValidatorStatus.Pending); - - vm.prank(depositPool); - manager.registerValidator(pubkey); - } - - function test_RegisterValidator_ByOwner() public { - bytes memory pubkey = _generatePubkey(1); - uint256 validatorId = manager.registerValidator(pubkey); - assertEq(validatorId, 1); - } - - function test_RegisterValidator_NotAuthorized_Reverts() public { - bytes memory pubkey = _generatePubkey(1); - - vm.prank(randomUser); - vm.expectRevert(ValidatorManager.NotAuthorized.selector); - manager.registerValidator(pubkey); - } - - function test_RegisterValidator_InvalidPubkeyLength_Reverts() public { - bytes memory shortPubkey = new bytes(100); - - vm.prank(depositPool); - vm.expectRevert(ValidatorManager.InvalidPubkeyLength.selector); - manager.registerValidator(shortPubkey); - } - - function test_RegisterValidator_EmptyPubkey_Reverts() public { - bytes memory emptyPubkey = new bytes(0); - - vm.prank(depositPool); - vm.expectRevert(ValidatorManager.InvalidPubkeyLength.selector); - manager.registerValidator(emptyPubkey); - } - - function test_RegisterValidator_Duplicate_Reverts() public { - bytes memory pubkey = _generatePubkey(1); - - vm.prank(depositPool); - manager.registerValidator(pubkey); - - vm.prank(depositPool); - vm.expectRevert(ValidatorManager.ValidatorAlreadyExists.selector); - manager.registerValidator(pubkey); - } - - function test_RegisterValidator_MultipleValidators() public { - for (uint256 i = 1; i <= 5; i++) { - bytes memory pubkey = _generatePubkey(i); - vm.prank(depositPool); - uint256 validatorId = manager.registerValidator(pubkey); - assertEq(validatorId, i); - } - - assertEq(manager.totalValidators(), 5); - assertEq(manager.pendingValidatorCount(), 5); - } - - // ========================================================================= - // VALIDATOR ACTIVATION TESTS - // ========================================================================= - - function test_ActivateValidator() public { - (uint256 validatorId,) = _registerValidator(1); - - assertEq(manager.pendingValidatorCount(), 1); - assertEq(manager.activeValidatorCount(), 0); - - manager.activateValidator(validatorId); - - assertEq(manager.pendingValidatorCount(), 0); - assertEq(manager.activeValidatorCount(), 1); - - (, ValidatorManager.ValidatorStatus status, uint256 activatedBlock,) = manager.getValidator(validatorId); - - assertEq(uint256(status), uint256(ValidatorManager.ValidatorStatus.Active)); - assertEq(activatedBlock, block.number); - } - - function test_ActivateValidator_EmitsEvent() public { - (uint256 validatorId,) = _registerValidator(1); - - vm.expectEmit(true, false, false, true); - emit ValidatorActivated(validatorId, block.number); - - manager.activateValidator(validatorId); - } - - function test_ActivateValidator_NotOwner_Reverts() public { - (uint256 validatorId,) = _registerValidator(1); - - vm.prank(randomUser); - vm.expectRevert(ValidatorManager.NotOwner.selector); - manager.activateValidator(validatorId); - } - - function test_ActivateValidator_NotPending_Reverts() public { - (uint256 validatorId,) = _registerAndActivate(1); - - // Already active, cannot activate again - vm.expectRevert(ValidatorManager.InvalidStatusTransition.selector); - manager.activateValidator(validatorId); - } - - function test_ActivateValidator_NonExistent_Reverts() public { - // Validator 999 doesn't exist (status is None) - vm.expectRevert(ValidatorManager.InvalidStatusTransition.selector); - manager.activateValidator(999); - } - - // ========================================================================= - // BATCH ACTIVATION TESTS - // ========================================================================= - - function test_BatchActivateValidators() public { - // Register 5 validators - uint256[] memory ids = new uint256[](5); - for (uint256 i = 0; i < 5; i++) { - (ids[i],) = _registerValidator(i + 1); - } - - assertEq(manager.pendingValidatorCount(), 5); - assertEq(manager.activeValidatorCount(), 0); - - manager.batchActivateValidators(ids); - - assertEq(manager.pendingValidatorCount(), 0); - assertEq(manager.activeValidatorCount(), 5); - } - - function test_BatchActivateValidators_SkipsNonPending() public { - // Register 3 validators - (uint256 id1,) = _registerValidator(1); - (uint256 id2,) = _registerValidator(2); - (uint256 id3,) = _registerValidator(3); - - // Activate id2 individually first - manager.activateValidator(id2); - - uint256[] memory ids = new uint256[](3); - ids[0] = id1; - ids[1] = id2; // Already active, should be skipped - ids[2] = id3; - - manager.batchActivateValidators(ids); - - // All should be active now - assertEq(manager.pendingValidatorCount(), 0); - assertEq(manager.activeValidatorCount(), 3); - } - - function test_BatchActivateValidators_EmptyArray() public { - uint256[] memory ids = new uint256[](0); - manager.batchActivateValidators(ids); - // Should not revert, just do nothing - } - - function test_BatchActivateValidators_NotOwner_Reverts() public { - (uint256 validatorId,) = _registerValidator(1); - uint256[] memory ids = new uint256[](1); - ids[0] = validatorId; - - vm.prank(randomUser); - vm.expectRevert(ValidatorManager.NotOwner.selector); - manager.batchActivateValidators(ids); - } - - // ========================================================================= - // EXIT REQUEST TESTS - // ========================================================================= - - function test_RequestValidatorExit() public { - (uint256 validatorId,) = _registerAndActivate(1); - - manager.requestValidatorExit(validatorId); - - (, ValidatorManager.ValidatorStatus status,,) = manager.getValidator(validatorId); - assertEq(uint256(status), uint256(ValidatorManager.ValidatorStatus.Exiting)); - - // Counter should still show as active (exiting validators count as active until fully exited) - assertEq(manager.activeValidatorCount(), 1); - } - - function test_RequestValidatorExit_EmitsEvent() public { - (uint256 validatorId,) = _registerAndActivate(1); - - vm.expectEmit(true, false, false, true); - emit ValidatorExitRequested(validatorId, block.number); - - manager.requestValidatorExit(validatorId); - } - - function test_RequestValidatorExit_NotOwner_Reverts() public { - (uint256 validatorId,) = _registerAndActivate(1); - - vm.prank(randomUser); - vm.expectRevert(ValidatorManager.NotOwner.selector); - manager.requestValidatorExit(validatorId); - } - - function test_RequestValidatorExit_NotActive_Reverts() public { - (uint256 validatorId,) = _registerValidator(1); - - // Still pending, cannot request exit - vm.expectRevert(ValidatorManager.InvalidStatusTransition.selector); - manager.requestValidatorExit(validatorId); - } - - // ========================================================================= - // MARK EXITED TESTS - // ========================================================================= - - function test_MarkValidatorExited() public { - (uint256 validatorId,) = _registerAndActivate(1); - manager.requestValidatorExit(validatorId); - - assertEq(manager.activeValidatorCount(), 1); - - manager.markValidatorExited(validatorId); - - (, ValidatorManager.ValidatorStatus status,, uint256 exitedBlock) = manager.getValidator(validatorId); - assertEq(uint256(status), uint256(ValidatorManager.ValidatorStatus.Exited)); - assertEq(exitedBlock, block.number); - assertEq(manager.activeValidatorCount(), 0); - } - - function test_MarkValidatorExited_EmitsEvent() public { - (uint256 validatorId,) = _registerAndActivate(1); - manager.requestValidatorExit(validatorId); - - vm.expectEmit(true, false, false, true); - emit ValidatorExited(validatorId, block.number); - - manager.markValidatorExited(validatorId); - } - - function test_MarkValidatorExited_NotOwner_Reverts() public { - (uint256 validatorId,) = _registerAndActivate(1); - manager.requestValidatorExit(validatorId); - - vm.prank(randomUser); - vm.expectRevert(ValidatorManager.NotOwner.selector); - manager.markValidatorExited(validatorId); - } - - function test_MarkValidatorExited_NotExiting_Reverts() public { - (uint256 validatorId,) = _registerAndActivate(1); - - // Still active, not exiting - vm.expectRevert(ValidatorManager.InvalidStatusTransition.selector); - manager.markValidatorExited(validatorId); - } - - // ========================================================================= - // SLASHING TESTS (M-1 FIX VERIFICATION) - // ========================================================================= - - function test_MarkValidatorSlashed_FromActive() public { - (uint256 validatorId,) = _registerAndActivate(1); - - assertEq(manager.activeValidatorCount(), 1); - - manager.markValidatorSlashed(validatorId); - - (, ValidatorManager.ValidatorStatus status,, uint256 exitedBlock) = manager.getValidator(validatorId); - assertEq(uint256(status), uint256(ValidatorManager.ValidatorStatus.Slashed)); - assertEq(exitedBlock, block.number); - - // M-1 FIX: Counter should decrement when slashing from Active - assertEq(manager.activeValidatorCount(), 0); - } - - function test_MarkValidatorSlashed_FromExiting() public { - (uint256 validatorId,) = _registerAndActivate(1); - manager.requestValidatorExit(validatorId); - - assertEq(manager.activeValidatorCount(), 1); - - manager.markValidatorSlashed(validatorId); - - (, ValidatorManager.ValidatorStatus status,,) = manager.getValidator(validatorId); - assertEq(uint256(status), uint256(ValidatorManager.ValidatorStatus.Slashed)); - - // Counter should decrement - Exiting validators still count as active - assertEq(manager.activeValidatorCount(), 0); - } - - function test_MarkValidatorSlashed_MultipleActiveValidators() public { - // Register and activate 3 validators - (uint256 id1,) = _registerAndActivate(1); - (uint256 id2,) = _registerAndActivate(2); - (uint256 id3,) = _registerAndActivate(3); - - assertEq(manager.activeValidatorCount(), 3); - - // Slash the middle one - manager.markValidatorSlashed(id2); - - // M-1 FIX: Counter should be 2 now - assertEq(manager.activeValidatorCount(), 2); - - // Slash another - manager.markValidatorSlashed(id1); - assertEq(manager.activeValidatorCount(), 1); - - // Slash the last one - manager.markValidatorSlashed(id3); - assertEq(manager.activeValidatorCount(), 0); - } - - function test_MarkValidatorSlashed_EmitsEvent() public { - (uint256 validatorId,) = _registerAndActivate(1); - - vm.expectEmit(true, false, false, true); - emit ValidatorSlashed(validatorId, block.number); - - manager.markValidatorSlashed(validatorId); - } - - function test_MarkValidatorSlashed_NotOwner_Reverts() public { - (uint256 validatorId,) = _registerAndActivate(1); - - vm.prank(randomUser); - vm.expectRevert(ValidatorManager.NotOwner.selector); - manager.markValidatorSlashed(validatorId); - } - - function test_MarkValidatorSlashed_FromPending_Reverts() public { - (uint256 validatorId,) = _registerValidator(1); - - vm.expectRevert(ValidatorManager.InvalidStatusTransition.selector); - manager.markValidatorSlashed(validatorId); - } - - function test_MarkValidatorSlashed_FromExited_Reverts() public { - (uint256 validatorId,) = _registerAndActivate(1); - manager.requestValidatorExit(validatorId); - manager.markValidatorExited(validatorId); - - vm.expectRevert(ValidatorManager.InvalidStatusTransition.selector); - manager.markValidatorSlashed(validatorId); - } - - function test_MarkValidatorSlashed_AlreadySlashed_Reverts() public { - (uint256 validatorId,) = _registerAndActivate(1); - manager.markValidatorSlashed(validatorId); - - vm.expectRevert(ValidatorManager.InvalidStatusTransition.selector); - manager.markValidatorSlashed(validatorId); - } - - // ========================================================================= - // VIEW FUNCTION TESTS - // ========================================================================= - - function test_GetValidatorIdByPubkey() public { - bytes memory pubkey = _generatePubkey(42); - - vm.prank(depositPool); - uint256 validatorId = manager.registerValidator(pubkey); - - uint256 lookupId = manager.getValidatorIdByPubkey(pubkey); - assertEq(lookupId, validatorId); - } - - function test_GetValidatorIdByPubkey_NotFound() public view { - bytes memory unknownPubkey = _generatePubkey(999); - uint256 lookupId = manager.getValidatorIdByPubkey(unknownPubkey); - assertEq(lookupId, 0); - } - - function test_GetValidatorStatus() public { - bytes memory pubkey = _generatePubkey(1); - - vm.prank(depositPool); - manager.registerValidator(pubkey); - - ValidatorManager.ValidatorStatus status = manager.getValidatorStatus(pubkey); - assertEq(uint256(status), uint256(ValidatorManager.ValidatorStatus.Pending)); - } - - function test_GetValidatorStatus_NotFound() public view { - bytes memory unknownPubkey = _generatePubkey(999); - ValidatorManager.ValidatorStatus status = manager.getValidatorStatus(unknownPubkey); - assertEq(uint256(status), uint256(ValidatorManager.ValidatorStatus.None)); - } - - function test_GetStats() public { - // Register 3 validators - _registerValidator(1); - _registerValidator(2); - (uint256 id3,) = _registerValidator(3); - - // Activate 1 - manager.activateValidator(id3); - - (uint256 total, uint256 pending, uint256 active, uint256 totalStaked) = manager.getStats(); - - assertEq(total, 3); - assertEq(pending, 2); - assertEq(active, 1); - assertEq(totalStaked, VALIDATOR_STAKE); - } - - function test_GetValidatorsByStatus() public { - // Register 5 validators - _registerValidator(1); - (uint256 id2,) = _registerValidator(2); - _registerValidator(3); - (uint256 id4,) = _registerValidator(4); - _registerValidator(5); - - // Activate some - manager.activateValidator(id2); - manager.activateValidator(id4); - - // Get pending validators - uint256[] memory pendingIds = manager.getValidatorsByStatus(ValidatorManager.ValidatorStatus.Pending); - assertEq(pendingIds.length, 3); - - // Get active validators - uint256[] memory activeIds = manager.getValidatorsByStatus(ValidatorManager.ValidatorStatus.Active); - assertEq(activeIds.length, 2); - assertEq(activeIds[0], id2); - assertEq(activeIds[1], id4); - - // Request exit for one - manager.requestValidatorExit(id2); - uint256[] memory exitingIds = manager.getValidatorsByStatus(ValidatorManager.ValidatorStatus.Exiting); - assertEq(exitingIds.length, 1); - assertEq(exitingIds[0], id2); - } - - function test_GetValidatorsByStatus_None() public view { - uint256[] memory noneIds = manager.getValidatorsByStatus(ValidatorManager.ValidatorStatus.None); - assertEq(noneIds.length, 0); - } - - // ========================================================================= - // ADMIN FUNCTION TESTS - // ========================================================================= - - function test_SetDepositPool() public { - ValidatorManager newManager = new ValidatorManager(); - address newDepositPool = address(0x999); - - newManager.setDepositPool(newDepositPool); - - assertEq(newManager.depositPool(), newDepositPool); - } - - function test_SetDepositPool_EmitsEvent() public { - ValidatorManager newManager = new ValidatorManager(); - address newDepositPool = address(0x999); - - vm.expectEmit(true, false, false, false); - emit DepositPoolSet(newDepositPool); - - newManager.setDepositPool(newDepositPool); - } - - function test_SetDepositPool_NotOwner_Reverts() public { - ValidatorManager newManager = new ValidatorManager(); - - vm.prank(randomUser); - vm.expectRevert(ValidatorManager.NotOwner.selector); - newManager.setDepositPool(address(0x999)); - } - - function test_SetDepositPool_ZeroAddress_Reverts() public { - ValidatorManager newManager = new ValidatorManager(); - - vm.expectRevert(ValidatorManager.ZeroAddress.selector); - newManager.setDepositPool(address(0)); - } - - function test_TransferOwnership() public { - address newOwner = address(0x888); - - manager.transferOwnership(newOwner); - - assertEq(manager.owner(), newOwner); - } - - function test_TransferOwnership_EmitsEvent() public { - address newOwner = address(0x888); - - vm.expectEmit(true, true, false, false); - emit OwnershipTransferred(owner, newOwner); - - manager.transferOwnership(newOwner); - } - - function test_TransferOwnership_NotOwner_Reverts() public { - vm.prank(randomUser); - vm.expectRevert(ValidatorManager.NotOwner.selector); - manager.transferOwnership(address(0x888)); - } - - function test_TransferOwnership_ZeroAddress_Reverts() public { - vm.expectRevert(ValidatorManager.ZeroAddress.selector); - manager.transferOwnership(address(0)); - } - - function test_TransferOwnership_NewOwnerCanOperate() public { - address newOwner = address(0x888); - manager.transferOwnership(newOwner); - - (uint256 validatorId,) = _registerValidator(1); - - // New owner can activate - vm.prank(newOwner); - manager.activateValidator(validatorId); - - assertEq(manager.activeValidatorCount(), 1); - } - - // ========================================================================= - // FULL LIFECYCLE TEST - // ========================================================================= - - function test_FullValidatorLifecycle() public { - // 1. Register - bytes memory pubkey = _generatePubkey(1); - vm.prank(depositPool); - uint256 validatorId = manager.registerValidator(pubkey); - - (, ValidatorManager.ValidatorStatus status,,) = manager.getValidator(validatorId); - assertEq(uint256(status), uint256(ValidatorManager.ValidatorStatus.Pending)); - - // 2. Activate - manager.activateValidator(validatorId); - (, status,,) = manager.getValidator(validatorId); - assertEq(uint256(status), uint256(ValidatorManager.ValidatorStatus.Active)); - - // 3. Request exit - manager.requestValidatorExit(validatorId); - (, status,,) = manager.getValidator(validatorId); - assertEq(uint256(status), uint256(ValidatorManager.ValidatorStatus.Exiting)); - - // 4. Mark exited - manager.markValidatorExited(validatorId); - (, status,,) = manager.getValidator(validatorId); - assertEq(uint256(status), uint256(ValidatorManager.ValidatorStatus.Exited)); - } - - function test_FullValidatorLifecycle_WithSlashing() public { - // 1. Register - bytes memory pubkey = _generatePubkey(1); - vm.prank(depositPool); - uint256 validatorId = manager.registerValidator(pubkey); - - // 2. Activate - manager.activateValidator(validatorId); - assertEq(manager.activeValidatorCount(), 1); - - // 3. Slashed while active - manager.markValidatorSlashed(validatorId); - - (, ValidatorManager.ValidatorStatus status,,) = manager.getValidator(validatorId); - assertEq(uint256(status), uint256(ValidatorManager.ValidatorStatus.Slashed)); - assertEq(manager.activeValidatorCount(), 0); - } - - // ========================================================================= - // FUZZ TESTS - // ========================================================================= - - function testFuzz_RegisterMultipleValidators(uint8 count) public { - vm.assume(count > 0 && count <= 50); - - for (uint256 i = 1; i <= count; i++) { - bytes memory pubkey = _generatePubkey(i); - vm.prank(depositPool); - uint256 validatorId = manager.registerValidator(pubkey); - assertEq(validatorId, i); - } - - assertEq(manager.totalValidators(), count); - assertEq(manager.pendingValidatorCount(), count); - } - - function testFuzz_SlashingCounterCorrectness(uint8 activeCount, uint8 slashCount) public { - vm.assume(activeCount > 0 && activeCount <= 20); - vm.assume(slashCount <= activeCount); - - // Register and activate validators - uint256[] memory ids = new uint256[](activeCount); - for (uint256 i = 0; i < activeCount; i++) { - (ids[i],) = _registerAndActivate(i + 1); - } - - assertEq(manager.activeValidatorCount(), activeCount); - - // Slash some validators - for (uint256 i = 0; i < slashCount; i++) { - manager.markValidatorSlashed(ids[i]); - } - - // Verify counter is correct (M-1 fix verification) - assertEq(manager.activeValidatorCount(), activeCount - slashCount); - } -} diff --git a/contracts/test/hyperion/stQRL-v2.t.hyp b/contracts/test/hyperion/stQRL-v2.t.hyp deleted file mode 100644 index c6796b9..0000000 --- a/contracts/test/hyperion/stQRL-v2.t.hyp +++ /dev/null @@ -1,985 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -// Generated from ../test/stQRL-v2.t.sol by scripts/sync-hyperion.js. -// Edit the Solidity source first, then re-run this script. -pragma hyperion >=0.0; - -import "forge-std/Test.hyp"; -import "../../hyperion/stQRL-v2.hyp"; - -/** - * @title stQRL v2 Tests - * @notice Unit tests for the fixed-balance stQRL token - */ -contract stQRLv2Test is Test { - stQRLv2 public token; - address public owner; - address public depositPool; - address public user1; - address public user2; - - event Transfer(address indexed from, address indexed to, uint256 value); - event SharesMinted(address indexed to, uint256 sharesAmount, uint256 qrlAmount); - event SharesBurned(address indexed from, uint256 sharesAmount, uint256 qrlAmount); - event TotalPooledQRLUpdated(uint256 previousAmount, uint256 newAmount); - - function setUp() public { - owner = address(this); - depositPool = address(0x1); - user1 = address(0x2); - user2 = address(0x3); - - token = new stQRLv2(); - token.setDepositPool(depositPool); - - // Legacy tests assume freshly minted shares are immediately spendable. - // The minimum stake lock has its own dedicated test section below, - // which re-enables it per test. - token.setMinStakeBlocks(0); - } - - // ========================================================================= - // INITIALIZATION TESTS - // ========================================================================= - - function test_InitialState() public view { - assertEq(token.name(), "Staked QRL"); - assertEq(token.symbol(), "stQRL"); - assertEq(token.decimals(), 18); - assertEq(token.totalSupply(), 0); - assertEq(token.totalShares(), 0); - assertEq(token.totalPooledQRL(), 0); - assertEq(token.owner(), owner); - assertEq(token.depositPool(), depositPool); - } - - function test_InitialExchangeRate() public view { - // Before any deposits, exchange rate should be 1:1 - assertEq(token.getExchangeRate(), 1e18); - } - - // ========================================================================= - // SHARE & VALUE MATH TESTS - // ========================================================================= - - function test_FirstDeposit_OneToOneRatio() public { - uint256 amount = 100 quanta; - - // Order matters with virtual shares: mint FIRST, then update pooled - // This matches how DepositPool.deposit() works - vm.startPrank(depositPool); - uint256 shares = token.mintShares(user1, amount); - token.updateTotalPooledQRL(amount); - vm.stopPrank(); - - // First deposit should be 1:1 - assertEq(shares, amount); - assertEq(token.balanceOf(user1), amount); // balanceOf returns shares - assertEq(token.sharesOf(user1), amount); - assertEq(token.totalSupply(), amount); // totalSupply returns total shares - assertEq(token.getQRLValue(user1), amount); // QRL value equals shares at 1:1 - } - - function test_RewardsIncreaseQRLValue() public { - // Initial deposit of 100 QRL - uint256 initialDeposit = 100 quanta; - - // Mint first, then update (matches DepositPool behavior) - vm.startPrank(depositPool); - token.mintShares(user1, initialDeposit); - token.updateTotalPooledQRL(initialDeposit); - vm.stopPrank(); - - assertEq(token.balanceOf(user1), 100 quanta); // shares - assertApproxEqRel(token.getQRLValue(user1), 100 quanta, 1e14); // QRL value (tiny precision diff from virtual shares) - - // Simulate 10 QRL rewards (10% increase) - vm.prank(depositPool); - token.updateTotalPooledQRL(110 quanta); - - // User's shares remain the same (fixed-balance) - assertEq(token.balanceOf(user1), 100 quanta); - // But QRL value increases (use approx due to virtual shares precision) - assertApproxEqRel(token.getQRLValue(user1), 110 quanta, 1e14); - assertEq(token.sharesOf(user1), 100 quanta); - } - - function test_SlashingDecreasesQRLValue() public { - // Initial deposit of 100 QRL - uint256 initialDeposit = 100 quanta; - - // Mint first, then update (matches DepositPool behavior) - vm.startPrank(depositPool); - token.mintShares(user1, initialDeposit); - token.updateTotalPooledQRL(initialDeposit); - vm.stopPrank(); - - assertEq(token.balanceOf(user1), 100 quanta); // shares - assertApproxEqRel(token.getQRLValue(user1), 100 quanta, 1e14); // QRL value - - // Simulate 5% slashing (pool drops to 95 QRL) - vm.prank(depositPool); - token.updateTotalPooledQRL(95 quanta); - - // User's shares remain the same (fixed-balance) - assertEq(token.balanceOf(user1), 100 quanta); - // But QRL value decreases (use approx due to virtual shares precision) - assertApproxEqRel(token.getQRLValue(user1), 95 quanta, 1e14); - assertEq(token.sharesOf(user1), 100 quanta); - } - - function test_MultipleUsers_RewardDistribution() public { - // User1 deposits 100 QRL - // Order: mint shares FIRST (calculates at current rate), THEN update pooled - vm.startPrank(depositPool); - token.mintShares(user1, 100 quanta); - token.updateTotalPooledQRL(100 quanta); - vm.stopPrank(); - - // User2 deposits 50 QRL (total now 150 QRL) - // Same order: mint first (at 1:1 rate), then update - vm.startPrank(depositPool); - token.mintShares(user2, 50 quanta); - token.updateTotalPooledQRL(150 quanta); - vm.stopPrank(); - - // Check shares (fixed-balance: balanceOf returns shares) - assertEq(token.balanceOf(user1), 100 quanta); - assertEq(token.balanceOf(user2), 50 quanta); - - // Check QRL values before rewards (approx due to virtual shares) - assertApproxEqRel(token.getQRLValue(user1), 100 quanta, 1e14); - assertApproxEqRel(token.getQRLValue(user2), 50 quanta, 1e14); - - // Add 30 QRL rewards (20% increase, total now 180 QRL) - vm.prank(depositPool); - token.updateTotalPooledQRL(180 quanta); - - // Shares remain the same (fixed-balance) - assertEq(token.balanceOf(user1), 100 quanta); - assertEq(token.balanceOf(user2), 50 quanta); - - // QRL values should be distributed proportionally (approx due to virtual shares) - // User1 has 100/150 = 66.67% of shares -> gets 66.67% of 180 = 120 QRL - // User2 has 50/150 = 33.33% of shares -> gets 33.33% of 180 = 60 QRL - assertApproxEqRel(token.getQRLValue(user1), 120 quanta, 1e14); - assertApproxEqRel(token.getQRLValue(user2), 60 quanta, 1e14); - } - - function test_ShareConversion_AfterRewards() public { - // Deposit 100 QRL - mint first, then update - vm.startPrank(depositPool); - token.mintShares(user1, 100 quanta); - token.updateTotalPooledQRL(100 quanta); - vm.stopPrank(); - - // Add 50 QRL rewards (now 150 QRL, still 100 shares) - vm.prank(depositPool); - token.updateTotalPooledQRL(150 quanta); - - // New deposit should get fewer shares - // With virtual shares: 100 * (100e18 + 1000) / (150e18 + 1000) ≈ 66.67 shares - uint256 expectedShares = token.getSharesByPooledQRL(100 quanta); - // At rate of 1.5 QRL/share, 100 QRL ≈ 66.67 shares - assertApproxEqRel(expectedShares, 66.67 quanta, 1e16); // 1% tolerance - - // And those shares should be worth 100 QRL - assertApproxEqRel( - token.getPooledQRLByShares(expectedShares), - 100 quanta, - 1e15 // 0.1% tolerance for rounding - ); - } - - // ========================================================================= - // EDGE CASE TESTS - // ========================================================================= - - function test_ZeroShares_ReturnsZeroBalance() public view { - assertEq(token.balanceOf(user1), 0); - assertEq(token.getPooledQRLByShares(0), 0); - } - - function test_ZeroPooled_ZeroTotalShares() public view { - // Before any deposits, with virtual shares the math is: - // getSharesByPooledQRL(100e18) = 100e18 * (0 + 1000) / (0 + 1000) = 100e18 - assertEq(token.getSharesByPooledQRL(100 quanta), 100 quanta); - // getPooledQRLByShares(100e18) = 100e18 * (0 + 1000) / (0 + 1000) = 100e18 - // Virtual shares ensure 1:1 ratio even with empty pool - assertEq(token.getPooledQRLByShares(100 quanta), 100 quanta); - } - - function test_LargeNumbers() public { - uint256 largeAmount = 1_000_000_000 quanta; // 1 billion QRL - - // Mint first, then update (matches DepositPool behavior) - vm.startPrank(depositPool); - token.mintShares(user1, largeAmount); - token.updateTotalPooledQRL(largeAmount); - vm.stopPrank(); - - assertEq(token.balanceOf(user1), largeAmount); // shares - assertApproxEqRel(token.getQRLValue(user1), largeAmount, 1e14); // QRL value (approx due to virtual shares) - - // Add 10% rewards - uint256 newTotal = largeAmount + (largeAmount / 10); - vm.prank(depositPool); - token.updateTotalPooledQRL(newTotal); - - // Shares unchanged (fixed-balance) - assertEq(token.balanceOf(user1), largeAmount); - // QRL value reflects rewards (approx due to virtual shares) - assertApproxEqRel(token.getQRLValue(user1), newTotal, 1e14); - } - - function test_SmallNumbers() public { - uint256 smallAmount = 1; // 1 planck - - // Mint first, then update (matches DepositPool behavior) - vm.startPrank(depositPool); - token.mintShares(user1, smallAmount); - token.updateTotalPooledQRL(smallAmount); - vm.stopPrank(); - - assertEq(token.balanceOf(user1), smallAmount); - assertEq(token.sharesOf(user1), smallAmount); - } - - function testFuzz_ExchangeRateMath(uint256 deposit, uint256 rewardPercent) public { - // Bound inputs to reasonable ranges - deposit = bound(deposit, 1 quanta, 1_000_000_000 quanta); - rewardPercent = bound(rewardPercent, 0, 100); // 0-100% rewards - - // Mint first, then update (matches DepositPool behavior) - vm.startPrank(depositPool); - token.mintShares(user1, deposit); - token.updateTotalPooledQRL(deposit); - vm.stopPrank(); - - uint256 rewards = (deposit * rewardPercent) / 100; - uint256 newTotal = deposit + rewards; - - vm.prank(depositPool); - token.updateTotalPooledQRL(newTotal); - - // Shares unchanged (fixed-balance) - assertEq(token.balanceOf(user1), deposit); - // QRL value should equal new total (user owns all shares) - // Use approx due to tiny precision difference from virtual shares - assertApproxEqRel(token.getQRLValue(user1), newTotal, 1e14); - } - - // ========================================================================= - // ERC-20 TRANSFER TESTS - // ========================================================================= - - function test_Transfer() public { - // Setup: user1 has 100 shares - mint first, then update - vm.startPrank(depositPool); - token.mintShares(user1, 100 quanta); - token.updateTotalPooledQRL(100 quanta); - vm.stopPrank(); - - // Transfer 30 shares to user2 - vm.prank(user1); - token.transfer(user2, 30 quanta); - - assertEq(token.balanceOf(user1), 70 quanta); - assertEq(token.balanceOf(user2), 30 quanta); - } - - function test_TransferAfterRewards() public { - // Setup: user1 has 100 shares - mint first, then update - vm.startPrank(depositPool); - token.mintShares(user1, 100 quanta); - token.updateTotalPooledQRL(100 quanta); - vm.stopPrank(); - - // Add 50% rewards (user1's shares now worth 150 QRL) - vm.prank(depositPool); - token.updateTotalPooledQRL(150 quanta); - - assertEq(token.balanceOf(user1), 100 quanta); // still 100 shares - assertApproxEqRel(token.getQRLValue(user1), 150 quanta, 1e14); // worth 150 QRL (approx) - - // Transfer 50 shares (half) to user2 - vm.prank(user1); - token.transfer(user2, 50 quanta); - - // Each user has 50 shares - assertEq(token.balanceOf(user1), 50 quanta); - assertEq(token.balanceOf(user2), 50 quanta); - // Each user's shares worth 75 QRL (half of 150 total) (approx due to virtual shares) - assertApproxEqRel(token.getQRLValue(user1), 75 quanta, 1e14); - assertApproxEqRel(token.getQRLValue(user2), 75 quanta, 1e14); - } - - function test_TransferFrom() public { - // Setup: user1 has 100 shares - mint first, then update - vm.startPrank(depositPool); - token.mintShares(user1, 100 quanta); - token.updateTotalPooledQRL(100 quanta); - vm.stopPrank(); - - // user1 approves user2 - vm.prank(user1); - token.approve(user2, 50 quanta); - - // user2 transfers from user1 - vm.prank(user2); - token.transferFrom(user1, user2, 50 quanta); - - assertEq(token.balanceOf(user1), 50 quanta); - assertEq(token.balanceOf(user2), 50 quanta); - } - - // ========================================================================= - // ACCESS CONTROL TESTS - // ========================================================================= - - function test_OnlyDepositPoolCanMint() public { - vm.prank(user1); - vm.expectRevert(stQRLv2.NotDepositPool.selector); - token.mintShares(user1, 100 quanta); - } - - function test_OnlyDepositPoolCanBurn() public { - // First mint some shares - vm.startPrank(depositPool); - token.updateTotalPooledQRL(100 quanta); - token.mintShares(user1, 100 quanta); - vm.stopPrank(); - - vm.prank(user1); - vm.expectRevert(stQRLv2.NotDepositPool.selector); - token.burnShares(user1, 50 quanta); - } - - function test_OnlyDepositPoolCanUpdatePooledQRL() public { - vm.prank(user1); - vm.expectRevert(stQRLv2.NotDepositPool.selector); - token.updateTotalPooledQRL(100 quanta); - } - - function test_OnlyOwnerCanSetDepositPool() public { - vm.prank(user1); - vm.expectRevert(stQRLv2.NotOwner.selector); - token.setDepositPool(address(0x123)); - } - - function test_DepositPoolCanOnlyBeSetOnce() public { - // Already set in setUp, should revert - vm.expectRevert(stQRLv2.DepositPoolAlreadySet.selector); - token.setDepositPool(address(0x123)); - } - - // ========================================================================= - // PAUSE TESTS - // ========================================================================= - - function test_PauseBlocksTransfers() public { - // Setup - vm.startPrank(depositPool); - token.updateTotalPooledQRL(100 quanta); - token.mintShares(user1, 100 quanta); - vm.stopPrank(); - - // Pause - token.pause(); - - // Transfer should fail - vm.prank(user1); - vm.expectRevert(stQRLv2.ContractPaused.selector); - token.transfer(user2, 50 quanta); - } - - function test_UnpauseAllowsTransfers() public { - // Setup - mint first, then update (correct order) - vm.startPrank(depositPool); - token.mintShares(user1, 100 quanta); - token.updateTotalPooledQRL(100 quanta); - vm.stopPrank(); - - // Pause then unpause - token.pause(); - token.unpause(); - - // Transfer should work - vm.prank(user1); - token.transfer(user2, 50 quanta); - assertEq(token.balanceOf(user2), 50 quanta); - } - - // ========================================================================= - // APPROVE TESTS - // ========================================================================= - - function test_Approve() public { - // Setup - vm.startPrank(depositPool); - token.updateTotalPooledQRL(100 quanta); - token.mintShares(user1, 100 quanta); - vm.stopPrank(); - - // Approve - vm.prank(user1); - bool success = token.approve(user2, 50 quanta); - - assertTrue(success); - assertEq(token.allowance(user1, user2), 50 quanta); - } - - function test_Approve_ZeroAddress_Reverts() public { - vm.startPrank(depositPool); - token.updateTotalPooledQRL(100 quanta); - token.mintShares(user1, 100 quanta); - vm.stopPrank(); - - vm.prank(user1); - vm.expectRevert(stQRLv2.ZeroAddress.selector); - token.approve(address(0), 50 quanta); - } - - function test_Approve_EmitsEvent() public { - vm.startPrank(depositPool); - token.updateTotalPooledQRL(100 quanta); - token.mintShares(user1, 100 quanta); - vm.stopPrank(); - - vm.prank(user1); - vm.expectEmit(true, true, false, true); - emit Approval(user1, user2, 50 quanta); - token.approve(user2, 50 quanta); - } - - // ========================================================================= - // TRANSFER ERROR TESTS - // ========================================================================= - - function test_Transfer_ToZeroAddress_Reverts() public { - vm.startPrank(depositPool); - token.updateTotalPooledQRL(100 quanta); - token.mintShares(user1, 100 quanta); - vm.stopPrank(); - - vm.prank(user1); - vm.expectRevert(stQRLv2.ZeroAddress.selector); - token.transfer(address(0), 50 quanta); - } - - function test_Transfer_ZeroAmount_Reverts() public { - vm.startPrank(depositPool); - token.updateTotalPooledQRL(100 quanta); - token.mintShares(user1, 100 quanta); - vm.stopPrank(); - - vm.prank(user1); - vm.expectRevert(stQRLv2.ZeroAmount.selector); - token.transfer(user2, 0); - } - - function test_Transfer_InsufficientBalance_Reverts() public { - vm.startPrank(depositPool); - token.updateTotalPooledQRL(100 quanta); - token.mintShares(user1, 100 quanta); - vm.stopPrank(); - - vm.prank(user1); - vm.expectRevert(stQRLv2.InsufficientBalance.selector); - token.transfer(user2, 150 quanta); - } - - function test_Transfer_EmitsEvent() public { - // Mint first, then update (correct order) - vm.startPrank(depositPool); - token.mintShares(user1, 100 quanta); - token.updateTotalPooledQRL(100 quanta); - vm.stopPrank(); - - vm.prank(user1); - vm.expectEmit(true, true, false, true); - emit Transfer(user1, user2, 50 quanta); - token.transfer(user2, 50 quanta); - } - - // ========================================================================= - // TRANSFERFROM ERROR TESTS - // ========================================================================= - - function test_TransferFrom_ZeroAmount_Reverts() public { - // Mint first, then update (correct order) - vm.startPrank(depositPool); - token.mintShares(user1, 100 quanta); - token.updateTotalPooledQRL(100 quanta); - vm.stopPrank(); - - vm.prank(user1); - token.approve(user2, 50 quanta); - - vm.prank(user2); - vm.expectRevert(stQRLv2.ZeroAmount.selector); - token.transferFrom(user1, user2, 0); - } - - function test_TransferFrom_InsufficientAllowance_Reverts() public { - // Mint first, then update (correct order) - vm.startPrank(depositPool); - token.mintShares(user1, 100 quanta); - token.updateTotalPooledQRL(100 quanta); - vm.stopPrank(); - - vm.prank(user1); - token.approve(user2, 30 quanta); - - vm.prank(user2); - vm.expectRevert(stQRLv2.InsufficientAllowance.selector); - token.transferFrom(user1, user2, 50 quanta); - } - - function test_TransferFrom_UnlimitedAllowance() public { - // Mint first, then update (correct order) - vm.startPrank(depositPool); - token.mintShares(user1, 100 quanta); - token.updateTotalPooledQRL(100 quanta); - vm.stopPrank(); - - // Approve unlimited - vm.prank(user1); - token.approve(user2, type(uint256).max); - - // Transfer - vm.prank(user2); - token.transferFrom(user1, user2, 50 quanta); - - // Allowance should remain unlimited - assertEq(token.allowance(user1, user2), type(uint256).max); - } - - function test_TransferFrom_WhenPaused_Reverts() public { - // Mint first, then update (correct order) - vm.startPrank(depositPool); - token.mintShares(user1, 100 quanta); - token.updateTotalPooledQRL(100 quanta); - vm.stopPrank(); - - vm.prank(user1); - token.approve(user2, 50 quanta); - - token.pause(); - - vm.prank(user2); - vm.expectRevert(stQRLv2.ContractPaused.selector); - token.transferFrom(user1, user2, 50 quanta); - } - - // ========================================================================= - // MINT/BURN ERROR TESTS - // ========================================================================= - - function test_MintShares_ToZeroAddress_Reverts() public { - vm.prank(depositPool); - vm.expectRevert(stQRLv2.ZeroAddress.selector); - token.mintShares(address(0), 100 quanta); - } - - function test_MintShares_ZeroAmount_Reverts() public { - vm.prank(depositPool); - vm.expectRevert(stQRLv2.ZeroAmount.selector); - token.mintShares(user1, 0); - } - - function test_MintShares_WhenPaused_Reverts() public { - token.pause(); - - vm.prank(depositPool); - vm.expectRevert(stQRLv2.ContractPaused.selector); - token.mintShares(user1, 100 quanta); - } - - function test_MintShares_EmitsEvents() public { - // Mint first (correct order) - pool is empty so 1:1 ratio - vm.prank(depositPool); - vm.expectEmit(true, false, false, true); - emit SharesMinted(user1, 100 quanta, 100 quanta); - vm.expectEmit(true, true, false, true); - emit Transfer(address(0), user1, 100 quanta); - token.mintShares(user1, 100 quanta); - } - - function test_BurnShares_FromZeroAddress_Reverts() public { - // Mint first, then update (correct order) - vm.startPrank(depositPool); - token.mintShares(user1, 100 quanta); - token.updateTotalPooledQRL(100 quanta); - vm.stopPrank(); - - vm.prank(depositPool); - vm.expectRevert(stQRLv2.ZeroAddress.selector); - token.burnShares(address(0), 50 quanta); - } - - function test_BurnShares_ZeroAmount_Reverts() public { - // Mint first, then update (correct order) - vm.startPrank(depositPool); - token.mintShares(user1, 100 quanta); - token.updateTotalPooledQRL(100 quanta); - vm.stopPrank(); - - vm.prank(depositPool); - vm.expectRevert(stQRLv2.ZeroAmount.selector); - token.burnShares(user1, 0); - } - - function test_BurnShares_InsufficientBalance_Reverts() public { - // Mint first, then update (correct order) - vm.startPrank(depositPool); - token.mintShares(user1, 100 quanta); - token.updateTotalPooledQRL(100 quanta); - vm.stopPrank(); - - vm.prank(depositPool); - vm.expectRevert(stQRLv2.InsufficientBalance.selector); - token.burnShares(user1, 150 quanta); - } - - function test_BurnShares_WhenPaused_Reverts() public { - // Mint first, then update (correct order) - vm.startPrank(depositPool); - token.mintShares(user1, 100 quanta); - token.updateTotalPooledQRL(100 quanta); - vm.stopPrank(); - - token.pause(); - - vm.prank(depositPool); - vm.expectRevert(stQRLv2.ContractPaused.selector); - token.burnShares(user1, 50 quanta); - } - - function test_BurnShares_EmitsEvents() public { - // Mint first, then update (correct order) - vm.startPrank(depositPool); - token.mintShares(user1, 100 quanta); - token.updateTotalPooledQRL(100 quanta); - vm.stopPrank(); - - // At 1:1 rate, 50 shares = 50 QRL (with tiny virtual shares diff) - uint256 expectedQRL = token.getPooledQRLByShares(50 quanta); - - vm.prank(depositPool); - vm.expectEmit(true, false, false, true); - emit SharesBurned(user1, 50 quanta, expectedQRL); - vm.expectEmit(true, true, false, true); - emit Transfer(user1, address(0), 50 quanta); - token.burnShares(user1, 50 quanta); - } - - function test_BurnShares_ReturnsCorrectQRLAmount() public { - // Mint first, then update (correct order) - vm.startPrank(depositPool); - token.mintShares(user1, 100 quanta); - token.updateTotalPooledQRL(100 quanta); - // Add 50% rewards - token.updateTotalPooledQRL(150 quanta); - vm.stopPrank(); - - vm.prank(depositPool); - uint256 qrlAmount = token.burnShares(user1, 50 quanta); - - // 50 shares at ~1.5 QRL/share ≈ 75 QRL (approx due to virtual shares) - assertApproxEqRel(qrlAmount, 75 quanta, 1e14); - } - - // ========================================================================= - // ADMIN FUNCTION TESTS - // ========================================================================= - - function test_SetDepositPool_ZeroAddress_Reverts() public { - // Deploy fresh token without depositPool set - stQRLv2 freshToken = new stQRLv2(); - - vm.expectRevert(stQRLv2.ZeroAddress.selector); - freshToken.setDepositPool(address(0)); - } - - function test_TransferOwnership() public { - address newOwner = address(0x999); - - token.transferOwnership(newOwner); - - assertEq(token.owner(), newOwner); - } - - function test_TransferOwnership_ZeroAddress_Reverts() public { - vm.expectRevert(stQRLv2.ZeroAddress.selector); - token.transferOwnership(address(0)); - } - - function test_TransferOwnership_NotOwner_Reverts() public { - vm.prank(user1); - vm.expectRevert(stQRLv2.NotOwner.selector); - token.transferOwnership(user1); - } - - function test_TransferOwnership_EmitsEvent() public { - address newOwner = address(0x999); - - vm.expectEmit(true, true, false, false); - emit OwnershipTransferred(owner, newOwner); - token.transferOwnership(newOwner); - } - - function test_RenounceOwnership() public { - token.renounceOwnership(); - - assertEq(token.owner(), address(0)); - } - - function test_RenounceOwnership_NotOwner_Reverts() public { - vm.prank(user1); - vm.expectRevert(stQRLv2.NotOwner.selector); - token.renounceOwnership(); - } - - function test_RenounceOwnership_EmitsEvent() public { - vm.expectEmit(true, true, false, false); - emit OwnershipTransferred(owner, address(0)); - token.renounceOwnership(); - } - - function test_OnlyOwnerCanPause() public { - vm.prank(user1); - vm.expectRevert(stQRLv2.NotOwner.selector); - token.pause(); - } - - function test_OnlyOwnerCanUnpause() public { - token.pause(); - - vm.prank(user1); - vm.expectRevert(stQRLv2.NotOwner.selector); - token.unpause(); - } - - // ========================================================================= - // GETQRLVALUE TESTS - // ========================================================================= - - function test_GetQRLValue_ReturnsCorrectValue() public { - // Mint first, then update (correct order) - vm.startPrank(depositPool); - token.mintShares(user1, 100 quanta); - token.updateTotalPooledQRL(100 quanta); - vm.stopPrank(); - - assertApproxEqRel(token.getQRLValue(user1), 100 quanta, 1e14); - - // Add rewards - vm.prank(depositPool); - token.updateTotalPooledQRL(150 quanta); - - assertApproxEqRel(token.getQRLValue(user1), 150 quanta, 1e14); - } - - function test_GetQRLValue_ZeroShares() public view { - assertEq(token.getQRLValue(user1), 0); - } - - // ========================================================================= - // MINIMUM STAKE LOCK TESTS - // ========================================================================= - - uint256 internal constant LOCK_BLOCKS = 1536; - - /// @dev Enable the lock and mint shares to an account in one step - function _mintWithLock(address to, uint256 qrlAmount) internal { - token.setMinStakeBlocks(LOCK_BLOCKS); - vm.startPrank(depositPool); - token.mintShares(to, qrlAmount); - token.updateTotalPooledQRL(token.totalPooledQRL() + qrlAmount); - vm.stopPrank(); - } - - function test_MinStakeLock_DefaultValue() public { - // setUp() zeroes the lock for legacy tests; a fresh deploy carries it - stQRLv2 freshToken = new stQRLv2(); - assertEq(freshToken.minStakeBlocks(), 1536); - } - - function test_MinStakeLock_ImmatureSharesTracked() public { - _mintWithLock(user1, 100 quanta); - - assertEq(token.immatureSharesOf(user1), 100 quanta); - assertEq(token.matureAtBlockOf(user1), block.number + LOCK_BLOCKS); - } - - function test_MinStakeLock_TransferImmature_Reverts() public { - _mintWithLock(user1, 100 quanta); - - vm.prank(user1); - vm.expectRevert(stQRLv2.InsufficientMaturedShares.selector); - token.transfer(user2, 1 quanta); - } - - function test_MinStakeLock_TransferAfterMaturity() public { - _mintWithLock(user1, 100 quanta); - - vm.roll(block.number + LOCK_BLOCKS); - assertEq(token.immatureSharesOf(user1), 0); - - vm.prank(user1); - token.transfer(user2, 100 quanta); - assertEq(token.balanceOf(user2), 100 quanta); - } - - function test_MinStakeLock_MaturedPortionTransferable() public { - // First deposit matures, second stays locked - _mintWithLock(user1, 100 quanta); - vm.roll(block.number + LOCK_BLOCKS); - - vm.prank(depositPool); - token.mintShares(user1, 50 quanta); - assertEq(token.immatureSharesOf(user1), 50 quanta); - - // The matured 100 can move, the immature 50 cannot - vm.prank(user1); - token.transfer(user2, 100 quanta); - assertEq(token.balanceOf(user1), 50 quanta); - - vm.prank(user1); - vm.expectRevert(stQRLv2.InsufficientMaturedShares.selector); - token.transfer(user2, 1 quanta); - } - - function test_MinStakeLock_TopUpResetsMaturity() public { - uint256 startBlock = block.number; - _mintWithLock(user1, 60 quanta); - - // Top up 700 blocks in: immature bucket accumulates, maturity resets - vm.roll(startBlock + 700); - vm.prank(depositPool); - token.mintShares(user1, 40 quanta); - - assertEq(token.immatureSharesOf(user1), 100 quanta); - assertEq(token.matureAtBlockOf(user1), startBlock + 700 + LOCK_BLOCKS); - - // Original maturity block has passed but the bucket was reset - vm.roll(startBlock + LOCK_BLOCKS); - assertEq(token.immatureSharesOf(user1), 100 quanta); - - vm.roll(startBlock + 700 + LOCK_BLOCKS); - assertEq(token.immatureSharesOf(user1), 0); - } - - function test_MinStakeLock_TransferDoesNotAffectRecipientLock() public { - // user1 fully matured, user2 holds a fresh immature deposit - _mintWithLock(user1, 100 quanta); - vm.roll(block.number + LOCK_BLOCKS); - - vm.prank(depositPool); - token.mintShares(user2, 50 quanta); - assertEq(token.immatureSharesOf(user2), 50 quanta); - - // Receiving mature shares must not extend or grow user2's lock - vm.prank(user1); - token.transfer(user2, 30 quanta); - - assertEq(token.immatureSharesOf(user2), 50 quanta); - assertEq(token.balanceOf(user2), 80 quanta); - - // user2 can immediately move the received mature shares - vm.prank(user2); - token.transfer(user1, 30 quanta); - assertEq(token.balanceOf(user2), 50 quanta); - } - - function test_MinStakeLock_OwnerExempt() public { - token.setMinStakeBlocks(LOCK_BLOCKS); - - vm.prank(depositPool); - token.mintShares(owner, 100 quanta); - - assertEq(token.immatureSharesOf(owner), 0); - - // Owner can transfer immediately (bridge capital stays nimble) - token.transfer(user1, 100 quanta); - assertEq(token.balanceOf(user1), 100 quanta); - } - - function test_MinStakeLock_DisabledWhenZero() public { - // setUp() already set minStakeBlocks = 0 - vm.prank(depositPool); - token.mintShares(user1, 100 quanta); - - assertEq(token.immatureSharesOf(user1), 0); - - vm.prank(user1); - token.transfer(user2, 100 quanta); - assertEq(token.balanceOf(user2), 100 quanta); - } - - function test_SetMinStakeBlocks_OnlyOwner() public { - vm.prank(user1); - vm.expectRevert(stQRLv2.NotOwner.selector); - token.setMinStakeBlocks(LOCK_BLOCKS); - } - - function test_SetMinStakeBlocks_CapEnforced() public { - uint256 cap = token.MAX_MIN_STAKE_BLOCKS(); - - vm.expectRevert(stQRLv2.MinStakeBlocksTooHigh.selector); - token.setMinStakeBlocks(cap + 1); - - token.setMinStakeBlocks(cap); - assertEq(token.minStakeBlocks(), cap); - } - - function test_SetMinStakeBlocks_EmitsEvent() public { - vm.expectEmit(false, false, false, true); - emit MinStakeBlocksUpdated(0, LOCK_BLOCKS); - token.setMinStakeBlocks(LOCK_BLOCKS); - } - - function testFuzz_MinStakeLock_LockedPlusImmatureNeverExceedsBalance( - uint96 maturedRaw, - uint96 freshRaw, - uint96 lockRaw - ) public { - uint256 matured = bound(uint256(maturedRaw), 1 quanta, 1_000_000 quanta); - uint256 fresh = bound(uint256(freshRaw), 1 quanta, 1_000_000 quanta); - uint256 locked = bound(uint256(lockRaw), 0, matured); - - // Matured deposit, then a withdrawal lock on part of it, then a fresh deposit - _mintWithLock(user1, matured); - vm.roll(block.number + LOCK_BLOCKS); - - if (locked > 0) { - vm.prank(depositPool); - token.lockShares(user1, locked); - } - - vm.prank(depositPool); - token.mintShares(user1, fresh); - - uint256 balance = token.balanceOf(user1); - uint256 immature = token.immatureSharesOf(user1); - assertLe(token.lockedSharesOf(user1) + immature, balance); - - // Spendable amount transfers cleanly; one share more reverts - uint256 spendable = balance - token.lockedSharesOf(user1) - immature; - if (spendable > 0) { - vm.prank(user1); - token.transfer(user2, spendable); - } - vm.prank(user1); - vm.expectRevert(); - token.transfer(user2, 1); - } - - // ========================================================================= - // EVENT DECLARATIONS - // ========================================================================= - - event Approval(address indexed owner, address indexed spender, uint256 value); - event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); - event MinStakeBlocksUpdated(uint256 previousValue, uint256 newValue); -} diff --git a/contracts/test/stQRL-v2.t.sol b/contracts/test/stQRL-v2.t.sol deleted file mode 100644 index f027c92..0000000 --- a/contracts/test/stQRL-v2.t.sol +++ /dev/null @@ -1,983 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.24; - -import "forge-std/Test.sol"; -import "../solidity/stQRL-v2.sol"; - -/** - * @title stQRL v2 Tests - * @notice Unit tests for the fixed-balance stQRL token - */ -contract stQRLv2Test is Test { - stQRLv2 public token; - address public owner; - address public depositPool; - address public user1; - address public user2; - - event Transfer(address indexed from, address indexed to, uint256 value); - event SharesMinted(address indexed to, uint256 sharesAmount, uint256 qrlAmount); - event SharesBurned(address indexed from, uint256 sharesAmount, uint256 qrlAmount); - event TotalPooledQRLUpdated(uint256 previousAmount, uint256 newAmount); - - function setUp() public { - owner = address(this); - depositPool = address(0x1); - user1 = address(0x2); - user2 = address(0x3); - - token = new stQRLv2(); - token.setDepositPool(depositPool); - - // Legacy tests assume freshly minted shares are immediately spendable. - // The minimum stake lock has its own dedicated test section below, - // which re-enables it per test. - token.setMinStakeBlocks(0); - } - - // ========================================================================= - // INITIALIZATION TESTS - // ========================================================================= - - function test_InitialState() public view { - assertEq(token.name(), "Staked QRL"); - assertEq(token.symbol(), "stQRL"); - assertEq(token.decimals(), 18); - assertEq(token.totalSupply(), 0); - assertEq(token.totalShares(), 0); - assertEq(token.totalPooledQRL(), 0); - assertEq(token.owner(), owner); - assertEq(token.depositPool(), depositPool); - } - - function test_InitialExchangeRate() public view { - // Before any deposits, exchange rate should be 1:1 - assertEq(token.getExchangeRate(), 1e18); - } - - // ========================================================================= - // SHARE & VALUE MATH TESTS - // ========================================================================= - - function test_FirstDeposit_OneToOneRatio() public { - uint256 amount = 100 ether; - - // Order matters with virtual shares: mint FIRST, then update pooled - // This matches how DepositPool.deposit() works - vm.startPrank(depositPool); - uint256 shares = token.mintShares(user1, amount); - token.updateTotalPooledQRL(amount); - vm.stopPrank(); - - // First deposit should be 1:1 - assertEq(shares, amount); - assertEq(token.balanceOf(user1), amount); // balanceOf returns shares - assertEq(token.sharesOf(user1), amount); - assertEq(token.totalSupply(), amount); // totalSupply returns total shares - assertEq(token.getQRLValue(user1), amount); // QRL value equals shares at 1:1 - } - - function test_RewardsIncreaseQRLValue() public { - // Initial deposit of 100 QRL - uint256 initialDeposit = 100 ether; - - // Mint first, then update (matches DepositPool behavior) - vm.startPrank(depositPool); - token.mintShares(user1, initialDeposit); - token.updateTotalPooledQRL(initialDeposit); - vm.stopPrank(); - - assertEq(token.balanceOf(user1), 100 ether); // shares - assertApproxEqRel(token.getQRLValue(user1), 100 ether, 1e14); // QRL value (tiny precision diff from virtual shares) - - // Simulate 10 QRL rewards (10% increase) - vm.prank(depositPool); - token.updateTotalPooledQRL(110 ether); - - // User's shares remain the same (fixed-balance) - assertEq(token.balanceOf(user1), 100 ether); - // But QRL value increases (use approx due to virtual shares precision) - assertApproxEqRel(token.getQRLValue(user1), 110 ether, 1e14); - assertEq(token.sharesOf(user1), 100 ether); - } - - function test_SlashingDecreasesQRLValue() public { - // Initial deposit of 100 QRL - uint256 initialDeposit = 100 ether; - - // Mint first, then update (matches DepositPool behavior) - vm.startPrank(depositPool); - token.mintShares(user1, initialDeposit); - token.updateTotalPooledQRL(initialDeposit); - vm.stopPrank(); - - assertEq(token.balanceOf(user1), 100 ether); // shares - assertApproxEqRel(token.getQRLValue(user1), 100 ether, 1e14); // QRL value - - // Simulate 5% slashing (pool drops to 95 QRL) - vm.prank(depositPool); - token.updateTotalPooledQRL(95 ether); - - // User's shares remain the same (fixed-balance) - assertEq(token.balanceOf(user1), 100 ether); - // But QRL value decreases (use approx due to virtual shares precision) - assertApproxEqRel(token.getQRLValue(user1), 95 ether, 1e14); - assertEq(token.sharesOf(user1), 100 ether); - } - - function test_MultipleUsers_RewardDistribution() public { - // User1 deposits 100 QRL - // Order: mint shares FIRST (calculates at current rate), THEN update pooled - vm.startPrank(depositPool); - token.mintShares(user1, 100 ether); - token.updateTotalPooledQRL(100 ether); - vm.stopPrank(); - - // User2 deposits 50 QRL (total now 150 QRL) - // Same order: mint first (at 1:1 rate), then update - vm.startPrank(depositPool); - token.mintShares(user2, 50 ether); - token.updateTotalPooledQRL(150 ether); - vm.stopPrank(); - - // Check shares (fixed-balance: balanceOf returns shares) - assertEq(token.balanceOf(user1), 100 ether); - assertEq(token.balanceOf(user2), 50 ether); - - // Check QRL values before rewards (approx due to virtual shares) - assertApproxEqRel(token.getQRLValue(user1), 100 ether, 1e14); - assertApproxEqRel(token.getQRLValue(user2), 50 ether, 1e14); - - // Add 30 QRL rewards (20% increase, total now 180 QRL) - vm.prank(depositPool); - token.updateTotalPooledQRL(180 ether); - - // Shares remain the same (fixed-balance) - assertEq(token.balanceOf(user1), 100 ether); - assertEq(token.balanceOf(user2), 50 ether); - - // QRL values should be distributed proportionally (approx due to virtual shares) - // User1 has 100/150 = 66.67% of shares -> gets 66.67% of 180 = 120 QRL - // User2 has 50/150 = 33.33% of shares -> gets 33.33% of 180 = 60 QRL - assertApproxEqRel(token.getQRLValue(user1), 120 ether, 1e14); - assertApproxEqRel(token.getQRLValue(user2), 60 ether, 1e14); - } - - function test_ShareConversion_AfterRewards() public { - // Deposit 100 QRL - mint first, then update - vm.startPrank(depositPool); - token.mintShares(user1, 100 ether); - token.updateTotalPooledQRL(100 ether); - vm.stopPrank(); - - // Add 50 QRL rewards (now 150 QRL, still 100 shares) - vm.prank(depositPool); - token.updateTotalPooledQRL(150 ether); - - // New deposit should get fewer shares - // With virtual shares: 100 * (100e18 + 1000) / (150e18 + 1000) ≈ 66.67 shares - uint256 expectedShares = token.getSharesByPooledQRL(100 ether); - // At rate of 1.5 QRL/share, 100 QRL ≈ 66.67 shares - assertApproxEqRel(expectedShares, 66.67 ether, 1e16); // 1% tolerance - - // And those shares should be worth 100 QRL - assertApproxEqRel( - token.getPooledQRLByShares(expectedShares), - 100 ether, - 1e15 // 0.1% tolerance for rounding - ); - } - - // ========================================================================= - // EDGE CASE TESTS - // ========================================================================= - - function test_ZeroShares_ReturnsZeroBalance() public view { - assertEq(token.balanceOf(user1), 0); - assertEq(token.getPooledQRLByShares(0), 0); - } - - function test_ZeroPooled_ZeroTotalShares() public view { - // Before any deposits, with virtual shares the math is: - // getSharesByPooledQRL(100e18) = 100e18 * (0 + 1000) / (0 + 1000) = 100e18 - assertEq(token.getSharesByPooledQRL(100 ether), 100 ether); - // getPooledQRLByShares(100e18) = 100e18 * (0 + 1000) / (0 + 1000) = 100e18 - // Virtual shares ensure 1:1 ratio even with empty pool - assertEq(token.getPooledQRLByShares(100 ether), 100 ether); - } - - function test_LargeNumbers() public { - uint256 largeAmount = 1_000_000_000 ether; // 1 billion QRL - - // Mint first, then update (matches DepositPool behavior) - vm.startPrank(depositPool); - token.mintShares(user1, largeAmount); - token.updateTotalPooledQRL(largeAmount); - vm.stopPrank(); - - assertEq(token.balanceOf(user1), largeAmount); // shares - assertApproxEqRel(token.getQRLValue(user1), largeAmount, 1e14); // QRL value (approx due to virtual shares) - - // Add 10% rewards - uint256 newTotal = largeAmount + (largeAmount / 10); - vm.prank(depositPool); - token.updateTotalPooledQRL(newTotal); - - // Shares unchanged (fixed-balance) - assertEq(token.balanceOf(user1), largeAmount); - // QRL value reflects rewards (approx due to virtual shares) - assertApproxEqRel(token.getQRLValue(user1), newTotal, 1e14); - } - - function test_SmallNumbers() public { - uint256 smallAmount = 1; // 1 wei - - // Mint first, then update (matches DepositPool behavior) - vm.startPrank(depositPool); - token.mintShares(user1, smallAmount); - token.updateTotalPooledQRL(smallAmount); - vm.stopPrank(); - - assertEq(token.balanceOf(user1), smallAmount); - assertEq(token.sharesOf(user1), smallAmount); - } - - function testFuzz_ExchangeRateMath(uint256 deposit, uint256 rewardPercent) public { - // Bound inputs to reasonable ranges - deposit = bound(deposit, 1 ether, 1_000_000_000 ether); - rewardPercent = bound(rewardPercent, 0, 100); // 0-100% rewards - - // Mint first, then update (matches DepositPool behavior) - vm.startPrank(depositPool); - token.mintShares(user1, deposit); - token.updateTotalPooledQRL(deposit); - vm.stopPrank(); - - uint256 rewards = (deposit * rewardPercent) / 100; - uint256 newTotal = deposit + rewards; - - vm.prank(depositPool); - token.updateTotalPooledQRL(newTotal); - - // Shares unchanged (fixed-balance) - assertEq(token.balanceOf(user1), deposit); - // QRL value should equal new total (user owns all shares) - // Use approx due to tiny precision difference from virtual shares - assertApproxEqRel(token.getQRLValue(user1), newTotal, 1e14); - } - - // ========================================================================= - // ERC-20 TRANSFER TESTS - // ========================================================================= - - function test_Transfer() public { - // Setup: user1 has 100 shares - mint first, then update - vm.startPrank(depositPool); - token.mintShares(user1, 100 ether); - token.updateTotalPooledQRL(100 ether); - vm.stopPrank(); - - // Transfer 30 shares to user2 - vm.prank(user1); - token.transfer(user2, 30 ether); - - assertEq(token.balanceOf(user1), 70 ether); - assertEq(token.balanceOf(user2), 30 ether); - } - - function test_TransferAfterRewards() public { - // Setup: user1 has 100 shares - mint first, then update - vm.startPrank(depositPool); - token.mintShares(user1, 100 ether); - token.updateTotalPooledQRL(100 ether); - vm.stopPrank(); - - // Add 50% rewards (user1's shares now worth 150 QRL) - vm.prank(depositPool); - token.updateTotalPooledQRL(150 ether); - - assertEq(token.balanceOf(user1), 100 ether); // still 100 shares - assertApproxEqRel(token.getQRLValue(user1), 150 ether, 1e14); // worth 150 QRL (approx) - - // Transfer 50 shares (half) to user2 - vm.prank(user1); - token.transfer(user2, 50 ether); - - // Each user has 50 shares - assertEq(token.balanceOf(user1), 50 ether); - assertEq(token.balanceOf(user2), 50 ether); - // Each user's shares worth 75 QRL (half of 150 total) (approx due to virtual shares) - assertApproxEqRel(token.getQRLValue(user1), 75 ether, 1e14); - assertApproxEqRel(token.getQRLValue(user2), 75 ether, 1e14); - } - - function test_TransferFrom() public { - // Setup: user1 has 100 shares - mint first, then update - vm.startPrank(depositPool); - token.mintShares(user1, 100 ether); - token.updateTotalPooledQRL(100 ether); - vm.stopPrank(); - - // user1 approves user2 - vm.prank(user1); - token.approve(user2, 50 ether); - - // user2 transfers from user1 - vm.prank(user2); - token.transferFrom(user1, user2, 50 ether); - - assertEq(token.balanceOf(user1), 50 ether); - assertEq(token.balanceOf(user2), 50 ether); - } - - // ========================================================================= - // ACCESS CONTROL TESTS - // ========================================================================= - - function test_OnlyDepositPoolCanMint() public { - vm.prank(user1); - vm.expectRevert(stQRLv2.NotDepositPool.selector); - token.mintShares(user1, 100 ether); - } - - function test_OnlyDepositPoolCanBurn() public { - // First mint some shares - vm.startPrank(depositPool); - token.updateTotalPooledQRL(100 ether); - token.mintShares(user1, 100 ether); - vm.stopPrank(); - - vm.prank(user1); - vm.expectRevert(stQRLv2.NotDepositPool.selector); - token.burnShares(user1, 50 ether); - } - - function test_OnlyDepositPoolCanUpdatePooledQRL() public { - vm.prank(user1); - vm.expectRevert(stQRLv2.NotDepositPool.selector); - token.updateTotalPooledQRL(100 ether); - } - - function test_OnlyOwnerCanSetDepositPool() public { - vm.prank(user1); - vm.expectRevert(stQRLv2.NotOwner.selector); - token.setDepositPool(address(0x123)); - } - - function test_DepositPoolCanOnlyBeSetOnce() public { - // Already set in setUp, should revert - vm.expectRevert(stQRLv2.DepositPoolAlreadySet.selector); - token.setDepositPool(address(0x123)); - } - - // ========================================================================= - // PAUSE TESTS - // ========================================================================= - - function test_PauseBlocksTransfers() public { - // Setup - vm.startPrank(depositPool); - token.updateTotalPooledQRL(100 ether); - token.mintShares(user1, 100 ether); - vm.stopPrank(); - - // Pause - token.pause(); - - // Transfer should fail - vm.prank(user1); - vm.expectRevert(stQRLv2.ContractPaused.selector); - token.transfer(user2, 50 ether); - } - - function test_UnpauseAllowsTransfers() public { - // Setup - mint first, then update (correct order) - vm.startPrank(depositPool); - token.mintShares(user1, 100 ether); - token.updateTotalPooledQRL(100 ether); - vm.stopPrank(); - - // Pause then unpause - token.pause(); - token.unpause(); - - // Transfer should work - vm.prank(user1); - token.transfer(user2, 50 ether); - assertEq(token.balanceOf(user2), 50 ether); - } - - // ========================================================================= - // APPROVE TESTS - // ========================================================================= - - function test_Approve() public { - // Setup - vm.startPrank(depositPool); - token.updateTotalPooledQRL(100 ether); - token.mintShares(user1, 100 ether); - vm.stopPrank(); - - // Approve - vm.prank(user1); - bool success = token.approve(user2, 50 ether); - - assertTrue(success); - assertEq(token.allowance(user1, user2), 50 ether); - } - - function test_Approve_ZeroAddress_Reverts() public { - vm.startPrank(depositPool); - token.updateTotalPooledQRL(100 ether); - token.mintShares(user1, 100 ether); - vm.stopPrank(); - - vm.prank(user1); - vm.expectRevert(stQRLv2.ZeroAddress.selector); - token.approve(address(0), 50 ether); - } - - function test_Approve_EmitsEvent() public { - vm.startPrank(depositPool); - token.updateTotalPooledQRL(100 ether); - token.mintShares(user1, 100 ether); - vm.stopPrank(); - - vm.prank(user1); - vm.expectEmit(true, true, false, true); - emit Approval(user1, user2, 50 ether); - token.approve(user2, 50 ether); - } - - // ========================================================================= - // TRANSFER ERROR TESTS - // ========================================================================= - - function test_Transfer_ToZeroAddress_Reverts() public { - vm.startPrank(depositPool); - token.updateTotalPooledQRL(100 ether); - token.mintShares(user1, 100 ether); - vm.stopPrank(); - - vm.prank(user1); - vm.expectRevert(stQRLv2.ZeroAddress.selector); - token.transfer(address(0), 50 ether); - } - - function test_Transfer_ZeroAmount_Reverts() public { - vm.startPrank(depositPool); - token.updateTotalPooledQRL(100 ether); - token.mintShares(user1, 100 ether); - vm.stopPrank(); - - vm.prank(user1); - vm.expectRevert(stQRLv2.ZeroAmount.selector); - token.transfer(user2, 0); - } - - function test_Transfer_InsufficientBalance_Reverts() public { - vm.startPrank(depositPool); - token.updateTotalPooledQRL(100 ether); - token.mintShares(user1, 100 ether); - vm.stopPrank(); - - vm.prank(user1); - vm.expectRevert(stQRLv2.InsufficientBalance.selector); - token.transfer(user2, 150 ether); - } - - function test_Transfer_EmitsEvent() public { - // Mint first, then update (correct order) - vm.startPrank(depositPool); - token.mintShares(user1, 100 ether); - token.updateTotalPooledQRL(100 ether); - vm.stopPrank(); - - vm.prank(user1); - vm.expectEmit(true, true, false, true); - emit Transfer(user1, user2, 50 ether); - token.transfer(user2, 50 ether); - } - - // ========================================================================= - // TRANSFERFROM ERROR TESTS - // ========================================================================= - - function test_TransferFrom_ZeroAmount_Reverts() public { - // Mint first, then update (correct order) - vm.startPrank(depositPool); - token.mintShares(user1, 100 ether); - token.updateTotalPooledQRL(100 ether); - vm.stopPrank(); - - vm.prank(user1); - token.approve(user2, 50 ether); - - vm.prank(user2); - vm.expectRevert(stQRLv2.ZeroAmount.selector); - token.transferFrom(user1, user2, 0); - } - - function test_TransferFrom_InsufficientAllowance_Reverts() public { - // Mint first, then update (correct order) - vm.startPrank(depositPool); - token.mintShares(user1, 100 ether); - token.updateTotalPooledQRL(100 ether); - vm.stopPrank(); - - vm.prank(user1); - token.approve(user2, 30 ether); - - vm.prank(user2); - vm.expectRevert(stQRLv2.InsufficientAllowance.selector); - token.transferFrom(user1, user2, 50 ether); - } - - function test_TransferFrom_UnlimitedAllowance() public { - // Mint first, then update (correct order) - vm.startPrank(depositPool); - token.mintShares(user1, 100 ether); - token.updateTotalPooledQRL(100 ether); - vm.stopPrank(); - - // Approve unlimited - vm.prank(user1); - token.approve(user2, type(uint256).max); - - // Transfer - vm.prank(user2); - token.transferFrom(user1, user2, 50 ether); - - // Allowance should remain unlimited - assertEq(token.allowance(user1, user2), type(uint256).max); - } - - function test_TransferFrom_WhenPaused_Reverts() public { - // Mint first, then update (correct order) - vm.startPrank(depositPool); - token.mintShares(user1, 100 ether); - token.updateTotalPooledQRL(100 ether); - vm.stopPrank(); - - vm.prank(user1); - token.approve(user2, 50 ether); - - token.pause(); - - vm.prank(user2); - vm.expectRevert(stQRLv2.ContractPaused.selector); - token.transferFrom(user1, user2, 50 ether); - } - - // ========================================================================= - // MINT/BURN ERROR TESTS - // ========================================================================= - - function test_MintShares_ToZeroAddress_Reverts() public { - vm.prank(depositPool); - vm.expectRevert(stQRLv2.ZeroAddress.selector); - token.mintShares(address(0), 100 ether); - } - - function test_MintShares_ZeroAmount_Reverts() public { - vm.prank(depositPool); - vm.expectRevert(stQRLv2.ZeroAmount.selector); - token.mintShares(user1, 0); - } - - function test_MintShares_WhenPaused_Reverts() public { - token.pause(); - - vm.prank(depositPool); - vm.expectRevert(stQRLv2.ContractPaused.selector); - token.mintShares(user1, 100 ether); - } - - function test_MintShares_EmitsEvents() public { - // Mint first (correct order) - pool is empty so 1:1 ratio - vm.prank(depositPool); - vm.expectEmit(true, false, false, true); - emit SharesMinted(user1, 100 ether, 100 ether); - vm.expectEmit(true, true, false, true); - emit Transfer(address(0), user1, 100 ether); - token.mintShares(user1, 100 ether); - } - - function test_BurnShares_FromZeroAddress_Reverts() public { - // Mint first, then update (correct order) - vm.startPrank(depositPool); - token.mintShares(user1, 100 ether); - token.updateTotalPooledQRL(100 ether); - vm.stopPrank(); - - vm.prank(depositPool); - vm.expectRevert(stQRLv2.ZeroAddress.selector); - token.burnShares(address(0), 50 ether); - } - - function test_BurnShares_ZeroAmount_Reverts() public { - // Mint first, then update (correct order) - vm.startPrank(depositPool); - token.mintShares(user1, 100 ether); - token.updateTotalPooledQRL(100 ether); - vm.stopPrank(); - - vm.prank(depositPool); - vm.expectRevert(stQRLv2.ZeroAmount.selector); - token.burnShares(user1, 0); - } - - function test_BurnShares_InsufficientBalance_Reverts() public { - // Mint first, then update (correct order) - vm.startPrank(depositPool); - token.mintShares(user1, 100 ether); - token.updateTotalPooledQRL(100 ether); - vm.stopPrank(); - - vm.prank(depositPool); - vm.expectRevert(stQRLv2.InsufficientBalance.selector); - token.burnShares(user1, 150 ether); - } - - function test_BurnShares_WhenPaused_Reverts() public { - // Mint first, then update (correct order) - vm.startPrank(depositPool); - token.mintShares(user1, 100 ether); - token.updateTotalPooledQRL(100 ether); - vm.stopPrank(); - - token.pause(); - - vm.prank(depositPool); - vm.expectRevert(stQRLv2.ContractPaused.selector); - token.burnShares(user1, 50 ether); - } - - function test_BurnShares_EmitsEvents() public { - // Mint first, then update (correct order) - vm.startPrank(depositPool); - token.mintShares(user1, 100 ether); - token.updateTotalPooledQRL(100 ether); - vm.stopPrank(); - - // At 1:1 rate, 50 shares = 50 QRL (with tiny virtual shares diff) - uint256 expectedQRL = token.getPooledQRLByShares(50 ether); - - vm.prank(depositPool); - vm.expectEmit(true, false, false, true); - emit SharesBurned(user1, 50 ether, expectedQRL); - vm.expectEmit(true, true, false, true); - emit Transfer(user1, address(0), 50 ether); - token.burnShares(user1, 50 ether); - } - - function test_BurnShares_ReturnsCorrectQRLAmount() public { - // Mint first, then update (correct order) - vm.startPrank(depositPool); - token.mintShares(user1, 100 ether); - token.updateTotalPooledQRL(100 ether); - // Add 50% rewards - token.updateTotalPooledQRL(150 ether); - vm.stopPrank(); - - vm.prank(depositPool); - uint256 qrlAmount = token.burnShares(user1, 50 ether); - - // 50 shares at ~1.5 QRL/share ≈ 75 QRL (approx due to virtual shares) - assertApproxEqRel(qrlAmount, 75 ether, 1e14); - } - - // ========================================================================= - // ADMIN FUNCTION TESTS - // ========================================================================= - - function test_SetDepositPool_ZeroAddress_Reverts() public { - // Deploy fresh token without depositPool set - stQRLv2 freshToken = new stQRLv2(); - - vm.expectRevert(stQRLv2.ZeroAddress.selector); - freshToken.setDepositPool(address(0)); - } - - function test_TransferOwnership() public { - address newOwner = address(0x999); - - token.transferOwnership(newOwner); - - assertEq(token.owner(), newOwner); - } - - function test_TransferOwnership_ZeroAddress_Reverts() public { - vm.expectRevert(stQRLv2.ZeroAddress.selector); - token.transferOwnership(address(0)); - } - - function test_TransferOwnership_NotOwner_Reverts() public { - vm.prank(user1); - vm.expectRevert(stQRLv2.NotOwner.selector); - token.transferOwnership(user1); - } - - function test_TransferOwnership_EmitsEvent() public { - address newOwner = address(0x999); - - vm.expectEmit(true, true, false, false); - emit OwnershipTransferred(owner, newOwner); - token.transferOwnership(newOwner); - } - - function test_RenounceOwnership() public { - token.renounceOwnership(); - - assertEq(token.owner(), address(0)); - } - - function test_RenounceOwnership_NotOwner_Reverts() public { - vm.prank(user1); - vm.expectRevert(stQRLv2.NotOwner.selector); - token.renounceOwnership(); - } - - function test_RenounceOwnership_EmitsEvent() public { - vm.expectEmit(true, true, false, false); - emit OwnershipTransferred(owner, address(0)); - token.renounceOwnership(); - } - - function test_OnlyOwnerCanPause() public { - vm.prank(user1); - vm.expectRevert(stQRLv2.NotOwner.selector); - token.pause(); - } - - function test_OnlyOwnerCanUnpause() public { - token.pause(); - - vm.prank(user1); - vm.expectRevert(stQRLv2.NotOwner.selector); - token.unpause(); - } - - // ========================================================================= - // GETQRLVALUE TESTS - // ========================================================================= - - function test_GetQRLValue_ReturnsCorrectValue() public { - // Mint first, then update (correct order) - vm.startPrank(depositPool); - token.mintShares(user1, 100 ether); - token.updateTotalPooledQRL(100 ether); - vm.stopPrank(); - - assertApproxEqRel(token.getQRLValue(user1), 100 ether, 1e14); - - // Add rewards - vm.prank(depositPool); - token.updateTotalPooledQRL(150 ether); - - assertApproxEqRel(token.getQRLValue(user1), 150 ether, 1e14); - } - - function test_GetQRLValue_ZeroShares() public view { - assertEq(token.getQRLValue(user1), 0); - } - - // ========================================================================= - // MINIMUM STAKE LOCK TESTS - // ========================================================================= - - uint256 internal constant LOCK_BLOCKS = 1536; - - /// @dev Enable the lock and mint shares to an account in one step - function _mintWithLock(address to, uint256 qrlAmount) internal { - token.setMinStakeBlocks(LOCK_BLOCKS); - vm.startPrank(depositPool); - token.mintShares(to, qrlAmount); - token.updateTotalPooledQRL(token.totalPooledQRL() + qrlAmount); - vm.stopPrank(); - } - - function test_MinStakeLock_DefaultValue() public { - // setUp() zeroes the lock for legacy tests; a fresh deploy carries it - stQRLv2 freshToken = new stQRLv2(); - assertEq(freshToken.minStakeBlocks(), 1536); - } - - function test_MinStakeLock_ImmatureSharesTracked() public { - _mintWithLock(user1, 100 ether); - - assertEq(token.immatureSharesOf(user1), 100 ether); - assertEq(token.matureAtBlockOf(user1), block.number + LOCK_BLOCKS); - } - - function test_MinStakeLock_TransferImmature_Reverts() public { - _mintWithLock(user1, 100 ether); - - vm.prank(user1); - vm.expectRevert(stQRLv2.InsufficientMaturedShares.selector); - token.transfer(user2, 1 ether); - } - - function test_MinStakeLock_TransferAfterMaturity() public { - _mintWithLock(user1, 100 ether); - - vm.roll(block.number + LOCK_BLOCKS); - assertEq(token.immatureSharesOf(user1), 0); - - vm.prank(user1); - token.transfer(user2, 100 ether); - assertEq(token.balanceOf(user2), 100 ether); - } - - function test_MinStakeLock_MaturedPortionTransferable() public { - // First deposit matures, second stays locked - _mintWithLock(user1, 100 ether); - vm.roll(block.number + LOCK_BLOCKS); - - vm.prank(depositPool); - token.mintShares(user1, 50 ether); - assertEq(token.immatureSharesOf(user1), 50 ether); - - // The matured 100 can move, the immature 50 cannot - vm.prank(user1); - token.transfer(user2, 100 ether); - assertEq(token.balanceOf(user1), 50 ether); - - vm.prank(user1); - vm.expectRevert(stQRLv2.InsufficientMaturedShares.selector); - token.transfer(user2, 1 ether); - } - - function test_MinStakeLock_TopUpResetsMaturity() public { - uint256 startBlock = block.number; - _mintWithLock(user1, 60 ether); - - // Top up 700 blocks in: immature bucket accumulates, maturity resets - vm.roll(startBlock + 700); - vm.prank(depositPool); - token.mintShares(user1, 40 ether); - - assertEq(token.immatureSharesOf(user1), 100 ether); - assertEq(token.matureAtBlockOf(user1), startBlock + 700 + LOCK_BLOCKS); - - // Original maturity block has passed but the bucket was reset - vm.roll(startBlock + LOCK_BLOCKS); - assertEq(token.immatureSharesOf(user1), 100 ether); - - vm.roll(startBlock + 700 + LOCK_BLOCKS); - assertEq(token.immatureSharesOf(user1), 0); - } - - function test_MinStakeLock_TransferDoesNotAffectRecipientLock() public { - // user1 fully matured, user2 holds a fresh immature deposit - _mintWithLock(user1, 100 ether); - vm.roll(block.number + LOCK_BLOCKS); - - vm.prank(depositPool); - token.mintShares(user2, 50 ether); - assertEq(token.immatureSharesOf(user2), 50 ether); - - // Receiving mature shares must not extend or grow user2's lock - vm.prank(user1); - token.transfer(user2, 30 ether); - - assertEq(token.immatureSharesOf(user2), 50 ether); - assertEq(token.balanceOf(user2), 80 ether); - - // user2 can immediately move the received mature shares - vm.prank(user2); - token.transfer(user1, 30 ether); - assertEq(token.balanceOf(user2), 50 ether); - } - - function test_MinStakeLock_OwnerExempt() public { - token.setMinStakeBlocks(LOCK_BLOCKS); - - vm.prank(depositPool); - token.mintShares(owner, 100 ether); - - assertEq(token.immatureSharesOf(owner), 0); - - // Owner can transfer immediately (bridge capital stays nimble) - token.transfer(user1, 100 ether); - assertEq(token.balanceOf(user1), 100 ether); - } - - function test_MinStakeLock_DisabledWhenZero() public { - // setUp() already set minStakeBlocks = 0 - vm.prank(depositPool); - token.mintShares(user1, 100 ether); - - assertEq(token.immatureSharesOf(user1), 0); - - vm.prank(user1); - token.transfer(user2, 100 ether); - assertEq(token.balanceOf(user2), 100 ether); - } - - function test_SetMinStakeBlocks_OnlyOwner() public { - vm.prank(user1); - vm.expectRevert(stQRLv2.NotOwner.selector); - token.setMinStakeBlocks(LOCK_BLOCKS); - } - - function test_SetMinStakeBlocks_CapEnforced() public { - uint256 cap = token.MAX_MIN_STAKE_BLOCKS(); - - vm.expectRevert(stQRLv2.MinStakeBlocksTooHigh.selector); - token.setMinStakeBlocks(cap + 1); - - token.setMinStakeBlocks(cap); - assertEq(token.minStakeBlocks(), cap); - } - - function test_SetMinStakeBlocks_EmitsEvent() public { - vm.expectEmit(false, false, false, true); - emit MinStakeBlocksUpdated(0, LOCK_BLOCKS); - token.setMinStakeBlocks(LOCK_BLOCKS); - } - - function testFuzz_MinStakeLock_LockedPlusImmatureNeverExceedsBalance( - uint96 maturedRaw, - uint96 freshRaw, - uint96 lockRaw - ) public { - uint256 matured = bound(uint256(maturedRaw), 1 ether, 1_000_000 ether); - uint256 fresh = bound(uint256(freshRaw), 1 ether, 1_000_000 ether); - uint256 locked = bound(uint256(lockRaw), 0, matured); - - // Matured deposit, then a withdrawal lock on part of it, then a fresh deposit - _mintWithLock(user1, matured); - vm.roll(block.number + LOCK_BLOCKS); - - if (locked > 0) { - vm.prank(depositPool); - token.lockShares(user1, locked); - } - - vm.prank(depositPool); - token.mintShares(user1, fresh); - - uint256 balance = token.balanceOf(user1); - uint256 immature = token.immatureSharesOf(user1); - assertLe(token.lockedSharesOf(user1) + immature, balance); - - // Spendable amount transfers cleanly; one share more reverts - uint256 spendable = balance - token.lockedSharesOf(user1) - immature; - if (spendable > 0) { - vm.prank(user1); - token.transfer(user2, spendable); - } - vm.prank(user1); - vm.expectRevert(); - token.transfer(user2, 1); - } - - // ========================================================================= - // EVENT DECLARATIONS - // ========================================================================= - - event Approval(address indexed owner, address indexed spender, uint256 value); - event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); - event MinStakeBlocksUpdated(uint256 previousValue, uint256 newValue); -} diff --git a/docs/QRL-UPSTREAM-BASELINE.md b/docs/QRL-UPSTREAM-BASELINE.md new file mode 100644 index 0000000..cf09de6 --- /dev/null +++ b/docs/QRL-UPSTREAM-BASELINE.md @@ -0,0 +1,56 @@ +# QRL source baseline for the native QRL redesign + +The redesign targets unmodified QRL clients. `config/qrl-upstream-sources.json` pins the cyyber `main` heads qualified on 2026-09-15, their relevant fix ancestry, and the QRVMC submodule revision. Refresh these pins deliberately when upstream changes. A floating branch name or a published `latest` image does not identify the implementation tested. + +| Source | Revision | +|---|---| +| go-qrl | `9b404c38a63bfebe07cfd07b5f5d144d1c2cf9b9` | +| qrysm | `3b816311ac3e86b7a7af40a062ae290318554f7a` | +| Hyperion | `cee9d335984c139c1dc0b0e84ec13290031ea4b1` | +| qrvmone | `bca84a21a046bc8506ee4b7a89b31ac9e621904f` | +| QRVMC, as pinned by qrvmone | `b70fb65874ce9a50a55475f800dcb33e39a5a261` | + +Use clean, separate clones at those revisions, with directories named `go-qrl`, `qrysm`, `hyperion`, and `qrvmone` below one source root. Initialize qrvmone's pinned QRVMC submodule. The source check rejects a different revision, tracked changes, untracked source files, or submodule changes. It performs no checkout, reset, patch, or network deployment. + +```sh +node scripts/check-qrl-upstream.js /path/to/source-root +node scripts/check-qrl-upstream.js /path/to/source-root --test +node scripts/check-qrl-upstream.js /path/to/source-root --require-semantic-runtime +``` + +The optional test command runs selected, existing Go tests with two compiler processes, `-mod=readonly`, workspace overlays disabled, and Qrysm's required `develop` build tag. It checks that every selected package actually executed tests. Its JSON output states the exact scope and revisions. The ancestry checks require enough clone history to contain the listed fixes. These checks do not execute all upstream suites or establish that a running network uses these sources. + +The older binary identities in `config/hyperion-toolchain.json` describe the retained pool's existing semantic baseline. They must only change after rebuilding and qualifying the compiler and semantic runtime together. Successful source qualification alone does not qualify those binaries, the pool bytecode, or the native accounting redesign. + +## Qualification results and current test dependency + +The selected Go tests passed at these exact revisions: 44 top-level tests across go-qrl's common, ABI, and VM packages, and 37 across Qrysm's configuration, deposit-contract, and block-operation packages. Both clients used Go 1.26.5. The source check also verified the listed fix ancestry and clean working trees. + +Hyperion's compiler and semantic runner and the qrvmone library built from the pinned sources. The existing three pool contracts compiled with identical ABIs and runtime sizes of 22,768 bytes for the pool, 6,136 bytes for the manager, and 5,443 bytes for the retained token. These compilation results describe the existing architecture. + +Semantic execution is blocked for this exact compiler/runtime pairing: Hyperion's embedded `test/qrvmc/qrvmc.h` identifies `QRVMC_ZOND` as 1, while qrvmone's pinned `qrvmc/include/qrvmc/qrvmc.h` identifies it as 0. Both advertise ABI version 2, so checking that version alone would miss the mismatch. The source check reports both identifiers; `--require-semantic-runtime` rejects the combination before any semantic execution. The build products were left separate from the older pinned pool toolchain. No source patch or older-runtime substitution was used to obtain a passing result. + +The native accounting verifier also needs tests against go-qrl's actual ML-DSA precompile. The current Hyperion test host handles precompile addresses 1, 2, 4, and 5 and has no ML-DSA implementation at address 3. Signature-dependent accounting cannot be qualified using that host alone. + +QuantaPool's existing beacon call, deposit-signing helper, and semantic beacon fixtures still omit the new RANDAO commitment. Their existing tests therefore do not establish funding compatibility with this current Qrysm deposit contract. The native redesign must update that integration as one coherent change. + +## Current protocol facts + +- Addresses and QRVM words are 64 bytes. Hashes and consensus signing roots remain 32 bytes. Address width does not determine hash width. +- A full validator has 40,000 QRL effective balance. The beacon contract now enforces a 2,000 QRL minimum per deposit. The pool's minimum user deposit is a separate application parameter. +- Deposit data includes a 32-byte `randao_commitment`, in addition to the 2,592-byte public key, 64-byte withdrawal recipient, amount, and 4,627-byte signature. The signature message and deposit-data SSZ root both include the commitment. The current deposit ABI and precompile input must be used together. +- The default configuration has 128 slots per epoch, 60 seconds per slot, a 128-position sync committee, and eight epochs per committee period. Deployment must check its actual network configuration. +- A repeated public-key deposit increases the existing validator's balance. It retains the original canonical withdrawal recipient. Checking the recipient in a new deposit message alone does not prove the recipient of that existing validator. +- Consensus withdrawals credit the execution account directly. The execution withdrawal tuple has no terminal flag, and a validator with zero balance generates no withdrawal payment. A raw pool balance change is insufficient to authenticate a complete validator portfolio or distinguish every loss from every reward. +- Normal voluntary exits require the validator's signature and at least 16 active epochs under the default configuration. A pool withdrawal address does not authorize a consensus exit. Public, verified exit authorizations are a possible application-level dependency; their validator-index binding, availability, eligibility, and fork-domain validity require validation before user-funded activation. +- The proposer chooses execution transaction-fee routing separately from the validator's withdrawal recipient. A pool can account deterministically for funds it receives. A contract cannot collect transaction fees that the proposer sends elsewhere. + +These facts come from the pinned `common/types.go`, `core/vm/contracts.go`, `core/types/withdrawal.go`, `consensus/beacon/consensus.go`, and `core/state_transition.go` in go-qrl, and `config/params/mainnet_config.go`, `contracts/deposit/deposit_contract.hyp`, `beacon-chain/core/blocks/deposit.go`, `beacon-chain/core/blocks/exit.go`, `beacon-chain/state/state-native/getters_withdrawal.go`, and `beacon-chain/rpc/qrysm/v1alpha1/validator/proposer_execution_payload.go` in Qrysm. + +## Application boundaries still to implement + +An application-layer verifier may use the existing ML-DSA verification precompile, sync-committee signatures, and SSZ state commitments. QuantaPool currently has no complete implementation of that verifier. It must authenticate validator identity, canonical withdrawal routing, balances, complete portfolio checkpoints, and zero-balance terminal states. Its bootstrap, fork handling, proof freshness, transition rules, gas bounds, and data availability need explicit validation. RPC metadata and operator-supplied roots do not provide that authentication. + +The planned operator fee remains 10%, payable in native QRL to an immutable recipient from economically earned rewards. Returned principal earns no fee. Fee calculation must follow verified accounting, including loss treatment, before any fee is claimable. + +The source baseline and checks support the implemented native pool, global withdrawal queue, proof-bound user accounting and clean token removal. See [current implementation evidence](../native/IMPLEMENTATION.md). Earlier source-only test results in this document retain their original scope. Upstream source remains unchanged. diff --git a/docs/QUANTAPOOL-FEE-SECURITY-REVIEW-2026-08-28.md b/docs/QUANTAPOOL-FEE-SECURITY-REVIEW-2026-08-28.md new file mode 100644 index 0000000..c101740 --- /dev/null +++ b/docs/QUANTAPOOL-FEE-SECURITY-REVIEW-2026-08-28.md @@ -0,0 +1,240 @@ +# QuantaPool Performance Fee Security Review + +Historical review of the retired token model. Its source locations, fee shares and deployment results do not describe the native implementation. See [current architecture](architecture.md) and [native review](../native/REVIEW.md). + +Date: 2026-08-28 + +Audience: QuantaPool internal. This review is separate from the Qrysm and go-qrl audit reports. + +Evidence tier: current source review, local executable validation, and deployment/readback on a disposable loopback Q128 enclave. No production deployment was performed. + +## Executive result + +No confirmed Critical or High vulnerability remains in the reviewed fee path. The review found no permissionless principal extraction, protocol fee overmint, or callback path from fee-share issuance. + +The fixed fee is 1,000 basis points, equal to 10% or one tenth of fee-eligible validator rewards. It is collected as dilution-priced stQRL shares after complete cohort settlement, delayed principal reconciliation, whole-epoch gain capping, and prior-loss recovery. + +One High liveness defect and one Low EL/CL finality race were fixed during this review. The High defect let a single out-of-scale checkpoint slot lock every user balance permanently; it is described in QP-FEE-005, together with the checkpoint bound and quiescent reseat that also remove the residual revenue-loss case. Three residual boundaries remain: one Medium revenue-undercollection policy issue, one Low cohort-accounting policy issue, and one Informational privileged trust boundary. + +## Implemented design + +- `contracts/hyperion/DepositPool-v2.hyp:196` fixes the fee at 1,000 basis points. +- `contracts/hyperion/DepositPool-v2.hyp:869-911` separates terminal settlement from delayed fee finalization, restores pending principal first, treats the finalized physical balance as a floor, and keeps unclassified drift fee-exempt. +- `contracts/hyperion/DepositPool-v2.hyp:1234-1331` caps eligible rewards by whole-epoch net gain, recovers prior losses, carries exact rounding residue, verifies the share quote, and records the fee-finalization block. +- `contracts/hyperion/stQRL-v2.hyp:462-521` restricts fee minting to DepositPool and uses post-mint dilution pricing rounded in favor of existing holders. +- `contracts/hyperion/DepositPool-v2.hyp:1173-1215` scales pool-level carry from a fixed close checkpoint only when burns create a new historical supply minimum. +- `contracts/hyperion/DepositPool-v2.hyp:1003-1006` and `contracts/hyperion/DepositPool-v2.hyp:1321` enforce a one-block accounting cooldown after fee finalization. +- `scripts/validator-lifecycle-qip55.js:1535-1829` block-tags every reward-classification read, binds Qrysm finality to canonical EL hashes, and rejects post-finality state drift. + +## Open findings + +### QP-FEE-001: Terminal and close-window rewards can remain fee-exempt + +Severity: Medium + +Class: POLICY, revenue undercollection + +Attacker model: a permissionless donor can create balance ambiguity, or normal terminal income can arrive in the same close window as returned principal. + +Violated objective: all genuine validator rewards should contribute to the 10% fee base exactly once. + +Evidence: `contracts/hyperion/DepositPool-v2.hyp:833` closes ordinary reward recognition once delayed finalization is pending. `contracts/hyperion/DepositPool-v2.hyp:1487-1502` restores retired principal and deliberately leaves surplus unclassified. `scripts/validator-lifecycle-qip55.js:2204-2209` sets the standard finalizer's `lateEligibleAmount` to zero because finalized balance and validator state do not distinguish validator income from a direct donation. + +Bounded reproduction: if 40,000 units of terminal principal return with 8 units of reward before settlement, settlement credits 40,000 as principal and leaves 8 unclassified. The standard delayed finalizer synchronizes the 8 as fee-exempt, so the protocol collects zero fee on that reward. + +Impact: protocol revenue can be lower than the intended 10%. User principal remains protected, and this path does not overmint fee shares. + +False-positive analysis: this is an intentional fail-safe under an information-theoretic ambiguity. Charging the unidentified surplus would let a direct donor force holders to pay a fee on a donation. + +Required resolution: consume a consensus-authenticated terminal-withdrawal receipt that identifies validator, recipient, amount, and terminal status. The proposed interface is documented in `docs/TERMINAL-WITHDRAWAL-RECEIPTS.md`. + +### QP-FEE-002: Replacement capital can inherit pool-level carry + +Severity: Low + +Class: POLICY, cohort fairness + +Attacker model: a depositor enters before old shares leave, keeping aggregate supply at or above the historical minimum while the holder cohort changes. + +Violated objective: loss shelter and fee rounding residue should follow the economic cohort that created them. + +Evidence: `contracts/hyperion/DepositPool-v2.hyp:1173-1215` recalculates carry only when remaining supply reaches a new historical minimum. `contracts/hyperion/DepositPool-v2.hyp:1220-1227` stores one pool-wide close checkpoint. + +Impact: replacement capital can receive part of an older cohort's loss shelter or fee dust. The practical result is conservative fee undercollection or small holder-to-holder accounting redistribution. It does not enable principal extraction or fee overmint. + +False-positive analysis: the current behavior is deterministic, path independent for equivalent burns, and resistant to deposit-then-burn carry washing. The residual comes from pool-level attribution rather than rounding instability. + +Required resolution: use per-share or per-account equalization if exact cohort attribution is a product requirement. + +### QP-FEE-003: Reward evidence remains owner-attested metadata + +Severity: Informational + +Class: TRUST + +Attacker model: compromised or malicious owner credentials. + +Trust boundary: `recognizeValidatorRewards` and `finalizeProtocolFeeEpoch` are owner-only. On-chain evidence validation checks nonzero roots, replay protection, and monotonic slots at `contracts/hyperion/DepositPool-v2.hyp:975-995`; the contract does not verify Qrysm consensus proofs itself. + +Impact: a compromised owner can affect availability and classify real positive surplus as fee-eligible within the whole-epoch net-gain cap. The cap and physical accounting prevent unsupported principal extraction or unbounded fee minting. + +Required resolution: replace owner-attested provenance with a consensus-authenticated receipt or proof before treating reward classification as trustless. + +## Resolved finding + +### QP-FEE-004: Reward classification used an unbound EL/CL snapshot + +Original severity: Low + +Class: CWE-367 + +Original risk: an execution balance was frozen before an older finalized validator snapshot was checked. A terminal return could therefore look like reward temporarily, inflating gross reward statistics and the provisional exchange rate. The whole-epoch net-gain cap prevented fee overmint. + +Resolution: `scripts/validator-lifecycle-qip55.js:1535-1664` now reads pool, token, fee, manager aggregate, validator enumeration, validator ID, canonical exit ID, balance, and block hash at one EL block. `scripts/validator-lifecycle-qip55.js:1738-1829` waits for a fresh non-optimistic Qrysm payload at or after that block and verifies its hash against canonical EL. `scripts/validator-lifecycle-qip55.js:2998-3077` rechecks exact current state and leaves later positive inflows unclassified. + +Validation: focused tests cover exact block tags, a stale payload, bounded polling with an immutable freeze block, canonical hash mismatch, manager and fee-state drift, and permitted positive post-freeze inflow. + +### QP-FEE-005: One out-of-scale checkpoint slot permanently locked every balance + +Original severity: High + +Class: CWE-670, liveness and permanent fund lock + +Original risk: `_consumeRewardEvidence` required a strictly increasing `finalizedCheckpointSlot`, and +`lastRewardCheckpointSlot` had no reset path anywhere in the contract. `finalizeProtocolFeeEpoch` consumed +evidence through the same helper and was the only caller of `_closeProtocolFeeEpoch`, so it was the only way +to clear `feeEpochClosePending`. That flag is the gate inside `_permissionlessSyncAllowed`, which +`deposit`, `claimWithdrawal`, `syncRewards` and `emergencyWithdraw` all require. + +The consequence: one successful `recognizeValidatorRewards` call carrying a mis-scaled slot, for example a +block number, a timestamp, or a wei-scale value supplied where a beacon slot was expected, raised +`lastRewardCheckpointSlot` beyond any slot the chain would reach. Every later `finalizeProtocolFeeEpoch` +then reverted with `RewardCheckpointNotMonotonic`, the epoch stayed pending forever, and deposits, +withdrawal claims, reward synchronization and emergency recovery were all permanently closed. No attacker +was needed and no recovery path existed. The operator script checked monotonicity off chain at +`scripts/validator-lifecycle-qip55.js:244` but never bounded the magnitude, so the contract accepted the +value that bricked it. + +A second, narrower path reached the same wedge: `getProtocolFeeSharesByPooledQRL` reverts +`InvalidProtocolFeeAssets` once the fee target reaches pooled assets, and `_closeProtocolFeeEpoch` called it +through `getProtocolFeeQuote` without a guard, so an unpriceable fee would have reverted the close rather +than deferring it. + +Resolution: `contracts/hyperion/DepositPool-v2.hyp:926-950` adds `forceFinalizeProtocolFeeEpoch`, an +owner-only evidence-free close. It keeps every safety precondition of the ordinary finalizer, namely zero +outstanding principal, the settlement-block delay, principal reconciliation and the physical-total floor, +and it classifies no new rewards. It therefore restores liveness without letting the owner mint a fee that +finalized evidence did not already support: rewards already verified under evidence still crystallize +normally on this path. `contracts/hyperion/DepositPool-v2.hyp:1277-1283` additionally retains an unpriceable +fee as carry instead of reverting the close. + +Validation: `contracts/test/hyperion/semantic/ProtocolFeeLivenessRegression.hyp` reproduces the wedge end to +end. It records a slot at wei scale, settles into a pending close, asserts that the evidence-bound finalizer +is now permanently unusable and that deposits are refused, then asserts that the force path closes the epoch, +leaves the poisoned checkpoint untouched, still charges the full one tenth of the verified reward, and +restores deposits on the following block. The suite passes under default and optimized code generation. + +Checkpoint hardening: the revenue-loss remainder is also closed. +`contracts/hyperion/DepositPool-v2.hyp:207` sets `MAX_CHECKPOINT_SLOT_ADVANCE` to 1,000,000 slots, and +`contracts/hyperion/DepositPool-v2.hyp:980-992` rejects any checkpoint that jumps further than that from the +previous one. QRL finalizes roughly one slot per minute, so the bound tolerates about 1.9 years of reporting +silence while refusing a value supplied at block, timestamp or wei scale. A zero pointer is treated as +unseeded, so the very first checkpoint still establishes the baseline and cannot be bounded against anything. +`contracts/hyperion/DepositPool-v2.hyp:952-970` covers that one remaining case with `resyncRewardCheckpoint`, +an owner-only reseat that requires a quiescent pool, meaning no open epoch and no pending close, so no +in-flight classification can be affected. Replay protection does not depend on the pointer: +`_usedRewardEvidenceRoots` retires each evidence root permanently and the resync never clears it, so no +consumed root becomes reusable. Because slot provenance is already owner-attested under QP-FEE-003, the +reseat grants no authority the owner did not already hold. + +Validation of the hardening: `ProtocolFeeLivenessRegression.hyp` now also reseats the unseeded poison while +quiescent and confirms the retired root stays retired, then funds a fresh validator and asserts that a +checkpoint one slot beyond the bound is refused without moving the pointer, consuming the root or booking a +reward, that a checkpoint inside the bound classifies normally, and that the ordinary evidence-bound +finalizer still closes the resulting epoch and charges the fee. + +Residual: the force path and the resync are owner-only and share the QP-FEE-003 trust boundary. Replacing +owner-attested slot provenance with a consensus-authenticated receipt, as QP-FEE-001 proposes, remains the +end state. + +## Informational observations + +- The loss branch at `contracts/hyperion/DepositPool-v2.hyp:1253-1256` clears `feeRewardRemainder` and + `feeAssetCarryforward` when an epoch ends below its starting pooled total. Carry is fee value the protocol + already earned but could not yet mint, so a loss epoch both adds the loss to the shelter and discards that + earned dust. The direction is conservative and favors holders, and the amounts are sub-share, but the + asymmetry is a policy choice worth stating rather than an arithmetic requirement. +- `PROTOCOL_FEE_BPS` at `contracts/hyperion/DepositPool-v2.hyp:196` is ABI and display surface only. All fee + arithmetic uses `PROTOCOL_FEE_DIVISOR`. The two agree because 1,000 basis points is exactly one tenth, and + `ProtocolFeeSemanticRegression.hyp:141` pins the published rate, but the compiler still cannot tie the two + constants together. The declaration now carries an explicit note that they must always describe the same + rate, and that the exact remainder carry requires the rate to stay a unit fraction. Any future rate change + must move both, and a rate that is not 1/N would require replacing the carry with a `mulDiv` form and a + fresh rounding proof. +- Independent re-derivation of the dilution price confirms it is exact rather than approximate. For a fee + target `f`, pooled `A` including the virtual offset and shares `T` including the virtual offset, the minted + share count `f * T / (A - f)` is worth exactly `f` after the mint, and both `mulDivDown` roundings move the + residual to existing holders. +- The pool refuses deposits and withdrawal claims for the entire time any validator principal is outstanding, + by way of `_permissionlessSyncAllowed`. That is a deliberate solvency-first choice and it is what makes the + `feeEpochStartShares` invariant hold structurally, but it constrains what the product can claim about + liquidity. Worth confirming the user-facing copy matches. + +## Important clean passes + +- Fee assets are capped at one tenth of verified rewards and by whole-epoch net gain. +- Prior losses absorb later candidate rewards before any fee accrues. +- Dilution pricing rounds in favor of existing holders and requires the exact previewed share count. +- Principal recovery is reconciled before reward classification. +- Historical-minimum checkpoints prevent split-burn rounding drift and temporary deposit carry washing. +- A one-unit donation cannot freeze delayed finalization because the finalized physical amount is a floor. +- Receipt-block deposit, claim, sync, reserve, and funding races are blocked by the finalization marker. +- Fee minting is restricted to DepositPool and invokes no recipient callback. +- Frontend and monitoring ABIs are checked against current Hyperion artifacts. + +## Validation record + +- `npm test`: passed. All selected Q128, release-blocker, unexpected-exit, protocol-fee and protocol-fee-liveness semantic suites passed under default and optimized code generation. Tooling passed 149 of 149 tests. +- Independent randomized arithmetic check: 200,000 fee-quote and carry cases passed. +- DepositPoolV2 runtime after the QP-FEE-005 fix, checkpoint hardening and the rate change: 22,772 bytes, leaving 1,804 bytes below the 24,576-byte limit. +- Frontend: 14 of 14 tests passed; lint passed; TypeScript and Vite production build passed. +- Monitoring: JavaScript syntax passed, Prometheus rule YAML parsed, and every inline ABI entry matched the canonical artifacts. +- Disposable Q128 enclave: fingerprinted deployment refused without exact confirmation, deployed all three predicted contracts, waited for configured finality, and verified reciprocal links, Q128 immutables, the published rate, paused launch state, zeroed fee state, bytecode size, and runtime bytes after expected immutable substitution. The enclave, loopback bridges, and temporary config were removed afterward. This run was performed at 1,250 bps and has NOT been repeated since the rate change; it must be re-run before any fee-enabled deployment. +- Text hygiene: `git diff --check` passed and no U+2014 was present in added lines or untracked files. + +## Rate change: 1,250 to 1,000 basis points (2026-08-29) + +The published fee was lowered from 12.5% to 10% before any fee-enabled deployment. Verified market context at +the time of the change: Lido charges 10% of staking rewards, split 5% to node operators and 5% to the DAO +treasury; Rocket Pool's post-Saturn-I structure takes roughly 14% from liquid stakers, as a 5% base +commission plus a 9% RPL-weighted revenue pool. 10% is the prevailing standard among Ethereum liquid-staking +protocols. Because `PROTOCOL_FEE_BPS` is `constant` with no setter, the rate can only change by redeploying, +so this was decided before launch rather than after. + +The change was safe to make cheaply because 10%, like 12.5%, is a unit fraction. The exact remainder carry +depends on the fee being `1/PROTOCOL_FEE_DIVISOR`, and its invariant `N * F + R == sum of eligible rewards` +is generic in `N`. A rate such as 11% or 12% would not have been a drop-in. + +What changed: `PROTOCOL_FEE_BPS` 1250 to 1000 and `PROTOCOL_FEE_DIVISOR` 8 to 10, the deployment-time rate +assertion in `scripts/deploy-hyperion.js`, the divisor-dependent constants in the rounding sweep and the +exact fee amounts in the net-gain-cap, late-income and prior-loss-recovery phases, both semantic expectation +traces, and the user-facing copy on the stake, legal and how-it-works pages. The displayed percentage was +already read live from the on-chain constant, so only prose was literal. + +The rate-change review found that `scripts/validator-lifecycle-qip55.js` still derived finalization +expectations from a local divisor of 8. A correct 10% finalization transaction would therefore have succeeded +on chain and then failed the tool's post-transaction verification. The local divisor was removed. The tool +now reads `PROTOCOL_FEE_BPS` in the same block-tagged fee snapshot, derives and validates its exact +unit-fraction divisor, binds the rate through finalization, and rejects a missing or unsupported rate. Its +fixtures now verify the 10% fee target, dilution-priced shares, realized fee assets, and carry state. Focused +validation passed 78 of 78 tests, and the complete tooling gate passed 149 of 149 tests. + +Re-validation of the arithmetic: an independent randomized check re-derived the carry invariant at N=10 over +600,000 multi-epoch cases plus an exhaustive residue sweep, confirming cumulative fee is exactly +`floor(total / 10)`, the remainder never leaves range, and the protocol never overcharges. The 1..8 planck +rounding sweep still triggers three carry rollovers at the new divisor, ending at remainder 6 planck and fee +3 planck. + +## Release status + +The source implementation and local validation are ready for project review. Production enablement still requires an explicit QIP-55 fee-recipient decision, pinned reviewed commits and compiler identity, acceptance or resolution of QP-FEE-001 and QP-FEE-002, and a fresh production deployment review. No commit, push, pull request, or production deployment was performed during this review. diff --git a/docs/TERMINAL-WITHDRAWAL-RECEIPTS.md b/docs/TERMINAL-WITHDRAWAL-RECEIPTS.md new file mode 100644 index 0000000..3242e8b --- /dev/null +++ b/docs/TERMINAL-WITHDRAWAL-RECEIPTS.md @@ -0,0 +1,181 @@ +# QRL Terminal Withdrawal Receipt Interface + +Status: protocol proposal and local audit design. This interface is not implemented in the reviewed Qrysm or go-qrl snapshots and is not a deployed security boundary. + +## Objective + +Give Hyperion contracts enough consensus-authenticated information to distinguish all three validator balance events: + +1. a partial reward withdrawal, +2. a terminal withdrawal that retires validator principal, and +3. a terminal zero-balance validator that produces no native balance delta. + +QuantaPool currently closes deposits and claims while any validator principal is outstanding because the execution balance and existing withdrawal tuple cannot prove which case occurred. The proposed receipt makes each validator transition explicit and replay protected. + +## Reviewed source boundary + +This design was checked against: + +- Qrysm `e07a6007f4424aae030dd0e5a69f3d57e69dc677`, where `Withdrawal` contains index, validator index, a 20-byte address, and amount, and `ExpectedWithdrawals` omits fully withdrawable validators whose balance is zero. +- go-qrl `39be3a83cb956adc9796ceee1783f97df3f7e2a5`, where `types.Withdrawal` contains index, validator index, address, and amount in Shor, and `consensus/beacon.Finalize` credits `amount * params.Shor` to the recipient. +- Qrysm Q128 commit `b53fd7c488f3f0d1d4163b270afac1749eed954b`, which is available locally but is not an ancestor of the reviewed Qrysm main snapshot. The terminal-receipt work must first port or rebase the Q128 withdrawal representation. + +The current execution withdrawal already authenticates its validator index through the consensus-generated withdrawals root. It does not authenticate whether the withdrawal is terminal or bind the full validator public key to a contract-side record. A block proof cannot reconstruct facts absent from the committed tuple. + +## Forked consensus tuple + +Introduce a fork-specific `WithdrawalV3` container: + +```text +WithdrawalV3 { + index: uint64 + validator_index: uint64 + recipient: bytes64 + amount_shor: uint64 + validator_pubkey_hash: bytes32 + terminal: bool +} +``` + +`recipient` is the full raw 64-byte QIP-55 execution address. `validator_pubkey_hash` is `Keccak256(validator.public_key)`, matching QuantaPool ValidatorManager's existing identity key. The hash algorithm and exact byte serialization are consensus constants and require fixed test vectors. + +`terminal` is derived by Qrysm from the pre-transition beacon state. A caller, builder, or execution client cannot select it. It is true exactly when the validator is fully withdrawable at the payload's epoch and has not previously issued a terminal receipt. + +## Zero-balance terminal rule + +Add a consensus-state bit for each validator: + +```text +terminal_withdrawal_issued: bool +``` + +At the new fork, expected-withdrawal generation follows this order: + +```text +if fully_withdrawable(validator, current_epoch) and not terminal_withdrawal_issued: + emit WithdrawalV3(amount_shor=balance, terminal=true) +else if partially_withdrawable(validator, balance): + emit WithdrawalV3(amount_shor=balance-max_effective_balance, terminal=false) +``` + +The terminal entry is emitted once even when `balance == 0`. Processing a terminal entry sets `terminal_withdrawal_issued = true`. Processing a zero amount is a valid no-op for beacon balance reduction and execution balance credit. Repeated terminal entries are invalid. + +This bit must be part of the forked beacon-state schema. A side database is insufficient because every consensus participant must derive the same expected withdrawals root after reorgs and state sync. + +## Engine API V3 + +Add versioned methods rather than changing V2 decoding in place: + +- `engine_forkchoiceUpdatedV3` +- `engine_getPayloadV3` +- `engine_newPayloadV3` + +The V3 execution payload carries `WithdrawalV3[]`. Qrysm validates the complete SSZ root against its expected list before changing balances or setting terminal markers. go-qrl validates the corresponding execution withdrawal root and rejects malformed field widths, non-Q128 recipients, invalid booleans, duplicate indexes, and payloads used on the wrong side of the activation timestamp. + +The new tuple changes both SSZ and RLP roots. It therefore needs a fresh consensus fork version and an execution activation time. A fresh testnet genesis is the simplest activation path for the current development network. + +## Consensus-written receipt inbox + +Reserve one Q128 system address outside precompile slots 1 through 6. Genesis installs immutable getter-only runtime code at that address. During `Beacon.Finalize`, go-qrl performs these operations atomically for every valid `WithdrawalV3`: + +1. credit `amount_shor * params.Shor` to `recipient`, +2. reject any previously stored receipt at `index`, +3. write the authenticated receipt record to the system account, +4. update the system account storage root in the same canonical state transition. + +The runtime exposes one view method: + +```text +withdrawalReceipt(uint64 index) returns ( + bool exists, + uint64 validatorIndex, + address recipient, + uint64 amountShor, + bytes32 validatorPubkeyHash, + bool terminal, + uint64 sourceBlock +) +``` + +Protocol constants must fix the inbox address, runtime bytecode, runtime code hash, storage layout, and ABI selector. User transactions cannot write the inbox. The execution client writes its storage only through the consensus finalization path. + +The receipt and native balance credit share one state root. A canonical reorg removes or replaces both together. QuantaPool does not need an independent finality oracle for fund safety, although operators should wait for finalized consensus before submitting receipts to reduce reverted transactions. + +## QuantaPool consumption + +QuantaPool stores the last consumed status for each withdrawal index. A permissionless receipt-consumption call must verify: + +- the inbox is the immutable protocol address and has the expected code hash, +- `exists` is true, +- the index has not been consumed, +- `recipient == address(this)`, +- `validatorPubkeyHash` maps to the supplied pool validator ID, +- `validatorIndex` matches the beacon index previously bound to that pool validator, +- `amountShor * 1e9` fits `uint256` and matches the observed native accounting unit, +- a terminal receipt retires that validator's fixed principal exactly once. + +A partial receipt increases pooled rewards. A terminal receipt applies its amount, retires fixed principal, and records any resulting reward or loss. A zero-amount terminal receipt retires principal and exposes the full loss without relying on a balance delta. + +Continuous deposits and claims require receipt consumption to be permissionless and recipient scoped. The contract can accept caller-supplied receipt indexes because the inbox authenticates every record. Missing receipt submission can delay accounting but cannot forge reward, principal, or terminal state. + +The current QuantaPool Manager does not store the consensus validator index. Deployment tooling must bind that index after the deposit becomes canonical, using a separate authenticated inclusion mechanism or an extension of the deposit receipt. Until that binding is trustless, the current cohort safety lock remains required. + +## Security invariants + +1. Consensus is the sole authority for `terminal` and `validator_pubkey_hash`. +2. One withdrawal index identifies one immutable receipt for the life of a canonical state. +3. One validator emits at most one terminal receipt. +4. A terminal zero balance still emits a receipt. +5. Native credit and inbox storage update are atomic. +6. The inbox has no user-write path, upgrade path, delegate call, or value-transfer method. +7. Pre-fork nodes reject V3 payloads and post-fork nodes reject V2 payloads. +8. Q128 recipient bytes remain unchanged across Qrysm SSZ, Engine JSON, go-qrl RLP, state credit, and Hyperion ABI decoding. + +## Required tests + +### Qrysm + +- partial withdrawal has `terminal=false` and the correct public-key hash, +- positive full withdrawal has `terminal=true`, +- zero-balance full withdrawal emits exactly once, +- terminal marker survives SSZ round trip and state sync, +- payload root changes if any new field changes, +- mismatched terminal flag, key hash, or recipient is rejected, +- Q128 recipient test vectors survive every JSON and protobuf conversion. + +### go-qrl + +- V3 JSON and RLP round trips preserve all fields, +- withdrawal root commits every new field, +- finalization credits the recipient and writes the inbox atomically, +- zero amount writes a receipt without changing balance, +- duplicate index and malformed Q128 recipient are rejected, +- reorg removes both credit and receipt, +- snapshot, sync, tracing, GraphQL, and RPC readers preserve V3 receipts, +- inbox runtime code and storage cannot be changed by a transaction. + +### Cross-client and QuantaPool + +- fixed vectors produce the same withdrawal root in both clients, +- partial rewards cannot retire principal, +- terminal principal cannot be counted as reward, +- zero-return slashing becomes visible and retires principal, +- duplicate receipt consumption reverts, +- a receipt for another pool or validator reverts, +- multiple validators returning in one block settle independently, +- continuous deposit and claim pricing remains correct across partial and terminal receipts. + +## Activation gates + +- Pin reviewed Qrysm, go-qrl, Hyperion, qrl-package, and QuantaPool commits. Do not build validator infrastructure from a floating branch or `latest` image. +- Start a fresh chain ID and genesis fork version for the receipt testnet. +- Publish the Engine V3 schema, Q128 test vectors, inbox address, code hash, and storage layout before genesis. +- Run cross-client root vectors and a bounded Kurtosis lifecycle covering positive, partial, slashed, and zero-balance terminal cases. +- Keep the QuantaPool cohort safety lock enabled until every invariant above is executable and the deployed inbox code hash is verified. + +## Open protocol decisions + +- How a contract trustlessly binds a beacon validator index to its original deposit. +- Whether the public-key commitment remains Keccak-256 for direct QuantaPool compatibility or moves to a QRL-wide hash with an explicit contract conversion path. +- The permanent reserved Q128 inbox address and governance for its genesis code hash. +- Whether V3 receipts become the general QRL withdrawal format or a new parallel receipt accumulator. diff --git a/docs/TESTNET-RETIREMENT.md b/docs/TESTNET-RETIREMENT.md new file mode 100644 index 0000000..c088890 --- /dev/null +++ b/docs/TESTNET-RETIREMENT.md @@ -0,0 +1,27 @@ +# Retirement of the v2 testnet experiments + +Decision recorded on 2026-09-15 from the project owner's clarification: the earlier QuantaPool contracts were public-testnet experiments, unavailable for normal use because of public-testnet limitations. They are abandoned as targets for the native-QRL redesign. + +## Clean native deployment + +The new protocol starts with newly deployed contracts and fresh accounting state. It has no dependency on old testnet balances, rewards, validators, addresses, ownership settings, storage layouts or receipt-token APIs. Legacy recovery and migration are removed from the development and launch prerequisites. No claim in the native pool can be created by importing a historical stQRL balance. + +There will be no compatibility wrapper, token redemption adapter, inherited proxy layout, old-address alias, or automatic transfer of historical stake. Existing native prototype contracts already use their own storage and constructor-bound protocol/verifier/account references. Generic wallet and source-validation helpers may be reused without carrying old economic state. + +The 10% operator fee remains a requirement for the new native reward model. Abandoning the testnet experiments does not remove authenticated accounting, loss allocation, deterministic withdrawal ordering, correct fee classification or native exit requirements. + +## Historical material + +The v2 source, token-oriented tests, deployment scripts, frontend and monitoring remain historical implementation material pending coordinated replacement. They impose no backwards-compatibility promise. Ordinary package build/test/deploy commands still target those old sources and must not be presented as native-pool qualification or deployment commands. + +The previous [README](legacy/V2-README.md) and [architecture](legacy/V2-ARCHITECTURE.md) are archived with their historical context. [Testnet deployment observations](V2-DEPLOYMENT-STATUS.md) retain addresses and earlier results for reference. Their older recovery requirements are superseded for the native redesign. + +This retirement decision does not query, erase, transfer, recover or modify existing network state. It does not assert that old addresses have zero balances. Any optional historical cleanup is separate work and is unnecessary for the new deployment. Upstream QRL implementations, network semantics and remote services remain outside this change. + +## Current development references + +- [Native architecture](architecture.md) +- [Selected defaults and remaining trust assumptions](../prototype/native/DECISIONS.md) +- [Implemented components and recorded validation](../prototype/native/VALIDATION.md) + +The next implementation target is the integrated native ledger and its application interface. Missing authentication or accounting guarantees remain technical work; legacy testnet availability is no longer a dependency. diff --git a/docs/UPSTREAM-FINDINGS.md b/docs/UPSTREAM-FINDINGS.md index ebaf5df..302a730 100644 --- a/docs/UPSTREAM-FINDINGS.md +++ b/docs/UPSTREAM-FINDINGS.md @@ -1,5 +1,7 @@ # Upstream QRL v2 Findings +Historical April 2026 source notes. Current native behavior and tested commits are recorded in [the source lock](../native/network/source-lock.json) and [native network qualification](../native/network/README.md). These old dimensions and protocol assumptions must not drive the current implementation. + Facts read directly from the cloned upstream repos at: - `qrysm` (QRL consensus client) - `go-qrl` (QRL execution client) diff --git a/docs/V2-DEPLOYMENT-STATUS.md b/docs/V2-DEPLOYMENT-STATUS.md index b373348..93b4c6e 100644 --- a/docs/V2-DEPLOYMENT-STATUS.md +++ b/docs/V2-DEPLOYMENT-STATUS.md @@ -1,12 +1,151 @@ -# QuantaPool v2 Testnet Deployment - Status & Handoff +# Abandoned QuantaPool v2 testnet experiments -**Last updated:** 2026-08-03 +**Retired as development targets:** 2026-09-15. These were public-testnet experiments. The native-QRL redesign uses a clean deployment and requires no migration, recovery, address compatibility or storage-layout compatibility with these contracts. See [testnet retirement](TESTNET-RETIREMENT.md) and the [current native architecture](architecture.md). + +The observations below are historical. They are not a current RPC verification, launch instruction or recovery work order. All earlier v2 release and migration prerequisites are superseded for the native redesign. No on-chain state is changed by retirement. + +**Historical record last updated:** 2026-08-28 **Branch:** `dev` **Network:** QRL v2 testnet, chainId `1337` **Deployment revision:** v2.3 security and accounting hardening --- +## Historical local v2.4 candidate - uncommitted and loopback-only + +The canonical Hyperion sources currently contain local release-blocker work that +has not been staged, committed, pushed, or deployed to a public network. Local +validation uses disposable contracts on chain ID `3151911` inside an existing +loopback Kurtosis enclave. The addresses below remain historical v2.3 and v2.2 +deployments. + +The local v2.4 candidate: + +- Makes ValidatorManager an immutable, pool-only identity and principal ledger. +- Registers each real beacon deposit atomically with a canonical 1-based ID and + rejects duplicate pubkeys or deposit roots before value transfer. +- Removes `fundValidatorMVP()` and the separate `recordValidatorExit(amount)` path + from production bytecode. +- Keeps reward sync permissionless only while no validator principal is + outstanding. Every deposit and claim stays closed while any principal record + remains, including when observable accounting appears equal. Validator-epoch + rewards use exact bounded owner recognition, including during a serialized + exit before terminal settlement. +- Starts one planned validator exit through `beginValidatorExit(id)` and settles + every Qrysm-confirmed terminal validator through `settleValidatorExits(ids)`. + Fixed-principal retirement, observed buffer refill, and Manager finalization + share one transaction. A later finalized checkpoint completes reward or loss + recognition and protocol fee crystallization. +- Allows a Qrysm-confirmed zero-return settlement, cancellation of an exit that + Qrysm still reports active, and recovery when a terminal return arrived before + the begin checkpoint. Cancellation leaves accumulated balance changes + unclassified for a separate bounded recognition step. +- Lets a claim whose value grew after reserve funding consume only synchronized + unbuffered liquid rewards, preserving deposit-origin validator liquidity. +- Pins the locally reviewed Hyperion and qrvmone toolchain identities. Hyperion + remains pre-release software with no tagged stable compiler distribution. +- Uses the latest QRL-specific Hyperion branch head (`6f862206`) plus a local, + uncommitted generated-getter fix that stores each mapping key using its actual + type. The focused wide-key semantic case passes default and optimized legacy + plus via-IR codegen. The compiler's 5,528 nonsemantic cases and 39,549 + assertions also pass without changing unrelated gas snapshots. +- Avoids legacy Hyperion's unsafe generated mapping getters for Q128 address and + `bytes32` keys. The affected mappings are private and expose explicit typed + accessors, with default and optimized legacy-codegen regressions plus ABI + gates that forbid the unsafe generated getter names. +- Adds a fixed 10% performance fee on finalized-evidence validator rewards. + The fee crystallizes as dilution-priced stQRL shares only after the complete + validator cohort settles and a later finalized accounting pass reconciles + late principal and reward credit. It is capped by whole-epoch net gain and + recovers prior epoch losses first. The immutable QIP-55 fee recipient is bound + into the deployment fingerprint and verified before config persistence. + +Pre-audit loopback evidence on the accessor-safe candidate includes eight +distinct Q128 stakers with uneven deposits totaling exactly 40,000 QRL. Every +preview matched its mint, aggregate assets, buffer, and supply matched exactly, +and one real validator funding transaction moved the full 40,000 QRL to the +beacon deposit contract while registering canonical manager ID 1 atomically. +Two additional full Q128 accounts exercised the live withdrawal storage boundary +with the same request ID. One 50-share request was cancelled while the other +remained pending, proving per-address isolation. The pending request then passed +the 128-block delay, consumed an exactly funded 50 QRL reserve, burned exactly 50 +shares, advanced only its own FIFO index, and left pooled assets and share supply +equal at 40,150 QRL. + +The real validator entered `active_ongoing` at epoch 737. A 1.774859636 QRL +consensus reward reached the Q128 pool recipient and was recognized by the +earlier active-principal sync path without retiring principal. All eight original +stakers increased by the exact pro-rata share formula; the ten-holder aggregate +differed from total pooled assets by five execution base units from integer +flooring. The planned exit checkpoint transaction was +`0xb3623234c859d4d7c74c0615c58914d6923ebe7d086bc28a7f60985de8eeb1f8`. +Qrysm accepted the matching voluntary exit, assigned exit epoch 759 and +withdrawable epoch 775, then finalized `withdrawal_done` with a zero beacon +balance and an exact 40,000 QRL terminal credit to the pool. + +Settlement transaction +`0x42bda88d8f1b5115d7605110ec6caa176779123c7f64ef8d1f9b355d2f047390` +retired the complete principal and restored the complete validator buffer. Final +state is 40,187.477185077 QRL pooled, 40,150 QRL shares, 40,150 QRL buffered, +37.477185077 QRL cumulative rewards, zero cumulative slashing, zero outstanding +principal, and manager counts `total=1, exited=1` with every other status count +zero. The ten holder values reconcile within six execution base units. Tooling +also accounts for go-qrl applying beacon withdrawals after transactions during +block finalization, so a legitimate same-block post-call inflow is reported as +unsynced while outflow and inconsistent accounting still fail closed. + +On 2026-08-28, an executable audit regression confirmed that an unannounced +40,000 QRL terminal return could be synchronized as reward and let one equal +staker claim about 60,000 QRL, leaving the other with about 20,000 QRL. Pool +pause did not stop the claim. A second regression identified a partial-settlement +race where validator B can return after a snapshot for terminal validator A; +B's returned balance can mask A's zero-return loss while B principal remains in +the Manager ledger. The local source now blocks claims while paused, keeps every +share-priced deposit and claim closed until all principal is retired, quarantines +balance deltas, and limits owner reward recognition to an explicit amount. These +changes remain uncommitted and undeployed. + +The remaining protocol-level limitation is terminal-withdrawal proof. Current +execution withdrawal data does not identify a withdrawal as partial or full, so +the owner tooling must verify exact Qrysm state before reward recognition, +cancellation, or settlement. A positive unannounced return stays quarantined. A +zero-return terminal loss creates no execution-balance delta and remains +invisible until principal retirement. The contract-local safety boundary closes +deposits and claims for the entire outstanding-principal interval. A finalized +withdrawal receipt primitive is required to restore continuous deposits and +withdrawals without trusting lifecycle reports. + +The fail-closed contract-local mitigation creates a High availability tradeoff: +one Pending or otherwise nonterminal validator keeps deposits, claims, reserve +funding, and emergency recovery closed for the whole cohort lifetime. Partial +settlement can leave `getQRLValue()` provisional for secondary-market consumers. +The owner also remains a lifecycle oracle; a compromised or mistaken owner can +retire live Active IDs through the recovery path and reopen pricing against an +invalid principal ledger. Finalized-state tooling reduces operator error but is +not an on-chain proof. + +The narrow protocol interface identified by the current Qrysm, go-qrl, QRVM, +and Hyperion source audit is a fork-versioned execution withdrawal receipt. It +must commit the withdrawal index, validator index, full Q128 recipient, amount, +validator public-key root, and a consensus-derived terminal flag. go-qrl can +write each canonical receipt to a fixed withdrawal inbox in the same state +transition that credits the recipient. QuantaPool can then verify recipient and +validator identity, distinguish partial rewards from terminal principal, and +consume each receipt exactly once. The current execution withdrawal tuple omits +the terminal flag and validator identity commitment. A block-header plus +withdrawal-trie proof can authenticate amount and recipient within the blockhash +window, but it cannot fill either missing fact. The local contract now +quarantines unexplained balance changes while principal is outstanding. Fully +trustless production safety still requires this interface or an equivalent +authenticated beacon-state path. + +The proposed fork interface, zero-balance terminal rule, consensus-written +receipt inbox, cross-client invariants, and activation gates are specified in +`docs/TERMINAL-WITHDRAWAL-RECEIPTS.md`. This is design evidence only and is not +implemented or deployed. + +--- + ## Current deployment on QRL v2 testnet (v2.3) | Contract | Address | @@ -19,9 +158,13 @@ The nonce-bound deployment completed on 2026-08-03 from the reviewed `dev` artif The deployment required eight transactions: three contract creations, two immediate pause calls, and three one-shot wiring calls. `config/testnet-hyperion.json` is updated only after 12 confirmations and final ownership, link, bytecode, and paused-state checks. +The deployed v2.3 contracts predate the local performance-fee work and charge +no protocol fee. Enabling the 10% fee requires a new reviewed deployment with +an explicit QIP-55 fee recipient. No current address is upgraded in place. + One earlier stQRL creation at `Qd7D63e681aF8aae122366Ee537943078ED47E63E` is abandoned after the hosted RPC proxy rejected the following oversized deployment body. It has zero supply and no deposit-pool authority, so no account can mint through it. It is absent from every active config. -## Legacy v2.2 deployment - paused and migration-bound +## Historical v2.2 deployment - previously observed paused | Contract | Address | |----------|---------| @@ -29,7 +172,7 @@ One earlier stQRL creation at `Qd7D63e681aF8aae122366Ee537943078ED47E63E` is aba | **DepositPoolV2** | `Q109d7C528a67b80eb638D4C85e7C4545ef9Bb9aC` | | **ValidatorManager** | `QA5b6e85B7713670589e4eAf2F039380Ec2792c8C` | -The legacy token and pool were emergency-paused on 2026-08-03. They retain historical stake and validator state. Do not call `syncRewards`, `requestWithdrawal`, or `claimWithdrawal` on this pool because its missing off-contract stake accounting would record phantom slashing. Migration must wait for an atomic or independently verifiable beacon exit and reward-settlement procedure. +The legacy token and pool were emergency-paused on 2026-08-03 and were observed with historical stake and validator state. Do not call `syncRewards`, `requestWithdrawal`, or `claimWithdrawal` on this pool because its missing off-contract stake accounting would record phantom slashing. Earlier recovery plans required independently verifiable beacon exit and reward settlement. Recovery is now separate optional historical work and imposes no prerequisite on the clean native deployment. **Real validator deposit executed 2026-04-14:** - Buffer top-up `pool.deposit(40000)` - tx `0x12e2b96b8f4ac2e80b8246a32af92d047dfdf6dcc3416e52a1dce5751c3fc8c6` @@ -69,20 +212,22 @@ Read-back smoke confirmed: --- -## Integration test coverage (`scripts/integration-test-v2.js`) +## Historical integration phase record (`scripts/integration-test-v2.js`) -All phases pass green on live testnet. Run any phase independently. +This table records mixed historical deployments. The current script keeps safe +generic phases and prints handoffs for real validator funding and Qrysm-gated +lifecycle mutations. Current candidate evidence is stated separately above. | Phase | What it exercises | Status | |-------|-------------------|--------| | `status` | Read-only dump: positions, rewards, pending requests, VM stats | ✓ | | `smoke` | Deposit 100 QRL → shares minted, totals consistent | ✓ | -| `rewards` | Donate 1 QRL + `syncRewards` → exchange rate 1.00 → 1.01 | ✓ | +| `rewards` | With zero principal, donate 1 QRL + `syncRewards` → rate 1.00 → 1.01 | ✓ | | `withdraw` | Request 50 shares → locked, `blocksRemaining=128`, canClaim=false | ✓ | -| `validator` | Deposit to 40k buffer → register → `fundValidatorMVP` → activate | ✓ | +| `validator` | Prints the real QIP-55 funding handoff; placeholder funding is retired | current tooling | | `errors` | 6 revert paths (below-min, zero, over-balance, one-shot guards, bad pubkey) | ✓ | | `pause` | `pause()` blocks deposit; `unpause()` restores | ✓ | -| `lifecycle` | VM state machine: Active → Exiting → Exited + idempotency guard | ✓ | +| `lifecycle` | Prints the finalized-Qrysm lifecycle handoff | current tooling | | `claim-prep` | `fundWithdrawalReserve` reclassifies pooled→reserve; claim still blocked on 128-block delay | ✓ (historical v2.2 behavior) | | `claim` | Actual `claimWithdrawal` after 128-block delay + reserve funded | ✓ (completed end-to-end on v2.0 2026-04-14: 50 shares burned, 50.5 QRL paid out before v2.1 redeploy) | | `wait-claim` | Polls `getWithdrawalRequest` every 60s, auto-claims when ready | ✓ | @@ -104,11 +249,11 @@ Confirmed against `qrysm/config/params/testnet_e2e_config.go:8` and `testdata/e2 ### 2. ~~Withdrawal-credential prefix byte was wrong~~ - **fixed in v2.1, kept in v2.2** -Qrysm uses `ExecutionAddressWithdrawalPrefixByte = byte(0)` (`mainnet_config.go:74`). Our `DepositPool-v2.sol` originally hardcoded `bytes1(0x01)` from Ethereum-spec muscle memory. Any real `staking-deposit-cli` deposit would have reverted with `InvalidWithdrawalCredentials` and stuck the stake. Locked in by 9 Foundry tests (`test_FundValidator_AcceptsZeroPrefix` / `RejectsEthereumOnePrefix` / `RejectsWrongContractAddress` / etc.). +Qrysm uses `ExecutionAddressWithdrawalPrefixByte = byte(0)` (`mainnet_config.go:74`). The historical Solidity `DepositPool-v2.sol` originally hardcoded `bytes1(0x01)` from Ethereum-spec muscle memory. Any real `staking-deposit-cli` deposit would have reverted with `InvalidWithdrawalCredentials` and stuck the stake. Nine cases covering the fix are retained in `contracts/test/hyperion/DepositPool-v2.t.hyp`. Their earlier Foundry execution is pre-removal historical evidence, not a current Hyperion test-runner result. ### 2b. ~~`SIGNATURE_LENGTH` was wrong~~ - **fixed + redeployed as v2.2 2026-04-14** -`DepositPool-v2.sol:78` hardcoded `SIGNATURE_LENGTH = 4595`, but qrysm's `crypto/ml_dsa_87/ml_dsa_87t/signature.go` enforces ML-DSA-87 signatures at exactly **4627 bytes**. Any real `fundValidator()` on v2.1 would have reverted with `InvalidSignatureLength` before reaching the beacon contract. Fix bumped the constant to 4627 and updated the 4 Foundry tests that hardcoded the old length. Full suite still **187 pass**. v2.2 live addresses ship the fixed bytecode and have already executed a real `fundValidator()` end-to-end (see "Real validator deposit executed" above). +The historical Solidity `DepositPool-v2.sol:78` hardcoded `SIGNATURE_LENGTH = 4595`, but qrysm's `crypto/ml_dsa_87/ml_dsa_87t/signature.go` enforces ML-DSA-87 signatures at exactly **4627 bytes**. Any real `fundValidator()` on v2.1 would have reverted with `InvalidSignatureLength` before reaching the beacon contract. The fix changed the constant to 4627 and updated four retained behavioral cases. At that point the former Foundry suite was observed at **187 passing tests**; this is pre-removal historical evidence. The v2.2 live addresses ship the fixed bytecode and have already executed a real `fundValidator()` end-to-end (see "Real validator deposit executed" above). ### 3. ~~Real validator deployment~~ - **done 2026-04-14** gqrl + qrysm beacon + qrysm validator running under systemd on the validator host. Beacon fully synced, validator key imported and listening for activation. Runbook is maintainer-internal (not in this public repo). @@ -117,7 +262,7 @@ gqrl + qrysm beacon + qrysm validator running under systemd on the validator hos Rewritten for v2 ABIs. Running under docker-compose on the validator host. After v2.2 redeploy: `pooled=40000 shares=40000 rate=1.0 validators=1`. Discord webhook wired for critical/warning/info receivers; `monitoring/prometheus/rules/*.yml` tuned this session to suppress false positives (`BeaconChainLowPeers` was matching the always-zero `state="Connecting"` bucket; `NetworkInterfaceDown` was firing on the unplugged secondary NIC). ### 5. Slashing path -Not testable on the testnet (can't force a validator to be slashed externally). Foundry unit tests in `contracts/test/` cover the `markValidatorSlashed` accounting at the Solidity level. Current qrysm slashing constants are **placeholders** per the QRL team (Discord, 2026-01-25) - snapshot captured in `docs/UPSTREAM-FINDINGS.md` §4 for later diffing. +Forced live slashing is unavailable on the testnet. The current targeted native Hyperion suite executes slashed principal, zero-return loss, and aggregate settlement algebra. The broader retained cases in `contracts/test/hyperion/ValidatorManager.t.hyp` remain parse-only specifications. Current qrysm slashing constants are **placeholders** per the QRL team (Discord, 2026-01-25) - snapshot captured in `docs/UPSTREAM-FINDINGS.md` §4 for later diffing. ### 6. Validator activation observation Validator `0xa40ca760bcc4…` is in the activation queue. Once it transitions to `ACTIVE`, the validator client will start signing attestations. Need a follow-up integration test that, after activation, polls `validator_statuses{}` and confirms the pool's `_syncRewards()` picks up beacon-chain rewards routed back via the withdrawal address. @@ -130,17 +275,56 @@ The deployed v2.2 `DepositPoolV2` decrements only `bufferedQRL` when `fundValida **This remains the legacy v2.2 state:** the real `fundValidator()` executed on 2026-04-14 means a `syncRewards()` against the v2.2 `DepositPoolV2` will report phantom slashing. Keep the legacy pool paused and do not trigger reward sync or withdrawals while planning migration. -**Fix (in `contracts/solidity/DepositPool-v2.sol`):** -- New `stakedQRL` accumulator, incremented by `fundValidator()` when principal leaves for the beacon contract. -- `_syncRewards()` now reconciles `balance + stakedQRL`, so funding a validator is balance-neutral. The current source keeps withdrawal reserve inside pooled assets until the matching shares burn. -- New owner-only `recordValidatorExit(amount)` decrements `stakedQRL` when exit proceeds return, preventing the returned principal from being double-counted as rewards. -- `emergencyWithdraw()` recoverable-amount calc excludes `stakedQRL` (it lives off-contract). -- **Phantom-reward front-run protection:** reward sync is permissionless only while `stakedQRL == 0`. Once principal is off-contract (`stakedQRL > 0`), `syncRewards()` is owner-only and claims reject unsettled on-chain balance deltas. Without this, an exit sweep lands principal in the balance before the owner can call `recordValidatorExit()`; an unrestricted sync in that window would book the principal as a phantom *reward* and spike the exchange rate. Gating sync during that window makes settlement and reward recognition owner-sequenced. The MVP path (`stakedQRL == 0`) stays fully permissionless. -- 13 new Foundry regression tests in `DepositPool-v2.t.sol`: the `OFF-CONTRACT STAKE ACCOUNTING` block (no-phantom-slashing after funding, rewards-while-staked, exit settlement, access control, emergency-withdraw carve-out) plus a `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). Suite now **200 pass**. - -`fundValidatorMVP()` is unaffected - it keeps QRL in the contract and never touches `stakedQRL`, so its sync stays permissionless. - -**Action:** keep v2.3 paused until the operator has an atomic or independently verifiable settlement procedure for beacon exits and returned rewards. The current owner-sequenced `recordValidatorExit` plus reward sync flow has a privileged timing dependency while `stakedQRL > 0`, so it is unsuitable for mainnet launch as-is. +**Local v2.4 replacement in canonical Hyperion sources:** + +- ValidatorManager records the only outstanding-principal total and exposes it + through the pool's compatibility `stakedQRL()` view. +- `fundValidator()` registers the exact pubkey and deposit root before forwarding + 40,000 QRL. The beacon call and Manager registration revert together. +- Normal-state `_syncRewards()` reconciles balance plus Manager principal and is + permissionless only with zero outstanding principal. Active-validator rewards + use bounded owner recognition after finalized Qrysm validation. +- `beginValidatorExit(id)` checkpoints pool balance and enters an explicit + one-exit settlement state without letting unsolicited inflow block progress. +- `settleValidatorExits(ids)` accepts no amount, requires the canonical begun + exit when one exists, retires every included fixed principal, restores observed + returned liquidity, and schedules fee-epoch closure after the last retirement. +- Missing returned principal remains a temporary receivable until a later block. + Fresh finalized Qrysm and canonical execution evidence then bounds + `finalizeProtocolFeeEpoch(...)`, which writes off only the remaining shortfall, + synchronizes the complete epoch, and mints the exact protocol fee shares. +- The standard finalizer supplies zero late eligible reward. Residual positive + close-window surplus remains fee-exempt because current finalized state cannot + distinguish validator reward from a direct donation. Exact rewards recognized + before settlement remain in the fee base. The finalized execution balance is + used as a floor, and positive receipt-time drift is accepted as fee-exempt so + a donation cannot indefinitely revert the close transaction. +- Accounting and share-supply changes remain closed through the successful + finalization block. This prevents later transactions in that block from + invalidating exact receipt-block verification and reopens normal flows in the + next block. +- Close-time loss and fee carries are checkpointed against the closing share + supply. Later burns reduce them only at a new historical supply minimum, so + near-total cohort exit cannot leave the full old loss shelter for replacement + capital and a temporary deposit-and-burn cycle cannot wash the checkpoint. +- Replacement deposits can keep aggregate supply above that minimum and inherit + the remaining pool-level loss shelter. This is conservative fee + undercollection. Exact cohort attribution is deferred to a per-share or + per-account equalization design. +- `cancelValidatorExit(id)` recovers a failed exit submission only while the + operator has verified exact `active_ongoing` state in Qrysm. +- The same batch path repairs accounting after terminal returns arrived before + the begin checkpoint. Bounded reward recognition does not reopen priced flows. +- Deposits and claims remain blocked whenever any Manager principal is + outstanding. This also covers zero-return loss and cross-validator return + masking that observable accounting equality cannot detect. + +**Action:** keep v2.3 paused. The targeted executable v2.4 behavior gate, +independent static review, and fresh QIP-55 many-staker validator lifecycle are +complete locally. The fail-closed source sacrifices continuous deposits and +claims while validators are funded. Mainnet still requires verifiable terminal +withdrawal evidence, or an explicit product decision to retain this cohort-based +liquidity model, before any deployment decision. ### 7b. Share and reserve accounting hardening - **deployed in paused v2.3** @@ -149,11 +333,11 @@ The current source includes additional accounting changes found during the v2.3 - Deposits reconcile rewards or losses that existed before `msg.value`, preventing new shares from capturing unsynced rewards. - `withdrawalReserve` remains part of `totalPooledQRL` until claim. Reserve funding therefore changes neither side of the share conversion rate. - Claims price shares after synchronized settlement. Request-time QRL values are informational estimates, so queued holders receive rewards and bear slashing until their shares burn. -- Exit settlement restores observed returned principal to `bufferedQRL`, capped at the nominal stake retired. This preserves restaking liquidity while preventing a slashed exit from creating unsupported buffer credit. -- Real and MVP validator funding require liquid balance net of reserve as well as sufficient `bufferedQRL`. Repeated reserve funding reduces the usable buffer without exposing earmarked funds. +- Exit settlement restores observed returned principal to `bufferedQRL` and carries missing retired principal as a temporary receivable through the later finalized close. This preserves restaking liquidity, allows late recovery across settlement batches, and records an unrecovered loss once. +- Real validator funding requires liquid balance net of reserve as well as sufficient `bufferedQRL`. Repeated reserve funding reduces the usable buffer without exposing earmarked funds. - Cancelled or overfunded requests can be unearmarked with `releaseWithdrawalReserve`; reserve provenance restores only validator buffer that was actually reserved. -- `emergencyWithdraw` rejects unsettled balance deltas. Native inflows after pool initialization are protected as possible validator rewards. -- The Foundry suite is now 226 tests, including deterministic and fuzz regressions for each accounting issue, repeated reserve funding, safe reserve release, and exit-principal provenance. +- `emergencyWithdraw` synchronizes normal-state native inflows and remains closed during exit settlement. +- Deterministic and fuzz regressions for each accounting issue, repeated reserve funding, safe reserve release, and exit-principal provenance are retained in the Hyperion test sources. The former Foundry suite reached 226 passing tests before the `.sol` and `.t.sol` tree was removed; that count is historical evidence rather than a current Hyperion runner result. These changes are present at the v2.3 addresses above. The new pool remains paused and empty while operational migration checks continue. The legacy v2.2 addresses retain the historical behavior. @@ -165,7 +349,7 @@ Fresh deposits now mature for `minStakeBlocks` (default 1536, ~1 day) before the - Immature shares are non-transferable (closes the fresh-address bypass); transfers never write to the recipient's bucket (no dust-grief vector). - Owner deposits are exempt so operator bridge capital can enter/exit without the wait. - `setMinStakeBlocks` owner-settable, capped at `MAX_MIN_STAKE_BLOCKS` (46500, ~30 days), `0` disables. -- The complete suite now passes **226 tests**, including the 16 maturity-lock regressions and invariant fuzz cases. The frontend handles missing views defensively against historical v2.2 and shows a maturing notice on the Withdrawals page. +- Sixteen maturity-lock regressions and invariant fuzz cases are retained in the Hyperion test sources. They were part of the historical **226-test** pre-removal Foundry result. The frontend handles missing views defensively against historical v2.2 and shows a maturing notice on the Withdrawals page. - The v2.3 contracts include the lock. It becomes relevant only after the paused pool is deliberately launched. --- @@ -185,7 +369,8 @@ Fresh deposits now mature for `minStakeBlocks` (default 1536, ~1 day) before the ```bash cd QuantaPool git status # expect clean on dev -forge test --summary # expect 226 pass +npm run compile:hyperion # compile canonical .hyp contracts +node --test scripts/deploy-hyperion-safety.test.js scripts/loadDeployer.test.js node scripts/integration-test-v2.js status # read-only v2.3 state # validator-host service health (gqrl/qrysm): see the maintainer-internal runbook ``` @@ -212,14 +397,14 @@ The `validator` phase locks 40,000 QRL into the pool per run. Recover via the `c - `config/testnet-hyperion.json` - provider URL, chainId, live addresses - `scripts/deploy-hyperion.js` - fingerprinted deploy, pause, wire, finality, and atomic config update - `scripts/integration-test-v2.js` - all 16 test phases (works) -- `scripts/sync-hyperion.js` - Solidity → Hyperion dialect translator - `scripts/lib/loadDeployer.js` - wallet.js v3 loader (34-word mnemonic, registers seed on `web3.qrl.wallet`) -- `contracts/solidity/` - canonical .sol sources -- `contracts/hyperion/` - generated .hyp mirrors (regenerate with `sync-hyperion`) -- `contracts/test/` - Foundry suite (226 tests, all pass) +- `contracts/hyperion/` - canonical, hand-edited `.hyp` contract sources +- `contracts/test/hyperion/semantic/` - targeted native release-blocker suite executed by `npm test` +- `contracts/test/hyperion/*.t.hyp` - historical pre-v2.4 specifications with retired API cases, parsed as archival evidence - `scripts/verify-deposit-data.js` - safety gate; validates a `deposit_data-*.json` against the live pool - `scripts/fund-validator-real.js` - broadcasts `pool.fundValidator()` (real beacon path) - `build/hyperion/{stQRLv2,DepositPoolV2,ValidatorManager}.{abi,bin}` - compiled artifacts (gitignored) - `.env` - `TESTNET_SEED` (gitignored) - `scripts/v1-deprecated/` - archived v1 scripts (do not run against v2) - `contracts/hyperion/README.md` - Hyperion dialect + hypc workflow notes +- `docs/TERMINAL-WITHDRAWAL-RECEIPTS.md` - proposed Qrysm/go-qrl terminal receipt interface diff --git a/docs/architecture.md b/docs/architecture.md index c754201..048876a 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,254 +1,91 @@ -# QuantaPool v2 Architecture - -## Overview - -QuantaPool is a decentralized liquid staking protocol for QRL. Users deposit QRL and receive stQRL tokens representing their stake. The protocol uses a **fixed-balance token model** (like Lido's wstETH) where share balances remain constant and QRL value grows with rewards. - -## Architecture Diagram - -``` -┌─────────────────────────────────────────────────────────────┐ -│ User │ -└───────────────────────────┬─────────────────────────────────┘ - │ deposit() / requestWithdrawal() - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ DepositPool-v2.sol │ -│ - Accepts QRL deposits, mints stQRL shares │ -│ - Manages withdrawal queue (128-block delay) │ -│ - Trustless reward sync via balance checking │ -│ - Funds validators via beacon deposit contract │ -└───────────────────────────┬─────────────────────────────────┘ - │ mintShares() / burnShares() - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ stQRL-v2.sol │ -│ - Fixed-balance QRC-20 token (shares-based) │ -│ - balanceOf() = shares (stable, tax-friendly) │ -│ - getQRLValue() = QRL equivalent (grows with rewards) │ -│ - Virtual shares prevent first-depositor attacks │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ ValidatorManager.sol │ -│ - Tracks validator lifecycle (Pending → Active → Exited) │ -│ - Stores Dilithium pubkeys (2592 bytes) │ -│ - MVP: single trusted operator model │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ QRL Beacon Deposit Contract │ -│ - 40,000 QRL per validator │ -│ - Withdrawal credentials → DepositPool │ -└─────────────────────────────────────────────────────────────┘ -``` - -## Core Contracts - -### stQRL-v2.sol - Liquid Staking Token - -**Fixed-balance model** where `balanceOf()` returns shares (stable) and `getQRLValue()` returns QRL equivalent (fluctuates with rewards/slashing). - -| Function | Returns | Changes When | -|----------|---------|--------------| -| `balanceOf(user)` | Shares | Only on deposit/withdraw/transfer | -| `getQRLValue(user)` | QRL equivalent | Rewards accrue or slashing occurs | -| `getExchangeRate()` | QRL per share (1e18 scaled) | Rewards/slashing | - -**Key Features:** -- Virtual shares/assets (1e3) prevent first-depositor inflation attacks -- All QRC-20 operations work with shares, not QRL amounts -- Tax-friendly: balance only changes on explicit user actions - -**Example:** -``` -1. User deposits 100 QRL when pool has 1000 QRL / 1000 shares -2. User receives 100 shares, balanceOf() = 100 -3. Validators earn 50 QRL rewards (pool now 1050 QRL) -4. User's balanceOf() still = 100 shares (unchanged) -5. User's getQRLValue() = 100 × 1050 / 1000 = 105 QRL -``` - -### DepositPool-v2.sol - User Entry Point - -Handles deposits, withdrawals, and reward synchronization. - -**Deposit Flow:** -1. User calls `deposit()` with QRL -2. Contract reconciles the balance that existed before `msg.value` arrived -3. Shares calculated at current exchange rate -4. `stQRL.mintShares()` called, shares minted to user -5. `totalPooledQRL` updated - -**Withdrawal Flow:** -1. User calls `requestWithdrawal(shares)` -2. Shares lock and the contract returns an informational QRL estimate -3. Request queued with 128-block delay (~2 hours) -4. The owner earmarks liquid QRL in `withdrawalReserve`; those assets remain in - `totalPooledQRL` while the queued shares remain in total supply -5. User calls `claimWithdrawal()` after the delay and settled accounting -6. The contract calculates the current QRL value, then atomically burns shares, - reduces `totalPooledQRL`, reduces the reserve, and transfers QRL -7. If a funded request is cancelled or needs less QRL after settlement, the - owner releases the unused earmark with `releaseWithdrawalReserve(amount)` - -Queued shares continue receiving rewards and bearing slashing losses until they -are burned. Reserve funding cannot change the exchange rate because the assets -and their corresponding shares leave the conversion totals together at claim. - -**Trustless Reward Sync:** -- No oracle needed for reward detection -- `_syncRewards()` reconciles `address(this).balance + stakedQRL` against `totalPooledQRL` -- `withdrawalReserve` is a liquid subset of `totalPooledQRL`, not an additional - liability outside pooled accounting -- Balance increase = rewards, decrease = slashing -- EIP-4895 withdrawals automatically credit the contract -- `stakedQRL` tracks principal forwarded to the beacon deposit contract by the - real `fundValidator()` path, so the outgoing 40k stake is not misread as a - slashing event. When exit proceeds return, the owner calls - `recordValidatorExit(amount)` to settle that principal back into the - on-contract balance. Observed returned principal refills `bufferedQRL`, capped - at the nominal stake retired, so a slashed exit cannot create unsupported - validator-funding credit and unused exit proceeds can be staked again. -- Permissionless while all principal is on-contract (`stakedQRL == 0`): anyone - may call `syncRewards()`. Once principal is staked off-contract - (`stakedQRL > 0`), reward sync - including the implicit sync inside - `requestWithdrawal`/`claimWithdrawal` - is restricted to the owner. This - closes a front-running window: an exit sweep lands principal in the balance a - block before the owner can `recordValidatorExit()`, and an unrestricted sync - in that window would book the principal as a phantom reward and spike the - rate. Claims with unsettled on-chain deltas revert. With sync owner-gated - during that window, settlement and reward recognition are sequenced by the - operator and cannot be front-run. - -> **Known limitation (production):** the balance-diff sync is fully trustless -> only while staked QRL sits in the contract (`fundValidatorMVP`). Once -> `fundValidator()` moves principal off-contract, reward sync becomes -> owner-driven (see above) and cannot observe a *live* validator's accruing -> beacon balance, inactivity leak, or slashing until those amounts are swept -> on-chain via EIP-4895 - and the principal/reward split on return depends on -> the owner calling `recordValidatorExit()`. A fully self-custodial production -> reward mechanism over live beacon balances will require either periodic -> beacon-state input or an automated exit-settlement path. This is acceptable -> for the MVP/testnet trust model (single trusted operator) but must be -> hardened before mainnet. - -**Key Parameters:** -- `WITHDRAWAL_DELAY`: 128 blocks (~2 hours on QRL v2 testnet at ~60s/block, verified) -- `minDeposit`: 100 QRL default (configurable by owner, down to `ABSOLUTE_MIN_DEPOSIT = 0.001 QRL`) -- `VALIDATOR_STAKE`: 40,000 QRL - -### ValidatorManager.sol - Validator Lifecycle - -Tracks validators through their lifecycle: - -``` -None → Pending → Active → Exiting → Exited - ↓ - Slashed -``` - -**State Transitions:** -- `registerValidator(pubkey)` → Pending -- `activateValidator(id)` → Active (confirmed on beacon chain) -- `requestValidatorExit(id)` → Exiting -- `markValidatorExited(id)` → Exited -- `markValidatorSlashed(id)` → Slashed (from Active or Exiting) - -**Access Control:** -- Owner can perform all operations (trusted operator MVP) -- DepositPool can register validators - -## Security Model - -### Access Control - -| Contract | Role | Capabilities | -|----------|------|--------------| -| stQRL | Owner | Set depositPool (once), pause/unpause | -| stQRL | DepositPool | Mint/burn shares, update totalPooledQRL | -| DepositPool | Owner | Pause, set parameters, emergency withdraw excess | -| ValidatorManager | Owner | All validator state transitions | - -### Attack Mitigations - -| Attack | Mitigation | -|--------|------------| -| First depositor inflation | Virtual shares/assets (1e3 offset) | -| Reentrancy | CEI pattern, no external calls before state changes | -| Withdrawal front-running | 128-block delay, FIFO queue | -| Reserve-funded share dilution | Reserve and queued shares remain in the rate until atomic claim settlement | -| Unsynced reward capture | Deposits reconcile pre-deposit assets before minting | -| Reserved QRL sent to validators | Funding requires both buffer and liquid balance net of reserve | -| Withdrawal slashing evasion | Claims use the settled share value rather than the request estimate | -| Emergency fund drain | emergencyWithdraw limited to excess balance only | - -### Slashing Protection - -When slashing occurs: -1. `_syncRewards()` detects balance decrease -2. `totalPooledQRL` reduced proportionally -3. All stQRL holders share the loss via reduced `getQRLValue()` -4. Share balances unchanged (loss is implicit) - -## QRL-Specific Adaptations - -| Parameter | Ethereum | QRL | -|-----------|----------|----------| -| Validator stake | 32 ETH | 40,000 QRL | -| Block time | ~12s | ~60s | -| Signature scheme | ECDSA | Dilithium (ML-DSA-87) | -| Pubkey size | 48 bytes | 2,592 bytes | -| Signature size | 96 bytes | 4,627 bytes | - -## Test Coverage - -**Unit (Foundry, `contracts/test/`):** 226 tests, all green. -- `stQRL-v2.t.sol`: 68 tests (shares, conversions, rewards, slashing, minimum stake lock) -- `DepositPool-v2.t.sol`: 103 tests (deposits, withdrawals, reserve invariants, sync, off-contract stake accounting, front-run protection, access control) -- `ValidatorManager.t.sol`: 55 tests (lifecycle, slashing, batch operations) - -**Integration (live testnet, `scripts/integration-test-v2.js`):** 16 phases, all verified against the deployed contracts on chainId 1337. Covers deposit/mint, reward sync via EIP-4895-style balance donation, withdrawal request → 128-block delay → reserve funding → claim, pause/unpause, revert paths, validator lifecycle, QRC-20 allowance, batch activation, cancel. See `docs/V2-DEPLOYMENT-STATUS.md` for the phase matrix and current live state. - -## Deployment Checklist - -Automated by `node scripts/deploy-hyperion.js` in a single run. For reference, the sequence it performs: - -The deploy script refuses to submit transactions unless the connected chain matches the configured -`chainId` and `HYPERION_DEPLOY_CONFIRM` exactly matches -`DEPLOY:::`. The fingerprint binds the provider, -chain, deployer, existing addresses, confirmation depth, pending starting nonce, predicted CREATE -addresses, ABIs, and the exact in-memory bytecode snapshot used for deployment. Replacing non-empty -contract addresses also requires `HYPERION_REPLACE_EXISTING=true`. Set these values only after -checking the printed provider endpoint, chain, deployer, nonce, predicted addresses, fingerprint, -and existing deployment. A chain-and-deployer lock serializes local runs. The script rechecks both -the pending nonce and the original config digest before its first transaction, uses explicit -consecutive nonces, and rejects any deployed address that differs from the confirmed prediction. -Before updating the address config, it waits for the final nonce-ordered wiring transaction to reach -the configured `txConfirmations` depth, verifies that its receipt remains in the same canonical -block, verifies all links, owners, and paused states, and rechecks the config digest. Persistence uses -a randomized exclusive temporary file, file and directory syncs, and an atomic rename. - -1. Deploy `stQRLv2` (no constructor args) -2. Deploy `DepositPoolV2` (no constructor args; sets `minDeposit = 100 QRL`, `lastSyncBlock = block.number`) -3. Deploy `ValidatorManager` (no constructor args) -4. Pause `DepositPoolV2` and `stQRLv2` before wiring enables deposits -5. `pool.setStQRL(stQRL)` (**one-shot, irreversible**) -6. `stQRL.setDepositPool(pool)` (**one-shot, irreversible**) -7. `vm.setDepositPool(pool)` (reversible by owner) -8. Wait for confirmation depth, then verify all links, owners, and paused states -9. Transfer ownership to multisig (optional for mainnet) - -Fresh deployments remain paused until a separate operator action completes read-only verification, -seed-liquidity planning, and address publication. - -The two one-shot steps mean that wiring to the wrong address requires full redeploy. `deploy-hyperion.js` deploys in one tx each and wires immediately afterward using the contract instances returned by `.deploy().send()` (the wallet is pre-bound on those; see `contracts/hyperion/README.md` for the `@theqrl/web3` wallet-binding notes). - -## Future Improvements - -- [ ] Multi-operator support (permissionless registration) -- [ ] Two-step ownership transfer pattern -- [ ] Pagination for `getValidatorsByStatus()` -- [ ] On-chain integration between DepositPool and ValidatorManager +# Native QRL pooled staking + +QuantaPool uses four immutable core contracts and an optional immutable checkpoint executor. A new deployment has fresh state and no legacy token compatibility. There is no transferable staking receipt, lending, leverage, rehypothecation, discretionary investment allocation, guaranteed principal or guaranteed return. + +## Contract map and asset flow + +| Contract | Authoritative state and responsibility | +|---|---| +| `NativeFinalityVerifier` | Initial trusted header, immutable chain/domain/timing policy, authenticated current and next committee roots, accepted finalized headers, trust deadline and irreversible expiry | +| `NativePortfolioVerifier` | Gate-only canonical validator registry; complete bounded proofs of pool cash, validator records, all intervening deposits and withdrawals; committed snapshot and terminal observations | +| `NativeValidatorGate` | Original operator bootstrap beneficiary; canonical deposit runtime/domain; valid public exits; exact native top-up transactions and deposit-index history | +| `NativeQrlPool` / `NativeLedger` | Immutable fee beneficiary, risk assets, nontransferable positions, fee basis, loss/reward budget, cash reserves, FIFO requests, historical cash flows and recovery rights | +| `NativeCheckpointExecutor` | Optional typed batching of proof completion, settlement and bounded ledger work; immutable pool/portfolio bindings, no funds or authority | + +Users send QRL directly to `NativeQrlPool.deposit()`. Pending principal stays fully cash-backed, belongs to the sender and can be refunded before admission. A later finalized execution cutoff admits it at the settled portfolio value. Native funding moves from the pool through the immutable gate to the canonical deposit contract in one transaction. The gate has no discretionary asset transfer or pool balance editor. Validator withdrawal recipients are the pool's full 64-byte address. Consensus principal and rewards return directly to that address. + +Ordinary deposits require the configured minimum. Validator funding requires 38,000 QRL of free cash plus a 5% buffer against risk assets after bootstrap adoption. Reserved user claims, pending deposits, earned fees and quarantined ownerless cash cannot fund validators. Funding also waits for an empty withdrawal queue. The lifetime registry is capped at 64 admitted validators, including terminal records, with no privileged override. A new pool instance is required for additional lifetime admissions. + +## Operator preparation and canonical recipient + +Only the immutable validator-operator address can pay a native 2,000 QRL preparation through the gate. That address authorizes future signing-key enrollment and supplies its own capital. Anyone may later submit the complete proofs and finish admission of that authorized preparation. That preparation is outside the active pool ledger until admission. The gate validates the native deposit signature and root; the protocol determines the canonical validator record. After finalization, the portfolio verifier proves the exact public key, original pool withdrawal recipient, unslashed inactive status, RANDAO commitment and exact 2,000 QRL balance. + +Before releasing pooled funds, the gate verifies and stores an epoch-zero exit for the proven validator index and validates the 38,000 QRL top-up signature. It then atomically adopts the operator's 2,000 QRL as ordinary loss-bearing principal at the already settled checkpoint price and deposits the remaining 38,000 QRL. Any failure rolls back registration, stake adoption, exit storage and pooled funding together. + +An existing public key cannot change its canonical recipient through a later deposit. A foreign recipient, wrong balance or incompatible validator state fails admission. An unsolicited top-up can make preparation ineligible. New preparation is rejected after lifetime capacity is full, but concurrent preparations can exhaust remaining admission capacity before an earlier preparation is ready. Failed or abandoned preparation can leave the operator's own 2,000 QRL locked under unchanged native rules, particularly if the key disappears before publishing an exit. Pooled user funds never reimburse that preparation by discretion. + +## Authentication and complete accounting + +A deployment supplies an independently reviewed recent finalized header and chain identity. This is the initial trust anchor. Later updates require signatures authenticating at least 86 of the native 128 committee positions, Merkle membership, finalized-header proofs and authenticated committee transitions. RPC responses and node agreement only provide public proof material; they confer no reporting authority. + +This is a sampled-committee verifier with explicit committee-key honesty and initial-anchor assumptions. It does not replay the complete native state transition or whole-network stake-weighted consensus. Repeated committee keys can occupy multiple positions. The implementation supports the pinned fixed fork, consecutive occupied signing slots and occupied finalized epoch boundaries. Unsupported histories fail closed. The 64-slot economic freshness and 4,096-slot recovery window used locally are test parameters, not established safe production values. + +Each portfolio snapshot authenticates one matched finalized beacon state and its execution cutoff: + +1. The execution state root, execution block and consumed native deposit index through SSZ proofs. +2. The exact pool account cash through its native 64-byte account trie proof. +3. Every registered validator, in registry order, including true zero balances and terminal status. +4. Every occupied parent-linked block back to the previous snapshot, with complete native withdrawal and deposit lists. All deposits to registered public keys are counted, including third-party top-ups. + +Validator and deposit indices are separate namespaces. In-flight pool funding counts only deposits sent at or before the execution cutoff whose native deposit index is still unconsumed at that same cutoff. The ledger normalizes the historical balance sheet using on-chain cumulative user/adopted deposits and actual cash payouts. It never combines current cash with historical validator balances without that reconciliation. + +For an interval, consensus gain or loss is `new validator balance + native withdrawals - old validator balance - consumed validator deposits - newly adopted bootstrap principal`. The remaining independently authenticated asset increase is fee-exempt. Returned principal and third-party deposits therefore cannot masquerade as consensus rewards. + +## Positions, precision and losses + +The pool stores a position for each immutable beneficiary: internal stake units, contributed principal basis, separate fee basis and fractional carry, pending deposits, reserved principal/reward claims, one active request and recovery payments. The global ledger stores risk assets, total internal units and the fee-exempt-basis accumulator. Positions cannot be transferred or approved to another spender. + +Unreserved position value is the user's fraction of risk assets. Positive value above contributed principal is the available gross gain; value below principal records current loss exposure. Unreserved gains remain exposed to later native losses. A global asset change allocates rewards or losses proportionally without visiting every staker. A fee-basis accumulator allocates verified gifts in constant cost per later user action. Historical earned amounts can be indexed from on-chain events; the operator cannot set them. + +Multiplication uses native 512-bit intermediates. Admissions round internal units down; sales round units up; fee-exempt basis rounds conservatively against charging fees. Internal scales are `1e27` and `1e54`. The last normal exit requires the same priced snapshot to prove every validator terminal and all funding consumed, allowing a complete cash drain. A zero-valued user retains its position while validators remain nonterminal. Ownerless cash is quarantined and cannot be swept by the operator. Recovery can retain less than one smallest native unit per participant as rounding residue. + +## Withdrawals and rewards + +`requestWithdrawal(amount)` and `requestRewards(amount)` join the same global FIFO. The maximum integer requests the full available amount. Requests retain their stake exposure until a strictly later authenticated execution cutoff and successful cash reservation. A deposit made just before settlement cannot capture earlier rewards; a request made after a loss cannot use an earlier cutoff to avoid it. + +Anyone can stage a checkpoint, process bounded admissions and process the FIFO. A head request without enough free cash stays first. The keeper processes available cash before requesting further validator exits; public signed exits remain independently usable. Once reserved, principal and reward claims are fixed and fully cash-backed. `claim()` pays only the original beneficiary. A failing recipient does not block other reserved claims. No administrator can reorder or selectively compensate requests. Owners can cancel their own unprocessed requests and cash-backed pending deposits. + +Staged work has its own authenticated freshness check. Anyone may abandon a stale stage while preserving completed admissions and reserved payouts, then continue from a new verified snapshot. A newer finality root cannot make an old pricing stage fresh again. + +## Operator fee + +The fee is fixed at 1,000 basis points, or 10%. Its base is eligible realized net consensus gain when a FIFO request reserves actual cash. Unresolved consensus losses consume the global gain budget and then accumulate as loss carry. Subsequent gains recover that carry before creating new eligible fee income. Per-user principal and fee bases also constrain the chargeable amount. Gifts and unclassified cash add fee basis and never create a consensus fee budget. + +Each user carries a remainder from zero through nine base units, so splitting claims cannot evade the cumulative floor of the 10% charge. Fees go into a cash-backed earned reserve. Anyone can execute `claimFees()`, which always pays the immutable fee recipient. There is no arbitrary fee withdrawal, rate change, recipient change, principal sweep or retroactive accounting report. Previously earned fees are not clawed back after later losses. Recovery earns zero new fees, including on unknown later receipts. + +## Exits, expiry and disappearance + +The gate exposes signed epoch-zero exits before pooled top-up. An independent relayer can retrieve and broadcast them after native activation and eligibility. Mechanical exit-work events follow funding order. The native signature remains independently usable, so anyone holding it can force an eligible exit even without a pool request. This affects staking uptime and replacement cost. Future fork changes can invalidate stored exit domains. + +Keepers can authenticate fresh updates or perform bounded historical catch-up before the previously established verifier trust deadline. Historical progress alone does not extend that deadline. The pool also has an immutable accounting recovery window, initially anchored to its constructor checkpoint and renewed only by a complete portfolio checkpoint applied to the ledger. Finality updates, staged proofs and unapplied portfolios cannot renew that pool deadline. Once either deadline expires, normal pool operations cannot restart. Anyone can begin recovery, including while native finality remains healthy; no owner can reset the root, swap the verifier or reopen the pool. + +Recovery freezes existing risk weights, protects pending refunds, reserved user claims and already earned fees, and allocates actual current and later free cash proportionally. Rights survive earlier recovery payouts. The remaining validators may return cash later or suffer losses. Recovery cannot impose a native withdrawal deadline or recreate unavailable cash. If all proof suppliers disappear, expiry can occur despite a healthy native chain. A portfolio-specific data or capacity stall can also trigger pool recovery even while finality updates continue. Public proof caching and adequate archive retention improve liveness without supplying economic authority. The lifetime registry and occupied-header workload need explicit capacity qualification; see [measured scale limits](../native/proofs/SCALE.md). + +## Privileges and trust + +| Actor | Power, effect on deposits, compromise and disappearance | +|---|---| +| Deployer | Chooses immutable initial anchor, chain policy, graph bindings, minimum deposit and fee beneficiary. A malicious initial configuration undermines the deployment. No later owner or upgrade power exists; users must review the deployment before depositing. | +| Validator preparation address | Immutable and restricted to authorizing new validator preparations with its own capital. Cannot redirect pooled principal, edit positions or block existing public exits. Compromise exposes future validator operations to harmful signing-key selection; disappearance prevents new preparations. | +| Proof submitter / keeper | Supplies public authenticated data and executes deterministic batches. Cannot edit balances, redirect funds, change fees or bypass expiry. Withholding all useful proofs harms liveness; other keepers may continue. | +| Validator signing-key holder | Performs native consensus duties, can exit, go offline, be slashed and select execution-tip routing. Cannot change the established consensus withdrawal recipient or user beneficiary. Public signatures remove the need for a new exit signature under the supported fork. | +| Bootstrap payer | Risks its own preparation capital. Successful admission creates ordinary principal for its original address. Cannot claim another depositor's principal or obtain a discretionary reimbursement. | +| Fee recipient | Receives only earned reserved fees. Cannot select another destination, take principal, alter rates or block user claims. A recipient that rejects payment leaves only its fee reserve unpaid. | +| User / recovery caller | Requests or claims its own entitlement; anyone can begin permitted mechanical work or irreversible recovery after expiry. Cannot select other beneficiaries or change weights. | +| Owner, admin, proxy admin, upgrader, pauser, oracle/reward reporter, emergency multisig | No such privilege exists in the native contracts. There is no administrative pause, confiscation, discretionary balance setter, rescue transfer or upgrade. | + +Execution transaction tips are a material operator-control limitation. Only received, authenticated assets enter enforceable accounting. The product must not promise every tip reaches the pool. Public exits, committee honesty, deployment trust, proof availability, unsupported forks and indefinite native recovery remain review items before launch. These technical facts support review of actual control under the supplied Dutch BV report; this implementation makes no new legal classification or clearance claim. + +## Storage and history + +All four contracts use new storage and immutable cross-contract references. The old token balances, approvals, exchange rates, proxy/admin slots and deployed validators are not imported. [Removed executable files](../native/RETIRED-FILES.json) and [historical documentation](legacy/V2-ARCHITECTURE.md) record the previous system. The compiler emits each new storage layout under `build/native/`; there is no in-place storage migration. diff --git a/docs/legacy/V2-ARCHITECTURE.md b/docs/legacy/V2-ARCHITECTURE.md new file mode 100644 index 0000000..b305084 --- /dev/null +++ b/docs/legacy/V2-ARCHITECTURE.md @@ -0,0 +1,371 @@ +# Archived v2 testnet architecture + +Retired on 2026-09-15. This is the previous token architecture. Its operator reporting, token accounting, release plans and suggested upstream changes are historical. Current work follows the [native-QRL architecture](../architecture.md) against unmodified QRL. + +# QuantaPool v2 Architecture + +## Overview + +QuantaPool is a decentralized liquid staking protocol for QRL. Users deposit QRL and receive stQRL tokens representing their stake. The protocol uses a **fixed-balance token model** (like Lido's wstETH) where share balances remain constant and QRL value grows with rewards. + +## Architecture Diagram + +``` +┌─────────────────────────────────────────────────────────────┐ +│ User │ +└───────────────────────────┬─────────────────────────────────┘ + │ deposit() / requestWithdrawal() + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ DepositPool-v2.hyp │ +│ - Accepts QRL deposits, mints stQRL shares │ +│ - Manages withdrawal queue (128-block delay) │ +│ - Permissionless normal-state reward sync │ +│ - Atomic validator registration and beacon funding │ +│ - Fixed performance fee settled after complete cohort exit │ +└───────────────────────────┬─────────────────────────────────┘ + │ mintShares() / burnShares() + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ stQRL-v2.hyp │ +│ - Fixed-balance QRC-20 token (shares-based) │ +│ - balanceOf() = shares (stable, tax-friendly) │ +│ - getQRLValue() = QRL equivalent (grows with rewards) │ +│ - Virtual shares prevent first-depositor attacks │ +│ - Mints dilution-priced protocol fee shares │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ ValidatorManager.hyp │ +│ - Tracks canonical validator identity and principal │ +│ - Stores Dilithium pubkeys (2592 bytes) │ +│ - Immutable DepositPool authority │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ QRL Beacon Deposit Contract │ +│ - 40,000 QRL per validator │ +│ - Withdrawal credentials → DepositPool │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Core Contracts + +### stQRL-v2.hyp - Liquid Staking Token + +**Fixed-balance model** where `balanceOf()` returns shares (stable) and `getQRLValue()` returns QRL equivalent (fluctuates with rewards/slashing). + +| Function | Returns | Changes When | +|----------|---------|--------------| +| `balanceOf(user)` | Shares | Only on deposit/withdraw/transfer | +| `getQRLValue(user)` | QRL equivalent | Rewards accrue or slashing occurs | +| `getExchangeRate()` | QRL per share (1e18 scaled) | Rewards/slashing | + +**Key Features:** +- Virtual shares/assets (1e3) prevent first-depositor inflation attacks +- All QRC-20 operations work with shares, not QRL amounts +- Tax-friendly: balance only changes on explicit user actions + +**Example:** +``` +1. The pool has 1000 QRL / 1000 shares; a user owns 100 shares +2. Validators earn gross rewards and the protocol fee settles as shares +3. The user's balanceOf() remains 100 shares +4. The user's getQRLValue() rises at the final fee-adjusted exchange rate +``` + +### DepositPool-v2.hyp - User Entry Point + +Handles deposits, withdrawals, and reward synchronization. + +**Deposit Flow:** +1. User calls `deposit()` with QRL while no validator principal is outstanding +2. Contract rejects the deposit if any principal record remains outstanding +3. Contract reconciles the balance that existed before `msg.value` arrived +4. Shares calculated at current exchange rate +5. `stQRL.mintShares()` called, shares minted to user +6. `totalPooledQRL` updated + +**Withdrawal Flow:** +1. User calls `requestWithdrawal(shares)` +2. Shares lock and the contract returns an informational QRL estimate +3. Request queued with 128-block delay (~2 hours) +4. With zero outstanding principal, the owner earmarks liquid QRL in + `withdrawalReserve`; those assets remain in `totalPooledQRL` while the queued + shares remain in total supply +5. User calls `claimWithdrawal()` after the delay, after every validator + principal record is retired, and after complete accounting settlement +6. The contract calculates the current QRL value, then atomically burns shares, + reduces `totalPooledQRL`, reduces the reserve, and transfers QRL +7. If newly synchronized rewards increase the payout above an exact earmark, + the claim may consume only the liquid unbuffered increase. Deposit-origin + validator buffer remains unavailable to the claim. +8. If a funded request is cancelled or needs less QRL after settlement, the + owner releases the unused earmark with `releaseWithdrawalReserve(amount)` + +Queued shares continue receiving rewards and bearing slashing losses until they +are burned. Reserve funding cannot change the exchange rate because the assets +and their corresponding shares leave the conversion totals together at claim. + +**Normal-State Reward Sync:** +- Ordinary liquid balance gains need no reward oracle while no validator principal is outstanding +- `_syncRewards()` reconciles `address(this).balance + stakedQRL()` against `totalPooledQRL` +- `withdrawalReserve` is a liquid subset of `totalPooledQRL`, not an additional + liability outside pooled accounting +- Balance increases from partial withdrawals or direct transfers are pooled gains +- EIP-4895 withdrawals automatically credit the contract +- `ValidatorManager.totalPrincipalOutstanding()` is the single source for + `stakedQRL()`. Funding registers the pubkey and root, records 40,000 QRL of + principal, and calls the beacon deposit contract in one transaction. +- `syncRewards()` remains permissionless only when no principal is outstanding. +- While any principal is outstanding, deposits and claims remain closed even + when the observable accounting equation is equal. Equality cannot prove that + an unannounced zero-return loss or correlated validator return did not occur. +- The owner uses + `recognizeValidatorRewards(amount, finalizedCheckpointSlot, evidenceRoot)` + after checking every outstanding validator in finalized Qrysm state. It + recognizes only the supplied amount, leaves later or larger balance credit + unclassified, and rejects reused or nonmonotonic evidence. + +**Protocol Performance Fee:** +- The fee is fixed at 1,000 basis points, or one tenth of fee-eligible + validator rewards. Deposits, withdrawals, and zero-principal donations have + no fee. +- The first validator funding transaction opens a fee epoch. User share supply + remains fixed while validator principal is outstanding. +- Finalized-evidence reward recognition accumulates the maximum fee base. A fee + cannot mint while any principal remains outstanding. +- The last terminal settlement schedules fee finalization and retains missing + principal as a temporary receivable. A later finalized Qrysm checkpoint + reconciles late principal and explicitly classified late rewards before the + epoch closes. +- Finalization caps eligible rewards by the complete epoch's net asset gain. + Prior epoch losses must be recovered before a later fee accrues. +- The fee is issued as stQRL shares to an immutable QIP-55 recipient. QRL stays + in the pool, so fee shares bear future slashing and use the normal withdrawal + queue. +- Dilution pricing uses the post-mint equation with the token's virtual offsets: + `feeShares = floor(feeAssets * (shares + 1000) / (pooled + 1000 - feeAssets))`. + Integer reward and share dust carries forward to a later profitable epoch. +- Each close checkpoints the live loss and fee carries against post-fee-mint + supply. Closed-epoch burns recompute carry only at a new historical supply + minimum. Temporary deposit-and-burn cycles cannot amplify or wash the + checkpoint, and split burns produce the same result. +- Fungible pool-level accounting cannot identify replacement capital while + aggregate supply stays above its historical minimum. That case can extend an + existing loss shelter and conservatively undercollect fees. Exact cohort + attribution requires per-share or per-account equalization in a later token + design. +- Current finalized Qrysm evidence is operator checked metadata. The whole-epoch + gain cap protects pooled principal from fee extraction, while complete + trustless reward classification depends on the terminal receipt proposal in + `docs/TERMINAL-WITHDRAWAL-RECEIPTS.md`. +- A finalized close-window surplus is only an upper bound because a donation is + observationally identical to reward at the contract address. Standard tooling + finalizes that residual as fee-exempt. Exact pre-settlement recognition remains + chargeable, and a future authenticated receipt can safely classify the late + remainder. +- The finalized execution balance is a minimum for the close transaction. + Positive receipt-time drift is synchronized as fee-exempt, preventing a + permissionless donation from repeatedly reverting finalization. A balance + below the finalized floor still fails closed. + +**Serialized Exit Settlement:** +1. `beginValidatorExit(id)` checkpoints the pool balance and moves the funded + validator from Active to Exiting atomically. Unclassified inflow cannot block + this lifecycle transition. +2. Deposits and claims stay closed while any principal remains outstanding. + Explicit exits also close permissionless reward sync, validator funding, + and reserve funding. Bounded finalized-evidence reward recognition remains + available until terminal settlement. Withdrawal requests, cancellations, + and token transfers remain available. +3. After one finalized Qrysm view identifies every validator that is fully + withdrawn, `settleValidatorExits(ids)` retires their fixed principal, credits + observed returned liquidity to the buffer, and finalizes every manager status + in one transaction. Missing return remains a temporary pooled receivable. +4. After the final principal record retires, accounting-changing entry points + remain locked. A newer finalized Qrysm checkpoint and its canonical execution + payload bind the later `finalizeProtocolFeeEpoch(...)` call. This call + reconciles late principal, recognizes only the supplied bounded late reward, + writes off any remaining receivable once, synchronizes the final pooled value, + and mints the exact dilution-priced fee shares. +5. Accounting and share-supply changing entry points stay closed through the + finalization block. This gives receipt-block RPC reads a stable close state; + the entry points reopen in the next block. Supply-neutral withdrawal requests, + cancellations, transfers, and direct positive inflow remain available. +6. If the voluntary-exit submission was not accepted and Qrysm still reports + the validator active, `cancelValidatorExit(id)` restores Active state and + clears the checkpoint. Any accumulated balance delta remains quarantined for + a separate bounded reward-recognition transaction. +7. If Qrysm reveals one or more terminal withdrawals after their returns have + already reached the pool, the same batch function provides an owner-only + accounting recovery path that retires all identified principal before + repricing shares. +8. Exact principal return produces no rate change. Surplus becomes reward and + a shortfall becomes a slashing loss. + +> **Consensus-proof limitation:** execution withdrawals do not currently commit +> whether a withdrawal is partial or terminal. The operator tooling verifies +> exact Qrysm state before reward recognition, cancellation, and settlement, +> but the contract cannot verify that REST evidence. Positive unannounced +> returns remain quarantined unless the trusted owner misclassifies them. A +> zero-return terminal loss creates no execution-balance delta and remains +> invisible until principal is retired. The contract therefore defers every +> share-priced deposit and claim until all principal is retired. A finalized +> withdrawal-receipt precompile, system accumulator, or equivalent QRL protocol +> primitive is required to restore safe continuous liquid-staking flows. +> Until that primitive exists, the owner remains a lifecycle oracle and can +> retire a live Active validator through the recovery path. A compromised or +> mistaken owner can reopen pricing against an invalid principal ledger. + +The fail-closed boundary has a deliberate availability cost. One Pending or +otherwise nonterminal validator keeps deposits, claims, reserve funding, and +emergency recovery closed for the complete cohort lifetime. `getQRLValue()` can +also remain provisional during partial settlement, so external protocols must +not treat it as finalized while principal is outstanding. + +**Key Parameters:** +- `WITHDRAWAL_DELAY`: 128 blocks (~2 hours on QRL v2 testnet at ~60s/block, verified) +- `minDeposit`: 100 QRL default (configurable by owner, down to `ABSOLUTE_MIN_DEPOSIT = 0.001 QRL`) +- `VALIDATOR_STAKE`: 40,000 QRL +- `PROTOCOL_FEE_BPS`: 1,000 (10% of fee-eligible validator rewards) +- `protocolFeeRecipient`: immutable full QIP-55 address selected at deployment + +### ValidatorManager.hyp - Validator Lifecycle + +Tracks validators through their lifecycle: + +``` +None → Pending → Active → Exiting → Exited + ↓ + Slashed +``` + +**State Transitions:** +- `DepositPool.fundValidator(...)` → manager Pending with 40,000 QRL outstanding +- `DepositPool.reportValidatorActive(id)` → Active (operator report gated on Qrysm) +- `DepositPool.beginValidatorExit(id)` → Exiting +- `DepositPool.cancelValidatorExit(id)` → Active when Qrysm still reports active +- `DepositPool.reportValidatorSlashed(id)` → Slashed with principal still outstanding +- `DepositPool.settleValidatorExits(ids)` → all identified terminal principal retired and statuses finalized atomically +- `DepositPool.finalizeProtocolFeeEpoch(...)` → late principal reconciled, remaining loss synchronized, and exact fee shares minted from fresh finalized evidence + +**Access Control:** +- ValidatorManager has one immutable DepositPool authority +- Direct registration and lifecycle mutation on ValidatorManager always revert +- DepositPool owner submits operational lifecycle reports + +## Security Model + +### Access Control + +| Contract | Role | Capabilities | +|----------|------|--------------| +| stQRL | Owner | Set depositPool (once), pause/unpause | +| stQRL | DepositPool | Mint/burn user shares, mint protocol fee shares, update totalPooledQRL | +| DepositPool | Owner | Pause, set parameters, fund validators, submit lifecycle reports | +| ValidatorManager | DepositPool | Register funded identities, transition status, retire principal | + +### Attack Mitigations + +| Attack | Mitigation | +|--------|------------| +| First depositor inflation | Virtual shares/assets (1e3 offset) | +| Reentrancy | CEI pattern, no external calls before state changes | +| Withdrawal front-running | 128-block delay, FIFO queue | +| Reserve-funded share dilution | Reserve and queued shares remain in the rate until atomic claim settlement | +| Unsynced reward capture | Deposits reconcile pre-deposit assets before minting | +| Forced balance donation while principal is outstanding | Deposits and claims stay closed until all principal is retired and accounting is settled | +| Reward growth above an exact claim reserve | Claim consumes only the synchronized unbuffered liquid increase | +| Reserved QRL sent to validators | Funding requires both buffer and liquid balance net of reserve | +| Withdrawal slashing evasion | Claims use the settled share value rather than the request estimate | +| Duplicate validator deposit | Pool-bound manager rejects duplicate pubkeys and deposit roots before value transfer | +| Exit principal booked as reward | Bounded recognition quarantines extra credit; priced flows remain closed until every principal record is retired | +| Fee charged against principal | Fee waits for later finalized zero-principal accounting and is capped by complete epoch net gain | +| Late principal counted as loss and reward | Retired principal remains a temporary receivable until later finalized fee-epoch closure | +| Fee charged before loss recovery | Persistent loss carryforward absorbs later eligible rewards first | +| Fee recipient callback or native transfer | Fee is minted as pool-backed shares to one immutable recipient | +| Reward evidence replay | Unique state-root evidence and strictly increasing finalized slots | +| Donation front-runs delayed finalization | Finalized balance is a floor; positive drift is synchronized fee-exempt and exact postconditions are checked at the receipt block | +| Deposit or claim follows finalization in the same block | One-block accounting cooldown preserves the exact receipt-block supply and carry checkpoint | +| Failed voluntary-exit submission | Qrysm-gated cancellation restores Active state and reopens synchronization | +| Emergency fund drain | emergencyWithdraw limited to excess balance only | + +### Slashing Protection + +When a terminal return is below the validator's fixed principal: +1. `settleValidatorExits(ids)` retires exactly the included principal and records any missing return as a temporary receivable +2. Later finalized epoch closure writes off only the unrecovered receivable and reduces `totalPooledQRL` +3. All stQRL holders share the loss via reduced `getQRLValue()` +4. Share balances unchanged (loss is implicit) + +## QRL-Specific Adaptations + +| Parameter | Ethereum | QRL | +|-----------|----------|----------| +| Validator stake | 32 ETH | 40,000 QRL | +| Block time | ~12s | ~60s | +| Signature scheme | ECDSA | Dilithium (ML-DSA-87) | +| Pubkey size | 48 bytes | 2,592 bytes | +| Signature size | 96 bytes | 4,627 bytes | + +## Validation Coverage + +**Current Hyperion validation:** +- `contracts/hyperion/*.hyp` are the canonical contract sources. +- `npm run compile:hyperion` compiles those sources directly with reviewed optimized via-IR `hypc` settings and writes ABI, bytecode, and manifest artifacts under `build/hyperion/`. The build rejects production artifacts that exceed go-qrl's 24,576-byte runtime limit. +- `npm test` verifies the artifact manifest and production ABI, executes the targeted release-blocker suite through pinned Hyperion and qrvmone binaries with default and optimized code generation, parses the broader retained `.t.hyp` specifications, and runs the executable Node tooling tests. +- The native suite covers unequal small stakers, atomic real funding rollback, exact-reserve reward growth, canonical validator identity, cancellation, correlated batch returns, reward and slashing algebra, zero-return loss, pre-arrived recovery, and replay guards. +- The broader `contracts/test/hyperion/*.t.hyp` unit and fuzz files are historical pre-v2.4 archives, include retired API cases, and remain parse-only evidence. + +**Historical pre-removal evidence:** the former Solidity and Foundry suite was observed passing 226 tests before the `.sol` contracts and `.t.sol` tests were removed: +- `stQRL-v2.t.sol`: 68 tests (shares, conversions, rewards, slashing, minimum stake lock) +- `DepositPool-v2.t.sol`: 103 tests (deposits, withdrawals, reserve invariants, sync, off-contract stake accounting, front-run protection, access control) +- `ValidatorManager.t.sol`: 55 tests (lifecycle, slashing, batch operations) + +The 226-test result is historical evidence about the pre-removal tree. It is not a current executable result for the canonical `.hyp` sources. + +**Integration (live testnet, `scripts/integration-test-v2.js`):** 16 phases, all verified against the deployed contracts on chainId 1337. Covers deposit/mint, reward sync via EIP-4895-style balance donation, withdrawal request → 128-block delay → reserve funding → claim, pause/unpause, revert paths, validator lifecycle, QRC-20 allowance, batch activation, cancel. See `docs/V2-DEPLOYMENT-STATUS.md` for the phase matrix and current live state. + +## Deployment Checklist + +Automated by `node scripts/deploy-hyperion.js` in a single run. For reference, the sequence it performs: + +The deploy script refuses to submit transactions unless the connected chain matches the configured +`chainId` and `HYPERION_DEPLOY_CONFIRM` exactly matches +`DEPLOY:::`. The fingerprint binds the provider, +chain, deployer, existing addresses, confirmation depth, pending starting nonce, predicted CREATE +addresses, the immutable protocol fee recipient, ABIs, and the exact in-memory bytecode snapshot used for deployment. Replacing non-empty +contract addresses also requires `HYPERION_REPLACE_EXISTING=true`. Set these values only after +checking the printed provider endpoint, chain, deployer, nonce, predicted addresses, fingerprint, +and existing deployment. A chain-and-deployer lock serializes local runs. The script rechecks both +the pending nonce and the original config digest before its first transaction, uses explicit +consecutive nonces, and rejects any deployed address that differs from the confirmed prediction. +Before updating the address config, it waits for the final nonce-ordered wiring transaction to reach +the configured `txConfirmations` depth, verifies that its receipt remains in the same canonical +block, verifies all links, owners, and paused states, and rechecks the config digest. Persistence uses +a randomized exclusive temporary file, file and directory syncs, and an atomic rename. + +1. Deploy `stQRLv2` (no constructor args) +2. Deploy `DepositPoolV2(depositContract, predictedValidatorManager, protocolFeeRecipient)` +3. Deploy `ValidatorManager(predictedDepositPool)` +4. Pause `DepositPoolV2` and `stQRLv2` before wiring enables deposits +5. `pool.setStQRL(stQRL)` (**one-shot, irreversible**) +6. `stQRL.setDepositPool(pool)` (**one-shot, irreversible**) +7. Wait for confirmation depth, then verify immutable reciprocal links, fee recipient, owners, and paused states +8. Transfer DepositPool and stQRL ownership to multisig (optional for mainnet) + +Fresh deployments remain paused until a separate operator action completes read-only verification, +seed-liquidity planning, and address publication. + +The two one-shot steps mean that wiring to the wrong address requires full redeploy. `deploy-hyperion.js` deploys in one tx each and wires immediately afterward using the contract instances returned by `.deploy().send()` (the wallet is pre-bound on those; see `contracts/hyperion/README.md` for the `@theqrl/web3` wallet-binding notes). + +## Future Improvements + +- [ ] Multi-operator support (permissionless registration) +- [ ] Two-step ownership transfer pattern +- [ ] Pagination for `getValidatorsByStatus()` +- [x] Atomic on-chain integration between DepositPool and ValidatorManager +- [ ] Verifiable consensus lifecycle and terminal-withdrawal receipt primitive specified in `docs/TERMINAL-WITHDRAWAL-RECEIPTS.md` diff --git a/docs/legacy/V2-README.md b/docs/legacy/V2-README.md new file mode 100644 index 0000000..d4a0bf3 --- /dev/null +++ b/docs/legacy/V2-README.md @@ -0,0 +1,303 @@ +# Archived v2 testnet README + +Retired on 2026-09-15. This document preserves the previous testnet design and its historical claims. It supplies no current launch instructions or migration requirements. See the [native-QRL architecture](../architecture.md) and [testnet retirement decision](../TESTNET-RETIREMENT.md). + +# QuantaPool + +Decentralized liquid staking protocol for QRL. Deposit QRL, receive stQRL, earn validator rewards automatically. + +## Overview + +QuantaPool enables QRL holders to participate in Proof-of-Stake validation without running their own validator nodes. Users deposit QRL and receive stQRL, a fixed-balance token where `balanceOf()` returns stable shares and `getQRLValue()` returns the current QRL equivalent (which grows with rewards). + +### Key Features + +- **Liquid Staking**: Receive stQRL tokens that can be transferred while underlying QRL earns rewards +- **Fixed-Balance Token**: Share balance stays constant (tax-friendly), QRL value grows with rewards +- **Slashing-Safe**: Fixed-balance design handles slashing by proportionally reducing all holders' QRL value +- **Bounded Reward Accounting**: Zero-principal sync is permissionless; active-validator rewards use amount-bound owner recognition after finalized Qrysm checks +- **Loss-Aware Performance Fee**: Fee-enabled deployments mint shares worth 10% of finalized-evidence validator rewards only after complete cohort settlement, a later finalized accounting pass, and prior loss recovery +- **Griefing-Resistant**: Fresh deposits mature for ~1 day (owner-tunable) before they can be transferred or withdrawn, blocking deposit/withdraw yo-yo attacks on pool liquidity +- **Post-Quantum Secure**: Built on QRL's Dilithium ML-DSA-87 signature scheme +- **Production Infrastructure**: Terraform + Ansible for automated validator deployment +- **Monitoring Stack**: Prometheus, Grafana dashboards, and Alertmanager with Discord/Telegram alerts +- **Web Frontend**: React staking app live at [quantapool.com](https://quantapool.com) and [quantapool.io](https://quantapool.io) + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ User │ +└───────────────────────────┬─────────────────────────────────┘ + │ deposit() / requestWithdrawal() + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ DepositPool-v2.hyp │ +│ - Accepts deposits, mints stQRL shares │ +│ - Queues and processes withdrawals │ +│ - Fail-closed validator reward and exit accounting │ +│ - Cohort-level performance fee accounting │ +│ - Funds validators via beacon deposit contract │ +└───────────────────────────┬─────────────────────────────────┘ + │ mintShares() / burnShares() + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ stQRL-v2.hyp │ +│ - Fixed-balance QRC-20 token │ +│ - Shares-based accounting (wstETH-style) │ +│ - balanceOf = shares, getQRLValue = QRL equivalent │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ ValidatorManager.hyp │ +│ - Tracks validator states (pending → active → exited) │ +│ - Stores Dilithium pubkeys (2,592 bytes) │ +│ - MVP: single trusted operator model │ +└───────────────────────────┬─────────────────────────────────┘ + │ + ┌─────────────┴─────────────┐ + ▼ ▼ +┌──────────────────────┐ ┌──────────────────────────────┐ +│ Infrastructure │ │ Monitoring │ +│ Terraform + Ansible │ │ Prometheus + Grafana │ +│ gqrl, qrysm nodes │ │ Contract exporter + alerts │ +└──────────────────────┘ └──────────────────────────────┘ +``` + +## Project Structure + +``` +QuantaPool/ +├── contracts/ # All on-chain code lives here +│ ├── hyperion/ # Canonical Hyperion sources +│ │ ├── stQRL-v2.hyp # Fixed-balance liquid staking token +│ │ ├── DepositPool-v2.hyp# Deposits, withdrawals, reward sync +│ │ ├── ValidatorManager.hyp # Validator lifecycle tracking +│ │ └── README.md # hypc workflow and validation status +│ └── test/hyperion/ # Hyperion behavioral test sources +│ ├── stQRL-v2.t.hyp +│ ├── DepositPool-v2.t.hyp +│ └── ValidatorManager.t.hyp +├── build/hyperion/ # hypc output (ABI, bin, manifest.json) - gitignored +├── frontend/ # React staking app (quantapool.com / quantapool.io) +├── infrastructure/ # Production validator deployment +│ ├── terraform/ # Hetzner Cloud provisioning +│ ├── ansible/ # Node configuration (gqrl, qrysm) +│ ├── scripts/ # deploy.sh, failover.sh, health-check.sh +│ └── docs/ # Runbooks and deployment guides +├── monitoring/ # Observability stack +│ ├── prometheus/ # Scrape config + alert rules +│ ├── grafana/ # Dashboards (validator, contract, system) +│ ├── alertmanager/ # Discord/Telegram routing by severity +│ └── contract-exporter/ # Custom Node.js exporter for on-chain metrics +├── key-management/ # Validator key lifecycle scripts +├── scripts/ # Build & deployment automation +├── config/ # Network deployment configs +└── docs/ # Architecture docs +``` + +## Contracts + +| Contract | LOC | Purpose | +|----------|-----|---------| +| `stQRL-v2.hyp` | 577 | Fixed-balance liquid staking token (shares-based, min-stake maturity lock) | +| `DepositPool-v2.hyp` | 1,145 | User entry point, deposits/withdrawals, bounded reward accounting | +| `ValidatorManager.hyp` | 335 | Validator lifecycle: Pending → Active → Exiting → Exited | + +All on-chain code lives under `contracts/`. Files in `contracts/hyperion/` are the canonical, hand-edited contract sources. `scripts/compile-hyperion.js` compiles them directly with `hypc`; there is no Solidity regeneration step. The targeted release-blocker suite under `contracts/test/hyperion/semantic/` executes against the canonical contracts with the pinned Hyperion runner and qrvmone. The broader root `.t.hyp` files are historical pre-v2.4 archives, include retired API cases, and remain parse-only. Compiled artifacts land in `build/hyperion/` (gitignored). + +## How Fixed-Balance Model Works + +1. The pool has 1000 QRL and 1000 shares; a user owns 100 shares +2. Validators earn gross rewards and the protocol fee settles as new fee-recipient shares +3. The user's `balanceOf()` remains **100 shares** (unchanged, tax-friendly) +4. The user's `getQRLValue()` rises at the final, fee-adjusted exchange rate + +If slashing occurs (pool drops to 950 QRL): +- User's `balanceOf()` still = **100 shares** +- User's `getQRLValue()` = 100 × 950 / 1000 = **95 QRL** +- Loss distributed proportionally to all holders + +## How Withdrawals Work + +Unstaking is a two-step flow. The QRL is never burned; the stQRL shares are. + +1. **Request** (`requestWithdrawal(shares)`): your shares are locked (non-transferable, still on your balance) and the contract returns a current QRL estimate. A 128-block delay (~2 hours) starts. You can `cancelWithdrawal()` any time before claiming; the shares simply unlock. +2. **Claim** (`claimWithdrawal()`, FIFO per account): after the delay, after all validator principal is retired, and after complete accounting settlement, the locked shares are valued at the current exchange rate and burned. You receive that settled QRL amount from the withdrawal reserve. Queued shares continue receiving rewards and bearing slashing losses until claim. + +Deposits and claims remain closed while any validator principal is outstanding. +An equal execution-balance equation cannot reveal a zero-return loss or a +correlated return from another validator. The operator can recognize a bounded +reward for accounting, but priced user flows reopen only after every principal +record is retired and the complete balance is reconciled. + +This safety mode is cohort-based: one Pending or otherwise nonterminal validator +keeps deposits, claims, reserve funding, and emergency recovery closed. The +operator remains a trusted lifecycle reporter until QRL exposes authenticated +per-validator terminal-withdrawal receipts. External protocols should treat the +displayed stQRL rate as provisional while principal is outstanding. + +## How the Protocol Fee Works + +Fee-enabled deployments charge 10% of fee-eligible validator rewards. The +rate is a contract constant. Deposits, withdrawals, and zero-principal +donations carry no fee. + +The contract opens one fee epoch when the first validator is funded. Finalized +Qrysm evidence can classify exact reward amounts while principal is +outstanding, but no fee shares mint during that interval. The last validator +settlement retires principal and schedules a separate fee finalization. A fresh, +later finalized Qrysm checkpoint gives late principal and rewards time to reach +the execution layer before `finalizeProtocolFeeEpoch(...)` fixes the epoch's +complete accounting. The finalizer caps the fee base by net gain, recovers prior +epoch loss first, and mints dilution-priced stQRL shares to the immutable fee +recipient. All QRL remains in the pool, and fee shares bear later slashing and +exit through the same withdrawal queue as other shares. + +When a closed epoch retains loss or fee dust, the contract checkpoints that +carry against the closing share supply. A later burn reduces it only when total +shares reach a new historical minimum. This makes split burns path independent, +prevents departed supply from leaving its full loss shelter behind, and prevents +temporary new deposits from washing the checkpoint. + +Retired principal that has not reached the pool is represented as a temporary +receivable during this close window. Late recovery replaces that receivable and +restores the validator buffer. Any amount still missing at finalization becomes +the epoch loss once. Deposits, claims, reserve funding, ordinary reward sync, +emergency recovery, and new validator funding stay locked until finalization. +Accounting and share-supply changes remain locked for the rest of the +finalization block so receipt-block verification observes one stable close. +They reopen from the next block. + +The current operator checks finalized Qrysm evidence off-chain. The contract's +whole-epoch gain cap protects principal, while trustless continuous accounting +requires the consensus receipt interface specified in +[`docs/TERMINAL-WITHDRAWAL-RECEIPTS.md`](../../docs/TERMINAL-WITHDRAWAL-RECEIPTS.md). +The standard finalization tool treats residual positive close-window surplus as +fee-exempt because a finalized balance proves only an upper bound, not whether +the source was validator reward or an arbitrary donation. Rewards recognized +from exact evidence before settlement still enter the 10% fee base. The +finalized execution balance is enforced as a floor. A later positive balance +change is synchronized as fee-exempt, so a direct donation cannot keep the +close transaction reverting and leave priced pool operations locked. + +**Example:** you hold 1,000 stQRL at rate 1.05. Request locks the shares and estimates 1,050 QRL. If the settled rate is 1.06 at claim, 1,000 stQRL burns and you receive 1,060 QRL. If slashing reduces the settled rate, the claim decreases proportionally. + +### Where does the claim QRL come from? + +The withdrawal reserve never holds the full TVL; it only covers pending claims. Pooled QRL lives in three places (all visible on-chain): validators (40,000 QRL each, staked on the beacon chain), the deposit buffer (accumulating toward the next validator), and the withdrawal reserve. Claims are sourced in this order: + +1. **Pre-funding buffer**: with zero outstanding principal, QRL in the buffer can be earmarked in the reserve. Reserved QRL remains in pooled accounting until claim burns the matching shares. +2. **Complete validator settlement**: funded validator stake is all-or-nothing. The complete outstanding cohort exits, a later finalized checkpoint closes its accounting epoch, and only then can reserve funding or a claim proceed. Returned principal refills the buffer. + +If a funded request is cancelled or its settled payout falls, the operator can +call `releaseWithdrawalReserve(amount)`. The contract restores only liquidity +that originally came from `bufferedQRL`, so simulated stake and unbuffered +rewards cannot be counted again as fresh validator principal. + +If validators are funded when a 5,000 QRL withdrawal is queued, the operator must retire the complete outstanding validator cohort before the claim can execute. Returned principal refills the buffer, complete settlement fixes the share rate, and the claim then consumes its earmarked liquidity. + +### Is there a cap? + +Deposits are uncapped while no validator principal is outstanding. A deposit reverts after validator funding until the complete outstanding cohort is retired. You can always *request* a withdrawal, while `claimWithdrawal()` waits for the reserve, the 128-block delay, and zero outstanding principal. Fresh deposits also carry a minimum stake lock (default ~1 day) before the shares can transfer or enter a withdrawal request; this is anti-griefing protection. + +## Infrastructure + +Production-ready validator infrastructure using Terraform and Ansible. + +**Components provisioned:** +- **Primary validator node** - gqrl (execution) + qrysm-beacon + qrysm-validator +- **Backup validator node** - hot standby with failover script +- **Monitoring server** - Prometheus, Grafana, Alertmanager + +**Key management scripts** handle the full Dilithium key lifecycle: generation, encryption, backup, restore, and import to the validator client. + +See `infrastructure/docs/DEPLOYMENT.md` for the step-by-step deployment guide and `infrastructure/docs/runbooks/` for operational procedures. + +## Monitoring + +Docker Compose stack providing full observability: + +- **Prometheus**: Scrapes metrics from gqrl, qrysm-beacon, qrysm-validator, and the custom contract exporter +- **Grafana**: Three dashboards - Validator Overview, Contract State, System Resources +- **Alertmanager**: Routes alerts by severity (Critical/Warning/Info) to Discord and Telegram +- **Contract Exporter**: Custom Node.js service exposing on-chain metrics (stQRL exchange rate, TVL, deposit queue, validator count) + +See `monitoring/README.md` for setup and configuration. + +## Development + +### Prerequisites + +- Node.js 22 and npm +- `hypc` for Hyperion compilation/deployment + +### Build + +```bash +npm run compile:hyperion +``` + +This compiles the canonical files in `contracts/hyperion/` directly. It does not regenerate them from another language. + +### Local validation + +```bash +npm test +``` + +This compiles the three canonical `.hyp` contracts with the reviewed compiler, checks generated artifacts and critical ABI entries, executes the targeted native semantic suite with default and optimized code generation, parses the broader retained `.t.hyp` specifications, and runs the executable Node tooling tests. + +### Hyperion workflow + +```bash +npm run compile:hyperion +npm run deploy:hyperion +``` + +See `contracts/hyperion/README.md` for the dedicated Hyperion layout and deploy config. + +### Validation gates + +The full local gate is `npm test`. Hosted CI runs `npm run test:tooling` because hosted runners do not currently have a reviewed `hypc` distribution. Historical Solidity and Foundry workflow results are not current Hyperion validation. + +## Validation Evidence + +- **Current executable semantics:** `npm test` runs the targeted Hyperion release-blocker suites with default and optimized code generation. They cover 12 unequal small stakers, atomic real funding rollback, canonical validator identity, bounded reward recognition, pause enforcement, unexpected terminal returns, cross-validator return masking, the outstanding-principal safety lock, reward and slashing algebra, zero-return loss, pre-arrived recovery, and replay guards. +- **Current tooling and source gates:** the same command compiles all canonical contracts with the pinned compiler, verifies the manifest and production ABI, parses the broader retained `.t.hyp` specifications, and runs the Node deployment and lifecycle tooling tests. +- **Historical pre-removal evidence:** the former Solidity and Foundry suite was observed passing 226 tests before the project `.sol` contracts and `.t.sol` tests were removed. That result covered share/QRL conversion math, multi-user rewards, slashing, withdrawals, validator lifecycle, virtual shares, access control, pause behavior, reentrancy protection, and fuzz cases. It is not a current executable result for the `.hyp` sources. +- **Historical broad specifications:** `contracts/test/hyperion/*.t.hyp` preserve pre-v2.4 cases, including retired APIs. The current gate parses those files as archival evidence only. + +## Status + +**v2.3 is deployed and paused on QRL v2 testnet** with the reviewed accounting and launch-safety fixes. The legacy v2.2 pool is also paused while its historical validator stake is migration-bound. The staking frontend serves at [quantapool.com](https://quantapool.com) and [quantapool.io](https://quantapool.io). Keep v2.3 paused until beacon exit and reward settlement can be independently verified. Addresses and operational detail: `docs/V2-DEPLOYMENT-STATUS.md`. + +### Roadmap + +- [x] v2 fixed-balance contracts with audit remediations +- [x] Validator infrastructure (Terraform + Ansible) +- [x] Monitoring and alerting stack +- [x] Key management tooling +- [x] Deploy v2 contracts to QRL v2 testnet (v2.2, two validators funded) +- [x] Staking frontend live at quantapool.com and quantapool.io +- [x] Redeploy paused v2.3 with off-contract stake accounting and security fixes +- [ ] Complete legacy migration and independently verified settlement procedures +- [ ] Implement and activate authenticated terminal withdrawal receipts across Qrysm and go-qrl +- [ ] Integrate staking UI into [qrlwallet.com](https://qrlwallet.com) + +## Security + +- Historical Solidity-era Slither analysis completed with 0 critical/high findings before the `.sol` sources were removed +- Virtual shares (1e3) to prevent first-depositor/inflation attacks +- See `slither-report.txt` for that historical analysis + +## Acknowledgments + +- [Lido](https://lido.fi/) and [Rocket Pool](https://rocketpool.net/) for pioneering liquid staking designs +- [The QRL Core Team](https://www.theqrl.org/) for building post-quantum secure blockchain infrastructure +- [Robyer](https://github.com/robyer) for community feedback on the fixed-balance token model (tax implications of rebasing) + +## License + +GPL-3.0 diff --git a/foundry.toml b/foundry.toml deleted file mode 100644 index 00972ef..0000000 --- a/foundry.toml +++ /dev/null @@ -1,12 +0,0 @@ -[profile.default] -src = "contracts/solidity" -out = "out" -libs = ["lib"] -test = "contracts/test" -solc = "0.8.24" - -# Optimizer settings -optimizer = true -optimizer_runs = 200 - -# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/frontend/.env.example b/frontend/.env.example index 9dc355a..e240703 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,18 +1,12 @@ -# QuantaPool frontend configuration. -# All values have sensible testnet defaults baked in (see src/config/networks.ts), -# so an empty .env works for development. +# Copy to ignored .env.local for the intended fresh native deployment. +# Required: no RPC, chain ID or pool address is provided by default. +VITE_RPC_URL= +VITE_CHAIN_ID= +VITE_NATIVE_POOL_ADDRESS= -# Active network: TEST_NET or MAIN_NET +# Optional display and explorer settings. VITE_NETWORK=TEST_NET - -# RPC endpoint overrides -VITE_RPC_URL_TESTNET=https://qrlwallet.com/api/qrl-rpc/testnet -VITE_RPC_URL_MAINNET= - -# Explorer base URL -VITE_EXPLORER_URL=https://zondscan.com - -# Contract address overrides (defaults match config/testnet-hyperion.json) -VITE_DEPOSIT_POOL_ADDRESS= -VITE_STQRL_ADDRESS= -VITE_VALIDATOR_MANAGER_ADDRESS= +VITE_NETWORK_NAME=Native QRL pool +VITE_NETWORK_LABEL=Development +VITE_EXPLORER_URL= +VITE_DEPLOYMENT_BLOCK=0 diff --git a/frontend/README.md b/frontend/README.md index 6b5622d..9750034 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,64 +1,45 @@ -# QuantaPool Frontend +# QuantaPool native QRL interface -Minimal web app for the QuantaPool liquid staking protocol. Stake QRL, receive -stQRL, track the pool, and manage withdrawals. - -Built to match the [MyQRLWallet](https://qrlwallet.com) design system: -Vite 7, React 19, TypeScript, MobX, TailwindCSS 4, Radix primitives. +React, TypeScript, MobX and Vite interface for the native QRL pool. Deposits, internal positions, rewards, queued requests and cash claims use native QRL. There is no transferable staking receipt or approval flow. ## Development -```bash -cd frontend -npm install -npm run dev # http://127.0.0.1:5173 -npm run build # type-check + production build -npm run lint # ESLint, zero-warnings policy +```sh +npm ci +npm run test +npm run lint +npm run typecheck +npm run build +npm run dev -- --host 127.0.0.1 ``` -Configuration is optional - testnet defaults are baked in. Copy `.env.example` -to `.env` to override the RPC endpoint, explorer, or contract addresses. +Create an ignored `.env.local` with `VITE_RPC_URL`, `VITE_CHAIN_ID` and `VITE_NATIVE_POOL_ADDRESS` for the intended fresh deployment. The pool address must be a native 64-byte QIP-55 address. Optional settings are `VITE_EXPLORER_URL`, `VITE_DEPLOYMENT_BLOCK`, `VITE_NETWORK_NAME`, `VITE_NETWORK_LABEL`, and `VITE_NETWORK=TEST_NET` or `MAIN_NET`. The network selector is descriptive; the explicit chain ID is authoritative. No RPC, explorer or contract address is supplied by default. An unconfigured build displays the development interface and disables transaction entry. -## Architecture +The read RPC must report the configured chain ID. Before each send, the wallet must report that same chain through `qrl_chainId`. The transaction includes the expected chain ID. The app requires deployed pool code and the expected fixed fee policy. Deployment operators must independently verify the configured contract and immutable dependencies; UI checks are not a code audit or consensus proof. -``` -src/ -├── abi/ # Contract ABIs (generated from contracts/solidity) -├── components/ -│ ├── Layout/ # Header (nav + connect), Footer -│ ├── UI/ # Shadcn-style primitives (Button, Card, Input, Tabs…) -│ ├── AmountInput # Amount field with 25/50/75/Max quick buttons -│ ├── StatsBar # Protocol stats row -│ └── TxBanner # Floating transaction status -├── config/networks.ts # RPC endpoints + contract addresses per network -├── pages/ # Stake (home), Withdrawals (request/claim), Stats -├── stores/ # MobX: poolStore drives all chain state + actions -└── utils/ - ├── format.ts # BigInt unit conversion + display formatting - ├── nativeApp.ts # MyQRLWallet app WebView detection - └── web3/ # Lazy @theqrl/web3 loader, EIP-6963 extension connect -``` +Both wallet transports receive explicit gas limits after a successful RPC estimate. Normal accounting calls use the estimate plus 30%, rounded up. Deposits, pending refunds, reserved claims and recovery claims also receive 100,000 gas for next-block cash-flow history growth, with a 300,000-gas minimum. The app rejects a buffered limit above the current block limit and rechecks wallet identity and chain before requesting a send. Estimation failures stop preparation; failed transactions are never automatically retried. -### Wallet connectivity +## Native actions -- **QRL Wallet extension** via EIP-6963 discovery (`theqrl.org` rdns) and the - `qrl_requestAccounts` / `qrl_sendTransaction` provider methods - the same - flow myqrlwallet-frontend uses. -- **MyQRLWallet mobile app**: detected via User-Agent. Designed so the - myqrlwallet-connect SDK can slot in as an additional provider source later. +| Action | Native pool call | +| ---------------------------- | ----------------------------------------------------------- | +| Deposit | `deposit()` with native value | +| Request QRL | `requestWithdrawal(amount)` | +| Request earnings | `requestRewards(amount)` | +| Request full value | Either request with the contract's maximum-uint256 sentinel | +| Claim reserved cash | `claim()` | +| Cancel queued request | `cancelRequest()` | +| Refund unadmitted deposit | `cancelPending(id)` | +| Claim after permanent expiry | `claimRecovery()` | -### Contract flows +Positions show remaining principal basis, current staked value, unreserved earnings, principal shortfall, pending deposits and claimable cash. Earnings remain exposed to losses until reservation. Requests wait for a future authenticated cutoff and strict FIFO liquidity allocation. Protocol eligibility and validator returns determine waiting time. There is no fixed countdown or promised APR. -| Action | Contract call | -|---|---| -| Stake | `DepositPool.deposit()` (payable) | -| Request withdrawal | `DepositPool.requestWithdrawal(shares)` - locks shares, 128-block delay | -| Claim | `DepositPool.claimWithdrawal()` - FIFO, oldest request first | -| Cancel | `DepositPool.cancelWithdrawal(requestId)` | -| Pool data | `getPoolStatus()`, `getRewardStats()`, `ValidatorManager.getStats()` | +The immutable 10% operator fee applies to eligible net consensus gains at payout reservation. Gifts, outside top-ups and principal are excluded, and losses constrain eligibility. Execution-tip routing remains operator-controlled, so receipt of every tip is not guaranteed. Recovery estimates show currently distributable cash; later returns remain claimable under frozen positions. -Regenerate ABIs after contract changes: +Wallet discovery, extension signing and the MyQRLWallet encrypted relay, QR, deep-link, reconnect and disconnect flows are retained. Only native QIP-55 accounts authorize transactions. Optional explorer links are omitted when no explorer is configured. Event history depends on RPC log availability; the owner can supply a pending deposit ID directly for refunds when history cannot be fetched. The latest 64 pending-deposit event IDs are shown. -```bash -node scripts/compile.js # from the repo root, then copy abi arrays into frontend/src/abi -``` +`src/abi/NativeQrlPool.ts` is generated from the pinned compiler's `build/native/NativeQrlPool.abi`. After compiling native contracts from the repository root, run `npm run abi:sync` in this directory and review the generated change. No older contract ABI or deployment address is required. + +Automated utility tests cover exact amount conversion, full-request semantics, loss display, chain ID validation, QIP-55 accounts and relay lifecycle guards. Production readiness and live consensus behavior require the separate contract proof and network tests. This frontend is not publicly deployed by these commands. + +Native event reads use explicit 64-byte `qrl_getLogs` topics and the native ABI decoder. Signature hashes occupy the high 32 bytes with trailing zero padding; indexed beneficiaries retain all 64 address bytes. This bypasses the SDK event wrapper's narrower filter validation without modifying the SDK or node. Returned events are checked against the configured pool and full beneficiary before decoding. diff --git a/frontend/index.html b/frontend/index.html index 138fac1..109d8e2 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -6,10 +6,10 @@ - QuantaPool | Liquid Staking on QRL 2.0 + QuantaPool | Native QRL Pooled Staking on QRL 2.0 @@ -17,25 +17,31 @@ - + - + @@ -61,15 +67,10 @@ "@id": "https://quantapool.com/#webapp", "name": "QuantaPool", "url": "https://quantapool.com/", - "description": "Liquid staking protocol on QRL 2.0, the post-quantum blockchain. Stake QRL, receive stQRL, and earn validator rewards automatically.", + "description": "Native QRL pooled staking protocol on QRL 2.0, the post-quantum blockchain. Deposit native QRL, track your on-chain position, and claim QRL from protocol validation.", "applicationCategory": "FinanceApplication", "operatingSystem": "Web", "image": "https://quantapool.com/og-image.png", - "offers": { - "@type": "Offer", - "price": "0", - "priceCurrency": "USD" - }, "publisher": { "@id": "https://quantapool.com/#organization" } } ] diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 17bf6d9..6da5094 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,11 +11,12 @@ "@fontsource-variable/instrument-sans": "^5.2.8", "@fontsource-variable/jetbrains-mono": "^5.2.8", "@fontsource-variable/sora": "^5.2.8", + "@noble/hashes": "^2.4.0", "@qrlwallet/connect": "4.0.0", "@qrlwallet/connect-ui": "0.2.1", "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-tabs": "^1.1.1", - "@theqrl/web3": "^1.0.1", + "@theqrl/web3": "^1.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.562.0", @@ -664,96 +665,6 @@ "node": ">=20" } }, - "node_modules/@ethersproject/abstract-provider": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.8.0.tgz", - "integrity": "sha512-wC9SFcmh4UK0oKuLJQItoQdzS/qZ51EJegK6EmAWlh+OptpQ/npECOR3QqECd8iGHC0RJb4WKbVdSfif4ammrg==", - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "license": "MIT", - "dependencies": { - "@ethersproject/bignumber": "^5.8.0", - "@ethersproject/bytes": "^5.8.0", - "@ethersproject/logger": "^5.8.0", - "@ethersproject/networks": "^5.8.0", - "@ethersproject/properties": "^5.8.0", - "@ethersproject/transactions": "^5.8.0", - "@ethersproject/web": "^5.8.0" - } - }, - "node_modules/@ethersproject/abstract-signer": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.8.0.tgz", - "integrity": "sha512-N0XhZTswXcmIZQdYtUnd79VJzvEwXQw6PK0dTl9VoYrEBxxCPXqS0Eod7q5TNKRxe1/5WUMuR0u0nqTF/avdCA==", - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "license": "MIT", - "dependencies": { - "@ethersproject/abstract-provider": "^5.8.0", - "@ethersproject/bignumber": "^5.8.0", - "@ethersproject/bytes": "^5.8.0", - "@ethersproject/logger": "^5.8.0", - "@ethersproject/properties": "^5.8.0" - } - }, - "node_modules/@ethersproject/address": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.8.0.tgz", - "integrity": "sha512-GhH/abcC46LJwshoN+uBNoKVFPxUuZm6dA257z0vZkKmU1+t8xTn8oK7B9qrj8W2rFRMch4gbJl6PmVxjxBEBA==", - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "license": "MIT", - "dependencies": { - "@ethersproject/bignumber": "^5.8.0", - "@ethersproject/bytes": "^5.8.0", - "@ethersproject/keccak256": "^5.8.0", - "@ethersproject/logger": "^5.8.0", - "@ethersproject/rlp": "^5.8.0" - } - }, - "node_modules/@ethersproject/base64": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.8.0.tgz", - "integrity": "sha512-lN0oIwfkYj9LbPx4xEkie6rAMJtySbpOAFXSDVQaBnAzYfB4X2Qr+FXJGxMoc3Bxp2Sm8OwvzMrywxyw0gLjIQ==", - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "license": "MIT", - "dependencies": { - "@ethersproject/bytes": "^5.8.0" - } - }, "node_modules/@ethersproject/bignumber": { "version": "5.8.0", "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.8.0.tgz", @@ -813,53 +724,6 @@ "@ethersproject/bignumber": "^5.8.0" } }, - "node_modules/@ethersproject/hash": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.8.0.tgz", - "integrity": "sha512-ac/lBcTbEWW/VGJij0CNSw/wPcw9bSRgCB0AIBz8CvED/jfvDoV9hsIIiWfvWmFEi8RcXtlNwp2jv6ozWOsooA==", - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "license": "MIT", - "dependencies": { - "@ethersproject/abstract-signer": "^5.8.0", - "@ethersproject/address": "^5.8.0", - "@ethersproject/base64": "^5.8.0", - "@ethersproject/bignumber": "^5.8.0", - "@ethersproject/bytes": "^5.8.0", - "@ethersproject/keccak256": "^5.8.0", - "@ethersproject/logger": "^5.8.0", - "@ethersproject/properties": "^5.8.0", - "@ethersproject/strings": "^5.8.0" - } - }, - "node_modules/@ethersproject/keccak256": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.8.0.tgz", - "integrity": "sha512-A1pkKLZSz8pDaQ1ftutZoaN46I6+jvuqugx5KYNeQOPqq+JZ0Txm7dlWesCHB5cndJSu5vP2VKptKf7cksERng==", - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "license": "MIT", - "dependencies": { - "@ethersproject/bytes": "^5.8.0", - "js-sha3": "0.8.0" - } - }, "node_modules/@ethersproject/logger": { "version": "5.8.0", "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.8.0.tgz", @@ -876,25 +740,6 @@ ], "license": "MIT" }, - "node_modules/@ethersproject/networks": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.8.0.tgz", - "integrity": "sha512-egPJh3aPVAzbHwq8DD7Po53J4OUSsA1MjQp8Vf/OZPav5rlmWUaFLiq8cvQiGK0Z5K6LYzm29+VA/p4RL1FzNg==", - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "license": "MIT", - "dependencies": { - "@ethersproject/logger": "^5.8.0" - } - }, "node_modules/@ethersproject/properties": { "version": "5.8.0", "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.8.0.tgz", @@ -914,50 +759,6 @@ "@ethersproject/logger": "^5.8.0" } }, - "node_modules/@ethersproject/rlp": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.8.0.tgz", - "integrity": "sha512-LqZgAznqDbiEunaUvykH2JAoXTT9NV0Atqk8rQN9nx9SEgThA/WMx5DnW8a9FOufo//6FZOCHZ+XiClzgbqV9Q==", - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "license": "MIT", - "dependencies": { - "@ethersproject/bytes": "^5.8.0", - "@ethersproject/logger": "^5.8.0" - } - }, - "node_modules/@ethersproject/signing-key": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.8.0.tgz", - "integrity": "sha512-LrPW2ZxoigFi6U6aVkFN/fa9Yx/+4AtIUe4/HACTvKJdhm0eeb107EVCIQcrLZkxaSIgc/eCrX8Q1GtbH+9n3w==", - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "license": "MIT", - "dependencies": { - "@ethersproject/bytes": "^5.8.0", - "@ethersproject/logger": "^5.8.0", - "@ethersproject/properties": "^5.8.0", - "bn.js": "^5.2.1", - "elliptic": "6.6.1", - "hash.js": "1.1.7" - } - }, "node_modules/@ethersproject/strings": { "version": "5.8.0", "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.8.0.tgz", @@ -979,56 +780,6 @@ "@ethersproject/logger": "^5.8.0" } }, - "node_modules/@ethersproject/transactions": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.8.0.tgz", - "integrity": "sha512-UglxSDjByHG0TuU17bDfCemZ3AnKO2vYrL5/2n2oXvKzvb7Cz+W9gOWXKARjp2URVwcWlQlPOEQyAviKwT4AHg==", - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "license": "MIT", - "dependencies": { - "@ethersproject/address": "^5.8.0", - "@ethersproject/bignumber": "^5.8.0", - "@ethersproject/bytes": "^5.8.0", - "@ethersproject/constants": "^5.8.0", - "@ethersproject/keccak256": "^5.8.0", - "@ethersproject/logger": "^5.8.0", - "@ethersproject/properties": "^5.8.0", - "@ethersproject/rlp": "^5.8.0", - "@ethersproject/signing-key": "^5.8.0" - } - }, - "node_modules/@ethersproject/web": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.8.0.tgz", - "integrity": "sha512-j7+Ksi/9KfGviws6Qtf9Q7KCqRhpwrYKQPs+JBA/rKVFF/yaWLHJEH3zfVP2plVu+eys0d2DlFmhoQJayFewcw==", - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "license": "MIT", - "dependencies": { - "@ethersproject/base64": "^5.8.0", - "@ethersproject/bytes": "^5.8.0", - "@ethersproject/logger": "^5.8.0", - "@ethersproject/properties": "^5.8.0", - "@ethersproject/strings": "^5.8.0" - } - }, "node_modules/@fontsource-variable/instrument-sans": { "version": "5.2.8", "resolved": "https://registry.npmjs.org/@fontsource-variable/instrument-sans/-/instrument-sans-5.2.8.tgz", @@ -1187,7 +938,7 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@noble/hashes": { + "node_modules/@noble/curves/node_modules/@noble/hashes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", @@ -1199,6 +950,18 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@noble/hashes": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz", + "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@noble/post-quantum": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/@noble/post-quantum/-/post-quantum-0.5.4.tgz", @@ -1215,6 +978,18 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@noble/post-quantum/node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@qrlwallet/connect": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@qrlwallet/connect/-/connect-4.0.0.tgz", @@ -1248,30 +1023,6 @@ } } }, - "node_modules/@qrlwallet/connect/node_modules/@noble/hashes": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", - "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@qrlwallet/connect/node_modules/@theqrl/mldsa87": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@theqrl/mldsa87/-/mldsa87-2.1.3.tgz", - "integrity": "sha512-sQxeMZTeJG84v8lK8gRztuHHC2hB6RG2yWQc7oHv19lExG6vB4qvn8nk8/0ffseYXMqfqU5dmZ4jdabk9P3TEg==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "2.2.0" - }, - "engines": { - "node": ">=20.19.0" - } - }, "node_modules/@radix-ui/primitive": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.4.tgz", @@ -2523,46 +2274,55 @@ } }, "node_modules/@theqrl/abi": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@theqrl/abi/-/abi-1.0.1.tgz", - "integrity": "sha512-QvM+8QCSw8OSo/WpevJlkwytEdnDK99QpCm7cmyVMDjLgNnMpO7id9Tgj901WELdzGq/rWLEJCDevUGJF3m+YQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@theqrl/abi/-/abi-1.0.3.tgz", + "integrity": "sha512-Pd+1jtkSXVETIow3ZjKJhcMJswOfs0dyhJyys45PXNaVx0K3GzY27cVwuBe9EAD5di5eQ9uaVJZjbiZIRkg4IA==", "license": "MIT", "dependencies": { - "@ethersproject/address": "5.8.0", "@ethersproject/bignumber": "5.8.0", "@ethersproject/bytes": "5.8.0", "@ethersproject/constants": "5.8.0", - "@ethersproject/hash": "5.8.0", - "@ethersproject/keccak256": "5.8.0", "@ethersproject/logger": "5.8.0", "@ethersproject/properties": "5.8.0", "@ethersproject/strings": "5.8.0", - "@theqrl/web3-utils": "1.0.1" + "@theqrl/web3-utils": "1.0.3" }, "engines": { - "node": ">=20" + "node": ">=20.19.0" } }, "node_modules/@theqrl/mldsa87": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@theqrl/mldsa87/-/mldsa87-2.0.1.tgz", - "integrity": "sha512-BPEvwrrphkMjyPnDorMzQdfrtICTZsiUxiBnwFkv2SfIcQwTTPQV3kTVfidZXXE7033ZCvhu1u5Aiw6TrjxPPQ==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@theqrl/mldsa87/-/mldsa87-2.1.3.tgz", + "integrity": "sha512-sQxeMZTeJG84v8lK8gRztuHHC2hB6RG2yWQc7oHv19lExG6vB4qvn8nk8/0ffseYXMqfqU5dmZ4jdabk9P3TEg==", "license": "MIT", "dependencies": { - "@noble/hashes": "2.0.1" + "@noble/hashes": "2.2.0" }, "engines": { "node": ">=20.19.0" } }, + "node_modules/@theqrl/mldsa87/node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@theqrl/qrl-cryptography": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@theqrl/qrl-cryptography/-/qrl-cryptography-0.1.3.tgz", - "integrity": "sha512-rtgii8JB6W0Gk7jen2WZU6Iubl+mHKFUobGG6zzipippVxhbeA0Vb6SJ/Hi5IEbGr44KlY4sBZMjWEBLaSEQpw==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@theqrl/qrl-cryptography/-/qrl-cryptography-0.3.0.tgz", + "integrity": "sha512-2d7QcK/ZQKOmphnj0lSRJLyeXO3o0degg0mjEMHH+5g4LXvrgoVV1i/bTqzS78DgGRB4Mn3kUPXg6QW3KO712w==", "license": "MIT", "dependencies": { - "@noble/hashes": "^2.2.0", - "@theqrl/mldsa87": "2.0.4" + "@noble/hashes": "2.2.0", + "@theqrl/mldsa87": "2.1.3" }, "engines": { "node": ">=20.19" @@ -2580,22 +2340,23 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@theqrl/qrl-cryptography/node_modules/@theqrl/mldsa87": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@theqrl/mldsa87/-/mldsa87-2.0.4.tgz", - "integrity": "sha512-xUcrFSZ1OeRSyzU5xCd/TuB+dE/z7U9MafNtKZYmPhRwcucNbhobsXHab8GO5w02dy732sdPidOT2Tkn+vjZuA==", + "node_modules/@theqrl/wallet.js": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/@theqrl/wallet.js/-/wallet.js-6.2.3.tgz", + "integrity": "sha512-1wQcPKDqtLpM5sn6NKEbn4UIJwZ9BWVqG1TbrW1zBRuw8NUaiejjDZQcKwoHrnNV3TQe9dJ1jnu+PLxq9LOqyw==", "license": "MIT", "dependencies": { - "@noble/hashes": "2.0.1" + "@noble/hashes": "2.2.0", + "@theqrl/mldsa87": "2.1.3" }, "engines": { - "node": ">=20.19.0" + "node": ">=20.19" } }, - "node_modules/@theqrl/qrl-cryptography/node_modules/@theqrl/mldsa87/node_modules/@noble/hashes": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", - "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "node_modules/@theqrl/wallet.js/node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", "license": "MIT", "engines": { "node": ">= 20.19.0" @@ -2604,318 +2365,294 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@theqrl/wallet.js": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@theqrl/wallet.js/-/wallet.js-2.0.2.tgz", - "integrity": "sha512-gqmPE1D7qk2d6Qir0/iZeRGGr2eAVjps9Zql0P4pIw/uBjs81FBYio90c+RawkuKyc2WjlOLF6l3VI02z/rMSg==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "2.0.1", - "@theqrl/mldsa87": "2.0.1" - } - }, "node_modules/@theqrl/web3": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@theqrl/web3/-/web3-1.0.1.tgz", - "integrity": "sha512-ykqJFoYhl83RsiD4QdMUsjHGMTi1DlJm70MjSJamtisfuE1T6WDgDoXohPHR8NL3sStn1os/4+cUAPiOu7sWRw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@theqrl/web3/-/web3-1.0.3.tgz", + "integrity": "sha512-AO/BeNtU/l7//87xAFYtrM/GFdV99DwVqGSqTzw38+WQpplBr3dURmd/ixXVHMsYEuEy+ewCR1h/IqypKeR6Lg==", "license": "LGPL-3.0", "dependencies": { - "@theqrl/web3-core": "1.0.1", - "@theqrl/web3-errors": "1.0.1", - "@theqrl/web3-net": "1.0.1", - "@theqrl/web3-providers-http": "1.0.1", - "@theqrl/web3-providers-ws": "1.0.1", - "@theqrl/web3-qrl": "1.0.1", - "@theqrl/web3-qrl-abi": "1.0.1", - "@theqrl/web3-qrl-accounts": "1.0.1", - "@theqrl/web3-qrl-contract": "1.0.1", - "@theqrl/web3-qrl-iban": "1.0.1", - "@theqrl/web3-qrl-qrns": "1.0.1", - "@theqrl/web3-rpc-methods": "1.0.1", - "@theqrl/web3-types": "1.0.1", - "@theqrl/web3-utils": "1.0.1", - "@theqrl/web3-validator": "1.0.1" + "@theqrl/web3-core": "1.0.3", + "@theqrl/web3-errors": "1.0.2", + "@theqrl/web3-net": "1.0.3", + "@theqrl/web3-providers-http": "1.0.3", + "@theqrl/web3-providers-ws": "1.0.3", + "@theqrl/web3-qrl": "1.0.3", + "@theqrl/web3-qrl-abi": "1.0.3", + "@theqrl/web3-qrl-accounts": "1.0.3", + "@theqrl/web3-qrl-contract": "1.0.3", + "@theqrl/web3-qrl-iban": "1.0.3", + "@theqrl/web3-qrl-qrns": "1.0.3", + "@theqrl/web3-rpc-methods": "1.0.3", + "@theqrl/web3-types": "1.0.2", + "@theqrl/web3-utils": "1.0.3", + "@theqrl/web3-validator": "1.0.3" }, "engines": { - "node": ">=20" + "node": ">=20.19.0" } }, "node_modules/@theqrl/web3-core": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@theqrl/web3-core/-/web3-core-1.0.1.tgz", - "integrity": "sha512-oiT+RlEIMTDSYSpPp0i80T55vmuoBXbkGaVao338+6jlemO3zW0NYmnkqrNqxkMk1HO4cePnPCUxFvMj9h9QMg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@theqrl/web3-core/-/web3-core-1.0.3.tgz", + "integrity": "sha512-s8PUFBBC/f/HxYUxKz6Xju38tfUZ3GBlzwPS5bdyxipwEzhgfh8BZgrROZ2ZrRL1HowZI7OblY7oN9FQw6EWLQ==", "license": "LGPL-3.0", "dependencies": { - "@theqrl/web3-errors": "1.0.1", - "@theqrl/web3-providers-http": "1.0.1", - "@theqrl/web3-providers-ws": "1.0.1", - "@theqrl/web3-qrl-iban": "1.0.1", - "@theqrl/web3-types": "1.0.1", - "@theqrl/web3-utils": "1.0.1", - "@theqrl/web3-validator": "1.0.1" + "@theqrl/web3-errors": "1.0.2", + "@theqrl/web3-providers-http": "1.0.3", + "@theqrl/web3-providers-ws": "1.0.3", + "@theqrl/web3-qrl-iban": "1.0.3", + "@theqrl/web3-types": "1.0.2", + "@theqrl/web3-utils": "1.0.3", + "@theqrl/web3-validator": "1.0.3" }, "engines": { - "node": ">=20" + "node": ">=20.19.0" }, "optionalDependencies": { - "@theqrl/web3-providers-ipc": "1.0.1" + "@theqrl/web3-providers-ipc": "1.0.3" } }, "node_modules/@theqrl/web3-errors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@theqrl/web3-errors/-/web3-errors-1.0.1.tgz", - "integrity": "sha512-aAirwyS84SA8e4LYkpJ4BF1mut4jSrIt1HksveTmKHZgojJr4y/D/o4bP4w+MgBMymrmdXRzZUJHhk1N3pRktQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@theqrl/web3-errors/-/web3-errors-1.0.2.tgz", + "integrity": "sha512-UwUPXAbA5+Z+OtKEknZJnCCssnohriycMuVaWnboBRQPLv4hHrFWnubOLJi6D3+xGlCk3TCbMlm2SiuZumbq8A==", "license": "LGPL-3.0", "dependencies": { - "@theqrl/web3-types": "1.0.1" + "@theqrl/web3-types": "1.0.2" }, "engines": { - "node": ">=20" + "node": ">=20.19.0" } }, "node_modules/@theqrl/web3-net": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@theqrl/web3-net/-/web3-net-1.0.1.tgz", - "integrity": "sha512-F+DDxtqRf1DnD8B6rLRn/L0NJslaF71CcexWBOmeDVlhiUJ2bL2Jd497QZGcya96atobqa/RUGcBxIrjerA29Q==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@theqrl/web3-net/-/web3-net-1.0.3.tgz", + "integrity": "sha512-KRn3mBoRy45Kl9vgzTyeg0TWOxuiGmW2KQE5dI3fCyYfrnQroBNXiw7wlRwZM3DNofZFCzdt1CIbkOlE/iOnRA==", "license": "LGPL-3.0", "dependencies": { - "@theqrl/web3-core": "1.0.1", - "@theqrl/web3-rpc-methods": "1.0.1", - "@theqrl/web3-types": "1.0.1", - "@theqrl/web3-utils": "1.0.1" + "@theqrl/web3-core": "1.0.3", + "@theqrl/web3-rpc-methods": "1.0.3", + "@theqrl/web3-types": "1.0.2", + "@theqrl/web3-utils": "1.0.3" }, "engines": { - "node": ">=20" + "node": ">=20.19.0" } }, "node_modules/@theqrl/web3-providers-http": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@theqrl/web3-providers-http/-/web3-providers-http-1.0.1.tgz", - "integrity": "sha512-XhhvPROFEoeVSOyXXxM+kP0fe/M31LpOOwtbeKcc+1VfEdfy1CqFrdAK0VkFT9aXu58Kc96cFJPA1FcQyID1ew==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@theqrl/web3-providers-http/-/web3-providers-http-1.0.3.tgz", + "integrity": "sha512-dFDqpOFft8vhxLZ9omZ8qDiLqkHcaKo0HvI8IX8lhFxuXsxRa28NracYn2MjxTR3p45oqrc9VbK3BJ2aHG5PxA==", "license": "LGPL-3.0", "dependencies": { - "@theqrl/web3-errors": "1.0.1", - "@theqrl/web3-types": "1.0.1", - "@theqrl/web3-utils": "1.0.1", + "@theqrl/web3-errors": "1.0.2", + "@theqrl/web3-types": "1.0.2", + "@theqrl/web3-utils": "1.0.3", "cross-fetch": "4.1.0" }, "engines": { - "node": ">=20" + "node": ">=20.19.0" } }, "node_modules/@theqrl/web3-providers-ipc": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@theqrl/web3-providers-ipc/-/web3-providers-ipc-1.0.1.tgz", - "integrity": "sha512-rbsWTbM+nWXljibMFESXYqZ3v6vk8QL6KYjMb75WGPShjur8s3fhkTDbttJ1GaFJNzguJ7pmCNitnJPBQFTFig==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@theqrl/web3-providers-ipc/-/web3-providers-ipc-1.0.3.tgz", + "integrity": "sha512-ALqjX/8iB9SgAnCihZvOCjIS4efyEpthZ/72ZjOza4TX8HNx8n88vVXO9fBF0JOZSJDxq8EFcPyVqoa8HqKJmg==", "license": "LGPL-3.0", "optional": true, "dependencies": { - "@theqrl/web3-errors": "1.0.1", - "@theqrl/web3-types": "1.0.1", - "@theqrl/web3-utils": "1.0.1" + "@theqrl/web3-errors": "1.0.2", + "@theqrl/web3-types": "1.0.2", + "@theqrl/web3-utils": "1.0.3" }, "engines": { - "node": ">=20" + "node": ">=20.19.0" } }, "node_modules/@theqrl/web3-providers-ws": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@theqrl/web3-providers-ws/-/web3-providers-ws-1.0.1.tgz", - "integrity": "sha512-2k+LYAmK6F93Ydh4EBaQ+LGsyQKiCqgiMT6WimV/j0GYxaXfZQoBabvg8dkVtMPLZOXEcUkqPGSs+6DfHNvPdQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@theqrl/web3-providers-ws/-/web3-providers-ws-1.0.3.tgz", + "integrity": "sha512-kFYTTbpuRZpRw+i/+EKHXHZgvj3xC64tQZjNeU+1+S3qW9rZNy5i8gJxZAPzOkRwMYncbS3wv5KRxkbDh/KEGw==", "license": "LGPL-3.0", "dependencies": { - "@theqrl/web3-errors": "1.0.1", - "@theqrl/web3-types": "1.0.1", - "@theqrl/web3-utils": "1.0.1", + "@theqrl/web3-errors": "1.0.2", + "@theqrl/web3-types": "1.0.2", + "@theqrl/web3-utils": "1.0.3", "@types/ws": "8.18.1", "isomorphic-ws": "5.0.0", "ws": "8.21.0" }, "engines": { - "node": ">=20" + "node": ">=20.19.0" } }, "node_modules/@theqrl/web3-qrl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@theqrl/web3-qrl/-/web3-qrl-1.0.1.tgz", - "integrity": "sha512-9pEYoxJ1hs76D0rWrFdCKNV+teu6VtKLo3ELt0u/5E3T7p+i6PmFy1SAwTftlT6RePmnj01ohI5w8jF/JIAZrg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@theqrl/web3-qrl/-/web3-qrl-1.0.3.tgz", + "integrity": "sha512-t1wG9jfrzKGIlPS7YDNiR/HzR52R5tpf/Q/+BEaO19rZFS0dD371Q2zmL43W/4qsfxiIXif9A6oD+nDqh54+oA==", "license": "LGPL-3.0", "dependencies": { - "@theqrl/wallet.js": "2.0.2", - "@theqrl/web3-core": "1.0.1", - "@theqrl/web3-errors": "1.0.1", - "@theqrl/web3-net": "1.0.1", - "@theqrl/web3-providers-ws": "1.0.1", - "@theqrl/web3-qrl-abi": "1.0.1", - "@theqrl/web3-qrl-accounts": "1.0.1", - "@theqrl/web3-rpc-methods": "1.0.1", - "@theqrl/web3-types": "1.0.1", - "@theqrl/web3-utils": "1.0.1", - "@theqrl/web3-validator": "1.0.1", + "@theqrl/wallet.js": "6.2.3", + "@theqrl/web3-core": "1.0.3", + "@theqrl/web3-errors": "1.0.2", + "@theqrl/web3-net": "1.0.3", + "@theqrl/web3-providers-ws": "1.0.3", + "@theqrl/web3-qrl-abi": "1.0.3", + "@theqrl/web3-qrl-accounts": "1.0.3", + "@theqrl/web3-rpc-methods": "1.0.3", + "@theqrl/web3-types": "1.0.2", + "@theqrl/web3-utils": "1.0.3", + "@theqrl/web3-validator": "1.0.3", "setimmediate": "1.0.5" }, "engines": { - "node": ">=20" + "node": ">=20.19.0" } }, "node_modules/@theqrl/web3-qrl-abi": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@theqrl/web3-qrl-abi/-/web3-qrl-abi-1.0.1.tgz", - "integrity": "sha512-i+vZ9Rtoz0lfDa/fJm/XxkLr76WyXJWkNx7VoVrZYDzzoSnCuouKOVVSVRSy/5bm1a6XHtMelfv/v3CYQX39jQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@theqrl/web3-qrl-abi/-/web3-qrl-abi-1.0.3.tgz", + "integrity": "sha512-thtgZLMVnfoukBfp8/n8QP/DgIZJhfR4G3w0Wm2KHVP+l2zjylkjnup0ZKxjDcDKTHPr+8q+B+50OttHAt1v2w==", "license": "LGPL-3.0", "dependencies": { "@ethersproject/bignumber": "5.8.0", - "@theqrl/abi": "1.0.1", - "@theqrl/wallet.js": "2.0.2", - "@theqrl/web3-errors": "1.0.1", - "@theqrl/web3-types": "1.0.1", - "@theqrl/web3-utils": "1.0.1", - "@theqrl/web3-validator": "1.0.1", - "crc-32": "1.2.2", - "sha3": "2.1.4" + "@theqrl/abi": "1.0.3", + "@theqrl/wallet.js": "6.2.3", + "@theqrl/web3-errors": "1.0.2", + "@theqrl/web3-types": "1.0.2", + "@theqrl/web3-utils": "1.0.3", + "@theqrl/web3-validator": "1.0.3", + "crc-32": "1.2.2" }, "engines": { - "node": ">=20" + "node": ">=20.19.0" } }, "node_modules/@theqrl/web3-qrl-accounts": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@theqrl/web3-qrl-accounts/-/web3-qrl-accounts-1.0.1.tgz", - "integrity": "sha512-A+07dRhMynD0xCbJI5/M/TAxbhgpaJV9m2VjtZXqq4SG78TOICqveJ7cSaz7rkS1aQFTtKKaVqkR0EES34jkxA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@theqrl/web3-qrl-accounts/-/web3-qrl-accounts-1.0.3.tgz", + "integrity": "sha512-LhnI2GKczPPImsdKDv8Cx0sQAMnBK89FrA9oIe2XB+rVizlzG5TRwNZbN+QuoKhhpkWzk3bKpb2UUXM/1Zm0rA==", "license": "LGPL-3.0", "dependencies": { "@ethereumjs/rlp": "10.1.1", - "@theqrl/mldsa87": "2.0.4", - "@theqrl/qrl-cryptography": "0.1.3", - "@theqrl/wallet.js": "2.0.2", - "@theqrl/web3-errors": "1.0.1", - "@theqrl/web3-types": "1.0.1", - "@theqrl/web3-utils": "1.0.1", - "@theqrl/web3-validator": "1.0.1", - "crc-32": "1.2.2", - "sha3": "2.1.4" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@theqrl/web3-qrl-accounts/node_modules/@theqrl/mldsa87": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@theqrl/mldsa87/-/mldsa87-2.0.4.tgz", - "integrity": "sha512-xUcrFSZ1OeRSyzU5xCd/TuB+dE/z7U9MafNtKZYmPhRwcucNbhobsXHab8GO5w02dy732sdPidOT2Tkn+vjZuA==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "2.0.1" + "@theqrl/mldsa87": "2.1.3", + "@theqrl/qrl-cryptography": "0.3.0", + "@theqrl/wallet.js": "6.2.3", + "@theqrl/web3-errors": "1.0.2", + "@theqrl/web3-types": "1.0.2", + "@theqrl/web3-utils": "1.0.3", + "@theqrl/web3-validator": "1.0.3", + "crc-32": "1.2.2" }, "engines": { "node": ">=20.19.0" } }, "node_modules/@theqrl/web3-qrl-contract": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@theqrl/web3-qrl-contract/-/web3-qrl-contract-1.0.1.tgz", - "integrity": "sha512-9Pv/5l2IsXg9BgUJgHeMQ5m9WgxRB/zsqHBwtBHD7aiK3f/bEQH7wKq2lkFLg9c8GbzulmUIbMDAjEy0waUVOw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@theqrl/web3-qrl-contract/-/web3-qrl-contract-1.0.3.tgz", + "integrity": "sha512-iLH1PbWQpPuMaDKVYhc0Bmu47C+T2TFkolsuTaIhwNy98a0gMq/qUfeIZpMADVH/O+IkYxTpZ349u0KiMiqjMA==", "license": "LGPL-3.0", "dependencies": { - "@theqrl/web3-core": "1.0.1", - "@theqrl/web3-errors": "1.0.1", - "@theqrl/web3-qrl": "1.0.1", - "@theqrl/web3-qrl-abi": "1.0.1", - "@theqrl/web3-types": "1.0.1", - "@theqrl/web3-utils": "1.0.1", - "@theqrl/web3-validator": "1.0.1" + "@theqrl/web3-core": "1.0.3", + "@theqrl/web3-errors": "1.0.2", + "@theqrl/web3-qrl": "1.0.3", + "@theqrl/web3-qrl-abi": "1.0.3", + "@theqrl/web3-types": "1.0.2", + "@theqrl/web3-utils": "1.0.3", + "@theqrl/web3-validator": "1.0.3" }, "engines": { - "node": ">=20" + "node": ">=20.19.0" } }, "node_modules/@theqrl/web3-qrl-iban": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@theqrl/web3-qrl-iban/-/web3-qrl-iban-1.0.1.tgz", - "integrity": "sha512-Ag+I8OfL6m8mDMjd3/TH0jdi0e0/77n4ecDCRp1yeQw6d1EbIgL3ACkLdOO+xSgu6Ho1c6ebP54vMHvjxSAiSw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@theqrl/web3-qrl-iban/-/web3-qrl-iban-1.0.3.tgz", + "integrity": "sha512-WstKSRoGHnoSifHxmQQXPAmTqN94mWSdEX9VRPtNyKOMRiMAGGn+vYzvCnyKzCdGUYQLJilj2VxlU1J5F/Qdpw==", "license": "LGPL-3.0", "dependencies": { - "@theqrl/web3-errors": "1.0.1", - "@theqrl/web3-types": "1.0.1", - "@theqrl/web3-utils": "1.0.1", - "@theqrl/web3-validator": "1.0.1" + "@theqrl/web3-errors": "1.0.2", + "@theqrl/web3-types": "1.0.2", + "@theqrl/web3-utils": "1.0.3", + "@theqrl/web3-validator": "1.0.3" }, "engines": { - "node": ">=20" + "node": ">=20.19.0" } }, "node_modules/@theqrl/web3-qrl-qrns": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@theqrl/web3-qrl-qrns/-/web3-qrl-qrns-1.0.1.tgz", - "integrity": "sha512-QOMYaLHzQyyjKhiQ2vjDP/CwSBghG2GAzbMKLwLj1/bRLepzvXVYB/SMD8N3TZxZhzdni/DtiQg1OrbkdAfjcA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@theqrl/web3-qrl-qrns/-/web3-qrl-qrns-1.0.3.tgz", + "integrity": "sha512-ROe9CzPKxgywfqYbv7BdMvtHCUyqJyx/+BEQBkTNFw5M8GAt/mxRro5DoGP7jgngevQ7pw47RQUEKbGKscUyfQ==", "license": "LGPL-3.0", "dependencies": { "@adraffy/ens-normalize": "1.11.1", - "@theqrl/web3-core": "1.0.1", - "@theqrl/web3-errors": "1.0.1", - "@theqrl/web3-net": "1.0.1", - "@theqrl/web3-qrl": "1.0.1", - "@theqrl/web3-qrl-contract": "1.0.1", - "@theqrl/web3-types": "1.0.1", - "@theqrl/web3-utils": "1.0.1", - "@theqrl/web3-validator": "1.0.1" + "@theqrl/web3-core": "1.0.3", + "@theqrl/web3-errors": "1.0.2", + "@theqrl/web3-net": "1.0.3", + "@theqrl/web3-qrl": "1.0.3", + "@theqrl/web3-qrl-contract": "1.0.3", + "@theqrl/web3-types": "1.0.2", + "@theqrl/web3-utils": "1.0.3", + "@theqrl/web3-validator": "1.0.3" }, "engines": { - "node": ">=20" + "node": ">=20.19.0" } }, "node_modules/@theqrl/web3-rpc-methods": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@theqrl/web3-rpc-methods/-/web3-rpc-methods-1.0.1.tgz", - "integrity": "sha512-elgQ7tg4vqRf0je9hQ5mM4N7VdX6jUUOHnYv2NGovDPMMuVJisNbl8itDqRk50/7cUNRrp9lNEPiSbdqHLIxSw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@theqrl/web3-rpc-methods/-/web3-rpc-methods-1.0.3.tgz", + "integrity": "sha512-1vmo0Vko7oh5V52kc9tUKOxxmvFFdavoEcdEBrQyLQAMzLRNPGHHRjO9d7kMGOgJPiYzGduGYae29SKn2JIRpg==", "license": "LGPL-3.0", "dependencies": { - "@theqrl/web3-core": "1.0.1", - "@theqrl/web3-types": "1.0.1", - "@theqrl/web3-validator": "1.0.1" + "@theqrl/web3-core": "1.0.3", + "@theqrl/web3-types": "1.0.2", + "@theqrl/web3-validator": "1.0.3" }, "engines": { - "node": ">=20" + "node": ">=20.19.0" } }, "node_modules/@theqrl/web3-types": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@theqrl/web3-types/-/web3-types-1.0.1.tgz", - "integrity": "sha512-uc+HBs7nMQjM7eD+39Gz/lDQ/1+o98y5xAaPApSvDYfcpDobMW+uV+LrMgBgak0y//OO0w0gtzzd7ohEk7OJmA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@theqrl/web3-types/-/web3-types-1.0.2.tgz", + "integrity": "sha512-Coz6+BuNPbgR34hbVwxnLEETvD3PSYCRQKQ4kolIPDczk8mDEYIwa7TyCegRcklbrH9Hc765l4BMsDZI0QsmDQ==", "license": "LGPL-3.0", "engines": { - "node": ">=20" + "node": ">=20.19.0" } }, "node_modules/@theqrl/web3-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@theqrl/web3-utils/-/web3-utils-1.0.1.tgz", - "integrity": "sha512-X5H3RIWu3fUXkBz3B6T5pJlGnyUbh414I1HpQ9zUKj/ZZ8Z+EMgO/UMHWSblDRgeucI8DFFjIFGVSuD2ChB5mA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@theqrl/web3-utils/-/web3-utils-1.0.3.tgz", + "integrity": "sha512-ChCz/HUqUIP7XFivTIYpzZLqEow0FcIOGvZ/ayiyXLjneOdXhd2ylyqUy8eb/494FYDj7KujfPUPe3OUgt4gFQ==", "license": "LGPL-3.0", "dependencies": { - "@theqrl/qrl-cryptography": "0.1.3", - "@theqrl/web3-errors": "1.0.1", - "@theqrl/web3-types": "1.0.1", - "@theqrl/web3-validator": "1.0.1" + "@theqrl/qrl-cryptography": "0.3.0", + "@theqrl/web3-errors": "1.0.2", + "@theqrl/web3-types": "1.0.2", + "@theqrl/web3-validator": "1.0.3" }, "engines": { - "node": ">=20" + "node": ">=20.19.0" } }, "node_modules/@theqrl/web3-validator": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@theqrl/web3-validator/-/web3-validator-1.0.1.tgz", - "integrity": "sha512-uwzArtbrk9/JByyPph/ZPeuYs8f5jp8TtXU8BN5yTMOO+WpGDYpqMiaUshLzQdslpUC5vlQqlTW0+3oac1DYxQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@theqrl/web3-validator/-/web3-validator-1.0.3.tgz", + "integrity": "sha512-O5VaGMf9z/Zy4kH9nqnwVcGoVnXiIXW81YzG9RUY0S9IURtp9+bdCiPiOo8KVJHUXpgubIvEWXeLLAqO6+kvjQ==", "license": "LGPL-3.0", "dependencies": { - "@theqrl/qrl-cryptography": "0.1.3", - "@theqrl/web3-errors": "1.0.1", - "@theqrl/web3-types": "1.0.1", + "@theqrl/qrl-cryptography": "0.3.0", + "@theqrl/web3-errors": "1.0.2", + "@theqrl/web3-types": "1.0.2", "util": "0.12.5", "zod": "3.22.3" }, "engines": { - "node": ">=20" + "node": ">=20.19.0" } }, "node_modules/@types/estree": { @@ -3362,30 +3099,10 @@ "dev": true, "license": "MIT" }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/bn.js": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", - "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.5.tgz", + "integrity": "sha512-Vq886eXykuP5E6HcKSSStP3bJgrE6In5WKxVUvJ8XGpWWYs2xZHWqUwzCtGgEtBcxyd57KBFDPFoUfNzdaHCNg==", "license": "MIT" }, "node_modules/brace-expansion": { @@ -3399,36 +3116,6 @@ "concat-map": "0.0.1" } }, - "node_modules/brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", - "license": "MIT" - }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -3698,27 +3385,6 @@ "node": ">= 0.4" } }, - "node_modules/elliptic": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", - "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", - "license": "MIT", - "dependencies": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "node_modules/elliptic/node_modules/bn.js": { - "version": "4.12.3", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", - "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", - "license": "MIT" - }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -4314,16 +3980,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, "node_modules/hasown": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", @@ -4336,37 +3992,6 @@ "node": ">= 0.4" } }, - "node_modules/hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", - "license": "MIT", - "dependencies": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4548,16 +4173,10 @@ "jiti": "lib/jiti-cli.mjs" } }, - "node_modules/js-sha3": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", - "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", - "license": "MIT" - }, "node_modules/js-yaml": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", - "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, "funding": [ { @@ -4934,18 +4553,6 @@ "node": ">= 0.4" } }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "license": "ISC" - }, - "node_modules/minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", - "license": "MIT" - }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -5001,9 +4608,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { @@ -5461,15 +5068,6 @@ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "license": "MIT" }, - "node_modules/sha3": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/sha3/-/sha3-2.1.4.tgz", - "integrity": "sha512-S8cNxbyb0UGUM2VhRD4Poe5N58gJnJsLJ5vC7FYWGUmGhcsj4++WaIOBFVDxlG0W3To6xBuiRh+i0Qp2oNCOtg==", - "license": "MIT", - "dependencies": { - "buffer": "6.0.3" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5509,9 +5107,9 @@ } }, "node_modules/socket.io-parser": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", - "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.7.tgz", + "integrity": "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==", "license": "MIT", "dependencies": { "@socket.io/component-emitter": "~3.1.0", diff --git a/frontend/package.json b/frontend/package.json index b4fac2b..76c72f8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,24 +3,27 @@ "private": true, "version": "0.1.0", "type": "module", - "description": "QuantaPool - post-quantum liquid staking for QRL", + "description": "QuantaPool native QRL pooled staking interface", "scripts": { "dev": "vite", "dev:lan": "vite --host 0.0.0.0", "test": "node --test --experimental-strip-types src/utils/*.test.ts", "build": "tsc -b && vite build", "lint": "eslint . --max-warnings 0", - "preview": "vite preview" + "preview": "vite preview", + "typecheck": "tsc -b --noEmit", + "abi:sync": "node scripts/sync-native-abi.js" }, "dependencies": { "@fontsource-variable/instrument-sans": "^5.2.8", "@fontsource-variable/jetbrains-mono": "^5.2.8", "@fontsource-variable/sora": "^5.2.8", + "@noble/hashes": "^2.4.0", "@qrlwallet/connect": "4.0.0", "@qrlwallet/connect-ui": "0.2.1", "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-tabs": "^1.1.1", - "@theqrl/web3": "^1.0.1", + "@theqrl/web3": "^1.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.562.0", @@ -55,7 +58,9 @@ "brace-expansion@1": "1.1.18", "brace-expansion@5": "5.0.9", "esbuild": "0.28.1", - "js-yaml": "4.3.0", - "postcss": "8.5.25" + "js-yaml": "4.3.2", + "nanoid": "3.3.18", + "postcss": "8.5.25", + "socket.io-parser": "4.2.7" } } diff --git a/frontend/public/og-image.png b/frontend/public/og-image.png index 6a5e589bca74feeaaec2448bb590b675acc07486..28777e95c3b2b7e91e2914397c040adb4baf221b 100644 GIT binary patch literal 239584 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 Sqqi6{W?RG-OvwpEkb+s5$ua0P=hNG=aD$y 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

WRnWEjOiJJvjcl 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%0A87@QbeOhkw*BI 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 + + + + + + + + + QuantaPool + Native QRL pooled staking. + On-chain positions. Native QRL claims. + quantapool.com + + diff --git a/frontend/scripts/sync-native-abi.js b/frontend/scripts/sync-native-abi.js new file mode 100644 index 0000000..5986cfe --- /dev/null +++ b/frontend/scripts/sync-native-abi.js @@ -0,0 +1,11 @@ +import fs from "node:fs"; +const artifact = new URL( + "../../build/native/NativeQrlPool.abi", + import.meta.url, +); +const abi = JSON.parse(fs.readFileSync(artifact, "utf8")); +fs.writeFileSync( + new URL("../src/abi/NativeQrlPool.ts", import.meta.url), + "// Generated from native/contracts/NativeQrlPool.hyp using the pinned Hyperion compiler.\n" + + `export const NativeQrlPoolABI = ${JSON.stringify(abi, null, 2)} as const;\n`, +); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index aa73352..7789488 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -31,7 +31,7 @@ const App = observer(() => {

)} diff --git a/frontend/src/abi/DepositPoolV2.ts b/frontend/src/abi/DepositPoolV2.ts deleted file mode 100644 index ab4afc2..0000000 --- a/frontend/src/abi/DepositPoolV2.ts +++ /dev/null @@ -1,1079 +0,0 @@ -export const DepositPoolV2ABI = [ - { - "inputs": [], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "inputs": [], - "name": "BelowAbsoluteMin", - "type": "error" - }, - { - "inputs": [], - "name": "BelowMinDeposit", - "type": "error" - }, - { - "inputs": [], - "name": "BelowMinDepositFloor", - "type": "error" - }, - { - "inputs": [], - "name": "ContractPaused", - "type": "error" - }, - { - "inputs": [], - "name": "ExceedsRecoverableAmount", - "type": "error" - }, - { - "inputs": [], - "name": "InsufficientBuffer", - "type": "error" - }, - { - "inputs": [], - "name": "InsufficientReserve", - "type": "error" - }, - { - "inputs": [], - "name": "InsufficientShares", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidCredentialsLength", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidPubkeyLength", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidSignatureLength", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidWithdrawalCredentials", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidWithdrawalIndex", - "type": "error" - }, - { - "inputs": [], - "name": "NoWithdrawalPending", - "type": "error" - }, - { - "inputs": [], - "name": "NotOwner", - "type": "error" - }, - { - "inputs": [], - "name": "ReentrancyGuard", - "type": "error" - }, - { - "inputs": [], - "name": "StQRLAlreadySet", - "type": "error" - }, - { - "inputs": [], - "name": "StQRLNotSet", - "type": "error" - }, - { - "inputs": [], - "name": "TransferFailed", - "type": "error" - }, - { - "inputs": [], - "name": "WithdrawalNotReady", - "type": "error" - }, - { - "inputs": [], - "name": "ZeroAddress", - "type": "error" - }, - { - "inputs": [], - "name": "ZeroAmount", - "type": "error" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "user", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "qrlAmount", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "sharesReceived", - "type": "uint256" - } - ], - "name": "Deposited", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "EmergencyWithdrawal", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "newFloor", - "type": "uint256" - } - ], - "name": "MinDepositFloorUpdated", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "newMinDeposit", - "type": "uint256" - } - ], - "name": "MinDepositUpdated", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "previousOwner", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "OwnershipTransferred", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "Paused", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "rewardsAmount", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "newTotalPooled", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "blockNumber", - "type": "uint256" - } - ], - "name": "RewardsSynced", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "lossAmount", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "newTotalPooled", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "blockNumber", - "type": "uint256" - } - ], - "name": "SlashingDetected", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "stQRL", - "type": "address" - } - ], - "name": "StQRLSet", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "Unpaused", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "uint256", - "name": "validatorId", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "bytes", - "name": "pubkey", - "type": "bytes" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "ValidatorFunded", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "user", - "type": "address" - }, - { - "indexed": true, - "internalType": "uint256", - "name": "requestId", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "shares", - "type": "uint256" - } - ], - "name": "WithdrawalCancelled", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "user", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "shares", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "qrlAmount", - "type": "uint256" - } - ], - "name": "WithdrawalClaimed", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "user", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "shares", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "qrlAmount", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "requestBlock", - "type": "uint256" - } - ], - "name": "WithdrawalRequested", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "WithdrawalReserveFunded", - "type": "event" - }, - { - "inputs": [], - "name": "ABSOLUTE_MIN_DEPOSIT", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "DEPOSIT_CONTRACT", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "VALIDATOR_STAKE", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "WITHDRAWAL_DELAY", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "bufferedQRL", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "canFundValidator", - "outputs": [ - { - "internalType": "bool", - "name": "possible", - "type": "bool" - }, - { - "internalType": "uint256", - "name": "bufferedAmount", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "requestId", - "type": "uint256" - } - ], - "name": "cancelWithdrawal", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "claimWithdrawal", - "outputs": [ - { - "internalType": "uint256", - "name": "qrlAmount", - "type": "uint256" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "deposit", - "outputs": [ - { - "internalType": "uint256", - "name": "shares", - "type": "uint256" - } - ], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "emergencyWithdraw", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes", - "name": "pubkey", - "type": "bytes" - }, - { - "internalType": "bytes", - "name": "withdrawal_credentials", - "type": "bytes" - }, - { - "internalType": "bytes", - "name": "signature", - "type": "bytes" - }, - { - "internalType": "bytes32", - "name": "deposit_data_root", - "type": "bytes32" - } - ], - "name": "fundValidator", - "outputs": [ - { - "internalType": "uint256", - "name": "validatorId", - "type": "uint256" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "fundValidatorMVP", - "outputs": [ - { - "internalType": "uint256", - "name": "validatorId", - "type": "uint256" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "fundWithdrawalReserve", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "getPoolStatus", - "outputs": [ - { - "internalType": "uint256", - "name": "totalPooled", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "totalShares", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "buffered", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "validators", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "pendingWithdrawalShares", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "reserveBalance", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "exchangeRate", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getRewardStats", - "outputs": [ - { - "internalType": "uint256", - "name": "totalRewards", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "totalSlashing", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "netRewards", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "lastSync", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "user", - "type": "address" - }, - { - "internalType": "uint256", - "name": "requestId", - "type": "uint256" - } - ], - "name": "getWithdrawalRequest", - "outputs": [ - { - "internalType": "uint256", - "name": "shares", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "currentQRLValue", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "requestBlock", - "type": "uint256" - }, - { - "internalType": "bool", - "name": "canClaim", - "type": "bool" - }, - { - "internalType": "uint256", - "name": "blocksRemaining", - "type": "uint256" - }, - { - "internalType": "bool", - "name": "claimed", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "user", - "type": "address" - } - ], - "name": "getWithdrawalRequestCount", - "outputs": [ - { - "internalType": "uint256", - "name": "total", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "pending", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "lastSyncBlock", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "minDeposit", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "minDepositFloor", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "name": "nextWithdrawalIndex", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "owner", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "pause", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "paused", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "qrlAmount", - "type": "uint256" - } - ], - "name": "previewDeposit", - "outputs": [ - { - "internalType": "uint256", - "name": "shares", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "shares", - "type": "uint256" - } - ], - "name": "requestWithdrawal", - "outputs": [ - { - "internalType": "uint256", - "name": "requestId", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "qrlAmount", - "type": "uint256" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_minDeposit", - "type": "uint256" - } - ], - "name": "setMinDeposit", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_floor", - "type": "uint256" - } - ], - "name": "setMinDepositFloor", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_stQRL", - "type": "address" - } - ], - "name": "setStQRL", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "stQRL", - "outputs": [ - { - "internalType": "contract IstQRL", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "syncRewards", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "totalRewardsReceived", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "totalSlashingLosses", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "totalWithdrawalShares", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "transferOwnership", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "unpause", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "validatorCount", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - }, - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "name": "withdrawalRequests", - "outputs": [ - { - "internalType": "uint256", - "name": "shares", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "qrlAmount", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "requestBlock", - "type": "uint256" - }, - { - "internalType": "bool", - "name": "claimed", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "withdrawalReserve", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "stateMutability": "payable", - "type": "receive" - } -] as const; diff --git a/frontend/src/abi/NativeQrlPool.ts b/frontend/src/abi/NativeQrlPool.ts new file mode 100644 index 0000000..bed3dbd --- /dev/null +++ b/frontend/src/abi/NativeQrlPool.ts @@ -0,0 +1,1429 @@ +// Generated from native/contracts/NativeQrlPool.hyp using the pinned Hyperion compiler. +export const NativeQrlPoolABI = [ + { + inputs: [ + { + internalType: "address", + name: "finalityAddress", + type: "address", + }, + { + internalType: "address", + name: "portfolioAddress", + type: "address", + }, + { + internalType: "address", + name: "gateAddress", + type: "address", + }, + { + internalType: "address", + name: "immutableFeeRecipient", + type: "address", + }, + { + internalType: "uint256", + name: "minimumDeposit", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "beneficiary", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "units", + type: "uint256", + }, + ], + name: "CapitalAdopted", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint64", + name: "id", + type: "uint64", + }, + { + indexed: true, + internalType: "uint64", + name: "executionBlock", + type: "uint64", + }, + { + indexed: false, + internalType: "uint256", + name: "assets", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "exemptGain", + type: "uint256", + }, + ], + name: "CheckpointSettled", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "beneficiary", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "Claimed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "beneficiary", + type: "address", + }, + { + indexed: true, + internalType: "uint256", + name: "id", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "units", + type: "uint256", + }, + ], + name: "DepositAdmitted", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "beneficiary", + type: "address", + }, + { + indexed: true, + internalType: "uint256", + name: "id", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "DepositQueued", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "beneficiary", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "FeesClaimed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "beneficiary", + type: "address", + }, + { + indexed: true, + internalType: "uint256", + name: "id", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "PendingCancelled", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "beneficiary", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "RecoveryClaimed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint256", + name: "shares", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "cash", + type: "uint256", + }, + ], + name: "RecoveryStarted", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "beneficiary", + type: "address", + }, + { + indexed: true, + internalType: "uint256", + name: "id", + type: "uint256", + }, + ], + name: "RequestCancelled", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint64", + name: "id", + type: "uint64", + }, + ], + name: "StagingAborted", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "beneficiary", + type: "address", + }, + { + indexed: true, + internalType: "uint256", + name: "id", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + indexed: false, + internalType: "bool", + name: "rewardsOnly", + type: "bool", + }, + ], + name: "WithdrawalQueued", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "beneficiary", + type: "address", + }, + { + indexed: true, + internalType: "uint256", + name: "id", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "gross", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "fee", + type: "uint256", + }, + ], + name: "WithdrawalReserved", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "beneficiary", + type: "address", + }, + { + indexed: true, + internalType: "uint256", + name: "id", + type: "uint256", + }, + ], + name: "ZeroValueRequestCompleted", + type: "event", + }, + { + inputs: [], + name: "BASIS_SCALE", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "BOOTSTRAP_AMOUNT", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "FEE_BPS", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "LIQUIDITY_BUFFER_BPS", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "MAX_BATCH", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "SHARE_SCALE", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "TOP_UP_AMOUNT", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "VALIDATOR_SIZE", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "abortStaging", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "accFeeBasisPerShare", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "limit", + type: "uint256", + }, + ], + name: "activateDeposits", + outputs: [ + { + internalType: "uint256", + name: "processed", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "bootstrapper", + type: "address", + }, + ], + name: "adoptAndReleaseBootstrap", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "appliedSlot", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "appliedStateRoot", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "beginRecovery", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "id", + type: "uint256", + }, + ], + name: "cancelPending", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "cancelRequest", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "claim", + outputs: [ + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "claimFees", + outputs: [ + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "claimRecovery", + outputs: [ + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "claimReserve", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "beneficiary", + type: "address", + }, + ], + name: "claimable", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "consensusLossCarry", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "cumulativeCashPayments", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "cumulativeExternalDeposits", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "deposit", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "payable", + type: "function", + }, + { + inputs: [], + name: "eligibleConsensusRewardBudget", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "feeRecipient", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "feeReserve", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "finality", + outputs: [ + { + internalType: "contract INativeEconomicFinality", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "freeCash", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "frozenShares", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "fundingGate", + outputs: [ + { + internalType: "contract INativeFundingGate", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "genesisTime", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "id", + type: "uint256", + }, + ], + name: "getPending", + outputs: [ + { + components: [ + { + internalType: "address", + name: "beneficiary", + type: "address", + }, + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + internalType: "uint64", + name: "requestedBlock", + type: "uint64", + }, + { + internalType: "bool", + name: "cancelled", + type: "bool", + }, + ], + internalType: "struct NativeLedger.PendingDeposit", + name: "", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "beneficiary", + type: "address", + }, + ], + name: "getPosition", + outputs: [ + { + components: [ + { + internalType: "uint256", + name: "shares", + type: "uint256", + }, + { + internalType: "uint256", + name: "principalBasis", + type: "uint256", + }, + { + internalType: "uint256", + name: "feeBasis", + type: "uint256", + }, + { + internalType: "uint256", + name: "feeBasisDebt", + type: "uint256", + }, + { + internalType: "uint256", + name: "feeBasisFraction", + type: "uint256", + }, + { + internalType: "uint256", + name: "pending", + type: "uint256", + }, + { + internalType: "uint256", + name: "principalClaim", + type: "uint256", + }, + { + internalType: "uint256", + name: "rewardClaim", + type: "uint256", + }, + { + internalType: "uint256", + name: "feeRemainder", + type: "uint256", + }, + { + internalType: "uint256", + name: "activeRequestPlusOne", + type: "uint256", + }, + { + internalType: "uint256", + name: "recoveryClaimed", + type: "uint256", + }, + ], + internalType: "struct NativeLedger.Position", + name: "result", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "id", + type: "uint256", + }, + ], + name: "getRequest", + outputs: [ + { + components: [ + { + internalType: "address", + name: "beneficiary", + type: "address", + }, + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + internalType: "uint64", + name: "requestedBlock", + type: "uint64", + }, + { + internalType: "bool", + name: "rewardsOnly", + type: "bool", + }, + { + internalType: "bool", + name: "cancelled", + type: "bool", + }, + ], + internalType: "struct NativeLedger.WithdrawalRequest", + name: "", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "initialPoolCheckpointSlot", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "lastAdmissions", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "lastCheckpointBlock", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "lastCheckpointId", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "lastDeposits", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "lastValidatorBalance", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "lastWithdrawals", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "maxAgeSlots", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "minDeposit", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "orphanCash", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "pendingCount", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "pendingHead", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "pendingTotal", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "poolRecoveryDeadlineSlot", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "poolRecoveryWindowSlots", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "poolStatus", + outputs: [ + { + internalType: "uint8", + name: "", + type: "uint8", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "portfolio", + outputs: [ + { + internalType: "contract NativePortfolioVerifier", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "beneficiary", + type: "address", + }, + ], + name: "positionValue", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "limit", + type: "uint256", + }, + ], + name: "processQueue", + outputs: [ + { + internalType: "uint256", + name: "processed", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "queueHead", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "recovering", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "recoveryCash", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "recoveryPaid", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "requestCount", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "requestRewards", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "requestWithdrawal", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "beneficiary", + type: "address", + }, + ], + name: "rewardValue", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "riskAssets", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "secondsPerSlot", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "settleCheckpoint", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "stage", + outputs: [ + { + internalType: "uint8", + name: "", + type: "uint8", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "stageCutoff", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "totalFeesPaid", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "totalShares", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + stateMutability: "payable", + type: "receive", + }, +] as const; diff --git a/frontend/src/abi/StQRLV2.ts b/frontend/src/abi/StQRLV2.ts deleted file mode 100644 index bfc379a..0000000 --- a/frontend/src/abi/StQRLV2.ts +++ /dev/null @@ -1,775 +0,0 @@ -export const StQRLV2ABI = [ - { - "inputs": [], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "inputs": [], - "name": "ContractPaused", - "type": "error" - }, - { - "inputs": [], - "name": "DepositPoolAlreadySet", - "type": "error" - }, - { - "inputs": [], - "name": "InsufficientAllowance", - "type": "error" - }, - { - "inputs": [], - "name": "InsufficientBalance", - "type": "error" - }, - { - "inputs": [], - "name": "InsufficientUnlockedShares", - "type": "error" - }, - { - "inputs": [], - "name": "NotDepositPool", - "type": "error" - }, - { - "inputs": [], - "name": "NotOwner", - "type": "error" - }, - { - "inputs": [], - "name": "ZeroAddress", - "type": "error" - }, - { - "inputs": [], - "name": "ZeroAmount", - "type": "error" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "owner", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "spender", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "name": "Approval", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "previousPool", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "newPool", - "type": "address" - } - ], - "name": "DepositPoolSet", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "previousOwner", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "OwnershipTransferred", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "Paused", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "sharesAmount", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "qrlAmount", - "type": "uint256" - } - ], - "name": "SharesBurned", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "sharesAmount", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "qrlAmount", - "type": "uint256" - } - ], - "name": "SharesMinted", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "previousAmount", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "newAmount", - "type": "uint256" - } - ], - "name": "TotalPooledQRLUpdated", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "name": "Transfer", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "Unpaused", - "type": "event" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_owner", - "type": "address" - }, - { - "internalType": "address", - "name": "spender", - "type": "address" - } - ], - "name": "allowance", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "spender", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "approve", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "balanceOf", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "internalType": "uint256", - "name": "sharesAmount", - "type": "uint256" - } - ], - "name": "burnShares", - "outputs": [ - { - "internalType": "uint256", - "name": "qrlAmount", - "type": "uint256" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "decimals", - "outputs": [ - { - "internalType": "uint8", - "name": "", - "type": "uint8" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "depositPool", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getExchangeRate", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "sharesAmount", - "type": "uint256" - } - ], - "name": "getPooledQRLByShares", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "getQRLValue", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "qrlAmount", - "type": "uint256" - } - ], - "name": "getSharesByPooledQRL", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - }, - { - "internalType": "uint256", - "name": "sharesAmount", - "type": "uint256" - } - ], - "name": "lockShares", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "lockedSharesOf", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "internalType": "uint256", - "name": "qrlAmount", - "type": "uint256" - } - ], - "name": "mintShares", - "outputs": [ - { - "internalType": "uint256", - "name": "shares", - "type": "uint256" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "name", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "owner", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "pause", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "paused", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "renounceOwnership", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_depositPool", - "type": "address" - } - ], - "name": "setDepositPool", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "sharesOf", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "symbol", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "totalPooledQRL", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "totalShares", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "totalSupply", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "transfer", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "transferFrom", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "transferOwnership", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - }, - { - "internalType": "uint256", - "name": "sharesAmount", - "type": "uint256" - } - ], - "name": "unlockShares", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "unpause", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "newTotalPooledQRL", - "type": "uint256" - } - ], - "name": "updateTotalPooledQRL", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "immatureSharesOf", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "matureAtBlockOf", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "minStakeBlocks", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - } -] as const; diff --git a/frontend/src/abi/ValidatorManager.ts b/frontend/src/abi/ValidatorManager.ts deleted file mode 100644 index bf7d09c..0000000 --- a/frontend/src/abi/ValidatorManager.ts +++ /dev/null @@ -1,540 +0,0 @@ -export const ValidatorManagerABI = [ - { - "inputs": [], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "inputs": [], - "name": "InvalidPubkeyLength", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidStatusTransition", - "type": "error" - }, - { - "inputs": [], - "name": "NotAuthorized", - "type": "error" - }, - { - "inputs": [], - "name": "NotDepositPool", - "type": "error" - }, - { - "inputs": [], - "name": "NotOwner", - "type": "error" - }, - { - "inputs": [], - "name": "ValidatorAlreadyExists", - "type": "error" - }, - { - "inputs": [], - "name": "ValidatorNotFound", - "type": "error" - }, - { - "inputs": [], - "name": "ZeroAddress", - "type": "error" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "depositPool", - "type": "address" - } - ], - "name": "DepositPoolSet", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "previousOwner", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "OwnershipTransferred", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "uint256", - "name": "validatorId", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "activatedBlock", - "type": "uint256" - } - ], - "name": "ValidatorActivated", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "uint256", - "name": "validatorId", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "requestBlock", - "type": "uint256" - } - ], - "name": "ValidatorExitRequested", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "uint256", - "name": "validatorId", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "exitedBlock", - "type": "uint256" - } - ], - "name": "ValidatorExited", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "uint256", - "name": "validatorId", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "bytes", - "name": "pubkey", - "type": "bytes" - }, - { - "indexed": false, - "internalType": "enum ValidatorManager.ValidatorStatus", - "name": "status", - "type": "uint8" - } - ], - "name": "ValidatorRegistered", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "uint256", - "name": "validatorId", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "slashedBlock", - "type": "uint256" - } - ], - "name": "ValidatorSlashed", - "type": "event" - }, - { - "inputs": [], - "name": "VALIDATOR_STAKE", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "validatorId", - "type": "uint256" - } - ], - "name": "activateValidator", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "activeValidatorCount", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256[]", - "name": "validatorIds", - "type": "uint256[]" - } - ], - "name": "batchActivateValidators", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "depositPool", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getStats", - "outputs": [ - { - "internalType": "uint256", - "name": "total", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "pending", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "active", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "totalStaked", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "validatorId", - "type": "uint256" - } - ], - "name": "getValidator", - "outputs": [ - { - "internalType": "bytes", - "name": "pubkey", - "type": "bytes" - }, - { - "internalType": "enum ValidatorManager.ValidatorStatus", - "name": "status", - "type": "uint8" - }, - { - "internalType": "uint256", - "name": "activatedBlock", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "exitedBlock", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes", - "name": "pubkey", - "type": "bytes" - } - ], - "name": "getValidatorIdByPubkey", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes", - "name": "pubkey", - "type": "bytes" - } - ], - "name": "getValidatorStatus", - "outputs": [ - { - "internalType": "enum ValidatorManager.ValidatorStatus", - "name": "", - "type": "uint8" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "enum ValidatorManager.ValidatorStatus", - "name": "status", - "type": "uint8" - } - ], - "name": "getValidatorsByStatus", - "outputs": [ - { - "internalType": "uint256[]", - "name": "validatorIds", - "type": "uint256[]" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "validatorId", - "type": "uint256" - } - ], - "name": "markValidatorExited", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "validatorId", - "type": "uint256" - } - ], - "name": "markValidatorSlashed", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "owner", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "pendingValidatorCount", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "name": "pubkeyToIndex", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes", - "name": "pubkey", - "type": "bytes" - } - ], - "name": "registerValidator", - "outputs": [ - { - "internalType": "uint256", - "name": "validatorId", - "type": "uint256" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "validatorId", - "type": "uint256" - } - ], - "name": "requestValidatorExit", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_depositPool", - "type": "address" - } - ], - "name": "setDepositPool", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "totalValidators", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "transferOwnership", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "name": "validators", - "outputs": [ - { - "internalType": "bytes", - "name": "pubkey", - "type": "bytes" - }, - { - "internalType": "enum ValidatorManager.ValidatorStatus", - "name": "status", - "type": "uint8" - }, - { - "internalType": "uint256", - "name": "activatedBlock", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "exitedBlock", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - } -] as const; diff --git a/frontend/src/components/ActivityCard.tsx b/frontend/src/components/ActivityCard.tsx index 898f438..dfbe83c 100644 --- a/frontend/src/components/ActivityCard.tsx +++ b/frontend/src/components/ActivityCard.tsx @@ -1,24 +1,69 @@ import { observer } from "mobx-react-lite"; -import { ArrowDownToLine, ArrowUpFromLine, Clock, ExternalLink, Undo2 } from "lucide-react"; +import { + ArrowDownToLine, + ArrowUpFromLine, + Clock, + ExternalLink, + Undo2, +} from "lucide-react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/UI/Card"; import { useStore } from "@/stores/store"; import type { ActivityType } from "@/stores/poolStore"; -import { getExplorerAddressUrl, getExplorerTxUrl, NATIVE_UNIT } from "@/config/networks"; +import { + getExplorerAddressUrl, + getExplorerTxUrl, + NATIVE_UNIT, +} from "@/config/networks"; import { formatAmount } from "@/utils/format"; const ACTIVITY_META: Record< ActivityType, - { label: string; icon: React.ComponentType<{ className?: string }>; color: string } + { + label: string; + icon: React.ComponentType<{ className?: string }>; + color: string; + } > = { - deposit: { label: "Staked", icon: ArrowDownToLine, color: "text-success" }, - request: { label: "Withdrawal requested", icon: Clock, color: "text-secondary" }, - claim: { label: "Withdrawal claimed", icon: ArrowUpFromLine, color: "text-identity-accent" }, - cancel: { label: "Request cancelled", icon: Undo2, color: "text-muted-foreground" }, + deposit: { + label: "Deposit queued", + icon: ArrowDownToLine, + color: "text-success", + }, + request: { + label: "Withdrawal requested", + icon: Clock, + color: "text-secondary", + }, + claim: { + label: "QRL claimed", + icon: ArrowUpFromLine, + color: "text-identity-accent", + }, + admitted: { + label: "Deposit admitted", + icon: ArrowDownToLine, + color: "text-success", + }, + recovery: { + label: "Recovery cash claimed", + icon: ArrowUpFromLine, + color: "text-identity-accent", + }, + cancel: { + label: "Request cancelled", + icon: Undo2, + color: "text-muted-foreground", + }, + completed: { + label: "Request completed with no payout", + icon: Clock, + color: "text-muted-foreground", + }, }; const MAX_ROWS = 8; -/** The connected account's staking history, sourced from DepositPool events. */ +/** The connected account's staking history, sourced from native pool events. */ export const ActivityCard = observer(() => { const { poolStore } = useStore(); const account = poolStore.account; @@ -31,21 +76,23 @@ export const ActivityCard = observer(() => { {rows.length === 0 ? (

{poolStore.activityError - ? "Activity is unavailable right now. Use the Zondscan link above." + ? "Activity history is unavailable from this RPC. Position and claim data are read separately." : "No staking activity yet for this address."}

) : ( @@ -61,6 +108,12 @@ export const ActivityCard = observer(() => {

{meta.label}

+ {item.type === "completed" && ( +

+ Your position retains its rights to later returns and + recovery cash. +

+ )}

Block {item.blockNumber.toString()}

@@ -71,13 +124,8 @@ export const ActivityCard = observer(() => { {formatAmount(item.qrlAmount)} {NATIVE_UNIT}

)} - {item.shares !== null && ( -

- {formatAmount(item.shares)} stQRL -

- )}
- {item.txHash && ( + {item.txHash && poolStore.network.explorer && ( { )} {poolStore.activity.length > MAX_ROWS && (

- Showing the latest {MAX_ROWS} of {poolStore.activity.length}. Full history on Zondscan. + Showing the latest {MAX_ROWS} of {poolStore.activity.length}. + Earlier activity remains in the chain transaction history.

)}
diff --git a/frontend/src/components/AmountInput.tsx b/frontend/src/components/AmountInput.tsx index d5bae05..aa5b4ec 100644 --- a/frontend/src/components/AmountInput.tsx +++ b/frontend/src/components/AmountInput.tsx @@ -14,7 +14,13 @@ interface AmountInputProps { const PERCENTAGES = [25, 50, 75] as const; /** Numeric amount input with the wallet's 25/50/75/Max quick buttons. */ -export function AmountInput({ value, onChange, balance, symbol, disabled }: AmountInputProps) { +export function AmountInput({ + value, + onChange, + balance, + symbol, + disabled, +}: AmountInputProps) { const setFraction = (percent: number) => { if (balance === null) return; const amount = (balance * BigInt(percent)) / 100n; @@ -26,6 +32,7 @@ export function AmountInput({ value, onChange, balance, symbol, disabled }: Amou
{ href={getExplorerAddressUrl(poolStore.account.address)} target="_blank" rel="noreferrer" - title={`${poolStore.account.address}: view on Zondscan`} + title={poolStore.account.address} + aria-label={`View wallet ${poolStore.account.address} on explorer`} className="inline-flex min-h-9 items-center whitespace-nowrap rounded-md border border-identity-accent/15 bg-identity-accent/[0.04] px-2.5 font-data text-xs text-identity-accent transition-colors hover:border-identity-accent/40 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring" > {shortenAddress(poolStore.account.address)} @@ -37,9 +38,18 @@ export const ConnectButton = observer(() => { } return ( - ); }); diff --git a/frontend/src/components/Layout/Footer.tsx b/frontend/src/components/Layout/Footer.tsx index ecf8104..10d6051 100644 --- a/frontend/src/components/Layout/Footer.tsx +++ b/frontend/src/components/Layout/Footer.tsx @@ -7,8 +7,8 @@ export function Footer() { return (