@dev.sail.money/sailor 1.3.0-98 → 1.4.0-242

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.
Files changed (231) hide show
  1. package/AGENTS.md +7 -7
  2. package/LICENSE +1 -1
  3. package/README.md +64 -472
  4. package/package.json +11 -6
  5. package/packages/cli/README.md +1 -1
  6. package/packages/cli/dist/index.cjs +1469 -463
  7. package/packages/cli/dist/server.cjs +19796 -21737
  8. package/packages/sdk/README.md +1 -1
  9. package/packages/sdk/dist/abis/ConfigurablePermission.d.ts +130 -0
  10. package/packages/sdk/dist/abis/ConfigurablePermission.d.ts.map +1 -0
  11. package/packages/sdk/dist/abis/ConfigurablePermission.js +79 -0
  12. package/packages/sdk/dist/abis/ConfigurablePermission.js.map +1 -0
  13. package/packages/sdk/dist/abis/SailKernel.d.ts +42 -0
  14. package/packages/sdk/dist/abis/SailKernel.d.ts.map +1 -1
  15. package/packages/sdk/dist/abis/SailKernel.js +34 -0
  16. package/packages/sdk/dist/abis/SailKernel.js.map +1 -1
  17. package/packages/sdk/dist/abis/index.d.ts +1 -0
  18. package/packages/sdk/dist/abis/index.d.ts.map +1 -1
  19. package/packages/sdk/dist/abis/index.js +1 -0
  20. package/packages/sdk/dist/abis/index.js.map +1 -1
  21. package/packages/sdk/dist/chains.d.ts +0 -7
  22. package/packages/sdk/dist/chains.d.ts.map +1 -1
  23. package/packages/sdk/dist/chains.js +81 -32
  24. package/packages/sdk/dist/chains.js.map +1 -1
  25. package/packages/sdk/dist/client.d.ts +1 -1
  26. package/packages/sdk/dist/client.d.ts.map +1 -1
  27. package/packages/sdk/dist/client.js +137 -16
  28. package/packages/sdk/dist/client.js.map +1 -1
  29. package/packages/sdk/dist/deployments.d.ts +6 -4
  30. package/packages/sdk/dist/deployments.d.ts.map +1 -1
  31. package/packages/sdk/dist/deployments.js +120 -118
  32. package/packages/sdk/dist/deployments.js.map +1 -1
  33. package/packages/sdk/dist/eip712.d.ts +84 -0
  34. package/packages/sdk/dist/eip712.d.ts.map +1 -1
  35. package/packages/sdk/dist/eip712.js +148 -0
  36. package/packages/sdk/dist/eip712.js.map +1 -1
  37. package/packages/sdk/dist/fees.d.ts +2 -1
  38. package/packages/sdk/dist/fees.d.ts.map +1 -1
  39. package/packages/sdk/dist/fees.js +2 -1
  40. package/packages/sdk/dist/fees.js.map +1 -1
  41. package/packages/sdk/dist/index.d.ts +3 -2
  42. package/packages/sdk/dist/index.d.ts.map +1 -1
  43. package/packages/sdk/dist/index.js +3 -2
  44. package/packages/sdk/dist/index.js.map +1 -1
  45. package/packages/sdk/dist/intelligence.d.ts +1 -1
  46. package/packages/sdk/dist/intelligence.js +1 -1
  47. package/packages/sdk/dist/safe.d.ts +49 -0
  48. package/packages/sdk/dist/safe.d.ts.map +1 -1
  49. package/packages/sdk/dist/safe.js +74 -0
  50. package/packages/sdk/dist/safe.js.map +1 -1
  51. package/packages/sdk/package.json +6 -2
  52. package/packages/ui/dist/assets/{add-BX5reKtK.js → add-CBbu2pe_.js} +2 -2
  53. package/packages/ui/dist/assets/{all-wallets-IZ3PlVF-.js → all-wallets-C1oy4NUu.js} +2 -2
  54. package/packages/ui/dist/assets/{app-store-ARgRdMdF.js → app-store-DKqyf0Zv.js} +1 -1
  55. package/packages/ui/dist/assets/{apple-VCaqsI9w.js → apple-DyoIbCFy.js} +2 -2
  56. package/packages/ui/dist/assets/{arrow-bottom-circle-h5vFiZ2I.js → arrow-bottom-circle-CMS-OqZQ.js} +2 -2
  57. package/packages/ui/dist/assets/arrow-bottom-pUWeEqYu.js +8 -0
  58. package/packages/ui/dist/assets/arrow-left-B81HMJNL.js +8 -0
  59. package/packages/ui/dist/assets/arrow-right-Z5czoKlC.js +8 -0
  60. package/packages/ui/dist/assets/arrow-top-C6ym1oll.js +8 -0
  61. package/packages/ui/dist/assets/{bank-6c28yOM9.js → bank-B41nlslf.js} +2 -2
  62. package/packages/ui/dist/assets/{basic-CJnbTfSd.js → basic-CWqOb7CV.js} +177 -177
  63. package/packages/ui/dist/assets/{browser-CDKlxM3D.js → browser-DDKdqoZ9.js} +2 -2
  64. package/packages/ui/dist/assets/{card-DQdED02q.js → card-BgOmTajq.js} +2 -2
  65. package/packages/ui/dist/assets/ccip-CtJUKWRd.js +1 -0
  66. package/packages/ui/dist/assets/{checkmark-JoRNVoFV.js → checkmark-CGqQipH4.js} +2 -2
  67. package/packages/ui/dist/assets/{checkmark-bold-B6bGHVIs.js → checkmark-bold-Ci5xf90j.js} +2 -2
  68. package/packages/ui/dist/assets/chevron-bottom-D7Xert17.js +8 -0
  69. package/packages/ui/dist/assets/chevron-left-BGbUebR2.js +8 -0
  70. package/packages/ui/dist/assets/chevron-right-xVsESnHU.js +8 -0
  71. package/packages/ui/dist/assets/chevron-top-DEA9cR6v.js +8 -0
  72. package/packages/ui/dist/assets/{chrome-store-D-fiJ9qJ.js → chrome-store-BsyI-4PI.js} +2 -2
  73. package/packages/ui/dist/assets/{clock-xCPkzwjj.js → clock-Dct9oQ3M.js} +2 -2
  74. package/packages/ui/dist/assets/{close-BkqmPpWG.js → close-BvSCnI2y.js} +2 -2
  75. package/packages/ui/dist/assets/{coinPlaceholder-PIvN9oq_.js → coinPlaceholder-p7RiWkon.js} +2 -2
  76. package/packages/ui/dist/assets/{compass-CW5kWcxW.js → compass-BUeeZqX4.js} +2 -2
  77. package/packages/ui/dist/assets/{copy-DUZlAQJJ.js → copy-C5jXbxLq.js} +2 -2
  78. package/packages/ui/dist/assets/core-CiAm42Mq.js +907 -0
  79. package/packages/ui/dist/assets/cursor-C0T7JWFO.js +3 -0
  80. package/packages/ui/dist/assets/{cursor-transparent-BUAMB0G1.js → cursor-transparent-BZp3-JmA.js} +2 -2
  81. package/packages/ui/dist/assets/{desktop-BxdlwV8P.js → desktop-BIui_Lzb.js} +2 -2
  82. package/packages/ui/dist/assets/{disconnect-GelNh3mn.js → disconnect-B3Bxdchp.js} +2 -2
  83. package/packages/ui/dist/assets/{discord-N0jnxkzC.js → discord-VWI0yDyx.js} +2 -2
  84. package/packages/ui/dist/assets/{etherscan-B41u7wF4.js → etherscan-BpBvUT9i.js} +2 -2
  85. package/packages/ui/dist/assets/events-DQ172AOg.js +1 -0
  86. package/packages/ui/dist/assets/{exclamation-triangle-BWJC56FC.js → exclamation-triangle-o1TJHIUw.js} +2 -2
  87. package/packages/ui/dist/assets/{extension-G6I6oogw.js → extension-4RaLvbh5.js} +2 -2
  88. package/packages/ui/dist/assets/external-link-BHIwjDgj.js +8 -0
  89. package/packages/ui/dist/assets/{facebook-47twy32b.js → facebook-Bes5g-wN.js} +2 -2
  90. package/packages/ui/dist/assets/{farcaster-DYqnqCqs.js → farcaster-CiJ-352m.js} +2 -2
  91. package/packages/ui/dist/assets/{filters-dF6xeJnE.js → filters-DMOteumb.js} +2 -2
  92. package/packages/ui/dist/assets/{github-DbW4Ihz2.js → github-D2nDec9X.js} +2 -2
  93. package/packages/ui/dist/assets/{google-TKm7G8LF.js → google-lZZlQZPQ.js} +2 -2
  94. package/packages/ui/dist/assets/{help-circle-CY3qTmsG.js → help-circle-D4cSSziR.js} +1 -1
  95. package/packages/ui/dist/assets/{id-JpHW4q14.js → id-BmhXZq5P.js} +2 -2
  96. package/packages/ui/dist/assets/{image-Dip2fG98.js → image-Dz4FPeET.js} +2 -2
  97. package/packages/ui/dist/assets/index-B07bCKJ8.css +1 -0
  98. package/packages/ui/dist/assets/index-BV8s6ez1.js +1 -0
  99. package/packages/ui/dist/assets/index-BeUp3nOq.js +1 -0
  100. package/packages/ui/dist/assets/index-CvRylmZJ.js +16 -0
  101. package/packages/ui/dist/assets/{index-q0mGElxZ.js → index-DeoQO45V.js} +3 -3
  102. package/packages/ui/dist/assets/index-Et5S3ZaW.js +47 -0
  103. package/packages/ui/dist/assets/index-LiKeH8Cd.js +1780 -0
  104. package/packages/ui/dist/assets/index.es-dfpEui-9.js +26 -0
  105. package/packages/ui/dist/assets/{info-B1enFsqh.js → info-77RSvIMg.js} +2 -2
  106. package/packages/ui/dist/assets/{info-circle-D4j3FISs.js → info-circle-CWsII-NN.js} +2 -2
  107. package/packages/ui/dist/assets/{lightbulb-DZbKr-vS.js → lightbulb-BZXzm7XE.js} +2 -2
  108. package/packages/ui/dist/assets/{mail-D-TO_uVQ.js → mail-DMG1YqB0.js} +2 -2
  109. package/packages/ui/dist/assets/metamask-sdk-K1x_iqcc.js +542 -0
  110. package/packages/ui/dist/assets/{mobile-Bkmwzav2.js → mobile-DuJfOBSk.js} +2 -2
  111. package/packages/ui/dist/assets/{more-D1_I_jiB.js → more-Cs9ySK2F.js} +2 -2
  112. package/packages/ui/dist/assets/{network-placeholder-DUaFjoXh.js → network-placeholder-CMDPmB1E.js} +2 -2
  113. package/packages/ui/dist/assets/{nftPlaceholder-Cc0iUryh.js → nftPlaceholder-Cj0f9y2u.js} +2 -2
  114. package/packages/ui/dist/assets/{off-BvX71sse.js → off-9zSSsm-e.js} +2 -2
  115. package/packages/ui/dist/assets/{play-store-XzLbtbTd.js → play-store-CPwIb8Gj.js} +2 -2
  116. package/packages/ui/dist/assets/{plus-B77_RmYR.js → plus-BOHQri8o.js} +2 -2
  117. package/packages/ui/dist/assets/{qr-code-BNeIhB1d.js → qr-code-DqhbK276.js} +1 -1
  118. package/packages/ui/dist/assets/{recycle-horizontal-B9sX-yty.js → recycle-horizontal-BpGM56Qz.js} +2 -2
  119. package/packages/ui/dist/assets/{refresh-BWRygnjm.js → refresh-BPzZlhTP.js} +2 -2
  120. package/packages/ui/dist/assets/{reown-logo-DCAEsGFm.js → reown-logo-OCkI0xzo.js} +2 -2
  121. package/packages/ui/dist/assets/{search-ZXIqrawS.js → search-B1JEJvwq.js} +2 -2
  122. package/packages/ui/dist/assets/{secp256k1-Doz05h4g.js → secp256k1-BqUeV9Cb.js} +1 -1
  123. package/packages/ui/dist/assets/{send-BQcO0jNC.js → send-JNsDlNoq.js} +2 -2
  124. package/packages/ui/dist/assets/{swapHorizontal-4yZEzz2K.js → swapHorizontal-C3Z1FImK.js} +2 -2
  125. package/packages/ui/dist/assets/{swapHorizontalBold-DxBKu3sl.js → swapHorizontalBold-Cmdy2iAn.js} +2 -2
  126. package/packages/ui/dist/assets/{swapHorizontalMedium-DAQYJ2JK.js → swapHorizontalMedium-DFX57Nn9.js} +2 -2
  127. package/packages/ui/dist/assets/{swapHorizontalRoundedBold-CUlZzFJ6.js → swapHorizontalRoundedBold-baB389c8.js} +2 -2
  128. package/packages/ui/dist/assets/{swapVertical-CjWHAaDM.js → swapVertical-DmAYGfK1.js} +2 -2
  129. package/packages/ui/dist/assets/{telegram-JBeabiJP.js → telegram-BHbYE-nI.js} +2 -2
  130. package/packages/ui/dist/assets/{three-dots-D3R8OjSx.js → three-dots-BwRWl6qJ.js} +2 -2
  131. package/packages/ui/dist/assets/{twitch-1f9z7xVO.js → twitch-Drx0q6B6.js} +2 -2
  132. package/packages/ui/dist/assets/{twitterIcon-BW7n2xnp.js → twitterIcon-AuJ2KQ6p.js} +2 -2
  133. package/packages/ui/dist/assets/{verify-Cs5owI38.js → verify-Dkrd3q7o.js} +2 -2
  134. package/packages/ui/dist/assets/{verify-filled-Ci5sx5qS.js → verify-filled-B9e3-nz-.js} +2 -2
  135. package/packages/ui/dist/assets/{w3m-modal-DnWtss8u.js → w3m-modal-DtkEjQA7.js} +1 -1
  136. package/packages/ui/dist/assets/{wallet-MaGNqaW-.js → wallet-CqYzSiBd.js} +2 -2
  137. package/packages/ui/dist/assets/{wallet-placeholder-kTOy12_L.js → wallet-placeholder-B05kTN_K.js} +2 -2
  138. package/packages/ui/dist/assets/{walletconnect-CyuP92jg.js → walletconnect-BwoPXLaS.js} +3 -3
  139. package/packages/ui/dist/assets/{warning-circle-BImUCdpG.js → warning-circle-DK9Ai8rT.js} +2 -2
  140. package/packages/ui/dist/assets/{x-II45JHpV.js → x-BNdh5DVW.js} +2 -2
  141. package/packages/ui/dist/index.html +2 -2
  142. package/templates/default/.agents/skills/sail-mandates/SKILL.md +1 -1
  143. package/templates/default/.agents/skills/sail-mandates/references/approvals.md +3 -3
  144. package/templates/default/.agents/skills/sail-mandates/references/examples-index.md +3 -3
  145. package/templates/default/.agents/skills/sail-onboarding/SKILL.md +34 -33
  146. package/templates/default/.agents/skills/sail-servers/SKILL.md +1 -0
  147. package/templates/default/.agents/skills/sail-swap-quote/SKILL.md +76 -0
  148. package/templates/default/.agents/skills/sail-template-approve-batch/SKILL.md +98 -0
  149. package/templates/default/.agents/skills/sail-template-borrow/SKILL.md +68 -0
  150. package/templates/default/.agents/skills/sail-template-deposit/SKILL.md +61 -0
  151. package/templates/default/.agents/skills/sail-template-swap/SKILL.md +207 -0
  152. package/templates/default/.agents/skills/sail-template-swap-no-oracle/SKILL.md +100 -0
  153. package/templates/default/.agents/skills/sail-template-transfer/SKILL.md +52 -0
  154. package/templates/default/.agents/skills/sail-template-withdraw/SKILL.md +50 -0
  155. package/templates/default/.agents/skills/sail-templates/SKILL.md +142 -0
  156. package/templates/default/.agents/skills/sail-templates/catalog.mjs +200 -0
  157. package/templates/default/.agents/skills/sail-templates/deployed.json +133 -0
  158. package/templates/default/.agents/skills/sail-templates/oracles/IOracle.sol +13 -0
  159. package/templates/default/.agents/skills/sail-templates/oracles/UniV3TwapOracle.sol +114 -0
  160. package/templates/default/.agents/skills/sail-templates/oracles/libs/FullMath.sol +62 -0
  161. package/templates/default/.agents/skills/sail-templates/oracles/libs/TickMath.sol +44 -0
  162. package/templates/default/.agents/skills/sail-templates/references/config-schemas.md +139 -0
  163. package/templates/default/.agents/skills/sail-templates/references/reuse-flow.md +139 -0
  164. package/templates/default/.agents/skills/sail-token-resolve/SKILL.md +174 -0
  165. package/templates/default/.env.example +9 -0
  166. package/templates/default/AGENTS.md +107 -13
  167. package/templates/default/docs/PERMISSION_MODEL.md +2 -2
  168. package/{examples → templates/default/examples}/custom-mandate/.sail/contracts/interfaces/IPermission.sol +4 -0
  169. package/{examples → templates/default/examples}/custom-mandate/README.md +4 -1
  170. package/{examples → templates/default/examples}/permissions/interfaces/IBatchPermission.sol +2 -0
  171. package/{examples → templates/default/examples}/permissions/interfaces/IPermission.sol +4 -0
  172. package/templates/default/scripts/quote-swap.mjs +211 -0
  173. package/templates/default/scripts/resolve-token.mjs +992 -0
  174. package/templates/default/scripts/shared-template-addr.mjs +110 -0
  175. package/templates/default/test/BoundedCallPermission.t.sol +2 -1
  176. package/docs/PERMISSION_MODEL.md +0 -93
  177. package/examples/README.md +0 -24
  178. package/examples/lifi-permissions/LifiBoundedApprovePermissionCloneable.sol +0 -84
  179. package/examples/lifi-permissions/LifiDiamondSwapPermissionCloneable.sol +0 -97
  180. package/examples/lifi-permissions/README.md +0 -53
  181. package/packages/ui/dist/assets/arrow-bottom-Cwptw8fW.js +0 -8
  182. package/packages/ui/dist/assets/arrow-left-bM1TRcV9.js +0 -8
  183. package/packages/ui/dist/assets/arrow-right-BYd_N4Jr.js +0 -8
  184. package/packages/ui/dist/assets/arrow-top-CLUnwVE2.js +0 -8
  185. package/packages/ui/dist/assets/ccip-BL7RIPfY.js +0 -1
  186. package/packages/ui/dist/assets/chevron-bottom-BMCelIEu.js +0 -8
  187. package/packages/ui/dist/assets/chevron-left-ryPLhHCc.js +0 -8
  188. package/packages/ui/dist/assets/chevron-right-CdDNRv7f.js +0 -8
  189. package/packages/ui/dist/assets/chevron-top-BxWIjjGk.js +0 -8
  190. package/packages/ui/dist/assets/core-von2c1sc.js +0 -907
  191. package/packages/ui/dist/assets/cursor-BzRVY7BD.js +0 -3
  192. package/packages/ui/dist/assets/events-C9icG2YB.js +0 -1
  193. package/packages/ui/dist/assets/external-link-BPaGQf4a.js +0 -8
  194. package/packages/ui/dist/assets/fallback-BTfJeGjh.js +0 -1
  195. package/packages/ui/dist/assets/index-BGJ_8fMk.js +0 -1
  196. package/packages/ui/dist/assets/index-Bq3PAopp.js +0 -16
  197. package/packages/ui/dist/assets/index-C4_cA5XW.js +0 -1
  198. package/packages/ui/dist/assets/index-CaDN0gL4.js +0 -1
  199. package/packages/ui/dist/assets/index-DLElIcBr.js +0 -1789
  200. package/packages/ui/dist/assets/index-lhNfD4jA.css +0 -1
  201. package/packages/ui/dist/assets/index.es-BaCKS_tW.js +0 -26
  202. package/packages/ui/dist/assets/metamask-sdk-DljW3QD1.js +0 -542
  203. package/packages/ui/dist/assets/native-CJ5et6AR.js +0 -1
  204. package/packages/ui/dist/assets/parseSignature-BAS-wKoj.js +0 -1
  205. package/scripts/check-docs.mjs +0 -309
  206. package/scripts/check-init.mjs +0 -123
  207. package/scripts/check-update.mjs +0 -177
  208. package/scripts/clean.mjs +0 -17
  209. package/scripts/pack.mjs +0 -21
  210. package/templates/default/.agents/skills/sail-overview/SKILL.md +0 -100
  211. package/templates/default/.agents/skills/sail-sailor/SKILL.md +0 -190
  212. package/templates/default/.agents/skills/sail-setup/SKILL.md +0 -38
  213. package/templates/default/.gitlab-ci.yml +0 -58
  214. /package/{examples → templates/default/examples}/custom-mandate/foundry.toml +0 -0
  215. /package/{examples → templates/default/examples}/custom-mandate/mandates/BoundedCallPermission.sol +0 -0
  216. /package/{examples → templates/default/examples}/custom-mandate/mandates/README.md +0 -0
  217. /package/{examples → templates/default/examples}/custom-mandate/mandates/SailCalldata.sol +0 -0
  218. /package/{examples → templates/default/examples}/permissions/BoundedApproveAndCallBatch.sol +0 -0
  219. /package/{examples → templates/default/examples}/permissions/BoundedBorrow_AaveV3_Arbitrum.sol +0 -0
  220. /package/{examples → templates/default/examples}/permissions/BoundedLimitless_Base.sol +0 -0
  221. /package/{examples → templates/default/examples}/permissions/BoundedPerp_GMXv2_Arbitrum.sol +0 -0
  222. /package/{examples → templates/default/examples}/permissions/BoundedStake_Venice_Base.sol +0 -0
  223. /package/{examples → templates/default/examples}/permissions/BoundedSupply_AaveV3_Arbitrum.sol +0 -0
  224. /package/{examples → templates/default/examples}/permissions/BoundedSwapNative_UniswapV3_Base.sol +0 -0
  225. /package/{examples → templates/default/examples}/permissions/BoundedSwap_UniswapV3_Base.sol +0 -0
  226. /package/{examples → templates/default/examples}/permissions/BoundedSwap_UniswapV4_Unichain.sol +0 -0
  227. /package/{examples → templates/default/examples}/permissions/BoundedTransfer_ERC20_Ethereum.sol +0 -0
  228. /package/{examples → templates/default/examples}/permissions/BoundedVault_ERC4626_Base.sol +0 -0
  229. /package/{examples → templates/default/examples}/permissions/README.md +0 -0
  230. /package/{examples → templates/default/examples}/permissions/SailCalldata.sol +0 -0
  231. /package/{examples → templates/default/examples}/permissions/foundry.toml +0 -0
