Evaluation order
Everyexecute_pull runs the same checks in the same order, and stops at the first one that fails. Nothing is written to the policy and no CPI happens unless all of them pass.
Two further checks run before the policy logic itself: the signer must be the policy’s registered
agent (Unauthorized otherwise), and the receiver token account passed in must equal the destination being checked — without that second check a caller could satisfy the allowlist with one address while paying out to another.
How the window works
The window is time-based and fixed:- It is measured in seconds (
velocity_window_s) against the cluster’s Unix timestamp. It is not a slot count. - A window begins when the policy is created and covers
[window_start_ts, window_start_ts + velocity_window_s). - Each allowed pull adds its amount to
window_pulled. - The first pull at or after the end of the window resets
window_pulledto zero and starts a new window at that pull’s timestamp.
Worked example
A policy withper_call_cap = 50, velocity_cap = 200, velocity_window_s = 3600, created at t = 0:
Fixed windows allow a boundary burst
This is the standard property of fixed-window limiters, and it’s a deliberate simplicity tradeoff: the whole policy state is three integers, which keeps the evaluation logic small enough to verify exhaustively (see Security). If a 2× burst at a boundary is unacceptable for your use case, sizevelocity_cap as half of your true worst-case tolerance.
Three layers, three owners
A pull has to clear limits enforced by two different programs. Knowing which layer owns which limit tells you where to look when a pull is rejected.
The S&A delegation is the outer envelope — the most the agent can ever pull in total, ever — and Shugo’s caps shape how fast the agent can move within it.
Changing a policy mid-window
update_policy changes the caps, the window length, the agent, and the allowlist. It deliberately does not touch window_pulled or window_start_ts — those are runtime state, not configuration.
One consequence worth knowing: if you lower velocity_cap while a window is in progress, the amount already pulled in that window is immediately measured against the new cap. Tightening a limit can only make the policy stricter for the rest of the window, never looser.
Error reference
Shugo’s errors follow Anchor’s standard numbering: custom errors start at 6000 and increase in declaration order.
In transaction logs these appear as hex:
Paused is 0x1770 (6000), ExceedsPerCallCap is 0x1771, and so on. Errors from the underlying Subscriptions & Allowances program use its own separate range starting at 100 — for example 136 (0x88), StaleSubscriptionAuthority.(1).png?fit=max&auto=format&n=qzWg2aQwcoMXkrks&q=85&s=7bd9594b6c05a51ce5a41a26f4cb7e42)
(1).png?fit=max&auto=format&n=qzWg2aQwcoMXkrks&q=85&s=f7ad867613529ca7109dfe0b0dabd815)