@dev.sail.money/sailor 0.0.2 → 1.0.0-38

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 (199) hide show
  1. package/AGENTS.md +43 -15
  2. package/README.md +217 -126
  3. package/examples/permissions/BoundedApproveAndCallBatch.sol +179 -0
  4. package/examples/permissions/BoundedBet_Limitless_Base.sol +8 -7
  5. package/examples/permissions/BoundedBorrow_AaveV3_Arbitrum.sol +13 -13
  6. package/examples/permissions/BoundedPerp_GMXv2_Arbitrum.sol +50 -39
  7. package/examples/permissions/BoundedStake_Venice_Base.sol +85 -0
  8. package/examples/permissions/BoundedSupply_AaveV3_Arbitrum.sol +82 -0
  9. package/examples/permissions/BoundedSwap_UniswapV3_Base.sol +15 -12
  10. package/examples/permissions/BoundedSwap_UniswapV4_Unichain.sol +14 -8
  11. package/examples/permissions/BoundedTransfer_ERC20_Ethereum.sol +6 -6
  12. package/examples/permissions/BoundedVault_ERC4626_Base.sol +97 -0
  13. package/examples/permissions/README.md +29 -2
  14. package/examples/permissions/SailCalldata.sol +118 -0
  15. package/examples/permissions/interfaces/IBatchPermission.sol +38 -0
  16. package/package.json +17 -12
  17. package/packages/cli/dist/index.cjs +4164 -2508
  18. package/packages/cli/dist/server.cjs +897 -1566
  19. package/packages/sdk/dist/chains.d.ts +12 -0
  20. package/packages/sdk/dist/chains.d.ts.map +1 -0
  21. package/packages/sdk/dist/chains.js +94 -0
  22. package/packages/sdk/dist/chains.js.map +1 -0
  23. package/packages/sdk/dist/deployments.d.ts +14 -7
  24. package/packages/sdk/dist/deployments.d.ts.map +1 -1
  25. package/packages/sdk/dist/deployments.js +132 -141
  26. package/packages/sdk/dist/deployments.js.map +1 -1
  27. package/packages/sdk/dist/index.d.ts +3 -2
  28. package/packages/sdk/dist/index.d.ts.map +1 -1
  29. package/packages/sdk/dist/index.js +3 -2
  30. package/packages/sdk/dist/index.js.map +1 -1
  31. package/packages/sdk/dist/intelligence.d.ts +1 -1
  32. package/packages/sdk/dist/intelligence.js +1 -1
  33. package/packages/sdk/dist/lifi.d.ts +17 -0
  34. package/packages/sdk/dist/lifi.d.ts.map +1 -1
  35. package/packages/sdk/dist/lifi.js +24 -0
  36. package/packages/sdk/dist/lifi.js.map +1 -1
  37. package/packages/sdk/dist/safe.d.ts +83 -0
  38. package/packages/sdk/dist/safe.d.ts.map +1 -1
  39. package/packages/sdk/dist/safe.js +92 -1
  40. package/packages/sdk/dist/safe.js.map +1 -1
  41. package/packages/sdk/dist/templates/ammLiquidity.d.ts +24 -11
  42. package/packages/sdk/dist/templates/ammLiquidity.d.ts.map +1 -1
  43. package/packages/sdk/dist/templates/ammLiquidity.js +39 -31
  44. package/packages/sdk/dist/templates/ammLiquidity.js.map +1 -1
  45. package/packages/sdk/dist/templates/approveAndCallBatch.d.ts +24 -10
  46. package/packages/sdk/dist/templates/approveAndCallBatch.d.ts.map +1 -1
  47. package/packages/sdk/dist/templates/approveAndCallBatch.js +36 -23
  48. package/packages/sdk/dist/templates/approveAndCallBatch.js.map +1 -1
  49. package/packages/sdk/dist/templates/boundedBorrow.d.ts +19 -9
  50. package/packages/sdk/dist/templates/boundedBorrow.d.ts.map +1 -1
  51. package/packages/sdk/dist/templates/boundedBorrow.js +28 -19
  52. package/packages/sdk/dist/templates/boundedBorrow.js.map +1 -1
  53. package/packages/sdk/dist/templates/boundedSwap.d.ts +19 -9
  54. package/packages/sdk/dist/templates/boundedSwap.d.ts.map +1 -1
  55. package/packages/sdk/dist/templates/boundedSwap.js +30 -20
  56. package/packages/sdk/dist/templates/boundedSwap.js.map +1 -1
  57. package/packages/sdk/dist/templates/defiBundle.d.ts +35 -9
  58. package/packages/sdk/dist/templates/defiBundle.d.ts.map +1 -1
  59. package/packages/sdk/dist/templates/defiBundle.js +84 -22
  60. package/packages/sdk/dist/templates/defiBundle.js.map +1 -1
  61. package/packages/sdk/dist/templates/pendle.d.ts +23 -8
  62. package/packages/sdk/dist/templates/pendle.d.ts.map +1 -1
  63. package/packages/sdk/dist/templates/pendle.js +34 -14
  64. package/packages/sdk/dist/templates/pendle.js.map +1 -1
  65. package/packages/sdk/dist/templates/transferTarget.d.ts +11 -3
  66. package/packages/sdk/dist/templates/transferTarget.d.ts.map +1 -1
  67. package/packages/sdk/dist/templates/transferTarget.js +14 -7
  68. package/packages/sdk/dist/templates/transferTarget.js.map +1 -1
  69. package/packages/sdk/dist/types.d.ts +19 -1
  70. package/packages/sdk/dist/types.d.ts.map +1 -1
  71. package/packages/sdk/package.json +28 -0
  72. package/packages/ui/dist/assets/{add-DaJhwIBV.js → add-Dl1etsL9.js} +1 -1
  73. package/packages/ui/dist/assets/{all-wallets-BUxsqWXi.js → all-wallets-C0eHLOGG.js} +1 -1
  74. package/packages/ui/dist/assets/{app-store-DkltwTqE.js → app-store-B-VMDEZ3.js} +1 -1
  75. package/packages/ui/dist/assets/{apple-owVOeaIT.js → apple-DkDXzKns.js} +1 -1
  76. package/packages/ui/dist/assets/{arrow-bottom-D2mmNJve.js → arrow-bottom-DtPzuS76.js} +1 -1
  77. package/packages/ui/dist/assets/{arrow-bottom-circle-CbNYijx-.js → arrow-bottom-circle-D7odSAO8.js} +1 -1
  78. package/packages/ui/dist/assets/{arrow-left-DJB61s4C.js → arrow-left-zJV9tpx0.js} +1 -1
  79. package/packages/ui/dist/assets/{arrow-right-BBrsQ9R4.js → arrow-right-BOREfe7o.js} +1 -1
  80. package/packages/ui/dist/assets/{arrow-top-Cil6bOc8.js → arrow-top-CipQc3Af.js} +1 -1
  81. package/packages/ui/dist/assets/{bank-CbwEmRo3.js → bank-C5s7eoV5.js} +1 -1
  82. package/packages/ui/dist/assets/{basic-CLNfjw3m.js → basic-D2es4Vq8.js} +1 -1
  83. package/packages/ui/dist/assets/{browser-B5TtF4Pb.js → browser-DITQWDC9.js} +1 -1
  84. package/packages/ui/dist/assets/{card-CO7BVB-C.js → card-C3DDkaYK.js} +1 -1
  85. package/packages/ui/dist/assets/{ccip-2W7K3_J3.js → ccip-UBXL3JiN.js} +1 -1
  86. package/packages/ui/dist/assets/{checkmark-bold-D9xGHzPE.js → checkmark-bold-D8yW0_K_.js} +1 -1
  87. package/packages/ui/dist/assets/{checkmark-BEtSHq9m.js → checkmark-ngef3MAl.js} +1 -1
  88. package/packages/ui/dist/assets/{chevron-bottom-BDztht6i.js → chevron-bottom-C56BipDR.js} +1 -1
  89. package/packages/ui/dist/assets/{chevron-left-EV4GFNbc.js → chevron-left-BmIPtPl_.js} +1 -1
  90. package/packages/ui/dist/assets/{chevron-right-B4_bB9oR.js → chevron-right-BnySHQ8h.js} +1 -1
  91. package/packages/ui/dist/assets/{chevron-top-D54xPNzF.js → chevron-top-BDGZnNW3.js} +1 -1
  92. package/packages/ui/dist/assets/{chrome-store-DYUpAJJq.js → chrome-store-BYIqJZVF.js} +1 -1
  93. package/packages/ui/dist/assets/{clock-Ca1T1Soz.js → clock-Bl4mUHAM.js} +1 -1
  94. package/packages/ui/dist/assets/{close-BZqWjurK.js → close-B9rhEX6U.js} +1 -1
  95. package/packages/ui/dist/assets/{coinPlaceholder-e6fl2XDo.js → coinPlaceholder-1cO0FQsl.js} +1 -1
  96. package/packages/ui/dist/assets/{compass-DCLC7zIh.js → compass-7i-VuXu2.js} +1 -1
  97. package/packages/ui/dist/assets/{copy-Th2AaD-O.js → copy-OqqXix2J.js} +1 -1
  98. package/packages/ui/dist/assets/{core-Ckx_cyuH.js → core-tX9kIIDJ.js} +3 -3
  99. package/packages/ui/dist/assets/cursor-BoyeQ9fN.js +3 -0
  100. package/packages/ui/dist/assets/{cursor-transparent-BKHeABKB.js → cursor-transparent-5aoRH67u.js} +1 -1
  101. package/packages/ui/dist/assets/{desktop-CBjY8t6F.js → desktop-BaPXK9R6.js} +1 -1
  102. package/packages/ui/dist/assets/{disconnect-DbSs2cli.js → disconnect-LlK5K1CF.js} +1 -1
  103. package/packages/ui/dist/assets/{discord-ZlLOAUkM.js → discord-BdcQNWY_.js} +1 -1
  104. package/packages/ui/dist/assets/{etherscan-CKUrqWYN.js → etherscan-Bb-WxpO1.js} +1 -1
  105. package/packages/ui/dist/assets/{events-CiKP71cK.js → events-DjdZr6no.js} +1 -1
  106. package/packages/ui/dist/assets/{exclamation-triangle-DA1QzFiO.js → exclamation-triangle-COx4VtPV.js} +1 -1
  107. package/packages/ui/dist/assets/{extension-BVJkmvpJ.js → extension-DF63DTWO.js} +1 -1
  108. package/packages/ui/dist/assets/{external-link-D_bsR7B2.js → external-link-DyghCkQu.js} +1 -1
  109. package/packages/ui/dist/assets/{facebook-CmFmhojx.js → facebook-Dcg4bZMR.js} +1 -1
  110. package/packages/ui/dist/assets/{fallback-Ofl6uSnB.js → fallback-DJIr_fH3.js} +1 -1
  111. package/packages/ui/dist/assets/{farcaster-Co-M3Ss8.js → farcaster-BkmV5HjO.js} +1 -1
  112. package/packages/ui/dist/assets/{filters-B1WwNaFU.js → filters-DLHj1T_P.js} +1 -1
  113. package/packages/ui/dist/assets/{github-CP4fP6gn.js → github-BFDCgKrF.js} +1 -1
  114. package/packages/ui/dist/assets/{google-CsOIXJ6V.js → google-C08SpmIy.js} +1 -1
  115. package/packages/ui/dist/assets/{help-circle-DiMkomdF.js → help-circle-DU1IFmWp.js} +1 -1
  116. package/packages/ui/dist/assets/{id-lmscL5LX.js → id-DtDRGf3L.js} +1 -1
  117. package/packages/ui/dist/assets/{image-B-ubJrY5.js → image-CgCXJEjT.js} +1 -1
  118. package/packages/ui/dist/assets/{index-Dbh5V1Z0.js → index-8chM4S5Y.js} +1 -1
  119. package/packages/ui/dist/assets/{index-BaukYv-x.js → index-B5sCtNuq.js} +1 -1
  120. package/packages/ui/dist/assets/{index-CF0KMmke.js → index-BBfBEazf.js} +3 -3
  121. package/packages/ui/dist/assets/{index-CZR1Qjhs.js → index-BhXPwltt.js} +1 -1
  122. package/packages/ui/dist/assets/index-Cm05Py20.css +1 -0
  123. package/packages/ui/dist/assets/{index-DVgfCzCo.js → index-D37bD6Yt.js} +1 -1
  124. package/packages/ui/dist/assets/index-DZfBh-cg.js +1775 -0
  125. package/packages/ui/dist/assets/{index.es-C78cE5SI.js → index.es-BEcNQEn-.js} +4 -4
  126. package/packages/ui/dist/assets/{info-Cqg57EVo.js → info-ClsdYA4P.js} +1 -1
  127. package/packages/ui/dist/assets/{info-circle-DkeSWNKV.js → info-circle-DJmn4Bsv.js} +1 -1
  128. package/packages/ui/dist/assets/{lightbulb-DNlO4qKh.js → lightbulb-CXSftjXS.js} +1 -1
  129. package/packages/ui/dist/assets/{mail-kVQ8Jb9Y.js → mail-cdYKOl9P.js} +1 -1
  130. package/packages/ui/dist/assets/{metamask-sdk-CBalSvz7.js → metamask-sdk-Co3aIEln.js} +1 -1
  131. package/packages/ui/dist/assets/{mobile-BEteuhF7.js → mobile-DEHYlk8L.js} +1 -1
  132. package/packages/ui/dist/assets/{more-DBWmXQli.js → more-Cq_fo8pI.js} +1 -1
  133. package/packages/ui/dist/assets/{network-placeholder-Dg1uUHiL.js → network-placeholder-_dLCK4xB.js} +1 -1
  134. package/packages/ui/dist/assets/{nftPlaceholder-i3AHSiD9.js → nftPlaceholder-Dz4HKEr4.js} +1 -1
  135. package/packages/ui/dist/assets/{off-BtMm0fi2.js → off-B1k1lhkr.js} +1 -1
  136. package/packages/ui/dist/assets/{parseSignature-Cb5FlWWg.js → parseSignature-Cr0ptV2X.js} +1 -1
  137. package/packages/ui/dist/assets/{play-store-iKKkXa6a.js → play-store-lYqe4eeL.js} +1 -1
  138. package/packages/ui/dist/assets/{plus-CA5NaRtb.js → plus-QMgh1krr.js} +1 -1
  139. package/packages/ui/dist/assets/{qr-code-D2kiqR7h.js → qr-code-ClVHbZWN.js} +1 -1
  140. package/packages/ui/dist/assets/{recycle-horizontal-Dcme7R03.js → recycle-horizontal-CxGYnWid.js} +1 -1
  141. package/packages/ui/dist/assets/{refresh-Dega3sDp.js → refresh-CPysMza_.js} +1 -1
  142. package/packages/ui/dist/assets/{reown-logo-xNkksyWJ.js → reown-logo-OoL_zJd0.js} +1 -1
  143. package/packages/ui/dist/assets/{search-HYl7NO8x.js → search-2GaRbf1I.js} +1 -1
  144. package/packages/ui/dist/assets/{secp256k1-Cxd6_SiH.js → secp256k1-BrB8qSSy.js} +1 -1
  145. package/packages/ui/dist/assets/{send-CJU8CUAo.js → send-CC2UuIfD.js} +1 -1
  146. package/packages/ui/dist/assets/{swapHorizontal-IMUKiUre.js → swapHorizontal-BRqYwsqT.js} +1 -1
  147. package/packages/ui/dist/assets/{swapHorizontalBold-CNYnNJ9-.js → swapHorizontalBold-Bj0GSRq9.js} +1 -1
  148. package/packages/ui/dist/assets/{swapHorizontalMedium-B9VxEYsT.js → swapHorizontalMedium-oLOjpU2A.js} +1 -1
  149. package/packages/ui/dist/assets/{swapHorizontalRoundedBold-Dz33l_Jh.js → swapHorizontalRoundedBold-TJ652QXb.js} +1 -1
  150. package/packages/ui/dist/assets/{swapVertical-CHUmjVJ0.js → swapVertical-e0NLyV3x.js} +1 -1
  151. package/packages/ui/dist/assets/{telegram-kl9S2mbU.js → telegram-WhJHVeoU.js} +1 -1
  152. package/packages/ui/dist/assets/{three-dots-U5lhA1Am.js → three-dots-BlBAOyW-.js} +1 -1
  153. package/packages/ui/dist/assets/{twitch-KTEUWXEp.js → twitch-BH7vWmPc.js} +1 -1
  154. package/packages/ui/dist/assets/{twitterIcon-BHiq8mRg.js → twitterIcon-As0Nkanp.js} +1 -1
  155. package/packages/ui/dist/assets/{verify-CfN-BXNd.js → verify-BEJ0QuLl.js} +1 -1
  156. package/packages/ui/dist/assets/{verify-filled-DwZccetj.js → verify-filled-B8Ww2N7z.js} +1 -1
  157. package/packages/ui/dist/assets/{w3m-modal-CS-PFqPE.js → w3m-modal-5rOSZgOR.js} +1 -1
  158. package/packages/ui/dist/assets/{wallet-DVlGkhOY.js → wallet-CAfC3aml.js} +1 -1
  159. package/packages/ui/dist/assets/{wallet-placeholder-CvR_iEWX.js → wallet-placeholder-4RZI464Z.js} +1 -1
  160. package/packages/ui/dist/assets/{walletconnect-8pZBDvVI.js → walletconnect-BiltKqAe.js} +1 -1
  161. package/packages/ui/dist/assets/{warning-circle-ylLEE0Yp.js → warning-circle-CI4jqpHo.js} +1 -1
  162. package/packages/ui/dist/assets/{x-C_TBsTMj.js → x-FBttjBWO.js} +1 -1
  163. package/packages/ui/dist/index.html +2 -2
  164. package/scripts/check-init.mjs +2 -3
  165. package/templates/custom-mandate/README.md +31 -0
  166. package/templates/custom-mandate/mandates/BoundedCallPermission.sol +8 -2
  167. package/templates/custom-mandate/mandates/SailCalldata.sol +118 -0
  168. package/templates/default/.env.example +20 -0
  169. package/templates/{dca-rebalancer → default}/.github/workflows/agent-tick.yml +8 -7
  170. package/templates/{dca-rebalancer → default}/.sail/config.json +2 -2
  171. package/templates/default/AGENTS.md +171 -0
  172. package/templates/default/README.md +16 -0
  173. package/templates/default/examples/dca/README.md +16 -0
  174. package/templates/default/examples/dca/agent.ts +174 -0
  175. package/templates/{dca-rebalancer/src → default/examples/dca}/mandate.ts +9 -31
  176. package/templates/{dca-rebalancer → default}/package.json +2 -2
  177. package/templates/default/src/agent.ts +37 -0
  178. package/templates/default/src/config.ts +24 -0
  179. package/templates/default/src/mandate.ts +22 -0
  180. package/templates/default/tsconfig.json +17 -0
  181. package/templates/lifi-permissions/README.md +1 -1
  182. package/packages/ui/dist/assets/cursor-DV7rOqbJ.js +0 -3
  183. package/packages/ui/dist/assets/index-CKxgNxS9.css +0 -1
  184. package/packages/ui/dist/assets/index-Q2Yai4Fe.js +0 -2103
  185. package/scripts/postinstall.js +0 -366
  186. package/templates/dca-rebalancer/.env.example +0 -6
  187. package/templates/dca-rebalancer/AGENTS.md +0 -246
  188. package/templates/dca-rebalancer/AGENT_PLAYBOOK.md +0 -110
  189. package/templates/dca-rebalancer/README.md +0 -16
  190. package/templates/dca-rebalancer/src/agent.ts +0 -253
  191. package/templates/dca-rebalancer/src/config.ts +0 -27
  192. package/templates/dca-rebalancer/tsconfig.json +0 -8
  193. /package/templates/{dca-rebalancer → default}/.cursor/rules +0 -0
  194. /package/templates/{dca-rebalancer → default}/.sail/.gitkeep +0 -0
  195. /package/templates/{dca-rebalancer → default}/.sail/README.md +0 -0
  196. /package/templates/{dca-rebalancer → default}/CLAUDE.md +0 -0
  197. /package/templates/{dca-rebalancer → default}/_gitignore +0 -0
  198. /package/templates/{dca-rebalancer → default}/docs/PERMISSION_MODEL.md +0 -0
  199. /package/templates/{dca-rebalancer → default}/ui/README.md +0 -0
