@crediolabs/policy-synth 0.2.0 → 0.3.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/README.md +4 -4
- package/dist/adapters/interpreter/adapter.d.ts +19 -25
- package/dist/adapters/interpreter/adapter.js +54 -487
- package/dist/adapters/interpreter/index.d.ts +1 -1
- package/dist/adapters/interpreter/index.js +1 -1
- package/dist/adapters/oz/adapter.js +1 -15
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +1 -4
- package/dist/index.js +1 -4
- package/dist/install/build-add-context-rule.d.ts +3 -19
- package/dist/install/build-add-context-rule.js +3 -98
- package/dist/install/build-install-policy.d.ts +4 -53
- package/dist/install/build-install-policy.js +44 -123
- package/dist/install/index.d.ts +0 -2
- package/dist/install/index.js +0 -7
- package/dist/ir/index.d.ts +1 -1
- package/dist/ir/types.d.ts +7 -76
- package/dist/ir/types.js +0 -2
- package/dist/predicate/decode.d.ts +1 -1
- package/dist/predicate/decode.js +2 -70
- package/dist/predicate/encode.js +45 -229
- package/dist/predicate/from-json.js +1 -42
- package/dist/record/decode.js +3 -8
- package/dist/review-card/builder.d.ts +4 -3
- package/dist/review-card/builder.js +18 -162
- package/dist/review-card/cross-check.js +2 -105
- package/dist/review-card/render-leaf.d.ts +4 -0
- package/dist/review-card/render-leaf.js +47 -0
- package/dist/run/index.d.ts +36 -139
- package/dist/run/index.js +125 -437
- package/dist/run/schemas.d.ts +168 -1929
- package/dist/run/schemas.js +40 -269
- package/dist/seams/types.d.ts +0 -2
- package/dist/simulate/deny-cases.d.ts +26 -0
- package/dist/simulate/deny-cases.js +282 -0
- package/dist/simulate/evaluate.d.ts +18 -0
- package/dist/simulate/evaluate.js +271 -0
- package/dist/simulate/index.d.ts +4 -0
- package/dist/simulate/index.js +8 -0
- package/dist/synth/compose-from-recording.d.ts +24 -51
- package/dist/synth/compose-from-recording.js +120 -282
- package/dist/synth/deny-cases.d.ts +5 -7
- package/dist/synth/deny-cases.js +23 -373
- package/dist/synth/evaluate.d.ts +0 -21
- package/dist/synth/evaluate.js +56 -336
- package/dist/synth/index.d.ts +0 -5
- package/dist/synth/index.js +0 -5
- package/dist/synth/permit-context.d.ts +2 -9
- package/dist/synth/permit-context.js +1 -82
- package/dist/synth/synthesize-from-recording.d.ts +1 -29
- package/dist/synth/synthesize-from-recording.js +41 -356
- package/dist/types.d.ts +8 -60
- package/dist/types.js +5 -2
- package/dist/verify/simulate.d.ts +1 -14
- package/dist/verify/simulate.js +2 -88
- package/dist/verify/verify.d.ts +1 -7
- package/dist/verify/verify.js +0 -8
- package/dist-cjs/adapters/interpreter/adapter.d.ts +19 -25
- package/dist-cjs/adapters/interpreter/adapter.js +56 -489
- package/dist-cjs/adapters/interpreter/index.d.ts +1 -1
- package/dist-cjs/adapters/interpreter/index.js +1 -2
- package/dist-cjs/adapters/oz/adapter.js +1 -15
- package/dist-cjs/errors.d.ts +1 -1
- package/dist-cjs/index.d.ts +1 -4
- package/dist-cjs/index.js +1 -4
- package/dist-cjs/install/build-add-context-rule.d.ts +3 -19
- package/dist-cjs/install/build-add-context-rule.js +1 -97
- package/dist-cjs/install/build-install-policy.d.ts +4 -53
- package/dist-cjs/install/build-install-policy.js +44 -122
- package/dist-cjs/install/index.d.ts +0 -2
- package/dist-cjs/install/index.js +2 -23
- package/dist-cjs/ir/index.d.ts +1 -1
- package/dist-cjs/ir/types.d.ts +7 -76
- package/dist-cjs/ir/types.js +0 -2
- package/dist-cjs/predicate/decode.d.ts +1 -1
- package/dist-cjs/predicate/decode.js +2 -70
- package/dist-cjs/predicate/encode.js +45 -229
- package/dist-cjs/predicate/from-json.js +1 -42
- package/dist-cjs/record/decode.js +3 -8
- package/dist-cjs/review-card/builder.d.ts +4 -3
- package/dist-cjs/review-card/builder.js +23 -167
- package/dist-cjs/review-card/cross-check.js +7 -110
- package/dist-cjs/review-card/render-leaf.d.ts +4 -0
- package/dist-cjs/review-card/render-leaf.js +52 -0
- package/dist-cjs/run/index.d.ts +36 -139
- package/dist-cjs/run/index.js +125 -444
- package/dist-cjs/run/schemas.d.ts +168 -1929
- package/dist-cjs/run/schemas.js +41 -270
- package/dist-cjs/seams/types.d.ts +0 -2
- package/dist-cjs/simulate/deny-cases.d.ts +26 -0
- package/dist-cjs/simulate/deny-cases.js +286 -0
- package/dist-cjs/simulate/evaluate.d.ts +18 -0
- package/dist-cjs/simulate/evaluate.js +274 -0
- package/dist-cjs/simulate/index.d.ts +4 -0
- package/dist-cjs/simulate/index.js +13 -0
- package/dist-cjs/synth/compose-from-recording.d.ts +24 -51
- package/dist-cjs/synth/compose-from-recording.js +120 -282
- package/dist-cjs/synth/deny-cases.d.ts +5 -7
- package/dist-cjs/synth/deny-cases.js +23 -374
- package/dist-cjs/synth/evaluate.d.ts +0 -21
- package/dist-cjs/synth/evaluate.js +57 -337
- package/dist-cjs/synth/index.d.ts +0 -5
- package/dist-cjs/synth/index.js +1 -11
- package/dist-cjs/synth/permit-context.d.ts +2 -9
- package/dist-cjs/synth/permit-context.js +1 -82
- package/dist-cjs/synth/synthesize-from-recording.d.ts +1 -29
- package/dist-cjs/synth/synthesize-from-recording.js +39 -354
- package/dist-cjs/types.d.ts +8 -60
- package/dist-cjs/types.js +6 -3
- package/dist-cjs/verify/simulate.d.ts +1 -14
- package/dist-cjs/verify/simulate.js +2 -88
- package/dist-cjs/verify/verify.d.ts +1 -7
- package/dist-cjs/verify/verify.js +0 -8
- package/package.json +4 -4
- package/src/adapters/interpreter/adapter.ts +76 -572
- package/src/errors.ts +0 -19
- package/src/index.ts +1 -4
- package/src/install/build-add-context-rule.ts +5 -139
- package/src/install/build-install-policy.ts +93 -214
- package/src/install/index.ts +0 -34
- package/src/predicate/decode.ts +2 -70
- package/src/predicate/encode.ts +49 -261
- package/src/predicate/from-json.ts +1 -43
- package/src/record/decode.ts +3 -8
- package/src/review-card/builder.ts +19 -168
- package/src/review-card/cross-check.ts +3 -105
- package/src/review-card/render-leaf.ts +48 -0
- package/src/run/index.ts +144 -572
- package/src/run/schemas.ts +42 -291
- package/src/simulate/deny-cases.ts +334 -0
- package/src/simulate/evaluate.ts +284 -0
- package/src/simulate/index.ts +11 -0
- package/src/synth/compose-from-recording.ts +148 -347
- package/src/synth/index.ts +0 -13
- package/src/synth/synthesize-from-recording.ts +43 -456
- package/src/types.ts +13 -49
- package/dist/install/authority-overlap.d.ts +0 -134
- package/dist/install/authority-overlap.js +0 -0
- package/dist/install/build-install-predicate.d.ts +0 -96
- package/dist/install/build-install-predicate.js +0 -444
- package/dist/install/build-merge-policy.d.ts +0 -70
- package/dist/install/build-merge-policy.js +0 -130
- package/dist/install/plan-merge-policy.d.ts +0 -49
- package/dist/install/plan-merge-policy.js +0 -86
- package/dist/install/read-account-rules.d.ts +0 -100
- package/dist/install/read-account-rules.js +0 -283
- package/dist-cjs/install/authority-overlap.d.ts +0 -134
- package/dist-cjs/install/authority-overlap.js +0 -0
- package/dist-cjs/install/build-install-predicate.d.ts +0 -96
- package/dist-cjs/install/build-install-predicate.js +0 -479
- package/dist-cjs/install/build-merge-policy.d.ts +0 -70
- package/dist-cjs/install/build-merge-policy.js +0 -134
- package/dist-cjs/install/plan-merge-policy.d.ts +0 -49
- package/dist-cjs/install/plan-merge-policy.js +0 -90
- package/dist-cjs/install/read-account-rules.d.ts +0 -100
- package/dist-cjs/install/read-account-rules.js +0 -296
- package/src/adapters/interpreter/index.ts +0 -8
- package/src/adapters/oz/adapter.ts +0 -376
- package/src/adapters/oz/index.ts +0 -9
- package/src/codegen/compile-gate.ts +0 -167
- package/src/codegen/index.ts +0 -17
- package/src/codegen/template.ts +0 -165
- package/src/install/authority-overlap.ts +0 -0
- package/src/install/build-merge-policy.ts +0 -219
- package/src/install/plan-merge-policy.ts +0 -133
- package/src/install/read-account-rules.ts +0 -376
- package/src/ir/index.ts +0 -13
- package/src/ir/types.ts +0 -132
- package/src/mandate/index.ts +0 -4
- package/src/mandate/to-ir.ts +0 -71
- package/src/mandate/types.ts +0 -21
- package/src/seams/index.ts +0 -11
- package/src/seams/types.ts +0 -81
- package/src/synth/deny-cases.ts +0 -663
- package/src/synth/evaluate.ts +0 -613
- package/src/synth/harness.ts +0 -68
- package/src/synth/minimize.ts +0 -48
- package/src/synth/permit-context.ts +0 -136
- package/src/synth/predicate-literals.ts +0 -27
- package/src/synth/synthesize-from-mandate.ts +0 -82
- package/src/verify/envelope.ts +0 -28
- package/src/verify/index.ts +0 -5
- package/src/verify/simulate.ts +0 -311
- package/src/verify/verify.ts +0 -243
|
@@ -1,205 +1,108 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// src/synth/compose-from-recording.ts - facts + scope ->
|
|
2
|
+
// src/synth/compose-from-recording.ts - facts + scope -> a composed rule.
|
|
3
3
|
//
|
|
4
4
|
// Fail-closed composition rules:
|
|
5
5
|
// - unknown top-level protocol (registry.identifyProtocol returns null) ->
|
|
6
|
-
// emit no
|
|
7
|
-
//
|
|
8
|
-
// call never compiles to a permissive
|
|
9
|
-
// -
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// the
|
|
14
|
-
// (userResponses.windowSeconds). A single recorded spend does NOT authorise
|
|
15
|
-
// that amount every window, so the observed amount is NEVER used as an
|
|
16
|
-
// auto-ceiling: missing limit -> AMOUNT_BOUND_MISSING (observed amount
|
|
17
|
-
// offered as a suggestion); missing window -> DURATION_UNSPECIFIED. EVERY
|
|
6
|
+
// emit no constraint from the spend; scope is kept (contract + method) and
|
|
7
|
+
// every inferred bound surfaces as a descriptive warning. An unrecognised
|
|
8
|
+
// call never compiles to a permissive policy.
|
|
9
|
+
// - a spend cap is emitted ONLY when the caller supplies `limitAmount` AND
|
|
10
|
+
// the call carries an amount argument to bind it to. A single recorded
|
|
11
|
+
// spend does NOT authorise that amount on every call, so the observed
|
|
12
|
+
// amount is NEVER used as an auto-ceiling: without both,
|
|
13
|
+
// AMOUNT_BOUND_MISSING carries the observed amount as a suggestion. EVERY
|
|
18
14
|
// spent token is handled - none is silently dropped.
|
|
19
|
-
// -
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// - the IR carries ONLY constraints justified by the recording + explicit
|
|
23
|
-
// user input. Nothing invented: no oracle price fabricated from a slippage
|
|
24
|
-
// bound, no synthetic exact-path compare. Those needs surface as warnings.
|
|
25
|
-
//
|
|
26
|
-
// Split rule (P3 wiring): `ComposeResult` carries BOTH `ir` (OZ-shape) and
|
|
27
|
-
// `interpreterIr` (predicate-shape). Each constraint is routed to EXACTLY ONE
|
|
28
|
-
// adapter:
|
|
29
|
-
// - `window_spent(token, w) <= limit` where `token === scope.contract` and
|
|
30
|
-
// protocol is known -> `ir` (OZ lowers to spending_limit).
|
|
31
|
-
// - everything else (recipient allowlists, per-method scope.method,
|
|
32
|
-
// invocation_count, eq_seq swap paths, oracle_price, AND window_spent
|
|
33
|
-
// where token != scope.contract i.e. SoroSwap input-token cap) ->
|
|
34
|
-
// `interpreterIr`.
|
|
35
|
-
//
|
|
36
|
-
// This prevents the interpreter adapter from emitting a duplicate
|
|
37
|
-
// `window_spent` predicate leaf alongside an OZ `spending_limit` primitive
|
|
38
|
-
// covering the same spend semantic - the two adapters never overlap.
|
|
15
|
+
// - the IR carries ONLY constraints justified by the recording plus an
|
|
16
|
+
// explicit bound, no synthetic exact-path compare. Other needs surface as
|
|
17
|
+
// warnings.
|
|
39
18
|
//
|
|
40
19
|
// Default policy is `deny_all` (OZ context rules are deny-by-default).
|
|
41
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
21
|
exports.composeFromRecording = composeFromRecording;
|
|
43
22
|
const identify_ts_1 = require("../registry/identify.js");
|
|
44
|
-
|
|
23
|
+
const protocols_ts_1 = require("../registry/protocols.js");
|
|
24
|
+
/** Compose a rule from the lowered facts + the resolved scope.
|
|
45
25
|
* Pure (no randomness, no clock); same inputs -> byte-identical result. */
|
|
46
26
|
function composeFromRecording(facts, scopeContract, topLevel, opts) {
|
|
47
|
-
const interpreterEnabled = opts.interpreterEnabled === true;
|
|
48
27
|
const ambiguities = [];
|
|
49
28
|
const warnings = [];
|
|
50
|
-
const ozConstraints = [];
|
|
51
29
|
const interpreterConstraints = [];
|
|
52
|
-
// Route to the matching IR. When the interpreter is enabled, constraints the
|
|
53
|
-
// OZ adapter cannot lower go there; otherwise they go to OZ (which flags them
|
|
54
|
-
// as uncovered) so today's warning-driven behaviour is preserved.
|
|
55
|
-
const routeToAdapter = (cond) => {
|
|
56
|
-
if (interpreterEnabled)
|
|
57
|
-
interpreterConstraints.push(cond);
|
|
58
|
-
else
|
|
59
|
-
ozConstraints.push(cond);
|
|
60
|
-
};
|
|
61
30
|
const protocol = topLevel
|
|
62
31
|
? (0, identify_ts_1.identifyProtocol)(topLevel.contract, topLevel.fn, topLevel.args, opts.network)
|
|
63
32
|
: null;
|
|
64
33
|
const known = protocol !== null;
|
|
65
|
-
const windowSeconds = opts.userResponses?.windowSeconds;
|
|
66
34
|
const limitAmount = opts.userResponses?.limitAmount;
|
|
67
35
|
const spendTokens = Object.keys(facts.spendByToken);
|
|
68
|
-
// Outgoing spend ->
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
// Routing: a `window_spent(token, w) <= limit` constraint goes to the OZ IR
|
|
74
|
-
// only when token === scope.contract (OZ's spending_limit binds the
|
|
75
|
-
// CallContract target, not a token parameter). Otherwise it goes to the
|
|
76
|
-
// interpreter IR.
|
|
36
|
+
// Outgoing spend -> a cap on the call's own amount ARGUMENT. The interpreter
|
|
37
|
+
// is passed one authorised call, not the transaction's token movements, so a
|
|
38
|
+
// rolling per-window total is not something it can read; the enforceable
|
|
39
|
+
// shape is a bound on the amount the call itself carries. A multi-token flow
|
|
40
|
+
// has no single argument to bind, so each token surfaces AMOUNT_BOUND_MISSING.
|
|
77
41
|
if (spendTokens.length > 0 && topLevel) {
|
|
78
|
-
let durationFlagged = false;
|
|
79
42
|
for (const token of spendTokens) {
|
|
80
43
|
const observed = facts.spendByToken[token];
|
|
81
44
|
if (observed === undefined)
|
|
82
45
|
continue;
|
|
83
46
|
if (!known) {
|
|
84
|
-
warnings.push(`spend of ${observed} (token ${token}) not bounded: unrecognised protocol,
|
|
47
|
+
warnings.push(`spend of ${observed} (token ${token}) not bounded: unrecognised protocol, so the amount argument cannot be located`);
|
|
85
48
|
continue;
|
|
86
49
|
}
|
|
87
50
|
const limit = spendTokens.length === 1 ? limitAmount : undefined;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
code: 'DURATION_UNSPECIFIED',
|
|
91
|
-
question: `Recording shows a ${observed}-${token} spend. What rolling window (seconds) should the spending_limit use?`,
|
|
92
|
-
});
|
|
93
|
-
durationFlagged = true;
|
|
94
|
-
}
|
|
95
|
-
if (limit === undefined) {
|
|
51
|
+
const amountArgIndex = limitArgumentIndex(protocol, topLevel);
|
|
52
|
+
if (limit === undefined || amountArgIndex === null) {
|
|
96
53
|
ambiguities.push({
|
|
97
54
|
code: 'AMOUNT_BOUND_MISSING',
|
|
98
|
-
question: `Recording shows a ${observed}-${token} spend, but a single spend does not authorise that amount every
|
|
55
|
+
question: `Recording shows a ${observed}-${token} spend, but a single spend does not authorise that amount on every call. What per-call cap should apply? (observed amount, suggestion only: ${observed})`,
|
|
99
56
|
});
|
|
100
57
|
continue;
|
|
101
58
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
// accumulate and the counter would never move. OZ reports the case it
|
|
108
|
-
// cannot cover (limit pins to the context contract) - the honest
|
|
109
|
-
// outcome; the old fallback produced an interpreter predicate that
|
|
110
|
-
// silently never bound.
|
|
111
|
-
ozConstraints.push({
|
|
112
|
-
op: 'compare',
|
|
113
|
-
compare: {
|
|
114
|
-
selector: { kind: 'window_spent', token, windowSeconds },
|
|
115
|
-
operator: 'lte',
|
|
116
|
-
value: limit,
|
|
117
|
-
},
|
|
118
|
-
});
|
|
119
|
-
if (interpreterEnabled && token !== scopeContract) {
|
|
120
|
-
warnings.push(`rolling spend cap on ${token} cannot be enforced on chain: OZ spending_limit pins the limit to the context contract, and the interpreter cannot observe token movements. Bound the per-call value with an argument cap plus an invocation-count limit instead.`);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
59
|
+
interpreterConstraints.push({
|
|
60
|
+
op: 'lte',
|
|
61
|
+
left: { kind: 'call_arg', index: amountArgIndex },
|
|
62
|
+
right: { kind: 'literal_i128', value: limit },
|
|
63
|
+
});
|
|
123
64
|
}
|
|
124
65
|
}
|
|
125
|
-
// Incoming-only / frequency intent:
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
// invocation_count); otherwise to the OZ IR (which flags it as uncovered).
|
|
66
|
+
// Incoming-only / frequency intent: the interpreter is passed one authorised
|
|
67
|
+
// call, so it cannot count prior calls - no frequency bound is emitted and
|
|
68
|
+
// never a fabricated `<= 1`. The need surfaces as a prompt plus a warning.
|
|
129
69
|
//
|
|
130
70
|
// A recognised swap is NOT an incoming-only flow: it has an outgoing input
|
|
131
71
|
// leg whose spend simply was not attributed to the source account
|
|
132
72
|
// (fee-sponsored / holder != source). Its real restrictions - exact path,
|
|
133
73
|
// recipient, input-amount cap - come from the protocol-specific pass, so it
|
|
134
|
-
// does NOT get the incoming-only frequency prompt.
|
|
135
|
-
// rate-limit the swap can still supply an invocationLimit + window, which
|
|
136
|
-
// lowers to an invocation_count for any flow.
|
|
74
|
+
// does NOT get the incoming-only frequency prompt.
|
|
137
75
|
if (spendTokens.length === 0 && topLevel) {
|
|
138
|
-
const invocationLimit = opts.userResponses?.invocationLimit;
|
|
139
76
|
const isRecognisedSwap = protocol?.protocol === 'soroswap';
|
|
140
|
-
if (
|
|
141
|
-
routeToAdapter({
|
|
142
|
-
op: 'compare',
|
|
143
|
-
compare: {
|
|
144
|
-
selector: { kind: 'invocation_count', windowSeconds },
|
|
145
|
-
// `lt`, not `lte`: the leaf reports the calls ALREADY made in the
|
|
146
|
-
// window, so `< N` is what permits N of them. With `lte` a limit
|
|
147
|
-
// of N let an N+1th call through.
|
|
148
|
-
operator: 'lt',
|
|
149
|
-
value: String(invocationLimit),
|
|
150
|
-
},
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
else if (!isRecognisedSwap) {
|
|
77
|
+
if (!isRecognisedSwap) {
|
|
154
78
|
ambiguities.push({
|
|
155
79
|
code: 'FREQUENCY_BOUND_MISSING',
|
|
156
|
-
question: 'Incoming-only flow -
|
|
80
|
+
question: 'Incoming-only flow - the policy does not bound how often this call may be made. Bound it outside the policy, or scope the rule more tightly.',
|
|
157
81
|
});
|
|
158
|
-
warnings.push('frequency
|
|
82
|
+
warnings.push('incoming-only flow: call frequency is NOT bounded - the interpreter reads only the authorized call, so it cannot count prior calls');
|
|
159
83
|
}
|
|
160
84
|
}
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
if (oracleBounds) {
|
|
166
|
-
for (const b of oracleBounds) {
|
|
167
|
-
routeToAdapter({
|
|
168
|
-
op: 'compare',
|
|
169
|
-
compare: {
|
|
170
|
-
selector: { kind: 'oracle_price', asset: b.asset },
|
|
171
|
-
operator: b.operator,
|
|
172
|
-
value: b.value,
|
|
173
|
-
valueDecimals: b.decimals,
|
|
174
|
-
},
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
// Observed recipient allowlist (SEP-41) is a real, recorded constraint the OZ
|
|
179
|
-
// adapter flags as not covered; the interpreter adapter lowers it to an `in`
|
|
180
|
-
// predicate. Unknown protocols emit nothing here. SoroSwap's swap recipient
|
|
181
|
-
// (arg[3]) is the source-of-truth for the swapRecipientAllowlist surface.
|
|
85
|
+
// Observed recipient allowlist (SEP-41) is a real, recorded constraint the
|
|
86
|
+
// interpreter adapter lowers to an `in` predicate. Unknown protocols emit
|
|
87
|
+
// nothing here. SoroSwap's swap recipient (arg[3]) is the source-of-truth for
|
|
88
|
+
// the swapRecipientAllowlist surface.
|
|
182
89
|
if (topLevel && protocol !== null) {
|
|
183
|
-
// A SoroSwap input-amount cap binds the caller's limitAmount to
|
|
184
|
-
//
|
|
185
|
-
//
|
|
90
|
+
// A SoroSwap input-amount cap binds the caller's limitAmount to the swap's
|
|
91
|
+
// input-amount argument ONLY when no outgoing spend was detected for the
|
|
92
|
+
// source account - i.e. the input token never moved FROM the source
|
|
186
93
|
// (fee-sponsored swaps, or a holder != source). When a spend WAS detected,
|
|
187
|
-
// the
|
|
188
|
-
//
|
|
94
|
+
// the cap above already consumed the limit, so this one is skipped to avoid
|
|
95
|
+
// binding one limit to two different args.
|
|
189
96
|
const swapInputAmountCap = spendTokens.length === 0 ? limitAmount : undefined;
|
|
190
|
-
appendProtocolSpecificConstraints(interpreterConstraints,
|
|
97
|
+
appendProtocolSpecificConstraints(interpreterConstraints, warnings, ambiguities, facts, topLevel, protocol, opts.userResponses?.swapRecipientAllowlist, swapInputAmountCap);
|
|
191
98
|
}
|
|
192
|
-
// `scope.method`
|
|
193
|
-
//
|
|
194
|
-
// `call_fn == <method>` predicate leaf (a real restriction); the OZ adapter
|
|
195
|
-
// flags it as uncovered (CallContract alone permits any method on the
|
|
196
|
-
// contract).
|
|
99
|
+
// The interpreter adapter lowers `scope.method` into a `call_fn == <method>`
|
|
100
|
+
// predicate leaf.
|
|
197
101
|
const scope = { contract: scopeContract };
|
|
198
102
|
if (topLevel?.fn)
|
|
199
103
|
scope.method = topLevel.fn;
|
|
200
104
|
const buildRule = (constraints) => {
|
|
201
105
|
const rule = {
|
|
202
|
-
roles: [],
|
|
203
106
|
scope: { ...scope },
|
|
204
107
|
constraints,
|
|
205
108
|
};
|
|
@@ -208,39 +111,47 @@ function composeFromRecording(facts, scopeContract, topLevel, opts) {
|
|
|
208
111
|
}
|
|
209
112
|
return rule;
|
|
210
113
|
};
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
114
|
+
return { interpreterRule: buildRule(interpreterConstraints), ambiguities, warnings };
|
|
115
|
+
}
|
|
116
|
+
/** Index of the argument a caller-supplied `limitAmount` binds to, or null when
|
|
117
|
+
* the call carries no such argument. The interpreter is passed one authorised
|
|
118
|
+
* call, not the transaction's token movements, so bounding an argument the
|
|
119
|
+
* call itself carries is the only enforceable shape. */
|
|
120
|
+
function limitArgumentIndex(protocol, topLevel) {
|
|
121
|
+
if (!protocol || !topLevel)
|
|
122
|
+
return null;
|
|
123
|
+
const named = amountArgumentIndex(protocol);
|
|
124
|
+
if (named !== null)
|
|
125
|
+
return named;
|
|
126
|
+
// SoroSwap names its input argument `amount_in` / `amount_in_max`, so the
|
|
127
|
+
// lookup by name misses it and the index is function-specific.
|
|
128
|
+
if (protocol.protocol !== 'soroswap')
|
|
129
|
+
return null;
|
|
130
|
+
const i = soroswapInputAmountArgIndex(protocol.fn);
|
|
131
|
+
// Defense in depth: identification's argsMatchAbi already pins it to i128.
|
|
132
|
+
return i !== undefined && topLevel.args[i]?.type === 'i128' ? i : null;
|
|
222
133
|
}
|
|
223
|
-
/**
|
|
224
|
-
*
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
134
|
+
/** Index of the argument the protocol ABI calls `amount`, or null when the ABI
|
|
135
|
+
* does not name one. */
|
|
136
|
+
function amountArgumentIndex(protocol) {
|
|
137
|
+
const abi = (0, protocols_ts_1.getAbi)(protocol.protocol)[protocol.fn];
|
|
138
|
+
if (!abi)
|
|
139
|
+
return null;
|
|
140
|
+
const i = abi.args.findIndex((a) => a.name === 'amount');
|
|
141
|
+
return i >= 0 ? i : null;
|
|
142
|
+
}
|
|
143
|
+
/** Add the protocol-specific constraints the recording justifies: recipient
|
|
144
|
+
* allowlists, exact swap paths and per-call argument caps. */
|
|
145
|
+
function appendProtocolSpecificConstraints(interpreterConstraints, warnings, ambiguities, facts, topLevel, protocol, swapRecipientAllowlist, swapInputAmountCap) {
|
|
233
146
|
// SEP-41 transfer / mint: the `to` arg (index 1) is the recipient. Emit a
|
|
234
147
|
// single-element allowlist; the interpreter adapter lowers it to `in`.
|
|
235
|
-
// When interpreter is not enabled, route to OZ so the caller sees today's
|
|
236
|
-
// `value allowlist on arg 1` warning.
|
|
237
148
|
if (protocol.protocol === 'sep41' && (protocol.fn === 'transfer' || protocol.fn === 'mint')) {
|
|
238
149
|
const toArg = topLevel.args[1];
|
|
239
150
|
if (toArg && toArg.type === 'address') {
|
|
240
|
-
|
|
151
|
+
interpreterConstraints.push({
|
|
241
152
|
op: 'in',
|
|
242
|
-
|
|
243
|
-
|
|
153
|
+
needle: { kind: 'call_arg', index: 1 },
|
|
154
|
+
haystack: [{ kind: 'literal_address', value: toArg.value }],
|
|
244
155
|
});
|
|
245
156
|
}
|
|
246
157
|
}
|
|
@@ -255,10 +166,10 @@ function appendProtocolSpecificConstraints(interpreterConstraints, routeToAdapte
|
|
|
255
166
|
// claim(from, reserve_token_ids, to).
|
|
256
167
|
const toArg = topLevel.args[2];
|
|
257
168
|
if (toArg && toArg.type === 'address') {
|
|
258
|
-
|
|
169
|
+
interpreterConstraints.push({
|
|
259
170
|
op: 'in',
|
|
260
|
-
|
|
261
|
-
|
|
171
|
+
needle: { kind: 'call_arg', index: 2 },
|
|
172
|
+
haystack: [{ kind: 'literal_address', value: toArg.value }],
|
|
262
173
|
});
|
|
263
174
|
}
|
|
264
175
|
}
|
|
@@ -272,17 +183,14 @@ function appendProtocolSpecificConstraints(interpreterConstraints, routeToAdapte
|
|
|
272
183
|
// -> Borrow on a different asset, any amount, then auction fills). Length
|
|
273
184
|
// + per-element pinning is total; a quantifier over elements is not. If we
|
|
274
185
|
// cannot emit BOTH, we surface AMBIGUITY rather than emit a partial bind.
|
|
275
|
-
if (protocol.protocol === 'blend' && protocol.fn === 'submit'
|
|
186
|
+
if (protocol.protocol === 'blend' && protocol.fn === 'submit') {
|
|
276
187
|
const requestsArg = topLevel.args[3];
|
|
277
188
|
if (requestsArg && requestsArg.type === 'vec') {
|
|
278
189
|
const elements = requestsArg.value;
|
|
279
190
|
interpreterConstraints.push({
|
|
280
|
-
op: '
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
operator: 'eq',
|
|
284
|
-
value: String(elements.length),
|
|
285
|
-
},
|
|
191
|
+
op: 'eq',
|
|
192
|
+
left: { kind: 'call_arg_len', index: 3 },
|
|
193
|
+
right: { kind: 'literal_u32', value: elements.length },
|
|
286
194
|
});
|
|
287
195
|
for (let i = 0; i < elements.length; i++) {
|
|
288
196
|
const element = elements[i];
|
|
@@ -323,46 +231,19 @@ function appendProtocolSpecificConstraints(interpreterConstraints, routeToAdapte
|
|
|
323
231
|
continue;
|
|
324
232
|
}
|
|
325
233
|
interpreterConstraints.push({
|
|
326
|
-
op: '
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
kind: 'arg_field',
|
|
330
|
-
argIndex: 3,
|
|
331
|
-
element: i,
|
|
332
|
-
field: 'request_type',
|
|
333
|
-
scalarType: 'u32',
|
|
334
|
-
},
|
|
335
|
-
operator: 'eq',
|
|
336
|
-
value: requestType,
|
|
337
|
-
},
|
|
234
|
+
op: 'eq',
|
|
235
|
+
left: { kind: 'call_arg_field', index: 3, element: i, field: 'request_type' },
|
|
236
|
+
right: { kind: 'literal_u32', value: Number.parseInt(requestType, 10) },
|
|
338
237
|
});
|
|
339
238
|
interpreterConstraints.push({
|
|
340
|
-
op: '
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
kind: 'arg_field',
|
|
344
|
-
argIndex: 3,
|
|
345
|
-
element: i,
|
|
346
|
-
field: 'address',
|
|
347
|
-
scalarType: 'address',
|
|
348
|
-
},
|
|
349
|
-
operator: 'eq',
|
|
350
|
-
value: address,
|
|
351
|
-
},
|
|
239
|
+
op: 'eq',
|
|
240
|
+
left: { kind: 'call_arg_field', index: 3, element: i, field: 'address' },
|
|
241
|
+
right: { kind: 'literal_address', value: address },
|
|
352
242
|
});
|
|
353
243
|
interpreterConstraints.push({
|
|
354
|
-
op: '
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
kind: 'arg_field',
|
|
358
|
-
argIndex: 3,
|
|
359
|
-
element: i,
|
|
360
|
-
field: 'amount',
|
|
361
|
-
scalarType: 'i128',
|
|
362
|
-
},
|
|
363
|
-
operator: 'lte',
|
|
364
|
-
value: amount,
|
|
365
|
-
},
|
|
244
|
+
op: 'lte',
|
|
245
|
+
left: { kind: 'call_arg_field', index: 3, element: i, field: 'amount' },
|
|
246
|
+
right: { kind: 'literal_i128', value: amount },
|
|
366
247
|
});
|
|
367
248
|
}
|
|
368
249
|
}
|
|
@@ -376,17 +257,20 @@ function appendProtocolSpecificConstraints(interpreterConstraints, routeToAdapte
|
|
|
376
257
|
if (protocol.protocol === 'soroswap') {
|
|
377
258
|
const paths = facts.allowedPaths?.[topLevel.contract];
|
|
378
259
|
const route = paths?.length === 1 ? paths[0] : undefined;
|
|
379
|
-
if (route && route.length > 0
|
|
260
|
+
if (route && route.length > 0) {
|
|
380
261
|
const pathArgIndex = 2; // SoroSwap swap_exact_tokens_for_tokens: args = [amount_in, amount_out_min, path, to, deadline]
|
|
381
262
|
interpreterConstraints.push({
|
|
382
|
-
op: '
|
|
383
|
-
|
|
384
|
-
|
|
263
|
+
op: 'eq',
|
|
264
|
+
left: { kind: 'call_arg', index: pathArgIndex },
|
|
265
|
+
right: {
|
|
266
|
+
kind: 'literal_vec',
|
|
267
|
+
elements: route.map((v) => ({ kind: 'literal_address', value: v })),
|
|
268
|
+
},
|
|
385
269
|
});
|
|
386
270
|
}
|
|
387
271
|
else {
|
|
388
272
|
const pathText = route && route.length > 0 ? `; observed path: ${route.join(' -> ')}` : '';
|
|
389
|
-
warnings.push(`SoroSwap swap:
|
|
273
|
+
warnings.push(`SoroSwap swap: the exact hop path needs the interpreter predicate${pathText}`);
|
|
390
274
|
}
|
|
391
275
|
// Input-amount cap: bind the caller's limitAmount to the swap's input-amount
|
|
392
276
|
// argument as `call_arg[i] <= limit`. The index is function-specific -
|
|
@@ -396,43 +280,20 @@ function appendProtocolSpecificConstraints(interpreterConstraints, routeToAdapte
|
|
|
396
280
|
// as arg[1] (its arg[0] is the exact OUTPUT, so binding arg[0] there would
|
|
397
281
|
// cap the wrong value and leave the input unbounded). This is a per-call cap
|
|
398
282
|
// that does NOT depend on attributing a token movement to the source account
|
|
399
|
-
// (the fee-sponsored / holder != source case, where
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
// it uncovered (a warning). The `type === 'i128'` check is defense in depth -
|
|
404
|
-
// protocol identification's argsMatchAbi already pins the input arg to i128.
|
|
283
|
+
// (the fee-sponsored / holder != source case, where no spend is detected).
|
|
284
|
+
// Fail-closed: it only ever restricts the permitted input. The
|
|
285
|
+
// `type === 'i128'` check is defense in depth - protocol identification's
|
|
286
|
+
// argsMatchAbi already pins the input arg to i128.
|
|
405
287
|
const inputArgIndex = soroswapInputAmountArgIndex(protocol.fn);
|
|
406
288
|
const inputAmountArg = inputArgIndex !== undefined ? topLevel.args[inputArgIndex] : undefined;
|
|
407
289
|
if (swapInputAmountCap !== undefined &&
|
|
408
290
|
inputArgIndex !== undefined &&
|
|
409
291
|
inputAmountArg &&
|
|
410
292
|
inputAmountArg.type === 'i128') {
|
|
411
|
-
|
|
412
|
-
op: '
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
operator: 'lte',
|
|
416
|
-
value: swapInputAmountCap,
|
|
417
|
-
},
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
// Slippage floor: `out >= in * num/den`. Only when the caller supplied the
|
|
421
|
-
// ratio - see `swapMinOutRatio`. Without it the output arg stays free,
|
|
422
|
-
// which is the case the unbounded-swap warning above describes.
|
|
423
|
-
const outMinArgIndex = soroswapMinOutArgIndex(protocol.fn);
|
|
424
|
-
const minOutRatio = swapMinOutRatio;
|
|
425
|
-
if (minOutRatio !== undefined &&
|
|
426
|
-
inputArgIndex !== undefined &&
|
|
427
|
-
outMinArgIndex !== undefined &&
|
|
428
|
-
inputAmountArg &&
|
|
429
|
-
inputAmountArg.type === 'i128') {
|
|
430
|
-
routeToAdapter({
|
|
431
|
-
op: 'slippage_floor',
|
|
432
|
-
outArgIndex: outMinArgIndex,
|
|
433
|
-
inArgIndex: inputArgIndex,
|
|
434
|
-
num: minOutRatio.num,
|
|
435
|
-
den: minOutRatio.den,
|
|
293
|
+
interpreterConstraints.push({
|
|
294
|
+
op: 'lte',
|
|
295
|
+
left: { kind: 'call_arg', index: inputArgIndex },
|
|
296
|
+
right: { kind: 'literal_i128', value: swapInputAmountCap },
|
|
436
297
|
});
|
|
437
298
|
}
|
|
438
299
|
// Swap recipient (call_arg[3]): when the caller supplies
|
|
@@ -443,23 +304,20 @@ function appendProtocolSpecificConstraints(interpreterConstraints, routeToAdapte
|
|
|
443
304
|
// it unconstrained would permit an arbitrary recipient (an evil twin with
|
|
444
305
|
// call_arg[3] = attacker_wallet). RECIPIENT_ALLOWLIST_EMPTY is still
|
|
445
306
|
// surfaced, but as INFORMATIONAL (the recipient was pinned; here is how to
|
|
446
|
-
// widen it), never as a silent free pass.
|
|
447
|
-
// via the interpreter predicate, so the pin (and the ambiguity) apply only
|
|
448
|
-
// when the interpreter is enabled - otherwise the swap recipient is ignored
|
|
449
|
-
// (today's behaviour, matching the other SoroSwap constraints).
|
|
307
|
+
// widen it), never as a silent free pass.
|
|
450
308
|
const recipientArg = topLevel.args[3];
|
|
451
309
|
if (swapRecipientAllowlist && swapRecipientAllowlist.length > 0) {
|
|
452
|
-
|
|
310
|
+
interpreterConstraints.push({
|
|
453
311
|
op: 'in',
|
|
454
|
-
|
|
455
|
-
|
|
312
|
+
needle: { kind: 'call_arg', index: 3 },
|
|
313
|
+
haystack: swapRecipientAllowlist.map((v) => ({ kind: 'literal_address', value: v })),
|
|
456
314
|
});
|
|
457
315
|
}
|
|
458
|
-
else if (
|
|
316
|
+
else if (recipientArg && recipientArg.type === 'address') {
|
|
459
317
|
interpreterConstraints.push({
|
|
460
318
|
op: 'in',
|
|
461
|
-
|
|
462
|
-
|
|
319
|
+
needle: { kind: 'call_arg', index: 3 },
|
|
320
|
+
haystack: [{ kind: 'literal_address', value: recipientArg.value }],
|
|
463
321
|
});
|
|
464
322
|
ambiguities.push({
|
|
465
323
|
code: 'RECIPIENT_ALLOWLIST_EMPTY',
|
|
@@ -468,26 +326,6 @@ function appendProtocolSpecificConstraints(interpreterConstraints, routeToAdapte
|
|
|
468
326
|
}
|
|
469
327
|
}
|
|
470
328
|
}
|
|
471
|
-
/** Positional index of the input-amount argument for a recognized SoroSwap swap
|
|
472
|
-
* function. `swap_exact_tokens_for_tokens` and `swap_exact_in_for_tokens` take
|
|
473
|
-
* the exact input as arg[0]; `swap_tokens_for_exact_tokens` takes the maximum
|
|
474
|
-
* input (`amount_in_max`) as arg[1] - its arg[0] is the exact OUTPUT. Any other
|
|
475
|
-
* function has no positional input-amount argument -> undefined (no cap bound). */
|
|
476
|
-
/** The argument carrying the swap's MINIMUM ACCEPTABLE OUTPUT.
|
|
477
|
-
*
|
|
478
|
-
* Only the exact-input entrypoints have one: `swap_tokens_for_exact_tokens`
|
|
479
|
-
* fixes the output and varies the input, so its output needs no floor (its
|
|
480
|
-
* arg[1] is `amount_in_max`, already bounded by the input cap). Returning
|
|
481
|
-
* undefined there keeps a floor from being pinned to the wrong argument. */
|
|
482
|
-
function soroswapMinOutArgIndex(fn) {
|
|
483
|
-
switch (fn) {
|
|
484
|
-
case 'swap_exact_tokens_for_tokens':
|
|
485
|
-
case 'swap_exact_in_for_tokens':
|
|
486
|
-
return 1;
|
|
487
|
-
default:
|
|
488
|
-
return undefined;
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
329
|
function soroswapInputAmountArgIndex(fn) {
|
|
492
330
|
switch (fn) {
|
|
493
331
|
case 'swap_exact_tokens_for_tokens':
|
|
@@ -14,16 +14,14 @@ export interface GeneratedCases {
|
|
|
14
14
|
denies: DenyCase[];
|
|
15
15
|
}
|
|
16
16
|
declare const OVERPERMISSIVE_DIMENSIONS: readonly ["argument_reorder"];
|
|
17
|
-
|
|
18
|
-
export { ORIGINAL_DIMENSIONS, OVERPERMISSIVE_DIMENSIONS };
|
|
17
|
+
export { OVERPERMISSIVE_DIMENSIONS };
|
|
19
18
|
/** Build deterministic model-evaluated alternatives without mutating the intended call.
|
|
20
19
|
*
|
|
21
20
|
* @param predicate - the synthesized predicate
|
|
22
|
-
* @param permitCtx
|
|
23
|
-
* @param dimensions
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* existing fixtures do not regress. */
|
|
21
|
+
* @param permitCtx - EvalContext for the intended (permitted) call
|
|
22
|
+
* @param dimensions - optional whitelist of dimension names to emit; when omitted
|
|
23
|
+
* all known dimensions (including over-permissiveness mutations)
|
|
24
|
+
* are emitted. */
|
|
27
25
|
export declare function generateCases(predicate: PredicateNode, permitCtx: EvalContext, dimensions?: string[]): GeneratedCases;
|
|
28
26
|
/** Deep-copy an ScVal so a mutation cannot alias the recorded call.
|
|
29
27
|
* Exported so the permit-context builder shares this one implementation. */
|