Unstake queue & claim
Venice DIEM has a 1-day cooldown between initiateUnstake and unstake. Worse, every initiateUnstake call refreshes the cooldown end for the whole staked balance — so naively letting every user trigger their own unstake would mean one late user keeps everyone waiting.
DailyUnstakeQueue solves this by batching all unstake requests per UTC day into a single Venice cooldown window.
The three states a day can be in
Requested → Unstaking → Claimable| State | Meaning |
|---|---|
Requested | Users have queued unstakes for this day; not yet sent to Venice |
Unstaking | Venice's initiateUnstake called for the aggregate; cooldown running |
Claimable | Cooldown elapsed, Vault drained, users can pull their DIEM |
End-to-end timeline
Day D (your maturity)
You call Redeemer.redeemAndRequestUnstake(pt, amount, you)
→ Queue records: requestedByUser[D][you] += amount
→ Day D is now in state Requested
Day D+1 (any time after 00:00 UTC)
Anyone calls Queue.sync()
→ Queue scans days since nextSyncDay
→ Aggregates all Requested days into ONE Venice initiateUnstake
→ Day D becomes Unstaking, cooldown end = now + 1 day
Day D+2 (after cooldown end)
Anyone calls Queue.sync() again
→ Queue checks: cooldown elapsed?
→ Vault.claimUnstake() drains the bucket into Vault
→ Day D (and any others in this window) become Claimable
Day D+2 onward
You call Queue.claim(D, you)
→ Vault.transferOut(you, amount)
→ DIEM lands in your walletSo in the typical case: request today, claim ~36 hours later (depending on when sync runs).
Why daily aggregation
If the backend calls Queue.sync() once a day at 00:01 UTC:
- All unstake requests from the previous UTC day collapse into one Venice
initiateUnstake. - Cooldown refresh happens once, not per-user.
- Next day at 00:01 UTC, that batch is drained and marked Claimable; yesterday's batch starts its own cooldown.
If the backend misses a day, no problem — Queue.sync() is permissionless. The next person who interacts (any caller, any new request) drives the queue forward via the same _sync() logic.
Permissionless sync, request, claim
All three external functions trigger _sync() before doing their thing:
| Function | Caller | Trigger sync? |
|---|---|---|
Queue.sync() | anyone | yes (sole purpose) |
Queue.requestUnstake(amount, beneficiary) | Redeemer only | yes |
Queue.claim(day, beneficiary) | anyone | yes |
This means the queue self-heals at every interaction. The backend cron is convenience, not requirement.
Aggregating multiple missed days
If days D, D+1, and D+2 all had requests but nobody synced, then at D+3 the first sync() will:
- If there's an active window from before D, drain it first.
- Then collapse D, D+1, D+2 into a single Venice initiateUnstake (one cooldown).
- Mark them all
Unstakingwith the sameunstakeReadyAt.
This is normal. Each user still claims from their original request day (Queue.claim(D, you) works whether D was synced alone or with others).
Claim is per-day, per-beneficiary
You don't claim "everything you're owed" at once. You claim per (requestDay, beneficiary) pair:
queue.claim(D, you); // claims your share of day D
queue.claim(D2, you); // claims your share of day D2 (separately)If you have requests across multiple maturities, you'll make multiple claim calls — one per day. (A batched claimMany could be added later if there's demand.)
Bounded gas
The aggregation scan walks days from nextSyncDay to today linearly. If the backend is silent for a long time, this loop grows — eventually the sync call becomes expensive in gas. The protocol does not cap this loop in the current design (rationale: backend cron makes this essentially never matter; if it ever does, anyone can pay the gas to clear the backlog and restore normal operation).
If the protocol's inference backend dies completely for, e.g., 60 days, the next sync would cost extra gas to walk through. This is the only known operational corner case in the queue — it is not a money-loss path.
What if Venice changes cooldownDuration
Venice's admin can change cooldownDuration. The Queue handles this:
- Cooldown end is read live from Venice's
stakedInfos(not cached locally). If Venice extends cooldown mid-batch, the next sync will see the newcoolDownEndand just wait longer. - If Venice shortens cooldown, the active window can be drained sooner. The queue's
_windowReady()check usesblock.timestamp >= max(activeUnstakeReadyAt, liveCoolDownEnd), which gracefully handles both directions.
Emergency state
If the owner has armed and executed an emergency (see Safety), the Vault rejects further initiateUnstake and claimUnstake calls. The Queue will revert at the corresponding _sync() step.
This is by design — emergency mode is meant to freeze normal flows so the owner can drain to a rescue address. Users with pending requests in the queue at the time of emergency lose access to those funds unless they unwind through the 7-day arm window.