@rhinestone/sdk 2.4.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.
- package/dist/src/actions/mfa.d.ts +5 -4
- package/dist/src/actions/mfa.d.ts.map +1 -1
- package/dist/src/actions/mfa.js +11 -4
- package/dist/src/api/account.d.ts.map +1 -1
- package/dist/src/api/account.js +118 -0
- package/dist/src/api/compose-types.d.ts +7 -0
- package/dist/src/api/compose-types.d.ts.map +1 -1
- package/dist/src/clients/orchestrator/client.js +1 -1
- package/dist/src/clients/orchestrator/mappers.js +40 -2
- package/dist/src/clients/orchestrator/public.d.ts +16 -1
- package/dist/src/clients/orchestrator/public.d.ts.map +1 -1
- package/dist/src/clients/orchestrator/types.d.ts +6 -0
- package/dist/src/clients/orchestrator/types.d.ts.map +1 -1
- package/dist/src/clients/orchestrator/wire.d.ts +15 -2
- package/dist/src/clients/orchestrator/wire.d.ts.map +1 -1
- package/dist/src/config/account.d.ts +140 -6
- package/dist/src/config/account.d.ts.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/modules/validators/ens.d.ts +9 -0
- package/dist/src/modules/validators/ens.d.ts.map +1 -1
- package/dist/src/modules/validators/ens.js +21 -3
- package/dist/src/modules/validators/multi-factor.d.ts +4 -1
- package/dist/src/modules/validators/multi-factor.d.ts.map +1 -1
- package/dist/src/modules/validators/multi-factor.js +2 -2
- package/dist/src/modules/validators/ownable.d.ts +10 -0
- package/dist/src/modules/validators/ownable.d.ts.map +1 -1
- package/dist/src/modules/validators/ownable.js +26 -12
- package/dist/src/modules/validators/permissions.d.ts +13 -0
- package/dist/src/modules/validators/permissions.d.ts.map +1 -1
- package/dist/src/modules/validators/permissions.js +140 -2
- package/dist/src/modules/validators/policies/claim/arbiters.d.ts.map +1 -1
- package/dist/src/modules/validators/policies/claim/arbiters.js +8 -0
- package/dist/src/modules/validators/resolve.d.ts +6 -0
- package/dist/src/modules/validators/resolve.d.ts.map +1 -1
- package/dist/src/modules/validators/resolve.js +28 -6
- package/dist/src/modules/validators/smart-sessions/resolve.d.ts.map +1 -1
- package/dist/src/modules/validators/smart-sessions/resolve.js +112 -13
- package/dist/src/modules/validators/smart-sessions/swap/fynd.d.ts +92 -0
- package/dist/src/modules/validators/smart-sessions/swap/fynd.d.ts.map +1 -0
- package/dist/src/modules/validators/smart-sessions/swap/fynd.js +101 -0
- package/dist/src/modules/validators/smart-sessions/swap/rhinestone.d.ts +147 -0
- package/dist/src/modules/validators/smart-sessions/swap/rhinestone.d.ts.map +1 -0
- package/dist/src/modules/validators/smart-sessions/swap/rhinestone.js +305 -0
- package/dist/src/modules/validators/smart-sessions/swap/rules.d.ts +73 -0
- package/dist/src/modules/validators/smart-sessions/swap/rules.d.ts.map +1 -0
- package/dist/src/modules/validators/smart-sessions/swap/rules.js +105 -0
- package/dist/src/modules/validators/smart-sessions/swap/scope.d.ts +31 -0
- package/dist/src/modules/validators/smart-sessions/swap/scope.d.ts.map +1 -0
- package/dist/src/modules/validators/smart-sessions/swap/scope.js +168 -0
- package/dist/src/modules/validators/smart-sessions/swap/zero-ex.d.ts +143 -0
- package/dist/src/modules/validators/smart-sessions/swap/zero-ex.d.ts.map +1 -0
- package/dist/src/modules/validators/smart-sessions/swap/zero-ex.js +164 -0
- package/dist/src/modules/validators/smart-sessions/types.d.ts +78 -0
- package/dist/src/modules/validators/smart-sessions/types.d.ts.map +1 -1
- package/dist/src/modules/validators/types.d.ts +5 -1
- package/dist/src/modules/validators/types.d.ts.map +1 -1
- package/dist/src/modules/validators/webauthn.d.ts +33 -1
- package/dist/src/modules/validators/webauthn.d.ts.map +1 -1
- package/dist/src/modules/validators/webauthn.js +128 -13
- package/dist/src/signing/context.d.ts.map +1 -1
- package/dist/src/signing/context.js +2 -5
- package/dist/src/smart-sessions/index.d.ts +10 -5
- package/dist/src/smart-sessions/index.d.ts.map +1 -1
- package/dist/src/smart-sessions/index.js +6 -1
- package/dist/src/transactions/intents/session-signing.d.ts.map +1 -1
- package/dist/src/transactions/intents/session-signing.js +7 -1
- package/dist/src/transactions/user-operations/validator-account.d.ts.map +1 -1
- package/dist/src/transactions/user-operations/validator-account.js +2 -2
- package/package.json +1 -1
|
@@ -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"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared rule builders for swap-venue scoping.
|
|
3
|
+
*
|
|
4
|
+
* Venue modules describe *what* to pin; these turn that into policy rules with
|
|
5
|
+
* the security-relevant details (cumulative accumulation, zero native value)
|
|
6
|
+
* decided in exactly one place.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Pin a calldata word to an exact numeric value.
|
|
10
|
+
*
|
|
11
|
+
* Used for ABI *shape* words — array pointers, lengths, element offsets. Pinning
|
|
12
|
+
* those is what makes pinning anything inside a dynamic tail sound: without
|
|
13
|
+
* them a caller can re-lay-out the encoding so a fixed offset lands on a
|
|
14
|
+
* different word, and the rule silently validates the wrong bytes.
|
|
15
|
+
*/
|
|
16
|
+
export function pinValue(calldataOffset, referenceValue) {
|
|
17
|
+
return { condition: 'equal', calldataOffset, referenceValue };
|
|
18
|
+
}
|
|
19
|
+
/** Pin a calldata word to an exact 32-byte value. */
|
|
20
|
+
export function pinWord(calldataOffset, referenceValue) {
|
|
21
|
+
return { condition: 'equal', calldataOffset, referenceValue };
|
|
22
|
+
}
|
|
23
|
+
/** Pin a calldata word to an exact address. */
|
|
24
|
+
export function pin(calldataOffset, referenceValue) {
|
|
25
|
+
return { condition: 'equal', calldataOffset, referenceValue };
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Cap a swap's own sell amount, cumulatively across every call.
|
|
29
|
+
*
|
|
30
|
+
* `usageLimit` is what makes this cumulative: it sets `isLimited` + `usage.limit`
|
|
31
|
+
* on-chain, so the policy accumulates the observed value and reverts once the
|
|
32
|
+
* running total would exceed the cap.
|
|
33
|
+
*
|
|
34
|
+
* A bare comparison would be per-call, which is not enough. A reusable session
|
|
35
|
+
* could then run N swaps of `cap` each, and any allowance that already existed
|
|
36
|
+
* before the session was created is spent without the approve's spending-limit
|
|
37
|
+
* ever observing it.
|
|
38
|
+
*/
|
|
39
|
+
export function cumulativeCap(calldataOffset, cap) {
|
|
40
|
+
return {
|
|
41
|
+
condition: 'lessThanOrEqual',
|
|
42
|
+
calldataOffset,
|
|
43
|
+
referenceValue: cap,
|
|
44
|
+
usageLimit: cap,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Wrap rules into the action's policy.
|
|
49
|
+
*
|
|
50
|
+
* `valueLimitPerUse: 0n` because the approve cap only bounds ERC-20 pulls —
|
|
51
|
+
* without it, a payable swap selector could still carry arbitrary native value
|
|
52
|
+
* through the router.
|
|
53
|
+
*/
|
|
54
|
+
/** Every rule must hold, as a right-folded AND over the expression tree. */
|
|
55
|
+
function allOf(rules) {
|
|
56
|
+
return rules
|
|
57
|
+
.map((rule) => ({ type: 'rule', rule }))
|
|
58
|
+
.reduceRight((right, left) => ({ type: 'and', left, right }));
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Wrap rules into the action's policy.
|
|
62
|
+
*
|
|
63
|
+
* `valueLimitPerUse: 0n` because the approve cap only bounds ERC-20 pulls —
|
|
64
|
+
* without it, a payable swap selector could still carry arbitrary native value
|
|
65
|
+
* through the router.
|
|
66
|
+
*
|
|
67
|
+
* UniversalActionPolicy while the rules fit its fixed 16-slot array, which is
|
|
68
|
+
* every venue except a fully-pinned wrapped route — those pin the shape words,
|
|
69
|
+
* both call targets, the approve and the nested exec, and run past 16. ArgPolicy
|
|
70
|
+
* takes a dynamic rule list, so it carries the overflow rather than forcing a
|
|
71
|
+
* pin to be dropped. Preferring the simpler policy keeps the common case on the
|
|
72
|
+
* contract it has always used.
|
|
73
|
+
*/
|
|
74
|
+
const UNIVERSAL_ACTION_MAX_RULES = 16;
|
|
75
|
+
export function swapAction(target, selector, rules,
|
|
76
|
+
/**
|
|
77
|
+
* Mutually exclusive rule sets, at least one of which must hold — used when
|
|
78
|
+
* several venues authorise the SAME call but pin its tail to different
|
|
79
|
+
* aggregators. One on-chain action id cannot carry two policies, and dropping
|
|
80
|
+
* the pins to share it would authorise a tail neither venue named, so the
|
|
81
|
+
* alternatives become an OR instead.
|
|
82
|
+
*/
|
|
83
|
+
alternatives = []) {
|
|
84
|
+
const usable = alternatives.filter((set) => set.length > 0);
|
|
85
|
+
const policy = usable.length > 0
|
|
86
|
+
? {
|
|
87
|
+
type: 'arg-policy',
|
|
88
|
+
valueLimitPerUse: 0n,
|
|
89
|
+
expression: {
|
|
90
|
+
type: 'and',
|
|
91
|
+
left: allOf(rules),
|
|
92
|
+
right: usable
|
|
93
|
+
.map(allOf)
|
|
94
|
+
.reduce((left, right) => ({ type: 'or', left, right })),
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
: rules.length <= UNIVERSAL_ACTION_MAX_RULES
|
|
98
|
+
? {
|
|
99
|
+
type: 'universal-action',
|
|
100
|
+
valueLimitPerUse: 0n,
|
|
101
|
+
rules: rules,
|
|
102
|
+
}
|
|
103
|
+
: { type: 'arg-policy', valueLimitPerUse: 0n, expression: allOf(rules) };
|
|
104
|
+
return { target, selector, policies: [policy] };
|
|
105
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { FyndVenue, Permission, RhinestoneSwapVenue, ScopedAction, SwapScopeInput, SwapVenue, ZeroExVenue } from '../types.js';
|
|
2
|
+
import { type FyndChainId } from './fynd.js';
|
|
3
|
+
import { type ZeroExChainId } from './zero-ex.js';
|
|
4
|
+
/**
|
|
5
|
+
* Venue-scoped swap sessions (RHI-6286).
|
|
6
|
+
*
|
|
7
|
+
* A session scoped with `swap` may do exactly two things: approve the sell token
|
|
8
|
+
* to a listed venue's spender, and call that venue's swap entrypoint with the
|
|
9
|
+
* sell token, buy token and recipient pinned. Everything else reverts.
|
|
10
|
+
*
|
|
11
|
+
* Callers name venues (`zeroEx(...)`, `fynd()`); routers, selectors and calldata
|
|
12
|
+
* offsets live in the per-venue modules and never reach the public surface, so
|
|
13
|
+
* they stay patchable without a breaking change.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Venues valid on a given chain.
|
|
17
|
+
*
|
|
18
|
+
* Narrows `swap.via` by the session's chain id, so naming a venue that is not
|
|
19
|
+
* deployed there is a compile error rather than a revert at swap time.
|
|
20
|
+
*/
|
|
21
|
+
export type SwapVenueFor<TChainId extends number> = number extends TChainId ? SwapVenue : RhinestoneSwapVenue | (TChainId extends ZeroExChainId ? ZeroExVenue : never) | (TChainId extends FyndChainId ? FyndVenue : never);
|
|
22
|
+
export interface ResolvedSwapScope {
|
|
23
|
+
readonly permissions: Permission[];
|
|
24
|
+
readonly actions: ScopedAction[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Compile a `swap` scope into one merged approve permission plus one scoped
|
|
28
|
+
* action per venue.
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveSwapScope(scope: SwapScopeInput, chainId: number, environment?: 'production' | 'development'): ResolvedSwapScope;
|
|
31
|
+
//# sourceMappingURL=scope.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scope.d.ts","sourceRoot":"","sources":["../../../../../../modules/validators/smart-sessions/swap/scope.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,SAAS,EACT,UAAU,EACV,mBAAmB,EAEnB,YAAY,EACZ,cAAc,EACd,SAAS,EACT,WAAW,EACZ,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,KAAK,WAAW,EAAa,MAAM,QAAQ,CAAA;AAGpD,OAAO,EAAe,KAAK,aAAa,EAAE,MAAM,WAAW,CAAA;AAE3D;;;;;;;;;;GAUG;AAEH;;;;;GAKG;AACH,MAAM,MAAM,YAAY,CAAC,QAAQ,SAAS,MAAM,IAAI,MAAM,SAAS,QAAQ,GAMvE,SAAS,GAGL,mBAAmB,GACnB,CAAC,QAAQ,SAAS,aAAa,GAAG,WAAW,GAAG,KAAK,CAAC,GACtD,CAAC,QAAQ,SAAS,WAAW,GAAG,SAAS,GAAG,KAAK,CAAC,CAAA;AAE1D,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,WAAW,EAAE,UAAU,EAAE,CAAA;IAClC,QAAQ,CAAC,OAAO,EAAE,YAAY,EAAE,CAAA;CACjC;AAuDD;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,cAAc,EACrB,OAAO,EAAE,MAAM,EACf,WAAW,GAAE,YAAY,GAAG,aAA4B,GACvD,iBAAiB,CAqGnB"}
|