@crediolabs/policy-synth 0.1.10 → 0.1.12

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 (84) hide show
  1. package/dist/adapters/interpreter/adapter.js +83 -11
  2. package/dist/adapters/oz/adapter.js +4 -0
  3. package/dist/errors.d.ts +1 -1
  4. package/dist/install/build-add-context-rule.d.ts +48 -0
  5. package/dist/install/build-add-context-rule.js +304 -0
  6. package/dist/ir/types.d.ts +23 -0
  7. package/dist/predicate/decode.d.ts +11 -0
  8. package/dist/predicate/decode.js +234 -0
  9. package/dist/predicate/encode.js +79 -14
  10. package/dist/predicate/from-json.d.ts +4 -0
  11. package/dist/predicate/from-json.js +113 -0
  12. package/dist/predicate/index.d.ts +2 -0
  13. package/dist/predicate/index.js +5 -1
  14. package/dist/registry/identify.js +21 -2
  15. package/dist/registry/protocols.d.ts +50 -1
  16. package/dist/registry/protocols.js +88 -0
  17. package/dist/review-card/builder.d.ts +13 -0
  18. package/dist/review-card/builder.js +63 -5
  19. package/dist/review-card/cross-check.js +32 -3
  20. package/dist/review-card/index.d.ts +1 -1
  21. package/dist/review-card/index.js +1 -1
  22. package/dist/run/schemas.d.ts +4 -4
  23. package/dist/synth/compose-from-recording.d.ts +18 -0
  24. package/dist/synth/compose-from-recording.js +75 -10
  25. package/dist/synth/deny-cases.d.ts +4 -1
  26. package/dist/synth/deny-cases.js +3 -1
  27. package/dist/synth/evaluate.js +130 -3
  28. package/dist/synth/permit-context.d.ts +15 -0
  29. package/dist/synth/permit-context.js +116 -0
  30. package/dist/synth/synthesize-from-recording.js +14 -2
  31. package/dist/types.d.ts +9 -0
  32. package/dist-cjs/adapters/interpreter/adapter.js +83 -11
  33. package/dist-cjs/adapters/oz/adapter.js +4 -0
  34. package/dist-cjs/errors.d.ts +1 -1
  35. package/dist-cjs/install/build-add-context-rule.d.ts +48 -0
  36. package/dist-cjs/install/build-add-context-rule.js +308 -0
  37. package/dist-cjs/ir/types.d.ts +23 -0
  38. package/dist-cjs/predicate/decode.d.ts +11 -0
  39. package/dist-cjs/predicate/decode.js +239 -0
  40. package/dist-cjs/predicate/encode.js +79 -14
  41. package/dist-cjs/predicate/from-json.d.ts +4 -0
  42. package/dist-cjs/predicate/from-json.js +116 -0
  43. package/dist-cjs/predicate/index.d.ts +2 -0
  44. package/dist-cjs/predicate/index.js +10 -2
  45. package/dist-cjs/registry/identify.js +20 -1
  46. package/dist-cjs/registry/protocols.d.ts +50 -1
  47. package/dist-cjs/registry/protocols.js +89 -1
  48. package/dist-cjs/review-card/builder.d.ts +13 -0
  49. package/dist-cjs/review-card/builder.js +64 -5
  50. package/dist-cjs/review-card/cross-check.js +32 -3
  51. package/dist-cjs/review-card/index.d.ts +1 -1
  52. package/dist-cjs/review-card/index.js +2 -1
  53. package/dist-cjs/run/schemas.d.ts +4 -4
  54. package/dist-cjs/synth/compose-from-recording.d.ts +18 -0
  55. package/dist-cjs/synth/compose-from-recording.js +75 -10
  56. package/dist-cjs/synth/deny-cases.d.ts +4 -1
  57. package/dist-cjs/synth/deny-cases.js +3 -0
  58. package/dist-cjs/synth/evaluate.js +130 -3
  59. package/dist-cjs/synth/permit-context.d.ts +15 -0
  60. package/dist-cjs/synth/permit-context.js +119 -0
  61. package/dist-cjs/synth/synthesize-from-recording.js +14 -2
  62. package/dist-cjs/types.d.ts +9 -0
  63. package/package.json +5 -2
  64. package/src/adapters/interpreter/adapter.ts +93 -11
  65. package/src/adapters/oz/adapter.ts +4 -0
  66. package/src/contracts/policy-template/OZ_POLICY_TRAIT.md +28 -3
  67. package/src/errors.ts +13 -0
  68. package/src/install/build-add-context-rule.ts +429 -0
  69. package/src/ir/types.ts +23 -0
  70. package/src/predicate/decode.ts +242 -0
  71. package/src/predicate/encode.ts +110 -13
  72. package/src/predicate/from-json.ts +124 -0
  73. package/src/predicate/index.ts +5 -1
  74. package/src/registry/identify.ts +22 -2
  75. package/src/registry/protocols.ts +90 -1
  76. package/src/review-card/builder.ts +58 -5
  77. package/src/review-card/cross-check.ts +36 -3
  78. package/src/review-card/index.ts +1 -0
  79. package/src/synth/compose-from-recording.ts +101 -7
  80. package/src/synth/deny-cases.ts +3 -1
  81. package/src/synth/evaluate.ts +130 -3
  82. package/src/synth/permit-context.ts +136 -0
  83. package/src/synth/synthesize-from-recording.ts +13 -2
  84. package/src/types.ts +12 -0
