Abstract
This ERC standardizes a request/fulfill oracle interface for executing smart-contract callbacks with encrypted arguments and, optionally, an encrypted call descriptor, with controlled on-chain disclosure.
A request submits EncryptedHashedArguments (ciphertext plus a commitment argsHash = keccak256(argsPlain)) together
with either a clear CallDescriptor or an EncryptedCallDescriptor.
An off-chain operator observes the standardized request events, enforces eligibility/policy, decrypts the payload,
and calls fulfill*, which forwards (requestId, argsPlain) to the target selector; receivers can verify integrity
via a hash (argsHash).
The scheme defers visibility of arguments until fulfillment (not permanent unlinkability). 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).
The ERC is compatible with existing decryption-oracle designs such as ERC-7573 and can be implemented as an extension of such oracles.
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). This ERC 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
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Encrypted Arguments
Encrypted arguments are independent of any particular call descriptor and can be reused.
Upon off-chain encryption, 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
argsPlainis anabi.encode(args...)byte sequence and part of theArgsDescriptor(see below). -
The producer MUST set
ciphertextto the encryption of exactlyabi.encode(argsDescriptor)under the key identified by
publicKeyId, whereargsDescriptoris anArgsDescriptoras defined below. -
The producer MUST set
argsHashto the value computed above.
This ERC 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.
This ERC 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...) (see router).
*/
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.
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.
*/
bytes4 selector;
/**
* Optional expiry (block number). 0 means "no explicit expiry".
*/
uint256 validUntilBlock;
}Plain vs. Encrypted Call Descriptors
A call descriptor can be:
- Plain:
CallDescriptoris passed in clear on-chain. - Encrypted:
CallDescriptoris 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 exactlyabi.encode(CallDescriptor)under the key identified bypublicKeyId.
Encryption/Decryption Method and optional Second Factor
The encryption/decryption method is an implementation detail of the call decryption oracle contract and bound to the contract address. Different instances may utilize different methods. The publicKeyId can serve as an indication in case the oracle performs a key rotation.
Second Factor
The oracle interface to request a call with encrypted arguments allows passing an optional secondFactor.
The interpretation and encoding of the second factor is an implementation detail of a concrete oracle
implementation.
The second factor enables decryption schemes in which the owner/operator of the decryption oracle cannot decrypt the encrypted arguments before a request is made that provides this factor. This is useful in use-cases such as order books, where the oracle operator should not be able to inspect orders before a reveal / decryption phase. In such setups, trust in the oracle is primarily about timely fulfilment, not about keeping arguments secret forever.
The passed second factor may be an abi.encode(...) of a more complex structure, e.g. an encoded
bytes[], thus allowing for multiple factors to implement a k-out-of-n threshold decryption off-chain.
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.
Request Identifiers (requestId)
Each request registered by a Call Decryption Oracle is assigned a requestId (returned from
requestCall / requestEncryptedCall and emitted in the corresponding CallRequested /
EncryptedCallRequested event).
Implementations MUST ensure that:
requestIdvalues are unique for the lifetime of the oracle contract instance, across bothrequestCallandrequestEncryptedCall.- A
requestIdMUST NOT be reused after it has been fulfilled or rejected (even if the stored request state is deleted). - All subsequent
fulfill*andrejectCalloperations and the resultingCallFulfilled/CallRejectedevents MUST reference the samerequestId.
Off-chain systems SHOULD treat (oracleAddress, requestId) as the unique identifier for a request.
Implementations SHOULD start requestId at 1 and increment monotonically to support deterministic
indexing and avoid ambiguity with default 0 mapping values.
interface ICallDecryptionOracle {
/// @notice Emitted when a request with transparent call descriptor + encrypted arguments is registered.
event CallRequested(
uint256 indexed requestId,
address indexed requester,
address targetContract,
bytes4 selector,
uint256 validUntilBlock,
bytes32 argsPublicKeyId,
bytes argsCiphertext,
bytes32 argsHash,
bytes secondFactor
);
/// @notice Emitted when a request with encrypted call descriptor + encrypted arguments is registered.
event EncryptedCallRequested(
uint256 indexed requestId,
address indexed requester,
bytes32 callPublicKeyId,
bytes callCiphertext,
bytes32 argsPublicKeyId,
bytes argsCiphertext,
bytes32 argsHash,
bytes secondFactor
);
/// @notice Emitted when a call has been fulfilled.
event CallFulfilled(
uint256 indexed requestId,
bytes returnData
);
enum RejectionReason {
// "unspecified/other" for forward-compatibility
Unspecified,
RequestNotFound,
Expired,
ArgsHashMismatch,
CallerNotEligible,
OperatorPolicy,
TargetCallFailed
}
/// @notice Emitted when a call has been rejected.
event CallRejected(
uint256 indexed requestId,
RejectionReason reason,
bytes details // optional extra info, may be empty
);
/**
* @notice Emitted when the oracle provides a new public key.
* @dev Announcement of a new key does not imply key rotation semantics.
* Key expiry / rotation is an off-chain policy detail.
*/
event PublicKeyUpdated(
bytes newPublicKey,
bytes32 newKeyId
);
/**
* @notice Returns a public key that can be used to encrypt arguments and the public key id.
*/
function getPublicKey() external view returns (bytes memory key, bytes32 keyId);
/**
* @notice Request execution with transparent call descriptor + encrypted arguments.
*
* @param callDescriptor Describes the target of the call (target address and selector of the method (method name)).
* @param encArgs Encrypted arguments that will be decrypted.
* @param secondFactor An optional second factor that may be required to decrypt the encrypted arguments
* If a secondFactor is required depends on the implementation of the decryption, if not leave empty (0x).
*
* @dev MUST:
* - register a unique requestId and store callDescriptor data + requester,
* - emit CallRequested.
*/
function requestCall(
CallDescriptor calldata callDescriptor,
EncryptedHashedArguments calldata encArgs,
bytes calldata secondFactor
) external payable returns (uint256 requestId);
/**
* @notice Request execution with encrypted call descriptor + encrypted arguments.
*
* @param encCall Encrypted CallDescriptor that describes the target of the call (target address and selector of the method (method name)).
* @param encArgs Encrypted arguments that will be decrypted.
* @param secondFactor An optional second factor that may be required to decrypt the encrypted arguments
* If a secondFactor is required depends on the implementation of the decryption, if not leave empty (0x).
*
* @dev MUST:
* - register a unique requestId,
* - store (requestId โ requester, argsHash, and auxiliary metadata),
* - emit EncryptedCallRequested.
*/
function requestEncryptedCall(
EncryptedCallDescriptor calldata encCall,
EncryptedHashedArguments calldata encArgs,
bytes calldata secondFactor
) external payable returns (uint256 requestId);
/**
* @notice Fulfill a transparent-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 storedCall.validUntilBlock is zero or >= current block.number,
* - verify that keccak256(argsPlain) equals the stored argsHash,
* - perform low-level call:
* storedCall.targetContract.call(abi.encodePacked(storedCall.selector, argsPlain))
* - emit CallFulfilled(requestId, returnData),
* - clean up stored state for this requestId.
*/
function fulfillCall(
uint256 requestId,
bytes calldata argsPlain
) external;
/**
* @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,
* - load stored CallDescriptor from state,
* - verify storedCall.validUntilBlock is zero or >= current block.number,
* - verify that keccak256(argsPlain) equals the stored argsHash,
* - perform low-level call:
* callDescriptor.targetContract.call(abi.encodePacked(callDescriptor.selector, argsPlain))
* - emit CallFulfilled(requestId, returnData),
* - clean up stored state for this requestId.
* Note that eligibility / expiry / policy must be checked off-chain before calling fulfill*.
*/
function fulfillEncryptedCall(
uint256 requestId,
CallDescriptor calldata callDescriptor,
bytes calldata argsPlain
) external;
/**
* @notice Reject a previously registered request (transparent or encrypted).
*
* @param requestId The id obtained from requestCall or requestEncryptedCall.
* @param reason RejectionReason enum.
* @param details Additional details (may encode error text, revert data, policy code, ...).
*
* @dev MUST:
* - verify that requestId exists (or, if it does not, MAY emit RequestNotFound),
* - emit CallRejected(requestId, reason, details),
* - clean up stored state for this requestId.
*/
function rejectCall(
uint256 requestId,
RejectionReason reason,
bytes calldata details
) external;
}Note: This ERC 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 thefulfill*functions.
Target Contract
A common pattern is that the target contract
- In an initialization phase, receives and stores an encrypted argument
encArgtogether with its hashargsHash, whereargsHashis the hash of the plaintext argument payload. - In the execution phase, the caller issues a request to the call decryption oracle, obtains the
requestIdreturned byrequestCall/requestEncryptedCall, and passes thisrequestIdalong withargsHashto the call target (may be the same contract) in the same transaction. The call target then receives the decrypted argument payloadargsPlain(under a callback selector) from the call decryption oracle, with the correspondingrequestIdand recomputes the hash and compares it to the stored value. - The call target can then proceed 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:
argsHashis the hash commit ofargsPlaincreated upon off-chain argument construction.requestIdis 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
argsPlainfield passed tofulfill*is the exact byte payload used to computeargsHash. The callback (target or router) decodesargsPlainto 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.
Linkage of Request and 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 this ERC.
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 this ERC.
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), verifiesrequestIdandargsHashas above, decodesargsPlaininto 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
This ERC 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 reference implementation of the on-chain decryption oracle proxy-contract (Solidity) is provided in CallDecryptionOracle.sol. The implementation performs:
- storage of pending requests,
- event emission for both encrypted and plain call descriptors,
- validation of hash bindings, and
- low-level call execution.
A reference implementation of the off-chain decryption step is currently not included.
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 including nonces or sequence numbers in higher-level protocols. Depending on the implementation of the encryption, the same argument may result in the same encrypted argument. If is an issue, the argument can be made to include a nonce or sequence number.
Privacy Note: argsHash can leak low-entropy arguments
Since argsHash is public, observers can brute-force small domains (e.g., โbuy/sellโ, small quantities, etc.).
Implementations should mitigate privacy concerencs by include a nonce/salt inside argsPlain
(or in the business-layer encoding) before hashing/encrypting.
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
This ERC 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.
See Linkage of Request and Fulfillment above.
Copyright
Copyright and related rights waived via CC0.
