@crediolabs/policy-synth 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/adapters/interpreter/adapter.d.ts +19 -25
- package/dist/adapters/interpreter/adapter.js +54 -487
- package/dist/adapters/interpreter/index.d.ts +1 -1
- package/dist/adapters/interpreter/index.js +1 -1
- package/dist/adapters/oz/adapter.js +1 -15
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +1 -4
- package/dist/index.js +1 -4
- package/dist/install/build-add-context-rule.d.ts +3 -19
- package/dist/install/build-add-context-rule.js +3 -98
- package/dist/install/build-install-policy.d.ts +4 -53
- package/dist/install/build-install-policy.js +44 -123
- package/dist/install/index.d.ts +0 -2
- package/dist/install/index.js +0 -7
- package/dist/ir/index.d.ts +1 -1
- package/dist/ir/types.d.ts +7 -76
- package/dist/ir/types.js +0 -2
- package/dist/predicate/decode.d.ts +1 -1
- package/dist/predicate/decode.js +2 -70
- package/dist/predicate/encode.js +45 -229
- package/dist/predicate/from-json.js +1 -42
- package/dist/record/decode.js +3 -8
- package/dist/review-card/builder.d.ts +4 -3
- package/dist/review-card/builder.js +18 -162
- package/dist/review-card/cross-check.js +2 -105
- package/dist/review-card/render-leaf.d.ts +4 -0
- package/dist/review-card/render-leaf.js +47 -0
- package/dist/run/index.d.ts +36 -139
- package/dist/run/index.js +125 -437
- package/dist/run/schemas.d.ts +168 -1929
- package/dist/run/schemas.js +40 -269
- package/dist/seams/types.d.ts +0 -2
- package/dist/simulate/deny-cases.d.ts +26 -0
- package/dist/simulate/deny-cases.js +282 -0
- package/dist/simulate/evaluate.d.ts +18 -0
- package/dist/simulate/evaluate.js +271 -0
- package/dist/simulate/index.d.ts +4 -0
- package/dist/simulate/index.js +8 -0
- package/dist/synth/compose-from-recording.d.ts +24 -51
- package/dist/synth/compose-from-recording.js +120 -282
- package/dist/synth/deny-cases.d.ts +5 -7
- package/dist/synth/deny-cases.js +23 -373
- package/dist/synth/evaluate.d.ts +0 -21
- package/dist/synth/evaluate.js +56 -336
- package/dist/synth/index.d.ts +0 -5
- package/dist/synth/index.js +0 -5
- package/dist/synth/permit-context.d.ts +2 -9
- package/dist/synth/permit-context.js +1 -82
- package/dist/synth/synthesize-from-recording.d.ts +1 -29
- package/dist/synth/synthesize-from-recording.js +41 -356
- package/dist/types.d.ts +8 -60
- package/dist/types.js +5 -2
- package/dist/verify/simulate.d.ts +1 -14
- package/dist/verify/simulate.js +2 -88
- package/dist/verify/verify.d.ts +1 -7
- package/dist/verify/verify.js +0 -8
- package/dist-cjs/adapters/interpreter/adapter.d.ts +19 -25
- package/dist-cjs/adapters/interpreter/adapter.js +56 -489
- package/dist-cjs/adapters/interpreter/index.d.ts +1 -1
- package/dist-cjs/adapters/interpreter/index.js +1 -2
- package/dist-cjs/adapters/oz/adapter.js +1 -15
- package/dist-cjs/errors.d.ts +1 -1
- package/dist-cjs/index.d.ts +1 -4
- package/dist-cjs/index.js +1 -4
- package/dist-cjs/install/build-add-context-rule.d.ts +3 -19
- package/dist-cjs/install/build-add-context-rule.js +1 -97
- package/dist-cjs/install/build-install-policy.d.ts +4 -53
- package/dist-cjs/install/build-install-policy.js +44 -122
- package/dist-cjs/install/index.d.ts +0 -2
- package/dist-cjs/install/index.js +2 -23
- package/dist-cjs/ir/index.d.ts +1 -1
- package/dist-cjs/ir/types.d.ts +7 -76
- package/dist-cjs/ir/types.js +0 -2
- package/dist-cjs/predicate/decode.d.ts +1 -1
- package/dist-cjs/predicate/decode.js +2 -70
- package/dist-cjs/predicate/encode.js +45 -229
- package/dist-cjs/predicate/from-json.js +1 -42
- package/dist-cjs/record/decode.js +3 -8
- package/dist-cjs/review-card/builder.d.ts +4 -3
- package/dist-cjs/review-card/builder.js +23 -167
- package/dist-cjs/review-card/cross-check.js +7 -110
- package/dist-cjs/review-card/render-leaf.d.ts +4 -0
- package/dist-cjs/review-card/render-leaf.js +52 -0
- package/dist-cjs/run/index.d.ts +36 -139
- package/dist-cjs/run/index.js +125 -444
- package/dist-cjs/run/schemas.d.ts +168 -1929
- package/dist-cjs/run/schemas.js +41 -270
- package/dist-cjs/seams/types.d.ts +0 -2
- package/dist-cjs/simulate/deny-cases.d.ts +26 -0
- package/dist-cjs/simulate/deny-cases.js +286 -0
- package/dist-cjs/simulate/evaluate.d.ts +18 -0
- package/dist-cjs/simulate/evaluate.js +274 -0
- package/dist-cjs/simulate/index.d.ts +4 -0
- package/dist-cjs/simulate/index.js +13 -0
- package/dist-cjs/synth/compose-from-recording.d.ts +24 -51
- package/dist-cjs/synth/compose-from-recording.js +120 -282
- package/dist-cjs/synth/deny-cases.d.ts +5 -7
- package/dist-cjs/synth/deny-cases.js +23 -374
- package/dist-cjs/synth/evaluate.d.ts +0 -21
- package/dist-cjs/synth/evaluate.js +57 -337
- package/dist-cjs/synth/index.d.ts +0 -5
- package/dist-cjs/synth/index.js +1 -11
- package/dist-cjs/synth/permit-context.d.ts +2 -9
- package/dist-cjs/synth/permit-context.js +1 -82
- package/dist-cjs/synth/synthesize-from-recording.d.ts +1 -29
- package/dist-cjs/synth/synthesize-from-recording.js +39 -354
- package/dist-cjs/types.d.ts +8 -60
- package/dist-cjs/types.js +6 -3
- package/dist-cjs/verify/simulate.d.ts +1 -14
- package/dist-cjs/verify/simulate.js +2 -88
- package/dist-cjs/verify/verify.d.ts +1 -7
- package/dist-cjs/verify/verify.js +0 -8
- package/package.json +4 -4
- package/src/adapters/interpreter/adapter.ts +76 -572
- package/src/errors.ts +0 -19
- package/src/index.ts +1 -4
- package/src/install/build-add-context-rule.ts +5 -139
- package/src/install/build-install-policy.ts +93 -214
- package/src/install/index.ts +0 -34
- package/src/predicate/decode.ts +2 -70
- package/src/predicate/encode.ts +49 -261
- package/src/predicate/from-json.ts +1 -43
- package/src/record/decode.ts +3 -8
- package/src/review-card/builder.ts +19 -168
- package/src/review-card/cross-check.ts +3 -105
- package/src/review-card/render-leaf.ts +48 -0
- package/src/run/index.ts +144 -572
- package/src/run/schemas.ts +42 -291
- package/src/simulate/deny-cases.ts +334 -0
- package/src/simulate/evaluate.ts +284 -0
- package/src/simulate/index.ts +11 -0
- package/src/synth/compose-from-recording.ts +148 -347
- package/src/synth/index.ts +0 -13
- package/src/synth/synthesize-from-recording.ts +43 -456
- package/src/types.ts +13 -49
- package/dist/install/authority-overlap.d.ts +0 -134
- package/dist/install/authority-overlap.js +0 -0
- package/dist/install/build-install-predicate.d.ts +0 -96
- package/dist/install/build-install-predicate.js +0 -444
- package/dist/install/build-merge-policy.d.ts +0 -70
- package/dist/install/build-merge-policy.js +0 -130
- package/dist/install/plan-merge-policy.d.ts +0 -49
- package/dist/install/plan-merge-policy.js +0 -86
- package/dist/install/read-account-rules.d.ts +0 -100
- package/dist/install/read-account-rules.js +0 -283
- package/dist-cjs/install/authority-overlap.d.ts +0 -134
- package/dist-cjs/install/authority-overlap.js +0 -0
- package/dist-cjs/install/build-install-predicate.d.ts +0 -96
- package/dist-cjs/install/build-install-predicate.js +0 -479
- package/dist-cjs/install/build-merge-policy.d.ts +0 -70
- package/dist-cjs/install/build-merge-policy.js +0 -134
- package/dist-cjs/install/plan-merge-policy.d.ts +0 -49
- package/dist-cjs/install/plan-merge-policy.js +0 -90
- package/dist-cjs/install/read-account-rules.d.ts +0 -100
- package/dist-cjs/install/read-account-rules.js +0 -296
- package/src/adapters/interpreter/index.ts +0 -8
- package/src/adapters/oz/adapter.ts +0 -376
- package/src/adapters/oz/index.ts +0 -9
- package/src/codegen/compile-gate.ts +0 -167
- package/src/codegen/index.ts +0 -17
- package/src/codegen/template.ts +0 -165
- package/src/install/authority-overlap.ts +0 -0
- package/src/install/build-merge-policy.ts +0 -219
- package/src/install/plan-merge-policy.ts +0 -133
- package/src/install/read-account-rules.ts +0 -376
- package/src/ir/index.ts +0 -13
- package/src/ir/types.ts +0 -132
- package/src/mandate/index.ts +0 -4
- package/src/mandate/to-ir.ts +0 -71
- package/src/mandate/types.ts +0 -21
- package/src/seams/index.ts +0 -11
- package/src/seams/types.ts +0 -81
- package/src/synth/deny-cases.ts +0 -663
- package/src/synth/evaluate.ts +0 -613
- package/src/synth/harness.ts +0 -68
- package/src/synth/minimize.ts +0 -48
- package/src/synth/permit-context.ts +0 -136
- package/src/synth/predicate-literals.ts +0 -27
- package/src/synth/synthesize-from-mandate.ts +0 -82
- package/src/verify/envelope.ts +0 -28
- package/src/verify/index.ts +0 -5
- package/src/verify/simulate.ts +0 -311
- package/src/verify/verify.ts +0 -243
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
// src/adapters/interpreter/adapter.ts - the interpreter-policy CustodyAdapter.
|
|
2
2
|
//
|
|
3
3
|
// Compiles a PolicyIR to a single interpreter `PolicyDocument` + `PolicyRef`
|
|
4
|
-
// carrying the canonical predicate encoding from `predicate/encode.ts`.
|
|
5
|
-
//
|
|
6
|
-
// IR constructs between them.
|
|
4
|
+
// carrying the canonical predicate encoding from `predicate/encode.ts`. This is
|
|
5
|
+
// the only backend; the compose step lowers every constraint to it.
|
|
7
6
|
//
|
|
8
7
|
// Three fail-closed enforcement gates (per spec):
|
|
9
|
-
// - oracle_price leaves MUST sit directly under the top-level `and`; nesting
|
|
10
|
-
// under `not` / `or` / inside `in` throws ORACLE_LEAF_INVALID_POSITION.
|
|
11
|
-
// - oracleParams overrides may only TIGHTEN vs wasm defaults
|
|
12
|
-
// (maxStalenessSeconds <= 600, maxDeviationBps <= 200); a widening value
|
|
13
|
-
// throws ORACLE_PARAMS_OUT_OF_RANGE at compile time.
|
|
14
8
|
// - an `in` allowlist (or a `compare eq` vs an address, or any value in an
|
|
15
9
|
// `eq_seq`) that targets the smart account's own address throws
|
|
16
10
|
// SCOPE_SELF_CALL.
|
|
@@ -19,43 +13,24 @@
|
|
|
19
13
|
// expiry, etc.) is named in `uncovered` rather than silently dropped.
|
|
20
14
|
|
|
21
15
|
import type { ToolError } from '../../errors.ts'
|
|
22
|
-
import type {
|
|
23
|
-
IRCompare,
|
|
24
|
-
IRCondition,
|
|
25
|
-
IRPolicyRule,
|
|
26
|
-
IRScalarType,
|
|
27
|
-
IRSelector,
|
|
28
|
-
PolicyIR,
|
|
29
|
-
} from '../../ir/types.ts'
|
|
30
16
|
import { encodePredicate } from '../../predicate/encode.ts'
|
|
31
|
-
import type {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
PolicyRef,
|
|
43
|
-
PredicateLeaf,
|
|
44
|
-
PredicateNode,
|
|
45
|
-
ProposedPolicy,
|
|
17
|
+
import type { ComposedRule } from '../../synth/compose-from-recording.ts'
|
|
18
|
+
import {
|
|
19
|
+
type ContextRuleDraft,
|
|
20
|
+
GRAMMAR_VERSION,
|
|
21
|
+
type Network,
|
|
22
|
+
type ParseConfidence,
|
|
23
|
+
type PolicyDocument,
|
|
24
|
+
type PolicyRef,
|
|
25
|
+
type PredicateLeaf,
|
|
26
|
+
type PredicateNode,
|
|
27
|
+
type ProposedPolicy,
|
|
46
28
|
} from '../../types.ts'
|
|
47
29
|
|
|
48
30
|
/** [VERIFY] NOT a real deployed address. The interpreter is a per-network
|
|
49
31
|
* deploy artifact we do not have yet; install is a later phase. */
|
|
50
32
|
export const PLACEHOLDER_INTERPRETER_ADDRESS = 'VERIFY-interpreter-address'
|
|
51
33
|
|
|
52
|
-
/** Wasm-level oracle defaults (mirrors the interpreter's `OracleParams`).
|
|
53
|
-
* Per-policy overrides may TIGHTEN only - they may never exceed these. */
|
|
54
|
-
export const ORACLE_DEFAULTS = {
|
|
55
|
-
maxStalenessSeconds: 600,
|
|
56
|
-
maxDeviationBps: 200,
|
|
57
|
-
} as const
|
|
58
|
-
|
|
59
34
|
export interface InterpreterAdapterConfig {
|
|
60
35
|
network: Network
|
|
61
36
|
/** Per-rule install nonce (first install = 1; replay-protected). */
|
|
@@ -64,25 +39,20 @@ export interface InterpreterAdapterConfig {
|
|
|
64
39
|
* by the self-call gate: any `in`-allowlist containing this address is
|
|
65
40
|
* rejected as `SCOPE_SELF_CALL`. */
|
|
66
41
|
smartAccountAddress: string
|
|
67
|
-
/** Optional per-policy oracle overrides. Must TIGHTEN vs ORACLE_DEFAULTS
|
|
68
|
-
* (i.e. <= each default). A widening value throws ORACLE_PARAMS_OUT_OF_RANGE
|
|
69
|
-
* at compile time. */
|
|
70
|
-
oracleParams?: { maxStalenessSeconds?: number; maxDeviationBps?: number }
|
|
71
42
|
}
|
|
72
43
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
supportsGeneralPredicate: true,
|
|
44
|
+
/** The result of compiling a PolicyIR for this backend. */
|
|
45
|
+
export interface CompileResult {
|
|
46
|
+
/** false => some IR construct this backend cannot express (see `uncovered`). */
|
|
47
|
+
covered: boolean
|
|
48
|
+
/** Human-readable list of unsupported constructs. */
|
|
49
|
+
uncovered: string[]
|
|
50
|
+
/** The installable policy, assembled when a rule lowered. */
|
|
51
|
+
proposed?: ProposedPolicy
|
|
82
52
|
}
|
|
83
53
|
|
|
84
|
-
/** Parse confidence for a deterministic (non-decoded) input: full (1.0).
|
|
85
|
-
*
|
|
54
|
+
/** Parse confidence for a deterministic (non-decoded) input: full (1.0). An
|
|
55
|
+
* input that needs no decoding has nothing for the gate to judge. */
|
|
86
56
|
const FULL_PARSE_CONFIDENCE: ParseConfidence = {
|
|
87
57
|
overall: 1,
|
|
88
58
|
knownContracts: [],
|
|
@@ -91,61 +61,34 @@ const FULL_PARSE_CONFIDENCE: ParseConfidence = {
|
|
|
91
61
|
thresholdUsed: 1,
|
|
92
62
|
}
|
|
93
63
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
compile: (ir) => compile(ir, config),
|
|
100
|
-
simulate: () => simulateStub(),
|
|
101
|
-
export: (ir) => canonicalStringify(ir),
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** Lower a single IR rule to the canonical pre-encoding `PredicateNode`. The
|
|
106
|
-
* orchestrator uses this to wire the self-verify + minimise pipeline: after
|
|
107
|
-
* `compile(ir)` succeeds it re-derives the PredicateNode via this helper to
|
|
108
|
-
* drive `minimize` and `runHarness` on the SAME shape the encoder saw. Pure
|
|
109
|
-
* and deterministic: same `rule + config` -> byte-identical PredicateNode.
|
|
110
|
-
* The `uncovered` list is NOT re-derived - callers needing it must use
|
|
111
|
-
* `compile(ir)`. */
|
|
64
|
+
/** Lower a single rule to the canonical pre-encoding `PredicateNode`. The
|
|
65
|
+
* orchestrator re-derives the PredicateNode through this helper so it holds the
|
|
66
|
+
* SAME shape the encoder saw. Pure and deterministic: same `rule + config` ->
|
|
67
|
+
* byte-identical PredicateNode. The `uncovered` list is NOT re-derived -
|
|
68
|
+
* callers needing it must use `compileInterpreterPolicy(rule, config)`. */
|
|
112
69
|
export function lowerRuleToPredicate(
|
|
113
|
-
rule:
|
|
70
|
+
rule: ComposedRule,
|
|
114
71
|
config: InterpreterAdapterConfig
|
|
115
72
|
): PredicateNode {
|
|
116
73
|
return lowerRule(rule, config).predicate
|
|
117
74
|
}
|
|
118
75
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
if (!firstRule) {
|
|
126
|
-
return { covered: false, uncovered: ['empty PolicyIR (no rules to compile)'] }
|
|
127
|
-
}
|
|
128
|
-
if (ir.rules.length > 1) {
|
|
129
|
-
return {
|
|
130
|
-
covered: false,
|
|
131
|
-
uncovered: [
|
|
132
|
-
`multi-rule PolicyIR: ${ir.rules.length - 1} rule(s) beyond the first are not compiled (the interpreter adapter handles one rule per document in this slice)`,
|
|
133
|
-
],
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const lowered = lowerRule(firstRule, config)
|
|
76
|
+
/** Compile a composed rule to an installable interpreter policy. */
|
|
77
|
+
export function compileInterpreterPolicy(
|
|
78
|
+
rule: ComposedRule,
|
|
79
|
+
config: InterpreterAdapterConfig
|
|
80
|
+
): CompileResult {
|
|
81
|
+
const lowered = lowerRule(rule, config)
|
|
138
82
|
if (lowered.uncovered.length > 0) {
|
|
139
83
|
return { covered: false, uncovered: lowered.uncovered }
|
|
140
84
|
}
|
|
141
85
|
|
|
142
86
|
const { encodedPredicate, predicateHash } = encodePredicate(lowered.predicate)
|
|
143
87
|
const policyDocument: PolicyDocument = {
|
|
144
|
-
grammarVersion:
|
|
88
|
+
grammarVersion: GRAMMAR_VERSION,
|
|
145
89
|
installNonce: config.installNonce,
|
|
146
90
|
encodedPredicate,
|
|
147
91
|
predicateHash,
|
|
148
|
-
...(config.oracleParams ? { oracleParams: config.oracleParams } : {}),
|
|
149
92
|
}
|
|
150
93
|
const policyRef: PolicyRef = {
|
|
151
94
|
kind: 'interpreter',
|
|
@@ -169,75 +112,24 @@ interface LoweredRule {
|
|
|
169
112
|
uncovered: string[]
|
|
170
113
|
}
|
|
171
114
|
|
|
172
|
-
function lowerRule(rule:
|
|
115
|
+
function lowerRule(rule: ComposedRule, config: InterpreterAdapterConfig): LoweredRule {
|
|
173
116
|
const uncovered: string[] = []
|
|
174
117
|
|
|
175
118
|
// scope -> context rule + sibling predicates. contract/method each become
|
|
176
119
|
// their own `eq` leaf and are merged into the top-level `and` alongside the
|
|
177
|
-
// constraints. The top-level MUST be `and`
|
|
178
|
-
// under it (mandatory per the oracle position rule + canonical hash stability).
|
|
120
|
+
// constraints. The top-level MUST be `and` for canonical hash stability.
|
|
179
121
|
const scopeContract: string | undefined = rule.scope.contract
|
|
180
122
|
const scopeMethod: string | undefined = rule.scope.method
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
)
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (rule.roles.length > 0) {
|
|
188
|
-
uncovered.push(
|
|
189
|
-
`roles [${rule.roles.join(', ')}] dropped (role-to-signer mapping is a later phase; interpreter policies reference addresses, not role names)`
|
|
190
|
-
)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (rule.guard) {
|
|
194
|
-
uncovered.push(
|
|
195
|
-
`guard: ${describeCondition(rule.guard)} (interpreter adapter currently treats guards as uncovered)`
|
|
196
|
-
)
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// expiry -> context rule validUntilLedger only. Unix-timestamp expiry is
|
|
200
|
-
// honoured by the interpreter (the `valid_until` selector), but we surface
|
|
201
|
-
// it here as Path-B because the IR contract already says "OZ context rules
|
|
202
|
-
// expire by ledger sequence" and we keep one expiry model for both adapters.
|
|
203
|
-
let validUntilLedger: number | null = null
|
|
204
|
-
if (rule.expiry) {
|
|
205
|
-
if (rule.expiry.validUntilLedger !== undefined) {
|
|
206
|
-
validUntilLedger = rule.expiry.validUntilLedger
|
|
207
|
-
} else if (rule.expiry.validUntilUnixSeconds !== undefined) {
|
|
208
|
-
uncovered.push(
|
|
209
|
-
'time expiry given as a unix timestamp (interpreter adapter currently lowers expiry only via the OZ context-rule validUntilLedger; supply expiry.validUntilLedger)'
|
|
210
|
-
)
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// approval is the OZ adapter's job (it lowers to simple_threshold /
|
|
215
|
-
// weighted_threshold primitives). Surface it here so a caller that drops it
|
|
216
|
-
// through to the interpreter adapter sees an explicit uncovered entry
|
|
217
|
-
// instead of a silently-dropped M-of-N gate.
|
|
218
|
-
if (rule.approval) {
|
|
219
|
-
uncovered.push(
|
|
220
|
-
`approval threshold ${rule.approval.threshold} not emitted by the interpreter adapter (thresholds lower to OZ built-in primitives in the OZ adapter)`
|
|
221
|
-
)
|
|
222
|
-
}
|
|
123
|
+
// expiry -> the context rule's own validUntilLedger. The interpreter has no
|
|
124
|
+
// expiry selector; a policy's lifetime is the OZ context rule's lifetime.
|
|
125
|
+
const validUntilLedger: number | null = rule.expiry?.validUntilLedger ?? null
|
|
223
126
|
|
|
224
127
|
// Pre-scan constraints: anything the interpreter adapter cannot express is
|
|
225
128
|
// named in `uncovered` and skipped from the predicate lowering. We surface
|
|
226
129
|
// these BEFORE lowering so the predicate is built from only the
|
|
227
130
|
// expressible subset.
|
|
228
|
-
const expressibleConstraints: IRCondition[] = []
|
|
229
|
-
for (const c of rule.constraints) {
|
|
230
|
-
const unsupp = unsupportedConstruct(c)
|
|
231
|
-
if (unsupp !== null) {
|
|
232
|
-
uncovered.push(unsupp)
|
|
233
|
-
} else {
|
|
234
|
-
expressibleConstraints.push(c)
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
131
|
// Build the top-level `and` of scope + expressible constraints. The
|
|
239
|
-
// top-level MUST be `and`
|
|
240
|
-
// position rule is enforced inside `lowerCondition`.
|
|
132
|
+
// top-level MUST be `and` for canonical hash stability.
|
|
241
133
|
const topChildren: PredicateNode[] = []
|
|
242
134
|
if (scopeContract !== undefined) {
|
|
243
135
|
topChildren.push({
|
|
@@ -253,8 +145,13 @@ function lowerRule(rule: IRPolicyRule, config: InterpreterAdapterConfig): Lowere
|
|
|
253
145
|
right: { kind: 'literal_symbol', value: scopeMethod },
|
|
254
146
|
})
|
|
255
147
|
}
|
|
256
|
-
|
|
257
|
-
|
|
148
|
+
topChildren.push(...rule.constraints)
|
|
149
|
+
|
|
150
|
+
// One walk over the whole tree rejects any literal naming the smart account
|
|
151
|
+
// itself. Previously three separate call sites did this per construct, which
|
|
152
|
+
// meant a new construct could be added without one.
|
|
153
|
+
for (const node of topChildren) {
|
|
154
|
+
assertNoSelfCall(node, config)
|
|
258
155
|
}
|
|
259
156
|
|
|
260
157
|
const predicate: PredicateNode =
|
|
@@ -262,8 +159,14 @@ function lowerRule(rule: IRPolicyRule, config: InterpreterAdapterConfig): Lowere
|
|
|
262
159
|
? topChildren[0]
|
|
263
160
|
: { op: 'and', children: topChildren }
|
|
264
161
|
|
|
162
|
+
// Scope the context rule to the contract when the rule names one, matching
|
|
163
|
+
// what OZ's CallContract scoping did. A `default` rule would route EVERY
|
|
164
|
+
// call through this policy instead of only calls to the scoped contract.
|
|
265
165
|
const contextRule: ContextRuleDraft = {
|
|
266
|
-
contextRuleType:
|
|
166
|
+
contextRuleType:
|
|
167
|
+
scopeContract !== undefined
|
|
168
|
+
? { kind: 'call_contract', contract: scopeContract }
|
|
169
|
+
: { kind: 'default' },
|
|
267
170
|
name: 'interpreter',
|
|
268
171
|
validUntilLedger,
|
|
269
172
|
signers: [],
|
|
@@ -273,435 +176,36 @@ function lowerRule(rule: IRPolicyRule, config: InterpreterAdapterConfig): Lowere
|
|
|
273
176
|
return { predicate, contextRule, uncovered }
|
|
274
177
|
}
|
|
275
178
|
|
|
276
|
-
/**
|
|
277
|
-
*
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
// construct can be written off as uncovered - otherwise a subtree that
|
|
287
|
-
// is BOTH misplaced-oracle and unsourceable would be silently dropped
|
|
288
|
-
// instead of rejected.
|
|
289
|
-
assertNoOracleDescendants(cond)
|
|
290
|
-
return (
|
|
291
|
-
unsourceableSelector(cond.selector) ??
|
|
292
|
-
(cond.selector.kind === 'calldata' || cond.selector.kind === 'value'
|
|
293
|
-
? `EVM \`${cond.selector.kind}\` selector on allowlist (predicate DSL)`
|
|
294
|
-
: null)
|
|
295
|
-
)
|
|
296
|
-
case 'eq_seq':
|
|
297
|
-
return (
|
|
298
|
-
unsourceableSelector(cond.selector) ??
|
|
299
|
-
(cond.selector.kind === 'calldata' || cond.selector.kind === 'value'
|
|
300
|
-
? `EVM \`${cond.selector.kind}\` selector on ordered-sequence equality (predicate DSL)`
|
|
301
|
-
: null)
|
|
302
|
-
)
|
|
303
|
-
case 'compare': {
|
|
304
|
-
const s = cond.compare.selector
|
|
305
|
-
if (s.kind === 'calldata') return 'EVM calldata comparison (predicate DSL)'
|
|
306
|
-
if (s.kind === 'value') return 'tx.value comparison (predicate DSL)'
|
|
307
|
-
// The on-chain interpreter sees ONE authorized call - no
|
|
308
|
-
// `Context.sub_invocations` in v1 - so it cannot observe token
|
|
309
|
-
// movements. `amount` has no value to read, and `window_spent`
|
|
310
|
-
// accumulates BY that amount, so its counter would never move.
|
|
311
|
-
// Deriving either from the call payload would quietly swap "value
|
|
312
|
-
// actually moved" for "value the caller declared" - a weaker guarantee
|
|
313
|
-
// than the review card would be claiming.
|
|
314
|
-
//
|
|
315
|
-
// Rolling spend caps belong to the OZ `spending_limit` primitive (already
|
|
316
|
-
// audited, emitted by the OZ adapter). A per-call cap is expressible here
|
|
317
|
-
// as `arg_field`; bounding it with `invocation_count` gives an enforceable
|
|
318
|
-
// ceiling per window.
|
|
319
|
-
return unsourceableSelector(s)
|
|
320
|
-
}
|
|
321
|
-
// Recurse: a nested `and`/`or`/`not` must not smuggle a selector past the
|
|
322
|
-
// pre-scan, which only sees top-level constraints.
|
|
323
|
-
case 'and':
|
|
324
|
-
return cond.children.map(unsupportedConstruct).find((u) => u !== null) ?? null
|
|
325
|
-
case 'or':
|
|
326
|
-
assertNoOracleDescendants(cond)
|
|
327
|
-
return cond.children.map(unsupportedConstruct).find((u) => u !== null) ?? null
|
|
328
|
-
case 'not':
|
|
329
|
-
assertNoOracleDescendants(cond)
|
|
330
|
-
return unsupportedConstruct(cond.child)
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/** Selectors whose value the on-chain interpreter has no way to obtain. */
|
|
335
|
-
function unsourceableSelector(s: IRSelector): string | null {
|
|
336
|
-
if (s.kind === 'amount') {
|
|
337
|
-
return `per-call amount comparison on ${s.token} - the interpreter cannot observe token movements; express a per-call cap with arg_field, or a rolling cap with the OZ spending_limit primitive`
|
|
338
|
-
}
|
|
339
|
-
if (s.kind === 'window_spent') {
|
|
340
|
-
return `rolling spend cap on ${s.token} over ${s.windowSeconds}s - not enforceable by the interpreter; use the OZ spending_limit primitive, or bound per-call value with arg_field plus invocation_count`
|
|
341
|
-
}
|
|
342
|
-
return null
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/** Lower one IR condition to a PredicateNode. Enforces the oracle position
|
|
346
|
-
* rule (oracle_price leaves must sit directly under the top-level `and`) and
|
|
347
|
-
* the self-call rule (`in` allowlist / `compare eq` vs the smart account
|
|
348
|
-
* address -> SCOPE_SELF_CALL). */
|
|
349
|
-
function lowerCondition(cond: IRCondition, config: InterpreterAdapterConfig): PredicateNode {
|
|
350
|
-
switch (cond.op) {
|
|
351
|
-
// `out >= in * num/den`. The contract refuses den == 0 or a non-positive
|
|
352
|
-
// ratio at install, so a malformed floor fails loudly rather than
|
|
353
|
-
// silently inverting the comparison.
|
|
354
|
-
case 'slippage_floor':
|
|
355
|
-
return {
|
|
356
|
-
op: 'gte',
|
|
357
|
-
left: { kind: 'call_arg', index: cond.outArgIndex },
|
|
358
|
-
right: {
|
|
359
|
-
kind: 'call_arg_scaled',
|
|
360
|
-
index: cond.inArgIndex,
|
|
361
|
-
num: cond.num,
|
|
362
|
-
den: cond.den,
|
|
363
|
-
},
|
|
364
|
-
}
|
|
365
|
-
case 'and':
|
|
366
|
-
return { op: 'and', children: cond.children.map((c) => lowerCondition(c, config)) }
|
|
367
|
-
case 'or':
|
|
368
|
-
// `or` at this depth: oracle_price anywhere inside is invalid (would
|
|
369
|
-
// not be a direct child of the top-level `and`).
|
|
370
|
-
assertNoOracleDescendants(cond)
|
|
371
|
-
return { op: 'or', children: cond.children.map((c) => lowerCondition(c, config)) }
|
|
372
|
-
case 'not':
|
|
373
|
-
// `not` at this depth: oracle_price anywhere inside is invalid.
|
|
374
|
-
assertNoOracleDescendants(cond)
|
|
375
|
-
return { op: 'not', child: lowerCondition(cond.child, config) }
|
|
376
|
-
case 'in': {
|
|
377
|
-
assertNoOracleDescendants(cond)
|
|
378
|
-
for (const v of cond.values) {
|
|
379
|
-
assertNotSelfCallAddress(v, config)
|
|
380
|
-
}
|
|
381
|
-
// `in` is PURE set membership; the encoder sorts the haystack. An exact
|
|
382
|
-
// ordered sequence (e.g. swap hop path) is expressed as `eq_seq` and
|
|
383
|
-
// lowers to `eq(selector, literal_vec)` instead - never to `in` with an
|
|
384
|
-
// `ordered` flag.
|
|
385
|
-
return {
|
|
386
|
-
op: 'in',
|
|
387
|
-
needle: lowerSelector(cond.selector),
|
|
388
|
-
haystack: cond.values.map((v) => literalFromScalar(v, selectorScalarType(cond.selector))),
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
case 'eq_seq': {
|
|
392
|
-
assertNoOracleDescendants(cond)
|
|
393
|
-
for (const v of cond.values) {
|
|
394
|
-
assertNotSelfCallAddress(v, config)
|
|
395
|
-
}
|
|
396
|
-
// Exact ordered sequence equality: the right-hand side is a literal_vec
|
|
397
|
-
// whose element order is preserved verbatim by the encoder. `eq` does
|
|
398
|
-
// deep equality at evaluate time - this is the ONLY way to express an
|
|
399
|
-
// exact ordered sequence in the predicate grammar (the path of a swap).
|
|
400
|
-
return {
|
|
401
|
-
op: 'eq',
|
|
402
|
-
left: lowerSelector(cond.selector),
|
|
403
|
-
right: {
|
|
404
|
-
kind: 'literal_vec',
|
|
405
|
-
elements: cond.values.map((v) => literalFromScalar(v, selectorScalarType(cond.selector))),
|
|
406
|
-
},
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
case 'compare': {
|
|
410
|
-
// Self-call on an address eq.
|
|
411
|
-
if (
|
|
412
|
-
cond.compare.operator === 'eq' &&
|
|
413
|
-
cond.compare.selector.kind === 'arg' &&
|
|
414
|
-
cond.compare.selector.scalarType === 'address'
|
|
415
|
-
) {
|
|
416
|
-
assertNotSelfCallAddress(cond.compare.value, config)
|
|
417
|
-
}
|
|
418
|
-
return {
|
|
419
|
-
op: cond.compare.operator,
|
|
420
|
-
left: lowerSelector(cond.compare.selector),
|
|
421
|
-
right: literalFromIRCompare(cond.compare),
|
|
422
|
-
}
|
|
179
|
+
/** Reject a value that targets the smart account's own address (a self-call
|
|
180
|
+
* is a structural privilege-escalation hole the interpreter forbids). */
|
|
181
|
+
/** Reject any address literal anywhere in the predicate that names the smart
|
|
182
|
+
* account itself: a policy permitting the account to call itself would let an
|
|
183
|
+
* agent re-enter the guarded account and escape the scope the rule pins. */
|
|
184
|
+
function assertNoSelfCall(node: PredicateNode, config: InterpreterAdapterConfig): void {
|
|
185
|
+
const checkLeaf = (leaf: PredicateLeaf): void => {
|
|
186
|
+
if (leaf.kind === 'literal_vec') {
|
|
187
|
+
for (const e of leaf.elements) checkLeaf(e)
|
|
188
|
+
return
|
|
423
189
|
}
|
|
424
|
-
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
/** Lower an IR selector to the matching PredicateLeaf. */
|
|
428
|
-
function lowerSelector(s: IRSelector): PredicateLeaf {
|
|
429
|
-
switch (s.kind) {
|
|
430
|
-
case 'arg':
|
|
431
|
-
return { kind: 'call_arg', index: s.argIndex }
|
|
432
|
-
case 'arg_len':
|
|
433
|
-
return { kind: 'call_arg_len', index: s.argIndex }
|
|
434
|
-
case 'arg_field':
|
|
435
|
-
return { kind: 'call_arg_field', index: s.argIndex, element: s.element, field: s.field }
|
|
436
|
-
// `amount` / `window_spent` are filtered out by `unsupportedConstruct`
|
|
437
|
-
// before lowering - the interpreter cannot source either on chain.
|
|
438
|
-
// Reaching here means the pre-scan was bypassed; fail loudly rather than
|
|
439
|
-
// emit a leaf the contract will refuse.
|
|
440
|
-
case 'amount':
|
|
441
|
-
case 'window_spent':
|
|
442
|
-
throw new Error(
|
|
443
|
-
`interpreter adapter cannot lower \`${s.kind}\`: it should have been reported as uncovered`
|
|
444
|
-
)
|
|
445
|
-
case 'invocation_count':
|
|
446
|
-
return { kind: 'invocation_count_in_window', windowSecs: s.windowSeconds }
|
|
447
|
-
case 'now':
|
|
448
|
-
return { kind: 'now' }
|
|
449
|
-
case 'valid_until':
|
|
450
|
-
return { kind: 'valid_until' }
|
|
451
|
-
case 'oracle_price':
|
|
452
|
-
return { kind: 'oracle_price', asset: s.asset }
|
|
453
|
-
case 'calldata':
|
|
454
|
-
case 'value':
|
|
455
|
-
// Unreachable: the caller flagged these as Path-B before reaching here.
|
|
190
|
+
if (leaf.kind === 'literal_address' && leaf.value === config.smartAccountAddress) {
|
|
456
191
|
throw toolError(
|
|
457
|
-
'
|
|
458
|
-
`
|
|
459
|
-
)
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
/** Build a literal leaf from the right-hand side of an IRCompare, mapping the
|
|
464
|
-
* selector kind to the matching `literal_*` kind. The IR compare value is a
|
|
465
|
-
* raw string (i128-safe); the selector kind fixes the canonical wire type:
|
|
466
|
-
* - arg -> IR scalarType (set by the recorder/parser)
|
|
467
|
-
* - arg_len -> u32 (vec length is a small non-negative integer)
|
|
468
|
-
* - arg_field -> IR scalarType (the field's recorded type)
|
|
469
|
-
* - amount -> i128 (canonical Stellar token amount encoding)
|
|
470
|
-
* - window_spent -> i128 (canonical amount encoding)
|
|
471
|
-
* - oracle_price -> i128 (canonical price encoding)
|
|
472
|
-
* - invocation_count -> u32 (counts are small non-negative integers)
|
|
473
|
-
* - now / valid_until -> u64 (unix timestamps in seconds) */
|
|
474
|
-
function literalFromIRCompare(c: IRCompare): PredicateLeaf {
|
|
475
|
-
// An oracle comparand is not a bare literal: the interpreter refuses one,
|
|
476
|
-
// because assuming it shares the normalised 9-dp basis is what made a raw
|
|
477
|
-
// 14-dp threshold permit everything it was written to deny.
|
|
478
|
-
if (c.selector.kind === 'oracle_price') {
|
|
479
|
-
if (c.valueDecimals === undefined) {
|
|
480
|
-
throw toolError(
|
|
481
|
-
'ORACLE_THRESHOLD_BASIS_REQUIRED',
|
|
482
|
-
'An oracle price bound must declare the decimal basis of its threshold ' +
|
|
483
|
-
'(oracle prices normalise to 9 decimals).'
|
|
192
|
+
'SCOPE_SELF_CALL',
|
|
193
|
+
`value \`${leaf.value}\` is the smart account's own address (self-call in an allowlist / compare is rejected)`
|
|
484
194
|
)
|
|
485
195
|
}
|
|
486
|
-
return { kind: 'oracle_threshold', value: c.value, decimals: c.valueDecimals }
|
|
487
|
-
}
|
|
488
|
-
const scalarType: IRScalarType = selectorScalarType(c.selector)
|
|
489
|
-
return literalFromScalar(c.value, scalarType)
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
function literalScalarForSelector(kind: IRSelector['kind']): IRScalarType {
|
|
493
|
-
switch (kind) {
|
|
494
|
-
case 'amount':
|
|
495
|
-
case 'window_spent':
|
|
496
|
-
case 'oracle_price':
|
|
497
|
-
return 'i128'
|
|
498
|
-
case 'invocation_count':
|
|
499
|
-
case 'arg_len':
|
|
500
|
-
return 'u32'
|
|
501
|
-
case 'now':
|
|
502
|
-
case 'valid_until':
|
|
503
|
-
return 'u64'
|
|
504
|
-
case 'arg':
|
|
505
|
-
case 'arg_field':
|
|
506
|
-
case 'calldata':
|
|
507
|
-
case 'value':
|
|
508
|
-
// arg / arg_field -> caller handles scalarType; calldata/value -> unreachable (Path-B).
|
|
509
|
-
return 'i128'
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
/** Build a literal leaf from a raw string value + an IRScalarType hint. */
|
|
514
|
-
function literalFromScalar(value: string, scalarType: IRScalarType): PredicateLeaf {
|
|
515
|
-
switch (scalarType) {
|
|
516
|
-
case 'address':
|
|
517
|
-
return { kind: 'literal_address', value }
|
|
518
|
-
case 'i128':
|
|
519
|
-
case 'u128':
|
|
520
|
-
return { kind: 'literal_i128', value }
|
|
521
|
-
case 'u32':
|
|
522
|
-
return { kind: 'literal_u32', value: Number.parseInt(value, 10) }
|
|
523
|
-
case 'u64':
|
|
524
|
-
case 'i64':
|
|
525
|
-
return { kind: 'literal_u64', value }
|
|
526
|
-
case 'symbol':
|
|
527
|
-
return { kind: 'literal_symbol', value }
|
|
528
|
-
case 'bytes':
|
|
529
|
-
return { kind: 'literal_bytes', value }
|
|
530
|
-
case 'bool':
|
|
531
|
-
throw toolError(
|
|
532
|
-
'MALFORMED_PREDICATE',
|
|
533
|
-
`boolean literal not supported in v1 predicate grammar`
|
|
534
|
-
)
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
/** Scalar type of an IRSelector for the purpose of building literal leaves
|
|
539
|
-
* (the right-hand side of `eq` / elements of an `in` haystack / elements of
|
|
540
|
-
* a `literal_vec`). Mirrors `literalScalarForSelector` for OZ extensions and
|
|
541
|
-
* uses the selector's own `scalarType` for `arg` and `arg_field` selectors. */
|
|
542
|
-
function selectorScalarType(selector: IRSelector): IRScalarType {
|
|
543
|
-
if (selector.kind === 'arg') return selector.scalarType
|
|
544
|
-
if (selector.kind === 'arg_field') return selector.scalarType
|
|
545
|
-
return literalScalarForSelector(selector.kind)
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
/** Walk a condition tree and throw if any oracle_price leaf is found anywhere
|
|
549
|
-
* inside. Used by `lowerCondition` for `not`, `or`, and `in` - the three
|
|
550
|
-
* positions where oracle leaves would not be direct children of the
|
|
551
|
-
* top-level `and`. */
|
|
552
|
-
function assertNoOracleDescendants(node: IRCondition): void {
|
|
553
|
-
if (containsOracle(node)) {
|
|
554
|
-
throw toolError(
|
|
555
|
-
'ORACLE_LEAF_INVALID_POSITION',
|
|
556
|
-
'oracle_price leaves must sit directly under the top-level `and`'
|
|
557
|
-
)
|
|
558
196
|
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
function containsOracle(node: IRCondition): boolean {
|
|
562
197
|
switch (node.op) {
|
|
563
|
-
case 'slippage_floor':
|
|
564
|
-
return false
|
|
565
|
-
case 'compare':
|
|
566
|
-
return node.compare.selector.kind === 'oracle_price'
|
|
567
|
-
case 'in':
|
|
568
|
-
return node.selector.kind === 'oracle_price'
|
|
569
|
-
case 'eq_seq':
|
|
570
|
-
return node.selector.kind === 'oracle_price'
|
|
571
|
-
case 'not':
|
|
572
|
-
return containsOracle(node.child)
|
|
573
198
|
case 'and':
|
|
574
|
-
|
|
575
|
-
return
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
/** Reject a value that targets the smart account's own address (a self-call
|
|
580
|
-
* is a structural privilege-escalation hole the interpreter forbids). */
|
|
581
|
-
function assertNotSelfCallAddress(value: string, config: InterpreterAdapterConfig): void {
|
|
582
|
-
if (value === config.smartAccountAddress) {
|
|
583
|
-
throw toolError(
|
|
584
|
-
'SCOPE_SELF_CALL',
|
|
585
|
-
`value \`${value}\` is the smart account's own address (self-call in an allowlist / compare is rejected)`
|
|
586
|
-
)
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
/** Tighten-only oracle-params gate. Any widening value (above the wasm-level
|
|
591
|
-
* defaults) throws ORACLE_PARAMS_OUT_OF_RANGE at compile time. */
|
|
592
|
-
function assertOracleParamsTighten(params: InterpreterAdapterConfig['oracleParams']): void {
|
|
593
|
-
if (!params) return
|
|
594
|
-
if (
|
|
595
|
-
params.maxStalenessSeconds !== undefined &&
|
|
596
|
-
params.maxStalenessSeconds > ORACLE_DEFAULTS.maxStalenessSeconds
|
|
597
|
-
) {
|
|
598
|
-
throw toolError(
|
|
599
|
-
'ORACLE_PARAMS_OUT_OF_RANGE',
|
|
600
|
-
`maxStalenessSeconds ${params.maxStalenessSeconds} exceeds default ${ORACLE_DEFAULTS.maxStalenessSeconds}`
|
|
601
|
-
)
|
|
602
|
-
}
|
|
603
|
-
if (
|
|
604
|
-
params.maxDeviationBps !== undefined &&
|
|
605
|
-
params.maxDeviationBps > ORACLE_DEFAULTS.maxDeviationBps
|
|
606
|
-
) {
|
|
607
|
-
throw toolError(
|
|
608
|
-
'ORACLE_PARAMS_OUT_OF_RANGE',
|
|
609
|
-
`maxDeviationBps ${params.maxDeviationBps} exceeds default ${ORACLE_DEFAULTS.maxDeviationBps}`
|
|
610
|
-
)
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
/** Human-readable descriptor for a construct the interpreter adapter cannot
|
|
615
|
-
* express. Mirrors the OZ adapter's `describeCondition` for parity. */
|
|
616
|
-
function describeCondition(cond: IRCondition): string {
|
|
617
|
-
switch (cond.op) {
|
|
618
|
-
case 'slippage_floor':
|
|
619
|
-
return `slippage floor: arg[${cond.outArgIndex}] >= arg[${cond.inArgIndex}] * ${cond.num}/${cond.den}`
|
|
199
|
+
for (const c of node.children) assertNoSelfCall(c, config)
|
|
200
|
+
return
|
|
620
201
|
case 'in':
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
return
|
|
624
|
-
case 'not':
|
|
625
|
-
return 'negated condition (predicate DSL)'
|
|
626
|
-
case 'and':
|
|
627
|
-
case 'or':
|
|
628
|
-
return `nested ${cond.op} condition (predicate DSL)`
|
|
629
|
-
case 'compare': {
|
|
630
|
-
const s = cond.compare.selector
|
|
631
|
-
switch (s.kind) {
|
|
632
|
-
case 'oracle_price':
|
|
633
|
-
return `oracle price condition on ${s.asset}`
|
|
634
|
-
case 'invocation_count':
|
|
635
|
-
return `invocation-count window (${s.windowSeconds}s) condition`
|
|
636
|
-
case 'window_spent':
|
|
637
|
-
return `spend-window comparison with operator '${cond.compare.operator}'`
|
|
638
|
-
case 'amount':
|
|
639
|
-
return `per-call amount comparison on ${s.token}`
|
|
640
|
-
case 'arg':
|
|
641
|
-
return `argument comparison on arg ${s.argIndex}`
|
|
642
|
-
case 'arg_len':
|
|
643
|
-
return `vec length comparison on arg ${s.argIndex}`
|
|
644
|
-
case 'arg_field':
|
|
645
|
-
return `map field comparison on arg ${s.argIndex}.${s.field}`
|
|
646
|
-
case 'calldata':
|
|
647
|
-
return 'EVM calldata comparison'
|
|
648
|
-
case 'value':
|
|
649
|
-
return 'tx.value comparison'
|
|
650
|
-
case 'now':
|
|
651
|
-
case 'valid_until':
|
|
652
|
-
return 'time comparison'
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
function describeSelector(s: IRSelector): string {
|
|
659
|
-
switch (s.kind) {
|
|
660
|
-
case 'arg':
|
|
661
|
-
return `arg ${s.argIndex}`
|
|
662
|
-
case 'arg_len':
|
|
663
|
-
return `arg_len(${s.argIndex})`
|
|
664
|
-
case 'arg_field':
|
|
665
|
-
return `arg_field(${s.argIndex}, ${s.element}, ${s.field})`
|
|
666
|
-
case 'amount':
|
|
667
|
-
return `amount(${s.token})`
|
|
668
|
-
case 'window_spent':
|
|
669
|
-
return `window_spent(${s.token})`
|
|
670
|
-
case 'oracle_price':
|
|
671
|
-
return `oracle_price(${s.asset})`
|
|
672
|
-
case 'invocation_count':
|
|
673
|
-
return `invocation_count(${s.windowSeconds}s)`
|
|
674
|
-
case 'calldata':
|
|
675
|
-
return `calldata[${s.offset}:${s.offset + s.length}]`
|
|
202
|
+
// The needle is a selector; only the haystack carries literals.
|
|
203
|
+
for (const h of node.haystack) checkLeaf(h)
|
|
204
|
+
return
|
|
676
205
|
default:
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
function simulateStub(): SimulationResult {
|
|
682
|
-
return {
|
|
683
|
-
backend: 'ts-model',
|
|
684
|
-
permitted: null,
|
|
685
|
-
evaluations: [],
|
|
686
|
-
notes: ['stub: real permit/deny semantics wiring is a later phase'],
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
/** Canonical JSON with recursively sorted object keys (stable across runs). */
|
|
691
|
-
function canonicalStringify(value: unknown): string {
|
|
692
|
-
return JSON.stringify(sortKeys(value))
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
function sortKeys(value: unknown): unknown {
|
|
696
|
-
if (Array.isArray(value)) return value.map(sortKeys)
|
|
697
|
-
if (value && typeof value === 'object') {
|
|
698
|
-
const out: Record<string, unknown> = {}
|
|
699
|
-
for (const key of Object.keys(value as Record<string, unknown>).sort()) {
|
|
700
|
-
out[key] = sortKeys((value as Record<string, unknown>)[key])
|
|
701
|
-
}
|
|
702
|
-
return out
|
|
206
|
+
checkLeaf(node.left)
|
|
207
|
+
checkLeaf(node.right)
|
|
703
208
|
}
|
|
704
|
-
return value
|
|
705
209
|
}
|
|
706
210
|
|
|
707
211
|
/** Build a synthetic Error carrying the ToolError code/severity/retryable
|