Callcium

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 an EnforceResult. It never throws on a violation.
  • PolicyEnforcer.enforce(policy, callData, context?) throws PolicyViolationError when 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, matchedGroup is the zero-based index of the constraint group that matched. A policy with a single group matches as group 0; 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, violations holds 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 than MAX_QUANTIFIED_ARRAY_LENGTH end 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:

FieldTypeDescription
codeViolationCodeDiscriminant: which failure occurred.
groupnumberGroup index. Absent on MISSING_SELECTOR and SELECTOR_MISMATCH.
rulenumberIndex of the root rule within the group: the test, or the loop of a body test. Same absences as group.
scopeScopeCodeScope.CALLDATA, Scope.CONTEXT, or Scope.LOOP for a failure at a loop's array. Present whenever path is.
pathHexPath of the failing rule: a context test's property ID, a loop's array path; a body test's path starts at the element.
operatorDecodedOperatorThe 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.
typeCodenumberType code at the failing site.
resolvedValueHexValue loaded from calldata or context. For length operators and QUANTIFIER_LIMIT_EXCEEDED, a hex-encoded count.
resolvedOperandHexValue of the context property a context operand names, on VALUE_MISMATCH.
expectedValueHexExpected value for selector-precheck failures (SELECTOR_MISMATCH).
bodyRulenumberPosition of the failing test in its loop's body. Absent for a root test.
elementIndexnumberElement 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

CodeMeaning
VALUE_MISMATCHA constraint did not hold for the value at its target path.
SELECTOR_MISMATCHCalldata selector differs from the one embedded in the policy.
MISSING_SELECTORCalldata is shorter than 4 bytes and the policy is function-bound.
NON_CANONICAL_VALUEA resolved word is not the canonical encoding of its declared type.
CALLDATA_OUT_OF_BOUNDSA rule targeted a byte range outside the calldata.
ARRAY_INDEX_OUT_OF_BOUNDSA rule indexed an array element that does not exist.
MISSING_CONTEXTThe policy references a context property the caller did not supply.
QUANTIFIER_LIMIT_EXCEEDEDA loop's array is longer than MAX_QUANTIFIED_ARRAY_LENGTH.
QUANTIFIER_NO_MATCHNo 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_MISMATCH violation 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);
});

On this page