@rhinestone/sdk 2.5.0 → 2.6.0

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.
Files changed (39) hide show
  1. package/dist/src/api/account.d.ts.map +1 -1
  2. package/dist/src/api/account.js +118 -0
  3. package/dist/src/api/compose-types.d.ts +7 -0
  4. package/dist/src/api/compose-types.d.ts.map +1 -1
  5. package/dist/src/clients/orchestrator/client.js +1 -1
  6. package/dist/src/clients/orchestrator/public.d.ts +8 -1
  7. package/dist/src/clients/orchestrator/public.d.ts.map +1 -1
  8. package/dist/src/clients/orchestrator/types.d.ts +6 -0
  9. package/dist/src/clients/orchestrator/types.d.ts.map +1 -1
  10. package/dist/src/config/account.d.ts +130 -6
  11. package/dist/src/config/account.d.ts.map +1 -1
  12. package/dist/src/index.d.ts +2 -2
  13. package/dist/src/index.d.ts.map +1 -1
  14. package/dist/src/modules/validators/permissions.d.ts +13 -0
  15. package/dist/src/modules/validators/permissions.d.ts.map +1 -1
  16. package/dist/src/modules/validators/permissions.js +140 -2
  17. package/dist/src/modules/validators/smart-sessions/resolve.d.ts.map +1 -1
  18. package/dist/src/modules/validators/smart-sessions/resolve.js +112 -13
  19. package/dist/src/modules/validators/smart-sessions/swap/fynd.d.ts +92 -0
  20. package/dist/src/modules/validators/smart-sessions/swap/fynd.d.ts.map +1 -0
  21. package/dist/src/modules/validators/smart-sessions/swap/fynd.js +101 -0
  22. package/dist/src/modules/validators/smart-sessions/swap/rhinestone.d.ts +147 -0
  23. package/dist/src/modules/validators/smart-sessions/swap/rhinestone.d.ts.map +1 -0
  24. package/dist/src/modules/validators/smart-sessions/swap/rhinestone.js +305 -0
  25. package/dist/src/modules/validators/smart-sessions/swap/rules.d.ts +73 -0
  26. package/dist/src/modules/validators/smart-sessions/swap/rules.d.ts.map +1 -0
  27. package/dist/src/modules/validators/smart-sessions/swap/rules.js +105 -0
  28. package/dist/src/modules/validators/smart-sessions/swap/scope.d.ts +31 -0
  29. package/dist/src/modules/validators/smart-sessions/swap/scope.d.ts.map +1 -0
  30. package/dist/src/modules/validators/smart-sessions/swap/scope.js +168 -0
  31. package/dist/src/modules/validators/smart-sessions/swap/zero-ex.d.ts +143 -0
  32. package/dist/src/modules/validators/smart-sessions/swap/zero-ex.d.ts.map +1 -0
  33. package/dist/src/modules/validators/smart-sessions/swap/zero-ex.js +164 -0
  34. package/dist/src/modules/validators/smart-sessions/types.d.ts +78 -0
  35. package/dist/src/modules/validators/smart-sessions/types.d.ts.map +1 -1
  36. package/dist/src/smart-sessions/index.d.ts +10 -5
  37. package/dist/src/smart-sessions/index.d.ts.map +1 -1
  38. package/dist/src/smart-sessions/index.js +6 -1
  39. package/package.json +1 -1
