Skip to content

DailyUnstakeQueue

Day-native unstake queue. Aggregates user unstake requests into Venice's single cooldown bucket.

contracts/src/platforms/venice/DailyUnstakeQueue.sol

Inherits

Ownable2Step, ReentrancyGuard

Constants

NameValue
ROLE_DELAY7 days
DAY1 days

Day status enum

solidity
enum DayStatus { None, Requested, Unstaking, Claimable }

A day transitions: NoneRequestedUnstakingClaimable.

Immutables

NameType
vaultVeniceStakingVault

Mutable state

NamePurpose
redeemerAuthorized to call requestUnstake
pendingRedeemer, redeemerEffectiveAtRotation buffer
_days[day]DayInfo { requested, unstakeReadyAt, status } per UTC day
requestedByUser[day][beneficiary]Per-user amounts
claimedByUser[day][beneficiary]One-shot claim flag
nextSyncDayEarliest day with unflushed Requested
activeUnstakeFromDay, activeUnstakeToDay, activeUnstakeReadyAtSnapshot of the currently active Venice unstake window

Wiring setters

solidity
function setRedeemerInitial(address redeemer_) external onlyOwner;
function proposeRedeemer(address redeemer_) external onlyOwner;
function activateRedeemer() external;
function cancelRedeemerChange() external onlyOwner;

Cross-checks: Redeemer(_).unstakeQueue() == address(this).

Redeemer-driven

solidity
function requestUnstake(uint256 amount, address beneficiary)
    external onlyRedeemer nonReentrant
    returns (uint64 requestDay);

Calls _sync() first (so the queue always self-heals before recording), then increments requested and requestedByUser for today's requestDay.

Permissionless sync

solidity
function sync() external nonReentrant;

The full sync logic:

  1. If there's an active unstake window:
    • Check whether it's ready (block.timestamp >= max(activeUnstakeReadyAt, liveCoolDownEnd)).
    • If yes, call vault.claimUnstake(), mark all days in [from, to] as Claimable, clear active window state.
    • If no, exit early (don't try to start a new one — Venice has only one cooldown bucket).
  2. Aggregate all Requested days from nextSyncDay to today - 1 into one vault.initiateUnstake(total).
  3. Mark them all Unstaking with the same unstakeReadyAt.

Claim

solidity
function claim(uint64 requestDay, address beneficiary)
    external nonReentrant
    returns (uint256 amount);

Permissionless: anyone can trigger the transfer, the funds always go to the recorded beneficiary. Calls _sync() first.

Views

solidity
function currentRequestDay() external view returns (uint64);
function dayInfo(uint64 requestDay) external view returns (DayInfo memory);
function claimableAmount(uint64 requestDay, address beneficiary) external view returns (uint256);

claimableAmount does not sync — it returns the current snapshot. UIs that need a guaranteed-fresh value should call sync() first.

Events

RedeemerProposed, RedeemerActivated, RedeemerChangeCancelled,
UnstakeRequested,
DaysEnteredUnstaking, DaysBecameClaimable,
Claimed

Errors

ZeroAmount, ZeroAddress, NotRedeemer,
DayNotClaimable, NothingToClaim, AlreadyClaimed,
AlreadySet, NoPending, TooEarly, WiringMismatch

Operational notes

  • Backend cron at 00:00:30 UTC should call sync() daily.
  • If the backend skips a day, the queue still works — the next interaction (any request or claim) drives sync.
  • The internal _commitClosedRequestedDays loop walks days linearly. Long silences make the next sync expensive in gas, but this is permissionless and a one-time cost.
  • Emergency mode in Vault makes vault.initiateUnstake/claimUnstake revert; _sync() then reverts, blocking all queue operations. By design — emergency mode freezes normal flows.

Released under the MIT License.