@dev.sail.money/sailor 1.3.0-224 → 1.3.0-225
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/packages/cli/dist/index.cjs +694 -133
- package/packages/cli/dist/server.cjs +15884 -17950
- package/packages/sdk/dist/abis/ConfigurablePermission.d.ts +130 -0
- package/packages/sdk/dist/abis/ConfigurablePermission.d.ts.map +1 -0
- package/packages/sdk/dist/abis/ConfigurablePermission.js +79 -0
- package/packages/sdk/dist/abis/ConfigurablePermission.js.map +1 -0
- package/packages/sdk/dist/abis/index.d.ts +1 -0
- package/packages/sdk/dist/abis/index.d.ts.map +1 -1
- package/packages/sdk/dist/abis/index.js +1 -0
- package/packages/sdk/dist/abis/index.js.map +1 -1
- package/packages/sdk/dist/index.d.ts +1 -0
- package/packages/sdk/dist/index.d.ts.map +1 -1
- package/packages/sdk/dist/index.js +1 -0
- package/packages/sdk/dist/index.js.map +1 -1
- package/packages/sdk/dist/intelligence.d.ts +1 -1
- package/packages/sdk/dist/intelligence.js +1 -1
- package/packages/ui/dist/assets/{add-HBssdE_q.js → add-QIu9SGBU.js} +1 -1
- package/packages/ui/dist/assets/{all-wallets-kgnKzcam.js → all-wallets-2auxp9E5.js} +1 -1
- package/packages/ui/dist/assets/{app-store-C7gfCF4e.js → app-store-BVtVvk-F.js} +1 -1
- package/packages/ui/dist/assets/{apple-DmsD7XsU.js → apple-Dq7Ppvqv.js} +1 -1
- package/packages/ui/dist/assets/{arrow-bottom-B3CywkhY.js → arrow-bottom-DpT3R4Rr.js} +1 -1
- package/packages/ui/dist/assets/{arrow-bottom-circle-Cl21xaXp.js → arrow-bottom-circle-hkf9bWhW.js} +1 -1
- package/packages/ui/dist/assets/{arrow-left-Bp_pqRAk.js → arrow-left-3PhiE4SD.js} +1 -1
- package/packages/ui/dist/assets/{arrow-right-CRK93G5I.js → arrow-right-DUIXJHib.js} +1 -1
- package/packages/ui/dist/assets/{arrow-top-DyDJtPkJ.js → arrow-top-Cf--02la.js} +1 -1
- package/packages/ui/dist/assets/{bank-BthuR5Ky.js → bank-CkRSSgJy.js} +1 -1
- package/packages/ui/dist/assets/{basic-BSdElBI1.js → basic-D9puI79I.js} +1 -1
- package/packages/ui/dist/assets/{browser-BMfpXAJE.js → browser-BC8qo0Bh.js} +1 -1
- package/packages/ui/dist/assets/{card-BEvVqDzc.js → card-BYuGTxNc.js} +1 -1
- package/packages/ui/dist/assets/{ccip-BALQiE-q.js → ccip-BS_rjbk9.js} +1 -1
- package/packages/ui/dist/assets/{checkmark-BqYZHVCR.js → checkmark-C-MZeapS.js} +1 -1
- package/packages/ui/dist/assets/{checkmark-bold-DjNGbayE.js → checkmark-bold-Bs3hadsW.js} +1 -1
- package/packages/ui/dist/assets/{chevron-bottom-BEHbtNKD.js → chevron-bottom-C8mqtkD4.js} +1 -1
- package/packages/ui/dist/assets/{chevron-left-C__dvO-Z.js → chevron-left-Dxj-vVDJ.js} +1 -1
- package/packages/ui/dist/assets/{chevron-right-9abfT5a8.js → chevron-right-BoIrOTtL.js} +1 -1
- package/packages/ui/dist/assets/{chevron-top-B2VeyvGH.js → chevron-top-BlgziB_P.js} +1 -1
- package/packages/ui/dist/assets/{chrome-store-bPC0PXv4.js → chrome-store-sDKm_6es.js} +1 -1
- package/packages/ui/dist/assets/{clock-BolGF8wp.js → clock-BwfFymxk.js} +1 -1
- package/packages/ui/dist/assets/{close-Bo18lqFQ.js → close-BewD5_E0.js} +1 -1
- package/packages/ui/dist/assets/{coinPlaceholder-Bz-0VLbz.js → coinPlaceholder-DRPvf0NA.js} +1 -1
- package/packages/ui/dist/assets/{compass-CKKiFCLR.js → compass-BHPvd2dw.js} +1 -1
- package/packages/ui/dist/assets/{copy-CYIav_IY.js → copy-BMv4Y3KF.js} +1 -1
- package/packages/ui/dist/assets/{core-8m_0OoS0.js → core-BMNH6XFj.js} +3 -3
- package/packages/ui/dist/assets/cursor-ir0cjMUl.js +3 -0
- package/packages/ui/dist/assets/{cursor-transparent-DE9tExWo.js → cursor-transparent-DT8YBqHU.js} +1 -1
- package/packages/ui/dist/assets/{desktop-CWNluv_t.js → desktop-BeePUUqi.js} +1 -1
- package/packages/ui/dist/assets/{disconnect-Db6qWs6T.js → disconnect-C2qT29qY.js} +1 -1
- package/packages/ui/dist/assets/{discord-Cj6mVoBK.js → discord-I4ToELfU.js} +1 -1
- package/packages/ui/dist/assets/{etherscan-CCFLsPvo.js → etherscan-3psXC5Ef.js} +1 -1
- package/packages/ui/dist/assets/{events-BGQ3xBjc.js → events-Cdt6abbf.js} +1 -1
- package/packages/ui/dist/assets/{exclamation-triangle-DJWx46rU.js → exclamation-triangle-BCaWksGJ.js} +1 -1
- package/packages/ui/dist/assets/{extension-BtaF8Z9u.js → extension-DfYavvbY.js} +1 -1
- package/packages/ui/dist/assets/{external-link-5wYZl3Lh.js → external-link-B8uBskz7.js} +1 -1
- package/packages/ui/dist/assets/{facebook-BoGz1nMk.js → facebook-BSvrXy3w.js} +1 -1
- package/packages/ui/dist/assets/{fallback-B2d-ODZG.js → fallback-BSMYROva.js} +1 -1
- package/packages/ui/dist/assets/{farcaster-AsPTw3iB.js → farcaster-CcAuzEDi.js} +1 -1
- package/packages/ui/dist/assets/{filters-D1-1MQtc.js → filters-umCx-UFU.js} +1 -1
- package/packages/ui/dist/assets/{github-B1FrwkiF.js → github-CBNoBl67.js} +1 -1
- package/packages/ui/dist/assets/{google-DnrnTDFz.js → google-yyR1jNOC.js} +1 -1
- package/packages/ui/dist/assets/{help-circle-CI10dCJN.js → help-circle-C0WjKnIM.js} +1 -1
- package/packages/ui/dist/assets/{id-B627Euj0.js → id-BwIK6e_N.js} +1 -1
- package/packages/ui/dist/assets/{image-3O2KHUgY.js → image-D5oAo1Te.js} +1 -1
- package/packages/ui/dist/assets/{index-CgsvP_iE.js → index-B_Gc8xPp.js} +1 -1
- package/packages/ui/dist/assets/{index-DDe72Fsi.js → index-BaEe036r.js} +1 -1
- package/packages/ui/dist/assets/{index-DKCg39xU.js → index-BuqyrXYX.js} +1 -1
- package/packages/ui/dist/assets/index-CPrhuDfP.js +1782 -0
- package/packages/ui/dist/assets/{index-DhVzwxqf.js → index-DCWU1RuS.js} +1 -1
- package/packages/ui/dist/assets/{index-CeZ-3oZK.js → index-DMNSiu42.js} +3 -3
- package/packages/ui/dist/assets/{index.es-ClarUTak.js → index.es-DImsg59a.js} +4 -4
- package/packages/ui/dist/assets/{info-CMmDZSrf.js → info-CtcvKk8x.js} +1 -1
- package/packages/ui/dist/assets/{info-circle-ChsNDy1e.js → info-circle-BIm6XYyt.js} +1 -1
- package/packages/ui/dist/assets/{lightbulb-DDlT2eHQ.js → lightbulb-CMj21RIi.js} +1 -1
- package/packages/ui/dist/assets/{mail-DyTAk3JR.js → mail-C805tcaU.js} +1 -1
- package/packages/ui/dist/assets/{metamask-sdk-CoU-3z33.js → metamask-sdk-DDk4vkrl.js} +1 -1
- package/packages/ui/dist/assets/{mobile-B1ZTQh8S.js → mobile-BEii3NyZ.js} +1 -1
- package/packages/ui/dist/assets/{more-BNIrTkTn.js → more-RSnRt81X.js} +1 -1
- package/packages/ui/dist/assets/{network-placeholder-BRBk5JfG.js → network-placeholder-s9JaqzCQ.js} +1 -1
- package/packages/ui/dist/assets/{nftPlaceholder-xtnGz2yb.js → nftPlaceholder-Ccur2yVc.js} +1 -1
- package/packages/ui/dist/assets/{off-BfgRQBAE.js → off-DUtG4rlI.js} +1 -1
- package/packages/ui/dist/assets/{parseSignature-DPmCI80m.js → parseSignature-BbS8pxpB.js} +1 -1
- package/packages/ui/dist/assets/{play-store-BFjPDZvv.js → play-store-Bt2HvO3e.js} +1 -1
- package/packages/ui/dist/assets/{plus-C3dNKGgI.js → plus-BtEzQu0h.js} +1 -1
- package/packages/ui/dist/assets/{qr-code-BovNFnu8.js → qr-code-xbQZ-Nio.js} +1 -1
- package/packages/ui/dist/assets/{recycle-horizontal-DdcYEE6i.js → recycle-horizontal-d9pzE6vq.js} +1 -1
- package/packages/ui/dist/assets/{refresh-P6W_osB6.js → refresh-B3-2Sch0.js} +1 -1
- package/packages/ui/dist/assets/{reown-logo-DbesEx7U.js → reown-logo-DBkmQAX8.js} +1 -1
- package/packages/ui/dist/assets/{search-DMB73W4z.js → search-dgDA8Ou5.js} +1 -1
- package/packages/ui/dist/assets/{secp256k1-URVniZ2R.js → secp256k1-C4g4FCze.js} +1 -1
- package/packages/ui/dist/assets/{send-BUsSvitv.js → send-CEDXr4l7.js} +1 -1
- package/packages/ui/dist/assets/{swapHorizontal-CjaXc-eY.js → swapHorizontal-Co6_f8oH.js} +1 -1
- package/packages/ui/dist/assets/{swapHorizontalBold-DuNmGzT_.js → swapHorizontalBold-k4d22FNy.js} +1 -1
- package/packages/ui/dist/assets/{swapHorizontalMedium-CgZZIpJT.js → swapHorizontalMedium-Des6w_mz.js} +1 -1
- package/packages/ui/dist/assets/{swapHorizontalRoundedBold-BC1bVmtH.js → swapHorizontalRoundedBold-CVd4538j.js} +1 -1
- package/packages/ui/dist/assets/{swapVertical-PyYv49m9.js → swapVertical-Cn_eRfv8.js} +1 -1
- package/packages/ui/dist/assets/{telegram-DaCrUlL0.js → telegram-BE46RC0s.js} +1 -1
- package/packages/ui/dist/assets/{three-dots-D9tEmhOK.js → three-dots-CvqWsUWa.js} +1 -1
- package/packages/ui/dist/assets/{twitch-2m8_XY_5.js → twitch-9V7YXB0X.js} +1 -1
- package/packages/ui/dist/assets/{twitterIcon-BQ4fWtR1.js → twitterIcon-DbUxk6yk.js} +1 -1
- package/packages/ui/dist/assets/{verify-DJtFLNUq.js → verify-CQSnwVrh.js} +1 -1
- package/packages/ui/dist/assets/{verify-filled-B2cSEFoP.js → verify-filled-DG_hDTr8.js} +1 -1
- package/packages/ui/dist/assets/{w3m-modal-YBSDQDS1.js → w3m-modal-BsHGIrg7.js} +1 -1
- package/packages/ui/dist/assets/{wallet-C7Gv760P.js → wallet-lbVIReb_.js} +1 -1
- package/packages/ui/dist/assets/{wallet-placeholder-DtZ8_f4p.js → wallet-placeholder-C_ndmKAs.js} +1 -1
- package/packages/ui/dist/assets/{walletconnect-DsIIuQfT.js → walletconnect-ByDpG-E0.js} +1 -1
- package/packages/ui/dist/assets/{warning-circle-CTMOrGt2.js → warning-circle-hrS3II0d.js} +1 -1
- package/packages/ui/dist/assets/{x-BmL9CVvs.js → x-BPMn3x5J.js} +1 -1
- package/packages/ui/dist/index.html +1 -1
- package/templates/default/.agents/skills/sail-mandates/references/approvals.md +3 -3
- package/templates/default/.agents/skills/sail-swap-quote/SKILL.md +76 -0
- package/templates/default/.agents/skills/sail-template-approve-batch/SKILL.md +98 -0
- package/templates/default/.agents/skills/sail-template-borrow/SKILL.md +68 -0
- package/templates/default/.agents/skills/sail-template-deposit/SKILL.md +61 -0
- package/templates/default/.agents/skills/sail-template-swap/SKILL.md +207 -0
- package/templates/default/.agents/skills/sail-template-swap-no-oracle/SKILL.md +100 -0
- package/templates/default/.agents/skills/sail-template-transfer/SKILL.md +52 -0
- package/templates/default/.agents/skills/sail-template-withdraw/SKILL.md +50 -0
- package/templates/default/.agents/skills/sail-templates/SKILL.md +142 -0
- package/templates/default/.agents/skills/sail-templates/catalog.mjs +200 -0
- package/templates/default/.agents/skills/sail-templates/deployed.json +133 -0
- package/templates/default/.agents/skills/sail-templates/oracles/IOracle.sol +13 -0
- package/templates/default/.agents/skills/sail-templates/oracles/UniV3TwapOracle.sol +114 -0
- package/templates/default/.agents/skills/sail-templates/oracles/libs/FullMath.sol +62 -0
- package/templates/default/.agents/skills/sail-templates/oracles/libs/TickMath.sol +44 -0
- package/templates/default/.agents/skills/sail-templates/references/config-schemas.md +139 -0
- package/templates/default/.agents/skills/sail-templates/references/reuse-flow.md +139 -0
- package/templates/default/.agents/skills/sail-token-resolve/SKILL.md +174 -0
- package/templates/default/AGENTS.md +21 -2
- package/templates/default/scripts/quote-swap.mjs +211 -0
- package/templates/default/scripts/resolve-token.mjs +992 -0
- package/templates/default/scripts/shared-template-addr.mjs +110 -0
- package/packages/ui/dist/assets/cursor-D10zW2HG.js +0 -3
- package/packages/ui/dist/assets/index-ohx1EwWJ.js +0 -1789
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sail-template-swap-no-oracle
|
|
3
|
+
description: Gate an SMA's DEX swaps for tokens that have NO oracle by REUSING the shared SwapPermissionNoOracle singleton (Protocol/contracts/templates/SwapPermissionNoOracle.sol) — register + configure, no per-SMA deploy. Use for a bounded swap mandate on Uniswap V3/V3-02/V2 (and forks) where no manipulation-resistant price feed exists: router + token-in/out allowlists, a per-tx cap, recipient pinned to the SMA, and a per-pair live-pool "hallucination band". NOT manipulation-resistant — if the token HAS an oracle use sail-template-swap instead; for the LI.FI aggregator use sail-lifi-swap. Deployed on all 11 Sailor-bundled chains (recorded in sail-templates/deployed.json).
|
|
4
|
+
compatibility: A Sailor project (`@sail/sdk`, `sailor` CLI). Requires SwapPermissionNoOracle deployed on the target chain (recorded in sail-templates/deployed.json); run sail-templates first.
|
|
5
|
+
metadata:
|
|
6
|
+
workspace: sailor-harness
|
|
7
|
+
classification: generic
|
|
8
|
+
status: draft
|
|
9
|
+
origin: Protocol/contracts/templates/SwapPermissionNoOracle.sol
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# sail-template-swap-no-oracle — bounded DEX swap for oracle-less tokens
|
|
13
|
+
|
|
14
|
+
Reuse the shared **`SwapPermissionNoOracle`** singleton instead of authoring/deploying a swap
|
|
15
|
+
contract. Register its address on the SMA and `configure()` your routers, token allowlists, cap,
|
|
16
|
+
and a **reference pool per tradeable pair**. Family overview + flow:
|
|
17
|
+
[`sail-templates`](../sail-templates/SKILL.md). For the oracle-gated tier see
|
|
18
|
+
[`sail-template-swap`](../sail-template-swap/SKILL.md).
|
|
19
|
+
|
|
20
|
+
> ⚠️ **Pick the right tier.** This template provides **NO manipulation-resistant slippage
|
|
21
|
+
> protection**. Its price band reads a *single pool's live spot price*, which any party can move
|
|
22
|
+
> within the same transaction (flash-loan / sandwich / MEV). It only catches an **honest mistake**
|
|
23
|
+
> — a confused manager/agent quoting a wildly wrong number (a "hallucination guard"). If the token
|
|
24
|
+
> has an independent, freshness-checked price feed, use [`sail-template-swap`](../sail-template-swap/SKILL.md)
|
|
25
|
+
> instead. Only reach for this when no such oracle exists.
|
|
26
|
+
|
|
27
|
+
## What it enforces (per account, from source)
|
|
28
|
+
|
|
29
|
+
Supported selectors (any other ⇒ `false`) — identical decode region to `SwapPermission`:
|
|
30
|
+
|
|
31
|
+
| Selector | Function | Venue |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| `0x414bf389` | `exactInputSingle((tokenIn,tokenOut,fee,recipient,deadline,amountIn,amountOutMinimum,sqrtPriceLimitX96))` | Uniswap V3 SwapRouter |
|
|
34
|
+
| `0x04e45aaf` | `exactInputSingle((tokenIn,tokenOut,fee,recipient,amountIn,amountOutMinimum,sqrtPriceLimitX96))` | Uniswap V3 SwapRouter02 |
|
|
35
|
+
| `0x38ed1739` | `swapExactTokensForTokens(amountIn,amountOutMin,path[],to,deadline)` | Uniswap V2 Router |
|
|
36
|
+
|
|
37
|
+
Structural invariants: `target ∈ routers`; `tokenIn`/`path[0] ∈ tokensIn`; `tokenOut`/`path[last]
|
|
38
|
+
∈ tokensOut`; `recipient`/`to == SMA` (funds can't leave the account); `amountIn ≤ maxAmountPerTx`.
|
|
39
|
+
|
|
40
|
+
**Price band (the only difference from `SwapPermission`):** for the swap's `(tokenIn, tokenOut)`
|
|
41
|
+
pair, read the live spot price of the operator-named reference pool and require
|
|
42
|
+
`amountOutMin ≥ expectedOut × (10_000 − toleranceBps)/10_000` **and** `amountOutMin > 0`. Fails
|
|
43
|
+
closed (denies) if the pool is missing, unreadable, illiquid, doesn't price the pair, or the floor
|
|
44
|
+
truncates to zero.
|
|
45
|
+
|
|
46
|
+
> **V2 intermediate hops are NOT checked** — only `path[0]`/`path[last]`. Restrict to V3 or ensure
|
|
47
|
+
> any plausible intermediate token is acceptable. Does NOT cover Universal Router, Uniswap V4, or
|
|
48
|
+
> aggregators (opaque calldata).
|
|
49
|
+
|
|
50
|
+
## Config blob (authoritative — `config-schemas.md`)
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
abi.encode(address[] routers, address[] tokensIn, address[] tokensOut,
|
|
54
|
+
uint256 maxAmountPerTx, ReferencePool[] referencePools)
|
|
55
|
+
|
|
56
|
+
struct ReferencePool { address tokenIn; address tokenOut; address pool;
|
|
57
|
+
PoolKind kind /* 0=V2, 1=V3 */; uint256 toleranceBps; }
|
|
58
|
+
```
|
|
59
|
+
| Field | Notes |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `routers` | DEX routers the agent may call |
|
|
62
|
+
| `tokensIn` / `tokensOut` | sell-side / buy-side allowlists |
|
|
63
|
+
| `maxAmountPerTx` | per-swap cap, base units (e.g. `25_000_000` = 25 USDC) |
|
|
64
|
+
| `referencePools` | one per directional `(tokenIn, tokenOut)` pair — see coverage rule below |
|
|
65
|
+
| `ReferencePool.pool` | a V2 pair or V3 pool that actually prices the pair (validated at configure) |
|
|
66
|
+
| `ReferencePool.kind` | `0` = V2 (`getReserves`), `1` = V3 (`slot0`+`liquidity`) |
|
|
67
|
+
| `ReferencePool.toleranceBps` | band width, **≤ 5_000 (50%)** or configure reverts. e.g. `1_000` = 10% |
|
|
68
|
+
|
|
69
|
+
**Strict coverage rule:** `configure()` reverts `MissingReferencePool` unless every non-self
|
|
70
|
+
directional combination of `tokensIn × tokensOut` has a `ReferencePool`. The pool's
|
|
71
|
+
`token0()`/`token1()` must match the pair (orientation is precomputed at configure time), else it
|
|
72
|
+
reverts `PoolTokenMismatch`. `pool == 0` reverts `ZeroPool`.
|
|
73
|
+
|
|
74
|
+
Size the cap with `uniswap-v3-quote`. Verify the SDK encoder's param tuple matches the source blob
|
|
75
|
+
above before using it (see [`sail-templates`](../sail-templates/SKILL.md) notes).
|
|
76
|
+
|
|
77
|
+
## Steps
|
|
78
|
+
|
|
79
|
+
Register → configure → simulate → reconfigure mechanics (and the encoding gotcha) live in
|
|
80
|
+
[`sail-templates` reuse-flow](../sail-templates/references/reuse-flow.md) — follow it.
|
|
81
|
+
`sailor mandate attach` registers only; `configureDirect` (owner tx) is the half that makes the
|
|
82
|
+
permission live.
|
|
83
|
+
|
|
84
|
+
Template-specific bits:
|
|
85
|
+
|
|
86
|
+
- **Singleton:** `SwapPermissionNoOracle` — `node SKILLS/sail-templates/catalog.mjs --chain <id>`.
|
|
87
|
+
- **Spec to confirm:** sell/buy tokens, per-swap cap, router/fee tier, recipient = SMA, and for each
|
|
88
|
+
pair the reference pool + tolerance. **Explicitly confirm the no-oracle trade-off** with the user:
|
|
89
|
+
this is a hallucination guard, not slippage/manipulation protection.
|
|
90
|
+
- **Blob:** `abi.encode(routers[], tokensIn[], tokensOut[], maxAmountPerTx, ReferencePool[])` —
|
|
91
|
+
**flat params, no wrapper**; `ReferencePool{tokenIn, tokenOut, pool, kind, toleranceBps}`.
|
|
92
|
+
- **Simulate (mandatory — unaudited example):** an allowed swap passes; wrong recipient, over-cap,
|
|
93
|
+
disallowed token, and an `amountOutMin` below the pool-implied floor are rejected.
|
|
94
|
+
|
|
95
|
+
## Notes
|
|
96
|
+
- The band is **not** manipulation-resistant — re-state this whenever authorizing. Prefer
|
|
97
|
+
[`sail-template-swap`](../sail-template-swap/SKILL.md) wherever an oracle exists.
|
|
98
|
+
- `toleranceBps` is capped at 50% in source; a wider band is meaningless and rejected at configure.
|
|
99
|
+
- Unaudited example — step 4 is mandatory.
|
|
100
|
+
- Aggregator routing (opaque calldata) → [`sail-lifi-swap`](../sail-lifi-swap/SKILL.md).
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sail-template-transfer
|
|
3
|
+
description: Gate an SMA's ERC-20 transfers by REUSING the shared TransferPermission singleton (Protocol/contracts/templates/TransferPermission.sol) — register + configure, no per-SMA deploy. Use to let an agent move approved tokens, within a per-tx cap, only to a recipient allowlist (partner protocols, CEX deposit addrs, co-manager wallets). For returning funds to a single fixed Safe, prefer sail-template-withdraw. NOTE: `sailor mandate attach` only registers — you must also configure per-account (see steps).
|
|
4
|
+
compatibility: A Sailor project (`@sail/sdk`, `sailor` CLI). Requires TransferPermission deployed on the target chain (recorded in sail-templates/deployed.json); run sail-templates first.
|
|
5
|
+
metadata:
|
|
6
|
+
workspace: sailor-harness
|
|
7
|
+
classification: generic
|
|
8
|
+
status: draft
|
|
9
|
+
origin: Protocol/contracts/templates/TransferPermission.sol
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# sail-template-transfer — bounded transfer to an allowlist via the shared singleton
|
|
13
|
+
|
|
14
|
+
Reuse the shared **`TransferPermission`** singleton. Family overview + flow:
|
|
15
|
+
[`sail-templates`](../sail-templates/SKILL.md).
|
|
16
|
+
|
|
17
|
+
## What it enforces (per account, from source)
|
|
18
|
+
|
|
19
|
+
Selectors: `0xa9059cbb` `transfer`, `0x23b872dd` `transferFrom`. Invariants: `value == 0`;
|
|
20
|
+
`allowedRecipients`/`allowedTokens` must be non-empty with no zero addresses
|
|
21
|
+
(`EmptyAllowlist`/`ZeroAddress` revert at configure otherwise); `target (token) ∈
|
|
22
|
+
allowedTokens`; `to ∈ allowedRecipients`; `amount ≤ maxAmountPerTx`; **`transferFrom` requires
|
|
23
|
+
`from == SMA`** (cannot pull from third parties that approved the Safe). Max 50 entries per
|
|
24
|
+
allowlist.
|
|
25
|
+
|
|
26
|
+
## Config blob (authoritative — `config-schemas.md`)
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
abi.encode(address[] allowedRecipients, address[] allowedTokens, uint256 maxAmountPerTx)
|
|
30
|
+
```
|
|
31
|
+
| Field | Notes |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `allowedRecipients` | addresses the agent may send to (≤ 50) |
|
|
34
|
+
| `allowedTokens` | ERC-20 tokens the agent may move (≤ 50) |
|
|
35
|
+
| `maxAmountPerTx` | per-transfer cap, base units |
|
|
36
|
+
|
|
37
|
+
> Recipient/token allowlists are mutable via `reconfigure` (permissionSigner). In production
|
|
38
|
+
> use a multisig/timelock as the signer — it can widen the allowlist. Vet recipient contracts.
|
|
39
|
+
|
|
40
|
+
## Steps
|
|
41
|
+
|
|
42
|
+
Register → configure → simulate → reconfigure mechanics (and the encoding gotcha) live in
|
|
43
|
+
[`sail-templates` reuse-flow](../sail-templates/references/reuse-flow.md) — follow it.
|
|
44
|
+
`sailor mandate attach` registers only; `configureDirect` (owner tx) is the half that makes the
|
|
45
|
+
permission live. Template-specific bits:
|
|
46
|
+
|
|
47
|
+
- **Singleton:** `TransferPermission` — `node SKILLS/sail-templates/catalog.mjs --chain <id>`.
|
|
48
|
+
- **Spec to confirm:** recipients, tokens, cap.
|
|
49
|
+
- **Blob:** `abi.encode(allowedRecipients[], allowedTokens[], maxAmountPerTx)` — **flat params, no
|
|
50
|
+
wrapper**.
|
|
51
|
+
- **Simulate (mandatory — unaudited example):** allowed recipient/token within cap passes; off-list
|
|
52
|
+
recipient, wrong token, over-cap, or `transferFrom` with `from != SMA` is rejected.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sail-template-withdraw
|
|
3
|
+
description: Gate an SMA's ERC-20 withdrawals by REUSING the shared WithdrawPermission singleton (Protocol/contracts/templates/WithdrawPermission.sol) — register + configure, no per-SMA deploy. Use when the agent may move approved tokens, within a per-tx cap, only to ONE fixed recipient (typically the owner's Safe) for safe-to-safe consolidation. For a mutable multi-recipient allowlist, use sail-template-transfer instead. NOTE: `sailor mandate attach` only registers — you must also configure per-account (see steps).
|
|
4
|
+
compatibility: A Sailor project (`@sail/sdk`, `sailor` CLI). Requires WithdrawPermission deployed on the target chain (recorded in sail-templates/deployed.json); run sail-templates first.
|
|
5
|
+
metadata:
|
|
6
|
+
workspace: sailor-harness
|
|
7
|
+
classification: generic
|
|
8
|
+
status: draft
|
|
9
|
+
origin: Protocol/contracts/templates/WithdrawPermission.sol
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# sail-template-withdraw — bounded withdraw to a fixed recipient via the shared singleton
|
|
13
|
+
|
|
14
|
+
Reuse the shared **`WithdrawPermission`** singleton. Family overview + flow:
|
|
15
|
+
[`sail-templates`](../sail-templates/SKILL.md).
|
|
16
|
+
|
|
17
|
+
## What it enforces (per account, from source)
|
|
18
|
+
|
|
19
|
+
Selectors: `0xa9059cbb` `transfer`, `0x23b872dd` `transferFrom`. Invariants: `value == 0`;
|
|
20
|
+
`tokens` must be non-empty and `allowedRecipient` non-zero, with no zero-address tokens
|
|
21
|
+
(`EmptyAllowlist`/`ZeroAddress` revert at configure otherwise); `target (token) ∈ tokens`;
|
|
22
|
+
`to == allowedRecipient` (a single address from config); `amount ≤ maxAmountPerTx`;
|
|
23
|
+
**`transferFrom` requires `from == SMA`**.
|
|
24
|
+
|
|
25
|
+
> Single recipient per config. To change it, `reconfigure` with a new blob (no redeploy). This
|
|
26
|
+
> is the consolidation primitive — funds can only flow to the one approved address.
|
|
27
|
+
|
|
28
|
+
## Config blob (authoritative — `config-schemas.md`)
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
abi.encode(address[] tokens, address allowedRecipient, uint256 maxAmountPerTx)
|
|
32
|
+
```
|
|
33
|
+
| Field | Notes |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `tokens` | ERC-20 tokens the agent may move |
|
|
36
|
+
| `allowedRecipient` | the single destination (e.g. owner's Safe) |
|
|
37
|
+
| `maxAmountPerTx` | per-withdraw cap, base units |
|
|
38
|
+
|
|
39
|
+
## Steps
|
|
40
|
+
|
|
41
|
+
Register → configure → simulate → reconfigure mechanics (and the encoding gotcha) live in
|
|
42
|
+
[`sail-templates` reuse-flow](../sail-templates/references/reuse-flow.md) — follow it.
|
|
43
|
+
`sailor mandate attach` registers only; `configureDirect` (owner tx) is the half that makes the
|
|
44
|
+
permission live. Template-specific bits:
|
|
45
|
+
|
|
46
|
+
- **Singleton:** `WithdrawPermission` — `node SKILLS/sail-templates/catalog.mjs --chain <id>`.
|
|
47
|
+
- **Spec to confirm:** tokens, the single recipient, cap.
|
|
48
|
+
- **Blob:** `abi.encode(tokens[], allowedRecipient, maxAmountPerTx)` — **flat params, no wrapper**.
|
|
49
|
+
- **Simulate (mandatory — unaudited example):** withdraw to the recipient within cap passes; any
|
|
50
|
+
other `to`, wrong token, over-cap, or `transferFrom` with `from != SMA` is rejected.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sail-templates
|
|
3
|
+
description: Registry + reuse guide for Sail's shared permission templates (Protocol/contracts/templates). Load this when you need to know which permission primitives are available as reusable templates and want to gate an SMA's mandate by REUSING a configurable singleton — deploy once, then register + configure per SMA (no per-SMA deploy). Covers swap (oracle-gated and no-oracle), borrow, transfer, deposit, withdraw, and approve-and-call-batch. All seven are deployed today, at the SAME address on every supported chain (CREATE2); addresses live in deployed.json.
|
|
4
|
+
compatibility: Node 18+; a Sailor project (`@sail/sdk`, `sailor` CLI); read access to the workspace `Protocol/contracts/templates/` (or set SAIL_PROTOCOL_DIR).
|
|
5
|
+
metadata:
|
|
6
|
+
workspace: sailor-harness
|
|
7
|
+
classification: generic
|
|
8
|
+
status: draft
|
|
9
|
+
origin: Protocol/contracts/templates source contracts
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Sail shared permission templates — registry & reuse
|
|
13
|
+
|
|
14
|
+
`Protocol/contracts/templates/` holds seven **shared permission templates** — reusable
|
|
15
|
+
`IPermission` patterns covering the most common DeFi primitives. Each extends
|
|
16
|
+
`ConfigurablePermission`, which means it is a **configurable singleton**:
|
|
17
|
+
|
|
18
|
+
> Deploy the contract **once per chain**. Every SMA then reuses that single address via
|
|
19
|
+
> **register + configure** — its own routers / tokens / caps live in the singleton's
|
|
20
|
+
> `mapping(address => …)`. **No per-SMA deploy, no per-SMA audit, minimal gas.**
|
|
21
|
+
|
|
22
|
+
> **Deployment status (2026-07-01, by `0xB01dCE…815B6`):** **all seven** templates are live on
|
|
23
|
+
> every chain Sailor bundles — Ethereum, Base, Arbitrum, Optimism, Unichain, BSC, World Chain,
|
|
24
|
+
> HyperEVM, MegaETH, Base Sepolia, and Eth Sepolia (11 chains) — all bound to the current
|
|
25
|
+
> CREATE2 kernel `0x38b508756c976e876EFF05a29E731A4d348BA6ED`. This is a CREATE2 deploy with a
|
|
26
|
+
> global salt, so the kernel and every template address are **identical on every chain**. Live
|
|
27
|
+
> addresses are in [`deployed.json`](deployed.json) (keyed by chainId → contract name, though
|
|
28
|
+
> the value is the same for all 11 keys). This supersedes the prior 2026-06-09 deploy (kernel
|
|
29
|
+
> `0x02ABC18B65A328de2e749F56ba79ACF2718a6659`) — those addresses are dead.
|
|
30
|
+
|
|
31
|
+
> ⚠️ **Audit scope.** These are marked *"UNAUDITED EXAMPLE — NOT PART OF THE TRUSTED CORE"* in
|
|
32
|
+
> source. The kernel runs them safely (staticcall + gas cap + fail-closed) but does not verify
|
|
33
|
+
> their internal logic. Always `sailor mandate simulate` against your SMA before authorizing.
|
|
34
|
+
|
|
35
|
+
> ⚠️ **CRITICAL — "register" ≠ "configure" in the shipped CLI.** A shared template is useless
|
|
36
|
+
> until it is both **registered** on the kernel AND **configured** per-account. `sailor mandate
|
|
37
|
+
> attach` does **only the register** half (it submits `RegisterPermission` and nothing more). A
|
|
38
|
+
> registered-but-unconfigured singleton has `isConfigured == false` and the kernel **denies every
|
|
39
|
+
> call**. Run `sailor mandate configure` as the separate second step (see the reuse flow below).
|
|
40
|
+
|
|
41
|
+
## The seven templates
|
|
42
|
+
|
|
43
|
+
| Primitive | Contract | Deployed? | Skill |
|
|
44
|
+
|---|---|---|---|
|
|
45
|
+
| DEX swap (oracle band) | `SwapPermission` | ✅ 11 chains | [`sail-template-swap`](../sail-template-swap/SKILL.md) |
|
|
46
|
+
| DEX swap (no oracle) | `SwapPermissionNoOracle` | ✅ 11 chains | [`sail-template-swap-no-oracle`](../sail-template-swap-no-oracle/SKILL.md) |
|
|
47
|
+
| Lending borrow | `BorrowPermission` | ✅ 11 chains | [`sail-template-borrow`](../sail-template-borrow/SKILL.md) |
|
|
48
|
+
| Transfer (allowlist) | `TransferPermission` | ✅ 11 chains | [`sail-template-transfer`](../sail-template-transfer/SKILL.md) |
|
|
49
|
+
| Vault/lending deposit | `DepositPermission` | ✅ 11 chains | [`sail-template-deposit`](../sail-template-deposit/SKILL.md) |
|
|
50
|
+
| Withdraw to fixed addr | `WithdrawPermission` | ✅ 11 chains | [`sail-template-withdraw`](../sail-template-withdraw/SKILL.md) |
|
|
51
|
+
| Approve+call batch | `ApproveAndCallBatchPermission` | ✅ 11 chains | [`sail-template-approve-batch`](../sail-template-approve-batch/SKILL.md) |
|
|
52
|
+
|
|
53
|
+
Authoritative config tuples + enforced invariants (from source):
|
|
54
|
+
[references/config-schemas.md](references/config-schemas.md).
|
|
55
|
+
|
|
56
|
+
### One source of truth per concern
|
|
57
|
+
|
|
58
|
+
This hub is canonical for everything shared; each spoke skill carries only what's unique to its
|
|
59
|
+
primitive (selectors, invariants, config blob, probe cases) and defers the rest here. When
|
|
60
|
+
something changes, edit it in ONE place:
|
|
61
|
+
|
|
62
|
+
| Concern | Lives in | Spokes do |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| Which templates exist | source contracts, auto-detected by [`catalog.mjs`](catalog.mjs) | — |
|
|
65
|
+
| Deployed addresses (per chain) | [`deployed.json`](deployed.json) | link here |
|
|
66
|
+
| Register → configure → simulate flow | [references/reuse-flow.md](references/reuse-flow.md) | link here, don't restate |
|
|
67
|
+
| Config tuples + invariants | [references/config-schemas.md](references/config-schemas.md) | quote only their own tuple |
|
|
68
|
+
|
|
69
|
+
So a change to the CLI flow is a single edit to
|
|
70
|
+
`reuse-flow.md`, not seven spoke edits. The deliberately-fuller [`sail-template-swap`](../sail-template-swap/SKILL.md)
|
|
71
|
+
is the worked-example exemplar; the other spokes stay thin.
|
|
72
|
+
|
|
73
|
+
## Step 1 — see what exists / deployment status
|
|
74
|
+
|
|
75
|
+
The list is auto-detected from source; deployment status comes from `deployed.json`:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
node SKILLS/sail-templates/catalog.mjs # all templates
|
|
79
|
+
node SKILLS/sail-templates/catalog.mjs --chain 8453 # status on one chain
|
|
80
|
+
node SKILLS/sail-templates/catalog.mjs --json # machine-readable
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
(The catalog warns if a new source contract appears with no curated metadata — a signal to
|
|
84
|
+
add it here and write a skill.)
|
|
85
|
+
|
|
86
|
+
## Step 2 — the reuse flow (per template)
|
|
87
|
+
|
|
88
|
+
> The on-chain **intended** design is one signed call (`MandateFactory.attach`) that registers
|
|
89
|
+
> the address on the kernel and writes the per-account config together. The **shipped** CLI
|
|
90
|
+
> does not implement that combined call yet, so today these are two separate steps:
|
|
91
|
+
> `sailor mandate attach` (register) followed by `sailor mandate configure` (configure). The
|
|
92
|
+
> flow below describes what actually works now.
|
|
93
|
+
|
|
94
|
+
1. **Deploy once** (one-time, Protocol team / `DeploySharedTemplates`, CREATE2 — same address on
|
|
95
|
+
every chain) → record the address in `deployed.json`. (Already done for all seven.)
|
|
96
|
+
2. **Build the config blob** for your SMA (per-primitive skill gives the exact params).
|
|
97
|
+
3. **Register** the singleton address on the SMA's kernel (this does NOT configure it):
|
|
98
|
+
```bash
|
|
99
|
+
sailor mandate attach --address <DEPLOYED_ADDRESS> --sma <SMA> --label "<primitive>"
|
|
100
|
+
```
|
|
101
|
+
A comma-separated `--address` list registers several in one signature (`attachBatch`).
|
|
102
|
+
4. **Configure** the per-account bounds — this is the step that makes the permission actually
|
|
103
|
+
allow calls:
|
|
104
|
+
```bash
|
|
105
|
+
sailor mandate configure --address <DEPLOYED_ADDRESS> --sma <SMA> \
|
|
106
|
+
--template <TemplateName> --args-file ./config.json
|
|
107
|
+
```
|
|
108
|
+
This drives `configureDirect(account, <config blob>)` as an owner transaction (the owner is
|
|
109
|
+
the `permissionSigner`), pre-flighted with an `eth_call` before any signing or gas. See
|
|
110
|
+
[references/reuse-flow.md](references/reuse-flow.md) for the exact encoding gotcha and the
|
|
111
|
+
signing-station path.
|
|
112
|
+
5. **Simulate** off-chain (no gas) — prove allowed calls pass and bad ones fail:
|
|
113
|
+
```bash
|
|
114
|
+
sailor mandate simulate --address <DEPLOYED_ADDRESS> --sma <SMA> --calls ./probe.json
|
|
115
|
+
```
|
|
116
|
+
6. **Reconfigure** later (new caps / allowlists) via `configure`/`configureDirect` on the same
|
|
117
|
+
singleton — same address, no re-register. (On-chain `MandateFactory.reconfigure` is the
|
|
118
|
+
intended batch path for this.)
|
|
119
|
+
|
|
120
|
+
Full mechanics (EIP-712 sigs, `configure`/`configureDirect`, `attachBatch`, `replace`,
|
|
121
|
+
`detach`, and the register-vs-configure split): [references/reuse-flow.md](references/reuse-flow.md).
|
|
122
|
+
|
|
123
|
+
## When NOT to use these
|
|
124
|
+
|
|
125
|
+
No shared template covers: **perps** (GMX, Gains, Synthetix), **prediction markets** (Azuro,
|
|
126
|
+
Limitless), or the **LI.FI aggregator**. For those, author a bespoke `IPermission` via
|
|
127
|
+
`sailor mandate deploy` — see [`sail-lifi-swap`](../sail-lifi-swap/SKILL.md) for the
|
|
128
|
+
custody-bound aggregator pattern and [`sail-pendle`](../sail-pendle/SKILL.md) for Pendle.
|
|
129
|
+
|
|
130
|
+
## Notes
|
|
131
|
+
|
|
132
|
+
- The kernel and every template address are identical on every chain (CREATE2 global salt) —
|
|
133
|
+
but that pairing is generation-specific: a template address from one deploy generation (e.g.
|
|
134
|
+
the superseded 2026-06-09 deploy) is never valid against a different generation's kernel.
|
|
135
|
+
Always resolve both from the same `deployed.json` entry / SDK deployment.
|
|
136
|
+
- Caps/amounts are in **base units**. Size them with `sail-pyth-prices` / `uniswap-v3-quote`.
|
|
137
|
+
- The config encoder must match each contract's `_applyConfig` decode exactly — use the SDK
|
|
138
|
+
builder under `@sail/sdk/templates` **only after** verifying its param tuple equals the
|
|
139
|
+
source blob in `config-schemas.md` (the SDK builders track a previously-deployed set and may
|
|
140
|
+
differ from these source contracts).
|
|
141
|
+
- **CLI gap:** `sailor mandate attach` (register) and `sailor mandate configure` (configure) are
|
|
142
|
+
still separate commands — no combined one-step call ships yet. Always run both.
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// sail-templates / catalog.mjs
|
|
3
|
+
// Tracks the shared permission templates that live in
|
|
4
|
+
// Protocol/contracts/templates/*.sol
|
|
5
|
+
// These are configurable singletons (extend ConfigurablePermission): deploy once
|
|
6
|
+
// per chain, then every SMA reuses that address via REGISTER + CONFIGURE (attach only
|
|
7
|
+
// registers on the kernel; configure writes the per-account bounds — see SKILL.md).
|
|
8
|
+
// All seven are deployed (2026-07-01, CREATE2 global salt) — same address on every chain.
|
|
9
|
+
// Deployment status is read from deployed.json next to this file.
|
|
10
|
+
//
|
|
11
|
+
// The template LIST is auto-detected from source (so it tracks additions/removals),
|
|
12
|
+
// the rich per-template detail is curated below, and deployment status is read from
|
|
13
|
+
// deployed.json.
|
|
14
|
+
//
|
|
15
|
+
// Usage:
|
|
16
|
+
// node catalog.mjs # human view, all detected templates
|
|
17
|
+
// node catalog.mjs --json # machine-readable
|
|
18
|
+
// node catalog.mjs --chain 8453 # deployment status for one chain
|
|
19
|
+
// node catalog.mjs --protocol /path/to/Protocol # override Protocol dir
|
|
20
|
+
|
|
21
|
+
import { readFileSync, readdirSync, existsSync } from "node:fs";
|
|
22
|
+
import { dirname, join, resolve } from "node:path";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
24
|
+
|
|
25
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
|
|
27
|
+
const CHAIN_NAMES = {
|
|
28
|
+
1: "Ethereum", 8453: "Base", 42161: "Arbitrum", 10: "Optimism", 130: "Unichain",
|
|
29
|
+
56: "BSC", 480: "World Chain", 999: "HyperEVM", 4326: "MegaETH",
|
|
30
|
+
84532: "Base Sepolia", 11155111: "Sepolia",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Curated detail, keyed by the Solidity contract name. Config blobs are the
|
|
34
|
+
// authoritative abi.encode tuples taken from each contract's `_applyConfig`.
|
|
35
|
+
const META = {
|
|
36
|
+
SwapPermission: {
|
|
37
|
+
primitive: "DEX swaps (Uniswap V3 / V3-02 / V2)",
|
|
38
|
+
skill: "sail-template-swap",
|
|
39
|
+
config: "(address[] routers, address[] tokensIn, address[] tokensOut, uint256 maxAmountPerTx, uint256 maxSlippageBps, address priceOracle, uint256 maxPriceAgeSec)",
|
|
40
|
+
},
|
|
41
|
+
SwapPermissionNoOracle: {
|
|
42
|
+
primitive: "DEX swaps for tokens with NO oracle — live-pool hallucination band (NOT manipulation-resistant)",
|
|
43
|
+
skill: "sail-template-swap-no-oracle",
|
|
44
|
+
config: "(address[] routers, address[] tokensIn, address[] tokensOut, uint256 maxAmountPerTx, ReferencePool[] referencePools) — ReferencePool{address tokenIn, address tokenOut, address pool, PoolKind kind (0=V2,1=V3), uint256 toleranceBps}",
|
|
45
|
+
},
|
|
46
|
+
BorrowPermission: {
|
|
47
|
+
primitive: "Lending borrows (Aave / Morpho / Compound) with LTV check",
|
|
48
|
+
skill: "sail-template-borrow",
|
|
49
|
+
config: "(address[] protocols, address[] assets, uint256 maxAmountPerTx, uint256 maxLtvBps, address collateralOracle, address borrowOracle, uint256 maxPriceAgeSec)",
|
|
50
|
+
},
|
|
51
|
+
TransferPermission: {
|
|
52
|
+
primitive: "ERC-20 transfers to a recipient allowlist (from == account)",
|
|
53
|
+
skill: "sail-template-transfer",
|
|
54
|
+
config: "(address[] allowedRecipients, address[] allowedTokens, uint256 maxAmountPerTx)",
|
|
55
|
+
},
|
|
56
|
+
DepositPermission: {
|
|
57
|
+
primitive: "Vault / lending deposits (ERC-4626 + Aave v2/v3)",
|
|
58
|
+
skill: "sail-template-deposit",
|
|
59
|
+
config: "(address[] targets, address[] tokens, uint256 maxAmountPerTx)",
|
|
60
|
+
},
|
|
61
|
+
WithdrawPermission: {
|
|
62
|
+
primitive: "ERC-20 withdrawals to a single fixed recipient",
|
|
63
|
+
skill: "sail-template-withdraw",
|
|
64
|
+
config: "(address[] tokens, address allowedRecipient, uint256 maxAmountPerTx)",
|
|
65
|
+
},
|
|
66
|
+
ApproveAndCallBatchPermission: {
|
|
67
|
+
primitive: "Atomic approve / consuming-call / reset-to-zero batch",
|
|
68
|
+
skill: "sail-template-approve-batch",
|
|
69
|
+
config: "Config{ address[] tokens, address[] spenders, ConsumingPair[] consumingPairs /* (address target, bytes4 selector) */, uint256[] maxApprovalAmounts, bool requireAmountMatch, bool allowUnconstrainedRecipient /* default false = recipient pinned to account; true = opt out */ }",
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function argValue(flag) {
|
|
74
|
+
const i = process.argv.indexOf(flag);
|
|
75
|
+
return i >= 0 ? process.argv[i + 1] : null;
|
|
76
|
+
}
|
|
77
|
+
const hasFlag = (flag) => process.argv.includes(flag);
|
|
78
|
+
|
|
79
|
+
function findProtocolDir() {
|
|
80
|
+
const fromArg = argValue("--protocol");
|
|
81
|
+
if (fromArg) return resolve(fromArg);
|
|
82
|
+
if (process.env.SAIL_PROTOCOL_DIR) return resolve(process.env.SAIL_PROTOCOL_DIR);
|
|
83
|
+
for (const start of [process.cwd(), HERE]) {
|
|
84
|
+
let dir = start;
|
|
85
|
+
for (let i = 0; i < 8; i++) {
|
|
86
|
+
if (existsSync(join(dir, "Protocol", "contracts", "templates"))) return join(dir, "Protocol");
|
|
87
|
+
const parent = dirname(dir);
|
|
88
|
+
if (parent === dir) break;
|
|
89
|
+
dir = parent;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Detect concrete templates from source: `contract X is ... ConfigurablePermission ...`
|
|
96
|
+
// (skips `abstract contract`, e.g. the ConfigurablePermission base itself).
|
|
97
|
+
function detectTemplates(protocolDir) {
|
|
98
|
+
const dir = join(protocolDir, "contracts", "templates");
|
|
99
|
+
const out = [];
|
|
100
|
+
for (const file of readdirSync(dir).filter((f) => f.endsWith(".sol"))) {
|
|
101
|
+
const src = readFileSync(join(dir, file), "utf8");
|
|
102
|
+
const m = src.match(/^\s*contract\s+(\w+)\s+is\s+([^{]+)\{/m);
|
|
103
|
+
if (!m) continue;
|
|
104
|
+
if (!/ConfigurablePermission/.test(m[2])) continue;
|
|
105
|
+
out.push({ name: m[1], file: `contracts/templates/${file}` });
|
|
106
|
+
}
|
|
107
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function loadDeployed() {
|
|
111
|
+
const path = join(HERE, "deployed.json");
|
|
112
|
+
if (!existsSync(path)) return { chains: {} };
|
|
113
|
+
try {
|
|
114
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
115
|
+
} catch {
|
|
116
|
+
return { chains: {} };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function statusFor(deployed, name) {
|
|
121
|
+
const rows = [];
|
|
122
|
+
for (const [chainId, map] of Object.entries(deployed.chains ?? {})) {
|
|
123
|
+
const addr = map?.[name];
|
|
124
|
+
rows.push({ chainId: Number(chainId), name: CHAIN_NAMES[chainId] ?? `chain ${chainId}`, address: addr ?? null });
|
|
125
|
+
}
|
|
126
|
+
return rows.sort((a, b) => a.chainId - b.chainId);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function main() {
|
|
130
|
+
const protocolDir = findProtocolDir();
|
|
131
|
+
if (!protocolDir) {
|
|
132
|
+
console.error("Could not locate Protocol/contracts/templates. Pass --protocol <path> or set SAIL_PROTOCOL_DIR.");
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
const templates = detectTemplates(protocolDir);
|
|
136
|
+
const deployed = loadDeployed();
|
|
137
|
+
const onlyChain = argValue("--chain");
|
|
138
|
+
|
|
139
|
+
const catalog = templates.map((t) => ({
|
|
140
|
+
...t,
|
|
141
|
+
...(META[t.name] ?? { primitive: "(uncurated — see source)", skill: null, config: "(see source)" }),
|
|
142
|
+
deployments: statusFor(deployed, t.name).filter((r) => !onlyChain || r.chainId === Number(onlyChain)),
|
|
143
|
+
}));
|
|
144
|
+
|
|
145
|
+
// IOracle adapters usable as `priceOracle` in SwapPermission config (deployed.json `oracles`).
|
|
146
|
+
const oracles = [];
|
|
147
|
+
for (const [chainId, byLabel] of Object.entries(deployed.oracles ?? {})) {
|
|
148
|
+
if (onlyChain && Number(chainId) !== Number(onlyChain)) continue;
|
|
149
|
+
for (const [label, o] of Object.entries(byLabel ?? {})) {
|
|
150
|
+
oracles.push({ chainId: Number(chainId), name: CHAIN_NAMES[chainId] ?? `chain ${chainId}`, label, ...o });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (hasFlag("--json")) {
|
|
155
|
+
console.log(JSON.stringify({ protocolDir, templates: catalog, oracles }, null, 2));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
console.log("Sail shared permission templates — source: Protocol/contracts/templates\n");
|
|
160
|
+
console.log("Model: configurable singletons. Deploy ONCE per chain, then every SMA reuses the");
|
|
161
|
+
console.log("address via register (`sailor mandate attach`) + configure (no per-SMA deploy).");
|
|
162
|
+
console.log("NOTE: `sailor mandate attach` registers ONLY — you must also configure per-account");
|
|
163
|
+
console.log("(configureDirect today); see SKILL.md / references/reuse-flow.md.\n");
|
|
164
|
+
const anyUncurated = catalog.some((t) => !META[t.name]);
|
|
165
|
+
for (const t of catalog) {
|
|
166
|
+
const flag = META[t.name] ? "" : " ⚠️ NEW/uncurated — add to META + write a skill";
|
|
167
|
+
console.log(`━━ ${t.name}${flag} ━━`);
|
|
168
|
+
console.log(` primitive: ${t.primitive}`);
|
|
169
|
+
console.log(` source: ${t.file}`);
|
|
170
|
+
if (t.skill) console.log(` skill: ${t.skill}`);
|
|
171
|
+
console.log(` config: ${t.config}`);
|
|
172
|
+
const live = t.deployments.filter((d) => d.address);
|
|
173
|
+
if (live.length) {
|
|
174
|
+
console.log(" deployed:");
|
|
175
|
+
for (const d of t.deployments) {
|
|
176
|
+
console.log(` ${d.name} (${d.chainId}): ${d.address ?? "— not yet deployed"}`);
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
console.log(" deployed: not yet on any tracked chain (record in deployed.json once deployed)");
|
|
180
|
+
}
|
|
181
|
+
console.log("");
|
|
182
|
+
}
|
|
183
|
+
if (oracles.length) {
|
|
184
|
+
console.log("━━ IOracle adapters (usable as SwapPermission `priceOracle`) ━━");
|
|
185
|
+
for (const o of oracles) {
|
|
186
|
+
const pairs = (o.pairs ?? []).map((p) => p.label ?? `${p.base}/${p.quote}`).join(", ");
|
|
187
|
+
console.log(` ${o.label} (${o.name} ${o.chainId}): ${o.address}${o.default ? " [default]" : ""}`);
|
|
188
|
+
console.log(` kind: ${o.kind ?? "?"}${o.twapWindowSec ? `, twap=${o.twapWindowSec}s` : ""}, decimals: ${o.priceDecimals ?? "?"}, pairs: ${pairs || "?"}`);
|
|
189
|
+
if (o.note) console.log(` ↳ ${o.note}`);
|
|
190
|
+
}
|
|
191
|
+
console.log("");
|
|
192
|
+
}
|
|
193
|
+
if (anyUncurated) {
|
|
194
|
+
console.log("⚠️ A source template has no curated metadata above — the source folder changed.");
|
|
195
|
+
}
|
|
196
|
+
console.log("⚠️ Shared templates are UNAUDITED EXAMPLES — not part of the trusted core.");
|
|
197
|
+
console.log(" Always `sailor mandate simulate` against your SMA before authorizing on-chain.");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
main();
|