@crediolabs/policy-synth 0.1.3 → 0.1.5
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/LICENSE +21 -0
- package/README.md +193 -5
- package/dist/adapters/interpreter/adapter.d.ts +38 -0
- package/dist/adapters/interpreter/adapter.js +522 -0
- package/dist/adapters/interpreter/index.d.ts +1 -0
- package/dist/adapters/interpreter/index.js +2 -0
- package/dist/adapters/oz/adapter.js +2 -0
- package/dist/codegen/compile-gate.d.ts +33 -0
- package/dist/codegen/compile-gate.js +119 -0
- package/dist/codegen/index.d.ts +2 -0
- package/dist/codegen/index.js +8 -0
- package/dist/codegen/template.d.ts +18 -0
- package/dist/codegen/template.js +131 -0
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/ir/types.d.ts +13 -2
- package/dist/predicate/encode.d.ts +10 -0
- package/dist/predicate/encode.js +249 -0
- package/dist/predicate/index.d.ts +1 -0
- package/dist/predicate/index.js +2 -0
- package/dist/record/decode.d.ts +6 -3
- package/dist/record/decode.js +13 -3
- package/dist/review-card/builder.d.ts +14 -0
- package/dist/review-card/builder.js +261 -0
- package/dist/review-card/conflict.d.ts +40 -0
- package/dist/review-card/conflict.js +111 -0
- package/dist/review-card/cross-check.d.ts +11 -0
- package/dist/review-card/cross-check.js +148 -0
- package/dist/review-card/index.d.ts +3 -0
- package/dist/review-card/index.js +4 -0
- package/dist/run/index.d.ts +31 -0
- package/dist/run/index.js +164 -0
- package/dist/run/schemas.d.ts +2003 -0
- package/dist/run/schemas.js +215 -0
- package/dist/synth/compose-from-recording.d.ts +35 -7
- package/dist/synth/compose-from-recording.js +225 -34
- package/dist/synth/deny-cases.d.ts +12 -0
- package/dist/synth/deny-cases.js +377 -0
- package/dist/synth/evaluate.d.ts +39 -0
- package/dist/synth/evaluate.js +425 -0
- package/dist/synth/harness.d.ts +16 -0
- package/dist/synth/harness.js +26 -0
- package/dist/synth/index.d.ts +4 -0
- package/dist/synth/index.js +4 -0
- package/dist/synth/minimize.d.ts +4 -0
- package/dist/synth/minimize.js +38 -0
- package/dist/synth/predicate-literals.d.ts +5 -0
- package/dist/synth/predicate-literals.js +25 -0
- package/dist/synth/synthesize-from-recording.d.ts +37 -5
- package/dist/synth/synthesize-from-recording.js +550 -18
- package/dist/types.d.ts +40 -1
- package/dist/types.js +17 -0
- package/dist/verify/envelope.d.ts +15 -0
- package/dist/verify/envelope.js +22 -0
- package/dist/verify/index.d.ts +3 -0
- package/dist/verify/index.js +3 -0
- package/dist/verify/simulate.d.ts +31 -0
- package/dist/verify/simulate.js +258 -0
- package/dist/verify/verify.d.ts +21 -0
- package/dist/verify/verify.js +189 -0
- package/dist-cjs/adapters/interpreter/adapter.d.ts +38 -0
- package/dist-cjs/adapters/interpreter/adapter.js +527 -0
- package/dist-cjs/adapters/interpreter/index.d.ts +1 -0
- package/dist-cjs/adapters/interpreter/index.js +8 -0
- package/dist-cjs/adapters/oz/adapter.d.ts +20 -0
- package/dist-cjs/adapters/oz/adapter.js +289 -0
- package/dist-cjs/adapters/oz/index.d.ts +1 -0
- package/dist-cjs/adapters/oz/index.js +8 -0
- package/dist-cjs/codegen/compile-gate.d.ts +33 -0
- package/dist-cjs/codegen/compile-gate.js +123 -0
- package/dist-cjs/codegen/index.d.ts +2 -0
- package/dist-cjs/codegen/index.js +14 -0
- package/dist-cjs/codegen/template.d.ts +18 -0
- package/dist-cjs/codegen/template.js +134 -0
- package/dist-cjs/errors.d.ts +37 -0
- package/dist-cjs/errors.js +3 -0
- package/dist-cjs/index.d.ts +11 -0
- package/dist-cjs/index.js +27 -0
- package/dist-cjs/ir/index.d.ts +1 -0
- package/dist-cjs/ir/index.js +3 -0
- package/dist-cjs/ir/types.d.ts +108 -0
- package/dist-cjs/ir/types.js +12 -0
- package/dist-cjs/mandate/index.d.ts +2 -0
- package/dist-cjs/mandate/index.js +6 -0
- package/dist-cjs/mandate/to-ir.d.ts +3 -0
- package/dist-cjs/mandate/to-ir.js +63 -0
- package/dist-cjs/mandate/types.d.ts +20 -0
- package/dist-cjs/mandate/types.js +9 -0
- package/dist-cjs/package.json +3 -0
- package/dist-cjs/predicate/encode.d.ts +10 -0
- package/dist-cjs/predicate/encode.js +252 -0
- package/dist-cjs/predicate/index.d.ts +1 -0
- package/dist-cjs/predicate/index.js +6 -0
- package/dist-cjs/record/decode.d.ts +79 -0
- package/dist-cjs/record/decode.js +398 -0
- package/dist-cjs/record/freshness.d.ts +17 -0
- package/dist-cjs/record/freshness.js +55 -0
- package/dist-cjs/record/index.d.ts +21 -0
- package/dist-cjs/record/index.js +166 -0
- package/dist-cjs/record/movements.d.ts +20 -0
- package/dist-cjs/record/movements.js +192 -0
- package/dist-cjs/record/rpc.d.ts +22 -0
- package/dist-cjs/record/rpc.js +74 -0
- package/dist-cjs/record/validate.d.ts +22 -0
- package/dist-cjs/record/validate.js +63 -0
- package/dist-cjs/registry/identify.d.ts +11 -0
- package/dist-cjs/registry/identify.js +87 -0
- package/dist-cjs/registry/index.d.ts +3 -0
- package/dist-cjs/registry/index.js +15 -0
- package/dist-cjs/registry/known-addresses.d.ts +16 -0
- package/dist-cjs/registry/known-addresses.js +53 -0
- package/dist-cjs/registry/protocols.d.ts +38 -0
- package/dist-cjs/registry/protocols.js +153 -0
- package/dist-cjs/review-card/builder.d.ts +14 -0
- package/dist-cjs/review-card/builder.js +264 -0
- package/dist-cjs/review-card/conflict.d.ts +40 -0
- package/dist-cjs/review-card/conflict.js +114 -0
- package/dist-cjs/review-card/cross-check.d.ts +11 -0
- package/dist-cjs/review-card/cross-check.js +151 -0
- package/dist-cjs/review-card/index.d.ts +3 -0
- package/dist-cjs/review-card/index.js +10 -0
- package/dist-cjs/run/index.d.ts +31 -0
- package/dist-cjs/run/index.js +178 -0
- package/dist-cjs/run/schemas.d.ts +2003 -0
- package/dist-cjs/run/schemas.js +218 -0
- package/dist-cjs/seams/index.d.ts +1 -0
- package/dist-cjs/seams/index.js +3 -0
- package/dist-cjs/seams/types.d.ts +66 -0
- package/dist-cjs/seams/types.js +12 -0
- package/dist-cjs/synth/compose-from-recording.d.ts +64 -0
- package/dist-cjs/synth/compose-from-recording.js +356 -0
- package/dist-cjs/synth/deny-cases.d.ts +12 -0
- package/dist-cjs/synth/deny-cases.js +380 -0
- package/dist-cjs/synth/evaluate.d.ts +39 -0
- package/dist-cjs/synth/evaluate.js +428 -0
- package/dist-cjs/synth/harness.d.ts +16 -0
- package/dist-cjs/synth/harness.js +29 -0
- package/dist-cjs/synth/index.d.ts +9 -0
- package/dist-cjs/synth/index.js +23 -0
- package/dist-cjs/synth/lower.d.ts +23 -0
- package/dist-cjs/synth/lower.js +119 -0
- package/dist-cjs/synth/minimize.d.ts +4 -0
- package/dist-cjs/synth/minimize.js +41 -0
- package/dist-cjs/synth/predicate-literals.d.ts +5 -0
- package/dist-cjs/synth/predicate-literals.js +28 -0
- package/dist-cjs/synth/scope.d.ts +26 -0
- package/dist-cjs/synth/scope.js +81 -0
- package/dist-cjs/synth/synthesize-from-mandate.d.ts +5 -0
- package/dist-cjs/synth/synthesize-from-mandate.js +37 -0
- package/dist-cjs/synth/synthesize-from-recording.d.ts +49 -0
- package/dist-cjs/synth/synthesize-from-recording.js +711 -0
- package/dist-cjs/types.d.ts +288 -0
- package/dist-cjs/types.js +55 -0
- package/dist-cjs/verify/envelope.d.ts +15 -0
- package/dist-cjs/verify/envelope.js +23 -0
- package/dist-cjs/verify/index.d.ts +3 -0
- package/dist-cjs/verify/index.js +8 -0
- package/dist-cjs/verify/simulate.d.ts +31 -0
- package/dist-cjs/verify/simulate.js +261 -0
- package/dist-cjs/verify/verify.d.ts +21 -0
- package/dist-cjs/verify/verify.js +192 -0
- package/package.json +43 -3
- package/src/adapters/interpreter/adapter.ts +642 -0
- package/src/adapters/interpreter/index.ts +8 -0
- package/src/adapters/oz/adapter.ts +2 -0
- package/src/codegen/compile-gate.ts +162 -0
- package/src/codegen/index.ts +17 -0
- package/src/codegen/template.ts +148 -0
- package/src/errors.ts +2 -0
- package/src/index.ts +2 -0
- package/src/ir/types.ts +9 -2
- package/src/predicate/encode.ts +307 -0
- package/src/predicate/index.ts +3 -0
- package/src/record/decode.ts +21 -10
- package/src/review-card/builder.ts +303 -0
- package/src/review-card/conflict.ts +143 -0
- package/src/review-card/cross-check.ts +158 -0
- package/src/review-card/index.ts +12 -0
- package/src/run/index.ts +213 -0
- package/src/run/schemas.ts +242 -0
- package/src/synth/compose-from-recording.ts +277 -43
- package/src/synth/deny-cases.ts +466 -0
- package/src/synth/evaluate.ts +493 -0
- package/src/synth/harness.ts +40 -0
- package/src/synth/index.ts +12 -0
- package/src/synth/minimize.ts +44 -0
- package/src/synth/predicate-literals.ts +27 -0
- package/src/synth/synthesize-from-recording.ts +655 -19
- package/src/types.ts +38 -2
- package/src/verify/envelope.ts +28 -0
- package/src/verify/index.ts +5 -0
- package/src/verify/simulate.ts +311 -0
- package/src/verify/verify.ts +243 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import type { PredicateLeaf, PredicateNode, ScVal } from '../types.ts'
|
|
2
|
+
import { MAX_SCVAL_CLONE_DEPTH } from '../types.ts'
|
|
3
|
+
import type { EvalContext } from './evaluate.ts'
|
|
4
|
+
import { literalNumericBigInt } from './predicate-literals.ts'
|
|
5
|
+
|
|
6
|
+
export interface DenyCase {
|
|
7
|
+
dimension: string
|
|
8
|
+
ctx: EvalContext
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface GeneratedCases {
|
|
12
|
+
permit: EvalContext
|
|
13
|
+
denies: DenyCase[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type ComparisonOperator = 'eq' | 'lt' | 'lte' | 'gt' | 'gte'
|
|
17
|
+
|
|
18
|
+
type ComparisonNode = {
|
|
19
|
+
op: ComparisonOperator
|
|
20
|
+
left: PredicateLeaf
|
|
21
|
+
right: PredicateLeaf
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type MembershipNode = {
|
|
25
|
+
op: 'in'
|
|
26
|
+
needle: PredicateLeaf
|
|
27
|
+
haystack: PredicateLeaf[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface PredicateFacts {
|
|
31
|
+
comparisons: ComparisonNode[]
|
|
32
|
+
memberships: MembershipNode[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const ORACLE_CASES = [
|
|
36
|
+
['oracle_stale', 'stale'],
|
|
37
|
+
['oracle_missing', 'missing'],
|
|
38
|
+
['oracle_deviation_exceeded', 'deviation'],
|
|
39
|
+
['oracle_paused', 'paused'],
|
|
40
|
+
] as const
|
|
41
|
+
|
|
42
|
+
// Deterministic XLM/USDC adjacency fixture; the shared registry can replace this boundary later.
|
|
43
|
+
const ADJACENT_ASSETS = [
|
|
44
|
+
'CAS3J7GYLGXMF6TDJ5WQ2PEN4GRVNXJUIQ2TZU3ZB3OQ2V4DRCWI7WPF',
|
|
45
|
+
'CCWCLTASNDT57N3BCHOSVB5QWMV5URK4BXLDDF6ZZQYMBQ4OKZA3ZB2N',
|
|
46
|
+
] as const
|
|
47
|
+
|
|
48
|
+
/** Build deterministic model-evaluated alternatives without mutating the intended call. */
|
|
49
|
+
export function generateCases(predicate: PredicateNode, permitCtx: EvalContext): GeneratedCases {
|
|
50
|
+
const facts = inspectPredicate(predicate)
|
|
51
|
+
const denies: DenyCase[] = []
|
|
52
|
+
|
|
53
|
+
for (const comparison of facts.comparisons) {
|
|
54
|
+
if (comparison.left.kind !== 'amount') continue
|
|
55
|
+
const mutated = mutateBigIntRecord(
|
|
56
|
+
permitCtx,
|
|
57
|
+
'amountByToken',
|
|
58
|
+
comparison.left.token,
|
|
59
|
+
comparison
|
|
60
|
+
)
|
|
61
|
+
if (mutated) denies.push({ dimension: 'amount', ctx: mutated })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const movedTokens = new Set<string>()
|
|
65
|
+
for (const comparison of facts.comparisons) {
|
|
66
|
+
if (comparison.left.kind === 'amount' || comparison.left.kind === 'window_spent') {
|
|
67
|
+
movedTokens.add(comparison.left.token)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
for (const token of movedTokens) {
|
|
71
|
+
denies.push({ dimension: 'asset', ctx: mutateAsset(predicate, permitCtx, token) })
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const contractConstraints = [
|
|
75
|
+
...facts.comparisons.filter(
|
|
76
|
+
(node) =>
|
|
77
|
+
node.op === 'eq' &&
|
|
78
|
+
node.left.kind === 'call_contract' &&
|
|
79
|
+
node.right.kind === 'literal_address'
|
|
80
|
+
),
|
|
81
|
+
...facts.memberships.filter((node) => node.needle.kind === 'call_contract'),
|
|
82
|
+
]
|
|
83
|
+
for (const _constraint of contractConstraints) {
|
|
84
|
+
const ctx = cloneContext(permitCtx)
|
|
85
|
+
ctx.contract = distinctText(permitCtx.contract, 'contract')
|
|
86
|
+
denies.push({ dimension: 'contract', ctx })
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const functionConstraints = [
|
|
90
|
+
...facts.comparisons.filter(
|
|
91
|
+
(node) =>
|
|
92
|
+
node.op === 'eq' && node.left.kind === 'call_fn' && node.right.kind === 'literal_symbol'
|
|
93
|
+
),
|
|
94
|
+
...facts.memberships.filter((node) => node.needle.kind === 'call_fn'),
|
|
95
|
+
]
|
|
96
|
+
for (const _constraint of functionConstraints) {
|
|
97
|
+
const ctx = cloneContext(permitCtx)
|
|
98
|
+
ctx.fn = distinctText(permitCtx.fn, 'function')
|
|
99
|
+
denies.push({ dimension: 'function', ctx })
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (permitCtx.validUntilLedger !== undefined) {
|
|
103
|
+
const ctx = cloneContext(permitCtx)
|
|
104
|
+
ctx.atLedger = permitCtx.validUntilLedger + 1
|
|
105
|
+
denies.push({ dimension: 'timing', ctx })
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (const comparison of facts.comparisons) {
|
|
109
|
+
if (comparison.left.kind !== 'window_spent') continue
|
|
110
|
+
const mutated = mutateBigIntRecord(
|
|
111
|
+
permitCtx,
|
|
112
|
+
'windowSpentByToken',
|
|
113
|
+
comparison.left.token,
|
|
114
|
+
comparison,
|
|
115
|
+
false
|
|
116
|
+
)
|
|
117
|
+
if (mutated) denies.push({ dimension: 'time_window', ctx: mutated })
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
for (const comparison of facts.comparisons) {
|
|
121
|
+
if (comparison.left.kind !== 'invocation_count_in_window') continue
|
|
122
|
+
if (comparison.right.kind !== 'literal_u32') continue
|
|
123
|
+
const ctx = cloneContext(permitCtx)
|
|
124
|
+
ctx.invocationCountByWindow[comparison.left.windowSecs] = violatingNumber(
|
|
125
|
+
comparison.op,
|
|
126
|
+
comparison.right.value
|
|
127
|
+
)
|
|
128
|
+
denies.push({ dimension: 'invocation_count', ctx })
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Ordered numeric bound on a call_arg (e.g. a SoroSwap input-amount cap
|
|
132
|
+
// `call_arg[0] <= limit`). A violating deny case pushes the arg past the
|
|
133
|
+
// bound so the leaf is exercised - and, critically, so `minimize` keeps it:
|
|
134
|
+
// a conjunct that no deny case needs is pruned as redundant, which would
|
|
135
|
+
// silently drop a caller-requested restriction.
|
|
136
|
+
for (const comparison of facts.comparisons) {
|
|
137
|
+
if (comparison.op === 'eq' || comparison.left.kind !== 'call_arg') continue
|
|
138
|
+
const violating = violatingArgScVal(comparison.op, comparison.right)
|
|
139
|
+
if (!violating) continue
|
|
140
|
+
const ctx = cloneContext(permitCtx)
|
|
141
|
+
ctx.args[comparison.left.index] = violating
|
|
142
|
+
denies.push({ dimension: 'arg_amount_bound', ctx })
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const argumentConstraints: Array<{ index: number }> = []
|
|
146
|
+
for (const comparison of facts.comparisons) {
|
|
147
|
+
if (
|
|
148
|
+
comparison.op === 'eq' &&
|
|
149
|
+
comparison.left.kind === 'call_arg' &&
|
|
150
|
+
comparison.right.kind !== 'literal_vec'
|
|
151
|
+
) {
|
|
152
|
+
argumentConstraints.push({ index: comparison.left.index })
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
for (const membership of facts.memberships) {
|
|
156
|
+
if (membership.needle.kind === 'call_arg') {
|
|
157
|
+
argumentConstraints.push({ index: membership.needle.index })
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
for (const constraint of argumentConstraints) {
|
|
161
|
+
const ctx = cloneContext(permitCtx)
|
|
162
|
+
ctx.args[constraint.index] = { type: 'other', value: 'deny-case-opaque-argument' }
|
|
163
|
+
denies.push({ dimension: 'arg_bound', ctx })
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const scopedArgumentIndices = new Set<number>(argumentConstraints.map(({ index }) => index))
|
|
167
|
+
for (const comparison of facts.comparisons) {
|
|
168
|
+
if (
|
|
169
|
+
comparison.op === 'eq' &&
|
|
170
|
+
comparison.left.kind === 'call_arg' &&
|
|
171
|
+
comparison.right.kind === 'literal_vec'
|
|
172
|
+
) {
|
|
173
|
+
scopedArgumentIndices.add(comparison.left.index)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (
|
|
177
|
+
contractConstraints.length > 0 &&
|
|
178
|
+
functionConstraints.length > 0 &&
|
|
179
|
+
scopedArgumentIndices.size > 0
|
|
180
|
+
) {
|
|
181
|
+
const ctx = cloneContext(permitCtx)
|
|
182
|
+
ctx.contract = distinctText(permitCtx.contract, 'authorized-call-contract')
|
|
183
|
+
ctx.fn = distinctText(permitCtx.fn, 'authorized-call-function')
|
|
184
|
+
for (const index of scopedArgumentIndices) {
|
|
185
|
+
ctx.args[index] = { type: 'other', value: 'deny-case-authorized-call-argument' }
|
|
186
|
+
}
|
|
187
|
+
denies.push({ dimension: 'scope_contract_fn_arg', ctx })
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const oracleComparisons = facts.comparisons.filter((node) => node.left.kind === 'oracle_price')
|
|
191
|
+
for (const [dimension, error] of ORACLE_CASES) {
|
|
192
|
+
for (const comparison of oracleComparisons) {
|
|
193
|
+
if (comparison.left.kind !== 'oracle_price') continue
|
|
194
|
+
const ctx = cloneContext(permitCtx)
|
|
195
|
+
ctx.oraclePriceByAsset[comparison.left.asset] = { error }
|
|
196
|
+
denies.push({ dimension, ctx })
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
for (const comparison of facts.comparisons) {
|
|
201
|
+
if (
|
|
202
|
+
comparison.op !== 'eq' ||
|
|
203
|
+
comparison.left.kind !== 'call_arg' ||
|
|
204
|
+
comparison.right.kind !== 'literal_vec'
|
|
205
|
+
) {
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
const ctx = cloneContext(permitCtx)
|
|
209
|
+
ctx.args[comparison.left.index] = differentVector(ctx.args[comparison.left.index])
|
|
210
|
+
denies.push({ dimension: 'soroswap_allowed_path', ctx })
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Version mismatch, malformed predicates, master authorization, and nonce replay are install-time checks and are intentionally omitted from model-evaluated cases.
|
|
214
|
+
return { permit: cloneContext(permitCtx), denies }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function inspectPredicate(predicate: PredicateNode): PredicateFacts {
|
|
218
|
+
const facts: PredicateFacts = { comparisons: [], memberships: [] }
|
|
219
|
+
visit(predicate, facts)
|
|
220
|
+
return facts
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function visit(node: PredicateNode, facts: PredicateFacts): void {
|
|
224
|
+
switch (node.op) {
|
|
225
|
+
case 'and':
|
|
226
|
+
case 'or':
|
|
227
|
+
for (const child of node.children) visit(child, facts)
|
|
228
|
+
return
|
|
229
|
+
case 'not':
|
|
230
|
+
visit(node.child, facts)
|
|
231
|
+
return
|
|
232
|
+
case 'in':
|
|
233
|
+
facts.memberships.push(node)
|
|
234
|
+
return
|
|
235
|
+
case 'eq':
|
|
236
|
+
case 'lt':
|
|
237
|
+
case 'lte':
|
|
238
|
+
case 'gt':
|
|
239
|
+
case 'gte':
|
|
240
|
+
facts.comparisons.push(node)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function mutateBigIntRecord(
|
|
245
|
+
permitCtx: EvalContext,
|
|
246
|
+
recordKey: 'amountByToken' | 'windowSpentByToken',
|
|
247
|
+
token: string,
|
|
248
|
+
comparison: ComparisonNode,
|
|
249
|
+
preferScaledAmount = true
|
|
250
|
+
): EvalContext | null {
|
|
251
|
+
if (comparison.right.kind !== 'literal_i128') return null
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
const current = BigInt(permitCtx[recordKey][token] ?? '0')
|
|
255
|
+
const bound = BigInt(comparison.right.value)
|
|
256
|
+
const value = violatingBigInt(comparison.op, current, bound, preferScaledAmount)
|
|
257
|
+
const ctx = cloneContext(permitCtx)
|
|
258
|
+
ctx[recordKey][token] = value.toString()
|
|
259
|
+
return ctx
|
|
260
|
+
} catch {
|
|
261
|
+
return null
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function violatingBigInt(
|
|
266
|
+
op: ComparisonOperator,
|
|
267
|
+
current: bigint,
|
|
268
|
+
bound: bigint,
|
|
269
|
+
preferScaledAmount: boolean
|
|
270
|
+
): bigint {
|
|
271
|
+
if (preferScaledAmount) {
|
|
272
|
+
const scaledCandidates = [(current * 101n) / 100n, current * 10n]
|
|
273
|
+
for (const candidate of scaledCandidates) {
|
|
274
|
+
if (!bigIntComparison(op, candidate, bound)) return candidate
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return boundaryViolatingBigInt(op, bound)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** The single value at the boundary that violates `<selector> op bound`:
|
|
282
|
+
* bound+1 for `lte`/`eq`, bound-1 for `gte`, and bound itself for the strict
|
|
283
|
+
* `lt`/`gt`. */
|
|
284
|
+
function boundaryViolatingBigInt(op: ComparisonOperator, bound: bigint): bigint {
|
|
285
|
+
switch (op) {
|
|
286
|
+
case 'lt':
|
|
287
|
+
case 'gt':
|
|
288
|
+
return bound
|
|
289
|
+
case 'lte':
|
|
290
|
+
case 'eq':
|
|
291
|
+
return bound + 1n
|
|
292
|
+
case 'gte':
|
|
293
|
+
return bound - 1n
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function violatingNumber(op: ComparisonOperator, bound: number): number {
|
|
298
|
+
switch (op) {
|
|
299
|
+
case 'lt':
|
|
300
|
+
case 'gt':
|
|
301
|
+
return bound
|
|
302
|
+
case 'lte':
|
|
303
|
+
case 'eq':
|
|
304
|
+
return bound + 1
|
|
305
|
+
case 'gte':
|
|
306
|
+
return bound - 1
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Build an ScVal that VIOLATES an ordered numeric bound on a call_arg, given
|
|
311
|
+
* the comparison op and its numeric-literal right-hand side. Returns null when
|
|
312
|
+
* the literal is not an integer (the bound is not a numeric compare). The arg
|
|
313
|
+
* is emitted as an i128 - the recorder's numeric args are read via BigInt, so
|
|
314
|
+
* the wire type only needs to be a numeric ScVal to exercise the bound. */
|
|
315
|
+
function violatingArgScVal(op: ComparisonOperator, right: PredicateLeaf): ScVal | null {
|
|
316
|
+
const bound = literalNumericBigInt(right)
|
|
317
|
+
if (bound === null) return null
|
|
318
|
+
return { type: 'i128', value: boundaryViolatingBigInt(op, bound).toString() }
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function bigIntComparison(op: ComparisonOperator, value: bigint, bound: bigint): boolean {
|
|
322
|
+
switch (op) {
|
|
323
|
+
case 'eq':
|
|
324
|
+
return value === bound
|
|
325
|
+
case 'lt':
|
|
326
|
+
return value < bound
|
|
327
|
+
case 'lte':
|
|
328
|
+
return value <= bound
|
|
329
|
+
case 'gt':
|
|
330
|
+
return value > bound
|
|
331
|
+
case 'gte':
|
|
332
|
+
return value >= bound
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function mutateAsset(predicate: PredicateNode, permitCtx: EvalContext, token: string): EvalContext {
|
|
337
|
+
const binding = findAddressBinding(predicate, token)
|
|
338
|
+
const adjacent = adjacentAsset(token)
|
|
339
|
+
const ctx = cloneContext(permitCtx)
|
|
340
|
+
moveRecordEntry(ctx.amountByToken, token, adjacent)
|
|
341
|
+
moveRecordEntry(ctx.windowSpentByToken, token, adjacent)
|
|
342
|
+
if (binding.contract && ctx.contract === token) ctx.contract = adjacent
|
|
343
|
+
for (const index of binding.argumentIndices) {
|
|
344
|
+
const value = ctx.args[index]
|
|
345
|
+
if (value) ctx.args[index] = replaceAddress(value, token, adjacent)
|
|
346
|
+
}
|
|
347
|
+
return ctx
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function findAddressBinding(
|
|
351
|
+
predicate: PredicateNode,
|
|
352
|
+
address: string
|
|
353
|
+
): { contract: boolean; argumentIndices: Set<number> } {
|
|
354
|
+
const facts = inspectPredicate(predicate)
|
|
355
|
+
let contract = false
|
|
356
|
+
const argumentIndices = new Set<number>()
|
|
357
|
+
|
|
358
|
+
for (const comparison of facts.comparisons) {
|
|
359
|
+
if (comparison.op !== 'eq' || !leafContainsAddress(comparison.right, address)) continue
|
|
360
|
+
if (comparison.left.kind === 'call_contract') contract = true
|
|
361
|
+
if (comparison.left.kind === 'call_arg') argumentIndices.add(comparison.left.index)
|
|
362
|
+
}
|
|
363
|
+
for (const membership of facts.memberships) {
|
|
364
|
+
if (!membership.haystack.some((leaf) => leafContainsAddress(leaf, address))) continue
|
|
365
|
+
if (membership.needle.kind === 'call_contract') contract = true
|
|
366
|
+
if (membership.needle.kind === 'call_arg') argumentIndices.add(membership.needle.index)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return { contract, argumentIndices }
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function leafContainsAddress(leaf: PredicateLeaf, address: string): boolean {
|
|
373
|
+
if (leaf.kind === 'literal_address') return leaf.value === address
|
|
374
|
+
if (leaf.kind === 'literal_vec') {
|
|
375
|
+
return leaf.elements.some((element) => leafContainsAddress(element, address))
|
|
376
|
+
}
|
|
377
|
+
return false
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function moveRecordEntry(record: Record<string, string>, from: string, to: string): void {
|
|
381
|
+
const value = record[from]
|
|
382
|
+
if (value === undefined) return
|
|
383
|
+
delete record[from]
|
|
384
|
+
record[to] = value
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function replaceAddress(value: ScVal, from: string, to: string): ScVal {
|
|
388
|
+
if (value.type === 'address') {
|
|
389
|
+
return value.value === from ? { type: 'address', value: to } : { ...value }
|
|
390
|
+
}
|
|
391
|
+
if (value.type === 'vec') {
|
|
392
|
+
return { type: 'vec', value: value.value.map((item) => replaceAddress(item, from, to)) }
|
|
393
|
+
}
|
|
394
|
+
return { ...value }
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function differentVector(actual: ScVal | undefined): ScVal {
|
|
398
|
+
if (actual?.type !== 'vec') return { type: 'vec', value: [] }
|
|
399
|
+
if (actual.value.length === 0) {
|
|
400
|
+
return { type: 'vec', value: [{ type: 'other', value: 'deny-case-extra-hop' }] }
|
|
401
|
+
}
|
|
402
|
+
if (actual.value.length > 1) {
|
|
403
|
+
const reversed = actual.value.map(cloneScVal).reverse()
|
|
404
|
+
if (JSON.stringify(reversed) !== JSON.stringify(actual.value)) {
|
|
405
|
+
return { type: 'vec', value: reversed }
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const value = actual.value.map(cloneScVal)
|
|
409
|
+
value[0] = { type: 'other', value: 'deny-case-different-hop' }
|
|
410
|
+
return { type: 'vec', value }
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function adjacentAsset(asset: string): string {
|
|
414
|
+
return asset === ADJACENT_ASSETS[0] ? ADJACENT_ASSETS[1] : ADJACENT_ASSETS[0]
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function distinctText(value: string, label: string): string {
|
|
418
|
+
return `${value}#${label}`
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function cloneContext(ctx: EvalContext): EvalContext {
|
|
422
|
+
const cloned: EvalContext = {
|
|
423
|
+
contract: ctx.contract,
|
|
424
|
+
fn: ctx.fn,
|
|
425
|
+
args: ctx.args.map(cloneScVal),
|
|
426
|
+
atLedger: ctx.atLedger,
|
|
427
|
+
nowSeconds: ctx.nowSeconds,
|
|
428
|
+
amountByToken: { ...ctx.amountByToken },
|
|
429
|
+
windowSpentByToken: { ...ctx.windowSpentByToken },
|
|
430
|
+
invocationCountByWindow: { ...ctx.invocationCountByWindow },
|
|
431
|
+
oraclePriceByAsset: Object.fromEntries(
|
|
432
|
+
Object.entries(ctx.oraclePriceByAsset).map(([asset, entry]) => [
|
|
433
|
+
asset,
|
|
434
|
+
'error' in entry ? { error: entry.error } : { ...entry },
|
|
435
|
+
])
|
|
436
|
+
),
|
|
437
|
+
}
|
|
438
|
+
if (ctx.validUntilLedger !== undefined) cloned.validUntilLedger = ctx.validUntilLedger
|
|
439
|
+
if (ctx.signerWeights !== undefined) cloned.signerWeights = { ...ctx.signerWeights }
|
|
440
|
+
return cloned
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function cloneScVal(value: ScVal, depth = 0): ScVal {
|
|
444
|
+
// Recursion is bounded by MAX_SCVAL_CLONE_DEPTH so a hand-crafted nested-vec
|
|
445
|
+
// payload cannot RangeError the JS stack during deny-case mutation. Over-depth
|
|
446
|
+
// throws a ToolError-shaped error that the `synthesizeFromRecording` envelope
|
|
447
|
+
// (item 3) converts to a structured `{ok:false, error}`.
|
|
448
|
+
if (value.type === 'vec') {
|
|
449
|
+
if (depth >= MAX_SCVAL_CLONE_DEPTH) {
|
|
450
|
+
throw cloneDepthError(value)
|
|
451
|
+
}
|
|
452
|
+
return { type: 'vec', value: value.value.map((v) => cloneScVal(v, depth + 1)) }
|
|
453
|
+
}
|
|
454
|
+
return { ...value }
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function cloneDepthError(value: ScVal): never {
|
|
458
|
+
const err = new Error(
|
|
459
|
+
`ScVal clone depth exceeds MAX_SCVAL_CLONE_DEPTH (${MAX_SCVAL_CLONE_DEPTH})`
|
|
460
|
+
) as Error & { code: string; severity: string; retryable: boolean; depthContext: unknown }
|
|
461
|
+
err.code = 'SYNTHESIS_ERROR'
|
|
462
|
+
err.severity = 'error'
|
|
463
|
+
err.retryable = false
|
|
464
|
+
err.depthContext = value.type
|
|
465
|
+
throw err
|
|
466
|
+
}
|