@crediolabs/policy-synth 0.1.7 → 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 +115 -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 +115 -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 +113 -0
- package/src/synth/deny-cases.ts +86 -1
- package/src/synth/evaluate.ts +41 -0
- package/src/types.ts +10 -0
package/src/synth/deny-cases.ts
CHANGED
|
@@ -51,6 +51,15 @@ const ADJACENT_ASSETS = [
|
|
|
51
51
|
const OVERPERMISSIVE_DIMENSIONS = ['argument_reorder'] as const
|
|
52
52
|
|
|
53
53
|
// The 15 dimensions the synth pipeline uses for self-verify and minimise.
|
|
54
|
+
// Phase 1 grammar extension: `vec_append` and `map_field_flip` are listed below
|
|
55
|
+
// alongside the existing dimensions so the per-element binds emitted for
|
|
56
|
+
// Blend `submit` (call_arg_len + 3 call_arg_field per element) survive
|
|
57
|
+
// minimise. Without these, no deny case exercises the new leaves and the
|
|
58
|
+
// minimise pass drops them as "redundant" - reopening the element-append
|
|
59
|
+
// hole they were added to close. Both dimensions are no-ops for any fixture
|
|
60
|
+
// whose predicate has no `call_arg_len` / `call_arg_field` leaves (the
|
|
61
|
+
// generator short-circuits on leaf-kind), so they cannot make production
|
|
62
|
+
// synthesis refuse for fixtures that do not exercise the new grammar.
|
|
54
63
|
const ORIGINAL_DIMENSIONS: string[] = [
|
|
55
64
|
'amount',
|
|
56
65
|
'asset',
|
|
@@ -67,6 +76,8 @@ const ORIGINAL_DIMENSIONS: string[] = [
|
|
|
67
76
|
'oracle_deviation_exceeded',
|
|
68
77
|
'oracle_paused',
|
|
69
78
|
'soroswap_allowed_path',
|
|
79
|
+
'vec_append',
|
|
80
|
+
'map_field_flip',
|
|
70
81
|
]
|
|
71
82
|
|
|
72
83
|
export { ORIGINAL_DIMENSIONS, OVERPERMISSIVE_DIMENSIONS }
|
|
@@ -263,7 +274,9 @@ export function generateCases(
|
|
|
263
274
|
hasConstrainedArg &&
|
|
264
275
|
permitCtx.args.length >= 2 &&
|
|
265
276
|
permitCtx.args[0]?.type === 'address' &&
|
|
266
|
-
permitCtx.args[1]?.type === 'address'
|
|
277
|
+
permitCtx.args[1]?.type === 'address' &&
|
|
278
|
+
(permitCtx.args[0] as { type: 'address'; value: string }).value !==
|
|
279
|
+
(permitCtx.args[1] as { type: 'address'; value: string }).value
|
|
267
280
|
) {
|
|
268
281
|
const ctx = cloneContext(permitCtx)
|
|
269
282
|
// Guard above guarantees args[0] and args[1] exist and are address-typed.
|
|
@@ -274,6 +287,55 @@ export function generateCases(
|
|
|
274
287
|
denies.push({ dimension: 'argument_reorder', ctx })
|
|
275
288
|
}
|
|
276
289
|
|
|
290
|
+
// --- map_field_flip: flip a bound map field to a different valid value of
|
|
291
|
+
// the same type. Mirrors argument_reorder: OPT-IN only, never ORIGINAL.
|
|
292
|
+
// Targets each `call_arg_field` leaf and produces a ctx whose vec element
|
|
293
|
+
// has a different value of the recorded field (different request_type,
|
|
294
|
+
// different amount, different address). The blend-submit case uses this
|
|
295
|
+
// to detect a missing request_type pin. ---
|
|
296
|
+
if (!dimensions || dimensions.includes('map_field_flip')) {
|
|
297
|
+
for (const comparison of facts.comparisons) {
|
|
298
|
+
const sel = comparison.left
|
|
299
|
+
if (sel.kind !== 'call_arg_field') continue
|
|
300
|
+
const arg = permitCtx.args[sel.index]
|
|
301
|
+
if (!arg || arg.type !== 'vec') continue
|
|
302
|
+
const element = arg.value[sel.element]
|
|
303
|
+
if (!element || element.type !== 'map' || !Array.isArray(element.value)) continue
|
|
304
|
+
const entry = element.value.find((e) => e.key === sel.field)
|
|
305
|
+
if (!entry) continue
|
|
306
|
+
const flipped = flipFieldValue(entry.val)
|
|
307
|
+
if (flipped === null) continue
|
|
308
|
+
const ctx = cloneContext(permitCtx)
|
|
309
|
+
const clonedVec = arg.value.map(cloneScVal)
|
|
310
|
+
const clonedElement = clonedVec[sel.element]
|
|
311
|
+
if (clonedElement?.type !== 'map' || !Array.isArray(clonedElement.value)) continue
|
|
312
|
+
clonedElement.value = clonedElement.value.map((e) =>
|
|
313
|
+
e.key === sel.field ? { key: e.key, val: flipped } : e
|
|
314
|
+
)
|
|
315
|
+
clonedVec[sel.element] = clonedElement
|
|
316
|
+
ctx.args[sel.index] = { type: 'vec', value: clonedVec }
|
|
317
|
+
denies.push({ dimension: 'map_field_flip', ctx })
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// --- vec_append: append a new element to a bound vec. Mirrors map_field_flip:
|
|
322
|
+
// OPT-IN only, never ORIGINAL. Targets every `call_arg_len` leaf; without the
|
|
323
|
+
// length pin a caller can append an extra element to defeat per-element binds. ---
|
|
324
|
+
if (!dimensions || dimensions.includes('vec_append')) {
|
|
325
|
+
for (const comparison of facts.comparisons) {
|
|
326
|
+
const sel = comparison.left
|
|
327
|
+
if (sel.kind !== 'call_arg_len') continue
|
|
328
|
+
const arg = permitCtx.args[sel.index]
|
|
329
|
+
if (!arg || arg.type !== 'vec') continue
|
|
330
|
+
const ctx = cloneContext(permitCtx)
|
|
331
|
+
ctx.args[sel.index] = {
|
|
332
|
+
type: 'vec',
|
|
333
|
+
value: [...arg.value, { type: 'other', value: 'deny-case-vec-append' }],
|
|
334
|
+
}
|
|
335
|
+
denies.push({ dimension: 'vec_append', ctx })
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
277
339
|
// Version mismatch, malformed predicates, master authorization, and nonce replay are install-time checks and are intentionally omitted from model-evaluated cases.
|
|
278
340
|
return { permit: cloneContext(permitCtx), denies }
|
|
279
341
|
}
|
|
@@ -528,3 +590,26 @@ function cloneDepthError(value: ScVal): never {
|
|
|
528
590
|
err.depthContext = value.type
|
|
529
591
|
throw err
|
|
530
592
|
}
|
|
593
|
+
|
|
594
|
+
/** Build a value of the SAME ScVal type that differs from the recorded one,
|
|
595
|
+
* used by the `map_field_flip` mutation to defeat a per-element pin without
|
|
596
|
+
* changing the wire type (a type/arity change is rejected by host dispatch
|
|
597
|
+
* before Policy::enforce, so it is not the predicate's job). Returns null
|
|
598
|
+
* when the ScVal type has no obvious different value (e.g. opaque/other). */
|
|
599
|
+
function flipFieldValue(val: ScVal): ScVal | null {
|
|
600
|
+
switch (val.type) {
|
|
601
|
+
case 'address': {
|
|
602
|
+
const a = 'GBFKRGJYZXLTDEI36ZCQEIM225NMOCR2VDBOIHJTXJ54FEFFVL2FKALE'
|
|
603
|
+
const b = 'GD6XSMQJ47EHHJOWXQOND5YDVZC37JWZJHYHBKE6QJFSLLJ5KQXM5QS5'
|
|
604
|
+
return { type: 'address', value: val.value === a ? b : a }
|
|
605
|
+
}
|
|
606
|
+
case 'i128':
|
|
607
|
+
case 'u64':
|
|
608
|
+
case 'u32':
|
|
609
|
+
return { type: val.type, value: String(BigInt(val.value) + 1n) }
|
|
610
|
+
case 'symbol':
|
|
611
|
+
return { type: 'symbol', value: `${val.value}x` }
|
|
612
|
+
default:
|
|
613
|
+
return null
|
|
614
|
+
}
|
|
615
|
+
}
|
package/src/synth/evaluate.ts
CHANGED
|
@@ -216,6 +216,33 @@ function evalCompare(
|
|
|
216
216
|
return evalArgEq(op, actual, right, ctx)
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
// --- step 4c: call_arg_len: the length of a vec-typed argument as a u32.
|
|
220
|
+
// Fails closed on a non-vec arg, an absent arg, or a non-u32 literal.
|
|
221
|
+
if (left.kind === 'call_arg_len') {
|
|
222
|
+
const actual = ctx.args[left.index]
|
|
223
|
+
if (!actual || actual.type !== 'vec') return { permit: false, reason: 'ARG_MISMATCH' }
|
|
224
|
+
if (right.kind !== 'literal_u32') return { permit: false, reason: 'ARG_MISMATCH' }
|
|
225
|
+
return actual.value.length === right.value
|
|
226
|
+
? { permit: true }
|
|
227
|
+
: { permit: false, reason: 'ARG_MISMATCH' }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// --- step 4d: call_arg_field: the value of a field in the map at element i
|
|
231
|
+
// of the vec at argument index. Fails closed on a non-vec arg, an
|
|
232
|
+
// out-of-range element, a missing field, a non-map element, or a type
|
|
233
|
+
// mismatch between the field ScVal and the literal leaf.
|
|
234
|
+
if (left.kind === 'call_arg_field') {
|
|
235
|
+
const actual = ctx.args[left.index]
|
|
236
|
+
if (!actual || actual.type !== 'vec') return { permit: false, reason: 'ARG_MISMATCH' }
|
|
237
|
+
const element = actual.value[left.element]
|
|
238
|
+
if (!element || element.type !== 'map') return { permit: false, reason: 'ARG_MISMATCH' }
|
|
239
|
+
if (!Array.isArray(element.value)) return { permit: false, reason: 'ARG_MISMATCH' }
|
|
240
|
+
const entry = element.value.find((e) => e.key === left.field)
|
|
241
|
+
if (!entry) return { permit: false, reason: 'ARG_MISMATCH' }
|
|
242
|
+
if (op === 'eq') return evalArgEq(op, entry.val, right, ctx)
|
|
243
|
+
return evalArgOrderedCompare(op, entry.val, right)
|
|
244
|
+
}
|
|
245
|
+
|
|
219
246
|
// --- step 6: AMOUNT_BOUND ---
|
|
220
247
|
if (left.kind === 'amount' && op !== 'eq') {
|
|
221
248
|
return evalAmountCompare(op, left.token, right, ctx)
|
|
@@ -383,6 +410,20 @@ function resolveLeaf(leaf: PredicateLeaf, ctx: EvalContext): ScVal | undefined {
|
|
|
383
410
|
return { type: 'symbol', value: ctx.fn }
|
|
384
411
|
case 'call_arg':
|
|
385
412
|
return ctx.args[leaf.index]
|
|
413
|
+
case 'call_arg_len':
|
|
414
|
+
// No direct ScVal projection: the length is an integer the comparator
|
|
415
|
+
// resolves against the right-hand literal. Returning undefined keeps
|
|
416
|
+
// the `in` membership path structurally informed (no haystack match).
|
|
417
|
+
return undefined
|
|
418
|
+
case 'call_arg_field': {
|
|
419
|
+
const actual = ctx.args[leaf.index]
|
|
420
|
+
if (!actual || actual.type !== 'vec') return undefined
|
|
421
|
+
const element = actual.value[leaf.element]
|
|
422
|
+
if (!element || element.type !== 'map') return undefined
|
|
423
|
+
if (!Array.isArray(element.value)) return undefined
|
|
424
|
+
const entry = element.value.find((e) => e.key === leaf.field)
|
|
425
|
+
return entry ? entry.val : undefined
|
|
426
|
+
}
|
|
386
427
|
case 'amount':
|
|
387
428
|
case 'window_spent':
|
|
388
429
|
case 'oracle_price':
|
package/src/types.ts
CHANGED
|
@@ -145,6 +145,16 @@ export type PredicateLeaf =
|
|
|
145
145
|
| { kind: 'call_contract' }
|
|
146
146
|
| { kind: 'call_fn' }
|
|
147
147
|
| { kind: 'call_arg'; index: number }
|
|
148
|
+
// Length of a vec-typed argument as a u32. Binds the OUTER vec length so
|
|
149
|
+
// a caller cannot append a new element to defeat a per-element pin.
|
|
150
|
+
// Paired with `call_arg_field` per element - pinning only the field leaves
|
|
151
|
+
// an element-append vulnerability; this leaf closes it.
|
|
152
|
+
| { kind: 'call_arg_len'; index: number }
|
|
153
|
+
// Value of `field` in the map at `element` of the vec at argument `index`.
|
|
154
|
+
// Fixed-arity: index, element, field. The leaf is resolved against the
|
|
155
|
+
// recorded ScVal type. A non-vec arg, missing element, missing field, or
|
|
156
|
+
// type mismatch all DENY (fail-closed).
|
|
157
|
+
| { kind: 'call_arg_field'; index: number; element: number; field: string }
|
|
148
158
|
// `call_sub_invocation[i]` is NOT in v1 grammar. OZ `Policy::enforce`
|
|
149
159
|
// receives one `Context`, not a sub-invocation tree; sub-invocations live under
|
|
150
160
|
// `InvokerContractAuthEntry::Contract(SubContractInvocation)`, not in the
|