@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.
- package/dist/adapters/interpreter/adapter.js +83 -11
- package/dist/adapters/oz/adapter.js +4 -0
- package/dist/errors.d.ts +1 -1
- package/dist/install/build-add-context-rule.d.ts +48 -0
- package/dist/install/build-add-context-rule.js +304 -0
- package/dist/ir/types.d.ts +23 -0
- package/dist/predicate/decode.d.ts +11 -0
- package/dist/predicate/decode.js +234 -0
- package/dist/predicate/encode.js +79 -14
- package/dist/predicate/from-json.d.ts +4 -0
- package/dist/predicate/from-json.js +113 -0
- package/dist/predicate/index.d.ts +2 -0
- package/dist/predicate/index.js +5 -1
- package/dist/registry/identify.js +21 -2
- package/dist/registry/protocols.d.ts +50 -1
- package/dist/registry/protocols.js +88 -0
- package/dist/review-card/builder.d.ts +13 -0
- package/dist/review-card/builder.js +63 -5
- package/dist/review-card/cross-check.js +32 -3
- package/dist/review-card/index.d.ts +1 -1
- package/dist/review-card/index.js +1 -1
- package/dist/run/schemas.d.ts +4 -4
- package/dist/synth/compose-from-recording.d.ts +18 -0
- package/dist/synth/compose-from-recording.js +75 -10
- package/dist/synth/deny-cases.d.ts +4 -1
- package/dist/synth/deny-cases.js +3 -1
- package/dist/synth/evaluate.js +130 -3
- package/dist/synth/permit-context.d.ts +15 -0
- package/dist/synth/permit-context.js +116 -0
- package/dist/synth/synthesize-from-recording.js +14 -2
- package/dist/types.d.ts +9 -0
- package/dist-cjs/adapters/interpreter/adapter.js +83 -11
- package/dist-cjs/adapters/oz/adapter.js +4 -0
- package/dist-cjs/errors.d.ts +1 -1
- package/dist-cjs/install/build-add-context-rule.d.ts +48 -0
- package/dist-cjs/install/build-add-context-rule.js +308 -0
- package/dist-cjs/ir/types.d.ts +23 -0
- package/dist-cjs/predicate/decode.d.ts +11 -0
- package/dist-cjs/predicate/decode.js +239 -0
- package/dist-cjs/predicate/encode.js +79 -14
- package/dist-cjs/predicate/from-json.d.ts +4 -0
- package/dist-cjs/predicate/from-json.js +116 -0
- package/dist-cjs/predicate/index.d.ts +2 -0
- package/dist-cjs/predicate/index.js +10 -2
- package/dist-cjs/registry/identify.js +20 -1
- package/dist-cjs/registry/protocols.d.ts +50 -1
- package/dist-cjs/registry/protocols.js +89 -1
- package/dist-cjs/review-card/builder.d.ts +13 -0
- package/dist-cjs/review-card/builder.js +64 -5
- package/dist-cjs/review-card/cross-check.js +32 -3
- package/dist-cjs/review-card/index.d.ts +1 -1
- package/dist-cjs/review-card/index.js +2 -1
- package/dist-cjs/run/schemas.d.ts +4 -4
- package/dist-cjs/synth/compose-from-recording.d.ts +18 -0
- package/dist-cjs/synth/compose-from-recording.js +75 -10
- package/dist-cjs/synth/deny-cases.d.ts +4 -1
- package/dist-cjs/synth/deny-cases.js +3 -0
- package/dist-cjs/synth/evaluate.js +130 -3
- package/dist-cjs/synth/permit-context.d.ts +15 -0
- package/dist-cjs/synth/permit-context.js +119 -0
- package/dist-cjs/synth/synthesize-from-recording.js +14 -2
- package/dist-cjs/types.d.ts +9 -0
- package/package.json +5 -2
- package/src/adapters/interpreter/adapter.ts +93 -11
- package/src/adapters/oz/adapter.ts +4 -0
- package/src/contracts/policy-template/OZ_POLICY_TRAIT.md +28 -3
- package/src/errors.ts +13 -0
- package/src/install/build-add-context-rule.ts +429 -0
- package/src/ir/types.ts +23 -0
- package/src/predicate/decode.ts +242 -0
- package/src/predicate/encode.ts +110 -13
- package/src/predicate/from-json.ts +124 -0
- package/src/predicate/index.ts +5 -1
- package/src/registry/identify.ts +22 -2
- package/src/registry/protocols.ts +90 -1
- package/src/review-card/builder.ts +58 -5
- package/src/review-card/cross-check.ts +36 -3
- package/src/review-card/index.ts +1 -0
- package/src/synth/compose-from-recording.ts +101 -7
- package/src/synth/deny-cases.ts +3 -1
- package/src/synth/evaluate.ts +130 -3
- package/src/synth/permit-context.ts +136 -0
- package/src/synth/synthesize-from-recording.ts +13 -2
- 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 !== '
|
|
1012
|
+
if (leaf.kind !== 'oracle_threshold') return undefined
|
|
1013
|
+
let value: bigint
|
|
1009
1014
|
try {
|
|
1010
|
-
|
|
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 }
|