@dev.sail.money/sailor 1.3.0-95 → 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 (237) hide show
  1. package/AGENTS.md +10 -7
  2. package/LICENSE +1 -1
  3. package/README.md +64 -472
  4. package/package.json +12 -8
  5. package/packages/cli/README.md +1 -1
  6. package/packages/cli/dist/index.cjs +1633 -535
  7. package/packages/cli/dist/server.cjs +19791 -21738
  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/SailGovernance.d.ts +6 -5
  14. package/packages/sdk/dist/abis/SailGovernance.d.ts.map +1 -1
  15. package/packages/sdk/dist/abis/SailGovernance.js +6 -5
  16. package/packages/sdk/dist/abis/SailGovernance.js.map +1 -1
  17. package/packages/sdk/dist/abis/SailKernel.d.ts +42 -0
  18. package/packages/sdk/dist/abis/SailKernel.d.ts.map +1 -1
  19. package/packages/sdk/dist/abis/SailKernel.js +34 -0
  20. package/packages/sdk/dist/abis/SailKernel.js.map +1 -1
  21. package/packages/sdk/dist/abis/index.d.ts +1 -0
  22. package/packages/sdk/dist/abis/index.d.ts.map +1 -1
  23. package/packages/sdk/dist/abis/index.js +1 -0
  24. package/packages/sdk/dist/abis/index.js.map +1 -1
  25. package/packages/sdk/dist/chains.d.ts +0 -7
  26. package/packages/sdk/dist/chains.d.ts.map +1 -1
  27. package/packages/sdk/dist/chains.js +81 -32
  28. package/packages/sdk/dist/chains.js.map +1 -1
  29. package/packages/sdk/dist/client.d.ts +1 -1
  30. package/packages/sdk/dist/client.d.ts.map +1 -1
  31. package/packages/sdk/dist/client.js +137 -16
  32. package/packages/sdk/dist/client.js.map +1 -1
  33. package/packages/sdk/dist/deployments.d.ts +6 -4
  34. package/packages/sdk/dist/deployments.d.ts.map +1 -1
  35. package/packages/sdk/dist/deployments.js +120 -118
  36. package/packages/sdk/dist/deployments.js.map +1 -1
  37. package/packages/sdk/dist/eip712.d.ts +84 -0
  38. package/packages/sdk/dist/eip712.d.ts.map +1 -1
  39. package/packages/sdk/dist/eip712.js +148 -0
  40. package/packages/sdk/dist/eip712.js.map +1 -1
  41. package/packages/sdk/dist/fees.d.ts +59 -10
  42. package/packages/sdk/dist/fees.d.ts.map +1 -1
  43. package/packages/sdk/dist/fees.js +74 -41
  44. package/packages/sdk/dist/fees.js.map +1 -1
  45. package/packages/sdk/dist/index.d.ts +5 -3
  46. package/packages/sdk/dist/index.d.ts.map +1 -1
  47. package/packages/sdk/dist/index.js +4 -3
  48. package/packages/sdk/dist/index.js.map +1 -1
  49. package/packages/sdk/dist/intelligence.d.ts +7 -2
  50. package/packages/sdk/dist/intelligence.d.ts.map +1 -1
  51. package/packages/sdk/dist/intelligence.js +2 -2
  52. package/packages/sdk/dist/intelligence.js.map +1 -1
  53. package/packages/sdk/dist/safe.d.ts +49 -0
  54. package/packages/sdk/dist/safe.d.ts.map +1 -1
  55. package/packages/sdk/dist/safe.js +74 -0
  56. package/packages/sdk/dist/safe.js.map +1 -1
  57. package/packages/sdk/dist/signing.d.ts +14 -0
  58. package/packages/sdk/dist/signing.d.ts.map +1 -1
  59. package/packages/sdk/package.json +6 -2
  60. package/packages/ui/dist/assets/{add-CLs0-B0p.js → add-CBbu2pe_.js} +2 -2
  61. package/packages/ui/dist/assets/{all-wallets-CDOk28sj.js → all-wallets-C1oy4NUu.js} +2 -2
  62. package/packages/ui/dist/assets/{app-store-DVzpi1qN.js → app-store-DKqyf0Zv.js} +1 -1
  63. package/packages/ui/dist/assets/{apple-C9U9iH1l.js → apple-DyoIbCFy.js} +2 -2
  64. package/packages/ui/dist/assets/{arrow-bottom-circle-jxial9Si.js → arrow-bottom-circle-CMS-OqZQ.js} +2 -2
  65. package/packages/ui/dist/assets/arrow-bottom-pUWeEqYu.js +8 -0
  66. package/packages/ui/dist/assets/arrow-left-B81HMJNL.js +8 -0
  67. package/packages/ui/dist/assets/arrow-right-Z5czoKlC.js +8 -0
  68. package/packages/ui/dist/assets/arrow-top-C6ym1oll.js +8 -0
  69. package/packages/ui/dist/assets/{bank-B98it1qN.js → bank-B41nlslf.js} +2 -2
  70. package/packages/ui/dist/assets/{basic-DsgeyPNu.js → basic-CWqOb7CV.js} +177 -177
  71. package/packages/ui/dist/assets/{browser-j4vCeHwE.js → browser-DDKdqoZ9.js} +2 -2
  72. package/packages/ui/dist/assets/{card-CzxbMvEd.js → card-BgOmTajq.js} +2 -2
  73. package/packages/ui/dist/assets/ccip-CtJUKWRd.js +1 -0
  74. package/packages/ui/dist/assets/{checkmark-D5sWBbk3.js → checkmark-CGqQipH4.js} +2 -2
  75. package/packages/ui/dist/assets/{checkmark-bold-BW6w3KCC.js → checkmark-bold-Ci5xf90j.js} +2 -2
  76. package/packages/ui/dist/assets/chevron-bottom-D7Xert17.js +8 -0
  77. package/packages/ui/dist/assets/chevron-left-BGbUebR2.js +8 -0
  78. package/packages/ui/dist/assets/chevron-right-xVsESnHU.js +8 -0
  79. package/packages/ui/dist/assets/chevron-top-DEA9cR6v.js +8 -0
  80. package/packages/ui/dist/assets/{chrome-store-B1OMtzC-.js → chrome-store-BsyI-4PI.js} +2 -2
  81. package/packages/ui/dist/assets/{clock-e0HueehS.js → clock-Dct9oQ3M.js} +2 -2
  82. package/packages/ui/dist/assets/{close-Cy1ytr26.js → close-BvSCnI2y.js} +2 -2
  83. package/packages/ui/dist/assets/{coinPlaceholder-DqJu3ZPV.js → coinPlaceholder-p7RiWkon.js} +2 -2
  84. package/packages/ui/dist/assets/{compass-XDz_N2Hg.js → compass-BUeeZqX4.js} +2 -2
  85. package/packages/ui/dist/assets/{copy-DmDo3Nob.js → copy-C5jXbxLq.js} +2 -2
  86. package/packages/ui/dist/assets/core-CiAm42Mq.js +907 -0
  87. package/packages/ui/dist/assets/cursor-C0T7JWFO.js +3 -0
  88. package/packages/ui/dist/assets/{cursor-transparent-DF2aA10D.js → cursor-transparent-BZp3-JmA.js} +2 -2
  89. package/packages/ui/dist/assets/{desktop-CVydOWLn.js → desktop-BIui_Lzb.js} +2 -2
  90. package/packages/ui/dist/assets/{disconnect-BuN2Whnz.js → disconnect-B3Bxdchp.js} +2 -2
  91. package/packages/ui/dist/assets/{discord-DMNPvTB2.js → discord-VWI0yDyx.js} +2 -2
  92. package/packages/ui/dist/assets/{etherscan-CacW-93f.js → etherscan-BpBvUT9i.js} +2 -2
  93. package/packages/ui/dist/assets/events-DQ172AOg.js +1 -0
  94. package/packages/ui/dist/assets/{exclamation-triangle-B26hIvqO.js → exclamation-triangle-o1TJHIUw.js} +2 -2
  95. package/packages/ui/dist/assets/{extension-B7rI0gv7.js → extension-4RaLvbh5.js} +2 -2
  96. package/packages/ui/dist/assets/external-link-BHIwjDgj.js +8 -0
  97. package/packages/ui/dist/assets/{facebook-BDqbE6kl.js → facebook-Bes5g-wN.js} +2 -2
  98. package/packages/ui/dist/assets/{farcaster-zQ3oQzZ8.js → farcaster-CiJ-352m.js} +2 -2
  99. package/packages/ui/dist/assets/{filters-DCglZYsM.js → filters-DMOteumb.js} +2 -2
  100. package/packages/ui/dist/assets/{github-BH-BYjss.js → github-D2nDec9X.js} +2 -2
  101. package/packages/ui/dist/assets/{google-DYFWxZbC.js → google-lZZlQZPQ.js} +2 -2
  102. package/packages/ui/dist/assets/{help-circle-BXH_DtZ_.js → help-circle-D4cSSziR.js} +1 -1
  103. package/packages/ui/dist/assets/{id-Dr2yTeX1.js → id-BmhXZq5P.js} +2 -2
  104. package/packages/ui/dist/assets/{image-DeJey1Eg.js → image-Dz4FPeET.js} +2 -2
  105. package/packages/ui/dist/assets/index-B07bCKJ8.css +1 -0
  106. package/packages/ui/dist/assets/index-BV8s6ez1.js +1 -0
  107. package/packages/ui/dist/assets/index-BeUp3nOq.js +1 -0
  108. package/packages/ui/dist/assets/index-CvRylmZJ.js +16 -0
  109. package/packages/ui/dist/assets/{index-DVjAiPHI.js → index-DeoQO45V.js} +3 -3
  110. package/packages/ui/dist/assets/index-Et5S3ZaW.js +47 -0
  111. package/packages/ui/dist/assets/index-LiKeH8Cd.js +1780 -0
  112. package/packages/ui/dist/assets/index.es-dfpEui-9.js +26 -0
  113. package/packages/ui/dist/assets/{info-B91pD5Rw.js → info-77RSvIMg.js} +2 -2
  114. package/packages/ui/dist/assets/{info-circle-CySl3_on.js → info-circle-CWsII-NN.js} +2 -2
  115. package/packages/ui/dist/assets/{lightbulb-wwG8LQLo.js → lightbulb-BZXzm7XE.js} +2 -2
  116. package/packages/ui/dist/assets/{mail-D617YZ8y.js → mail-DMG1YqB0.js} +2 -2
  117. package/packages/ui/dist/assets/metamask-sdk-K1x_iqcc.js +542 -0
  118. package/packages/ui/dist/assets/{mobile-CD9KlA0m.js → mobile-DuJfOBSk.js} +2 -2
  119. package/packages/ui/dist/assets/{more-BQ_rpPXo.js → more-Cs9ySK2F.js} +2 -2
  120. package/packages/ui/dist/assets/{network-placeholder-BA7nl9e6.js → network-placeholder-CMDPmB1E.js} +2 -2
  121. package/packages/ui/dist/assets/{nftPlaceholder-D1EaaeDn.js → nftPlaceholder-Cj0f9y2u.js} +2 -2
  122. package/packages/ui/dist/assets/{off-DnGG35_w.js → off-9zSSsm-e.js} +2 -2
  123. package/packages/ui/dist/assets/{play-store-C5u2FvYB.js → play-store-CPwIb8Gj.js} +2 -2
  124. package/packages/ui/dist/assets/{plus-CM4xJkvI.js → plus-BOHQri8o.js} +2 -2
  125. package/packages/ui/dist/assets/{qr-code-kM3FUIdR.js → qr-code-DqhbK276.js} +1 -1
  126. package/packages/ui/dist/assets/{recycle-horizontal-DWrRWIBM.js → recycle-horizontal-BpGM56Qz.js} +2 -2
  127. package/packages/ui/dist/assets/{refresh-CztbjK2d.js → refresh-BPzZlhTP.js} +2 -2
  128. package/packages/ui/dist/assets/{reown-logo-DUHEIodF.js → reown-logo-OCkI0xzo.js} +2 -2
  129. package/packages/ui/dist/assets/{search-BJtb4mn1.js → search-B1JEJvwq.js} +2 -2
  130. package/packages/ui/dist/assets/{secp256k1-BjrTZpQx.js → secp256k1-BqUeV9Cb.js} +1 -1
  131. package/packages/ui/dist/assets/{send-B3_kavG1.js → send-JNsDlNoq.js} +2 -2
  132. package/packages/ui/dist/assets/{swapHorizontal-BrF9POiv.js → swapHorizontal-C3Z1FImK.js} +2 -2
  133. package/packages/ui/dist/assets/{swapHorizontalBold-Dgf8OjXj.js → swapHorizontalBold-Cmdy2iAn.js} +2 -2
  134. package/packages/ui/dist/assets/{swapHorizontalMedium-C6wX0JZj.js → swapHorizontalMedium-DFX57Nn9.js} +2 -2
  135. package/packages/ui/dist/assets/{swapHorizontalRoundedBold-DTAVw6oF.js → swapHorizontalRoundedBold-baB389c8.js} +2 -2
  136. package/packages/ui/dist/assets/{swapVertical-KGlTFNoi.js → swapVertical-DmAYGfK1.js} +2 -2
  137. package/packages/ui/dist/assets/{telegram-DnLrK-4y.js → telegram-BHbYE-nI.js} +2 -2
  138. package/packages/ui/dist/assets/{three-dots-C6_oMJfw.js → three-dots-BwRWl6qJ.js} +2 -2
  139. package/packages/ui/dist/assets/{twitch-DvPqPD4f.js → twitch-Drx0q6B6.js} +2 -2
  140. package/packages/ui/dist/assets/{twitterIcon-BZdk82Am.js → twitterIcon-AuJ2KQ6p.js} +2 -2
  141. package/packages/ui/dist/assets/{verify-CbESD5G0.js → verify-Dkrd3q7o.js} +2 -2
  142. package/packages/ui/dist/assets/{verify-filled-BRjTrLxw.js → verify-filled-B9e3-nz-.js} +2 -2
  143. package/packages/ui/dist/assets/{w3m-modal-D9b2TGHc.js → w3m-modal-DtkEjQA7.js} +1 -1
  144. package/packages/ui/dist/assets/{wallet-B87E5UId.js → wallet-CqYzSiBd.js} +2 -2
  145. package/packages/ui/dist/assets/{wallet-placeholder-DmssSbYf.js → wallet-placeholder-B05kTN_K.js} +2 -2
  146. package/packages/ui/dist/assets/{walletconnect-CZkLKWMZ.js → walletconnect-BwoPXLaS.js} +3 -3
  147. package/packages/ui/dist/assets/{warning-circle-BPTpHB1P.js → warning-circle-DK9Ai8rT.js} +2 -2
  148. package/packages/ui/dist/assets/{x-D_dEtEDU.js → x-BNdh5DVW.js} +2 -2
  149. package/packages/ui/dist/index.html +2 -2
  150. package/templates/default/.agents/skills/sail-mandates/SKILL.md +13 -1
  151. package/templates/default/.agents/skills/sail-mandates/references/approvals.md +3 -3
  152. package/templates/default/.agents/skills/sail-mandates/references/examples-index.md +3 -3
  153. package/templates/default/.agents/skills/sail-onboarding/SKILL.md +2 -2
  154. package/templates/default/.agents/skills/sail-project-info/SKILL.md +2 -0
  155. package/templates/default/.agents/skills/sail-servers/SKILL.md +1 -0
  156. package/templates/default/.agents/skills/sail-swap-quote/SKILL.md +76 -0
  157. package/templates/default/.agents/skills/sail-template-approve-batch/SKILL.md +98 -0
  158. package/templates/default/.agents/skills/sail-template-borrow/SKILL.md +68 -0
  159. package/templates/default/.agents/skills/sail-template-deposit/SKILL.md +61 -0
  160. package/templates/default/.agents/skills/sail-template-swap/SKILL.md +207 -0
  161. package/templates/default/.agents/skills/sail-template-swap-no-oracle/SKILL.md +100 -0
  162. package/templates/default/.agents/skills/sail-template-transfer/SKILL.md +52 -0
  163. package/templates/default/.agents/skills/sail-template-withdraw/SKILL.md +50 -0
  164. package/templates/default/.agents/skills/sail-templates/SKILL.md +142 -0
  165. package/templates/default/.agents/skills/sail-templates/catalog.mjs +200 -0
  166. package/templates/default/.agents/skills/sail-templates/deployed.json +133 -0
  167. package/templates/default/.agents/skills/sail-templates/oracles/IOracle.sol +13 -0
  168. package/templates/default/.agents/skills/sail-templates/oracles/UniV3TwapOracle.sol +114 -0
  169. package/templates/default/.agents/skills/sail-templates/oracles/libs/FullMath.sol +62 -0
  170. package/templates/default/.agents/skills/sail-templates/oracles/libs/TickMath.sol +44 -0
  171. package/templates/default/.agents/skills/sail-templates/references/config-schemas.md +139 -0
  172. package/templates/default/.agents/skills/sail-templates/references/reuse-flow.md +139 -0
  173. package/templates/default/.agents/skills/sail-token-resolve/SKILL.md +174 -0
  174. package/templates/default/.env.example +9 -0
  175. package/templates/default/AGENTS.md +21 -2
  176. package/templates/default/docs/PERMISSION_MODEL.md +2 -2
  177. package/{examples → templates/default/examples}/custom-mandate/.sail/contracts/interfaces/IPermission.sol +4 -0
  178. package/{examples → templates/default/examples}/custom-mandate/README.md +4 -1
  179. package/{examples → templates/default/examples}/permissions/interfaces/IBatchPermission.sol +2 -0
  180. package/{examples → templates/default/examples}/permissions/interfaces/IPermission.sol +4 -0
  181. package/templates/default/scripts/quote-swap.mjs +211 -0
  182. package/templates/default/scripts/resolve-token.mjs +992 -0
  183. package/templates/default/scripts/shared-template-addr.mjs +110 -0
  184. package/templates/default/test/BoundedCallPermission.t.sol +2 -1
  185. package/docs/PERMISSION_MODEL.md +0 -93
  186. package/examples/README.md +0 -24
  187. package/examples/lifi-permissions/LifiBoundedApprovePermissionCloneable.sol +0 -84
  188. package/examples/lifi-permissions/LifiDiamondSwapPermissionCloneable.sol +0 -97
  189. package/examples/lifi-permissions/README.md +0 -53
  190. package/packages/cli/dist/mcp.cjs +0 -15534
  191. package/packages/ui/dist/assets/arrow-bottom-DjQ4jGMe.js +0 -8
  192. package/packages/ui/dist/assets/arrow-left-BjAyI8D1.js +0 -8
  193. package/packages/ui/dist/assets/arrow-right-BlFO4Tcx.js +0 -8
  194. package/packages/ui/dist/assets/arrow-top-DJ3oh3DH.js +0 -8
  195. package/packages/ui/dist/assets/ccip-U9yiu_EK.js +0 -1
  196. package/packages/ui/dist/assets/chevron-bottom-BbHhmTQq.js +0 -8
  197. package/packages/ui/dist/assets/chevron-left-D8BDH4Wo.js +0 -8
  198. package/packages/ui/dist/assets/chevron-right-BQuQk5zc.js +0 -8
  199. package/packages/ui/dist/assets/chevron-top-1oeSXkx4.js +0 -8
  200. package/packages/ui/dist/assets/core-DbkMGSYW.js +0 -907
  201. package/packages/ui/dist/assets/cursor-DRFqL8nZ.js +0 -3
  202. package/packages/ui/dist/assets/events-CZkVDFih.js +0 -1
  203. package/packages/ui/dist/assets/external-link-CX8skCbH.js +0 -8
  204. package/packages/ui/dist/assets/fallback-DWQYKf3l.js +0 -1
  205. package/packages/ui/dist/assets/index-BP-UuQ8g.js +0 -1
  206. package/packages/ui/dist/assets/index-BksbyFjf.js +0 -1
  207. package/packages/ui/dist/assets/index-C55-Ne92.js +0 -16
  208. package/packages/ui/dist/assets/index-D6_Zei-R.js +0 -1790
  209. package/packages/ui/dist/assets/index-DZ2C2rHq.css +0 -1
  210. package/packages/ui/dist/assets/index-uSdXE5b_.js +0 -1
  211. package/packages/ui/dist/assets/index.es-dZlNPpfR.js +0 -26
  212. package/packages/ui/dist/assets/metamask-sdk-RbLbxUgF.js +0 -542
  213. package/packages/ui/dist/assets/native-CJ5et6AR.js +0 -1
  214. package/packages/ui/dist/assets/parseSignature-E29x8poM.js +0 -1
  215. package/scripts/check-docs.mjs +0 -309
  216. package/scripts/check-init.mjs +0 -123
  217. package/scripts/check-update.mjs +0 -177
  218. package/scripts/clean.mjs +0 -17
  219. package/scripts/pack.mjs +0 -21
  220. /package/{examples → templates/default/examples}/custom-mandate/foundry.toml +0 -0
  221. /package/{examples → templates/default/examples}/custom-mandate/mandates/BoundedCallPermission.sol +0 -0
  222. /package/{examples → templates/default/examples}/custom-mandate/mandates/README.md +0 -0
  223. /package/{examples → templates/default/examples}/custom-mandate/mandates/SailCalldata.sol +0 -0
  224. /package/{examples → templates/default/examples}/permissions/BoundedApproveAndCallBatch.sol +0 -0
  225. /package/{examples → templates/default/examples}/permissions/BoundedBorrow_AaveV3_Arbitrum.sol +0 -0
  226. /package/{examples → templates/default/examples}/permissions/BoundedLimitless_Base.sol +0 -0
  227. /package/{examples → templates/default/examples}/permissions/BoundedPerp_GMXv2_Arbitrum.sol +0 -0
  228. /package/{examples → templates/default/examples}/permissions/BoundedStake_Venice_Base.sol +0 -0
  229. /package/{examples → templates/default/examples}/permissions/BoundedSupply_AaveV3_Arbitrum.sol +0 -0
  230. /package/{examples → templates/default/examples}/permissions/BoundedSwapNative_UniswapV3_Base.sol +0 -0
  231. /package/{examples → templates/default/examples}/permissions/BoundedSwap_UniswapV3_Base.sol +0 -0
  232. /package/{examples → templates/default/examples}/permissions/BoundedSwap_UniswapV4_Unichain.sol +0 -0
  233. /package/{examples → templates/default/examples}/permissions/BoundedTransfer_ERC20_Ethereum.sol +0 -0
  234. /package/{examples → templates/default/examples}/permissions/BoundedVault_ERC4626_Base.sol +0 -0
  235. /package/{examples → templates/default/examples}/permissions/README.md +0 -0
  236. /package/{examples → templates/default/examples}/permissions/SailCalldata.sol +0 -0
  237. /package/{examples → templates/default/examples}/permissions/foundry.toml +0 -0
