Skip to main content

Evaluation order

Every execute_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_pulled to zero and starts a new window at that pull’s timestamp.

Worked example

A policy with per_call_cap = 50, velocity_cap = 200, velocity_window_s = 3600, created at t = 0:

Fixed windows allow a boundary burst

Because the window resets completely rather than sliding, an agent can pull up to velocity_cap in the last moments of one window and up to velocity_cap again at the start of the next. The worst case across a window boundary is 2 × velocity_cap in a short span, even though no single window ever exceeds its cap.
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, size velocity_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.