@callcium/sdk 0.12.0 → 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 +659 -611
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2285 -2071
- package/dist/index.js.map +1 -1
- package/package.json +13 -10
- package/src/bytes.ts +1 -0
- package/src/calldata-reader.ts +3 -3
- package/src/constants.ts +156 -86
- package/src/constraint.ts +34 -24
- package/src/descriptor-coder.ts +126 -103
- package/src/descriptor.ts +79 -288
- package/src/errors.ts +30 -19
- package/src/hint.ts +237 -0
- package/src/index.ts +29 -10
- package/src/operators.ts +168 -267
- package/src/path.ts +7 -31
- package/src/policy-builder.ts +201 -181
- package/src/policy-coder.ts +1156 -523
- package/src/policy-enforcer.ts +499 -486
- package/src/policy-validator.ts +557 -1046
- package/src/signature.ts +7 -11
- package/src/type-desc.ts +6 -6
- package/src/types.ts +230 -129
- 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(
|
|
@@ -125,14 +52,13 @@ function reject(written: bigint, folded: bigint): never {
|
|
|
125
52
|
|
|
126
53
|
/**
|
|
127
54
|
* Reject an operand that two's complement folds onto a value the target admits. Such an operand and the value it folds
|
|
128
|
-
* onto encode to one word
|
|
129
|
-
* more than one word, which leaves two ways across
|
|
130
|
-
* word
|
|
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.
|
|
131
58
|
*/
|
|
132
59
|
function checkOperandDomain(constraint: Constraint, typeCode: number): void {
|
|
133
|
-
const bounds = targetBounds(typeCode);
|
|
134
60
|
const extremes = readOperandExtremes(constraint);
|
|
135
|
-
if (
|
|
61
|
+
if (extremes === undefined) return;
|
|
136
62
|
|
|
137
63
|
// A negative operand encodes as its unsigned alias's word, which only a whole-word target can hold too.
|
|
138
64
|
if (extremes.leastNegative < 0n && typeCode === TypeCode.UINT_MAX) {
|
|
@@ -142,16 +68,68 @@ function checkOperandDomain(constraint: Constraint, typeCode: number): void {
|
|
|
142
68
|
if (!isSigned(typeCode)) return;
|
|
143
69
|
// An operand at or above the signed minimum's own word denotes that negative, and every such operand already exceeds
|
|
144
70
|
// the signed maximum.
|
|
145
|
-
|
|
71
|
+
const signedMinimumWord = WORD_VALUES - (1n << BigInt(8 * operandWidth(typeCode)! - 1));
|
|
72
|
+
if (extremes.greatest >= signedMinimumWord) {
|
|
146
73
|
reject(extremes.greatest, extremes.greatest - WORD_VALUES);
|
|
147
74
|
}
|
|
148
75
|
}
|
|
149
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
|
+
|
|
150
128
|
///////////////////////////////////////////////////////////////////////////
|
|
151
129
|
// PolicyBuilder
|
|
152
130
|
///////////////////////////////////////////////////////////////////////////
|
|
153
131
|
|
|
154
|
-
/** Fluent
|
|
132
|
+
/** Fluent API for drafting policies from constraints. */
|
|
155
133
|
export class PolicyBuilder {
|
|
156
134
|
private draft: PolicyDraft;
|
|
157
135
|
|
|
@@ -161,142 +139,160 @@ export class PolicyBuilder {
|
|
|
161
139
|
|
|
162
140
|
/**
|
|
163
141
|
* Create a builder from a function signature.
|
|
164
|
-
* @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.
|
|
165
145
|
*/
|
|
166
146
|
static create(signature: string): PolicyBuilder {
|
|
167
147
|
const parsed = SignatureParser.parse(signature);
|
|
168
148
|
const descriptor = DescriptorCoder.fromTypes(parsed.types);
|
|
169
|
-
return new PolicyBuilder({
|
|
170
|
-
isSelectorless: false,
|
|
171
|
-
selector: parsed.selector,
|
|
172
|
-
descriptor,
|
|
173
|
-
groups: [[]],
|
|
174
|
-
groupPathKeys: new Set(),
|
|
175
|
-
});
|
|
149
|
+
return new PolicyBuilder({ isSelectorless: false, selector: parsed.selector, descriptor, groups: [[]] });
|
|
176
150
|
}
|
|
177
151
|
|
|
178
152
|
/**
|
|
179
|
-
* Create a selectorless builder from a
|
|
180
|
-
* @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.
|
|
181
157
|
*/
|
|
182
158
|
static createRaw(typesCsv: string): PolicyBuilder {
|
|
183
159
|
const descriptor = DescriptorCoder.fromTypes(typesCsv);
|
|
184
|
-
return new PolicyBuilder({
|
|
185
|
-
isSelectorless: true,
|
|
186
|
-
selector: "0x00000000",
|
|
187
|
-
descriptor,
|
|
188
|
-
groups: [[]],
|
|
189
|
-
groupPathKeys: new Set(),
|
|
190
|
-
});
|
|
160
|
+
return new PolicyBuilder({ isSelectorless: true, selector: "0x00000000", descriptor, groups: [[]] });
|
|
191
161
|
}
|
|
192
162
|
|
|
193
163
|
/**
|
|
194
|
-
* Add a constraint to the
|
|
195
|
-
*
|
|
196
|
-
* @
|
|
197
|
-
*
|
|
198
|
-
*
|
|
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.
|
|
199
170
|
*/
|
|
200
171
|
add(constraint: Constraint): this {
|
|
201
|
-
|
|
202
|
-
const added: Constraint = {
|
|
203
|
-
scope: constraint.scope,
|
|
204
|
-
path: constraint.path,
|
|
205
|
-
operators: [...constraint.operators],
|
|
206
|
-
...("hint" in constraint && constraint.hint !== undefined && { hint: constraint.hint }),
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
if (added.operators.length === 0) {
|
|
172
|
+
if (constraint.operators.length === 0) {
|
|
210
173
|
throw new CallciumError("NO_CONSTRAINT_OPERATORS", "Constraint must have at least one operator");
|
|
211
174
|
}
|
|
212
175
|
|
|
213
176
|
// Path shape is established before the scope decides how to navigate it.
|
|
214
|
-
const steps = parsePathSteps(
|
|
177
|
+
const steps = parsePathSteps(constraint.path);
|
|
215
178
|
if (steps.length === 0) {
|
|
216
179
|
throw new CallciumError("EMPTY_PATH", "Path must have at least one step");
|
|
217
180
|
}
|
|
218
181
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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);
|
|
224
188
|
} else {
|
|
225
|
-
throw new CallciumError("
|
|
189
|
+
throw new CallciumError("UNKNOWN_SCOPE", `Unknown scope value ${scope}`);
|
|
226
190
|
}
|
|
227
|
-
|
|
228
|
-
checkOperandDomain(constraint, targetTypeCode);
|
|
229
|
-
|
|
230
|
-
const key = `${added.scope}:${added.path.toLowerCase()}`;
|
|
231
|
-
if (this.draft.groupPathKeys.has(key)) {
|
|
232
|
-
throw new CallciumError("DUPLICATE_PATH_IN_GROUP", `Duplicate path ${added.path} in the same group`);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
this.draft.groupPathKeys.add(key);
|
|
236
|
-
this.draft.groups[this.draft.groups.length - 1]!.push(added);
|
|
237
191
|
return this;
|
|
238
192
|
}
|
|
239
193
|
|
|
240
|
-
/**
|
|
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
|
+
*/
|
|
241
199
|
or(): this {
|
|
242
200
|
const lastGroup = this.draft.groups[this.draft.groups.length - 1]!;
|
|
243
201
|
if (lastGroup.length === 0) {
|
|
244
202
|
throw new CallciumError("EMPTY_GROUP", "Cannot start a new group when the current group is empty");
|
|
245
203
|
}
|
|
246
204
|
this.draft.groups.push([]);
|
|
247
|
-
this.draft.groupPathKeys.clear();
|
|
248
205
|
return this;
|
|
249
206
|
}
|
|
250
207
|
|
|
251
208
|
/**
|
|
252
|
-
* Build the policy
|
|
253
|
-
*
|
|
254
|
-
* @
|
|
255
|
-
* @throws {CallciumError} If
|
|
256
|
-
* @throws {ValidationError} If validation finds any issue.
|
|
257
|
-
* @throws {CallciumError} With code `RULE_SIZE_OVERFLOW` when a rule's encoded bytes exceed 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.
|
|
258
213
|
*/
|
|
259
214
|
build(): Hex {
|
|
260
|
-
this.
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
throw new ValidationError(issues);
|
|
265
|
-
}
|
|
266
|
-
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;
|
|
267
219
|
}
|
|
268
220
|
|
|
269
221
|
/**
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
* @
|
|
273
|
-
* @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.
|
|
274
225
|
*/
|
|
275
|
-
|
|
276
|
-
this.
|
|
277
|
-
return PolicyCoder.encode(this.toPolicyData());
|
|
226
|
+
validate(): Issue[] {
|
|
227
|
+
return PolicyValidator.validate(this.buildUnsafe());
|
|
278
228
|
}
|
|
279
229
|
|
|
280
230
|
/**
|
|
281
|
-
*
|
|
282
|
-
* @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.
|
|
283
234
|
*/
|
|
284
|
-
|
|
285
|
-
this.
|
|
286
|
-
const policyData = this.toPolicyData();
|
|
287
|
-
return PolicyValidator.validate(policyData);
|
|
235
|
+
buildUnsafe(): Hex {
|
|
236
|
+
return PolicyCoder.encode(this.toPolicyData());
|
|
288
237
|
}
|
|
289
238
|
|
|
290
|
-
/**
|
|
291
|
-
private
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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 });
|
|
295
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 });
|
|
296
287
|
}
|
|
297
288
|
}
|
|
298
289
|
|
|
299
|
-
/**
|
|
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. */
|
|
300
296
|
private toPolicyData(): PolicyData {
|
|
301
297
|
return {
|
|
302
298
|
isSelectorless: this.draft.isSelectorless,
|
|
@@ -306,3 +302,27 @@ export class PolicyBuilder {
|
|
|
306
302
|
};
|
|
307
303
|
}
|
|
308
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
|
+
}
|