@@ -5,8 +5,8 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <meta name="theme-color" content="#040b16" />
7
7
  <title>Sail - Unlocking Personalized Money</title>
8
- <script type="module" crossorigin src="/assets/index-D6_Zei-R.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-DZ2C2rHq.css">
8
+ <script type="module" crossorigin src="/assets/index-LiKeH8Cd.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-B07bCKJ8.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
@@ -94,6 +94,18 @@ Only now is the permission live. The owner (mandate signer) signs in the browser
94
94
 
95
95
  When a strategy needs several permissions (e.g. a bounded-approve alongside the protocol permission), attach them all at once by passing a comma-separated list of addresses — the registration approvals collapse to a **single** browser signature via the kernel's `registerPermissions`. The earlier per-contract deploy approvals (Gate 5) are separate and unavoidable. A single permission attaches exactly as before with `--address <one>`.
96
96
 
97
+ ## Registration fee
98
+
99
+ Registering a permission charges a **per-permission fee**, paid on-chain by the agent wallet at the moment of registration. It is a public protocol parameter — `permissionRegistrationFee()` on `SailGovernance` — read **live from the chain**, never hardcoded: it is `0` on the current deploy and higher in production, and the same flow surfaces whichever value the connected chain returns.
100
+
101
+ - **A mandate is a SET of permissions, so a mandate of N permissions costs `N × fee`.** Three permissions at `0.00001 ETH` each cost `0.00003 ETH` total.
102
+ - **When it's charged:** once per permission, on registration (the `attach` / `deploy-clone` step). Already-registered permissions are not re-charged when you re-run `sailor mandate sign`. Revoking does not refund.
103
+ - **Disclosure before signing:** `sailor mandate prepare` reads the live fee and records it in the draft, and `sailor mandate sign` prints `Registration fee: <total> ETH (<N> permissions × <fee> ETH)` before you confirm. The browser sign-time screen shows the same total.
104
+ - **Preflight:** before requesting the owner's signature, the agent wallet's ETH balance is checked against the total fee; an underfunded wallet fails early with `Insufficient ETH for the <X> ETH registration fee` instead of an on-chain revert. **Fund the agent wallet before attaching.**
105
+ - **Recorded:** each `permission_registered` activity entry carries the fee actually paid (`fee` in wei, `feeEth` formatted), so Recent Activity shows the real cost.
106
+
107
+ The fee is read via `readPermissionRegistrationFee()` in `packages/sdk/src/fees.ts` and applied as `fee × N` — the same number used for disclosure, the preflight, the tx `value`, and the activity record.
108
+
97
109
  ## Maintenance