@@ -10,8 +10,8 @@ pragma solidity 0.8.26;
10
10
  // Target : Universal Router 0xef740bf23acae26f6492b10de645d6b98dc8eaf3
11
11
  // (verify on Uniscan before use)
12
12
  //
13
- // ENFORCED ON-CHAIN (via kernel evaluate() on every dispatch):
14
- // execute(bytes,bytes[]) or execute(bytes,bytes[],uint256):
13
+ // ENFORCES ON-CHAIN (kernel calls evaluate() on every dispatch; false ⇒ dispatch blocked):
14
+ // execute(bytes,bytes[],uint256) selector 0x3593564c / execute(bytes,bytes[]) selector 0x24856bc3
15
15
  // • target must be UNIVERSAL_ROUTER
16
16
  // • first command byte (masking the allow-failure MSB) must be V4_SWAP (0x10)
17
17
  // • exactly one command (single-swap path — disallow multi-hop command strings)
@@ -19,9 +19,13 @@ pragma solidity 0.8.26;
19
19
  // • tokenIn (from poolKey, derived by zeroForOne) must be FIXED_CURRENCY_IN
20
20
  // • tokenOut must be in ALLOWED_CURRENCIES_OUT
21
21
  // • amountIn ≤ MAX_AMOUNT_IN
