@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.
Files changed (194) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +193 -5
  3. package/dist/adapters/interpreter/adapter.d.ts +38 -0
  4. package/dist/adapters/interpreter/adapter.js +522 -0
  5. package/dist/adapters/interpreter/index.d.ts +1 -0
  6. package/dist/adapters/interpreter/index.js +2 -0
  7. package/dist/adapters/oz/adapter.js +2 -0
  8. package/dist/codegen/compile-gate.d.ts +33 -0
  9. package/dist/codegen/compile-gate.js +119 -0
  10. package/dist/codegen/index.d.ts +2 -0
  11. package/dist/codegen/index.js +8 -0
  12. package/dist/codegen/template.d.ts +18 -0
  13. package/dist/codegen/template.js +131 -0
  14. package/dist/errors.d.ts +1 -1
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.js +2 -0
  17. package/dist/ir/types.d.ts +13 -2
  18. package/dist/predicate/encode.d.ts +10 -0
  19. package/dist/predicate/encode.js +249 -0
  20. package/dist/predicate/index.d.ts +1 -0
  21. package/dist/predicate/index.js +2 -0
  22. package/dist/record/decode.d.ts +6 -3
  23. package/dist/record/decode.js +13 -3
  24. package/dist/review-card/builder.d.ts +14 -0
  25. package/dist/review-card/builder.js +261 -0
  26. package/dist/review-card/conflict.d.ts +40 -0
  27. package/dist/review-card/conflict.js +111 -0
  28. package/dist/review-card/cross-check.d.ts +11 -0
  29. package/dist/review-card/cross-check.js +148 -0
  30. package/dist/review-card/index.d.ts +3 -0
  31. package/dist/review-card/index.js +4 -0
  32. package/dist/run/index.d.ts +31 -0
  33. package/dist/run/index.js +164 -0
  34. package/dist/run/schemas.d.ts +2003 -0
  35. package/dist/run/schemas.js +215 -0
  36. package/dist/synth/compose-from-recording.d.ts +35 -7
  37. package/dist/synth/compose-from-recording.js +225 -34
  38. package/dist/synth/deny-cases.d.ts +12 -0
  39. package/dist/synth/deny-cases.js +377 -0
  40. package/dist/synth/evaluate.d.ts +39 -0
  41. package/dist/synth/evaluate.js +425 -0
  42. package/dist/synth/harness.d.ts +16 -0
  43. package/dist/synth/harness.js +26 -0
  44. package/dist/synth/index.d.ts +4 -0
  45. package/dist/synth/index.js +4 -0
  46. package/dist/synth/minimize.d.ts +4 -0
  47. package/dist/synth/minimize.js +38 -0
  48. package/dist/synth/predicate-literals.d.ts +5 -0
  49. package/dist/synth/predicate-literals.js +25 -0
  50. package/dist/synth/synthesize-from-recording.d.ts +37 -5
  51. package/dist/synth/synthesize-from-recording.js +550 -18
  52. package/dist/types.d.ts +40 -1
  53. package/dist/types.js +17 -0
  54. package/dist/verify/envelope.d.ts +15 -0
  55. package/dist/verify/envelope.js +22 -0
  56. package/dist/verify/index.d.ts +3 -0
  57. package/dist/verify/index.js +3 -0
  58. package/dist/verify/simulate.d.ts +31 -0
  59. package/dist/verify/simulate.js +258 -0
  60. package/dist/verify/verify.d.ts +21 -0
  61. package/dist/verify/verify.js +189 -0
  62. package/dist-cjs/adapters/interpreter/adapter.d.ts +38 -0
  63. package/dist-cjs/adapters/interpreter/adapter.js +527 -0
  64. package/dist-cjs/adapters/interpreter/index.d.ts +1 -0
  65. package/dist-cjs/adapters/interpreter/index.js +8 -0
  66. package/dist-cjs/adapters/oz/adapter.d.ts +20 -0
  67. package/dist-cjs/adapters/oz/adapter.js +289 -0
  68. package/dist-cjs/adapters/oz/index.d.ts +1 -0
  69. package/dist-cjs/adapters/oz/index.js +8 -0
  70. package/dist-cjs/codegen/compile-gate.d.ts +33 -0
  71. package/dist-cjs/codegen/compile-gate.js +123 -0
  72. package/dist-cjs/codegen/index.d.ts +2 -0
  73. package/dist-cjs/codegen/index.js +14 -0
  74. package/dist-cjs/codegen/template.d.ts +18 -0
  75. package/dist-cjs/codegen/template.js +134 -0
  76. package/dist-cjs/errors.d.ts +37 -0
  77. package/dist-cjs/errors.js +3 -0
  78. package/dist-cjs/index.d.ts +11 -0
  79. package/dist-cjs/index.js +27 -0
  80. package/dist-cjs/ir/index.d.ts +1 -0
  81. package/dist-cjs/ir/index.js +3 -0
  82. package/dist-cjs/ir/types.d.ts +108 -0
  83. package/dist-cjs/ir/types.js +12 -0
  84. package/dist-cjs/mandate/index.d.ts +2 -0
  85. package/dist-cjs/mandate/index.js +6 -0
  86. package/dist-cjs/mandate/to-ir.d.ts +3 -0
  87. package/dist-cjs/mandate/to-ir.js +63 -0
  88. package/dist-cjs/mandate/types.d.ts +20 -0
  89. package/dist-cjs/mandate/types.js +9 -0
  90. package/dist-cjs/package.json +3 -0
  91. package/dist-cjs/predicate/encode.d.ts +10 -0
  92. package/dist-cjs/predicate/encode.js +252 -0
  93. package/dist-cjs/predicate/index.d.ts +1 -0
  94. package/dist-cjs/predicate/index.js +6 -0
  95. package/dist-cjs/record/decode.d.ts +79 -0
  96. package/dist-cjs/record/decode.js +398 -0
  97. package/dist-cjs/record/freshness.d.ts +17 -0
  98. package/dist-cjs/record/freshness.js +55 -0
  99. package/dist-cjs/record/index.d.ts +21 -0
  100. package/dist-cjs/record/index.js +166 -0
  101. package/dist-cjs/record/movements.d.ts +20 -0
  102. package/dist-cjs/record/movements.js +192 -0
  103. package/dist-cjs/record/rpc.d.ts +22 -0
  104. package/dist-cjs/record/rpc.js +74 -0
  105. package/dist-cjs/record/validate.d.ts +22 -0
  106. package/dist-cjs/record/validate.js +63 -0
  107. package/dist-cjs/registry/identify.d.ts +11 -0
  108. package/dist-cjs/registry/identify.js +87 -0
  109. package/dist-cjs/registry/index.d.ts +3 -0
  110. package/dist-cjs/registry/index.js +15 -0
  111. package/dist-cjs/registry/known-addresses.d.ts +16 -0
  112. package/dist-cjs/registry/known-addresses.js +53 -0
  113. package/dist-cjs/registry/protocols.d.ts +38 -0
  114. package/dist-cjs/registry/protocols.js +153 -0
  115. package/dist-cjs/review-card/builder.d.ts +14 -0
  116. package/dist-cjs/review-card/builder.js +264 -0
  117. package/dist-cjs/review-card/conflict.d.ts +40 -0
  118. package/dist-cjs/review-card/conflict.js +114 -0
  119. package/dist-cjs/review-card/cross-check.d.ts +11 -0
  120. package/dist-cjs/review-card/cross-check.js +151 -0
  121. package/dist-cjs/review-card/index.d.ts +3 -0
  122. package/dist-cjs/review-card/index.js +10 -0
  123. package/dist-cjs/run/index.d.ts +31 -0
  124. package/dist-cjs/run/index.js +178 -0
  125. package/dist-cjs/run/schemas.d.ts +2003 -0
  126. package/dist-cjs/run/schemas.js +218 -0
  127. package/dist-cjs/seams/index.d.ts +1 -0
  128. package/dist-cjs/seams/index.js +3 -0
  129. package/dist-cjs/seams/types.d.ts +66 -0
  130. package/dist-cjs/seams/types.js +12 -0
  131. package/dist-cjs/synth/compose-from-recording.d.ts +64 -0
  132. package/dist-cjs/synth/compose-from-recording.js +356 -0
  133. package/dist-cjs/synth/deny-cases.d.ts +12 -0
  134. package/dist-cjs/synth/deny-cases.js +380 -0
  135. package/dist-cjs/synth/evaluate.d.ts +39 -0
  136. package/dist-cjs/synth/evaluate.js +428 -0
  137. package/dist-cjs/synth/harness.d.ts +16 -0
  138. package/dist-cjs/synth/harness.js +29 -0
  139. package/dist-cjs/synth/index.d.ts +9 -0
  140. package/dist-cjs/synth/index.js +23 -0
  141. package/dist-cjs/synth/lower.d.ts +23 -0
  142. package/dist-cjs/synth/lower.js +119 -0
  143. package/dist-cjs/synth/minimize.d.ts +4 -0
  144. package/dist-cjs/synth/minimize.js +41 -0
  145. package/dist-cjs/synth/predicate-literals.d.ts +5 -0
  146. package/dist-cjs/synth/predicate-literals.js +28 -0
  147. package/dist-cjs/synth/scope.d.ts +26 -0
  148. package/dist-cjs/synth/scope.js +81 -0
  149. package/dist-cjs/synth/synthesize-from-mandate.d.ts +5 -0
  150. package/dist-cjs/synth/synthesize-from-mandate.js +37 -0
  151. package/dist-cjs/synth/synthesize-from-recording.d.ts +49 -0
  152. package/dist-cjs/synth/synthesize-from-recording.js +711 -0
  153. package/dist-cjs/types.d.ts +288 -0
  154. package/dist-cjs/types.js +55 -0
  155. package/dist-cjs/verify/envelope.d.ts +15 -0
  156. package/dist-cjs/verify/envelope.js +23 -0
  157. package/dist-cjs/verify/index.d.ts +3 -0
  158. package/dist-cjs/verify/index.js +8 -0
  159. package/dist-cjs/verify/simulate.d.ts +31 -0
  160. package/dist-cjs/verify/simulate.js +261 -0
  161. package/dist-cjs/verify/verify.d.ts +21 -0
  162. package/dist-cjs/verify/verify.js +192 -0
  163. package/package.json +43 -3
  164. package/src/adapters/interpreter/adapter.ts +642 -0
  165. package/src/adapters/interpreter/index.ts +8 -0
  166. package/src/adapters/oz/adapter.ts +2 -0
  167. package/src/codegen/compile-gate.ts +162 -0
  168. package/src/codegen/index.ts +17 -0
  169. package/src/codegen/template.ts +148 -0
  170. package/src/errors.ts +2 -0
  171. package/src/index.ts +2 -0
  172. package/src/ir/types.ts +9 -2
  173. package/src/predicate/encode.ts +307 -0
  174. package/src/predicate/index.ts +3 -0
  175. package/src/record/decode.ts +21 -10
  176. package/src/review-card/builder.ts +303 -0
  177. package/src/review-card/conflict.ts +143 -0
  178. package/src/review-card/cross-check.ts +158 -0
  179. package/src/review-card/index.ts +12 -0
  180. package/src/run/index.ts +213 -0
  181. package/src/run/schemas.ts +242 -0
  182. package/src/synth/compose-from-recording.ts +277 -43
  183. package/src/synth/deny-cases.ts +466 -0
  184. package/src/synth/evaluate.ts +493 -0
  185. package/src/synth/harness.ts +40 -0
  186. package/src/synth/index.ts +12 -0
  187. package/src/synth/minimize.ts +44 -0
  188. package/src/synth/predicate-literals.ts +27 -0
  189. package/src/synth/synthesize-from-recording.ts +655 -19
  190. package/src/types.ts +38 -2
  191. package/src/verify/envelope.ts +28 -0
  192. package/src/verify/index.ts +5 -0
  193. package/src/verify/simulate.ts +311 -0
  194. package/src/verify/verify.ts +243 -0
