@dev.sail.money/sailor 0.0.2 → 0.1.0-local

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 (211) hide show
  1. package/AGENTS.md +140 -111
  2. package/LICENSE +21 -21
  3. package/README.md +430 -337
  4. package/docs/PERMISSION_MODEL.md +93 -93
  5. package/examples/permissions/BoundedApproveAndCallBatch.sol +179 -0
  6. package/examples/permissions/BoundedBet_Limitless_Base.sol +97 -96
  7. package/examples/permissions/BoundedBorrow_AaveV3_Arbitrum.sol +94 -94
  8. package/examples/permissions/BoundedPerp_GMXv2_Arbitrum.sol +154 -143
  9. package/examples/permissions/BoundedStake_Venice_Base.sol +85 -0
  10. package/examples/permissions/BoundedSupply_AaveV3_Arbitrum.sol +82 -0
  11. package/examples/permissions/BoundedSwap_UniswapV3_Base.sol +116 -113
  12. package/examples/permissions/BoundedSwap_UniswapV4_Unichain.sol +150 -144
  13. package/examples/permissions/BoundedTransfer_ERC20_Ethereum.sol +73 -73
  14. package/examples/permissions/BoundedVault_ERC4626_Base.sol +97 -0
  15. package/examples/permissions/README.md +79 -52
  16. package/examples/permissions/SailCalldata.sol +118 -0
  17. package/examples/permissions/foundry.toml +10 -10
  18. package/examples/permissions/interfaces/IBatchPermission.sol +38 -0
  19. package/examples/permissions/interfaces/IPermission.sol +18 -18
  20. package/package.json +45 -39
  21. package/packages/cli/README.md +34 -34
  22. package/packages/cli/dist/index.cjs +4571 -2944
  23. package/packages/cli/dist/server.cjs +1252 -2010
  24. package/packages/sdk/README.md +65 -65
  25. package/packages/sdk/dist/chains.d.ts +12 -0
  26. package/packages/sdk/dist/chains.d.ts.map +1 -0
  27. package/packages/sdk/dist/chains.js +94 -0
  28. package/packages/sdk/dist/chains.js.map +1 -0
  29. package/packages/sdk/dist/deployments.d.ts +14 -7
  30. package/packages/sdk/dist/deployments.d.ts.map +1 -1
  31. package/packages/sdk/dist/deployments.js +132 -141
  32. package/packages/sdk/dist/deployments.js.map +1 -1
  33. package/packages/sdk/dist/index.d.ts +3 -2
  34. package/packages/sdk/dist/index.d.ts.map +1 -1
  35. package/packages/sdk/dist/index.js +3 -2
  36. package/packages/sdk/dist/index.js.map +1 -1
  37. package/packages/sdk/dist/intelligence.d.ts +1 -1
  38. package/packages/sdk/dist/intelligence.js +1 -1
  39. package/packages/sdk/dist/lifi.d.ts +17 -0
  40. package/packages/sdk/dist/lifi.d.ts.map +1 -1
  41. package/packages/sdk/dist/lifi.js +24 -0
  42. package/packages/sdk/dist/lifi.js.map +1 -1
  43. package/packages/sdk/dist/safe.d.ts +83 -0
  44. package/packages/sdk/dist/safe.d.ts.map +1 -1
  45. package/packages/sdk/dist/safe.js +92 -1
  46. package/packages/sdk/dist/safe.js.map +1 -1
  47. package/packages/sdk/dist/templates/ammLiquidity.d.ts +24 -11
  48. package/packages/sdk/dist/templates/ammLiquidity.d.ts.map +1 -1
  49. package/packages/sdk/dist/templates/ammLiquidity.js +39 -31
  50. package/packages/sdk/dist/templates/ammLiquidity.js.map +1 -1
  51. package/packages/sdk/dist/templates/approveAndCallBatch.d.ts +24 -10
  52. package/packages/sdk/dist/templates/approveAndCallBatch.d.ts.map +1 -1
  53. package/packages/sdk/dist/templates/approveAndCallBatch.js +36 -23
  54. package/packages/sdk/dist/templates/approveAndCallBatch.js.map +1 -1
  55. package/packages/sdk/dist/templates/boundedBorrow.d.ts +19 -9
  56. package/packages/sdk/dist/templates/boundedBorrow.d.ts.map +1 -1
  57. package/packages/sdk/dist/templates/boundedBorrow.js +28 -19
  58. package/packages/sdk/dist/templates/boundedBorrow.js.map +1 -1
  59. package/packages/sdk/dist/templates/boundedSwap.d.ts +19 -9
  60. package/packages/sdk/dist/templates/boundedSwap.d.ts.map +1 -1
  61. package/packages/sdk/dist/templates/boundedSwap.js +30 -20
  62. package/packages/sdk/dist/templates/boundedSwap.js.map +1 -1
  63. package/packages/sdk/dist/templates/defiBundle.d.ts +35 -9
  64. package/packages/sdk/dist/templates/defiBundle.d.ts.map +1 -1
  65. package/packages/sdk/dist/templates/defiBundle.js +84 -22
  66. package/packages/sdk/dist/templates/defiBundle.js.map +1 -1
  67. package/packages/sdk/dist/templates/pendle.d.ts +23 -8
  68. package/packages/sdk/dist/templates/pendle.d.ts.map +1 -1
  69. package/packages/sdk/dist/templates/pendle.js +34 -14
  70. package/packages/sdk/dist/templates/pendle.js.map +1 -1
  71. package/packages/sdk/dist/templates/transferTarget.d.ts +11 -3
  72. package/packages/sdk/dist/templates/transferTarget.d.ts.map +1 -1
  73. package/packages/sdk/dist/templates/transferTarget.js +14 -7
  74. package/packages/sdk/dist/templates/transferTarget.js.map +1 -1
  75. package/packages/sdk/dist/types.d.ts +19 -1
  76. package/packages/sdk/dist/types.d.ts.map +1 -1
  77. package/packages/sdk/package.json +80 -52
  78. package/packages/ui/dist/assets/{add-DaJhwIBV.js → add-BxpXfVWe.js} +1 -1
  79. package/packages/ui/dist/assets/{all-wallets-BUxsqWXi.js → all-wallets-BKTn_sWK.js} +1 -1
  80. package/packages/ui/dist/assets/{app-store-DkltwTqE.js → app-store-CfuKbwxR.js} +1 -1
  81. package/packages/ui/dist/assets/{apple-owVOeaIT.js → apple-BKSBbNYg.js} +1 -1
  82. package/packages/ui/dist/assets/{arrow-bottom-D2mmNJve.js → arrow-bottom-D4bG6gZi.js} +1 -1
  83. package/packages/ui/dist/assets/{arrow-bottom-circle-CbNYijx-.js → arrow-bottom-circle-BNTs1p0T.js} +1 -1
  84. package/packages/ui/dist/assets/{arrow-left-DJB61s4C.js → arrow-left-2uee3vYv.js} +1 -1
  85. package/packages/ui/dist/assets/{arrow-right-BBrsQ9R4.js → arrow-right-BktjMV6h.js} +1 -1
  86. package/packages/ui/dist/assets/{arrow-top-Cil6bOc8.js → arrow-top-Izu28fX4.js} +1 -1
  87. package/packages/ui/dist/assets/{bank-CbwEmRo3.js → bank-USBaAyFM.js} +1 -1
  88. package/packages/ui/dist/assets/{basic-CLNfjw3m.js → basic-C_9KjTEH.js} +1 -1
  89. package/packages/ui/dist/assets/{browser-B5TtF4Pb.js → browser-DAEMAKV7.js} +1 -1
  90. package/packages/ui/dist/assets/{card-CO7BVB-C.js → card-DT8yDkKN.js} +1 -1
  91. package/packages/ui/dist/assets/{ccip-2W7K3_J3.js → ccip-CkqfGSxX.js} +1 -1
  92. package/packages/ui/dist/assets/{checkmark-BEtSHq9m.js → checkmark-CsgdEXFj.js} +1 -1
  93. package/packages/ui/dist/assets/{checkmark-bold-D9xGHzPE.js → checkmark-bold-D2gjOQo2.js} +1 -1
  94. package/packages/ui/dist/assets/{chevron-bottom-BDztht6i.js → chevron-bottom-tprFynYV.js} +1 -1
  95. package/packages/ui/dist/assets/{chevron-left-EV4GFNbc.js → chevron-left-D2Zj1gNB.js} +1 -1
  96. package/packages/ui/dist/assets/{chevron-right-B4_bB9oR.js → chevron-right-D1rRuAVe.js} +1 -1
  97. package/packages/ui/dist/assets/{chevron-top-D54xPNzF.js → chevron-top-24dL1mbL.js} +1 -1
  98. package/packages/ui/dist/assets/{chrome-store-DYUpAJJq.js → chrome-store-Vy-5niYX.js} +1 -1
  99. package/packages/ui/dist/assets/{clock-Ca1T1Soz.js → clock-qBjLnVdJ.js} +1 -1
  100. package/packages/ui/dist/assets/{close-BZqWjurK.js → close-DARDwgcu.js} +1 -1
  101. package/packages/ui/dist/assets/{coinPlaceholder-e6fl2XDo.js → coinPlaceholder-BvpIbPlD.js} +1 -1
  102. package/packages/ui/dist/assets/{compass-DCLC7zIh.js → compass-BMTO0ayt.js} +1 -1
  103. package/packages/ui/dist/assets/{copy-Th2AaD-O.js → copy-PaXeRHza.js} +1 -1
  104. package/packages/ui/dist/assets/{core-Ckx_cyuH.js → core-BFnStQd-.js} +3 -3
  105. package/packages/ui/dist/assets/cursor-BDvw-B17.js +3 -0
  106. package/packages/ui/dist/assets/{cursor-transparent-BKHeABKB.js → cursor-transparent-BEMdi-8q.js} +1 -1
  107. package/packages/ui/dist/assets/{desktop-CBjY8t6F.js → desktop-CfuLLThw.js} +1 -1
  108. package/packages/ui/dist/assets/{disconnect-DbSs2cli.js → disconnect-DhwgJMiR.js} +1 -1
  109. package/packages/ui/dist/assets/{discord-ZlLOAUkM.js → discord-po8qoN1s.js} +1 -1
  110. package/packages/ui/dist/assets/{etherscan-CKUrqWYN.js → etherscan-BEsz0_yx.js} +1 -1
  111. package/packages/ui/dist/assets/{events-CiKP71cK.js → events-Bz33Unzu.js} +1 -1
  112. package/packages/ui/dist/assets/{exclamation-triangle-DA1QzFiO.js → exclamation-triangle-7CjTAGOQ.js} +1 -1
  113. package/packages/ui/dist/assets/{extension-BVJkmvpJ.js → extension-CmxjEWEt.js} +1 -1
  114. package/packages/ui/dist/assets/{external-link-D_bsR7B2.js → external-link-CmQ--bNS.js} +1 -1
  115. package/packages/ui/dist/assets/{facebook-CmFmhojx.js → facebook-CIBn9b65.js} +1 -1
  116. package/packages/ui/dist/assets/{fallback-Ofl6uSnB.js → fallback-DATyrQlb.js} +1 -1
  117. package/packages/ui/dist/assets/{farcaster-Co-M3Ss8.js → farcaster-OJ3Jasxg.js} +1 -1
  118. package/packages/ui/dist/assets/{filters-B1WwNaFU.js → filters-D4x09zeL.js} +1 -1
  119. package/packages/ui/dist/assets/{github-CP4fP6gn.js → github-ZlIuMArp.js} +1 -1
  120. package/packages/ui/dist/assets/{google-CsOIXJ6V.js → google-Gwg85sfv.js} +1 -1
  121. package/packages/ui/dist/assets/{help-circle-DiMkomdF.js → help-circle-D1uOWYcX.js} +1 -1
  122. package/packages/ui/dist/assets/{id-lmscL5LX.js → id-C0-5UdYk.js} +1 -1
  123. package/packages/ui/dist/assets/{image-B-ubJrY5.js → image-D_DUsv8-.js} +1 -1
  124. package/packages/ui/dist/assets/{index-CZR1Qjhs.js → index-BCzex_R6.js} +1 -1
  125. package/packages/ui/dist/assets/index-BUhrHLpY.js +1775 -0
  126. package/packages/ui/dist/assets/index-Cq02kQmy.css +1 -0
  127. package/packages/ui/dist/assets/{index-BaukYv-x.js → index-CrYzBWfD.js} +1 -1
  128. package/packages/ui/dist/assets/{index-CF0KMmke.js → index-DdbJhIdl.js} +3 -3
  129. package/packages/ui/dist/assets/{index-DVgfCzCo.js → index-DiojfeVM.js} +1 -1
  130. package/packages/ui/dist/assets/{index-Dbh5V1Z0.js → index-izd7vu_r.js} +1 -1
  131. package/packages/ui/dist/assets/{index.es-C78cE5SI.js → index.es-DdkHhQAj.js} +4 -4
  132. package/packages/ui/dist/assets/{info-Cqg57EVo.js → info-CiRd_kEG.js} +1 -1
  133. package/packages/ui/dist/assets/{info-circle-DkeSWNKV.js → info-circle-ypxjqarK.js} +1 -1
  134. package/packages/ui/dist/assets/{lightbulb-DNlO4qKh.js → lightbulb-B-pxLxd8.js} +1 -1
  135. package/packages/ui/dist/assets/{mail-kVQ8Jb9Y.js → mail-BYmicuVZ.js} +1 -1
  136. package/packages/ui/dist/assets/{metamask-sdk-CBalSvz7.js → metamask-sdk-Ccl6DG7Q.js} +1 -1
  137. package/packages/ui/dist/assets/{mobile-BEteuhF7.js → mobile-CtP5PqVT.js} +1 -1
  138. package/packages/ui/dist/assets/{more-DBWmXQli.js → more-6C2733we.js} +1 -1
  139. package/packages/ui/dist/assets/{network-placeholder-Dg1uUHiL.js → network-placeholder-CdhxMzqd.js} +1 -1
  140. package/packages/ui/dist/assets/{nftPlaceholder-i3AHSiD9.js → nftPlaceholder-DVmTWEAY.js} +1 -1
  141. package/packages/ui/dist/assets/{off-BtMm0fi2.js → off-DNYLughs.js} +1 -1
  142. package/packages/ui/dist/assets/{parseSignature-Cb5FlWWg.js → parseSignature-Dq2B5Bu3.js} +1 -1
  143. package/packages/ui/dist/assets/{play-store-iKKkXa6a.js → play-store-D7Qut5ta.js} +1 -1
  144. package/packages/ui/dist/assets/{plus-CA5NaRtb.js → plus-kqMyjt3q.js} +1 -1
  145. package/packages/ui/dist/assets/{qr-code-D2kiqR7h.js → qr-code-DiUCWRbz.js} +1 -1
  146. package/packages/ui/dist/assets/{recycle-horizontal-Dcme7R03.js → recycle-horizontal-Boe3XiS-.js} +1 -1
  147. package/packages/ui/dist/assets/{refresh-Dega3sDp.js → refresh-CrBgBQYO.js} +1 -1
  148. package/packages/ui/dist/assets/{reown-logo-xNkksyWJ.js → reown-logo-CFZCCHSx.js} +1 -1
  149. package/packages/ui/dist/assets/{search-HYl7NO8x.js → search-ChTDrghU.js} +1 -1
  150. package/packages/ui/dist/assets/{secp256k1-Cxd6_SiH.js → secp256k1-DAV5Q_FR.js} +1 -1
  151. package/packages/ui/dist/assets/{send-CJU8CUAo.js → send-DLFbBFe1.js} +1 -1
  152. package/packages/ui/dist/assets/{swapHorizontal-IMUKiUre.js → swapHorizontal-BEs3emfG.js} +1 -1
  153. package/packages/ui/dist/assets/{swapHorizontalBold-CNYnNJ9-.js → swapHorizontalBold-CC-Hfa7W.js} +1 -1
  154. package/packages/ui/dist/assets/{swapHorizontalMedium-B9VxEYsT.js → swapHorizontalMedium-BmR0H8DC.js} +1 -1
  155. package/packages/ui/dist/assets/{swapHorizontalRoundedBold-Dz33l_Jh.js → swapHorizontalRoundedBold-BdP5NGIH.js} +1 -1
  156. package/packages/ui/dist/assets/{swapVertical-CHUmjVJ0.js → swapVertical-CPrGEJPY.js} +1 -1
  157. package/packages/ui/dist/assets/{telegram-kl9S2mbU.js → telegram-CxNoZ80Q.js} +1 -1
  158. package/packages/ui/dist/assets/{three-dots-U5lhA1Am.js → three-dots-BRa6SBpL.js} +1 -1
  159. package/packages/ui/dist/assets/{twitch-KTEUWXEp.js → twitch-BC338bG5.js} +1 -1
  160. package/packages/ui/dist/assets/{twitterIcon-BHiq8mRg.js → twitterIcon-BGZmt2i9.js} +1 -1
  161. package/packages/ui/dist/assets/{verify-CfN-BXNd.js → verify-CEstW0zw.js} +1 -1
  162. package/packages/ui/dist/assets/{verify-filled-DwZccetj.js → verify-filled-OkZb0weU.js} +1 -1
  163. package/packages/ui/dist/assets/{w3m-modal-CS-PFqPE.js → w3m-modal-pS09ECwE.js} +1 -1
  164. package/packages/ui/dist/assets/{wallet-DVlGkhOY.js → wallet-BXVKCgC9.js} +1 -1
  165. package/packages/ui/dist/assets/{wallet-placeholder-CvR_iEWX.js → wallet-placeholder-C_kNhB1c.js} +1 -1
  166. package/packages/ui/dist/assets/{walletconnect-8pZBDvVI.js → walletconnect-CRKIuUHH.js} +1 -1
  167. package/packages/ui/dist/assets/{warning-circle-ylLEE0Yp.js → warning-circle-DB2NnwlJ.js} +1 -1
  168. package/packages/ui/dist/assets/{x-C_TBsTMj.js → x-DT4RmwL5.js} +1 -1
  169. package/packages/ui/dist/index.html +14 -14
  170. package/scripts/check-docs.mjs +262 -262
  171. package/scripts/check-init.mjs +108 -109
  172. package/scripts/postinstall.js +81 -366
  173. package/templates/custom-mandate/.sail/contracts/interfaces/IPermission.sol +18 -18
  174. package/templates/custom-mandate/README.md +116 -85
  175. package/templates/custom-mandate/foundry.toml +8 -8
  176. package/templates/custom-mandate/mandates/BoundedCallPermission.sol +41 -35
  177. package/templates/custom-mandate/mandates/README.md +16 -16
  178. package/templates/custom-mandate/mandates/SailCalldata.sol +118 -0
  179. package/templates/{dca-rebalancer → default}/.cursor/rules +25 -25
  180. package/templates/default/.env.example +20 -0
  181. package/templates/{dca-rebalancer → default}/.github/workflows/agent-tick.yml +33 -32
  182. package/templates/{dca-rebalancer → default}/.sail/README.md +13 -13
  183. package/templates/{dca-rebalancer → default}/.sail/config.json +10 -10
  184. package/templates/default/AGENTS.md +171 -0
  185. package/templates/{dca-rebalancer → default}/CLAUDE.md +2 -2
  186. package/templates/default/README.md +16 -0
  187. package/templates/{dca-rebalancer → default}/_gitignore +13 -13
  188. package/templates/{dca-rebalancer → default}/docs/PERMISSION_MODEL.md +93 -93
  189. package/templates/default/examples/dca/README.md +16 -0
  190. package/templates/default/examples/dca/agent.ts +174 -0
  191. package/templates/{dca-rebalancer/src → default/examples/dca}/mandate.ts +45 -67
  192. package/templates/{dca-rebalancer → default}/package.json +17 -17
  193. package/templates/default/src/agent.ts +37 -0
  194. package/templates/default/src/config.ts +24 -0
  195. package/templates/default/src/mandate.ts +22 -0
  196. package/templates/default/tsconfig.json +17 -0
  197. package/templates/{dca-rebalancer → default}/ui/README.md +3 -3
  198. package/templates/lifi-permissions/LifiBoundedApprovePermissionCloneable.sol +84 -84
  199. package/templates/lifi-permissions/LifiDiamondSwapPermissionCloneable.sol +97 -97
  200. package/templates/lifi-permissions/README.md +53 -53
  201. package/packages/ui/dist/assets/cursor-DV7rOqbJ.js +0 -3
  202. package/packages/ui/dist/assets/index-CKxgNxS9.css +0 -1
  203. package/packages/ui/dist/assets/index-Q2Yai4Fe.js +0 -2103
  204. package/templates/dca-rebalancer/.env.example +0 -6
  205. package/templates/dca-rebalancer/AGENTS.md +0 -246
  206. package/templates/dca-rebalancer/AGENT_PLAYBOOK.md +0 -110
  207. package/templates/dca-rebalancer/README.md +0 -16
  208. package/templates/dca-rebalancer/src/agent.ts +0 -253
  209. package/templates/dca-rebalancer/src/config.ts +0 -27
  210. package/templates/dca-rebalancer/tsconfig.json +0 -8
  211. /package/templates/{dca-rebalancer → default}/.sail/.gitkeep +0 -0