98
110
 
99
111
  - `sailor mandate revoke --address <P> --sma <SMA> --json` (or `--all`) — owner signs `RevokePermissions` in the browser (BLOCKS); agent submits. Revocations are recorded to the activity log; `state/mandates.json` keeps the historical record.
@@ -104,4 +116,4 @@ When a strategy needs several permissions (e.g. a bounded-approve alongside the
104
116
 
105
117
  ## Clone templates (deploy-clone)
106
118
 
107
- `sailor mandate deploy-clone --template boundedApprove --sma <SMA> --tokens <csv> --spenders <csv> --max <wei> --json` deploys + registers an EIP-1167 clone of a published implementation in one transaction (owner signs `RegisterPermission` for the predicted clone address — BLOCKS; agent submits `deployAndAttach`). The only template key is `boundedApprove`. Implementations come from the SDK deployment registry (`standaloneTemplates`) — currently **empty on all six chains** pending redeployment against the new kernel, so deploy-clone errors with a clear message and you should write and deploy a bounded-approve permission with `sailor mandate deploy` instead. Check availability with `sailor mandate templates --json`.
119
+ `sailor mandate deploy-clone --template boundedApprove --sma <SMA> --tokens <csv> --spenders <csv> --max <wei> --json` deploys + registers an EIP-1167 clone of a published implementation in one transaction (owner signs `RegisterPermission` for the predicted clone address — BLOCKS; agent submits `deployAndAttach`). The only template key is `boundedApprove`. Implementations come from the SDK deployment registry (`standaloneTemplates`) — that map is now populated with the shared swap/borrow/deposit/withdraw/transfer/approve-and-call-batch templates on all eleven chains, but no `boundedApprove` clone implementation is deployed yet, so deploy-clone errors with a clear message and you should write and deploy a bounded-approve permission with `sailor mandate deploy` instead. Check availability with `sailor mandate templates --json`.
@@ -25,12 +25,12 @@ This needs no batch support and works on every kernel.
25
25
 
26
26
  Approve + action run as one `dispatchBatch` — atomic, single transaction. A batch dispatch consults **exactly one batch-aware `IBatchPermission`**, never a pair of `IPermission`s:
27
27
 
28
- - The permission implements `@sail/interfaces/IBatchPermission.sol` — `evaluateBatch(Call[] calls, BatchContext ctx)` validates the WHOLE sequence (ordering, the approve's spender/amount, the action, and mandatory allowance cleanup) and `isBatchPermission()` returns true. `examples/permissions/BoundedApproveAndCallBatch.sol` is the model.
28
+ - The permission implements `@sail/interfaces/IBatchPermission.sol` — `evaluateBatch(Call[] calls, BatchContext ctx)` validates the WHOLE sequence (ordering, the approve's spender/amount, the action, and mandatory allowance cleanup) and `isBatchPermission()` returns true. **For the canonical `approve → swap/deposit/supply → reset` shape, a shared `ApproveAndCallBatchPermission` singleton is deployed** on Base, Arbitrum, Unichain, Sepolia, and Base Sepolia (resolve it with `node scripts/shared-template-addr.mjs ApproveAndCallBatchPermission`) — reuse it via `attach` + `configure` rather than authoring one. `examples/permissions/BoundedApproveAndCallBatch.sol` remains the model for a non-standard shape you must author yourself.
29
29
  - A normal `IPermission` placed in a batch is rejected by the kernel with `PermissionNotBatchAware`. You cannot assemble a batch from two narrow per-call permissions — that was the trap.
30
30
  - Batch is a **selective-kernel** feature (`dispatchBatch` / `previewBatch`); conjunctive kernels have neither. Confirm the model with `sailor doctor`; details in `docs/PERMISSION_MODEL.md`.
31
- - At runtime, return one `Dispatch` whose `calls` array is `[approveCall, actionCall]`; the runner detects `calls.length > 1` and routes through `dispatch.batch`.
31
+ - At runtime, return one `Dispatch` whose `calls` array is the batch. The shared `ApproveAndCallBatchPermission` requires **exactly** `[approve(spender, amount), action, approve(spender, 0)]` the trailing reset to zero is mandatory, and the pre-batch allowance on that `(token, spender)` pair must already be zero (so never combine it with a standing approve). The runner detects `calls.length > 1` and routes through `dispatch.batch`.
32
32
 
33
- Use Model B only when atomicity genuinely matters (e.g. the approve must not be observable between calls). Otherwise prefer Model A less contract to author and test.
33
+ **For autonomous recurring actions (a DCA or rebalancer the agent runs on its own), Model B is the default not an advanced option.** Model A's separate approve dispatch needs the owner back in the loop every time the bounded allowance runs out, which a recurring agent cannot do; on the first tick after the allowance is consumed it stalls (or, in a simulation, is tempted to cheat the allowance in). Reach for Model A when the owner is genuinely in the loop per action anyway. For bounded swaps specifically, [`sail-template-swap`'s "Approve coverage"](../../sail-template-swap/SKILL.md) works through the choice with concrete addresses.
34
34
 
35
35
  ## Verifying each model
36
36
 
@@ -15,7 +15,7 @@ Permissions only bound on-chain actions: venues with off-chain order matching (P
15
15
  | `BoundedStake_Venice_Base.sol` | Venice (VVV) staking · Base | Recipient binding on stake; claim() structurally routes rewards to caller (the SMA) | `0xadc9772e` stake(address,uint256), `0x4e71d92d` claim() |
16
16
  | `BoundedTransfer_ERC20_Ethereum.sol` | ERC-20 · any EVM | Token allowlist + recipient allowlist + per-transfer cap; caps are per-token base units (USDC 6 decimals, WETH 18) — one permission per token if amounts differ | `0xa9059cbb` transfer(address,uint256) |
17
17
  | `BoundedPerp_GMXv2_Arbitrum.sol` | GMX v2 ExchangeRouter · Arbitrum | Market allowlist + collateral cap + size cap (USD is 1e30-scaled) + long/short flags on a struct-typed call | `0x212234c3` createOrder(CreateOrderParams) — MUST re-verify, see below |
18
- | `BoundedBet_Limitless_Base.sol` | Limitless CTF exchange · Base | Condition allowlist + stake cap + outcome allowlist on a prediction market | buy(bytes32,uint256,uint256) — **ABI UNVERIFIED**, see below |
18
+ | `BoundedLimitless_Base.sol` | Limitless CTF exchange · Base | Condition allowlist + stake cap + outcome allowlist on a prediction market | buy(bytes32,uint256,uint256) — **ABI UNVERIFIED**, see below |
19
19
  | `BoundedApproveAndCallBatch.sol` | Sail batch dispatch · any selective kernel | `IBatchPermission`: atomic approve → consume → reset-to-0 in exactly 3 calls; single-call evaluate() deliberately returns false | `0x095ea7b3` approve (calls 0 and 2) |
20
20
 
21
21
  ## Lessons the examples encode
@@ -29,6 +29,6 @@ Permissions only bound on-chain actions: venues with off-chain order matching (P
29
29
 
30
30
  ## Interfaces (vendored in `examples/permissions/interfaces/`)
31
31
 
32
- - `IPermission.evaluate(bytes txData, Context ctx) → bool`; Context: `account, manager, submitter, target, selector, value, blockTimestamp, blockNumber`.
33
- - `IBatchPermission.evaluateBatch(Call[] calls, BatchContext ctx) → bool` + `isBatchPermission() → true`; Call: `(target, value, data)`; BatchContext: `account, manager, submitter, permission, batchHash, blockTimestamp, blockNumber`.
32
+ - `IPermission.evaluate(bytes txData, Context ctx) → bool`; Context: `account, manager, submitter, target, selector, value, blockTimestamp, blockNumber, configEpoch`.
33
+ - `IBatchPermission.evaluateBatch(Call[] calls, BatchContext ctx) → bool` + `isBatchPermission() → true`; Call: `(target, value, data)`; BatchContext: `account, manager, submitter, permission, batchHash, blockTimestamp, blockNumber, configEpoch`.
34
34
  - `SailCalldata.sol`: bounded slot-indexed calldata readers (`hasParams`, `asAddress`, `asUint256`, …) — use these instead of `abi.decode` to avoid wrong-offset reads.
@@ -58,7 +58,7 @@ Stage machine keyed off `.sail/`. Read the state, enter at the right stage, neve
58
58
  | `account.json` exists, `state/mandates.json` empty or absent | SMA live, no permissions — hand off to **sail-mandates** |
59
59
  | `account.json` + tracked mandates | SMA live with permissions — hand off to **sail-transactions** to run. Once the agent is live, the flow is not done: **offer Extend (stage 5 — notifications + a custom dashboard) by default**, unless the operator opts out, then hand off to **sail-extend**. The end state is "stage 5 offered," not "running" |
60
60
 
61
- Supported chains: Ethereum (1), Base (8453), Arbitrum (42161), Unichain (130), Base Sepolia (84532), Eth Sepolia (11155111). `sailor chains --json` lists them with kernel addresses.
61
+ Supported chains: Ethereum (1), Base (8453), Arbitrum (42161), Optimism (10), Unichain (130), BSC (56), World Chain (480), HyperEVM (999), MegaETH (4326), Base Sepolia (84532), Eth Sepolia (11155111). `sailor chains --json` lists them with kernel addresses.
62
62
 
63
63
  ## Stage 1 — Deploy the SMA and create the agent wallet
64
64
 
@@ -104,7 +104,7 @@ If `deploy-chain` refuses with an address mismatch, the SMA was deployed against
104
104
  ## Gas requirements
105
105
 
106
106
  - Owner wallet: SMA deployment, mandate signing (EIP-712), any additional-chain deployment.
107
- - Agent wallet: `mandate deploy`, `mandate attach`, `mandate revoke`, and every dispatch submission once the agent runs, plus permission registration fees on fee-charging chains.
107
+ - Agent wallet: `mandate deploy`, `mandate attach`, `mandate revoke`, and every dispatch submission once the agent runs, plus permission registration fees on fee-charging chains. The registration fee is a **per-permission flat charge** (`permissionRegistrationFee()` on `SailGovernance`, read live — currently `0` on this deploy). A mandate of N permissions costs `N × fee` on top of gas; fund the agent wallet for the full amount before attaching. The fee is disclosed at sign time (`Registration fee: X ETH`) and recorded in the activity log. An underfunded agent wallet fails early with `Insufficient ETH for the X ETH registration fee` rather than reverting on-chain.
108
108
 
109
109
  `sailor doctor` — read-only preflight: kernel dispatch model, permission health, RPC reachability, gas balances in both wallets. Do not proceed to Stage 3 with a failing doctor.
110
110
 
@@ -28,3 +28,5 @@ Every command here is read-only and supports `--json` for machine reading. Prefe
28
28
  - "Is anything running?" — `.sail/runtime/ui.json` (dashboard), `.sail/runtime/server.json` (signing station), agent pid via `sailor status`.
29
29
 
30
30
  Run `sailor doctor` before any operation that spends gas — it is the cheapest way to catch a dead RPC, an unfunded wallet, or a kernel mismatch first.
31
+
32
+ - "What is the permission registration fee?" — `permissionRegistrationFee()` on `SailGovernance` (read live; currently `0` on this deploy). `sailor mandate sign` and `sailor mandate prepare` print the fee before signing. Each `permission_registered` entry in the activity log includes `fee` (wei) and `feeEth` (formatted), so Recent Activity shows the real cost paid per registration.
@@ -20,6 +20,7 @@ sailor ui start --expose tailscale # also serve it on the tailnet over HTTPS (
20
20
  - The server serves the pre-built React app (`SERVE_DIST=1`) and a local `/api` that reads `.sail/` state (`SAIL_DIR` env).
21
21
  - `ui start` does not block — no `&` needed.
22
22
  - `--expose tailscale` (optional): proxies the dashboard onto the operator's tailnet over HTTPS via `tailscale serve` (tailnet-private, never `funnel`). Requires `tailscale` installed + logged in, and Serve + HTTPS enabled for the tailnet (else the command prints the enable link). `ui stop` tears the proxy down. To allow extra browser origins, set `SAILOR_CORS_ORIGINS` (comma-separated; the local origin is always allowed).
23
+ - Binds `127.0.0.1` by default (local only). To expose without `--expose tailscale` — e.g. behind a reverse proxy on a domain — set `SAILOR_HOST=0.0.0.0`. The `/api` key-management endpoints are **unauthenticated**, so only do this behind your own auth (reverse-proxy basic-auth or a private tailnet), and throttle them with `SAILOR_RATE_LIMIT_PER_MIN` (default 100; or `rateLimitPerMin` in `.sail/config.json`).
23
24
 
24
25
  ## Signing station — `sailor station`
25
26
 
@@ -0,0 +1,76 @@
1
+ ---
2
+ name: sail-swap-quote
3
+ description: Fetch a live Uniswap V3 quote via QuoterV2 and compute a slippage-adjusted amountOutMinimum for a swap. Use after sail-token-resolve to show the user the current price and to produce the amountOutMinimum floor the agent must embed in every swap dispatch. Runs the bundled `scripts/quote-swap.mjs` (no dependencies, no gas).
4
+ ---
5
+
6
+ # sail-swap-quote — live price + amountOutMinimum floor
7
+
8
+ After [`sail-token-resolve`](../sail-token-resolve/SKILL.md) confirms a token is
9
+ swap-ready, get the actual number: how much tokenOut the user's amountIn buys
10
+ right now, and the `amountOutMinimum` floor that protects against slippage.
11
+
12
+ ## When to load
13
+
14
+ - After `sail-token-resolve` returned `swapReady: true`.
15
+ - Before presenting a mandate to the user (show them the current price).
16
+ - To validate `amountOutMinimum` before the agent dispatches a real swap.
17
+ - Whenever the user asks "what's the price of X?" or "how much WETH for 25 USDC?".
18
+
19
+ ## Run it
20
+
21
+ ```bash
22
+ # From the project root. Inputs come from sail-token-resolve's output.
23
+ node scripts/quote-swap.mjs \
24
+ --token-in 0x078D782b760474a361dDA0AF3839290b0EF57AD6 --decimals-in 6 \
25
+ --token-out 0x4200000000000000000000000000000000000006 --decimals-out 18 \
26
+ --amount 25000000 --fee 3000 --slippage-bps 100
27
+ ```
28
+
29
+ - `--amount` is in **base units** of tokenIn (25 USDC = `25000000`, not `25`).
30
+ - `--fee` is the tier from `sail-token-resolve` (deepest pool).
31
+ - `--slippage-bps` defaults to `100` (1%). Tighten (e.g. `50`) for stable pairs,
32
+ loosen for volatile ones. `0` removes the floor — testing only.
33
+ - `--chain` / `--rpc` override the project's active chain/RPC.
34
+
35
+ Output: one JSON object on stdout; human notes on stderr.
36
+
37
+ ## What it returns
38
+
39
+ ```json
40
+ {
41
+ "amountIn": "25000000", "amountOut": "15805313193992907",
42
+ "amountOutMinimum": "15647260062052977", // amountOut × (10000-slip)/10000
43
+ "slippageBps": 100, "fee": 3000,
44
+ "human": { "amountIn": "25.0", "amountOut": "0.015805313193992907",
45
+ "amountOutMinimum": "0.015647260062052977",
46
+ "price": "0.015805313193992907 out per 25.0 in" }
47
+ }
48
+ ```
49
+
50
+ ## How the floor is computed
51
+
52
+ ```
53
+ amountOutMinimum = amountOut × (10000 − slippageBps) / 10000
54
+ ```
55
+
56
+ At 1% slippage, `amountOut × 9900 / 10000`. The agent must embed a value **at
57
+ least this high** in its swap calldata's `amountOutMinimum` field — the
58
+ `SwapPermission` mandate rejects any swap whose `amountOutMinimum` is below the
59
+ pool-implied floor (fail-closed). Re-quote close to dispatch time; a stale quote
60
+ is the most common reason a legitimate swap gets rejected.
61
+
62
+ ## What to do with it
63
+
64
+ 1. **Show the user** the `human.price` line before they approve the mandate.
65
+ 2. **Record `amountOutMinimum`** as the floor the agent's dispatch logic must
66
+ respect. Hand the full quote to [`sail-template-swap`](../sail-template-swap/SKILL.md)
67
+ — the mandate's `maxSlippageBps` bound is the on-chain enforcement of this.
68
+ 3. **Re-quote at dispatch time** — pool state moves; a quote taken minutes ago may
69
+ no longer match.
70
+
71
+ ## If it fails
72
+
73
+ | Error | Meaning |
74
+ |---|---|
75
+ | `QuoterV2 reverted` / `amountOut == 0` | No pool at this fee tier (despite resolve-token). Try the next tier from resolve-token's `probedTiers`, or re-run resolve-token — liquidity may have shifted. |
76
+ | `No RPC for chain` | Run from the project root, or pass `--rpc`. |
@@ -0,0 +1,98 @@
1
+ ---
2
+ name: sail-template-approve-batch
3
+ description: Gate an SMA's "approve → call → reset" interactions by REUSING the shared ApproveAndCallBatchPermission singleton (Protocol/contracts/templates/ApproveAndCallBatchPermission.sol) — register + configure, no per-SMA deploy. Use when an agent must approve an ERC-20 to a protocol, make one consuming call, and reset the allowance to zero — all in one atomic batch — with token/spender/target/selector allowlists and a per-token approval cap. NOTE: `sailor mandate attach` only registers — you must also configure per-account (see steps).
4
+ compatibility: A Sailor project (`@sail/sdk`, `sailor` CLI). Requires ApproveAndCallBatchPermission 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/ApproveAndCallBatchPermission.sol
10
+ ---
11
+
12
+ # sail-template-approve-batch — atomic approve/call/reset via the shared singleton
13
+
14
+ Reuse the shared **`ApproveAndCallBatchPermission`** singleton — the safest way to bracket a
15
+ single protocol interaction that needs an allowance. Family overview + flow:
16
+ [`sail-templates`](../sail-templates/SKILL.md).
17
+
18
+ ## What it enforces (per account, from source)
19
+
20
+ Authorises exactly this 3-call batch:
21
+ ```
22
+ [0] approve(spender, amount) on an allowlisted ERC-20
23
+ [1] consuming call on an allowlisted (target, selector)
24
+ [2] approve(spender, 0) reset to zero (same atomic batch)
25
+ ```
26
+ Invariants: `tokens`, `spenders`, and `consumingPairs` must each be non-empty with no zero
27
+ addresses/selectors (`EmptyAllowlist` reverts at configure otherwise); `token ∈ tokens` and
28
+ `amount ≤ maxApprovalAmount[token]`; `spender ∈ spenders`; `(target, selector) ∈ consumingPairs`;
29
+ the pre-batch allowance on `(token, spender)` must be zero; the consuming call must target the
30
+ approved `spender` and pull the approved `token`; the reset-to-zero is required; if
31
+ `requireAmountMatch`, the consuming call's leading `uint256` arg must equal the approve amount.
32
+ Malformed calldata reverts (kernel treats revert as deny).
33
+
34
+ > **Output recipient — `allowUnconstrainedRecipient` (opt-out, default-safe).** By DEFAULT
35
+ > (`allowUnconstrainedRecipient = false`, i.e. omitted) the consuming call's output recipient is
36
+ > decoded and **must equal the SMA** — the safe posture — and any selector outside the decodable
37
+ > set below is denied (fail-closed). Set `allowUnconstrainedRecipient = true` ONLY to deliberately
38
+ > leave the recipient unconstrained (e.g. to authorize a selector outside the decodable set); then
39
+ > only allowlist `(target, selector)` pairs whose semantics you trust to deliver output to the SMA.
40
+ > ⚠️ This is an **opt-out** flag, not an opt-in — do not set it `true` thinking that "requires"
41
+ > or "pins" the recipient; that gets you the opposite (unconstrained) behavior on-chain.
42
+ >
43
+ > **Decodable set** (recipient AND consumed-asset both resolvable at a fixed calldata offset —
44
+ > any other selector is denied regardless of `allowUnconstrainedRecipient`, since the consumed
45
+ > asset can never be bound to the approved token): `swapExactTokensForTokens` (V2), V3
46
+ > `exactInputSingle` (both the SwapRouter and SwapRouter02 layouts), Aave V2 `deposit` / Aave V3
47
+ > `supply`, ERC-4626 `deposit` / `mint`.
48
+
49
+ ## Config blob (authoritative — `config-schemas.md`)
50
+
51
+ ```
52
+ abi.encode(Config{
53
+ address[] tokens; // approvable ERC-20s
54
+ address[] spenders; // allowed allowance recipients
55
+ ConsumingPair[] consumingPairs; // {address target; bytes4 selector} — pair bound together
56
+ uint256[] maxApprovalAmounts; // index-parallel with tokens; per-token cap
57
+ bool requireAmountMatch; // call[1] leading uint256 must equal approve amount
58
+ bool allowUnconstrainedRecipient; // OPT-OUT (default false = call[1] output recipient decoded, must == account)
59
+ })
60
+ struct ConsumingPair { address target; bytes4 selector; }
61
+ ```
62
+ `maxApprovalAmounts.length` must equal `tokens.length` (configure reverts otherwise).
63
+ `consumingPairs` is a **struct array** `(address target, bytes4 selector)[]` — NOT two flat
64
+ `address[]`/`bytes4[]` arrays. A selector is authorised only on the target it is paired with.
65
+
66
+ **ABI tuple (verified round-trip):** `(address[],address[],(address,bytes4)[],uint256[],bool,bool)`.
67
+ Encode with cast — note `selector` is a `bytes4` (left-aligned), and both trailing bools are required:
68
+ ```bash
69
+ cast abi-encode 'cfg((address[],address[],(address,bytes4)[],uint256[],bool,bool))' \
70
+ '([<token>],[<spender>],[(<target>,<selector>)],[<maxApproval>],<requireAmountMatch>,<allowUnconstrainedRecipient>)'
71
+ ```
72
+ `allowUnconstrainedRecipient` is an **opt-out** — leave it `false` unless you deliberately want
73
+ the consuming call's output recipient unconstrained (see the field note above).
74
+ The leading word is the `0x20` struct offset — that is correct, not flat-param corruption. A
75
+ generic (un-named) revert inside `_applyConfig` means the blob did not decode to this exact tuple
76
+ (usually: flat params with no `0x20` offset, the two flat arrays instead of `ConsumingPair[]`, or a
77
+ missing trailing bool). The named errors (`TokensAndAmountsLengthMismatch`, `EmptyAllowlist`,
78
+ `AllowlistTooLong`) fire only after a successful decode.
79
+
80
+ ## Steps
81
+
82
+ Register → configure → simulate → reconfigure mechanics live in
83
+ [`sail-templates` reuse-flow](../sail-templates/references/reuse-flow.md) — follow it.
84
+ `sailor mandate attach` registers only; `configureDirect` (owner tx) is the half that makes the
85
+ permission live. Template-specific bits:
86
+
87
+ - **Singleton:** `ApproveAndCallBatchPermission` — `node SKILLS/sail-templates/catalog.mjs --chain
88
+ <id>`.
89
+ - **Spec to confirm:** tokens+caps, spenders, `(target, selector)` consuming pairs,
90
+ `requireAmountMatch`, `allowUnconstrainedRecipient`. Get each selector with `cast sig` and verify
91
+ it delivers output to the SMA (leave `allowUnconstrainedRecipient = false`, the default, to
92
+ enforce that on-chain — set `true` only to deliberately opt out).
93
+ - **Blob — ⚠️ NOT flat params.** `abi.encode(Config{ … })` — a **single wrapped struct**, so the
94
+ blob starts with a `0x20` offset word. `consumingPairs` is `(address,bytes4)[]` (a struct array),
95
+ and both trailing bools are required — see the verified `cast abi-encode` form above. Unlike
96
+ transfer/withdraw/deposit/borrow/swap, wrapping is required here; a flat blob reverts at configure.
97
+ - **Simulate (mandatory — unaudited example):** the full allowed approve/call/reset batch passes;
98
+ missing reset, off-list spender/target/selector, or over-cap approve is rejected.
@@ -0,0 +1,68 @@
1
+ ---
2
+ name: sail-template-borrow
3
+ description: Gate an SMA's lending borrows by REUSING the shared BorrowPermission singleton (Protocol/contracts/templates/BorrowPermission.sol) — register + configure, no per-SMA deploy. Use for a bounded borrow mandate against Aave / Morpho / Compound with a protocol + asset allowlist, per-tx cap, and an on-chain LTV check via collateral + borrow oracles. NOTE: `sailor mandate attach` only registers — you must also configure per-account (see steps).
4
+ compatibility: A Sailor project (`@sail/sdk`, `sailor` CLI). Requires BorrowPermission 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/BorrowPermission.sol
10
+ ---
11
+
12
+ # sail-template-borrow — bounded lending borrow via the shared singleton
13
+
14
+ Reuse the shared **`BorrowPermission`** singleton. Family overview + flow:
15
+ [`sail-templates`](../sail-templates/SKILL.md).
16
+
17
+ ## What it enforces (per account, from source)
18
+
19
+ Selectors (any other ⇒ `false`): Aave `borrow(address,uint256,uint256,uint16,address)`
20
+ (**variable-rate only** — a stable-rate borrow, `rateMode != 2`, is rejected), Morpho
21
+ **Optimizer/Morpho-Aave** `borrow(address,uint256,address,address)` (**NOT Morpho Blue** — its
22
+ ABI differs and simply won't match this selector, i.e. fails closed; target a Blue-specific
23
+ permission instead), Compound `borrow(uint256)` (the call target is the **cToken**; the template
24
+ resolves `cToken.underlying()` and keys the allowlist/cap/LTV on the underlying — a target with
25
+ no `underlying()`, e.g. cETH, is denied). Invariants: `target ∈ protocols`; `asset ∈ assets`
26
+ (Aave/Morpho decode the asset; Compound resolves it via `underlying()`); `amount ≤
27
+ maxAmountPerTx`; `onBehalfOf`/`receiver == SMA`; **`_ltvCheck` passes** — the projected debt
28
+ value (via `borrowOracle`) against collateral value (via `collateralOracle`) must stay within
29
+ `maxLtvBps`, when oracles are configured (see ORACLE MODES below).
30
+
31
+ > **Oracle modes:** configure **zero** oracles for amount-cap-only borrowing (no LTV ceiling is
32
+ > applied at all, despite `maxLtvBps` being stored) or **both** — exactly one oracle reverts
33
+ > `OracleConfigInconsistent` at configure (a single feed can't price a ratio). When both are set,
34
+ > the check still depends on their honesty/freshness — see staleness via `maxPriceAgeSec`.
35
+
36
+ ## Config blob (authoritative — `config-schemas.md`)
37
+
38
+ ```
39
+ abi.encode(address[] protocols, address[] assets, uint256 maxAmountPerTx,
40
+ uint256 maxLtvBps, address collateralOracle, address borrowOracle,
41
+ uint256 maxPriceAgeSec)
42
+ ```
43
+ | Field | Notes |
44
+ |---|---|
45
+ | `protocols` | lending pools the agent may borrow from |
46
+ | `assets` | borrowable ERC-20 allowlist |
47
+ | `maxAmountPerTx` | per-borrow cap, base units |
48
+ | `maxLtvBps` | max loan-to-value, bps (e.g. `5000` = 50%) |
49
+ | `collateralOracle` / `borrowOracle` | price sources for the LTV check |
50
+ | `maxPriceAgeSec` | oracle freshness bound; `> 0` when oracles are set |
51
+
52
+ > `maxLtvBps` is a **per-transaction** projection, not a tracked lifetime LTV. For
53
+ > portfolio-level exposure, rely on the protocol's health factor or pair with a
54
+ > position-monitoring permission.
55
+
56
+ ## Steps
57
+
58
+ Register → configure → simulate → reconfigure mechanics (and the encoding gotcha) live in
59
+ [`sail-templates` reuse-flow](../sail-templates/references/reuse-flow.md) — follow it.
60
+ `sailor mandate attach` registers only; `configureDirect` (owner tx) is the half that makes the
61
+ permission live. Template-specific bits:
62
+
63
+ - **Singleton:** `BorrowPermission` — `node SKILLS/sail-templates/catalog.mjs --chain <id>`.
64
+ - **Spec to confirm:** protocols, assets, cap, max LTV, oracles.
65
+ - **Blob:** `abi.encode(protocols[], assets[], maxAmountPerTx, maxLtvBps, collateralOracle,
66
+ borrowOracle, maxPriceAgeSec)` — **flat params, no wrapper**.
67
+ - **Simulate (mandatory — unaudited example):** allowed borrow within LTV passes; over-cap,
68
+ over-LTV, or wrong recipient is rejected.
@@ -0,0 +1,61 @@
1
+ ---
2
+ name: sail-template-deposit
3
+ description: Gate an SMA's deposits into vaults / lending pools by REUSING the shared DepositPermission singleton (Protocol/contracts/templates/DepositPermission.sol) — register + configure, no per-SMA deploy. Use for a bounded deposit mandate into ERC-4626 vaults (deposit/mint) or Aave v2/v3 (deposit/supply) with a target + token allowlist and a per-tx cap; the resulting position always accrues to the SMA. NOTE: `sailor mandate attach` only registers — you must also configure per-account (see steps).
4
+ compatibility: A Sailor project (`@sail/sdk`, `sailor` CLI). Requires DepositPermission 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/DepositPermission.sol
10
+ ---
11
+
12
+ # sail-template-deposit — bounded vault/lending deposit via the shared singleton
13
+
14
+ Reuse the shared **`DepositPermission`** singleton. Family overview + flow:
15
+ [`sail-templates`](../sail-templates/SKILL.md). Discover yield targets first with
16
+ `sail-defillama-yields`.
17
+
18
+ ## What it enforces (per account, from source)
19
+
20
+ Selectors (any other ⇒ `false`):
21
+
22
+ | Function | Venue |
23
+ |---|---|
24
+ | `deposit(uint256 assets, address receiver)` | ERC-4626 / simple vault |
25
+ | `mint(uint256 shares, address receiver)` | ERC-4626 |
26
+ | `deposit(address asset, uint256 amount, address onBehalfOf, uint16)` | Aave v2 |
27
+ | `supply(address asset, uint256 amount, address onBehalfOf, uint16)` | Aave v3 |
28
+
29
+ Invariants: `value == 0`; `targets`/`tokens` must be non-empty with no zero addresses
30
+ (`EmptyAllowlist`/`ZeroAddress` revert at configure otherwise); `target ∈ targets`; token
31
+ allowlist enforced — Aave checks the `asset` arg, ERC-4626 requires the `target` (vault) itself
32
+ ∈ `tokens`; `amount`/`shares ≤ maxAmountPerTx`; `receiver`/`onBehalfOf == SMA`.
33
+
34
+ > `mint`'s cap is in **shares**, not underlying. At a high share price the effective asset cap
35
+ > is `maxAmountPerTx × sharePrice` — size accordingly. For ERC-4626 `deposit`/`mint` the token
36
+ > allowlist is the last line of defence (no asset in calldata) — only allowlist vaults whose
37
+ > accepted token you trust.
38
+
39
+ ## Config blob (authoritative — `config-schemas.md`)
40
+
41
+ ```
42
+ abi.encode(address[] targets, address[] tokens, uint256 maxAmountPerTx)
43
+ ```
44
+ | Field | Notes |
45
+ |---|---|
46
+ | `targets` | vault / lending-pool addresses |
47
+ | `tokens` | ERC-20 allowlist (the asset for Aave; the vault itself for ERC-4626) |
48
+ | `maxAmountPerTx` | per-deposit cap (assets, or shares for `mint`), base units |
49
+
50
+ ## Steps
51
+
52
+ Register → configure → simulate → reconfigure mechanics (and the encoding gotcha) live in
53
+ [`sail-templates` reuse-flow](../sail-templates/references/reuse-flow.md) — follow it.
54
+ `sailor mandate attach` registers only; `configureDirect` (owner tx) is the half that makes the
55
+ permission live. Template-specific bits:
56
+
57
+ - **Singleton:** `DepositPermission` — `node SKILLS/sail-templates/catalog.mjs --chain <id>`.
58
+ - **Spec to confirm:** targets, tokens, cap (note the share-price caveat for `mint`).
59
+ - **Blob:** `abi.encode(targets[], tokens[], maxAmountPerTx)` — **flat params, no wrapper**.
60
+ - **Simulate (mandatory — unaudited example):** allowed deposit within cap passes; off-list
61
+ target/token, over-cap, or `receiver != SMA` is rejected.
@@ -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.