@@ -0,0 +1,101 @@
1
+ import { toFunctionSelector } from 'viem';
2
+ import { namedParamOffsets } from '../../permissions.js';
3
+ import { cumulativeCap, pin, swapAction } from './rules.js';
4
+ /**
5
+ * fynd — Rhinestone's self-hosted Tycho aggregator.
6
+ *
7
+ * Unlike 0x there is no allowance-holder indirection: the TychoRouter is both
8
+ * the swap target and the ERC-20 approval spender, and every field we care about
9
+ * is a named static argument in the calldata head. That means nothing here is a
10
+ * magic offset — the rules are addressed by ABI parameter name.
11
+ */
12
+ /** Chains with a deployed, whitelisted TychoRouter. */
13
+ export const FYND_CHAIN_IDS = [1, 56, 130, 137, 8453, 9745, 42161];
14
+ /**
15
+ * TychoRouter per chain — the contract fynd's encoded fills target.
16
+ *
17
+ * Mirrors `fynd.routers` in the orchestrator's quoters.jsonnet. A chain is
18
+ * listed only when the router is whitelisted in `IntentExecutionPolicy`, since a
19
+ * quoter enabled without a whitelisted target produces quotes that revert.
20
+ */
21
+ export const FYND_ROUTERS = {
22
+ 1: '0xda892c989d07a18b5dd3f392d949f00df15c5736',
23
+ 56: '0x99748cbd931cb367dad265c5b2b4bd306d448e99',
24
+ 130: '0x764bc67b1036b00bc91221e988261f971a1c7ce4',
25
+ 137: '0x7cb3e87095f6cf95982dc6f57445171a6d3b511c',
26
+ 8453: '0x2d3524b9b5dae34b646614eebb1e038d403e4cac',
27
+ 9745: '0x8f9b3b0451efff0ae8100428aee35fa3cbc0b769',
28
+ 42161: '0xc1f838a5382bbb5729a0801c8ba73dfc861c4d34',
29
+ };
30
+ /**
31
+ * TychoRouter `singleSwap`, the entrypoint fynd's encoded quotes call.
32
+ *
33
+ * Only the argument TYPES determine the selector and the head layout, and those
34
+ * are pinned by {@link FYND_SWAP_SELECTOR}'s assertion in the test suite. The
35
+ * NAMES of the first five are corroborated by observed fynd calldata; the
36
+ * trailing tuple's component names are our own labels and carry no guarantee —
37
+ * we never address them, since a tuple containing `bytes` is dynamic and sits
38
+ * behind a pointer.
39
+ */
40
+ export const tychoRouterAbi = [
41
+ {
42
+ type: 'function',
43
+ name: 'singleSwap',
44
+ stateMutability: 'payable',
45
+ inputs: [
46
+ { name: 'amountIn', type: 'uint256' },
47
+ { name: 'tokenIn', type: 'address' },
48
+ { name: 'tokenOut', type: 'address' },
49
+ { name: 'minAmountOut', type: 'uint256' },
50
+ { name: 'receiver', type: 'address' },
51
+ {
52
+ name: 'permit',
53
+ type: 'tuple',
54
+ components: [
55
+ { name: 'nonce', type: 'uint16' },
56
+ { name: 'token', type: 'address' },
57
+ { name: 'amount', type: 'uint256' },
58
+ { name: 'deadline', type: 'uint256' },
59
+ { name: 'signature', type: 'bytes' },
60
+ ],
61
+ },
62
+ { name: 'swap', type: 'bytes' },
63
+ ],
64
+ outputs: [{ name: 'amountOut', type: 'uint256' }],
65
+ },
66
+ ];
67
+ export const FYND_SWAP_SELECTOR = toFunctionSelector(tychoRouterAbi[0]);
68
+ /** Head offsets derived from the ABI — never hardcoded. */
69
+ const OFFSETS = namedParamOffsets(tychoRouterAbi, 'singleSwap');
70
+ /**
71
+ * Scope a session to fynd swaps.
72
+ *
73
+ * Needs no addresses: the router is a per-chain deployment the SDK knows, and
74
+ * because it is also the approval spender there is no separate allowance target
75
+ * to reason about.
76
+ */
77
+ export function fynd(options = {}) {
78
+ return {
79
+ id: 'fynd',
80
+ ...(options.maxSpend !== undefined ? { maxSpend: options.maxSpend } : {}),
81
+ };
82
+ }
83
+ export function scopeFynd(ctx) {
84
+ const router = FYND_ROUTERS[ctx.chainId];
85
+ if (router === undefined) {
86
+ throw new Error(`fynd is not available on chain ${ctx.chainId}. ` +
87
+ `Supported: ${FYND_CHAIN_IDS.join(', ')}.`);
88
+ }
89
+ const rules = [
90
+ pin(OFFSETS.tokenIn, ctx.sellToken),
91
+ pin(OFFSETS.tokenOut, ctx.buyToken),
92
+ pin(OFFSETS.receiver, ctx.recipient),
93
+ ];
94
+ if (ctx.cap !== undefined) {
95
+ rules.push(cumulativeCap(OFFSETS.amountIn, ctx.cap));
96
+ }
97
+ return {
98
+ approveSpenders: [router],
99
+ actions: [swapAction(router, FYND_SWAP_SELECTOR, rules)],
100
+ };
101
+ }
@@ -0,0 +1,147 @@
1
+ import { type Address } from 'viem';
2
+ import type { RhinestoneSwapVenue, RoutedRhinestoneSwapVenue } from '../types.js';
3
+ import type { VenueContext, VenueScoping } from './rules.js';
4
+ /**
5
+ * The Rhinestone Swapper — the route the orchestrator actually uses for
6
+ * same-chain smart-account swaps, and the right layer to scope a session at.
7
+ *
8
+ * Scoping an aggregator directly (see `zero-ex.ts`, `fynd.ts`) describes a call
9
+ * the account does not make on this route: the orchestrator wraps whichever
10
+ * quoter wins behind its own Swapper, so a session scoped to 0x's
11
+ * AllowanceHolder rejects its own intended swap with `InvalidSignature()`.
12
+ * Verified live on Plasma — the account's sell token moves to the Swapper, and
13
+ * the aggregator never appears in the account's ops.
14
+ *
15
+ * Scoping here is also strictly better:
16
+ * - aggregator-agnostic, so whichever quoter wins is irrelevant
17
+ * - no 0x Settler rotation problem, because this contract is ours
18
+ * - one approve target per chain (the proxy) instead of one per venue
19
+ * - the fields we care about are typed head args on a contract we control,
20
+ * with the aggregator route demoted to an opaque `calls[]` tail
21
+ *
22
+ * What the Swapper guarantees (verified in compact-utils/src/swapper):
23
+ * at most `amountIn` of `tokenIn` is pulled, once, from `msg.sender`; the
24
+ * recipient's measured balance delta must be >= `minAmountOut` or it reverts;
25
+ * every refund and sweep goes to `msg.sender`. The account cannot be drained
26
+ * beyond the pulled amount: the tail runs as the Swapper, which holds no
27
+ * allowance from the account, and `SwapperLib.runRoute` forbids the tail from
28
+ * calling the proxy that does.
29
+ *
30
+ * Be precise about what that bounds, though. A capped `amountIn` bounds how
31
+ * much can LEAVE; it does not ensure anything comes back. `minAmountOut` is a
32
+ * caller-supplied argument this scoping does not pin, and the contract accepts
33
+ * zero — so with an unconstrained route a compromised key can spend up to the
34
+ * cap and receive nothing. Naming a venue closes that by pinning the
35
+ * route, leaving the pulled input nowhere to go but a real aggregator.
36
+ */
37
+ /**
38
+ * Both entrypoints share a head layout, which is what lets one venue cover
39
+ * both: `tokenIn`@0, sell amount@32, `tokenOut`@64, `recipient`@160. The sell
40
+ * amount is `amountIn` for exact-in and `amountInMax` for exact-out — in both
41
+ * cases the ceiling on what leaves the account, which is exactly what to cap.
42
+ */
43
+ export declare const swapperAbi: readonly [{
44
+ readonly type: "function";
45
+ readonly name: "swapExactIn";
46
+ readonly stateMutability: "payable";
47
+ readonly inputs: readonly [{
48
+ readonly name: "tokenIn";
49
+ readonly type: "address";
50
+ }, {
51
+ readonly name: "amountIn";
52
+ readonly type: "uint256";
53
+ }, {
54
+ readonly name: "tokenOut";
55
+ readonly type: "address";
56
+ }, {
57
+ readonly name: "minAmountOut";
58
+ readonly type: "uint256";
59
+ }, {
60
+ readonly name: "quotedAmountOut";
61
+ readonly type: "uint256";
62
+ }, {
63
+ readonly name: "recipient";
64
+ readonly type: "address";
65
+ }, {
66
+ readonly name: "orderRef";
67
+ readonly type: "uint256";
68
+ }, {
69
+ readonly name: "calls";
70
+ readonly type: "tuple[]";
71
+ readonly components: readonly [{
72
+ readonly name: "target";
73
+ readonly type: "address";
74
+ }, {
75
+ readonly name: "value";
76
+ readonly type: "uint256";
77
+ }, {
78
+ readonly name: "data";
79
+ readonly type: "bytes";
80
+ }];
81
+ }];
82
+ readonly outputs: readonly [{
83
+ readonly name: "amountOut";
84
+ readonly type: "uint256";
85
+ }];
86
+ }, {
87
+ readonly type: "function";
88
+ readonly name: "swapExactOut";
89
+ readonly stateMutability: "payable";
90
+ readonly inputs: readonly [{
91
+ readonly name: "tokenIn";
92
+ readonly type: "address";
93
+ }, {
94
+ readonly name: "amountInMax";
95
+ readonly type: "uint256";
96
+ }, {
97
+ readonly name: "tokenOut";
98
+ readonly type: "address";
99
+ }, {
100
+ readonly name: "amountOut";
101
+ readonly type: "uint256";
102
+ }, {
103
+ readonly name: "quotedAmountIn";
104
+ readonly type: "uint256";
105
+ }, {
106
+ readonly name: "recipient";
107
+ readonly type: "address";
108
+ }, {
109
+ readonly name: "orderRef";
110
+ readonly type: "uint256";
111
+ }, {
112
+ readonly name: "calls";
113
+ readonly type: "tuple[]";
114
+ readonly components: readonly [{
115
+ readonly name: "target";
116
+ readonly type: "address";
117
+ }, {
118
+ readonly name: "value";
119
+ readonly type: "uint256";
120
+ }, {
121
+ readonly name: "data";
122
+ readonly type: "bytes";
123
+ }];
124
+ }];
125
+ readonly outputs: readonly [{
126
+ readonly name: "amountSpent";
127
+ readonly type: "uint256";
128
+ }];
129
+ }];
130
+ export declare const SWAP_EXACT_IN_SELECTOR: `0x${string}`;
131
+ export declare const SWAP_EXACT_OUT_SELECTOR: `0x${string}`;
132
+ export declare function swapperAddresses(environment: 'production' | 'development'): {
133
+ swapper: Address;
134
+ proxy: Address;
135
+ };
136
+ /**
137
+ * Route swaps through the Rhinestone Swapper — the default, and the only venue
138
+ * that matches what the orchestrator emits for same-chain smart-account swaps.
139
+ *
140
+ * Takes no addresses and no aggregator: which quoter fills the swap is the
141
+ * orchestrator's decision, and this scoping holds whichever one wins.
142
+ */
143
+ export declare function rhinestoneSwap(options?: {
144
+ maxSpend?: bigint;
145
+ }): RhinestoneSwapVenue;
146
+ export declare function scopeRhinestone(venue: RoutedRhinestoneSwapVenue, ctx: VenueContext): VenueScoping;
147
+ //# sourceMappingURL=rhinestone.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rhinestone.d.ts","sourceRoot":"","sources":["../../../../../../modules/validators/smart-sessions/swap/rhinestone.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,OAAO,EAAgC,MAAM,MAAM,CAAA;AAE3E,OAAO,KAAK,EACV,mBAAmB,EACnB,yBAAyB,EAE1B,MAAM,UAAU,CAAA;AAEjB,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAIzD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiDC,CAAA;AAExB,eAAO,MAAM,sBAAsB,eAAoC,CAAA;AACvE,eAAO,MAAM,uBAAuB,eAAoC,CAAA;AAqBxE,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,YAAY,GAAG,aAAa,GAAG;IAC3E,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,OAAO,CAAA;CACf,CAIA;AAkJD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAClC,mBAAmB,CAKrB;AACD,wBAAgB,eAAe,CAC7B,KAAK,EAAE,yBAAyB,EAChC,GAAG,EAAE,YAAY,GAChB,YAAY,CAgFd"}
@@ -0,0 +1,305 @@
1
+ import { toFunctionSelector } from 'viem';
2
+ import { namedParamOffsets } from '../../permissions.js';
3
+ import { FYND_CHAIN_IDS, FYND_ROUTERS } from './fynd.js';
4
+ import { cumulativeCap, pin, pinValue, pinWord, swapAction } from './rules.js';
5
+ import { ZEROX_ALLOWANCE_HOLDER, ZEROX_CHAIN_IDS } from './zero-ex.js';
6
+ /**
7
+ * The Rhinestone Swapper — the route the orchestrator actually uses for
8
+ * same-chain smart-account swaps, and the right layer to scope a session at.
9
+ *
10
+ * Scoping an aggregator directly (see `zero-ex.ts`, `fynd.ts`) describes a call
11
+ * the account does not make on this route: the orchestrator wraps whichever
12
+ * quoter wins behind its own Swapper, so a session scoped to 0x's
13
+ * AllowanceHolder rejects its own intended swap with `InvalidSignature()`.
14
+ * Verified live on Plasma — the account's sell token moves to the Swapper, and
15
+ * the aggregator never appears in the account's ops.
16
+ *
17
+ * Scoping here is also strictly better:
18
+ * - aggregator-agnostic, so whichever quoter wins is irrelevant
19
+ * - no 0x Settler rotation problem, because this contract is ours
20
+ * - one approve target per chain (the proxy) instead of one per venue
21
+ * - the fields we care about are typed head args on a contract we control,
22
+ * with the aggregator route demoted to an opaque `calls[]` tail
23
+ *
24
+ * What the Swapper guarantees (verified in compact-utils/src/swapper):
25
+ * at most `amountIn` of `tokenIn` is pulled, once, from `msg.sender`; the
26
+ * recipient's measured balance delta must be >= `minAmountOut` or it reverts;
27
+ * every refund and sweep goes to `msg.sender`. The account cannot be drained
28
+ * beyond the pulled amount: the tail runs as the Swapper, which holds no
29
+ * allowance from the account, and `SwapperLib.runRoute` forbids the tail from
30
+ * calling the proxy that does.
31
+ *
32
+ * Be precise about what that bounds, though. A capped `amountIn` bounds how
33
+ * much can LEAVE; it does not ensure anything comes back. `minAmountOut` is a
34
+ * caller-supplied argument this scoping does not pin, and the contract accepts
35
+ * zero — so with an unconstrained route a compromised key can spend up to the
36
+ * cap and receive nothing. Naming a venue closes that by pinning the
37
+ * route, leaving the pulled input nowhere to go but a real aggregator.
38
+ */
39
+ /**
40
+ * Both entrypoints share a head layout, which is what lets one venue cover
41
+ * both: `tokenIn`@0, sell amount@32, `tokenOut`@64, `recipient`@160. The sell
42
+ * amount is `amountIn` for exact-in and `amountInMax` for exact-out — in both
43
+ * cases the ceiling on what leaves the account, which is exactly what to cap.
44
+ */
45
+ export const swapperAbi = [
46
+ {
47
+ type: 'function',
48
+ name: 'swapExactIn',
49
+ stateMutability: 'payable',
50
+ inputs: [
51
+ { name: 'tokenIn', type: 'address' },
52
+ { name: 'amountIn', type: 'uint256' },
53
+ { name: 'tokenOut', type: 'address' },
54
+ { name: 'minAmountOut', type: 'uint256' },
55
+ { name: 'quotedAmountOut', type: 'uint256' },
56
+ { name: 'recipient', type: 'address' },
57
+ { name: 'orderRef', type: 'uint256' },
58
+ {
59
+ name: 'calls',
60
+ type: 'tuple[]',
61
+ components: [
62
+ { name: 'target', type: 'address' },
63
+ { name: 'value', type: 'uint256' },
64
+ { name: 'data', type: 'bytes' },
65
+ ],
66
+ },
67
+ ],
68
+ outputs: [{ name: 'amountOut', type: 'uint256' }],
69
+ },
70
+ {
71
+ type: 'function',
72
+ name: 'swapExactOut',
73
+ stateMutability: 'payable',
74
+ inputs: [
75
+ { name: 'tokenIn', type: 'address' },
76
+ { name: 'amountInMax', type: 'uint256' },
77
+ { name: 'tokenOut', type: 'address' },
78
+ { name: 'amountOut', type: 'uint256' },
79
+ { name: 'quotedAmountIn', type: 'uint256' },
80
+ { name: 'recipient', type: 'address' },
81
+ { name: 'orderRef', type: 'uint256' },
82
+ {
83
+ name: 'calls',
84
+ type: 'tuple[]',
85
+ components: [
86
+ { name: 'target', type: 'address' },
87
+ { name: 'value', type: 'uint256' },
88
+ { name: 'data', type: 'bytes' },
89
+ ],
90
+ },
91
+ ],
92
+ outputs: [{ name: 'amountSpent', type: 'uint256' }],
93
+ },
94
+ ];
95
+ export const SWAP_EXACT_IN_SELECTOR = toFunctionSelector(swapperAbi[0]);
96
+ export const SWAP_EXACT_OUT_SELECTOR = toFunctionSelector(swapperAbi[1]);
97
+ const EXACT_IN = namedParamOffsets(swapperAbi, 'swapExactIn');
98
+ const EXACT_OUT = namedParamOffsets(swapperAbi, 'swapExactOut');
99
+ /**
100
+ * Deployed via CREATE2, so one address covers every chain. The proxy is
101
+ * `new`'d in the Swapper's constructor and the binding is immutable both ways,
102
+ * so a Swapper redeploy also changes the proxy — verify both together.
103
+ *
104
+ * Confirmed on Plasma: `Swapper.PROXY()` returns the proxy below.
105
+ */
106
+ const SWAPPER_PRODUCTION = '0x40CE38e0cbB8ec54a601256E4FacfED5679bccD0';
107
+ const PROXY_PRODUCTION = '0x5afCe415B4370E5EfD8B9BE784d21C331bEAb965';
108
+ const SWAPPER_DEVELOPMENT = '0x8206052a213AA7cafB18ec7898e8D1D421C02100';
109
+ const PROXY_DEVELOPMENT = '0x21416a06e81fe115a5c8bf554b2b01383bd9b9f3';
110
+ export function swapperAddresses(environment) {
111
+ return environment === 'development'
112
+ ? { swapper: SWAPPER_DEVELOPMENT, proxy: PROXY_DEVELOPMENT }
113
+ : { swapper: SWAPPER_PRODUCTION, proxy: PROXY_PRODUCTION };
114
+ }
115
+ /**
116
+ * Byte offsets of the `calls[]` ABI *shape* words, measured in the Swapper's
117
+ * calldata past the selector. Identical for `swapExactIn` and `swapExactOut`
118
+ * because both have the same eight-word head.
119
+ *
120
+ * Pinning a target inside a dynamic array is only sound if the layout is also
121
+ * pinned: a compromised session key controls the encoding and could otherwise
122
+ * relocate elements so a fixed offset reads an innocuous word. Pinning the array
123
+ * pointer, the length, and each element pointer fixes every position, making the
124
+ * target offsets exact.
125
+ *
126
+ * Derived from live Plasma calldata and reproduced by
127
+ * `zeroExRouteRules` in the test suite:
128
+ * @224 array pointer = 256 (tail begins right after the head)
129
+ * @256 length = 2 (approve + aggregator call)
130
+ * @288 elem[0] pointer= 64 (element table is 2 words)
131
+ * @320 elem[1] pointer= 288 (64 + elem[0] size: 4 words + a 68-byte approve)
132
+ * @352 calls[0].target (the sell-token approve)
133
+ * @576 calls[1].target (the aggregator)
134
+ *
135
+ * `calls[1].data` — the variable-length aggregator blob — is LAST in the
136
+ * encoding, so its length shifts nothing that is pinned. That is what makes
137
+ * these offsets stable across quotes rather than coincidental.
138
+ */
139
+ const CALLS_POINTER_OFFSET = 224n;
140
+ const CALLS_LENGTH_OFFSET = 256n;
141
+ const CALLS_ELEM0_POINTER_OFFSET = 288n;
142
+ const CALLS_ELEM1_POINTER_OFFSET = 320n;
143
+ const CALLS_ELEM0_TARGET_OFFSET = 352n;
144
+ const CALLS_ELEM1_TARGET_OFFSET = 576n;
145
+ const CALLS_ELEM0_VALUE_OFFSET = 384n;
146
+ const CALLS_ELEM1_VALUE_OFFSET = 608n;
147
+ const NESTED_EXEC_TOKEN_OFFSET = 740n;
148
+ /**
149
+ * The nested fynd call: `swap(amountIn, tokenIn, tokenOut, minOut, receiver)`.
150
+ * Verified against production calldata — the receiver is the Swapper itself,
151
+ * which collects the output and forwards it to the scope's recipient, so that
152
+ * is what gets pinned rather than the end recipient.
153
+ */
154
+ const NESTED_FYND_TOKEN_IN_OFFSET = 740n;
155
+ const NESTED_FYND_TOKEN_OUT_OFFSET = 772n;
156
+ const NESTED_FYND_RECEIVER_OFFSET = 836n;
157
+ const CALLS_ELEM1_DATA_POINTER_OFFSET = 640n;
158
+ /** Head words of the `AllowanceHolder.exec` nested in `calls[1].data`. */
159
+ const NESTED_EXEC_OPERATOR_OFFSET = 708n;
160
+ const NESTED_EXEC_TARGET_OFFSET = 804n;
161
+ const CALLS_ELEM1_DATA_POINTER = 96n;
162
+ const CALLS_ELEM0_DATA_POINTER_OFFSET = 416n;
163
+ const CALLS_ELEM0_DATA_LENGTH_OFFSET = 448n;
164
+ /**
165
+ * The word straddling `calls[0].data`'s selector and the high bytes of its
166
+ * first argument. Pinning the spender alone leaves the selector free, so the
167
+ * same word layout also satisfies `transfer(aggregator, amount)` — which sends
168
+ * the pulled input to the aggregator instead of approving it, and with
169
+ * `minAmountOut` free to be zero the Swapper does not object.
170
+ */
171
+ const CALLS_ELEM0_DATA_HEAD_OFFSET = 480n;
172
+ const ERC20_APPROVE_SELECTOR = '095ea7b3';
173
+ /** `approve` selector followed by the leading 28 bytes of the spender word. */
174
+ function approveHeadWord(spender) {
175
+ return `0x${ERC20_APPROVE_SELECTOR}${'00'.repeat(12)}${spender
176
+ .slice(2, 34)
177
+ .toLowerCase()}`;
178
+ }
179
+ /** The `spender` argument of `calls[0]`'s `approve`, past its 4-byte selector. */
180
+ const CALLS_ELEM0_SPENDER_OFFSET = 484n;
181
+ const CALLS_ELEM0_DATA_POINTER = 96n;
182
+ /** `approve(address,uint256)` — selector + two words. */
183
+ const APPROVE_CALLDATA_LENGTH = 68n;
184
+ const CALLS_POINTER = 256n;
185
+ const CALLS_LENGTH = 2n;
186
+ const CALLS_ELEM0_POINTER = 64n;
187
+ const CALLS_ELEM1_POINTER = 288n;
188
+ /**
189
+ * Pin the two-call `approve + aggregator` route inside the Swapper's `calls[]`.
190
+ *
191
+ * Without this the tail is unconstrained: the Swapper only requires that the
192
+ * recipient nets `minAmountOut`, and `minAmountOut` may be zero, so a
193
+ * compromised key could hand the pulled input to any address and satisfy the
194
+ * contract. Constraining every call target to a real aggregator removes the
195
+ * place those funds could go.
196
+ *
197
+ * Fails closed. A route that deviates from this shape — a different call count,
198
+ * an added permit or approval reset — reverts rather than slipping through, so
199
+ * the cost of the pin is availability, not safety.
200
+ */
201
+ function routeRules(sellToken, aggregator, settler,
202
+ /** Set for the fynd route: pins the nested swap's tokens and receiver. */
203
+ fyndNested) {
204
+ const nestedFynd = fyndNested
205
+ ? [
206
+ // Pinning calls[1].target to the Tycho router still leaves the swap it
207
+ // performs free to name any tokens and any receiver — so the output
208
+ // could land anywhere while the outer Swapper pins still hold.
209
+ pinValue(CALLS_ELEM1_DATA_POINTER_OFFSET, CALLS_ELEM1_DATA_POINTER),
210
+ pin(NESTED_FYND_TOKEN_IN_OFFSET, sellToken),
211
+ pin(NESTED_FYND_TOKEN_OUT_OFFSET, fyndNested.buyToken),
212
+ pin(NESTED_FYND_RECEIVER_OFFSET, fyndNested.swapper),
213
+ ]
214
+ : [];
215
+ const nested = settler
216
+ ? [
217
+ // Pinning calls[1]'s target to the AllowanceHolder still leaves the
218
+ // exec it forwards free to name any operator and target, which is where
219
+ // the pulled input would go. Pin those too when the Settler is known.
220
+ pinValue(CALLS_ELEM1_VALUE_OFFSET, 0n),
221
+ pinValue(CALLS_ELEM1_DATA_POINTER_OFFSET, CALLS_ELEM1_DATA_POINTER),
222
+ pin(NESTED_EXEC_OPERATOR_OFFSET, settler),
223
+ pin(NESTED_EXEC_TOKEN_OFFSET, sellToken),
224
+ pin(NESTED_EXEC_TARGET_OFFSET, settler),
225
+ ]
226
+ : [];
227
+ return [
228
+ ...nested,
229
+ ...nestedFynd,
230
+ pinValue(CALLS_POINTER_OFFSET, CALLS_POINTER),
231
+ pinValue(CALLS_LENGTH_OFFSET, CALLS_LENGTH),
232
+ pinValue(CALLS_ELEM0_POINTER_OFFSET, CALLS_ELEM0_POINTER),
233
+ pinValue(CALLS_ELEM1_POINTER_OFFSET, CALLS_ELEM1_POINTER),
234
+ pin(CALLS_ELEM0_TARGET_OFFSET, sellToken),
235
+ pin(CALLS_ELEM1_TARGET_OFFSET, aggregator),
236
+ // Pinning calls[0]'s target alone still lets it be any call to the sell
237
+ // token — `transfer(attacker, amountIn)` as easily as an approve. Fixing
238
+ // its length and the address it names leaves the aggregator as the only
239
+ // party the pulled input can reach.
240
+ pinValue(CALLS_ELEM0_VALUE_OFFSET, 0n),
241
+ pinValue(CALLS_ELEM0_DATA_POINTER_OFFSET, CALLS_ELEM0_DATA_POINTER),
242
+ pinValue(CALLS_ELEM0_DATA_LENGTH_OFFSET, APPROVE_CALLDATA_LENGTH),
243
+ pinWord(CALLS_ELEM0_DATA_HEAD_OFFSET, approveHeadWord(aggregator)),
244
+ pin(CALLS_ELEM0_SPENDER_OFFSET, aggregator),
245
+ ];
246
+ }
247
+ /**
248
+ * Route swaps through the Rhinestone Swapper — the default, and the only venue
249
+ * that matches what the orchestrator emits for same-chain smart-account swaps.
250
+ *
251
+ * Takes no addresses and no aggregator: which quoter fills the swap is the
252
+ * orchestrator's decision, and this scoping holds whichever one wins.
253
+ */
254
+ export function rhinestoneSwap(options = {}) {
255
+ return {
256
+ id: 'rhinestone',
257
+ ...(options.maxSpend !== undefined ? { maxSpend: options.maxSpend } : {}),
258
+ };
259
+ }
260
+ export function scopeRhinestone(venue, ctx) {
261
+ const { swapper, proxy } = swapperAddresses(ctx.environment);
262
+ if (venue.route === 'zeroEx' && !ZEROX_CHAIN_IDS.includes(ctx.chainId)) {
263
+ throw new Error(`0x is not available on chain ${ctx.chainId}. ` +
264
+ `Supported: ${ZEROX_CHAIN_IDS.join(', ')}.`);
265
+ }
266
+ if (venue.route === 'fynd' && !FYND_CHAIN_IDS.includes(ctx.chainId)) {
267
+ throw new Error(`fynd is not available on chain ${ctx.chainId}. ` +
268
+ `Supported: ${FYND_CHAIN_IDS.join(', ')}.`);
269
+ }
270
+ // Which router the pinned tail must call. Undefined route = unconstrained.
271
+ const routeAggregator = venue.route === 'zeroEx'
272
+ ? ZEROX_ALLOWANCE_HOLDER
273
+ : venue.route === 'fynd'
274
+ ? FYND_ROUTERS[ctx.chainId]
275
+ : undefined;
276
+ const rulesFor = (offsets, sellAmountParam) => {
277
+ const rules = [
278
+ pin(offsets.tokenIn, ctx.sellToken),
279
+ pin(offsets.tokenOut, ctx.buyToken),
280
+ pin(offsets.recipient, ctx.recipient),
281
+ ];
282
+ if (ctx.cap !== undefined) {
283
+ rules.push(cumulativeCap(offsets[sellAmountParam], ctx.cap));
284
+ }
285
+ if (venue.routes === undefined && routeAggregator !== undefined) {
286
+ rules.push(...routeRules(ctx.sellToken, routeAggregator, venue.route === 'zeroEx' ? venue.settler : undefined, venue.route === 'fynd'
287
+ ? { buyToken: ctx.buyToken, swapper }
288
+ : undefined));
289
+ }
290
+ return rules;
291
+ };
292
+ /** One rule set per authorised aggregator; the policy requires any one. */
293
+ const routeAlternatives = (venue.routes ?? []).map((route) => routeRules(ctx.sellToken, route === 'zeroEx'
294
+ ? ZEROX_ALLOWANCE_HOLDER
295
+ : FYND_ROUTERS[ctx.chainId], route === 'zeroEx' ? venue.settler : undefined, route === 'fynd' ? { buyToken: ctx.buyToken, swapper } : undefined));
296
+ return {
297
+ // The account approves the proxy, never the Swapper and never a router —
298
+ // one fixed spender per chain, whichever aggregator ends up filling.
299
+ approveSpenders: [proxy],
300
+ actions: [
301
+ swapAction(swapper, SWAP_EXACT_IN_SELECTOR, rulesFor(EXACT_IN, 'amountIn'), routeAlternatives),
302
+ swapAction(swapper, SWAP_EXACT_OUT_SELECTOR, rulesFor(EXACT_OUT, 'amountInMax'), routeAlternatives),
303
+ ],
304
+ };
305
+ }
@@ -0,0 +1,73 @@
1
+ import type { Address, Hex } from 'viem';
2
+ import type { ScopedAction, UniversalActionPolicyParamRule } from '../types.js';
3
+ /**
4
+ * Shared rule builders for swap-venue scoping.
5
+ *
6
+ * Venue modules describe *what* to pin; these turn that into policy rules with
7
+ * the security-relevant details (cumulative accumulation, zero native value)
8
+ * decided in exactly one place.
9
+ */
10
+ /**
11
+ * Pin a calldata word to an exact numeric value.
12
+ *
13
+ * Used for ABI *shape* words — array pointers, lengths, element offsets. Pinning
14
+ * those is what makes pinning anything inside a dynamic tail sound: without
15
+ * them a caller can re-lay-out the encoding so a fixed offset lands on a
16
+ * different word, and the rule silently validates the wrong bytes.
17
+ */
18
+ export declare function pinValue(calldataOffset: bigint, referenceValue: bigint): UniversalActionPolicyParamRule;
19
+ /** Pin a calldata word to an exact 32-byte value. */
20
+ export declare function pinWord(calldataOffset: bigint, referenceValue: Hex): UniversalActionPolicyParamRule;
21
+ /** Pin a calldata word to an exact address. */
22
+ export declare function pin(calldataOffset: bigint, referenceValue: Address): UniversalActionPolicyParamRule;
23
+ /**
24
+ * Cap a swap's own sell amount, cumulatively across every call.
25
+ *
26
+ * `usageLimit` is what makes this cumulative: it sets `isLimited` + `usage.limit`
27
+ * on-chain, so the policy accumulates the observed value and reverts once the
28
+ * running total would exceed the cap.
29
+ *
30
+ * A bare comparison would be per-call, which is not enough. A reusable session
31
+ * could then run N swaps of `cap` each, and any allowance that already existed
32
+ * before the session was created is spent without the approve's spending-limit
33
+ * ever observing it.
34
+ */
35
+ export declare function cumulativeCap(calldataOffset: bigint, cap: bigint): UniversalActionPolicyParamRule;
36
+ export declare function swapAction(target: Address, selector: `0x${string}`, rules: UniversalActionPolicyParamRule[],
37
+ /**
38
+ * Mutually exclusive rule sets, at least one of which must hold — used when
39
+ * several venues authorise the SAME call but pin its tail to different
40
+ * aggregators. One on-chain action id cannot carry two policies, and dropping
41
+ * the pins to share it would authorise a tail neither venue named, so the
42
+ * alternatives become an OR instead.
43
+ */
44
+ alternatives?: UniversalActionPolicyParamRule[][]): ScopedAction;
45
+ /** What a venue module returns for one configured venue. */
46
+ export interface VenueScoping {
47
+ /**
48
+ * ERC-20 spenders the session may approve the sell token to. Plural because a
49
+ * venue reachable by more than one call shape has a different allowance
50
+ * target per shape — 0x direct pulls via its AllowanceHolder, the same swap
51
+ * wrapped pulls via the Swapper's proxy.
52
+ */
53
+ readonly approveSpenders: readonly Address[];
54
+ /**
55
+ * One or more scoped actions. Plural because a venue may expose several
56
+ * entrypoints that are all legitimate for the same scope — the Rhinestone
57
+ * Swapper has separate exact-in and exact-out selectors, and which one the
58
+ * orchestrator picks is its choice, not the caller's.
59
+ */
60
+ readonly actions: readonly ScopedAction[];
61
+ }
62
+ /** Everything a venue needs to know about the swap being scoped. */
63
+ export interface VenueContext {
64
+ readonly chainId: number;
65
+ /** Selects the Swapper/proxy deployment pair. */
66
+ readonly environment: 'production' | 'development';
67
+ readonly sellToken: Address;
68
+ readonly buyToken: Address;
69
+ readonly recipient: Address;
70
+ /** Cumulative sell-token cap, or undefined for no cap. */
71
+ readonly cap: bigint | undefined;
72
+ }
73
+ //# sourceMappingURL=rules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../../../../../modules/validators/smart-sessions/swap/rules.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,MAAM,CAAA;AACxC,OAAO,KAAK,EAEV,YAAY,EAEZ,8BAA8B,EAC/B,MAAM,UAAU,CAAA;AAEjB;;;;;;GAMG;AAEH;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CACtB,cAAc,EAAE,MAAM,EACtB,cAAc,EAAE,MAAM,GACrB,8BAA8B,CAEhC;AAED,qDAAqD;AACrD,wBAAgB,OAAO,CACrB,cAAc,EAAE,MAAM,EACtB,cAAc,EAAE,GAAG,GAClB,8BAA8B,CAEhC;AAED,+CAA+C;AAC/C,wBAAgB,GAAG,CACjB,cAAc,EAAE,MAAM,EACtB,cAAc,EAAE,OAAO,GACtB,8BAA8B,CAEhC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAC3B,cAAc,EAAE,MAAM,EACtB,GAAG,EAAE,MAAM,GACV,8BAA8B,CAOhC;AAgCD,wBAAgB,UAAU,CACxB,MAAM,EAAE,OAAO,EACf,QAAQ,EAAE,KAAK,MAAM,EAAE,EACvB,KAAK,EAAE,8BAA8B,EAAE;AACvC;;;;;;GAMG;AACH,YAAY,GAAE,8BAA8B,EAAE,EAAO,GACpD,YAAY,CA0Bd;AAED,4DAA4D;AAC5D,MAAM,WAAW,YAAY;IAC3B;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,EAAE,SAAS,OAAO,EAAE,CAAA;IAC5C;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,CAAA;CAC1C;AAED,oEAAoE;AACpE,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,iDAAiD;IACjD,QAAQ,CAAC,WAAW,EAAE,YAAY,GAAG,aAAa,CAAA;IAClD,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;IAC3B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;IAC1B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;IAC3B,0DAA0D;IAC1D,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CACjC"}