@@ -0,0 +1,82 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.26;
3
+
4
+ // ─────────────────────────────────────────────────────────────────────────────
5
+ // Protocol : Aave V3
6
+ // Version : Pool (proxy) — fully on-chain, oracle-based accounting
7
+ // Chain : Arbitrum mainnet (42161)
8
+ // Target : Aave V3 Pool 0x794a61358D6845594F94dc1DB02A252b5b4814aD (verified on Arbiscan;
9
+ // same Pool as BoundedBorrow_AaveV3_Arbitrum — supply 0x617ba037 confirmed present
10
+ // in the live implementation 0xf05fd3cc...)
11
+ //
12
+ // ENFORCES ON-CHAIN (kernel calls evaluate() on every dispatch; false ⇒ dispatch blocked):
13
+ // supply(address asset,uint256 amount,address onBehalfOf,uint16 referralCode) selector 0x617ba037
14
+ // • target must be AAVE_POOL
15
+ // • asset must be in ALLOWED_ASSETS
16
+ // • amount ≤ MAX_SUPPLY_AMOUNT
17
+ // • onBehalfOf must equal ctx.account (the SMA — collateral is credited to the SMA, not
18
+ // to another account)
19
+ //
20
+ // AGENT-ENFORCED / NOT BOUNDED HERE (off-chain — can change without redeploying this contract):
21
+ // • referralCode (informational only, does not affect fund safety)
22
+ // • Supply timing / cadence and choice of asset within ALLOWED_ASSETS
23
+ // • Withdrawal: not gated here (add a withdraw(asset,amount,to) permission with to==SMA if needed)
24
+ // • A supply also needs a prior ERC-20 approve of the Pool — gate that separately
25
+ // (see BoundedApproveAndCallBatch.sol for the atomic approve→call→reset pattern).
26
+ //
27
+ // VERIFY BEFORE USE:
28
+ // • Selector 0x617ba037 = supply(address,uint256,address,uint16) — verified via `cast sig`
29
+ // and confirmed present in the deployed Pool implementation on Arbitrum.
30
+ // • This is the canonical Aave V3 Pool.supply. (V3.x also exposes supplyWithPermit — not
31
+ // gated here; add it if your agent supplies via EIP-2612 permits.)
32
+ // • MAX_SUPPLY_AMOUNT is in the asset's base units (e.g. USDC = 6 decimals).
33
+ // ─────────────────────────────────────────────────────────────────────────────
34
+
35
+ import {IPermission, Context} from "@sail/interfaces/IPermission.sol";
36
+ import {SailCalldata} from "./SailCalldata.sol";
37
+
38
+ contract BoundedSupply_AaveV3_Arbitrum is IPermission {
39
+ bytes32 private constant DISCRIMINATOR = keccak256("BoundedSupply_AaveV3_Arbitrum");
40
+
41
+ address public immutable AAVE_POOL;
42
+ mapping(address => bool) public isAllowedAsset;
43
+ uint256 public immutable MAX_SUPPLY_AMOUNT;
44
+
45
+ // supply(address,uint256,address,uint16) — verified 0x617ba037
46
+ bytes4 private constant SEL_SUPPLY = 0x617ba037;
47
+
48
+ /// @param aavePool Aave V3 Pool proxy address
49
+ /// @param allowedAssets Assets the agent may supply (must be non-empty)
50
+ /// @param maxSupplyAmount Per-call supply cap in asset base units (must be > 0)
51
+ constructor(address aavePool, address[] memory allowedAssets, uint256 maxSupplyAmount) {
52
+ require(aavePool != address(0), "zero pool address");
53
+ require(allowedAssets.length > 0, "empty asset allowlist");
54
+ require(maxSupplyAmount > 0, "zero supply cap");
55
+ AAVE_POOL = aavePool;
56
+ MAX_SUPPLY_AMOUNT = maxSupplyAmount;
57
+ for (uint256 i = 0; i < allowedAssets.length; i++) {
58
+ require(allowedAssets[i] != address(0), "zero asset address");
59
+ isAllowedAsset[allowedAssets[i]] = true;
60
+ }
61
+ }
62
+
63
+ function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool) {
64
+ if (ctx.target != AAVE_POOL) return false;
65
+ if (ctx.selector != SEL_SUPPLY) return false;
66
+ // supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode)
67
+ if (!SailCalldata.hasParams(txData, 4)) return false;
68
+
69
+ address asset = SailCalldata.asAddress(txData, 0);
70
+ uint256 amount = SailCalldata.asUint256(txData, 1);
71
+ address onBehalfOf = SailCalldata.asAddress(txData, 2);
72
+ // slot 3 = referralCode (uint16) — informational, not bounded
73
+
74
+ if (!isAllowedAsset[asset]) return false;
75
+ if (amount > MAX_SUPPLY_AMOUNT) return false;
76
+ if (onBehalfOf != ctx.account) return false; // collateral credited only to the SMA
77
+
78
+ return true;
79
+ }
80
+
81
+ function discriminator() external pure returns (bytes32) { return DISCRIMINATOR; }
82
+ }
@@ -1,113 +1,116 @@
1
- // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
3
-
4
- // ─────────────────────────────────────────────────────────────────────────────
5
- // Protocol : Uniswap V3
6
- // Version : SwapRouter02 (NOT the older SwapRouter — different selectors)
7
- // Chain : Base mainnet
8
- // Target : SwapRouter02 0x2626664c2603336E57B271c5C0b26F421741e481
9
- //
10
- // ENFORCED ON-CHAIN (via kernel evaluate() on every dispatch):
11
- // exactInputSingle path (selector 0x04e45aaf):
12
- // • target must be SWAP_ROUTER
13
- // • tokenIn must equal FIXED_TOKEN_IN
14
- // • tokenOut must be in ALLOWED_TOKENS_OUT
15
- // • amountIn ≤ MAX_AMOUNT_IN
16
- // • amountOutMinimum ≥ amountIn × MIN_BPS / 10 000 (slippage floor)
17
- // approve path (selector 0x095ea7b3):
18
- // • target must be FIXED_TOKEN_IN (the ERC-20 being approved)
19
- // • spender must be SWAP_ROUTER
20
- // • amount ≤ MAX_AMOUNT_IN
21
- //
22
- // NOT ENFORCED (agent code — can change without a new contract):
23
- // • fee tier, sqrtPriceLimitX96, recipient address
24
- // • swap frequency / cadence
25
- //
26
- // VERIFY BEFORE USE:
27
- // Confirm SwapRouter02 address on your chain (Base default shown above).
28
- // • Selector 0x04e45aaf = exactInputSingle((address,address,uint24,address,uint256,uint256,uint160))
29
- // on SwapRouter02. The older SwapRouter uses a different selector (0x414577b7).
30
- // Confirm that amountOutMinimum slippage floor fits your price-impact expectations for the
31
- // chosen pool. Large amountIn values may trigger price impact beyond MIN_BPS.
32
- // ─────────────────────────────────────────────────────────────────────────────
33
-
34
- import {IPermission, Context} from "@sail/interfaces/IPermission.sol";
35
-
36
- contract BoundedSwap_UniswapV3_Base is IPermission {
37
- bytes32 private constant DISCRIMINATOR = keccak256("BoundedSwap_UniswapV3_Base");
38
-
39
- address public immutable SWAP_ROUTER;
40
- address public immutable FIXED_TOKEN_IN;
41
- mapping(address => bool) public isAllowedTokenOut;
42
- uint256 public immutable MAX_AMOUNT_IN;
43
- /// @dev Slippage floor in basis points. 9 900 = allow at most 1% slippage.
44
- uint256 public immutable MIN_BPS;
45
-
46
- bytes4 private constant SEL_EXACT_INPUT_SINGLE = 0x04e45aaf;
47
- bytes4 private constant SEL_APPROVE = 0x095ea7b3;
48
-
49
- struct ExactInputSingleParams {
50
- address tokenIn;
51
- address tokenOut;
52
- uint24 fee;
53
- address recipient;
54
- uint256 amountIn;
55
- uint256 amountOutMinimum;
56
- uint160 sqrtPriceLimitX96;
57
- }
58
-
59
- /// @param swapRouter SwapRouter02 address (0x2626... on Base)
60
- /// @param fixedTokenIn The one token the agent is allowed to sell
61
- /// @param allowedTokensOut Tokens the agent may receive
62
- /// @param maxAmountIn Per-call spend cap in fixedTokenIn base units
63
- /// @param minBps Minimum amountOutMinimum as fraction of amountIn ×10 000
64
- /// e.g. 9900 require amountOutMinimum 99% of amountIn
65
- /// (denominated in fixedTokenIn units, not USD set per your pair)
66
- constructor(
67
- address swapRouter,
68
- address fixedTokenIn,
69
- address[] memory allowedTokensOut,
70
- uint256 maxAmountIn,
71
- uint256 minBps
72
- ) {
73
- require(minBps <= 10_000, "minBps > 10000");
74
- SWAP_ROUTER = swapRouter;
75
- FIXED_TOKEN_IN = fixedTokenIn;
76
- MAX_AMOUNT_IN = maxAmountIn;
77
- MIN_BPS = minBps;
78
- for (uint256 i = 0; i < allowedTokensOut.length; i++) {
79
- isAllowedTokenOut[allowedTokensOut[i]] = true;
80
- }
81
- }
82
-
83
- function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool) {
84
- if (txData.length < 4) return false;
85
-
86
- // exactInputSingle: agent swaps FIXED_TOKEN_IN for an allowed output token
87
- if (ctx.target == SWAP_ROUTER && ctx.selector == SEL_EXACT_INPUT_SINGLE) {
88
- if (txData.length < 4 + 7 * 32) return false;
89
- ExactInputSingleParams memory p = abi.decode(txData[4:], (ExactInputSingleParams));
90
- if (p.tokenIn != FIXED_TOKEN_IN) return false;
91
- if (!isAllowedTokenOut[p.tokenOut]) return false;
92
- if (p.amountIn > MAX_AMOUNT_IN) return false;
93
- // Slippage floor: require amountOutMinimum ≥ amountIn * MIN_BPS / 10 000.
94
- // This is not a USD check — it's a ratio check in tokenIn units.
95
- // For a USDC→WETH swap, set MIN_BPS conservatively (e.g. 9 900 for 1% slippage).
96
- if (p.amountOutMinimum < (p.amountIn * MIN_BPS) / 10_000) return false;
97
- return true;
98
- }
99
-
100
- // approve: agent approves the router to spend FIXED_TOKEN_IN
101
- if (ctx.target == FIXED_TOKEN_IN && ctx.selector == SEL_APPROVE) {
102
- if (txData.length < 4 + 2 * 32) return false;
103
- (address spender, uint256 amount) = abi.decode(txData[4:], (address, uint256));
104
- if (spender != SWAP_ROUTER) return false;
105
- if (amount > MAX_AMOUNT_IN) return false;
106
- return true;
107
- }
108
-
109
- return false;
110
- }
111
-
112
- function discriminator() external pure returns (bytes32) { return DISCRIMINATOR; }
113
- }
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.26;
3
+
4
+ // ─────────────────────────────────────────────────────────────────────────────
5
+ // Protocol : Uniswap V3
6
+ // Version : SwapRouter02 (NOT the older SwapRouter — different selectors)
7
+ // Chain : Base mainnet (8453)
8
+ // Target : SwapRouter02 0x2626664c2603336E57B271c5C0b26F421741e481 (verified on Basescan)
9
+ //
10
+ // ENFORCES ON-CHAIN (kernel calls evaluate() on every dispatch; false ⇒ dispatch blocked):
11
+ // exactInputSingle((address,address,uint24,address,uint256,uint256,uint160)) selector 0x04e45aaf
12
+ // • target must be SWAP_ROUTER
13
+ // • tokenIn must equal FIXED_TOKEN_IN
14
+ // • tokenOut must be in ALLOWED_TOKENS_OUT
15
+ // • amountIn ≤ MAX_AMOUNT_IN
16
+ // • amountOutMinimum ≥ amountIn × MIN_BPS / 10 000 (slippage floor — see caveat below)
17
+ // approve(address,uint256) selector 0x095ea7b3
18
+ // • target must be FIXED_TOKEN_IN (the ERC-20 being approved)
19
+ // • spender must be SWAP_ROUTER
20
+ // • amount ≤ MAX_AMOUNT_IN
21
+ //
22
+ // AGENT-ENFORCED / NOT BOUNDED HERE (off-chain — can change without redeploying this contract):
23
+ // • fee tier, sqrtPriceLimitX96, recipient address
24
+ // • swap frequency / cadence
25
+ // • real (cross-denomination) slippage — see MIN_BPS caveat in evaluate()
26
+ //
27
+ // VERIFY BEFORE USE:
28
+ // • Confirm SwapRouter02 address on your chain (Base default shown above; verified on Basescan).
29
+ // Selector 0x04e45aaf = exactInputSingle((address,address,uint24,address,uint256,uint256,uint160))
30
+ // on SwapRouter02 (verified via `cast sig`). The OLDER SwapRouter's exactInputSingle (the
31
+ // deadline variant) is 0x414bf389 a different selector; do not confuse the two.
32
+ // Confirm that amountOutMinimum slippage floor fits your price-impact expectations for the
33
+ // chosen pool. Large amountIn values may trigger price impact beyond MIN_BPS.
34
+ // ─────────────────────────────────────────────────────────────────────────────
35
+
36
+ import {IPermission, Context} from "@sail/interfaces/IPermission.sol";
37
+
38
+ contract BoundedSwap_UniswapV3_Base is IPermission {
39
+ bytes32 private constant DISCRIMINATOR = keccak256("BoundedSwap_UniswapV3_Base");
40
+
41
+ address public immutable SWAP_ROUTER;
42
+ address public immutable FIXED_TOKEN_IN;
43
+ mapping(address => bool) public isAllowedTokenOut;
44
+ uint256 public immutable MAX_AMOUNT_IN;
45
+ /// @dev Slippage floor in basis points. 9 900 = allow at most 1% slippage.
46
+ uint256 public immutable MIN_BPS;
47
+
48
+ bytes4 private constant SEL_EXACT_INPUT_SINGLE = 0x04e45aaf;
49
+ bytes4 private constant SEL_APPROVE = 0x095ea7b3;
50
+
51
+ struct ExactInputSingleParams {
52
+ address tokenIn;
53
+ address tokenOut;
54
+ uint24 fee;
55
+ address recipient;
56
+ uint256 amountIn;
57
+ uint256 amountOutMinimum;
58
+ uint160 sqrtPriceLimitX96;
59
+ }
60
+
61
+ /// @param swapRouter SwapRouter02 address (0x2626... on Base)
62
+ /// @param fixedTokenIn The one token the agent is allowed to sell
63
+ /// @param allowedTokensOut Tokens the agent may receive
64
+ /// @param maxAmountIn Per-call spend cap in fixedTokenIn base units
65
+ /// @param minBps Minimum amountOutMinimum as fraction of amountIn ×10 000
66
+ /// e.g. 9900 → require amountOutMinimum ≥ 99% of amountIn
67
+ /// (denominated in fixedTokenIn units, not USD — set per your pair)
68
+ constructor(
69
+ address swapRouter,
70
+ address fixedTokenIn,
71
+ address[] memory allowedTokensOut,
72
+ uint256 maxAmountIn,
73
+ uint256 minBps
74
+ ) {
75
+ require(minBps <= 10_000, "minBps > 10000");
76
+ SWAP_ROUTER = swapRouter;
77
+ FIXED_TOKEN_IN = fixedTokenIn;
78
+ MAX_AMOUNT_IN = maxAmountIn;
79
+ MIN_BPS = minBps;
80
+ for (uint256 i = 0; i < allowedTokensOut.length; i++) {
81
+ isAllowedTokenOut[allowedTokensOut[i]] = true;
82
+ }
83
+ }
84
+
85
+ function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool) {
86
+ if (txData.length < 4) return false;
87
+
88
+ // exactInputSingle: agent swaps FIXED_TOKEN_IN for an allowed output token
89
+ if (ctx.target == SWAP_ROUTER && ctx.selector == SEL_EXACT_INPUT_SINGLE) {
90
+ if (txData.length < 4 + 7 * 32) return false;
91
+ ExactInputSingleParams memory p = abi.decode(txData[4:], (ExactInputSingleParams));
92
+ if (p.tokenIn != FIXED_TOKEN_IN) return false;
93
+ if (!isAllowedTokenOut[p.tokenOut]) return false;
94
+ if (p.amountIn > MAX_AMOUNT_IN) return false;
95
+ // Slippage floor: amountOutMinimum amountIn × MIN_BPS / 10 000.
96
+ // WARNING: compares tokenOut against tokenIn base units. For same-price/same-decimal
97
+ // pairs this maps to a slippage %. For cross-price pairs (e.g. USDC→WETH) it is
98
+ // trivially satisfied — real slippage is enforced by the agent off-chain, not here.
99
+ if (p.amountOutMinimum < (p.amountIn * MIN_BPS) / 10_000) return false;
100
+ return true;
101
+ }
102
+
103
+ // approve: agent approves the router to spend FIXED_TOKEN_IN
104
+ if (ctx.target == FIXED_TOKEN_IN && ctx.selector == SEL_APPROVE) {
105
+ if (txData.length < 4 + 2 * 32) return false;
106
+ (address spender, uint256 amount) = abi.decode(txData[4:], (address, uint256));
107
+ if (spender != SWAP_ROUTER) return false;
108
+ if (amount > MAX_AMOUNT_IN) return false;
109
+ return true;
110
+ }
111
+
112
+ return false;
113
+ }
114
+
115
+ function discriminator() external pure returns (bytes32) { return DISCRIMINATOR; }
116
+ }
@@ -1,144 +1,150 @@
1
- // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
3
-
4
- // ─────────────────────────────────────────────────────────────────────────────
5
- // Protocol : Uniswap V4
6
- // Version : Universal Router + PoolManager (V4 singleton)
7
- // NOTE: V4 is NOT the same as V3. Calldata encoding is completely
8
- // different — do NOT adapt a V3 permission for V4.
9
- // Chain : Unichain mainnet
10
- // Target : Universal Router 0xef740bf23acae26f6492b10de645d6b98dc8eaf3
11
- // (verify on Uniscan before use)
12
- //
13
- // ENFORCED ON-CHAIN (via kernel evaluate() on every dispatch):
14
- // execute(bytes,bytes[]) or execute(bytes,bytes[],uint256):
15
- // • target must be UNIVERSAL_ROUTER
16
- // • first command byte (masking the allow-failure MSB) must be V4_SWAP (0x10)
17
- // • exactly one command (single-swap path — disallow multi-hop command strings)
18
- // • V4 action inside must be SWAP_EXACT_IN_SINGLE (0x00)
19
- // • tokenIn (from poolKey, derived by zeroForOne) must be FIXED_CURRENCY_IN
20
- // • tokenOut must be in ALLOWED_CURRENCIES_OUT
21
- // • amountIn ≤ MAX_AMOUNT_IN
22
- // • amountOutMinimum ≥ amountIn × MIN_BPS / 10 000
23
- //
24
- // NOT ENFORCEDdocumented limitations:
25
- // • hookData is not inspected (hooks can alter swap behavior on-chain; if the
26
- // pool uses a hook that significantly changes execution, this permission cannot
27
- // constrain it. Deploy against pools with address(0) hooks or audited hooks only.)
28
- // fee tier and tickSpacing within the PoolKey are not constrained here
29
- // (add pool-key checks if you want to restrict to a specific pool)
30
- // The ALL_CURRENCY_PAIR constant (FIXED_CURRENCY_IN, allowedCurrenciesOut) does
31
- // not constrain which pool is used when multiple pools share the same currency pair
32
- //
33
- // VERIFY BEFORE USE:
34
- // • Confirm Universal Router address on Unichain (shown above; verify on Uniscan).
35
- // V4_SWAP command byte = 0x10, SWAP_EXACT_IN_SINGLE action = 0x00 verify
36
- // against deployed UniversalRouter and V4Router on Unichain if contract is updated.
37
- // PoolKey struct layout (currency0, currency1, fee, tickSpacing, hooks) must
38
- // match the deployed PoolManager on Unichain. If layout changes, update struct.
39
- // • hookData is not bounded. Only use with unhookyed pools or audited, bounded hooks.
40
- // Calldata revert = false (kernel treats revert as denial) — malformed inputs
41
- // are safely rejected, but verify with actual calldata samples before deployment.
42
- // ─────────────────────────────────────────────────────────────────────────────
43
-
44
- import {IPermission, Context} from "@sail/interfaces/IPermission.sol";
45
-
46
- contract BoundedSwap_UniswapV4_Unichain is IPermission {
47
- bytes32 private constant DISCRIMINATOR = keccak256("BoundedSwap_UniswapV4_Unichain");
48
-
49
- address public immutable UNIVERSAL_ROUTER;
50
- address public immutable FIXED_CURRENCY_IN;
51
- mapping(address => bool) public isAllowedCurrencyOut;
52
- uint256 public immutable MAX_AMOUNT_IN;
53
- uint256 public immutable MIN_BPS;
54
-
55
- // execute(bytes,bytes[],uint256) with deadline
56
- bytes4 private constant SEL_EXECUTE_DEADLINE = 0x3593564c;
57
- // execute(bytes,bytes[]) — without deadline
58
- bytes4 private constant SEL_EXECUTE = 0x24856bc3;
59
- // Universal Router command byte for V4_SWAP
60
- uint8 private constant CMD_V4_SWAP = 0x10;
61
- // Bit mask to strip the "allow failure" MSB from a command byte
62
- uint8 private constant CMD_MASK = 0x3f;
63
- // V4Router action: SWAP_EXACT_IN_SINGLE
64
- uint8 private constant ACT_SWAP_EXACT_IN_SINGLE = 0x00;
65
-
66
- // PoolKey layout must match the deployed V4 PoolManager on Unichain
67
- struct PoolKey {
68
- address currency0; // Currency address type in V4
69
- address currency1;
70
- uint24 fee;
71
- int24 tickSpacing;
72
- address hooks; // IHooks — address(0) for unhookyed pools
73
- }
74
-
75
- struct ExactInputSingleParams {
76
- PoolKey poolKey;
77
- bool zeroForOne;
78
- uint128 amountIn;
79
- uint128 amountOutMinimum;
80
- bytes hookData; // not inspected — see limitations header
81
- }
82
-
83
- constructor(
84
- address universalRouter,
85
- address fixedCurrencyIn,
86
- address[] memory allowedCurrenciesOut,
87
- uint256 maxAmountIn,
88
- uint256 minBps
89
- ) {
90
- require(minBps <= 10_000, "minBps > 10000");
91
- UNIVERSAL_ROUTER = universalRouter;
92
- FIXED_CURRENCY_IN = fixedCurrencyIn;
93
- MAX_AMOUNT_IN = maxAmountIn;
94
- MIN_BPS = minBps;
95
- for (uint256 i = 0; i < allowedCurrenciesOut.length; i++) {
96
- isAllowedCurrenciesOut[allowedCurrenciesOut[i]] = true;
97
- }
98
- }
99
-
100
- mapping(address => bool) private isAllowedCurrenciesOut;
101
-
102
- function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool) {
103
- if (ctx.target != UNIVERSAL_ROUTER) return false;
104
- if (ctx.selector != SEL_EXECUTE_DEADLINE && ctx.selector != SEL_EXECUTE) return false;
105
- if (txData.length < 4) return false;
106
-
107
- // Decode the execute call. Both overloads start with (bytes commands, bytes[] inputs).
108
- // abi.decode ignores trailing fields, so decoding as (bytes, bytes[]) works for both.
109
- (bytes memory commands, bytes[] memory inputs) = abi.decode(txData[4:], (bytes, bytes[]));
110
-
111
- // Enforce: exactly one command, and it must be V4_SWAP
112
- if (commands.length != 1) return false;
113
- if ((uint8(commands[0]) & CMD_MASK) != CMD_V4_SWAP) return false;
114
- if (inputs.length != 1) return false;
115
-
116
- // Decode the V4 router call (actions + per-action params)
117
- (bytes memory v4Actions, bytes[] memory v4Params) =
118
- abi.decode(inputs[0], (bytes, bytes[]));
119
-
120
- // Enforce: exactly one V4 action, and it must be SWAP_EXACT_IN_SINGLE
121
- if (v4Actions.length != 1) return false;
122
- if (uint8(v4Actions[0]) != ACT_SWAP_EXACT_IN_SINGLE) return false;
123
- if (v4Params.length != 1) return false;
124
-
125
- // Decode ExactInputSingleParams from the action param.
126
- // hookData is a dynamic bytes field — revert here means false (fail closed).
127
- ExactInputSingleParams memory p = abi.decode(v4Params[0], (ExactInputSingleParams));
128
-
129
- // Derive tokenIn and tokenOut from the PoolKey and zeroForOne flag.
130
- // In V4, currency0 < currency1 (sorted by address). zeroForOne = true means
131
- // trading currency0 for currency1.
132
- address tokenIn = p.zeroForOne ? p.poolKey.currency0 : p.poolKey.currency1;
133
- address tokenOut = p.zeroForOne ? p.poolKey.currency1 : p.poolKey.currency0;
134
-
135
- if (tokenIn != FIXED_CURRENCY_IN) return false;
136
- if (!isAllowedCurrenciesOut[tokenOut]) return false;
137
- if (p.amountIn > MAX_AMOUNT_IN) return false;
138
- if (p.amountOutMinimum < (uint256(p.amountIn) * MIN_BPS) / 10_000) return false;
139
-
140
- return true;
141
- }
142
-
143
- function discriminator() external pure returns (bytes32) { return DISCRIMINATOR; }
144
- }
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.26;
3
+
4
+ // ─────────────────────────────────────────────────────────────────────────────
5
+ // Protocol : Uniswap V4
6
+ // Version : Universal Router + PoolManager (V4 singleton)
7
+ // NOTE: V4 is NOT the same as V3. Calldata encoding is completely
8
+ // different — do NOT adapt a V3 permission for V4.
9
+ // Chain : Unichain mainnet
10
+ // Target : Universal Router 0xef740bf23acae26f6492b10de645d6b98dc8eaf3
11
+ // (verify on Uniscan before use)
12
+ //
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
+ // • target must be UNIVERSAL_ROUTER
16
+ // • first command byte (masking the allow-failure MSB) must be V4_SWAP (0x10)
17
+ // • exactly one command (single-swap path — disallow multi-hop command strings)
18
+ // • V4 action inside must be SWAP_EXACT_IN_SINGLE (0x00)
19
+ // • tokenIn (from poolKey, derived by zeroForOne) must be FIXED_CURRENCY_IN
20
+ // • tokenOut must be in ALLOWED_CURRENCIES_OUT
21
+ // • amountIn ≤ MAX_AMOUNT_IN
22
+ // • amountOutMinimum ≥ amountIn × MIN_BPS / 10 000 (slippage floor — see caveat below)
23
+ //
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):
29
+ // hookData is not inspected (hooks can alter swap behavior on-chain; if the
30
+ // pool uses a hook that significantly changes execution, this permission cannot
31
+ // constrain it. Deploy against pools with address(0) hooks or audited hooks only.)
32
+ // • fee tier and tickSpacing within the PoolKey are not constrained here
33
+ // (add pool-key checks if you want to restrict to a specific pool)
34
+ // • The ALL_CURRENCY_PAIR constant (FIXED_CURRENCY_IN, allowedCurrenciesOut) does
35
+ // not constrain which pool is used when multiple pools share the same currency pair
36
+ //
37
+ // VERIFY BEFORE USE:
38
+ // Confirm Universal Router address on Unichain (shown above; verify on Uniscan).
39
+ // • V4_SWAP command byte = 0x10, SWAP_EXACT_IN_SINGLE action = 0x00 verify
40
+ // against deployed UniversalRouter and V4Router on Unichain if contract is updated.
41
+ // PoolKey struct layout (currency0, currency1, fee, tickSpacing, hooks) must
42
+ // match the deployed PoolManager on Unichain. If layout changes, update struct.
43
+ // • hookData is not bounded. Only use with unhookyed pools or audited, bounded hooks.
44
+ // • Calldata revert = false (kernel treats revert as denial) — malformed inputs
45
+ // are safely rejected, but verify with actual calldata samples before deployment.
46
+ // ─────────────────────────────────────────────────────────────────────────────
47
+
48
+ import {IPermission, Context} from "@sail/interfaces/IPermission.sol";
49
+
50
+ contract BoundedSwap_UniswapV4_Unichain is IPermission {
51
+ bytes32 private constant DISCRIMINATOR = keccak256("BoundedSwap_UniswapV4_Unichain");
52
+
53
+ address public immutable UNIVERSAL_ROUTER;
54
+ address public immutable FIXED_CURRENCY_IN;
55
+ mapping(address => bool) public isAllowedCurrencyOut;
56
+ uint256 public immutable MAX_AMOUNT_IN;
57
+ uint256 public immutable MIN_BPS;
58
+
59
+ // execute(bytes,bytes[],uint256) with deadline
60
+ bytes4 private constant SEL_EXECUTE_DEADLINE = 0x3593564c;
61
+ // execute(bytes,bytes[]) — without deadline
62
+ bytes4 private constant SEL_EXECUTE = 0x24856bc3;
63
+ // Universal Router command byte for V4_SWAP
64
+ uint8 private constant CMD_V4_SWAP = 0x10;
65
+ // Bit mask to strip the "allow failure" MSB from a command byte
66
+ uint8 private constant CMD_MASK = 0x3f;
67
+ // V4Router action: SWAP_EXACT_IN_SINGLE
68
+ uint8 private constant ACT_SWAP_EXACT_IN_SINGLE = 0x00;
69
+
70
+ // PoolKey layout must match the deployed V4 PoolManager on Unichain
71
+ struct PoolKey {
72
+ address currency0; // Currency — address type in V4
73
+ address currency1;
74
+ uint24 fee;
75
+ int24 tickSpacing;
76
+ address hooks; // IHooks — address(0) for unhookyed pools
77
+ }
78
+
79
+ struct ExactInputSingleParams {
80
+ PoolKey poolKey;
81
+ bool zeroForOne;
82
+ uint128 amountIn;
83
+ uint128 amountOutMinimum;
84
+ bytes hookData; // not inspected — see limitations header
85
+ }
86
+
87
+ constructor(
88
+ address universalRouter,
89
+ address fixedCurrencyIn,
90
+ address[] memory allowedCurrenciesOut,
91
+ uint256 maxAmountIn,
92
+ uint256 minBps
93
+ ) {
94
+ require(minBps <= 10_000, "minBps > 10000");
95
+ UNIVERSAL_ROUTER = universalRouter;
96
+ FIXED_CURRENCY_IN = fixedCurrencyIn;
97
+ MAX_AMOUNT_IN = maxAmountIn;
98
+ MIN_BPS = minBps;
99
+ for (uint256 i = 0; i < allowedCurrenciesOut.length; i++) {
100
+ isAllowedCurrencyOut[allowedCurrenciesOut[i]] = true;
101
+ }
102
+ }
103
+
104
+ function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool) {
105
+ if (ctx.target != UNIVERSAL_ROUTER) return false;
106
+ if (ctx.selector != SEL_EXECUTE_DEADLINE && ctx.selector != SEL_EXECUTE) return false;
107
+ if (txData.length < 4) return false;
108
+
109
+ // Decode the execute call. Both overloads start with (bytes commands, bytes[] inputs).
110
+ // abi.decode ignores trailing fields, so decoding as (bytes, bytes[]) works for both.
111
+ (bytes memory commands, bytes[] memory inputs) = abi.decode(txData[4:], (bytes, bytes[]));
112
+
113
+ // Enforce: exactly one command, and it must be V4_SWAP
114
+ if (commands.length != 1) return false;
115
+ if ((uint8(commands[0]) & CMD_MASK) != CMD_V4_SWAP) return false;
116
+ if (inputs.length != 1) return false;
117
+
118
+ // Decode the V4 router call (actions + per-action params)
119
+ (bytes memory v4Actions, bytes[] memory v4Params) =
120
+ abi.decode(inputs[0], (bytes, bytes[]));
121
+
122
+ // Enforce: exactly one V4 action, and it must be SWAP_EXACT_IN_SINGLE
123
+ if (v4Actions.length != 1) return false;
124
+ if (uint8(v4Actions[0]) != ACT_SWAP_EXACT_IN_SINGLE) return false;
125
+ if (v4Params.length != 1) return false;
126
+
127
+ // Decode ExactInputSingleParams from the action param.
128
+ // hookData is a dynamic bytes field — revert here means false (fail closed).
129
+ ExactInputSingleParams memory p = abi.decode(v4Params[0], (ExactInputSingleParams));
130
+
131
+ // Derive tokenIn and tokenOut from the PoolKey and zeroForOne flag.
132
+ // In V4, currency0 < currency1 (sorted by address). zeroForOne = true means
133
+ // trading currency0 for currency1.
134
+ address tokenIn = p.zeroForOne ? p.poolKey.currency0 : p.poolKey.currency1;
135
+ address tokenOut = p.zeroForOne ? p.poolKey.currency1 : p.poolKey.currency0;
136
+
137
+ if (tokenIn != FIXED_CURRENCY_IN) return false;
138
+ if (!isAllowedCurrencyOut[tokenOut]) return false;
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.
144
+ if (p.amountOutMinimum < (uint256(p.amountIn) * MIN_BPS) / 10_000) return false;
145
+
146
+ return true;
147
+ }
148
+
149
+ function discriminator() external pure returns (bytes32) { return DISCRIMINATOR; }
150
+ }