@dev.sail.money/sailor 0.0.2 → 0.1.0-local
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +140 -111
- package/LICENSE +21 -21
- package/README.md +430 -337
- package/docs/PERMISSION_MODEL.md +93 -93
- package/examples/permissions/BoundedApproveAndCallBatch.sol +179 -0
- package/examples/permissions/BoundedBet_Limitless_Base.sol +97 -96
- package/examples/permissions/BoundedBorrow_AaveV3_Arbitrum.sol +94 -94
- package/examples/permissions/BoundedPerp_GMXv2_Arbitrum.sol +154 -143
- package/examples/permissions/BoundedStake_Venice_Base.sol +85 -0
- package/examples/permissions/BoundedSupply_AaveV3_Arbitrum.sol +82 -0
- package/examples/permissions/BoundedSwap_UniswapV3_Base.sol +116 -113
- package/examples/permissions/BoundedSwap_UniswapV4_Unichain.sol +150 -144
- package/examples/permissions/BoundedTransfer_ERC20_Ethereum.sol +73 -73
- package/examples/permissions/BoundedVault_ERC4626_Base.sol +97 -0
- package/examples/permissions/README.md +79 -52
- package/examples/permissions/SailCalldata.sol +118 -0
- package/examples/permissions/foundry.toml +10 -10
- package/examples/permissions/interfaces/IBatchPermission.sol +38 -0
- package/examples/permissions/interfaces/IPermission.sol +18 -18
- package/package.json +45 -39
- package/packages/cli/README.md +34 -34
- package/packages/cli/dist/index.cjs +4571 -2944
- package/packages/cli/dist/server.cjs +1252 -2010
- package/packages/sdk/README.md +65 -65
- package/packages/sdk/dist/chains.d.ts +12 -0
- package/packages/sdk/dist/chains.d.ts.map +1 -0
- package/packages/sdk/dist/chains.js +94 -0
- package/packages/sdk/dist/chains.js.map +1 -0
- package/packages/sdk/dist/deployments.d.ts +14 -7
- package/packages/sdk/dist/deployments.d.ts.map +1 -1
- package/packages/sdk/dist/deployments.js +132 -141
- package/packages/sdk/dist/deployments.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/lifi.d.ts +17 -0
- package/packages/sdk/dist/lifi.d.ts.map +1 -1
- package/packages/sdk/dist/lifi.js +24 -0
- package/packages/sdk/dist/lifi.js.map +1 -1
- package/packages/sdk/dist/safe.d.ts +83 -0
- package/packages/sdk/dist/safe.d.ts.map +1 -1
- package/packages/sdk/dist/safe.js +92 -1
- package/packages/sdk/dist/safe.js.map +1 -1
- package/packages/sdk/dist/templates/ammLiquidity.d.ts +24 -11
- package/packages/sdk/dist/templates/ammLiquidity.d.ts.map +1 -1
- package/packages/sdk/dist/templates/ammLiquidity.js +39 -31
- package/packages/sdk/dist/templates/ammLiquidity.js.map +1 -1
- package/packages/sdk/dist/templates/approveAndCallBatch.d.ts +24 -10
- package/packages/sdk/dist/templates/approveAndCallBatch.d.ts.map +1 -1
- package/packages/sdk/dist/templates/approveAndCallBatch.js +36 -23
- package/packages/sdk/dist/templates/approveAndCallBatch.js.map +1 -1
- package/packages/sdk/dist/templates/boundedBorrow.d.ts +19 -9
- package/packages/sdk/dist/templates/boundedBorrow.d.ts.map +1 -1
- package/packages/sdk/dist/templates/boundedBorrow.js +28 -19
- package/packages/sdk/dist/templates/boundedBorrow.js.map +1 -1
- package/packages/sdk/dist/templates/boundedSwap.d.ts +19 -9
- package/packages/sdk/dist/templates/boundedSwap.d.ts.map +1 -1
- package/packages/sdk/dist/templates/boundedSwap.js +30 -20
- package/packages/sdk/dist/templates/boundedSwap.js.map +1 -1
- package/packages/sdk/dist/templates/defiBundle.d.ts +35 -9
- package/packages/sdk/dist/templates/defiBundle.d.ts.map +1 -1
- package/packages/sdk/dist/templates/defiBundle.js +84 -22
- package/packages/sdk/dist/templates/defiBundle.js.map +1 -1
- package/packages/sdk/dist/templates/pendle.d.ts +23 -8
- package/packages/sdk/dist/templates/pendle.d.ts.map +1 -1
- package/packages/sdk/dist/templates/pendle.js +34 -14
- package/packages/sdk/dist/templates/pendle.js.map +1 -1
- package/packages/sdk/dist/templates/transferTarget.d.ts +11 -3
- package/packages/sdk/dist/templates/transferTarget.d.ts.map +1 -1
- package/packages/sdk/dist/templates/transferTarget.js +14 -7
- package/packages/sdk/dist/templates/transferTarget.js.map +1 -1
- package/packages/sdk/dist/types.d.ts +19 -1
- package/packages/sdk/dist/types.d.ts.map +1 -1
- package/packages/sdk/package.json +80 -52
- package/packages/ui/dist/assets/{add-DaJhwIBV.js → add-BxpXfVWe.js} +1 -1
- package/packages/ui/dist/assets/{all-wallets-BUxsqWXi.js → all-wallets-BKTn_sWK.js} +1 -1
- package/packages/ui/dist/assets/{app-store-DkltwTqE.js → app-store-CfuKbwxR.js} +1 -1
- package/packages/ui/dist/assets/{apple-owVOeaIT.js → apple-BKSBbNYg.js} +1 -1
- package/packages/ui/dist/assets/{arrow-bottom-D2mmNJve.js → arrow-bottom-D4bG6gZi.js} +1 -1
- package/packages/ui/dist/assets/{arrow-bottom-circle-CbNYijx-.js → arrow-bottom-circle-BNTs1p0T.js} +1 -1
- package/packages/ui/dist/assets/{arrow-left-DJB61s4C.js → arrow-left-2uee3vYv.js} +1 -1
- package/packages/ui/dist/assets/{arrow-right-BBrsQ9R4.js → arrow-right-BktjMV6h.js} +1 -1
- package/packages/ui/dist/assets/{arrow-top-Cil6bOc8.js → arrow-top-Izu28fX4.js} +1 -1
- package/packages/ui/dist/assets/{bank-CbwEmRo3.js → bank-USBaAyFM.js} +1 -1
- package/packages/ui/dist/assets/{basic-CLNfjw3m.js → basic-C_9KjTEH.js} +1 -1
- package/packages/ui/dist/assets/{browser-B5TtF4Pb.js → browser-DAEMAKV7.js} +1 -1
- package/packages/ui/dist/assets/{card-CO7BVB-C.js → card-DT8yDkKN.js} +1 -1
- package/packages/ui/dist/assets/{ccip-2W7K3_J3.js → ccip-CkqfGSxX.js} +1 -1
- package/packages/ui/dist/assets/{checkmark-BEtSHq9m.js → checkmark-CsgdEXFj.js} +1 -1
- package/packages/ui/dist/assets/{checkmark-bold-D9xGHzPE.js → checkmark-bold-D2gjOQo2.js} +1 -1
- package/packages/ui/dist/assets/{chevron-bottom-BDztht6i.js → chevron-bottom-tprFynYV.js} +1 -1
- package/packages/ui/dist/assets/{chevron-left-EV4GFNbc.js → chevron-left-D2Zj1gNB.js} +1 -1
- package/packages/ui/dist/assets/{chevron-right-B4_bB9oR.js → chevron-right-D1rRuAVe.js} +1 -1
- package/packages/ui/dist/assets/{chevron-top-D54xPNzF.js → chevron-top-24dL1mbL.js} +1 -1
- package/packages/ui/dist/assets/{chrome-store-DYUpAJJq.js → chrome-store-Vy-5niYX.js} +1 -1
- package/packages/ui/dist/assets/{clock-Ca1T1Soz.js → clock-qBjLnVdJ.js} +1 -1
- package/packages/ui/dist/assets/{close-BZqWjurK.js → close-DARDwgcu.js} +1 -1
- package/packages/ui/dist/assets/{coinPlaceholder-e6fl2XDo.js → coinPlaceholder-BvpIbPlD.js} +1 -1
- package/packages/ui/dist/assets/{compass-DCLC7zIh.js → compass-BMTO0ayt.js} +1 -1
- package/packages/ui/dist/assets/{copy-Th2AaD-O.js → copy-PaXeRHza.js} +1 -1
- package/packages/ui/dist/assets/{core-Ckx_cyuH.js → core-BFnStQd-.js} +3 -3
- package/packages/ui/dist/assets/cursor-BDvw-B17.js +3 -0
- package/packages/ui/dist/assets/{cursor-transparent-BKHeABKB.js → cursor-transparent-BEMdi-8q.js} +1 -1
- package/packages/ui/dist/assets/{desktop-CBjY8t6F.js → desktop-CfuLLThw.js} +1 -1
- package/packages/ui/dist/assets/{disconnect-DbSs2cli.js → disconnect-DhwgJMiR.js} +1 -1
- package/packages/ui/dist/assets/{discord-ZlLOAUkM.js → discord-po8qoN1s.js} +1 -1
- package/packages/ui/dist/assets/{etherscan-CKUrqWYN.js → etherscan-BEsz0_yx.js} +1 -1
- package/packages/ui/dist/assets/{events-CiKP71cK.js → events-Bz33Unzu.js} +1 -1
- package/packages/ui/dist/assets/{exclamation-triangle-DA1QzFiO.js → exclamation-triangle-7CjTAGOQ.js} +1 -1
- package/packages/ui/dist/assets/{extension-BVJkmvpJ.js → extension-CmxjEWEt.js} +1 -1
- package/packages/ui/dist/assets/{external-link-D_bsR7B2.js → external-link-CmQ--bNS.js} +1 -1
- package/packages/ui/dist/assets/{facebook-CmFmhojx.js → facebook-CIBn9b65.js} +1 -1
- package/packages/ui/dist/assets/{fallback-Ofl6uSnB.js → fallback-DATyrQlb.js} +1 -1
- package/packages/ui/dist/assets/{farcaster-Co-M3Ss8.js → farcaster-OJ3Jasxg.js} +1 -1
- package/packages/ui/dist/assets/{filters-B1WwNaFU.js → filters-D4x09zeL.js} +1 -1
- package/packages/ui/dist/assets/{github-CP4fP6gn.js → github-ZlIuMArp.js} +1 -1
- package/packages/ui/dist/assets/{google-CsOIXJ6V.js → google-Gwg85sfv.js} +1 -1
- package/packages/ui/dist/assets/{help-circle-DiMkomdF.js → help-circle-D1uOWYcX.js} +1 -1
- package/packages/ui/dist/assets/{id-lmscL5LX.js → id-C0-5UdYk.js} +1 -1
- package/packages/ui/dist/assets/{image-B-ubJrY5.js → image-D_DUsv8-.js} +1 -1
- package/packages/ui/dist/assets/{index-CZR1Qjhs.js → index-BCzex_R6.js} +1 -1
- package/packages/ui/dist/assets/index-BUhrHLpY.js +1775 -0
- package/packages/ui/dist/assets/index-Cq02kQmy.css +1 -0
- package/packages/ui/dist/assets/{index-BaukYv-x.js → index-CrYzBWfD.js} +1 -1
- package/packages/ui/dist/assets/{index-CF0KMmke.js → index-DdbJhIdl.js} +3 -3
- package/packages/ui/dist/assets/{index-DVgfCzCo.js → index-DiojfeVM.js} +1 -1
- package/packages/ui/dist/assets/{index-Dbh5V1Z0.js → index-izd7vu_r.js} +1 -1
- package/packages/ui/dist/assets/{index.es-C78cE5SI.js → index.es-DdkHhQAj.js} +4 -4
- package/packages/ui/dist/assets/{info-Cqg57EVo.js → info-CiRd_kEG.js} +1 -1
- package/packages/ui/dist/assets/{info-circle-DkeSWNKV.js → info-circle-ypxjqarK.js} +1 -1
- package/packages/ui/dist/assets/{lightbulb-DNlO4qKh.js → lightbulb-B-pxLxd8.js} +1 -1
- package/packages/ui/dist/assets/{mail-kVQ8Jb9Y.js → mail-BYmicuVZ.js} +1 -1
- package/packages/ui/dist/assets/{metamask-sdk-CBalSvz7.js → metamask-sdk-Ccl6DG7Q.js} +1 -1
- package/packages/ui/dist/assets/{mobile-BEteuhF7.js → mobile-CtP5PqVT.js} +1 -1
- package/packages/ui/dist/assets/{more-DBWmXQli.js → more-6C2733we.js} +1 -1
- package/packages/ui/dist/assets/{network-placeholder-Dg1uUHiL.js → network-placeholder-CdhxMzqd.js} +1 -1
- package/packages/ui/dist/assets/{nftPlaceholder-i3AHSiD9.js → nftPlaceholder-DVmTWEAY.js} +1 -1
- package/packages/ui/dist/assets/{off-BtMm0fi2.js → off-DNYLughs.js} +1 -1
- package/packages/ui/dist/assets/{parseSignature-Cb5FlWWg.js → parseSignature-Dq2B5Bu3.js} +1 -1
- package/packages/ui/dist/assets/{play-store-iKKkXa6a.js → play-store-D7Qut5ta.js} +1 -1
- package/packages/ui/dist/assets/{plus-CA5NaRtb.js → plus-kqMyjt3q.js} +1 -1
- package/packages/ui/dist/assets/{qr-code-D2kiqR7h.js → qr-code-DiUCWRbz.js} +1 -1
- package/packages/ui/dist/assets/{recycle-horizontal-Dcme7R03.js → recycle-horizontal-Boe3XiS-.js} +1 -1
- package/packages/ui/dist/assets/{refresh-Dega3sDp.js → refresh-CrBgBQYO.js} +1 -1
- package/packages/ui/dist/assets/{reown-logo-xNkksyWJ.js → reown-logo-CFZCCHSx.js} +1 -1
- package/packages/ui/dist/assets/{search-HYl7NO8x.js → search-ChTDrghU.js} +1 -1
- package/packages/ui/dist/assets/{secp256k1-Cxd6_SiH.js → secp256k1-DAV5Q_FR.js} +1 -1
- package/packages/ui/dist/assets/{send-CJU8CUAo.js → send-DLFbBFe1.js} +1 -1
- package/packages/ui/dist/assets/{swapHorizontal-IMUKiUre.js → swapHorizontal-BEs3emfG.js} +1 -1
- package/packages/ui/dist/assets/{swapHorizontalBold-CNYnNJ9-.js → swapHorizontalBold-CC-Hfa7W.js} +1 -1
- package/packages/ui/dist/assets/{swapHorizontalMedium-B9VxEYsT.js → swapHorizontalMedium-BmR0H8DC.js} +1 -1
- package/packages/ui/dist/assets/{swapHorizontalRoundedBold-Dz33l_Jh.js → swapHorizontalRoundedBold-BdP5NGIH.js} +1 -1
- package/packages/ui/dist/assets/{swapVertical-CHUmjVJ0.js → swapVertical-CPrGEJPY.js} +1 -1
- package/packages/ui/dist/assets/{telegram-kl9S2mbU.js → telegram-CxNoZ80Q.js} +1 -1
- package/packages/ui/dist/assets/{three-dots-U5lhA1Am.js → three-dots-BRa6SBpL.js} +1 -1
- package/packages/ui/dist/assets/{twitch-KTEUWXEp.js → twitch-BC338bG5.js} +1 -1
- package/packages/ui/dist/assets/{twitterIcon-BHiq8mRg.js → twitterIcon-BGZmt2i9.js} +1 -1
- package/packages/ui/dist/assets/{verify-CfN-BXNd.js → verify-CEstW0zw.js} +1 -1
- package/packages/ui/dist/assets/{verify-filled-DwZccetj.js → verify-filled-OkZb0weU.js} +1 -1
- package/packages/ui/dist/assets/{w3m-modal-CS-PFqPE.js → w3m-modal-pS09ECwE.js} +1 -1
- package/packages/ui/dist/assets/{wallet-DVlGkhOY.js → wallet-BXVKCgC9.js} +1 -1
- package/packages/ui/dist/assets/{wallet-placeholder-CvR_iEWX.js → wallet-placeholder-C_kNhB1c.js} +1 -1
- package/packages/ui/dist/assets/{walletconnect-8pZBDvVI.js → walletconnect-CRKIuUHH.js} +1 -1
- package/packages/ui/dist/assets/{warning-circle-ylLEE0Yp.js → warning-circle-DB2NnwlJ.js} +1 -1
- package/packages/ui/dist/assets/{x-C_TBsTMj.js → x-DT4RmwL5.js} +1 -1
- package/packages/ui/dist/index.html +14 -14
- package/scripts/check-docs.mjs +262 -262
- package/scripts/check-init.mjs +108 -109
- package/scripts/postinstall.js +81 -366
- package/templates/custom-mandate/.sail/contracts/interfaces/IPermission.sol +18 -18
- package/templates/custom-mandate/README.md +116 -85
- package/templates/custom-mandate/foundry.toml +8 -8
- package/templates/custom-mandate/mandates/BoundedCallPermission.sol +41 -35
- package/templates/custom-mandate/mandates/README.md +16 -16
- package/templates/custom-mandate/mandates/SailCalldata.sol +118 -0
- package/templates/{dca-rebalancer → default}/.cursor/rules +25 -25
- package/templates/default/.env.example +20 -0
- package/templates/{dca-rebalancer → default}/.github/workflows/agent-tick.yml +33 -32
- package/templates/{dca-rebalancer → default}/.sail/README.md +13 -13
- package/templates/{dca-rebalancer → default}/.sail/config.json +10 -10
- package/templates/default/AGENTS.md +171 -0
- package/templates/{dca-rebalancer → default}/CLAUDE.md +2 -2
- package/templates/default/README.md +16 -0
- package/templates/{dca-rebalancer → default}/_gitignore +13 -13
- package/templates/{dca-rebalancer → default}/docs/PERMISSION_MODEL.md +93 -93
- package/templates/default/examples/dca/README.md +16 -0
- package/templates/default/examples/dca/agent.ts +174 -0
- package/templates/{dca-rebalancer/src → default/examples/dca}/mandate.ts +45 -67
- package/templates/{dca-rebalancer → default}/package.json +17 -17
- package/templates/default/src/agent.ts +37 -0
- package/templates/default/src/config.ts +24 -0
- package/templates/default/src/mandate.ts +22 -0
- package/templates/default/tsconfig.json +17 -0
- package/templates/{dca-rebalancer → default}/ui/README.md +3 -3
- package/templates/lifi-permissions/LifiBoundedApprovePermissionCloneable.sol +84 -84
- package/templates/lifi-permissions/LifiDiamondSwapPermissionCloneable.sol +97 -97
- package/templates/lifi-permissions/README.md +53 -53
- package/packages/ui/dist/assets/cursor-DV7rOqbJ.js +0 -3
- package/packages/ui/dist/assets/index-CKxgNxS9.css +0 -1
- package/packages/ui/dist/assets/index-Q2Yai4Fe.js +0 -2103
- package/templates/dca-rebalancer/.env.example +0 -6
- package/templates/dca-rebalancer/AGENTS.md +0 -246
- package/templates/dca-rebalancer/AGENT_PLAYBOOK.md +0 -110
- package/templates/dca-rebalancer/README.md +0 -16
- package/templates/dca-rebalancer/src/agent.ts +0 -253
- package/templates/dca-rebalancer/src/config.ts +0 -27
- package/templates/dca-rebalancer/tsconfig.json +0 -8
- /package/templates/{dca-rebalancer → default}/.sail/.gitkeep +0 -0
|
@@ -1,73 +1,73 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity 0.8.26;
|
|
3
|
-
|
|
4
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
-
// Protocol : ERC-20
|
|
6
|
-
// Version : Standard ERC-20 (version-agnostic)
|
|
7
|
-
// Chain : Ethereum mainnet (works on any EVM — the most general example)
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// transfer(address to,
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// NOT
|
|
16
|
-
// • transfer frequency / timing
|
|
17
|
-
// • choice of token within ALLOWED_TOKENS
|
|
18
|
-
// • choice of recipient within ALLOWED_RECIPIENTS
|
|
19
|
-
//
|
|
20
|
-
// VERIFY BEFORE USE:
|
|
21
|
-
// • Selector 0xa9059cbb = transfer(address,uint256) — universally standard.
|
|
22
|
-
// • ALLOWED_TOKENS prevents the agent from transferring tokens not in the set.
|
|
23
|
-
// If a protocol uses non-standard transfer methods (e.g. transferFrom or
|
|
24
|
-
// proprietary hooks), add separate selector entries.
|
|
25
|
-
// • MAX_AMOUNT_PER_TRANSFER is denominated in the token's base units.
|
|
26
|
-
// Different tokens have different decimals (USDC = 6, WETH = 18).
|
|
27
|
-
// Set one permission per token if amounts differ across tokens.
|
|
28
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
29
|
-
|
|
30
|
-
import {IPermission, Context} from "@sail/interfaces/IPermission.sol";
|
|
31
|
-
|
|
32
|
-
contract BoundedTransfer_ERC20_Ethereum is IPermission {
|
|
33
|
-
bytes32 private constant DISCRIMINATOR = keccak256("BoundedTransfer_ERC20_Ethereum");
|
|
34
|
-
|
|
35
|
-
mapping(address => bool) public isAllowedToken;
|
|
36
|
-
mapping(address => bool) public isAllowedRecipient;
|
|
37
|
-
uint256 public immutable MAX_AMOUNT_PER_TRANSFER;
|
|
38
|
-
|
|
39
|
-
// transfer(address,uint256)
|
|
40
|
-
bytes4 private constant SEL_TRANSFER = 0xa9059cbb;
|
|
41
|
-
|
|
42
|
-
/// @param allowedTokens ERC-20 contracts the agent may transfer from
|
|
43
|
-
/// @param allowedRecipients Addresses the agent may send to
|
|
44
|
-
/// @param maxAmountPerTransfer Per-call amount cap (in token base units)
|
|
45
|
-
constructor(
|
|
46
|
-
address[] memory allowedTokens,
|
|
47
|
-
address[] memory allowedRecipients,
|
|
48
|
-
uint256 maxAmountPerTransfer
|
|
49
|
-
) {
|
|
50
|
-
MAX_AMOUNT_PER_TRANSFER = maxAmountPerTransfer;
|
|
51
|
-
for (uint256 i = 0; i < allowedTokens.length; i++) {
|
|
52
|
-
isAllowedToken[allowedTokens[i]] = true;
|
|
53
|
-
}
|
|
54
|
-
for (uint256 i = 0; i < allowedRecipients.length; i++) {
|
|
55
|
-
isAllowedRecipient[allowedRecipients[i]] = true;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool) {
|
|
60
|
-
if (!isAllowedToken[ctx.target]) return false;
|
|
61
|
-
if (ctx.selector != SEL_TRANSFER) return false;
|
|
62
|
-
if (txData.length < 4 + 2 * 32) return false;
|
|
63
|
-
|
|
64
|
-
(address to, uint256 amount) = abi.decode(txData[4:], (address, uint256));
|
|
65
|
-
|
|
66
|
-
if (!isAllowedRecipient[to]) return false;
|
|
67
|
-
if (amount > MAX_AMOUNT_PER_TRANSFER) return false;
|
|
68
|
-
|
|
69
|
-
return true;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function discriminator() external pure returns (bytes32) { return DISCRIMINATOR; }
|
|
73
|
-
}
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
// Protocol : ERC-20
|
|
6
|
+
// Version : Standard ERC-20 (version-agnostic)
|
|
7
|
+
// Chain : Ethereum mainnet (works on any EVM — the most general example)
|
|
8
|
+
//
|
|
9
|
+
// ENFORCES ON-CHAIN (kernel calls evaluate() on every dispatch; false ⇒ dispatch blocked):
|
|
10
|
+
// transfer(address to,uint256 amount) selector 0xa9059cbb
|
|
11
|
+
// • target must be in ALLOWED_TOKENS
|
|
12
|
+
// • recipient (to) must be in ALLOWED_RECIPIENTS
|
|
13
|
+
// • amount ≤ MAX_AMOUNT_PER_TRANSFER
|
|
14
|
+
//
|
|
15
|
+
// AGENT-ENFORCED / NOT BOUNDED HERE (off-chain — can change without redeploying this contract):
|
|
16
|
+
// • transfer frequency / timing
|
|
17
|
+
// • choice of token within ALLOWED_TOKENS
|
|
18
|
+
// • choice of recipient within ALLOWED_RECIPIENTS
|
|
19
|
+
//
|
|
20
|
+
// VERIFY BEFORE USE:
|
|
21
|
+
// • Selector 0xa9059cbb = transfer(address,uint256) — universally standard.
|
|
22
|
+
// • ALLOWED_TOKENS prevents the agent from transferring tokens not in the set.
|
|
23
|
+
// If a protocol uses non-standard transfer methods (e.g. transferFrom or
|
|
24
|
+
// proprietary hooks), add separate selector entries.
|
|
25
|
+
// • MAX_AMOUNT_PER_TRANSFER is denominated in the token's base units.
|
|
26
|
+
// Different tokens have different decimals (USDC = 6, WETH = 18).
|
|
27
|
+
// Set one permission per token if amounts differ across tokens.
|
|
28
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
import {IPermission, Context} from "@sail/interfaces/IPermission.sol";
|
|
31
|
+
|
|
32
|
+
contract BoundedTransfer_ERC20_Ethereum is IPermission {
|
|
33
|
+
bytes32 private constant DISCRIMINATOR = keccak256("BoundedTransfer_ERC20_Ethereum");
|
|
34
|
+
|
|
35
|
+
mapping(address => bool) public isAllowedToken;
|
|
36
|
+
mapping(address => bool) public isAllowedRecipient;
|
|
37
|
+
uint256 public immutable MAX_AMOUNT_PER_TRANSFER;
|
|
38
|
+
|
|
39
|
+
// transfer(address,uint256)
|
|
40
|
+
bytes4 private constant SEL_TRANSFER = 0xa9059cbb;
|
|
41
|
+
|
|
42
|
+
/// @param allowedTokens ERC-20 contracts the agent may transfer from
|
|
43
|
+
/// @param allowedRecipients Addresses the agent may send to
|
|
44
|
+
/// @param maxAmountPerTransfer Per-call amount cap (in token base units)
|
|
45
|
+
constructor(
|
|
46
|
+
address[] memory allowedTokens,
|
|
47
|
+
address[] memory allowedRecipients,
|
|
48
|
+
uint256 maxAmountPerTransfer
|
|
49
|
+
) {
|
|
50
|
+
MAX_AMOUNT_PER_TRANSFER = maxAmountPerTransfer;
|
|
51
|
+
for (uint256 i = 0; i < allowedTokens.length; i++) {
|
|
52
|
+
isAllowedToken[allowedTokens[i]] = true;
|
|
53
|
+
}
|
|
54
|
+
for (uint256 i = 0; i < allowedRecipients.length; i++) {
|
|
55
|
+
isAllowedRecipient[allowedRecipients[i]] = true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool) {
|
|
60
|
+
if (!isAllowedToken[ctx.target]) return false;
|
|
61
|
+
if (ctx.selector != SEL_TRANSFER) return false;
|
|
62
|
+
if (txData.length < 4 + 2 * 32) return false;
|
|
63
|
+
|
|
64
|
+
(address to, uint256 amount) = abi.decode(txData[4:], (address, uint256));
|
|
65
|
+
|
|
66
|
+
if (!isAllowedRecipient[to]) return false;
|
|
67
|
+
if (amount > MAX_AMOUNT_PER_TRANSFER) return false;
|
|
68
|
+
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function discriminator() external pure returns (bytes32) { return DISCRIMINATOR; }
|
|
73
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
// Protocol : ERC-4626 (tokenized vault standard)
|
|
6
|
+
// Version : Standard EIP-4626 interface (version-agnostic)
|
|
7
|
+
// Chain : Base mainnet (8453) — reference vault below; works on any EVM
|
|
8
|
+
// Target : Reference vault — Moonwell Flagship USDC (MetaMorpho, ERC-4626)
|
|
9
|
+
// 0xc1256Ae5FF1cf2719D4937adb3bbCCab2E00A2Ca (verified ERC-4626 on Base: asset()=USDC)
|
|
10
|
+
// Written against the STANDARD interface, so it generalizes to any 4626 vault.
|
|
11
|
+
//
|
|
12
|
+
// ENFORCES ON-CHAIN (kernel calls evaluate() on every dispatch; false ⇒ dispatch blocked):
|
|
13
|
+
// deposit(uint256 assets,address receiver) selector 0x6e553f65
|
|
14
|
+
// • target (the vault) must be in ALLOWED_VAULTS
|
|
15
|
+
// • assets ≤ MAX_DEPOSIT_ASSETS
|
|
16
|
+
// • receiver must equal ctx.account (the SMA — shares cannot be minted to another address)
|
|
17
|
+
// withdraw(uint256 assets,address receiver,address owner) selector 0xb460af94
|
|
18
|
+
// redeem(uint256 shares,address receiver,address owner) selector 0xba087652
|
|
19
|
+
// • target (the vault) must be in ALLOWED_VAULTS
|
|
20
|
+
// • receiver must equal ctx.account (assets cannot be sent elsewhere)
|
|
21
|
+
// • owner must equal ctx.account (cannot burn shares the SMA merely has allowance over)
|
|
22
|
+
//
|
|
23
|
+
// AGENT-ENFORCED / NOT BOUNDED HERE (off-chain — can change without redeploying this contract):
|
|
24
|
+
// • Which vault within ALLOWED_VAULTS to use, and deposit/withdraw timing/cadence
|
|
25
|
+
// • Withdraw/redeem amount (only the receiver/owner are bound, not the size — the SMA can
|
|
26
|
+
// always withdraw its own position in full; add a cap here if your strategy needs one)
|
|
27
|
+
// • Underlying ASSET: enforced TRANSITIVELY via ALLOWED_VAULTS, not from calldata. A 4626
|
|
28
|
+
// vault's asset() is fixed at deploy; the deposit/withdraw calldata carries NO asset field,
|
|
29
|
+
// so the asset cannot be read from it. Allowlist only vaults whose asset() you have verified.
|
|
30
|
+
//
|
|
31
|
+
// VERIFY BEFORE USE:
|
|
32
|
+
// • Selectors are the EIP-4626 standard (verified via `cast sig`): deposit 0x6e553f65,
|
|
33
|
+
// withdraw 0xb460af94, redeem 0xba087652. (mint(uint256,address)=0x94bf804d is NOT gated
|
|
34
|
+
// here — add it if your agent mints shares by exact share count.)
|
|
35
|
+
// • Confirm each allowlisted vault actually implements ERC-4626 and its asset() is what you
|
|
36
|
+
// expect (e.g. on the reference vault, asset() == USDC 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913).
|
|
37
|
+
// • A deposit also needs a prior ERC-20 approve of the vault — gate that with a separate
|
|
38
|
+
// approve permission (see BoundedApproveAndCallBatch.sol for the atomic approve→call→reset pattern).
|
|
39
|
+
// • MAX_DEPOSIT_ASSETS is in the asset's base units (USDC = 6 decimals).
|
|
40
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
import {IPermission, Context} from "@sail/interfaces/IPermission.sol";
|
|
43
|
+
|
|
44
|
+
contract BoundedVault_ERC4626_Base is IPermission {
|
|
45
|
+
bytes32 private constant DISCRIMINATOR = keccak256("BoundedVault_ERC4626_Base");
|
|
46
|
+
|
|
47
|
+
mapping(address => bool) public isAllowedVault;
|
|
48
|
+
uint256 public immutable MAX_DEPOSIT_ASSETS;
|
|
49
|
+
|
|
50
|
+
// EIP-4626 standard selectors
|
|
51
|
+
bytes4 private constant SEL_DEPOSIT = 0x6e553f65; // deposit(uint256,address)
|
|
52
|
+
bytes4 private constant SEL_WITHDRAW = 0xb460af94; // withdraw(uint256,address,address)
|
|
53
|
+
bytes4 private constant SEL_REDEEM = 0xba087652; // redeem(uint256,address,address)
|
|
54
|
+
|
|
55
|
+
/// @param allowedVaults ERC-4626 vaults the agent may deposit into / withdraw from
|
|
56
|
+
/// @param maxDepositAssets Per-deposit cap in the asset's base units (must be > 0)
|
|
57
|
+
constructor(address[] memory allowedVaults, uint256 maxDepositAssets) {
|
|
58
|
+
require(allowedVaults.length > 0, "empty vault allowlist");
|
|
59
|
+
require(maxDepositAssets > 0, "zero deposit cap");
|
|
60
|
+
MAX_DEPOSIT_ASSETS = maxDepositAssets;
|
|
61
|
+
for (uint256 i = 0; i < allowedVaults.length; i++) {
|
|
62
|
+
require(allowedVaults[i] != address(0), "zero vault address");
|
|
63
|
+
isAllowedVault[allowedVaults[i]] = true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool) {
|
|
68
|
+
if (!isAllowedVault[ctx.target]) return false;
|
|
69
|
+
if (txData.length < 4) return false;
|
|
70
|
+
|
|
71
|
+
// deposit(uint256 assets, address receiver)
|
|
72
|
+
if (ctx.selector == SEL_DEPOSIT) {
|
|
73
|
+
if (txData.length < 4 + 2 * 32) return false;
|
|
74
|
+
(uint256 assets, address receiver) = abi.decode(txData[4:], (uint256, address));
|
|
75
|
+
if (assets > MAX_DEPOSIT_ASSETS) return false;
|
|
76
|
+
if (receiver != ctx.account) return false; // shares must accrue to the SMA
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// withdraw(uint256 assets, address receiver, address owner)
|
|
81
|
+
// redeem(uint256 shares, address receiver, address owner)
|
|
82
|
+
// Identical parameter layout: (uint256, address, address).
|
|
83
|
+
// Amount (assets/shares) is NOT bounded — the SMA can always exit its full position.
|
|
84
|
+
// Add a maxWithdrawAssets constructor param and check here if your strategy needs a cap.
|
|
85
|
+
if (ctx.selector == SEL_WITHDRAW || ctx.selector == SEL_REDEEM) {
|
|
86
|
+
if (txData.length < 4 + 3 * 32) return false;
|
|
87
|
+
(, address receiver, address owner) = abi.decode(txData[4:], (uint256, address, address));
|
|
88
|
+
if (receiver != ctx.account) return false; // assets must return to the SMA
|
|
89
|
+
if (owner != ctx.account) return false; // only the SMA's own shares may be burned
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function discriminator() external pure returns (bytes32) { return DISCRIMINATOR; }
|
|
97
|
+
}
|
|
@@ -1,52 +1,79 @@
|
|
|
1
|
-
# Permission Examples
|
|
2
|
-
|
|
3
|
-
These are example permission contracts for common DeFi protocols, maintained by Sailor.
|
|
4
|
-
|
|
5
|
-
**They are not part of Sail Protocol. They are not audited by Sail. They are not a supported
|
|
6
|
-
or exhaustive set.** The protocol accepts any contract implementing IPermission — these examples
|
|
7
|
-
teach the bounding pattern for specific protocols so you (and your AI agent) can adapt them or
|
|
8
|
-
write your own.
|
|
9
|
-
|
|
10
|
-
## Permissions are only as strong as the protocol is on-chain
|
|
11
|
-
|
|
12
|
-
A permission is evaluated by the kernel on every dispatch — but it can only see what happens
|
|
13
|
-
on-chain. For venues with off-chain order matching (e.g. Polymarket, Hyperliquid), a permission
|
|
14
|
-
can constrain deposits, withdrawals, and sub-accounts, but NOT the orders your agent signs
|
|
15
|
-
off-chain. Prefer fully on-chain venues — Uniswap, Aave, GMX, Synthetix, Limitless — where every
|
|
16
|
-
action passes through the kernel and your bounds actually hold.
|
|
17
|
-
|
|
18
|
-
## Permissions are protocol- and version-specific
|
|
19
|
-
|
|
20
|
-
Calldata differs by protocol and by version. A Uniswap V3 swap is a different decode than a
|
|
21
|
-
Uniswap V4 swap. Use the example closest to YOUR exact protocol+version+chain, verify the decode
|
|
22
|
-
against the protocol's real ABI, and confirm what it enforces before deploying.
|
|
23
|
-
|
|
24
|
-
You own what you deploy.
|
|
25
|
-
|
|
26
|
-
---
|
|
27
|
-
|
|
28
|
-
## Examples
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
|
35
|
-
|
|
36
|
-
| `
|
|
37
|
-
| `
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
1
|
+
# Permission Examples
|
|
2
|
+
|
|
3
|
+
These are example permission contracts for common DeFi protocols, maintained by Sailor.
|
|
4
|
+
|
|
5
|
+
**They are not part of Sail Protocol. They are not audited by Sail. They are not a supported
|
|
6
|
+
or exhaustive set.** The protocol accepts any contract implementing IPermission — these examples
|
|
7
|
+
teach the bounding pattern for specific protocols so you (and your AI agent) can adapt them or
|
|
8
|
+
write your own.
|
|
9
|
+
|
|
10
|
+
## Permissions are only as strong as the protocol is on-chain
|
|
11
|
+
|
|
12
|
+
A permission is evaluated by the kernel on every dispatch — but it can only see what happens
|
|
13
|
+
on-chain. For venues with off-chain order matching (e.g. Polymarket, Hyperliquid), a permission
|
|
14
|
+
can constrain deposits, withdrawals, and sub-accounts, but NOT the orders your agent signs
|
|
15
|
+
off-chain. Prefer fully on-chain venues — Uniswap, Aave, GMX, Synthetix, Limitless — where every
|
|
16
|
+
action passes through the kernel and your bounds actually hold.
|
|
17
|
+
|
|
18
|
+
## Permissions are protocol- and version-specific
|
|
19
|
+
|
|
20
|
+
Calldata differs by protocol and by version. A Uniswap V3 swap is a different decode than a
|
|
21
|
+
Uniswap V4 swap. Use the example closest to YOUR exact protocol+version+chain, verify the decode
|
|
22
|
+
against the protocol's real ABI, and confirm what it enforces before deploying.
|
|
23
|
+
|
|
24
|
+
You own what you deploy.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Examples
|
|
29
|
+
|
|
30
|
+
Each header has two blocks: **ENFORCES ON-CHAIN** (bounds the kernel checks in `evaluate()` on
|
|
31
|
+
every dispatch — these actually hold) and **AGENT-ENFORCED / NOT BOUNDED HERE** (left to your
|
|
32
|
+
off-chain agent code — these do *not* hold on-chain). Read both before deploying.
|
|
33
|
+
|
|
34
|
+
| File | Protocol | Version | Chain | Status |
|
|
35
|
+
|---|---|---|---|---|
|
|
36
|
+
| `BoundedSwap_UniswapV3_Base.sol` | Uniswap Swap | V3 SwapRouter02 | Base | Full decode |
|
|
37
|
+
| `BoundedSwap_UniswapV4_Unichain.sol` | Uniswap Swap | V4 Universal Router | Unichain | Partial decode — see header |
|
|
38
|
+
| `BoundedBorrow_AaveV3_Arbitrum.sol` | Aave Borrow | V3 Pool | Arbitrum | Full decode |
|
|
39
|
+
| `BoundedSupply_AaveV3_Arbitrum.sol` | Aave Supply | V3 Pool | Arbitrum | Full decode |
|
|
40
|
+
| `BoundedVault_ERC4626_Base.sol` | ERC-4626 Vault deposit/withdraw | EIP-4626 standard | Base (any EVM) | Full decode |
|
|
41
|
+
| `BoundedStake_Venice_Base.sol` | Venice (VVV) staking | sVVV staking | Base | Full decode |
|
|
42
|
+
| `BoundedTransfer_ERC20_Ethereum.sol` | ERC-20 Transfer | — | Ethereum (any EVM) | Full decode |
|
|
43
|
+
| `BoundedPerp_GMXv2_Arbitrum.sol` | GMX Perpetuals | V2 ExchangeRouter | Arbitrum | Reference pattern — verify selector/struct/router against live GMX ABI (see header) |
|
|
44
|
+
| `BoundedBet_Limitless_Base.sol` | Limitless Prediction | CTF Exchange | Base | UNVERIFIED ABI — see header |
|
|
45
|
+
| `BoundedApproveAndCallBatch.sol` | Atomic approve→call→reset | Sail `IBatchPermission` | Any selective kernel | Full decode — batch-only (see note below) |
|
|
46
|
+
|
|
47
|
+
The `interfaces/` directory holds `IPermission.sol` (single-call permissions) and
|
|
48
|
+
`IBatchPermission.sol` (batch permissions — see the batch example).
|
|
49
|
+
|
|
50
|
+
## Using these examples
|
|
51
|
+
|
|
52
|
+
Each file is self-contained and explains in its header exactly what is and is not enforced.
|
|
53
|
+
Read the header before deploying.
|
|
54
|
+
|
|
55
|
+
To compile:
|
|
56
|
+
```bash
|
|
57
|
+
forge build
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
To deploy within a Sailor project (copy the .sol file to `mandates/` first):
|
|
61
|
+
```bash
|
|
62
|
+
sailor mandate deploy --contract <Name> --args '[...]' --attach --sma <SMA>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Verify before you authorize
|
|
66
|
+
|
|
67
|
+
Prove a permission accepts the calls you want and rejects the ones you don't — before paying
|
|
68
|
+
registration gas — with `sailor mandate simulate` (off-chain `eth_call`, no gas, signs nothing):
|
|
69
|
+
```bash
|
|
70
|
+
sailor mandate simulate --address <permission> --calls calls.json
|
|
71
|
+
```
|
|
72
|
+
Every example here was checked this way (valid calls PASS, out-of-bounds calls FAIL).
|
|
73
|
+
|
|
74
|
+
**Batch permissions are different.** `BoundedApproveAndCallBatch.sol` implements `IBatchPermission`
|
|
75
|
+
and is gated by the kernel's `dispatchBatch`, not single `dispatch` — its single-call `evaluate()`
|
|
76
|
+
deliberately returns `false`. `sailor mandate simulate` only probes single `evaluate()`, so it
|
|
77
|
+
**cannot** verify a batch permission. Until simulate gains a batch mode, verify a batch permission
|
|
78
|
+
by calling its `evaluateBatch(calls, ctx)` view directly (e.g. `cast call`) with the call sequences
|
|
79
|
+
you expect to pass and fail — same fail-closed `eth_call` semantics, the correct entrypoint.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
// SailCalldata — safe static-parameter extraction for IPermission.evaluate()
|
|
6
|
+
//
|
|
7
|
+
// PROBLEM
|
|
8
|
+
// evaluate(bytes calldata txData, Context calldata ctx) receives raw ABI-
|
|
9
|
+
// encoded calldata. Extracting parameters by hand is the most dangerous part
|
|
10
|
+
// of permission writing:
|
|
11
|
+
// • Forgetting the length check before decoding → silent wrong value or
|
|
12
|
+
// revert instead of clean `return false`.
|
|
13
|
+
// • Wrong slot index → decoding the wrong parameter (type-checks, still wrong).
|
|
14
|
+
// • Truncation bugs when casting uint256 slots to address (padding bits).
|
|
15
|
+
//
|
|
16
|
+
// SOLUTION
|
|
17
|
+
// This library centralises the three operations into named helpers:
|
|
18
|
+
// 1. hasParams() — explicit length guard (call once, at the top of evaluate)
|
|
19
|
+
// 2. asAddress() — extract + mask to 20 bytes
|
|
20
|
+
// 3. asUint256(), asInt256(), asBytes32(), asBool(), asUint128() — typed slots
|
|
21
|
+
//
|
|
22
|
+
// All helpers are view/pure and add zero gas overhead beyond the slice itself.
|
|
23
|
+
//
|
|
24
|
+
// USAGE (replace abi.decode pattern)
|
|
25
|
+
//
|
|
26
|
+
// // Before:
|
|
27
|
+
// if (txData.length < 4 + 3 * 32) return false;
|
|
28
|
+
// (address asset, uint256 amount, address onBehalfOf) =
|
|
29
|
+
// abi.decode(txData[4:], (address, uint256, address));
|
|
30
|
+
//
|
|
31
|
+
// // After:
|
|
32
|
+
// if (!SailCalldata.hasParams(txData, 3)) return false;
|
|
33
|
+
// address asset = SailCalldata.asAddress(txData, 0);
|
|
34
|
+
// uint256 amount = SailCalldata.asUint256(txData, 1);
|
|
35
|
+
// address onBehalfOf = SailCalldata.asAddress(txData, 2);
|
|
36
|
+
//
|
|
37
|
+
// LIMITATIONS
|
|
38
|
+
// Only covers static (fixed-size) ABI types. Dynamic types (bytes, string,
|
|
39
|
+
// arrays) use pointer indirection — decode them with abi.decode(txData[4:], ...)
|
|
40
|
+
// after the hasParams() guard, or write a dedicated extractor.
|
|
41
|
+
//
|
|
42
|
+
// SLOT INDEXING
|
|
43
|
+
// Slots are 0-indexed from the first constructor parameter (after the 4-byte
|
|
44
|
+
// selector). Slot 0 = bytes [4..35], slot 1 = bytes [36..67], etc.
|
|
45
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
library SailCalldata {
|
|
48
|
+
// ── Guards ────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/// @notice Returns true when txData is long enough to hold `params` static
|
|
51
|
+
/// 32-byte ABI slots after the 4-byte selector.
|
|
52
|
+
/// @dev Call this once at the top of evaluate() before any slot access.
|
|
53
|
+
/// Returns false (not revert) so evaluate() can return false cleanly.
|
|
54
|
+
function hasParams(bytes calldata txData, uint256 params) internal pure returns (bool) {
|
|
55
|
+
return txData.length >= 4 + params * 32;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── Static-type extractors ────────────────────────────────────────────────
|
|
59
|
+
// All assume hasParams() has already been checked for the relevant slot.
|
|
60
|
+
// Accessing an out-of-bounds slot will cause the calldata slice to revert —
|
|
61
|
+
// always guard with hasParams() first.
|
|
62
|
+
|
|
63
|
+
/// @notice Extract an address from ABI slot `i` (0-indexed after selector).
|
|
64
|
+
/// Masks to 20 bytes, discarding the ABI zero-padding in the upper 12.
|
|
65
|
+
function asAddress(bytes calldata txData, uint256 i) internal pure returns (address) {
|
|
66
|
+
return address(uint160(uint256(bytes32(txData[4 + i * 32 : 4 + (i + 1) * 32]))));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// @notice Extract a uint256 from ABI slot `i`.
|
|
70
|
+
function asUint256(bytes calldata txData, uint256 i) internal pure returns (uint256) {
|
|
71
|
+
return uint256(bytes32(txData[4 + i * 32 : 4 + (i + 1) * 32]));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// @notice Extract an int256 from ABI slot `i`.
|
|
75
|
+
function asInt256(bytes calldata txData, uint256 i) internal pure returns (int256) {
|
|
76
|
+
return int256(uint256(bytes32(txData[4 + i * 32 : 4 + (i + 1) * 32])));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// @notice Extract a bytes32 from ABI slot `i`.
|
|
80
|
+
function asBytes32(bytes calldata txData, uint256 i) internal pure returns (bytes32) {
|
|
81
|
+
return bytes32(txData[4 + i * 32 : 4 + (i + 1) * 32]);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// @notice Extract a bool from ABI slot `i` (true when slot value is non-zero).
|
|
85
|
+
function asBool(bytes calldata txData, uint256 i) internal pure returns (bool) {
|
|
86
|
+
return asUint256(txData, i) != 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// @notice Extract a uint128 from ABI slot `i` (lower 128 bits of the slot).
|
|
90
|
+
function asUint128(bytes calldata txData, uint256 i) internal pure returns (uint128) {
|
|
91
|
+
return uint128(asUint256(txData, i));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// @notice Extract a uint64 from ABI slot `i`.
|
|
95
|
+
function asUint64(bytes calldata txData, uint256 i) internal pure returns (uint64) {
|
|
96
|
+
return uint64(asUint256(txData, i));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// @notice Extract a uint32 from ABI slot `i`.
|
|
100
|
+
function asUint32(bytes calldata txData, uint256 i) internal pure returns (uint32) {
|
|
101
|
+
return uint32(asUint256(txData, i));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// @notice Extract a uint24 from ABI slot `i` (e.g. Uniswap fee tier).
|
|
105
|
+
function asUint24(bytes calldata txData, uint256 i) internal pure returns (uint24) {
|
|
106
|
+
return uint24(asUint256(txData, i));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// @notice Extract a uint16 from ABI slot `i` (e.g. Aave referral code).
|
|
110
|
+
function asUint16(bytes calldata txData, uint256 i) internal pure returns (uint16) {
|
|
111
|
+
return uint16(asUint256(txData, i));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// @notice Extract bytes4 (a function selector) from ABI slot `i`.
|
|
115
|
+
function asBytes4(bytes calldata txData, uint256 i) internal pure returns (bytes4) {
|
|
116
|
+
return bytes4(asBytes32(txData, i));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
[profile.default]
|
|
2
|
-
src = "."
|
|
3
|
-
out = "out"
|
|
4
|
-
libs = ["lib"]
|
|
5
|
-
remappings = ["@sail/interfaces/=interfaces/"]
|
|
6
|
-
solc = "0.8.26"
|
|
7
|
-
optimizer = true
|
|
8
|
-
optimizer_runs = 200
|
|
9
|
-
# Examples compile with: forge build (from this directory)
|
|
10
|
-
# Deploy within a Sailor project using: sailor mandate deploy --contract <Name> --args '[...]'
|
|
1
|
+
[profile.default]
|
|
2
|
+
src = "."
|
|
3
|
+
out = "out"
|
|
4
|
+
libs = ["lib"]
|
|
5
|
+
remappings = ["@sail/interfaces/=interfaces/"]
|
|
6
|
+
solc = "0.8.26"
|
|
7
|
+
optimizer = true
|
|
8
|
+
optimizer_runs = 200
|
|
9
|
+
# Examples compile with: forge build (from this directory)
|
|
10
|
+
# Deploy within a Sailor project using: sailor mandate deploy --contract <Name> --args '[...]'
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
// Mirrors SailProtocol contracts/interfaces/IBatchPermission.sol. A batch-aware
|
|
5
|
+
// permission gates `dispatchBatch` — an ordered array of subcalls executed as one
|
|
6
|
+
// atomic transaction. Exactly ONE batch permission is consulted per batch dispatch
|
|
7
|
+
// (selective model), and it owns validation of every subcall AND the cross-call
|
|
8
|
+
// invariants (ordering, matching amounts, mandatory cleanup) that no per-call
|
|
9
|
+
// IPermission can express.
|
|
10
|
+
|
|
11
|
+
/// @notice A single subcall in a batch dispatch.
|
|
12
|
+
/// @dev The kernel executes each Call as safe.execTransactionFromModule(target, value, data, 0)
|
|
13
|
+
/// — operation is always CALL, never DELEGATECALL.
|
|
14
|
+
struct Call {
|
|
15
|
+
address target; // MUST NOT equal the kernel (enforced by the kernel)
|
|
16
|
+
uint256 value; // native ETH forwarded (wei)
|
|
17
|
+
bytes data; // calldata for the subcall
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// @notice Execution context passed to evaluateBatch on each batch dispatch (read-only snapshot).
|
|
21
|
+
struct BatchContext {
|
|
22
|
+
address account; // the Safe (SMA) whose assets are moved
|
|
23
|
+
address manager; // the delegated signer who authorised the batch
|
|
24
|
+
address submitter; // msg.sender of the dispatch (may differ from manager via a relayer)
|
|
25
|
+
address permission; // this batch permission contract
|
|
26
|
+
bytes32 batchHash; // keccak256(abi.encode(calls)) — stable id for the exact sequence
|
|
27
|
+
uint256 blockTimestamp; // block.timestamp at dispatch
|
|
28
|
+
uint256 blockNumber; // block.number at dispatch
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface IBatchPermission {
|
|
32
|
+
/// @notice Validate an entire batch of subcalls. Called via staticcall under a gas cap;
|
|
33
|
+
/// a revert / OOG / malformed return is treated as `false` (fail-closed).
|
|
34
|
+
function evaluateBatch(Call[] calldata calls, BatchContext calldata ctx) external view returns (bool);
|
|
35
|
+
|
|
36
|
+
/// @notice Marker the kernel uses (try/catch) to detect batch-aware permissions. MUST return true.
|
|
37
|
+
function isBatchPermission() external pure returns (bool);
|
|
38
|
+
}
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity 0.8.26;
|
|
3
|
-
|
|
4
|
-
struct Context {
|
|
5
|
-
address account;
|
|
6
|
-
address manager;
|
|
7
|
-
address submitter;
|
|
8
|
-
address target;
|
|
9
|
-
bytes4 selector;
|
|
10
|
-
uint256 value;
|
|
11
|
-
uint256 blockTimestamp;
|
|
12
|
-
uint256 blockNumber;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
interface IPermission {
|
|
16
|
-
function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool);
|
|
17
|
-
function discriminator() external view returns (bytes32);
|
|
18
|
-
}
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
struct Context {
|
|
5
|
+
address account;
|
|
6
|
+
address manager;
|
|
7
|
+
address submitter;
|
|
8
|
+
address target;
|
|
9
|
+
bytes4 selector;
|
|
10
|
+
uint256 value;
|
|
11
|
+
uint256 blockTimestamp;
|
|
12
|
+
uint256 blockNumber;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface IPermission {
|
|
16
|
+
function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool);
|
|
17
|
+
function discriminator() external view returns (bytes32);
|
|
18
|
+
}
|