Theoretical Background


Introduction

The decryption oracle solves the problem of observing parameters of a function prior to function execution.

The call decription oracle allows to decrypt an encrypted-hashed argument list of a function call, when requested from an eligible contract.

The design is about when arguments and call descriptors become visible on-chain, not about making calls or value flows permanently unlinkable. Once a request is fulfilled, anyone can correlate the fulfilled call with the original requester via the standardized events.

It separates

  1. a reusable, verifiable container for encrypted arguments, and
  2. a call descriptor (what to call, and any validity constraints),

and defines how an off-chain oracle decrypts and executes such calls.

Abstract

The call decryption oracle interface standardizes how smart contracts can request a function execution with encrypted arguments, optionally with an encrypted call descriptor, using a stateless decryption oracle (a Call Decryption Oracle).

The goal is to support use-cases like privacy-preserving order books (preventing front-running) or conditional DvP flows, while keeping the standard itself as generic as possible.

The design is about when arguments and call descriptors become visible on-chain, not about making calls or value flows permanently unlinkable. Once a request is fulfilled, anyone can correlate the fulfilled call with the original requester via the standardized events.

The call decryption oracle interface defines a data format and contract interface for executing smart contract calls where:

  1. The arguments are encrypted and reusable and associated with a hash commit for verification (EncryptedHashedArguments), and
  2. The call descriptor (target contract, selector, validity) is either
    • encrypted (EncryptedCallDescriptor), or
    • plain (CallDescriptor).

An on-chain call decryption oracle contract offers a request/fulfill pattern to request a call with an encrypted argument descriptor (encrypted arguments), which is fulfilled if admissible.

The argument descriptor may contain a list of contracts eligible to request decryption.

The target contract or the call may store a hash commitment to the plaintext arguments and later verifies that the decrypted argument payload matches the stored commitment.

An off-chain call decryption oracle listens to standardized events, decrypts payloads, enforces access-control policy, and calls back into the on-chain oracle to perform the requested call.

The contract receiving the decrypted arguments can pass these on to other contracts, which can, if necessary, validate the arguments against the previously stored hash commitment.

The decrypted arguments are transported as (uint256 requestId, bytes argsPlain) to allow correlating the call to the request. Here argsPlain is an ABI-encoding of a typed argument list (depending on the business logic).

Motivation

Privacy- and conditionality-preserving protocols often need to:

  • Keep arguments confidential until some condition is met (e.g. order books).
  • Optionally keep the target address and function itself confidential.
  • Allow reusable encrypted argument blobs that can be passed between contracts and stored on-chain.
  • Allow the receiver of a call to verify that the decrypted arguments used in the call are exactly those that were committed to earlier.

Existing work like ERC-7573 focuses on a specific decryption-oracle use-case with fixed callbacks (e.g. DvP). The call decryption oracle interface generalizes that pattern to a generic function execution mechanism, designed around:

  • a clear separation of argument encryption and call encryption, and
  • an explicit hash commitment enabling verification of the arguments by the receiving contract.

Exemplary Use-Cases

Order Book Build Process avoiding Front Running

A possible use-case is the construction of an auction / order book preventing front-running, where the proposals can be made during a predefined phase. Here participants submit their proposals as encrypted hashed arguments, which are stored inside a smart contract. Once the order phase is closed, the smart contract calls the call decryption oracle (passing itself as the callback target) to receive the decrypted arguments in a call that will build the order book.

Specification

1. Encrypted arguments

Encrypted arguments are independent of any particular call descriptor and can be reused.

Upon (off-chain) encryption (initialization) a hash of the (plain) arguments is generated accompanying the encrypted arguments to allow later verification.

struct EncryptedHashedArguments {
    /**
     * Commitment to the plaintext argument payload.
     * The target contract should check keccak256(argsPlain) == argsHash.
     */
    bytes32 argsHash;

    /**
     * Identifier of the public key used for encryption (e.g. keccak256 of key material).
     */
    bytes32 publicKeyId;

    /**
     * Ciphertext of abi.encode(ArgsDescriptor), encrypted under publicKeyId.
     */
    bytes ciphertext;
}

Normative requirements (EncryptedHashedArguments)

