Skip to main contentPASS
Read the docs

Execution

Adapters

Why arbitrary calldata cannot be trusted to describe itself, and how protocol-specific decoding produces validated policy inputs.

An actor that proposes an execution also proposes an account of what that execution does. The two are independent. The account is a claim; the call is the thing that will actually run. Nothing in a signature, and nothing in a well formed transaction, binds one to the other.

This matters because policy is only as good as the description it is evaluated against. If a mandate limits a single transaction to $2,500 in AAPL or NVDA, and the authorization layer checks that limit against a field supplied by the actor that says "this is a $500 AAPL trade", the check is cosmetic. It constrains what the actor is willing to say about itself, not what the actor is about to do. An actor that mis-describes an execution — through a bug, a stale price, a mistaken assumption about the target, or deliberate construction — passes the check and executes something else. Authority evaluated against self-report is not authority. It is paperwork.

Adapters exist to close that gap. An adapter is the component that derives the description from the call itself, so that policy is evaluated against what will execute rather than against what was asserted.

Why raw calldata is opaque

A transaction is an address, a value, and a byte string. Those bytes are not self-describing, and reading them is harder than it first appears.

This is not a defect in the execution layer. A node establishes that a call is well formed and will run, and it does that job well. That is validity, and validity is a different question from what a call means for authority. Nothing in the transaction format was designed to carry the second answer, so something above it has to derive one.

The same bytes mean different things to different targets. A four-byte selector is a truncated hash of a function signature, not a registered identifier. Two unrelated contracts can expose the same selector with different semantics, and a contract can expose a function whose name suggests one behaviour and whose implementation performs another. The bytes do not resolve their own meaning; the target resolves it. Decoding without knowing precisely which contract is being called is guessing.

Token amounts are not values. A call carrying the integer 5000000 says nothing about exposure. The decimal precision belongs to the token contract, the instrument's identity belongs to the token contract, and the monetary value belongs to neither — it requires a price, from a source, at a time. A limit expressed in dollars cannot be enforced against a quantity expressed in units without a valuation step, and that step is part of the authorization decision rather than an accessory to it.

Indirection compounds both problems. Routers, multicall wrappers, permit-and-swap patterns and delegated execution mean the outer call often does not name the instrument at all. The asset that ends up moving may appear only inside a nested parameter, or may be determined at execution time by a path the caller supplied.

The consequence is that a general-purpose decoder cannot be relied on. Universal calldata parsing would have to carry every protocol's semantics, and would fail quietly wherever it did not. PASS takes the opposite approach: narrow decoders, each trusted for exactly one protocol and required to refuse anything outside it.

The adapter model

An adapter is protocol-specific decoding logic that turns a raw call into a validated Execution Intent. It is trusted for one protocol and no more. Within its scope it is authoritative about what a call means; outside its scope it has no opinion and must say so.

The Execution Intent is the structure policy actually sees: an identified asset, an action type, a computed value, a verified target, and the actor and subject the execution is attributed to. It contains no field the actor supplied as a description. Every field is derived, and any field that cannot be derived with confidence prevents the intent from being formed at all.

RAW CALL
  actor 0x81...29F -> target, selector, calldata
        |
        v
ADAPTER  (trusted for one protocol)
  verify target
  decode selector
  identify asset
  compute value
        |
        +--- cannot decode --> INVALID_EXECUTION_CONTEXT
        |
        v
VALIDATED EXECUTION INTENT
  BUY | AAPL | 10 units | $2,410 | venue verified
        |
        v
POLICY CHECK  (mandate + passport)
        |
        v
AUTHORIZED  |  BLOCKED + REASON
        |
        v
EXECUTION

The ordering is the point. Decoding happens before evaluation, and evaluation happens before execution. Preflight never reasons about bytes, and an adapter never reasons about limits. Each component does one thing that the other cannot do correctly.

What an adapter is responsible for

ResponsibilityWhat it establishes
Target verificationThe call is addressed to a contract this adapter is trusted to decode, at an expected address, not a look-alike
Selector decodingThe function being invoked is one this adapter knows, and its parameters are decoded against the real signature
Asset identificationThe instrument that will move is resolved to a canonical identifier — AAPL, NVDA — rather than to an unqualified token address
Value computationMonetary exposure is computed from units, decimals and a price, with a stated valuation source and timestamp
RefusalAnything that does not satisfy the above produces no intent

Target verification comes first because it determines whether any of the rest is meaningful. Selector decoding against the wrong target yields confident nonsense.

Value computation deserves particular care, because it is where an authorization system quietly acquires a market dependency. A dollar limit is enforceable only against a valuation, and a valuation carries the assumptions of its source. The architecture requires the source and the observation time to be part of the intent so that a decision can be reconstructed later, and so that a stale or unavailable price is a refusal rather than an approximation. PASS constrains authorization risk. It does not remove the oracle risk that valuation introduces, and the security model treats that dependency directly.

A worked derivation

Take the mandate held by actor 0x81...29F: allowed assets AAPL and NVDA, a maximum transaction of $2,500, a daily exposure limit of $10,000, approved adapters only, STOCK_TOKEN_ELIGIBLE required, bridging blocked. The actor proposes a swap through a router and labels it a $500 AAPL purchase.

The adapter ignores the label. It verifies the router address, decodes the selector against that router's real signature, follows the swap path to the instrument that will actually be received, resolves it to AAPL, applies the token contract's decimals, and values the resulting position at a stated price and observation time.

FieldActor's descriptionAdapter's derivation
AssetAAPLAAPL
ActionBUYBUY
Unitsnot stated13
Value$500$3,133