22
- // • amountOutMinimum ≥ amountIn × MIN_BPS / 10 000
22
+ // • amountOutMinimum ≥ amountIn × MIN_BPS / 10 000 (slippage floor — see caveat below)
23
23
  //
24
- // NOT ENFORCEDdocumented limitations:
24
+ // AGENT-ENFORCED / NOT BOUNDED HERE (off-chain can change without redeploying this contract):
25
+ // • real (cross-denomination) slippage — see MIN_BPS caveat in evaluate()
26
+ // • swap frequency / cadence
27
+ //
28
+ // DOCUMENTED LIMITATIONS (on-chain, but intentionally not constrained):
25
29
  // • hookData is not inspected (hooks can alter swap behavior on-chain; if the
26
30
  // pool uses a hook that significantly changes execution, this permission cannot
27
31
  // constrain it. Deploy against pools with address(0) hooks or audited hooks only.)
@@ -93,12 +97,10 @@ contract BoundedSwap_UniswapV4_Unichain is IPermission {
93
97
  MAX_AMOUNT_IN = maxAmountIn;
94
98
  MIN_BPS = minBps;
95
99
  for (uint256 i = 0; i < allowedCurrenciesOut.length; i++) {
96
- isAllowedCurrenciesOut[allowedCurrenciesOut[i]] = true;
100
+ isAllowedCurrencyOut[allowedCurrenciesOut[i]] = true;
97
101
  }
98
102
  }