@@ -0,0 +1,136 @@
1
+ // src/synth/permit-context.ts - build the EvalContext that represents the
2
+ // recorded call being replayed.
3
+ //
4
+ // Extracted from the over-permissiveness harness so the same construction
5
+ // feeds both the in-process battery and the on-chain replay
6
+ // (scripts/verify-mutations-testnet.ts). Two callers building this
7
+ // separately would be two chances to disagree about what "the recorded
8
+ // call" means, which is the one thing both must share.
9
+
10
+ import type { PredicateLeaf, PredicateNode, RecordedTransaction } from '../types.ts'
11
+ import { cloneScVal } from './deny-cases.ts'
12
+ import type { EvalContext } from './evaluate.ts'
13
+
14
+ export interface PermitContextResponses {
15
+ windowSeconds: number
16
+ invocationLimit?: number
17
+ limitAmount?: string
18
+ validUntilLedger: number
19
+ oraclePriceBound?: Array<{ asset: string; operator: string; value: string; decimals: number }>
20
+ }
21
+
22
+ export function buildPermitContext(
23
+ tx: RecordedTransaction,
24
+ responses: PermitContextResponses,
25
+ predicate: PredicateNode
26
+ ): EvalContext {
27
+ const amountByToken: Record<string, string> = {}
28
+ const totals = new Map<string, bigint>()
29
+ for (const m of tx.tokenMovements) {
30
+ const current = totals.get(m.token) ?? 0n
31
+ totals.set(m.token, current + BigInt(m.amount))
32
+ }
33
+ for (const [token, total] of totals) {
34
+ amountByToken[token] = total.toString()
35
+ }
36
+
37
+ const topLevel = tx.invocations[0]
38
+ const scopeContract = topLevel?.contract ?? ''
39
+
40
+ const oraclePriceByAsset: EvalContext['oraclePriceByAsset'] = {}
41
+ visitOracleLeaves(predicate, (asset, op, bound) => {
42
+ let price: bigint
43
+ switch (op) {
44
+ case 'lt':
45
+ price = bound - 1n
46
+ break
47
+ case 'gt':
48
+ price = bound + 1n
49
+ break
50
+ default:
51
+ price = bound
52
+ }
53
+ if (price < 0n) price = 0n
54
+ oraclePriceByAsset[asset] = { price: price.toString(), timestampSeconds: tx.fetchedAt }
55
+ })
56
+
57
+ const ctx: EvalContext = {
58
+ contract: scopeContract,
59
+ fn: topLevel?.fn ?? '',
60
+ args: (topLevel?.args ?? []).map(cloneScVal),
61
+ atLedger: tx.ledgerSequence,
62
+ nowSeconds: tx.fetchedAt,
63
+ amountByToken,
64
+ windowSpentByToken: {},
65
+ invocationCountByWindow: {},
66
+ oraclePriceByAsset,
67
+ }
68
+ if (responses.validUntilLedger !== undefined) {
69
+ ctx.validUntilLedger = responses.validUntilLedger
70
+ }
71
+ return ctx
72
+ }
73
+
74
+ function visitOracleLeaves(
75
+ node: PredicateNode,
76
+ visit: (asset: string, op: string, bound: bigint) => void
77
+ ): void {
78
+ switch (node.op) {
79
+ case 'and':
80
+ case 'or':
81
+ for (const child of node.children) visitOracleLeaves(child, visit)
82
+ return
83
+ case 'not':
84
+ visitOracleLeaves(node.child, visit)
85
+ return
86
+ case 'eq':
87
+ case 'lt':
88
+ case 'lte':
89
+ case 'gt':
90
+ case 'gte': {
91
+ const leftIsOracle = node.left.kind === 'oracle_price'
92
+ const rightIsOracle = node.right.kind === 'oracle_price'
93
+ let asset: string | undefined
94
+ let literal: bigint | undefined
95
+ if (leftIsOracle) {
96
+ asset = node.left.kind === 'oracle_price' ? node.left.asset : undefined
97
+ literal = oracleThresholdNormalised(node.right)
98
+ } else if (rightIsOracle) {
99
+ asset = node.right.kind === 'oracle_price' ? node.right.asset : undefined
100
+ literal = oracleThresholdNormalised(node.left)
101
+ }
102
+ if (asset !== undefined && literal !== undefined) {
103
+ visit(asset, node.op, literal)
104
+ }
105
+ return
106
+ }
107
+ case 'in':
108
+ return
109
+ }
110
+ }
111
+
112
+ /** Oracle prices normalise to 9 decimals; mirrors NORMALISED_DECIMALS in
113
+ * oracle.rs. */
114
+ const NORMALISED_DECIMALS = 9n
115
+
116
+ /** A threshold restated on the normalised basis, so a derived permit price is
117
+ * comparable to it. Thresholds carry their own basis, so the conversion has
118
+ * to happen here - reading the digits raw would build a context off by a
119
+ * factor of 10^(decimals-9). Returns undefined for any other leaf. */
120
+ function oracleThresholdNormalised(leaf: PredicateLeaf): bigint | undefined {
121
+ if (leaf.kind !== 'oracle_threshold') return undefined
122
+ let value: bigint
123
+ try {
124
+ value = BigInt(leaf.value)
125
+ } catch {
126
+ return undefined
127
+ }
128
+ const decimals = BigInt(leaf.decimals)
129
+ if (decimals <= NORMALISED_DECIMALS) {
130
+ return value * 10n ** (NORMALISED_DECIMALS - decimals)
131
+ }
132
+ // Floor: a finer-grained threshold has no exact 9-dp representation. The
133
+ // caller only needs a price that lands on the right side of the bound, and
134
+ // it offsets by one from here.
135
+ return value / 10n ** (decimals - NORMALISED_DECIMALS)
136
+ }
@@ -1004,11 +1004,22 @@ function visitOracleLeaves(
1004
1004
  }
