@crediolabs/policy-synth 0.1.8 → 0.1.9
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/adapters/interpreter/adapter.js +21 -3
- package/dist/adapters/oz/adapter.js +8 -0
- package/dist/ir/types.d.ts +9 -0
- package/dist/predicate/encode.js +9 -0
- package/dist/review-card/builder.js +2 -0
- package/dist/review-card/cross-check.js +2 -0
- package/dist/synth/compose-from-recording.js +89 -0
- package/dist/synth/deny-cases.js +89 -1
- package/dist/synth/evaluate.js +49 -0
- package/dist/types.d.ts +8 -0
- package/dist-cjs/adapters/interpreter/adapter.js +21 -3
- package/dist-cjs/adapters/oz/adapter.js +8 -0
- package/dist-cjs/ir/types.d.ts +9 -0
- package/dist-cjs/predicate/encode.js +9 -0
- package/dist-cjs/review-card/builder.js +2 -0
- package/dist-cjs/review-card/cross-check.js +2 -0
- package/dist-cjs/synth/compose-from-recording.js +89 -0
- package/dist-cjs/synth/deny-cases.js +89 -1
- package/dist-cjs/synth/evaluate.js +49 -0
- package/dist-cjs/types.d.ts +8 -0
- package/package.json +1 -1
- package/src/adapters/interpreter/adapter.ts +20 -4
- package/src/adapters/oz/adapter.ts +8 -0
- package/src/ir/types.ts +8 -0
- package/src/predicate/encode.ts +9 -0
- package/src/review-card/builder.ts +2 -0
- package/src/review-card/cross-check.ts +2 -0
- package/src/synth/compose-from-recording.ts +87 -0
- package/src/synth/deny-cases.ts +86 -1
- package/src/synth/evaluate.ts +41 -0
- package/src/types.ts +10 -0
|
@@ -307,6 +307,10 @@ function lowerSelector(s) {
|
|
|
307
307
|
switch (s.kind) {
|
|
308
308
|
case 'arg':
|
|
309
309
|
return { kind: 'call_arg', index: s.argIndex };
|
|
310
|
+
case 'arg_len':
|
|
311
|
+
return { kind: 'call_arg_len', index: s.argIndex };
|
|
312
|
+
case 'arg_field':
|
|
313
|
+
return { kind: 'call_arg_field', index: s.argIndex, element: s.element, field: s.field };
|
|
310
314
|
case 'amount':
|
|
311
315
|
return { kind: 'amount', token: s.token };
|
|
312
316
|
case 'window_spent':
|
|
@@ -329,13 +333,15 @@ function lowerSelector(s) {
|
|
|
329
333
|
* selector kind to the matching `literal_*` kind. The IR compare value is a
|
|
330
334
|
* raw string (i128-safe); the selector kind fixes the canonical wire type:
|
|
331
335
|
* - arg -> IR scalarType (set by the recorder/parser)
|
|
336
|
+
* - arg_len -> u32 (vec length is a small non-negative integer)
|
|
337
|
+
* - arg_field -> IR scalarType (the field's recorded type)
|
|
332
338
|
* - amount -> i128 (canonical Stellar token amount encoding)
|
|
333
339
|
* - window_spent -> i128 (canonical amount encoding)
|
|
334
340
|
* - oracle_price -> i128 (canonical price encoding)
|
|
335
341
|
* - invocation_count -> u32 (counts are small non-negative integers)
|
|
336
342
|
* - now / valid_until -> u64 (unix timestamps in seconds) */
|
|
337
343
|
function literalFromIRCompare(c) {
|
|
338
|
-
const scalarType =
|
|
344
|
+
const scalarType = selectorScalarType(c.selector);
|
|
339
345
|
return literalFromScalar(c.value, scalarType);
|
|
340
346
|
}
|
|
341
347
|
function literalScalarForSelector(kind) {
|
|
@@ -345,14 +351,16 @@ function literalScalarForSelector(kind) {
|
|
|
345
351
|
case 'oracle_price':
|
|
346
352
|
return 'i128';
|
|
347
353
|
case 'invocation_count':
|
|
354
|
+
case 'arg_len':
|
|
348
355
|
return 'u32';
|
|
349
356
|
case 'now':
|
|
350
357
|
case 'valid_until':
|
|
351
358
|
return 'u64';
|
|
352
359
|
case 'arg':
|
|
360
|
+
case 'arg_field':
|
|
353
361
|
case 'calldata':
|
|
354
362
|
case 'value':
|
|
355
|
-
// arg -> caller handles scalarType; calldata/value -> unreachable (Path-B).
|
|
363
|
+
// arg / arg_field -> caller handles scalarType; calldata/value -> unreachable (Path-B).
|
|
356
364
|
return 'i128';
|
|
357
365
|
}
|
|
358
366
|
}
|
|
@@ -380,10 +388,12 @@ function literalFromScalar(value, scalarType) {
|
|
|
380
388
|
/** Scalar type of an IRSelector for the purpose of building literal leaves
|
|
381
389
|
* (the right-hand side of `eq` / elements of an `in` haystack / elements of
|
|
382
390
|
* a `literal_vec`). Mirrors `literalScalarForSelector` for OZ extensions and
|
|
383
|
-
* uses the selector's own `scalarType` for `arg` selectors. */
|
|
391
|
+
* uses the selector's own `scalarType` for `arg` and `arg_field` selectors. */
|
|
384
392
|
function selectorScalarType(selector) {
|
|
385
393
|
if (selector.kind === 'arg')
|
|
386
394
|
return selector.scalarType;
|
|
395
|
+
if (selector.kind === 'arg_field')
|
|
396
|
+
return selector.scalarType;
|
|
387
397
|
return literalScalarForSelector(selector.kind);
|
|
388
398
|
}
|
|
389
399
|
/** Walk a condition tree and throw if any oracle_price leaf is found anywhere
|
|
@@ -457,6 +467,10 @@ function describeCondition(cond) {
|
|
|
457
467
|
return `per-call amount comparison on ${s.token}`;
|
|
458
468
|
case 'arg':
|
|
459
469
|
return `argument comparison on arg ${s.argIndex}`;
|
|
470
|
+
case 'arg_len':
|
|
471
|
+
return `vec length comparison on arg ${s.argIndex}`;
|
|
472
|
+
case 'arg_field':
|
|
473
|
+
return `map field comparison on arg ${s.argIndex}.${s.field}`;
|
|
460
474
|
case 'calldata':
|
|
461
475
|
return 'EVM calldata comparison';
|
|
462
476
|
case 'value':
|
|
@@ -472,6 +486,10 @@ function describeSelector(s) {
|
|
|
472
486
|
switch (s.kind) {
|
|
473
487
|
case 'arg':
|
|
474
488
|
return `arg ${s.argIndex}`;
|
|
489
|
+
case 'arg_len':
|
|
490
|
+
return `arg_len(${s.argIndex})`;
|
|
491
|
+
case 'arg_field':
|
|
492
|
+
return `arg_field(${s.argIndex}, ${s.element}, ${s.field})`;
|
|
475
493
|
case 'amount':
|
|
476
494
|
return `amount(${s.token})`;
|
|
477
495
|
case 'window_spent':
|
|
@@ -229,6 +229,10 @@ function describeCondition(cond) {
|
|
|
229
229
|
return `per-call amount comparison on ${s.token} (predicate DSL)`;
|
|
230
230
|
case 'arg':
|
|
231
231
|
return `argument comparison on arg ${s.argIndex} (predicate DSL)`;
|
|
232
|
+
case 'arg_len':
|
|
233
|
+
return `vec length comparison on arg ${s.argIndex} (predicate DSL)`;
|
|
234
|
+
case 'arg_field':
|
|
235
|
+
return `map field comparison on arg ${s.argIndex}.${s.field} (predicate DSL)`;
|
|
232
236
|
case 'calldata':
|
|
233
237
|
return 'EVM calldata comparison (predicate DSL)';
|
|
234
238
|
case 'value':
|
|
@@ -244,6 +248,10 @@ function describeSelector(s) {
|
|
|
244
248
|
switch (s.kind) {
|
|
245
249
|
case 'arg':
|
|
246
250
|
return `arg ${s.argIndex}`;
|
|
251
|
+
case 'arg_len':
|
|
252
|
+
return `arg_len(${s.argIndex})`;
|
|
253
|
+
case 'arg_field':
|
|
254
|
+
return `arg_field(${s.argIndex}, ${s.element}, ${s.field})`;
|
|
247
255
|
case 'amount':
|
|
248
256
|
return `amount(${s.token})`;
|
|
249
257
|
case 'window_spent':
|
package/dist/ir/types.d.ts
CHANGED
|
@@ -14,6 +14,15 @@ export type IRSelector = {
|
|
|
14
14
|
fieldIndex?: number;
|
|
15
15
|
vecMode?: IRVecMode;
|
|
16
16
|
scalarType: IRScalarType;
|
|
17
|
+
} | {
|
|
18
|
+
kind: 'arg_len';
|
|
19
|
+
argIndex: number;
|
|
20
|
+
} | {
|
|
21
|
+
kind: 'arg_field';
|
|
22
|
+
argIndex: number;
|
|
23
|
+
element: number;
|
|
24
|
+
field: string;
|
|
25
|
+
scalarType: IRScalarType;
|
|
17
26
|
} | {
|
|
18
27
|
kind: 'calldata';
|
|
19
28
|
offset: number;
|
package/dist/predicate/encode.js
CHANGED
|
@@ -167,6 +167,15 @@ function encodeLeaf(leaf) {
|
|
|
167
167
|
return xdr.ScVal.scvVec([symbol('call_fn')]);
|
|
168
168
|
case 'call_arg':
|
|
169
169
|
return xdr.ScVal.scvVec([symbol('call_arg'), xdr.ScVal.scvU32(leaf.index)]);
|
|
170
|
+
case 'call_arg_len':
|
|
171
|
+
return xdr.ScVal.scvVec([symbol('call_arg_len'), xdr.ScVal.scvU32(leaf.index)]);
|
|
172
|
+
case 'call_arg_field':
|
|
173
|
+
return xdr.ScVal.scvVec([
|
|
174
|
+
symbol('call_arg_field'),
|
|
175
|
+
xdr.ScVal.scvU32(leaf.index),
|
|
176
|
+
xdr.ScVal.scvU32(leaf.element),
|
|
177
|
+
xdr.ScVal.scvSymbol(leaf.field),
|
|
178
|
+
]);
|
|
170
179
|
case 'amount':
|
|
171
180
|
return xdr.ScVal.scvVec([symbol('amount'), scvAddressFromStrkey(leaf.token)]);
|
|
172
181
|
case 'window_spent':
|
|
@@ -276,6 +276,95 @@ function appendProtocolSpecificConstraints(ozConstraints, interpreterConstraints
|
|
|
276
276
|
}
|
|
277
277
|
}
|
|
278
278
|
}
|
|
279
|
+
// Blend `submit` ONLY (not `claim`, whose vec arg is a vec<u32> of
|
|
280
|
+
// reserve_token_ids - no map fields to bind). The `requests` vec
|
|
281
|
+
// (call_arg[3]) is a vec<Request{ address, amount, request_type }>. Each
|
|
282
|
+
// Request is the per-reserve action selector - 0 Supply, 1 Withdraw,
|
|
283
|
+
// 2 SupplyCollateral, 3 WithdrawCollateral, 4 Borrow, 5 Repay, 6-9
|
|
284
|
+
// liquidation/auction fills. Pinning only one element is unsafe: a caller
|
|
285
|
+
// can append a second element with a different action (WithdrawCollateral
|
|
286
|
+
// -> Borrow on a different asset, any amount, then auction fills). Length
|
|
287
|
+
// + per-element pinning is total; a quantifier over elements is not. If we
|
|
288
|
+
// cannot emit BOTH, we surface AMBIGUITY rather than emit a partial bind.
|
|
289
|
+
if (protocol.protocol === 'blend' && protocol.fn === 'submit' && interpreterEnabled) {
|
|
290
|
+
const requestsArg = topLevel.args[3];
|
|
291
|
+
if (requestsArg && requestsArg.type === 'vec') {
|
|
292
|
+
const elements = requestsArg.value;
|
|
293
|
+
interpreterConstraints.push({
|
|
294
|
+
op: 'compare',
|
|
295
|
+
compare: {
|
|
296
|
+
selector: { kind: 'arg_len', argIndex: 3 },
|
|
297
|
+
operator: 'eq',
|
|
298
|
+
value: String(elements.length),
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
for (let i = 0; i < elements.length; i++) {
|
|
302
|
+
const element = elements[i];
|
|
303
|
+
if (!element || element.type !== 'map')
|
|
304
|
+
continue;
|
|
305
|
+
const fields = element.value;
|
|
306
|
+
const fieldValue = (name, scalarType) => {
|
|
307
|
+
const entry = fields.find((e) => e.key === name);
|
|
308
|
+
if (!entry)
|
|
309
|
+
return null;
|
|
310
|
+
if (entry.val.type === 'address' && scalarType === 'address')
|
|
311
|
+
return entry.val.value;
|
|
312
|
+
if (entry.val.type === 'i128' && scalarType === 'i128')
|
|
313
|
+
return entry.val.value;
|
|
314
|
+
if (entry.val.type === 'u32' && scalarType === 'u32')
|
|
315
|
+
return entry.val.value;
|
|
316
|
+
return null;
|
|
317
|
+
};
|
|
318
|
+
const address = fieldValue('address', 'address');
|
|
319
|
+
const amount = fieldValue('amount', 'i128');
|
|
320
|
+
const requestType = fieldValue('request_type', 'u32');
|
|
321
|
+
if (address === null || amount === null || requestType === null)
|
|
322
|
+
continue;
|
|
323
|
+
interpreterConstraints.push({
|
|
324
|
+
op: 'compare',
|
|
325
|
+
compare: {
|
|
326
|
+
selector: {
|
|
327
|
+
kind: 'arg_field',
|
|
328
|
+
argIndex: 3,
|
|
329
|
+
element: i,
|
|
330
|
+
field: 'request_type',
|
|
331
|
+
scalarType: 'u32',
|
|
332
|
+
},
|
|
333
|
+
operator: 'eq',
|
|
334
|
+
value: requestType,
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
interpreterConstraints.push({
|
|
338
|
+
op: 'compare',
|
|
339
|
+
compare: {
|
|
340
|
+
selector: {
|
|
341
|
+
kind: 'arg_field',
|
|
342
|
+
argIndex: 3,
|
|
343
|
+
element: i,
|
|
344
|
+
field: 'address',
|
|
345
|
+
scalarType: 'address',
|
|
346
|
+
},
|
|
347
|
+
operator: 'eq',
|
|
348
|
+
value: address,
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
interpreterConstraints.push({
|
|
352
|
+
op: 'compare',
|
|
353
|
+
compare: {
|
|
354
|
+
selector: {
|
|
355
|
+
kind: 'arg_field',
|
|
356
|
+
argIndex: 3,
|
|
357
|
+
element: i,
|
|
358
|
+
field: 'amount',
|
|
359
|
+
scalarType: 'i128',
|
|
360
|
+
},
|
|
361
|
+
operator: 'lte',
|
|
362
|
+
value: amount,
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
279
368
|
// SoroSwap: the recorded hop path -> eq_seq on the path arg (call_arg[2]).
|
|
280
369
|
// The interpreter adapter is the ONLY way to express an exact ordered
|
|
281
370
|
// sequence (OZ built-ins cannot). Element order is preserved verbatim.
|
package/dist/synth/deny-cases.js
CHANGED
|
@@ -16,6 +16,15 @@ const ADJACENT_ASSETS = [
|
|
|
16
16
|
// them as FINDINGS against the already-emitted policy.
|
|
17
17
|
const OVERPERMISSIVE_DIMENSIONS = ['argument_reorder'];
|
|
18
18
|
// The 15 dimensions the synth pipeline uses for self-verify and minimise.
|
|
19
|
+
// Phase 1 grammar extension: `vec_append` and `map_field_flip` are listed below
|
|
20
|
+
// alongside the existing dimensions so the per-element binds emitted for
|
|
21
|
+
// Blend `submit` (call_arg_len + 3 call_arg_field per element) survive
|
|
22
|
+
// minimise. Without these, no deny case exercises the new leaves and the
|
|
23
|
+
// minimise pass drops them as "redundant" - reopening the element-append
|
|
24
|
+
// hole they were added to close. Both dimensions are no-ops for any fixture
|
|
25
|
+
// whose predicate has no `call_arg_len` / `call_arg_field` leaves (the
|
|
26
|
+
// generator short-circuits on leaf-kind), so they cannot make production
|
|
27
|
+
// synthesis refuse for fixtures that do not exercise the new grammar.
|
|
19
28
|
const ORIGINAL_DIMENSIONS = [
|
|
20
29
|
'amount',
|
|
21
30
|
'asset',
|
|
@@ -32,6 +41,8 @@ const ORIGINAL_DIMENSIONS = [
|
|
|
32
41
|
'oracle_deviation_exceeded',
|
|
33
42
|
'oracle_paused',
|
|
34
43
|
'soroswap_allowed_path',
|
|
44
|
+
'vec_append',
|
|
45
|
+
'map_field_flip',
|
|
35
46
|
];
|
|
36
47
|
export { ORIGINAL_DIMENSIONS, OVERPERMISSIVE_DIMENSIONS };
|
|
37
48
|
/** Build deterministic model-evaluated alternatives without mutating the intended call.
|
|
@@ -191,7 +202,9 @@ export function generateCases(predicate, permitCtx, dimensions) {
|
|
|
191
202
|
hasConstrainedArg &&
|
|
192
203
|
permitCtx.args.length >= 2 &&
|
|
193
204
|
permitCtx.args[0]?.type === 'address' &&
|
|
194
|
-
permitCtx.args[1]?.type === 'address'
|
|
205
|
+
permitCtx.args[1]?.type === 'address' &&
|
|
206
|
+
permitCtx.args[0].value !==
|
|
207
|
+
permitCtx.args[1].value) {
|
|
195
208
|
const ctx = cloneContext(permitCtx);
|
|
196
209
|
// Guard above guarantees args[0] and args[1] exist and are address-typed.
|
|
197
210
|
const a0 = ctx.args[0];
|
|
@@ -200,6 +213,59 @@ export function generateCases(predicate, permitCtx, dimensions) {
|
|
|
200
213
|
ctx.args[1] = a0;
|
|
201
214
|
denies.push({ dimension: 'argument_reorder', ctx });
|
|
202
215
|
}
|
|
216
|
+
// --- map_field_flip: flip a bound map field to a different valid value of
|
|
217
|
+
// the same type. Mirrors argument_reorder: OPT-IN only, never ORIGINAL.
|
|
218
|
+
// Targets each `call_arg_field` leaf and produces a ctx whose vec element
|
|
219
|
+
// has a different value of the recorded field (different request_type,
|
|
220
|
+
// different amount, different address). The blend-submit case uses this
|
|
221
|
+
// to detect a missing request_type pin. ---
|
|
222
|
+
if (!dimensions || dimensions.includes('map_field_flip')) {
|
|
223
|
+
for (const comparison of facts.comparisons) {
|
|
224
|
+
const sel = comparison.left;
|
|
225
|
+
if (sel.kind !== 'call_arg_field')
|
|
226
|
+
continue;
|
|
227
|
+
const arg = permitCtx.args[sel.index];
|
|
228
|
+
if (!arg || arg.type !== 'vec')
|
|
229
|
+
continue;
|
|
230
|
+
const element = arg.value[sel.element];
|
|
231
|
+
if (!element || element.type !== 'map' || !Array.isArray(element.value))
|
|
232
|
+
continue;
|
|
233
|
+
const entry = element.value.find((e) => e.key === sel.field);
|
|
234
|
+
if (!entry)
|
|
235
|
+
continue;
|
|
236
|
+
const flipped = flipFieldValue(entry.val);
|
|
237
|
+
if (flipped === null)
|
|
238
|
+
continue;
|
|
239
|
+
const ctx = cloneContext(permitCtx);
|
|
240
|
+
const clonedVec = arg.value.map(cloneScVal);
|
|
241
|
+
const clonedElement = clonedVec[sel.element];
|
|
242
|
+
if (clonedElement?.type !== 'map' || !Array.isArray(clonedElement.value))
|
|
243
|
+
continue;
|
|
244
|
+
clonedElement.value = clonedElement.value.map((e) => e.key === sel.field ? { key: e.key, val: flipped } : e);
|
|
245
|
+
clonedVec[sel.element] = clonedElement;
|
|
246
|
+
ctx.args[sel.index] = { type: 'vec', value: clonedVec };
|
|
247
|
+
denies.push({ dimension: 'map_field_flip', ctx });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// --- vec_append: append a new element to a bound vec. Mirrors map_field_flip:
|
|
251
|
+
// OPT-IN only, never ORIGINAL. Targets every `call_arg_len` leaf; without the
|
|
252
|
+
// length pin a caller can append an extra element to defeat per-element binds. ---
|
|
253
|
+
if (!dimensions || dimensions.includes('vec_append')) {
|
|
254
|
+
for (const comparison of facts.comparisons) {
|
|
255
|
+
const sel = comparison.left;
|
|
256
|
+
if (sel.kind !== 'call_arg_len')
|
|
257
|
+
continue;
|
|
258
|
+
const arg = permitCtx.args[sel.index];
|
|
259
|
+
if (!arg || arg.type !== 'vec')
|
|
260
|
+
continue;
|
|
261
|
+
const ctx = cloneContext(permitCtx);
|
|
262
|
+
ctx.args[sel.index] = {
|
|
263
|
+
type: 'vec',
|
|
264
|
+
value: [...arg.value, { type: 'other', value: 'deny-case-vec-append' }],
|
|
265
|
+
};
|
|
266
|
+
denies.push({ dimension: 'vec_append', ctx });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
203
269
|
// Version mismatch, malformed predicates, master authorization, and nonce replay are install-time checks and are intentionally omitted from model-evaluated cases.
|
|
204
270
|
return { permit: cloneContext(permitCtx), denies };
|
|
205
271
|
}
|
|
@@ -431,3 +497,25 @@ function cloneDepthError(value) {
|
|
|
431
497
|
err.depthContext = value.type;
|
|
432
498
|
throw err;
|
|
433
499
|
}
|
|
500
|
+
/** Build a value of the SAME ScVal type that differs from the recorded one,
|
|
501
|
+
* used by the `map_field_flip` mutation to defeat a per-element pin without
|
|
502
|
+
* changing the wire type (a type/arity change is rejected by host dispatch
|
|
503
|
+
* before Policy::enforce, so it is not the predicate's job). Returns null
|
|
504
|
+
* when the ScVal type has no obvious different value (e.g. opaque/other). */
|
|
505
|
+
function flipFieldValue(val) {
|
|
506
|
+
switch (val.type) {
|
|
507
|
+
case 'address': {
|
|
508
|
+
const a = 'GBFKRGJYZXLTDEI36ZCQEIM225NMOCR2VDBOIHJTXJ54FEFFVL2FKALE';
|
|
509
|
+
const b = 'GD6XSMQJ47EHHJOWXQOND5YDVZC37JWZJHYHBKE6QJFSLLJ5KQXM5QS5';
|
|
510
|
+
return { type: 'address', value: val.value === a ? b : a };
|
|
511
|
+
}
|
|
512
|
+
case 'i128':
|
|
513
|
+
case 'u64':
|
|
514
|
+
case 'u32':
|
|
515
|
+
return { type: val.type, value: String(BigInt(val.value) + 1n) };
|
|
516
|
+
case 'symbol':
|
|
517
|
+
return { type: 'symbol', value: `${val.value}x` };
|
|
518
|
+
default:
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
}
|
package/dist/synth/evaluate.js
CHANGED
|
@@ -175,6 +175,38 @@ function evalCompare(op, left, right, ctx) {
|
|
|
175
175
|
return evalArgOrderedCompare(op, actual, right);
|
|
176
176
|
return evalArgEq(op, actual, right, ctx);
|
|
177
177
|
}
|
|
178
|
+
// --- step 4c: call_arg_len: the length of a vec-typed argument as a u32.
|
|
179
|
+
// Fails closed on a non-vec arg, an absent arg, or a non-u32 literal.
|
|
180
|
+
if (left.kind === 'call_arg_len') {
|
|
181
|
+
const actual = ctx.args[left.index];
|
|
182
|
+
if (!actual || actual.type !== 'vec')
|
|
183
|
+
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
184
|
+
if (right.kind !== 'literal_u32')
|
|
185
|
+
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
186
|
+
return actual.value.length === right.value
|
|
187
|
+
? { permit: true }
|
|
188
|
+
: { permit: false, reason: 'ARG_MISMATCH' };
|
|
189
|
+
}
|
|
190
|
+
// --- step 4d: call_arg_field: the value of a field in the map at element i
|
|
191
|
+
// of the vec at argument index. Fails closed on a non-vec arg, an
|
|
192
|
+
// out-of-range element, a missing field, a non-map element, or a type
|
|
193
|
+
// mismatch between the field ScVal and the literal leaf.
|
|
194
|
+
if (left.kind === 'call_arg_field') {
|
|
195
|
+
const actual = ctx.args[left.index];
|
|
196
|
+
if (!actual || actual.type !== 'vec')
|
|
197
|
+
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
198
|
+
const element = actual.value[left.element];
|
|
199
|
+
if (!element || element.type !== 'map')
|
|
200
|
+
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
201
|
+
if (!Array.isArray(element.value))
|
|
202
|
+
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
203
|
+
const entry = element.value.find((e) => e.key === left.field);
|
|
204
|
+
if (!entry)
|
|
205
|
+
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
206
|
+
if (op === 'eq')
|
|
207
|
+
return evalArgEq(op, entry.val, right, ctx);
|
|
208
|
+
return evalArgOrderedCompare(op, entry.val, right);
|
|
209
|
+
}
|
|
178
210
|
// --- step 6: AMOUNT_BOUND ---
|
|
179
211
|
if (left.kind === 'amount' && op !== 'eq') {
|
|
180
212
|
return evalAmountCompare(op, left.token, right, ctx);
|
|
@@ -334,6 +366,23 @@ function resolveLeaf(leaf, ctx) {
|
|
|
334
366
|
return { type: 'symbol', value: ctx.fn };
|
|
335
367
|
case 'call_arg':
|
|
336
368
|
return ctx.args[leaf.index];
|
|
369
|
+
case 'call_arg_len':
|
|
370
|
+
// No direct ScVal projection: the length is an integer the comparator
|
|
371
|
+
// resolves against the right-hand literal. Returning undefined keeps
|
|
372
|
+
// the `in` membership path structurally informed (no haystack match).
|
|
373
|
+
return undefined;
|
|
374
|
+
case 'call_arg_field': {
|
|
375
|
+
const actual = ctx.args[leaf.index];
|
|
376
|
+
if (!actual || actual.type !== 'vec')
|
|
377
|
+
return undefined;
|
|
378
|
+
const element = actual.value[leaf.element];
|
|
379
|
+
if (!element || element.type !== 'map')
|
|
380
|
+
return undefined;
|
|
381
|
+
if (!Array.isArray(element.value))
|
|
382
|
+
return undefined;
|
|
383
|
+
const entry = element.value.find((e) => e.key === leaf.field);
|
|
384
|
+
return entry ? entry.val : undefined;
|
|
385
|
+
}
|
|
337
386
|
case 'amount':
|
|
338
387
|
case 'window_spent':
|
|
339
388
|
case 'oracle_price':
|
package/dist/types.d.ts
CHANGED
|
@@ -182,6 +182,14 @@ export type PredicateLeaf = {
|
|
|
182
182
|
} | {
|
|
183
183
|
kind: 'call_arg';
|
|
184
184
|
index: number;
|
|
185
|
+
} | {
|
|
186
|
+
kind: 'call_arg_len';
|
|
187
|
+
index: number;
|
|
188
|
+
} | {
|
|
189
|
+
kind: 'call_arg_field';
|
|
190
|
+
index: number;
|
|
191
|
+
element: number;
|
|
192
|
+
field: string;
|
|
185
193
|
} | {
|
|
186
194
|
kind: 'amount';
|
|
187
195
|
token: string;
|
|
@@ -312,6 +312,10 @@ function lowerSelector(s) {
|
|
|
312
312
|
switch (s.kind) {
|
|
313
313
|
case 'arg':
|
|
314
314
|
return { kind: 'call_arg', index: s.argIndex };
|
|
315
|
+
case 'arg_len':
|
|
316
|
+
return { kind: 'call_arg_len', index: s.argIndex };
|
|
317
|
+
case 'arg_field':
|
|
318
|
+
return { kind: 'call_arg_field', index: s.argIndex, element: s.element, field: s.field };
|
|
315
319
|
case 'amount':
|
|
316
320
|
return { kind: 'amount', token: s.token };
|
|
317
321
|
case 'window_spent':
|
|
@@ -334,13 +338,15 @@ function lowerSelector(s) {
|
|
|
334
338
|
* selector kind to the matching `literal_*` kind. The IR compare value is a
|
|
335
339
|
* raw string (i128-safe); the selector kind fixes the canonical wire type:
|
|
336
340
|
* - arg -> IR scalarType (set by the recorder/parser)
|
|
341
|
+
* - arg_len -> u32 (vec length is a small non-negative integer)
|
|
342
|
+
* - arg_field -> IR scalarType (the field's recorded type)
|
|
337
343
|
* - amount -> i128 (canonical Stellar token amount encoding)
|
|
338
344
|
* - window_spent -> i128 (canonical amount encoding)
|
|
339
345
|
* - oracle_price -> i128 (canonical price encoding)
|
|
340
346
|
* - invocation_count -> u32 (counts are small non-negative integers)
|
|
341
347
|
* - now / valid_until -> u64 (unix timestamps in seconds) */
|
|
342
348
|
function literalFromIRCompare(c) {
|
|
343
|
-
const scalarType =
|
|
349
|
+
const scalarType = selectorScalarType(c.selector);
|
|
344
350
|
return literalFromScalar(c.value, scalarType);
|
|
345
351
|
}
|
|
346
352
|
function literalScalarForSelector(kind) {
|
|
@@ -350,14 +356,16 @@ function literalScalarForSelector(kind) {
|
|
|
350
356
|
case 'oracle_price':
|
|
351
357
|
return 'i128';
|
|
352
358
|
case 'invocation_count':
|
|
359
|
+
case 'arg_len':
|
|
353
360
|
return 'u32';
|
|
354
361
|
case 'now':
|
|
355
362
|
case 'valid_until':
|
|
356
363
|
return 'u64';
|
|
357
364
|
case 'arg':
|
|
365
|
+
case 'arg_field':
|
|
358
366
|
case 'calldata':
|
|
359
367
|
case 'value':
|
|
360
|
-
// arg -> caller handles scalarType; calldata/value -> unreachable (Path-B).
|
|
368
|
+
// arg / arg_field -> caller handles scalarType; calldata/value -> unreachable (Path-B).
|
|
361
369
|
return 'i128';
|
|
362
370
|
}
|
|
363
371
|
}
|
|
@@ -385,10 +393,12 @@ function literalFromScalar(value, scalarType) {
|
|
|
385
393
|
/** Scalar type of an IRSelector for the purpose of building literal leaves
|
|
386
394
|
* (the right-hand side of `eq` / elements of an `in` haystack / elements of
|
|
387
395
|
* a `literal_vec`). Mirrors `literalScalarForSelector` for OZ extensions and
|
|
388
|
-
* uses the selector's own `scalarType` for `arg` selectors. */
|
|
396
|
+
* uses the selector's own `scalarType` for `arg` and `arg_field` selectors. */
|
|
389
397
|
function selectorScalarType(selector) {
|
|
390
398
|
if (selector.kind === 'arg')
|
|
391
399
|
return selector.scalarType;
|
|
400
|
+
if (selector.kind === 'arg_field')
|
|
401
|
+
return selector.scalarType;
|
|
392
402
|
return literalScalarForSelector(selector.kind);
|
|
393
403
|
}
|
|
394
404
|
/** Walk a condition tree and throw if any oracle_price leaf is found anywhere
|
|
@@ -462,6 +472,10 @@ function describeCondition(cond) {
|
|
|
462
472
|
return `per-call amount comparison on ${s.token}`;
|
|
463
473
|
case 'arg':
|
|
464
474
|
return `argument comparison on arg ${s.argIndex}`;
|
|
475
|
+
case 'arg_len':
|
|
476
|
+
return `vec length comparison on arg ${s.argIndex}`;
|
|
477
|
+
case 'arg_field':
|
|
478
|
+
return `map field comparison on arg ${s.argIndex}.${s.field}`;
|
|
465
479
|
case 'calldata':
|
|
466
480
|
return 'EVM calldata comparison';
|
|
467
481
|
case 'value':
|
|
@@ -477,6 +491,10 @@ function describeSelector(s) {
|
|
|
477
491
|
switch (s.kind) {
|
|
478
492
|
case 'arg':
|
|
479
493
|
return `arg ${s.argIndex}`;
|
|
494
|
+
case 'arg_len':
|
|
495
|
+
return `arg_len(${s.argIndex})`;
|
|
496
|
+
case 'arg_field':
|
|
497
|
+
return `arg_field(${s.argIndex}, ${s.element}, ${s.field})`;
|
|
480
498
|
case 'amount':
|
|
481
499
|
return `amount(${s.token})`;
|
|
482
500
|
case 'window_spent':
|
|
@@ -234,6 +234,10 @@ function describeCondition(cond) {
|
|
|
234
234
|
return `per-call amount comparison on ${s.token} (predicate DSL)`;
|
|
235
235
|
case 'arg':
|
|
236
236
|
return `argument comparison on arg ${s.argIndex} (predicate DSL)`;
|
|
237
|
+
case 'arg_len':
|
|
238
|
+
return `vec length comparison on arg ${s.argIndex} (predicate DSL)`;
|
|
239
|
+
case 'arg_field':
|
|
240
|
+
return `map field comparison on arg ${s.argIndex}.${s.field} (predicate DSL)`;
|
|
237
241
|
case 'calldata':
|
|
238
242
|
return 'EVM calldata comparison (predicate DSL)';
|
|
239
243
|
case 'value':
|
|
@@ -249,6 +253,10 @@ function describeSelector(s) {
|
|
|
249
253
|
switch (s.kind) {
|
|
250
254
|
case 'arg':
|
|
251
255
|
return `arg ${s.argIndex}`;
|
|
256
|
+
case 'arg_len':
|
|
257
|
+
return `arg_len(${s.argIndex})`;
|
|
258
|
+
case 'arg_field':
|
|
259
|
+
return `arg_field(${s.argIndex}, ${s.element}, ${s.field})`;
|
|
252
260
|
case 'amount':
|
|
253
261
|
return `amount(${s.token})`;
|
|
254
262
|
case 'window_spent':
|
package/dist-cjs/ir/types.d.ts
CHANGED
|
@@ -14,6 +14,15 @@ export type IRSelector = {
|
|
|
14
14
|
fieldIndex?: number;
|
|
15
15
|
vecMode?: IRVecMode;
|
|
16
16
|
scalarType: IRScalarType;
|
|
17
|
+
} | {
|
|
18
|
+
kind: 'arg_len';
|
|
19
|
+
argIndex: number;
|
|
20
|
+
} | {
|
|
21
|
+
kind: 'arg_field';
|
|
22
|
+
argIndex: number;
|
|
23
|
+
element: number;
|
|
24
|
+
field: string;
|
|
25
|
+
scalarType: IRScalarType;
|
|
17
26
|
} | {
|
|
18
27
|
kind: 'calldata';
|
|
19
28
|
offset: number;
|
|
@@ -170,6 +170,15 @@ function encodeLeaf(leaf) {
|
|
|
170
170
|
return stellar_sdk_1.xdr.ScVal.scvVec([symbol('call_fn')]);
|
|
171
171
|
case 'call_arg':
|
|
172
172
|
return stellar_sdk_1.xdr.ScVal.scvVec([symbol('call_arg'), stellar_sdk_1.xdr.ScVal.scvU32(leaf.index)]);
|
|
173
|
+
case 'call_arg_len':
|
|
174
|
+
return stellar_sdk_1.xdr.ScVal.scvVec([symbol('call_arg_len'), stellar_sdk_1.xdr.ScVal.scvU32(leaf.index)]);
|
|
175
|
+
case 'call_arg_field':
|
|
176
|
+
return stellar_sdk_1.xdr.ScVal.scvVec([
|
|
177
|
+
symbol('call_arg_field'),
|
|
178
|
+
stellar_sdk_1.xdr.ScVal.scvU32(leaf.index),
|
|
179
|
+
stellar_sdk_1.xdr.ScVal.scvU32(leaf.element),
|
|
180
|
+
stellar_sdk_1.xdr.ScVal.scvSymbol(leaf.field),
|
|
181
|
+
]);
|
|
173
182
|
case 'amount':
|
|
174
183
|
return stellar_sdk_1.xdr.ScVal.scvVec([symbol('amount'), scvAddressFromStrkey(leaf.token)]);
|
|
175
184
|
case 'window_spent':
|