For producers of EncryptedHashedArguments:

  • The producer MUST compute

    argsHash = keccak256(argsPlain);
    

    where argsPlain is an abi.encode(args...) byte sequence and part of the ArgsDescriptor (see below).

  • The producer MUST set ciphertext to the encryption of exactly

    abi.encode(argsDescriptor)
    

    under the key identified by publicKeyId, where argsDescriptor is an ArgsDescriptor as defined below.

  • The producer MUST set argsHash to the value computed above.

A call decryption oracle implementation MAY provide a command line tool or endpoint to generate EncryptedHashedArguments from plaintext arguments.

The call decryption oracle interface does not standardize the encryption algorithm or key management; those are implementation-specific (similar to ERC-7573). Implementations SHOULD document how publicKeyId is derived from the underlying key material.

Argument Descriptor structure (normative for eligibility)

Prior to encryption, the arguments are bundled with an (optional) list of eligibleCallers, to allow a Call Decryption Oracle to enforce eligibility of the requester in a consistent way. This prevents decryption by other contracts through observing encrypted arguments and requesting a call to the call decryption oracle.

The call decryption oracle interface standardizes the layout of the decrypted payload as:

struct ArgsDescriptor {
    /**
     * List of addresses allowed to request decryption.
     * If empty, any requester is allowed. This is enforced off-chain by the oracle operator.
     */
    address[] eligibleCaller;

    /**
     * Plain argument payload, may be abi.encode(args...).
     */
    bytes argsPlain;
}

In this case, producers set

bytes32 argsHash = keccak256(argsDescriptor.argsPlain);
bytes   ciphertext = ENC_publicKeyId(abi.encode(argsDescriptor));

The eligibleCaller list is not visible to on-chain contracts; it is only used off-chain by the oracle operator to decide whether to honor a decryption request.

2. Call descriptor

A call descriptor defines:

  • which contract and function will be called, and
  • any validity constraint (e.g. expiry block).
struct CallDescriptor {
    /**
     * Contract that will be called by the oracle.
     */
    address targetContract;

    /**
     * Function that will be called by the oracle.
     * 4-byte function selector for the targetContract whose signature MUST be (uint256, bytes).
     */
    bytes4 selector;

    /**
     * Optional expiry (block number). 0 means "no explicit expiry".
     */
    uint256 validUntilBlock;
}

Plain vs. Encrypted Call Descriptors

A call descriptor can be:

  • Plain: CallDescriptor is passed in clear on-chain.
  • Encrypted: CallDescriptor is wrapped into:
struct EncryptedCallDescriptor {
    /**
     * Identifier of the public key used for encryption.
     */
    bytes32 publicKeyId;

    /**
     * Ciphertext of abi.encode(CallDescriptor), encrypted under publicKeyId.
     */
    bytes ciphertext;
}

Normative requirements (CallDescriptor and EncryptedCallDescriptor)

  • When using EncryptedCallDescriptor, the ciphertext MUST be the encryption of exactly abi.encode(CallDescriptor) under the key identified by publicKeyId.

3. Oracle interface

The oracle exposes a request/fulfill pattern. Requests are cheap and do not require on-chain decryption; fulfillment is called by an off-chain operator after decryption.

interface ICallDecryptionOracle {
    /// Raised when a request with encrypted call descriptor + encrypted args is registered.
    event EncryptedCallRequested(
        uint256 indexed requestId,
        address indexed requester,
        bytes32 callPublicKeyId,
        bytes   callCiphertext,
        bytes32 argsPublicKeyId,
        bytes   argsCiphertext,
        bytes32 argsHash
    );

    /// Raised when a request with plain call descriptor + encrypted args is registered.
    event CallRequested(
        uint256 indexed requestId,
        address indexed requester,
        address   targetContract,
        bytes4    selector,
        uint256   validUntilBlock,
        bytes32   argsPublicKeyId,
        bytes     argsCiphertext,
        bytes32   argsHash
    );

    /// Raised when an execution attempt has been fulfilled by the oracle operator.
    event CallFulfilled(
        uint256 indexed requestId,
        bool    success,
        bytes   returnData
    );

    /**
     * @notice Request execution with encrypted call descriptor + encrypted arguments.
     *
     * @dev MUST:
     * - register a unique requestId,
     * - store (requestId → requester, argsHash, and auxiliary metadata),
     * - emit EncryptedCallRequested.
     */
    function requestEncryptedCall(
        EncryptedCallDescriptor   calldata encCall,
        EncryptedHashedArguments  calldata encArgs
    ) external returns (uint256 requestId);