The instrument is allowed and the intent is complete, so decoding succeeds and policy is reached. Policy compares $3,133 against the $2,500 per-transaction ceiling, and the decision is blocked with TX_LIMIT_EXCEEDED. The $10,000 daily figure is untouched, because a blocked execution consumes no exposure. A layer that had evaluated the same proposal against the actor's own $500 description would have returned AUTHORIZED. The difference between the two outcomes is the derivation, and nothing below the authorization layer was in a position to perform it.

Refusal

An adapter that cannot confidently decode a call must refuse. It must not fall back to a permissive default, must not emit a partially populated intent, and must not substitute the actor's own description for a field it failed to derive. Refusal produces the decision Reason Code INVALID_EXECUTION_CONTEXT.

This is a deliberate asymmetry. A refused execution is an operational problem: an actor is blocked, a strategy stalls, someone extends adapter coverage. An incorrectly decoded execution is an authorization failure that policy cannot detect, because policy sees only the intent and the intent is wrong. The first is recoverable. The second is the failure the layer exists to prevent. Where the two trade off, the architecture takes the first.

What this shifts rather than eliminates

Adapters relocate trust; they do not abolish it. This is worth stating plainly, because a security model that overstates itself is harder to reason about than one that names its assumptions.

Adapters are trusted components. Policy evaluation is only as sound as the intent it receives, so adapter correctness is a security property of the system, on the same footing as the policy engine itself. An adapter with a decoding bug can mis-describe an execution, and a mis-described execution can be authorized against limits it does not actually satisfy. An adapter that mistakes one instrument for another will let a mandate allowing AAPL and NVDA approve something else.

What the model does change is the shape of the trust. Instead of an unbounded assumption that every actor describes itself honestly and accurately, there is a bounded set of named, versioned components, each covering one protocol, each reviewable in isolation, each replaceable. That is a smaller and more tractable surface, and it is auditable in a way that self-reported metadata never is. It is not zero. See security for how adapter trust sits alongside the other assumptions in the model.

Allow-listing and versioning

Because adapters are trusted, a mandate does not accept intents from arbitrary decoders. It names the adapters whose output it will act on. The mandate held by actor 0x81...29F restricts venues to approved adapters; a proposed execution routed through any other decoder yields ADAPTER_NOT_ALLOWED, regardless of whether the underlying trade would otherwise have satisfied every limit. The instrument, the size and the credentials are not reached, because the description of the execution has no accepted provenance.

Adapters are versioned, and the version is part of the identity. Protocols upgrade, selectors change, and a decoder that was correct against one deployment can be wrong against its successor — often in ways that still return a plausible-looking result. Treating a new version as a distinct allow-listed component means an upgrade requires an explicit authorization change rather than inheriting trust. It also means a decision can be replayed against the exact decoder that produced it, which is what makes an audit trail meaningful.

Interface sketch

The following is a conceptual interface, not a published artifact. It shows the shape of the contract an adapter is expected to satisfy.

// Conceptual. An adapter is a decoder trusted for exactly
// one protocol, and required to refuse everything else.
interface IExecutionAdapter {
    struct ExecutionIntent {
        address actor;        // proposing actor
        address subject;      // party the mandate acts for
        address target;       // verified execution venue
        bytes4  selector;     // decoded function selector
        uint8   action;       // BUY, SELL, TRANSFER, BRIDGE
        bytes32 asset;        // canonical instrument id
        uint256 amount;       // instrument units
        uint256 valueUsd;     // computed, never reported
        bytes32 priceSource;  // valuation provenance
        uint64  observedAt;   // valuation timestamp
    }
 
    // The protocol this adapter is trusted to decode.
    function protocolId() external view returns (bytes32);
 
    // Part of the adapter's allow-listed identity.
    function version() external view returns (uint32);
 
    // Non-reverting probe used before decoding.
    function canDecode(address target, bytes4 selector)
        external
        view
        returns (bool);
 
    // Derive a validated intent from a proposed call.
    // MUST revert rather than return a low-confidence
    // result. Reverts surface as INVALID_EXECUTION_CONTEXT.
    function decode(
        address actor,
        address target,
        uint256 callValue,
        bytes calldata data
    )
        external
        view
        returns (ExecutionIntent memory);
}

canDecode and decode are separate because refusal and failure are different events. A probe that returns false lets preflight report cleanly that no trusted decoder covers this call. A revert inside decode signals that a decoder which should have understood the call could not complete the derivation — a missing price, an unexpected parameter shape, an unverified target. Both end in a blocked decision carrying INVALID_EXECUTION_CONTEXT, but the decision record retains which branch produced it. "Nothing here can read this call" and "the decoder for this call could not finish" call for different responses from whoever operates the mandate.

Reason Codes originating in the adapter path

CodeCondition
ADAPTER_NOT_ALLOWEDThe decoder is not named by the mandate, or its version is not the allow-listed one
INVALID_EXECUTION_CONTEXTNo trusted adapter covers the call, or the adapter could not derive a complete intent
ACTION_NOT_ALLOWEDThe intent decoded to an action the mandate forbids — bridging, for the mandate in the running example
ASSET_NOT_ALLOWEDThe identified instrument is outside the mandate's allowed set
TX_LIMIT_EXCEEDEDThe computed value exceeds the mandate's per-transaction ceiling — $2,500 in the running example

The last three are policy outcomes rather than decoding outcomes, and they appear here to make the boundary visible. The adapter established that the call bridges, that the instrument is neither AAPL nor NVDA, or that the position is worth $3,133. The mandate decided what that means. Keeping derivation and evaluation apart is what allows a decision to be explained: the intent records what the execution was, and the Reason Code records why authority did or did not extend to it. The full code set and its semantics are given in the specification.