Core Functions¶
The five core functions that investors, the keeper, and governance interact with directly.
deposit¶
Access: Any address
No pause check — deposits are always accepted regardless of pause state. The vault can receive capital at any time.
Description: Accept USDC from a depositor, mint vault shares to the receiver. Capital stays idle in idleReserve until the operator deploys it via openPosition().
Behavior:
- Transfers
assetsUSDC frommsg.senderto the vault (requires prior ERC-20 approval) - Computes shares to mint:
The
if totalShares == 0: sharesToMint = assets × 1e12 // first-deposit initialisation rule else: sharesToMint = assets × totalShares / aggModeledNav() // floor division1e12factor on the first deposit aligns USDC (6 decimals) with shares (18 decimals). See Section 6.10 — Arithmetic Precision. - Adds
assetstoidleReserve - Mints
sharesToMinttoreceiver - Emits
Deposit(sender, receiver, assets, shares)
Decoupled from deployment
Deposit and capital deployment are intentionally decoupled. The vault can accept deposits at any time without needing a ready market. The operator deploys idle capital when conditions are suitable — not when deposits arrive.
requestWithdraw¶
Access: Any address (shares are pulled from msg.sender)
Description: Queue a withdrawal request. Returns a requestId for tracking. No immediate USDC payout — the request joins the FIFO queue and is processed by the keeper.
Behavior:
- Assigns
requestId = nextRequestId, then incrementsnextRequestId - Transfers
sharesfrommsg.senderto the vault (escrowed — held by vault from this point) - Stores
queue[requestId] = Request(owner=msg.sender, receiver, shares, timestamp=block.timestamp) - Emits
WithdrawRequested(requestId, owner, receiver, shares, timestamp) - Returns
requestId
Shares are locked at request time
Shares are transferred to the vault at the moment requestWithdraw is called — not at processing time. Once escrowed, they cannot be transferred, used as collateral, or queued again until the request is processed or cancelled.
processWithdrawals¶
Access: Keeper only — require(msg.sender == keeper)
Description: Process up to maxCount queued withdrawal requests in FIFO order. Computes exact average exit prices using the closed-form quadratic curve integral, pays USDC from idleReserve, sends fees to HouseBuffer, burns shares.
Execution sequence (12 steps per live request):
Preamble — executed once at the start of the call:
- Day roll: If
block.timestamp >= dayStart + 86400, setdayStart = block.timestamp,redeemedToday = 0, emitDayRolled(newDayStart, previousRedeemed). The day is defined as an 86400-second window fromdayStart— not a calendar day. Roll happens before any request is processed. - Pause check:
require(!paused())— revert the entire call if the system is paused.
Per-request loop (up to maxCount live requests):
- Cancelled slot skip: If
queue[nextProcessId].shares == 0(tombstone), incrementnextProcessIdand continue to the next slot. Does not count towardmaxCount. Does not revert.
For each live request:
- Compute
requestValue = request.shares * aggModeledNAV / totalShares - If
redeemedToday + requestValue > dailyCap: stop — do not process this or any subsequent request in this call - Compute fill values scaled to 1e18 fixed-point:
!!! danger "1e18 scaling is mandatory" Do NOT write
fillBefore = redeemedToday * 1e18 / dailyCap fillAfter = (redeemedToday + requestValue) * 1e18 / dailyCapfillBefore = redeemedToday / dailyCap. BothredeemedTodayanddailyCapare USDC (6 dec). Integer division produces0for all partial fills, silently collapsing the exit curve toaggMarketNAVon every withdrawal. The 1e18 scaling is required so the values are treated as fixed-point in[0, 1e18]. - Compute
curveNAV = _avgCurvePrice(fillBefore, fillAfter, aggModeledNAV, aggMarketNAV)— the curve-weighted aggregate vault NAV (USDC, 6 dec). This is a total vault valuation betweenaggMarketNAVandaggModeledNAV. It is not a per-share price. - Compute
exitValue = request.shares * curveNAV / totalShares— the USDC owed to this share count at the curve price. !!! danger "Divisor must be totalShares, not 1e18" Do NOT writerequest.shares * curveNAV / 1e18.curveNAVis a total vault valuation in USDC, not a per-share price. Dividing by1e18would produce a nonsensically small result. The correct divisor istotalShares. - Compute
fee = ceiling((exitValue * liquidityFeeBps) / 10000)— rounds up, favours HouseBuffer - Compute
payout = exitValue - fee require(idleReserve >= exitValue)— if this fails, revert the entire transaction. All state changes from this call are rolled back. The queue is unchanged. The keeper must retry after the reserve is restored. !!! danger "No skip-and-continue on reserve failure" Do NOT usetry/catchorif/elseto skip this request and continue the batch. Partial batch processing leaves accounting inconsistent. In normal operation thisrequireshould never trigger: step (2) ensures each request fits within the daily cap (2% of TVL) and the reserve target (15% of TVL) is always larger. A trigger indicates a serious system condition requiring investigation.idleReserve -= exitValue;redeemedToday += requestValue- Transfer
payoutUSDC torequest.receiver; transferfeeUSDC tohouseBuffer - Burn
request.sharesfrom vault escrow;delete queue[nextProcessId]; incrementnextProcessId; emitWithdrawProcessed(requestId, receiver, payout, fee, curveNAV)
Post-loop: If idleReserve < aggMarketNAV * reserveTargetBps / 10000 / 2, call houseBuffer.topUpReserve() and emit ReserveTopupRequested(amount).
Daily cap stops processing
When a request would exceed the daily cap, processing halts for the entire call. The request is not partially filled — it will be the first request processed on the next keeper call when redeemedToday resets.
cancelWithdraw¶
Access: Original request owner only — queue[requestId].owner
Description: Cancel a pending withdrawal request. Returns escrowed shares to the original requester.
Behavior:
require(msg.sender == queue[requestId].owner)require(queue[requestId].shares > 0)— revert if already cancelled or processed- Transfer
request.sharesback torequest.owner delete queue[requestId]— zeroes all struct fields includingsharesrequestStatus[requestId] = CANCELLED— marks the slot as cancelled in the status mapping- Emit
WithdrawCancelled(requestId, owner, sharesReturned)
Delete + status, not tombstone
cancelWithdraw uses delete rather than setting shares = 0 directly. The deleted struct has shares == 0, so processWithdrawals's skip logic (queue[pid].shares == 0) behaves identically. The requestStatus mapping provides an explicit CANCELLED state that getRequest uses to return a typed error rather than a zeroed struct.
Shares returned, not assets
Cancellation returns the original shares, not USDC. The USDC value of those shares may differ from when the request was submitted, as the share price reflects ongoing NAV changes.
setKeeper¶
Access: Governance only
Description: Rotate the keeper address. The new address immediately becomes the sole authorised caller of processWithdrawals.
Behavior:
require(msg.sender == governance)require(newKeeper != address(0))— zero address is not permitted; it would breakprocessWithdrawals- Sets
keeper = newKeeper - Emits
KeeperUpdated(oldKeeper, newKeeper)
setOperator¶
Access: Governance only
Description: Rotate the operator address. The new address immediately gains access to all position management functions. Using the same address as governance collapses to the original two-role model.
Behavior:
require(msg.sender == governance)require(newOperator != address(0))- Sets
operator = newOperator - Emits
OperatorUpdated(oldOperator, newOperator)
proposeGovernance¶
Access: Governance only
Description: Initiate a governance transfer. Sets pendingGovernance to the proposed address. Does not transfer governance immediately — the proposed address must call acceptGovernance() to complete the transfer.
Behavior:
require(msg.sender == governance)- Sets
pendingGovernance = newGovernance - Emits
GovernanceProposed(currentGovernance, proposedGovernance)
No enforced time delay
The two-step pattern prevents fat-finger transfers to wrong addresses, but there is no on-chain delay between proposal and acceptance. A compromised governance key can transfer in a single block if the attacker also controls the target address. Multisig governance mitigates this risk.
acceptGovernance¶
Access: pendingGovernance address only
Description: Complete a pending governance transfer. The proposed address calls this function to accept governance. Clears pendingGovernance on completion.
Behavior:
require(msg.sender == pendingGovernance)- Sets
governance = pendingGovernance - Clears
pendingGovernance = address(0) - Emits
GovernanceTransferred(oldGovernance, newGovernance)