@dev.sail.money/sailor 0.0.2-20 → 0.0.2-21
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/examples/permissions/BoundedApproveAndCallBatch.sol +179 -0
- package/examples/permissions/BoundedBet_Limitless_Base.sol +8 -7
- package/examples/permissions/BoundedBorrow_AaveV3_Arbitrum.sol +13 -13
- package/examples/permissions/BoundedPerp_GMXv2_Arbitrum.sol +50 -39
- package/examples/permissions/BoundedStake_Venice_Base.sol +85 -0
- package/examples/permissions/BoundedSupply_AaveV3_Arbitrum.sol +84 -0
- package/examples/permissions/BoundedSwap_UniswapV3_Base.sol +11 -9
- package/examples/permissions/BoundedSwap_UniswapV4_Unichain.sol +10 -8
- package/examples/permissions/BoundedTransfer_ERC20_Ethereum.sol +6 -6
- package/examples/permissions/BoundedVault_ERC4626_Base.sol +97 -0
- package/examples/permissions/README.md +29 -2
- package/examples/permissions/interfaces/IBatchPermission.sol +38 -0
- package/package.json +1 -1
- package/packages/cli/dist/index.cjs +1158 -574
- package/packages/sdk/dist/index.d.ts +1 -1
- package/packages/sdk/dist/index.d.ts.map +1 -1
- package/packages/sdk/dist/index.js +1 -1
- 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 +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/ui/dist/assets/{add-BrylwREe.js → add-BzRDG6go.js} +1 -1
- package/packages/ui/dist/assets/{all-wallets-DGR35dSU.js → all-wallets-C6juL2cm.js} +1 -1
- package/packages/ui/dist/assets/{app-store-BpM1x6bI.js → app-store-DSJ1ow5G.js} +1 -1
- package/packages/ui/dist/assets/{apple-xvs7JX23.js → apple-CUEgIX9k.js} +1 -1
- package/packages/ui/dist/assets/{arrow-bottom-CF6xPQbH.js → arrow-bottom-BOhBj1Je.js} +1 -1
- package/packages/ui/dist/assets/{arrow-bottom-circle-r0A6jlRF.js → arrow-bottom-circle-C4CzuHiR.js} +1 -1
- package/packages/ui/dist/assets/{arrow-left-Ce6MJKC0.js → arrow-left-BTD8zZ12.js} +1 -1
- package/packages/ui/dist/assets/{arrow-right-CiCNIsI0.js → arrow-right-qcOukPwm.js} +1 -1
- package/packages/ui/dist/assets/{arrow-top-TI5RQSIc.js → arrow-top-C0f2945G.js} +1 -1
- package/packages/ui/dist/assets/{bank-CmrK2TRa.js → bank-BSWzLP3R.js} +1 -1
- package/packages/ui/dist/assets/{basic-BhoXad_6.js → basic-DGTBsmnG.js} +1 -1
- package/packages/ui/dist/assets/{browser-BWCT-XSy.js → browser-DJNepafc.js} +1 -1
- package/packages/ui/dist/assets/{card-v1fl0QNj.js → card-u08LJx43.js} +1 -1
- package/packages/ui/dist/assets/{ccip-BkGqsEgG.js → ccip-DcWZjG37.js} +1 -1
- package/packages/ui/dist/assets/{checkmark-CiQnNFee.js → checkmark-DVD-obDl.js} +1 -1
- package/packages/ui/dist/assets/{checkmark-bold-B63f9SW6.js → checkmark-bold-CSXDqIAx.js} +1 -1
- package/packages/ui/dist/assets/{chevron-bottom-DpBpDiYb.js → chevron-bottom-Be7f0gi2.js} +1 -1
- package/packages/ui/dist/assets/{chevron-left-o_G-ctyg.js → chevron-left-v8cgKRhQ.js} +1 -1
- package/packages/ui/dist/assets/{chevron-right-I9LAJ9De.js → chevron-right-B0ux2X-3.js} +1 -1
- package/packages/ui/dist/assets/{chevron-top-JRwGHMKH.js → chevron-top-De-a8tmA.js} +1 -1
- package/packages/ui/dist/assets/{chrome-store-Cw9skzSt.js → chrome-store-BIIIRGPA.js} +1 -1
- package/packages/ui/dist/assets/{clock-BK_Lu0EB.js → clock-8d5kvRPQ.js} +1 -1
- package/packages/ui/dist/assets/{close-DWFjOyu7.js → close-BSSJkFv0.js} +1 -1
- package/packages/ui/dist/assets/{coinPlaceholder-gi3wDlPb.js → coinPlaceholder-CLJaQiUO.js} +1 -1
- package/packages/ui/dist/assets/{compass-Dlpsn_k-.js → compass-CR1zP0b-.js} +1 -1
- package/packages/ui/dist/assets/{copy-BzNKdXeG.js → copy-CGkuIFo6.js} +1 -1
- package/packages/ui/dist/assets/{core-BN9UnMip.js → core-ea860JM2.js} +3 -3
- package/packages/ui/dist/assets/cursor-DCRoxgSY.js +3 -0
- package/packages/ui/dist/assets/{cursor-transparent-B3RZ6Udt.js → cursor-transparent-C8s5LY_P.js} +1 -1
- package/packages/ui/dist/assets/{desktop-rngwE1T9.js → desktop-CQyixryE.js} +1 -1
- package/packages/ui/dist/assets/{disconnect-_DzDsvsZ.js → disconnect-Ct0234l0.js} +1 -1
- package/packages/ui/dist/assets/{discord-B9qQB66m.js → discord-haYPGMDl.js} +1 -1
- package/packages/ui/dist/assets/{etherscan-D7hlQ8Z3.js → etherscan-BTLrS1KK.js} +1 -1
- package/packages/ui/dist/assets/{events-DrkgavVp.js → events-wdo_D3Zy.js} +1 -1
- package/packages/ui/dist/assets/{exclamation-triangle-iCvLVoM-.js → exclamation-triangle-CMlYpOat.js} +1 -1
- package/packages/ui/dist/assets/{extension-CjGcOeuB.js → extension-CNGBCbo8.js} +1 -1
- package/packages/ui/dist/assets/{external-link-DTkyjOr4.js → external-link-DmYJSKcL.js} +1 -1
- package/packages/ui/dist/assets/{facebook-DoO2uCiE.js → facebook-F0iMVTem.js} +1 -1
- package/packages/ui/dist/assets/{fallback-Y8-BiCNG.js → fallback-BqeFDEuW.js} +1 -1
- package/packages/ui/dist/assets/{farcaster-Dyz6wiZi.js → farcaster-CYr9I6UA.js} +1 -1
- package/packages/ui/dist/assets/{filters-Bd5PxMiz.js → filters-CxE97nqU.js} +1 -1
- package/packages/ui/dist/assets/{github-CmqSJCq6.js → github-CjRht-Wv.js} +1 -1
- package/packages/ui/dist/assets/{google-i_MKKqQP.js → google-3G0o4pR9.js} +1 -1
- package/packages/ui/dist/assets/{help-circle-_ER8-V35.js → help-circle-C5gNChqZ.js} +1 -1
- package/packages/ui/dist/assets/{id-D2adRwvh.js → id-C6_zK0Tb.js} +1 -1
- package/packages/ui/dist/assets/{image-q_rpi7cn.js → image-DsU8Irlu.js} +1 -1
- package/packages/ui/dist/assets/{index-DJQDBPts.js → index-BCb0Nju4.js} +39 -39
- package/packages/ui/dist/assets/{index-BwoSt5mL.js → index-CjvcQefO.js} +1 -1
- package/packages/ui/dist/assets/{index-CzevBC-K.js → index-D1lgDFZV.js} +1 -1
- package/packages/ui/dist/assets/{index-Bdl623mb.js → index-DQ44LBvq.js} +3 -3
- package/packages/ui/dist/assets/{index-BLD08Nrb.js → index-Drc17uEc.js} +1 -1
- package/packages/ui/dist/assets/{index-B6Wb3mjQ.js → index-c8ZmMTds.js} +1 -1
- package/packages/ui/dist/assets/{index.es-D6UPoR2j.js → index.es-BhDQmlR4.js} +4 -4
- package/packages/ui/dist/assets/{info-Ck7GYYmo.js → info-CAKKH6T2.js} +1 -1
- package/packages/ui/dist/assets/{info-circle-DtA5FQXd.js → info-circle-CHV1idfy.js} +1 -1
- package/packages/ui/dist/assets/{lightbulb-DR3jyzkK.js → lightbulb-ew10LUMl.js} +1 -1
- package/packages/ui/dist/assets/{mail-BK_pW_tL.js → mail-CACYeWXj.js} +1 -1
- package/packages/ui/dist/assets/{metamask-sdk-IZbe4GJ_.js → metamask-sdk-BUYu4RDE.js} +1 -1
- package/packages/ui/dist/assets/{mobile-CLG-4Yut.js → mobile-DX601q1y.js} +1 -1
- package/packages/ui/dist/assets/{more-BBqkkVbe.js → more-CQGeX45N.js} +1 -1
- package/packages/ui/dist/assets/{network-placeholder-kf7EWfmj.js → network-placeholder-BQw8E4X-.js} +1 -1
- package/packages/ui/dist/assets/{nftPlaceholder-BEFwtFPl.js → nftPlaceholder-CifJ2CzA.js} +1 -1
- package/packages/ui/dist/assets/{off-DKjJt-l0.js → off-B6oUArCZ.js} +1 -1
- package/packages/ui/dist/assets/{parseSignature-KWl5TpQf.js → parseSignature-DjWIdHkx.js} +1 -1
- package/packages/ui/dist/assets/{play-store-DBVkWbwG.js → play-store-Kq51oh3r.js} +1 -1
- package/packages/ui/dist/assets/{plus-BQzagKUL.js → plus-BxZxVpff.js} +1 -1
- package/packages/ui/dist/assets/{qr-code-DVwEgFEM.js → qr-code-CSBFpyhP.js} +1 -1
- package/packages/ui/dist/assets/{recycle-horizontal-D2E6B1Kh.js → recycle-horizontal-D0HyLkot.js} +1 -1
- package/packages/ui/dist/assets/{refresh-CUavEsqn.js → refresh-BAQo228i.js} +1 -1
- package/packages/ui/dist/assets/{reown-logo-mAAAaf4E.js → reown-logo-DeUbwRp6.js} +1 -1
- package/packages/ui/dist/assets/{search-BAnb6iT_.js → search-C8Sd0Mpz.js} +1 -1
- package/packages/ui/dist/assets/{secp256k1-CBUZgsE-.js → secp256k1-BujG3JoP.js} +1 -1
- package/packages/ui/dist/assets/{send-BYjfitxS.js → send-B0JwUp6Q.js} +1 -1
- package/packages/ui/dist/assets/{swapHorizontal-C-4qjxwd.js → swapHorizontal-B2k5yDqc.js} +1 -1
- package/packages/ui/dist/assets/{swapHorizontalBold-zA-cWlfT.js → swapHorizontalBold-yQqq0yPi.js} +1 -1
- package/packages/ui/dist/assets/{swapHorizontalMedium-gIIVXh3I.js → swapHorizontalMedium-NafYmdDj.js} +1 -1
- package/packages/ui/dist/assets/{swapHorizontalRoundedBold-CwtQpc9E.js → swapHorizontalRoundedBold-BUn0GwEK.js} +1 -1
- package/packages/ui/dist/assets/{swapVertical-wrdYsAkk.js → swapVertical-Be1m6suD.js} +1 -1
- package/packages/ui/dist/assets/{telegram-3aqbBSMX.js → telegram-D-u7QHT5.js} +1 -1
- package/packages/ui/dist/assets/{three-dots-BNRpZX3L.js → three-dots-B6Y4DFOR.js} +1 -1
- package/packages/ui/dist/assets/{twitch-Bya9c1W1.js → twitch-DSy1rhWQ.js} +1 -1
- package/packages/ui/dist/assets/{twitterIcon-1thOj_Z5.js → twitterIcon-tac1plSa.js} +1 -1
- package/packages/ui/dist/assets/{verify-D8ggGSfI.js → verify-6MylluBY.js} +1 -1
- package/packages/ui/dist/assets/{verify-filled-CeZM7920.js → verify-filled-CZc0otb8.js} +1 -1
- package/packages/ui/dist/assets/{w3m-modal-CUwD6iiA.js → w3m-modal-DbT03Pyz.js} +1 -1
- package/packages/ui/dist/assets/{wallet-Baz0DeMJ.js → wallet-473-ObZE.js} +1 -1
- package/packages/ui/dist/assets/{wallet-placeholder-3yAnyuC4.js → wallet-placeholder-B9h3WvTk.js} +1 -1
- package/packages/ui/dist/assets/{walletconnect-CfkgbqAq.js → walletconnect-5GHIf5FR.js} +1 -1
- package/packages/ui/dist/assets/{warning-circle-BfOqu3HN.js → warning-circle-8A009Dx3.js} +1 -1
- package/packages/ui/dist/assets/{x-CxAcJv-O.js → x-B4jK8e8X.js} +1 -1
- package/packages/ui/dist/index.html +1 -1
- package/templates/default/AGENTS.md +18 -1
- package/packages/ui/dist/assets/cursor-BWdjWDLC.js +0 -3
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
// Protocol : Sail batch dispatch (IBatchPermission) — protocol-agnostic
|
|
6
|
+
// Version : Single-tenant, constructor-configured (mirrors the multi-tenant
|
|
7
|
+
// SharedApproveAndCallBatchPermission shape from SailProtocol)
|
|
8
|
+
// Chain : Any EVM with a SELECTIVE kernel (Base, Arbitrum, Unichain, Base Sepolia)
|
|
9
|
+
//
|
|
10
|
+
// WHAT THIS TEACHES — the atomic approve → call → reset pattern.
|
|
11
|
+
// A bare ERC-20 approve is dangerous: it leaves a standing allowance an attacker can drain.
|
|
12
|
+
// The safe shape is to approve, consume the allowance in the SAME transaction, then reset it
|
|
13
|
+
// to zero — all-or-nothing. No single-call IPermission can enforce "the reset must be the
|
|
14
|
+
// final call", because per-call evaluation never sees the other calls. A batch permission
|
|
15
|
+
// sees the WHOLE sequence at once, so it can.
|
|
16
|
+
//
|
|
17
|
+
// Gated via the kernel's dispatchBatch (NOT dispatch). This contract's single-call evaluate()
|
|
18
|
+
// deliberately returns false — it is batch-only. The manager names this permission in the
|
|
19
|
+
// batch signature; only it is consulted (selective model).
|
|
20
|
+
//
|
|
21
|
+
// ENFORCES ON-CHAIN (kernel calls evaluateBatch() via staticcall; false/revert ⇒ batch blocked):
|
|
22
|
+
// The batch MUST be exactly these 3 calls, in this order, each with value == 0:
|
|
23
|
+
// calls[0] = approve(spender,amount) selector 0x095ea7b3 on an allowlisted token
|
|
24
|
+
// • token (calls[0].target) must be in ALLOWED_TOKENS (cap > 0)
|
|
25
|
+
// • spender must be in ALLOWED_SPENDERS
|
|
26
|
+
// • 0 < amount ≤ maxApprovalAmount[token]
|
|
27
|
+
// calls[1] = <consuming call> on an allowlisted (target, selector)
|
|
28
|
+
// • target must be in ALLOWED_CONSUMING_TARGETS
|
|
29
|
+
// • selector must be in ALLOWED_CONSUMING_SELECTORS
|
|
30
|
+
// • if REQUIRE_AMOUNT_MATCH: c1.data[4:36] (the first ABI-encoded argument of the
|
|
31
|
+
// consuming call) must equal the approve amount. Only enable this flag if the
|
|
32
|
+
// consuming function's FIRST parameter is the amount — if the amount appears in a
|
|
33
|
+
// later position the check reads the wrong slot and is silently bypassed.
|
|
34
|
+
// calls[2] = approve(spender,0) selector 0x095ea7b3 — mandatory reset
|
|
35
|
+
// • same token and same spender as calls[0]
|
|
36
|
+
// • amount must be exactly 0
|
|
37
|
+
// Any deviation (wrong length, wrong token/spender/target/selector, over-cap, non-zero reset,
|
|
38
|
+
// reordering, non-zero value, malformed calldata) ⇒ false or revert.
|
|
39
|
+
//
|
|
40
|
+
// ⚠ FUND DESTINATION NOT BOUNDED: unlike every single-call permission in this repo, this contract
|
|
41
|
+
// makes NO assertion that the consuming call routes funds to ctx.account. The recipient, receiver,
|
|
42
|
+
// or beneficiary inside calls[1] is unchecked. An agent bug that names an external address in the
|
|
43
|
+
// consuming call's arguments will not be blocked here. To close this gap, either:
|
|
44
|
+
// a) use a consuming target/selector that structurally routes output to msg.sender (the SMA), or
|
|
45
|
+
// b) pair this contract with a protocol-specific single-call permission that bounds the recipient.
|
|
46
|
+
//
|
|
47
|
+
// AGENT-ENFORCED / NOT BOUNDED HERE (off-chain — can change without redeploying this contract):
|
|
48
|
+
// • The full calldata of the consuming call beyond its leading uint256 (recipients, paths, etc.)
|
|
49
|
+
// — bound those with a protocol-specific permission if the consuming call needs tighter limits.
|
|
50
|
+
//
|
|
51
|
+
// VERIFY BEFORE USE:
|
|
52
|
+
// • approve selector 0x095ea7b3 is the ERC-20 standard. ALLOWED_CONSUMING_SELECTORS must match
|
|
53
|
+
// the real selector(s) of the protocol call you intend to bracket.
|
|
54
|
+
// • Per-token caps are in each token's base units (decimals differ — USDC 6, WETH 18).
|
|
55
|
+
// • Requires a SELECTIVE kernel (dispatchBatch exists). Conjunctive kernels have no batch path.
|
|
56
|
+
// • `sailor mandate simulate` probes single evaluate() only; it cannot probe evaluateBatch().
|
|
57
|
+
// Verify this contract by calling evaluateBatch(calls, ctx) directly (eth_call) with PASS/FAIL
|
|
58
|
+
// batches before authorizing on-chain.
|
|
59
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
import {IPermission, Context} from "@sail/interfaces/IPermission.sol";
|
|
62
|
+
import {IBatchPermission, Call, BatchContext} from "@sail/interfaces/IBatchPermission.sol";
|
|
63
|
+
|
|
64
|
+
contract BoundedApproveAndCallBatch is IPermission, IBatchPermission {
|
|
65
|
+
bytes32 private constant DISCRIMINATOR = keccak256("BoundedApproveAndCallBatch");
|
|
66
|
+
|
|
67
|
+
bytes4 private constant SEL_APPROVE = 0x095ea7b3; // approve(address,uint256)
|
|
68
|
+
uint256 private constant APPROVE_CALLDATA_LEN = 68; // 4 + 32 (spender) + 32 (amount)
|
|
69
|
+
uint256 private constant CONSUMING_MIN_LEN = 36; // 4 + 32 (leading uint256 arg)
|
|
70
|
+
|
|
71
|
+
mapping(address => uint256) public maxApprovalAmount; // token => cap (0 = not allowed)
|
|
72
|
+
mapping(address => bool) public isSpender; // spender allowlist
|
|
73
|
+
mapping(address => bool) public isConsumingTarget; // consuming-call target allowlist
|
|
74
|
+
mapping(bytes4 => bool) public isConsumingSelector; // consuming-call selector allowlist
|
|
75
|
+
bool public immutable REQUIRE_AMOUNT_MATCH;
|
|
76
|
+
|
|
77
|
+
/// @param tokens Allowlisted ERC-20 tokens that may be approved
|
|
78
|
+
/// @param maxApprovalAmounts Per-token approve cap, index-parallel with `tokens` (each > 0)
|
|
79
|
+
/// @param spenders Allowlisted spenders that may receive the allowance
|
|
80
|
+
/// @param consumingTargets Allowlisted targets for the middle (consuming) call
|
|
81
|
+
/// @param consumingSelectors Allowlisted selectors for the middle (consuming) call
|
|
82
|
+
/// @param requireAmountMatch If true, the consuming call's leading uint256 must equal the approve amount
|
|
83
|
+
constructor(
|
|
84
|
+
address[] memory tokens,
|
|
85
|
+
uint256[] memory maxApprovalAmounts,
|
|
86
|
+
address[] memory spenders,
|
|
87
|
+
address[] memory consumingTargets,
|
|
88
|
+
bytes4[] memory consumingSelectors,
|
|
89
|
+
bool requireAmountMatch
|
|
90
|
+
) {
|
|
91
|
+
require(tokens.length == maxApprovalAmounts.length, "tokens/amounts length mismatch");
|
|
92
|
+
require(tokens.length > 0 && spenders.length > 0, "empty token/spender allowlist");
|
|
93
|
+
require(consumingTargets.length > 0 && consumingSelectors.length > 0, "empty consuming allowlist");
|
|
94
|
+
|
|
95
|
+
for (uint256 i = 0; i < tokens.length; i++) {
|
|
96
|
+
require(tokens[i] != address(0) && maxApprovalAmounts[i] > 0, "bad token/cap");
|
|
97
|
+
maxApprovalAmount[tokens[i]] = maxApprovalAmounts[i];
|
|
98
|
+
}
|
|
99
|
+
for (uint256 i = 0; i < spenders.length; i++) {
|
|
100
|
+
require(spenders[i] != address(0), "zero spender");
|
|
101
|
+
isSpender[spenders[i]] = true;
|
|
102
|
+
}
|
|
103
|
+
for (uint256 i = 0; i < consumingTargets.length; i++) {
|
|
104
|
+
require(consumingTargets[i] != address(0), "zero target");
|
|
105
|
+
isConsumingTarget[consumingTargets[i]] = true;
|
|
106
|
+
}
|
|
107
|
+
for (uint256 i = 0; i < consumingSelectors.length; i++) {
|
|
108
|
+
require(consumingSelectors[i] != bytes4(0), "zero selector");
|
|
109
|
+
isConsumingSelector[consumingSelectors[i]] = true;
|
|
110
|
+
}
|
|
111
|
+
REQUIRE_AMOUNT_MATCH = requireAmountMatch;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── IBatchPermission ─────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
/// @inheritdoc IBatchPermission
|
|
117
|
+
function isBatchPermission() external pure returns (bool) { return true; }
|
|
118
|
+
|
|
119
|
+
/// @inheritdoc IBatchPermission
|
|
120
|
+
function evaluateBatch(Call[] calldata calls, BatchContext calldata ctx) external view returns (bool) {
|
|
121
|
+
ctx; // batch context unused — bounds depend only on the call sequence, not the SMA
|
|
122
|
+
if (calls.length != 3) return false;
|
|
123
|
+
|
|
124
|
+
// ── calls[0]: approve(spender, amount) on an allowlisted token ───────
|
|
125
|
+
Call calldata c0 = calls[0];
|
|
126
|
+
if (c0.value != 0) return false;
|
|
127
|
+
if (c0.data.length != APPROVE_CALLDATA_LEN) return false;
|
|
128
|
+
if (bytes4(c0.data[0:4]) != SEL_APPROVE) return false;
|
|
129
|
+
|
|
130
|
+
address token = c0.target;
|
|
131
|
+
uint256 cap = maxApprovalAmount[token];
|
|
132
|
+
if (cap == 0) return false; // token not allowlisted
|
|
133
|
+
|
|
134
|
+
(address spender, uint256 approveAmount) = _decodeApprove(c0.data);
|
|
135
|
+
if (!isSpender[spender]) return false;
|
|
136
|
+
if (approveAmount == 0) return false;
|
|
137
|
+
if (approveAmount > cap) return false;
|
|
138
|
+
|
|
139
|
+
// ── calls[1]: consuming call on an allowlisted (target, selector) ────
|
|
140
|
+
Call calldata c1 = calls[1];
|
|
141
|
+
if (c1.value != 0) return false;
|
|
142
|
+
if (!isConsumingTarget[c1.target]) return false;
|
|
143
|
+
if (c1.data.length < CONSUMING_MIN_LEN) return false;
|
|
144
|
+
if (!isConsumingSelector[bytes4(c1.data[0:4])]) return false;
|
|
145
|
+
if (REQUIRE_AMOUNT_MATCH) {
|
|
146
|
+
if (uint256(bytes32(c1.data[4:36])) != approveAmount) return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── calls[2]: approve(spender, 0) — mandatory reset of same token+spender ─
|
|
150
|
+
Call calldata c2 = calls[2];
|
|
151
|
+
if (c2.value != 0) return false;
|
|
152
|
+
if (c2.target != token) return false;
|
|
153
|
+
if (c2.data.length != APPROVE_CALLDATA_LEN) return false;
|
|
154
|
+
if (bytes4(c2.data[0:4]) != SEL_APPROVE) return false;
|
|
155
|
+
|
|
156
|
+
(address resetSpender, uint256 resetAmount) = _decodeApprove(c2.data);
|
|
157
|
+
if (resetSpender != spender) return false;
|
|
158
|
+
if (resetAmount != 0) return false;
|
|
159
|
+
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── IPermission (batch-only: single dispatch is never authorised) ────────
|
|
164
|
+
|
|
165
|
+
/// @inheritdoc IPermission
|
|
166
|
+
function evaluate(bytes calldata, Context calldata) external pure returns (bool) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/// @inheritdoc IPermission
|
|
171
|
+
function discriminator() external pure returns (bytes32) { return DISCRIMINATOR; }
|
|
172
|
+
|
|
173
|
+
// ── internal calldata decoding (bounds-checked by callers above) ─────────
|
|
174
|
+
|
|
175
|
+
function _decodeApprove(bytes calldata data) internal pure returns (address spender, uint256 amount) {
|
|
176
|
+
spender = address(uint160(uint256(bytes32(data[4:36]))));
|
|
177
|
+
amount = uint256(bytes32(data[36:68]));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -21,14 +21,15 @@ pragma solidity 0.8.26;
|
|
|
21
21
|
// Deploying this contract with an unverified ABI may silently PASS or FAIL
|
|
22
22
|
// all dispatches depending on whether the selector matches.
|
|
23
23
|
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
24
|
+
// ENFORCES ON-CHAIN (kernel calls evaluate() on every dispatch; false ⇒ dispatch blocked)
|
|
25
|
+
// — ASSUMING the unverified ABI above is correct:
|
|
26
|
+
// buy(bytes32 conditionId,uint256 amount,uint256 outcomeIndex) selector = keccak256(sig)[0:4]
|
|
27
|
+
// • target must be LIMITLESS_EXCHANGE
|
|
28
|
+
// • conditionId must be in ALLOWED_CONDITIONS
|
|
29
|
+
// • amount ≤ MAX_STAKE
|
|
30
|
+
// • outcomeIndex must be in ALLOWED_OUTCOMES
|
|
30
31
|
//
|
|
31
|
-
// NOT
|
|
32
|
+
// AGENT-ENFORCED / NOT BOUNDED HERE (off-chain — can change without redeploying this contract):
|
|
32
33
|
// • Market price / odds (on-chain prediction market prices fluctuate)
|
|
33
34
|
// • Timing / frequency of bets
|
|
34
35
|
//
|
|
@@ -4,24 +4,24 @@ pragma solidity 0.8.26;
|
|
|
4
4
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
5
|
// Protocol : Aave V3
|
|
6
6
|
// Version : Pool (proxy) — fully on-chain, oracle-based liquidation
|
|
7
|
-
// Chain : Arbitrum mainnet
|
|
8
|
-
// Target : Aave V3 Pool 0x794a61358D6845594F94dc1DB02A252b5b4814aD
|
|
7
|
+
// Chain : Arbitrum mainnet (42161)
|
|
8
|
+
// Target : Aave V3 Pool 0x794a61358D6845594F94dc1DB02A252b5b4814aD (verified on Arbiscan)
|
|
9
9
|
//
|
|
10
|
-
//
|
|
11
|
-
// borrow(address asset,
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
10
|
+
// ENFORCES ON-CHAIN (kernel calls evaluate() on every dispatch; false ⇒ dispatch blocked):
|
|
11
|
+
// borrow(address asset,uint256 amount,uint256 interestRateMode,uint16 referralCode,address onBehalfOf)
|
|
12
|
+
// selector 0xa415bcad
|
|
13
|
+
// • target must be AAVE_POOL
|
|
14
|
+
// • asset must be in ALLOWED_ASSETS
|
|
15
|
+
// • amount ≤ MAX_BORROW_AMOUNT
|
|
16
|
+
// • onBehalfOf must equal ctx.account (the SMA — agent cannot borrow on behalf of others)
|
|
17
|
+
// • interestRateMode must be in ALLOWED_RATE_MODES
|
|
18
|
+
// (1 = stable [deprecated in V3.1], 2 = variable; restrict to [2] for V3.1+)
|
|
19
19
|
//
|
|
20
|
-
// NOT
|
|
20
|
+
// AGENT-ENFORCED / NOT BOUNDED HERE (off-chain — can change without redeploying this contract):
|
|
21
21
|
// • Health factor management — the kernel cannot check post-borrow health factor
|
|
22
22
|
// • referralCode (informational only, does not affect fund safety)
|
|
23
23
|
// • Repayment timing — agent decides when to repay
|
|
24
|
-
// • Collateral composition — managed by prior deposit permissions
|
|
24
|
+
// • Collateral composition — managed by prior deposit/supply permissions
|
|
25
25
|
//
|
|
26
26
|
// VERIFY BEFORE USE:
|
|
27
27
|
// • Confirm Aave V3 Pool address on Arbitrum (0x794a... — verify on Arbiscan).
|
|
@@ -5,44 +5,48 @@ pragma solidity 0.8.26;
|
|
|
5
5
|
// Protocol : GMX V2 (gmx-synthetics)
|
|
6
6
|
// Version : ExchangeRouter / OrderHandler — fully on-chain oracle execution
|
|
7
7
|
// NOT Hyperliquid (off-chain order book — permissions cannot bound orders)
|
|
8
|
-
// Chain : Arbitrum mainnet
|
|
9
|
-
// Target : ExchangeRouter 0x7c68c7866a64fa2160f78eeae12217ffbf871fa8
|
|
10
|
-
// (verify on Arbiscan — GMX may redeploy; check their official docs)
|
|
8
|
+
// Chain : Arbitrum mainnet (42161)
|
|
11
9
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
10
|
+
// ⚠ REFERENCE PATTERN — VERIFY SELECTOR, STRUCT, AND ROUTER AGAINST THE LIVE GMX ABI ⚠
|
|
11
|
+
// GMX runs MULTIPLE versioned ExchangeRouter deployments on Arbitrum (e.g.
|
|
12
|
+
// 0x7c68c7866a64fa2160f78eeae12217ffbf871fa8, 0x602b805EedddBbD9ddff44A7dcBD46cb07849685,
|
|
13
|
+
// and others) and HAS EVOLVED the CreateOrderParams struct over time (it added
|
|
14
|
+
// `cancellationReceiver` to the addresses tuple and a trailing `dataList` bytes32[]).
|
|
15
|
+
// The struct + selector below are taken from the CURRENT canonical source
|
|
16
|
+
// (gmx-io/gmx-synthetics, contracts/order/IBaseOrderUtils.sol, main branch) and are
|
|
17
|
+
// mutually consistent — but the specific router YOU target may run an OLDER struct
|
|
18
|
+
// with a DIFFERENT selector. Selector mismatch ⇒ evaluate() returns false for every
|
|
19
|
+
// legitimate order (fail-closed: safe, but the permission silently does nothing useful).
|
|
20
|
+
// Before deploying you MUST:
|
|
21
|
+
// 1. Pick the exact ExchangeRouter your agent will call and read its verified ABI.
|
|
22
|
+
// 2. Confirm its createOrder selector == SEL_CREATE_ORDER below (recompute with
|
|
23
|
+
// `cast sig "createOrder(<exact tuple>)"`); if not, update SEL_CREATE_ORDER and
|
|
24
|
+
// the inline struct to match that router's version.
|
|
25
|
+
// 3. Set EXCHANGE_ROUTER (constructor arg) to that same router address.
|
|
19
26
|
//
|
|
20
|
-
//
|
|
27
|
+
// ENFORCES ON-CHAIN (kernel calls evaluate() on every dispatch; false ⇒ dispatch blocked):
|
|
28
|
+
// createOrder(IBaseOrderUtils.CreateOrderParams) selector 0x212234c3 (current canonical struct)
|
|
29
|
+
// • target must be EXCHANGE_ROUTER
|
|
30
|
+
// • market must be in ALLOWED_MARKETS
|
|
31
|
+
// • initialCollateralDeltaAmount ≤ MAX_COLLATERAL_AMOUNT
|
|
32
|
+
// • sizeDeltaUsd ≤ MAX_SIZE_DELTA_USD
|
|
33
|
+
// • isLong must be allowed (ALLOW_LONG / ALLOW_SHORT)
|
|
34
|
+
//
|
|
35
|
+
// AGENT-ENFORCED / NOT BOUNDED HERE (off-chain — can change without redeploying this contract):
|
|
21
36
|
// • Leverage ratio: sizeDeltaUsd vs collateralDeltaAmount are bounded separately
|
|
22
|
-
// but their ratio (effective leverage) is not
|
|
23
|
-
// it depends on collateral price. Add a price-oracle leverage check if needed.
|
|
37
|
+
// but their ratio (effective leverage) is not enforced — it depends on collateral price.
|
|
24
38
|
// • acceptablePrice / triggerPrice: not bounded — agent controls entry price.
|
|
25
|
-
// Add bounds if the strategy requires a price range check.
|
|
26
39
|
// • decreasePositionSwapType, shouldUnwrapNativeToken, autoCancel: not bounded.
|
|
27
|
-
// • swapPath (inside
|
|
28
|
-
//
|
|
29
|
-
// • receiver address: not bounded — set to ctx.account in your agent.
|
|
30
|
-
//
|
|
31
|
-
// STRUCT LAYOUT NOTE:
|
|
32
|
-
// This contract defines CreateOrderParams inline. It must match EXACTLY the
|
|
33
|
-
// struct layout in the deployed ExchangeRouter's IBaseOrderUtils. If GMX
|
|
34
|
-
// updates the struct (e.g. adds a field), this permission will misparse calldata
|
|
35
|
-
// and return false (fail closed — safe but non-functional).
|
|
36
|
-
// VERIFY against BaseOrderUtils.sol at:
|
|
37
|
-
// https://github.com/gmx-io/gmx-synthetics/blob/main/contracts/order/BaseOrderUtils.sol
|
|
40
|
+
// • swapPath (inside the addresses tuple): not bounded — any intermediate tokens allowed.
|
|
41
|
+
// • receiver / cancellationReceiver: not bounded — set to ctx.account in your agent.
|
|
38
42
|
//
|
|
39
43
|
// VERIFY BEFORE USE:
|
|
40
|
-
// •
|
|
41
|
-
//
|
|
42
|
-
// Verify against the deployed contract's ABI tab on Arbiscan.
|
|
44
|
+
// • SEL_CREATE_ORDER = 0x212234c3 was computed (via `cast sig`) from the CURRENT canonical
|
|
45
|
+
// tuple. Older routers differ — see the loud banner above. ALWAYS reconfirm.
|
|
43
46
|
// • sizeDeltaUsd is in USD with 30 decimals (GMX V2 standard). E.g. $1000 = 1e33.
|
|
44
47
|
// • initialCollateralDeltaAmount is in collateral token base units.
|
|
45
|
-
// • Test with real calldata samples from
|
|
48
|
+
// • Test with real calldata samples from your chosen GMX router before mainnet.
|
|
49
|
+
// • Source: https://github.com/gmx-io/gmx-synthetics/blob/main/contracts/order/IBaseOrderUtils.sol
|
|
46
50
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
47
51
|
|
|
48
52
|
import {IPermission, Context} from "@sail/interfaces/IPermission.sol";
|
|
@@ -59,23 +63,29 @@ contract BoundedPerp_GMXv2_Arbitrum is IPermission {
|
|
|
59
63
|
bool public immutable ALLOW_SHORT;
|
|
60
64
|
|
|
61
65
|
// createOrder(IBaseOrderUtils.CreateOrderParams)
|
|
62
|
-
//
|
|
63
|
-
|
|
66
|
+
// Computed via `cast sig` (split across lines for readability — paste as one string in the shell):
|
|
67
|
+
// "createOrder((address,address,address,address,address,address,address[]),"
|
|
68
|
+
// "(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256),"
|
|
69
|
+
// "uint8,uint8,bool,bool,bool,bytes32,bytes32[])"
|
|
70
|
+
// == 0x212234c3
|
|
71
|
+
// ⚠ Older GMX routers use an earlier struct (no cancellationReceiver / no dataList) and a
|
|
72
|
+
// DIFFERENT selector. Reconfirm against your chosen router's ABI — see header banner.
|
|
73
|
+
bytes4 private constant SEL_CREATE_ORDER = 0x212234c3;
|
|
64
74
|
|
|
65
|
-
// ── Inline struct definitions —
|
|
66
|
-
//
|
|
67
|
-
// https://github.com/gmx-io/gmx-synthetics/blob/main/contracts/order/BaseOrderUtils.sol
|
|
75
|
+
// ── Inline struct definitions — match the CURRENT canonical IBaseOrderUtils.CreateOrderParams ──
|
|
76
|
+
// Source: https://github.com/gmx-io/gmx-synthetics/blob/main/contracts/order/IBaseOrderUtils.sol
|
|
68
77
|
|
|
69
|
-
struct
|
|
78
|
+
struct CreateOrderParamsAddresses {
|
|
70
79
|
address receiver;
|
|
80
|
+
address cancellationReceiver; // added in a later GMX version — present in current struct
|
|
71
81
|
address callbackContract;
|
|
72
82
|
address uiFeeReceiver;
|
|
73
83
|
address market;
|
|
74
84
|
address initialCollateralToken;
|
|
75
|
-
address[] swapPath;
|
|
85
|
+
address[] swapPath; // dynamic — makes this struct dynamic
|
|
76
86
|
}
|
|
77
87
|
|
|
78
|
-
struct
|
|
88
|
+
struct CreateOrderParamsNumbers {
|
|
79
89
|
uint256 sizeDeltaUsd;
|
|
80
90
|
uint256 initialCollateralDeltaAmount;
|
|
81
91
|
uint256 triggerPrice;
|
|
@@ -87,14 +97,15 @@ contract BoundedPerp_GMXv2_Arbitrum is IPermission {
|
|
|
87
97
|
}
|
|
88
98
|
|
|
89
99
|
struct CreateOrderParams {
|
|
90
|
-
|
|
91
|
-
|
|
100
|
+
CreateOrderParamsAddresses addresses;
|
|
101
|
+
CreateOrderParamsNumbers numbers;
|
|
92
102
|
uint8 orderType;
|
|
93
103
|
uint8 decreasePositionSwapType;
|
|
94
104
|
bool isLong;
|
|
95
105
|
bool shouldUnwrapNativeToken;
|
|
96
106
|
bool autoCancel;
|
|
97
107
|
bytes32 referralCode;
|
|
108
|
+
bytes32[] dataList; // added in a later GMX version — present in current struct
|
|
98
109
|
}
|
|
99
110
|
|
|
100
111
|
/// @param exchangeRouter GMX V2 ExchangeRouter address
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
// Protocol : Venice (VVV) staking
|
|
6
|
+
// Version : sVVV staking contract (proxy) — fully on-chain
|
|
7
|
+
// Chain : Base mainnet (8453)
|
|
8
|
+
// Target : Staking proxy 0x321b7ff75154472B18EDb199033fF4D116F340Ff ("Staked Venice Token")
|
|
9
|
+
// (impl 0xe37a7920dbc11253ac6d031c29f592f71b348dca — proxy is the stable target)
|
|
10
|
+
// Staked asset: VVV 0xacfE6019Ed1A7Dc6f7B508C02d1b04ec88cC21bf
|
|
11
|
+
//
|
|
12
|
+
// ⚠ SELECTOR NOTE — the function is stake(address,uint256), NOT stake(uint256).
|
|
13
|
+
// Verified against the live contract (bytecode + 4byte registry): the staking entrypoint is
|
|
14
|
+
// stake(address recipient, uint256 amount) = 0xadc9772e. The single-arg stake(uint256)
|
|
15
|
+
// = 0xa694fc3a is ABSENT. Gating the wrong selector silently rejects every real stake
|
|
16
|
+
// (fail-closed but non-functional). ALWAYS confirm the selector against the contract you target.
|
|
17
|
+
//
|
|
18
|
+
// ENFORCES ON-CHAIN (kernel calls evaluate() on every dispatch; false ⇒ dispatch blocked):
|
|
19
|
+
// stake(address recipient,uint256 amount) selector 0xadc9772e
|
|
20
|
+
// • target must be STAKING_CONTRACT
|
|
21
|
+
// • amount ≤ MAX_STAKE_AMOUNT
|
|
22
|
+
// • recipient must equal ctx.account (the SMA — the staked position cannot be assigned
|
|
23
|
+
// to another address; verified empirically: this arg is the position beneficiary)
|
|
24
|
+
// claim() selector 0x4e71d92d
|
|
25
|
+
// • target must be STAKING_CONTRACT
|
|
26
|
+
// • takes NO recipient argument — rewards always accrue to the caller (the SMA when the
|
|
27
|
+
// kernel dispatches), so "claim-rewards-to-SMA-only" holds structurally
|
|
28
|
+
//
|
|
29
|
+
// AGENT-ENFORCED / NOT BOUNDED HERE (off-chain — can change without redeploying this contract):
|
|
30
|
+
// • Stake timing / cadence and how often rewards are claimed
|
|
31
|
+
// • Unstaking: this permission does NOT allow initiateUnstake(uint256) (0xae5ac921) — add it
|
|
32
|
+
// with its own bounds if the agent should manage exits; left out to keep this single-purpose.
|
|
33
|
+
// • Staked ASSET: enforced TRANSITIVELY via STAKING_CONTRACT (the contract accepts one fixed
|
|
34
|
+
// token, VVV). The stake calldata carries no token field, so the asset is pinned by the target.
|
|
35
|
+
//
|
|
36
|
+
// VERIFY BEFORE USE:
|
|
37
|
+
// • Confirm STAKING_CONTRACT and that its stake selector is 0xadc9772e on the contract you target.
|
|
38
|
+
// • Confirm the stake address arg is the position recipient (not, e.g., a token address) on your
|
|
39
|
+
// contract — on Venice it is the recipient (verified by staking to a third party on a fork).
|
|
40
|
+
// • MAX_STAKE_AMOUNT is in VVV base units (18 decimals).
|
|
41
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
import {IPermission, Context} from "@sail/interfaces/IPermission.sol";
|
|
44
|
+
|
|
45
|
+
contract BoundedStake_Venice_Base is IPermission {
|
|
46
|
+
bytes32 private constant DISCRIMINATOR = keccak256("BoundedStake_Venice_Base");
|
|
47
|
+
|
|
48
|
+
address public immutable STAKING_CONTRACT;
|
|
49
|
+
uint256 public immutable MAX_STAKE_AMOUNT;
|
|
50
|
+
|
|
51
|
+
// Verified against the live Venice staking contract (bytecode + 4byte registry):
|
|
52
|
+
bytes4 private constant SEL_STAKE = 0xadc9772e; // stake(address recipient,uint256 amount)
|
|
53
|
+
bytes4 private constant SEL_CLAIM = 0x4e71d92d; // claim()
|
|
54
|
+
|
|
55
|
+
/// @param stakingContract Venice staking contract (the proxy address)
|
|
56
|
+
/// @param maxStakeAmount Per-stake cap in VVV base units (18 decimals; must be > 0)
|
|
57
|
+
constructor(address stakingContract, uint256 maxStakeAmount) {
|
|
58
|
+
require(stakingContract != address(0), "zero staking contract");
|
|
59
|
+
require(maxStakeAmount > 0, "zero stake cap");
|
|
60
|
+
STAKING_CONTRACT = stakingContract;
|
|
61
|
+
MAX_STAKE_AMOUNT = maxStakeAmount;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool) {
|
|
65
|
+
if (ctx.target != STAKING_CONTRACT) return false;
|
|
66
|
+
|
|
67
|
+
// stake(address recipient, uint256 amount)
|
|
68
|
+
if (ctx.selector == SEL_STAKE) {
|
|
69
|
+
if (txData.length < 4 + 2 * 32) return false;
|
|
70
|
+
(address recipient, uint256 amount) = abi.decode(txData[4:], (address, uint256));
|
|
71
|
+
if (amount > MAX_STAKE_AMOUNT) return false;
|
|
72
|
+
if (recipient != ctx.account) return false; // stake only to the SMA's own position
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// claim() — no args; rewards go to the caller (the SMA). No recipient to bound.
|
|
77
|
+
if (ctx.selector == SEL_CLAIM) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function discriminator() external pure returns (bytes32) { return DISCRIMINATOR; }
|
|
85
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
// Protocol : Aave V3
|
|
6
|
+
// Version : Pool (proxy) — fully on-chain, oracle-based accounting
|
|
7
|
+
// Chain : Arbitrum mainnet (42161)
|
|
8
|
+
// Target : Aave V3 Pool 0x794a61358D6845594F94dc1DB02A252b5b4814aD (verified on Arbiscan;
|
|
9
|
+
// same Pool as BoundedBorrow_AaveV3_Arbitrum — supply 0x617ba037 confirmed present
|
|
10
|
+
// in the live implementation 0xf05fd3cc...)
|
|
11
|
+
//
|
|
12
|
+
// ENFORCES ON-CHAIN (kernel calls evaluate() on every dispatch; false ⇒ dispatch blocked):
|
|
13
|
+
// supply(address asset,uint256 amount,address onBehalfOf,uint16 referralCode) selector 0x617ba037
|
|
14
|
+
// • target must be AAVE_POOL
|
|
15
|
+
// • asset must be in ALLOWED_ASSETS
|
|
16
|
+
// • amount ≤ MAX_SUPPLY_AMOUNT
|
|
17
|
+
// • onBehalfOf must equal ctx.account (the SMA — collateral is credited to the SMA, not
|
|
18
|
+
// to another account)
|
|
19
|
+
//
|
|
20
|
+
// AGENT-ENFORCED / NOT BOUNDED HERE (off-chain — can change without redeploying this contract):
|
|
21
|
+
// • referralCode (informational only, does not affect fund safety)
|
|
22
|
+
// • Supply timing / cadence and choice of asset within ALLOWED_ASSETS
|
|
23
|
+
// • Withdrawal: not gated here (add a withdraw(asset,amount,to) permission with to==SMA if needed)
|
|
24
|
+
// • A supply also needs a prior ERC-20 approve of the Pool — gate that separately
|
|
25
|
+
// (see BoundedApproveAndCallBatch.sol for the atomic approve→call→reset pattern).
|
|
26
|
+
//
|
|
27
|
+
// VERIFY BEFORE USE:
|
|
28
|
+
// • Selector 0x617ba037 = supply(address,uint256,address,uint16) — verified via `cast sig`
|
|
29
|
+
// and confirmed present in the deployed Pool implementation on Arbitrum.
|
|
30
|
+
// • This is the canonical Aave V3 Pool.supply. (V3.x also exposes supplyWithPermit — not
|
|
31
|
+
// gated here; add it if your agent supplies via EIP-2612 permits.)
|
|
32
|
+
// • MAX_SUPPLY_AMOUNT is in the asset's base units (e.g. USDC = 6 decimals).
|
|
33
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
import {IPermission, Context} from "@sail/interfaces/IPermission.sol";
|
|
36
|
+
|
|
37
|
+
contract BoundedSupply_AaveV3_Arbitrum is IPermission {
|
|
38
|
+
bytes32 private constant DISCRIMINATOR = keccak256("BoundedSupply_AaveV3_Arbitrum");
|
|
39
|
+
|
|
40
|
+
address public immutable AAVE_POOL;
|
|
41
|
+
mapping(address => bool) public isAllowedAsset;
|
|
42
|
+
uint256 public immutable MAX_SUPPLY_AMOUNT;
|
|
43
|
+
|
|
44
|
+
// supply(address,uint256,address,uint16) — verified 0x617ba037
|
|
45
|
+
bytes4 private constant SEL_SUPPLY = 0x617ba037;
|
|
46
|
+
|
|
47
|
+
/// @param aavePool Aave V3 Pool proxy address
|
|
48
|
+
/// @param allowedAssets Assets the agent may supply (must be non-empty)
|
|
49
|
+
/// @param maxSupplyAmount Per-call supply cap in asset base units (must be > 0)
|
|
50
|
+
constructor(address aavePool, address[] memory allowedAssets, uint256 maxSupplyAmount) {
|
|
51
|
+
require(aavePool != address(0), "zero pool address");
|
|
52
|
+
require(allowedAssets.length > 0, "empty asset allowlist");
|
|
53
|
+
require(maxSupplyAmount > 0, "zero supply cap");
|
|
54
|
+
AAVE_POOL = aavePool;
|
|
55
|
+
MAX_SUPPLY_AMOUNT = maxSupplyAmount;
|
|
56
|
+
for (uint256 i = 0; i < allowedAssets.length; i++) {
|
|
57
|
+
require(allowedAssets[i] != address(0), "zero asset address");
|
|
58
|
+
isAllowedAsset[allowedAssets[i]] = true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool) {
|
|
63
|
+
if (ctx.target != AAVE_POOL) return false;
|
|
64
|
+
if (ctx.selector != SEL_SUPPLY) return false;
|
|
65
|
+
// supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode)
|
|
66
|
+
// = 4 ABI-encoded 32-byte slots after the 4-byte selector
|
|
67
|
+
if (txData.length < 4 + 4 * 32) return false;
|
|
68
|
+
|
|
69
|
+
(
|
|
70
|
+
address asset,
|
|
71
|
+
uint256 amount,
|
|
72
|
+
address onBehalfOf,
|
|
73
|
+
/* uint16 referralCode — not bounded */
|
|
74
|
+
) = abi.decode(txData[4:], (address, uint256, address, uint16));
|
|
75
|
+
|
|
76
|
+
if (!isAllowedAsset[asset]) return false;
|
|
77
|
+
if (amount > MAX_SUPPLY_AMOUNT) return false;
|
|
78
|
+
if (onBehalfOf != ctx.account) return false; // collateral credited only to the SMA
|
|
79
|
+
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function discriminator() external pure returns (bytes32) { return DISCRIMINATOR; }
|
|
84
|
+
}
|
|
@@ -4,29 +4,31 @@ pragma solidity 0.8.26;
|
|
|
4
4
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
5
|
// Protocol : Uniswap V3
|
|
6
6
|
// Version : SwapRouter02 (NOT the older SwapRouter — different selectors)
|
|
7
|
-
// Chain : Base mainnet
|
|
8
|
-
// Target : SwapRouter02 0x2626664c2603336E57B271c5C0b26F421741e481
|
|
7
|
+
// Chain : Base mainnet (8453)
|
|
8
|
+
// Target : SwapRouter02 0x2626664c2603336E57B271c5C0b26F421741e481 (verified on Basescan)
|
|
9
9
|
//
|
|
10
|
-
//
|
|
11
|
-
// exactInputSingle
|
|
10
|
+
// ENFORCES ON-CHAIN (kernel calls evaluate() on every dispatch; false ⇒ dispatch blocked):
|
|
11
|
+
// exactInputSingle((address,address,uint24,address,uint256,uint256,uint160)) selector 0x04e45aaf
|
|
12
12
|
// • target must be SWAP_ROUTER
|
|
13
13
|
// • tokenIn must equal FIXED_TOKEN_IN
|
|
14
14
|
// • tokenOut must be in ALLOWED_TOKENS_OUT
|
|
15
15
|
// • amountIn ≤ MAX_AMOUNT_IN
|
|
16
|
-
// • amountOutMinimum ≥ amountIn × MIN_BPS / 10 000 (slippage floor)
|
|
17
|
-
// approve
|
|
16
|
+
// • amountOutMinimum ≥ amountIn × MIN_BPS / 10 000 (slippage floor — see caveat below)
|
|
17
|
+
// approve(address,uint256) selector 0x095ea7b3
|
|
18
18
|
// • target must be FIXED_TOKEN_IN (the ERC-20 being approved)
|
|
19
19
|
// • spender must be SWAP_ROUTER
|
|
20
20
|
// • amount ≤ MAX_AMOUNT_IN
|
|
21
21
|
//
|
|
22
|
-
// NOT
|
|
22
|
+
// AGENT-ENFORCED / NOT BOUNDED HERE (off-chain — can change without redeploying this contract):
|
|
23
23
|
// • fee tier, sqrtPriceLimitX96, recipient address
|
|
24
24
|
// • swap frequency / cadence
|
|
25
|
+
// • real (cross-denomination) slippage — see MIN_BPS caveat in evaluate()
|
|
25
26
|
//
|
|
26
27
|
// VERIFY BEFORE USE:
|
|
27
|
-
// • Confirm SwapRouter02 address on your chain (Base default shown above).
|
|
28
|
+
// • Confirm SwapRouter02 address on your chain (Base default shown above; verified on Basescan).
|
|
28
29
|
// • Selector 0x04e45aaf = exactInputSingle((address,address,uint24,address,uint256,uint256,uint160))
|
|
29
|
-
// on SwapRouter02. The
|
|
30
|
+
// on SwapRouter02 (verified via `cast sig`). The OLDER SwapRouter's exactInputSingle (the
|
|
31
|
+
// deadline variant) is 0x414bf389 — a different selector; do not confuse the two.
|
|
30
32
|
// • Confirm that amountOutMinimum slippage floor fits your price-impact expectations for the
|
|
31
33
|
// chosen pool. Large amountIn values may trigger price impact beyond MIN_BPS.
|
|
32
34
|
// ─────────────────────────────────────────────────────────────────────────────
|