@@ -0,0 +1,207 @@
1
+ ---
2
+ name: sail-template-swap
3
+ description: Gate an SMA's DEX swaps by REUSING the shared SwapPermission singleton (Protocol/contracts/templates/SwapPermission.sol) — register + configure, no per-SMA deploy. Use for a bounded swap / DCA mandate on Uniswap V3, V3-02, or V2 with router + token-in/out allowlists, a per-tx cap, and a MANDATORY oracle slippage band (priceOracle is required — for no-oracle tokens use sail-template-swap-no-oracle). For the LI.FI aggregator or Pendle, author a bespoke permission via sail-mandates. NOTE: `sailor mandate attach` only registers — you must also configure per-account (see steps).
4
+ compatibility: A Sailor project (`@sail/sdk`, `sailor` CLI). Requires SwapPermission 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/SwapPermission.sol
10
+ ---
11
+
12
+ # sail-template-swap — bounded DEX swap via the shared singleton
13
+ Reuse the shared **`SwapPermission`** singleton instead of authoring/deploying a swap contract.
14
+ Register its address on the SMA and `configure()` your routers, token allowlists, cap, and
15
+
16
+ slippage. Family overview + flow: [`sail-templates`](../sail-templates/SKILL.md).
17
+
18
+ ## What it enforces (per account, from source)
19
+
20
+ Supported selectors (any other ⇒ `false`):
21
+
22
+ | Selector | Function | Venue |
23
+ |---|---|---|
24
+ | `0x414bf389` | `exactInputSingle((tokenIn,tokenOut,fee,recipient,deadline,amountIn,amountOutMinimum,sqrtPriceLimitX96))` | Uniswap V3 SwapRouter |
25
+ | `0x04e45aaf` | `exactInputSingle((tokenIn,tokenOut,fee,recipient,amountIn,amountOutMinimum,sqrtPriceLimitX96))` | Uniswap V3 SwapRouter02 |
26
+ | `0x38ed1739` | `swapExactTokensForTokens(amountIn,amountOutMin,path[],to,deadline)` | Uniswap V2 Router |
27
+
28
+ Invariants: `ctx.value == 0` (native value rejected — ERC-20→ERC-20 only); `target ∈ routers`;
29
+ **`tokenIn != tokenOut` / `path[0] != path[last]`** (self-routes denied — a round-trip burns AMM
30
+ fees while an oracle reporting base==quote would otherwise clear the band); `tokenIn`/`path[0] ∈
31
+ tokensIn`; `tokenOut`/`path[last] ∈ tokensOut`; `recipient`/`to == SMA` (funds can't leave the
32
+ account); `amountIn ≤ maxAmountPerTx`; **oracle band ALWAYS enforced** (the oracle is
33
+ mandatory — see below): `amountOutMin ≥ amountIn × price/10^dec ×
34
+ (10_000 − maxSlippageBps)/10_000`. Dust trades whose computed floor truncates to `0` are
35
+ **denied** (fail-closed).
36
+
37
+ > **⛔ The oracle is MANDATORY (contract `v2`, hardened in PR #45).** `_applyConfig` reverts
38
+ > `OracleRequired()` if `priceOracle == 0` and `MissingPriceAge()` if `maxPriceAgeSec == 0`.
39
+ > There is **no oracle-disabled mode** on this template — for tokens with no oracle use the
40
+ > separate [`SwapPermissionNoOracle`](../sail-template-swap-no-oracle/SKILL.md) (live-pool
41
+ > hallucination band). `maxSlippageBps == 0` is **NOT** a bypass — it means zero tolerance
42
+ > (exact-out-or-better), the strictest valid setting. `maxSlippageBps > 9_999` reverts
43
+ > `SlippageBpsTooLarge`.
44
+
45
+ > **V2 intermediate hops are NOT checked** — only `path[0]`/`path[last]`. Restrict to V3 or
46
+ > ensure any plausible intermediate token is acceptable.
47
+
48
+ ## ⚠️ Approve coverage — the hidden precondition
49
+
50
+ `SwapPermission` authorizes the swap **call** only. The router pulls `tokenIn` via an ERC-20
51
+ allowance, and the `approve(router, amount)` that establishes it is a **separate transaction this
52
+ permission does not cover** (same rule as every protocol permission — see
53
+ [`sail-mandates/references/approvals.md`](../sail-mandates/references/approvals.md)). A swap with
54
+ no/insufficient allowance reverts inside the router and the tick fails. **Every** bounded swap
55
+ needs the allowance handled — decide how at mandate-build time, not when the first tick reverts.
56
+
57
+ - **Autonomous agent (DCA / rebalancer / treasury) — default:** do NOT use `SwapPermission` alone;
58
+ register the shared **`ApproveAndCallBatchPermission`** singleton and dispatch each tick as one
59
+ atomic `[approve(router, amountIn), swap, approve(router, 0)]` batch — no lingering allowance,
60
+ no per-tick owner signature. See [`sail-template-approve-batch`](../sail-template-approve-batch/SKILL.md).
61
+ - **Owner in the loop per swap:** keep `SwapPermission`; the owner pre-approves the router. The
62
+ agent must read `allowance(SMA, router)` and **stall (never self-approve)** when it is below
63
+ `amountIn` — `approve()` is an owner-side action the agent cannot take on its own.
64
+
65
+ ## Config blob (authoritative — `config-schemas.md`)
66
+
67
+ ```
68
+ abi.encode(address[] routers, address[] tokensIn, address[] tokensOut,
69
+ uint256 maxAmountPerTx, uint256 maxSlippageBps,
70
+ address priceOracle, uint256 maxPriceAgeSec)
71
+ ```
72
+ | Field | Notes |
73
+ |---|---|
74
+ | `routers` | DEX routers the agent may call |
75
+ | `tokensIn` / `tokensOut` | sell-side / buy-side allowlists (multiple out ⇒ portfolio DCA) |
76
+ | `maxAmountPerTx` | per-swap cap, base units (e.g. `25_000_000` = 25 USDC) |
77
+ | `maxSlippageBps` | e.g. `100` = 1%. `0` = zero tolerance (strictest), NOT a bypass. Must be `≤ 9_999`. |
78
+ | `priceOracle` | **REQUIRED, non-zero.** An `IOracle` adapter exposing `getPrice(tokenIn, tokenOut) → (price, dec, updatedAt)` — **not** a raw Chainlink/Pyth feed. `0` reverts `OracleRequired`. |
79
+ | `maxPriceAgeSec` | **REQUIRED, `> 0`** (oracle staleness bound). `0` reverts `MissingPriceAge`. |
80
+
81
+ Size the cap and slippage floor with `uniswap-v3-quote` / `sail-pyth-prices`. The SDK
82
+ `boundedSwapTemplate` encoder matches this tuple — fine to use after a quick verify.
83
+
84
+ ### Worked example — single-leg USDC → WETH
85
+
86
+ Concrete params for a 25-USDC-per-swap, 1%-slippage DCA leg. These are the values you hand to
87
+ `boundedSwapTemplate.encoder.encode(...)` to produce the `configureDirect` blob (step 3b) — they
88
+ are **not** an args-file for a CLI command. `priceOracle` must be a **real, non-zero `IOracle`
89
+ adapter** for this pair on this chain (`0x0` reverts):
90
+
91
+ ```json
92
+ {
93
+ "routers": ["0x73855d06DE49d0fe4A9c42636Ba96c62da12FF9C"],
94
+ "tokensIn": ["0x078D782b760474a361dDA0AF3839290b0EF57AD6"],
95
+ "tokensOut": ["0x4200000000000000000000000000000000000006"],
96
+ "maxAmountPerTx": "25000000",
97
+ "maxSlippageBps": 100,
98
+ "priceOracle": "0x9d84C11626d13C5DC9540fA12A3Ff7B85Ac3c1B9",
99
+ "maxPriceAgeSec": 3600
100
+ }
101
+ ```
102
+
103
+ > The `priceOracle` above is the **default Unichain USDC/WETH `IOracle` adapter** — a verified
104
+ > Uniswap V3 30-min TWAP (`UniV3TwapOracle`, pool `0x65081C…DBcF1`, 0.05% tier). It serves the
105
+ > USDC↔WETH pair both directions and reverts `UnsupportedPair` for anything else. `catalog.mjs`
106
+ > lists it under **IOracle adapters**; on other chains/pairs you must supply your own adapter
107
+ > (`address(0)` reverts). Because it's a live TWAP, `updatedAt` is always current — `maxPriceAgeSec`
108
+ > just needs to be a few minutes (e.g. `3600`).
109
+
110
+ | Field | From | Notes |
111
+ |---|---|---|
112
+ | `routers` | SwapRouter02 for the chain (resolve-token's chain table) | V3 router; the agent calls `exactInputSingle` on it |
113
+ | `tokensIn` | `resolve-token` | sell-side allowlist (usually just USDC) |
114
+ | `tokensOut` | `resolve-token` | buy-side allowlist; **multiple ⇒ portfolio DCA** |
115
+ | `maxAmountPerTx` | user's per-swap size, **base units** (string) | `"25000000"` = 25 USDC (6 dec) |
116
+ | `maxSlippageBps` | `quote-swap`'s recommendation | `100` = 1%. `0` = zero tolerance (strictest), not a bypass. `≤ 9_999`. |
117
+ | `priceOracle` | an `IOracle` adapter for the chain (Pyth/Chainlink-backed) | **REQUIRED, non-zero.** Must expose `getPrice(tokenIn,tokenOut)` — not the raw feed contract. |
118
+ | `maxPriceAgeSec` | seconds | **REQUIRED, `> 0`** — staleness bound. e.g. `3600` = 1h. |
119
+
120
+ > **Caps are base units.** A decimals mismatch (USDC is 6, most tokens 18) silently mis-sizes
121
+ > every bound. `resolve-token` verified decimals on-chain — use that value, never a guess.
122
+ > For a **portfolio DCA**, list every buy-side token in `tokensOut` (one config, many legs).
123
+
124
+ ## Steps
125
+
126
+ > **Register ≠ configure.** `sailor mandate attach` only registers the singleton on the kernel;
127
+ > it does NOT configure it. A registered-but-unconfigured singleton denies every call. You must
128
+ > do both — steps 3a (register) and 3b (configure). Full mechanics + the encoding gotcha:
129
+ > [`sail-templates` reuse-flow](../sail-templates/references/reuse-flow.md).
130
+
131
+ 1. **Address:** `node SKILLS/sail-templates/catalog.mjs --chain <id>` → `SwapPermission`
132
+ address. It's the same address on every chain (CREATE2); see `deployed.json`.
133
+ 2. **Confirm the spec with the user** (sell/buy tokens, per-swap cap, slippage, router/fee
134
+ tier, recipient = SMA) — print the explainer's humanReadable + warnings. No gas before
135
+ approval.
136
+ 3. **a. Register** the singleton on the SMA's kernel (does NOT configure):
137
+ ```bash
138
+ sailor mandate attach --address <SWAP_PERMISSION> --sma <SMA> --label "bounded-swap"
139
+ ```
140
+ **b. Configure** the per-account bounds — this is what makes the permission live. Build a
141
+ JSON file with the flat template params (`routers`, `tokensIn`, `tokensOut`, `maxAmountPerTx`,
142
+ `maxSlippageBps`, `priceOracle`, `maxPriceAgeSec` — matches the SDK `boundedSwapTemplate`
143
+ encoder tuple, no wrapper), then run:
144
+ ```bash
145
+ sailor mandate configure --address <SWAP_PERMISSION> --sma <SMA> \
146
+ --template SwapPermission --args-file ./swap-config.json
147
+ ```
148
+ `configureDirect` requires `msg.sender == permissionSigner` (`kernel.configs(<SMA>)` — the
149
+ owner only when they collapse to the same address); the command pre-flights the blob with an
150
+ `eth_call` BEFORE any signing or gas — it **reverts** on `priceOracle == 0` (`OracleRequired`),
151
+ `maxPriceAgeSec == 0` (`MissingPriceAge`), or `maxSlippageBps > 9_999` (`SlippageBpsTooLarge`) —
152
+ then requests the `configureDirect` tx from `permissionSigner` through the signing station and
153
+ verifies `isConfigured(<SMA>) == true` on receipt. Pass `--simulate-only` to stop after the
154
+ pre-flight (no signing, no gas).
155
+ *(The signed `configure(account, params, deadline, sig)` path uses EIP-712 domain `("SwapPermission","2")`.)*
156
+ 4. **Simulate** — prove an allowed swap passes and a bad one (wrong recipient / over-cap /
157
+ disallowed token) fails:
158
+ ```bash
159
+ sailor mandate simulate --address <SWAP_PERMISSION> --sma <SMA> --calls ./swap-probe.json
160
+ ```
161
+ 5. **Reconfigure** later (new cap / extra output token) — re-run step 3b with a new blob; same
162
+ address, no re-register.
163
+
164
+ ## Agent config (off-chain, within the on-chain bounds)
165
+
166
+ The mandate bounds what the agent MAY do; the agent's code decides what it WILL do and when. Wire
167
+ the legs from the resolve + quote outputs:
168
+
169
+ ```typescript
170
+ export const DCA_LEGS = [
171
+ {
172
+ tokenIn: "<USDC_ADDR>", tokenOut: "<WETH_ADDR>",
173
+ tokenInDecimals: 6, tokenOutDecimals: 18,
174
+ feeTier: 3000, amountPerSwap: 25_000_000n, // base units, ≤ maxAmountPerTx
175
+ slippageBps: 100,
176
+ },
177
+ ] as const;
178
+ ```
179
+
180
+ The agent must re-quote via [`sail-swap-quote`](../sail-swap-quote/SKILL.md) close to dispatch time
181
+ and embed the floor `amountOutMinimum` in the swap calldata — the on-chain `maxSlippageBps` enforces
182
+ it regardless. **Match the tick's dispatch shape to your approve model:** under the batch model
183
+ (default) the agent returns one `Dispatch` whose `calls` is the 3-element
184
+ `[approve(router, amountIn), swap, approve(router, 0)]` and must NOT pre-approve out of band (the
185
+ batch requires a zero pre-batch allowance); under `SwapPermission` + owner pre-approve it emits a
186
+ plain single-call swap and MUST read `allowance(SMA, router)` and **stall (not self-approve)** when
187
+ it is below `amountIn`.
188
+
189
+ ## When NOT to use this
190
+ - **Token has no `IOracle` adapter** → this template REQUIRES a non-zero oracle and will not
191
+ configure without one. Use [`sail-template-swap-no-oracle`](../sail-template-swap-no-oracle/SKILL.md)
192
+ (live-pool hallucination band — catches honest mistakes, NOT MEV/flash-loan attacks) or author a
193
+ bespoke permission. There is no oracle-off mode on `SwapPermission`.
194
+ - **Aggregator (LI.FI) or opaque calldata** → the mandate can't inspect the route; use
195
+ [`sail-mandates`](../sail-mandates/SKILL.md).
196
+ - **`SwapPermission` not deployed on your chain** → check `deployed.json` first (it's live on all
197
+ 11 Sailor-bundled chains as of the current deploy); for anything outside that set, author your
198
+ own via [`sail-mandates`](../sail-mandates/SKILL.md).
199
+
200
+ ## Notes
201
+ - The oracle is **mandatory** and the band is **always** enforced; `maxSlippageBps = 0` is the
202
+ strictest setting (zero tolerance), not a way to disable the check.
203
+ - `priceOracle` must be an `IOracle` adapter (`getPrice(tokenIn,tokenOut) → (price, dec, updatedAt)`),
204
+ not a raw Chainlink/Pyth feed; it must return a meaningful `updatedAt` or evaluate fails closed.
205
+ - Native value is rejected (`ctx.value != 0 ⇒ deny`) — ERC-20→ERC-20 only.
206
+ - Unaudited example — step 4 is mandatory.
207
+ - `recipient = SMA` is non-negotiable and enforced in the contract, not just config.
@@ -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 author a bespoke permission via sail-mandates. 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) → author a bespoke permission via [`sail-mandates`](../sail-mandates/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-mandates`](../sail-mandates/SKILL.md) for the
128
+ authoring flow and `examples/permissions/` for worked per-protocol patterns.
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.