@@ -0,0 +1,356 @@
1
+ "use strict";
2
+ // src/synth/compose-from-recording.ts - facts + scope -> PolicyIR (OZ-shape + interpreter-shape).
3
+ //
4
+ // Composes the canonical IR rules the two backends compile FROM. Fail-closed
5
+ // composition rules:
6
+ //
7
+ // - identify the protocol of the top-level call (registry.identifyProtocol).
8
+ // When it is unknown (null), emit NO OZ-primitive-producing IR node: the
9
+ // scope is kept (CallContract + method) and every inferred bound is surfaced
10
+ // as a descriptive warning. An unrecognised call never compiles to a
11
+ // permissive OZ primitive.
12
+ //
13
+ // - carry the recorded top-level function into `rule.scope.method` so the OZ
14
+ // adapter flags per-method scoping as not covered (CallContract permits every
15
+ // method on the contract; a per-method restriction needs the interpreter predicate).
16
+ //
17
+ // - a `spending_limit` (window_spent(token, w) <= limit) is emitted ONLY when
18
+ // the caller supplies BOTH the limit (userResponses.limitAmount) and the
19
+ // window (userResponses.windowSeconds). A single recorded spend does NOT
20
+ // authorise that amount every window, so the observed amount is NEVER used as
21
+ // an auto-ceiling: a missing limit surfaces AMOUNT_BOUND_MISSING (the observed
22
+ // amount is offered as a suggestion), a missing window surfaces
23
+ // DURATION_UNSPECIFIED. EVERY spent token is handled - none is silently
24
+ // dropped; a single caller limit binds only an unambiguous single-token spend.
25
+ //
26
+ // - incoming-only flows (e.g. Blend yield claim) emit an `invocation_count`
27
+ // bound ONLY when the caller supplies the count (userResponses.invocationLimit)
28
+ // and a window; otherwise FREQUENCY_BOUND_MISSING is surfaced with no
29
+ // fabricated count.
30
+ //
31
+ // - the IR carries ONLY constraints justified by the recording (observed
32
+ // recipient allowlist) + explicit user input. Nothing invented: no oracle
33
+ // price fabricated from a slippage bound, no synthetic exact-path compare.
34
+ // Those needs are surfaced as descriptive warnings instead.
35
+ //
36
+ // Split rule (P3 wiring): `ComposeResult` carries BOTH `ir` (OZ-shape) and
37
+ // `interpreterIr` (predicate-shape). Each constraint is routed to EXACTLY ONE
38
+ // adapter:
39
+ // - `compare window_spent(token, w) <= limit` where `token === scope.contract`
40
+ // and the protocol is known -> `ir` (OZ lowers to spending_limit).
41
+ // - everything else the compose step emits (recipient allowlists, per-method
42
+ // scoping via scope.method, invocation_count bounds, eq_seq swap paths,
43
+ // oracle_price bounds, AND window_spent where token != scope.contract, i.e.
44
+ // a SoroSwap input-token cap) -> `interpreterIr`.
45
+ //
46
+ // This prevents the interpreter adapter from emitting a duplicate
47
+ // `window_spent` predicate leaf alongside an OZ `spending_limit` primitive
48
+ // covering the same spend semantic - the two adapters never overlap.
49
+ //
50
+ // The default behavior is `deny_all` (OZ context rules are deny-by-default).
51
+ Object.defineProperty(exports, "__esModule", { value: true });
52
+ exports.composeFromRecording = composeFromRecording;
53
+ const identify_ts_1 = require("../registry/identify.js");
54
+ /** Compose a PolicyIR pair from the lowered facts + the resolved scope.
55
+ * Pure (no randomness, no clock); same inputs -> byte-identical result. */
56
+ function composeFromRecording(facts, scopeContract, topLevel, opts) {
57
+ const interpreterEnabled = opts.interpreterEnabled === true;
58
+ const ambiguities = [];
59
+ const warnings = [];
60
+ const ozConstraints = [];
61
+ const interpreterConstraints = [];
62
+ const protocol = topLevel
63
+ ? (0, identify_ts_1.identifyProtocol)(topLevel.contract, topLevel.fn, topLevel.args, opts.network)
64
+ : null;
65
+ const known = protocol !== null;
66
+ const windowSeconds = opts.userResponses?.windowSeconds;
67
+ const limitAmount = opts.userResponses?.limitAmount;
68
+ const spendTokens = Object.keys(facts.spendByToken);
69
+ // Outgoing spend -> one spending_limit per spent token. A single caller limit
70
+ // binds only an unambiguous single-token spend; a multi-token flow needs a
71
+ // per-token limit, so each unmatched token surfaces AMOUNT_BOUND_MISSING.
72
+ //
73
+ // Routing: a `window_spent(token, w) <= limit` constraint goes to the OZ IR
74
+ // only when token === scope.contract (OZ's spending_limit binds the
75
+ // CallContract target, not a token parameter). Otherwise it goes to the
76
+ // interpreter IR (the interpreter lowers window_spent to a predicate leaf).
77
+ if (spendTokens.length > 0 && topLevel) {
78
+ let durationFlagged = false;
79
+ for (const token of spendTokens) {
80
+ const observed = facts.spendByToken[token];
81
+ if (observed === undefined)
82
+ continue;
83
+ if (!known) {
84
+ warnings.push(`spend of ${observed} (token ${token}) not bounded: unrecognised protocol, spend cap needs the interpreter predicate`);
85
+ continue;
86
+ }
87
+ const limit = spendTokens.length === 1 ? limitAmount : undefined;
88
+ if (windowSeconds === undefined && !durationFlagged) {
89
+ ambiguities.push({
90
+ code: 'DURATION_UNSPECIFIED',
91
+ question: `Recording shows a ${observed}-${token} spend. What rolling window (seconds) should the spending_limit use?`,
92
+ });
93
+ durationFlagged = true;
94
+ }
95
+ if (limit === undefined) {
96
+ ambiguities.push({
97
+ code: 'AMOUNT_BOUND_MISSING',
98
+ question: `Recording shows a ${observed}-${token} spend, but a single spend does not authorise that amount every window. What per-window spending_limit should apply? (observed amount, suggestion only: ${observed})`,
99
+ });
100
+ continue;
101
+ }
102
+ if (windowSeconds !== undefined && limit !== undefined) {
103
+ const spendCond = {
104
+ op: 'compare',
105
+ compare: {
106
+ selector: { kind: 'window_spent', token, windowSeconds },
107
+ operator: 'lte',
108
+ value: limit,
109
+ },
110
+ };
111
+ if (!interpreterEnabled || token === scopeContract) {
112
+ ozConstraints.push(spendCond);
113
+ }
114
+ else {
115
+ interpreterConstraints.push(spendCond);
116
+ }
117
+ }
118
+ }
119
+ }
120
+ // Incoming-only / frequency intent: an invocation_count bound is emitted ONLY
121
+ // when the caller supplies the count AND a window - never a fabricated `<= 1`.
122
+ // Routed to the interpreter IR when interpreter is enabled (OZ cannot lower
123
+ // invocation_count); otherwise to the OZ IR (which flags it as uncovered).
124
+ //
125
+ // A recognised swap is NOT an incoming-only flow: it has an outgoing input leg
126
+ // whose spend simply was not attributed to the source account (the
127
+ // fee-sponsored / holder != source case). Its real restrictions - exact path,
128
+ // recipient, and the input-amount cap - come from the protocol-specific pass,
129
+ // so it does NOT get the incoming-only frequency prompt. A caller who wants to
130
+ // rate-limit the swap can still supply an invocationLimit + window, which
131
+ // lowers to an invocation_count for any flow.
132
+ if (spendTokens.length === 0 && topLevel) {
133
+ const invocationLimit = opts.userResponses?.invocationLimit;
134
+ const isRecognisedSwap = protocol?.protocol === 'soroswap';
135
+ if (known && windowSeconds !== undefined && invocationLimit !== undefined) {
136
+ const icCond = {
137
+ op: 'compare',
138
+ compare: {
139
+ selector: { kind: 'invocation_count', windowSeconds },
140
+ operator: 'lte',
141
+ value: String(invocationLimit),
142
+ },
143
+ };
144
+ if (interpreterEnabled) {
145
+ interpreterConstraints.push(icCond);
146
+ }
147
+ else {
148
+ ozConstraints.push(icCond);
149
+ }
150
+ }
151
+ else if (!isRecognisedSwap) {
152
+ ambiguities.push({
153
+ code: 'FREQUENCY_BOUND_MISSING',
154
+ question: 'Incoming-only flow - what max invocations per window should the policy enforce?',
155
+ });
156
+ warnings.push('frequency bound needed for the incoming-only flow (needs the interpreter predicate); no invocation cap inferred');
157
+ }
158
+ }
159
+ // Per-asset oracle-price bound(s) supplied by the caller -> one
160
+ // oracle_price compare per entry. Routed to the interpreter IR when enabled;
161
+ // otherwise to the OZ IR (which flags it as uncovered).
162
+ const oracleBounds = opts.userResponses?.oraclePriceBound;
163
+ if (oracleBounds) {
164
+ for (const b of oracleBounds) {
165
+ const oracleCond = {
166
+ op: 'compare',
167
+ compare: {
168
+ selector: { kind: 'oracle_price', asset: b.asset },
169
+ operator: b.operator,
170
+ value: b.value,
171
+ },
172
+ };
173
+ if (interpreterEnabled) {
174
+ interpreterConstraints.push(oracleCond);
175
+ }
176
+ else {
177
+ ozConstraints.push(oracleCond);
178
+ }
179
+ }
180
+ }
181
+ // Observed recipient allowlist (SEP-41) is a real, recorded constraint the OZ
182
+ // adapter flags as not covered; the interpreter adapter lowers it to an `in`
183
+ // predicate. Unknown protocols emit nothing here. SoroSwap's swap recipient
184
+ // (arg[3]) is the source-of-truth for the swapRecipientAllowlist surface.
185
+ if (topLevel && protocol !== null) {
186
+ // A SoroSwap input-amount cap binds the caller's limitAmount to call_arg[0]
187
+ // (the exact amount_in) ONLY when no cumulative outgoing spend was detected
188
+ // for the source account - i.e. the input token never moved FROM the source
189
+ // (fee-sponsored swaps, or a holder != source). When a spend WAS detected,
190
+ // the window_spent path above already consumed the limit, so the per-call
191
+ // arg cap is skipped to avoid binding one limit to two different semantics.
192
+ const swapInputAmountCap = spendTokens.length === 0 ? limitAmount : undefined;
193
+ appendProtocolSpecificConstraints(ozConstraints, interpreterConstraints, warnings, ambiguities, facts, topLevel, protocol, opts.userResponses?.swapRecipientAllowlist, swapInputAmountCap, interpreterEnabled);
194
+ }
195
+ // `scope.method` is carried on BOTH IRs so each adapter produces a
196
+ // self-consistent rule. The interpreter adapter lowers scope.method into a
197
+ // `call_fn == <method>` predicate leaf (a real restriction); the OZ adapter
198
+ // flags it as uncovered (CallContract alone permits any method on the
199
+ // contract).
200
+ const scope = { contract: scopeContract };
201
+ if (topLevel?.fn)
202
+ scope.method = topLevel.fn;
203
+ const buildRule = (constraints) => {
204
+ const rule = {
205
+ roles: [],
206
+ scope: { ...scope },
207
+ constraints,
208
+ };
209
+ if (opts.userResponses?.validUntilLedger !== undefined) {
210
+ rule.expiry = { validUntilLedger: opts.userResponses.validUntilLedger };
211
+ }
212
+ return rule;
213
+ };
214
+ const ir = {
215
+ chain: 'stellar',
216
+ defaultBehavior: 'deny_all',
217
+ rules: [buildRule(ozConstraints)],
218
+ };
219
+ const interpreterIr = {
220
+ chain: 'stellar',
221
+ defaultBehavior: 'deny_all',
222
+ rules: [buildRule(interpreterConstraints)],
223
+ };
224
+ return { ir, interpreterIr, ambiguities, warnings };
225
+ }
226
+ /** Add the constraints justified by the recording that the OZ backend cannot
227
+ * express natively. Each constraint is routed to either `ozConstraints` (the
228
+ * OZ adapter lowers it) or `interpreterConstraints` (the interpreter adapter
229
+ * lowers it). When `interpreterEnabled` is false, every protocol-specific
230
+ * constraint is routed to `ozConstraints` (which flags it as uncovered) so
231
+ * callers who haven't opted in keep today's warning-driven behaviour.
232
+ * SoroSwap's slippage / oracle / exact-path needs come from `userResponses`
233
+ * (oraclePriceBound + limitAmount) + the recorded path (eq_seq on
234
+ * call_arg[2]). */
235
+ function appendProtocolSpecificConstraints(ozConstraints, interpreterConstraints, warnings, ambiguities, facts, topLevel, protocol, swapRecipientAllowlist, swapInputAmountCap, interpreterEnabled) {
236
+ // SEP-41 transfer / mint: the `to` arg (index 1) is the recipient. Emit it as
237
+ // a single-element allowlist; the interpreter adapter lowers it to `in`.
238
+ // When interpreter is not enabled, route to OZ so the caller sees today's
239
+ // `value allowlist on arg 1 (arg allowlist)` warning.
240
+ if (protocol.protocol === 'sep41' && (protocol.fn === 'transfer' || protocol.fn === 'mint')) {
241
+ const toArg = topLevel.args[1];
242
+ if (toArg && toArg.type === 'address') {
243
+ const cond = {
244
+ op: 'in',
245
+ selector: { kind: 'arg', argIndex: 1, scalarType: 'address' },
246
+ values: [toArg.value],
247
+ };
248
+ if (interpreterEnabled) {
249
+ interpreterConstraints.push(cond);
250
+ }
251
+ else {
252
+ ozConstraints.push(cond);
253
+ }
254
+ }
255
+ }
256
+ // SoroSwap: the recorded hop path -> eq_seq on the path arg (call_arg[2]).
257
+ // The interpreter adapter is the ONLY way to express an exact ordered
258
+ // sequence (OZ built-ins cannot). Element order is preserved verbatim.
259
+ // When multiple paths were recorded, the intersection is emitted as the
260
+ // canonical single path only if all observations agree; otherwise each is
261
+ // surfaced descriptively.
262
+ if (protocol.protocol === 'soroswap') {
263
+ const paths = facts.allowedPaths?.[topLevel.contract];
264
+ const route = paths?.length === 1 ? paths[0] : undefined;
265
+ if (route && route.length > 0 && interpreterEnabled) {
266
+ const pathArgIndex = 2; // SoroSwap swap_exact_tokens_for_tokens: args = [amount_in, amount_out_min, path, to, deadline]
267
+ interpreterConstraints.push({
268
+ op: 'eq_seq',
269
+ selector: { kind: 'arg', argIndex: pathArgIndex, scalarType: 'address' },
270
+ values: route,
271
+ });
272
+ }
273
+ else {
274
+ const pathText = route && route.length > 0 ? `; observed path: ${route.join(' -> ')}` : '';
275
+ warnings.push(`SoroSwap swap: slippage / oracle price bound and exact hop path need the interpreter predicate${pathText}`);
276
+ }
277
+ // Input-amount cap: bind the caller's limitAmount to the swap's input-amount
278
+ // argument as `call_arg[i] <= limit`. The index is function-specific -
279
+ // SoroSwap has three swap entrypoints: `swap_exact_tokens_for_tokens` and
280
+ // `swap_exact_in_for_tokens` take the exact input as arg[0], while
281
+ // `swap_tokens_for_exact_tokens` takes the MAXIMUM input (`amount_in_max`)
282
+ // as arg[1] (its arg[0] is the exact OUTPUT, so binding arg[0] there would
283
+ // cap the wrong value and leave the input unbounded). This is a per-call cap
284
+ // that does NOT depend on attributing a token movement to the source account
285
+ // (the fee-sponsored / holder != source case, where the window_spent path
286
+ // detects no spend). Fail-closed: it only ever restricts the permitted input.
287
+ // Routed to the interpreter predicate (OZ built-ins cannot express a per-arg
288
+ // i128 bound); when the interpreter is not enabled it goes to OZ, which flags
289
+ // it uncovered (a warning). The `type === 'i128'` check is defense in depth -
290
+ // protocol identification's argsMatchAbi already pins the input arg to i128.
291
+ const inputArgIndex = soroswapInputAmountArgIndex(protocol.fn);
292
+ const inputAmountArg = inputArgIndex !== undefined ? topLevel.args[inputArgIndex] : undefined;
293
+ if (swapInputAmountCap !== undefined &&
294
+ inputArgIndex !== undefined &&
295
+ inputAmountArg &&
296
+ inputAmountArg.type === 'i128') {
297
+ const cond = {
298
+ op: 'compare',
299
+ compare: {
300
+ selector: { kind: 'arg', argIndex: inputArgIndex, scalarType: 'i128' },
301
+ operator: 'lte',
302
+ value: swapInputAmountCap,
303
+ },
304
+ };
305
+ if (interpreterEnabled) {
306
+ interpreterConstraints.push(cond);
307
+ }
308
+ else {
309
+ ozConstraints.push(cond);
310
+ }
311
+ }
312
+ // Swap recipient (call_arg[3]): when the caller supplies
313
+ // swapRecipientAllowlist, emit it as an `in` constraint on the recipient
314
+ // arg. When absent, surface RECIPIENT_ALLOWLIST_EMPTY so the caller is
315
+ // prompted; proceeding without an allowlist leaves the recipient
316
+ // unconstrained in the predicate. The ambiguity is only surfaced when
317
+ // the interpreter is enabled - if it isn't, the swap recipient is just
318
+ // ignored (today's behaviour).
319
+ const recipientArg = topLevel.args[3];
320
+ if (swapRecipientAllowlist && swapRecipientAllowlist.length > 0) {
321
+ const cond = {
322
+ op: 'in',
323
+ selector: { kind: 'arg', argIndex: 3, scalarType: 'address' },
324
+ values: [...swapRecipientAllowlist],
325
+ };
326
+ if (interpreterEnabled) {
327
+ interpreterConstraints.push(cond);
328
+ }
329
+ else {
330
+ ozConstraints.push(cond);
331
+ }
332
+ }
333
+ else if (interpreterEnabled && recipientArg && recipientArg.type === 'address') {
334
+ ambiguities.push({
335
+ code: 'RECIPIENT_ALLOWLIST_EMPTY',
336
+ question: `Recording shows the swap sending to ${recipientArg.value}. What recipient allowlist should the interpreter predicate bind? (omit to leave recipient unconstrained)`,
337
+ });
338
+ }
339
+ }
340
+ }
341
+ /** Positional index of the input-amount argument for a recognized SoroSwap swap
342
+ * function. `swap_exact_tokens_for_tokens` and `swap_exact_in_for_tokens` take
343
+ * the exact input as arg[0]; `swap_tokens_for_exact_tokens` takes the maximum
344
+ * input (`amount_in_max`) as arg[1] - its arg[0] is the exact OUTPUT. Any other
345
+ * function has no positional input-amount argument -> undefined (no cap bound). */
346
+ function soroswapInputAmountArgIndex(fn) {
347
+ switch (fn) {
348
+ case 'swap_exact_tokens_for_tokens':
349
+ case 'swap_exact_in_for_tokens':
350
+ return 0;
351
+ case 'swap_tokens_for_exact_tokens':
352
+ return 1;
353
+ default:
354
+ return undefined;
355
+ }
356
+ }
@@ -0,0 +1,12 @@
1
+ import type { PredicateNode } from '../types.ts';
2
+ import type { EvalContext } from './evaluate.ts';
3
+ export interface DenyCase {
4
+ dimension: string;
5
+ ctx: EvalContext;
6
+ }
7
+ export interface GeneratedCases {
8
+ permit: EvalContext;
9
+ denies: DenyCase[];
10
+ }
11
+ /** Build deterministic model-evaluated alternatives without mutating the intended call. */
12
+ export declare function generateCases(predicate: PredicateNode, permitCtx: EvalContext): GeneratedCases;