@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
@@ -1,93 +1,93 @@
1
- # Sail permission model: conjunctive vs selective
2
-
3
- The deployed SailKernel ships in **two incompatible dispatch models**. Which one a
4
- chain runs changes how dispatches are signed *and* how permissions must be written.
5
- Get this wrong and every dispatch reverts with an opaque selector. This is the single
6
- most important thing to understand before operating an SMA.
7
-
8
- ## TL;DR
9
-
10
- | | **Conjunctive** (older) | **Selective** (newer) |
11
- |---|---|---|
12
- | Chains today (bundled kernels) | None — all bundled kernels moved to selective | Base (8453), Base Sepolia (84532), Arbitrum (42161), Unichain (130) |
13
- | `dispatch(...)` | `(account, target, value, data, sig, deadline)` — **no `permission`** | `(account, permission, target, value, data, sig, deadline)` |
14
- | Which permissions are checked | **ALL** registered permissions; **all must return true** | only the **one** permission named in the call |
15
- | EIP-712 `Dispatch` struct | no `permission` field | includes `permission` |
16
- | Batch (`dispatchBatch`/`previewBatch`) | **not available** | available |
17
- | **Permission design rule** | **MUST pass through calls outside its domain** | may return false freely |
18
-
19
- Don't guess from a version string — **detect it on-chain** (see below).
20
-
21
- ## The conjunctive pass-through rule (the big footgun)
22
-
23
- On a conjunctive kernel the kernel calls `evaluate(txData, ctx)` on **every**
24
- registered permission and ANDs the results. A single `false` blocks the whole
25
- dispatch (`PermissionDenied`).
26
-
27
- So a permission that only cares about, say, approvals **must still return `true` for
28
- every call it doesn't govern**:
29
-
30
- ```solidity
31
- function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool) {
32
- // Pass through calls outside this permission's domain (conjunctive model).
33
- if (ctx.selector != APPROVE) return true; // <-- without this line, this
34
- // permission bricks swaps,
35
- // transfers, everything.
36
- // ...domain-specific checks for approve...
37
- }
38
- ```
39
-
40
- A permission that returns `false` (or reverts, or runs out of gas — both treated as
41
- `false`) on unrelated calls **bricks the entire account**: no dispatch of any kind
42
- can pass. We hit exactly this during bring-up with permissions that "blocked each
43
- other." The fix was to redeploy every permission with pass-through semantics.
44
-
45
- Corollary: on a conjunctive kernel you **cannot** have two permissions that each
46
- enforce a different token's approve — each would reject the other's token. To support
47
- approving both DAI and USDC you need **one** approve permission that allows both (see
48
- `templates/lifi-permissions/`), not two narrow ones.
49
-
50
- Selective kernels don't have this problem: each dispatch names one permission and
51
- only that one is consulted.
52
-
53
- ## Detect the model on-chain
54
-
55
- The SDK reads each kernel's public `DISPATCH_TYPEHASH` constant and matches it
56
- against the canonical hashes for each model. Never assume.
57
-
58
- ```ts
59
- const caps = await client.capabilities();
60
- // caps.dispatchModel: "conjunctive" | "selective"
61
- // caps.dispatchTypehash, caps.source ("onchain-typehash" | "static-hint")
62
- ```
63
-
64
- Verified typehashes:
65
-
66
- - conjunctive `DISPATCH_TYPEHASH` = `0x7510c80e…`
67
- `Dispatch(address account,address target,uint256 value,bytes32 dataHash,uint256 nonce,uint256 deadline)`
68
- - selective `DISPATCH_TYPEHASH` =
69
- `Dispatch(address account,address permission,address target,uint256 value,bytes32 dataHash,uint256 nonce,uint256 deadline)`
70
-
71
- `client.dispatch.single(...)` already signs the correct struct and uses the correct
72
- ABI for the detected model — you don't sign by hand. `client.dispatch.batch` /
73
- `preview` throw a clear error on conjunctive kernels (no `dispatchBatch`).
74
-
75
- ## Roles (unchanged across models)
76
-
77
- | Role | Authority |
78
- |------|-----------|
79
- | **Owner** | Holds the Safe; custody anchor. |
80
- | **Permission Signer** | Authorizes which `IPermission` contracts apply (EIP-712 `RegisterPermissions` / `RevokePermissions`). Signed in the browser signing station — the agent never holds this key. |
81
- | **Manager** | Executes dispatches within the registered permissions (ECDSA / ERC-1271). The agent's hot key. |
82
-
83
- ## Preflight before spending gas
84
-
85
- Run `sailor doctor` (read-only, no gas, no keys):
86
-
87
- - detects the dispatch model,
88
- - lists registered permissions,
89
- - on a conjunctive kernel, **probes each permission for pass-through** and flags any
90
- that would brick dispatch.
91
-
92
- See [AGENTS.md](../AGENTS.md) for the operational decision tree and the
93
- revert failure-mode catalog.
1
+ # Sail permission model: conjunctive vs selective
2
+
3
+ The deployed SailKernel ships in **two incompatible dispatch models**. Which one a
4
+ chain runs changes how dispatches are signed *and* how permissions must be written.
5
+ Get this wrong and every dispatch reverts with an opaque selector. This is the single
6
+ most important thing to understand before operating an SMA.
7
+
8
+ ## TL;DR
9
+
10
+ | | **Conjunctive** (older) | **Selective** (newer) |
11
+ |---|---|---|
12
+ | Chains today (bundled kernels) | None — all bundled kernels moved to selective | Base (8453), Base Sepolia (84532), Arbitrum (42161), Unichain (130) |
13
+ | `dispatch(...)` | `(account, target, value, data, sig, deadline)` — **no `permission`** | `(account, permission, target, value, data, sig, deadline)` |
14
+ | Which permissions are checked | **ALL** registered permissions; **all must return true** | only the **one** permission named in the call |
15
+ | EIP-712 `Dispatch` struct | no `permission` field | includes `permission` |
16
+ | Batch (`dispatchBatch`/`previewBatch`) | **not available** | available |
17
+ | **Permission design rule** | **MUST pass through calls outside its domain** | may return false freely |
18
+
19
+ Don't guess from a version string — **detect it on-chain** (see below).
20
+
21
+ ## The conjunctive pass-through rule (the big footgun)
22
+
23
+ On a conjunctive kernel the kernel calls `evaluate(txData, ctx)` on **every**
24
+ registered permission and ANDs the results. A single `false` blocks the whole
25
+ dispatch (`PermissionDenied`).
26
+
27
+ So a permission that only cares about, say, approvals **must still return `true` for
28
+ every call it doesn't govern**:
29
+
30
+ ```solidity
31
+ function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool) {
32
+ // Pass through calls outside this permission's domain (conjunctive model).
33
+ if (ctx.selector != APPROVE) return true; // <-- without this line, this
34
+ // permission bricks swaps,
35
+ // transfers, everything.
36
+ // ...domain-specific checks for approve...
37
+ }
38
+ ```
39
+
40
+ A permission that returns `false` (or reverts, or runs out of gas — both treated as
41
+ `false`) on unrelated calls **bricks the entire account**: no dispatch of any kind
42
+ can pass. We hit exactly this during bring-up with permissions that "blocked each
43
+ other." The fix was to redeploy every permission with pass-through semantics.
44
+
45
+ Corollary: on a conjunctive kernel you **cannot** have two permissions that each
46
+ enforce a different token's approve — each would reject the other's token. To support
47
+ approving both DAI and USDC you need **one** approve permission that allows both (see
48
+ `templates/lifi-permissions/`), not two narrow ones.
49
+
50
+ Selective kernels don't have this problem: each dispatch names one permission and
51
+ only that one is consulted.
52
+
53
+ ## Detect the model on-chain
54
+
55
+ The SDK reads each kernel's public `DISPATCH_TYPEHASH` constant and matches it
56
+ against the canonical hashes for each model. Never assume.
57
+
58
+ ```ts
59
+ const caps = await client.capabilities();
60
+ // caps.dispatchModel: "conjunctive" | "selective"
61
+ // caps.dispatchTypehash, caps.source ("onchain-typehash" | "static-hint")
62
+ ```
63
+
64
+ Verified typehashes:
65
+
66
+ - conjunctive `DISPATCH_TYPEHASH` = `0x7510c80e…`
67
+ `Dispatch(address account,address target,uint256 value,bytes32 dataHash,uint256 nonce,uint256 deadline)`
68
+ - selective `DISPATCH_TYPEHASH` =
69
+ `Dispatch(address account,address permission,address target,uint256 value,bytes32 dataHash,uint256 nonce,uint256 deadline)`
70
+
71
+ `client.dispatch.single(...)` already signs the correct struct and uses the correct
72
+ ABI for the detected model — you don't sign by hand. `client.dispatch.batch` /
73
+ `preview` throw a clear error on conjunctive kernels (no `dispatchBatch`).
74
+
75
+ ## Roles (unchanged across models)
76
+
77
+ | Role | Authority |
78
+ |------|-----------|
79
+ | **Owner** | Holds the Safe; custody anchor. |
80
+ | **Permission Signer** | Authorizes which `IPermission` contracts apply (EIP-712 `RegisterPermissions` / `RevokePermissions`). Signed in the browser signing station — the agent never holds this key. |
81
+ | **Manager** | Executes dispatches within the registered permissions (ECDSA / ERC-1271). The agent's hot key. |
82
+
83
+ ## Preflight before spending gas
84
+
85
+ Run `sailor doctor` (read-only, no gas, no keys):
86
+
87
+ - detects the dispatch model,
88
+ - lists registered permissions,
89
+ - on a conjunctive kernel, **probes each permission for pass-through** and flags any
90
+ that would brick dispatch.
91
+
92
+ See [AGENTS.md](../AGENTS.md) for the operational decision tree and the
93
+ revert failure-mode catalog.
@@ -0,0 +1,179 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.26;
3
+
4
+ // ─────────────────────────────────────────────────────────────────────────────
5
+ // Protocol : Sail batch dispatch (IBatchPermission) — protocol-agnostic
6
+ // Version : Single-tenant, constructor-configured (mirrors the multi-tenant
7
+ // SharedApproveAndCallBatchPermission shape from SailProtocol)
8
+ // Chain : Any EVM with a SELECTIVE kernel (Base, Arbitrum, Unichain, Base Sepolia)
9
+ //
10
+ // WHAT THIS TEACHES — the atomic approve → call → reset pattern.
11
+ // A bare ERC-20 approve is dangerous: it leaves a standing allowance an attacker can drain.
12
+ // The safe shape is to approve, consume the allowance in the SAME transaction, then reset it
13
+ // to zero — all-or-nothing. No single-call IPermission can enforce "the reset must be the
14
+ // final call", because per-call evaluation never sees the other calls. A batch permission
15
+ // sees the WHOLE sequence at once, so it can.
16
+ //
17
+ // Gated via the kernel's dispatchBatch (NOT dispatch). This contract's single-call evaluate()
18
+ // deliberately returns false — it is batch-only. The manager names this permission in the
19
+ // batch signature; only it is consulted (selective model).
20
+ //
21
+ // ENFORCES ON-CHAIN (kernel calls evaluateBatch() via staticcall; false/revert ⇒ batch blocked):
22
+ // The batch MUST be exactly these 3 calls, in this order, each with value == 0:
23
+ // calls[0] = approve(spender,amount) selector 0x095ea7b3 on an allowlisted token
24
+ // • token (calls[0].target) must be in ALLOWED_TOKENS (cap > 0)
25
+ // • spender must be in ALLOWED_SPENDERS
26
+ // • 0 < amount ≤ maxApprovalAmount[token]
27
+ // calls[1] = <consuming call> on an allowlisted (target, selector)
28
+ // • target must be in ALLOWED_CONSUMING_TARGETS
29
+ // • selector must be in ALLOWED_CONSUMING_SELECTORS
30
+ // • if REQUIRE_AMOUNT_MATCH: c1.data[4:36] (the first ABI-encoded argument of the
31
+ // consuming call) must equal the approve amount. Only enable this flag if the
32
+ // consuming function's FIRST parameter is the amount — if the amount appears in a
33
+ // later position the check reads the wrong slot and is silently bypassed.
34
+ // calls[2] = approve(spender,0) selector 0x095ea7b3 — mandatory reset
35
+ // • same token and same spender as calls[0]
36
+ // • amount must be exactly 0
37
+ // Any deviation (wrong length, wrong token/spender/target/selector, over-cap, non-zero reset,
38
+ // reordering, non-zero value, malformed calldata) ⇒ false or revert.
39
+ //
40
+ // ⚠ FUND DESTINATION NOT BOUNDED: unlike every single-call permission in this repo, this contract
41
+ // makes NO assertion that the consuming call routes funds to ctx.account. The recipient, receiver,
42
+ // or beneficiary inside calls[1] is unchecked. An agent bug that names an external address in the
43
+ // consuming call's arguments will not be blocked here. To close this gap, either:
44
+ // a) use a consuming target/selector that structurally routes output to msg.sender (the SMA), or
45
+ // b) pair this contract with a protocol-specific single-call permission that bounds the recipient.
46
+ //
47
+ // AGENT-ENFORCED / NOT BOUNDED HERE (off-chain — can change without redeploying this contract):
48
+ // • The full calldata of the consuming call beyond its leading uint256 (recipients, paths, etc.)
49
+ // — bound those with a protocol-specific permission if the consuming call needs tighter limits.
50
+ //
51
+ // VERIFY BEFORE USE:
52
+ // • approve selector 0x095ea7b3 is the ERC-20 standard. ALLOWED_CONSUMING_SELECTORS must match
53
+ // the real selector(s) of the protocol call you intend to bracket.
54
+ // • Per-token caps are in each token's base units (decimals differ — USDC 6, WETH 18).
55
+ // • Requires a SELECTIVE kernel (dispatchBatch exists). Conjunctive kernels have no batch path.
56
+ // • `sailor mandate simulate` probes single evaluate() only; it cannot probe evaluateBatch().
57
+ // Verify this contract by calling evaluateBatch(calls, ctx) directly (eth_call) with PASS/FAIL
58
+ // batches before authorizing on-chain.
59
+ // ─────────────────────────────────────────────────────────────────────────────
60
+
61
+ import {IPermission, Context} from "@sail/interfaces/IPermission.sol";
62
+ import {IBatchPermission, Call, BatchContext} from "@sail/interfaces/IBatchPermission.sol";
63
+
64
+ contract BoundedApproveAndCallBatch is IPermission, IBatchPermission {
65
+ bytes32 private constant DISCRIMINATOR = keccak256("BoundedApproveAndCallBatch");
66
+
67
+ bytes4 private constant SEL_APPROVE = 0x095ea7b3; // approve(address,uint256)
68
+ uint256 private constant APPROVE_CALLDATA_LEN = 68; // 4 + 32 (spender) + 32 (amount)
69
+ uint256 private constant CONSUMING_MIN_LEN = 36; // 4 + 32 (leading uint256 arg)
70
+
71
+ mapping(address => uint256) public maxApprovalAmount; // token => cap (0 = not allowed)
72
+ mapping(address => bool) public isSpender; // spender allowlist
73
+ mapping(address => bool) public isConsumingTarget; // consuming-call target allowlist
74
+ mapping(bytes4 => bool) public isConsumingSelector; // consuming-call selector allowlist
75
+ bool public immutable REQUIRE_AMOUNT_MATCH;
76
+
77
+ /// @param tokens Allowlisted ERC-20 tokens that may be approved
78
+ /// @param maxApprovalAmounts Per-token approve cap, index-parallel with `tokens` (each > 0)
79
+ /// @param spenders Allowlisted spenders that may receive the allowance
80
+ /// @param consumingTargets Allowlisted targets for the middle (consuming) call
81
+ /// @param consumingSelectors Allowlisted selectors for the middle (consuming) call
82
+ /// @param requireAmountMatch If true, the consuming call's leading uint256 must equal the approve amount
83
+ constructor(
84
+ address[] memory tokens,
85
+ uint256[] memory maxApprovalAmounts,
86
+ address[] memory spenders,
87
+ address[] memory consumingTargets,
88
+ bytes4[] memory consumingSelectors,
89
+ bool requireAmountMatch
90
+ ) {
91
+ require(tokens.length == maxApprovalAmounts.length, "tokens/amounts length mismatch");
92
+ require(tokens.length > 0 && spenders.length > 0, "empty token/spender allowlist");
93
+ require(consumingTargets.length > 0 && consumingSelectors.length > 0, "empty consuming allowlist");
94
+
95
+ for (uint256 i = 0; i < tokens.length; i++) {
96
+ require(tokens[i] != address(0) && maxApprovalAmounts[i] > 0, "bad token/cap");
97
+ maxApprovalAmount[tokens[i]] = maxApprovalAmounts[i];
98
+ }
99
+ for (uint256 i = 0; i < spenders.length; i++) {
100
+ require(spenders[i] != address(0), "zero spender");
101
+ isSpender[spenders[i]] = true;
102
+ }
103
+ for (uint256 i = 0; i < consumingTargets.length; i++) {
104
+ require(consumingTargets[i] != address(0), "zero target");
105
+ isConsumingTarget[consumingTargets[i]] = true;
106
+ }
107
+ for (uint256 i = 0; i < consumingSelectors.length; i++) {
108
+ require(consumingSelectors[i] != bytes4(0), "zero selector");
109
+ isConsumingSelector[consumingSelectors[i]] = true;
110
+ }
111
+ REQUIRE_AMOUNT_MATCH = requireAmountMatch;
112
+ }
113
+
114
+ // ── IBatchPermission ─────────────────────────────────────────────────────
115
+
116
+ /// @inheritdoc IBatchPermission
117
+ function isBatchPermission() external pure returns (bool) { return true; }
118
+
119
+ /// @inheritdoc IBatchPermission
120
+ function evaluateBatch(Call[] calldata calls, BatchContext calldata ctx) external view returns (bool) {
121
+ ctx; // batch context unused — bounds depend only on the call sequence, not the SMA
122
+ if (calls.length != 3) return false;
123
+
124
+ // ── calls[0]: approve(spender, amount) on an allowlisted token ───────
125
+ Call calldata c0 = calls[0];
126
+ if (c0.value != 0) return false;
127
+ if (c0.data.length != APPROVE_CALLDATA_LEN) return false;
128
+ if (bytes4(c0.data[0:4]) != SEL_APPROVE) return false;
129
+
130
+ address token = c0.target;
131
+ uint256 cap = maxApprovalAmount[token];
132
+ if (cap == 0) return false; // token not allowlisted
133
+
134
+ (address spender, uint256 approveAmount) = _decodeApprove(c0.data);
135
+ if (!isSpender[spender]) return false;
136
+ if (approveAmount == 0) return false;
137
+ if (approveAmount > cap) return false;
138
+
139
+ // ── calls[1]: consuming call on an allowlisted (target, selector) ────
140
+ Call calldata c1 = calls[1];
141
+ if (c1.value != 0) return false;
142
+ if (!isConsumingTarget[c1.target]) return false;
143
+ if (c1.data.length < CONSUMING_MIN_LEN) return false;
144
+ if (!isConsumingSelector[bytes4(c1.data[0:4])]) return false;
145
+ if (REQUIRE_AMOUNT_MATCH) {
146
+ if (uint256(bytes32(c1.data[4:36])) != approveAmount) return false;
147
+ }
148
+
149
+ // ── calls[2]: approve(spender, 0) — mandatory reset of same token+spender ─
150
+ Call calldata c2 = calls[2];
151
+ if (c2.value != 0) return false;
152
+ if (c2.target != token) return false;
153
+ if (c2.data.length != APPROVE_CALLDATA_LEN) return false;
154
+ if (bytes4(c2.data[0:4]) != SEL_APPROVE) return false;
155
+
156
+ (address resetSpender, uint256 resetAmount) = _decodeApprove(c2.data);
157
+ if (resetSpender != spender) return false;
158
+ if (resetAmount != 0) return false;
159
+
160
+ return true;
161
+ }
162
+
163
+ // ── IPermission (batch-only: single dispatch is never authorised) ────────
164
+
165
+ /// @inheritdoc IPermission
166
+ function evaluate(bytes calldata, Context calldata) external pure returns (bool) {
167
+ return false;
168
+ }
169
+
170
+ /// @inheritdoc IPermission
171
+ function discriminator() external pure returns (bytes32) { return DISCRIMINATOR; }
172
+
173
+ // ── internal calldata decoding (bounds-checked by callers above) ─────────
174
+
175
+ function _decodeApprove(bytes calldata data) internal pure returns (address spender, uint256 amount) {
176
+ spender = address(uint160(uint256(bytes32(data[4:36]))));
177
+ amount = uint256(bytes32(data[36:68]));
178
+ }
179
+ }
@@ -1,96 +1,97 @@
1
- // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
3
-
4
- // ─────────────────────────────────────────────────────────────────────────────
5
- // Protocol : Limitless
6
- // Version : CTF-based prediction market (on-chain settlement on Base)
7
- // NOT Polymarket (Polymarket's CLOB matches orders off-chain on Polygon —
8
- // a permission cannot bound the orders your agent signs off-chain.
9
- // Limitless settles bets on-chain on Base, so the kernel sees every action.)
10
- // Chain : Base mainnet
11
- //
12
- // ⚠ WARNING — ABI UNVERIFIED ⚠
13
- // The Limitless exchange contract address and bet-placement function signature
14
- // below are based on published CTF exchange patterns and public documentation.
15
- // They have NOT been independently verified against the deployed contracts.
16
- // YOU MUST verify these before deploying with real funds:
17
- // 1. Find the Limitless exchange contract address on Basescan.
18
- // 2. Read its verified ABI and confirm the buy/place function signature.
19
- // 3. Recompute the selector and update SEL_BUY.
20
- // 4. Confirm the parameter layout matches BetParams below.
21
- // Deploying this contract with an unverified ABI may silently PASS or FAIL
22
- // all dispatches depending on whether the selector matches.
23
- //
24
- // ENFORCED ON-CHAIN (assuming verified ABI see warning above):
25
- // buy(bytes32 conditionId, uint256 amount, uint256 outcomeIndex)
26
- // target must be LIMITLESS_EXCHANGE
27
- // conditionId must be in ALLOWED_CONDITIONS
28
- // amount MAX_STAKE
29
- // outcomeIndex must be in ALLOWED_OUTCOMES
30
- //
31
- // NOT ENFORCED:
32
- // Market price / odds (on-chain prediction market prices fluctuate)
33
- // • Timing / frequency of bets
34
- //
35
- // VERIFY BEFORE USE:
36
- // Confirm Limitless exchange address on Base (Basescan).
37
- // • Confirm buy function signature and compute selector:
38
- // keccak256("buy(bytes32,uint256,uint256)")[0:4] == 0x??? verify on-chain.
39
- // Confirm conditionId encoding matches the deployed market IDs.
40
- // • Update this contract if the ABI or parameter order differs.
41
- // ─────────────────────────────────────────────────────────────────────────────
42
-
43
- import {IPermission, Context} from "@sail/interfaces/IPermission.sol";
44
-
45
- contract BoundedBet_Limitless_Base is IPermission {
46
- bytes32 private constant DISCRIMINATOR = keccak256("BoundedBet_Limitless_Base");
47
-
48
- /// @dev ⚠ UNVERIFIED — replace with verified Limitless exchange address on Base.
49
- address public immutable LIMITLESS_EXCHANGE;
50
- mapping(bytes32 => bool) public isAllowedCondition;
51
- uint256 public immutable MAX_STAKE;
52
- mapping(uint256 => bool) public isAllowedOutcome;
53
-
54
- /// @dev ⚠ UNVERIFIED selector. Compute keccak256("buy(bytes32,uint256,uint256)")[0:4]
55
- /// and confirm it matches the deployed Limitless exchange contract before use.
56
- bytes4 private constant SEL_BUY = bytes4(keccak256("buy(bytes32,uint256,uint256)"));
57
-
58
- /// @param limitlessExchange ⚠ VERIFY — Limitless CTF exchange address on Base
59
- /// @param allowedConditions conditionIds (bytes32) of markets the agent may bet on
60
- /// @param maxStake Per-bet stake cap in collateral base units
61
- /// @param allowedOutcomes Outcome indices the agent may select (e.g. [0] for YES only)
62
- constructor(
63
- address limitlessExchange,
64
- bytes32[] memory allowedConditions,
65
- uint256 maxStake,
66
- uint256[] memory allowedOutcomes
67
- ) {
68
- LIMITLESS_EXCHANGE = limitlessExchange;
69
- MAX_STAKE = maxStake;
70
- for (uint256 i = 0; i < allowedConditions.length; i++) {
71
- isAllowedCondition[allowedConditions[i]] = true;
72
- }
73
- for (uint256 i = 0; i < allowedOutcomes.length; i++) {
74
- isAllowedOutcome[allowedOutcomes[i]] = true;
75
- }
76
- }
77
-
78
- function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool) {
79
- if (ctx.target != LIMITLESS_EXCHANGE) return false;
80
- if (ctx.selector != SEL_BUY) return false;
81
- if (txData.length < 4 + 3 * 32) return false;
82
-
83
- // ⚠ Assumes: buy(bytes32 conditionId, uint256 amount, uint256 outcomeIndex)
84
- // Verify parameter order against deployed contract ABI before use.
85
- (bytes32 conditionId, uint256 amount, uint256 outcomeIndex) =
86
- abi.decode(txData[4:], (bytes32, uint256, uint256));
87
-
88
- if (!isAllowedCondition[conditionId]) return false;
89
- if (amount > MAX_STAKE) return false;
90
- if (!isAllowedOutcome[outcomeIndex]) return false;
91
-
92
- return true;
93
- }
94
-
95
- function discriminator() external pure returns (bytes32) { return DISCRIMINATOR; }
96
- }
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.26;
3
+
4
+ // ─────────────────────────────────────────────────────────────────────────────
5
+ // Protocol : Limitless
6
+ // Version : CTF-based prediction market (on-chain settlement on Base)
7
+ // NOT Polymarket (Polymarket's CLOB matches orders off-chain on Polygon —
8
+ // a permission cannot bound the orders your agent signs off-chain.
9
+ // Limitless settles bets on-chain on Base, so the kernel sees every action.)
10
+ // Chain : Base mainnet
11
+ //
12
+ // ⚠ WARNING — ABI UNVERIFIED ⚠
13
+ // The Limitless exchange contract address and bet-placement function signature
14
+ // below are based on published CTF exchange patterns and public documentation.
15
+ // They have NOT been independently verified against the deployed contracts.
16
+ // YOU MUST verify these before deploying with real funds:
17
+ // 1. Find the Limitless exchange contract address on Basescan.
18
+ // 2. Read its verified ABI and confirm the buy/place function signature.
19
+ // 3. Recompute the selector and update SEL_BUY.
20
+ // 4. Confirm the parameter layout matches BetParams below.
21
+ // Deploying this contract with an unverified ABI may silently PASS or FAIL
22
+ // all dispatches depending on whether the selector matches.
23
+ //
24
+ // ENFORCES ON-CHAIN (kernel calls evaluate() on every dispatch; false ⇒ dispatch blocked)
25
+ // ASSUMING the unverified ABI above is correct:
26
+ // buy(bytes32 conditionId,uint256 amount,uint256 outcomeIndex) selector = keccak256(sig)[0:4]
27
+ // target must be LIMITLESS_EXCHANGE
28
+ // conditionId must be in ALLOWED_CONDITIONS
29
+ // amount MAX_STAKE
30
+ // • outcomeIndex must be in ALLOWED_OUTCOMES
31
+ //
32
+ // AGENT-ENFORCED / NOT BOUNDED HERE (off-chain can change without redeploying this contract):
33
+ // • Market price / odds (on-chain prediction market prices fluctuate)
34
+ // • Timing / frequency of bets
35
+ //
36
+ // VERIFY BEFORE USE:
37
+ // • Confirm Limitless exchange address on Base (Basescan).
38
+ // • Confirm buy function signature and compute selector:
39
+ // keccak256("buy(bytes32,uint256,uint256)")[0:4] == 0x??? verify on-chain.
40
+ // • Confirm conditionId encoding matches the deployed market IDs.
41
+ // Update this contract if the ABI or parameter order differs.
42
+ // ─────────────────────────────────────────────────────────────────────────────
43
+
44
+ import {IPermission, Context} from "@sail/interfaces/IPermission.sol";
45
+
46
+ contract BoundedBet_Limitless_Base is IPermission {
47
+ bytes32 private constant DISCRIMINATOR = keccak256("BoundedBet_Limitless_Base");
48
+
49
+ /// @dev ⚠ UNVERIFIED — replace with verified Limitless exchange address on Base.
50
+ address public immutable LIMITLESS_EXCHANGE;
51
+ mapping(bytes32 => bool) public isAllowedCondition;
52
+ uint256 public immutable MAX_STAKE;
53
+ mapping(uint256 => bool) public isAllowedOutcome;
54
+
55
+ /// @dev UNVERIFIED selector. Compute keccak256("buy(bytes32,uint256,uint256)")[0:4]
56
+ /// and confirm it matches the deployed Limitless exchange contract before use.
57
+ bytes4 private constant SEL_BUY = bytes4(keccak256("buy(bytes32,uint256,uint256)"));
58
+
59
+ /// @param limitlessExchange VERIFY Limitless CTF exchange address on Base
60
+ /// @param allowedConditions conditionIds (bytes32) of markets the agent may bet on
61
+ /// @param maxStake Per-bet stake cap in collateral base units
62
+ /// @param allowedOutcomes Outcome indices the agent may select (e.g. [0] for YES only)
63
+ constructor(
64
+ address limitlessExchange,
65
+ bytes32[] memory allowedConditions,
66
+ uint256 maxStake,
67
+ uint256[] memory allowedOutcomes
68
+ ) {
69
+ LIMITLESS_EXCHANGE = limitlessExchange;
70
+ MAX_STAKE = maxStake;
71
+ for (uint256 i = 0; i < allowedConditions.length; i++) {
72
+ isAllowedCondition[allowedConditions[i]] = true;
73
+ }
74
+ for (uint256 i = 0; i < allowedOutcomes.length; i++) {
75
+ isAllowedOutcome[allowedOutcomes[i]] = true;
76
+ }
77
+ }
78
+
79
+ function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool) {
80
+ if (ctx.target != LIMITLESS_EXCHANGE) return false;
81
+ if (ctx.selector != SEL_BUY) return false;
82
+ if (txData.length < 4 + 3 * 32) return false;
83
+
84
+ // Assumes: buy(bytes32 conditionId, uint256 amount, uint256 outcomeIndex)
85
+ // Verify parameter order against deployed contract ABI before use.
86
+ (bytes32 conditionId, uint256 amount, uint256 outcomeIndex) =
87
+ abi.decode(txData[4:], (bytes32, uint256, uint256));
88
+
89
+ if (!isAllowedCondition[conditionId]) return false;
90
+ if (amount > MAX_STAKE) return false;
91
+ if (!isAllowedOutcome[outcomeIndex]) return false;
92
+
93
+ return true;
94
+ }
95
+
96
+ function discriminator() external pure returns (bytes32) { return DISCRIMINATOR; }
97
+ }