@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,139 @@
|
|
|
1
|
+
# Config schemas & enforced invariants — authoritative (from source)
|
|
2
|
+
|
|
3
|
+
Each shared template stores per-account config under `mapping(address => …)` and decodes a
|
|
4
|
+
`configure(...)` blob in `_applyConfig`. The tuples below are taken **directly from
|
|
5
|
+
`Protocol/contracts/templates/*.sol`** and are the source of truth — encode with
|
|
6
|
+
`abi.encode(...)` in that exact order. (The `@sail/sdk/templates` builders track a
|
|
7
|
+
previously-deployed set; verify a builder's params match the tuple here before using it.)
|
|
8
|
+
|
|
9
|
+
All caps/amounts are in the relevant token's **base units**. Every template runs under the
|
|
10
|
+
kernel's `staticcall` + gas cap + fail-closed semantics: a revert ⇒ deny.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## SwapPermission
|
|
15
|
+
```
|
|
16
|
+
abi.encode(address[] routers, address[] tokensIn, address[] tokensOut,
|
|
17
|
+
uint256 maxAmountPerTx, uint256 maxSlippageBps,
|
|
18
|
+
address priceOracle, uint256 maxPriceAgeSec)
|
|
19
|
+
```
|
|
20
|
+
Selectors: `0x414bf389` (V3 `exactInputSingle` w/ deadline), `0x04e45aaf` (V3-02 no deadline),
|
|
21
|
+
`0x38ed1739` (V2 `swapExactTokensForTokens`). Invariants: `value == 0` (native value rejected);
|
|
22
|
+
`target ∈ routers`; **`tokenIn != tokenOut` / `path[0] != path[last]`** (self-routes denied — a
|
|
23
|
+
round-trip would burn AMM fees while an oracle reporting base==quote clears the band);
|
|
24
|
+
`tokenIn`/`path[0] ∈ tokensIn`; `tokenOut`/`path[last] ∈ tokensOut`;
|
|
25
|
+
`recipient`/`to == account`; `amountIn ≤ maxAmountPerTx`; **oracle slippage band ALWAYS enforced**.
|
|
26
|
+
The oracle is **mandatory** (contract `v2`): `_applyConfig` reverts `OracleRequired` if
|
|
27
|
+
`priceOracle == 0`, `MissingPriceAge` if `maxPriceAgeSec == 0`, and `SlippageBpsTooLarge` if
|
|
28
|
+
`maxSlippageBps > 9_999`. `maxSlippageBps == 0` ⇒ zero tolerance (strictest), NOT a bypass.
|
|
29
|
+
`priceOracle` is an `IOracle` adapter (`getPrice(base,quote) → (price, dec, updatedAt)`), not a raw
|
|
30
|
+
feed; dust trades whose floor truncates to 0 are denied. V2 intermediate hops are NOT checked.
|
|
31
|
+
(For no-oracle tokens use `SwapPermissionNoOracle`.)
|
|
32
|
+
|
|
33
|
+
## SwapPermissionNoOracle
|
|
34
|
+
```
|
|
35
|
+
abi.encode(address[] routers, address[] tokensIn, address[] tokensOut,
|
|
36
|
+
uint256 maxAmountPerTx, ReferencePool[] referencePools)
|
|
37
|
+
|
|
38
|
+
struct ReferencePool { address tokenIn; address tokenOut; address pool;
|
|
39
|
+
PoolKind kind /* 0=V2, 1=V3 */; uint256 toleranceBps; }
|
|
40
|
+
```
|
|
41
|
+
Same selectors and structural checks as `SwapPermission` (`0x414bf389`, `0x04e45aaf`,
|
|
42
|
+
`0x38ed1739`): `target ∈ routers`; `tokenIn`/`path[0] ∈ tokensIn`; `tokenOut`/`path[last] ∈
|
|
43
|
+
tokensOut`; `recipient`/`to == account`; `amountIn ≤ maxAmountPerTx`. **Differs** from
|
|
44
|
+
`SwapPermission` in the price judgement: instead of an oracle band it enforces a **pool-referenced
|
|
45
|
+
hallucination band** — `amountOutMin` must be ≥ the output implied by the live spot price of the
|
|
46
|
+
operator-named `referencePool` for that directional pair, minus `toleranceBps`, and `amountOutMin
|
|
47
|
+
> 0`. Config-time invariants (`_applyConfig` reverts ⇒ configure fails): `toleranceBps ≤ 5_000`
|
|
48
|
+
(50% max); `pool != 0`; pool's `token0()`/`token1()` must match the pair (orientation precomputed);
|
|
49
|
+
**strict coverage** — every non-self directional `(tokenIn, tokenOut)` combination MUST have a
|
|
50
|
+
reference pool or configure reverts `MissingReferencePool`. Fail-closed at evaluate if the pool is
|
|
51
|
+
missing/unreadable/illiquid or the floor truncates to zero.
|
|
52
|
+
|
|
53
|
+
> ⚠️ **NOT manipulation-resistant.** The reference is a single pool's LIVE spot price, movable
|
|
54
|
+
> within the same transaction (flash-loan / sandwich). This band only catches an *honest* mistake
|
|
55
|
+
> (a confused agent quoting a wildly wrong price); it is NOT slippage protection. For
|
|
56
|
+
> manipulation-resistant protection use the oracle-gated `SwapPermission`. Cap is per-tx, not
|
|
57
|
+
> cumulative.
|
|
58
|
+
|
|
59
|
+
## BorrowPermission
|
|
60
|
+
```
|
|
61
|
+
abi.encode(address[] protocols, address[] assets, uint256 maxAmountPerTx,
|
|
62
|
+
uint256 maxLtvBps, address collateralOracle, address borrowOracle,
|
|
63
|
+
uint256 maxPriceAgeSec)
|
|
64
|
+
```
|
|
65
|
+
Selectors: Aave `borrow(address,uint256,uint256,uint16,address)` (variable-rate only —
|
|
66
|
+
`rateMode != 2` denies; stable-rate debt is rejected), Morpho **Optimizer/Morpho-Aave**
|
|
67
|
+
`borrow(address,uint256,address,address)` (NOT Morpho Blue — its ABI differs and simply won't
|
|
68
|
+
match, i.e. fails closed), Compound `borrow(uint256)` (target is the cToken; the underlying is
|
|
69
|
+
resolved via `cToken.underlying()` and both the allowlist and LTV are underlying-denominated —
|
|
70
|
+
targets with no `underlying()`, e.g. cETH, are denied). Invariants: `protocols`/`assets` must be
|
|
71
|
+
non-empty with no zero addresses (`EmptyAllowlist`/`ZeroAddress` revert otherwise);
|
|
72
|
+
`target ∈ protocols`; `asset ∈ assets`; `amount ≤ maxAmountPerTx`; `onBehalfOf`/`receiver ==
|
|
73
|
+
account`. **Oracle modes:** zero oracles ⇒ amount-cap-only (no LTV ceiling applied at all, despite
|
|
74
|
+
`maxLtvBps` being stored); both oracles set ⇒ `_ltvCheck` enforces the LTV bound. Exactly one
|
|
75
|
+
oracle set reverts `OracleConfigInconsistent` at configure — it's an all-or-nothing pair.
|
|
76
|
+
|
|
77
|
+
## TransferPermission
|
|
78
|
+
```
|
|
79
|
+
abi.encode(address[] allowedRecipients, address[] allowedTokens, uint256 maxAmountPerTx)
|
|
80
|
+
```
|
|
81
|
+
Selectors: `0xa9059cbb` `transfer`, `0x23b872dd` `transferFrom`. Invariants: `value == 0`;
|
|
82
|
+
`allowedRecipients`/`allowedTokens` must be non-empty with no zero addresses
|
|
83
|
+
(`EmptyAllowlist`/`ZeroAddress` revert at configure otherwise); `target (token) ∈ allowedTokens`;
|
|
84
|
+
`to ∈ allowedRecipients`; `amount ≤ maxAmountPerTx`;
|
|
85
|
+
**`transferFrom` requires `from == account`** (stricter than the old TransferTarget).
|
|
86
|
+
Max 50 entries per allowlist.
|
|
87
|
+
|
|
88
|
+
## DepositPermission
|
|
89
|
+
```
|
|
90
|
+
abi.encode(address[] targets, address[] tokens, uint256 maxAmountPerTx)
|
|
91
|
+
```
|
|
92
|
+
Selectors: `deposit(uint256,address)` (ERC-4626), `mint(uint256,address)`,
|
|
93
|
+
`deposit(address,uint256,address,uint16)` (Aave v2), `supply(address,uint256,address,uint16)`
|
|
94
|
+
(Aave v3). Invariants: `value == 0`; `targets`/`tokens` must be non-empty with no zero addresses
|
|
95
|
+
(`EmptyAllowlist`/`ZeroAddress` revert at configure otherwise); `target ∈ targets`; token
|
|
96
|
+
allowlist enforced (Aave: the `asset` arg; ERC-4626: `target` itself must be in `tokens`);
|
|
97
|
+
`amount`/`shares ≤ maxAmountPerTx`; `receiver`/`onBehalfOf == account`. `mint` cap is in
|
|
98
|
+
**shares** — account for share price.
|
|
99
|
+
|
|
100
|
+
## WithdrawPermission
|
|
101
|
+
```
|
|
102
|
+
abi.encode(address[] tokens, address allowedRecipient, uint256 maxAmountPerTx)
|
|
103
|
+
```
|
|
104
|
+
Selectors: `0xa9059cbb` `transfer`, `0x23b872dd` `transferFrom`. Invariants: `value == 0`;
|
|
105
|
+
`tokens` must be non-empty and `allowedRecipient` non-zero, with no zero-address tokens
|
|
106
|
+
(`EmptyAllowlist`/`ZeroAddress` revert at configure otherwise); `target (token) ∈ tokens`;
|
|
107
|
+
`to == allowedRecipient` (single address per config); `amount ≤ maxAmountPerTx`;
|
|
108
|
+
**`transferFrom` requires `from == account`**. To change the recipient, `reconfigure` with a new
|
|
109
|
+
blob.
|
|
110
|
+
|
|
111
|
+
## ApproveAndCallBatchPermission
|
|
112
|
+
```
|
|
113
|
+
abi.encode(Config{
|
|
114
|
+
address[] tokens; address[] spenders; ConsumingPair[] consumingPairs;
|
|
115
|
+
uint256[] maxApprovalAmounts; bool requireAmountMatch; bool allowUnconstrainedRecipient;
|
|
116
|
+
})
|
|
117
|
+
struct ConsumingPair { address target; bytes4 selector; }
|
|
118
|
+
```
|
|
119
|
+
ABI tuple: `(address[],address[],(address,bytes4)[],uint256[],bool,bool)`. `consumingPairs` is a
|
|
120
|
+
**struct array**, not two flat arrays — each `(target, selector)` is bound together, so a selector
|
|
121
|
+
is authorised only on its paired target. Authorises exactly the 3-call batch:
|
|
122
|
+
`approve(spender, amount)` → consuming call → `approve(spender, 0)`. Invariants: `tokens`,
|
|
123
|
+
`spenders`, and `consumingPairs` must each be non-empty and contain no zero addresses/selectors
|
|
124
|
+
(`EmptyAllowlist` reverts otherwise); token ∈ `tokens` with `amount ≤ maxApprovalAmount[token]`;
|
|
125
|
+
`spender ∈ spenders`; `(target, selector)` ∈ `consumingPairs`; the pre-batch allowance on
|
|
126
|
+
`(token, spender)` must be zero; the consuming call must target the approved `spender` and pull the
|
|
127
|
+
approved `token`; allowance reset to zero in the same batch; if `requireAmountMatch`, the consuming
|
|
128
|
+
call's leading `uint256` arg must equal the approve amount. `maxApprovalAmounts` is index-parallel
|
|
129
|
+
with `tokens` (length mismatch reverts).
|
|
130
|
+
|
|
131
|
+
> ⚠️ **Field name/polarity — `allowUnconstrainedRecipient`, NOT `requireRecipientIsAccount`.**
|
|
132
|
+
> The field is an **opt-out**, default-safe: `false` (the default, i.e. an omitted/zero-value
|
|
133
|
+
> bool) means the consuming call's output recipient is decoded and **must equal the account** —
|
|
134
|
+
> the safe posture. `true` is a deliberate opt-out that leaves the recipient **unconstrained**.
|
|
135
|
+
> Setting the bool `true` thinking it "requires/pins the recipient" gets the **opposite**
|
|
136
|
+
> behaviour on-chain. Either way, the recipient (and the consumed asset) can only be decoded for
|
|
137
|
+
> selectors in the fixed-offset set below — any other selector is denied under the default pin:
|
|
138
|
+
> `swapExactTokensForTokens`, V3 `exactInputSingle` (both SwapRouter layouts), Aave V2/V3
|
|
139
|
+
> `deposit`/`supply`, ERC-4626 `deposit`/`mint`.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Reuse-first flow — register + configure a shared template
|
|
2
|
+
|
|
3
|
+
The whole point of a **shared** template: the logic is already deployed, so you skip the
|
|
4
|
+
deploy entirely. You give one SMA its own private config inside the singleton's
|
|
5
|
+
`mapping(address => …)` and register it on the kernel.
|
|
6
|
+
|
|
7
|
+
> **Two steps, not one (today).** The on-chain **intended** design is a single signed call —
|
|
8
|
+
> `MandateFactory.attach(account, template, params, configureDeadline, configureSig, kernelSig)`
|
|
9
|
+
> — that registers the template on the kernel AND writes its per-account config together.
|
|
10
|
+
> **The shipped `sailor` CLI does not implement that combined call yet.** `sailor mandate
|
|
11
|
+
> attach` performs **only the kernel registration** (`RegisterPermission` / the batch
|
|
12
|
+
> `registerPermissions`); it never calls `configure`/`configureDirect`, carries no `--params`,
|
|
13
|
+
> and builds no `Configure` EIP-712 signature. The SDK's `client.mandate.attach(...)` is a
|
|
14
|
+
> `notImplemented()` stub.
|
|
15
|
+
>
|
|
16
|
+
> A registered-but-unconfigured singleton has `isConfigured == false`, so the kernel's
|
|
17
|
+
> `evaluate()` **denies every call** — a registered-but-dead permission. You MUST configure
|
|
18
|
+
> separately, with `sailor mandate configure` (step 4 below). The flow below describes what
|
|
19
|
+
> actually works now.
|
|
20
|
+
|
|
21
|
+
## On-chain mechanics (`MandateFactory` + `ConfigurablePermission`)
|
|
22
|
+
|
|
23
|
+
These are the **on-chain** entry points — the contract truth for what the combined/intended
|
|
24
|
+
flow does. The shipped CLI uses a subset of them today (registration only); the configure
|
|
25
|
+
half is driven directly for now.
|
|
26
|
+
|
|
27
|
+
Registration (kernel side — what `sailor mandate attach` does today):
|
|
28
|
+
|
|
29
|
+
| Function | Use |
|
|
30
|
+
|---|---|
|
|
31
|
+
| kernel `registerPermission(account, permission, deadline, sig)` | Register one template address on an SMA |
|
|
32
|
+
| kernel `registerPermissions(account, permissions[], deadline, sig)` | Register several in one signature (the `--address a,b,c` batch path) |
|
|
33
|
+
| kernel `revokePermissions(account, permissions[], deadline, sig)` | Remove (`sailor mandate revoke`) |
|
|
34
|
+
|
|
35
|
+
Configuration (singleton side — the half the CLI does NOT do today):
|
|
36
|
+
|
|
37
|
+
| Function | Use |
|
|
38
|
+
|---|---|
|
|
39
|
+
| `configure(account, params, deadline, sig)` | Set per-account config, gated by an EIP-712 `Configure(account, paramsHash, nonce, deadline, epoch)` signature from the account's `permissionSigner` (ECDSA or ERC-1271). |
|
|
40
|
+
| `configureDirect(account, params)` | Set per-account config, gated by `msg.sender == permissionSigner`. The working path when the owner IS the permissionSigner — a plain owner tx, no signature. |
|
|
41
|
+
| `reconfigure(...)` | Intended batch re-config path (`MandateFactory.reconfigure`); functionally `configure` again with a new blob. |
|
|
42
|
+
|
|
43
|
+
The intended combined call (`MandateFactory.attach`) and its batch/replace/detach siblings are
|
|
44
|
+
defined on the factory contract; they are the target state for a future one-step CLI command,
|
|
45
|
+
not something the current CLI drives.
|
|
46
|
+
|
|
47
|
+
The kernel evaluates a registered permission via `staticcall` with a per-permission gas cap;
|
|
48
|
+
a revert or over-gas is treated as `false` (fail-closed), never a kernel revert.
|
|
49
|
+
|
|
50
|
+
## The steps (as they work today)
|
|
51
|
+
|
|
52
|
+
### 1. Discover the address
|
|
53
|
+
```bash
|
|
54
|
+
node SKILLS/sail-templates/catalog.mjs --chain <id>
|
|
55
|
+
```
|
|
56
|
+
The address comes from [`deployed.json`](../deployed.json). **If the template isn't deployed on
|
|
57
|
+
your chain yet** (e.g. `SwapPermissionNoOracle` is not deployed anywhere), that's the
|
|
58
|
+
prerequisite: deploy the singleton once (Protocol team / `DeploySharedTemplates`) and record
|
|
59
|
+
its address there. A template address is only meaningful paired with that chain's kernel.
|
|
60
|
+
|
|
61
|
+
### 2. Build the config blob
|
|
62
|
+
Encode the config tuple **exactly as the contract's `_applyConfig` decodes it** — the
|
|
63
|
+
authoritative tuples are in [config-schemas.md](config-schemas.md). Use `abi.encode(...)` in
|
|
64
|
+
that order, or the typed builder under `@sail/sdk/templates` **only after** verifying its param
|
|
65
|
+
tuple equals the source tuple (the SDK builders track a previously-deployed set and may differ
|
|
66
|
+
from these source contracts):
|
|
67
|
+
```ts
|
|
68
|
+
import { boundedSwapTemplate } from "@sail/sdk/templates"; // verify params vs config-schemas.md first
|
|
69
|
+
|
|
70
|
+
const params = { routers, tokensIn, tokensOut, maxAmountPerTx, maxSlippageBps,
|
|
71
|
+
priceOracle, maxPriceAgeSec };
|
|
72
|
+
const blob = boundedSwapTemplate.encoder.encode(params);
|
|
73
|
+
|
|
74
|
+
// Always surface the human-readable summary + warnings to the user first:
|
|
75
|
+
const explanation = boundedSwapTemplate.explainer.explain(params);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
> ⚠️ **Encoding gotcha (fails closed).** Templates do NOT all encode the same way.
|
|
79
|
+
> `TransferPermission` / `WithdrawPermission` / `DepositPermission` decode **flat top-level
|
|
80
|
+
> params** (`abi.encode(address[], address[], uint256)`). `ApproveAndCallBatchPermission`
|
|
81
|
+
> decodes a **single wrapped struct** (`abi.encode(Config{…})`) — its blob starts with a `0x20`
|
|
82
|
+
> tuple-offset word. Wrapping a flat-params blob (or vice-versa) reverts at configure. **Always
|
|
83
|
+
> round-trip-decode the blob and `cast call`-simulate `configureDirect` before sending.**
|
|
84
|
+
|
|
85
|
+
### 3. Register the singleton on the kernel (does NOT configure)
|
|
86
|
+
```bash
|
|
87
|
+
sailor mandate attach --address <SHARED_ADDRESS> --sma <SMA> --label "<primitive>"
|
|
88
|
+
```
|
|
89
|
+
This builds and submits the kernel `RegisterPermission` (or `registerPermissions` for a
|
|
90
|
+
comma-separated `--address` list). It does NOT call `configure`. After this step the address is
|
|
91
|
+
in `getPermissions(<SMA>)` but `isConfigured(<SMA>) == false`, so every dispatch is still
|
|
92
|
+
denied. Proceed to step 4.
|
|
93
|
+
|
|
94
|
+
### 4. Configure the per-account bounds (the half that makes it live)
|
|
95
|
+
When the owner IS the SMA's `permissionSigner` (the default Sailor onboarding), drive
|
|
96
|
+
`configureDirect(account, <config blob>)` as a plain owner transaction — `msg.sender ==
|
|
97
|
+
permissionSigner` holds and no EIP-712 signature is needed — via the shipped command:
|
|
98
|
+
```bash
|
|
99
|
+
sailor mandate configure --address <SHARED_ADDRESS> --sma <SMA> \
|
|
100
|
+
--template <TemplateName> --args-file ./config.json
|
|
101
|
+
# or with a pre-encoded blob:
|
|
102
|
+
sailor mandate configure --address <SHARED_ADDRESS> --sma <SMA> --params <0x-blob>
|
|
103
|
+
```
|
|
104
|
+
It does, in order:
|
|
105
|
+
1. **Pre-flight (no gas):** an `eth_call` simulation of `configureDirect` from `permissionSigner`. A revert here means the config is invalid before any gas is spent — fix the blob (see the encoding gotcha) and retry. Pass `--simulate-only` to stop here.
|
|
106
|
+
2. **Send it:** pushes the `configureDirect` call to the owner wallet as an `arbitrary-tx` request through the signing station; the owner approves in the browser and the owner wallet sends the transaction.
|
|
107
|
+
3. **Verify:** reads `isConfigured(<SMA>)` on the singleton and errors if it isn't `true`.
|
|
108
|
+
|
|
109
|
+
If the owner is **not** the `permissionSigner` (e.g. a separate mandate-signer / multisig), use
|
|
110
|
+
`configure(account, params, deadline, sig)` instead: construct the EIP-712 `Configure` digest
|
|
111
|
+
(bound to the kernel's current `registrationEpoch`), have the `permissionSigner` sign it, and
|
|
112
|
+
submit. `sailor mandate configure` does not build this signature for you — that path is still
|
|
113
|
+
manual.
|
|
114
|
+
|
|
115
|
+
### 5. Verify off-chain (no gas)
|
|
116
|
+
Prove it accepts what you want and rejects what you don't, before trusting it:
|
|
117
|
+
```bash
|
|
118
|
+
sailor mandate simulate --address <SHARED_ADDRESS> --sma <SMA> \
|
|
119
|
+
--target <router> --calldata <hex> --expect pass
|
|
120
|
+
# or a batch file:
|
|
121
|
+
sailor mandate simulate --address <SHARED_ADDRESS> --sma <SMA> --calls ./probe.json
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 6. Reconfigure when bounds change
|
|
125
|
+
New cap or allowlist? Re-encode the blob and repeat step 4 (`sailor mandate configure --force`,
|
|
126
|
+
or the manual `configure` signature path) — the address stays registered, no re-attach. (On-chain
|
|
127
|
+
`MandateFactory.reconfigure` is the intended batch path for this.)
|
|
128
|
+
|
|
129
|
+
## Gotchas
|
|
130
|
+
|
|
131
|
+
- **Unconfigured = deny.** A freshly-registered shared template with no `configure()` has
|
|
132
|
+
`isConfigured == false` and denies everything. Register AND configure — don't stop at
|
|
133
|
+
`sailor mandate attach`.
|
|
134
|
+
- **`sailor mandate attach` is register-only.** It does not configure. This is the single most
|
|
135
|
+
common trap; see steps 3 and 4.
|
|
136
|
+
- **Caps are in base units** of the relevant token (e.g. `25_000_000` = 25 USDC).
|
|
137
|
+
- **permissionSigner is the trust anchor** for config. In production use a multisig / timelock
|
|
138
|
+
— whoever holds it can widen allowlists and caps on every account they signed for.
|
|
139
|
+
- The shared singletons are **unaudited examples**. Simulate (Step 5) is not optional.
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sail-token-resolve
|
|
3
|
+
description: Resolve the tokens a user names by symbol or address into on-chain metadata (address, decimals) AND a cross-chain, cross-DEX liquidity map — which Sail chain and which protocol (Uniswap V3/V4, Sushiswap, PancakeSwap, Aerodrome…) actually hold liquidity, how deep each pool is, and whether the leg is swap-ready. Use BEFORE building any swap/DCA/LP/lending mandate, or whenever a user says "a portfolio of X and Y" / "DCA into A, B, C" / "can I swap X here?". Pass every symbol at once. Resolves ANY symbol — curated registry → keyless GeckoTerminal DEX index → on-chain symbol()+decimals() — and recommends which chain to act on. Runs the bundled `scripts/resolve-token.mjs` (no dependencies, no gas, no API key).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# sail-token-resolve — tokens → addresses + where the liquidity lives
|
|
7
|
+
|
|
8
|
+
Users name tokens by symbol ("WETH", "UNI", "HYPE", "MORPHO") far more often than by
|
|
9
|
+
address, and they think in *portfolios* ("a DCA of USDC, UNI and MORPHO"), not single
|
|
10
|
+
legs. This skill takes **one or many** symbols and, for each, returns: the verified
|
|
11
|
+
on-chain **address + decimals** per chain, a **liquidity venue map** (which chain, which
|
|
12
|
+
DEX/protocol, which pool, how deep), whether it is **swap-ready**, and a **recommendation**
|
|
13
|
+
when there's a discrepancy — liquidity split across chains, no pool on your chain but a deep
|
|
14
|
+
one elsewhere, or no pool on any Sail chain at all.
|
|
15
|
+
|
|
16
|
+
**"token exists" ≠ "token is swap-ready."** A token can have a valid contract with zero
|
|
17
|
+
routable liquidity. Swap-ready (on-chain confirmed) means Uniswap V3 QuoterV2 returns a
|
|
18
|
+
non-zero USDC→token quote — Sail's executable fast-path route.
|
|
19
|
+
|
|
20
|
+
## When to load
|
|
21
|
+
|
|
22
|
+
- The user names one or more tokens and you need addresses/decimals for a mandate.
|
|
23
|
+
- A user describes a **portfolio / DCA / basket** — resolve every symbol in one call.
|
|
24
|
+
- Before `sail-swap-quote` or `sail-template-swap` — both consume this skill's output.
|
|
25
|
+
- Whenever the user asks "can I swap X here?", "where's the best liquidity for X?", or
|
|
26
|
+
"which chain should I use for this strategy?"
|
|
27
|
+
|
|
28
|
+
## Run it
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# From the project root (reads .sail/.env.local for RPCs + chain):
|
|
32
|
+
node scripts/resolve-token.mjs WETH # single token, configured chain(s)
|
|
33
|
+
node scripts/resolve-token.mjs LINK --chain unichain # force one chain
|
|
34
|
+
node scripts/resolve-token.mjs 0x4200…0006 --chain base # address input
|
|
35
|
+
node scripts/resolve-token.mjs USDC UNI HYPE MORPHO # PORTFOLIO → rich JSON
|
|
36
|
+
node scripts/resolve-token.mjs UNI --all-chains --json # scan every Sail mainnet
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Flags: `--chain <ethereum|unichain|base|arbitrum>` forces one chain; `--rpc <url>` overrides
|
|
40
|
+
the endpoint; `--all-chains` maps every Sail mainnet (even ones without an RPC configured —
|
|
41
|
+
those use GeckoTerminal-only data) so you can recommend "put your SMA on chain Y"; `--json`
|
|
42
|
+
forces the rich per-token map for a single token.
|
|
43
|
+
|
|
44
|
+
## Output shapes (pick the right consumer)
|
|
45
|
+
|
|
46
|
+
| Invocation | stdout shape |
|
|
47
|
+
|---|---|
|
|
48
|
+
| 1 symbol, single configured chain / `--chain` | **bare object** — `{address, decimals, feeTier, swapReady, quote, venues[], …}` (the shape `sail-swap-quote` / `sail-template-swap` expect) |
|
|
49
|
+
| 1 symbol, ≥2 configured chains | **array** of bare objects (one per chain) |
|
|
50
|
+
| ≥2 symbols | **portfolio**: `{ tokens: [tokenWrapper…], summary }` |
|
|
51
|
+
| 1 symbol + `--json`/`--all-chains` | **token wrapper** (see below) |
|
|
52
|
+
|
|
53
|
+
A **token wrapper** is:
|
|
54
|
+
|
|
55
|
+
```jsonc
|
|
56
|
+
{
|
|
57
|
+
"query": "UNI",
|
|
58
|
+
"chains": { // keyed by chain name; each value is a bare per-chain object
|
|
59
|
+
"base": { "address": "0x…", "decimals": 18, "swapReady": true, "feeTier": 10000,
|
|
60
|
+
"venues": [ … ], "bestVenue": { … }, "onchainVerified": true, … },
|
|
61
|
+
"unichain": { … }
|
|
62
|
+
},
|
|
63
|
+
"chainsWithLiquidity": ["unichain","base","arbitrum"],
|
|
64
|
+
"onSailChain": true,
|
|
65
|
+
"crossChain": { "action": "route", "deepestChain": "base", "note": "…human guidance…" }
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
A **venue** (inside `venues[]`, sorted deepest-first, capped at 8; `venuesTotal` is the full
|
|
70
|
+
count) is:
|
|
71
|
+
|
|
72
|
+
```jsonc
|
|
73
|
+
{ "protocol": "uniswap-v3", // uniswap-v3 | uniswap-v4 | uniswap-v2 | sushiswap | pancakeswap | aerodrome | other
|
|
74
|
+
"dexId": "uniswap-v3-base", // raw GeckoTerminal id
|
|
75
|
+
"pool": "0x…", "feeTier": 500, // basis points (500 = 0.05%); null if the pool has no fee in its name
|
|
76
|
+
"pairedSymbol": "USDC", "pairedToken": "0x…",
|
|
77
|
+
"liquidityUsd": 8645941, "volume24hUsd": 22333927,
|
|
78
|
+
"sailRoutable": true, // Sail's fast path can route it (see below)
|
|
79
|
+
"quoteVerified": true } // a live on-chain QuoterV2 quote confirmed THIS venue
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## How it works
|
|
83
|
+
|
|
84
|
+
1. **Symbol → address** (two layers): curated registry (instant, offline) → GeckoTerminal
|
|
85
|
+
search (keyless CoinGecko DEX index) for anything not curated. Candidates are ranked by
|
|
86
|
+
pool liquidity; the on-chain `symbol()` check is the final authority (a wrong DEX-side
|
|
87
|
+
match is rejected, not trusted).
|
|
88
|
+
2. **On-chain verify** — `symbol()` + `decimals()` via eth_call on every chain that has an
|
|
89
|
+
RPC. This is the source of truth (`decimalsSource: "onchain"`). On an `--all-chains` scan
|
|
90
|
+
of a chain with no RPC, metadata falls back to GeckoTerminal (`decimalsSource:
|
|
91
|
+
"geckoterminal-unverified"`) — flag this to the user before wiring it into a mandate.
|
|
92
|
+
3. **Liquidity venue map** — `GET /networks/{net}/tokens/{addr}/pools` returns every pool
|
|
93
|
+
GeckoTerminal indexes, across **all DEXes** (Uniswap V3/V4, Sushiswap, PancakeSwap,
|
|
94
|
+
Aerodrome, …), with the protocol, pool address, fee tier and USD depth. One call per token
|
|
95
|
+
per chain.
|
|
96
|
+
4. **Swap-readiness (on-chain confirmed)** — quotes USDC→token across fee tiers 500/3000/10000
|
|
97
|
+
via Uniswap V3 QuoterV2 and marks the matching venue `quoteVerified: true`. This is the only
|
|
98
|
+
*executable* signal; everything else in `venues[]` is informational.
|
|
99
|
+
|
|
100
|
+
## `sailRoutable` — what Sail's fast path can actually swap
|
|
101
|
+
|
|
102
|
+
Sail's `sail-template-swap` fast path routes through **Uniswap V3** (everywhere) and the
|
|
103
|
+
**Uniswap V4** Universal Router (on Unichain). Those venues are marked `sailRoutable: true`.
|
|
104
|
+
Sushiswap, PancakeSwap, Aerodrome (and Uniswap V2) are detected and surfaced so you can see
|
|
105
|
+
*where the liquidity really is*, but they're `sailRoutable: false` — Sail can't route them via
|
|
106
|
+
the fast path. If the only liquidity is on a non-routable DEX, the token needs a custom mandate
|
|
107
|
+
(`sail-mandates`) or should be held. (Pools with absurd fees — >10% — are spam and are never
|
|
108
|
+
marked routable.)
|
|
109
|
+
|
|
110
|
+
## How to present results to the user
|
|
111
|
+
|
|
112
|
+
Read `crossChain.action` (per token) and the portfolio `summary`, then advise:
|
|
113
|
+
|
|
114
|
+
- **`route`** — swap-ready on a configured chain. If it's routable on **more than one**
|
|
115
|
+
configured chain, surface both with their depths and ask which to use (or pick by where the
|
|
116
|
+
rest of the basket lives). Hand the chosen chain's bare object to `sail-swap-quote`.
|
|
117
|
+
- **`suggest-sma`** — no routable pool on the configured chain(s), but a deep one on another
|
|
118
|
+
Sail chain. Tell the user and **recommend deploying an SMA on that chain** for this leg
|
|
119
|
+
(e.g. "MORPHO has no Uniswap pool on Base; the deep pool is Uniswap V3 on Unichain — consider
|
|
120
|
+
an SMA on Unichain"). Don't silently drop it.
|
|
121
|
+
- **`manual-address`** — liquidity exists but only on a DEX Sail can't fast-route (e.g. only on
|
|
122
|
+
Aerodrome). Offer a custom mandate via `sail-mandates`, or hold the leg.
|
|
123
|
+
- **`hold-skip`** — no pool on any scanned Sail chain (the token may live on a non-Sail chain,
|
|
124
|
+
e.g. a HyperEVM-only asset). Recommend holding/dropping it from the strategy.
|
|
125
|
+
|
|
126
|
+
Always show the resolved **address + decimals + the chain/protocol/depth** you're acting on, so
|
|
127
|
+
the user can sanity-check before anything is signed.
|
|
128
|
+
|
|
129
|
+
## Worked example — "create a DCA strategy of USDC, UNI, HYPE and MORPHO"
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
node scripts/resolve-token.mjs USDC UNI HYPE MORPHO --all-chains
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
A typical read of the result:
|
|
136
|
+
- **USDC** — the quote asset; swap-ready everywhere (`feeTier: null`, it *is* USDC).
|
|
137
|
+
- **UNI** — `route`; routable on Unichain/Base/Arbitrum. Pick by depth or by where the basket
|
|
138
|
+
concentrates.
|
|
139
|
+
- **HYPE** — `route` only on **Unichain** (a real ~$4M Uniswap V3 USDC pool); absent/illiquid
|
|
140
|
+
elsewhere. Tell the user HYPE is a Unichain-only leg here.
|
|
141
|
+
- **MORPHO** — `route` on **Base** (Uniswap V3 USDC pool); on other chains it exists but has no
|
|
142
|
+
routable USDC pool. If the project is configured for Base, good; if not, `suggest-sma`.
|
|
143
|
+
|
|
144
|
+
Then, for each `route`/chosen-chain leg, hand `(address, decimals, feeTier)` to
|
|
145
|
+
[`sail-swap-quote`](../sail-swap-quote/SKILL.md) → [`sail-template-swap`](../sail-template-swap/SKILL.md).
|
|
146
|
+
|
|
147
|
+
## Important
|
|
148
|
+
|
|
149
|
+
- **Decimals are critical** — 25 USDC = `25_000_000` (6 dec); 1 WETH =
|
|
150
|
+
`1_000_000_000_000_000_000` (18 dec). Every cap in a mandate is base units. Trust
|
|
151
|
+
`decimalsSource: "onchain"`; treat `geckoterminal-unverified` as provisional.
|
|
152
|
+
- **Addresses are per-chain** — WETH on Unichain ≠ WETH on Base ≠ WETH on Arbitrum. Resolve and
|
|
153
|
+
verify separately per chain; never copy an address across chains.
|
|
154
|
+
- **`getPool` is unreliable** on some forks — swap-readiness trusts a non-zero QuoterV2 quote,
|
|
155
|
+
never the V3 factory's `getPool`.
|
|
156
|
+
- **Symbol ambiguity / scam collisions.** When a symbol isn't curated, the deepest-pool
|
|
157
|
+
candidate whose on-chain `symbol()` matches is used. A scam token can share a real symbol; the
|
|
158
|
+
liquidity ranking favours the canonical one, but for an obscure symbol glance at the resolved
|
|
159
|
+
`address` and `source` before wiring it in — or pass the intended contract's `0x` address
|
|
160
|
+
directly (address-input bypasses the DEX lookup). Also sanity-check a venue's `volume24hUsd`
|
|
161
|
+
against its `liquidityUsd`: a huge pool with near-zero volume is inflated/non-trading, and a
|
|
162
|
+
big pool whose `pairedSymbol` is a look-alike (e.g. a "HYPE/BASEDHYPE" pool) is a *different*
|
|
163
|
+
asset — depth there does not make *your* token swap-ready.
|
|
164
|
+
- **`bestVenue` is USDC-relevant, not the biggest pool.** It's the deepest *Sail-routable,
|
|
165
|
+
USDC-paired* pool (the one a USDC DCA would route through), so it can be smaller than the
|
|
166
|
+
largest pool in `venues[]` (which may be a WETH or look-alike pair).
|
|
167
|
+
- **Missing venues ≠ no liquidity.** A chain can be `swapReady: true` (from the on-chain
|
|
168
|
+
QuoterV2 probe) yet show empty `venues[]` with `venuesError` set if GeckoTerminal rate-limited
|
|
169
|
+
that call. Trust `swapReady`; to repopulate the venue map for one chain, re-run
|
|
170
|
+
`node scripts/resolve-token.mjs <SYM> --chain <name> --json`.
|
|
171
|
+
- **Rate limits** — GeckoTerminal is keyless and rate-limited; the script throttles and caches
|
|
172
|
+
calls. A big `--all-chains` portfolio takes a moment. Set `GECKO_MIN_SPACING_MS` to tune the
|
|
173
|
+
spacing. A transient GeckoTerminal failure on one chain sets `venuesError` and leaves
|
|
174
|
+
`swapReady` intact (it's from the on-chain probe) — it never aborts the whole run.
|
|
@@ -18,3 +18,12 @@ CHAIN_ID=8453
|
|
|
18
18
|
|
|
19
19
|
# Optional: non-interactive passphrase (CI, GitHub Actions, launchd, systemd)
|
|
20
20
|
# SAIL_PASSPHRASE=change-me-to-a-strong-passphrase
|
|
21
|
+
|
|
22
|
+
# Advanced — exposing the dashboard/API beyond localhost.
|
|
23
|
+
# The UI server binds 127.0.0.1 (local only) by default. Its key-management
|
|
24
|
+
# endpoints are UNAUTHENTICATED, so only expose them behind your own auth
|
|
25
|
+
# (a reverse proxy with basic-auth, or a private tailnet — never the open internet).
|
|
26
|
+
# SAILOR_HOST=0.0.0.0 # bind all interfaces (LAN / behind a domain)
|
|
27
|
+
# SAILOR_TRUST_PROXY=1 # hop count (or IP allowlist) when behind a reverse proxy, so the rate limit keys on the real client IP (else it's a single global bucket)
|
|
28
|
+
# SAILOR_CORS_ORIGINS=https://sail.example.ts.net # extra allowed browser origins (comma-separated)
|
|
29
|
+
# SAILOR_RATE_LIMIT_PER_MIN=100 # throttle /api/signer + /api/onboard/generate-key (default 100; or set "rateLimitPerMin" in .sail/config.json)
|
|
@@ -1,26 +1,120 @@
|
|
|
1
|
-
This is
|
|
1
|
+
This guide is for agents operating a scaffolded Sailor project. (Contributors to the Sailor codebase: see AGENTS.md at the monorepo root.)
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
> **This is a standalone project, not a clone of the Sailor repo.** `sailor init` scaffolds an independent directory with its own (or no) git history — it does not share history with `github.com/sail-money/Sailor`. Do **not** add that repo as a remote or `git pull origin/main` from it: you'll hit "refusing to merge unrelated histories" and add/add conflicts on `AGENTS.md`, `README.md`, `package.json`, `.gitignore`, and `docs/`. To update the tooling, bump the `@sail.money/sailor` dependency (`npm i @sail.money/sailor@latest`), not by pulling the source repo.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Sail Protocol is infrastructure for onchain Separately Managed Accounts run by AI agents. You create an SMA, keep full custody, and define exactly what your agent can do — cryptographically bound permissions you approve and can always revoke. The agent executes within those bounds on every transaction. It cannot exceed them.
|
|
8
|
+
|
|
9
|
+
I'm Sailor, the operator that sets this up. I'll help you create your SMA, build the permissions that bound your agent, and get your strategy running.
|
|
10
|
+
|
|
11
|
+
Here's where we're headed:
|
|
12
|
+
|
|
13
|
+
1. Deploy your SMA and create your agent wallet
|
|
14
|
+
2. Define your strategy
|
|
15
|
+
3. Build, test, and sign your mandate
|
|
16
|
+
4. Run your agent — locally or on a schedule
|
|
17
|
+
5. Extend with notifications and a custom dashboard
|
|
18
|
+
|
|
19
|
+
Ready? Say **start** and I'll open the setup interface in your browser.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
# Instructions for the assistant
|
|
24
|
+
|
|
25
|
+
Everything below is for you, the assistant. The user sees the welcome above; you follow the flow below.
|
|
26
|
+
|
|
27
|
+
## Your job in mandate design
|
|
28
|
+
|
|
29
|
+
When designing a mandate, your job is to help the operator express the **tightest, most complete mandate that captures their strategy's intent** — not the smallest one that compiles. Enumerate every constraint the strategy implies *and* every one the protocol can express for the venues involved; explain what each protects against. Separate them into **safety bounds** (caps, allowlists, slippage floors — loss/theft surfaces, enforced on-chain by default) and **strategy parameters** (cadence, schedule, rebalance timing — how the strategy runs, not a safety surface, so they live in agent logic). A stated strategy parameter is still required: wire it as an agent-side guard and confirm it before go-live — don't try to force it on-chain. A minimal mandate that merely compiles is a failure mode; the goal is the tightest mandate that expresses the strategy.
|
|
30
|
+
|
|
31
|
+
## Voice
|
|
32
|
+
|
|
33
|
+
You are Sailor. Serious, precise, confident. No hype, no emojis, no exclamation marks. Explain *why*, not just *what* — the user is moving real funds. Use user-facing terms (SMA, mandate, permissions, agent wallet, owner). Assume crypto-native; teach the Sail-specific model.
|
|
34
|
+
|
|
35
|
+
Never overstate safety: custody is protected, but a mandate is only as correct as its permission contracts.
|
|
36
|
+
|
|
37
|
+
## Authorization rule
|
|
38
|
+
|
|
39
|
+
During **setup**, always ask before anything that costs gas. Once the **mandate is signed and the agent is running**, the mandate is the authorization — the agent transacts autonomously. Do not ask per-dispatch.
|
|
40
|
+
|
|
41
|
+
## First contact
|
|
42
|
+
|
|
43
|
+
When the user says start (or any first message), present the welcome above in full — definition, stage list, handoff line — before doing anything else. Do not launch the UI yet. After the user says start a second time (or confirms they are ready), THEN run `sailor ui start` and `sailor station start`. The welcome, the UI launch and the signing station launch are three separate beats separated by the user's go-ahead.
|
|
44
|
+
|
|
45
|
+
If the user's first message is an npm install command, run it, then present the welcome immediately after it completes — do not wait for another message.
|
|
46
|
+
|
|
47
|
+
## Stage flow — track to completion
|
|
48
|
+
|
|
49
|
+
The five stages above are a checklist you drive to completion, not a list you mention once. Track which stages are done (read `.sail/` to infer progress) and lead the operator to the next incomplete stage. The flow is not finished when the agent goes live — it is finished when stage 5 has been offered.
|
|
50
|
+
|
|
51
|
+
- [ ] 1. SMA + agent wallet deployed
|
|
52
|
+
- [ ] 2. Strategy defined
|
|
53
|
+
- [ ] 3. Mandate built, tested, signed
|
|
54
|
+
- [ ] 4. Agent running (locally or scheduled)
|
|
55
|
+
- [ ] 5. Extend — notifications and a custom dashboard
|
|
56
|
+
|
|
57
|
+
After the agent is live (stage 4), **offer stage 5 by default**: one line on run/transaction notifications and one line on a strategy-specific dashboard, then ask if the operator wants either. Skipping stage 5 requires an explicit operator opt-out — never drop it silently. Hand off to **sail-extend** to build whatever they accept.
|
|
58
|
+
|
|
59
|
+
## Project state — read `.sail/`, never ask
|
|
60
|
+
|
|
61
|
+
Determine the user's progress by reading `.sail/` — do not ask; read it.
|
|
62
|
+
|
|
63
|
+
| File | What it tells you |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `config.json` | Project manifest: name, `chainId` (null until the user picks a chain), contract addresses |
|
|
66
|
+
| `account.json` | Active SMA: safe, owner, manager (agent wallet), chainId, saltNonce, deployedChains |
|
|
67
|
+
| `mandate.json` | The signed mandate the runner executes against (absent = not signed yet) |
|
|
68
|
+
| `keys/` | Encrypted geth-v3 keystores (agent wallet, mandate signer) — never read or print contents |
|
|
69
|
+
| `state/mandates.json` | Append-only record of every permission deployed/attached from this project |
|
|
70
|
+
| `runtime/` | Live process state: `ui.json` (dashboard pid/port), `server.json` (signing station url/pid) |
|
|
71
|
+
| `activity.jsonl` | Unified activity log — agent dispatches and owner signing decisions, one JSON per line |
|
|
72
|
+
| `.env.local` | RPC_URL / CHAIN_ID / per-chain RPC vars / SAIL_PASSPHRASE — never commit or print |
|
|
6
73
|
|
|
7
74
|
## Skills
|
|
8
75
|
|
|
9
|
-
Detailed procedures live in skills
|
|
76
|
+
Detailed procedures live in skills. If your tooling does not auto-discover skills, open these files directly — they are plain markdown.
|
|
10
77
|
|
|
11
78
|
| Skill | Load when | Path |
|
|
12
79
|
|---|---|---|
|
|
13
|
-
| sail-
|
|
14
|
-
| sail-overview | How Sailor works, which skill to use when, and the safety invariants — read this first | `.agents/skills/sail-overview/SKILL.md` |
|
|
15
|
-
| sail-sailor | How to invoke sailor — local vs Docker, installMode detection, all CLI commands, `sailor update` | `.agents/skills/sail-sailor/SKILL.md` |
|
|
16
|
-
| sail-onboarding | New project setup, resuming a partially set-up project, or running a CLI command | `.agents/skills/sail-onboarding/SKILL.md` |
|
|
80
|
+
| sail-onboarding | New project setup, or resuming a partially set-up project, documentation of sailor commands | `.agents/skills/sail-onboarding/SKILL.md` |
|
|
17
81
|
| sail-project-info | Any question about project, account, mandate, chain, or environment state | `.agents/skills/sail-project-info/SKILL.md` |
|
|
18
82
|
| sail-servers | Starting, stopping, or health-checking the dashboard or signing station | `.agents/skills/sail-servers/SKILL.md` |
|
|
19
|
-
| sail-
|
|
20
|
-
| sail-
|
|
21
|
-
| sail-
|
|
83
|
+
| sail-token-resolve | Resolving a token symbol/address to its on-chain address + decimals, and checking whether a Uniswap V3 pool exists (swap-readiness) | `.agents/skills/sail-token-resolve/SKILL.md` |
|
|
84
|
+
| sail-swap-quote | Fetching a live Uniswap V3 quote and the slippage-adjusted amountOutMinimum floor | `.agents/skills/sail-swap-quote/SKILL.md` |
|
|
85
|
+
| sail-templates | Registry + reuse guide for Sail's shared permission singletons — which primitives exist as templates, per-chain deployment status (`deployed.json`), and the register→configure reuse flow. Start here before any template mandate | `.agents/skills/sail-templates/SKILL.md` |
|
|
86
|
+
| sail-template-swap | Bounded DEX swap / DCA mandate via the shared SwapPermission singleton (oracle-gated slippage band) — register + configure, no Solidity. The fast path for a swap/DCA strategy | `.agents/skills/sail-template-swap/SKILL.md` |
|
|
87
|
+
| sail-template-swap-no-oracle | Bounded swap for tokens with NO oracle via SwapPermissionNoOracle (single-pool hallucination guard, NOT manipulation-resistant) — reference-only, not yet deployed on any chain | `.agents/skills/sail-template-swap-no-oracle/SKILL.md` |
|
|
88
|
+
| sail-template-transfer | Bounded ERC-20 transfer to a mutable recipient allowlist via TransferPermission | `.agents/skills/sail-template-transfer/SKILL.md` |
|
|
89
|
+
| sail-template-withdraw | Bounded ERC-20 withdraw to ONE fixed recipient (owner-Safe consolidation) via WithdrawPermission | `.agents/skills/sail-template-withdraw/SKILL.md` |
|
|
90
|
+
| sail-template-deposit | Bounded deposit into ERC-4626 vaults / Aave v2–v3 via DepositPermission | `.agents/skills/sail-template-deposit/SKILL.md` |
|
|
91
|
+
| sail-template-borrow | Bounded lending borrow against Aave / Morpho / Compound with an on-chain LTV check via BorrowPermission | `.agents/skills/sail-template-borrow/SKILL.md` |
|
|
92
|
+
| sail-template-approve-batch | Atomic approve → call → reset (allowance bracket) in one batch via ApproveAndCallBatchPermission | `.agents/skills/sail-template-approve-batch/SKILL.md` |
|
|
93
|
+
| sail-transactions | Building dispatches or any EVM transaction for the agent | `.agents/skills/sail-transactions/SKILL.md` |
|
|
94
|
+
| sail-mandates | Designing, authoring, testing, deploying, or authorizing permission contracts (the bespoke-Solidity escape hatch) | `.agents/skills/sail-mandates/SKILL.md` |
|
|
95
|
+
| sail-automation | Automating the agent — GitHub Actions, self-hosted runner, Docker, or local daemon | `.agents/skills/sail-automation/SKILL.md` |
|
|
22
96
|
| sail-extend | Notifications or a custom dashboard, once the agent is live | `.agents/skills/sail-extend/SKILL.md` |
|
|
23
97
|
|
|
24
|
-
|
|
98
|
+
**Shared singletons are the default for the common DeFi primitives.** A bounded swap, transfer, withdraw, deposit, borrow, or approve-and-call mandate should first try to **reuse** the matching pre-deployed singleton — register + configure, no Solidity, no per-SMA deploy. Start at `sail-templates` (the registry: which primitives exist, per-chain deployment status in `.agents/skills/sail-templates/deployed.json`, and the register→configure flow), then use the matching per-primitive skill: `sail-template-swap`, `sail-template-swap-no-oracle`, `sail-template-transfer`, `sail-template-withdraw`, `sail-template-deposit`, `sail-template-borrow`, or `sail-template-approve-batch`. Six of seven singletons are live (Base, Arbitrum, Unichain, Sepolia, Base Sepolia); `SwapPermissionNoOracle` is source-only so far, and Ethereum mainnet is not yet deployed. Reach for `sail-mandates` (author + deploy your own `IPermission`) only when the strategy needs a venue, contract, or bound the singletons cannot express.
|
|
99
|
+
|
|
100
|
+
**Bundled scripts** under `scripts/` are dependency-free tools that make the fast path quick and deterministic — run them from the project root so they read `.sail/.env.local`:
|
|
101
|
+
- `resolve-token.mjs <SYMBOL|ADDRESS>` → on-chain address + decimals + swap-ready fee tier (QuoterV2 probe across 500/3000/10000).
|
|
102
|
+
- `quote-swap.mjs --token-in … --token-out … --amount <baseUnits> --fee <tier>` → live quote + `amountOutMinimum`.
|
|
103
|
+
- `shared-template-addr.mjs <TemplateName>` → the singleton's deployed address on the active chain.
|
|
104
|
+
|
|
105
|
+
## Invariants — apply to every turn
|
|
25
106
|
|
|
26
|
-
|
|
107
|
+
- Do not present the welcome and immediately launch the UI — wait for the second "start"
|
|
108
|
+
- Do not describe, mention, or present any code in `src/` or `examples/` as the user's strategy — treat strategy definition as a blank slate; ask what they want
|
|
109
|
+
- Do not ask a running agent to confirm individual dispatches within its mandate
|
|
110
|
+
- Do not put an owner key in the terminal — owner signing is browser-only
|
|
111
|
+
- Do not hand-roll dispatch EIP-712 signatures — use `buildDispatchSignature` from the SDK
|
|
112
|
+
- Do not hardcode the dispatch model — detect it on-chain with `detectKernelCapabilities`
|
|
113
|
+
- Do not present example permissions as audited or as a supported menu
|
|
114
|
+
- Do not commit `SAIL_PASSPHRASE` or private keys
|
|
115
|
+
- ERC-20 `approve()` calls are NOT covered by supply, swap, or deposit permissions — every approve the strategy makes needs explicit coverage. Two non-mixable models: per-call (separate single dispatches, one `IPermission` each — the default) or atomic batch (one `IBatchPermission` authorizing the whole `[approve, action]` sequence). A normal `IPermission` cannot authorize a batch. Details: `.agents/skills/sail-mandates/references/approvals.md`
|
|
116
|
+
- Never authorize (attach) a permission before `forge test` and `sailor mandate simulate` both pass against samples derived from the user's strategy
|
|
117
|
+
- **Register ≠ configure for shared singletons.** `sailor mandate attach` only registers the address on the kernel (`isConfigured` stays `false`); the kernel denies every call until you also run `sailor mandate configure`. Stopping at `attach` is the most common trap. See `sail-templates` (reuse-flow) and `sail-template-swap`.
|
|
118
|
+
- **Resolve tokens before binding them.** Never guess a token address or assume a token that exists is swap-ready. Run `scripts/resolve-token.mjs` for symbol→address+decimals+swap-readiness; a decimals mismatch (USDC 6 vs most 18) silently mis-sizes every cap, and a token with no V3 pool will fail-closed on every dispatch. Addresses are per-chain — never copy one across chains.
|
|
119
|
+
- Do not pass `--args` inline JSON from PowerShell — use `--args-file` instead
|
|
120
|
+
- Operator intent and the strategy's stated bounds outrank any example. If the operator asks for a bound an example omits, include it. Never let an example's shape narrow a mandate below what the operator requested
|
|
@@ -89,5 +89,5 @@ Run `sailor doctor` (read-only, no gas, no keys):
|
|
|
89
89
|
- on a conjunctive kernel, **probes each permission for pass-through** and flags any
|
|
90
90
|
that would brick dispatch.
|
|
91
91
|
|
|
92
|
-
See [
|
|
93
|
-
revert failure-mode catalog.
|
|
92
|
+
See [AGENTS.md](../AGENTS.md) (and the skills it routes to under `.agents/skills/`)
|
|
93
|
+
for the operational decision tree and the revert failure-mode catalog.
|
|
@@ -10,6 +10,10 @@ struct Context {
|
|
|
10
10
|
uint256 value;
|
|
11
11
|
uint256 blockTimestamp;
|
|
12
12
|
uint256 blockNumber;
|
|
13
|
+
/// @dev Kernel registrationEpoch(account, permission) at dispatch. Configurable
|
|
14
|
+
/// permissions fail closed on a mismatch with the epoch stamped at configure()
|
|
15
|
+
/// time; permissions without post-deploy configuration can ignore it.
|
|
16
|
+
uint256 configEpoch;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
interface IPermission {
|