@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
package/dist/synth/deny-cases.js
CHANGED
|
@@ -1,155 +1,23 @@
|
|
|
1
1
|
import { MAX_SCVAL_CLONE_DEPTH } from "../types.js";
|
|
2
|
-
import { literalNumericBigInt } from "./predicate-literals.js";
|
|
3
|
-
const ORACLE_CASES = [
|
|
4
|
-
['oracle_stale', 'stale'],
|
|
5
|
-
['oracle_missing', 'missing'],
|
|
6
|
-
['oracle_deviation_exceeded', 'deviation'],
|
|
7
|
-
['oracle_paused', 'paused'],
|
|
8
|
-
];
|
|
9
|
-
// Cross-layer reason contract: the TS evaluator's deny reason for each
|
|
10
|
-
// oracle error category must match the Rust interpreter's `DenyReason` code
|
|
11
|
-
// string (the same strings surface in the conformance fixture). The harness
|
|
12
|
-
// asserts them via the per-case `expectedReason`; a future divergence
|
|
13
|
-
// surfaces as a REASON_MISMATCH failure.
|
|
14
|
-
const ORACLE_ERROR_REASON = {
|
|
15
|
-
stale: 'ORACLE_STALE',
|
|
16
|
-
missing: 'ORACLE_MISSING',
|
|
17
|
-
deviation: 'ORACLE_DEVIATION_EXCEEDED',
|
|
18
|
-
paused: 'ORACLE_PAUSED',
|
|
19
|
-
};
|
|
20
2
|
// Deterministic XLM/USDC adjacency fixture; the shared registry can replace this boundary later.
|
|
21
|
-
const ADJACENT_ASSETS = [
|
|
22
|
-
'CAS3J7GYLGXMF6TDJ5WQ2PEN4GRVNXJUIQ2TZU3ZB3OQ2V4DRCWI7WPF',
|
|
23
|
-
'CCWCLTASNDT57N3BCHOSVB5QWMV5URK4BXLDDF6ZZQYMBQ4OKZA3ZB2N',
|
|
24
|
-
];
|
|
25
|
-
// Property-harness mutation dimensions excluded from the synth pipeline's
|
|
26
|
-
// self-verify call so existing fixtures still emit policies. The harness tests
|
|
27
|
-
// them as FINDINGS against the already-emitted policy.
|
|
28
3
|
const OVERPERMISSIVE_DIMENSIONS = ['argument_reorder'];
|
|
29
|
-
|
|
30
|
-
// is written here on purpose: this comment said 15 while the array held 17, and
|
|
31
|
-
// anyone checking the number should read the array rather than the prose.
|
|
32
|
-
// Phase 1 grammar extension: `vec_append` and `map_field_flip` are listed below
|
|
33
|
-
// alongside the existing dimensions so the per-element binds emitted for
|
|
34
|
-
// Blend `submit` (call_arg_len + 3 call_arg_field per element) survive
|
|
35
|
-
// minimise. Without these, no deny case exercises the new leaves and the
|
|
36
|
-
// minimise pass drops them as "redundant" - reopening the element-append
|
|
37
|
-
// hole they were added to close. Both dimensions are no-ops for any fixture
|
|
38
|
-
// whose predicate has no `call_arg_len` / `call_arg_field` leaves (the
|
|
39
|
-
// generator short-circuits on leaf-kind), so they cannot make production
|
|
40
|
-
// synthesis refuse for fixtures that do not exercise the new grammar.
|
|
41
|
-
const ORIGINAL_DIMENSIONS = [
|
|
42
|
-
'amount',
|
|
43
|
-
'asset',
|
|
44
|
-
'contract',
|
|
45
|
-
'function',
|
|
46
|
-
'timing',
|
|
47
|
-
'time_window',
|
|
48
|
-
'invocation_count',
|
|
49
|
-
'arg_amount_bound',
|
|
50
|
-
'arg_bound',
|
|
51
|
-
'scope_contract_fn_arg',
|
|
52
|
-
'oracle_stale',
|
|
53
|
-
'oracle_missing',
|
|
54
|
-
'oracle_deviation_exceeded',
|
|
55
|
-
'oracle_paused',
|
|
56
|
-
'soroswap_allowed_path',
|
|
57
|
-
'vec_append',
|
|
58
|
-
'map_field_flip',
|
|
59
|
-
];
|
|
60
|
-
export { ORIGINAL_DIMENSIONS, OVERPERMISSIVE_DIMENSIONS };
|
|
4
|
+
export { OVERPERMISSIVE_DIMENSIONS };
|
|
61
5
|
/** Build deterministic model-evaluated alternatives without mutating the intended call.
|
|
62
6
|
*
|
|
63
7
|
* @param predicate - the synthesized predicate
|
|
64
|
-
* @param permitCtx
|
|
65
|
-
* @param dimensions
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* existing fixtures do not regress. */
|
|
8
|
+
* @param permitCtx - EvalContext for the intended (permitted) call
|
|
9
|
+
* @param dimensions - optional whitelist of dimension names to emit; when omitted
|
|
10
|
+
* all known dimensions (including over-permissiveness mutations)
|
|
11
|
+
* are emitted. */
|
|
69
12
|
export function generateCases(predicate, permitCtx, dimensions) {
|
|
70
13
|
const facts = inspectPredicate(predicate);
|
|
71
14
|
const denies = [];
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
const movedTokens = new Set();
|
|
80
|
-
for (const comparison of facts.comparisons) {
|
|
81
|
-
if (comparison.left.kind === 'amount' || comparison.left.kind === 'window_spent') {
|
|
82
|
-
movedTokens.add(comparison.left.token);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
for (const token of movedTokens) {
|
|
86
|
-
// The `asset` deny case mutates BOTH the amount record AND the contract
|
|
87
|
-
// binding (token's value moved to an adjacent asset; the same swap applied
|
|
88
|
-
// to any address-typed `call_arg` / `call_contract` literal). The actual
|
|
89
|
-
// deny reason is therefore predicate-dependent - the order of evaluation
|
|
90
|
-
// is CONTRACT_SCOPE (step 3) before AMOUNT_BOUND (step 6), so a policy
|
|
91
|
-
// that has both bindings denies with CONTRACT_SCOPE, while a policy with
|
|
92
|
-
// only an amount binding denies with AMOUNT_BOUND. The reason assertion
|
|
93
|
-
// would be brittle here; the boolean decision is what we strictly need
|
|
94
|
-
// to pin. Leave `expectedReason` unset for this dimension.
|
|
95
|
-
denies.push({ dimension: 'asset', ctx: mutateAsset(predicate, permitCtx, token) });
|
|
96
|
-
}
|
|
97
|
-
const contractConstraints = [
|
|
98
|
-
...facts.comparisons.filter((node) => node.op === 'eq' &&
|
|
99
|
-
node.left.kind === 'call_contract' &&
|
|
100
|
-
node.right.kind === 'literal_address'),
|
|
101
|
-
...facts.memberships.filter((node) => node.needle.kind === 'call_contract'),
|
|
102
|
-
];
|
|
103
|
-
for (const _constraint of contractConstraints) {
|
|
104
|
-
const ctx = cloneContext(permitCtx);
|
|
105
|
-
ctx.contract = distinctText(permitCtx.contract, 'contract');
|
|
106
|
-
denies.push({ dimension: 'contract', ctx, expectedReason: 'CONTRACT_SCOPE' });
|
|
107
|
-
}
|
|
108
|
-
const functionConstraints = [
|
|
109
|
-
...facts.comparisons.filter((node) => node.op === 'eq' && node.left.kind === 'call_fn' && node.right.kind === 'literal_symbol'),
|
|
110
|
-
...facts.memberships.filter((node) => node.needle.kind === 'call_fn'),
|
|
111
|
-
];
|
|
112
|
-
for (const _constraint of functionConstraints) {
|
|
113
|
-
const ctx = cloneContext(permitCtx);
|
|
114
|
-
ctx.fn = distinctText(permitCtx.fn, 'function');
|
|
115
|
-
denies.push({ dimension: 'function', ctx, expectedReason: 'FN_MISMATCH' });
|
|
116
|
-
}
|
|
117
|
-
if (permitCtx.validUntilLedger !== undefined) {
|
|
118
|
-
const ctx = cloneContext(permitCtx);
|
|
119
|
-
ctx.atLedger = permitCtx.validUntilLedger + 1;
|
|
120
|
-
denies.push({ dimension: 'timing', ctx, expectedReason: 'EXPIRED' });
|
|
121
|
-
}
|
|
122
|
-
for (const comparison of facts.comparisons) {
|
|
123
|
-
if (comparison.left.kind !== 'window_spent')
|
|
124
|
-
continue;
|
|
125
|
-
const mutated = mutateBigIntRecord(permitCtx, 'windowSpentByToken', comparison.left.token, comparison, false);
|
|
126
|
-
if (mutated)
|
|
127
|
-
denies.push({ dimension: 'time_window', ctx: mutated, expectedReason: 'AMOUNT_BOUND' });
|
|
128
|
-
}
|
|
129
|
-
for (const comparison of facts.comparisons) {
|
|
130
|
-
if (comparison.left.kind !== 'invocation_count_in_window')
|
|
131
|
-
continue;
|
|
132
|
-
if (comparison.right.kind !== 'literal_u32')
|
|
133
|
-
continue;
|
|
134
|
-
const ctx = cloneContext(permitCtx);
|
|
135
|
-
ctx.invocationCountByWindow[comparison.left.windowSecs] = violatingNumber(comparison.op, comparison.right.value);
|
|
136
|
-
denies.push({ dimension: 'invocation_count', ctx, expectedReason: 'FREQUENCY' });
|
|
137
|
-
}
|
|
138
|
-
// Ordered numeric bound on a call_arg (e.g. a SoroSwap input-amount cap
|
|
139
|
-
// `call_arg[0] <= limit`). A violating deny case pushes the arg past the
|
|
140
|
-
// bound so the leaf is exercised - and, critically, so `minimize` keeps it:
|
|
141
|
-
// a conjunct that no deny case needs is pruned as redundant, which would
|
|
142
|
-
// silently drop a caller-requested restriction.
|
|
143
|
-
for (const comparison of facts.comparisons) {
|
|
144
|
-
if (comparison.op === 'eq' || comparison.left.kind !== 'call_arg')
|
|
145
|
-
continue;
|
|
146
|
-
const violating = violatingArgScVal(comparison.op, comparison.right);
|
|
147
|
-
if (!violating)
|
|
148
|
-
continue;
|
|
149
|
-
const ctx = cloneContext(permitCtx);
|
|
150
|
-
ctx.args[comparison.left.index] = violating;
|
|
151
|
-
denies.push({ dimension: 'arg_amount_bound', ctx, expectedReason: 'ARG_MISMATCH' });
|
|
152
|
-
}
|
|
15
|
+
// arg_bound: set a constrained arg to an opaque ScVal.
|
|
16
|
+
// The reason depends on the predicate shape:
|
|
17
|
+
// - an `eq(call_arg[i], literal)` denies with ARG_MISMATCH
|
|
18
|
+
// - an `in(call_arg[i], [literals])` denies with NOT_IN_ALLOWLIST
|
|
19
|
+
// Both are correct in the TS evaluator; the reason is predicate-dependent
|
|
20
|
+
// so we cannot pin a single canonical reason here.
|
|
153
21
|
const argumentConstraints = [];
|
|
154
22
|
for (const comparison of facts.comparisons) {
|
|
155
23
|
if (comparison.op === 'eq' &&
|
|
@@ -166,54 +34,10 @@ export function generateCases(predicate, permitCtx, dimensions) {
|
|
|
166
34
|
for (const constraint of argumentConstraints) {
|
|
167
35
|
const ctx = cloneContext(permitCtx);
|
|
168
36
|
ctx.args[constraint.index] = { type: 'other', value: 'deny-case-opaque-argument' };
|
|
169
|
-
// The `arg_bound` case sets the arg to an opaque ScVal. The deny reason
|
|
170
|
-
// depends on the predicate shape:
|
|
171
|
-
// - an `eq(call_arg[i], literal)` denies with ARG_MISMATCH
|
|
172
|
-
// - an `in(call_arg[i], [literals])` denies with NOT_IN_ALLOWLIST
|
|
173
|
-
// (opaque needles fail-closed at the `in` membership gate, step 5)
|
|
174
|
-
// Both are correct in the TS evaluator; the reason is predicate-dependent
|
|
175
|
-
// so we cannot pin a single canonical reason here. The boolean decision
|
|
176
|
-
// is what we strictly need to assert; the reason is recorded (not asserted)
|
|
177
|
-
// when the harness runs this case.
|
|
178
37
|
denies.push({ dimension: 'arg_bound', ctx });
|
|
179
38
|
}
|
|
180
|
-
|
|
181
|
-
for
|
|
182
|
-
if (comparison.op === 'eq' &&
|
|
183
|
-
comparison.left.kind === 'call_arg' &&
|
|
184
|
-
comparison.right.kind === 'literal_vec') {
|
|
185
|
-
scopedArgumentIndices.add(comparison.left.index);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
if (contractConstraints.length > 0 &&
|
|
189
|
-
functionConstraints.length > 0 &&
|
|
190
|
-
scopedArgumentIndices.size > 0) {
|
|
191
|
-
const ctx = cloneContext(permitCtx);
|
|
192
|
-
ctx.contract = distinctText(permitCtx.contract, 'authorized-call-contract');
|
|
193
|
-
ctx.fn = distinctText(permitCtx.fn, 'authorized-call-function');
|
|
194
|
-
for (const index of scopedArgumentIndices) {
|
|
195
|
-
ctx.args[index] = { type: 'other', value: 'deny-case-authorized-call-argument' };
|
|
196
|
-
}
|
|
197
|
-
// `scope_contract_fn_arg` changes contract, fn, AND args simultaneously.
|
|
198
|
-
// The first failing child of the AND decides the reason, and the contract
|
|
199
|
-
// check (step 3) does fire first in the evaluator. The reason is therefore
|
|
200
|
-
// CONTRACT_SCOPE for the canonical case, but a predicate that lists the
|
|
201
|
-
// call_fn leaf first (or that has a `call_arg_field` for that arg index)
|
|
202
|
-
// can flip the order. Pin the assertion here only when no `in` /
|
|
203
|
-
// call_arg_field binds the same arg index exist - the canonical blend
|
|
204
|
-
// case does have those binds, so the assertion would fire there.
|
|
205
|
-
denies.push({ dimension: 'scope_contract_fn_arg', ctx });
|
|
206
|
-
}
|
|
207
|
-
const oracleComparisons = facts.comparisons.filter((node) => node.left.kind === 'oracle_price');
|
|
208
|
-
for (const [dimension, error] of ORACLE_CASES) {
|
|
209
|
-
for (const comparison of oracleComparisons) {
|
|
210
|
-
if (comparison.left.kind !== 'oracle_price')
|
|
211
|
-
continue;
|
|
212
|
-
const ctx = cloneContext(permitCtx);
|
|
213
|
-
ctx.oraclePriceByAsset[comparison.left.asset] = { error };
|
|
214
|
-
denies.push({ dimension, ctx, expectedReason: ORACLE_ERROR_REASON[error] });
|
|
215
|
-
}
|
|
216
|
-
}
|
|
39
|
+
// soroswap_allowed_path: replace a bound vec arg with a different vector.
|
|
40
|
+
// Still valid for eq comparisons on literal_vec leaves.
|
|
217
41
|
for (const comparison of facts.comparisons) {
|
|
218
42
|
if (comparison.op !== 'eq' ||
|
|
219
43
|
comparison.left.kind !== 'call_arg' ||
|
|
@@ -224,7 +48,7 @@ export function generateCases(predicate, permitCtx, dimensions) {
|
|
|
224
48
|
ctx.args[comparison.left.index] = differentVector(ctx.args[comparison.left.index]);
|
|
225
49
|
denies.push({ dimension: 'soroswap_allowed_path', ctx });
|
|
226
50
|
}
|
|
227
|
-
//
|
|
51
|
+
// argument_reorder: swap first two address args.
|
|
228
52
|
// Skipped when dimensions filter is active so the synth pipeline can emit a policy;
|
|
229
53
|
// the over-permissiveness harness then tests this mutation as a FINDING.
|
|
230
54
|
const constrainedArgIndices = new Set();
|
|
@@ -252,12 +76,10 @@ export function generateCases(predicate, permitCtx, dimensions) {
|
|
|
252
76
|
ctx.args[1] = a0;
|
|
253
77
|
denies.push({ dimension: 'argument_reorder', ctx });
|
|
254
78
|
}
|
|
255
|
-
//
|
|
256
|
-
// the same type.
|
|
79
|
+
// map_field_flip: flip a bound map field to a different valid value of
|
|
80
|
+
// the same type. OPT-IN only, never ORIGINAL.
|
|
257
81
|
// Targets each `call_arg_field` leaf and produces a ctx whose vec element
|
|
258
|
-
// has a different value of the recorded field
|
|
259
|
-
// different amount, different address). The blend-submit case uses this
|
|
260
|
-
// to detect a missing request_type pin. ---
|
|
82
|
+
// has a different value of the recorded field. ---
|
|
261
83
|
if (!dimensions || dimensions.includes('map_field_flip')) {
|
|
262
84
|
for (const comparison of facts.comparisons) {
|
|
263
85
|
const sel = comparison.left;
|
|
@@ -286,9 +108,9 @@ export function generateCases(predicate, permitCtx, dimensions) {
|
|
|
286
108
|
denies.push({ dimension: 'map_field_flip', ctx });
|
|
287
109
|
}
|
|
288
110
|
}
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
//
|
|
111
|
+
// vec_append: append a new element to a bound vec. OPT-IN only, never ORIGINAL.
|
|
112
|
+
// Targets every `call_arg_len` leaf; without the length pin a caller can
|
|
113
|
+
// append an extra element to defeat per-element binds. ---
|
|
292
114
|
if (!dimensions || dimensions.includes('vec_append')) {
|
|
293
115
|
for (const comparison of facts.comparisons) {
|
|
294
116
|
const sel = comparison.left;
|
|
@@ -305,7 +127,6 @@ export function generateCases(predicate, permitCtx, dimensions) {
|
|
|
305
127
|
denies.push({ dimension: 'vec_append', ctx });
|
|
306
128
|
}
|
|
307
129
|
}
|
|
308
|
-
// Version mismatch, malformed predicates, master authorization, and nonce replay are install-time checks and are intentionally omitted from model-evaluated cases.
|
|
309
130
|
return { permit: cloneContext(permitCtx), denies };
|
|
310
131
|
}
|
|
311
132
|
function inspectPredicate(predicate) {
|
|
@@ -316,162 +137,17 @@ function inspectPredicate(predicate) {
|
|
|
316
137
|
function visit(node, facts) {
|
|
317
138
|
switch (node.op) {
|
|
318
139
|
case 'and':
|
|
319
|
-
case 'or':
|
|
320
140
|
for (const child of node.children)
|
|
321
141
|
visit(child, facts);
|
|
322
142
|
return;
|
|
323
|
-
case 'not':
|
|
324
|
-
visit(node.child, facts);
|
|
325
|
-
return;
|
|
326
143
|
case 'in':
|
|
327
144
|
facts.memberships.push(node);
|
|
328
145
|
return;
|
|
329
146
|
case 'eq':
|
|
330
|
-
case 'lt':
|
|
331
147
|
case 'lte':
|
|
332
|
-
case 'gt':
|
|
333
|
-
case 'gte':
|
|
334
148
|
facts.comparisons.push(node);
|
|
335
149
|
}
|
|
336
150
|
}
|
|
337
|
-
function mutateBigIntRecord(permitCtx, recordKey, token, comparison, preferScaledAmount = true) {
|
|
338
|
-
if (comparison.right.kind !== 'literal_i128')
|
|
339
|
-
return null;
|
|
340
|
-
try {
|
|
341
|
-
const current = BigInt(permitCtx[recordKey][token] ?? '0');
|
|
342
|
-
const bound = BigInt(comparison.right.value);
|
|
343
|
-
const value = violatingBigInt(comparison.op, current, bound, preferScaledAmount);
|
|
344
|
-
const ctx = cloneContext(permitCtx);
|
|
345
|
-
ctx[recordKey][token] = value.toString();
|
|
346
|
-
return ctx;
|
|
347
|
-
}
|
|
348
|
-
catch {
|
|
349
|
-
return null;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
function violatingBigInt(op, current, bound, preferScaledAmount) {
|
|
353
|
-
if (preferScaledAmount) {
|
|
354
|
-
const scaledCandidates = [(current * 101n) / 100n, current * 10n];
|
|
355
|
-
for (const candidate of scaledCandidates) {
|
|
356
|
-
if (!bigIntComparison(op, candidate, bound))
|
|
357
|
-
return candidate;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
return boundaryViolatingBigInt(op, bound);
|
|
361
|
-
}
|
|
362
|
-
/** The single value at the boundary that violates `<selector> op bound`:
|
|
363
|
-
* bound+1 for `lte`/`eq`, bound-1 for `gte`, and bound itself for the strict
|
|
364
|
-
* `lt`/`gt`. */
|
|
365
|
-
function boundaryViolatingBigInt(op, bound) {
|
|
366
|
-
switch (op) {
|
|
367
|
-
case 'lt':
|
|
368
|
-
case 'gt':
|
|
369
|
-
return bound;
|
|
370
|
-
case 'lte':
|
|
371
|
-
case 'eq':
|
|
372
|
-
return bound + 1n;
|
|
373
|
-
case 'gte':
|
|
374
|
-
return bound - 1n;
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
function violatingNumber(op, bound) {
|
|
378
|
-
switch (op) {
|
|
379
|
-
case 'lt':
|
|
380
|
-
case 'gt':
|
|
381
|
-
return bound;
|
|
382
|
-
case 'lte':
|
|
383
|
-
case 'eq':
|
|
384
|
-
return bound + 1;
|
|
385
|
-
case 'gte':
|
|
386
|
-
return bound - 1;
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
/** Build an ScVal that VIOLATES an ordered numeric bound on a call_arg, given
|
|
390
|
-
* the comparison op and its numeric-literal right-hand side. Returns null when
|
|
391
|
-
* the literal is not an integer (the bound is not a numeric compare). The arg
|
|
392
|
-
* is emitted as an i128 - the recorder's numeric args are read via BigInt, so
|
|
393
|
-
* the wire type only needs to be a numeric ScVal to exercise the bound. */
|
|
394
|
-
function violatingArgScVal(op, right) {
|
|
395
|
-
const bound = literalNumericBigInt(right);
|
|
396
|
-
if (bound === null)
|
|
397
|
-
return null;
|
|
398
|
-
return { type: 'i128', value: boundaryViolatingBigInt(op, bound).toString() };
|
|
399
|
-
}
|
|
400
|
-
function bigIntComparison(op, value, bound) {
|
|
401
|
-
switch (op) {
|
|
402
|
-
case 'eq':
|
|
403
|
-
return value === bound;
|
|
404
|
-
case 'lt':
|
|
405
|
-
return value < bound;
|
|
406
|
-
case 'lte':
|
|
407
|
-
return value <= bound;
|
|
408
|
-
case 'gt':
|
|
409
|
-
return value > bound;
|
|
410
|
-
case 'gte':
|
|
411
|
-
return value >= bound;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
function mutateAsset(predicate, permitCtx, token) {
|
|
415
|
-
const binding = findAddressBinding(predicate, token);
|
|
416
|
-
const adjacent = adjacentAsset(token);
|
|
417
|
-
const ctx = cloneContext(permitCtx);
|
|
418
|
-
moveRecordEntry(ctx.amountByToken, token, adjacent);
|
|
419
|
-
moveRecordEntry(ctx.windowSpentByToken, token, adjacent);
|
|
420
|
-
if (binding.contract && ctx.contract === token)
|
|
421
|
-
ctx.contract = adjacent;
|
|
422
|
-
for (const index of binding.argumentIndices) {
|
|
423
|
-
const value = ctx.args[index];
|
|
424
|
-
if (value)
|
|
425
|
-
ctx.args[index] = replaceAddress(value, token, adjacent);
|
|
426
|
-
}
|
|
427
|
-
return ctx;
|
|
428
|
-
}
|
|
429
|
-
function findAddressBinding(predicate, address) {
|
|
430
|
-
const facts = inspectPredicate(predicate);
|
|
431
|
-
let contract = false;
|
|
432
|
-
const argumentIndices = new Set();
|
|
433
|
-
for (const comparison of facts.comparisons) {
|
|
434
|
-
if (comparison.op !== 'eq' || !leafContainsAddress(comparison.right, address))
|
|
435
|
-
continue;
|
|
436
|
-
if (comparison.left.kind === 'call_contract')
|
|
437
|
-
contract = true;
|
|
438
|
-
if (comparison.left.kind === 'call_arg')
|
|
439
|
-
argumentIndices.add(comparison.left.index);
|
|
440
|
-
}
|
|
441
|
-
for (const membership of facts.memberships) {
|
|
442
|
-
if (!membership.haystack.some((leaf) => leafContainsAddress(leaf, address)))
|
|
443
|
-
continue;
|
|
444
|
-
if (membership.needle.kind === 'call_contract')
|
|
445
|
-
contract = true;
|
|
446
|
-
if (membership.needle.kind === 'call_arg')
|
|
447
|
-
argumentIndices.add(membership.needle.index);
|
|
448
|
-
}
|
|
449
|
-
return { contract, argumentIndices };
|
|
450
|
-
}
|
|
451
|
-
function leafContainsAddress(leaf, address) {
|
|
452
|
-
if (leaf.kind === 'literal_address')
|
|
453
|
-
return leaf.value === address;
|
|
454
|
-
if (leaf.kind === 'literal_vec') {
|
|
455
|
-
return leaf.elements.some((element) => leafContainsAddress(element, address));
|
|
456
|
-
}
|
|
457
|
-
return false;
|
|
458
|
-
}
|
|
459
|
-
function moveRecordEntry(record, from, to) {
|
|
460
|
-
const value = record[from];
|
|
461
|
-
if (value === undefined)
|
|
462
|
-
return;
|
|
463
|
-
delete record[from];
|
|
464
|
-
record[to] = value;
|
|
465
|
-
}
|
|
466
|
-
function replaceAddress(value, from, to) {
|
|
467
|
-
if (value.type === 'address') {
|
|
468
|
-
return value.value === from ? { type: 'address', value: to } : { ...value };
|
|
469
|
-
}
|
|
470
|
-
if (value.type === 'vec') {
|
|
471
|
-
return { type: 'vec', value: value.value.map((item) => replaceAddress(item, from, to)) };
|
|
472
|
-
}
|
|
473
|
-
return { ...value };
|
|
474
|
-
}
|
|
475
151
|
function differentVector(actual) {
|
|
476
152
|
if (actual?.type !== 'vec')
|
|
477
153
|
return { type: 'vec', value: [] };
|
|
@@ -488,40 +164,16 @@ function differentVector(actual) {
|
|
|
488
164
|
value[0] = { type: 'other', value: 'deny-case-different-hop' };
|
|
489
165
|
return { type: 'vec', value };
|
|
490
166
|
}
|
|
491
|
-
function adjacentAsset(asset) {
|
|
492
|
-
return asset === ADJACENT_ASSETS[0] ? ADJACENT_ASSETS[1] : ADJACENT_ASSETS[0];
|
|
493
|
-
}
|
|
494
|
-
function distinctText(value, label) {
|
|
495
|
-
return `${value}#${label}`;
|
|
496
|
-
}
|
|
497
167
|
function cloneContext(ctx) {
|
|
498
|
-
|
|
168
|
+
return {
|
|
499
169
|
contract: ctx.contract,
|
|
500
170
|
fn: ctx.fn,
|
|
501
171
|
args: ctx.args.map(cloneScVal),
|
|
502
|
-
atLedger: ctx.atLedger,
|
|
503
|
-
nowSeconds: ctx.nowSeconds,
|
|
504
|
-
amountByToken: { ...ctx.amountByToken },
|
|
505
|
-
windowSpentByToken: { ...ctx.windowSpentByToken },
|
|
506
|
-
invocationCountByWindow: { ...ctx.invocationCountByWindow },
|
|
507
|
-
oraclePriceByAsset: Object.fromEntries(Object.entries(ctx.oraclePriceByAsset).map(([asset, entry]) => [
|
|
508
|
-
asset,
|
|
509
|
-
'error' in entry ? { error: entry.error } : { ...entry },
|
|
510
|
-
])),
|
|
511
172
|
};
|
|
512
|
-
if (ctx.validUntilLedger !== undefined)
|
|
513
|
-
cloned.validUntilLedger = ctx.validUntilLedger;
|
|
514
|
-
if (ctx.signerWeights !== undefined)
|
|
515
|
-
cloned.signerWeights = { ...ctx.signerWeights };
|
|
516
|
-
return cloned;
|
|
517
173
|
}
|
|
518
174
|
/** Deep-copy an ScVal so a mutation cannot alias the recorded call.
|
|
519
175
|
* Exported so the permit-context builder shares this one implementation. */
|
|
520
176
|
export function cloneScVal(value, depth = 0) {
|
|
521
|
-
// Recursion is bounded by MAX_SCVAL_CLONE_DEPTH so a hand-crafted nested-vec
|
|
522
|
-
// payload cannot RangeError the JS stack during deny-case mutation. Over-depth
|
|
523
|
-
// throws a ToolError-shaped error that the `synthesizeFromRecording` envelope
|
|
524
|
-
// (item 3) converts to a structured `{ok:false, error}`.
|
|
525
177
|
if (value.type === 'vec') {
|
|
526
178
|
if (depth >= MAX_SCVAL_CLONE_DEPTH) {
|
|
527
179
|
throw cloneDepthError(value);
|
|
@@ -540,9 +192,8 @@ function cloneDepthError(value) {
|
|
|
540
192
|
}
|
|
541
193
|
/** Build a value of the SAME ScVal type that differs from the recorded one,
|
|
542
194
|
* used by the `map_field_flip` mutation to defeat a per-element pin without
|
|
543
|
-
* changing the wire type
|
|
544
|
-
*
|
|
545
|
-
* when the ScVal type has no obvious different value (e.g. opaque/other). */
|
|
195
|
+
* changing the wire type. Returns null when the ScVal type has no obvious
|
|
196
|
+
* different value (e.g. opaque/other). */
|
|
546
197
|
function flipFieldValue(val) {
|
|
547
198
|
switch (val.type) {
|
|
548
199
|
case 'address': {
|
|
@@ -551,7 +202,6 @@ function flipFieldValue(val) {
|
|
|
551
202
|
return { type: 'address', value: val.value === a ? b : a };
|
|
552
203
|
}
|
|
553
204
|
case 'i128':
|
|
554
|
-
case 'u64':
|
|
555
205
|
case 'u32':
|
|
556
206
|
return { type: val.type, value: String(BigInt(val.value) + 1n) };
|
|
557
207
|
case 'symbol':
|
package/dist/synth/evaluate.d.ts
CHANGED
|
@@ -6,27 +6,6 @@ export interface EvalContext {
|
|
|
6
6
|
fn: string;
|
|
7
7
|
/** Decoded `ScVal[]` of the top-level authorized call. */
|
|
8
8
|
args: ScVal[];
|
|
9
|
-
/** Ledger sequence at which the interpreter is invoked. */
|
|
10
|
-
atLedger: number;
|
|
11
|
-
/** Optional policy expiry; absent -> step 1 is skipped. */
|
|
12
|
-
validUntilLedger?: number;
|
|
13
|
-
/** Unix seconds; used by `now` / `valid_until` leaves. */
|
|
14
|
-
nowSeconds: number;
|
|
15
|
-
/** Per-token amount moved by the current call (i128 decimal string). */
|
|
16
|
-
amountByToken: Record<string, string>;
|
|
17
|
-
/** Per-token window-rolling spend prior to this call (i128 decimal string). */
|
|
18
|
-
windowSpentByToken: Record<string, string>;
|
|
19
|
-
/** Recorded invocation counts keyed by window seconds. */
|
|
20
|
-
invocationCountByWindow: Record<number, number>;
|
|
21
|
-
/** Optional signer-weight map for the threshold gate. Absent -> skip. */
|
|
22
|
-
signerWeights?: Record<string, number>;
|
|
23
|
-
/** Per-asset oracle snapshot. Missing keys default to ORACLE_STALE. */
|
|
24
|
-
oraclePriceByAsset: Record<string, {
|
|
25
|
-
price: string;
|
|
26
|
-
timestampSeconds: number;
|
|
27
|
-
} | {
|
|
28
|
-
error: 'stale' | 'missing' | 'deviation' | 'paused' | 'decimals' | 'fingerprint';
|
|
29
|
-
}>;
|
|
30
9
|
}
|
|
31
10
|
export type EvalResult = {
|
|
32
11
|
permit: true;
|