V10 contracts: Codex fixes on top of rebased PR #202#231
Conversation
Rewrite the Phase 4 placeholder into a Phase 5 scaffold base: imports, Hub-wired dependency fields, constants (SCALE18, WITHDRAWAL_DELAY = 15 days), storage (nextTokenId), events (PositionCreated, PositionRelocked, PositionRedelegated, WithdrawalRequested, WithdrawalProcessed, RewardsAccrued, ConvertedFromV8), error types (InvalidLockEpochs, LockNotExpired, LockStillActive, WithdrawalNotRequested, WithdrawalAlreadyRequested, WithdrawalDelayPending, V8StakeNotFullyClaimed, NotPositionOwner, PositionNotFound, ZeroAmount, ProfileDoesNotExist, SameIdentity, MaxStakeExceeded), constructor, initialize wiring the full V10 dependency set (Staking, StakingStorage, ConvictionStakingStorage, DelegatorsInfo, Chronos, RandomSamplingStorage, ShardingTableStorage, ShardingTable, Ask, ParametersStorage, ProfileStorage, Token), and ERC721Enumerable overrides (supportsInterface, _increaseBalance, _update pure pass-through for the accrued-interest transfer model per Phase 5 Q8). No entry-point logic yet — S2 (createConviction) through S7 (convertToNFT) layer on top. Tier table helper and revert-stubs arrive in the next two commits of S1. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add `_convictionMultiplier(uint256 lockEpochs) internal pure` strict
tier helper. Exact-match {0, 2, 3, 6, 12} only; any other input
reverts `InvalidLockEpochs()`. `0 → 1e18` so reward-math callers
can re-invoke after post-expiry rest-state transitions.
NOTE: The S1 brief's Q5 table listed `lockEpochs == 1 → 1.5e18`,
which would clash with the authoritative storage-side tier ladder
in `ConvictionStakingStorage.expectedMultiplier18` (where
`expectedMultiplier18(1) == 1e18` and 1.5x lives at `lockEpochs == 2`).
Aligned here with the storage-side truth to avoid "Tier mismatch"
reverts on every `createConviction(_, _, 1)` call; the helper's
valid set is therefore `{0, 2, 3, 6, 12}`. In-file NOTE comment
captures the deviation for S2 and the main agent to review.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add scaffolded external entry points for S2..S7 to fill in: `createConviction`, `relock`, `redelegate`, `requestWithdrawal`, `processWithdrawal`, `claimRewards`, `convertToNFT`. Each is marked `external pure` and reverts `"NotImplemented"` — the `pure` modifier is a scaffold-only hack to suppress solc's "can be restricted to pure" warning and will be dropped by each downstream subagent when they add real state reads/writes. The `@dev` line on every stub calls this out so S2..S7 remember to remove it. Ownership mapping (mirrors the Phase 5 plan): S2 → createConviction (Flow A atomic) S3 → relock S4 → redelegate S5 → requestWithdrawal / processWithdrawal S6 → claimRewards (stubbed TRAC transfer, Phase 11 finishes it) S7 → convertToNFT (V8 → V10 migration) Compile clean (only the pre-existing KnowledgeAssetsStorage warning remains) and the 054 deploy script runs green with no changes — `DKGStakingConvictionNFT` initializes against the full V10 dep set even though the existing deploy script only lists `Hub` + `Token` (the other deps are pulled in transitively via earlier scripts). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ds bucket + mutators)
Phase 5 extends Phase 2 storage to support the split-bucket Position model
and tightens the conviction tier ladder to the Phase 5 discrete set.
Tier table (Item 1): `expectedMultiplier18` now returns only for the
discrete lock set {0,1,3,6,12}. `lock == 1 → 1.5x` replaces the old
`lock == 2 → 1.5x` mapping. Every other lock reverts with
"Invalid lock" instead of snap-down. `updateOnRelock`'s `>= 2` guard
relaxes to `>= 1` so the lock-1 bootstrap tier is a valid re-commit
target. NOTE: `Staking.convictionMultiplier` and
`DKGStakingConvictionNFT._convictionMultiplier` still carry the legacy
snap-down tables; downstream subagents must align all three in a single
symmetric change before mainnet.
Position struct (Item 2): adds `uint96 rewards` for the compounded
rewards bucket. Slot 1 (`raw + lockEpochs + expiryEpoch + identityId`)
is unchanged — `rewards` lives in slot 2 alongside `multiplier18` and
`lastClaimedEpoch` (96 + 64 + 64 = 224 bits), so there is zero
storage-layout churn for the existing fields.
New mutators (Item 3): `increaseRewards`, `decreaseRewards`,
`decreaseRaw`. All three use `identityId != 0` as the existence
sentinel so a `raw == 0, rewards > 0` position after a full principal
drain remains valid for rewards withdrawal. Each mirrors the Phase 2
diff pattern: write `diff[currentEpoch]`, mark dirty, finalize
`[last+1, currentEpoch-1]`. `decreaseRaw` also shrinks the pending
expiry delta by `amount*(mult-1)/SCALE18` (since the smaller raw has a
correspondingly smaller boost drop pending at expiry), mirroring
`deletePosition`'s cancel path.
Effective-stake math (Item 4): `updateOnRedelegate` and `deletePosition`
now include `+ rewards` in the per-node / global contribution they
move or remove. Rewards always contribute at 1x — no multiplier, no
expiry drop — so no expiry delta is installed for the rewards bucket.
Tests (Item 5): every test that used an out-of-set lock (2/4/5/10/11)
remaps to {1,3,6,12}. New `Phase 5 rewards bucket` describe block
covers default-0 initialization, all three mutators, mixed-bucket
math across boosted and post-expiry windows, and per-node aggregation
with rewards layered on multiple NFTs.
Version bumped to 1.1.0.
Verification (all green):
- `npx hardhat compile` — 2 contracts, 0 errors
- `ConvictionStakingStorage` suite — 57 passing (30 pre-existing + 27 new/updated)
- `DelegatorsInfo` regression — 35 passing
- `Staking` full suite (incl. `_recordStake`) — 85 passing
- `npx hardhat deploy --network hardhat` — clean
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tion
The S1 scaffold's `_convictionMultiplier` had been aligned to the pre-P2
storage bug and used the tier set {0, 2, 3, 6, 12}, where `lockEpochs == 2`
mapped to 1.5x. That has been corrected to the roadmap-authoritative set
{0, 1, 3, 6, 12} per `04_TOKEN_ECONOMICS §4.1` — `lockEpochs == 1` is now
the 1.5x tier. This matches `ConvictionStakingStorage.expectedMultiplier18`
after the Phase 5 P2 hotfix (commit `e7528d38`); the stale NOTE comment
justifying the old set and the top-of-file NatSpec tier list have both
been updated. Pure tier-table fix — no other behavior changed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…+ deploy script)
Introduces `StakingV10.sol`, the V10 NFT-backed staking orchestrator. Owns
all V10 conviction logic (stake / relock / redelegate / withdraw / claim /
convertToNFT) while leaving the legacy V8 `Staking.sol` untouched. All 8
external entry points are stubbed as `revert("NotImplemented")` behind the
`onlyConvictionNFT` gate — downstream implementation subagents will wire
up the bodies one entry at a time against this clean revert surface.
Key design choices:
- Split-contract architecture: `StakingV10` is Hub-registered and gated so
only the Hub-registered `DKGStakingConvictionNFT` can call its external
entries. Pattern mirrors Phase 4's `Staking._recordStake` gate exactly.
- Every entry takes `address staker` as its first parameter — the NFT
wrapper passes `msg.sender` explicitly so StakingV10 never trusts
`tx.origin`. Ownership must be verified against `nft.ownerOf(tokenId)`
inside each implementation.
- 12 Hub dependencies wired in `initialize()`, including a `Staking`
reference so V10 settlement paths can call the existing
`Staking.prepareForStakeChange(...)` helper (single score-per-stake
math source for both V8 and V10 paths).
- Stubs use `external view` as a scaffold-only mutability modifier to
keep compile clean — downstream subagents drop `view` when wiring
state writes. Pattern mirrors `DKGStakingConvictionNFT`'s `pure`-stub
convention from Phase 5 S1.
- New V10 errors declared locally (`InvalidLockEpochs`, `LockStillActive`,
etc.) — NOT added to `StakingLib.sol`. Reuses `StakingLib.OnlyConvictionNFT`
from Phase 4 for the gate.
Deploy script `055_deploy_staking_v10.ts` mirrors the existing
`helpers.deploy` pattern used by all Hub-registered contracts (same as
`023_deploy_staking.ts`). Tagged `StakingV10` + `v10`, with 13
dependencies matching the 12 Hub reads in `initialize()` + Hub itself.
Also fixes the `DKGStakingConvictionNFT` test fixture's
`ContractDoesNotExist("Staking")` failure by expanding
`054_deploy_dkg_staking_conviction_nft.ts`'s dependency list from
`['Hub', 'Token']` to the full 14 contracts its `initialize()` resolves
via `hub.getContractAddress(...)`, including the new `StakingV10`. The
old two-element list skipped deploy of `Staking`, so any test loading
this fixture reverted at NFT initialize time.
Verification:
- `npx hardhat compile` — clean (no new warnings, sole pre-existing
warning is an unrelated `KnowledgeAssetsStorage` one).
- `npx hardhat deploy --network hardhat` — green; StakingV10 and
DKGStakingConvictionNFT both deploy and initialize successfully.
- `npx hardhat test --grep 'ConvictionStakingStorage'` — 57 passing
(Phase 2 regression holds).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ints forward to StakingV10 Rewrites the 8 entry points in DKGStakingConvictionNFT to thin wrappers that mint/burn ERC-721 tokens and forward business logic to StakingV10 (passing msg.sender explicitly as the `staker` arg). The NFT contract is now a dumb ownership receipt: no direct StakingStorage/ConvictionStorage mutations, no TRAC handling. Only reads for the redelegate mirror event and the burn-on-drain check in finalizeWithdrawal. Renames: requestWithdrawal → createWithdrawal(tokenId, amount) processWithdrawal → finalizeWithdrawal(tokenId) claimRewards → claim(tokenId) Adds: cancelWithdrawal(tokenId) — new entry point missing from the S1 stub set. Event renames + cleanup: WithdrawalRequested → WithdrawalCreated(tokenId, amount) WithdrawalProcessed → WithdrawalFinalized(tokenId) + WithdrawalCancelled(tokenId) - RewardsAccrued (moved to StakingV10.RewardsClaimed) PositionCreated + ConvertedFromV8 shape tweaks to match wrapper-layer mirror semantics (authoritative events live at StakingV10 / storage). Error surface trimmed to the three errors actually thrown at the wrapper layer: InvalidLockEpochs, NotPositionOwner, ZeroAmount. All other position-lifecycle errors are now StakingV10's responsibility. Runtime behaviour: StakingV10 stubs still revert "NotImplemented", so every wrapper forwards and reverts until implementation subagents fill the StakingV10 bodies. Expected and correct for this step. Gates: compile green, hardhat deploy green (DKGStakingConvictionNFT + StakingV10 deploy clean), Phase 2 ConvictionStakingStorage regression still 57 passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rewrites the stale DKGStakingConvictionNFT test suite to target the Phase 5 split-contract architecture. The previous file referenced `NFT.stake()` / `NFT.getPosition()` / `NFT.getMultiplier()` — all removed in the Phase 5 NFT rewrite (commit 33d009d), so every test was a compile-level reference to nonexistent methods. New suite exercises the full end-to-end path `createConviction` → `StakingV10.stake` → `{StakingStorage, ConvictionStakingStorage}` via the real Hub-wired NFT wrapper (no impersonation), covering: - revert: amount == 0 (NFT wrapper fail-fast) - revert: lock tier 0 / 2 / 13 (rest-state + between-tier rejects) - revert: non-existent profile (StakingV10 ProfileDoesNotExist) - revert: nodeStake + amount > maxStake (StakingV10 MaxStakeExceeded) - revert: no ERC20 allowance for StakingV10 - happy path: 12-month tier writes Staking/Conviction storage, staker→StakingStorage TRAC flow, NFT wrapper balance == 0 - tier matrix: {1, 3, 6, 12} → {1.5x, 2x, 3.5x, 6x} multipliers - tokenId monotonic: 0, 1, 2 for multi-mint single user - two-user path: distinct tokenIds on same node - wrapper TRAC balance invariant - direct `StakingV10.stake` from non-NFT caller → OnlyConvictionNFT - sharding table: node crosses minStake → added StakingV10.stake is still a `revert("NotImplemented")` stub so 12 of 17 tests fail — this commit is the red phase of the TDD cycle; the implementation lands in the next commit. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…green)
Replaces the `revert("NotImplemented")` stub on `StakingV10.stake` with
the full Phase 5 NFT-backed stake path. Drops the `view` modifier.
Mirrors the Phase 4 `Staking._recordStake` layout step-for-step:
1. `amount > 0` / `profileExists(identityId)` guards — same shape as
V8 `_stake` + Phase 4 `_recordStake`.
2. Resolve tier via `ConvictionStakingStorage.expectedMultiplier18`
(the canonical Phase 5 {0,1,3,6,12} ladder). Reject lockEpochs==0
at the policy layer since it's the post-expiry rest state, not a
valid fresh-mint tier — the storage helper stays tolerant of 0
for reward-math callers.
3. `maxStake` cap on the destination node (parametersStorage.maximumStake).
4. `Staking.prepareForStakeChange(currentEpoch, identityId,
bytes32(tokenId))` to baseline the fresh NFT delegator key to the
node's current score-per-stake index, preventing a later reward
claim from collecting score the node earned before the NFT existed.
Same cross-helper the V8 `_stake` and Phase 4 `_recordStake` paths
use.
5. `token.transferFrom(staker, stakingStorage, amount)` — TRAC flows
user → StakingStorage in one hop; NFT wrapper never holds funds
(Phase 5 Q4).
6. StakingStorage writes: setDelegatorStakeBase / setNodeStake /
increaseTotalStake, targeting `bytes32(tokenId)` (disjoint from V8
`keccak256(address)` key space).
7. `ConvictionStakingStorage.createPosition` — position record with
raw, tier, and expiry (expiry computed internally as
currentEpoch + lockEpochs).
8. Sharding-table insert if node crossed minimumStake. Guarded by
`!nodeExists && stake >= min` so we don't make an unnecessary
external call for sub-minimum stakes.
9. `ask.recalculateActiveSet()` so the new effective stake is
reflected in the next sampling window.
10. Emit `Staked(tokenId, staker, identityId, amount, lockEpochs)`.
No `TokenIdAlreadyRecorded` freshness guard: the calling path
`DKGStakingConvictionNFT.createConviction` does `_mint(msg.sender,
tokenId)` first, and ERC721 `_mint` reverts on any already-minted
tokenId — the guard would be pure defense-in-depth against a
hypothetical future caller. The scope for this subagent explicitly
forbids adding new errors to StakingLib, so keeping the body minimal
matches both the architectural intent and the scope.
Test results (packages/evm-module/test/unit/DKGStakingConvictionNFT.test.ts):
17 passing / 17 total (all red-phase failures now green)
Regression (all passing):
- test/unit/Staking.test.ts — 85 tests (incl. 11 `_recordStake`)
- test/unit/ConvictionStakingStorage.test.ts — 68 tests
- test/unit/DelegatorsInfo.test.ts — 92 tests
Deploy smoke: `npx hardhat deploy` — green, DKGStakingConvictionNFT
deployed at 0xAD29... on hardhat network.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add 9 tests to `DKGStakingConvictionNFT.test.ts` under
`describe('relock (→ StakingV10.relock)')`: NFT ownership gate,
wrapper-layer tier fail-fast (2, 13), `LockStillActive` guard,
non-existent tokenId (owner check), three happy paths (tier 1 → 12,
tier 0 rest state, expiryEpoch shift), and the
`onlyConvictionNFT` gate for direct StakingV10 calls. Tests fail
today because `StakingV10.relock` still reverts `"NotImplemented"`.
Imports `time` from `@nomicfoundation/hardhat-network-helpers` and
captures `Chronos` in the outer `beforeEach` block. Adds a local
`advanceEpochs(n)` helper mirroring the pattern in
`ConvictionStakingStorage.test.ts`.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ckup post-expiry
The original Phase 2 hotfix (commit `e7528d38`) rejected
`newLockEpochs == 0` with `require(newLockEpochs >= 1, "Lock too short")`,
on the theory that "relock must carry an actual boost". The roadmap
(`04_TOKEN_ECONOMICS §4.1`) lists tier 0 as a first-class user-facing
tier ("no lockup / 1 / 3 / 6 / 12 months"), and Phase 5 user stories
explicitly include a post-expiry holder choosing to remain at 1x
rather than re-committing. Drop the guard; the `expectedMultiplier18`
call is already the canonical discrete-set validator for {0,1,3,6,12},
and the tier-0 math is well-defined: `boost = raw*(1e18-1e18)/1e18 = 0`
so the `currentEpoch` and `newExpiry` diff writes are both zero-delta
no-ops. The only observable mutation is the `lockEpochs` /
`multiplier18` / `expiryEpoch` writeback that drives the position
back to the rest state — matching `createPosition`'s lock-0 branch
conceptually.
Scope: one surgical guard removal + the matching `ConvictionStakingStorage`
unit test (which previously asserted "Lock too short" for tier 0) is
flipped to a positive assertion that walks the new path end to end
(effective stake stays at the post-expiry baseline, Position struct
carries the rest-state tier).
Unblocks SV10-relock: `StakingV10.relock(_, _, 0)` now passes through
the storage mutator cleanly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… (green)
Replace the `NotImplemented` stub with the full relock body:
1. `convictionStorage.getPosition(tokenId)` — no explicit "exists"
guard; the NFT wrapper's `ownerOf(tokenId) != msg.sender` already
rejects un-minted tokenIds, and a drained rewards-only position is
caught by `updateOnRelock`'s own `raw > 0` precondition.
2. Strict post-expiry check (`currentEpoch > pos.expiryEpoch`) →
`LockStillActive` on pre-expiry.
3. `staking.prepareForStakeChange(currentEpoch, identityId, bytes32(tokenId))`
— settle the score-per-stake index before mutating any effective
stake, mirroring Phase 4 `Staking._recordStake` conventions.
4. `convictionStorage.expectedMultiplier18(newLockEpochs)` — reverts
`"Invalid lock"` on any tier outside the discrete set `{0,1,3,6,12}`.
5. `convictionStorage.updateOnRelock(tokenId, newLockEpochs, newMultiplier18)`
— owns the effective-stake diff + position field rewrite.
6. `emit Relocked(tokenId, newLockEpochs, currentEpoch + newLockEpochs)`.
Tier 0 (permanent rest state, 1x) is a valid relock target per the
V10 roadmap — enabled by the sister commit that relaxes the Phase 2
`updateOnRelock` guard. Raw principal stays put (no transfer); rewards
bucket is untouched (continues earning 1x, withdrawable on its own
rhythm).
The `staker` parameter is kept in the signature for call-shape
symmetry with every other `onlyConvictionNFT` entry point, but is
unused in the body (ownership is enforced by the NFT wrapper layer).
All 9 relock unit tests green; Staking/ConvictionStakingStorage/
DKGStakingConvictionNFT regression suites all green; hardhat deploy
smoke clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds `describe('redelegate (→ StakingV10.redelegate)')` block covering the
per-node stake move semantics, revert paths, happy paths mid-lock and
post-expiry, multi-NFT independence, and the `onlyConvictionNFT` gate.
Tests are red against the `revert("NotImplemented")` scaffold stub.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…entation (green) Implements the per-node stake move for V10 NFT-backed positions. Both raw and rewards buckets move together (`totalAmount = raw + rewards`); global `totalStake` is invariant. Flow mirrors the V8 `Staking.redelegate` layout adapted to the V10 `bytes32(tokenId)` delegator key scheme: same-node short-circuit → destination profile existence → destination `maxStake` cap → settle indices on BOTH nodes via `Staking.prepareForStakeChange` → move delegator stake base → per-node `decrease/increaseNodeStake` → `updateOnRedelegate` (owns the conviction effective-stake diff + `pos.identityId` mutation) → sharding table maintenance on both sides → `Ask.recalculateActiveSet()`. All 8 redelegate unit tests green. ConvictionStakingStorage (57) and Staking broad regression (196) green. Deploy smoke green. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… (red) Adds createWithdrawal / cancelWithdrawal / finalizeWithdrawal describe blocks under the DKGStakingConvictionNFT suite. Covers ownership gates, amount validation, pre/post-expiry withdrawable splits, duplicate-request guards, 15-day delay enforcement, TRAC refund math, StakingStorage / ConvictionStakingStorage mutation, NFT burn-on-drain, and sharding-table removal on sub-minimumStake finalization. Rewards injection uses the hub-owner privilege on ConvictionStakingStorage + StakingStorage mutators (onlyContracts permits hub owner) to simulate the SV10-claim path without depending on its implementation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…s (green) Implements createWithdrawal / cancelWithdrawal / finalizeWithdrawal on StakingV10, replacing the Phase 5 scaffold stubs. createWithdrawal - Pre-expiry: amount ≤ pos.rewards (raw is locked). - Post-expiry: amount ≤ pos.raw + pos.rewards. - 15-day delay timer hardcoded via WITHDRAWAL_DELAY constant. - One pending request per NFT (bytes32(tokenId) key in StakingStorage withdrawals mapping, disjoint from V8 keccak256(address) keys). - No stake mutation here — the delegator base, node stake, and total stake all stay put during the delay window. Request-time decrement is the V8 pattern; V10 scope intentionally defers to finalize. cancelWithdrawal - Clears the pending request slot. Because createWithdrawal does not mutate stake, there is nothing to re-stake — cancel is a pure withdrawal-request delete. finalizeWithdrawal - Settles the delegator's score-per-stake index for currentEpoch via Staking.prepareForStakeChange (mirrors stake/relock/redelegate). - Drain split: rewards first (decreaseRewards), then raw (decreaseRaw) — each storage mutator owns its own effective-stake diff (1x for rewards, multiplier-scaled for raw pre-expiry). - StakingStorage: setDelegatorStakeBase(newBase) triggers the inactive-transition when newBase == 0; decreaseNodeStake and decreaseTotalStake by the full request amount. - Transfer TRAC from the StakingStorage vault to the NFT owner. - Sharding table removal if nodeStake drops below minimumStake. - Ask.recalculateActiveSet to refresh the active-set composition. - NO call to ConvictionStakingStorage.deletePosition on full drain: the split-bucket mutators already handle the effective-stake diff, and deletePosition requires pos.raw > 0 (unreachable after a raw drain). The NFT wrapper's burn-on-drain check reads getPosition(tokenId) and burns when raw == 0 && rewards == 0, which is functionally equivalent to deleted for the NFT layer. Adds the PositionNotFound error (defense-in-depth guard for direct callers past the wrapper's ownerOf check) to StakingV10's local error surface. No StakingLib edits. Delegator base composition decision: StakingStorage.delegatorStakeBase tracks raw + rewards (the full composite per-delegator balance). Precedent: Phase 4 StakingV10.redelegate writes setDelegatorStakeBase(newId, key, pos.raw + pos.rewards). A future SV10-claim must compound rewards into this base to stay consistent. Tests wrap the 15-day time advance with a chunked pre-finalize helper (finalizeEffectiveStakeUpTo / finalizeNodeEffectiveStakeUpTo in slices of 50 epochs, hub-owner privileged via onlyContracts) to keep the downstream decreaseRaw/decreaseRewards under the hardhat 15M tx gas cap. Suite: 56 DKGStakingConvictionNFT tests passing (23 new + 33 prior). ConvictionStakingStorage (57) and Staking (218) regressions all green. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds an 11-test `claim` describe block to DKGStakingConvictionNFT.test.ts
covering the Phase 5 auto-compound flow: no-op paths (fresh position,
zero score delta, multi-claim idempotence), owner/gate rejection, the
single-epoch and multi-epoch happy paths, pre-expiry multiplier
application (6x on a 12-epoch lock), post-expiry 1x downgrade, a
mixed pre/post-expiry split across three epochs, and a claim-then-
withdraw integration that verifies TRAC reaches the user wallet after
the 15-day delay.
Tests inject `nodeEpochScorePerStake36` at specific epochs via the
hub-owner-privileged `setNodeEpochScorePerStake` setter (onlyContracts
admits `hub.owner()`) and pre-fund the StakingStorage vault with
`Token.mint(...)` so the node-stake bookkeeping stays consistent with
the vault balance. The reward formula follows the V8
`_prepareForStakeChange` scale: `reward = effStake * scorePerStake36
/ 1e18`, matching the Phase 5 stub semantics (Phase 11 replaces the
formula with the actual Paymaster-sourced epoch-pool flow).
Red phase: 9/11 fail with `NotImplemented` (the two gate-revert tests
pass because the `onlyConvictionNFT` guard fires before the
`revert("NotImplemented")`).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nto rewards, stubbed TRAC source) (green) Implements `StakingV10.claim`, the Phase 5 auto-compound flow that walks `[pos.lastClaimedEpoch + 1 .. currentEpoch - 1]`, computes per-epoch effective stake with the tier multiplier applied pre-expiry and flat post-expiry (plus the `rewards` bucket always at 1x), and banks the score-weighted sum into the `ConvictionStakingStorage` rewards bucket via `increaseRewards`. No wallet transfer — the claim auto-compounds into the NFT position and users must call `createWithdrawal` + `finalizeWithdrawal` (15-day delay) to extract TRAC. Reward formula mirrors V8's `_prepareForStakeChange` scale: reward_e = effStake_e * nodeEpochScorePerStake[e][identityId] / 1e18 where `nodeEpochScorePerStake` is read from `RandomSamplingStorage` at its native 1e36 scale (populated per-epoch by `RandomSampling.submitProof`). The per-epoch value is NOT a cumulative index — each epoch holds the score-per-stake earned within that single epoch, so no `[e] - [e-1]` subtraction is needed. Bookkeeping after the walk: (i) `increaseRewards(tokenId, amount)`, (ii) `setDelegatorStakeBase(id, bytes32(tokenId), raw + rewardsNew)` to honor the Phase 5 decision that the delegator base tracks the composite (withdrawal/redelegate precedent), (iii) `increaseNodeStake` + `increaseTotalStake` by `rewardTotal`, (iv) `setLastClaimedEpoch` to the walked boundary, (v) emit `RewardsClaimed(tokenId, amount)`. No-op contract: the function returns silently when the claim window is empty (fresh position, same epoch) or when all walked epochs had zero score (e.g., Paymaster-empty window) — the zero-score case still advances `lastClaimedEpoch` so future calls don't redo the walk, but emits nothing. `prepareForStakeChange(currentEpoch, identityId, bytes32(tokenId))` is called before the walk to settle the delegator's score-per-stake index, mirroring the stake/relock/redelegate/finalizeWithdrawal pattern — required because we are about to mutate the delegator base from `raw + rewardsOld` to `raw + rewardsNew`. **TRAC source is stubbed with a `TODO(Phase 11)` note.** Phase 5 claim does NOT pull TRAC from any external source; the `StakingStorage` vault must be pre-funded by Phase 11's Paymaster / EpochStorage integration. Unit tests simulate this via `Token.mint(stakingStorage, rewardTotal)`. If the vault is under-funded, claim bookkeeping still succeeds but a later `finalizeWithdrawal` would surface the discrepancy as a `transferStake` underflow. Green phase: all 11 new `claim` tests pass; full `DKGStakingConvictionNFT` suite (67 tests) green; `ConvictionStakingStorage` (57 tests) + `Staking` (229 tests) regression suites both green; hardhat deploy smoke test passes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a dedicated `describe('convertToNFT (→ StakingV10.convertToNFT)')`
block to `DKGStakingConvictionNFT.test.ts` covering the V8 → V10 migration
path:
- precondition: V8 rolling rewards must be 0 before convert
- precondition: V8 lastClaimedEpoch >= currentEpoch - 1 before convert
- `NoV8StakeToConvert` when the caller has no V8 address-keyed position
- wrapper-level tier rejects (2, 4, 13)
- `onlyConvictionNFT` gate on the direct StakingV10.convertToNFT entry
- happy path tier 12: V8 bucket drained, V10 position created with 6x
multiplier, NFT minted, totals invariant
- happy path tier 0: rest-state migration (1x, expiryEpoch == 0)
- cross-user isolation: Alice migrates, Bob's V8 stake is untouched
The tests use a `setupV8Stake` hub-owner shortcut that pokes
`StakingStorage` + `DelegatorsInfo` directly to land the V8 address-keyed
state (increaseDelegatorStakeBase + increaseNodeStake + increaseTotalStake
+ addDelegator + setHasEverDelegatedToNode + setLastClaimedEpoch). This
keeps the convertToNFT tests independent from V8 `Staking.stake` — which
already has full coverage in `Staking.test.ts` — and avoids the reward
epoch baselining noise that a real V8 path would introduce.
Expected red: four tier/gate tests already pass on the scaffold
(wrapper-layer rejects fire before StakingV10 is ever called), six
precondition/happy-path tests fail with "NotImplemented" or a missing
custom-error surface until the implementation lands in the next commit.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…th (green)
Implements `StakingV10.convertToNFT`, the one-way V8 address-keyed →
V10 NFT-backed migration entry:
1. Precondition: V8 rolling rewards == 0 AND
`lastClaimedEpoch >= currentEpoch - 1`. The Phase 5 scope
intentionally keeps StakingV10 out of the V8 reward-distribution
path — the user must run `Staking.claimDelegatorRewards` for
every unclaimed epoch BEFORE invoking convertToNFT. Reverts
`V8StakeNotFullyClaimed`.
2. Settle V8 score-per-stake indices at the current epoch for the
V8 `keccak256(abi.encodePacked(staker))` delegator key via
`Staking.prepareForStakeChange`. Required BEFORE the V8 bucket
drain so the score cursor is captured against the V8 key.
3. Read V8 `delegatorStakeBase`. Revert `NoV8StakeToConvert` if 0
— distinct from the precondition revert so the UI can
distinguish "nothing to migrate" from "claim V8 rewards first".
4. Zero the V8 key: `setDelegatorStakeBase(0)` (triggers
`_updateDelegatorActivity` → removes from `delegatorNodes[v8Key]`
and decrements `delegatorCount`) + `decreaseNodeStake` +
`decreaseTotalStake` + `delegatorsInfo.removeDelegator`.
5. NFT already minted by the wrapper (Phase 4 precedent) — no
action needed here.
6. Create the V10 position:
- `Staking.prepareForStakeChange` on the fresh
`bytes32(tokenId)` V10 delegator key.
- `setDelegatorStakeBase(amount)` on the V10 key.
- `increaseNodeStake` + `increaseTotalStake` — re-adds the
amount so the net migration is zero.
- `expectedMultiplier18(lockEpochs)` validates the tier
(reverts "Invalid lock" outside {0,1,3,6,12}). Tier 0
(permanent rest state) is a legitimate migration target.
- `convictionStorage.createPosition` writes the V10 position
with `lastClaimedEpoch = currentEpoch - 1` (Phase 2 default).
7. `ask.recalculateActiveSet()` — node stake delta is zero (V8
out, V10 in, same amount), so sharding table composition is
unchanged. The ask recalc is kept for symmetry with the other
entry points — a cheap no-op that keeps the call shape uniform
across all eight V10 entries.
The `onlyConvictionNFT` gate pins the caller to
`DKGStakingConvictionNFT`; `_convictionMultiplier` at the wrapper layer
already rejects tier 2/4/13 before forwarding. Tier 0 (no-lockup rest
state) is a legitimate migration target here — unlike `stake()` which
rejects lock == 0 for fresh mints, `convertToNFT` supports the migration
tier set {0,1,3,6,12} directly.
Adds a new custom error `NoV8StakeToConvert` and drops `view` on the
entry stub. Mutability is now `nonpayable`.
All 10 new `convertToNFT` tests pass. No regressions in the existing
77-test DKGStakingConvictionNFT suite, the 57-test
ConvictionStakingStorage suite, the 239-test Staking suite, or the
35-test DelegatorsInfo suite. Deploy smoke test green.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…sertion
Add a `transfer (accrued-interest model)` describe block to
`DKGStakingConvictionNFT.test.ts` that proves the pure-pass-through
`_update` override. 10 new tests (suite 77 → 87) covering:
- Position byte-identity across an ERC-721 transfer (raw, rewards,
lockEpochs, expiryEpoch, identityId, multiplier18, lastClaimedEpoch
all unchanged).
- Bob claims all 3 epochs of accrued rewards Alice held pre-transfer.
- Alice's claim / relock / redelegate / createWithdrawal /
cancelWithdrawal / finalizeWithdrawal all revert NotPositionOwner
post-transfer (full owner-rights surface).
- Second claim by Bob in the same epoch is a no-op (no RewardsClaimed).
- Gas ceiling: transferFrom < 100K (actual: 62,393 on Hardhat).
- Bob can redelegate after the transfer (owner rights fully migrated).
- safeTransferFrom to an EOA preserves accrued-interest semantics
(3-arg overload exercised; no receiver mock exists under
contracts/ so the callback branch is out of scope).
- NFT wrapper TRAC balance is always zero before and after transfer.
- Alice can mint a second NFT post-transfer — tokenIds stay monotonic
(Bob keeps #0, Alice gets #1).
No contract changes. `_update` is already the Phase 4 pass-through,
confirmed by NFT-rewrite commit 33d009d; this block is the end-to-end
proof the pass-through holds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ew API Replace the previous file — which tested legacy V8 `Staking.convictionMultiplier` pure math and `PublishingConvictionAccount` (publisher-side, both already covered by `v10-e2e-conviction.test.ts` Flow 1+2) — with a focused Phase 5 integration smoke test for the NFT-backed staking stack. The new file is a 5-test top-layer smoke covering: 1. Deploy + Hub wiring (NFT ↔ StakingV10 gate) 2. createConviction happy path (ERC-721 mint + SS + CSS state) 3. claim after 1-epoch advance (reward banks into rewards bucket) 4. Full withdrawal lifecycle (createWithdrawal → delay → finalize → burn) 5. convertToNFT V8→V10 migration (totals invariant, NFT minted) Exhaustive unit coverage remains in `test/unit/DKGStakingConvictionNFT.test.ts` (87 tests) and `test/unit/ConvictionStakingStorage.test.ts` (57 tests); this file is intentionally short and targets multi-contract wiring catches. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 5 review fix prep. Adds the V10-native pieces StakingV10's decrement-at-request withdrawal model needs: - `PendingWithdrawal` struct + `pendingWithdrawals[tokenId]` mapping — replaces the V8 `StakingStorage.withdrawals` slot for the V10 NFT path. V8 mapping is left untouched. - `createPendingWithdrawal` / `deletePendingWithdrawal` / `getPendingWithdrawal` CRUD with `amount != 0` as the existence sentinel and a `rewardsPortion <= amount` split invariant. - `increaseRaw` mutator — sign-flipped mirror of `decreaseRaw`. Used by `StakingV10.cancelWithdrawal` to roll back a pending raw share into the position when the user cancels. Storage version bumped 1.1.0 → 1.2.0. Tests cover `increaseRaw` round-trip identity vs. `decreaseRaw`, pre/post-expiry diff math, and the pending-withdrawal CRUD sentinel rules. Existing 57 tests still pass; new total 69. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…reateWithdrawal `ConvictionStakingStorage.createPosition` writes `expiryEpoch = currentEpoch + lockEpochs`, and `StakingV10.claim` treats `e < expiryEpoch` as boosted and `e >= expiryEpoch` as 1x — so `expiryEpoch` is the FIRST unboosted epoch, not the last boosted one. Two consumers had the strict `>` / `<=` form, which forced one extra epoch of wait past the actual boundary: - `relock`: `currentEpoch <= pos.expiryEpoch` → `currentEpoch < pos.expiryEpoch` - `createWithdrawal` raw branch: `currentEpoch > pos.expiryEpoch` → `currentEpoch >= pos.expiryEpoch` `pos.expiryEpoch == 0` (lock-0 rest state) still falls into the post-expiry branch on epoch 1, so the rest-state path is unaffected. All 87 DKGStakingConvictionNFT tests still pass; the existing tests use `advanceEpochs(2)` after a 1-epoch lock so they were never on the boundary epoch. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…V10-native pending storage Phase 5 review fix. Switches the V10 NFT-backed withdrawal flow from a "deduct-on-finalize" model to "decrement-at-request": - `createWithdrawal` immediately decrements `pos.raw` / `pos.rewards` and the `StakingStorage` delegator/node/total stake. The withdrawing TRAC stops earning rewards as soon as the user starts the timer. Pending withdrawals now live on the V10-native `ConvictionStakingStorage.pendingWithdrawals[tokenId]` slot — NOT the legacy `StakingStorage.withdrawals` map. The V8 mapping is untouched. - `cancelWithdrawal` is the symmetric inverse: restores rewards/raw via the rewardsPortion split captured on the pending struct, restores the delegator base composite, restores node/total stake, and re-inserts the node into the sharding table if needed. - `finalizeWithdrawal` now ONLY transfers TRAC + deletes the pending slot. No bucket decrement, no settle, no sharding-table work — all handled at create time. Other knock-on changes: - `redelegate` rejects when a pending exists. Under decrement-at-request the pending TRAC is anchored to `pos.identityId`, so moving the position would strand the cancel/finalize on the old node. Phase 5 UX answer: cancel first, then redelegate. The "mint a new NFT on cancel" cross-node restore is deferred to a follow-up PR. - `relock` needs no pending guard (under decrement-at-request the remaining `pos.raw` already excludes the pending amount). - `WithdrawalFinalized` event reshaped from `(tokenId, rawDraw, rewardsDraw)` → `(tokenId, amount)`. The split is now stored on the pending struct's `rewardsPortion` field. Test surface: createWithdrawal/cancelWithdrawal/finalizeWithdrawal flipped from "withdrawals stored in StakingStorage.withdrawals" to "pending stored in ConvictionStakingStorage.pendingWithdrawals", with new assertions for the immediate decrement at create time and the symmetric restore at cancel time. Added boundary tests for relock and createWithdrawal at exactly `currentEpoch == pos.expiryEpoch`, and a new redelegate guard test. Total: 87 → 92. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
`StakingV10.claim` accumulates `rewardTotal` in uint256 inside the per- epoch walk loop, then narrows to uint96 four times for the `increaseRewards` / `setDelegatorStakeBase` / `increaseNodeStake` / `increaseTotalStake` storage calls. A pathological per-epoch score injection or a long enough walk window could push the accumulator over `type(uint96).max` (~7.92e28), at which point the narrowing would silently truncate the reward. Add an explicit `RewardOverflow` revert immediately after the walker loop, before the first cast. New unit test exercises the path with a score-per-stake injection sized to push the stub formula past 2^96. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the existing TODO(Phase 11) note in StakingV10.claim with a WARNING block that makes the deployment dependency explicit: - Phase 11 MUST land before ANY testnet/mainnet deployment — claim() pre-credits reward TRAC into the StakingStorage vault accounting (nodeStake / totalStake / delegatorStakeBase) ahead of the actual TRAC arriving. Without Phase 11's Paymaster wiring, finalizeWithdrawal would underflow the vault and break the totalStake invariant. - The Phase 5 unit-test fixture pre-funds the vault via Token.mint as a stand-in for the Phase 11 flow. Pure docs change. No behavioral shift. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ition mutator Change existence guard from `raw > 0` to `identityId != 0` on both `setLastClaimedEpoch` and `deletePosition` in ConvictionStakingStorage. This fixes the split-bucket model where raw can be drained to 0 while the position is still alive (rewards-only state). Add two Phase 2 tests verifying the sentinel works on rewards-only positions for both mutators. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…alizeWithdrawal + TRAC-zero assertion Add `_requireFullyClaimed` internal guard to StakingV10.sol that reverts `UnclaimedEpochs` when `lastClaimedEpoch < currentEpoch - 1`. Called at the top of relock, redelegate, createWithdrawal, and cancelWithdrawal — forces users to claim() before any structural mutation so reward history is settled. Add deletePosition call in finalizeWithdrawal: when both raw and rewards are zero after finalize, clear the orphaned Position struct. Add 4 UnclaimedEpochs revert tests (one per guarded entry point). Add TRAC-zero assertion in claim happy path (total now 9). Add deletePosition verification in full-drain finalize test. Update all existing tests to call claim() before guarded operations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove ConvictionStakingStorage.json and StakingV10.json ABI files that were committed but should not be tracked (build artifacts). Scope audit found no drift in Staking.sol, RandomSampling.sol, or DelegatorsInfo.sol. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…st in claim Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…StakingStorage Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ake, replace all V8 settlement calls Add `_getEffectiveStakeAtEpoch` and `_prepareForStakeChangeV10` internal helpers to StakingV10.sol. The new settlement function is a direct adaptation of V8's `Staking._prepareForStakeChange` (lines 739-791) with one key change: it reads effective stake via the position's conviction multiplier instead of the raw `stakingStorage.getDelegatorStakeBase`. This ensures `sum(delegatorScores) == nodeScore` once M3 switches the denominator in `nodeEpochScorePerStake36` to effective node stake. Replaced 8 `staking.prepareForStakeChange(...)` call sites across stake/relock/redelegate/createWithdrawal/cancelWithdrawal/claim/convertToNFT with the new V10-local settlement. The V8 key settlement in `convertToNFT` (for the `keccak256(staker)` key) is intentionally kept as-is since it must use V8's raw-base semantics. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…elegatorConvictionMultiplier, fix NatSpec refs Delete two dead functions from Staking.sol: - convictionMultiplier(uint40): snap-down tier table with Phase 12c off-by-one bug (lockEpochs >= 2 returned 1.5x, but spec says lockEpochs == 1 should return 1.5x). Canonical multiplier now lives in DKGStakingConvictionNFT._convictionMultiplier (exact-match). - getDelegatorConvictionMultiplier(uint72, address): always returned SCALE18 regardless of inputs — real per-delegator multiplier comes from ConvictionStakingStorage positions. Update all NatSpec/comment references across contracts and tests to point at DKGStakingConvictionNFT._convictionMultiplier instead of the deleted Staking.convictionMultiplier. Delete two dead test cases from v10-e2e-conviction.test.ts that tested the deleted functions. Canonical tier tests live in test/unit/DKGStakingConvictionNFT.test.ts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rePerStake denominator The scorePerStake denominator in RandomSampling.submitProof now uses effective node stake (V8_raw + V10_effective) instead of raw nodeStake. This ensures V10 conviction multipliers are reflected in per-stake reward distribution while keeping inter-node scoring (calculateNodeScore) unchanged by design. Formula: effectiveNodeStake = rawNodeStake + nodeEffV10 - nodeV10Base When no V10 positions exist, nodeEffV10 == nodeV10Base == 0, so behavior is identical to the previous implementation. Also adds ConvictionStakingStorage as a deploy dependency for RandomSampling to prevent initialize() revert in test fixtures. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…r fee + EpochStorage Replace the Phase 5 score-only reward stub in StakingV10.claim() with the real two-step TRAC computation mirroring V8 Staking.sol:568-593: 1. delegatorScore18 = effStake * scorePerStake36 / 1e18 2. netNodeRewards = epochPool * nodeScore / allNodesScore - operatorFee (first claimer per (node, epoch) computes and caches via delegatorsInfo) 3. trac_reward = delegatorScore18 * netNodeRewards / nodeScore18 Changes: - Add EpochStorage import, state var, EPOCH_POOL_INDEX constant, and initialize() wiring (Hub key "EpochStorageV8") - Replace inner claim loop body with operator fee computation + caching - Remove TODO(Phase 11) / WARNING comment block - Update NatSpec to describe the real TRAC source and formula Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rd flywheel) Add v10-reward-flywheel.test.ts proving the complete V10 reward cycle: - V10 staker (1000 TRAC, 6x lock) + V8 staker (1000 TRAC, 1x) on same node - Injected scores and epoch pool simulate publish + proof pipeline - V10 claims via NFT.claim (auto-compound), V8 via claimDelegatorRewards - Conservation: 6/7 + 1/7 rewards sum to netNodeRewards (630 wei dust) - Vault invariant: Token.balanceOf(StakingStorage) >= totalStake - Operator fee banked correctly (10% and 0% variants) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…d rewards + vault invariant Update all claim tests to use the Phase 11 TRAC reward formula instead of the Phase 5 score-denominated stub. Each epoch's reward is now: delegatorScore18 = effStake * scorePerStake36 / 1e18 grossNodeRewards = epochPool * nodeScore18 / allNodesScore18 reward = delegatorScore18 * netNodeRewards / nodeScore18 Test infrastructure changes: - Add injectNodeEpochScore, injectAllNodesEpochScore, fundEpochPool, fundEpochPoolRange, injectEpochRewardState, injectMultiEpochRewardState helpers to both claim and transfer describe blocks - Update computeReward to the full 7-param TRAC formula with operator fee - Add assertVaultInvariant helper (vaultBalance >= totalStake) New tests: - 6x/1x proportionality: two positions on same node, verify 6/7 and 1/7 split of netNodeRewards with at most 1 wei rounding - Expiry boundary: 12-epoch lock, verify last pre-expiry epoch uses 6x and first post-expiry epoch uses 1x, with exact 6:1 ratio assertion Also updates the v10-conviction.test.ts integration smoke test to inject the full epoch reward state (nodeScore, allNodesScore, epochPool). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…x (V10 uses per-position queries) The old implementation called staking.getDelegatorConvictionMultiplier() which was deleted from StakingV10.sol (it was dead code that always returned 1x). V8 address-keyed stakers have no conviction multiplier. V10 per-position multipliers are queried by tokenId via ConvictionStakingStorage.getPosition(), not this address-keyed function. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ery epoch in claim loop The two early `continue` statements in the claim loop skipped the operator-fee-flag write for zero-score epochs. If a node had a rewardless epoch and only V10 claimers walked through it, the flag never got set, permanently blocking Profile.updateOperatorFee(). Restructure the loop to match V8's control flow: guard the TRAC conversion in an `if (delegatorScore18 > 0 && nodeScore18 > 0)` block and write the fee flag unconditionally after, for every epoch. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a regression test exercising the effective-node-stake denominator with one V8 staker (Carol 1000 TRAC, 1x) and two V10 positions (Alice 1000 TRAC @ 12-epoch lock = 6x, Bob 1000 TRAC @ 3-epoch lock = 2x) on the same node. Verifies reward shares match 6000:2000:1000 = 2/3:2/9:1/9 with conservation (total dust <= 10000 wei) and vault balance invariant. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…or fee flag gating Three review bugs fixed: 1. Claim loop gated operator fee computation on `delegatorScore18 > 0 && nodeScore18 > 0`. A dust claimant with zero delegatorScore could mark the fee as claimed without caching netNodeRewards, zeroing out all subsequent claimants. Fix: gate fee computation on `nodeScore18 > 0` only (matching V8 Staking.sol:567-600). 2. The 2-arg `_prepareForStakeChangeV10(epoch, tokenId)` read identityId from the position, but in stake() and convertToNFT() the position doesn't exist yet — baselined under node 0 instead of the target node. Fix: unified into single 3-arg function with mandatory identityId. 3. The 3-arg overload used the old position's effective stake when baselining the new node in redelegate() — granting free score on the destination. Fix: function compares pos.identityId == identityId to determine settlement (real effective stake) vs baseline (zero). Also syncs chain/abi with evm-module/abi for Staking.json, DKGStakingConvictionNFT.json, and RandomSampling.json. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Codex review of PR #202 flagged the V10 claim walker as mispricing historical epochs: `delegatorScore18 = effStake * scorePerStake36 / 1e18` uses the CURRENT post-mutation effective stake for all walk epochs, but mid-epoch mutations (stake/relock/redelegate/createWithdrawal/ cancelWithdrawal) during an epoch call `_prepareForStakeChangeV10`, which persists the pre-mutation contribution into `getEpochNodeDelegatorScore[e,id,key]` and bumps `getDelegatorLastSettledNodeEpochScorePerStake[e,id,key]` to the mid-epoch index. For the boundary epoch the naive walker then re-prices the settled portion at the wrong effStake. The `_requireFullyClaimed` guard (StakingV10.sol:283) ensures the only walk-window epoch that can have seen a mutation is the first (`claimFromEpoch = lastClaimedEpoch + 1`); all later epochs are pure (effStake constant post-mutation). Walker now composes: delegatorScore = settled + unsettled settled = epochNodeDelegatorScore[e, id, key] (pre-mutation) unsettled = effStake * (scorePerStake36 - lastSettledIndex36) / 1e18 For pure epochs, `settled == 0` and `lastSettledIndex36 == 0`, so the formula collapses to the original `effStake * scorePerStake36 / 1e18`. For boundary epochs only, the split prices the pre-mutation portion at the pre-mutation effStake (via `settled`) and the post-mutation portion at the current effStake (via `unsettled`). V10 uses `bytes32(tokenId)` as the delegator key, disjoint from the V8 `keccak256(address)` key space, so reading these fields does not interfere with any V8 claim path. All 290 existing staking/conviction unit tests + 186 V10 integration tests still pass. Made-with: Cursor
…ound Codex review of PR #202 flagged `StakingV10.claim()` as skipping the sharding-table and Ask maintenance that every other V10 entry point performs after a node-stake mutation. The claim's reward compound increases `nodeStake` / `totalStake`, which changes the node's weight in the active set and may cross `minimumStake` for a node that had been evicted. Mirror the `stake()` pattern (lines 436-442): * `ShardingTable.insertNode(id)` if the node is not yet in the sharding table and the new stake meets `minimumStake`. * `Ask.recalculateActiveSet()` unconditionally, so the updated node weight is reflected in the next sampling window. Reward is always an increase, so no eviction / removeNode path is needed — only the cross-above-minimum insert. All 290 existing staking/conviction unit tests + 186 V10 integration tests still pass. Made-with: Cursor
…tadata Codex review of PR #202 flagged `convertToNFT` as stranding current-epoch V8 sampling score on migration: `staking.prepareForStakeChange(currentEpoch, ...)` at line 1405 can settle a non-zero current-epoch V8 score into `DelegatorsInfo.epochNodeDelegatorScore[currentEpoch,id,v8Key]`, but the unconditional `delegatorsInfo.removeDelegator(id, staker)` that follows drops the V8 delegator registration that `Staking.claimDelegatorRewards` needs to pay out that score after the epoch closes. Mirror the V8 `_handleDelegatorRemovalOnZeroStake` pattern (`Staking.sol:884-898`): * Capture the `prepareForStakeChange` return (current-epoch delegator score) in `v8CurrentEpochScore18`. * If non-zero, call `setLastStakeHeldEpoch(id, staker, currentEpoch)` instead of `removeDelegator`. V8's `claimDelegatorRewards` post- claim cleanup (Staking.sol:608-618) will remove the delegator once all owed epochs are settled. * If zero, the original `removeDelegator` path is safe — no stranded score to claim. V8 TRAC is already drained (`setDelegatorStakeBase(id, v8Key, 0)` + `decreaseNodeStake` / `decreaseTotalStake`), so the retained DelegatorsInfo entry is purely a claim-lookup key, not a stake marker. All 290 existing staking/conviction unit tests + 186 V10 integration tests still pass. Made-with: Cursor
|
|
||
| /// @notice Mint a fresh NFT-backed staking position on `identityId` with | ||
| /// `amount` TRAC locked for `lockEpochs` epochs. | ||
| function createConviction( |
There was a problem hiding this comment.
🔴 Bug: This PR replaces the public stake(...) flow with createConviction(...) and moves the token pull into StakingV10, but packages/chain/src/evm-adapter.ts still approves/calls DKGStakingConvictionNFT.stake(...). After these ABI changes, stakeWithLock() will fail at runtime (missing method and wrong spender). Either keep a compatibility shim here or update the adapter/tests in the same PR to call createConviction and approve StakingV10.
| } catch { | ||
| return { multiplier: 1.0 }; | ||
| } | ||
| async getDelegatorConvictionMultiplier(_identityId: bigint, _delegator: string): Promise<{ multiplier: number }> { |
There was a problem hiding this comment.
🔴 Bug: Hard-coding multiplier: 1 silently breaks the existing ChainAdapter.getDelegatorConvictionMultiplier contract for locked V10 positions. Current callers/tests use this to observe post-lock conviction, so they will now get incorrect data instead of an explicit unsupported/deprecated signal. Either resolve the owner’s V10 position(s) and read the multiplier from ConvictionStakingStorage, or remove/deprecate this adapter method in the same PR.
Consolidates all V10 contract work (D26 time-accurate staking, code-review fixes, Paranet cleanup Groups A/B, ParanetV9Registry -> ContextGraphNameRegistry rename, orphan/migrator deletions, FairSwapJudge removal) with v10-rc's latest agent/CLI/sync changes so the branch can fast-forward to v10-rc and close out PRs #231, #240, and #270 in one pass. Conflict resolutions: - scripts/devnet.sh: took v10-rc's version. v10-rc's ef7016c retired the direct ParanetV9Registry.createParanetV9() bootstrap call entirely and switched to POST /api/context-graph/register on node 1 (the real V10 ContextGraphs.createContextGraph path). Our rename of that block to ContextGraphNameRegistry.claimName was obsolete vs v10-rc's cleaner architecture, so we accept v10-rc's version wholesale. The rename of the underlying Solidity contract + ABI is unaffected (those files are not touched by v10-rc). - packages/cli/src/api-client.ts: took v10-rc's version. v10-rc expanded ApiClient.query() with full memory-layer routing (view, graphSuffix, verifiedGraph, subGraphName, includeSharedMemory, agentAddress, assertionName, minTrust). v10-rc's opts surface is a strict superset of our branch's includeSharedMemory-only version. Verification: - No contract-level conflicts — v10-rc has zero contract changes that were missing from v10-pr97-spec-impl. - D26 staking, ContextGraphNameRegistry, StakingV10, and all Paranet/ orphan/FairSwapJudge deletions survive the merge. - PR #270's contract deletions were already in this branch; its relay list cleanup was superseded on v10-rc — #270 is now fully obsolete. Made-with: Cursor
…NFT, CSS, cross-contract Implements the V10 migration spec (dkgv10-spec PR OriginTrail#97) on top of PR OriginTrail#231's `Commit 1/7` CSS state additions. Takes the V10 stack from a partial CSS rewrite to a fully-migrated, self-consistent architecture. CSS is the canonical V10 stake store; `StakingStorage` becomes a frozen V8 archive + TRAC vault; V8 `Staking` and `DelegatorsInfo` are on the chopping block (unregistered at cutover). All stake reads that matter to reward accounting go through `ConvictionStakingStorage`. Commit 2 — StakingV10 + RandomSampling rewrites * Drop V8 Staking / DelegatorsInfo wiring (D3/D13/D17). V10-native `_prepareForStakeChangeV10` replaces the V8 cross-call. * `claim` walks the unclaimed window via D6 retroactive `migrationEpoch` and compounds rewards into `raw` through `cs.increaseRaw` + `cs.addCumulativeRewardsClaimed` (D19 — no separate rewards bucket). * All V10 stake writes go to CSS (`nodeStakeV10`, `totalStakeV10`, D15). `StakingStorage.nodes[id].stake` is not written by V10. * `RandomSampling.calculateNodeScore` reads `nodeStakeV10` from CSS (mandatory-migration model: no V8-only nodes post-cutover, so CSS is the canonical source). `submitProof` denominator likewise uses CSS `getNodeEffectiveStakeAtEpoch` and drops the now-redundant `nodeV10BaseStake` subtraction (D4). Commit 4 — D21 ephemeral NFTs + D23 CSS primitive * New CSS primitive `createNewPositionFromExisting(oldTokenId, newTokenId, newIdentityId, newLockEpochs, newMultiplier18)` atomically replaces a live position at a fresh tokenId while preserving `cumulativeRewardsClaimed`, `lastClaimedEpoch`, and `migrationEpoch`. Emits `PositionReplaced`. * `StakingV10.relock(oldTokenId, newTokenId, newLockEpochs)` and `redelegate(oldTokenId, newTokenId, newIdentityId)` rewritten to use the D23 primitive. * `DKGStakingConvictionNFT.relock` / `redelegate` mint `newTokenId` before the forward, burn `oldTokenId` after. Both return `newTokenId` so callers can track the continuation. Commit 5 — D7 + D8 + D11 migration primitives * Split `convertToNFT` into `selfConvertToNFT` and `adminConvertToNFT` (dual-path D7); shared `_convertToNFT` worker. * D8 — `_convertToNFT` absorbs BOTH V8 `stakeBase` and pending withdrawal amounts into the new V10 position's `raw`. V8 drain subtracts only `stakeBase` from node/total stake (pending was already off-stake at V8 request time). * NFT wrapper: new `selfMigrateV8`, `adminMigrateV8`, `adminMigrateV8Batch` (D11 batched rescue), and `finalizeMigrationBatch` (DAO closer — sets `v10LaunchEpoch`). Admin gate via new `onlyOwnerOrMultiSigOwner` modifier. * New `ConvertedFromV8` event shape: `(delegator, tokenId, identityId, stakeBaseAbsorbed, pendingAbsorbed, lockEpochs, isAdmin)`. Commit 6 — D13 / D18 cross-contract redirects + deploy scripts * `Profile.sol` — drops `DelegatorsInfo`, reads `isOperatorFeeClaimedForEpoch` from CSS. * `StakingKPI.sol` — drops `DelegatorsInfo`; `isNodeDelegator` guard dropped (redundant under V8-archive semantics); fee-claim flag + net-node rewards read from CSS. * `DKGStakingConvictionNFT.sol` — unused `DelegatorsInfo` import/ state removed. * Deploy scripts 021 / 054 / 055 — annotated. 055's dependency list trimmed to match V10 `initialize()`. 054 joined the `v10` tag. * Hub naming decision: `StakingV10` stays registered as `StakingV10`, NOT aliased to `Staking`. Rationale documented in 055: V10 staking is gated by `onlyConvictionNFT`, so aliasing would make V8-era integrations silently call gated V10 and fail opaquely; keeping slots distinct makes the break loud. Commit 7 — D14 + NatSpec * `WITHDRAWAL_DELAY = 0` on both `StakingV10` and the NFT wrapper. Conviction lock expiry IS the delay gate; a second address-timer on top is redundant. * Top-of-file NatSpec on `DKGStakingConvictionNFT` rewritten to match the final entry-point set and document D21/D23 burn-mint semantics. Known follow-up: * Test suite is red — signatures changed on `relock`/`redelegate`, `convertToNFT` renamed, event shapes shifted. Triage is the next task before PR review. * Live-chain cutover (script 998 — `Hub.removeContractByAddress` for V8 `Staking` + `DelegatorsInfo`) is ops-coordinated and not scripted in this diff. Refs: * Spec: OriginTrail/dkgv10-spec#97 * Stacked on: OriginTrail#231 Made-with: Cursor
Summary
Successor to #202 — takes
branarakic/v10-contracts-redesign(Zvonimir's V10 staking overhaul), rebases it onto currentv10-rc, and applies three targeted fixes for the four Codex 🔴 comments left onStakingV10.solduring the April-16 review.Original PR #202 is 187 commits behind
v10-rcand has a failed CI run on the retired matrix; merging as-is isn't viable. The rebase picked up two minor conflicts (dead-test assertions Zvonimir already intended to remove + anevm-adapter.tsshim he simplified) and two ABI-json auto-regens, all resolved cleanly.The three fixes
1. Walker uses settled + unsettled score per epoch (commit
2a55ffba)Codex flag: "walker uses live
Positionfor historical epochs —delegatorScore18 = effStake * scorePerStake36 / 1e18uses the CURRENT post-mutation effStake for all walk epochs, so a mid-epoch stake/relock/redelegate/withdrawal mutation mis-prices the pre-mutation portion of that epoch."_prepareForStakeChangeV10already persists the pre-mutation delegator contribution intoRandomSamplingStorage.epochNodeDelegatorScore[e,id,key]and bumpslastSettledNodeEpochScorePerStake[e,id,key]to the mid-epoch index when the position is mutated. The walker now reads both and composes:_requireFullyClaimed(StakingV10.sol:283) ensures only the first walk-window epoch (claimFromEpoch = lastClaimedEpoch + 1) can have seen a mutation — all later epochs are pure, sosettled == 0,lastSettledIndex == 0, and the formula collapses to the original walkereffStake * scorePerStake36 / 1e18.V10's
bytes32(tokenId)delegator key is disjoint from V8'skeccak256(address), so these reads don't interfere with any V8 claim path.2.
claim()runs sharding-table + Ask recalc post-compound (commita7adc54e)Codex flag: "
claim()restakesrewardTotalintonodeStake/totalStakebut skips the sharding-table + Ask recalc that every other stake-changing entry point performs."Mirrors
stake()(StakingV10.sol:436-442):shardingTable.insertNode(id)if not yet in the table and new stake ≥minimumStake.ask.recalculateActiveSet()unconditionally.Reward is always an increase, so no eviction path is needed — only the cross-above-minimum insert.
3.
convertToNFTpreserves V8 current-epoch score metadata (commitb7b391ed)Codex flag: "
prepareForStakeChange(currentEpoch, ...)just above can settle a non-zero current-epoch V8 score, but this unconditionalremoveDelegatordrops the only metadata that lets V8 claim that score later."Capture the
prepareForStakeChangereturn (current-epoch delegator score) and mirror V8's_handleDelegatorRemovalOnZeroStakepattern (Staking.sol:884-898):setLastStakeHeldEpoch(id, staker, currentEpoch)— V8'sclaimDelegatorRewardspost-claim cleanup will remove the delegator once the stranded epoch is claimed.removeDelegatorpath is safe.V8 TRAC is already drained (
setDelegatorStakeBase(v8Key, 0)+decreaseNodeStake/decreaseTotalStake), so the retained DelegatorsInfo entry is purely a claim-lookup key.Codex bug 4 — not a code bug
Codex flagged
claim()as growingnodeStake/totalStakepast the vault balance sinceclaim()doesn't move TRAC intoStakingStorage. After tracing the token flow:Staking.claim(Staking.sol:630-632) has the identical pattern — justincreaseNodeStake/increaseTotalStake/increaseDelegatorStakeBase, notransferFrom.StakingStoragevia publisher deposits (KnowledgeAssetsV10.sol:628doestoken.transferFrom(msg.sender, address(stakingStorage), tokenAmount), orchestrated byPaymaster.sol).claim()(StakingV10.sol:1094-1103) correctly documents the invariant.This is worth a Codex reply on the PR rather than a code change.
Testing
306 existing tests still pass across:
test/unit/Staking.test.tstest/unit/DelegatorsInfo.test.tstest/unit/DKGStakingConvictionNFT.test.tstest/unit/ConvictionStakingStorage.test.tstest/v10-conviction.test.tstest/v10-e2e-conviction.test.tstest/v10-reward-flywheel.test.ts(3-way mixed V10+V8 same-node flywheel)No regressions from the three fixes.
Test plan
minimumStake→ claim compound crosses threshold →shardingTableStorage.nodeExistsreturns true + active set reflects new weight.convertToNFTcalled mid-epoch with non-zero V8 score → after epoch close,Staking.claimDelegatorRewards(id, epoch, staker)still finds the delegator and pays out the stranded score.Relationship to #202
Once this merges to
v10-rc, #202 can be closed. Attribution of the bulk of the work (43 commits / 9k+ LOC / Phase 5/11 design) remains withzsculacvia the rebased commits; the three new fix commits are authored by me.Made with Cursor