@dev.sail.money/sailor 1.3.0-98 → 1.4.0-242
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +7 -7
- package/LICENSE +1 -1
- package/README.md +64 -472
- package/package.json +11 -6
- package/packages/cli/README.md +1 -1
- package/packages/cli/dist/index.cjs +1469 -463
- package/packages/cli/dist/server.cjs +19796 -21737
- package/packages/sdk/README.md +1 -1
- 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/SailKernel.d.ts +42 -0
- package/packages/sdk/dist/abis/SailKernel.d.ts.map +1 -1
- package/packages/sdk/dist/abis/SailKernel.js +34 -0
- package/packages/sdk/dist/abis/SailKernel.js.map +1 -1
- 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/chains.d.ts +0 -7
- package/packages/sdk/dist/chains.d.ts.map +1 -1
- package/packages/sdk/dist/chains.js +81 -32
- package/packages/sdk/dist/chains.js.map +1 -1
- package/packages/sdk/dist/client.d.ts +1 -1
- package/packages/sdk/dist/client.d.ts.map +1 -1
- package/packages/sdk/dist/client.js +137 -16
- package/packages/sdk/dist/client.js.map +1 -1
- package/packages/sdk/dist/deployments.d.ts +6 -4
- package/packages/sdk/dist/deployments.d.ts.map +1 -1
- package/packages/sdk/dist/deployments.js +120 -118
- package/packages/sdk/dist/deployments.js.map +1 -1
- package/packages/sdk/dist/eip712.d.ts +84 -0
- package/packages/sdk/dist/eip712.d.ts.map +1 -1
- package/packages/sdk/dist/eip712.js +148 -0
- package/packages/sdk/dist/eip712.js.map +1 -1
- package/packages/sdk/dist/fees.d.ts +2 -1
- package/packages/sdk/dist/fees.d.ts.map +1 -1
- package/packages/sdk/dist/fees.js +2 -1
- package/packages/sdk/dist/fees.js.map +1 -1
- package/packages/sdk/dist/index.d.ts +3 -2
- package/packages/sdk/dist/index.d.ts.map +1 -1
- package/packages/sdk/dist/index.js +3 -2
- 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/sdk/dist/safe.d.ts +49 -0
- package/packages/sdk/dist/safe.d.ts.map +1 -1
- package/packages/sdk/dist/safe.js +74 -0
- package/packages/sdk/dist/safe.js.map +1 -1
- package/packages/sdk/package.json +6 -2
- package/packages/ui/dist/assets/{add-BX5reKtK.js → add-CBbu2pe_.js} +2 -2
- package/packages/ui/dist/assets/{all-wallets-IZ3PlVF-.js → all-wallets-C1oy4NUu.js} +2 -2
- package/packages/ui/dist/assets/{app-store-ARgRdMdF.js → app-store-DKqyf0Zv.js} +1 -1
- package/packages/ui/dist/assets/{apple-VCaqsI9w.js → apple-DyoIbCFy.js} +2 -2
- package/packages/ui/dist/assets/{arrow-bottom-circle-h5vFiZ2I.js → arrow-bottom-circle-CMS-OqZQ.js} +2 -2
- package/packages/ui/dist/assets/arrow-bottom-pUWeEqYu.js +8 -0
- package/packages/ui/dist/assets/arrow-left-B81HMJNL.js +8 -0
- package/packages/ui/dist/assets/arrow-right-Z5czoKlC.js +8 -0
- package/packages/ui/dist/assets/arrow-top-C6ym1oll.js +8 -0
- package/packages/ui/dist/assets/{bank-6c28yOM9.js → bank-B41nlslf.js} +2 -2
- package/packages/ui/dist/assets/{basic-CJnbTfSd.js → basic-CWqOb7CV.js} +177 -177
- package/packages/ui/dist/assets/{browser-CDKlxM3D.js → browser-DDKdqoZ9.js} +2 -2
- package/packages/ui/dist/assets/{card-DQdED02q.js → card-BgOmTajq.js} +2 -2
- package/packages/ui/dist/assets/ccip-CtJUKWRd.js +1 -0
- package/packages/ui/dist/assets/{checkmark-JoRNVoFV.js → checkmark-CGqQipH4.js} +2 -2
- package/packages/ui/dist/assets/{checkmark-bold-B6bGHVIs.js → checkmark-bold-Ci5xf90j.js} +2 -2
- package/packages/ui/dist/assets/chevron-bottom-D7Xert17.js +8 -0
- package/packages/ui/dist/assets/chevron-left-BGbUebR2.js +8 -0
- package/packages/ui/dist/assets/chevron-right-xVsESnHU.js +8 -0
- package/packages/ui/dist/assets/chevron-top-DEA9cR6v.js +8 -0
- package/packages/ui/dist/assets/{chrome-store-D-fiJ9qJ.js → chrome-store-BsyI-4PI.js} +2 -2
- package/packages/ui/dist/assets/{clock-xCPkzwjj.js → clock-Dct9oQ3M.js} +2 -2
- package/packages/ui/dist/assets/{close-BkqmPpWG.js → close-BvSCnI2y.js} +2 -2
- package/packages/ui/dist/assets/{coinPlaceholder-PIvN9oq_.js → coinPlaceholder-p7RiWkon.js} +2 -2
- package/packages/ui/dist/assets/{compass-CW5kWcxW.js → compass-BUeeZqX4.js} +2 -2
- package/packages/ui/dist/assets/{copy-DUZlAQJJ.js → copy-C5jXbxLq.js} +2 -2
- package/packages/ui/dist/assets/core-CiAm42Mq.js +907 -0
- package/packages/ui/dist/assets/cursor-C0T7JWFO.js +3 -0
- package/packages/ui/dist/assets/{cursor-transparent-BUAMB0G1.js → cursor-transparent-BZp3-JmA.js} +2 -2
- package/packages/ui/dist/assets/{desktop-BxdlwV8P.js → desktop-BIui_Lzb.js} +2 -2
- package/packages/ui/dist/assets/{disconnect-GelNh3mn.js → disconnect-B3Bxdchp.js} +2 -2
- package/packages/ui/dist/assets/{discord-N0jnxkzC.js → discord-VWI0yDyx.js} +2 -2
- package/packages/ui/dist/assets/{etherscan-B41u7wF4.js → etherscan-BpBvUT9i.js} +2 -2
- package/packages/ui/dist/assets/events-DQ172AOg.js +1 -0
- package/packages/ui/dist/assets/{exclamation-triangle-BWJC56FC.js → exclamation-triangle-o1TJHIUw.js} +2 -2
- package/packages/ui/dist/assets/{extension-G6I6oogw.js → extension-4RaLvbh5.js} +2 -2
- package/packages/ui/dist/assets/external-link-BHIwjDgj.js +8 -0
- package/packages/ui/dist/assets/{facebook-47twy32b.js → facebook-Bes5g-wN.js} +2 -2
- package/packages/ui/dist/assets/{farcaster-DYqnqCqs.js → farcaster-CiJ-352m.js} +2 -2
- package/packages/ui/dist/assets/{filters-dF6xeJnE.js → filters-DMOteumb.js} +2 -2
- package/packages/ui/dist/assets/{github-DbW4Ihz2.js → github-D2nDec9X.js} +2 -2
- package/packages/ui/dist/assets/{google-TKm7G8LF.js → google-lZZlQZPQ.js} +2 -2
- package/packages/ui/dist/assets/{help-circle-CY3qTmsG.js → help-circle-D4cSSziR.js} +1 -1
- package/packages/ui/dist/assets/{id-JpHW4q14.js → id-BmhXZq5P.js} +2 -2
- package/packages/ui/dist/assets/{image-Dip2fG98.js → image-Dz4FPeET.js} +2 -2
- package/packages/ui/dist/assets/index-B07bCKJ8.css +1 -0
- package/packages/ui/dist/assets/index-BV8s6ez1.js +1 -0
- package/packages/ui/dist/assets/index-BeUp3nOq.js +1 -0
- package/packages/ui/dist/assets/index-CvRylmZJ.js +16 -0
- package/packages/ui/dist/assets/{index-q0mGElxZ.js → index-DeoQO45V.js} +3 -3
- package/packages/ui/dist/assets/index-Et5S3ZaW.js +47 -0
- package/packages/ui/dist/assets/index-LiKeH8Cd.js +1780 -0
- package/packages/ui/dist/assets/index.es-dfpEui-9.js +26 -0
- package/packages/ui/dist/assets/{info-B1enFsqh.js → info-77RSvIMg.js} +2 -2
- package/packages/ui/dist/assets/{info-circle-D4j3FISs.js → info-circle-CWsII-NN.js} +2 -2
- package/packages/ui/dist/assets/{lightbulb-DZbKr-vS.js → lightbulb-BZXzm7XE.js} +2 -2
- package/packages/ui/dist/assets/{mail-D-TO_uVQ.js → mail-DMG1YqB0.js} +2 -2
- package/packages/ui/dist/assets/metamask-sdk-K1x_iqcc.js +542 -0
- package/packages/ui/dist/assets/{mobile-Bkmwzav2.js → mobile-DuJfOBSk.js} +2 -2
- package/packages/ui/dist/assets/{more-D1_I_jiB.js → more-Cs9ySK2F.js} +2 -2
- package/packages/ui/dist/assets/{network-placeholder-DUaFjoXh.js → network-placeholder-CMDPmB1E.js} +2 -2
- package/packages/ui/dist/assets/{nftPlaceholder-Cc0iUryh.js → nftPlaceholder-Cj0f9y2u.js} +2 -2
- package/packages/ui/dist/assets/{off-BvX71sse.js → off-9zSSsm-e.js} +2 -2
- package/packages/ui/dist/assets/{play-store-XzLbtbTd.js → play-store-CPwIb8Gj.js} +2 -2
- package/packages/ui/dist/assets/{plus-B77_RmYR.js → plus-BOHQri8o.js} +2 -2
- package/packages/ui/dist/assets/{qr-code-BNeIhB1d.js → qr-code-DqhbK276.js} +1 -1
- package/packages/ui/dist/assets/{recycle-horizontal-B9sX-yty.js → recycle-horizontal-BpGM56Qz.js} +2 -2
- package/packages/ui/dist/assets/{refresh-BWRygnjm.js → refresh-BPzZlhTP.js} +2 -2
- package/packages/ui/dist/assets/{reown-logo-DCAEsGFm.js → reown-logo-OCkI0xzo.js} +2 -2
- package/packages/ui/dist/assets/{search-ZXIqrawS.js → search-B1JEJvwq.js} +2 -2
- package/packages/ui/dist/assets/{secp256k1-Doz05h4g.js → secp256k1-BqUeV9Cb.js} +1 -1
- package/packages/ui/dist/assets/{send-BQcO0jNC.js → send-JNsDlNoq.js} +2 -2
- package/packages/ui/dist/assets/{swapHorizontal-4yZEzz2K.js → swapHorizontal-C3Z1FImK.js} +2 -2
- package/packages/ui/dist/assets/{swapHorizontalBold-DxBKu3sl.js → swapHorizontalBold-Cmdy2iAn.js} +2 -2
- package/packages/ui/dist/assets/{swapHorizontalMedium-DAQYJ2JK.js → swapHorizontalMedium-DFX57Nn9.js} +2 -2
- package/packages/ui/dist/assets/{swapHorizontalRoundedBold-CUlZzFJ6.js → swapHorizontalRoundedBold-baB389c8.js} +2 -2
- package/packages/ui/dist/assets/{swapVertical-CjWHAaDM.js → swapVertical-DmAYGfK1.js} +2 -2
- package/packages/ui/dist/assets/{telegram-JBeabiJP.js → telegram-BHbYE-nI.js} +2 -2
- package/packages/ui/dist/assets/{three-dots-D3R8OjSx.js → three-dots-BwRWl6qJ.js} +2 -2
- package/packages/ui/dist/assets/{twitch-1f9z7xVO.js → twitch-Drx0q6B6.js} +2 -2
- package/packages/ui/dist/assets/{twitterIcon-BW7n2xnp.js → twitterIcon-AuJ2KQ6p.js} +2 -2
- package/packages/ui/dist/assets/{verify-Cs5owI38.js → verify-Dkrd3q7o.js} +2 -2
- package/packages/ui/dist/assets/{verify-filled-Ci5sx5qS.js → verify-filled-B9e3-nz-.js} +2 -2
- package/packages/ui/dist/assets/{w3m-modal-DnWtss8u.js → w3m-modal-DtkEjQA7.js} +1 -1
- package/packages/ui/dist/assets/{wallet-MaGNqaW-.js → wallet-CqYzSiBd.js} +2 -2
- package/packages/ui/dist/assets/{wallet-placeholder-kTOy12_L.js → wallet-placeholder-B05kTN_K.js} +2 -2
- package/packages/ui/dist/assets/{walletconnect-CyuP92jg.js → walletconnect-BwoPXLaS.js} +3 -3
- package/packages/ui/dist/assets/{warning-circle-BImUCdpG.js → warning-circle-DK9Ai8rT.js} +2 -2
- package/packages/ui/dist/assets/{x-II45JHpV.js → x-BNdh5DVW.js} +2 -2
- package/packages/ui/dist/index.html +2 -2
- package/templates/default/.agents/skills/sail-mandates/SKILL.md +1 -1
- package/templates/default/.agents/skills/sail-mandates/references/approvals.md +3 -3
- package/templates/default/.agents/skills/sail-mandates/references/examples-index.md +3 -3
- package/templates/default/.agents/skills/sail-onboarding/SKILL.md +34 -33
- package/templates/default/.agents/skills/sail-servers/SKILL.md +1 -0
- 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/.env.example +9 -0
- package/templates/default/AGENTS.md +107 -13
- package/templates/default/docs/PERMISSION_MODEL.md +2 -2
- package/{examples → templates/default/examples}/custom-mandate/.sail/contracts/interfaces/IPermission.sol +4 -0
- package/{examples → templates/default/examples}/custom-mandate/README.md +4 -1
- package/{examples → templates/default/examples}/permissions/interfaces/IBatchPermission.sol +2 -0
- package/{examples → templates/default/examples}/permissions/interfaces/IPermission.sol +4 -0
- 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/templates/default/test/BoundedCallPermission.t.sol +2 -1
- package/docs/PERMISSION_MODEL.md +0 -93
- package/examples/README.md +0 -24
- package/examples/lifi-permissions/LifiBoundedApprovePermissionCloneable.sol +0 -84
- package/examples/lifi-permissions/LifiDiamondSwapPermissionCloneable.sol +0 -97
- package/examples/lifi-permissions/README.md +0 -53
- package/packages/ui/dist/assets/arrow-bottom-Cwptw8fW.js +0 -8
- package/packages/ui/dist/assets/arrow-left-bM1TRcV9.js +0 -8
- package/packages/ui/dist/assets/arrow-right-BYd_N4Jr.js +0 -8
- package/packages/ui/dist/assets/arrow-top-CLUnwVE2.js +0 -8
- package/packages/ui/dist/assets/ccip-BL7RIPfY.js +0 -1
- package/packages/ui/dist/assets/chevron-bottom-BMCelIEu.js +0 -8
- package/packages/ui/dist/assets/chevron-left-ryPLhHCc.js +0 -8
- package/packages/ui/dist/assets/chevron-right-CdDNRv7f.js +0 -8
- package/packages/ui/dist/assets/chevron-top-BxWIjjGk.js +0 -8
- package/packages/ui/dist/assets/core-von2c1sc.js +0 -907
- package/packages/ui/dist/assets/cursor-BzRVY7BD.js +0 -3
- package/packages/ui/dist/assets/events-C9icG2YB.js +0 -1
- package/packages/ui/dist/assets/external-link-BPaGQf4a.js +0 -8
- package/packages/ui/dist/assets/fallback-BTfJeGjh.js +0 -1
- package/packages/ui/dist/assets/index-BGJ_8fMk.js +0 -1
- package/packages/ui/dist/assets/index-Bq3PAopp.js +0 -16
- package/packages/ui/dist/assets/index-C4_cA5XW.js +0 -1
- package/packages/ui/dist/assets/index-CaDN0gL4.js +0 -1
- package/packages/ui/dist/assets/index-DLElIcBr.js +0 -1789
- package/packages/ui/dist/assets/index-lhNfD4jA.css +0 -1
- package/packages/ui/dist/assets/index.es-BaCKS_tW.js +0 -26
- package/packages/ui/dist/assets/metamask-sdk-DljW3QD1.js +0 -542
- package/packages/ui/dist/assets/native-CJ5et6AR.js +0 -1
- package/packages/ui/dist/assets/parseSignature-BAS-wKoj.js +0 -1
- package/scripts/check-docs.mjs +0 -309
- package/scripts/check-init.mjs +0 -123
- package/scripts/check-update.mjs +0 -177
- package/scripts/clean.mjs +0 -17
- package/scripts/pack.mjs +0 -21
- package/templates/default/.agents/skills/sail-overview/SKILL.md +0 -100
- package/templates/default/.agents/skills/sail-sailor/SKILL.md +0 -190
- package/templates/default/.agents/skills/sail-setup/SKILL.md +0 -38
- package/templates/default/.gitlab-ci.yml +0 -58
- /package/{examples → templates/default/examples}/custom-mandate/foundry.toml +0 -0
- /package/{examples → templates/default/examples}/custom-mandate/mandates/BoundedCallPermission.sol +0 -0
- /package/{examples → templates/default/examples}/custom-mandate/mandates/README.md +0 -0
- /package/{examples → templates/default/examples}/custom-mandate/mandates/SailCalldata.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/BoundedApproveAndCallBatch.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/BoundedBorrow_AaveV3_Arbitrum.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/BoundedLimitless_Base.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/BoundedPerp_GMXv2_Arbitrum.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/BoundedStake_Venice_Base.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/BoundedSupply_AaveV3_Arbitrum.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/BoundedSwapNative_UniswapV3_Base.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/BoundedSwap_UniswapV3_Base.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/BoundedSwap_UniswapV4_Unichain.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/BoundedTransfer_ERC20_Ethereum.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/BoundedVault_ERC4626_Base.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/README.md +0 -0
- /package/{examples → templates/default/examples}/permissions/SailCalldata.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/foundry.toml +0 -0
|
@@ -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 or Pendle, author a bespoke permission via sail-mandates. 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-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.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sail-template-swap-no-oracle
|
|
3
|
+
description: Gate an SMA's DEX swaps for tokens that have NO oracle by REUSING the shared SwapPermissionNoOracle singleton (Protocol/contracts/templates/SwapPermissionNoOracle.sol) — register + configure, no per-SMA deploy. Use for a bounded swap mandate on Uniswap V3/V3-02/V2 (and forks) where no manipulation-resistant price feed exists: router + token-in/out allowlists, a per-tx cap, recipient pinned to the SMA, and a per-pair live-pool "hallucination band". NOT manipulation-resistant — if the token HAS an oracle use sail-template-swap instead; for the LI.FI aggregator author a bespoke permission via sail-mandates. Deployed on all 11 Sailor-bundled chains (recorded in sail-templates/deployed.json).
|
|
4
|
+
compatibility: A Sailor project (`@sail/sdk`, `sailor` CLI). Requires SwapPermissionNoOracle 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/SwapPermissionNoOracle.sol
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# sail-template-swap-no-oracle — bounded DEX swap for oracle-less tokens
|
|
13
|
+
|
|
14
|
+
Reuse the shared **`SwapPermissionNoOracle`** singleton instead of authoring/deploying a swap
|
|
15
|
+
contract. Register its address on the SMA and `configure()` your routers, token allowlists, cap,
|
|
16
|
+
and a **reference pool per tradeable pair**. Family overview + flow:
|
|
17
|
+
[`sail-templates`](../sail-templates/SKILL.md). For the oracle-gated tier see
|
|
18
|
+
[`sail-template-swap`](../sail-template-swap/SKILL.md).
|
|
19
|
+
|
|
20
|
+
> ⚠️ **Pick the right tier.** This template provides **NO manipulation-resistant slippage
|
|
21
|
+
> protection**. Its price band reads a *single pool's live spot price*, which any party can move
|
|
22
|
+
> within the same transaction (flash-loan / sandwich / MEV). It only catches an **honest mistake**
|
|
23
|
+
> — a confused manager/agent quoting a wildly wrong number (a "hallucination guard"). If the token
|
|
24
|
+
> has an independent, freshness-checked price feed, use [`sail-template-swap`](../sail-template-swap/SKILL.md)
|
|
25
|
+
> instead. Only reach for this when no such oracle exists.
|
|
26
|
+
|
|
27
|
+
## What it enforces (per account, from source)
|
|
28
|
+
|
|
29
|
+
Supported selectors (any other ⇒ `false`) — identical decode region to `SwapPermission`:
|
|
30
|
+
|
|
31
|
+
| Selector | Function | Venue |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| `0x414bf389` | `exactInputSingle((tokenIn,tokenOut,fee,recipient,deadline,amountIn,amountOutMinimum,sqrtPriceLimitX96))` | Uniswap V3 SwapRouter |
|
|
34
|
+
| `0x04e45aaf` | `exactInputSingle((tokenIn,tokenOut,fee,recipient,amountIn,amountOutMinimum,sqrtPriceLimitX96))` | Uniswap V3 SwapRouter02 |
|
|
35
|
+
| `0x38ed1739` | `swapExactTokensForTokens(amountIn,amountOutMin,path[],to,deadline)` | Uniswap V2 Router |
|
|
36
|
+
|
|
37
|
+
Structural invariants: `target ∈ routers`; `tokenIn`/`path[0] ∈ tokensIn`; `tokenOut`/`path[last]
|
|
38
|
+
∈ tokensOut`; `recipient`/`to == SMA` (funds can't leave the account); `amountIn ≤ maxAmountPerTx`.
|
|
39
|
+
|
|
40
|
+
**Price band (the only difference from `SwapPermission`):** for the swap's `(tokenIn, tokenOut)`
|
|
41
|
+
pair, read the live spot price of the operator-named reference pool and require
|
|
42
|
+
`amountOutMin ≥ expectedOut × (10_000 − toleranceBps)/10_000` **and** `amountOutMin > 0`. Fails
|
|
43
|
+
closed (denies) if the pool is missing, unreadable, illiquid, doesn't price the pair, or the floor
|
|
44
|
+
truncates to zero.
|
|
45
|
+
|
|
46
|
+
> **V2 intermediate hops are NOT checked** — only `path[0]`/`path[last]`. Restrict to V3 or ensure
|
|
47
|
+
> any plausible intermediate token is acceptable. Does NOT cover Universal Router, Uniswap V4, or
|
|
48
|
+
> aggregators (opaque calldata).
|
|
49
|
+
|
|
50
|
+
## Config blob (authoritative — `config-schemas.md`)
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
abi.encode(address[] routers, address[] tokensIn, address[] tokensOut,
|
|
54
|
+
uint256 maxAmountPerTx, ReferencePool[] referencePools)
|
|
55
|
+
|
|
56
|
+
struct ReferencePool { address tokenIn; address tokenOut; address pool;
|
|
57
|
+
PoolKind kind /* 0=V2, 1=V3 */; uint256 toleranceBps; }
|
|
58
|
+
```
|
|
59
|
+
| Field | Notes |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `routers` | DEX routers the agent may call |
|
|
62
|
+
| `tokensIn` / `tokensOut` | sell-side / buy-side allowlists |
|
|
63
|
+
| `maxAmountPerTx` | per-swap cap, base units (e.g. `25_000_000` = 25 USDC) |
|
|
64
|
+
| `referencePools` | one per directional `(tokenIn, tokenOut)` pair — see coverage rule below |
|
|
65
|
+
| `ReferencePool.pool` | a V2 pair or V3 pool that actually prices the pair (validated at configure) |
|
|
66
|
+
| `ReferencePool.kind` | `0` = V2 (`getReserves`), `1` = V3 (`slot0`+`liquidity`) |
|
|
67
|
+
| `ReferencePool.toleranceBps` | band width, **≤ 5_000 (50%)** or configure reverts. e.g. `1_000` = 10% |
|
|
68
|
+
|
|
69
|
+
**Strict coverage rule:** `configure()` reverts `MissingReferencePool` unless every non-self
|
|
70
|
+
directional combination of `tokensIn × tokensOut` has a `ReferencePool`. The pool's
|
|
71
|
+
`token0()`/`token1()` must match the pair (orientation is precomputed at configure time), else it
|
|
72
|
+
reverts `PoolTokenMismatch`. `pool == 0` reverts `ZeroPool`.
|
|
73
|
+
|
|
74
|
+
Size the cap with `uniswap-v3-quote`. Verify the SDK encoder's param tuple matches the source blob
|
|
75
|
+
above before using it (see [`sail-templates`](../sail-templates/SKILL.md) notes).
|
|
76
|
+
|
|
77
|
+
## Steps
|
|
78
|
+
|
|
79
|
+
Register → configure → simulate → reconfigure mechanics (and the encoding gotcha) live in
|
|
80
|
+
[`sail-templates` reuse-flow](../sail-templates/references/reuse-flow.md) — follow it.
|
|
81
|
+
`sailor mandate attach` registers only; `configureDirect` (owner tx) is the half that makes the
|
|
82
|
+
permission live.
|
|
83
|
+
|
|
84
|
+
Template-specific bits:
|
|
85
|
+
|
|
86
|
+
- **Singleton:** `SwapPermissionNoOracle` — `node SKILLS/sail-templates/catalog.mjs --chain <id>`.
|
|
87
|
+
- **Spec to confirm:** sell/buy tokens, per-swap cap, router/fee tier, recipient = SMA, and for each
|
|
88
|
+
pair the reference pool + tolerance. **Explicitly confirm the no-oracle trade-off** with the user:
|
|
89
|
+
this is a hallucination guard, not slippage/manipulation protection.
|
|
90
|
+
- **Blob:** `abi.encode(routers[], tokensIn[], tokensOut[], maxAmountPerTx, ReferencePool[])` —
|
|
91
|
+
**flat params, no wrapper**; `ReferencePool{tokenIn, tokenOut, pool, kind, toleranceBps}`.
|
|
92
|
+
- **Simulate (mandatory — unaudited example):** an allowed swap passes; wrong recipient, over-cap,
|
|
93
|
+
disallowed token, and an `amountOutMin` below the pool-implied floor are rejected.
|
|
94
|
+
|
|
95
|
+
## Notes
|
|
96
|
+
- The band is **not** manipulation-resistant — re-state this whenever authorizing. Prefer
|
|
97
|
+
[`sail-template-swap`](../sail-template-swap/SKILL.md) wherever an oracle exists.
|
|
98
|
+
- `toleranceBps` is capped at 50% in source; a wider band is meaningless and rejected at configure.
|
|
99
|
+
- Unaudited example — step 4 is mandatory.
|
|
100
|
+
- Aggregator routing (opaque calldata) → author a bespoke permission via [`sail-mandates`](../sail-mandates/SKILL.md).
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sail-template-transfer
|
|
3
|
+
description: Gate an SMA's ERC-20 transfers by REUSING the shared TransferPermission singleton (Protocol/contracts/templates/TransferPermission.sol) — register + configure, no per-SMA deploy. Use to let an agent move approved tokens, within a per-tx cap, only to a recipient allowlist (partner protocols, CEX deposit addrs, co-manager wallets). For returning funds to a single fixed Safe, prefer sail-template-withdraw. NOTE: `sailor mandate attach` only registers — you must also configure per-account (see steps).
|
|
4
|
+
compatibility: A Sailor project (`@sail/sdk`, `sailor` CLI). Requires TransferPermission 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/TransferPermission.sol
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# sail-template-transfer — bounded transfer to an allowlist via the shared singleton
|
|
13
|
+
|
|
14
|
+
Reuse the shared **`TransferPermission`** singleton. Family overview + flow:
|
|
15
|
+
[`sail-templates`](../sail-templates/SKILL.md).
|
|
16
|
+
|
|
17
|
+
## What it enforces (per account, from source)
|
|
18
|
+
|
|
19
|
+
Selectors: `0xa9059cbb` `transfer`, `0x23b872dd` `transferFrom`. Invariants: `value == 0`;
|
|
20
|
+
`allowedRecipients`/`allowedTokens` must be non-empty with no zero addresses
|
|
21
|
+
(`EmptyAllowlist`/`ZeroAddress` revert at configure otherwise); `target (token) ∈
|
|
22
|
+
allowedTokens`; `to ∈ allowedRecipients`; `amount ≤ maxAmountPerTx`; **`transferFrom` requires
|
|
23
|
+
`from == SMA`** (cannot pull from third parties that approved the Safe). Max 50 entries per
|
|
24
|
+
allowlist.
|
|
25
|
+
|
|
26
|
+
## Config blob (authoritative — `config-schemas.md`)
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
abi.encode(address[] allowedRecipients, address[] allowedTokens, uint256 maxAmountPerTx)
|
|
30
|
+
```
|
|
31
|
+
| Field | Notes |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `allowedRecipients` | addresses the agent may send to (≤ 50) |
|
|
34
|
+
| `allowedTokens` | ERC-20 tokens the agent may move (≤ 50) |
|
|
35
|
+
| `maxAmountPerTx` | per-transfer cap, base units |
|
|
36
|
+
|
|
37
|
+
> Recipient/token allowlists are mutable via `reconfigure` (permissionSigner). In production
|
|
38
|
+
> use a multisig/timelock as the signer — it can widen the allowlist. Vet recipient contracts.
|
|
39
|
+
|
|
40
|
+
## Steps
|
|
41
|
+
|
|
42
|
+
Register → configure → simulate → reconfigure mechanics (and the encoding gotcha) live in
|
|
43
|
+
[`sail-templates` reuse-flow](../sail-templates/references/reuse-flow.md) — follow it.
|
|
44
|
+
`sailor mandate attach` registers only; `configureDirect` (owner tx) is the half that makes the
|
|
45
|
+
permission live. Template-specific bits:
|
|
46
|
+
|
|
47
|
+
- **Singleton:** `TransferPermission` — `node SKILLS/sail-templates/catalog.mjs --chain <id>`.
|
|
48
|
+
- **Spec to confirm:** recipients, tokens, cap.
|
|
49
|
+
- **Blob:** `abi.encode(allowedRecipients[], allowedTokens[], maxAmountPerTx)` — **flat params, no
|
|
50
|
+
wrapper**.
|
|
51
|
+
- **Simulate (mandatory — unaudited example):** allowed recipient/token within cap passes; off-list
|
|
52
|
+
recipient, wrong token, over-cap, or `transferFrom` with `from != SMA` is rejected.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sail-template-withdraw
|
|
3
|
+
description: Gate an SMA's ERC-20 withdrawals by REUSING the shared WithdrawPermission singleton (Protocol/contracts/templates/WithdrawPermission.sol) — register + configure, no per-SMA deploy. Use when the agent may move approved tokens, within a per-tx cap, only to ONE fixed recipient (typically the owner's Safe) for safe-to-safe consolidation. For a mutable multi-recipient allowlist, use sail-template-transfer instead. NOTE: `sailor mandate attach` only registers — you must also configure per-account (see steps).
|
|
4
|
+
compatibility: A Sailor project (`@sail/sdk`, `sailor` CLI). Requires WithdrawPermission 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/WithdrawPermission.sol
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# sail-template-withdraw — bounded withdraw to a fixed recipient via the shared singleton
|
|
13
|
+
|
|
14
|
+
Reuse the shared **`WithdrawPermission`** singleton. Family overview + flow:
|
|
15
|
+
[`sail-templates`](../sail-templates/SKILL.md).
|
|
16
|
+
|
|
17
|
+
## What it enforces (per account, from source)
|
|
18
|
+
|
|
19
|
+
Selectors: `0xa9059cbb` `transfer`, `0x23b872dd` `transferFrom`. Invariants: `value == 0`;
|
|
20
|
+
`tokens` must be non-empty and `allowedRecipient` non-zero, with no zero-address tokens
|
|
21
|
+
(`EmptyAllowlist`/`ZeroAddress` revert at configure otherwise); `target (token) ∈ tokens`;
|
|
22
|
+
`to == allowedRecipient` (a single address from config); `amount ≤ maxAmountPerTx`;
|
|
23
|
+
**`transferFrom` requires `from == SMA`**.
|
|
24
|
+
|
|
25
|
+
> Single recipient per config. To change it, `reconfigure` with a new blob (no redeploy). This
|
|
26
|
+
> is the consolidation primitive — funds can only flow to the one approved address.
|
|
27
|
+
|
|
28
|
+
## Config blob (authoritative — `config-schemas.md`)
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
abi.encode(address[] tokens, address allowedRecipient, uint256 maxAmountPerTx)
|
|
32
|
+
```
|
|
33
|
+
| Field | Notes |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `tokens` | ERC-20 tokens the agent may move |
|
|
36
|
+
| `allowedRecipient` | the single destination (e.g. owner's Safe) |
|
|
37
|
+
| `maxAmountPerTx` | per-withdraw cap, base units |
|
|
38
|
+
|
|
39
|
+
## Steps
|
|
40
|
+
|
|
41
|
+
Register → configure → simulate → reconfigure mechanics (and the encoding gotcha) live in
|
|
42
|
+
[`sail-templates` reuse-flow](../sail-templates/references/reuse-flow.md) — follow it.
|
|
43
|
+
`sailor mandate attach` registers only; `configureDirect` (owner tx) is the half that makes the
|
|
44
|
+
permission live. Template-specific bits:
|
|
45
|
+
|
|
46
|
+
- **Singleton:** `WithdrawPermission` — `node SKILLS/sail-templates/catalog.mjs --chain <id>`.
|
|
47
|
+
- **Spec to confirm:** tokens, the single recipient, cap.
|
|
48
|
+
- **Blob:** `abi.encode(tokens[], allowedRecipient, maxAmountPerTx)` — **flat params, no wrapper**.
|
|
49
|
+
- **Simulate (mandatory — unaudited example):** withdraw to the recipient within cap passes; any
|
|
50
|
+
other `to`, wrong token, over-cap, or `transferFrom` with `from != SMA` is rejected.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sail-templates
|
|
3
|
+
description: Registry + reuse guide for Sail's shared permission templates (Protocol/contracts/templates). Load this when you need to know which permission primitives are available as reusable templates and want to gate an SMA's mandate by REUSING a configurable singleton — deploy once, then register + configure per SMA (no per-SMA deploy). Covers swap (oracle-gated and no-oracle), borrow, transfer, deposit, withdraw, and approve-and-call-batch. All seven are deployed today, at the SAME address on every supported chain (CREATE2); addresses live in deployed.json.
|
|
4
|
+
compatibility: Node 18+; a Sailor project (`@sail/sdk`, `sailor` CLI); read access to the workspace `Protocol/contracts/templates/` (or set SAIL_PROTOCOL_DIR).
|
|
5
|
+
metadata:
|
|
6
|
+
workspace: sailor-harness
|
|
7
|
+
classification: generic
|
|
8
|
+
status: draft
|
|
9
|
+
origin: Protocol/contracts/templates source contracts
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Sail shared permission templates — registry & reuse
|
|
13
|
+
|
|
14
|
+
`Protocol/contracts/templates/` holds seven **shared permission templates** — reusable
|
|
15
|
+
`IPermission` patterns covering the most common DeFi primitives. Each extends
|
|
16
|
+
`ConfigurablePermission`, which means it is a **configurable singleton**:
|
|
17
|
+
|
|
18
|
+
> Deploy the contract **once per chain**. Every SMA then reuses that single address via
|
|
19
|
+
> **register + configure** — its own routers / tokens / caps live in the singleton's
|
|
20
|
+
> `mapping(address => …)`. **No per-SMA deploy, no per-SMA audit, minimal gas.**
|
|
21
|
+
|
|
22
|
+
> **Deployment status (2026-07-01, by `0xB01dCE…815B6`):** **all seven** templates are live on
|
|
23
|
+
> every chain Sailor bundles — Ethereum, Base, Arbitrum, Optimism, Unichain, BSC, World Chain,
|
|
24
|
+
> HyperEVM, MegaETH, Base Sepolia, and Eth Sepolia (11 chains) — all bound to the current
|
|
25
|
+
> CREATE2 kernel `0x38b508756c976e876EFF05a29E731A4d348BA6ED`. This is a CREATE2 deploy with a
|
|
26
|
+
> global salt, so the kernel and every template address are **identical on every chain**. Live
|
|
27
|
+
> addresses are in [`deployed.json`](deployed.json) (keyed by chainId → contract name, though
|
|
28
|
+
> the value is the same for all 11 keys). This supersedes the prior 2026-06-09 deploy (kernel
|
|
29
|
+
> `0x02ABC18B65A328de2e749F56ba79ACF2718a6659`) — those addresses are dead.
|
|
30
|
+
|
|
31
|
+
> ⚠️ **Audit scope.** These are marked *"UNAUDITED EXAMPLE — NOT PART OF THE TRUSTED CORE"* in
|
|
32
|
+
> source. The kernel runs them safely (staticcall + gas cap + fail-closed) but does not verify
|
|
33
|
+
> their internal logic. Always `sailor mandate simulate` against your SMA before authorizing.
|
|
34
|
+
|
|
35
|
+
> ⚠️ **CRITICAL — "register" ≠ "configure" in the shipped CLI.** A shared template is useless
|
|
36
|
+
> until it is both **registered** on the kernel AND **configured** per-account. `sailor mandate
|
|
37
|
+
> attach` does **only the register** half (it submits `RegisterPermission` and nothing more). A
|
|
38
|
+
> registered-but-unconfigured singleton has `isConfigured == false` and the kernel **denies every
|
|
39
|
+
> call**. Run `sailor mandate configure` as the separate second step (see the reuse flow below).
|
|
40
|
+
|
|
41
|
+
## The seven templates
|
|
42
|
+
|
|
43
|
+
| Primitive | Contract | Deployed? | Skill |
|
|
44
|
+
|---|---|---|---|
|
|
45
|
+
| DEX swap (oracle band) | `SwapPermission` | ✅ 11 chains | [`sail-template-swap`](../sail-template-swap/SKILL.md) |
|
|
46
|
+
| DEX swap (no oracle) | `SwapPermissionNoOracle` | ✅ 11 chains | [`sail-template-swap-no-oracle`](../sail-template-swap-no-oracle/SKILL.md) |
|
|
47
|
+
| Lending borrow | `BorrowPermission` | ✅ 11 chains | [`sail-template-borrow`](../sail-template-borrow/SKILL.md) |
|
|
48
|
+
| Transfer (allowlist) | `TransferPermission` | ✅ 11 chains | [`sail-template-transfer`](../sail-template-transfer/SKILL.md) |
|
|
49
|
+
| Vault/lending deposit | `DepositPermission` | ✅ 11 chains | [`sail-template-deposit`](../sail-template-deposit/SKILL.md) |
|
|
50
|
+
| Withdraw to fixed addr | `WithdrawPermission` | ✅ 11 chains | [`sail-template-withdraw`](../sail-template-withdraw/SKILL.md) |
|
|
51
|
+
| Approve+call batch | `ApproveAndCallBatchPermission` | ✅ 11 chains | [`sail-template-approve-batch`](../sail-template-approve-batch/SKILL.md) |
|
|
52
|
+
|
|
53
|
+
Authoritative config tuples + enforced invariants (from source):
|
|
54
|
+
[references/config-schemas.md](references/config-schemas.md).
|
|
55
|
+
|
|
56
|
+
### One source of truth per concern
|
|
57
|
+
|
|
58
|
+
This hub is canonical for everything shared; each spoke skill carries only what's unique to its
|
|
59
|
+
primitive (selectors, invariants, config blob, probe cases) and defers the rest here. When
|
|
60
|
+
something changes, edit it in ONE place:
|
|
61
|
+
|
|
62
|
+
| Concern | Lives in | Spokes do |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| Which templates exist | source contracts, auto-detected by [`catalog.mjs`](catalog.mjs) | — |
|
|
65
|
+
| Deployed addresses (per chain) | [`deployed.json`](deployed.json) | link here |
|
|
66
|
+
| Register → configure → simulate flow | [references/reuse-flow.md](references/reuse-flow.md) | link here, don't restate |
|
|
67
|
+
| Config tuples + invariants | [references/config-schemas.md](references/config-schemas.md) | quote only their own tuple |
|
|
68
|
+
|
|
69
|
+
So a change to the CLI flow is a single edit to
|
|
70
|
+
`reuse-flow.md`, not seven spoke edits. The deliberately-fuller [`sail-template-swap`](../sail-template-swap/SKILL.md)
|
|
71
|
+
is the worked-example exemplar; the other spokes stay thin.
|
|
72
|
+
|
|
73
|
+
## Step 1 — see what exists / deployment status
|
|
74
|
+
|
|
75
|
+
The list is auto-detected from source; deployment status comes from `deployed.json`:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
node SKILLS/sail-templates/catalog.mjs # all templates
|
|
79
|
+
node SKILLS/sail-templates/catalog.mjs --chain 8453 # status on one chain
|
|
80
|
+
node SKILLS/sail-templates/catalog.mjs --json # machine-readable
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
(The catalog warns if a new source contract appears with no curated metadata — a signal to
|
|
84
|
+
add it here and write a skill.)
|
|
85
|
+
|
|
86
|
+
## Step 2 — the reuse flow (per template)
|
|
87
|
+
|
|
88
|
+
> The on-chain **intended** design is one signed call (`MandateFactory.attach`) that registers
|
|
89
|
+
> the address on the kernel and writes the per-account config together. The **shipped** CLI
|
|
90
|
+
> does not implement that combined call yet, so today these are two separate steps:
|
|
91
|
+
> `sailor mandate attach` (register) followed by `sailor mandate configure` (configure). The
|
|
92
|
+
> flow below describes what actually works now.
|
|
93
|
+
|
|
94
|
+
1. **Deploy once** (one-time, Protocol team / `DeploySharedTemplates`, CREATE2 — same address on
|
|
95
|
+
every chain) → record the address in `deployed.json`. (Already done for all seven.)
|
|
96
|
+
2. **Build the config blob** for your SMA (per-primitive skill gives the exact params).
|
|
97
|
+
3. **Register** the singleton address on the SMA's kernel (this does NOT configure it):
|
|
98
|
+
```bash
|
|
99
|
+
sailor mandate attach --address <DEPLOYED_ADDRESS> --sma <SMA> --label "<primitive>"
|
|
100
|
+
```
|
|
101
|
+
A comma-separated `--address` list registers several in one signature (`attachBatch`).
|
|
102
|
+
4. **Configure** the per-account bounds — this is the step that makes the permission actually
|
|
103
|
+
allow calls:
|
|
104
|
+
```bash
|
|
105
|
+
sailor mandate configure --address <DEPLOYED_ADDRESS> --sma <SMA> \
|
|
106
|
+
--template <TemplateName> --args-file ./config.json
|
|
107
|
+
```
|
|
108
|
+
This drives `configureDirect(account, <config blob>)` as an owner transaction (the owner is
|
|
109
|
+
the `permissionSigner`), pre-flighted with an `eth_call` before any signing or gas. See
|
|
110
|
+
[references/reuse-flow.md](references/reuse-flow.md) for the exact encoding gotcha and the
|
|
111
|
+
signing-station path.
|
|
112
|
+
5. **Simulate** off-chain (no gas) — prove allowed calls pass and bad ones fail:
|
|
113
|
+
```bash
|
|
114
|
+
sailor mandate simulate --address <DEPLOYED_ADDRESS> --sma <SMA> --calls ./probe.json
|
|
115
|
+
```
|
|
116
|
+
6. **Reconfigure** later (new caps / allowlists) via `configure`/`configureDirect` on the same
|
|
117
|
+
singleton — same address, no re-register. (On-chain `MandateFactory.reconfigure` is the
|
|
118
|
+
intended batch path for this.)
|
|
119
|
+
|
|
120
|
+
Full mechanics (EIP-712 sigs, `configure`/`configureDirect`, `attachBatch`, `replace`,
|
|
121
|
+
`detach`, and the register-vs-configure split): [references/reuse-flow.md](references/reuse-flow.md).
|
|
122
|
+
|
|
123
|
+
## When NOT to use these
|
|
124
|
+
|
|
125
|
+
No shared template covers: **perps** (GMX, Gains, Synthetix), **prediction markets** (Azuro,
|
|
126
|
+
Limitless), or the **LI.FI aggregator**. For those, author a bespoke `IPermission` via
|
|
127
|
+
`sailor mandate deploy` — see [`sail-mandates`](../sail-mandates/SKILL.md) for the
|
|
128
|
+
authoring flow and `examples/permissions/` for worked per-protocol patterns.
|
|
129
|
+
|
|
130
|
+
## Notes
|
|
131
|
+
|
|
132
|
+
- The kernel and every template address are identical on every chain (CREATE2 global salt) —
|
|
133
|
+
but that pairing is generation-specific: a template address from one deploy generation (e.g.
|
|
134
|
+
the superseded 2026-06-09 deploy) is never valid against a different generation's kernel.
|
|
135
|
+
Always resolve both from the same `deployed.json` entry / SDK deployment.
|
|
136
|
+
- Caps/amounts are in **base units**. Size them with `sail-pyth-prices` / `uniswap-v3-quote`.
|
|
137
|
+
- The config encoder must match each contract's `_applyConfig` decode exactly — use the SDK
|
|
138
|
+
builder under `@sail/sdk/templates` **only after** verifying its param tuple equals the
|
|
139
|
+
source blob in `config-schemas.md` (the SDK builders track a previously-deployed set and may
|
|
140
|
+
differ from these source contracts).
|
|
141
|
+
- **CLI gap:** `sailor mandate attach` (register) and `sailor mandate configure` (configure) are
|
|
142
|
+
still separate commands — no combined one-step call ships yet. Always run both.
|