@crediolabs/policy-synth 0.2.0 → 0.3.1
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/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/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/record/freshness.d.ts +17 -11
- package/dist/record/freshness.js +28 -18
- 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/{synth → simulate}/deny-cases.d.ts +4 -8
- 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/index.d.ts +0 -5
- package/dist/synth/index.js +0 -5
- 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-cjs/adapters/interpreter/adapter.d.ts +19 -25
- package/dist-cjs/adapters/interpreter/adapter.js +56 -489
- 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/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/record/freshness.d.ts +17 -11
- package/dist-cjs/record/freshness.js +28 -18
- 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/{synth → simulate}/deny-cases.d.ts +4 -8
- 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/index.d.ts +0 -5
- package/dist-cjs/synth/index.js +1 -11
- 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/package.json +5 -5
- 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/record/freshness.ts +29 -18
- 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/adapters/interpreter/index.d.ts +0 -1
- package/dist/adapters/interpreter/index.js +0 -2
- package/dist/adapters/oz/adapter.d.ts +0 -20
- package/dist/adapters/oz/adapter.js +0 -295
- package/dist/adapters/oz/index.d.ts +0 -1
- package/dist/adapters/oz/index.js +0 -2
- package/dist/codegen/compile-gate.d.ts +0 -33
- package/dist/codegen/compile-gate.js +0 -124
- package/dist/codegen/index.d.ts +0 -2
- package/dist/codegen/index.js +0 -8
- package/dist/codegen/template.d.ts +0 -18
- package/dist/codegen/template.js +0 -148
- 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/ir/index.d.ts +0 -1
- package/dist/ir/index.js +0 -2
- package/dist/ir/types.d.ts +0 -140
- package/dist/ir/types.js +0 -11
- package/dist/mandate/index.d.ts +0 -2
- package/dist/mandate/index.js +0 -2
- package/dist/mandate/to-ir.d.ts +0 -3
- package/dist/mandate/to-ir.js +0 -60
- package/dist/mandate/types.d.ts +0 -20
- package/dist/mandate/types.js +0 -8
- package/dist/seams/index.d.ts +0 -1
- package/dist/seams/index.js +0 -2
- package/dist/seams/types.d.ts +0 -66
- package/dist/seams/types.js +0 -11
- package/dist/synth/deny-cases.js +0 -562
- package/dist/synth/evaluate.d.ts +0 -39
- package/dist/synth/evaluate.js +0 -551
- package/dist/synth/harness.d.ts +0 -28
- package/dist/synth/harness.js +0 -47
- package/dist/synth/minimize.d.ts +0 -4
- package/dist/synth/minimize.js +0 -38
- package/dist/synth/permit-context.d.ts +0 -15
- package/dist/synth/permit-context.js +0 -116
- package/dist/synth/predicate-literals.d.ts +0 -5
- package/dist/synth/predicate-literals.js +0 -25
- package/dist/synth/synthesize-from-mandate.d.ts +0 -20
- package/dist/synth/synthesize-from-mandate.js +0 -59
- package/dist/verify/envelope.d.ts +0 -15
- package/dist/verify/envelope.js +0 -22
- package/dist/verify/index.d.ts +0 -3
- package/dist/verify/index.js +0 -3
- package/dist/verify/simulate.d.ts +0 -31
- package/dist/verify/simulate.js +0 -258
- package/dist/verify/verify.d.ts +0 -21
- package/dist/verify/verify.js +0 -189
- package/dist-cjs/adapters/interpreter/index.d.ts +0 -1
- package/dist-cjs/adapters/interpreter/index.js +0 -8
- package/dist-cjs/adapters/oz/adapter.d.ts +0 -20
- package/dist-cjs/adapters/oz/adapter.js +0 -300
- package/dist-cjs/adapters/oz/index.d.ts +0 -1
- package/dist-cjs/adapters/oz/index.js +0 -8
- package/dist-cjs/codegen/compile-gate.d.ts +0 -33
- package/dist-cjs/codegen/compile-gate.js +0 -128
- package/dist-cjs/codegen/index.d.ts +0 -2
- package/dist-cjs/codegen/index.js +0 -14
- package/dist-cjs/codegen/template.d.ts +0 -18
- package/dist-cjs/codegen/template.js +0 -151
- 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/dist-cjs/ir/index.d.ts +0 -1
- package/dist-cjs/ir/index.js +0 -3
- package/dist-cjs/ir/types.d.ts +0 -140
- package/dist-cjs/ir/types.js +0 -12
- package/dist-cjs/mandate/index.d.ts +0 -2
- package/dist-cjs/mandate/index.js +0 -6
- package/dist-cjs/mandate/to-ir.d.ts +0 -3
- package/dist-cjs/mandate/to-ir.js +0 -63
- package/dist-cjs/mandate/types.d.ts +0 -20
- package/dist-cjs/mandate/types.js +0 -9
- package/dist-cjs/seams/index.d.ts +0 -1
- package/dist-cjs/seams/index.js +0 -3
- package/dist-cjs/seams/types.d.ts +0 -66
- package/dist-cjs/seams/types.js +0 -12
- package/dist-cjs/synth/deny-cases.js +0 -568
- package/dist-cjs/synth/evaluate.d.ts +0 -39
- package/dist-cjs/synth/evaluate.js +0 -554
- package/dist-cjs/synth/harness.d.ts +0 -28
- package/dist-cjs/synth/harness.js +0 -50
- package/dist-cjs/synth/minimize.d.ts +0 -4
- package/dist-cjs/synth/minimize.js +0 -41
- package/dist-cjs/synth/permit-context.d.ts +0 -15
- package/dist-cjs/synth/permit-context.js +0 -119
- package/dist-cjs/synth/predicate-literals.d.ts +0 -5
- package/dist-cjs/synth/predicate-literals.js +0 -28
- package/dist-cjs/synth/synthesize-from-mandate.d.ts +0 -20
- package/dist-cjs/synth/synthesize-from-mandate.js +0 -62
- package/dist-cjs/verify/envelope.d.ts +0 -15
- package/dist-cjs/verify/envelope.js +0 -23
- package/dist-cjs/verify/index.d.ts +0 -3
- package/dist-cjs/verify/index.js +0 -8
- package/dist-cjs/verify/simulate.d.ts +0 -31
- package/dist-cjs/verify/simulate.js +0 -261
- package/dist-cjs/verify/verify.d.ts +0 -21
- package/dist-cjs/verify/verify.js +0 -192
- 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,85 +1,36 @@
|
|
|
1
|
-
// src/synth/compose-from-recording.ts - facts + scope ->
|
|
1
|
+
// src/synth/compose-from-recording.ts - facts + scope -> a composed rule.
|
|
2
2
|
//
|
|
3
3
|
// Fail-closed composition rules:
|
|
4
4
|
// - unknown top-level protocol (registry.identifyProtocol returns null) ->
|
|
5
|
-
// emit no
|
|
6
|
-
//
|
|
7
|
-
// call never compiles to a permissive
|
|
8
|
-
// -
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// the
|
|
13
|
-
// (userResponses.windowSeconds). A single recorded spend does NOT authorise
|
|
14
|
-
// that amount every window, so the observed amount is NEVER used as an
|
|
15
|
-
// auto-ceiling: missing limit -> AMOUNT_BOUND_MISSING (observed amount
|
|
16
|
-
// offered as a suggestion); missing window -> DURATION_UNSPECIFIED. EVERY
|
|
5
|
+
// emit no constraint from the spend; scope is kept (contract + method) and
|
|
6
|
+
// every inferred bound surfaces as a descriptive warning. An unrecognised
|
|
7
|
+
// call never compiles to a permissive policy.
|
|
8
|
+
// - a spend cap is emitted ONLY when the caller supplies `limitAmount` AND
|
|
9
|
+
// the call carries an amount argument to bind it to. A single recorded
|
|
10
|
+
// spend does NOT authorise that amount on every call, so the observed
|
|
11
|
+
// amount is NEVER used as an auto-ceiling: without both,
|
|
12
|
+
// AMOUNT_BOUND_MISSING carries the observed amount as a suggestion. EVERY
|
|
17
13
|
// spent token is handled - none is silently dropped.
|
|
18
|
-
// -
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
// - the IR carries ONLY constraints justified by the recording + explicit
|
|
22
|
-
// user input. Nothing invented: no oracle price fabricated from a slippage
|
|
23
|
-
// bound, no synthetic exact-path compare. Those needs surface as warnings.
|
|
24
|
-
//
|
|
25
|
-
// Split rule (P3 wiring): `ComposeResult` carries BOTH `ir` (OZ-shape) and
|
|
26
|
-
// `interpreterIr` (predicate-shape). Each constraint is routed to EXACTLY ONE
|
|
27
|
-
// adapter:
|
|
28
|
-
// - `window_spent(token, w) <= limit` where `token === scope.contract` and
|
|
29
|
-
// protocol is known -> `ir` (OZ lowers to spending_limit).
|
|
30
|
-
// - everything else (recipient allowlists, per-method scope.method,
|
|
31
|
-
// invocation_count, eq_seq swap paths, oracle_price, AND window_spent
|
|
32
|
-
// where token != scope.contract i.e. SoroSwap input-token cap) ->
|
|
33
|
-
// `interpreterIr`.
|
|
34
|
-
//
|
|
35
|
-
// This prevents the interpreter adapter from emitting a duplicate
|
|
36
|
-
// `window_spent` predicate leaf alongside an OZ `spending_limit` primitive
|
|
37
|
-
// covering the same spend semantic - the two adapters never overlap.
|
|
14
|
+
// - the IR carries ONLY constraints justified by the recording plus an
|
|
15
|
+
// explicit bound, no synthetic exact-path compare. Other needs surface as
|
|
16
|
+
// warnings.
|
|
38
17
|
//
|
|
39
18
|
// Default policy is `deny_all` (OZ context rules are deny-by-default).
|
|
40
19
|
|
|
41
|
-
import type { IRCompOp, IRCondition, IRPolicyRule, PolicyIR } from '../ir/types.ts'
|
|
42
20
|
import { type IdentifiedProtocol, identifyProtocol } from '../registry/identify.ts'
|
|
43
|
-
import
|
|
21
|
+
import { getAbi } from '../registry/protocols.ts'
|
|
22
|
+
import type { AmbiguityPrompt, ContractInvocation, Network, PredicateNode } from '../types.ts'
|
|
44
23
|
import type { IntentFacts } from './lower.ts'
|
|
45
24
|
|
|
46
|
-
/** Per-asset oracle-price bound supplied by the caller (e.g. swap allowed only
|
|
47
|
-
* if oracle_price(XLM) < 5.00 USDC). One entry per asset; the recorder never
|
|
48
|
-
* fabricates a price bound from a slippage value (different units). */
|
|
49
|
-
export interface OraclePriceBound {
|
|
50
|
-
asset: string
|
|
51
|
-
operator: IRCompOp
|
|
52
|
-
value: string
|
|
53
|
-
/** Decimal basis `value` is written on. REQUIRED: oracle prices normalise to
|
|
54
|
-
* 9 dp, and a threshold silently assumed to share that basis is what let a
|
|
55
|
-
* raw 14-dp bound permit everything. The author states it; we convert. */
|
|
56
|
-
decimals: number
|
|
57
|
-
}
|
|
58
|
-
|
|
59
25
|
/** Caller-supplied answers to the ambiguity prompts. Every numeric bound the
|
|
60
26
|
* synth might apply must come from here - the recording supplies observed
|
|
61
27
|
* amounts (offered only as suggestions), never authorised ceilings. */
|
|
62
28
|
export interface ComposeUserResponses {
|
|
63
|
-
/** Rolling window (seconds) for a spending_limit / invocation_count. */
|
|
64
|
-
windowSeconds?: number
|
|
65
29
|
/** OZ context-rule expiry (ledger sequence). */
|
|
66
30
|
validUntilLedger?: number
|
|
67
|
-
/** Per-
|
|
68
|
-
*
|
|
31
|
+
/** Per-call ceiling on the amount the call carries (i128 decimal string).
|
|
32
|
+
* Required to bound the amount argument; absent -> AMOUNT_BOUND_MISSING. */
|
|
69
33
|
limitAmount?: string
|
|
70
|
-
/** Max invocations per window for an incoming-only flow. Required to emit an
|
|
71
|
-
* invocation_count bound; absent -> FREQUENCY_BOUND_MISSING. */
|
|
72
|
-
invocationLimit?: number
|
|
73
|
-
/** Per-asset oracle-price bound(s). Each entry lowers to a single
|
|
74
|
-
* `oracle_price(asset) OP value` compare in the interpreter IR. */
|
|
75
|
-
oraclePriceBound?: OraclePriceBound[]
|
|
76
|
-
/** Minimum acceptable swap output per unit of input, as `num/den` (e.g.
|
|
77
|
-
* `{num:'95',den:'100'}` = accept losing at most 5%). REQUIRED to be
|
|
78
|
-
* supplied by the caller: never derived from the recording (the recorded
|
|
79
|
-
* in/out pair is a price at one moment, and freezing it as policy would
|
|
80
|
-
* deny ordinary trades as soon as the rate moves). Absent, no floor is
|
|
81
|
-
* emitted and the existing unbounded-output warning stands. */
|
|
82
|
-
swapMinOutRatio?: { num: string; den: string }
|
|
83
34
|
/** Recipient allowlist for a swap (call_arg[3] on SoroSwap's
|
|
84
35
|
* swap_exact_tokens_for_tokens). When supplied, REPLACES the default pin.
|
|
85
36
|
* Absent -> recipient is pinned to the recorded value (mirroring SEP-41)
|
|
@@ -92,31 +43,31 @@ export interface ComposeUserResponses {
|
|
|
92
43
|
export interface ComposeOptions {
|
|
93
44
|
network: Network
|
|
94
45
|
userResponses?: ComposeUserResponses
|
|
95
|
-
/** When true, constraints the OZ adapter cannot lower are routed to
|
|
96
|
-
* `interpreterIr` (the predicate-shape IR) so the orchestrator can compile
|
|
97
|
-
* them via the interpreter adapter. When false (the default for callers
|
|
98
|
-
* who have not opted in), every constraint goes to `ir` and the OZ
|
|
99
|
-
* adapter's `uncovered` machinery generates the descriptive warnings -
|
|
100
|
-
* today's behaviour. The orchestrator passes this flag through based on
|
|
101
|
-
* whether `opts.interpreter` was supplied. */
|
|
102
|
-
interpreterEnabled?: boolean
|
|
103
46
|
}
|
|
104
47
|
|
|
105
|
-
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
|
|
110
|
-
|
|
48
|
+
/** One composed rule: what the call must be scoped to, the constraints on it,
|
|
49
|
+
* and how long the resulting context rule lives. The constraints are already
|
|
50
|
+
* predicate nodes - there is one enforcement backend, so there is nothing to
|
|
51
|
+
* translate between. */
|
|
52
|
+
export interface ComposedRule {
|
|
53
|
+
scope: { contract?: string; method?: string }
|
|
54
|
+
constraints: PredicateNode[]
|
|
55
|
+
expiry?: { validUntilLedger?: number }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Result of composition: the composed rule the interpreter compiler
|
|
59
|
+
* compiles, the ambiguities the caller must answer, and descriptive warnings
|
|
60
|
+
* for needs that are NOT expressed as an IR node (so no fabricated constraint
|
|
61
|
+
* is emitted). The orchestrator carries ambiguities into
|
|
62
|
+
* `ProposedPolicy.ambiguities` and merges warnings into
|
|
111
63
|
* `ProposedPolicy.warnings`. */
|
|
112
64
|
export interface ComposeResult {
|
|
113
|
-
|
|
114
|
-
interpreterIr: PolicyIR
|
|
65
|
+
interpreterRule: ComposedRule
|
|
115
66
|
ambiguities: AmbiguityPrompt[]
|
|
116
67
|
warnings: string[]
|
|
117
68
|
}
|
|
118
69
|
|
|
119
|
-
/** Compose a
|
|
70
|
+
/** Compose a rule from the lowered facts + the resolved scope.
|
|
120
71
|
* Pure (no randomness, no clock); same inputs -> byte-identical result. */
|
|
121
72
|
export function composeFromRecording(
|
|
122
73
|
facts: IntentFacts,
|
|
@@ -124,185 +75,106 @@ export function composeFromRecording(
|
|
|
124
75
|
topLevel: ContractInvocation | null,
|
|
125
76
|
opts: ComposeOptions
|
|
126
77
|
): ComposeResult {
|
|
127
|
-
const interpreterEnabled = opts.interpreterEnabled === true
|
|
128
78
|
const ambiguities: AmbiguityPrompt[] = []
|
|
129
79
|
const warnings: string[] = []
|
|
130
|
-
const
|
|
131
|
-
const interpreterConstraints: IRCondition[] = []
|
|
132
|
-
// Route to the matching IR. When the interpreter is enabled, constraints the
|
|
133
|
-
// OZ adapter cannot lower go there; otherwise they go to OZ (which flags them
|
|
134
|
-
// as uncovered) so today's warning-driven behaviour is preserved.
|
|
135
|
-
const routeToAdapter = (cond: IRCondition): void => {
|
|
136
|
-
if (interpreterEnabled) interpreterConstraints.push(cond)
|
|
137
|
-
else ozConstraints.push(cond)
|
|
138
|
-
}
|
|
80
|
+
const interpreterConstraints: PredicateNode[] = []
|
|
139
81
|
|
|
140
82
|
const protocol = topLevel
|
|
141
83
|
? identifyProtocol(topLevel.contract, topLevel.fn, topLevel.args, opts.network)
|
|
142
84
|
: null
|
|
143
85
|
const known = protocol !== null
|
|
144
86
|
|
|
145
|
-
const windowSeconds = opts.userResponses?.windowSeconds
|
|
146
87
|
const limitAmount = opts.userResponses?.limitAmount
|
|
147
88
|
const spendTokens = Object.keys(facts.spendByToken)
|
|
148
89
|
|
|
149
|
-
// Outgoing spend ->
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
// Routing: a `window_spent(token, w) <= limit` constraint goes to the OZ IR
|
|
155
|
-
// only when token === scope.contract (OZ's spending_limit binds the
|
|
156
|
-
// CallContract target, not a token parameter). Otherwise it goes to the
|
|
157
|
-
// interpreter IR.
|
|
90
|
+
// Outgoing spend -> a cap on the call's own amount ARGUMENT. The interpreter
|
|
91
|
+
// is passed one authorised call, not the transaction's token movements, so a
|
|
92
|
+
// rolling per-window total is not something it can read; the enforceable
|
|
93
|
+
// shape is a bound on the amount the call itself carries. A multi-token flow
|
|
94
|
+
// has no single argument to bind, so each token surfaces AMOUNT_BOUND_MISSING.
|
|
158
95
|
if (spendTokens.length > 0 && topLevel) {
|
|
159
|
-
let durationFlagged = false
|
|
160
96
|
for (const token of spendTokens) {
|
|
161
97
|
const observed = facts.spendByToken[token]
|
|
162
98
|
if (observed === undefined) continue
|
|
163
99
|
|
|
164
100
|
if (!known) {
|
|
165
101
|
warnings.push(
|
|
166
|
-
`spend of ${observed} (token ${token}) not bounded: unrecognised protocol,
|
|
102
|
+
`spend of ${observed} (token ${token}) not bounded: unrecognised protocol, so the amount argument cannot be located`
|
|
167
103
|
)
|
|
168
104
|
continue
|
|
169
105
|
}
|
|
170
106
|
|
|
171
107
|
const limit = spendTokens.length === 1 ? limitAmount : undefined
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
code: 'DURATION_UNSPECIFIED',
|
|
175
|
-
question: `Recording shows a ${observed}-${token} spend. What rolling window (seconds) should the spending_limit use?`,
|
|
176
|
-
})
|
|
177
|
-
durationFlagged = true
|
|
178
|
-
}
|
|
179
|
-
if (limit === undefined) {
|
|
108
|
+
const amountArgIndex = limitArgumentIndex(protocol, topLevel)
|
|
109
|
+
if (limit === undefined || amountArgIndex === null) {
|
|
180
110
|
ambiguities.push({
|
|
181
111
|
code: 'AMOUNT_BOUND_MISSING',
|
|
182
|
-
question: `Recording shows a ${observed}-${token} spend, but a single spend does not authorise that amount every
|
|
112
|
+
question: `Recording shows a ${observed}-${token} spend, but a single spend does not authorise that amount on every call. What per-call cap should apply? (observed amount, suggestion only: ${observed})`,
|
|
183
113
|
})
|
|
184
114
|
continue
|
|
185
115
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
// accumulate and the counter would never move. OZ reports the case it
|
|
192
|
-
// cannot cover (limit pins to the context contract) - the honest
|
|
193
|
-
// outcome; the old fallback produced an interpreter predicate that
|
|
194
|
-
// silently never bound.
|
|
195
|
-
ozConstraints.push({
|
|
196
|
-
op: 'compare',
|
|
197
|
-
compare: {
|
|
198
|
-
selector: { kind: 'window_spent', token, windowSeconds },
|
|
199
|
-
operator: 'lte',
|
|
200
|
-
value: limit,
|
|
201
|
-
},
|
|
202
|
-
})
|
|
203
|
-
if (interpreterEnabled && token !== scopeContract) {
|
|
204
|
-
warnings.push(
|
|
205
|
-
`rolling spend cap on ${token} cannot be enforced on chain: OZ spending_limit pins the limit to the context contract, and the interpreter cannot observe token movements. Bound the per-call value with an argument cap plus an invocation-count limit instead.`
|
|
206
|
-
)
|
|
207
|
-
}
|
|
208
|
-
}
|
|
116
|
+
interpreterConstraints.push({
|
|
117
|
+
op: 'lte',
|
|
118
|
+
left: { kind: 'call_arg', index: amountArgIndex },
|
|
119
|
+
right: { kind: 'literal_i128', value: limit },
|
|
120
|
+
})
|
|
209
121
|
}
|
|
210
122
|
}
|
|
211
123
|
|
|
212
|
-
// Incoming-only / frequency intent:
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
// invocation_count); otherwise to the OZ IR (which flags it as uncovered).
|
|
124
|
+
// Incoming-only / frequency intent: the interpreter is passed one authorised
|
|
125
|
+
// call, so it cannot count prior calls - no frequency bound is emitted and
|
|
126
|
+
// never a fabricated `<= 1`. The need surfaces as a prompt plus a warning.
|
|
216
127
|
//
|
|
217
128
|
// A recognised swap is NOT an incoming-only flow: it has an outgoing input
|
|
218
129
|
// leg whose spend simply was not attributed to the source account
|
|
219
130
|
// (fee-sponsored / holder != source). Its real restrictions - exact path,
|
|
220
131
|
// recipient, input-amount cap - come from the protocol-specific pass, so it
|
|
221
|
-
// does NOT get the incoming-only frequency prompt.
|
|
222
|
-
// rate-limit the swap can still supply an invocationLimit + window, which
|
|
223
|
-
// lowers to an invocation_count for any flow.
|
|
132
|
+
// does NOT get the incoming-only frequency prompt.
|
|
224
133
|
if (spendTokens.length === 0 && topLevel) {
|
|
225
|
-
const invocationLimit = opts.userResponses?.invocationLimit
|
|
226
134
|
const isRecognisedSwap = protocol?.protocol === 'soroswap'
|
|
227
|
-
if (
|
|
228
|
-
routeToAdapter({
|
|
229
|
-
op: 'compare',
|
|
230
|
-
compare: {
|
|
231
|
-
selector: { kind: 'invocation_count', windowSeconds },
|
|
232
|
-
// `lt`, not `lte`: the leaf reports the calls ALREADY made in the
|
|
233
|
-
// window, so `< N` is what permits N of them. With `lte` a limit
|
|
234
|
-
// of N let an N+1th call through.
|
|
235
|
-
operator: 'lt',
|
|
236
|
-
value: String(invocationLimit),
|
|
237
|
-
},
|
|
238
|
-
})
|
|
239
|
-
} else if (!isRecognisedSwap) {
|
|
135
|
+
if (!isRecognisedSwap) {
|
|
240
136
|
ambiguities.push({
|
|
241
137
|
code: 'FREQUENCY_BOUND_MISSING',
|
|
242
|
-
question:
|
|
138
|
+
question:
|
|
139
|
+
'Incoming-only flow - the policy does not bound how often this call may be made. Bound it outside the policy, or scope the rule more tightly.',
|
|
243
140
|
})
|
|
244
141
|
warnings.push(
|
|
245
|
-
'frequency
|
|
142
|
+
'incoming-only flow: call frequency is NOT bounded - the interpreter reads only the authorized call, so it cannot count prior calls'
|
|
246
143
|
)
|
|
247
144
|
}
|
|
248
145
|
}
|
|
249
146
|
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
if (oracleBounds) {
|
|
255
|
-
for (const b of oracleBounds) {
|
|
256
|
-
routeToAdapter({
|
|
257
|
-
op: 'compare',
|
|
258
|
-
compare: {
|
|
259
|
-
selector: { kind: 'oracle_price', asset: b.asset },
|
|
260
|
-
operator: b.operator,
|
|
261
|
-
value: b.value,
|
|
262
|
-
valueDecimals: b.decimals,
|
|
263
|
-
},
|
|
264
|
-
})
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Observed recipient allowlist (SEP-41) is a real, recorded constraint the OZ
|
|
269
|
-
// adapter flags as not covered; the interpreter adapter lowers it to an `in`
|
|
270
|
-
// predicate. Unknown protocols emit nothing here. SoroSwap's swap recipient
|
|
271
|
-
// (arg[3]) is the source-of-truth for the swapRecipientAllowlist surface.
|
|
147
|
+
// Observed recipient allowlist (SEP-41) is a real, recorded constraint the
|
|
148
|
+
// interpreter adapter lowers to an `in` predicate. Unknown protocols emit
|
|
149
|
+
// nothing here. SoroSwap's swap recipient (arg[3]) is the source-of-truth for
|
|
150
|
+
// the swapRecipientAllowlist surface.
|
|
272
151
|
if (topLevel && protocol !== null) {
|
|
273
|
-
// A SoroSwap input-amount cap binds the caller's limitAmount to
|
|
274
|
-
//
|
|
275
|
-
//
|
|
152
|
+
// A SoroSwap input-amount cap binds the caller's limitAmount to the swap's
|
|
153
|
+
// input-amount argument ONLY when no outgoing spend was detected for the
|
|
154
|
+
// source account - i.e. the input token never moved FROM the source
|
|
276
155
|
// (fee-sponsored swaps, or a holder != source). When a spend WAS detected,
|
|
277
|
-
// the
|
|
278
|
-
//
|
|
156
|
+
// the cap above already consumed the limit, so this one is skipped to avoid
|
|
157
|
+
// binding one limit to two different args.
|
|
279
158
|
const swapInputAmountCap = spendTokens.length === 0 ? limitAmount : undefined
|
|
280
159
|
appendProtocolSpecificConstraints(
|
|
281
160
|
interpreterConstraints,
|
|
282
|
-
routeToAdapter,
|
|
283
161
|
warnings,
|
|
284
162
|
ambiguities,
|
|
285
163
|
facts,
|
|
286
164
|
topLevel,
|
|
287
165
|
protocol,
|
|
288
166
|
opts.userResponses?.swapRecipientAllowlist,
|
|
289
|
-
swapInputAmountCap
|
|
290
|
-
opts.userResponses?.swapMinOutRatio,
|
|
291
|
-
interpreterEnabled
|
|
167
|
+
swapInputAmountCap
|
|
292
168
|
)
|
|
293
169
|
}
|
|
294
170
|
|
|
295
|
-
// `scope.method`
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
// flags it as uncovered (CallContract alone permits any method on the
|
|
299
|
-
// contract).
|
|
300
|
-
const scope: IRPolicyRule['scope'] = { contract: scopeContract }
|
|
171
|
+
// The interpreter adapter lowers `scope.method` into a `call_fn == <method>`
|
|
172
|
+
// predicate leaf.
|
|
173
|
+
const scope: ComposedRule['scope'] = { contract: scopeContract }
|
|
301
174
|
if (topLevel?.fn) scope.method = topLevel.fn
|
|
302
175
|
|
|
303
|
-
const buildRule = (constraints:
|
|
304
|
-
const rule:
|
|
305
|
-
roles: [],
|
|
176
|
+
const buildRule = (constraints: PredicateNode[]): ComposedRule => {
|
|
177
|
+
const rule: ComposedRule = {
|
|
306
178
|
scope: { ...scope },
|
|
307
179
|
constraints,
|
|
308
180
|
}
|
|
@@ -312,52 +184,58 @@ export function composeFromRecording(
|
|
|
312
184
|
return rule
|
|
313
185
|
}
|
|
314
186
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
187
|
+
return { interpreterRule: buildRule(interpreterConstraints), ambiguities, warnings }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Index of the argument a caller-supplied `limitAmount` binds to, or null when
|
|
191
|
+
* the call carries no such argument. The interpreter is passed one authorised
|
|
192
|
+
* call, not the transaction's token movements, so bounding an argument the
|
|
193
|
+
* call itself carries is the only enforceable shape. */
|
|
194
|
+
function limitArgumentIndex(
|
|
195
|
+
protocol: IdentifiedProtocol | null,
|
|
196
|
+
topLevel: ContractInvocation | null
|
|
197
|
+
): number | null {
|
|
198
|
+
if (!protocol || !topLevel) return null
|
|
199
|
+
const named = amountArgumentIndex(protocol)
|
|
200
|
+
if (named !== null) return named
|
|
201
|
+
// SoroSwap names its input argument `amount_in` / `amount_in_max`, so the
|
|
202
|
+
// lookup by name misses it and the index is function-specific.
|
|
203
|
+
if (protocol.protocol !== 'soroswap') return null
|
|
204
|
+
const i = soroswapInputAmountArgIndex(protocol.fn)
|
|
205
|
+
// Defense in depth: identification's argsMatchAbi already pins it to i128.
|
|
206
|
+
return i !== undefined && topLevel.args[i]?.type === 'i128' ? i : null
|
|
326
207
|
}
|
|
327
208
|
|
|
328
|
-
/**
|
|
329
|
-
*
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
209
|
+
/** Index of the argument the protocol ABI calls `amount`, or null when the ABI
|
|
210
|
+
* does not name one. */
|
|
211
|
+
function amountArgumentIndex(protocol: IdentifiedProtocol): number | null {
|
|
212
|
+
const abi = getAbi(protocol.protocol)[protocol.fn]
|
|
213
|
+
if (!abi) return null
|
|
214
|
+
const i = abi.args.findIndex((a) => a.name === 'amount')
|
|
215
|
+
return i >= 0 ? i : null
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Add the protocol-specific constraints the recording justifies: recipient
|
|
219
|
+
* allowlists, exact swap paths and per-call argument caps. */
|
|
337
220
|
function appendProtocolSpecificConstraints(
|
|
338
|
-
interpreterConstraints:
|
|
339
|
-
routeToAdapter: (cond: IRCondition) => void,
|
|
221
|
+
interpreterConstraints: PredicateNode[],
|
|
340
222
|
warnings: string[],
|
|
341
223
|
ambiguities: AmbiguityPrompt[],
|
|
342
224
|
facts: IntentFacts,
|
|
343
225
|
topLevel: ContractInvocation,
|
|
344
226
|
protocol: IdentifiedProtocol,
|
|
345
227
|
swapRecipientAllowlist: string[] | undefined,
|
|
346
|
-
swapInputAmountCap: string | undefined
|
|
347
|
-
swapMinOutRatio: { num: string; den: string } | undefined,
|
|
348
|
-
interpreterEnabled: boolean
|
|
228
|
+
swapInputAmountCap: string | undefined
|
|
349
229
|
): void {
|
|
350
230
|
// SEP-41 transfer / mint: the `to` arg (index 1) is the recipient. Emit a
|
|
351
231
|
// single-element allowlist; the interpreter adapter lowers it to `in`.
|
|
352
|
-
// When interpreter is not enabled, route to OZ so the caller sees today's
|
|
353
|
-
// `value allowlist on arg 1` warning.
|
|
354
232
|
if (protocol.protocol === 'sep41' && (protocol.fn === 'transfer' || protocol.fn === 'mint')) {
|
|
355
233
|
const toArg = topLevel.args[1]
|
|
356
234
|
if (toArg && toArg.type === 'address') {
|
|
357
|
-
|
|
235
|
+
interpreterConstraints.push({
|
|
358
236
|
op: 'in',
|
|
359
|
-
|
|
360
|
-
|
|
237
|
+
needle: { kind: 'call_arg', index: 1 },
|
|
238
|
+
haystack: [{ kind: 'literal_address', value: toArg.value }],
|
|
361
239
|
})
|
|
362
240
|
}
|
|
363
241
|
}
|
|
@@ -373,10 +251,10 @@ function appendProtocolSpecificConstraints(
|
|
|
373
251
|
// claim(from, reserve_token_ids, to).
|
|
374
252
|
const toArg = topLevel.args[2]
|
|
375
253
|
if (toArg && toArg.type === 'address') {
|
|
376
|
-
|
|
254
|
+
interpreterConstraints.push({
|
|
377
255
|
op: 'in',
|
|
378
|
-
|
|
379
|
-
|
|
256
|
+
needle: { kind: 'call_arg', index: 2 },
|
|
257
|
+
haystack: [{ kind: 'literal_address', value: toArg.value }],
|
|
380
258
|
})
|
|
381
259
|
}
|
|
382
260
|
}
|
|
@@ -391,17 +269,14 @@ function appendProtocolSpecificConstraints(
|
|
|
391
269
|
// -> Borrow on a different asset, any amount, then auction fills). Length
|
|
392
270
|
// + per-element pinning is total; a quantifier over elements is not. If we
|
|
393
271
|
// cannot emit BOTH, we surface AMBIGUITY rather than emit a partial bind.
|
|
394
|
-
if (protocol.protocol === 'blend' && protocol.fn === 'submit'
|
|
272
|
+
if (protocol.protocol === 'blend' && protocol.fn === 'submit') {
|
|
395
273
|
const requestsArg = topLevel.args[3]
|
|
396
274
|
if (requestsArg && requestsArg.type === 'vec') {
|
|
397
275
|
const elements = requestsArg.value
|
|
398
276
|
interpreterConstraints.push({
|
|
399
|
-
op: '
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
operator: 'eq',
|
|
403
|
-
value: String(elements.length),
|
|
404
|
-
},
|
|
277
|
+
op: 'eq',
|
|
278
|
+
left: { kind: 'call_arg_len', index: 3 },
|
|
279
|
+
right: { kind: 'literal_u32', value: elements.length },
|
|
405
280
|
})
|
|
406
281
|
for (let i = 0; i < elements.length; i++) {
|
|
407
282
|
const element = elements[i]
|
|
@@ -445,46 +320,19 @@ function appendProtocolSpecificConstraints(
|
|
|
445
320
|
continue
|
|
446
321
|
}
|
|
447
322
|
interpreterConstraints.push({
|
|
448
|
-
op: '
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
kind: 'arg_field',
|
|
452
|
-
argIndex: 3,
|
|
453
|
-
element: i,
|
|
454
|
-
field: 'request_type',
|
|
455
|
-
scalarType: 'u32',
|
|
456
|
-
},
|
|
457
|
-
operator: 'eq',
|
|
458
|
-
value: requestType,
|
|
459
|
-
},
|
|
323
|
+
op: 'eq',
|
|
324
|
+
left: { kind: 'call_arg_field', index: 3, element: i, field: 'request_type' },
|
|
325
|
+
right: { kind: 'literal_u32', value: Number.parseInt(requestType, 10) },
|
|
460
326
|
})
|
|
461
327
|
interpreterConstraints.push({
|
|
462
|
-
op: '
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
kind: 'arg_field',
|
|
466
|
-
argIndex: 3,
|
|
467
|
-
element: i,
|
|
468
|
-
field: 'address',
|
|
469
|
-
scalarType: 'address',
|
|
470
|
-
},
|
|
471
|
-
operator: 'eq',
|
|
472
|
-
value: address,
|
|
473
|
-
},
|
|
328
|
+
op: 'eq',
|
|
329
|
+
left: { kind: 'call_arg_field', index: 3, element: i, field: 'address' },
|
|
330
|
+
right: { kind: 'literal_address', value: address },
|
|
474
331
|
})
|
|
475
332
|
interpreterConstraints.push({
|
|
476
|
-
op: '
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
kind: 'arg_field',
|
|
480
|
-
argIndex: 3,
|
|
481
|
-
element: i,
|
|
482
|
-
field: 'amount',
|
|
483
|
-
scalarType: 'i128',
|
|
484
|
-
},
|
|
485
|
-
operator: 'lte',
|
|
486
|
-
value: amount,
|
|
487
|
-
},
|
|
333
|
+
op: 'lte',
|
|
334
|
+
left: { kind: 'call_arg_field', index: 3, element: i, field: 'amount' },
|
|
335
|
+
right: { kind: 'literal_i128', value: amount },
|
|
488
336
|
})
|
|
489
337
|
}
|
|
490
338
|
}
|
|
@@ -499,18 +347,19 @@ function appendProtocolSpecificConstraints(
|
|
|
499
347
|
if (protocol.protocol === 'soroswap') {
|
|
500
348
|
const paths = facts.allowedPaths?.[topLevel.contract]
|
|
501
349
|
const route = paths?.length === 1 ? paths[0] : undefined
|
|
502
|
-
if (route && route.length > 0
|
|
350
|
+
if (route && route.length > 0) {
|
|
503
351
|
const pathArgIndex = 2 // SoroSwap swap_exact_tokens_for_tokens: args = [amount_in, amount_out_min, path, to, deadline]
|
|
504
352
|
interpreterConstraints.push({
|
|
505
|
-
op: '
|
|
506
|
-
|
|
507
|
-
|
|
353
|
+
op: 'eq',
|
|
354
|
+
left: { kind: 'call_arg', index: pathArgIndex },
|
|
355
|
+
right: {
|
|
356
|
+
kind: 'literal_vec',
|
|
357
|
+
elements: route.map((v) => ({ kind: 'literal_address', value: v }) as const),
|
|
358
|
+
},
|
|
508
359
|
})
|
|
509
360
|
} else {
|
|
510
361
|
const pathText = route && route.length > 0 ? `; observed path: ${route.join(' -> ')}` : ''
|
|
511
|
-
warnings.push(
|
|
512
|
-
`SoroSwap swap: slippage / oracle price bound and exact hop path need the interpreter predicate${pathText}`
|
|
513
|
-
)
|
|
362
|
+
warnings.push(`SoroSwap swap: the exact hop path needs the interpreter predicate${pathText}`)
|
|
514
363
|
}
|
|
515
364
|
|
|
516
365
|
// Input-amount cap: bind the caller's limitAmount to the swap's input-amount
|
|
@@ -521,12 +370,10 @@ function appendProtocolSpecificConstraints(
|
|
|
521
370
|
// as arg[1] (its arg[0] is the exact OUTPUT, so binding arg[0] there would
|
|
522
371
|
// cap the wrong value and leave the input unbounded). This is a per-call cap
|
|
523
372
|
// that does NOT depend on attributing a token movement to the source account
|
|
524
|
-
// (the fee-sponsored / holder != source case, where
|
|
525
|
-
//
|
|
526
|
-
//
|
|
527
|
-
//
|
|
528
|
-
// it uncovered (a warning). The `type === 'i128'` check is defense in depth -
|
|
529
|
-
// protocol identification's argsMatchAbi already pins the input arg to i128.
|
|
373
|
+
// (the fee-sponsored / holder != source case, where no spend is detected).
|
|
374
|
+
// Fail-closed: it only ever restricts the permitted input. The
|
|
375
|
+
// `type === 'i128'` check is defense in depth - protocol identification's
|
|
376
|
+
// argsMatchAbi already pins the input arg to i128.
|
|
530
377
|
const inputArgIndex = soroswapInputAmountArgIndex(protocol.fn)
|
|
531
378
|
const inputAmountArg = inputArgIndex !== undefined ? topLevel.args[inputArgIndex] : undefined
|
|
532
379
|
if (
|
|
@@ -535,34 +382,10 @@ function appendProtocolSpecificConstraints(
|
|
|
535
382
|
inputAmountArg &&
|
|
536
383
|
inputAmountArg.type === 'i128'
|
|
537
384
|
) {
|
|
538
|
-
|
|
539
|
-
op: '
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
operator: 'lte',
|
|
543
|
-
value: swapInputAmountCap,
|
|
544
|
-
},
|
|
545
|
-
})
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
// Slippage floor: `out >= in * num/den`. Only when the caller supplied the
|
|
549
|
-
// ratio - see `swapMinOutRatio`. Without it the output arg stays free,
|
|
550
|
-
// which is the case the unbounded-swap warning above describes.
|
|
551
|
-
const outMinArgIndex = soroswapMinOutArgIndex(protocol.fn)
|
|
552
|
-
const minOutRatio = swapMinOutRatio
|
|
553
|
-
if (
|
|
554
|
-
minOutRatio !== undefined &&
|
|
555
|
-
inputArgIndex !== undefined &&
|
|
556
|
-
outMinArgIndex !== undefined &&
|
|
557
|
-
inputAmountArg &&
|
|
558
|
-
inputAmountArg.type === 'i128'
|
|
559
|
-
) {
|
|
560
|
-
routeToAdapter({
|
|
561
|
-
op: 'slippage_floor',
|
|
562
|
-
outArgIndex: outMinArgIndex,
|
|
563
|
-
inArgIndex: inputArgIndex,
|
|
564
|
-
num: minOutRatio.num,
|
|
565
|
-
den: minOutRatio.den,
|
|
385
|
+
interpreterConstraints.push({
|
|
386
|
+
op: 'lte',
|
|
387
|
+
left: { kind: 'call_arg', index: inputArgIndex },
|
|
388
|
+
right: { kind: 'literal_i128', value: swapInputAmountCap },
|
|
566
389
|
})
|
|
567
390
|
}
|
|
568
391
|
|
|
@@ -574,22 +397,21 @@ function appendProtocolSpecificConstraints(
|
|
|
574
397
|
// it unconstrained would permit an arbitrary recipient (an evil twin with
|
|
575
398
|
// call_arg[3] = attacker_wallet). RECIPIENT_ALLOWLIST_EMPTY is still
|
|
576
399
|
// surfaced, but as INFORMATIONAL (the recipient was pinned; here is how to
|
|
577
|
-
// widen it), never as a silent free pass.
|
|
578
|
-
// via the interpreter predicate, so the pin (and the ambiguity) apply only
|
|
579
|
-
// when the interpreter is enabled - otherwise the swap recipient is ignored
|
|
580
|
-
// (today's behaviour, matching the other SoroSwap constraints).
|
|
400
|
+
// widen it), never as a silent free pass.
|
|
581
401
|
const recipientArg = topLevel.args[3]
|
|
582
402
|
if (swapRecipientAllowlist && swapRecipientAllowlist.length > 0) {
|
|
583
|
-
|
|
403
|
+
interpreterConstraints.push({
|
|
584
404
|
op: 'in',
|
|
585
|
-
|
|
586
|
-
|
|
405
|
+
needle: { kind: 'call_arg', index: 3 },
|
|
406
|
+
haystack: swapRecipientAllowlist.map(
|
|
407
|
+
(v) => ({ kind: 'literal_address', value: v }) as const
|
|
408
|
+
),
|
|
587
409
|
})
|
|
588
|
-
} else if (
|
|
410
|
+
} else if (recipientArg && recipientArg.type === 'address') {
|
|
589
411
|
interpreterConstraints.push({
|
|
590
412
|
op: 'in',
|
|
591
|
-
|
|
592
|
-
|
|
413
|
+
needle: { kind: 'call_arg', index: 3 },
|
|
414
|
+
haystack: [{ kind: 'literal_address', value: recipientArg.value }],
|
|
593
415
|
})
|
|
594
416
|
ambiguities.push({
|
|
595
417
|
code: 'RECIPIENT_ALLOWLIST_EMPTY',
|
|
@@ -599,27 +421,6 @@ function appendProtocolSpecificConstraints(
|
|
|
599
421
|
}
|
|
600
422
|
}
|
|
601
423
|
|
|
602
|
-
/** Positional index of the input-amount argument for a recognized SoroSwap swap
|
|
603
|
-
* function. `swap_exact_tokens_for_tokens` and `swap_exact_in_for_tokens` take
|
|
604
|
-
* the exact input as arg[0]; `swap_tokens_for_exact_tokens` takes the maximum
|
|
605
|
-
* input (`amount_in_max`) as arg[1] - its arg[0] is the exact OUTPUT. Any other
|
|
606
|
-
* function has no positional input-amount argument -> undefined (no cap bound). */
|
|
607
|
-
/** The argument carrying the swap's MINIMUM ACCEPTABLE OUTPUT.
|
|
608
|
-
*
|
|
609
|
-
* Only the exact-input entrypoints have one: `swap_tokens_for_exact_tokens`
|
|
610
|
-
* fixes the output and varies the input, so its output needs no floor (its
|
|
611
|
-
* arg[1] is `amount_in_max`, already bounded by the input cap). Returning
|
|
612
|
-
* undefined there keeps a floor from being pinned to the wrong argument. */
|
|
613
|
-
function soroswapMinOutArgIndex(fn: string): number | undefined {
|
|
614
|
-
switch (fn) {
|
|
615
|
-
case 'swap_exact_tokens_for_tokens':
|
|
616
|
-
case 'swap_exact_in_for_tokens':
|
|
617
|
-
return 1
|
|
618
|
-
default:
|
|
619
|
-
return undefined
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
|
|
623
424
|
function soroswapInputAmountArgIndex(fn: string): number | undefined {
|
|
624
425
|
switch (fn) {
|
|
625
426
|
case 'swap_exact_tokens_for_tokens':
|