99
103
 
100
- mapping(address => bool) private isAllowedCurrenciesOut;
101
-
102
104
  function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool) {
103
105
  if (ctx.target != UNIVERSAL_ROUTER) return false;
104
106
  if (ctx.selector != SEL_EXECUTE_DEADLINE && ctx.selector != SEL_EXECUTE) return false;
@@ -133,8 +135,12 @@ contract BoundedSwap_UniswapV4_Unichain is IPermission {
133
135
  address tokenOut = p.zeroForOne ? p.poolKey.currency1 : p.poolKey.currency0;
134
136
 
135
137
  if (tokenIn != FIXED_CURRENCY_IN) return false;
136
- if (!isAllowedCurrenciesOut[tokenOut]) return false;
138
+ if (!isAllowedCurrencyOut[tokenOut]) return false;
137
139
  if (p.amountIn > MAX_AMOUNT_IN) return false;
140
+ // Slippage floor: amountOutMinimum ≥ amountIn × MIN_BPS / 10 000.
141
+ // WARNING: compares tokenOut against tokenIn base units. For same-price/same-decimal
142
+ // pairs this maps to a slippage %. For cross-price pairs it is trivially satisfied —
143
+ // real slippage is enforced by the agent off-chain, not by this contract.
138
144
  if (p.amountOutMinimum < (uint256(p.amountIn) * MIN_BPS) / 10_000) return false;
139
145
 
140
146
  return true;
@@ -6,13 +6,13 @@ pragma solidity 0.8.26;
6
6
  // Version : Standard ERC-20 (version-agnostic)
7
7
  // Chain : Ethereum mainnet (works on any EVM — the most general example)
8
8
  //
9
- // ENFORCED ON-CHAIN (via kernel evaluate() on every dispatch):
10
- // transfer(address to, uint256 amount)
11
- // • target must be in ALLOWED_TOKENS
12
- // • recipient (to) must be in ALLOWED_RECIPIENTS
13
- // • amount ≤ MAX_AMOUNT_PER_TRANSFER
9
+ // ENFORCES ON-CHAIN (kernel calls evaluate() on every dispatch; false ⇒ dispatch blocked):
10
+ // transfer(address to,uint256 amount) selector 0xa9059cbb
11
+ // • target must be in ALLOWED_TOKENS
12
+ // • recipient (to) must be in ALLOWED_RECIPIENTS
13
+ // • amount ≤ MAX_AMOUNT_PER_TRANSFER
14
14
  //
15
- // NOT ENFORCED (agent code — can change without a new contract):
15
+ // AGENT-ENFORCED / NOT BOUNDED HERE (off-chain — can change without redeploying this contract):
16
16
  // • transfer frequency / timing
17
17
  // • choice of token within ALLOWED_TOKENS
18
18
  // • choice of recipient within ALLOWED_RECIPIENTS
@@ -0,0 +1,97 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.26;
3
+
4
+ // ─────────────────────────────────────────────────────────────────────────────
5
+ // Protocol : ERC-4626 (tokenized vault standard)
6
+ // Version : Standard EIP-4626 interface (version-agnostic)
7
+ // Chain : Base mainnet (8453) — reference vault below; works on any EVM
8
+ // Target : Reference vault — Moonwell Flagship USDC (MetaMorpho, ERC-4626)
9
+ // 0xc1256Ae5FF1cf2719D4937adb3bbCCab2E00A2Ca (verified ERC-4626 on Base: asset()=USDC)
10
+ // Written against the STANDARD interface, so it generalizes to any 4626 vault.
11
+ //
12
+ // ENFORCES ON-CHAIN (kernel calls evaluate() on every dispatch; false ⇒ dispatch blocked):
13
+ // deposit(uint256 assets,address receiver) selector 0x6e553f65
14
+ // • target (the vault) must be in ALLOWED_VAULTS
15
+ // • assets ≤ MAX_DEPOSIT_ASSETS
16
+ // • receiver must equal ctx.account (the SMA — shares cannot be minted to another address)
17
+ // withdraw(uint256 assets,address receiver,address owner) selector 0xb460af94
18
+ // redeem(uint256 shares,address receiver,address owner) selector 0xba087652
19
+ // • target (the vault) must be in ALLOWED_VAULTS
20
+ // • receiver must equal ctx.account (assets cannot be sent elsewhere)
21
+ // • owner must equal ctx.account (cannot burn shares the SMA merely has allowance over)
22
+ //
23
+ // AGENT-ENFORCED / NOT BOUNDED HERE (off-chain — can change without redeploying this contract):
24
+ // • Which vault within ALLOWED_VAULTS to use, and deposit/withdraw timing/cadence
25
+ // • Withdraw/redeem amount (only the receiver/owner are bound, not the size — the SMA can
26
+ // always withdraw its own position in full; add a cap here if your strategy needs one)
27
+ // • Underlying ASSET: enforced TRANSITIVELY via ALLOWED_VAULTS, not from calldata. A 4626
28
+ // vault's asset() is fixed at deploy; the deposit/withdraw calldata carries NO asset field,
29
+ // so the asset cannot be read from it. Allowlist only vaults whose asset() you have verified.
30
+ //
31
+ // VERIFY BEFORE USE:
32
+ // • Selectors are the EIP-4626 standard (verified via `cast sig`): deposit 0x6e553f65,
33
+ // withdraw 0xb460af94, redeem 0xba087652. (mint(uint256,address)=0x94bf804d is NOT gated
34
+ // here — add it if your agent mints shares by exact share count.)
35
+ // • Confirm each allowlisted vault actually implements ERC-4626 and its asset() is what you
36
+ // expect (e.g. on the reference vault, asset() == USDC 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913).
37
+ // • A deposit also needs a prior ERC-20 approve of the vault — gate that with a separate
38
+ // approve permission (see BoundedApproveAndCallBatch.sol for the atomic approve→call→reset pattern).
39
+ // • MAX_DEPOSIT_ASSETS is in the asset's base units (USDC = 6 decimals).
40
+ // ─────────────────────────────────────────────────────────────────────────────
41
+
42
+ import {IPermission, Context} from "@sail/interfaces/IPermission.sol";
43
+
44
+ contract BoundedVault_ERC4626_Base is IPermission {
45
+ bytes32 private constant DISCRIMINATOR = keccak256("BoundedVault_ERC4626_Base");
46
+
47
+ mapping(address => bool) public isAllowedVault;
48
+ uint256 public immutable MAX_DEPOSIT_ASSETS;
49
+
50
+ // EIP-4626 standard selectors
51
+ bytes4 private constant SEL_DEPOSIT = 0x6e553f65; // deposit(uint256,address)
52
+ bytes4 private constant SEL_WITHDRAW = 0xb460af94; // withdraw(uint256,address,address)
53
+ bytes4 private constant SEL_REDEEM = 0xba087652; // redeem(uint256,address,address)
54
+
55
+ /// @param allowedVaults ERC-4626 vaults the agent may deposit into / withdraw from
56
+ /// @param maxDepositAssets Per-deposit cap in the asset's base units (must be > 0)
57
+ constructor(address[] memory allowedVaults, uint256 maxDepositAssets) {
58
+ require(allowedVaults.length > 0, "empty vault allowlist");
59
+ require(maxDepositAssets > 0, "zero deposit cap");
60
+ MAX_DEPOSIT_ASSETS = maxDepositAssets;
61
+ for (uint256 i = 0; i < allowedVaults.length; i++) {
62
+ require(allowedVaults[i] != address(0), "zero vault address");
63
+ isAllowedVault[allowedVaults[i]] = true;
64
+ }
65
+ }
66
+
67
+ function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool) {
68
+ if (!isAllowedVault[ctx.target]) return false;
69
+ if (txData.length < 4) return false;
70
+
71
+ // deposit(uint256 assets, address receiver)
72
+ if (ctx.selector == SEL_DEPOSIT) {
73
+ if (txData.length < 4 + 2 * 32) return false;
74
+ (uint256 assets, address receiver) = abi.decode(txData[4:], (uint256, address));
75
+ if (assets > MAX_DEPOSIT_ASSETS) return false;
76
+ if (receiver != ctx.account) return false; // shares must accrue to the SMA
77
+ return true;
78
+ }
79
+
80
+ // withdraw(uint256 assets, address receiver, address owner)
81
+ // redeem(uint256 shares, address receiver, address owner)
82
+ // Identical parameter layout: (uint256, address, address).
83
+ // Amount (assets/shares) is NOT bounded — the SMA can always exit its full position.
84
+ // Add a maxWithdrawAssets constructor param and check here if your strategy needs a cap.
85
+ if (ctx.selector == SEL_WITHDRAW || ctx.selector == SEL_REDEEM) {
86
+ if (txData.length < 4 + 3 * 32) return false;
87
+ (, address receiver, address owner) = abi.decode(txData[4:], (uint256, address, address));
88
+ if (receiver != ctx.account) return false; // assets must return to the SMA
89
+ if (owner != ctx.account) return false; // only the SMA's own shares may be burned
90
+ return true;
91
+ }
92
+
93
+ return false;
94
+ }
95
+
96
+ function discriminator() external pure returns (bytes32) { return DISCRIMINATOR; }
97
+ }
@@ -27,14 +27,25 @@ You own what you deploy.
27
27
 
28
28
  ## Examples
29
29
 
30
+ Each header has two blocks: **ENFORCES ON-CHAIN** (bounds the kernel checks in `evaluate()` on
31
+ every dispatch — these actually hold) and **AGENT-ENFORCED / NOT BOUNDED HERE** (left to your
32
+ off-chain agent code — these do *not* hold on-chain). Read both before deploying.
33
+
30
34
  | File | Protocol | Version | Chain | Status |
31
35
  |---|---|---|---|---|
32
36
  | `BoundedSwap_UniswapV3_Base.sol` | Uniswap Swap | V3 SwapRouter02 | Base | Full decode |
33
37
  | `BoundedSwap_UniswapV4_Unichain.sol` | Uniswap Swap | V4 Universal Router | Unichain | Partial decode — see header |
34
- | `BoundedBorrow_AaveV3_Arbitrum.sol` | Aave Borrow | V3 Pool | Arbitrum | Full decode — verify selector |
38
+ | `BoundedBorrow_AaveV3_Arbitrum.sol` | Aave Borrow | V3 Pool | Arbitrum | Full decode |
39
+ | `BoundedSupply_AaveV3_Arbitrum.sol` | Aave Supply | V3 Pool | Arbitrum | Full decode |
40
+ | `BoundedVault_ERC4626_Base.sol` | ERC-4626 Vault deposit/withdraw | EIP-4626 standard | Base (any EVM) | Full decode |
41
+ | `BoundedStake_Venice_Base.sol` | Venice (VVV) staking | sVVV staking | Base | Full decode |
35
42
  | `BoundedTransfer_ERC20_Ethereum.sol` | ERC-20 Transfer | — | Ethereum (any EVM) | Full decode |
36
- | `BoundedPerp_GMXv2_Arbitrum.sol` | GMX Perpetuals | V2 ExchangeRouter | Arbitrum | Partial decode — see header |
43
+ | `BoundedPerp_GMXv2_Arbitrum.sol` | GMX Perpetuals | V2 ExchangeRouter | Arbitrum | Reference patternverify selector/struct/router against live GMX ABI (see header) |
37
44
  | `BoundedBet_Limitless_Base.sol` | Limitless Prediction | CTF Exchange | Base | UNVERIFIED ABI — see header |
45
+ | `BoundedApproveAndCallBatch.sol` | Atomic approve→call→reset | Sail `IBatchPermission` | Any selective kernel | Full decode — batch-only (see note below) |
46
+
47
+ The `interfaces/` directory holds `IPermission.sol` (single-call permissions) and
48
+ `IBatchPermission.sol` (batch permissions — see the batch example).
38
49
 
39
50
  ## Using these examples
40
51
 
@@ -50,3 +61,19 @@ To deploy within a Sailor project (copy the .sol file to `mandates/` first):
50
61
  ```bash
51
62
  sailor mandate deploy --contract <Name> --args '[...]' --attach --sma <SMA>
52
63
  ```
64
+
65
+ ## Verify before you authorize
66
+
67
+ Prove a permission accepts the calls you want and rejects the ones you don't — before paying
68
+ registration gas — with `sailor mandate simulate` (off-chain `eth_call`, no gas, signs nothing):
69
+ ```bash
70
+ sailor mandate simulate --address <permission> --calls calls.json
71
+ ```
72
+ Every example here was checked this way (valid calls PASS, out-of-bounds calls FAIL).
73
+
74
+ **Batch permissions are different.** `BoundedApproveAndCallBatch.sol` implements `IBatchPermission`
75
+ and is gated by the kernel's `dispatchBatch`, not single `dispatch` — its single-call `evaluate()`
76
+ deliberately returns `false`. `sailor mandate simulate` only probes single `evaluate()`, so it
77
+ **cannot** verify a batch permission. Until simulate gains a batch mode, verify a batch permission
78
+ by calling its `evaluateBatch(calls, ctx)` view directly (e.g. `cast call`) with the call sequences
79
+ you expect to pass and fail — same fail-closed `eth_call` semantics, the correct entrypoint.
@@ -0,0 +1,118 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.26;
3
+
4
+ // ─────────────────────────────────────────────────────────────────────────────
5
+ // SailCalldata — safe static-parameter extraction for IPermission.evaluate()
6
+ //
7
+ // PROBLEM
8
+ // evaluate(bytes calldata txData, Context calldata ctx) receives raw ABI-
9
+ // encoded calldata. Extracting parameters by hand is the most dangerous part
10
+ // of permission writing:
11
+ // • Forgetting the length check before decoding → silent wrong value or
12
+ // revert instead of clean `return false`.
13
+ // • Wrong slot index → decoding the wrong parameter (type-checks, still wrong).
14
+ // • Truncation bugs when casting uint256 slots to address (padding bits).
15
+ //
16
+ // SOLUTION
17
+ // This library centralises the three operations into named helpers:
18
+ // 1. hasParams() — explicit length guard (call once, at the top of evaluate)
19
+ // 2. asAddress() — extract + mask to 20 bytes
20
+ // 3. asUint256(), asInt256(), asBytes32(), asBool(), asUint128() — typed slots
21
+ //
22
+ // All helpers are view/pure and add zero gas overhead beyond the slice itself.
23
+ //
24
+ // USAGE (replace abi.decode pattern)
25
+ //
26
+ // // Before:
27
+ // if (txData.length < 4 + 3 * 32) return false;
28
+ // (address asset, uint256 amount, address onBehalfOf) =
29
+ // abi.decode(txData[4:], (address, uint256, address));
30
+ //
31
+ // // After:
32
+ // if (!SailCalldata.hasParams(txData, 3)) return false;
33
+ // address asset = SailCalldata.asAddress(txData, 0);
34
+ // uint256 amount = SailCalldata.asUint256(txData, 1);
35
+ // address onBehalfOf = SailCalldata.asAddress(txData, 2);
36
+ //
37
+ // LIMITATIONS
38
+ // Only covers static (fixed-size) ABI types. Dynamic types (bytes, string,
39
+ // arrays) use pointer indirection — decode them with abi.decode(txData[4:], ...)
40
+ // after the hasParams() guard, or write a dedicated extractor.
41
+ //
42
+ // SLOT INDEXING
43
+ // Slots are 0-indexed from the first constructor parameter (after the 4-byte
44
+ // selector). Slot 0 = bytes [4..35], slot 1 = bytes [36..67], etc.
45
+ // ─────────────────────────────────────────────────────────────────────────────
46
+
47
+ library SailCalldata {
48
+ // ── Guards ────────────────────────────────────────────────────────────────
49
+
50
+ /// @notice Returns true when txData is long enough to hold `params` static
51
+ /// 32-byte ABI slots after the 4-byte selector.
52
+ /// @dev Call this once at the top of evaluate() before any slot access.
53
+ /// Returns false (not revert) so evaluate() can return false cleanly.
54
+ function hasParams(bytes calldata txData, uint256 params) internal pure returns (bool) {
55
+ return txData.length >= 4 + params * 32;
56
+ }
57
+
58
+ // ── Static-type extractors ────────────────────────────────────────────────
59
+ // All assume hasParams() has already been checked for the relevant slot.
60
+ // Accessing an out-of-bounds slot will cause the calldata slice to revert —
61
+ // always guard with hasParams() first.
62
+
63
+ /// @notice Extract an address from ABI slot `i` (0-indexed after selector).
64
+ /// Masks to 20 bytes, discarding the ABI zero-padding in the upper 12.
65
+ function asAddress(bytes calldata txData, uint256 i) internal pure returns (address) {
66
+ return address(uint160(uint256(bytes32(txData[4 + i * 32 : 4 + (i + 1) * 32]))));
67
+ }
68
+
69
+ /// @notice Extract a uint256 from ABI slot `i`.
70
+ function asUint256(bytes calldata txData, uint256 i) internal pure returns (uint256) {
71
+ return uint256(bytes32(txData[4 + i * 32 : 4 + (i + 1) * 32]));
72
+ }
73
+
74
+ /// @notice Extract an int256 from ABI slot `i`.
75
+ function asInt256(bytes calldata txData, uint256 i) internal pure returns (int256) {
76
+ return int256(uint256(bytes32(txData[4 + i * 32 : 4 + (i + 1) * 32])));
77
+ }
78
+
79
+ /// @notice Extract a bytes32 from ABI slot `i`.
80
+ function asBytes32(bytes calldata txData, uint256 i) internal pure returns (bytes32) {
81
+ return bytes32(txData[4 + i * 32 : 4 + (i + 1) * 32]);
82
+ }
83
+
84
+ /// @notice Extract a bool from ABI slot `i` (true when slot value is non-zero).
85
+ function asBool(bytes calldata txData, uint256 i) internal pure returns (bool) {
86
+ return asUint256(txData, i) != 0;
87
+ }
88
+
89
+ /// @notice Extract a uint128 from ABI slot `i` (lower 128 bits of the slot).
90
+ function asUint128(bytes calldata txData, uint256 i) internal pure returns (uint128) {
91
+ return uint128(asUint256(txData, i));
92
+ }
93
+
94
+ /// @notice Extract a uint64 from ABI slot `i`.
95
+ function asUint64(bytes calldata txData, uint256 i) internal pure returns (uint64) {
96
+ return uint64(asUint256(txData, i));
97
+ }
98
+
99
+ /// @notice Extract a uint32 from ABI slot `i`.
100
+ function asUint32(bytes calldata txData, uint256 i) internal pure returns (uint32) {
101
+ return uint32(asUint256(txData, i));
102
+ }
103
+
104
+ /// @notice Extract a uint24 from ABI slot `i` (e.g. Uniswap fee tier).
105
+ function asUint24(bytes calldata txData, uint256 i) internal pure returns (uint24) {
106
+ return uint24(asUint256(txData, i));
107
+ }
108
+
109
+ /// @notice Extract a uint16 from ABI slot `i` (e.g. Aave referral code).
110
+ function asUint16(bytes calldata txData, uint256 i) internal pure returns (uint16) {
111
+ return uint16(asUint256(txData, i));
112
+ }
113
+
114
+ /// @notice Extract bytes4 (a function selector) from ABI slot `i`.
115
+ function asBytes4(bytes calldata txData, uint256 i) internal pure returns (bytes4) {
116
+ return bytes4(asBytes32(txData, i));
117
+ }
118
+ }
@@ -0,0 +1,38 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.26;
3
+
4
+ // Mirrors SailProtocol contracts/interfaces/IBatchPermission.sol. A batch-aware
5
+ // permission gates `dispatchBatch` — an ordered array of subcalls executed as one
6
+ // atomic transaction. Exactly ONE batch permission is consulted per batch dispatch
7
+ // (selective model), and it owns validation of every subcall AND the cross-call
8
+ // invariants (ordering, matching amounts, mandatory cleanup) that no per-call
9
+ // IPermission can express.
10
+
11
+ /// @notice A single subcall in a batch dispatch.
12
+ /// @dev The kernel executes each Call as safe.execTransactionFromModule(target, value, data, 0)
13
+ /// — operation is always CALL, never DELEGATECALL.
14
+ struct Call {
15
+ address target; // MUST NOT equal the kernel (enforced by the kernel)
16
+ uint256 value; // native ETH forwarded (wei)
17
+ bytes data; // calldata for the subcall
18
+ }
19
+
20
+ /// @notice Execution context passed to evaluateBatch on each batch dispatch (read-only snapshot).
21
+ struct BatchContext {
22
+ address account; // the Safe (SMA) whose assets are moved
23
+ address manager; // the delegated signer who authorised the batch
24
+ address submitter; // msg.sender of the dispatch (may differ from manager via a relayer)
25
+ address permission; // this batch permission contract
26
+ bytes32 batchHash; // keccak256(abi.encode(calls)) — stable id for the exact sequence
27
+ uint256 blockTimestamp; // block.timestamp at dispatch
28
+ uint256 blockNumber; // block.number at dispatch
29
+ }
30
+
31
+ interface IBatchPermission {
32
+ /// @notice Validate an entire batch of subcalls. Called via staticcall under a gas cap;
33
+ /// a revert / OOG / malformed return is treated as `false` (fail-closed).
34
+ function evaluateBatch(Call[] calldata calls, BatchContext calldata ctx) external view returns (bool);
35
+
36
+ /// @notice Marker the kernel uses (try/catch) to detect batch-aware permissions. MUST return true.
37
+ function isBatchPermission() external pure returns (bool);
38
+ }
package/package.json CHANGED
@@ -1,10 +1,16 @@
1
1
  {
2
2
  "name": "@dev.sail.money/sailor",
3
- "version": "0.0.2",
3
+ "version": "1.0.0-38",
4
4
  "description": "Operator toolkit for Sail Protocol",
5
5
  "bin": {
6
6
  "sailor": "packages/cli/dist/index.cjs"
7
7
  },
8
+ "exports": {
9
+ "./sdk": {
10
+ "import": "./packages/sdk/dist/index.js",
11
+ "types": "./packages/sdk/dist/index.d.ts"
12
+ }
13
+ },
8
14
  "files": [
9
15
  "packages/cli/dist",
10
16
  "packages/sdk/dist",
@@ -17,23 +23,22 @@
17
23
  "README.md",
18
24
  "AGENTS.md"
19
25
  ],
20
- "devDependencies": {
21
- "@biomejs/biome": "^1.9.0",
22
- "typescript": "^5.5.0"
23
- },
24
- "dependencies": {
25
- "tsx": "^4.22.4"
26
- },
27
26
  "scripts": {
28
- "build": "pnpm --filter @sail/sdk build && pnpm --filter @sail/chains build && pnpm --filter sailor build && pnpm --filter sailor-ui build",
27
+ "build": "pnpm --filter @sail/sdk build && pnpm --filter sailor build && pnpm --filter sailor-ui build",
29
28
  "test": "pnpm --filter sailor-ui test",
30
29
  "test:ui": "pnpm --filter sailor-ui test:ui",
31
30
  "link:cli": "pnpm link --global",
32
- "postinstall": "node scripts/postinstall.js",
33
- "typecheck": "pnpm --filter @sail/sdk build && pnpm --filter @sail/chains build && pnpm -r typecheck",
31
+ "typecheck": "pnpm --filter @sail/sdk build && pnpm -r typecheck",
34
32
  "docs:check": "node scripts/check-docs.mjs",
35
33
  "init:check": "node scripts/check-init.mjs",
36
34
  "eval": "node evals/run.mjs",
37
35
  "lint": "biome check ."
36
+ },
37
+ "devDependencies": {
38
+ "@biomejs/biome": "^1.9.0",
39
+ "typescript": "^5.5.0"
40
+ },
41
+ "dependencies": {
42
+ "tsx": "^4.22.4"
38
43
  }
39
- }
44
+ }