@dev.sail.money/sailor 1.2.0-74 → 1.2.0-76
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/README.md +4 -1
- package/examples/permissions/BoundedSwapNative_UniswapV3_Base.sol +123 -0
- package/examples/permissions/BoundedSwap_UniswapV3_Base.sol +20 -18
- package/examples/permissions/BoundedSwap_UniswapV4_Unichain.sol +17 -12
- package/examples/permissions/README.md +10 -0
- package/package.json +1 -1
- package/packages/cli/dist/index.cjs +401 -244
- package/packages/cli/dist/server.cjs +29 -1
- package/packages/sdk/dist/intelligence.d.ts +1 -1
- package/packages/sdk/dist/intelligence.js +1 -1
- package/scripts/check-docs.mjs +1 -1
- package/scripts/check-init.mjs +16 -13
- package/scripts/check-update.mjs +177 -0
- package/templates/default/.agents/skills/sail-automation/SKILL.md +50 -0
- package/templates/default/.agents/skills/sail-automation/references/docker-vm.md +113 -0
- package/templates/default/.agents/skills/sail-automation/references/github-actions.md +50 -0
- package/templates/default/.agents/skills/sail-automation/references/local-daemon.md +34 -0
- package/templates/default/.agents/skills/sail-automation/references/self-hosted-runner.md +72 -0
- package/templates/default/.agents/skills/sail-mandates/references/examples-index.md +5 -2
- package/templates/default/.agents/skills/sail-onboarding/SKILL.md +2 -0
- package/templates/default/AGENTS.md +1 -1
- package/templates/default/Dockerfile +18 -0
- package/templates/default/_dockerignore +15 -0
- package/templates/default/.agents/skills/sail-ci/SKILL.md +0 -63
package/README.md
CHANGED
|
@@ -62,7 +62,7 @@ The path from nothing to a running agent follows the protocol lifecycle:
|
|
|
62
62
|
2. **Author your permissions** — describe what the agent may do. Permission contracts encode the bounds: tokens, amounts, venues, call targets. Author them in the scaffolded Foundry workspace.
|
|
63
63
|
3. **Simulate, deploy, and sign your mandate** — `sailor mandate simulate` probes a permission off-chain before authorizing it. `sailor mandate deploy --attach` deploys and registers it on-chain. `sailor mandate sign` builds and signs the registration payload against live on-chain state.
|
|
64
64
|
4. **Run** — `sailor run` executes the agent loop. Three execution hosts compose: run it locally on a schedule, install it as a local OS service (`sailor service install` — launchd/systemd/Task Scheduler) that restarts on crash, or let the GitHub Actions cron workflow the scaffold provides run it. `sailor trigger github` fires that workflow on demand.
|
|
65
|
-
5. **Operate** — `sailor doctor` checks kernel health and gas balances; `sailor chains` lists supported chains and deployment addresses; `sailor session pause` instantly revokes dispatch rights without touching Safe custody.
|
|
65
|
+
5. **Operate** — `sailor doctor` checks kernel health and gas balances; `sailor chains` lists supported chains and deployment addresses; `sailor session pause` instantly revokes dispatch rights without touching Safe custody. After a CLI upgrade, `sailor update` resyncs agent tooling files (skills, `AGENTS.md`, `Dockerfile`) without touching user code or runtime state.
|
|
66
66
|
|
|
67
67
|
Run `npx sailor init my-agent`, open the scaffolded folder in Claude Code, Cursor, or any AI coding assistant, and say **"start"**.
|
|
68
68
|
|
|
@@ -158,6 +158,9 @@ sailor trigger github # fire the agent's GitHub Actions workflow on demand
|
|
|
158
158
|
|
|
159
159
|
# Dashboard
|
|
160
160
|
sailor ui start # prints the per-project dashboard URL
|
|
161
|
+
|
|
162
|
+
# Maintenance
|
|
163
|
+
sailor update # re-sync skills, AGENTS.md, and tooling files after a CLI upgrade
|
|
161
164
|
```
|
|
162
165
|
|
|
163
166
|
`sailor run` writes reverted transactions to stderr as `reverted: <txHash> (gas used: N)`; successful dispatches are appended to `.sail/activity.jsonl`.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
// Protocol : Uniswap V3 — NATIVE-ASSET (ETH) swap
|
|
6
|
+
// Version : SwapRouter02 (NOT the older SwapRouter — different selectors)
|
|
7
|
+
// Chain : Base mainnet (8453)
|
|
8
|
+
// Target : SwapRouter02 0x2626664c2603336E57B271c5C0b26F421741e481 (verified on Basescan)
|
|
9
|
+
//
|
|
10
|
+
// WHY A SEPARATE EXAMPLE FOR NATIVE ETH:
|
|
11
|
+
// When the asset being spent is the chain's NATIVE asset (ETH), the value that actually
|
|
12
|
+
// leaves the account is the call's `msg.value` — exposed to the permission as `Context.value`
|
|
13
|
+
// (`ctx.value`) — NOT the calldata `amountIn`. SwapRouter02 swaps native ETH by wrapping the
|
|
14
|
+
// ETH you send (tokenIn = WETH in the calldata) into WETH inside the router. So a permission
|
|
15
|
+
// adapted naively from the ERC-20 swap example (BoundedSwap_UniswapV3_Base.sol) would bound
|
|
16
|
+
// `amountIn` while leaving `ctx.value` UNBOUNDED — and the on-chain bound would NOT cap the
|
|
17
|
+
// funds actually spent. That is the trap this example exists to close.
|
|
18
|
+
//
|
|
19
|
+
// THE RULE THIS EXAMPLE DEMONSTRATES:
|
|
20
|
+
// For ANY value-carrying call, `Context.value` MUST be explicitly bounded. Bounding the
|
|
21
|
+
// calldata amount is not enough — the real spend is `ctx.value`.
|
|
22
|
+
//
|
|
23
|
+
// ENFORCES ON-CHAIN (kernel calls evaluate() on every dispatch; false ⇒ dispatch blocked):
|
|
24
|
+
// exactInputSingle((address,address,uint24,address,uint256,uint256,uint160)) selector 0x04e45aaf
|
|
25
|
+
// • target must be SWAP_ROUTER
|
|
26
|
+
// • ctx.value ≤ MAX_AMOUNT_IN ← bounds the REAL native spend
|
|
27
|
+
// • amountIn == ctx.value ← forces the native path; no value/amount drift
|
|
28
|
+
// • tokenIn must equal WETH ← the router wraps ctx.value into WETH
|
|
29
|
+
// • tokenOut must be in ALLOWED_TOKENS_OUT
|
|
30
|
+
//
|
|
31
|
+
// AGENT-ENFORCED / NOT BOUNDED HERE (off-chain — can change without redeploying this contract):
|
|
32
|
+
// • fee tier, sqrtPriceLimitX96, recipient address
|
|
33
|
+
// • swap frequency / cadence
|
|
34
|
+
// • slippage — see the note below
|
|
35
|
+
//
|
|
36
|
+
// SLIPPAGE IS NOT BOUNDED ON-CHAIN HERE — AND CANNOT BE, without a price oracle:
|
|
37
|
+
// `amountOutMinimum` (output token) and `ctx.value`/`amountIn` (native ETH) are denominated in
|
|
38
|
+
// DIFFERENT tokens, so a ratio between them bounds nothing real (see BoundedSwap_UniswapV3_Base
|
|
39
|
+
// for the full explanation). Compute `amountOutMinimum` OFF-CHAIN from a live quote and pass it
|
|
40
|
+
// in per swap; the router reverts if the output falls below it. This contract only caps the
|
|
41
|
+
// native input spend (MAX_AMOUNT_IN).
|
|
42
|
+
//
|
|
43
|
+
// VERIFY BEFORE USE:
|
|
44
|
+
// • Confirm SwapRouter02 + WETH addresses on your chain (Base defaults shown; verify on-chain).
|
|
45
|
+
// • Native ETH swaps on SwapRouter02 are typically wrapped in a payable multicall (so the
|
|
46
|
+
// router can refundETH any unspent wei). This example bounds the single exactInputSingle
|
|
47
|
+
// leg; if your agent routes the swap through `multicall`, add a permission that decodes the
|
|
48
|
+
// multicall and applies these same ctx.value / amountIn checks to the inner call.
|
|
49
|
+
// • Selector 0x04e45aaf = exactInputSingle((address,address,uint24,address,uint256,uint256,uint160))
|
|
50
|
+
// on SwapRouter02 (verified via `cast sig`). The OLDER SwapRouter's exactInputSingle (the
|
|
51
|
+
// deadline variant) is 0x414bf389 — a different selector; do not confuse the two.
|
|
52
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
import {IPermission, Context} from "@sail/interfaces/IPermission.sol";
|
|
55
|
+
|
|
56
|
+
contract BoundedSwapNative_UniswapV3_Base is IPermission {
|
|
57
|
+
bytes32 private constant DISCRIMINATOR = keccak256("BoundedSwapNative_UniswapV3_Base");
|
|
58
|
+
|
|
59
|
+
address public immutable SWAP_ROUTER;
|
|
60
|
+
/// @dev The wrapped-native token (WETH). SwapRouter02 wraps the ETH sent as ctx.value
|
|
61
|
+
/// into this token, so the calldata tokenIn for a native swap is WETH.
|
|
62
|
+
address public immutable WETH;
|
|
63
|
+
mapping(address => bool) public isAllowedTokenOut;
|
|
64
|
+
/// @dev Per-call cap on the native spend, in wei. Bounds ctx.value, the REAL spend.
|
|
65
|
+
uint256 public immutable MAX_AMOUNT_IN;
|
|
66
|
+
|
|
67
|
+
bytes4 private constant SEL_EXACT_INPUT_SINGLE = 0x04e45aaf;
|
|
68
|
+
|
|
69
|
+
struct ExactInputSingleParams {
|
|
70
|
+
address tokenIn;
|
|
71
|
+
address tokenOut;
|
|
72
|
+
uint24 fee;
|
|
73
|
+
address recipient;
|
|
74
|
+
uint256 amountIn;
|
|
75
|
+
uint256 amountOutMinimum;
|
|
76
|
+
uint160 sqrtPriceLimitX96;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// @param swapRouter SwapRouter02 address (0x2626... on Base)
|
|
80
|
+
/// @param weth Wrapped-native token address (WETH on Base: 0x4200...0006)
|
|
81
|
+
/// @param allowedTokensOut Tokens the agent may receive
|
|
82
|
+
/// @param maxAmountIn Per-call native spend cap in wei (bounds ctx.value)
|
|
83
|
+
/// @dev No slippage parameter: slippage cannot be bounded on-chain without a price oracle
|
|
84
|
+
/// (see the header note). Pass a tight `amountOutMinimum`, computed off-chain from a
|
|
85
|
+
/// live quote, on each swap — the router enforces it by reverting.
|
|
86
|
+
constructor(
|
|
87
|
+
address swapRouter,
|
|
88
|
+
address weth,
|
|
89
|
+
address[] memory allowedTokensOut,
|
|
90
|
+
uint256 maxAmountIn
|
|
91
|
+
) {
|
|
92
|
+
SWAP_ROUTER = swapRouter;
|
|
93
|
+
WETH = weth;
|
|
94
|
+
MAX_AMOUNT_IN = maxAmountIn;
|
|
95
|
+
for (uint256 i = 0; i < allowedTokensOut.length; i++) {
|
|
96
|
+
isAllowedTokenOut[allowedTokensOut[i]] = true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool) {
|
|
101
|
+
if (txData.length < 4) return false;
|
|
102
|
+
if (ctx.target != SWAP_ROUTER || ctx.selector != SEL_EXACT_INPUT_SINGLE) return false;
|
|
103
|
+
if (txData.length < 4 + 7 * 32) return false;
|
|
104
|
+
|
|
105
|
+
ExactInputSingleParams memory p = abi.decode(txData[4:], (ExactInputSingleParams));
|
|
106
|
+
|
|
107
|
+
// Bound the REAL native spend. ctx.value is the ETH actually leaving the account;
|
|
108
|
+
// bounding amountIn alone would leave the spend uncapped.
|
|
109
|
+
if (ctx.value > MAX_AMOUNT_IN) return false;
|
|
110
|
+
// No drift between the declared amount and the native value sent — a native swap pays
|
|
111
|
+
// entirely with ctx.value, so amountIn must equal it exactly.
|
|
112
|
+
if (p.amountIn != ctx.value) return false;
|
|
113
|
+
|
|
114
|
+
// tokenIn is WETH: the router wraps ctx.value into WETH before swapping.
|
|
115
|
+
if (p.tokenIn != WETH) return false;
|
|
116
|
+
if (!isAllowedTokenOut[p.tokenOut]) return false;
|
|
117
|
+
// amountOutMinimum intentionally not checked — see header (slippage cannot be bounded
|
|
118
|
+
// on-chain; the router enforces the off-chain-computed value the agent passes in).
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function discriminator() external pure returns (bytes32) { return DISCRIMINATOR; }
|
|
123
|
+
}
|
|
@@ -13,7 +13,6 @@ pragma solidity 0.8.26;
|
|
|
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 — see caveat below)
|
|
17
16
|
// approve(address,uint256) selector 0x095ea7b3
|
|
18
17
|
// • target must be FIXED_TOKEN_IN (the ERC-20 being approved)
|
|
19
18
|
// • spender must be SWAP_ROUTER
|
|
@@ -22,15 +21,24 @@ pragma solidity 0.8.26;
|
|
|
22
21
|
// AGENT-ENFORCED / NOT BOUNDED HERE (off-chain — can change without redeploying this contract):
|
|
23
22
|
// • fee tier, sqrtPriceLimitX96, recipient address
|
|
24
23
|
// • swap frequency / cadence
|
|
25
|
-
// •
|
|
24
|
+
// • slippage — see the note below
|
|
25
|
+
//
|
|
26
|
+
// SLIPPAGE IS NOT BOUNDED ON-CHAIN HERE — AND CANNOT BE, without a price oracle:
|
|
27
|
+
// `amountOutMinimum` (output token) and `amountIn` (input token) are denominated in DIFFERENT
|
|
28
|
+
// tokens. Comparing them as a ratio — the pattern an earlier version of this example shipped —
|
|
29
|
+
// is meaningless for any pair whose tokens differ in price or decimals (e.g. USDC→WETH): the
|
|
30
|
+
// check is either trivially satisfied or trivially failed, giving false confidence while
|
|
31
|
+
// protecting nothing. So this permission deliberately does NOT constrain `amountOutMinimum`.
|
|
32
|
+
// Real slippage protection must be computed OFF-CHAIN from a live quote (e.g. the Quoter or a
|
|
33
|
+
// price feed) and passed in as `amountOutMinimum` on each swap — the router reverts if the
|
|
34
|
+
// output falls below it. Your agent owns choosing a tight, fresh value per call; this contract
|
|
35
|
+
// only caps the input spend (MAX_AMOUNT_IN).
|
|
26
36
|
//
|
|
27
37
|
// VERIFY BEFORE USE:
|
|
28
38
|
// • Confirm SwapRouter02 address on your chain (Base default shown above; verified on Basescan).
|
|
29
39
|
// • Selector 0x04e45aaf = exactInputSingle((address,address,uint24,address,uint256,uint256,uint160))
|
|
30
40
|
// on SwapRouter02 (verified via `cast sig`). The OLDER SwapRouter's exactInputSingle (the
|
|
31
41
|
// deadline variant) is 0x414bf389 — a different selector; do not confuse the two.
|
|
32
|
-
// • Confirm that amountOutMinimum slippage floor fits your price-impact expectations for the
|
|
33
|
-
// chosen pool. Large amountIn values may trigger price impact beyond MIN_BPS.
|
|
34
42
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
35
43
|
|
|
36
44
|
import {IPermission, Context} from "@sail/interfaces/IPermission.sol";
|
|
@@ -42,8 +50,6 @@ contract BoundedSwap_UniswapV3_Base is IPermission {
|
|
|
42
50
|
address public immutable FIXED_TOKEN_IN;
|
|
43
51
|
mapping(address => bool) public isAllowedTokenOut;
|
|
44
52
|
uint256 public immutable MAX_AMOUNT_IN;
|
|
45
|
-
/// @dev Slippage floor in basis points. 9 900 = allow at most 1% slippage.
|
|
46
|
-
uint256 public immutable MIN_BPS;
|
|
47
53
|
|
|
48
54
|
bytes4 private constant SEL_EXACT_INPUT_SINGLE = 0x04e45aaf;
|
|
49
55
|
bytes4 private constant SEL_APPROVE = 0x095ea7b3;
|
|
@@ -62,21 +68,18 @@ contract BoundedSwap_UniswapV3_Base is IPermission {
|
|
|
62
68
|
/// @param fixedTokenIn The one token the agent is allowed to sell
|
|
63
69
|
/// @param allowedTokensOut Tokens the agent may receive
|
|
64
70
|
/// @param maxAmountIn Per-call spend cap in fixedTokenIn base units
|
|
65
|
-
/// @
|
|
66
|
-
///
|
|
67
|
-
///
|
|
71
|
+
/// @dev No slippage parameter: slippage cannot be bounded on-chain without a price oracle
|
|
72
|
+
/// (see the header note). Pass a tight `amountOutMinimum`, computed off-chain from a
|
|
73
|
+
/// live quote, on each swap — the router enforces it by reverting.
|
|
68
74
|
constructor(
|
|
69
75
|
address swapRouter,
|
|
70
76
|
address fixedTokenIn,
|
|
71
77
|
address[] memory allowedTokensOut,
|
|
72
|
-
uint256 maxAmountIn
|
|
73
|
-
uint256 minBps
|
|
78
|
+
uint256 maxAmountIn
|
|
74
79
|
) {
|
|
75
|
-
require(minBps <= 10_000, "minBps > 10000");
|
|
76
80
|
SWAP_ROUTER = swapRouter;
|
|
77
81
|
FIXED_TOKEN_IN = fixedTokenIn;
|
|
78
82
|
MAX_AMOUNT_IN = maxAmountIn;
|
|
79
|
-
MIN_BPS = minBps;
|
|
80
83
|
for (uint256 i = 0; i < allowedTokensOut.length; i++) {
|
|
81
84
|
isAllowedTokenOut[allowedTokensOut[i]] = true;
|
|
82
85
|
}
|
|
@@ -92,11 +95,10 @@ contract BoundedSwap_UniswapV3_Base is IPermission {
|
|
|
92
95
|
if (p.tokenIn != FIXED_TOKEN_IN) return false;
|
|
93
96
|
if (!isAllowedTokenOut[p.tokenOut]) return false;
|
|
94
97
|
if (p.amountIn > MAX_AMOUNT_IN) return false;
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
if (p.amountOutMinimum < (p.amountIn * MIN_BPS) / 10_000) return false;
|
|
98
|
+
// amountOutMinimum is intentionally NOT checked here — it is denominated in the
|
|
99
|
+
// output token, so a ratio against amountIn (input token) bounds nothing real for a
|
|
100
|
+
// cross-price pair (see header). The router enforces the off-chain-computed
|
|
101
|
+
// amountOutMinimum the agent passes in.
|
|
100
102
|
return true;
|
|
101
103
|
}
|
|
102
104
|
|
|
@@ -19,12 +19,19 @@ pragma solidity 0.8.26;
|
|
|
19
19
|
// • tokenIn (from poolKey, derived by zeroForOne) must be FIXED_CURRENCY_IN
|
|
20
20
|
// • tokenOut must be in ALLOWED_CURRENCIES_OUT
|
|
21
21
|
// • amountIn ≤ MAX_AMOUNT_IN
|
|
22
|
-
// • amountOutMinimum ≥ amountIn × MIN_BPS / 10 000 (slippage floor — see caveat below)
|
|
23
22
|
//
|
|
24
23
|
// AGENT-ENFORCED / NOT BOUNDED HERE (off-chain — can change without redeploying this contract):
|
|
25
|
-
// •
|
|
24
|
+
// • slippage — see the note below
|
|
26
25
|
// • swap frequency / cadence
|
|
27
26
|
//
|
|
27
|
+
// SLIPPAGE IS NOT BOUNDED ON-CHAIN HERE — AND CANNOT BE, without a price oracle:
|
|
28
|
+
// `amountOutMinimum` (output currency) and `amountIn` (input currency) are denominated in
|
|
29
|
+
// DIFFERENT tokens. A ratio between them — the pattern an earlier version of this example
|
|
30
|
+
// shipped — bounds nothing real for any pair whose tokens differ in price or decimals. So this
|
|
31
|
+
// permission deliberately does NOT constrain `amountOutMinimum`. Compute it OFF-CHAIN from a
|
|
32
|
+
// live quote and pass it in per swap; the router reverts if the output falls below it. This
|
|
33
|
+
// contract only caps the input spend (MAX_AMOUNT_IN).
|
|
34
|
+
//
|
|
28
35
|
// DOCUMENTED LIMITATIONS (on-chain, but intentionally not constrained):
|
|
29
36
|
// • hookData is not inspected (hooks can alter swap behavior on-chain; if the
|
|
30
37
|
// pool uses a hook that significantly changes execution, this permission cannot
|
|
@@ -54,7 +61,6 @@ contract BoundedSwap_UniswapV4_Unichain is IPermission {
|
|
|
54
61
|
address public immutable FIXED_CURRENCY_IN;
|
|
55
62
|
mapping(address => bool) public isAllowedCurrencyOut;
|
|
56
63
|
uint256 public immutable MAX_AMOUNT_IN;
|
|
57
|
-
uint256 public immutable MIN_BPS;
|
|
58
64
|
|
|
59
65
|
// execute(bytes,bytes[],uint256) — with deadline
|
|
60
66
|
bytes4 private constant SEL_EXECUTE_DEADLINE = 0x3593564c;
|
|
@@ -84,18 +90,18 @@ contract BoundedSwap_UniswapV4_Unichain is IPermission {
|
|
|
84
90
|
bytes hookData; // not inspected — see limitations header
|
|
85
91
|
}
|
|
86
92
|
|
|
93
|
+
/// @dev No slippage parameter: slippage cannot be bounded on-chain without a price oracle
|
|
94
|
+
/// (see the header note). Pass a tight `amountOutMinimum`, computed off-chain from a
|
|
95
|
+
/// live quote, on each swap — the router enforces it by reverting.
|
|
87
96
|
constructor(
|
|
88
97
|
address universalRouter,
|
|
89
98
|
address fixedCurrencyIn,
|
|
90
99
|
address[] memory allowedCurrenciesOut,
|
|
91
|
-
uint256 maxAmountIn
|
|
92
|
-
uint256 minBps
|
|
100
|
+
uint256 maxAmountIn
|
|
93
101
|
) {
|
|
94
|
-
require(minBps <= 10_000, "minBps > 10000");
|
|
95
102
|
UNIVERSAL_ROUTER = universalRouter;
|
|
96
103
|
FIXED_CURRENCY_IN = fixedCurrencyIn;
|
|
97
104
|
MAX_AMOUNT_IN = maxAmountIn;
|
|
98
|
-
MIN_BPS = minBps;
|
|
99
105
|
for (uint256 i = 0; i < allowedCurrenciesOut.length; i++) {
|
|
100
106
|
isAllowedCurrencyOut[allowedCurrenciesOut[i]] = true;
|
|
101
107
|
}
|
|
@@ -137,11 +143,10 @@ contract BoundedSwap_UniswapV4_Unichain is IPermission {
|
|
|
137
143
|
if (tokenIn != FIXED_CURRENCY_IN) return false;
|
|
138
144
|
if (!isAllowedCurrencyOut[tokenOut]) return false;
|
|
139
145
|
if (p.amountIn > MAX_AMOUNT_IN) return false;
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
if (p.amountOutMinimum < (uint256(p.amountIn) * MIN_BPS) / 10_000) return false;
|
|
146
|
+
// amountOutMinimum is intentionally NOT checked — it is denominated in the output
|
|
147
|
+
// currency, so a ratio against amountIn (input currency) bounds nothing real for a
|
|
148
|
+
// cross-price pair (see header). The router enforces the off-chain-computed
|
|
149
|
+
// amountOutMinimum the agent passes in.
|
|
145
150
|
|
|
146
151
|
return true;
|
|
147
152
|
}
|
|
@@ -23,6 +23,15 @@ against the protocol's real ABI, and confirm what it enforces before deploying.
|
|
|
23
23
|
|
|
24
24
|
You own what you deploy.
|
|
25
25
|
|
|
26
|
+
## Bound `Context.value` on every value-carrying call
|
|
27
|
+
|
|
28
|
+
For any call that can carry native asset (ETH), the value actually leaving the account is the
|
|
29
|
+
call's `msg.value` — exposed to your permission as `Context.value` (`ctx.value`) — **not** the
|
|
30
|
+
calldata amount. A permission that bounds only a calldata `amount`/`amountIn` leaves the real
|
|
31
|
+
spend uncapped. So: **every value-carrying call must explicitly bound `Context.value`**, and where
|
|
32
|
+
the calldata also declares an amount, assert `amount == ctx.value` so the two cannot drift. See
|
|
33
|
+
`BoundedSwapNative_UniswapV3_Base.sol` for the worked native-ETH example.
|
|
34
|
+
|
|
26
35
|
---
|
|
27
36
|
|
|
28
37
|
## Examples
|
|
@@ -35,6 +44,7 @@ off-chain agent code — these do *not* hold on-chain). Read both before deployi
|
|
|
35
44
|
|---|---|---|---|---|
|
|
36
45
|
| `BoundedSwap_UniswapV3_Base.sol` | Uniswap Swap | V3 SwapRouter02 | Base | Full decode |
|
|
37
46
|
| `BoundedSwap_UniswapV4_Unichain.sol` | Uniswap Swap | V4 Universal Router | Unichain | Partial decode — see header |
|
|
47
|
+
| `BoundedSwapNative_UniswapV3_Base.sol` | Uniswap Swap (native ETH) | V3 SwapRouter02 | Base | Full decode — bounds `Context.value` (native spend) |
|
|
38
48
|
| `BoundedBorrow_AaveV3_Arbitrum.sol` | Aave Borrow | V3 Pool | Arbitrum | Full decode |
|
|
39
49
|
| `BoundedSupply_AaveV3_Arbitrum.sol` | Aave Supply | V3 Pool | Arbitrum | Full decode |
|
|
40
50
|
| `BoundedVault_ERC4626_Base.sol` | ERC-4626 Vault deposit/withdraw | EIP-4626 standard | Base (any EVM) | Full decode |
|