@callcium/sdk 0.9.0 → 0.11.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/index.d.ts +202 -145
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +763 -667
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/bytes.ts +9 -9
- package/src/calldata-reader.ts +14 -1
- package/src/constants.ts +85 -181
- package/src/constraint.ts +131 -94
- package/src/descriptor-coder.ts +53 -84
- package/src/descriptor.ts +65 -36
- package/src/errors.ts +12 -5
- package/src/index.ts +8 -17
- package/src/operators.ts +147 -11
- package/src/path.ts +80 -0
- package/src/policy-builder.ts +107 -44
- package/src/policy-coder.ts +102 -77
- package/src/policy-enforcer.ts +98 -128
- package/src/policy-validator.ts +70 -56
- package/src/signature.ts +21 -19
- package/src/type-desc.ts +15 -15
- package/src/types.ts +70 -3
- package/src/validation-issue.ts +4 -4
package/src/policy-validator.ts
CHANGED
|
@@ -1,32 +1,29 @@
|
|
|
1
1
|
import { bigintToHex, bytesToHex, hexToBytes } from "./bytes";
|
|
2
|
-
import {
|
|
3
|
-
classifyTypeCode,
|
|
4
|
-
isQuantifier,
|
|
5
|
-
isValidOperatorData,
|
|
6
|
-
Limits,
|
|
7
|
-
lookupContextProperty,
|
|
8
|
-
MAX_CONTEXT_PROPERTY_ID,
|
|
9
|
-
Op,
|
|
10
|
-
Quantifier,
|
|
11
|
-
Scope,
|
|
12
|
-
TypeCode,
|
|
13
|
-
} from "./constants";
|
|
2
|
+
import { fieldMax, findContextProperty, MAX_CONTEXT_PROPERTY_ID, Op, PolicyFormat, Scope, TypeCode } from "./constants";
|
|
14
3
|
import { Descriptor, type TypeInfo } from "./descriptor";
|
|
15
4
|
import { decodeDescriptor } from "./descriptor-coder";
|
|
16
5
|
import { CallciumError, type CallciumErrorCode } from "./errors";
|
|
17
|
-
import {
|
|
18
|
-
|
|
6
|
+
import {
|
|
7
|
+
canonicalize,
|
|
8
|
+
isLeftAligned,
|
|
9
|
+
isSigned,
|
|
10
|
+
isLengthOp,
|
|
11
|
+
isLengthValidType,
|
|
12
|
+
isElementary,
|
|
13
|
+
isValidOperatorData,
|
|
14
|
+
toOperatorBytes,
|
|
15
|
+
} from "./operators";
|
|
16
|
+
import { Quantifier, isQuantifier, parsePathSteps } from "./path";
|
|
19
17
|
import * as ValidationIssue from "./validation-issue";
|
|
20
18
|
|
|
21
|
-
import type { Constraint, Hex, Issue, PolicyData } from "./types";
|
|
19
|
+
import type { Constraint, Hex, Issue, IssueCode, PolicyData } from "./types";
|
|
22
20
|
|
|
23
21
|
///////////////////////////////////////////////////////////////////////////
|
|
24
22
|
// Internal types
|
|
25
23
|
///////////////////////////////////////////////////////////////////////////
|
|
26
24
|
|
|
27
25
|
/** Codes a path walk raises when the path does not navigate the descriptor (PV-1). */
|
|
28
|
-
const
|
|
29
|
-
"EMPTY_PATH",
|
|
26
|
+
const navigabilityCodes: ReadonlySet<CallciumErrorCode> = new Set<CallciumErrorCode>([
|
|
30
27
|
"PARAM_INDEX_OUT_OF_BOUNDS",
|
|
31
28
|
"TUPLE_FIELD_OUT_OF_BOUNDS",
|
|
32
29
|
"STATIC_ARRAY_INDEX_OUT_OF_BOUNDS",
|
|
@@ -91,8 +88,15 @@ const INT256_SIGN_BIT = 1n << 255n;
|
|
|
91
88
|
/** Signed-aware difference of two raw uint256 values: negative if a<b, zero if equal, positive if a>b. */
|
|
92
89
|
function signedCompare(a: bigint, b: bigint, signed: boolean): bigint {
|
|
93
90
|
if (!signed) return a - b;
|
|
94
|
-
const
|
|
95
|
-
return
|
|
91
|
+
const toSigned = (word: bigint) => (word >= INT256_SIGN_BIT ? word - (1n << 256n) : word);
|
|
92
|
+
return toSigned(a) - toSigned(b);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** True when a value lies outside the domain's physical bounds. */
|
|
96
|
+
function isOutsideDomain(domain: BoundDomain, value: bigint): boolean {
|
|
97
|
+
return (
|
|
98
|
+
signedCompare(value, domain.min, domain.isSigned) < 0n || signedCompare(value, domain.max, domain.isSigned) > 0n
|
|
99
|
+
);
|
|
96
100
|
}
|
|
97
101
|
|
|
98
102
|
///////////////////////////////////////////////////////////////////////////
|
|
@@ -159,13 +163,13 @@ function isBitmaskOp(opBase: number): boolean {
|
|
|
159
163
|
///////////////////////////////////////////////////////////////////////////
|
|
160
164
|
|
|
161
165
|
/** Return the incompatibility reason, or null if the operator is allowed. */
|
|
162
|
-
function getIncompat(opBase: number, typeInfo: TypeInfo): { code:
|
|
166
|
+
function getIncompat(opBase: number, typeInfo: TypeInfo): { code: IssueCode; message: string } | null {
|
|
163
167
|
const { typeCode, isDynamic, staticSize } = typeInfo;
|
|
164
168
|
if (isValueOp(opBase)) {
|
|
165
169
|
if (isDynamic || staticSize !== 32) {
|
|
166
170
|
return { code: "VALUE_OP_ON_DYNAMIC", message: "Value operator used on dynamic type" };
|
|
167
171
|
}
|
|
168
|
-
if (
|
|
172
|
+
if (!isElementary(typeCode)) {
|
|
169
173
|
return { code: "VALUE_OP_ON_COMPOSITE", message: "Value operator used on composite type" };
|
|
170
174
|
}
|
|
171
175
|
if (isComparisonOp(opBase) && !isNumericType(typeCode)) {
|
|
@@ -307,7 +311,8 @@ function initContext(scope: number, path: Hex, steps: number[], typeInfo: TypeIn
|
|
|
307
311
|
steps,
|
|
308
312
|
typeInfo,
|
|
309
313
|
numeric: emptyBoundDomain(isSigned(typeInfo.typeCode), min, max),
|
|
310
|
-
|
|
314
|
+
// Bits above the target's width are zero in every canonical value.
|
|
315
|
+
bitmask: { mustBeOne: 0n, mustBeZero: isBitmaskCompatible(typeInfo.typeCode) ? UINT256_MAX ^ max : 0n },
|
|
311
316
|
length: emptyBoundDomain(false, 0n, LENGTH_MAX),
|
|
312
317
|
set: {
|
|
313
318
|
hasIn: false,
|
|
@@ -334,6 +339,10 @@ function updateBound(
|
|
|
334
339
|
): void {
|
|
335
340
|
if (isNegated) {
|
|
336
341
|
if (base === Op.EQ) {
|
|
342
|
+
// A hole outside the type's domain excludes nothing.
|
|
343
|
+
if (isOutsideDomain(domain, value)) {
|
|
344
|
+
issues.push(ValidationIssue.outOfPhysicalBounds(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
345
|
+
}
|
|
337
346
|
if (domain.hasEq && domain.eq === value) {
|
|
338
347
|
issues.push(ValidationIssue.eqNeqContradiction(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
339
348
|
}
|
|
@@ -361,10 +370,7 @@ function updateBound(
|
|
|
361
370
|
}
|
|
362
371
|
|
|
363
372
|
// Physical bounds and impossibility.
|
|
364
|
-
if (
|
|
365
|
-
signedCompare(value, domain.min, domain.isSigned) < 0n ||
|
|
366
|
-
signedCompare(value, domain.max, domain.isSigned) > 0n
|
|
367
|
-
) {
|
|
373
|
+
if (isOutsideDomain(domain, value)) {
|
|
368
374
|
issues.push(ValidationIssue.outOfPhysicalBounds(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
369
375
|
} else if (base === Op.GT && value === domain.max) {
|
|
370
376
|
issues.push(ValidationIssue.impossibleGt(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
@@ -684,6 +690,13 @@ function updateSet(
|
|
|
684
690
|
constraintIndex: number,
|
|
685
691
|
issues: Issue[],
|
|
686
692
|
): void {
|
|
693
|
+
// Physical bounds: a member outside the type's domain never occurs, so it neither matches nor excludes anything.
|
|
694
|
+
for (const value of values) {
|
|
695
|
+
if (isOutsideDomain(ctx.numeric, value)) {
|
|
696
|
+
issues.push(ValidationIssue.outOfPhysicalBounds(false, groupIndex, constraintIndex, bigintToHex(value)));
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
687
700
|
if (isNegated) {
|
|
688
701
|
for (const value of values) {
|
|
689
702
|
if (ctx.numeric.hasEq && ctx.numeric.eq === value) {
|
|
@@ -705,16 +718,6 @@ function updateSet(
|
|
|
705
718
|
}
|
|
706
719
|
checkSetEmpty(ctx, groupIndex, constraintIndex, issues);
|
|
707
720
|
} else {
|
|
708
|
-
// Physical bounds: a member outside the type's domain can never be matched.
|
|
709
|
-
for (const value of values) {
|
|
710
|
-
if (
|
|
711
|
-
signedCompare(value, ctx.numeric.min, ctx.numeric.isSigned) < 0n ||
|
|
712
|
-
signedCompare(value, ctx.numeric.max, ctx.numeric.isSigned) > 0n
|
|
713
|
-
) {
|
|
714
|
-
issues.push(ValidationIssue.outOfPhysicalBounds(false, groupIndex, constraintIndex, bigintToHex(value)));
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
|
|
718
721
|
if (ctx.numeric.hasEq) {
|
|
719
722
|
let found = false;
|
|
720
723
|
for (const candidate of values) {
|
|
@@ -820,6 +823,11 @@ function checkFusibleRange(
|
|
|
820
823
|
}
|
|
821
824
|
}
|
|
822
825
|
|
|
826
|
+
/** Check whether a payload length fits the `dataLength` field and matches the operator's arity. */
|
|
827
|
+
function hasValidPayload(opBase: number, dataLength: number): boolean {
|
|
828
|
+
return dataLength <= fieldMax(PolicyFormat.RULE_DATALENGTH_SIZE) && isValidOperatorData(opBase, dataLength);
|
|
829
|
+
}
|
|
830
|
+
|
|
823
831
|
/** Return the operand pair when the operators contain exactly one of each unnegated bound opcode. */
|
|
824
832
|
function findLonePair(operators: Hex[], lowerOp: number, upperOp: number): { low: bigint; high: bigint } | null {
|
|
825
833
|
let lowerCount = 0;
|
|
@@ -830,7 +838,7 @@ function findLonePair(operators: Hex[], lowerOp: number, upperOp: number): { low
|
|
|
830
838
|
for (const opHex of operators) {
|
|
831
839
|
const opCode = parseInt(opHex.slice(2, 4), 16);
|
|
832
840
|
const dataLength = (opHex.length - 4) / 2;
|
|
833
|
-
if (
|
|
841
|
+
if (!hasValidPayload(opCode & ~Op.NOT, dataLength)) continue;
|
|
834
842
|
if (opCode === lowerOp) {
|
|
835
843
|
lowerCount++;
|
|
836
844
|
low = readValue(opHex);
|
|
@@ -859,12 +867,14 @@ function validateConstraint(
|
|
|
859
867
|
const underAny = constraint.scope === Scope.CALLDATA && ctx.steps.includes(Quantifier.ANY);
|
|
860
868
|
|
|
861
869
|
for (const opHex of operators) {
|
|
862
|
-
|
|
870
|
+
// Every later read slices the hex, so the operator is established as bytes first.
|
|
871
|
+
const opBytes = toOperatorBytes(opHex);
|
|
872
|
+
const opCode = opBytes[0]!;
|
|
863
873
|
const base = opCode & ~Op.NOT;
|
|
864
874
|
|
|
865
875
|
// An unassigned opcode or mismatched payload size has no defined semantics to analyze.
|
|
866
|
-
const dataLength =
|
|
867
|
-
if (
|
|
876
|
+
const dataLength = opBytes.length - 1;
|
|
877
|
+
if (!hasValidPayload(base, dataLength)) {
|
|
868
878
|
issues.push(
|
|
869
879
|
ValidationIssue.fromOpRule(
|
|
870
880
|
"UNKNOWN_OPERATOR",
|
|
@@ -901,7 +911,8 @@ function validateConstraint(
|
|
|
901
911
|
if (base === Op.EQ_CTX) {
|
|
902
912
|
// The operand names a context property, not a value: nothing to fold into a domain.
|
|
903
913
|
const ctxOperand = readValue(opHex);
|
|
904
|
-
|
|
914
|
+
const property = findContextProperty(Number(ctxOperand));
|
|
915
|
+
if (!property) {
|
|
905
916
|
issues.push(
|
|
906
917
|
ValidationIssue.unknownContextProperty(
|
|
907
918
|
groupIndex,
|
|
@@ -913,7 +924,7 @@ function validateConstraint(
|
|
|
913
924
|
} else {
|
|
914
925
|
// Compatibility has already narrowed the target to an address or unsigned type,
|
|
915
926
|
// so the pairing reduces to whether both sides are addresses.
|
|
916
|
-
const propertyIsAddress =
|
|
927
|
+
const propertyIsAddress = property.typeCode === TypeCode.ADDRESS;
|
|
917
928
|
const targetIsAddress = ctx.typeInfo.typeCode === TypeCode.ADDRESS;
|
|
918
929
|
if (propertyIsAddress !== targetIsAddress) {
|
|
919
930
|
issues.push(
|
|
@@ -1033,16 +1044,17 @@ function validateGroup(data: PolicyData, descBytes: Uint8Array, groupIndex: numb
|
|
|
1033
1044
|
|
|
1034
1045
|
if (!ctx) {
|
|
1035
1046
|
const steps = parsePathSteps(constraint.path);
|
|
1047
|
+
if (steps.length === 0) throw new CallciumError("EMPTY_PATH", "Path must have at least one step");
|
|
1036
1048
|
let typeInfo: TypeInfo;
|
|
1037
1049
|
if (constraint.scope === Scope.CALLDATA) {
|
|
1038
1050
|
// Compatibility warnings against the limits the spec fixes (PWF-17, PV-7).
|
|
1039
|
-
if (steps.length >
|
|
1051
|
+
if (steps.length > PolicyFormat.MAX_PATH_DEPTH) {
|
|
1040
1052
|
issues.push(
|
|
1041
1053
|
ValidationIssue.pathDepthExceeded(
|
|
1042
1054
|
groupIndex,
|
|
1043
1055
|
constraintIndex,
|
|
1044
1056
|
bigintToHex(BigInt(steps.length)),
|
|
1045
|
-
bigintToHex(BigInt(
|
|
1057
|
+
bigintToHex(BigInt(PolicyFormat.MAX_PATH_DEPTH)),
|
|
1046
1058
|
),
|
|
1047
1059
|
);
|
|
1048
1060
|
}
|
|
@@ -1051,7 +1063,7 @@ function validateGroup(data: PolicyData, descBytes: Uint8Array, groupIndex: numb
|
|
|
1051
1063
|
walk = Descriptor.walkPath(descBytes, steps);
|
|
1052
1064
|
} catch (error) {
|
|
1053
1065
|
// Navigability faults become issues; anything else propagates.
|
|
1054
|
-
if (error instanceof CallciumError &&
|
|
1066
|
+
if (error instanceof CallciumError && navigabilityCodes.has(error.code)) {
|
|
1055
1067
|
issues.push(ValidationIssue.unnavigablePath(groupIndex, constraintIndex));
|
|
1056
1068
|
continue;
|
|
1057
1069
|
}
|
|
@@ -1062,20 +1074,21 @@ function validateGroup(data: PolicyData, descBytes: Uint8Array, groupIndex: numb
|
|
|
1062
1074
|
issues.push(ValidationIssue.nestedQuantifier(groupIndex, constraintIndex));
|
|
1063
1075
|
continue;
|
|
1064
1076
|
}
|
|
1065
|
-
if (walk.quantifiedStaticLength >
|
|
1077
|
+
if (walk.quantifiedStaticLength > PolicyFormat.MAX_QUANTIFIED_ARRAY_LENGTH) {
|
|
1066
1078
|
issues.push(
|
|
1067
1079
|
ValidationIssue.quantifierOverStaticLimit(
|
|
1068
1080
|
groupIndex,
|
|
1069
1081
|
constraintIndex,
|
|
1070
1082
|
bigintToHex(BigInt(walk.quantifiedStaticLength)),
|
|
1071
|
-
bigintToHex(BigInt(
|
|
1083
|
+
bigintToHex(BigInt(PolicyFormat.MAX_QUANTIFIED_ARRAY_LENGTH)),
|
|
1072
1084
|
),
|
|
1073
1085
|
);
|
|
1074
1086
|
}
|
|
1075
1087
|
typeInfo = walk.typeInfo;
|
|
1076
1088
|
} else {
|
|
1077
1089
|
const ctxId = steps[0]!;
|
|
1078
|
-
|
|
1090
|
+
const property = findContextProperty(ctxId);
|
|
1091
|
+
if (!property) {
|
|
1079
1092
|
// An unassigned ID declares no type, so the analysis proceeds over the widest
|
|
1080
1093
|
// unsigned domain and the warning carries the ID.
|
|
1081
1094
|
issues.push(
|
|
@@ -1086,24 +1099,22 @@ function validateGroup(data: PolicyData, descBytes: Uint8Array, groupIndex: numb
|
|
|
1086
1099
|
bigintToHex(BigInt(MAX_CONTEXT_PROPERTY_ID)),
|
|
1087
1100
|
),
|
|
1088
1101
|
);
|
|
1089
|
-
typeInfo = { typeCode: TypeCode.UINT_MAX, isDynamic: false, staticSize: 32 };
|
|
1090
|
-
} else {
|
|
1091
|
-
const typeCode = lookupContextProperty(ctxId).typeCode;
|
|
1092
|
-
typeInfo = { typeCode, isDynamic: false, staticSize: 32 };
|
|
1093
1102
|
}
|
|
1103
|
+
typeInfo = { typeCode: property?.typeCode ?? TypeCode.UINT_MAX, isDynamic: false, staticSize: 32 };
|
|
1094
1104
|
}
|
|
1095
1105
|
ctx = initContext(constraint.scope, normalizedPath, steps, typeInfo);
|
|
1096
1106
|
contexts.push(ctx);
|
|
1097
1107
|
}
|
|
1098
1108
|
|
|
1099
1109
|
// A carried hint must equal the compilation of its own path; constraints without an encoding
|
|
1100
|
-
// carry none, and the encoder compiles theirs from
|
|
1101
|
-
// the path navigates the descriptor and quantifies at
|
|
1102
|
-
// admits remains to be established.
|
|
1110
|
+
// carry none, an empty one carries nothing to compare, and the encoder compiles theirs from
|
|
1111
|
+
// the same descriptor. Reaching here means the path navigates the descriptor and quantifies at
|
|
1112
|
+
// most once, so only the depth PWF-17 admits remains to be established.
|
|
1103
1113
|
if (
|
|
1104
1114
|
constraint.scope === Scope.CALLDATA &&
|
|
1105
1115
|
constraint.hint !== undefined &&
|
|
1106
|
-
|
|
1116
|
+
constraint.hint !== "0x" &&
|
|
1117
|
+
ctx.steps.length <= PolicyFormat.MAX_PATH_DEPTH
|
|
1107
1118
|
) {
|
|
1108
1119
|
const compiled = bytesToHex(Descriptor.compileHint(descBytes, ctx.steps));
|
|
1109
1120
|
if (compiled !== constraint.hint.toLowerCase()) {
|
|
@@ -1127,6 +1138,9 @@ function validateGroup(data: PolicyData, descBytes: Uint8Array, groupIndex: numb
|
|
|
1127
1138
|
* @returns All validation issues found, ordered by group and constraint index.
|
|
1128
1139
|
* @throws {CallciumError} If the descriptor is not well-formed; semantic validation is
|
|
1129
1140
|
* defined only over a well-formed descriptor.
|
|
1141
|
+
* @throws {CallciumError} With code `EMPTY_PATH` when a constraint carries no path step, and
|
|
1142
|
+
* `INVALID_HEX` or `INVALID_OPERATOR_BYTES` when an operator is not a readable encoding. Analysis
|
|
1143
|
+
* is defined over canonical data, so these reject the input rather than report an issue on it.
|
|
1130
1144
|
*/
|
|
1131
1145
|
function validate(data: PolicyData): Issue[] {
|
|
1132
1146
|
const descBytes = hexToBytes(data.descriptor);
|
package/src/signature.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { keccak_256 } from "@noble/hashes/sha3.js";
|
|
2
|
+
import { utf8ToBytes } from "@noble/hashes/utils.js";
|
|
2
3
|
|
|
3
4
|
import { bytesToHex } from "./bytes";
|
|
4
5
|
import { CallciumError } from "./errors";
|
|
@@ -18,13 +19,13 @@ export type ParsedSignature = {
|
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
/** Return true if the char code is an ASCII letter. */
|
|
21
|
-
function isAlpha(
|
|
22
|
-
return (
|
|
22
|
+
function isAlpha(char: number): boolean {
|
|
23
|
+
return (char >= 0x41 && char <= 0x5a) || (char >= 0x61 && char <= 0x7a);
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
/** Return true if the char code is an ASCII alphanumeric character. */
|
|
26
|
-
function isAlphanum(
|
|
27
|
-
return isAlpha(
|
|
27
|
+
function isAlphanum(char: number): boolean {
|
|
28
|
+
return isAlpha(char) || (char >= 0x30 && char <= 0x39);
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
/**
|
|
@@ -36,29 +37,27 @@ function isAlphanum(c: number): boolean {
|
|
|
36
37
|
* @param signature - A function signature string, e.g. `"transfer(address,uint256)"`.
|
|
37
38
|
* @returns The 4-byte selector and the comma-separated types string.
|
|
38
39
|
* @throws {CallciumError} with code `"MALFORMED_SIGNATURE"`, `"SIGNATURE_CONTAINS_WHITESPACE"`, or
|
|
39
|
-
* `"INVALID_FUNCTION_NAME"` if the signature is malformed.
|
|
40
|
+
* `"INVALID_FUNCTION_NAME"` if the signature is malformed. The types between the parentheses are
|
|
41
|
+
* returned unread; whether they name types is for the type parser to say.
|
|
40
42
|
*/
|
|
41
43
|
function parse(signature: string): ParsedSignature {
|
|
42
44
|
if (signature.length < 3) {
|
|
43
45
|
throw new CallciumError("MALFORMED_SIGNATURE", `Invalid signature: too short, got "${signature}"`);
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
// Scan for whitespace
|
|
48
|
+
// Scan for whitespace and the opening parenthesis in one pass.
|
|
47
49
|
let openParen = -1;
|
|
48
50
|
for (let i = 0; i < signature.length; i++) {
|
|
49
|
-
const
|
|
50
|
-
if (
|
|
51
|
+
const char = signature.charCodeAt(i);
|
|
52
|
+
if (char === 0x20 || char === 0x09 || char === 0x0a || char === 0x0d) {
|
|
51
53
|
throw new CallciumError("SIGNATURE_CONTAINS_WHITESPACE", "Signature must not contain whitespace");
|
|
52
54
|
}
|
|
53
|
-
if (
|
|
54
|
-
throw new CallciumError("MALFORMED_SIGNATURE", "Signature must contain only ASCII characters");
|
|
55
|
-
}
|
|
56
|
-
if (openParen === -1 && c === 0x28) {
|
|
55
|
+
if (openParen === -1 && char === 0x28) {
|
|
57
56
|
openParen = i;
|
|
58
57
|
}
|
|
59
58
|
}
|
|
60
59
|
|
|
61
|
-
if (openParen
|
|
60
|
+
if (openParen === -1) {
|
|
62
61
|
throw new CallciumError(
|
|
63
62
|
"MALFORMED_SIGNATURE",
|
|
64
63
|
`Invalid signature: must have a function name followed by parentheses, got "${signature}"`,
|
|
@@ -69,14 +68,19 @@ function parse(signature: string): ParsedSignature {
|
|
|
69
68
|
throw new CallciumError("MALFORMED_SIGNATURE", `Invalid signature: must end with ")", got "${signature}"`);
|
|
70
69
|
}
|
|
71
70
|
|
|
72
|
-
|
|
71
|
+
if (openParen === 0) {
|
|
72
|
+
throw new CallciumError("INVALID_FUNCTION_NAME", "Function name must not be empty");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Validate function name: [A-Za-z_][A-Za-z0-9_]*. Every character outside ASCII carries a code
|
|
76
|
+
// unit above the alphanumeric ranges, so the same test rejects it.
|
|
73
77
|
const firstChar = signature.charCodeAt(0);
|
|
74
78
|
if (!isAlpha(firstChar) && firstChar !== 0x5f) {
|
|
75
79
|
throw new CallciumError("INVALID_FUNCTION_NAME", "Function name must start with a letter or underscore");
|
|
76
80
|
}
|
|
77
81
|
for (let i = 1; i < openParen; i++) {
|
|
78
|
-
const
|
|
79
|
-
if (!isAlphanum(
|
|
82
|
+
const char = signature.charCodeAt(i);
|
|
83
|
+
if (!isAlphanum(char) && char !== 0x5f) {
|
|
80
84
|
throw new CallciumError(
|
|
81
85
|
"INVALID_FUNCTION_NAME",
|
|
82
86
|
"Function name must contain only alphanumeric characters or underscores",
|
|
@@ -84,9 +88,7 @@ function parse(signature: string): ParsedSignature {
|
|
|
84
88
|
}
|
|
85
89
|
}
|
|
86
90
|
|
|
87
|
-
const
|
|
88
|
-
for (let i = 0; i < signature.length; i++) encoded[i] = signature.charCodeAt(i);
|
|
89
|
-
const hash = keccak_256(encoded);
|
|
91
|
+
const hash = keccak_256(utf8ToBytes(signature));
|
|
90
92
|
|
|
91
93
|
const selector = bytesToHex(hash.subarray(0, 4));
|
|
92
94
|
const types = signature.slice(openParen + 1, signature.length - 1);
|
package/src/type-desc.ts
CHANGED
|
@@ -48,7 +48,7 @@ export function function_(): Uint8Array {
|
|
|
48
48
|
*/
|
|
49
49
|
export function uintN(bits: number): Uint8Array {
|
|
50
50
|
if (bits < 8 || bits > 256 || bits % 8 !== 0) {
|
|
51
|
-
throw new CallciumError("UNKNOWN_TYPE", `Invalid uintN bits: ${bits}. Must be 8–256 in steps of 8
|
|
51
|
+
throw new CallciumError("UNKNOWN_TYPE", `Invalid uintN bits: ${bits}. Must be 8–256 in steps of 8`);
|
|
52
52
|
}
|
|
53
53
|
return new Uint8Array([TypeCode.UINT_MIN + (bits / 8 - 1)]);
|
|
54
54
|
}
|
|
@@ -60,7 +60,7 @@ export function uintN(bits: number): Uint8Array {
|
|
|
60
60
|
*/
|
|
61
61
|
export function intN(bits: number): Uint8Array {
|
|
62
62
|
if (bits < 8 || bits > 256 || bits % 8 !== 0) {
|
|
63
|
-
throw new CallciumError("UNKNOWN_TYPE", `Invalid intN bits: ${bits}. Must be 8–256 in steps of 8
|
|
63
|
+
throw new CallciumError("UNKNOWN_TYPE", `Invalid intN bits: ${bits}. Must be 8–256 in steps of 8`);
|
|
64
64
|
}
|
|
65
65
|
return new Uint8Array([TypeCode.INT_MIN + (bits / 8 - 1)]);
|
|
66
66
|
}
|
|
@@ -77,14 +77,14 @@ export function string_(): Uint8Array {
|
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
79
|
* Build a descriptor node for `bytesN`.
|
|
80
|
-
* @param
|
|
81
|
-
* @throws {CallciumError} With code `UNKNOWN_TYPE` if
|
|
80
|
+
* @param length - Byte width, 1–32.
|
|
81
|
+
* @throws {CallciumError} With code `UNKNOWN_TYPE` if length is out of range.
|
|
82
82
|
*/
|
|
83
|
-
export function bytesN(
|
|
84
|
-
if (
|
|
85
|
-
throw new CallciumError("UNKNOWN_TYPE", `Invalid bytesN size: ${
|
|
83
|
+
export function bytesN(length: number): Uint8Array {
|
|
84
|
+
if (length < 1 || length > 32) {
|
|
85
|
+
throw new CallciumError("UNKNOWN_TYPE", `Invalid bytesN size: ${length}. Must be 1–32`);
|
|
86
86
|
}
|
|
87
|
-
return new Uint8Array([TypeCode.FIXED_BYTES_MIN + (
|
|
87
|
+
return new Uint8Array([TypeCode.FIXED_BYTES_MIN + (length - 1)]);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
///////////////////////////////////////////////////////////////////////////
|
|
@@ -108,7 +108,7 @@ export function array(elemDesc: Uint8Array, length?: number): Uint8Array {
|
|
|
108
108
|
if (nodeLength > DF.MAX_NODE_LENGTH) {
|
|
109
109
|
throw new CallciumError(
|
|
110
110
|
"NODE_LENGTH_TOO_LARGE",
|
|
111
|
-
`Dynamic array node length ${nodeLength} exceeds maximum ${DF.MAX_NODE_LENGTH}
|
|
111
|
+
`Dynamic array node length ${nodeLength} exceeds maximum ${DF.MAX_NODE_LENGTH}`,
|
|
112
112
|
);
|
|
113
113
|
}
|
|
114
114
|
const meta24 = (0 << DF.META_STATIC_WORDS_SHIFT) | nodeLength;
|
|
@@ -123,14 +123,14 @@ export function array(elemDesc: Uint8Array, length?: number): Uint8Array {
|
|
|
123
123
|
if (length <= 0 || length > DF.MAX_STATIC_ARRAY_LENGTH) {
|
|
124
124
|
throw new CallciumError(
|
|
125
125
|
"INVALID_ARRAY_LENGTH",
|
|
126
|
-
`Static array length ${length} out of range. Must be 1–${DF.MAX_STATIC_ARRAY_LENGTH}
|
|
126
|
+
`Static array length ${length} out of range. Must be 1–${DF.MAX_STATIC_ARRAY_LENGTH}`,
|
|
127
127
|
);
|
|
128
128
|
}
|
|
129
129
|
const nodeLength = DF.ARRAY_HEADER_SIZE + elemDesc.length + DF.ARRAY_LENGTH_SIZE;
|
|
130
130
|
if (nodeLength > DF.MAX_NODE_LENGTH) {
|
|
131
131
|
throw new CallciumError(
|
|
132
132
|
"NODE_LENGTH_TOO_LARGE",
|
|
133
|
-
`Static array node length ${nodeLength} exceeds maximum ${DF.MAX_NODE_LENGTH}
|
|
133
|
+
`Static array node length ${nodeLength} exceeds maximum ${DF.MAX_NODE_LENGTH}`,
|
|
134
134
|
);
|
|
135
135
|
}
|
|
136
136
|
const elemStaticWords = extractStaticWords(elemDesc);
|
|
@@ -138,7 +138,7 @@ export function array(elemDesc: Uint8Array, length?: number): Uint8Array {
|
|
|
138
138
|
if (staticWords > DF.MAX_STATIC_WORDS) {
|
|
139
139
|
throw new CallciumError(
|
|
140
140
|
"STATIC_WORDS_TOO_LARGE",
|
|
141
|
-
`Static array static words ${staticWords} exceeds maximum ${DF.MAX_STATIC_WORDS}
|
|
141
|
+
`Static array static words ${staticWords} exceeds maximum ${DF.MAX_STATIC_WORDS}`,
|
|
142
142
|
);
|
|
143
143
|
}
|
|
144
144
|
const meta24 = (staticWords << DF.META_STATIC_WORDS_SHIFT) | nodeLength;
|
|
@@ -162,7 +162,7 @@ export function tuple(fieldDescs: Uint8Array[]): Uint8Array {
|
|
|
162
162
|
if (fieldDescs.length === 0 || fieldDescs.length > DF.MAX_TUPLE_FIELDS) {
|
|
163
163
|
throw new CallciumError(
|
|
164
164
|
"INVALID_TUPLE_FIELD_COUNT",
|
|
165
|
-
`Tuple field count ${fieldDescs.length} out of range. Must be 1–${DF.MAX_TUPLE_FIELDS}
|
|
165
|
+
`Tuple field count ${fieldDescs.length} out of range. Must be 1–${DF.MAX_TUPLE_FIELDS}`,
|
|
166
166
|
);
|
|
167
167
|
}
|
|
168
168
|
const totalFieldBytes = fieldDescs.reduce((sum, f) => sum + f.length, 0);
|
|
@@ -170,7 +170,7 @@ export function tuple(fieldDescs: Uint8Array[]): Uint8Array {
|
|
|
170
170
|
if (nodeLength > DF.MAX_NODE_LENGTH) {
|
|
171
171
|
throw new CallciumError(
|
|
172
172
|
"NODE_LENGTH_TOO_LARGE",
|
|
173
|
-
`Tuple node length ${nodeLength} exceeds maximum ${DF.MAX_NODE_LENGTH}
|
|
173
|
+
`Tuple node length ${nodeLength} exceeds maximum ${DF.MAX_NODE_LENGTH}`,
|
|
174
174
|
);
|
|
175
175
|
}
|
|
176
176
|
|
|
@@ -187,7 +187,7 @@ export function tuple(fieldDescs: Uint8Array[]): Uint8Array {
|
|
|
187
187
|
if (staticWords > DF.MAX_STATIC_WORDS) {
|
|
188
188
|
throw new CallciumError(
|
|
189
189
|
"STATIC_WORDS_TOO_LARGE",
|
|
190
|
-
`Tuple static words ${staticWords} exceeds maximum ${DF.MAX_STATIC_WORDS}
|
|
190
|
+
`Tuple static words ${staticWords} exceeds maximum ${DF.MAX_STATIC_WORDS}`,
|
|
191
191
|
);
|
|
192
192
|
}
|
|
193
193
|
const meta24 = (staticWords << DF.META_STATIC_WORDS_SHIFT) | nodeLength;
|
package/src/types.ts
CHANGED
|
@@ -130,7 +130,7 @@ export type CalldataNavigationViolation =
|
|
|
130
130
|
| CalldataNavigationVariant<"CALLDATA_OUT_OF_BOUNDS">
|
|
131
131
|
| CalldataNavigationVariant<"ARRAY_INDEX_OUT_OF_BOUNDS">;
|
|
132
132
|
|
|
133
|
-
/** A quantified array exceeded `
|
|
133
|
+
/** A quantified array exceeded `PolicyFormat.MAX_QUANTIFIED_ARRAY_LENGTH`. */
|
|
134
134
|
export type QuantifierLimitExceededViolation = {
|
|
135
135
|
code: "QUANTIFIER_LIMIT_EXCEEDED";
|
|
136
136
|
group: number;
|
|
@@ -142,7 +142,7 @@ export type QuantifierLimitExceededViolation = {
|
|
|
142
142
|
};
|
|
143
143
|
|
|
144
144
|
/**
|
|
145
|
-
* A resolved word is not the canonical encoding of its declared type
|
|
145
|
+
* A resolved word is not the canonical encoding of its declared type.
|
|
146
146
|
*
|
|
147
147
|
* `resolvedValue` is the raw 32-byte word as it appeared in calldata, before any masking.
|
|
148
148
|
*/
|
|
@@ -274,13 +274,80 @@ export type IssueSeverity = "info" | "warning" | "error";
|
|
|
274
274
|
/** Category of a validation issue. */
|
|
275
275
|
export type IssueCategory = "typeMismatch" | "contradiction" | "redundancy" | "vacuity" | "compatibility";
|
|
276
276
|
|
|
277
|
+
/**
|
|
278
|
+
* Machine-readable code for a policy validation issue.
|
|
279
|
+
*
|
|
280
|
+
* Each code names one finding of the semantic validator and matches the constant of the same
|
|
281
|
+
* name in the Solidity `IssueCode` library.
|
|
282
|
+
*/
|
|
283
|
+
export type IssueCode =
|
|
284
|
+
// Type mismatch.
|
|
285
|
+
| "VALUE_OP_ON_DYNAMIC"
|
|
286
|
+
| "VALUE_OP_ON_COMPOSITE"
|
|
287
|
+
| "NUMERIC_OP_ON_NON_NUMERIC"
|
|
288
|
+
| "BITMASK_ON_INVALID"
|
|
289
|
+
| "IN_ON_BOOL"
|
|
290
|
+
| "LENGTH_ON_STATIC"
|
|
291
|
+
| "CONTEXT_TYPE_MISMATCH"
|
|
292
|
+
| "UNKNOWN_OPERATOR"
|
|
293
|
+
| "NON_CANONICAL_OPERAND"
|
|
294
|
+
| "UNNAVIGABLE_PATH"
|
|
295
|
+
| "NESTED_QUANTIFIER"
|
|
296
|
+
| "HINT_MISMATCH"
|
|
297
|
+
// Contradiction.
|
|
298
|
+
| "EQ_NEQ_CONTRADICTION"
|
|
299
|
+
| "CONFLICTING_EQUALITY"
|
|
300
|
+
| "OUT_OF_PHYSICAL_BOUNDS"
|
|
301
|
+
| "IMPOSSIBLE_GT"
|
|
302
|
+
| "IMPOSSIBLE_LT"
|
|
303
|
+
| "BOUNDS_EXCLUDE_EQUALITY"
|
|
304
|
+
| "IMPOSSIBLE_RANGE"
|
|
305
|
+
| "SET_EXCLUDES_EQUALITY"
|
|
306
|
+
| "EMPTY_SET_INTERSECTION"
|
|
307
|
+
| "SET_FULLY_EXCLUDED"
|
|
308
|
+
| "LENGTH_EQ_NEQ_CONTRADICTION"
|
|
309
|
+
| "CONFLICTING_LENGTH"
|
|
310
|
+
| "BOUNDS_EXCLUDE_LENGTH"
|
|
311
|
+
| "IMPOSSIBLE_LENGTH_RANGE"
|
|
312
|
+
| "OUT_OF_PHYSICAL_LENGTH_BOUNDS"
|
|
313
|
+
| "IMPOSSIBLE_LENGTH_GT"
|
|
314
|
+
| "IMPOSSIBLE_LENGTH_LT"
|
|
315
|
+
| "BITMASK_CONTRADICTION"
|
|
316
|
+
| "BITMASK_ANY_IMPOSSIBLE"
|
|
317
|
+
| "UNSORTED_IN_SET"
|
|
318
|
+
| "EMPTY_GROUP"
|
|
319
|
+
// Redundancy.
|
|
320
|
+
| "DOMINATED_BOUND"
|
|
321
|
+
| "REDUNDANT_BOUND"
|
|
322
|
+
| "SET_REDUCTION"
|
|
323
|
+
| "SET_REDUNDANCY"
|
|
324
|
+
| "SET_PARTIALLY_EXCLUDED"
|
|
325
|
+
| "DOMINATED_LENGTH_BOUND"
|
|
326
|
+
| "REDUNDANT_LENGTH_BOUND"
|
|
327
|
+
| "REDUNDANT_BITMASK"
|
|
328
|
+
| "DUPLICATE_CONSTRAINT"
|
|
329
|
+
| "FUSIBLE_RANGE"
|
|
330
|
+
| "FUSIBLE_LENGTH_RANGE"
|
|
331
|
+
// Vacuity.
|
|
332
|
+
| "VACUOUS_GTE"
|
|
333
|
+
| "VACUOUS_LTE"
|
|
334
|
+
| "VACUOUS_LENGTH_GTE"
|
|
335
|
+
| "VACUOUS_LENGTH_LTE"
|
|
336
|
+
| "VACUOUS_NEGATED_RANGE"
|
|
337
|
+
| "VACUOUS_NEGATED_LENGTH_RANGE"
|
|
338
|
+
// Compatibility.
|
|
339
|
+
| "PATH_DEPTH_EXCEEDED"
|
|
340
|
+
| "QUANTIFIER_OVER_STATIC_LIMIT"
|
|
341
|
+
| "UNKNOWN_CONTEXT_PROPERTY"
|
|
342
|
+
| "NEGATION_UNDER_ANY";
|
|
343
|
+
|
|
277
344
|
/** A single validation issue found during policy analysis. */
|
|
278
345
|
export type Issue = {
|
|
279
346
|
severity: IssueSeverity;
|
|
280
347
|
category: IssueCategory;
|
|
281
348
|
groupIndex: number;
|
|
282
349
|
constraintIndex: number;
|
|
283
|
-
code:
|
|
350
|
+
code: IssueCode;
|
|
284
351
|
value1: Hex;
|
|
285
352
|
value2: Hex;
|
|
286
353
|
message: string;
|
package/src/validation-issue.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Hex, Issue } from "./types";
|
|
1
|
+
import type { Hex, Issue, IssueCode } from "./types";
|
|
2
2
|
|
|
3
3
|
///////////////////////////////////////////////////////////////////////////
|
|
4
4
|
// Constants
|
|
@@ -12,7 +12,7 @@ const ZERO: Hex = `0x${"0".repeat(64)}`;
|
|
|
12
12
|
|
|
13
13
|
/** Create an issue from operator compatibility results. */
|
|
14
14
|
export function fromOpRule(
|
|
15
|
-
code:
|
|
15
|
+
code: IssueCode,
|
|
16
16
|
message: string,
|
|
17
17
|
groupIndex: number,
|
|
18
18
|
constraintIndex: number,
|
|
@@ -304,7 +304,7 @@ export function unsortedInSet(groupIndex: number, constraintIndex: number): Issu
|
|
|
304
304
|
code: "UNSORTED_IN_SET",
|
|
305
305
|
value1: ZERO,
|
|
306
306
|
value2: ZERO,
|
|
307
|
-
message: "isIn/notIn set is not strictly sorted and deduplicated
|
|
307
|
+
message: "isIn/notIn set is not strictly sorted and deduplicated",
|
|
308
308
|
};
|
|
309
309
|
}
|
|
310
310
|
|
|
@@ -318,7 +318,7 @@ export function emptyGroup(groupIndex: number): Issue {
|
|
|
318
318
|
code: "EMPTY_GROUP",
|
|
319
319
|
value1: ZERO,
|
|
320
320
|
value2: ZERO,
|
|
321
|
-
message: "Group contains zero constraints
|
|
321
|
+
message: "Group contains zero constraints",
|
|
322
322
|
};
|
|
323
323
|
}
|
|
324
324
|
|