A treasury with per-spender, per-token rolling spending limits. The owner funds a treasury and grants delegates a cap per token that refills every period (a rolling window, like a bank's daily limit). A delegate can pay out up to the remaining cap without the owner's keys, can never exceed the cap within a window, and the owner can revoke or resize the limit at any time. A Gnosis-Safe-allowance-module-style primitive for DAO ops and recurring vendors. MIT.
fund(token, amount)— anyone tops up the treasury (actual-received).setAllowance(spender, token, cap, period)— owner grants a rolling limit; a fresh grant starts a new window now.revokeAllowanceremoves it.spend(token, to, amount)— a delegate pays out from the treasury. The window rolls forward lazily on use (oncenow >= windowStart + period,spentresets), then the cap is enforced:spent + amount <= cap, and the treasury must hold the funds.ownerWithdraw(token, to, amount)— owner pulls treasury funds any time.remaining(spender, token)— how much the delegate can still spend right now (accounts for a rolled window).
- Rolling cap —
test_Spend_*prove a delegate can spend up to the cap, no more, and that the cap refills exactly one period later; over-spend revertsCapExceeded. Mutation-checked (removing the cap check fails these). - Window accounting invariant (fuzzed,
fail_on_revert=true, non-hollow, 12,800 calls) — across random grant / spend / warp / fund / withdraw, a delegate's in-windowspentnever exceeds itscap; the roll/reset logic can't be driven into an over-cap state. - Per-spender and per-token isolation, access control (owner-only grants/withdraws), reentrancy guards.
- The owner is fully trusted (they fund it and can withdraw all of it); delegates are trusted only up to their rolling cap — that's the point. There is no path for a delegate to exceed the cap or reach another token/spender's budget.
- Standard ERC-20 assumed (deposits are actual-received). Unaudited reference implementation.
forge test # 13 tests: rolling cap, refill, isolation, access control, window-accounting invariant