@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
@@ -0,0 +1,133 @@
1
+ {
2
+ "_comment": "Reuse registry for the shared permission templates in Protocol/contracts/templates. These are CONFIGURABLE SINGLETONS: deployed ONCE, then every SMA reuses that address via attach + configure (no per-SMA deploy). All seven templates bind to the current CREATE2 kernel 0x38b508756c976e876EFF05a29E731A4d348BA6ED. Deploy is CREATE2 with a global salt (gitCommit 1dc1960, deploy version create2-2026-07-01), so the kernel and every template land at the SAME address on every chain — the per-chain blocks below are identical by construction, not independent deployments. Deployed by 0xB01dCE443d052e44b7D13726c0EC9fFB7f5815B6 on 2026-07-01. Supersedes the prior EOA-governed 2026-06-09 deploy (kernel 0x02ABC18B65A328de2e749F56ba79ACF2718a6659) — those addresses are dead; do not reuse them.",
3
+ "_schema": "sail.skills.templates.deployed/v1",
4
+ "kernel": "0x38b508756c976e876EFF05a29E731A4d348BA6ED",
5
+ "chains": {
6
+ "1": {
7
+ "ApproveAndCallBatchPermission": "0x0535A4D51333484ef583103DAB1a9449756ab732",
8
+ "BorrowPermission": "0x3e2666051599223cEAb10De55C89A0842857d8AF",
9
+ "DepositPermission": "0xBfB5e13a97b12Ee89d2F2b9B65eCf7e0E371911f",
10
+ "SwapPermission": "0x35cEEa0db96997Cc3CF3beB42FFa36A499342F7C",
11
+ "SwapPermissionNoOracle": "0x34Ba96CbEd1f46c88A5265E645DC5fe41662b519",
12
+ "TransferPermission": "0xda909a1CC584fb7559Ce4A828b008B473Da095e1",
13
+ "WithdrawPermission": "0xF5eF5dda450a130e3020d54f565E830e4a7531f8"
14
+ },
15
+ "8453": {
16
+ "ApproveAndCallBatchPermission": "0x0535A4D51333484ef583103DAB1a9449756ab732",
17
+ "BorrowPermission": "0x3e2666051599223cEAb10De55C89A0842857d8AF",
18
+ "DepositPermission": "0xBfB5e13a97b12Ee89d2F2b9B65eCf7e0E371911f",
19
+ "SwapPermission": "0x35cEEa0db96997Cc3CF3beB42FFa36A499342F7C",
20
+ "SwapPermissionNoOracle": "0x34Ba96CbEd1f46c88A5265E645DC5fe41662b519",
21
+ "TransferPermission": "0xda909a1CC584fb7559Ce4A828b008B473Da095e1",
22
+ "WithdrawPermission": "0xF5eF5dda450a130e3020d54f565E830e4a7531f8"
23
+ },
24
+ "42161": {
25
+ "ApproveAndCallBatchPermission": "0x0535A4D51333484ef583103DAB1a9449756ab732",
26
+ "BorrowPermission": "0x3e2666051599223cEAb10De55C89A0842857d8AF",
27
+ "DepositPermission": "0xBfB5e13a97b12Ee89d2F2b9B65eCf7e0E371911f",
28
+ "SwapPermission": "0x35cEEa0db96997Cc3CF3beB42FFa36A499342F7C",
29
+ "SwapPermissionNoOracle": "0x34Ba96CbEd1f46c88A5265E645DC5fe41662b519",
30
+ "TransferPermission": "0xda909a1CC584fb7559Ce4A828b008B473Da095e1",
31
+ "WithdrawPermission": "0xF5eF5dda450a130e3020d54f565E830e4a7531f8"
32
+ },
33
+ "10": {
34
+ "ApproveAndCallBatchPermission": "0x0535A4D51333484ef583103DAB1a9449756ab732",
35
+ "BorrowPermission": "0x3e2666051599223cEAb10De55C89A0842857d8AF",
36
+ "DepositPermission": "0xBfB5e13a97b12Ee89d2F2b9B65eCf7e0E371911f",
37
+ "SwapPermission": "0x35cEEa0db96997Cc3CF3beB42FFa36A499342F7C",
38
+ "SwapPermissionNoOracle": "0x34Ba96CbEd1f46c88A5265E645DC5fe41662b519",
39
+ "TransferPermission": "0xda909a1CC584fb7559Ce4A828b008B473Da095e1",
40
+ "WithdrawPermission": "0xF5eF5dda450a130e3020d54f565E830e4a7531f8"
41
+ },
42
+ "130": {
43
+ "ApproveAndCallBatchPermission": "0x0535A4D51333484ef583103DAB1a9449756ab732",
44
+ "BorrowPermission": "0x3e2666051599223cEAb10De55C89A0842857d8AF",
45
+ "DepositPermission": "0xBfB5e13a97b12Ee89d2F2b9B65eCf7e0E371911f",
46
+ "SwapPermission": "0x35cEEa0db96997Cc3CF3beB42FFa36A499342F7C",
47
+ "SwapPermissionNoOracle": "0x34Ba96CbEd1f46c88A5265E645DC5fe41662b519",
48
+ "TransferPermission": "0xda909a1CC584fb7559Ce4A828b008B473Da095e1",
49
+ "WithdrawPermission": "0xF5eF5dda450a130e3020d54f565E830e4a7531f8"
50
+ },
51
+ "56": {
52
+ "ApproveAndCallBatchPermission": "0x0535A4D51333484ef583103DAB1a9449756ab732",
53
+ "BorrowPermission": "0x3e2666051599223cEAb10De55C89A0842857d8AF",
54
+ "DepositPermission": "0xBfB5e13a97b12Ee89d2F2b9B65eCf7e0E371911f",
55
+ "SwapPermission": "0x35cEEa0db96997Cc3CF3beB42FFa36A499342F7C",
56
+ "SwapPermissionNoOracle": "0x34Ba96CbEd1f46c88A5265E645DC5fe41662b519",
57
+ "TransferPermission": "0xda909a1CC584fb7559Ce4A828b008B473Da095e1",
58
+ "WithdrawPermission": "0xF5eF5dda450a130e3020d54f565E830e4a7531f8"
59
+ },
60
+ "480": {
61
+ "ApproveAndCallBatchPermission": "0x0535A4D51333484ef583103DAB1a9449756ab732",
62
+ "BorrowPermission": "0x3e2666051599223cEAb10De55C89A0842857d8AF",
63
+ "DepositPermission": "0xBfB5e13a97b12Ee89d2F2b9B65eCf7e0E371911f",
64
+ "SwapPermission": "0x35cEEa0db96997Cc3CF3beB42FFa36A499342F7C",
65
+ "SwapPermissionNoOracle": "0x34Ba96CbEd1f46c88A5265E645DC5fe41662b519",
66
+ "TransferPermission": "0xda909a1CC584fb7559Ce4A828b008B473Da095e1",
67
+ "WithdrawPermission": "0xF5eF5dda450a130e3020d54f565E830e4a7531f8"
68
+ },
69
+ "999": {
70
+ "ApproveAndCallBatchPermission": "0x0535A4D51333484ef583103DAB1a9449756ab732",
71
+ "BorrowPermission": "0x3e2666051599223cEAb10De55C89A0842857d8AF",
72
+ "DepositPermission": "0xBfB5e13a97b12Ee89d2F2b9B65eCf7e0E371911f",
73
+ "SwapPermission": "0x35cEEa0db96997Cc3CF3beB42FFa36A499342F7C",
74
+ "SwapPermissionNoOracle": "0x34Ba96CbEd1f46c88A5265E645DC5fe41662b519",
75
+ "TransferPermission": "0xda909a1CC584fb7559Ce4A828b008B473Da095e1",
76
+ "WithdrawPermission": "0xF5eF5dda450a130e3020d54f565E830e4a7531f8"
77
+ },
78
+ "4326": {
79
+ "ApproveAndCallBatchPermission": "0x0535A4D51333484ef583103DAB1a9449756ab732",
80
+ "BorrowPermission": "0x3e2666051599223cEAb10De55C89A0842857d8AF",
81
+ "DepositPermission": "0xBfB5e13a97b12Ee89d2F2b9B65eCf7e0E371911f",
82
+ "SwapPermission": "0x35cEEa0db96997Cc3CF3beB42FFa36A499342F7C",
83
+ "SwapPermissionNoOracle": "0x34Ba96CbEd1f46c88A5265E645DC5fe41662b519",
84
+ "TransferPermission": "0xda909a1CC584fb7559Ce4A828b008B473Da095e1",
85
+ "WithdrawPermission": "0xF5eF5dda450a130e3020d54f565E830e4a7531f8"
86
+ },
87
+ "84532": {
88
+ "ApproveAndCallBatchPermission": "0x0535A4D51333484ef583103DAB1a9449756ab732",
89
+ "BorrowPermission": "0x3e2666051599223cEAb10De55C89A0842857d8AF",
90
+ "DepositPermission": "0xBfB5e13a97b12Ee89d2F2b9B65eCf7e0E371911f",
91
+ "SwapPermission": "0x35cEEa0db96997Cc3CF3beB42FFa36A499342F7C",
92
+ "SwapPermissionNoOracle": "0x34Ba96CbEd1f46c88A5265E645DC5fe41662b519",
93
+ "TransferPermission": "0xda909a1CC584fb7559Ce4A828b008B473Da095e1",
94
+ "WithdrawPermission": "0xF5eF5dda450a130e3020d54f565E830e4a7531f8"
95
+ },
96
+ "11155111": {
97
+ "ApproveAndCallBatchPermission": "0x0535A4D51333484ef583103DAB1a9449756ab732",
98
+ "BorrowPermission": "0x3e2666051599223cEAb10De55C89A0842857d8AF",
99
+ "DepositPermission": "0xBfB5e13a97b12Ee89d2F2b9B65eCf7e0E371911f",
100
+ "SwapPermission": "0x35cEEa0db96997Cc3CF3beB42FFa36A499342F7C",
101
+ "SwapPermissionNoOracle": "0x34Ba96CbEd1f46c88A5265E645DC5fe41662b519",
102
+ "TransferPermission": "0xda909a1CC584fb7559Ce4A828b008B473Da095e1",
103
+ "WithdrawPermission": "0xF5eF5dda450a130e3020d54f565E830e4a7531f8"
104
+ }
105
+ },
106
+ "_oraclesComment": "IOracle adapters (getPrice(base,quote)) usable as `priceOracle` in SwapPermission config. SwapPermission requires a non-zero IOracle adapter. Keyed chainId -> label. `pairs` lists the directional token pairs each adapter serves (both directions); any other pair reverts. catalog.mjs surfaces these so the swap skill can pick a default oracle. An IOracle adapter reads only its named pool/tokens — it has no kernel or template-address dependency, so it remains valid against the current CREATE2 SwapPermission singleton.",
107
+ "oracles": {
108
+ "130": {
109
+ "UniV3TwapOracle-USDC-WETH": {
110
+ "address": "0x9d84C11626d13C5DC9540fA12A3Ff7B85Ac3c1B9",
111
+ "kind": "uniswap-v3-twap",
112
+ "pool": "0x65081CB48d74A32e9CCfED75164b8c09972DBcF1",
113
+ "poolFeeTier": 500,
114
+ "twapWindowSec": 1800,
115
+ "priceDecimals": 18,
116
+ "pairs": [
117
+ { "base": "0x078D782b760474a361dDA0AF3839290b0EF57AD6", "quote": "0x4200000000000000000000000000000000000006", "label": "USDC/WETH" }
118
+ ],
119
+ "tokens": {
120
+ "USDC": "0x078D782b760474a361dDA0AF3839290b0EF57AD6",
121
+ "WETH": "0x4200000000000000000000000000000000000006"
122
+ },
123
+ "deployedBy": "0xB01dCE443d052e44b7D13726c0EC9fFB7f5815B6",
124
+ "tx": "0x3ca0d7232b623afda5e53c6d2319f4c3bbdc00a28b924c9f1d994bd3abfc2b3c",
125
+ "deployedAt": "2026-06-30",
126
+ "verified": "https://uniscan.xyz/address/0x9d84c11626d13c5dc9540fa12a3ff7b85ac3c1b9",
127
+ "source": "sail-templates/oracles/UniV3TwapOracle.sol",
128
+ "default": true,
129
+ "note": "Default priceOracle for the USDC<->WETH pair on Unichain. Live 30-min TWAP from the 0.05% V3 pool (cardinality 2000); updatedAt = block.timestamp, so it is always fresh against maxPriceAgeSec. Manipulation resistance scales with pool liquidity + window; reads a single pool. Use with maxPriceAgeSec >= a few minutes. Deployed before the create2-2026-07-01 migration but kernel-agnostic (reads only the named pool/tokens), so it remains a valid priceOracle for the current SwapPermission singleton (0x35cEEa0db96997Cc3CF3beB42FFa36A499342F7C)."
130
+ }
131
+ }
132
+ }
133
+ }
@@ -0,0 +1,13 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.26;
3
+
4
+ /// @title IOracle
5
+ /// @notice Minimal price oracle interface consumed by the oracle-gated permission templates.
6
+ /// Vendored verbatim from Protocol/contracts/interfaces/IOracle.sol so this adapter
7
+ /// compiles standalone; the consuming SwapPermission only cares about the ABI shape.
8
+ interface IOracle {
9
+ function getPrice(address base, address quote)
10
+ external
11
+ view
12
+ returns (uint256 price, uint8 decimals, uint256 updatedAt);
13
+ }
@@ -0,0 +1,114 @@
1
+ // SPDX-License-Identifier: GPL-2.0-or-later
2
+ pragma solidity 0.8.26;
3
+
4
+ import {IOracle} from "./IOracle.sol";
5
+ import {TickMath} from "./libs/TickMath.sol";
6
+ import {FullMath} from "./libs/FullMath.sol";
7
+
8
+ interface IUniswapV3Pool {
9
+ function token0() external view returns (address);
10
+ function token1() external view returns (address);
11
+ function fee() external view returns (uint24);
12
+ function observe(uint32[] calldata secondsAgos)
13
+ external
14
+ view
15
+ returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s);
16
+ }
17
+
18
+ /// @title UniV3TwapOracle — single-pool Uniswap V3 TWAP adapter implementing IOracle
19
+ /// @notice A read-only `IOracle` adapter for the oracle-gated launch templates (SwapPermission).
20
+ /// It serves ONE Uniswap V3 pool and the single directional token pair it contains
21
+ /// (both directions). The price is a time-weighted average over `twapWindow` seconds,
22
+ /// read from `pool.observe`, so it is manipulation-resistant over that window (a one-block
23
+ /// spot move barely shifts the average) and needs no external price provider or per-call
24
+ /// update fee.
25
+ ///
26
+ /// IORACLE SEMANTICS. getPrice(base, quote) returns (price, decimals, updatedAt) such that
27
+ /// 1 base base-unit = price / 10**decimals quote base-units. `decimals` is fixed at 18.
28
+ /// `updatedAt` is the current block timestamp: a TWAP is computed live at call time, so it
29
+ /// is always fresh against the consuming template's `maxPriceAgeSec`.
30
+ ///
31
+ /// BOUNDARIES (honest). This is NOT a USD oracle — it reports the relative price of the two
32
+ /// pool tokens, which is exactly what SwapPermission's band needs. Manipulation resistance
33
+ /// is only as strong as (a) the pool's liquidity and (b) the chosen window: a long-lived,
34
+ /// well-capitalised attacker can still drag a TWAP. It reads a SINGLE pool — if that pool is
35
+ /// drained of liquidity the average degrades. `observe` reverts ("OLD") if the pool's
36
+ /// observation buffer does not yet span `twapWindow`; that revert propagates and the
37
+ /// consuming template fails closed (deny). Unsupported pairs revert `UnsupportedPair`.
38
+ contract UniV3TwapOracle is IOracle {
39
+ /// @notice Quote precision returned by getPrice (price is scaled by 10**18).
40
+ uint8 public constant PRICE_DECIMALS = 18;
41
+
42
+ address public immutable pool;
43
+ address public immutable token0;
44
+ address public immutable token1;
45
+ uint32 public immutable twapWindow;
46
+
47
+ error UnsupportedPair(address base, address quote);
48
+ error WindowZero();
49
+
50
+ /// @param _pool Uniswap V3 pool to read.
51
+ /// @param _twapWindow TWAP averaging window in seconds (must be > 0 and supported by the pool's
52
+ /// observation cardinality, else getPrice reverts).
53
+ constructor(address _pool, uint32 _twapWindow) {
54
+ if (_twapWindow == 0) revert WindowZero();
55
+ pool = _pool;
56
+ token0 = IUniswapV3Pool(_pool).token0();
57
+ token1 = IUniswapV3Pool(_pool).token1();
58
+ twapWindow = _twapWindow;
59
+ }
60
+
61
+ /// @inheritdoc IOracle
62
+ function getPrice(address base, address quote)
63
+ external
64
+ view
65
+ returns (uint256 price, uint8 decimals, uint256 updatedAt)
66
+ {
67
+ bool supported = (base == token0 && quote == token1) || (base == token1 && quote == token0);
68
+ if (!supported) revert UnsupportedPair(base, quote);
69
+
70
+ int24 tick = _consultMeanTick();
71
+ // quoteAmount for a base input of 10**PRICE_DECIMALS == price scaled to PRICE_DECIMALS.
72
+ price = _getQuoteAtTick(tick, uint128(10 ** uint256(PRICE_DECIMALS)), base, quote);
73
+ decimals = PRICE_DECIMALS;
74
+ updatedAt = block.timestamp;
75
+ }
76
+
77
+ /// @dev Time-weighted mean tick over `twapWindow`, rounded toward negative infinity (Uniswap's
78
+ /// OracleLibrary convention).
79
+ function _consultMeanTick() internal view returns (int24) {
80
+ uint32[] memory secondsAgos = new uint32[](2);
81
+ secondsAgos[0] = twapWindow;
82
+ secondsAgos[1] = 0;
83
+
84
+ (int56[] memory tickCumulatives, ) = IUniswapV3Pool(pool).observe(secondsAgos);
85
+ int56 delta = tickCumulatives[1] - tickCumulatives[0];
86
+ int56 window = int56(uint56(twapWindow));
87
+
88
+ int24 meanTick = int24(delta / window);
89
+ if (delta < 0 && (delta % window != 0)) meanTick--;
90
+ return meanTick;
91
+ }
92
+
93
+ /// @dev Uniswap OracleLibrary.getQuoteAtTick: quote-token amount for `baseAmount` of base token
94
+ /// at `tick`, honouring token ordering (no decimal normalisation — raw base units).
95
+ function _getQuoteAtTick(int24 tick, uint128 baseAmount, address baseToken, address quoteToken)
96
+ internal
97
+ pure
98
+ returns (uint256 quoteAmount)
99
+ {
100
+ uint160 sqrtRatioX96 = TickMath.getSqrtRatioAtTick(tick);
101
+
102
+ if (sqrtRatioX96 <= type(uint128).max) {
103
+ uint256 ratioX192 = uint256(sqrtRatioX96) * sqrtRatioX96;
104
+ quoteAmount = baseToken < quoteToken
105
+ ? FullMath.mulDiv(ratioX192, baseAmount, 1 << 192)
106
+ : FullMath.mulDiv(1 << 192, baseAmount, ratioX192);
107
+ } else {
108
+ uint256 ratioX128 = FullMath.mulDiv(sqrtRatioX96, sqrtRatioX96, 1 << 64);
109
+ quoteAmount = baseToken < quoteToken
110
+ ? FullMath.mulDiv(ratioX128, baseAmount, 1 << 128)
111
+ : FullMath.mulDiv(1 << 128, baseAmount, ratioX128);
112
+ }
113
+ }
114
+ }
@@ -0,0 +1,62 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.26;
3
+
4
+ /// @title Contains 512-bit math functions
5
+ /// @notice Vendored from Uniswap v3-core (0.8 port). Facilitates multiplication and division that
6
+ /// can have overflow of an intermediate value without any loss of precision.
7
+ library FullMath {
8
+ /// @notice Calculates floor(a×b÷denominator) with full precision.
9
+ function mulDiv(uint256 a, uint256 b, uint256 denominator) internal pure returns (uint256 result) {
10
+ unchecked {
11
+ uint256 prod0;
12
+ uint256 prod1;
13
+ assembly {
14
+ let mm := mulmod(a, b, not(0))
15
+ prod0 := mul(a, b)
16
+ prod1 := sub(sub(mm, prod0), lt(mm, prod0))
17
+ }
18
+
19
+ if (prod1 == 0) {
20
+ require(denominator > 0);
21
+ assembly {
22
+ result := div(prod0, denominator)
23
+ }
24
+ return result;
25
+ }
26
+
27
+ require(denominator > prod1);
28
+
29
+ uint256 remainder;
30
+ assembly {
31
+ remainder := mulmod(a, b, denominator)
32
+ }
33
+ assembly {
34
+ prod1 := sub(prod1, gt(remainder, prod0))
35
+ prod0 := sub(prod0, remainder)
36
+ }
37
+
38
+ uint256 twos = denominator & (~denominator + 1);
39
+ assembly {
40
+ denominator := div(denominator, twos)
41
+ }
42
+ assembly {
43
+ prod0 := div(prod0, twos)
44
+ }
45
+ assembly {
46
+ twos := add(div(sub(0, twos), twos), 1)
47
+ }
48
+ prod0 |= prod1 * twos;
49
+
50
+ uint256 inv = (3 * denominator) ^ 2;
51
+ inv *= 2 - denominator * inv;
52
+ inv *= 2 - denominator * inv;
53
+ inv *= 2 - denominator * inv;
54
+ inv *= 2 - denominator * inv;
55
+ inv *= 2 - denominator * inv;
56
+ inv *= 2 - denominator * inv;
57
+
58
+ result = prod0 * inv;
59
+ return result;
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,44 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.26;
3
+
4
+ /// @title Math library for computing sqrt prices from ticks
5
+ /// @notice Vendored from Uniswap v3-core (0.8 port). Only getSqrtRatioAtTick is needed here.
6
+ library TickMath {
7
+ int24 internal constant MIN_TICK = -887272;
8
+ int24 internal constant MAX_TICK = 887272;
9
+
10
+ /// @notice Calculates sqrt(1.0001^tick) * 2^96
11
+ function getSqrtRatioAtTick(int24 tick) internal pure returns (uint160 sqrtPriceX96) {
12
+ unchecked {
13
+ uint256 absTick = tick < 0 ? uint256(-int256(tick)) : uint256(int256(tick));
14
+ require(absTick <= uint256(int256(MAX_TICK)), "T");
15
+
16
+ uint256 ratio = absTick & 0x1 != 0 ? 0xfffcb933bd6fad37aa2d162d1a594001 : 0x100000000000000000000000000000000;
17
+ if (absTick & 0x2 != 0) ratio = (ratio * 0xfff97272373d413259a46990580e213a) >> 128;
18
+ if (absTick & 0x4 != 0) ratio = (ratio * 0xfff2e50f5f656932ef12357cf3c7fdcc) >> 128;
19
+ if (absTick & 0x8 != 0) ratio = (ratio * 0xffe5caca7e10e4e61c3624eaa0941cd0) >> 128;
20
+ if (absTick & 0x10 != 0) ratio = (ratio * 0xffcb9843d60f6159c9db58835c926644) >> 128;
21
+ if (absTick & 0x20 != 0) ratio = (ratio * 0xff973b41fa98c081472e6896dfb254c0) >> 128;
22
+ if (absTick & 0x40 != 0) ratio = (ratio * 0xff2ea16466c96a3843ec78b326b52861) >> 128;
23
+ if (absTick & 0x80 != 0) ratio = (ratio * 0xfe5dee046a99a2a811c461f1969c3053) >> 128;
24
+ if (absTick & 0x100 != 0) ratio = (ratio * 0xfcbe86c7900a88aedcffc83b479aa3a4) >> 128;
25
+ if (absTick & 0x200 != 0) ratio = (ratio * 0xf987a7253ac413176f2b074cf7815e54) >> 128;
26
+ if (absTick & 0x400 != 0) ratio = (ratio * 0xf3392b0822b70005940c7a398e4b70f3) >> 128;
27
+ if (absTick & 0x800 != 0) ratio = (ratio * 0xe7159475a2c29b7443b29c7fa6e889d9) >> 128;
28
+ if (absTick & 0x1000 != 0) ratio = (ratio * 0xd097f3bdfd2022b8845ad8f792aa5825) >> 128;
29
+ if (absTick & 0x2000 != 0) ratio = (ratio * 0xa9f746462d870fdf8a65dc1f90e061e5) >> 128;
30
+ if (absTick & 0x4000 != 0) ratio = (ratio * 0x70d869a156d2a1b890bb3df62baf32f7) >> 128;
31
+ if (absTick & 0x8000 != 0) ratio = (ratio * 0x31be135f97d08fd981231505542fcfa6) >> 128;
32
+ if (absTick & 0x10000 != 0) ratio = (ratio * 0x9aa508b5b7a84e1c677de54f3e99bc9) >> 128;
33
+ if (absTick & 0x20000 != 0) ratio = (ratio * 0x5d6af8dedb81196699c329225ee604) >> 128;
34
+ if (absTick & 0x40000 != 0) ratio = (ratio * 0x2216e584f5fa1ea926041bedfe98) >> 128;
35
+ if (absTick & 0x80000 != 0) ratio = (ratio * 0x48a170391f7dc42444e8fa2) >> 128;
36
+
37
+ if (tick > 0) ratio = type(uint256).max / ratio;
38
+
39
+ // this divides by 1<<32 rounding up to go from a Q128.128 to a Q128.96.
40
+ // we then downcast because the result always fits within 160 bits due to our tick input constraint
41
+ sqrtPriceX96 = uint160((ratio >> 32) + (ratio % (1 << 32) == 0 ? 0 : 1));
42
+ }
43
+ }
44
+ }
@@ -0,0 +1,139 @@
1
+ # Config schemas & enforced invariants — authoritative (from source)
2
+
3
+ Each shared template stores per-account config under `mapping(address => …)` and decodes a
4
+ `configure(...)` blob in `_applyConfig`. The tuples below are taken **directly from
5
+ `Protocol/contracts/templates/*.sol`** and are the source of truth — encode with
6
+ `abi.encode(...)` in that exact order. (The `@sail/sdk/templates` builders track a
7
+ previously-deployed set; verify a builder's params match the tuple here before using it.)
8
+
9
+ All caps/amounts are in the relevant token's **base units**. Every template runs under the
10
+ kernel's `staticcall` + gas cap + fail-closed semantics: a revert ⇒ deny.
11
+
12
+ ---
13
+
14
+ ## SwapPermission
15
+ ```
16
+ abi.encode(address[] routers, address[] tokensIn, address[] tokensOut,
17
+ uint256 maxAmountPerTx, uint256 maxSlippageBps,
18
+ address priceOracle, uint256 maxPriceAgeSec)
19
+ ```
20
+ Selectors: `0x414bf389` (V3 `exactInputSingle` w/ deadline), `0x04e45aaf` (V3-02 no deadline),
21
+ `0x38ed1739` (V2 `swapExactTokensForTokens`). Invariants: `value == 0` (native value rejected);
22
+ `target ∈ routers`; **`tokenIn != tokenOut` / `path[0] != path[last]`** (self-routes denied — a
23
+ round-trip would burn AMM fees while an oracle reporting base==quote clears the band);
24
+ `tokenIn`/`path[0] ∈ tokensIn`; `tokenOut`/`path[last] ∈ tokensOut`;
25
+ `recipient`/`to == account`; `amountIn ≤ maxAmountPerTx`; **oracle slippage band ALWAYS enforced**.
26
+ The oracle is **mandatory** (contract `v2`): `_applyConfig` reverts `OracleRequired` if
27
+ `priceOracle == 0`, `MissingPriceAge` if `maxPriceAgeSec == 0`, and `SlippageBpsTooLarge` if
28
+ `maxSlippageBps > 9_999`. `maxSlippageBps == 0` ⇒ zero tolerance (strictest), NOT a bypass.
29
+ `priceOracle` is an `IOracle` adapter (`getPrice(base,quote) → (price, dec, updatedAt)`), not a raw
30
+ feed; dust trades whose floor truncates to 0 are denied. V2 intermediate hops are NOT checked.
31
+ (For no-oracle tokens use `SwapPermissionNoOracle`.)
32
+
33
+ ## SwapPermissionNoOracle
34
+ ```
35
+ abi.encode(address[] routers, address[] tokensIn, address[] tokensOut,
36
+ uint256 maxAmountPerTx, ReferencePool[] referencePools)
37
+
38
+ struct ReferencePool { address tokenIn; address tokenOut; address pool;
39
+ PoolKind kind /* 0=V2, 1=V3 */; uint256 toleranceBps; }
40
+ ```
41
+ Same selectors and structural checks as `SwapPermission` (`0x414bf389`, `0x04e45aaf`,
42
+ `0x38ed1739`): `target ∈ routers`; `tokenIn`/`path[0] ∈ tokensIn`; `tokenOut`/`path[last] ∈
43
+ tokensOut`; `recipient`/`to == account`; `amountIn ≤ maxAmountPerTx`. **Differs** from
44
+ `SwapPermission` in the price judgement: instead of an oracle band it enforces a **pool-referenced
45
+ hallucination band** — `amountOutMin` must be ≥ the output implied by the live spot price of the
46
+ operator-named `referencePool` for that directional pair, minus `toleranceBps`, and `amountOutMin
47
+ > 0`. Config-time invariants (`_applyConfig` reverts ⇒ configure fails): `toleranceBps ≤ 5_000`
48
+ (50% max); `pool != 0`; pool's `token0()`/`token1()` must match the pair (orientation precomputed);
49
+ **strict coverage** — every non-self directional `(tokenIn, tokenOut)` combination MUST have a
50
+ reference pool or configure reverts `MissingReferencePool`. Fail-closed at evaluate if the pool is
51
+ missing/unreadable/illiquid or the floor truncates to zero.
52
+
53
+ > ⚠️ **NOT manipulation-resistant.** The reference is a single pool's LIVE spot price, movable
54
+ > within the same transaction (flash-loan / sandwich). This band only catches an *honest* mistake
55
+ > (a confused agent quoting a wildly wrong price); it is NOT slippage protection. For
56
+ > manipulation-resistant protection use the oracle-gated `SwapPermission`. Cap is per-tx, not
57
+ > cumulative.
58
+
59
+ ## BorrowPermission
60
+ ```
61
+ abi.encode(address[] protocols, address[] assets, uint256 maxAmountPerTx,
62
+ uint256 maxLtvBps, address collateralOracle, address borrowOracle,
63
+ uint256 maxPriceAgeSec)
64
+ ```
65
+ Selectors: Aave `borrow(address,uint256,uint256,uint16,address)` (variable-rate only —
66
+ `rateMode != 2` denies; stable-rate debt is rejected), Morpho **Optimizer/Morpho-Aave**
67
+ `borrow(address,uint256,address,address)` (NOT Morpho Blue — its ABI differs and simply won't
68
+ match, i.e. fails closed), Compound `borrow(uint256)` (target is the cToken; the underlying is
69
+ resolved via `cToken.underlying()` and both the allowlist and LTV are underlying-denominated —
70
+ targets with no `underlying()`, e.g. cETH, are denied). Invariants: `protocols`/`assets` must be
71
+ non-empty with no zero addresses (`EmptyAllowlist`/`ZeroAddress` revert otherwise);
72
+ `target ∈ protocols`; `asset ∈ assets`; `amount ≤ maxAmountPerTx`; `onBehalfOf`/`receiver ==
73
+ account`. **Oracle modes:** zero oracles ⇒ amount-cap-only (no LTV ceiling applied at all, despite
74
+ `maxLtvBps` being stored); both oracles set ⇒ `_ltvCheck` enforces the LTV bound. Exactly one
75
+ oracle set reverts `OracleConfigInconsistent` at configure — it's an all-or-nothing pair.
76
+
77
+ ## TransferPermission
78
+ ```
79
+ abi.encode(address[] allowedRecipients, address[] allowedTokens, uint256 maxAmountPerTx)
80
+ ```
81
+ Selectors: `0xa9059cbb` `transfer`, `0x23b872dd` `transferFrom`. Invariants: `value == 0`;
82
+ `allowedRecipients`/`allowedTokens` must be non-empty with no zero addresses
83
+ (`EmptyAllowlist`/`ZeroAddress` revert at configure otherwise); `target (token) ∈ allowedTokens`;
84
+ `to ∈ allowedRecipients`; `amount ≤ maxAmountPerTx`;
85
+ **`transferFrom` requires `from == account`** (stricter than the old TransferTarget).
86
+ Max 50 entries per allowlist.
87
+
88
+ ## DepositPermission
89
+ ```
90
+ abi.encode(address[] targets, address[] tokens, uint256 maxAmountPerTx)
91
+ ```
92
+ Selectors: `deposit(uint256,address)` (ERC-4626), `mint(uint256,address)`,
93
+ `deposit(address,uint256,address,uint16)` (Aave v2), `supply(address,uint256,address,uint16)`
94
+ (Aave v3). Invariants: `value == 0`; `targets`/`tokens` must be non-empty with no zero addresses
95
+ (`EmptyAllowlist`/`ZeroAddress` revert at configure otherwise); `target ∈ targets`; token
96
+ allowlist enforced (Aave: the `asset` arg; ERC-4626: `target` itself must be in `tokens`);
97
+ `amount`/`shares ≤ maxAmountPerTx`; `receiver`/`onBehalfOf == account`. `mint` cap is in
98
+ **shares** — account for share price.
99
+
100
+ ## WithdrawPermission
101
+ ```
102
+ abi.encode(address[] tokens, address allowedRecipient, uint256 maxAmountPerTx)
103
+ ```
104
+ Selectors: `0xa9059cbb` `transfer`, `0x23b872dd` `transferFrom`. Invariants: `value == 0`;
105
+ `tokens` must be non-empty and `allowedRecipient` non-zero, with no zero-address tokens
106
+ (`EmptyAllowlist`/`ZeroAddress` revert at configure otherwise); `target (token) ∈ tokens`;
107
+ `to == allowedRecipient` (single address per config); `amount ≤ maxAmountPerTx`;
108
+ **`transferFrom` requires `from == account`**. To change the recipient, `reconfigure` with a new
109
+ blob.
110
+
111
+ ## ApproveAndCallBatchPermission
112
+ ```
113
+ abi.encode(Config{
114
+ address[] tokens; address[] spenders; ConsumingPair[] consumingPairs;
115
+ uint256[] maxApprovalAmounts; bool requireAmountMatch; bool allowUnconstrainedRecipient;
116
+ })
117
+ struct ConsumingPair { address target; bytes4 selector; }
118
+ ```
119
+ ABI tuple: `(address[],address[],(address,bytes4)[],uint256[],bool,bool)`. `consumingPairs` is a
120
+ **struct array**, not two flat arrays — each `(target, selector)` is bound together, so a selector
121
+ is authorised only on its paired target. Authorises exactly the 3-call batch:
122
+ `approve(spender, amount)` → consuming call → `approve(spender, 0)`. Invariants: `tokens`,
123
+ `spenders`, and `consumingPairs` must each be non-empty and contain no zero addresses/selectors
124
+ (`EmptyAllowlist` reverts otherwise); token ∈ `tokens` with `amount ≤ maxApprovalAmount[token]`;
125
+ `spender ∈ spenders`; `(target, selector)` ∈ `consumingPairs`; the pre-batch allowance on
126
+ `(token, spender)` must be zero; the consuming call must target the approved `spender` and pull the
127
+ approved `token`; allowance reset to zero in the same batch; if `requireAmountMatch`, the consuming
128
+ call's leading `uint256` arg must equal the approve amount. `maxApprovalAmounts` is index-parallel
129
+ with `tokens` (length mismatch reverts).
130
+
131
+ > ⚠️ **Field name/polarity — `allowUnconstrainedRecipient`, NOT `requireRecipientIsAccount`.**
132
+ > The field is an **opt-out**, default-safe: `false` (the default, i.e. an omitted/zero-value
133
+ > bool) means the consuming call's output recipient is decoded and **must equal the account** —
134
+ > the safe posture. `true` is a deliberate opt-out that leaves the recipient **unconstrained**.
135
+ > Setting the bool `true` thinking it "requires/pins the recipient" gets the **opposite**
136
+ > behaviour on-chain. Either way, the recipient (and the consumed asset) can only be decoded for
137
+ > selectors in the fixed-offset set below — any other selector is denied under the default pin:
138
+ > `swapExactTokensForTokens`, V3 `exactInputSingle` (both SwapRouter layouts), Aave V2/V3
139
+ > `deposit`/`supply`, ERC-4626 `deposit`/`mint`.
@@ -0,0 +1,139 @@
1
+ # Reuse-first flow — register + configure a shared template
2
+
3
+ The whole point of a **shared** template: the logic is already deployed, so you skip the
4
+ deploy entirely. You give one SMA its own private config inside the singleton's
5
+ `mapping(address => …)` and register it on the kernel.
6
+
7
+ > **Two steps, not one (today).** The on-chain **intended** design is a single signed call —
8
+ > `MandateFactory.attach(account, template, params, configureDeadline, configureSig, kernelSig)`
9
+ > — that registers the template on the kernel AND writes its per-account config together.
10
+ > **The shipped `sailor` CLI does not implement that combined call yet.** `sailor mandate
11
+ > attach` performs **only the kernel registration** (`RegisterPermission` / the batch
12
+ > `registerPermissions`); it never calls `configure`/`configureDirect`, carries no `--params`,
13
+ > and builds no `Configure` EIP-712 signature. The SDK's `client.mandate.attach(...)` is a
14
+ > `notImplemented()` stub.
15
+ >
16
+ > A registered-but-unconfigured singleton has `isConfigured == false`, so the kernel's
17
+ > `evaluate()` **denies every call** — a registered-but-dead permission. You MUST configure
18
+ > separately, with `sailor mandate configure` (step 4 below). The flow below describes what
19
+ > actually works now.
20
+
21
+ ## On-chain mechanics (`MandateFactory` + `ConfigurablePermission`)
22
+
23
+ These are the **on-chain** entry points — the contract truth for what the combined/intended
24
+ flow does. The shipped CLI uses a subset of them today (registration only); the configure
25
+ half is driven directly for now.
26
+
27
+ Registration (kernel side — what `sailor mandate attach` does today):
28
+
29
+ | Function | Use |
30
+ |---|---|
31
+ | kernel `registerPermission(account, permission, deadline, sig)` | Register one template address on an SMA |
32
+ | kernel `registerPermissions(account, permissions[], deadline, sig)` | Register several in one signature (the `--address a,b,c` batch path) |
33
+ | kernel `revokePermissions(account, permissions[], deadline, sig)` | Remove (`sailor mandate revoke`) |
34
+
35
+ Configuration (singleton side — the half the CLI does NOT do today):
36
+
37
+ | Function | Use |
38
+ |---|---|
39
+ | `configure(account, params, deadline, sig)` | Set per-account config, gated by an EIP-712 `Configure(account, paramsHash, nonce, deadline, epoch)` signature from the account's `permissionSigner` (ECDSA or ERC-1271). |
40
+ | `configureDirect(account, params)` | Set per-account config, gated by `msg.sender == permissionSigner`. The working path when the owner IS the permissionSigner — a plain owner tx, no signature. |
41
+ | `reconfigure(...)` | Intended batch re-config path (`MandateFactory.reconfigure`); functionally `configure` again with a new blob. |
42
+
43
+ The intended combined call (`MandateFactory.attach`) and its batch/replace/detach siblings are
44
+ defined on the factory contract; they are the target state for a future one-step CLI command,
45
+ not something the current CLI drives.
46
+
47
+ The kernel evaluates a registered permission via `staticcall` with a per-permission gas cap;
48
+ a revert or over-gas is treated as `false` (fail-closed), never a kernel revert.
49
+
50
+ ## The steps (as they work today)
51
+
52
+ ### 1. Discover the address
53
+ ```bash
54
+ node SKILLS/sail-templates/catalog.mjs --chain <id>
55
+ ```
56
+ The address comes from [`deployed.json`](../deployed.json). **If the template isn't deployed on
57
+ your chain yet** (e.g. `SwapPermissionNoOracle` is not deployed anywhere), that's the
58
+ prerequisite: deploy the singleton once (Protocol team / `DeploySharedTemplates`) and record
59
+ its address there. A template address is only meaningful paired with that chain's kernel.
60
+
61
+ ### 2. Build the config blob
62
+ Encode the config tuple **exactly as the contract's `_applyConfig` decodes it** — the
63
+ authoritative tuples are in [config-schemas.md](config-schemas.md). Use `abi.encode(...)` in
64
+ that order, or the typed builder under `@sail/sdk/templates` **only after** verifying its param
65
+ tuple equals the source tuple (the SDK builders track a previously-deployed set and may differ
66
+ from these source contracts):
67
+ ```ts
68
+ import { boundedSwapTemplate } from "@sail/sdk/templates"; // verify params vs config-schemas.md first
69
+
70
+ const params = { routers, tokensIn, tokensOut, maxAmountPerTx, maxSlippageBps,
71
+ priceOracle, maxPriceAgeSec };
72
+ const blob = boundedSwapTemplate.encoder.encode(params);
73
+
74
+ // Always surface the human-readable summary + warnings to the user first:
75
+ const explanation = boundedSwapTemplate.explainer.explain(params);
76
+ ```
77
+
78
+ > ⚠️ **Encoding gotcha (fails closed).** Templates do NOT all encode the same way.
79
+ > `TransferPermission` / `WithdrawPermission` / `DepositPermission` decode **flat top-level
80
+ > params** (`abi.encode(address[], address[], uint256)`). `ApproveAndCallBatchPermission`
81
+ > decodes a **single wrapped struct** (`abi.encode(Config{…})`) — its blob starts with a `0x20`
82
+ > tuple-offset word. Wrapping a flat-params blob (or vice-versa) reverts at configure. **Always
83
+ > round-trip-decode the blob and `cast call`-simulate `configureDirect` before sending.**
84
+
85
+ ### 3. Register the singleton on the kernel (does NOT configure)
86
+ ```bash
87
+ sailor mandate attach --address <SHARED_ADDRESS> --sma <SMA> --label "<primitive>"
88
+ ```
89
+ This builds and submits the kernel `RegisterPermission` (or `registerPermissions` for a
90
+ comma-separated `--address` list). It does NOT call `configure`. After this step the address is
91
+ in `getPermissions(<SMA>)` but `isConfigured(<SMA>) == false`, so every dispatch is still
92
+ denied. Proceed to step 4.
93
+
94
+ ### 4. Configure the per-account bounds (the half that makes it live)
95
+ When the owner IS the SMA's `permissionSigner` (the default Sailor onboarding), drive
96
+ `configureDirect(account, <config blob>)` as a plain owner transaction — `msg.sender ==
97
+ permissionSigner` holds and no EIP-712 signature is needed — via the shipped command:
98
+ ```bash
99
+ sailor mandate configure --address <SHARED_ADDRESS> --sma <SMA> \
100
+ --template <TemplateName> --args-file ./config.json
101
+ # or with a pre-encoded blob:
102
+ sailor mandate configure --address <SHARED_ADDRESS> --sma <SMA> --params <0x-blob>
103
+ ```
104
+ It does, in order:
105
+ 1. **Pre-flight (no gas):** an `eth_call` simulation of `configureDirect` from `permissionSigner`. A revert here means the config is invalid before any gas is spent — fix the blob (see the encoding gotcha) and retry. Pass `--simulate-only` to stop here.
106
+ 2. **Send it:** pushes the `configureDirect` call to the owner wallet as an `arbitrary-tx` request through the signing station; the owner approves in the browser and the owner wallet sends the transaction.
107
+ 3. **Verify:** reads `isConfigured(<SMA>)` on the singleton and errors if it isn't `true`.
108
+
109
+ If the owner is **not** the `permissionSigner` (e.g. a separate mandate-signer / multisig), use
110
+ `configure(account, params, deadline, sig)` instead: construct the EIP-712 `Configure` digest
111
+ (bound to the kernel's current `registrationEpoch`), have the `permissionSigner` sign it, and
112
+ submit. `sailor mandate configure` does not build this signature for you — that path is still
113
+ manual.
114
+
115
+ ### 5. Verify off-chain (no gas)
116
+ Prove it accepts what you want and rejects what you don't, before trusting it:
117
+ ```bash
118
+ sailor mandate simulate --address <SHARED_ADDRESS> --sma <SMA> \
119
+ --target <router> --calldata <hex> --expect pass
120
+ # or a batch file:
121
+ sailor mandate simulate --address <SHARED_ADDRESS> --sma <SMA> --calls ./probe.json
122
+ ```
123
+
124
+ ### 6. Reconfigure when bounds change
125
+ New cap or allowlist? Re-encode the blob and repeat step 4 (`sailor mandate configure --force`,
126
+ or the manual `configure` signature path) — the address stays registered, no re-attach. (On-chain
127
+ `MandateFactory.reconfigure` is the intended batch path for this.)
128
+
129
+ ## Gotchas
130
+
131
+ - **Unconfigured = deny.** A freshly-registered shared template with no `configure()` has
132
+ `isConfigured == false` and denies everything. Register AND configure — don't stop at
133
+ `sailor mandate attach`.
134
+ - **`sailor mandate attach` is register-only.** It does not configure. This is the single most
135
+ common trap; see steps 3 and 4.
136
+ - **Caps are in base units** of the relevant token (e.g. `25_000_000` = 25 USDC).
137
+ - **permissionSigner is the trust anchor** for config. In production use a multisig / timelock
138
+ — whoever holds it can widen allowlists and caps on every account they signed for.
139
+ - The shared singletons are **unaudited examples**. Simulate (Step 5) is not optional.