    /**
     * @notice Request execution with plain call descriptor + encrypted arguments.
     *
     * @dev MUST:
     * - require encArgs.argsHash to be consistent with any application-level commitments,
     * - register a unique requestId and store callDescriptor data + requester,
     * - emit CallRequested.
     */
    function requestCall(
        CallDescriptor            calldata callDescriptor,
        EncryptedHashedArguments  calldata encArgs,
        bytes                     calldata secondFactor
    ) external returns (uint256 requestId);

    /**
     * @notice Fulfill an encrypted-call request after off-chain decryption.
     *
     * @param requestId      The id obtained from requestEncryptedCall.
     * @param callDescriptor The decrypted CallDescriptor.
     * @param argsPlain      The decrypted argument payload bytes.
     *
     * @dev MUST:
     * - verify that requestId exists and was created with requestEncryptedCall,
     * - verify callDescriptor.validUntilBlock is zero or >= current block.number,
     * - verify that keccak256(argsPlain) equals the stored argsHash,
     * - perform low-level call:
     *     callDescriptor.targetContract.call(
     *         abi.encodeWithSelector(callDescriptor.selector, requestId, argsPlain)
     *     )
     * - emit CallFulfilled(requestId, success, returnData),
     * - clean up stored state for this requestId.
     */
    function fulfillEncryptedCall(
        uint256          requestId,
        CallDescriptor   calldata callDescriptor,
        bytes            calldata argsPlain
    ) external;

    /**
     * @notice Fulfill a plain-call request after off-chain decryption of the arguments.
     *
     * @param requestId The id obtained from requestCall.
     * @param argsPlain The decrypted argument payload bytes.
     *
     * @dev MUST:
     * - verify that requestId exists and was created with requestCall,
     * - load stored CallDescriptor from state,
     * - verify callDescriptor.validUntilBlock is zero or >= current block.number,
     * - verify that keccak256(argsPlain) equals the stored argsHash,
     * - perform low-level call:
     *     callDescriptor.targetContract.call(
     *         abi.encodeWithSelector(callDescriptor.selector, requestId, argsPlain)
     *     )
     * - emit CallFulfilled(requestId, success, returnData),
     * - clean up stored state for this requestId.
     */
    function fulfillCall(
        uint256          requestId,
        bytes            calldata argsPlain
    ) external;
}

Note: The call decryption oracle interface does not standardize the exact storage layout of pending requests or the internal access control for fulfill* (e.g. onlyOracle). Implementations MUST ensure that only the intended oracle operator can call the fulfill* functions.

4. Target contract

A common pattern is that the target contract

  1. In an initialization phase, receives and stores an encrypted argument encArg together with its hash argsHash, where argsHash is the hash of the plaintext argument payload.
  2. In the execution phase, the caller issues a request to the call decryption oracle, obtains the requestId returned by requestCall / requestEncryptedCall, and passes this requestId along with argsHash to the call target (may be the same contract) in the same transaction. The call target then receives the decrypted argument payload argsPlain (under a callback selector) from the call decryption oracle, with the corresponding requestId and recomputes the hash and compares it to the stored value.
  3. The call target can the procees with the business logic operating on the decrypted (and verified) arguments.

For example, the producer of EncryptedHashedArguments may choose

bytes memory argsPlain = abi.encode(amount, beneficiary);
bytes32 argsHash = keccak256(argsPlain);

The target (or router) contract can then do:

mapping(uint256 => bytes32) public argsHashByRequestId;

function registerArguments(uint256 requestId, bytes32 argsHash) external {
    // In the same transaction as the request, store the commitment for this requestId.
    argsHashByRequestId[requestId] = argsHash;
}

/**
 * @dev Target selector: Called by the Call Decryption Oracle with decrypted arguments.
 * The oracle calls this function with signature
 *    callback(uint256 requestId, bytes argsPlain)
 * where requestId is the technical correlation id assigned by the oracle.
 */
function executeWithVerification(
    uint256 requestId,
    bytes   calldata argsPlain
) external {
    // Lookup the pre-committed hash from the init phase.
    bytes32 stored = argsHashByRequestId[requestId];
    require(stored != bytes32(0), "Unknown requestId");

    // Recompute the hash from the received bytes.
    bytes32 computed = keccak256(argsPlain);
    require(computed == stored, "Encrypted args mismatch");

    /**
     * Optional: decode argsPlain to use it, e.g.
     *   (uint256 amount, address beneficiary) = abi.decode(argsPlain, (uint256, address));
     * and route to business logic.
     */
}

