| name | sailor-mandates |
| description | The full permission-contract lifecycle — designing bounds with the user, authoring Solidity permissions, Foundry testing, deploying, simulating, and authorizing on the SMA, plus revoke/update/list and clone templates. Use when anything touches a permission contract or the mandate: writing or changing what the agent is allowed to do, deploying or registering permissions, or verifying them before authorization. |
sailor-mandates — the bespoke permission-contract lifecycle
The lifecycle is an ordered set of gates. The order is the correctness model — skipping a gate or reordering them is how funds get lost. Never authorize (register) anything that has not passed every earlier gate.
Gate 1 — Pin the strategy bounds with the user
Every constraint a strategy needs is one of two kinds, and you must tell the operator which is which so they sign knowing what is enforced where:
- Safety bounds — protect against loss or theft: amount caps, recipient allowlists, venue/router allowlists, slippage/min-out floors, LTV ceilings, and the like. These are enforced on-chain in a permission contract, default-ON. Dropping one requires an explicit, stated justification — never a silent omission. If a bound matters and it is not in a permission contract, it is not a bound.
- Strategy parameters — express how the strategy runs, not a theft/loss surface: cadence/frequency, schedule, rebalance timing. These live in agent logic by nature — permissions are stateless, and these are not safety surfaces (the safety bounds hold regardless of timing). If the operator states one (e.g. a cadence), it is a required agent-side guard that must be wired and confirmed before go-live — not optional, never silently dropped — but do not try to push it on-chain.
Enumerate from the operator's stated strategy and from what the protocol can express for the venues involved — do not work from a fixed checklist. Explain what each constraint protects against, classify it as a safety bound (on-chain) or a strategy parameter (agent-side), and say so to the operator.
Precedence. Operator intent and the strategy's stated bounds outrank any example. If the operator asks for a constraint an example omits, include it — never let an example's shape narrow the mandate below what the operator requested.
Examples are illustrations, not the supported set. Sail supports any token, venue, protocol, pool, or contract expressible as a permission — never treat an example's specific addresses as the only ones available. When the operator names something not in your examples, resolve it from authoritative sources (official docs, canonical lists, block explorers) and verify on-chain before binding a mandate to it. Caps are denominated in base units — a token decimals mismatch (USDC is 6, most ERC-20s are 18) silently mis-sizes every bound; confirm decimals on-chain before sizing caps.
Gate 2 — Enumerate approvals and pick the execution model
List every ERC-20 approve() the strategy implies — protocol permissions never cover approve(), so each needs explicit coverage. There are two non-mixable execution models — per-call (default; separate single-call dispatches) and atomic batch (one IBatchPermission for the whole approve → action → reset sequence). Which one to use, when, and how to author the coverage is owned by references/approvals.md — read it before writing any contract.
Gate 3 — Author the permission contracts
Permission contracts live in contracts/mandates/ — contracts/ is the project's one Foundry workspace, self-contained (its own foundry.toml, out/, vendored interfaces). The user authors, reviews, and owns them. Start from BoundedCallPermission.sol there — a neutral, protocol-agnostic IPermission scaffold (+ a Foundry test) — and extend it for the venue at hand. See references/authoring-patterns.md for the header discipline, named gotchas (selector verification, ABI drift, opaque-calldata boundaries), and the fail-closed authoring idiom. Never present a bespoke permission as audited.
- Implement
IPermission.evaluate(bytes txData, Context ctx) → bool (single-call) or IBatchPermission.evaluateBatch(Call[] calls, BatchContext ctx) → bool (batch). Both interfaces are vendored under contracts/.sail/contracts/interfaces/.
- Use the
SailCalldata library for bounded calldata decoding — slot-indexed reads after the 4-byte selector prevent silent truncation bugs.
- Bind recipients/beneficiaries to
ctx.account wherever the protocol exposes them — funds must route to the SMA.
- Selector correctness is life-or-death. Verify every selector against the venue's authoritative deployed ABI —
cast sig "fn(types…)" against the verified source — never from memory. A wrong selector fails closed (every legitimate call rejected) or worse, gates nothing. Precedents: authoring-patterns.md (Named gotchas).
- Pin every struct/ABI shape against the deployed contract, never the SDK. A venue's SDK can be newer than what's actually on-chain — see references/dark-reverts.md for the flagship example (a struct field the current Uniswap
v4-sdk had dropped that the deployed Base router still required) and the full diagnostic ladder for a call that reverts with no data at all.
- Integrating a venue beyond the shared templates (a fork with its own generations, Uniswap V4, any venue whose ABI or call-ordering isn't obvious from the docs) — references/venue-cookbook.md collects nine field-derived patterns (pool→factory→router verification, fork field-count mismatches, V4 PoolKey recovery, live-liquidity re-checks, viem tuple encoding, never swallowing a failed leg, V4 action ordering, multi-hop-as-chained-single-hops, chain-derived nonces) — read it before, not after, the first unexplained failure.
Prerequisite — Foundry. If forge is not found, you (the coding agent) can usually just run this install yourself rather than handing it back to the user:
curl -L https://foundry.paradigm.xyz | bash
foundryup
Gate 4 — Write and run Foundry tests BEFORE any deployment
contracts/test/BoundedCallPermission.t.sol is the scaffolded example — copy it for each permission you author. Write tests that call evaluate() (and evaluateBatch() for batch permissions) directly with calldata derived from the user's stated strategy:
- Accept cases: every call the strategy must make.
- Reject cases: out-of-bounds amounts, wrong tokens, wrong recipients, wrong selectors, unbound venues.
Run these from inside contracts/ — it's its own Foundry workspace, not the project root:
cd contracts
forge build
forge test
This gate comes before deployment because it is the only gate that exercises your boundary logic with full control of inputs, at zero cost. Do not deploy a permission whose tests do not pass.
Gate 5 — Deploy (deploy only — never register yet)
sailor mandate deploy --contract <Name> --sma <SMA> --json
Run this from the project root, not from inside contracts/ — unlike forge build/forge test above, sailor commands always run from the project root. The owner pays gas; the deployed address is read from the receipt and tracked in .sail/state/mandates.json. Add --build and the CLI runs forge build in contracts/ for you, then reads the artifact straight from contracts/out/ — the same tree Gate 4 just tested, never a stale copy.
When a strategy needs several permissions, deploy all of them first (don't register yet). Each deploy is its own owner-signed contract-creation transaction — those cannot be combined — but registering them is a single signature (Gate 7), so deploy the full set, then register it in one step.
Constructor args: --args '["0xToken","1000000"]' (JSON array, inline, bash) or --args-file args.json (any shell — required on PowerShell). Full per-shell quoting rules: references/constructor-args.md. Values are coerced to the constructor's ABI types (uint→bigint, etc.) and the array length is validated.
Gate 6 — Simulate against must-pass AND must-fail samples
evaluate() lives on the deployed contract, so simulate after deploy and before the irreversible authorization. Derive the calls from the bounds you just encoded — the probe pattern (one bound → one must-fail, plus one must-pass) with a worked example is in references/simulate-calls.md. The standard is the same one shared templates are held to: a must-fail probe PROVEN TO REJECT is what "passed" means — a happy-path-only run is not a passed simulation.
sailor mandate simulate --address <PermissionOrName> --sma <SMA> --calls calls.json --json
This is an off-chain eth_call — no gas, no signing. It reports what evaluate() returns per call, flags any target with no contract code on this chain (wrong or wrong-chain address), and checks whether each 4-byte selector actually routes on the target's bytecode. A mismatch between expect and the actual result exits non-zero. Zero mismatches required before proceeding. Simulate proves what the permission DOES; it does not guarantee it is correct.
If simulate passes clean but the live dispatch still reverts with no error data at all, the failure has moved from the permission to the venue call itself — see references/dark-reverts.md for the diagnostic ladder (trace it) rather than guessing at the encoding.
calls.json schema: references/calls-schema.md. How to design pass/fail cases: references/simulate-calls.md.
Batch permissions: simulate doesn't cover evaluateBatch() — see references/approvals.md (Model B) for how to verify one before registering.
Gate 7 — Register (authorize)
sailor mandate register --address <PermissionOrName> --sma <SMA> --json
sailor mandate register --address <addr1>,<addr2>,<addr3> --sma <SMA> --json
Only now is the permission live. The owner (mandate signer) signs in the browser; the agent submits the registration and pays gas plus any registration fee. Fund the agent wallet before registering, or this step fails with gas required exceeds allowance. The CLI verifies the signature came from the on-chain mandate signer — a wrong connected wallet is rejected. After confirmation it polls getPermissions() until the permissions appear.
When a strategy needs several permissions (e.g. a bounded-approve alongside the protocol permission), register them all at once by passing a comma-separated list of addresses — the registration approvals collapse to a single browser signature via the kernel's registerPermissions. The earlier per-contract deploy approvals (Gate 5) are separate and unavoidable. A single permission registers exactly as before with --address <one>.
Registration fee
Registering a permission charges a per-permission fee, paid on-chain by the agent wallet at the moment of registration. It is a governance-tunable protocol parameter — permissionRegistrationFee() on SailGovernance — so read it live from the chain at plan time; never hardcode or quote a rate (any number written here goes stale). The same flow surfaces whichever value the connected chain returns. The kernel requires msg.value >= fee × n for the registration and refunds any excess.
- A mandate is a SET of permissions, so a mandate of N permissions costs
N × fee at whatever the live fee is.
- When it's charged: once per permission, on registration (the
register / deploy-clone step). Already-registered permissions are not re-charged when you re-run sailor mandate sign. Revoking does not refund.
- Disclosure before signing:
sailor mandate prepare reads the live fee and records it in the draft, and sailor mandate sign prints Registration fee: <total> <native> (<N> permissions × <fee> <native>) before you confirm — where <native> is the chain's native token (ETH on most chains, BNB on BSC, HYPE on HyperEVM, …). The browser sign-time screen shows the same total.
- Preflight: before requesting the owner's signature, the agent wallet's native-token balance is checked against the total fee; an underfunded wallet fails early with
Insufficient <native> for the <X> <native> registration fee instead of an on-chain revert. Fund the agent wallet before registering.
- Recorded: each
permission_registered activity entry carries the fee actually paid (fee in wei, feeEth formatted), so Recent Activity shows the real cost.
The fee is read via readPermissionRegistrationFee() (from @sail.money/sailor/sdk) and applied as fee × N — the same number used for disclosure, the preflight, the tx value, and the activity record.
Maintenance
sailor mandate revoke --address <P> --sma <SMA> --json (or --all) — owner signs RevokePermissions in the browser (BLOCKS); agent submits. Revocations are recorded to the activity log; state/mandates.json keeps the historical record.
sailor mandate update --address <P> --name/--source-path/--artifact-path — fix tracked metadata.
sailor mandate list — everything deployed from this project, with registrations.
sailor mandate sign — reviews the permission set and reconciles against live on-chain getPermissions() before writing mandate.json; permissions revoked on-chain are excluded even if still in local state. --yes for non-interactive use.
sailor account rotate-signer — rotates the agent wallet and re-approves attached mandates (BLOCKS on browser); --reattach-only resumes after funding, --list shows known agent wallets.
Clone templates (deploy-clone)
sailor mandate deploy-clone --template boundedApprove --sma <SMA> --tokens <csv> --spenders <csv> --max <wei> --json deploys + registers an EIP-1167 clone of a published implementation in one transaction (owner signs RegisterPermission for the predicted clone address — BLOCKS; agent submits deployAndAttach — the on-chain function is named deployAndAttach; in Sailor and protocol vocabulary this operation is permission registration). The only template key is boundedApprove. Implementations come from the SDK deployment registry (standaloneTemplates) — that map is empty today (the seven shared templates you reuse via register/configure live in knownTemplates, a separate registry — see references/approvals.md), so deploy-clone errors with a clear message and you should write and deploy a bounded-approve permission with sailor mandate deploy instead — see references/authoring-patterns.md's BoundedErc20Approve worked example, one of the simplest bespoke permissions to write. Check availability with sailor mandate templates --json.
Next
Once this permission is deployed, simulate-verified (Gate 6), and registered (Gate 7), return to the mandate plan (sailor-mandate-planner) for the next permission. When every permission in the plan is deployed/registered/configured and simulate-verified, return there one last time — its Handoff step signs the mandate (sailor mandate sign) and hands off to Station 4.