Indeed I see the merit in both the protocol-breaking issues and the “should this be on-chain at all?” question are legitimate, and I’ve taken both on board. Two things to share: a correction that I think reframes the whole problem, and a hardened version of the PR for completeness.
1. The actively-earning base is already correct on-chain
While reworking this I went back and checked the assumption the original PR was built on, and it turns out to be wrong in a way that matters.
Completed-but-not-yet-withdrawn delegation never leaves pool.tokensThawing. Nothing decrements tokensThawing when the 28-day thaw clock merely expires — only withdrawDelegated fulfilling the request does. So:
getDelegatedTokensAvailable() = pool.tokens - pool.tokensThawing
already excludes both in-period and completed-but-unwithdrawn delegation. It is the correct APR/APY denominator today, with no protocol change. The distortion described in the original post comes from consumers dividing rewards by pool.tokens (the raw reserve) instead of by getDelegatedTokensAvailable().
So you’re right — this is fundamentally an off-chain / consumer issue. The only genuinely new information a stored bucket would add is the split of tokensThawing into “still in its window” vs “thaw complete, awaiting withdrawal”, and that split is fully derivable from the existing ThawRequestCreated / ThawRequestFulfilled events (they already carry shares, thawingUntil and nonce). No new storage or events are strictly required.
My recommendation is to fix this off-chain — most cheaply by having dashboards divide by getDelegatedTokensAvailable(), and optionally surfacing the in-period/completed split in the network subgraph for nicer reporting. I’ve written up the derivation (a standalone reference plus a network-subgraph mapping sketch) and am happy to open a subgraph PR if that’s the preferred route.
2. Hardened PR, in case a protocol-level split is still wanted
If there’s appetite for exposing the split on-chain anyway, I’ve reworked the PR to be correct and complete. The redesign is materially smaller and resolves every issue raised:
- Storage layout (critical): new fields are strictly appended after
thawingNonce (and after the deprecated Delegation fields). No mid-struct insertion, no nonce reset, and the public DelegationPool struct is left untouched — the withdrawable amount is exposed via a derived view rather than a stored field.
- Slashing (critical): the redesign never moves tokens out of the thawing pool. “Releasing” only records, per delegator, how many thawing-pool shares are withdrawable; the underlying tokens stay in
tokensThawing/sharesThawing and remain fully slashable until withdrawn. Consequences:
- No
getDelegatedTokensAvailable underflow / pool-brick — the formula is unchanged.
- No slash-evasion — moving a request from “thawing” to “released” is purely bookkeeping;
slash() scales the same principal either way. There’s a test that delegates → releases → slashes and asserts the released delegator bears the full slash.
- A full slash bumps the existing
thawingNonce, which invalidates released shares (one extra line in slash()).
getDelegatedTokensAvailable / _undelegate: reverted to the original tokens - tokensThawing (already correct), so the line-934/946 inconsistency is gone.
- Reuse: release now uses
LinkedList.traverse with a dedicated callback rather than hand-rolled pointer surgery; per-request ThawRequestFulfilled events are emitted on the release path so indexers keyed on them don’t miss activity.
- Tests + CI: full Foundry coverage added (permissionless release, idempotency, no-op-when-unmatured, the slashability / no-evasion / full-slash cases, the withdrawable view). All unit tests pass; solhint (
--max-warnings=0) and prettier are clean; the stale DelegationInternal test helper that broke the build is fixed. Rebased onto current main.
The PR still needs the audit label from a maintainer to clear that CI check.
Either way, happy to go whichever direction the team prefers — the off-chain route is my recommendation, and the hardened PR is there if a protocol-level split is judged worth the (now much smaller) surface.