Policy Enforcement
Evaluate ABI-encoded calldata against a policy.
PolicyEnforcer takes a policy blob, ABI-encoded calldata, and an optional execution context, and determines whether the data is valid against the policy.
check vs enforce
Two functions expose the enforcer:
PolicyEnforcer.check(policy, callData, context?)returns anEnforceResult. It never throws on a violation.PolicyEnforcer.enforce(policy, callData, context?)throwsPolicyViolationErrorwhen the data is invalid against the policy.
Both require a policy blob that passes storage validation, which PolicyCoder.decode runs. They throw CallciumError on a blob shorter than its fixed prefix or of an unsupported format version, on malformed hex, and on a context value that does not fit its property's type; on a blob that fails storage validation past its header, the result is undefined. Only enforce throws on a violation.
import { PolicyEnforcer } from "@callcium/sdk";
// Inspect violations without throwing.
const result = PolicyEnforcer.check(policy, calldata);
if (!result.ok) {
for (const violation of result.violations) {
console.warn(violation.code);
}
}
// Abort on violation.
PolicyEnforcer.enforce(policy, calldata);EnforceResult
EnforceResult is a discriminated union on the ok field:
type EnforceResult = { ok: true; matchedGroup: number } | { ok: false; violations: Violation[] };- On pass,
matchedGroupis the zero-based index of the constraint group that matched. A policy with a single group matches as group0; policies using.or()may match any of several groups. Group indices follow the policy's normal form, which orders groups by their encoding, not.or()authoring order. - On fail,
violationsholds one entry per group evaluated, or the selector failure alone. A non-canonical value, a read past the end of the calldata, and an array longer thanMAX_QUANTIFIED_ARRAY_LENGTHend the evaluation at the group they occur in.
Violation
Violation is a discriminated union on code — the per-variant field set is documented in the Types reference. Common fields:
| Field | Type | Description |
|---|---|---|
code | ViolationCode | Discriminant: which failure occurred. |
group | number | Group index. Absent on MISSING_SELECTOR and SELECTOR_MISMATCH. |
rule | number | Index of the root rule within the group: the test, or the loop of a body test. Same absences as group. |
scope | ScopeCode | Scope.CALLDATA, Scope.CONTEXT, or Scope.LOOP for a failure at a loop's array. Present whenever path is. |
path | Hex | Path of the failing rule: a context test's property ID, a loop's array path; a body test's path starts at the element. |
operator | DecodedOperator | The test's parsed operator. Present on VALUE_MISMATCH and NON_CANONICAL_VALUE; absent where a context test's subject or a loop's array fails. |
typeCode | number | Type code at the failing site. |
resolvedValue | Hex | Value loaded from calldata or context. For length operators and QUANTIFIER_LIMIT_EXCEEDED, a hex-encoded count. |
resolvedOperand | Hex | Value of the context property a context operand names, on VALUE_MISMATCH. |
expectedValue | Hex | Expected value for selector-precheck failures (SELECTOR_MISMATCH). |
bodyRule | number | Position of the failing test in its loop's body. Absent for a root test. |
elementIndex | number | Element the failing body test read. Absent for a root test. |
operator carries the comparison, its negation, and its operands as stored: constants as 32-byte words of operandType in values, or the context property the operand names in contextPropertyId.
Narrow on code to access variant-specific fields:
if (violation.code === "VALUE_MISMATCH") {
// violation.operator, violation.typeCode, violation.resolvedValue are all present here.
}Violation carries semantic data only — message strings are the consumer's responsibility.
ViolationCode
| Code | Meaning |
|---|---|
VALUE_MISMATCH | A constraint did not hold for the value at its target path. |
SELECTOR_MISMATCH | Calldata selector differs from the one embedded in the policy. |
MISSING_SELECTOR | Calldata is shorter than 4 bytes and the policy is function-bound. |
NON_CANONICAL_VALUE | A resolved word is not the canonical encoding of its declared type. |
CALLDATA_OUT_OF_BOUNDS | A rule targeted a byte range outside the calldata. |
ARRAY_INDEX_OUT_OF_BOUNDS | A rule indexed an array element that does not exist. |
MISSING_CONTEXT | The policy references a context property the caller did not supply. |
QUANTIFIER_LIMIT_EXCEEDED | A loop's array is longer than MAX_QUANTIFIED_ARRAY_LENGTH. |
QUANTIFIER_NO_MATCH | No element satisfied an ANY loop's body, the empty array included. |
PolicyViolationError, thrown by enforce, carries the same Violation[] on its violations property.
Supplying context
The context argument is optional. Provide it only when the policy constrains context properties (msgSender(), msgValue(), blockTimestamp(), blockNumber(), chainId(), txOrigin(), baseFee(), gasPrice()). Policies that reference no context properties take no third argument:
const result = PolicyEnforcer.check(policy, calldata);When the policy does reference context, supply the properties it uses. Nothing is inferred from a runtime environment:
import { PolicyEnforcer } from "@callcium/sdk";
import type { Context } from "@callcium/sdk";
const ctx: Context = {
msgSender: OPERATOR,
msgValue: 0n,
blockTimestamp: 1_700_000_000n,
chainId: 1n,
};
const result = PolicyEnforcer.check(policy, calldata, ctx);Every Context field is optional. Supply only the properties the policy actually references:
type Context = {
msgSender?: Address;
msgValue?: bigint;
blockTimestamp?: bigint;
blockNumber?: bigint;
chainId?: bigint;
txOrigin?: Address;
baseFee?: bigint;
gasPrice?: bigint;
};A property referenced by the policy but missing from context surfaces as a MISSING_CONTEXT violation.
Selector handling
For function-bound policies, the enforcer validates the first 4 bytes of calldata before evaluating any constraints:
- Match: the selector equals the policy's embedded selector. Arguments are read from byte 4 onward.
- Mismatch: a
SELECTOR_MISMATCHviolation is emitted; the group index is absent. - Missing: calldata shorter than 4 bytes emits
MISSING_SELECTOR.
Selector failures are distinct from argument-level VALUE_MISMATCH violations: they happen before any group is evaluated.
The match covers the selector alone. The embedded descriptor carries argument types but no function name, so a policy's selector and its descriptor are related only by the signature string given to create() — the enforcer cannot detect a descriptor authored for a different function with the same argument types.
For selectorless policies (see Selectorless Policies), the enforcer reads from byte 0. No selector check runs.
Preflight use
check is the common choice for preflight validation: running the enforcer against candidate calldata before submitting a transaction or accepting an incoming payload.
import { PolicyEnforcer } from "@callcium/sdk";
function canSubmit(calldata: Hex, ctx: Context): boolean {
return PolicyEnforcer.check(policy, calldata, ctx).ok;
}For tests, enforce integrates cleanly with assertion APIs that expect an exception on failure:
import { PolicyEnforcer, PolicyViolationError } from "@callcium/sdk";
import { expect, test } from "vitest";
test("rejects over-limit amount", () => {
expect(() => PolicyEnforcer.enforce(policy, overlimitCalldata)).toThrow(PolicyViolationError);
});