@dev.sail.money/sailor 1.3.0-224 → 1.3.0-225
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.
- package/package.json +1 -1
- package/packages/cli/dist/index.cjs +694 -133
- package/packages/cli/dist/server.cjs +15884 -17950
- package/packages/sdk/dist/abis/ConfigurablePermission.d.ts +130 -0
- package/packages/sdk/dist/abis/ConfigurablePermission.d.ts.map +1 -0
- package/packages/sdk/dist/abis/ConfigurablePermission.js +79 -0
- package/packages/sdk/dist/abis/ConfigurablePermission.js.map +1 -0
- package/packages/sdk/dist/abis/index.d.ts +1 -0
- package/packages/sdk/dist/abis/index.d.ts.map +1 -1
- package/packages/sdk/dist/abis/index.js +1 -0
- package/packages/sdk/dist/abis/index.js.map +1 -1
- package/packages/sdk/dist/index.d.ts +1 -0
- package/packages/sdk/dist/index.d.ts.map +1 -1
- package/packages/sdk/dist/index.js +1 -0
- package/packages/sdk/dist/index.js.map +1 -1
- package/packages/sdk/dist/intelligence.d.ts +1 -1
- package/packages/sdk/dist/intelligence.js +1 -1
- package/packages/ui/dist/assets/{add-HBssdE_q.js → add-QIu9SGBU.js} +1 -1
- package/packages/ui/dist/assets/{all-wallets-kgnKzcam.js → all-wallets-2auxp9E5.js} +1 -1
- package/packages/ui/dist/assets/{app-store-C7gfCF4e.js → app-store-BVtVvk-F.js} +1 -1
- package/packages/ui/dist/assets/{apple-DmsD7XsU.js → apple-Dq7Ppvqv.js} +1 -1
- package/packages/ui/dist/assets/{arrow-bottom-B3CywkhY.js → arrow-bottom-DpT3R4Rr.js} +1 -1
- package/packages/ui/dist/assets/{arrow-bottom-circle-Cl21xaXp.js → arrow-bottom-circle-hkf9bWhW.js} +1 -1
- package/packages/ui/dist/assets/{arrow-left-Bp_pqRAk.js → arrow-left-3PhiE4SD.js} +1 -1
- package/packages/ui/dist/assets/{arrow-right-CRK93G5I.js → arrow-right-DUIXJHib.js} +1 -1
- package/packages/ui/dist/assets/{arrow-top-DyDJtPkJ.js → arrow-top-Cf--02la.js} +1 -1
- package/packages/ui/dist/assets/{bank-BthuR5Ky.js → bank-CkRSSgJy.js} +1 -1
- package/packages/ui/dist/assets/{basic-BSdElBI1.js → basic-D9puI79I.js} +1 -1
- package/packages/ui/dist/assets/{browser-BMfpXAJE.js → browser-BC8qo0Bh.js} +1 -1
- package/packages/ui/dist/assets/{card-BEvVqDzc.js → card-BYuGTxNc.js} +1 -1
- package/packages/ui/dist/assets/{ccip-BALQiE-q.js → ccip-BS_rjbk9.js} +1 -1
- package/packages/ui/dist/assets/{checkmark-BqYZHVCR.js → checkmark-C-MZeapS.js} +1 -1
- package/packages/ui/dist/assets/{checkmark-bold-DjNGbayE.js → checkmark-bold-Bs3hadsW.js} +1 -1
- package/packages/ui/dist/assets/{chevron-bottom-BEHbtNKD.js → chevron-bottom-C8mqtkD4.js} +1 -1
- package/packages/ui/dist/assets/{chevron-left-C__dvO-Z.js → chevron-left-Dxj-vVDJ.js} +1 -1
- package/packages/ui/dist/assets/{chevron-right-9abfT5a8.js → chevron-right-BoIrOTtL.js} +1 -1
- package/packages/ui/dist/assets/{chevron-top-B2VeyvGH.js → chevron-top-BlgziB_P.js} +1 -1
- package/packages/ui/dist/assets/{chrome-store-bPC0PXv4.js → chrome-store-sDKm_6es.js} +1 -1
- package/packages/ui/dist/assets/{clock-BolGF8wp.js → clock-BwfFymxk.js} +1 -1
- package/packages/ui/dist/assets/{close-Bo18lqFQ.js → close-BewD5_E0.js} +1 -1
- package/packages/ui/dist/assets/{coinPlaceholder-Bz-0VLbz.js → coinPlaceholder-DRPvf0NA.js} +1 -1
- package/packages/ui/dist/assets/{compass-CKKiFCLR.js → compass-BHPvd2dw.js} +1 -1
- package/packages/ui/dist/assets/{copy-CYIav_IY.js → copy-BMv4Y3KF.js} +1 -1
- package/packages/ui/dist/assets/{core-8m_0OoS0.js → core-BMNH6XFj.js} +3 -3
- package/packages/ui/dist/assets/cursor-ir0cjMUl.js +3 -0
- package/packages/ui/dist/assets/{cursor-transparent-DE9tExWo.js → cursor-transparent-DT8YBqHU.js} +1 -1
- package/packages/ui/dist/assets/{desktop-CWNluv_t.js → desktop-BeePUUqi.js} +1 -1
- package/packages/ui/dist/assets/{disconnect-Db6qWs6T.js → disconnect-C2qT29qY.js} +1 -1
- package/packages/ui/dist/assets/{discord-Cj6mVoBK.js → discord-I4ToELfU.js} +1 -1
- package/packages/ui/dist/assets/{etherscan-CCFLsPvo.js → etherscan-3psXC5Ef.js} +1 -1
- package/packages/ui/dist/assets/{events-BGQ3xBjc.js → events-Cdt6abbf.js} +1 -1
- package/packages/ui/dist/assets/{exclamation-triangle-DJWx46rU.js → exclamation-triangle-BCaWksGJ.js} +1 -1
- package/packages/ui/dist/assets/{extension-BtaF8Z9u.js → extension-DfYavvbY.js} +1 -1
- package/packages/ui/dist/assets/{external-link-5wYZl3Lh.js → external-link-B8uBskz7.js} +1 -1
- package/packages/ui/dist/assets/{facebook-BoGz1nMk.js → facebook-BSvrXy3w.js} +1 -1
- package/packages/ui/dist/assets/{fallback-B2d-ODZG.js → fallback-BSMYROva.js} +1 -1
- package/packages/ui/dist/assets/{farcaster-AsPTw3iB.js → farcaster-CcAuzEDi.js} +1 -1
- package/packages/ui/dist/assets/{filters-D1-1MQtc.js → filters-umCx-UFU.js} +1 -1
- package/packages/ui/dist/assets/{github-B1FrwkiF.js → github-CBNoBl67.js} +1 -1
- package/packages/ui/dist/assets/{google-DnrnTDFz.js → google-yyR1jNOC.js} +1 -1
- package/packages/ui/dist/assets/{help-circle-CI10dCJN.js → help-circle-C0WjKnIM.js} +1 -1
- package/packages/ui/dist/assets/{id-B627Euj0.js → id-BwIK6e_N.js} +1 -1
- package/packages/ui/dist/assets/{image-3O2KHUgY.js → image-D5oAo1Te.js} +1 -1
- package/packages/ui/dist/assets/{index-CgsvP_iE.js → index-B_Gc8xPp.js} +1 -1
- package/packages/ui/dist/assets/{index-DDe72Fsi.js → index-BaEe036r.js} +1 -1
- package/packages/ui/dist/assets/{index-DKCg39xU.js → index-BuqyrXYX.js} +1 -1
- package/packages/ui/dist/assets/index-CPrhuDfP.js +1782 -0
- package/packages/ui/dist/assets/{index-DhVzwxqf.js → index-DCWU1RuS.js} +1 -1
- package/packages/ui/dist/assets/{index-CeZ-3oZK.js → index-DMNSiu42.js} +3 -3
- package/packages/ui/dist/assets/{index.es-ClarUTak.js → index.es-DImsg59a.js} +4 -4
- package/packages/ui/dist/assets/{info-CMmDZSrf.js → info-CtcvKk8x.js} +1 -1
- package/packages/ui/dist/assets/{info-circle-ChsNDy1e.js → info-circle-BIm6XYyt.js} +1 -1
- package/packages/ui/dist/assets/{lightbulb-DDlT2eHQ.js → lightbulb-CMj21RIi.js} +1 -1
- package/packages/ui/dist/assets/{mail-DyTAk3JR.js → mail-C805tcaU.js} +1 -1
- package/packages/ui/dist/assets/{metamask-sdk-CoU-3z33.js → metamask-sdk-DDk4vkrl.js} +1 -1
- package/packages/ui/dist/assets/{mobile-B1ZTQh8S.js → mobile-BEii3NyZ.js} +1 -1
- package/packages/ui/dist/assets/{more-BNIrTkTn.js → more-RSnRt81X.js} +1 -1
- package/packages/ui/dist/assets/{network-placeholder-BRBk5JfG.js → network-placeholder-s9JaqzCQ.js} +1 -1
- package/packages/ui/dist/assets/{nftPlaceholder-xtnGz2yb.js → nftPlaceholder-Ccur2yVc.js} +1 -1
- package/packages/ui/dist/assets/{off-BfgRQBAE.js → off-DUtG4rlI.js} +1 -1
- package/packages/ui/dist/assets/{parseSignature-DPmCI80m.js → parseSignature-BbS8pxpB.js} +1 -1
- package/packages/ui/dist/assets/{play-store-BFjPDZvv.js → play-store-Bt2HvO3e.js} +1 -1
- package/packages/ui/dist/assets/{plus-C3dNKGgI.js → plus-BtEzQu0h.js} +1 -1
- package/packages/ui/dist/assets/{qr-code-BovNFnu8.js → qr-code-xbQZ-Nio.js} +1 -1
- package/packages/ui/dist/assets/{recycle-horizontal-DdcYEE6i.js → recycle-horizontal-d9pzE6vq.js} +1 -1
- package/packages/ui/dist/assets/{refresh-P6W_osB6.js → refresh-B3-2Sch0.js} +1 -1
- package/packages/ui/dist/assets/{reown-logo-DbesEx7U.js → reown-logo-DBkmQAX8.js} +1 -1
- package/packages/ui/dist/assets/{search-DMB73W4z.js → search-dgDA8Ou5.js} +1 -1
- package/packages/ui/dist/assets/{secp256k1-URVniZ2R.js → secp256k1-C4g4FCze.js} +1 -1
- package/packages/ui/dist/assets/{send-BUsSvitv.js → send-CEDXr4l7.js} +1 -1
- package/packages/ui/dist/assets/{swapHorizontal-CjaXc-eY.js → swapHorizontal-Co6_f8oH.js} +1 -1
- package/packages/ui/dist/assets/{swapHorizontalBold-DuNmGzT_.js → swapHorizontalBold-k4d22FNy.js} +1 -1
- package/packages/ui/dist/assets/{swapHorizontalMedium-CgZZIpJT.js → swapHorizontalMedium-Des6w_mz.js} +1 -1
- package/packages/ui/dist/assets/{swapHorizontalRoundedBold-BC1bVmtH.js → swapHorizontalRoundedBold-CVd4538j.js} +1 -1
- package/packages/ui/dist/assets/{swapVertical-PyYv49m9.js → swapVertical-Cn_eRfv8.js} +1 -1
- package/packages/ui/dist/assets/{telegram-DaCrUlL0.js → telegram-BE46RC0s.js} +1 -1
- package/packages/ui/dist/assets/{three-dots-D9tEmhOK.js → three-dots-CvqWsUWa.js} +1 -1
- package/packages/ui/dist/assets/{twitch-2m8_XY_5.js → twitch-9V7YXB0X.js} +1 -1
- package/packages/ui/dist/assets/{twitterIcon-BQ4fWtR1.js → twitterIcon-DbUxk6yk.js} +1 -1
- package/packages/ui/dist/assets/{verify-DJtFLNUq.js → verify-CQSnwVrh.js} +1 -1
- package/packages/ui/dist/assets/{verify-filled-B2cSEFoP.js → verify-filled-DG_hDTr8.js} +1 -1
- package/packages/ui/dist/assets/{w3m-modal-YBSDQDS1.js → w3m-modal-BsHGIrg7.js} +1 -1
- package/packages/ui/dist/assets/{wallet-C7Gv760P.js → wallet-lbVIReb_.js} +1 -1
- package/packages/ui/dist/assets/{wallet-placeholder-DtZ8_f4p.js → wallet-placeholder-C_ndmKAs.js} +1 -1
- package/packages/ui/dist/assets/{walletconnect-DsIIuQfT.js → walletconnect-ByDpG-E0.js} +1 -1
- package/packages/ui/dist/assets/{warning-circle-CTMOrGt2.js → warning-circle-hrS3II0d.js} +1 -1
- package/packages/ui/dist/assets/{x-BmL9CVvs.js → x-BPMn3x5J.js} +1 -1
- package/packages/ui/dist/index.html +1 -1
- package/templates/default/.agents/skills/sail-mandates/references/approvals.md +3 -3
- package/templates/default/.agents/skills/sail-swap-quote/SKILL.md +76 -0
- package/templates/default/.agents/skills/sail-template-approve-batch/SKILL.md +98 -0
- package/templates/default/.agents/skills/sail-template-borrow/SKILL.md +68 -0
- package/templates/default/.agents/skills/sail-template-deposit/SKILL.md +61 -0
- package/templates/default/.agents/skills/sail-template-swap/SKILL.md +207 -0
- package/templates/default/.agents/skills/sail-template-swap-no-oracle/SKILL.md +100 -0
- package/templates/default/.agents/skills/sail-template-transfer/SKILL.md +52 -0
- package/templates/default/.agents/skills/sail-template-withdraw/SKILL.md +50 -0
- package/templates/default/.agents/skills/sail-templates/SKILL.md +142 -0
- package/templates/default/.agents/skills/sail-templates/catalog.mjs +200 -0
- package/templates/default/.agents/skills/sail-templates/deployed.json +133 -0
- package/templates/default/.agents/skills/sail-templates/oracles/IOracle.sol +13 -0
- package/templates/default/.agents/skills/sail-templates/oracles/UniV3TwapOracle.sol +114 -0
- package/templates/default/.agents/skills/sail-templates/oracles/libs/FullMath.sol +62 -0
- package/templates/default/.agents/skills/sail-templates/oracles/libs/TickMath.sol +44 -0
- package/templates/default/.agents/skills/sail-templates/references/config-schemas.md +139 -0
- package/templates/default/.agents/skills/sail-templates/references/reuse-flow.md +139 -0
- package/templates/default/.agents/skills/sail-token-resolve/SKILL.md +174 -0
- package/templates/default/AGENTS.md +21 -2
- package/templates/default/scripts/quote-swap.mjs +211 -0
- package/templates/default/scripts/resolve-token.mjs +992 -0
- package/templates/default/scripts/shared-template-addr.mjs +110 -0
- package/packages/ui/dist/assets/cursor-D10zW2HG.js +0 -3
- package/packages/ui/dist/assets/index-ohx1EwWJ.js +0 -1789
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{F as l}from"./core-
|
|
1
|
+
import{F as l}from"./core-BMNH6XFj.js";import"./index-CPrhuDfP.js";import"./events-Cdt6abbf.js";import"./index.es-DImsg59a.js";import"./fallback-BSMYROva.js";const o=l`<svg fill="none" viewBox="0 0 96 67">
|
|
2
2
|
<path
|
|
3
3
|
fill="currentColor"
|
|
4
4
|
d="M25.32 18.8a32.56 32.56 0 0 1 45.36 0l1.5 1.47c.63.62.63 1.61 0 2.22l-5.15 5.05c-.31.3-.82.3-1.14 0l-2.07-2.03a22.71 22.71 0 0 0-31.64 0l-2.22 2.18c-.31.3-.82.3-1.14 0l-5.15-5.05a1.55 1.55 0 0 1 0-2.22l1.65-1.62Zm56.02 10.44 4.59 4.5c.63.6.63 1.6 0 2.21l-20.7 20.26c-.62.61-1.63.61-2.26 0L48.28 41.83a.4.4 0 0 0-.56 0L33.03 56.21c-.63.61-1.64.61-2.27 0L10.07 35.95a1.55 1.55 0 0 1 0-2.22l4.59-4.5a1.63 1.63 0 0 1 2.27 0L31.6 43.63a.4.4 0 0 0 .57 0l14.69-14.38a1.63 1.63 0 0 1 2.26 0l14.69 14.38a.4.4 0 0 0 .57 0l14.68-14.38a1.63 1.63 0 0 1 2.27 0Z"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{F as r}from"./core-
|
|
1
|
+
import{F as r}from"./core-BMNH6XFj.js";import"./index-CPrhuDfP.js";import"./events-Cdt6abbf.js";import"./index.es-DImsg59a.js";import"./fallback-BSMYROva.js";const a=r`<svg fill="none" viewBox="0 0 20 20">
|
|
2
2
|
<path
|
|
3
3
|
fill="currentColor"
|
|
4
4
|
d="M11 6.67a1 1 0 1 0-2 0v2.66a1 1 0 0 0 2 0V6.67ZM10 14.5a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Z"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{F as l}from"./core-
|
|
1
|
+
import{F as l}from"./core-BMNH6XFj.js";import"./index-CPrhuDfP.js";import"./events-Cdt6abbf.js";import"./index.es-DImsg59a.js";import"./fallback-BSMYROva.js";const a=l`<svg fill="none" viewBox="0 0 41 40">
|
|
2
2
|
<g clip-path="url(#a)">
|
|
3
3
|
<path fill="#000" d="M.8 0h40v40H.8z" />
|
|
4
4
|
<path
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<meta name="theme-color" content="#040b16" />
|
|
7
7
|
<title>Sail - Unlocking Personalized Money</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-CPrhuDfP.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-CKoOBsKn.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
@@ -25,12 +25,12 @@ This needs no batch support and works on every kernel.
|
|
|
25
25
|
|
|
26
26
|
Approve + action run as one `dispatchBatch` — atomic, single transaction. A batch dispatch consults **exactly one batch-aware `IBatchPermission`**, never a pair of `IPermission`s:
|
|
27
27
|
|
|
28
|
-
- The permission implements `@sail/interfaces/IBatchPermission.sol` — `evaluateBatch(Call[] calls, BatchContext ctx)` validates the WHOLE sequence (ordering, the approve's spender/amount, the action, and mandatory allowance cleanup) and `isBatchPermission()` returns true. `examples/permissions/BoundedApproveAndCallBatch.sol`
|
|
28
|
+
- The permission implements `@sail/interfaces/IBatchPermission.sol` — `evaluateBatch(Call[] calls, BatchContext ctx)` validates the WHOLE sequence (ordering, the approve's spender/amount, the action, and mandatory allowance cleanup) and `isBatchPermission()` returns true. **For the canonical `approve → swap/deposit/supply → reset` shape, a shared `ApproveAndCallBatchPermission` singleton is deployed** on Base, Arbitrum, Unichain, Sepolia, and Base Sepolia (resolve it with `node scripts/shared-template-addr.mjs ApproveAndCallBatchPermission`) — reuse it via `attach` + `configure` rather than authoring one. `examples/permissions/BoundedApproveAndCallBatch.sol` remains the model for a non-standard shape you must author yourself.
|
|
29
29
|
- A normal `IPermission` placed in a batch is rejected by the kernel with `PermissionNotBatchAware`. You cannot assemble a batch from two narrow per-call permissions — that was the trap.
|
|
30
30
|
- Batch is a **selective-kernel** feature (`dispatchBatch` / `previewBatch`); conjunctive kernels have neither. Confirm the model with `sailor doctor`; details in `docs/PERMISSION_MODEL.md`.
|
|
31
|
-
- At runtime, return one `Dispatch` whose `calls` array is `[
|
|
31
|
+
- At runtime, return one `Dispatch` whose `calls` array is the batch. The shared `ApproveAndCallBatchPermission` requires **exactly** `[approve(spender, amount), action, approve(spender, 0)]` — the trailing reset to zero is mandatory, and the pre-batch allowance on that `(token, spender)` pair must already be zero (so never combine it with a standing approve). The runner detects `calls.length > 1` and routes through `dispatch.batch`.
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
**For autonomous recurring actions (a DCA or rebalancer the agent runs on its own), Model B is the default — not an advanced option.** Model A's separate approve dispatch needs the owner back in the loop every time the bounded allowance runs out, which a recurring agent cannot do; on the first tick after the allowance is consumed it stalls (or, in a simulation, is tempted to cheat the allowance in). Reach for Model A when the owner is genuinely in the loop per action anyway. For bounded swaps specifically, [`sail-template-swap`'s "Approve coverage"](../../sail-template-swap/SKILL.md) works through the choice with concrete addresses.
|
|
34
34
|
|
|
35
35
|
## Verifying each model
|
|
36
36
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sail-swap-quote
|
|
3
|
+
description: Fetch a live Uniswap V3 quote via QuoterV2 and compute a slippage-adjusted amountOutMinimum for a swap. Use after sail-token-resolve to show the user the current price and to produce the amountOutMinimum floor the agent must embed in every swap dispatch. Runs the bundled `scripts/quote-swap.mjs` (no dependencies, no gas).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# sail-swap-quote — live price + amountOutMinimum floor
|
|
7
|
+
|
|
8
|
+
After [`sail-token-resolve`](../sail-token-resolve/SKILL.md) confirms a token is
|
|
9
|
+
swap-ready, get the actual number: how much tokenOut the user's amountIn buys
|
|
10
|
+
right now, and the `amountOutMinimum` floor that protects against slippage.
|
|
11
|
+
|
|
12
|
+
## When to load
|
|
13
|
+
|
|
14
|
+
- After `sail-token-resolve` returned `swapReady: true`.
|
|
15
|
+
- Before presenting a mandate to the user (show them the current price).
|
|
16
|
+
- To validate `amountOutMinimum` before the agent dispatches a real swap.
|
|
17
|
+
- Whenever the user asks "what's the price of X?" or "how much WETH for 25 USDC?".
|
|
18
|
+
|
|
19
|
+
## Run it
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# From the project root. Inputs come from sail-token-resolve's output.
|
|
23
|
+
node scripts/quote-swap.mjs \
|
|
24
|
+
--token-in 0x078D782b760474a361dDA0AF3839290b0EF57AD6 --decimals-in 6 \
|
|
25
|
+
--token-out 0x4200000000000000000000000000000000000006 --decimals-out 18 \
|
|
26
|
+
--amount 25000000 --fee 3000 --slippage-bps 100
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- `--amount` is in **base units** of tokenIn (25 USDC = `25000000`, not `25`).
|
|
30
|
+
- `--fee` is the tier from `sail-token-resolve` (deepest pool).
|
|
31
|
+
- `--slippage-bps` defaults to `100` (1%). Tighten (e.g. `50`) for stable pairs,
|
|
32
|
+
loosen for volatile ones. `0` removes the floor — testing only.
|
|
33
|
+
- `--chain` / `--rpc` override the project's active chain/RPC.
|
|
34
|
+
|
|
35
|
+
Output: one JSON object on stdout; human notes on stderr.
|
|
36
|
+
|
|
37
|
+
## What it returns
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"amountIn": "25000000", "amountOut": "15805313193992907",
|
|
42
|
+
"amountOutMinimum": "15647260062052977", // amountOut × (10000-slip)/10000
|
|
43
|
+
"slippageBps": 100, "fee": 3000,
|
|
44
|
+
"human": { "amountIn": "25.0", "amountOut": "0.015805313193992907",
|
|
45
|
+
"amountOutMinimum": "0.015647260062052977",
|
|
46
|
+
"price": "0.015805313193992907 out per 25.0 in" }
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## How the floor is computed
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
amountOutMinimum = amountOut × (10000 − slippageBps) / 10000
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
At 1% slippage, `amountOut × 9900 / 10000`. The agent must embed a value **at
|
|
57
|
+
least this high** in its swap calldata's `amountOutMinimum` field — the
|
|
58
|
+
`SwapPermission` mandate rejects any swap whose `amountOutMinimum` is below the
|
|
59
|
+
pool-implied floor (fail-closed). Re-quote close to dispatch time; a stale quote
|
|
60
|
+
is the most common reason a legitimate swap gets rejected.
|
|
61
|
+
|
|
62
|
+
## What to do with it
|
|
63
|
+
|
|
64
|
+
1. **Show the user** the `human.price` line before they approve the mandate.
|
|
65
|
+
2. **Record `amountOutMinimum`** as the floor the agent's dispatch logic must
|
|
66
|
+
respect. Hand the full quote to [`sail-template-swap`](../sail-template-swap/SKILL.md)
|
|
67
|
+
— the mandate's `maxSlippageBps` bound is the on-chain enforcement of this.
|
|
68
|
+
3. **Re-quote at dispatch time** — pool state moves; a quote taken minutes ago may
|
|
69
|
+
no longer match.
|
|
70
|
+
|
|
71
|
+
## If it fails
|
|
72
|
+
|
|
73
|
+
| Error | Meaning |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `QuoterV2 reverted` / `amountOut == 0` | No pool at this fee tier (despite resolve-token). Try the next tier from resolve-token's `probedTiers`, or re-run resolve-token — liquidity may have shifted. |
|
|
76
|
+
| `No RPC for chain` | Run from the project root, or pass `--rpc`. |
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sail-template-approve-batch
|
|
3
|
+
description: Gate an SMA's "approve → call → reset" interactions by REUSING the shared ApproveAndCallBatchPermission singleton (Protocol/contracts/templates/ApproveAndCallBatchPermission.sol) — register + configure, no per-SMA deploy. Use when an agent must approve an ERC-20 to a protocol, make one consuming call, and reset the allowance to zero — all in one atomic batch — with token/spender/target/selector allowlists and a per-token approval cap. NOTE: `sailor mandate attach` only registers — you must also configure per-account (see steps).
|
|
4
|
+
compatibility: A Sailor project (`@sail/sdk`, `sailor` CLI). Requires ApproveAndCallBatchPermission deployed on the target chain (recorded in sail-templates/deployed.json); run sail-templates first.
|
|
5
|
+
metadata:
|
|
6
|
+
workspace: sailor-harness
|
|
7
|
+
classification: generic
|
|
8
|
+
status: draft
|
|
9
|
+
origin: Protocol/contracts/templates/ApproveAndCallBatchPermission.sol
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# sail-template-approve-batch — atomic approve/call/reset via the shared singleton
|
|
13
|
+
|
|
14
|
+
Reuse the shared **`ApproveAndCallBatchPermission`** singleton — the safest way to bracket a
|
|
15
|
+
single protocol interaction that needs an allowance. Family overview + flow:
|
|
16
|
+
[`sail-templates`](../sail-templates/SKILL.md).
|
|
17
|
+
|
|
18
|
+
## What it enforces (per account, from source)
|
|
19
|
+
|
|
20
|
+
Authorises exactly this 3-call batch:
|
|
21
|
+
```
|
|
22
|
+
[0] approve(spender, amount) on an allowlisted ERC-20
|
|
23
|
+
[1] consuming call on an allowlisted (target, selector)
|
|
24
|
+
[2] approve(spender, 0) reset to zero (same atomic batch)
|
|
25
|
+
```
|
|
26
|
+
Invariants: `tokens`, `spenders`, and `consumingPairs` must each be non-empty with no zero
|
|
27
|
+
addresses/selectors (`EmptyAllowlist` reverts at configure otherwise); `token ∈ tokens` and
|
|
28
|
+
`amount ≤ maxApprovalAmount[token]`; `spender ∈ spenders`; `(target, selector) ∈ consumingPairs`;
|
|
29
|
+
the pre-batch allowance on `(token, spender)` must be zero; the consuming call must target the
|
|
30
|
+
approved `spender` and pull the approved `token`; the reset-to-zero is required; if
|
|
31
|
+
`requireAmountMatch`, the consuming call's leading `uint256` arg must equal the approve amount.
|
|
32
|
+
Malformed calldata reverts (kernel treats revert as deny).
|
|
33
|
+
|
|
34
|
+
> **Output recipient — `allowUnconstrainedRecipient` (opt-out, default-safe).** By DEFAULT
|
|
35
|
+
> (`allowUnconstrainedRecipient = false`, i.e. omitted) the consuming call's output recipient is
|
|
36
|
+
> decoded and **must equal the SMA** — the safe posture — and any selector outside the decodable
|
|
37
|
+
> set below is denied (fail-closed). Set `allowUnconstrainedRecipient = true` ONLY to deliberately
|
|
38
|
+
> leave the recipient unconstrained (e.g. to authorize a selector outside the decodable set); then
|
|
39
|
+
> only allowlist `(target, selector)` pairs whose semantics you trust to deliver output to the SMA.
|
|
40
|
+
> ⚠️ This is an **opt-out** flag, not an opt-in — do not set it `true` thinking that "requires"
|
|
41
|
+
> or "pins" the recipient; that gets you the opposite (unconstrained) behavior on-chain.
|
|
42
|
+
>
|
|
43
|
+
> **Decodable set** (recipient AND consumed-asset both resolvable at a fixed calldata offset —
|
|
44
|
+
> any other selector is denied regardless of `allowUnconstrainedRecipient`, since the consumed
|
|
45
|
+
> asset can never be bound to the approved token): `swapExactTokensForTokens` (V2), V3
|
|
46
|
+
> `exactInputSingle` (both the SwapRouter and SwapRouter02 layouts), Aave V2 `deposit` / Aave V3
|
|
47
|
+
> `supply`, ERC-4626 `deposit` / `mint`.
|
|
48
|
+
|
|
49
|
+
## Config blob (authoritative — `config-schemas.md`)
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
abi.encode(Config{
|
|
53
|
+
address[] tokens; // approvable ERC-20s
|
|
54
|
+
address[] spenders; // allowed allowance recipients
|
|
55
|
+
ConsumingPair[] consumingPairs; // {address target; bytes4 selector} — pair bound together
|
|
56
|
+
uint256[] maxApprovalAmounts; // index-parallel with tokens; per-token cap
|
|
57
|
+
bool requireAmountMatch; // call[1] leading uint256 must equal approve amount
|
|
58
|
+
bool allowUnconstrainedRecipient; // OPT-OUT (default false = call[1] output recipient decoded, must == account)
|
|
59
|
+
})
|
|
60
|
+
struct ConsumingPair { address target; bytes4 selector; }
|
|
61
|
+
```
|
|
62
|
+
`maxApprovalAmounts.length` must equal `tokens.length` (configure reverts otherwise).
|
|
63
|
+
`consumingPairs` is a **struct array** `(address target, bytes4 selector)[]` — NOT two flat
|
|
64
|
+
`address[]`/`bytes4[]` arrays. A selector is authorised only on the target it is paired with.
|
|
65
|
+
|
|
66
|
+
**ABI tuple (verified round-trip):** `(address[],address[],(address,bytes4)[],uint256[],bool,bool)`.
|
|
67
|
+
Encode with cast — note `selector` is a `bytes4` (left-aligned), and both trailing bools are required:
|
|
68
|
+
```bash
|
|
69
|
+
cast abi-encode 'cfg((address[],address[],(address,bytes4)[],uint256[],bool,bool))' \
|
|
70
|
+
'([<token>],[<spender>],[(<target>,<selector>)],[<maxApproval>],<requireAmountMatch>,<allowUnconstrainedRecipient>)'
|
|
71
|
+
```
|
|
72
|
+
`allowUnconstrainedRecipient` is an **opt-out** — leave it `false` unless you deliberately want
|
|
73
|
+
the consuming call's output recipient unconstrained (see the field note above).
|
|
74
|
+
The leading word is the `0x20` struct offset — that is correct, not flat-param corruption. A
|
|
75
|
+
generic (un-named) revert inside `_applyConfig` means the blob did not decode to this exact tuple
|
|
76
|
+
(usually: flat params with no `0x20` offset, the two flat arrays instead of `ConsumingPair[]`, or a
|
|
77
|
+
missing trailing bool). The named errors (`TokensAndAmountsLengthMismatch`, `EmptyAllowlist`,
|
|
78
|
+
`AllowlistTooLong`) fire only after a successful decode.
|
|
79
|
+
|
|
80
|
+
## Steps
|
|
81
|
+
|
|
82
|
+
Register → configure → simulate → reconfigure mechanics live in
|
|
83
|
+
[`sail-templates` reuse-flow](../sail-templates/references/reuse-flow.md) — follow it.
|
|
84
|
+
`sailor mandate attach` registers only; `configureDirect` (owner tx) is the half that makes the
|
|
85
|
+
permission live. Template-specific bits:
|
|
86
|
+
|
|
87
|
+
- **Singleton:** `ApproveAndCallBatchPermission` — `node SKILLS/sail-templates/catalog.mjs --chain
|
|
88
|
+
<id>`.
|
|
89
|
+
- **Spec to confirm:** tokens+caps, spenders, `(target, selector)` consuming pairs,
|
|
90
|
+
`requireAmountMatch`, `allowUnconstrainedRecipient`. Get each selector with `cast sig` and verify
|
|
91
|
+
it delivers output to the SMA (leave `allowUnconstrainedRecipient = false`, the default, to
|
|
92
|
+
enforce that on-chain — set `true` only to deliberately opt out).
|
|
93
|
+
- **Blob — ⚠️ NOT flat params.** `abi.encode(Config{ … })` — a **single wrapped struct**, so the
|
|
94
|
+
blob starts with a `0x20` offset word. `consumingPairs` is `(address,bytes4)[]` (a struct array),
|
|
95
|
+
and both trailing bools are required — see the verified `cast abi-encode` form above. Unlike
|
|
96
|
+
transfer/withdraw/deposit/borrow/swap, wrapping is required here; a flat blob reverts at configure.
|
|
97
|
+
- **Simulate (mandatory — unaudited example):** the full allowed approve/call/reset batch passes;
|
|
98
|
+
missing reset, off-list spender/target/selector, or over-cap approve is rejected.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sail-template-borrow
|
|
3
|
+
description: Gate an SMA's lending borrows by REUSING the shared BorrowPermission singleton (Protocol/contracts/templates/BorrowPermission.sol) — register + configure, no per-SMA deploy. Use for a bounded borrow mandate against Aave / Morpho / Compound with a protocol + asset allowlist, per-tx cap, and an on-chain LTV check via collateral + borrow oracles. NOTE: `sailor mandate attach` only registers — you must also configure per-account (see steps).
|
|
4
|
+
compatibility: A Sailor project (`@sail/sdk`, `sailor` CLI). Requires BorrowPermission deployed on the target chain (recorded in sail-templates/deployed.json); run sail-templates first.
|
|
5
|
+
metadata:
|
|
6
|
+
workspace: sailor-harness
|
|
7
|
+
classification: generic
|
|
8
|
+
status: draft
|
|
9
|
+
origin: Protocol/contracts/templates/BorrowPermission.sol
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# sail-template-borrow — bounded lending borrow via the shared singleton
|
|
13
|
+
|
|
14
|
+
Reuse the shared **`BorrowPermission`** singleton. Family overview + flow:
|
|
15
|
+
[`sail-templates`](../sail-templates/SKILL.md).
|
|
16
|
+
|
|
17
|
+
## What it enforces (per account, from source)
|
|
18
|
+
|
|
19
|
+
Selectors (any other ⇒ `false`): Aave `borrow(address,uint256,uint256,uint16,address)`
|
|
20
|
+
(**variable-rate only** — a stable-rate borrow, `rateMode != 2`, is rejected), Morpho
|
|
21
|
+
**Optimizer/Morpho-Aave** `borrow(address,uint256,address,address)` (**NOT Morpho Blue** — its
|
|
22
|
+
ABI differs and simply won't match this selector, i.e. fails closed; target a Blue-specific
|
|
23
|
+
permission instead), Compound `borrow(uint256)` (the call target is the **cToken**; the template
|
|
24
|
+
resolves `cToken.underlying()` and keys the allowlist/cap/LTV on the underlying — a target with
|
|
25
|
+
no `underlying()`, e.g. cETH, is denied). Invariants: `target ∈ protocols`; `asset ∈ assets`
|
|
26
|
+
(Aave/Morpho decode the asset; Compound resolves it via `underlying()`); `amount ≤
|
|
27
|
+
maxAmountPerTx`; `onBehalfOf`/`receiver == SMA`; **`_ltvCheck` passes** — the projected debt
|
|
28
|
+
value (via `borrowOracle`) against collateral value (via `collateralOracle`) must stay within
|
|
29
|
+
`maxLtvBps`, when oracles are configured (see ORACLE MODES below).
|
|
30
|
+
|
|
31
|
+
> **Oracle modes:** configure **zero** oracles for amount-cap-only borrowing (no LTV ceiling is
|
|
32
|
+
> applied at all, despite `maxLtvBps` being stored) or **both** — exactly one oracle reverts
|
|
33
|
+
> `OracleConfigInconsistent` at configure (a single feed can't price a ratio). When both are set,
|
|
34
|
+
> the check still depends on their honesty/freshness — see staleness via `maxPriceAgeSec`.
|
|
35
|
+
|
|
36
|
+
## Config blob (authoritative — `config-schemas.md`)
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
abi.encode(address[] protocols, address[] assets, uint256 maxAmountPerTx,
|
|
40
|
+
uint256 maxLtvBps, address collateralOracle, address borrowOracle,
|
|
41
|
+
uint256 maxPriceAgeSec)
|
|
42
|
+
```
|
|
43
|
+
| Field | Notes |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `protocols` | lending pools the agent may borrow from |
|
|
46
|
+
| `assets` | borrowable ERC-20 allowlist |
|
|
47
|
+
| `maxAmountPerTx` | per-borrow cap, base units |
|
|
48
|
+
| `maxLtvBps` | max loan-to-value, bps (e.g. `5000` = 50%) |
|
|
49
|
+
| `collateralOracle` / `borrowOracle` | price sources for the LTV check |
|
|
50
|
+
| `maxPriceAgeSec` | oracle freshness bound; `> 0` when oracles are set |
|
|
51
|
+
|
|
52
|
+
> `maxLtvBps` is a **per-transaction** projection, not a tracked lifetime LTV. For
|
|
53
|
+
> portfolio-level exposure, rely on the protocol's health factor or pair with a
|
|
54
|
+
> position-monitoring permission.
|
|
55
|
+
|
|
56
|
+
## Steps
|
|
57
|
+
|
|
58
|
+
Register → configure → simulate → reconfigure mechanics (and the encoding gotcha) live in
|
|
59
|
+
[`sail-templates` reuse-flow](../sail-templates/references/reuse-flow.md) — follow it.
|
|
60
|
+
`sailor mandate attach` registers only; `configureDirect` (owner tx) is the half that makes the
|
|
61
|
+
permission live. Template-specific bits:
|
|
62
|
+
|
|
63
|
+
- **Singleton:** `BorrowPermission` — `node SKILLS/sail-templates/catalog.mjs --chain <id>`.
|
|
64
|
+
- **Spec to confirm:** protocols, assets, cap, max LTV, oracles.
|
|
65
|
+
- **Blob:** `abi.encode(protocols[], assets[], maxAmountPerTx, maxLtvBps, collateralOracle,
|
|
66
|
+
borrowOracle, maxPriceAgeSec)` — **flat params, no wrapper**.
|
|
67
|
+
- **Simulate (mandatory — unaudited example):** allowed borrow within LTV passes; over-cap,
|
|
68
|
+
over-LTV, or wrong recipient is rejected.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sail-template-deposit
|
|
3
|
+
description: Gate an SMA's deposits into vaults / lending pools by REUSING the shared DepositPermission singleton (Protocol/contracts/templates/DepositPermission.sol) — register + configure, no per-SMA deploy. Use for a bounded deposit mandate into ERC-4626 vaults (deposit/mint) or Aave v2/v3 (deposit/supply) with a target + token allowlist and a per-tx cap; the resulting position always accrues to the SMA. NOTE: `sailor mandate attach` only registers — you must also configure per-account (see steps).
|
|
4
|
+
compatibility: A Sailor project (`@sail/sdk`, `sailor` CLI). Requires DepositPermission deployed on the target chain (recorded in sail-templates/deployed.json); run sail-templates first.
|
|
5
|
+
metadata:
|
|
6
|
+
workspace: sailor-harness
|
|
7
|
+
classification: generic
|
|
8
|
+
status: draft
|
|
9
|
+
origin: Protocol/contracts/templates/DepositPermission.sol
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# sail-template-deposit — bounded vault/lending deposit via the shared singleton
|
|
13
|
+
|
|
14
|
+
Reuse the shared **`DepositPermission`** singleton. Family overview + flow:
|
|
15
|
+
[`sail-templates`](../sail-templates/SKILL.md). Discover yield targets first with
|
|
16
|
+
`sail-defillama-yields`.
|
|
17
|
+
|
|
18
|
+
## What it enforces (per account, from source)
|
|
19
|
+
|
|
20
|
+
Selectors (any other ⇒ `false`):
|
|
21
|
+
|
|
22
|
+
| Function | Venue |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `deposit(uint256 assets, address receiver)` | ERC-4626 / simple vault |
|
|
25
|
+
| `mint(uint256 shares, address receiver)` | ERC-4626 |
|
|
26
|
+
| `deposit(address asset, uint256 amount, address onBehalfOf, uint16)` | Aave v2 |
|
|
27
|
+
| `supply(address asset, uint256 amount, address onBehalfOf, uint16)` | Aave v3 |
|
|
28
|
+
|
|
29
|
+
Invariants: `value == 0`; `targets`/`tokens` must be non-empty with no zero addresses
|
|
30
|
+
(`EmptyAllowlist`/`ZeroAddress` revert at configure otherwise); `target ∈ targets`; token
|
|
31
|
+
allowlist enforced — Aave checks the `asset` arg, ERC-4626 requires the `target` (vault) itself
|
|
32
|
+
∈ `tokens`; `amount`/`shares ≤ maxAmountPerTx`; `receiver`/`onBehalfOf == SMA`.
|
|
33
|
+
|
|
34
|
+
> `mint`'s cap is in **shares**, not underlying. At a high share price the effective asset cap
|
|
35
|
+
> is `maxAmountPerTx × sharePrice` — size accordingly. For ERC-4626 `deposit`/`mint` the token
|
|
36
|
+
> allowlist is the last line of defence (no asset in calldata) — only allowlist vaults whose
|
|
37
|
+
> accepted token you trust.
|
|
38
|
+
|
|
39
|
+
## Config blob (authoritative — `config-schemas.md`)
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
abi.encode(address[] targets, address[] tokens, uint256 maxAmountPerTx)
|
|
43
|
+
```
|
|
44
|
+
| Field | Notes |
|
|
45
|
+
|---|---|
|
|
46
|
+
| `targets` | vault / lending-pool addresses |
|
|
47
|
+
| `tokens` | ERC-20 allowlist (the asset for Aave; the vault itself for ERC-4626) |
|
|
48
|
+
| `maxAmountPerTx` | per-deposit cap (assets, or shares for `mint`), base units |
|
|
49
|
+
|
|
50
|
+
## Steps
|
|
51
|
+
|
|
52
|
+
Register → configure → simulate → reconfigure mechanics (and the encoding gotcha) live in
|
|
53
|
+
[`sail-templates` reuse-flow](../sail-templates/references/reuse-flow.md) — follow it.
|
|
54
|
+
`sailor mandate attach` registers only; `configureDirect` (owner tx) is the half that makes the
|
|
55
|
+
permission live. Template-specific bits:
|
|
56
|
+
|
|
57
|
+
- **Singleton:** `DepositPermission` — `node SKILLS/sail-templates/catalog.mjs --chain <id>`.
|
|
58
|
+
- **Spec to confirm:** targets, tokens, cap (note the share-price caveat for `mint`).
|
|
59
|
+
- **Blob:** `abi.encode(targets[], tokens[], maxAmountPerTx)` — **flat params, no wrapper**.
|
|
60
|
+
- **Simulate (mandatory — unaudited example):** allowed deposit within cap passes; off-list
|
|
61
|
+
target/token, over-cap, or `receiver != SMA` is rejected.
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sail-template-swap
|
|
3
|
+
description: Gate an SMA's DEX swaps by REUSING the shared SwapPermission singleton (Protocol/contracts/templates/SwapPermission.sol) — register + configure, no per-SMA deploy. Use for a bounded swap / DCA mandate on Uniswap V3, V3-02, or V2 with router + token-in/out allowlists, a per-tx cap, and a MANDATORY oracle slippage band (priceOracle is required — for no-oracle tokens use sail-template-swap-no-oracle). For the LI.FI aggregator use sail-lifi-swap; for Pendle use sail-pendle. NOTE: `sailor mandate attach` only registers — you must also configure per-account (see steps).
|
|
4
|
+
compatibility: A Sailor project (`@sail/sdk`, `sailor` CLI). Requires SwapPermission deployed on the target chain (recorded in sail-templates/deployed.json); run sail-templates first.
|
|
5
|
+
metadata:
|
|
6
|
+
workspace: sailor-harness
|
|
7
|
+
classification: generic
|
|
8
|
+
status: draft
|
|
9
|
+
origin: Protocol/contracts/templates/SwapPermission.sol
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# sail-template-swap — bounded DEX swap via the shared singleton
|
|
13
|
+
Reuse the shared **`SwapPermission`** singleton instead of authoring/deploying a swap contract.
|
|
14
|
+
Register its address on the SMA and `configure()` your routers, token allowlists, cap, and
|
|
15
|
+
|
|
16
|
+
slippage. Family overview + flow: [`sail-templates`](../sail-templates/SKILL.md).
|
|
17
|
+
|
|
18
|
+
## What it enforces (per account, from source)
|
|
19
|
+
|
|
20
|
+
Supported selectors (any other ⇒ `false`):
|
|
21
|
+
|
|
22
|
+
| Selector | Function | Venue |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| `0x414bf389` | `exactInputSingle((tokenIn,tokenOut,fee,recipient,deadline,amountIn,amountOutMinimum,sqrtPriceLimitX96))` | Uniswap V3 SwapRouter |
|
|
25
|
+
| `0x04e45aaf` | `exactInputSingle((tokenIn,tokenOut,fee,recipient,amountIn,amountOutMinimum,sqrtPriceLimitX96))` | Uniswap V3 SwapRouter02 |
|
|
26
|
+
| `0x38ed1739` | `swapExactTokensForTokens(amountIn,amountOutMin,path[],to,deadline)` | Uniswap V2 Router |
|
|
27
|
+
|
|
28
|
+
Invariants: `ctx.value == 0` (native value rejected — ERC-20→ERC-20 only); `target ∈ routers`;
|
|
29
|
+
**`tokenIn != tokenOut` / `path[0] != path[last]`** (self-routes denied — a round-trip burns AMM
|
|
30
|
+
fees while an oracle reporting base==quote would otherwise clear the band); `tokenIn`/`path[0] ∈
|
|
31
|
+
tokensIn`; `tokenOut`/`path[last] ∈ tokensOut`; `recipient`/`to == SMA` (funds can't leave the
|
|
32
|
+
account); `amountIn ≤ maxAmountPerTx`; **oracle band ALWAYS enforced** (the oracle is
|
|
33
|
+
mandatory — see below): `amountOutMin ≥ amountIn × price/10^dec ×
|
|
34
|
+
(10_000 − maxSlippageBps)/10_000`. Dust trades whose computed floor truncates to `0` are
|
|
35
|
+
**denied** (fail-closed).
|
|
36
|
+
|
|
37
|
+
> **⛔ The oracle is MANDATORY (contract `v2`, hardened in PR #45).** `_applyConfig` reverts
|
|
38
|
+
> `OracleRequired()` if `priceOracle == 0` and `MissingPriceAge()` if `maxPriceAgeSec == 0`.
|
|
39
|
+
> There is **no oracle-disabled mode** on this template — for tokens with no oracle use the
|
|
40
|
+
> separate [`SwapPermissionNoOracle`](../sail-template-swap-no-oracle/SKILL.md) (live-pool
|
|
41
|
+
> hallucination band). `maxSlippageBps == 0` is **NOT** a bypass — it means zero tolerance
|
|
42
|
+
> (exact-out-or-better), the strictest valid setting. `maxSlippageBps > 9_999` reverts
|
|
43
|
+
> `SlippageBpsTooLarge`.
|
|
44
|
+
|
|
45
|
+
> **V2 intermediate hops are NOT checked** — only `path[0]`/`path[last]`. Restrict to V3 or
|
|
46
|
+
> ensure any plausible intermediate token is acceptable.
|
|
47
|
+
|
|
48
|
+
## ⚠️ Approve coverage — the hidden precondition
|
|
49
|
+
|
|
50
|
+
`SwapPermission` authorizes the swap **call** only. The router pulls `tokenIn` via an ERC-20
|
|
51
|
+
allowance, and the `approve(router, amount)` that establishes it is a **separate transaction this
|
|
52
|
+
permission does not cover** (same rule as every protocol permission — see
|
|
53
|
+
[`sail-mandates/references/approvals.md`](../sail-mandates/references/approvals.md)). A swap with
|
|
54
|
+
no/insufficient allowance reverts inside the router and the tick fails. **Every** bounded swap
|
|
55
|
+
needs the allowance handled — decide how at mandate-build time, not when the first tick reverts.
|
|
56
|
+
|
|
57
|
+
- **Autonomous agent (DCA / rebalancer / treasury) — default:** do NOT use `SwapPermission` alone;
|
|
58
|
+
register the shared **`ApproveAndCallBatchPermission`** singleton and dispatch each tick as one
|
|
59
|
+
atomic `[approve(router, amountIn), swap, approve(router, 0)]` batch — no lingering allowance,
|
|
60
|
+
no per-tick owner signature. See [`sail-template-approve-batch`](../sail-template-approve-batch/SKILL.md).
|
|
61
|
+
- **Owner in the loop per swap:** keep `SwapPermission`; the owner pre-approves the router. The
|
|
62
|
+
agent must read `allowance(SMA, router)` and **stall (never self-approve)** when it is below
|
|
63
|
+
`amountIn` — `approve()` is an owner-side action the agent cannot take on its own.
|
|
64
|
+
|
|
65
|
+
## Config blob (authoritative — `config-schemas.md`)
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
abi.encode(address[] routers, address[] tokensIn, address[] tokensOut,
|
|
69
|
+
uint256 maxAmountPerTx, uint256 maxSlippageBps,
|
|
70
|
+
address priceOracle, uint256 maxPriceAgeSec)
|
|
71
|
+
```
|
|
72
|
+
| Field | Notes |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `routers` | DEX routers the agent may call |
|
|
75
|
+
| `tokensIn` / `tokensOut` | sell-side / buy-side allowlists (multiple out ⇒ portfolio DCA) |
|
|
76
|
+
| `maxAmountPerTx` | per-swap cap, base units (e.g. `25_000_000` = 25 USDC) |
|
|
77
|
+
| `maxSlippageBps` | e.g. `100` = 1%. `0` = zero tolerance (strictest), NOT a bypass. Must be `≤ 9_999`. |
|
|
78
|
+
| `priceOracle` | **REQUIRED, non-zero.** An `IOracle` adapter exposing `getPrice(tokenIn, tokenOut) → (price, dec, updatedAt)` — **not** a raw Chainlink/Pyth feed. `0` reverts `OracleRequired`. |
|
|
79
|
+
| `maxPriceAgeSec` | **REQUIRED, `> 0`** (oracle staleness bound). `0` reverts `MissingPriceAge`. |
|
|
80
|
+
|
|
81
|
+
Size the cap and slippage floor with `uniswap-v3-quote` / `sail-pyth-prices`. The SDK
|
|
82
|
+
`boundedSwapTemplate` encoder matches this tuple — fine to use after a quick verify.
|
|
83
|
+
|
|
84
|
+
### Worked example — single-leg USDC → WETH
|
|
85
|
+
|
|
86
|
+
Concrete params for a 25-USDC-per-swap, 1%-slippage DCA leg. These are the values you hand to
|
|
87
|
+
`boundedSwapTemplate.encoder.encode(...)` to produce the `configureDirect` blob (step 3b) — they
|
|
88
|
+
are **not** an args-file for a CLI command. `priceOracle` must be a **real, non-zero `IOracle`
|
|
89
|
+
adapter** for this pair on this chain (`0x0` reverts):
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"routers": ["0x73855d06DE49d0fe4A9c42636Ba96c62da12FF9C"],
|
|
94
|
+
"tokensIn": ["0x078D782b760474a361dDA0AF3839290b0EF57AD6"],
|
|
95
|
+
"tokensOut": ["0x4200000000000000000000000000000000000006"],
|
|
96
|
+
"maxAmountPerTx": "25000000",
|
|
97
|
+
"maxSlippageBps": 100,
|
|
98
|
+
"priceOracle": "0x9d84C11626d13C5DC9540fA12A3Ff7B85Ac3c1B9",
|
|
99
|
+
"maxPriceAgeSec": 3600
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
> The `priceOracle` above is the **default Unichain USDC/WETH `IOracle` adapter** — a verified
|
|
104
|
+
> Uniswap V3 30-min TWAP (`UniV3TwapOracle`, pool `0x65081C…DBcF1`, 0.05% tier). It serves the
|
|
105
|
+
> USDC↔WETH pair both directions and reverts `UnsupportedPair` for anything else. `catalog.mjs`
|
|
106
|
+
> lists it under **IOracle adapters**; on other chains/pairs you must supply your own adapter
|
|
107
|
+
> (`address(0)` reverts). Because it's a live TWAP, `updatedAt` is always current — `maxPriceAgeSec`
|
|
108
|
+
> just needs to be a few minutes (e.g. `3600`).
|
|
109
|
+
|
|
110
|
+
| Field | From | Notes |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| `routers` | SwapRouter02 for the chain (resolve-token's chain table) | V3 router; the agent calls `exactInputSingle` on it |
|
|
113
|
+
| `tokensIn` | `resolve-token` | sell-side allowlist (usually just USDC) |
|
|
114
|
+
| `tokensOut` | `resolve-token` | buy-side allowlist; **multiple ⇒ portfolio DCA** |
|
|
115
|
+
| `maxAmountPerTx` | user's per-swap size, **base units** (string) | `"25000000"` = 25 USDC (6 dec) |
|
|
116
|
+
| `maxSlippageBps` | `quote-swap`'s recommendation | `100` = 1%. `0` = zero tolerance (strictest), not a bypass. `≤ 9_999`. |
|
|
117
|
+
| `priceOracle` | an `IOracle` adapter for the chain (Pyth/Chainlink-backed) | **REQUIRED, non-zero.** Must expose `getPrice(tokenIn,tokenOut)` — not the raw feed contract. |
|
|
118
|
+
| `maxPriceAgeSec` | seconds | **REQUIRED, `> 0`** — staleness bound. e.g. `3600` = 1h. |
|
|
119
|
+
|
|
120
|
+
> **Caps are base units.** A decimals mismatch (USDC is 6, most tokens 18) silently mis-sizes
|
|
121
|
+
> every bound. `resolve-token` verified decimals on-chain — use that value, never a guess.
|
|
122
|
+
> For a **portfolio DCA**, list every buy-side token in `tokensOut` (one config, many legs).
|
|
123
|
+
|
|
124
|
+
## Steps
|
|
125
|
+
|
|
126
|
+
> **Register ≠ configure.** `sailor mandate attach` only registers the singleton on the kernel;
|
|
127
|
+
> it does NOT configure it. A registered-but-unconfigured singleton denies every call. You must
|
|
128
|
+
> do both — steps 3a (register) and 3b (configure). Full mechanics + the encoding gotcha:
|
|
129
|
+
> [`sail-templates` reuse-flow](../sail-templates/references/reuse-flow.md).
|
|
130
|
+
|
|
131
|
+
1. **Address:** `node SKILLS/sail-templates/catalog.mjs --chain <id>` → `SwapPermission`
|
|
132
|
+
address. It's the same address on every chain (CREATE2); see `deployed.json`.
|
|
133
|
+
2. **Confirm the spec with the user** (sell/buy tokens, per-swap cap, slippage, router/fee
|
|
134
|
+
tier, recipient = SMA) — print the explainer's humanReadable + warnings. No gas before
|
|
135
|
+
approval.
|
|
136
|
+
3. **a. Register** the singleton on the SMA's kernel (does NOT configure):
|
|
137
|
+
```bash
|
|
138
|
+
sailor mandate attach --address <SWAP_PERMISSION> --sma <SMA> --label "bounded-swap"
|
|
139
|
+
```
|
|
140
|
+
**b. Configure** the per-account bounds — this is what makes the permission live. Build a
|
|
141
|
+
JSON file with the flat template params (`routers`, `tokensIn`, `tokensOut`, `maxAmountPerTx`,
|
|
142
|
+
`maxSlippageBps`, `priceOracle`, `maxPriceAgeSec` — matches the SDK `boundedSwapTemplate`
|
|
143
|
+
encoder tuple, no wrapper), then run:
|
|
144
|
+
```bash
|
|
145
|
+
sailor mandate configure --address <SWAP_PERMISSION> --sma <SMA> \
|
|
146
|
+
--template SwapPermission --args-file ./swap-config.json
|
|
147
|
+
```
|
|
148
|
+
`configureDirect` requires `msg.sender == permissionSigner` (`kernel.configs(<SMA>)` — the
|
|
149
|
+
owner only when they collapse to the same address); the command pre-flights the blob with an
|
|
150
|
+
`eth_call` BEFORE any signing or gas — it **reverts** on `priceOracle == 0` (`OracleRequired`),
|
|
151
|
+
`maxPriceAgeSec == 0` (`MissingPriceAge`), or `maxSlippageBps > 9_999` (`SlippageBpsTooLarge`) —
|
|
152
|
+
then requests the `configureDirect` tx from `permissionSigner` through the signing station and
|
|
153
|
+
verifies `isConfigured(<SMA>) == true` on receipt. Pass `--simulate-only` to stop after the
|
|
154
|
+
pre-flight (no signing, no gas).
|
|
155
|
+
*(The signed `configure(account, params, deadline, sig)` path uses EIP-712 domain `("SwapPermission","2")`.)*
|
|
156
|
+
4. **Simulate** — prove an allowed swap passes and a bad one (wrong recipient / over-cap /
|
|
157
|
+
disallowed token) fails:
|
|
158
|
+
```bash
|
|
159
|
+
sailor mandate simulate --address <SWAP_PERMISSION> --sma <SMA> --calls ./swap-probe.json
|
|
160
|
+
```
|
|
161
|
+
5. **Reconfigure** later (new cap / extra output token) — re-run step 3b with a new blob; same
|
|
162
|
+
address, no re-register.
|
|
163
|
+
|
|
164
|
+
## Agent config (off-chain, within the on-chain bounds)
|
|
165
|
+
|
|
166
|
+
The mandate bounds what the agent MAY do; the agent's code decides what it WILL do and when. Wire
|
|
167
|
+
the legs from the resolve + quote outputs:
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
export const DCA_LEGS = [
|
|
171
|
+
{
|
|
172
|
+
tokenIn: "<USDC_ADDR>", tokenOut: "<WETH_ADDR>",
|
|
173
|
+
tokenInDecimals: 6, tokenOutDecimals: 18,
|
|
174
|
+
feeTier: 3000, amountPerSwap: 25_000_000n, // base units, ≤ maxAmountPerTx
|
|
175
|
+
slippageBps: 100,
|
|
176
|
+
},
|
|
177
|
+
] as const;
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The agent must re-quote via [`sail-swap-quote`](../sail-swap-quote/SKILL.md) close to dispatch time
|
|
181
|
+
and embed the floor `amountOutMinimum` in the swap calldata — the on-chain `maxSlippageBps` enforces
|
|
182
|
+
it regardless. **Match the tick's dispatch shape to your approve model:** under the batch model
|
|
183
|
+
(default) the agent returns one `Dispatch` whose `calls` is the 3-element
|
|
184
|
+
`[approve(router, amountIn), swap, approve(router, 0)]` and must NOT pre-approve out of band (the
|
|
185
|
+
batch requires a zero pre-batch allowance); under `SwapPermission` + owner pre-approve it emits a
|
|
186
|
+
plain single-call swap and MUST read `allowance(SMA, router)` and **stall (not self-approve)** when
|
|
187
|
+
it is below `amountIn`.
|
|
188
|
+
|
|
189
|
+
## When NOT to use this
|
|
190
|
+
- **Token has no `IOracle` adapter** → this template REQUIRES a non-zero oracle and will not
|
|
191
|
+
configure without one. Use [`sail-template-swap-no-oracle`](../sail-template-swap-no-oracle/SKILL.md)
|
|
192
|
+
(live-pool hallucination band — catches honest mistakes, NOT MEV/flash-loan attacks) or author a
|
|
193
|
+
bespoke permission. There is no oracle-off mode on `SwapPermission`.
|
|
194
|
+
- **Aggregator (LI.FI) or opaque calldata** → the mandate can't inspect the route; use
|
|
195
|
+
[`sail-lifi-swap`](../sail-lifi-swap/SKILL.md) or [`sail-mandates`](../sail-mandates/SKILL.md).
|
|
196
|
+
- **`SwapPermission` not deployed on your chain** → check `deployed.json` first (it's live on all
|
|
197
|
+
11 Sailor-bundled chains as of the current deploy); for anything outside that set, author your
|
|
198
|
+
own via [`sail-mandates`](../sail-mandates/SKILL.md).
|
|
199
|
+
|
|
200
|
+
## Notes
|
|
201
|
+
- The oracle is **mandatory** and the band is **always** enforced; `maxSlippageBps = 0` is the
|
|
202
|
+
strictest setting (zero tolerance), not a way to disable the check.
|
|
203
|
+
- `priceOracle` must be an `IOracle` adapter (`getPrice(tokenIn,tokenOut) → (price, dec, updatedAt)`),
|
|
204
|
+
not a raw Chainlink/Pyth feed; it must return a meaningful `updatedAt` or evaluate fails closed.
|
|
205
|
+
- Native value is rejected (`ctx.value != 0 ⇒ deny`) — ERC-20→ERC-20 only.
|
|
206
|
+
- Unaudited example — step 4 is mandatory.
|
|
207
|
+
- `recipient = SMA` is non-negotiable and enforced in the contract, not just config.
|