@callcium/sdk 0.11.1 → 0.13.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/README.md +2 -2
- package/dist/index.d.ts +683 -622
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2297 -2051
- package/dist/index.js.map +1 -1
- package/package.json +13 -10
- package/src/bytes.ts +9 -25
- package/src/calldata-reader.ts +3 -3
- package/src/constants.ts +156 -82
- package/src/constraint.ts +61 -33
- package/src/descriptor-coder.ts +139 -125
- package/src/descriptor.ts +82 -292
- package/src/errors.ts +38 -30
- package/src/hint.ts +237 -0
- package/src/index.ts +30 -12
- package/src/operators.ts +173 -242
- package/src/path.ts +7 -32
- package/src/policy-builder.ts +205 -190
- package/src/policy-coder.ts +1160 -536
- package/src/policy-enforcer.ts +499 -488
- package/src/policy-validator.ts +563 -1045
- package/src/signature.ts +7 -11
- package/src/type-desc.ts +12 -12
- package/src/types.ts +232 -136
- package/src/validation-issue.ts +78 -465
package/src/policy-builder.ts
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
|
-
import { bytesToHex } from "./bytes";
|
|
2
|
-
import {
|
|
1
|
+
import { bigintToHex, bytesToHex, hexToBytes } from "./bytes";
|
|
2
|
+
import {
|
|
3
|
+
comparisonOf,
|
|
4
|
+
formatCode,
|
|
5
|
+
lookupContextProperty,
|
|
6
|
+
LoopQuantifier,
|
|
7
|
+
Op,
|
|
8
|
+
PolicyFormat as PF,
|
|
9
|
+
Scope,
|
|
10
|
+
Source,
|
|
11
|
+
sourceOf,
|
|
12
|
+
TypeCode,
|
|
13
|
+
} from "./constants";
|
|
3
14
|
import { readOperandExtremes } from "./constraint";
|
|
4
|
-
import { Descriptor } from "./descriptor";
|
|
5
15
|
import { DescriptorCoder } from "./descriptor-coder";
|
|
6
16
|
import { CallciumError, ValidationError } from "./errors";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
17
|
+
import { Hint } from "./hint";
|
|
18
|
+
import { canonicalize, isSigned, loadOperand, operandTypeOf, operandWidth, toBigInt } from "./operators";
|
|
19
|
+
import { encodePath, parsePathSteps, Quantifier } from "./path";
|
|
9
20
|
import { PolicyCoder } from "./policy-coder";
|
|
10
21
|
import { PolicyValidator } from "./policy-validator";
|
|
11
22
|
import { SignatureParser } from "./signature";
|
|
12
23
|
|
|
13
|
-
import type {
|
|
24
|
+
import type { Hex } from "./types";
|
|
25
|
+
import type { BodyRule, Constraint, Issue, PolicyData, Rule } from "./types";
|
|
14
26
|
|
|
15
27
|
///////////////////////////////////////////////////////////////////////////
|
|
16
28
|
// Internal types
|
|
@@ -20,80 +32,9 @@ type PolicyDraft = {
|
|
|
20
32
|
isSelectorless: boolean;
|
|
21
33
|
selector: Hex;
|
|
22
34
|
descriptor: Uint8Array;
|
|
23
|
-
groups:
|
|
24
|
-
/** Scope-qualified paths already taken in the group being built. */
|
|
25
|
-
groupPathKeys: Set<string>;
|
|
35
|
+
groups: Rule[][];
|
|
26
36
|
};
|
|
27
37
|
|
|
28
|
-
///////////////////////////////////////////////////////////////////////////
|
|
29
|
-
// Path validation
|
|
30
|
-
///////////////////////////////////////////////////////////////////////////
|
|
31
|
-
|
|
32
|
-
/** Validate a context-scope path and return the referenced property's type code. */
|
|
33
|
-
function validateContextPath(steps: number[]): number {
|
|
34
|
-
if (steps.length !== 1) {
|
|
35
|
-
throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope path must be exactly one step");
|
|
36
|
-
}
|
|
37
|
-
return lookupContextProperty(steps[0]!).typeCode;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Validate a calldata-scope path against the descriptor and return the target's type code. */
|
|
41
|
-
function validateCalldataPath(steps: number[], desc: Uint8Array): number {
|
|
42
|
-
const argIndex = steps[0]!;
|
|
43
|
-
const paramCount = Descriptor.paramCount(desc);
|
|
44
|
-
if (argIndex >= paramCount) {
|
|
45
|
-
throw new CallciumError(
|
|
46
|
-
"PARAM_INDEX_OUT_OF_BOUNDS",
|
|
47
|
-
`Param index ${argIndex} out of range (descriptor has ${paramCount} params)`,
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
let offset = Descriptor.paramOffset(desc, argIndex);
|
|
52
|
-
let hasQuantifier = false;
|
|
53
|
-
|
|
54
|
-
for (let i = 1; i < steps.length; i++) {
|
|
55
|
-
const step = steps[i]!;
|
|
56
|
-
const info = Descriptor.inspect(desc, offset);
|
|
57
|
-
const isArray = info.typeCode === TypeCode.STATIC_ARRAY || info.typeCode === TypeCode.DYNAMIC_ARRAY;
|
|
58
|
-
|
|
59
|
-
if (isQuantifier(step)) {
|
|
60
|
-
if (!isArray) {
|
|
61
|
-
throw new CallciumError("QUANTIFIER_ON_NON_ARRAY", "Quantifier step is only valid on an array node");
|
|
62
|
-
}
|
|
63
|
-
if (hasQuantifier) {
|
|
64
|
-
throw new CallciumError("NESTED_QUANTIFIER", "Nested quantifiers are not allowed");
|
|
65
|
-
}
|
|
66
|
-
hasQuantifier = true;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (info.typeCode === TypeCode.TUPLE) {
|
|
70
|
-
const fieldCount = Descriptor.tupleFieldCount(desc, offset);
|
|
71
|
-
if (step >= fieldCount) {
|
|
72
|
-
throw new CallciumError(
|
|
73
|
-
"TUPLE_FIELD_OUT_OF_BOUNDS",
|
|
74
|
-
`Tuple field index ${step} out of range (tuple has ${fieldCount} fields)`,
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
offset = Descriptor.tupleFieldOffset(desc, offset, step);
|
|
78
|
-
} else if (isArray) {
|
|
79
|
-
if (!isQuantifier(step) && info.typeCode === TypeCode.STATIC_ARRAY) {
|
|
80
|
-
const arrayLength = Descriptor.staticArrayLength(desc, offset);
|
|
81
|
-
if (step >= arrayLength) {
|
|
82
|
-
throw new CallciumError(
|
|
83
|
-
"STATIC_ARRAY_INDEX_OUT_OF_BOUNDS",
|
|
84
|
-
`Array index ${step} out of range (static array has ${arrayLength} elements)`,
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
offset = Descriptor.arrayElementOffset(offset);
|
|
89
|
-
} else {
|
|
90
|
-
throw new CallciumError("NOT_COMPOSITE", "Cannot descend into an elementary type");
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return Descriptor.inspect(desc, offset).typeCode;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
38
|
///////////////////////////////////////////////////////////////////////////
|
|
98
39
|
// Operand domain
|
|
99
40
|
///////////////////////////////////////////////////////////////////////////
|
|
@@ -101,20 +42,6 @@ function validateCalldataPath(steps: number[], desc: Uint8Array): number {
|
|
|
101
42
|
// Values a 32-byte word represents, which is the distance between an operand and its alias.
|
|
102
43
|
const WORD_VALUES = 1n << 256n;
|
|
103
44
|
|
|
104
|
-
/** Integers an integer target admits, or null for any other type. */
|
|
105
|
-
function targetBounds(typeCode: number): { min: bigint; max: bigint } | null {
|
|
106
|
-
if (typeCode >= TypeCode.UINT_MIN && typeCode <= TypeCode.UINT_MAX) {
|
|
107
|
-
const bits = BigInt(typeCode - TypeCode.UINT_MIN + 1) * 8n;
|
|
108
|
-
return { min: 0n, max: (1n << bits) - 1n };
|
|
109
|
-
}
|
|
110
|
-
if (typeCode >= TypeCode.INT_MIN && typeCode <= TypeCode.INT_MAX) {
|
|
111
|
-
const bits = BigInt(typeCode - TypeCode.INT_MIN + 1) * 8n;
|
|
112
|
-
const half = 1n << (bits - 1n);
|
|
113
|
-
return { min: -half, max: half - 1n };
|
|
114
|
-
}
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
45
|
/** Reject an operand outside the target's range whose word carries a value inside it. */
|
|
119
46
|
function reject(written: bigint, folded: bigint): never {
|
|
120
47
|
throw new CallciumError(
|
|
@@ -124,36 +51,85 @@ function reject(written: bigint, folded: bigint): never {
|
|
|
124
51
|
}
|
|
125
52
|
|
|
126
53
|
/**
|
|
127
|
-
* Reject an operand that two's complement folds onto a value the target admits.
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* reaches a signed range only at that type's own minimum word.
|
|
54
|
+
* Reject an operand that two's complement folds onto a value the target admits. Such an operand and the value it folds
|
|
55
|
+
* onto encode to one word. The operands as written are the only place the two stay distinct. An operand spans no
|
|
56
|
+
* more than one word, which leaves two ways across. A negative's alias clears every target narrower than the whole
|
|
57
|
+
* word. A positive reaches a signed range only at that type's own minimum word.
|
|
132
58
|
*/
|
|
133
59
|
function checkOperandDomain(constraint: Constraint, typeCode: number): void {
|
|
134
|
-
const bounds = targetBounds(typeCode);
|
|
135
60
|
const extremes = readOperandExtremes(constraint);
|
|
136
|
-
if (
|
|
61
|
+
if (extremes === undefined) return;
|
|
137
62
|
|
|
138
|
-
// A negative operand
|
|
139
|
-
// the whole word can hold too.
|
|
63
|
+
// A negative operand encodes as its unsigned alias's word, which only a whole-word target can hold too.
|
|
140
64
|
if (extremes.leastNegative < 0n && typeCode === TypeCode.UINT_MAX) {
|
|
141
65
|
reject(extremes.leastNegative, extremes.leastNegative + WORD_VALUES);
|
|
142
66
|
}
|
|
143
67
|
|
|
144
68
|
if (!isSigned(typeCode)) return;
|
|
145
|
-
// An operand at or above the signed minimum's own word denotes that negative, and every such
|
|
146
|
-
//
|
|
147
|
-
|
|
69
|
+
// An operand at or above the signed minimum's own word denotes that negative, and every such operand already exceeds
|
|
70
|
+
// the signed maximum.
|
|
71
|
+
const signedMinimumWord = WORD_VALUES - (1n << BigInt(8 * operandWidth(typeCode)! - 1));
|
|
72
|
+
if (extremes.greatest >= signedMinimumWord) {
|
|
148
73
|
reject(extremes.greatest, extremes.greatest - WORD_VALUES);
|
|
149
74
|
}
|
|
150
75
|
}
|
|
151
76
|
|
|
77
|
+
/** Reject an operand word its operand type cannot represent. */
|
|
78
|
+
function outOfRange(word: bigint): never {
|
|
79
|
+
throw new CallciumError(
|
|
80
|
+
"OUT_OF_PHYSICAL_BOUNDS",
|
|
81
|
+
`Operand ${bigintToHex(word)} is outside the range of its operand type`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Require every comparison of `constraint` to apply to a target of `typeCode` and every constant to be a value its
|
|
87
|
+
* operand type can represent. Return the operators with each mask as its operand type's word.
|
|
88
|
+
*/
|
|
89
|
+
function checkOperands(constraint: Constraint, typeCode: number): Hex[] {
|
|
90
|
+
checkOperandDomain(constraint, typeCode);
|
|
91
|
+
|
|
92
|
+
return constraint.operators.map((operatorHex) => {
|
|
93
|
+
const operator = hexToBytes(operatorHex);
|
|
94
|
+
// Only an opcode followed by whole words carries constants to check.
|
|
95
|
+
if (operator.length <= 1 || (operator.length - 1) % 32 !== 0) return operatorHex;
|
|
96
|
+
const opCode = operator[0]!;
|
|
97
|
+
const comparison = comparisonOf(opCode);
|
|
98
|
+
const operandType = operandTypeOf(comparison, typeCode);
|
|
99
|
+
if (operandType === undefined) {
|
|
100
|
+
throw new CallciumError("OPERATOR_TARGET_MISMATCH", "Comparison does not apply to the target type");
|
|
101
|
+
}
|
|
102
|
+
if (sourceOf(opCode) !== Source.CONSTANT) return operatorHex;
|
|
103
|
+
|
|
104
|
+
// The members of a set fill at most what the data length field holds.
|
|
105
|
+
const width = operandWidth(operandType)!;
|
|
106
|
+
const limit = Math.floor((2 ** (8 * PF.RULE_DATALENGTH_SIZE) - 1) / width);
|
|
107
|
+
if ((operator.length - 1) / 32 > limit) {
|
|
108
|
+
throw new CallciumError("SET_TOO_LARGE", `Set must contain at most ${limit} values`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const isMask = comparison >= Op.BITMASK_ALL && comparison <= Op.BITMASK_NONE;
|
|
112
|
+
const checked = operator.slice();
|
|
113
|
+
for (let offset = 1; offset < operator.length; offset += 32) {
|
|
114
|
+
const word = toBigInt(operator, offset);
|
|
115
|
+
if (!isMask) {
|
|
116
|
+
if (canonicalize(word, operandType) !== word) outOfRange(word);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
// A mask is a bit pattern of the type's width, or -1 for every bit.
|
|
120
|
+
if (word !== WORD_VALUES - 1n && word >> BigInt(8 * width) !== 0n) outOfRange(word);
|
|
121
|
+
const pattern = hexToBytes(bigintToHex(word)).subarray(32 - width);
|
|
122
|
+
checked.set(hexToBytes(bigintToHex(loadOperand(pattern, 0, operandType))), offset);
|
|
123
|
+
}
|
|
124
|
+
return bytesToHex(checked);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
152
128
|
///////////////////////////////////////////////////////////////////////////
|
|
153
129
|
// PolicyBuilder
|
|
154
130
|
///////////////////////////////////////////////////////////////////////////
|
|
155
131
|
|
|
156
|
-
/** Fluent
|
|
132
|
+
/** Fluent API for drafting policies from constraints. */
|
|
157
133
|
export class PolicyBuilder {
|
|
158
134
|
private draft: PolicyDraft;
|
|
159
135
|
|
|
@@ -163,145 +139,160 @@ export class PolicyBuilder {
|
|
|
163
139
|
|
|
164
140
|
/**
|
|
165
141
|
* Create a builder from a function signature.
|
|
166
|
-
* @param signature -
|
|
142
|
+
* @param signature - The function signature (for example "transfer(address,uint256)").
|
|
143
|
+
* @returns A new builder.
|
|
144
|
+
* @throws {CallciumError} If the signature is malformed.
|
|
167
145
|
*/
|
|
168
146
|
static create(signature: string): PolicyBuilder {
|
|
169
147
|
const parsed = SignatureParser.parse(signature);
|
|
170
148
|
const descriptor = DescriptorCoder.fromTypes(parsed.types);
|
|
171
|
-
return new PolicyBuilder({
|
|
172
|
-
isSelectorless: false,
|
|
173
|
-
selector: parsed.selector,
|
|
174
|
-
descriptor,
|
|
175
|
-
groups: [[]],
|
|
176
|
-
groupPathKeys: new Set(),
|
|
177
|
-
});
|
|
149
|
+
return new PolicyBuilder({ isSelectorless: false, selector: parsed.selector, descriptor, groups: [[]] });
|
|
178
150
|
}
|
|
179
151
|
|
|
180
152
|
/**
|
|
181
|
-
* Create a selectorless builder from a
|
|
182
|
-
* @param typesCsv -
|
|
153
|
+
* Create a selectorless builder from a comma-separated type list.
|
|
154
|
+
* @param typesCsv - The comma-separated ABI types (for example "uint256,address").
|
|
155
|
+
* @returns A new builder.
|
|
156
|
+
* @throws {CallciumError} If a type is malformed.
|
|
183
157
|
*/
|
|
184
158
|
static createRaw(typesCsv: string): PolicyBuilder {
|
|
185
159
|
const descriptor = DescriptorCoder.fromTypes(typesCsv);
|
|
186
|
-
return new PolicyBuilder({
|
|
187
|
-
isSelectorless: true,
|
|
188
|
-
selector: "0x00000000",
|
|
189
|
-
descriptor,
|
|
190
|
-
groups: [[]],
|
|
191
|
-
groupPathKeys: new Set(),
|
|
192
|
-
});
|
|
160
|
+
return new PolicyBuilder({ isSelectorless: true, selector: "0x00000000", descriptor, groups: [[]] });
|
|
193
161
|
}
|
|
194
162
|
|
|
195
163
|
/**
|
|
196
|
-
* Add a constraint to the
|
|
197
|
-
*
|
|
198
|
-
* @
|
|
199
|
-
*
|
|
200
|
-
*
|
|
164
|
+
* Add a constraint to the active group. A path through `Quantifier.ALL` or `Quantifier.ANY` adds its operators to the
|
|
165
|
+
* body of the group's loop over that array with that quantifier.
|
|
166
|
+
* @param constraint - The constraint to add.
|
|
167
|
+
* @returns This builder.
|
|
168
|
+
* @throws {CallciumError} If the constraint has no operator, or its path does not reach a target in the signature.
|
|
169
|
+
* If a comparison does not apply to its target, or an operand is a value its target's type cannot represent.
|
|
201
170
|
*/
|
|
202
171
|
add(constraint: Constraint): this {
|
|
203
|
-
|
|
204
|
-
const added: Constraint = {
|
|
205
|
-
scope: constraint.scope,
|
|
206
|
-
path: constraint.path,
|
|
207
|
-
operators: [...constraint.operators],
|
|
208
|
-
...("hint" in constraint && constraint.hint !== undefined && { hint: constraint.hint }),
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
if (added.operators.length === 0) {
|
|
172
|
+
if (constraint.operators.length === 0) {
|
|
212
173
|
throw new CallciumError("NO_CONSTRAINT_OPERATORS", "Constraint must have at least one operator");
|
|
213
174
|
}
|
|
214
175
|
|
|
215
176
|
// Path shape is established before the scope decides how to navigate it.
|
|
216
|
-
const steps = parsePathSteps(
|
|
177
|
+
const steps = parsePathSteps(constraint.path);
|
|
217
178
|
if (steps.length === 0) {
|
|
218
179
|
throw new CallciumError("EMPTY_PATH", "Path must have at least one step");
|
|
219
180
|
}
|
|
220
181
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
182
|
+
// A caller from JavaScript can pass any scope, so the value is read as a number.
|
|
183
|
+
const scope: number = constraint.scope;
|
|
184
|
+
if (scope === Scope.CONTEXT) {
|
|
185
|
+
this.addContextTests(constraint, steps);
|
|
186
|
+
} else if (scope === Scope.CALLDATA) {
|
|
187
|
+
this.addCalldataTests(constraint, steps);
|
|
226
188
|
} else {
|
|
227
|
-
throw new CallciumError("
|
|
189
|
+
throw new CallciumError("UNKNOWN_SCOPE", `Unknown scope value ${scope}`);
|
|
228
190
|
}
|
|
229
|
-
|
|
230
|
-
checkOperandDomain(constraint, targetTypeCode);
|
|
231
|
-
|
|
232
|
-
const key = `${added.scope}:${added.path.toLowerCase()}`;
|
|
233
|
-
if (this.draft.groupPathKeys.has(key)) {
|
|
234
|
-
throw new CallciumError("DUPLICATE_PATH_IN_GROUP", `Duplicate path ${added.path} in the same group`);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
this.draft.groupPathKeys.add(key);
|
|
238
|
-
this.draft.groups[this.draft.groups.length - 1]!.push(added);
|
|
239
191
|
return this;
|
|
240
192
|
}
|
|
241
193
|
|
|
242
|
-
/**
|
|
194
|
+
/**
|
|
195
|
+
* Start a new constraint group (OR semantics between groups).
|
|
196
|
+
* @returns This builder.
|
|
197
|
+
* @throws {CallciumError} If the current group is empty.
|
|
198
|
+
*/
|
|
243
199
|
or(): this {
|
|
244
200
|
const lastGroup = this.draft.groups[this.draft.groups.length - 1]!;
|
|
245
201
|
if (lastGroup.length === 0) {
|
|
246
202
|
throw new CallciumError("EMPTY_GROUP", "Cannot start a new group when the current group is empty");
|
|
247
203
|
}
|
|
248
204
|
this.draft.groups.push([]);
|
|
249
|
-
this.draft.groupPathKeys.clear();
|
|
250
205
|
return this;
|
|
251
206
|
}
|
|
252
207
|
|
|
253
208
|
/**
|
|
254
|
-
* Build the policy
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
* @
|
|
258
|
-
* @throws {CallciumError} If any group is empty.
|
|
259
|
-
* @throws {ValidationError} If validation finds any issue.
|
|
260
|
-
* @throws {CallciumError} With code `RULE_SIZE_OVERFLOW` when a rule's encoded bytes exceed the
|
|
261
|
-
* width of the rule size field.
|
|
209
|
+
* Build the policy blob and validate it.
|
|
210
|
+
* @returns The encoded policy.
|
|
211
|
+
* @throws {ValidationError} If validation reports any issue.
|
|
212
|
+
* @throws {CallciumError} If the encoder refuses the policy, or the blob fails storage validation.
|
|
262
213
|
*/
|
|
263
214
|
build(): Hex {
|
|
264
|
-
this.
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
throw new ValidationError(issues);
|
|
269
|
-
}
|
|
270
|
-
return PolicyCoder.encode(policyData);
|
|
215
|
+
const blob = this.buildUnsafe();
|
|
216
|
+
const issues = PolicyValidator.validate(blob);
|
|
217
|
+
if (issues.length > 0) throw new ValidationError(issues);
|
|
218
|
+
return blob;
|
|
271
219
|
}
|
|
272
220
|
|
|
273
221
|
/**
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
* @returns The policy as a 0x-prefixed hex string.
|
|
278
|
-
* @throws {CallciumError} If any group is empty.
|
|
222
|
+
* Validate the policy the builder contains.
|
|
223
|
+
* @returns Every issue validation reports on the encoded policy.
|
|
224
|
+
* @throws {CallciumError} If the encoder refuses the policy, or the blob fails storage validation.
|
|
279
225
|
*/
|
|
280
|
-
|
|
281
|
-
this.
|
|
282
|
-
return PolicyCoder.encode(this.toPolicyData());
|
|
226
|
+
validate(): Issue[] {
|
|
227
|
+
return PolicyValidator.validate(this.buildUnsafe());
|
|
283
228
|
}
|
|
284
229
|
|
|
285
230
|
/**
|
|
286
|
-
*
|
|
287
|
-
* @returns
|
|
231
|
+
* Build the policy blob without validation. The resulting policy can be invalid.
|
|
232
|
+
* @returns The encoded policy.
|
|
233
|
+
* @throws {CallciumError} If the encoder refuses the policy.
|
|
288
234
|
*/
|
|
289
|
-
|
|
290
|
-
this.
|
|
291
|
-
const policyData = this.toPolicyData();
|
|
292
|
-
return PolicyValidator.validate(policyData);
|
|
235
|
+
buildUnsafe(): Hex {
|
|
236
|
+
return PolicyCoder.encode(this.toPolicyData());
|
|
293
237
|
}
|
|
294
238
|
|
|
295
|
-
/**
|
|
296
|
-
private
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
239
|
+
/** Append a context test per operator of `constraint`, whose path names a context property. */
|
|
240
|
+
private addContextTests(constraint: Constraint, steps: number[]): void {
|
|
241
|
+
if (steps.length !== 1) {
|
|
242
|
+
throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope path must be exactly one step");
|
|
243
|
+
}
|
|
244
|
+
const { typeCode } = lookupContextProperty(steps[0]!);
|
|
245
|
+
for (const operator of checkOperands(constraint, typeCode)) {
|
|
246
|
+
this.currentGroup().push({ scope: Scope.CONTEXT, path: constraint.path, operator });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Append a calldata test per operator of `constraint`. If the path of `constraint` passes a quantifier step, append a
|
|
252
|
+
* body test per operator to the group's loop over that array instead.
|
|
253
|
+
*/
|
|
254
|
+
private addCalldataTests(constraint: Constraint, steps: number[]): void {
|
|
255
|
+
const desc = this.draft.descriptor;
|
|
256
|
+
const quantifierIndex = findQuantifierIndex(steps);
|
|
257
|
+
|
|
258
|
+
if (quantifierIndex === undefined) {
|
|
259
|
+
const target = Hint.requireCompiled(Hint.compileTest(desc, steps), steps, 0);
|
|
260
|
+
for (const operator of checkOperands(constraint, desc[target.node]!)) {
|
|
261
|
+
this.currentGroup().push({ scope: Scope.CALLDATA, path: constraint.path, operator });
|
|
300
262
|
}
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// The quantifier splits the path into the array's path and the body test's path from the element.
|
|
267
|
+
const arraySteps = steps.slice(0, quantifierIndex);
|
|
268
|
+
const bodySteps = steps.slice(quantifierIndex + 1);
|
|
269
|
+
const loop = Hint.requireCompiled(Hint.compileLoop(desc, arraySteps), steps, 0);
|
|
270
|
+
const target = Hint.requireCompiled(Hint.compileTest(desc, bodySteps, loop.node), steps, quantifierIndex + 1);
|
|
271
|
+
|
|
272
|
+
const bodyPath = encodePath(bodySteps);
|
|
273
|
+
const tests: BodyRule[] = checkOperands(constraint, desc[target.node]!).map((operator) => ({
|
|
274
|
+
scope: Scope.CALLDATA,
|
|
275
|
+
path: bodyPath,
|
|
276
|
+
operator,
|
|
277
|
+
}));
|
|
278
|
+
const quantifier = steps[quantifierIndex] === Quantifier.ALL ? LoopQuantifier.ALL : LoopQuantifier.ANY;
|
|
279
|
+
const arrayPath = encodePath(arraySteps);
|
|
280
|
+
const existing = this.currentGroup().find(
|
|
281
|
+
(rule) => rule.scope === Scope.LOOP && rule.quantifier === quantifier && rule.path === arrayPath,
|
|
282
|
+
);
|
|
283
|
+
if (existing !== undefined && existing.scope === Scope.LOOP) {
|
|
284
|
+
existing.body.push(...tests);
|
|
285
|
+
} else {
|
|
286
|
+
this.currentGroup().push({ scope: Scope.LOOP, quantifier, path: arrayPath, body: tests });
|
|
301
287
|
}
|
|
302
288
|
}
|
|
303
289
|
|
|
304
|
-
/**
|
|
290
|
+
/** The active group. */
|
|
291
|
+
private currentGroup(): Rule[] {
|
|
292
|
+
return this.draft.groups[this.draft.groups.length - 1]!;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** The draft as the encoder takes it. */
|
|
305
296
|
private toPolicyData(): PolicyData {
|
|
306
297
|
return {
|
|
307
298
|
isSelectorless: this.draft.isSelectorless,
|
|
@@ -311,3 +302,27 @@ export class PolicyBuilder {
|
|
|
311
302
|
};
|
|
312
303
|
}
|
|
313
304
|
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Return the index of the path's quantifier step, or undefined when the path has none.
|
|
308
|
+
* The first step is a parameter index.
|
|
309
|
+
* @throws {CallciumError} On a second quantifier step and on any other step in the reserved marker range.
|
|
310
|
+
*/
|
|
311
|
+
function findQuantifierIndex(steps: readonly number[]): number | undefined {
|
|
312
|
+
let quantifierIndex: number | undefined;
|
|
313
|
+
for (let stepIndex = 1; stepIndex < steps.length; stepIndex++) {
|
|
314
|
+
const step = steps[stepIndex]!;
|
|
315
|
+
if (step < PF.PATH_MARKER_BASE) continue;
|
|
316
|
+
if (step !== Quantifier.ALL && step !== Quantifier.ANY) {
|
|
317
|
+
throw new CallciumError(
|
|
318
|
+
"UNKNOWN_PATH_STEP",
|
|
319
|
+
`Path step ${formatCode(step, PF.PATH_STEP_SIZE)} is in the reserved marker range`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
if (quantifierIndex !== undefined) {
|
|
323
|
+
throw new CallciumError("INVALID_NESTED_LOOP", "Loop body holds a loop");
|
|
324
|
+
}
|
|
325
|
+
quantifierIndex = stepIndex;
|
|
326
|
+
}
|
|
327
|
+
return quantifierIndex;
|
|
328
|
+
}
|