@crediolabs/policy-synth 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/interpreter/adapter.d.ts +2 -2
- package/dist/adapters/interpreter/adapter.js +11 -3
- package/dist/errors.d.ts +6 -1
- package/dist/install/authority-overlap.d.ts +101 -0
- package/dist/install/authority-overlap.js +239 -0
- package/dist/install/build-add-context-rule.d.ts +1 -1
- package/dist/install/index.d.ts +1 -0
- package/dist/install/index.js +4 -0
- package/dist/install/read-account-rules.d.ts +79 -0
- package/dist/install/read-account-rules.js +241 -0
- package/dist/predicate/decode.js +22 -1
- package/dist/predicate/encode.js +52 -5
- package/dist/predicate/from-json.js +14 -1
- package/dist/record/index.d.ts +10 -0
- package/dist/record/index.js +32 -1
- package/dist/record/rpc.d.ts +4 -0
- package/dist/record/rpc.js +4 -1
- package/dist/registry/identify.d.ts +10 -1
- package/dist/registry/identify.js +4 -1
- package/dist/registry/on-chain-spec.d.ts +37 -0
- package/dist/registry/on-chain-spec.js +152 -0
- package/dist/review-card/builder.js +40 -0
- package/dist/review-card/cross-check.js +34 -0
- package/dist/review-card/render-leaf.d.ts +1 -1
- package/dist/review-card/render-leaf.js +7 -0
- package/dist/run/index.d.ts +25 -4
- package/dist/run/index.js +108 -4
- package/dist/run/schemas.d.ts +350 -6
- package/dist/run/schemas.js +95 -6
- package/dist/simulate/deny-cases.js +11 -0
- package/dist/simulate/evaluate.js +86 -5
- package/dist/synth/declare.d.ts +43 -0
- package/dist/synth/declare.js +127 -0
- package/dist/synth/index.d.ts +1 -0
- package/dist/synth/index.js +1 -0
- package/dist/synth/synthesize-from-recording.js +1 -1
- package/dist/types.d.ts +21 -1
- package/dist/types.js +1 -1
- package/dist-cjs/adapters/interpreter/adapter.d.ts +2 -2
- package/dist-cjs/adapters/interpreter/adapter.js +11 -3
- package/dist-cjs/errors.d.ts +6 -1
- package/dist-cjs/install/authority-overlap.d.ts +101 -0
- package/dist-cjs/install/authority-overlap.js +248 -0
- package/dist-cjs/install/build-add-context-rule.d.ts +1 -1
- package/dist-cjs/install/index.d.ts +1 -0
- package/dist-cjs/install/index.js +13 -2
- package/dist-cjs/install/read-account-rules.d.ts +79 -0
- package/dist-cjs/install/read-account-rules.js +252 -0
- package/dist-cjs/predicate/decode.js +22 -1
- package/dist-cjs/predicate/encode.js +52 -5
- package/dist-cjs/predicate/from-json.js +14 -1
- package/dist-cjs/record/index.d.ts +10 -0
- package/dist-cjs/record/index.js +31 -0
- package/dist-cjs/record/rpc.d.ts +4 -0
- package/dist-cjs/record/rpc.js +7 -3
- package/dist-cjs/registry/identify.d.ts +10 -1
- package/dist-cjs/registry/identify.js +4 -0
- package/dist-cjs/registry/on-chain-spec.d.ts +37 -0
- package/dist-cjs/registry/on-chain-spec.js +159 -0
- package/dist-cjs/review-card/builder.js +40 -0
- package/dist-cjs/review-card/cross-check.js +34 -0
- package/dist-cjs/review-card/render-leaf.d.ts +1 -1
- package/dist-cjs/review-card/render-leaf.js +7 -0
- package/dist-cjs/run/index.d.ts +25 -4
- package/dist-cjs/run/index.js +108 -2
- package/dist-cjs/run/schemas.d.ts +350 -6
- package/dist-cjs/run/schemas.js +96 -7
- package/dist-cjs/simulate/deny-cases.js +11 -0
- package/dist-cjs/simulate/evaluate.js +86 -5
- package/dist-cjs/synth/declare.d.ts +43 -0
- package/dist-cjs/synth/declare.js +130 -0
- package/dist-cjs/synth/index.d.ts +1 -0
- package/dist-cjs/synth/index.js +3 -1
- package/dist-cjs/synth/synthesize-from-recording.js +1 -1
- package/dist-cjs/types.d.ts +21 -1
- package/dist-cjs/types.js +1 -1
- package/package.json +1 -1
- package/src/adapters/interpreter/adapter.ts +13 -5
- package/src/errors.ts +5 -0
- package/src/install/authority-overlap.ts +323 -0
- package/src/install/index.ts +20 -0
- package/src/install/read-account-rules.ts +313 -0
- package/src/predicate/decode.ts +22 -1
- package/src/predicate/encode.ts +55 -5
- package/src/predicate/from-json.ts +14 -1
- package/src/record/index.ts +59 -2
- package/src/record/rpc.ts +4 -1
- package/src/registry/identify.ts +4 -1
- package/src/registry/on-chain-spec.ts +168 -0
- package/src/review-card/builder.ts +45 -2
- package/src/review-card/cross-check.ts +35 -1
- package/src/review-card/render-leaf.ts +8 -1
- package/src/run/index.ts +125 -2
- package/src/run/schemas.ts +100 -6
- package/src/simulate/deny-cases.ts +12 -1
- package/src/simulate/evaluate.ts +101 -9
- package/src/synth/declare.ts +206 -0
- package/src/synth/index.ts +5 -0
- package/src/synth/synthesize-from-recording.ts +1 -1
- package/src/types.ts +16 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src/simulate/evaluate.ts - TypeScript reference evaluator for grammar version
|
|
1
|
+
// src/simulate/evaluate.ts - TypeScript reference evaluator for grammar version 4.
|
|
2
2
|
//
|
|
3
3
|
// Pure function. Determinism: same `(predicate, ctx)` -> byte-identical result,
|
|
4
4
|
// no clock, no randomness. Deny order (deny on FIRST violation, stable reason):
|
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
// 3. `in` membership; empty haystack ALWAYS denies -> 'NOT_IN_ALLOWLIST'
|
|
9
9
|
// 4. otherwise permit.
|
|
10
10
|
//
|
|
11
|
-
// Grammar version
|
|
12
|
-
// Grammar version
|
|
11
|
+
// Grammar version 4 nodes: and, or, eq, lt, lte, gt, gte, in
|
|
12
|
+
// Grammar version 4 leaves: call_contract, call_fn, call_arg(i),
|
|
13
13
|
// call_arg_len(i), call_arg_field(i, element, field),
|
|
14
|
+
// call_arg_scaled(i, num, den),
|
|
14
15
|
// literal_address, literal_i128, literal_symbol, literal_u32, literal_vec
|
|
15
|
-
// Grammar version
|
|
16
|
-
// UNSUPPORTED_NODE, NOT_IN_ALLOWLIST
|
|
16
|
+
// Grammar version 4 deny reasons: ARG_MISMATCH, CONTRACT_SCOPE,
|
|
17
|
+
// ARITHMETIC_OVERFLOW, UNSUPPORTED_NODE, NOT_IN_ALLOWLIST, SLIPPAGE_FLOOR
|
|
17
18
|
/** Evaluate a `PredicateNode` against the candidate call described by `ctx`.
|
|
18
19
|
* Pure function. Returns `{ permit: true }` or `{ permit: false; reason }`. */
|
|
19
20
|
export function evaluate(predicate, ctx) {
|
|
@@ -33,8 +34,25 @@ function walk(node, ctx) {
|
|
|
33
34
|
}
|
|
34
35
|
return lastDeny ?? { permit: true };
|
|
35
36
|
}
|
|
37
|
+
// Permits on the first branch that holds; when none does, reports the
|
|
38
|
+
// FIRST branch's reason. Mirrors `Node::Or` in the Rust evaluator, which
|
|
39
|
+
// the conformance suite pins.
|
|
40
|
+
case 'or': {
|
|
41
|
+
let firstDeny = null;
|
|
42
|
+
for (const child of node.children) {
|
|
43
|
+
const r = walk(child, ctx);
|
|
44
|
+
if (r.permit)
|
|
45
|
+
return r;
|
|
46
|
+
if (firstDeny === null)
|
|
47
|
+
firstDeny = r;
|
|
48
|
+
}
|
|
49
|
+
return firstDeny ?? { permit: false, reason: 'UNSUPPORTED_NODE' };
|
|
50
|
+
}
|
|
36
51
|
case 'eq':
|
|
52
|
+
case 'lt':
|
|
37
53
|
case 'lte':
|
|
54
|
+
case 'gt':
|
|
55
|
+
case 'gte':
|
|
38
56
|
return evalCompare(node.op, node.left, node.right, ctx);
|
|
39
57
|
case 'in':
|
|
40
58
|
return evalIn(node.needle, node.haystack, ctx);
|
|
@@ -42,6 +60,16 @@ function walk(node, ctx) {
|
|
|
42
60
|
}
|
|
43
61
|
/** Comparison leaf evaluation. */
|
|
44
62
|
function evalCompare(op, left, right, ctx) {
|
|
63
|
+
// Scaled operands first, so the dedicated reasons reach the caller instead
|
|
64
|
+
// of a generic mismatch. Right-hand dispatch leads because
|
|
65
|
+
// `out >= in * num / den` is the canonical swap form. Mirrors the order in
|
|
66
|
+
// `eval_compare` on the Rust side.
|
|
67
|
+
if (right.kind === 'call_arg_scaled') {
|
|
68
|
+
return evalScaledCompare(op, left, right, true, ctx);
|
|
69
|
+
}
|
|
70
|
+
if (left.kind === 'call_arg_scaled') {
|
|
71
|
+
return evalScaledCompare(op, right, left, false, ctx);
|
|
72
|
+
}
|
|
45
73
|
// CONTRACT_SCOPE on call_contract eq
|
|
46
74
|
if (left.kind === 'call_contract' && op === 'eq') {
|
|
47
75
|
if (right.kind !== 'literal_address')
|
|
@@ -96,6 +124,53 @@ function evalCompare(op, left, right, ctx) {
|
|
|
96
124
|
// Unknown leaf/op combination - structural fail-closed.
|
|
97
125
|
return { permit: false, reason: 'UNSUPPORTED_NODE' };
|
|
98
126
|
}
|
|
127
|
+
/** i128 bounds. The contract computes in i128 and denies on overflow, so the
|
|
128
|
+
* reference has to draw the same line or the two layers disagree on inputs
|
|
129
|
+
* near the boundary. */
|
|
130
|
+
const I128_MIN = -(2n ** 127n);
|
|
131
|
+
const I128_MAX = 2n ** 127n - 1n;
|
|
132
|
+
/** Comparison where one side is `call_arg_scaled`. Mirrors
|
|
133
|
+
* `eval_scaled_arg_compare`: `args[index] * num / den` truncating toward
|
|
134
|
+
* zero, ARITHMETIC_OVERFLOW on arithmetic that does not fit or a zero
|
|
135
|
+
* denominator, SLIPPAGE_FLOOR on a comparison that simply fails. */
|
|
136
|
+
function evalScaledCompare(op, other, scaled, scaledOnRight, ctx) {
|
|
137
|
+
// Chaining two computed operands has no meaning a review card could state.
|
|
138
|
+
if (other.kind === 'call_arg_scaled')
|
|
139
|
+
return { permit: false, reason: 'UNSUPPORTED_NODE' };
|
|
140
|
+
// Could not READ the operand is a different failure from read-and-missed.
|
|
141
|
+
const input = argNumericBigInt(ctx.args[scaled.index]);
|
|
142
|
+
if (input === null)
|
|
143
|
+
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
144
|
+
let num;
|
|
145
|
+
let den;
|
|
146
|
+
try {
|
|
147
|
+
num = BigInt(scaled.num);
|
|
148
|
+
den = BigInt(scaled.den);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
152
|
+
}
|
|
153
|
+
if (den === 0n)
|
|
154
|
+
return { permit: false, reason: 'ARITHMETIC_OVERFLOW' };
|
|
155
|
+
const product = input * num;
|
|
156
|
+
if (product < I128_MIN || product > I128_MAX) {
|
|
157
|
+
return { permit: false, reason: 'ARITHMETIC_OVERFLOW' };
|
|
158
|
+
}
|
|
159
|
+
// BigInt division already truncates toward zero, matching i128 semantics.
|
|
160
|
+
const quotient = product / den;
|
|
161
|
+
if (quotient < I128_MIN || quotient > I128_MAX) {
|
|
162
|
+
return { permit: false, reason: 'ARITHMETIC_OVERFLOW' };
|
|
163
|
+
}
|
|
164
|
+
const otherVal = other.kind === 'call_arg'
|
|
165
|
+
? argNumericBigInt(ctx.args[other.index])
|
|
166
|
+
: literalNumericBigInt(other);
|
|
167
|
+
if (otherVal === null)
|
|
168
|
+
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
169
|
+
const [a, b] = scaledOnRight ? [otherVal, quotient] : [quotient, otherVal];
|
|
170
|
+
return bigintCmp(op, a.toString(), b.toString())
|
|
171
|
+
? { permit: true }
|
|
172
|
+
: { permit: false, reason: 'SLIPPAGE_FLOOR' };
|
|
173
|
+
}
|
|
99
174
|
/** Per-ScVal equality. Handles literal_vec as an EXACT ordered sequence:
|
|
100
175
|
* compare element-by-element in order; deny if length or any element differs.
|
|
101
176
|
* Opaque args (`type: 'other'`) fail closed. */
|
|
@@ -265,7 +340,13 @@ function bigintCmp(op, aStr, bStr) {
|
|
|
265
340
|
switch (op) {
|
|
266
341
|
case 'eq':
|
|
267
342
|
return a === b;
|
|
343
|
+
case 'lt':
|
|
344
|
+
return a < b;
|
|
268
345
|
case 'lte':
|
|
269
346
|
return a <= b;
|
|
347
|
+
case 'gt':
|
|
348
|
+
return a > b;
|
|
349
|
+
case 'gte':
|
|
350
|
+
return a >= b;
|
|
270
351
|
}
|
|
271
352
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { PredicateNode } from '../types.ts';
|
|
2
|
+
export interface PolicyDeclaration {
|
|
3
|
+
/** Method to pin. Required: a predicate with no selector leaf constrains
|
|
4
|
+
* nothing and the contract refuses it at install. */
|
|
5
|
+
fn: string;
|
|
6
|
+
/** Contract to pin, already resolved to a `C...` address. */
|
|
7
|
+
contract?: string;
|
|
8
|
+
/** Upper bound on the call's amount argument, in the token's SMALLEST
|
|
9
|
+
* unit as an unsigned decimal string (25 XLM = "250000000"). */
|
|
10
|
+
maxAmount?: string;
|
|
11
|
+
/** Which argument carries the amount. Defaults to the SEP-41 position. */
|
|
12
|
+
amountArgIndex?: number;
|
|
13
|
+
/** Recipient allowlist. */
|
|
14
|
+
recipients?: string[];
|
|
15
|
+
/** Which argument carries the recipient. Defaults to the SEP-41 position. */
|
|
16
|
+
recipientArgIndex?: number;
|
|
17
|
+
/** A cap of "0" denies every call, so it is refused unless asked for
|
|
18
|
+
* explicitly. A rule that permits nothing is a plausible thing to want and
|
|
19
|
+
* an implausible thing to want by accident. */
|
|
20
|
+
allowZeroCap?: boolean;
|
|
21
|
+
/** Minimum output as a ratio of the call's own input: the output argument
|
|
22
|
+
* must be at least `input * num / den`.
|
|
23
|
+
*
|
|
24
|
+
* A swap's acceptable output depends on the size of the trade, so a fixed
|
|
25
|
+
* floor would pin the policy to one trade size. The ratio is DECLARED, never
|
|
26
|
+
* inferred from a recording: a recorded rate is a price at one moment, and
|
|
27
|
+
* freezing it as policy would deny ordinary trades later. */
|
|
28
|
+
minOutputRatio?: {
|
|
29
|
+
num: string;
|
|
30
|
+
den: string;
|
|
31
|
+
inputArgIndex: number;
|
|
32
|
+
outputArgIndex: number;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export interface DeclaredPredicate {
|
|
36
|
+
predicate: PredicateNode;
|
|
37
|
+
/** Assumptions the caller should check. Never empty when an argument index
|
|
38
|
+
* was defaulted rather than supplied. */
|
|
39
|
+
warnings: string[];
|
|
40
|
+
}
|
|
41
|
+
/** Lower a declared constraint to a grammar-3 predicate. Pure and total:
|
|
42
|
+
* the same declaration always produces the same predicate. */
|
|
43
|
+
export declare function declarePredicate(d: PolicyDeclaration): DeclaredPredicate;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// src/synth/declare.ts - build a predicate from a DECLARED constraint.
|
|
2
|
+
//
|
|
3
|
+
// The second synthesis front-end, beside `synthesizeFromRecording`. That one
|
|
4
|
+
// infers a predicate from a transaction that happened; this one takes the
|
|
5
|
+
// constraint the user states outright. No transaction, no decoding, no
|
|
6
|
+
// inference, no RPC: the same declaration always lowers to a byte-identical
|
|
7
|
+
// predicate.
|
|
8
|
+
//
|
|
9
|
+
// This is the surviving half of the removed `MandateSpec`. Two of that type's
|
|
10
|
+
// five fields lowered to things that do not exist - `spendingLimit` became a
|
|
11
|
+
// `window_spent` compare the interpreter cannot evaluate (it is handed one
|
|
12
|
+
// call and keeps no state), and `approvalThreshold` needed OZ built-in policy
|
|
13
|
+
// contracts that were never deployed. Both are deliberately absent here, and
|
|
14
|
+
// the per-call `maxAmount` bound below is the honest replacement for the
|
|
15
|
+
// first: it constrains the amount in THIS call rather than implying a rolling
|
|
16
|
+
// total nothing tracks.
|
|
17
|
+
//
|
|
18
|
+
// What a declaration can say maps one-to-one onto grammar 4:
|
|
19
|
+
// fn -> eq(call_fn, literal_symbol)
|
|
20
|
+
// contract -> eq(call_contract, literal_address)
|
|
21
|
+
// maxAmount -> lte(call_arg(i), literal_i128)
|
|
22
|
+
// recipients -> in(call_arg(j), [literal_address, ...])
|
|
23
|
+
// minOutputRatio-> gte(call_arg(out), call_arg_scaled(in, num, den))
|
|
24
|
+
import { isStellarAddress } from "./address.js";
|
|
25
|
+
/** Argument positions of the SEP-41 `transfer(from, to, amount)` shape. A
|
|
26
|
+
* declaration that names a different method almost certainly has different
|
|
27
|
+
* positions, which is why using either default emits a warning naming the
|
|
28
|
+
* index it assumed - a bound on the wrong argument constrains something the
|
|
29
|
+
* user did not mean and fails silently. */
|
|
30
|
+
const SEP41_RECIPIENT_ARG = 1;
|
|
31
|
+
const SEP41_AMOUNT_ARG = 2;
|
|
32
|
+
/** Lower a declared constraint to a grammar-3 predicate. Pure and total:
|
|
33
|
+
* the same declaration always produces the same predicate. */
|
|
34
|
+
export function declarePredicate(d) {
|
|
35
|
+
if (!d.fn || d.fn.trim() === '') {
|
|
36
|
+
throw declareError('SYNTHESIS_ERROR', 'a declaration needs `fn`: the method to pin');
|
|
37
|
+
}
|
|
38
|
+
const warnings = [];
|
|
39
|
+
const children = [
|
|
40
|
+
{ op: 'eq', left: { kind: 'call_fn' }, right: { kind: 'literal_symbol', value: d.fn } },
|
|
41
|
+
];
|
|
42
|
+
if (d.contract !== undefined) {
|
|
43
|
+
if (!isStellarAddress(d.contract) || !d.contract.startsWith('C')) {
|
|
44
|
+
throw declareError('SYNTHESIS_ERROR', `contract must be a Stellar contract address (C...), got ${d.contract}`);
|
|
45
|
+
}
|
|
46
|
+
children.push({
|
|
47
|
+
op: 'eq',
|
|
48
|
+
left: { kind: 'call_contract' },
|
|
49
|
+
right: { kind: 'literal_address', value: d.contract },
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (d.recipients !== undefined) {
|
|
53
|
+
if (d.recipients.length === 0) {
|
|
54
|
+
throw declareError('SYNTHESIS_ERROR', 'recipients was supplied but empty; an empty `in` haystack is refused at decode. Omit it to leave recipients unconstrained.');
|
|
55
|
+
}
|
|
56
|
+
for (const r of d.recipients) {
|
|
57
|
+
if (!isStellarAddress(r)) {
|
|
58
|
+
throw declareError('SYNTHESIS_ERROR', `recipient is not a Stellar address: ${r}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const idx = d.recipientArgIndex ?? SEP41_RECIPIENT_ARG;
|
|
62
|
+
if (d.recipientArgIndex === undefined) {
|
|
63
|
+
warnings.push(`recipient allowlist bound to call_arg(${idx}), the SEP-41 \`transfer\` position. If \`${d.fn}\` carries the recipient elsewhere this constrains the wrong argument - pass recipientArgIndex.`);
|
|
64
|
+
}
|
|
65
|
+
children.push({
|
|
66
|
+
op: 'in',
|
|
67
|
+
needle: { kind: 'call_arg', index: idx },
|
|
68
|
+
haystack: d.recipients.map((value) => ({ kind: 'literal_address', value })),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (d.maxAmount !== undefined) {
|
|
72
|
+
if (!/^[0-9]+$/.test(d.maxAmount)) {
|
|
73
|
+
throw declareError('SYNTHESIS_ERROR', `maxAmount must be an unsigned integer in the token's smallest unit, got "${d.maxAmount}" (25 XLM = "250000000")`);
|
|
74
|
+
}
|
|
75
|
+
if (d.maxAmount === '0' && d.allowZeroCap !== true) {
|
|
76
|
+
throw declareError('SYNTHESIS_ERROR', 'maxAmount "0" denies every call: no amount satisfies the bound. Set allowZeroCap to declare that deliberately.');
|
|
77
|
+
}
|
|
78
|
+
const idx = d.amountArgIndex ?? SEP41_AMOUNT_ARG;
|
|
79
|
+
if (d.amountArgIndex === undefined) {
|
|
80
|
+
warnings.push(`amount cap bound to call_arg(${idx}), the SEP-41 \`transfer\` position. If \`${d.fn}\` carries the amount elsewhere this caps the wrong argument - pass amountArgIndex.`);
|
|
81
|
+
}
|
|
82
|
+
children.push({
|
|
83
|
+
op: 'lte',
|
|
84
|
+
left: { kind: 'call_arg', index: idx },
|
|
85
|
+
right: { kind: 'literal_i128', value: d.maxAmount },
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (d.minOutputRatio !== undefined) {
|
|
89
|
+
const { num, den, inputArgIndex, outputArgIndex } = d.minOutputRatio;
|
|
90
|
+
if (!/^[0-9]+$/.test(num) || !/^[0-9]+$/.test(den)) {
|
|
91
|
+
throw declareError('SYNTHESIS_ERROR', `minOutputRatio num/den must be unsigned integers, got "${num}"/"${den}" (a 1% slippage tolerance is num "99", den "100")`);
|
|
92
|
+
}
|
|
93
|
+
// Both are refused on chain at install (INVALID_SCALED_RATIO). Refusing
|
|
94
|
+
// here too means the caller learns before a transaction is built.
|
|
95
|
+
if (den === '0') {
|
|
96
|
+
throw declareError('SYNTHESIS_ERROR', 'minOutputRatio.den is zero: the ratio has no value');
|
|
97
|
+
}
|
|
98
|
+
if (num === '0') {
|
|
99
|
+
throw declareError('SYNTHESIS_ERROR', 'minOutputRatio.num is zero: the floor would be zero, which constrains nothing. Omit it instead.');
|
|
100
|
+
}
|
|
101
|
+
if (inputArgIndex === outputArgIndex) {
|
|
102
|
+
throw declareError('SYNTHESIS_ERROR', `minOutputRatio bounds arg[${inputArgIndex}] against itself, which is true for any ratio at or below 1 and false above it - never a slippage floor. Pass the distinct input and output positions.`);
|
|
103
|
+
}
|
|
104
|
+
if (BigInt(num) > BigInt(den)) {
|
|
105
|
+
// Demanding MORE out than went in is not slippage protection; it is a
|
|
106
|
+
// rule that denies every honest trade. Loud beats a policy that never
|
|
107
|
+
// permits.
|
|
108
|
+
warnings.push(`minOutputRatio ${num}/${den} is above 1: it requires the output to EXCEED the input, which no ordinary swap satisfies. Check the ratio is not inverted.`);
|
|
109
|
+
}
|
|
110
|
+
children.push({
|
|
111
|
+
op: 'gte',
|
|
112
|
+
left: { kind: 'call_arg', index: outputArgIndex },
|
|
113
|
+
right: { kind: 'call_arg_scaled', index: inputArgIndex, num, den },
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
// A single conjunct is emitted bare. `and` with one child encodes to
|
|
117
|
+
// different bytes than the child alone, and the extra node buys nothing.
|
|
118
|
+
const predicate = children.length === 1 ? children[0] : { op: 'and', children };
|
|
119
|
+
return { predicate, warnings };
|
|
120
|
+
}
|
|
121
|
+
function declareError(code, message) {
|
|
122
|
+
const err = new Error(message);
|
|
123
|
+
err.code = code;
|
|
124
|
+
err.severity = 'error';
|
|
125
|
+
err.retryable = false;
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
package/dist/synth/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { isStellarAddress } from './address.ts';
|
|
2
2
|
export { type ComposeOptions, type ComposeResult, composeFromRecording, } from './compose-from-recording.ts';
|
|
3
|
+
export { type DeclaredPredicate, declarePredicate, type PolicyDeclaration, } from './declare.ts';
|
|
3
4
|
export { type IntentFacts, lower } from './lower.ts';
|
|
4
5
|
export { type DecideScopeOptions, decideScope, type ScopeDecision, scopeToContextRuleType, } from './scope.ts';
|
|
5
6
|
export { type __TestInterpreterAdapterOptions, type SynthesizeFromRecordingOptions, synthesizeFromRecording, } from './synthesize-from-recording.ts';
|
package/dist/synth/index.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// and re-deriving it elsewhere means a second address check that can drift.
|
|
5
5
|
export { isStellarAddress } from "./address.js";
|
|
6
6
|
export { composeFromRecording, } from "./compose-from-recording.js";
|
|
7
|
+
export { declarePredicate, } from "./declare.js";
|
|
7
8
|
export { lower } from "./lower.js";
|
|
8
9
|
export { decideScope, scopeToContextRuleType, } from "./scope.js";
|
|
9
10
|
export { synthesizeFromRecording, } from "./synthesize-from-recording.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/synth/synthesize-from-recording.ts - recording-path orchestrator.
|
|
2
2
|
//
|
|
3
3
|
// `synthesizeFromRecording` INFERS a bounded policy from a `RecordedTransaction`
|
|
4
|
-
// via the
|
|
4
|
+
// via the composer + interpreter adapter pair.
|
|
5
5
|
// Flow: validate -> parseConfidence gate -> lower -> decideScope -> composeFromRecording
|
|
6
6
|
// -> interpreter compile + self-verify.
|
|
7
7
|
// Same (tx, opts) -> byte-identical ProposedPolicy (no randomness, clock, globals).
|
package/dist/types.d.ts
CHANGED
|
@@ -124,7 +124,7 @@ export type PolicyRef = {
|
|
|
124
124
|
* `contracts/policy-interpreter/src/version.rs`. Every value this package puts on
|
|
125
125
|
* the wire derives from here; `grammar-version-parity.test.ts` pins it to the
|
|
126
126
|
* contract so a skew cannot pass a green test run. */
|
|
127
|
-
export declare const GRAMMAR_VERSION:
|
|
127
|
+
export declare const GRAMMAR_VERSION: 4;
|
|
128
128
|
/** A serialised predicate document stored against a (smart_account, rule_id) pair. */
|
|
129
129
|
export interface PolicyDocument {
|
|
130
130
|
/** Grammar version baked into the interpreter wasm. Fail-closed strict match at install
|
|
@@ -146,14 +146,29 @@ export interface PolicyDocument {
|
|
|
146
146
|
export type PredicateNode = {
|
|
147
147
|
op: 'and';
|
|
148
148
|
children: PredicateNode[];
|
|
149
|
+
} | {
|
|
150
|
+
op: 'or';
|
|
151
|
+
children: PredicateNode[];
|
|
149
152
|
} | {
|
|
150
153
|
op: 'eq';
|
|
151
154
|
left: PredicateLeaf;
|
|
152
155
|
right: PredicateLeaf;
|
|
156
|
+
} | {
|
|
157
|
+
op: 'lt';
|
|
158
|
+
left: PredicateLeaf;
|
|
159
|
+
right: PredicateLeaf;
|
|
153
160
|
} | {
|
|
154
161
|
op: 'lte';
|
|
155
162
|
left: PredicateLeaf;
|
|
156
163
|
right: PredicateLeaf;
|
|
164
|
+
} | {
|
|
165
|
+
op: 'gt';
|
|
166
|
+
left: PredicateLeaf;
|
|
167
|
+
right: PredicateLeaf;
|
|
168
|
+
} | {
|
|
169
|
+
op: 'gte';
|
|
170
|
+
left: PredicateLeaf;
|
|
171
|
+
right: PredicateLeaf;
|
|
157
172
|
} | {
|
|
158
173
|
op: 'in';
|
|
159
174
|
needle: PredicateLeaf;
|
|
@@ -174,6 +189,11 @@ export type PredicateLeaf = {
|
|
|
174
189
|
index: number;
|
|
175
190
|
element: number;
|
|
176
191
|
field: string;
|
|
192
|
+
} | {
|
|
193
|
+
kind: 'call_arg_scaled';
|
|
194
|
+
index: number;
|
|
195
|
+
num: string;
|
|
196
|
+
den: string;
|
|
177
197
|
} | {
|
|
178
198
|
kind: 'literal_address';
|
|
179
199
|
value: string;
|
package/dist/types.js
CHANGED
|
@@ -52,4 +52,4 @@ export const PREDICATE_CAPS = {
|
|
|
52
52
|
* `contracts/policy-interpreter/src/version.rs`. Every value this package puts on
|
|
53
53
|
* the wire derives from here; `grammar-version-parity.test.ts` pins it to the
|
|
54
54
|
* contract so a skew cannot pass a green test run. */
|
|
55
|
-
export const GRAMMAR_VERSION =
|
|
55
|
+
export const GRAMMAR_VERSION = 4;
|
|
@@ -12,9 +12,9 @@ export interface InterpreterAdapterConfig {
|
|
|
12
12
|
* rejected as `SCOPE_SELF_CALL`. */
|
|
13
13
|
smartAccountAddress: string;
|
|
14
14
|
}
|
|
15
|
-
/** The result of compiling a
|
|
15
|
+
/** The result of compiling a `ComposedRule` for this backend. */
|
|
16
16
|
export interface CompileResult {
|
|
17
|
-
/** false => some
|
|
17
|
+
/** false => some constraint this backend cannot express (see `uncovered`). */
|
|
18
18
|
covered: boolean;
|
|
19
19
|
/** Human-readable list of unsupported constructs. */
|
|
20
20
|
uncovered: string[];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// src/adapters/interpreter/adapter.ts - the interpreter-policy
|
|
2
|
+
// src/adapters/interpreter/adapter.ts - the interpreter-policy adapter.
|
|
3
3
|
//
|
|
4
|
-
// Compiles a
|
|
4
|
+
// Compiles a `ComposedRule` to a single interpreter `PolicyDocument` + `PolicyRef`
|
|
5
5
|
// carrying the canonical predicate encoding from `predicate/encode.ts`. This is
|
|
6
6
|
// the only backend; the compose step lowers every constraint to it.
|
|
7
7
|
//
|
|
@@ -138,7 +138,11 @@ function assertNoSelfCall(node, config) {
|
|
|
138
138
|
}
|
|
139
139
|
};
|
|
140
140
|
switch (node.op) {
|
|
141
|
+
// Both boolean nodes recurse. `or` is listed explicitly rather than left
|
|
142
|
+
// to a default branch: this is a security check, and a new node kind must
|
|
143
|
+
// fail to compile here rather than silently skip it.
|
|
141
144
|
case 'and':
|
|
145
|
+
case 'or':
|
|
142
146
|
for (const c of node.children)
|
|
143
147
|
assertNoSelfCall(c, config);
|
|
144
148
|
return;
|
|
@@ -147,7 +151,11 @@ function assertNoSelfCall(node, config) {
|
|
|
147
151
|
for (const h of node.haystack)
|
|
148
152
|
checkLeaf(h);
|
|
149
153
|
return;
|
|
150
|
-
|
|
154
|
+
case 'eq':
|
|
155
|
+
case 'lt':
|
|
156
|
+
case 'lte':
|
|
157
|
+
case 'gt':
|
|
158
|
+
case 'gte':
|
|
151
159
|
checkLeaf(node.left);
|
|
152
160
|
checkLeaf(node.right);
|
|
153
161
|
}
|
package/dist-cjs/errors.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
export type ErrorCode = 'RECORDING_FAILED' | 'RECORDING_VALIDATION_FAILED' | 'SCOPE_UNRESOLVED' | 'SYNTHESIS_ERROR' | 'MALFORMED_PREDICATE' | 'SIMULATION_ERROR' | 'VERIFICATION_FAILED' | 'DENY_CASE_FAILURE' | 'PERMIT_CASE_FAILED' | 'SUMMARY_DRIFT' | 'INSTALL_BUILD_FAILED' | 'INSTALL_CONFIRM_MISSING' | 'INSTALL_CONFIRM_EXPIRED' | 'REVOKE_BUILD_FAILED' | 'REVOKE_CONFIRM_MISSING' | 'USER_REJECTED_SIGN' | 'WALLET_TIMEOUT' | 'WALLET_UNAVAILABLE' | 'PREDICATE_TOO_LARGE' | 'PREDICATE_TOO_DEEP' | 'TOO_MANY_LEAVES' | 'IN_OPERAND_LIMIT' | 'POLICY_CAP_EXCEEDED' | 'WASM_TOO_LARGE' | 'MASTER_AUTH_REQUIRED' | 'NONCE_REPLAY' | 'VERSION_MISMATCH' | 'ARITHMETIC_OVERFLOW' | 'AMOUNT_OVERFLOW' | 'RULE_SIGNERS_CHANGED' | 'SCOPE_SELF_CALL' | 'ARG_MISMATCH' | 'CONTRACT_SCOPE' | 'UNSUPPORTED_NODE' | 'STATEFUL_BOUND' | 'NOT_IN_ALLOWLIST'
|
|
1
|
+
export type ErrorCode = 'RECORDING_FAILED' | 'RECORDING_VALIDATION_FAILED' | 'SCOPE_UNRESOLVED' | 'SYNTHESIS_ERROR' | 'MALFORMED_PREDICATE' | 'SIMULATION_ERROR' | 'VERIFICATION_FAILED' | 'DENY_CASE_FAILURE' | 'PERMIT_CASE_FAILED' | 'SUMMARY_DRIFT' | 'INSTALL_BUILD_FAILED' | 'INSTALL_CONFIRM_MISSING' | 'INSTALL_CONFIRM_EXPIRED' | 'REVOKE_BUILD_FAILED' | 'REVOKE_CONFIRM_MISSING' | 'USER_REJECTED_SIGN' | 'WALLET_TIMEOUT' | 'WALLET_UNAVAILABLE' | 'PREDICATE_TOO_LARGE' | 'PREDICATE_TOO_DEEP' | 'TOO_MANY_LEAVES' | 'IN_OPERAND_LIMIT' | 'POLICY_CAP_EXCEEDED' | 'WASM_TOO_LARGE' | 'MASTER_AUTH_REQUIRED' | 'NONCE_REPLAY' | 'VERSION_MISMATCH' | 'ARITHMETIC_OVERFLOW' | 'AMOUNT_OVERFLOW' | 'RULE_SIGNERS_CHANGED' | 'SCOPE_SELF_CALL' | 'ARG_MISMATCH' | 'CONTRACT_SCOPE' | 'UNSUPPORTED_NODE' | 'STATEFUL_BOUND' | 'NOT_IN_ALLOWLIST'
|
|
2
|
+
/** A comparison against a `call_arg_scaled` operand failed (contract 107). */
|
|
3
|
+
| 'SLIPPAGE_FLOOR'
|
|
4
|
+
/** A `call_arg_scaled` ratio is zero or non-positive. Refused at install
|
|
5
|
+
* (contract 214) and mirrored at encode. */
|
|
6
|
+
| 'INVALID_SCALED_RATIO' | 'COMPILE_OK' | 'COMPILE_GATE_FAILED';
|
|
2
7
|
export interface ToolError {
|
|
3
8
|
code: ErrorCode;
|
|
4
9
|
message: string;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { PredicateNode, SignerDraft } from '../types.ts';
|
|
2
|
+
/** Wildcard component of a `Selector`: the predicate does not pin this half. */
|
|
3
|
+
export declare const ANY = "*";
|
|
4
|
+
/** A (contract, function) pair a predicate may permit. `ANY` in either half
|
|
5
|
+
* means unconstrained, so `{contract: ANY, fn: ANY}` is "any call at all". */
|
|
6
|
+
export interface Selector {
|
|
7
|
+
contract: string;
|
|
8
|
+
fn: string;
|
|
9
|
+
}
|
|
10
|
+
export type ContextType = {
|
|
11
|
+
kind: 'default';
|
|
12
|
+
} | {
|
|
13
|
+
kind: 'call_contract';
|
|
14
|
+
contract: string;
|
|
15
|
+
} | {
|
|
16
|
+
kind: 'create_contract';
|
|
17
|
+
wasmHash: string;
|
|
18
|
+
};
|
|
19
|
+
/** How much can be said about a neighbouring rule.
|
|
20
|
+
* - `interpreter`: policed by our interpreter and the predicate was readable,
|
|
21
|
+
* so its authority is known exactly.
|
|
22
|
+
* - `foreign`: policed by some other contract. The address is visible, the
|
|
23
|
+
* semantics are not, so it needs review by hand.
|
|
24
|
+
* - `unpoliced`: no policy at all. Whatever its context type allows, its
|
|
25
|
+
* signers may do without constraint. */
|
|
26
|
+
export type RuleClass = 'interpreter' | 'foreign' | 'unpoliced';
|
|
27
|
+
export interface ObservedRule {
|
|
28
|
+
id: number;
|
|
29
|
+
contextType: ContextType;
|
|
30
|
+
signers: SignerDraft[];
|
|
31
|
+
/** Policy contract addresses attached to the rule, in OZ's order. */
|
|
32
|
+
policyAddresses: string[];
|
|
33
|
+
/** Decoded predicate. Present only when the rule is policed by OUR
|
|
34
|
+
* interpreter and the stored document was readable. */
|
|
35
|
+
predicate?: PredicateNode;
|
|
36
|
+
}
|
|
37
|
+
export interface IntendedInstall {
|
|
38
|
+
/** Rule the predicate is being installed onto. A re-install onto the same
|
|
39
|
+
* id REPLACES its predicate rather than adding a second source of
|
|
40
|
+
* authority, so that id is skipped. */
|
|
41
|
+
ruleId: number;
|
|
42
|
+
contextType: ContextType;
|
|
43
|
+
signers: SignerDraft[];
|
|
44
|
+
predicate: PredicateNode;
|
|
45
|
+
}
|
|
46
|
+
export type OverlapSeverity =
|
|
47
|
+
/** A neighbouring rule imposes no constraint at all on the shared calls. */
|
|
48
|
+
'bypass'
|
|
49
|
+
/** A neighbouring policy exists but what it permits cannot be read. */
|
|
50
|
+
| 'unknown'
|
|
51
|
+
/** Both rules are ours. The new rule will not restrict the shared calls,
|
|
52
|
+
* because the signer names whichever is more permissive. */
|
|
53
|
+
| 'not-restricting';
|
|
54
|
+
export interface AuthorityOverlap {
|
|
55
|
+
ruleId: number;
|
|
56
|
+
ruleClass: RuleClass;
|
|
57
|
+
severity: OverlapSeverity;
|
|
58
|
+
/** Signers present in BOTH rules. An overlap is only reachable by a signer
|
|
59
|
+
* who can name both, so a rule sharing no signer is not a collision. */
|
|
60
|
+
sharedSigners: SignerDraft[];
|
|
61
|
+
/** The selectors both rules can serve. Non-empty by construction. */
|
|
62
|
+
sharedSelectors: Selector[];
|
|
63
|
+
advice: string;
|
|
64
|
+
}
|
|
65
|
+
/** Canonical key for signer equality. Mirrors OZ's `Signer` enum: a delegated
|
|
66
|
+
* signer is its address, an external signer is the verifier plus the key
|
|
67
|
+
* bytes, since one verifier may hold many keys. */
|
|
68
|
+
export declare function signerKey(s: SignerDraft): string;
|
|
69
|
+
/** Intersection of two selector SETS: every compatible pairing survives. */
|
|
70
|
+
export declare function intersectSelectors(a: Selector[], b: Selector[]): Selector[];
|
|
71
|
+
/**
|
|
72
|
+
* The set of `(contract, fn)` selectors a predicate may permit.
|
|
73
|
+
*
|
|
74
|
+
* A deliberate OVER-approximation: every call the predicate actually permits is
|
|
75
|
+
* covered by some returned selector, and unrecognised structure widens to the
|
|
76
|
+
* wildcard rather than narrowing. That direction is what makes the emptiness
|
|
77
|
+
* test sound. A call carries exactly one `(contract, fn)`, so if two
|
|
78
|
+
* predicates' over-approximations do not intersect, no single call can be
|
|
79
|
+
* routed to either and the rules provably cannot collide.
|
|
80
|
+
*
|
|
81
|
+
* Narrowing instead would be the fail-OPEN direction: it would let this report
|
|
82
|
+
* "no overlap" for rules that do collide.
|
|
83
|
+
*/
|
|
84
|
+
export declare function permittedSelectors(node: PredicateNode): Selector[];
|
|
85
|
+
/** Selectors a context type admits, before the predicate narrows them. */
|
|
86
|
+
export declare function selectorsForContextType(ct: ContextType): Selector[];
|
|
87
|
+
/** What a rule can actually authorise: its context type narrowed by its
|
|
88
|
+
* predicate. An unpoliced or unreadable rule contributes no narrowing. */
|
|
89
|
+
export declare function effectiveSelectors(rule: ObservedRule): Selector[];
|
|
90
|
+
/**
|
|
91
|
+
* Every existing rule a signer of the intended install could name instead.
|
|
92
|
+
*
|
|
93
|
+
* A rule collides when it shares at least one signer AND at least one selector.
|
|
94
|
+
* Both are needed for the signer to have a choice: same signer but disjoint
|
|
95
|
+
* calls means no call can be rerouted, and same calls but no shared signer
|
|
96
|
+
* means nobody can reroute them.
|
|
97
|
+
*/
|
|
98
|
+
export declare function findAuthorityOverlaps(args: {
|
|
99
|
+
intended: IntendedInstall;
|
|
100
|
+
existing: ObservedRule[];
|
|
101
|
+
}): AuthorityOverlap[];
|