In this pattern:

  • argsHash is the hash commit of argsPlain created upon off-chain argument construction.
  • requestId is the technical identifier assigned by the Call Decryption Oracle when the request is issued. It is useful for correlation, logging and mapping inside routers, but is not required for the hash check.
  • The argsPlain field passed to fulfill* is the exact byte payload used to compute argsHash. The callback (target or router) decodes argsPlain to recover the business arguments.
  • A router/adapter contract can implement the executeWithVerification(uint256 requestId, bytes argsPlain) callback, perform the verification and decoding, and then call an already deployed target contract with its original typed function signature.

Implementations SHOULD register the (requestId, argsHash) mapping in the same transaction that issues the request to the oracle, to avoid any race where a very fast oracle operator could attempt to fulfill a request before the mapping is written on-chain. Even if the mapping is missing, the callback pattern above will cause the fulfillment to revert (due to Unknown requestId); however, registering in the same transaction provides deterministic behaviour.

Rationale

  • The two-stage design (arguments vs. call) allows encrypted arguments to be reusable and independent of any particular call descriptor.
  • The explicit hash commitment (argsHash) binds arguments to a commitment stored by the receiving contract, while still allowing the arguments to be stored and passed separately as opaque bytes.
  • The request/fulfill pattern reflects that decryption is off-chain. Requests are cheap; fulfill is initiated when decryption is ready.
  • The use of abi.encodeWithSelector(selector, requestId, argsPlain) makes the on-chain oracle generic and able to support arbitrary function signatures while still providing a standard correlation identifier (requestId).
  • Router/adapter pattern: when integrating with already deployed contracts whose function signatures cannot be changed, a small router contract can serve as the callback. The router implements a function like executeWithVerification(uint256 requestId, bytes argsPlain), verifies requestId and argsHash as above, decodes argsPlain into typed arguments, and then calls the pre-existing target contract with its original typed function. This preserves compatibility with existing deployments while still using the standard call decryption oracle.

Backwards Compatibility

The call decryption oracle interface is designed to coexist with ERC-7573 decryption oracles. An existing ERC-7573 implementation can be extended to implement ICallDecryptionOracle without breaking existing interfaces.

Reference Implementation

A non-normative reference implementation (Solidity) and a matching Java/off-chain implementation are provided separately. They illustrate:

  • storage of pending requests,
  • event emission for both encrypted and plain call descriptors,
  • validation of hash bindings, and
  • low-level call execution.

These implementations are work-in-progress and may evolve independently of the ERC text.

Fees

Implementations MAY charge fees in ETH or ERC-20 tokens as part of their specific deployment.

Security Considerations

Oracle trust

The on-chain contract cannot verify correctness of decryption; it can only check that keccak256(argsPlain) == argsHash. Parties must trust the oracle operator (or design an incentive/penalty mechanism) to decrypt correctly and call fulfill* faithfully.

Replay

Implementations SHOULD mitigate replay by:

  • using validUntilBlock in CallDescriptor, and/or
  • including nonces or sequence numbers in higher-level protocols.

Access control

Access control is an application-level concern. A common pattern is to embed an access-control list such as address[] eligibleCaller inside the encrypted payload (in ArgsDescriptor) and have the off-chain oracle operator enforce that the original requester is contained in that list (unless the list is empty, meaning “any requester”). The standard does not prescribe a particular access-control mechanism beyond this guidance.

Traceability and non-mixing

The call decryption oracle is not intended to act as a mixer or general-purpose anonymization service for payments or calls. Its purpose is to defer the disclosure of arguments (and optionally the call descriptor) until fulfillment.

Implementations SHOULD preserve the ability for off-chain indexers and observers to correlate CallFulfilled events with the corresponding CallRequested / EncryptedCallRequested events, for example by strictly adhering to the requestId linkage defined in the call decryption oracle interface.

Once a request is fulfilled, an observer can always reconstruct which requester initiated the request and which call and arguments were eventually used. Protocols that deliberately try to break this correlation or to hide value flows are out of scope of the call decryption oracle interface.