WithdrawalCap: a stateful sliding-window rate limiter #
The first stateful primitive in Solanalib. Where WindowedDecay /
GrowthCurve / MonotoneSequence describe pure functions of time,
WithdrawalCap describes a resource that mutates: an accumulator
tracking how much has been withdrawn in the current time window, plus
the window-start timestamp that resets on interval elapse.
This is the per-reserve last line of defence against drain attacks in lending protocols.
Bug classes the theorems catch #
| Theorem | Bug class |
|---|---|
remaining_le_capacity | Saturating-arithmetic underflow on the remaining calculation |
tryAdd_preserves_invariant | current escaping above capacity after a successful add |
tryAdd_rejects_over_cap | Off-by-one on the boundary check |
interval_reset_idempotent | First add of a new interval forgetting to zero the accumulator |
Design notes #
- Invariant
current ≤ capacityis not embedded in the structure; it's aProp-valued predicate preserved bytryAdd. Keeps the structure a plain record (no proof obligation at construction). - Time arithmetic happens at the
.toNatlevel via theTimestampbridge — UInt64 subtraction would underflow if a caller passednow < windowStart; the.toNatform truncates to 0, which yields "not elapsed" — the safe default. intervalSeconds = 0models a disabled cap —tryAddis a no-op returningok cunchanged. Matches Kamino's on-chain behaviour.
A per-reserve withdrawal cap. intervalSeconds = 0 disables the cap.
- capacity : Timestamp
Maximum lamports addable per window.
- intervalSeconds : Timestamp
Window length in seconds.
0disables the cap. - current : Timestamp
Lamports added so far in the current window.
- windowStart : Timestamp
Wall-clock timestamp when the current window started.
Instances For
Equations
Equations
- One or more equations did not get rendered due to their size.
Instances For
Equations
- One or more equations did not get rendered due to their size.
Instances For
The invariant a well-formed WithdrawalCap should satisfy.
Instances For
Equations
Has the current window elapsed at time now? Computed at the
.toNat level so the comparison cannot underflow.
Equations
- c.IsElapsed now = (c.windowStart.toNat + c.intervalSeconds.toNat ≤ now.toNat)
Instances For
Equations
The maximum amount that can still be added in the current window.
When disabled, returns the largest representable Lamports (so callers
treating remaining as a hint won't refuse a valid add). When the
window has elapsed, returns the full capacity. Otherwise returns
capacity − current.
Equations
Instances For
The result of attempting to add to the cap.
- ok
(c' : WithdrawalCap)
: TryAdd
Success: the post-operation cap state.
- capReached : TryAdd
The add would have exceeded the cap; no state change.
Instances For
Equations
- One or more equations did not get rendered due to their size.
Instances For
Equations
Equations
- One or more equations did not get rendered due to their size.
- Solanalib.Finance.WithdrawalCap.instDecidableEqTryAdd.decEq (Solanalib.Finance.WithdrawalCap.TryAdd.ok c') Solanalib.Finance.WithdrawalCap.TryAdd.capReached = isFalse ⋯
- Solanalib.Finance.WithdrawalCap.instDecidableEqTryAdd.decEq Solanalib.Finance.WithdrawalCap.TryAdd.capReached (Solanalib.Finance.WithdrawalCap.TryAdd.ok c') = isFalse ⋯
- Solanalib.Finance.WithdrawalCap.instDecidableEqTryAdd.decEq Solanalib.Finance.WithdrawalCap.TryAdd.capReached Solanalib.Finance.WithdrawalCap.TryAdd.capReached = isTrue ⋯
Instances For
Try to add amount lamports to the cap at time now.
Three branches:
- Disabled (
intervalSeconds = 0): no-op, returnok c. - Window elapsed: reset to a fresh window and check whether
amountalone exceedscapacity. - Mid-window: check whether
current + amountexceedscapacity.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Theorems #
P1 — Bounded remaining. When the cap is enabled and the
invariant holds, remaining never exceeds capacity.
P2 — tryAdd preserves the invariant.
P3 — tryAdd rejects over-cap requests in mid-window.
P3' — tryAdd rejects over-cap requests at interval reset.
After a window elapses, an add still rejects if amount alone
exceeds capacity.
P4 — Interval-reset idempotence. After the window elapses,
a successful add zeroes the previous accumulator and starts a fresh
window: c'.current = amount and c'.windowStart = now.