1005
1005
  }
1006
1006
 
1007
+ /** An oracle threshold restated on the normalised 9-dp basis prices use.
1008
+ * Thresholds carry their own basis, so reading the digits raw would build a
1009
+ * permit context off by 10^(decimals-9) and the intended call would not
1010
+ * satisfy its own bound. Mirrors NORMALISED_DECIMALS in oracle.rs. */
1007
1011
  function oracleLiteralFromLeaf(leaf: PredicateLeaf): bigint | undefined {
1008
- if (leaf.kind !== 'literal_i128') return undefined
1012
+ if (leaf.kind !== 'oracle_threshold') return undefined
1013
+ let value: bigint
1009
1014
  try {
1010
- return BigInt(leaf.value)
1015
+ value = BigInt(leaf.value)
1011
1016
  } catch {
1012
1017
  return undefined
1013
1018
  }
1019
+ const normalised = 9n
1020
+ const decimals = BigInt(leaf.decimals)
1021
+ if (decimals <= normalised) return value * 10n ** (normalised - decimals)
1022
+ // Floor: a finer-grained threshold has no exact 9-dp representation; the
1023
+ // caller offsets by one from here to land on the right side of the bound.
1024
+ return value / 10n ** (decimals - normalised)
1014
1025
  }
package/src/types.ts CHANGED
@@ -155,6 +155,13 @@ export type PredicateLeaf =
155
155
  // recorded ScVal type. A non-vec arg, missing element, missing field, or
156
156
  // type mismatch all DENY (fail-closed).
157
157
  | { kind: 'call_arg_field'; index: number; element: number; field: string }
158
+ // `call_arg_scaled(index, num, den)` evaluates to `args[index] * num / den`
159
+ // (truncating toward zero). The slippage-floor leaf: a swap policy
160
+ // expresses `call_arg[out] >= call_arg_scaled(in, num, den)` as "the
161
+ // minimum output must be at least this ratio of the input". `num`/`den`
162
+ // are decimal strings on i128 (mirrors `literal_i128`); the contract
163
+ // refuses `den == 0` and `num <= 0` / `den <= 0` at install.
164
+ | { kind: 'call_arg_scaled'; index: number; num: string; den: string }
158
165
  // `call_sub_invocation[i]` is NOT in v1 grammar. OZ `Policy::enforce`
159
166
  // receives one `Context`, not a sub-invocation tree; sub-invocations live under
160
167
  // `InvokerContractAuthEntry::Contract(SubContractInvocation)`, not in the
@@ -166,6 +173,11 @@ export type PredicateLeaf =
166
173
  | { kind: 'valid_until' }
167
174
  | { kind: 'invocation_count_in_window'; windowSecs: number }
168
175
  | { kind: 'oracle_price'; asset: string } // Stellar Address (SAC address for SEP-41; XLM uses its network SAC)
176
+ // The right-hand side of an oracle comparison. It carries its own decimal
177
+ // basis because the contract cannot recover it: prices normalise to 9 dp, so
178
+ // a raw 14-dp threshold is ~10^5 too large and `price <= threshold` becomes
179
+ // trivially true. `value` is a decimal string on i128, like `literal_i128`.
180
+ | { kind: 'oracle_threshold'; value: string; decimals: number }
169
181
  // Literal leaves: bare ScVal on the wire (no selector-tuple wrapper). Right-hand side of
170
182
  // comparisons, elements of `in` haystacks, and operands to future arithmetic nodes.
171
183
  | { kind: 'literal_address'; value: string }