---
eip: 7573
title: Conditional-upon-Transfer-Decryption for DvP
description: A Protocol for Secure Delivery-versus-Payment across two Blockchains
author: Christian Fries (@cfries), Peter Kohl-Landgraf (@pekola)
discussions-to: https://ethereum-magicians.org/t/eip-7573-conditional-upon-transfer-decryption-for-delivery-versus-payment/17232
status: Draft
type: Standards Track
category: ERC
created: 2023-12-05
---


## Abstract

The interfaces in this proposal model a functional transaction scheme to establish a secure *delivery-versus-payment*
across two blockchains, where a) no intermediary is required and b) one of the two chains
can securely interact with a stateless "decryption oracle". Here, *delivery-versus-payment* refers to the exchange of,
e.g., an asset against a payment; however, the concept is generic to make a transfer of one token on one
chain (e.g., the payment) conditional to the successful transfer of another token on another chain (e.g., the asset).

The scheme is realized by two smart contracts, one on each chain.
One smart contract implements the `ILockingContract` interface on one chain (e.g. the "asset chain"), and another smart contract implements the `IDecryptionContract` interface on the other chain (e.g., the "payment chain").
An implementation that generates the encrypted keys asynchronously may additionally implement `IDecryptionContractWithKeyGeneration`.
On the same chain, an asset contract may implement `ILockingContractWithKeyGeneration` and authorize that decryption contract as its key source before the keys exist.
An on-chain consumer may implement `IDecryptionContractInceptionCallback` to receive an asynchronous completion notification and then read the immutable inception context and key material from standardized getters.
The smart contract implementing `ILockingContract` locks a token (e.g., the asset) on its chain until a presented key's hash or other locking representation matches one of two committed values.
The smart contract implementing `IDecryptionContract`, decrypts one of two keys (via the decryption oracle) conditional to the success or failure of the token transfer (e.g., the payment). A stateless decryption oracle is attached to the chain running `IDecryptionContract` for the decryption.

In addition, there are two interfaces that standardize the communication with external decryption oracle(s):

- `IKeyDecryptionOracle.sol` is implemented by a decryption oracle proxy contract (on-chain router/proxy for an off-chain oracle).
- `IKeyDecryptionOracleCallback.sol` is implemented by a callback receiving the decrypted key (or derived verification material).

**Fulfillment semantics note:** The oracle proxy may implement either
(i) **strict fulfillment** (reverting `fulfill*` when the callback fails) or
(ii) **best-effort fulfillment** (not reverting `fulfill*` on callback failure, but signaling failure via events).
This proposal describes both modes and their operational trade-offs.


## Motivation

Within the domain of financial transactions and distributed ledger technology (DLT), the Hash-Linked Contract (HLC) concept has been recognized as valuable and has been thoroughly investigated.
The concept may help to solve the challenge of delivery-versus-payment (DvP), especially in cases where the asset chain and payment system (which may be a chain, too) are separated.
A prominent application of smart contracts realizing a secure DvP is that of buying an asset, where the asset is managed on one chain (the asset chain), but the payments are executed on another chain (the payment chain).
Proposed solutions are based on an API-based interaction mechanism which bridges the communication between a so-called asset chain and a corresponding
payment system or requires complex and problematic time locks.[^1]

Here, we propose a protocol that facilitates secure delivery-versus-payment with less overhead, especially with a stateless oracle.[^2]


## Specification

### Methods

#### Smart Contract on the chain that performs the locking (e.g. the asset chain)

The following methods specify the functionality of the smart contract implementing
the locking. For further information, please also look at the interface
documentation [`ILockingContract.sol`](../assets/eip-7573/contracts/ILockingContract.sol).

##### Initiation of Transfer: `inceptTransfer`

```solidity
function inceptTransfer(
    uint256 id,
    int amount,
    address from,
    address to,
    bytes memory transaction,
    bytes memory keyHashedSeller,
    bytes memory keyEncryptedSeller
) external;
```

Initiates token transfer and emits a `TransferIncepted` event.
The parameter `id` is the lifetime-unique identifier of this transfer leg. The parameters `from` and `to` are the seller and buyer, respectively.
The parameter `transaction` contains immutable application-specific transfer data.
The parameter `keyHashedSeller` is a hash of the key that can be used by the seller to (re-)claim the token.
The parameter `keyEncryptedSeller` is an encryption of the key that can be used by the seller to (re-)claim the token.
It is possible to implement the protocol in a way where the hashing method agrees with the encryption method. See below on "encryption".

##### Confirmation of Transfer: `confirmTransfer`

```solidity
function confirmTransfer(
    uint256 id,
    int amount,
    address from,
    address to,
    bytes memory transaction,
    bytes memory keyHashedBuyer,
    bytes memory keyEncryptedBuyer
) external;
```

Confirms token transfer, locks the token, and emits a `TransferConfirmed` event.
The parameters `id`, `amount`, `from`, `to`, and `transaction` MUST exactly match the immutable inception.
The parameter `keyHashedBuyer` is a hash of the key that can be used by the buyer to (re-)claim the token.
The parameter `keyEncryptedBuyer` is an encryption of the key that can be used by the buyer to (re-)claim the token.
It is possible to implement the protocol in a way where the hashing method agrees with the encryption method. See below on "encryption".

If the trade specification, that is, (`id`, `amount`, `from`, `to`, `transaction`), in a call to `confirmTransfer`
matches that of a previous call to `inceptTransfer`, and the balance is sufficient, the corresponding `amount`
of tokens is locked (transferred from `from` to the smart contract) and `TransferConfirmed` is emitted.

Both methods receive `from` and `to` explicitly. `msg.sender` identifies only the caller and
MUST NOT, by itself, determine either participant. Implementations MAY require the caller to be
a participant or an authorized operator.

##### Cancellation of Transfer: `cancelTransfer`

```solidity
function cancelTransfer(
    uint256 id,
    int amount,
    address from,
    address to,
    bytes memory transaction,
    bytes memory keyHashedSeller,
    bytes memory keyEncryptedSeller
) external;
```

Cancels an incepted token transfer before confirmation. Every argument MUST exactly match the
immutable inception. Implementations MAY require the caller to be a participant or an authorized
operator. Cancellation does not permit the `id` to be reused for another transfer.

##### Transfer: `transferWithKey`

```solidity
function transferWithKey(uint256 id, bytes memory key) external;
```

The key may be submitted by the buyer, seller, a decryption contract, or another relayer.
An implementation may restrict callers. Where the participants and terminal destinations are
already stored, however, the matching key can provide the authorization and `msg.sender` need
not be a trade participant.

Subject to the implementation's caller policy, if the hashing of `key` matches `keyHashedBuyer`,
the locked tokens are transferred to the stored buyer (`to`). This emits `TokenClaimed`.

Subject to the implementation's caller policy, if the hashing of `key` matches `keyHashedSeller`,
the locked tokens are transferred (back) to the stored seller (`from`). This emits `TokenReclaimed`.

##### Summary

The interface `ILockingContract`:

```solidity
interface ILockingContract {
    event TransferIncepted(
        uint256 id,
        int amount,
        address from,
        address to,
        bytes transaction,
        bytes keyHashedSeller,
        bytes keyEncryptedSeller
    );
    event TransferConfirmed(
        uint256 id,
        int amount,
        address from,
        address to,
        bytes transaction,
        bytes keyHashedBuyer,
        bytes keyEncryptedBuyer
    );
    event TokenClaimed(uint256 id, bytes key);
    event TokenReclaimed(uint256 id, bytes key);

    function inceptTransfer(
        uint256 id,
        int amount,
        address from,
        address to,
        bytes memory transaction,
        bytes memory keyHashedSeller,
        bytes memory keyEncryptedSeller
    ) external;
    function confirmTransfer(
        uint256 id,
        int amount,
        address from,
        address to,
        bytes memory transaction,
        bytes memory keyHashedBuyer,
        bytes memory keyEncryptedBuyer
    ) external;
    function cancelTransfer(
        uint256 id,
        int amount,
        address from,
        address to,
        bytes memory transaction,
        bytes memory keyHashedSeller,
        bytes memory keyEncryptedSeller
    ) external;
    function transferWithKey(uint256 id, bytes memory key) external;
}
```

##### Optional Same-Chain Locking with Generated Keys

`ILockingContractWithKeyGeneration` adds a generated-key inception for deployments where both contracts can call each other.

```solidity
interface ILockingContractWithKeyGeneration is
    ILockingContract,
    IDecryptionContractInceptionCallback
{
    event TransferInceptedWithKeyGeneration(
        uint256 id,
        int amount,
        address from,
        address to,
        bytes transaction,
        IDecryptionContractWithKeyGeneration decryptionContract
    );

    function inceptTransfer(
        uint256 id,
        int amount,
        address from,
        address to,
        bytes memory transaction,
        IDecryptionContractWithKeyGeneration decryptionContract
    ) external;

    function completeLock(uint256 id) external;
    function cancelTransfer(uint256 id) external;
}
```

The generated-key inception supplies the seller (`from`), buyer (`to`), and application `transaction` explicitly.
`msg.sender` identifies only the caller and MUST NOT, by itself, determine either participant.
Implementations MAY require the caller to be a participant or an authorized operator.
Instead of supplying unavailable failure-key material, the call authorizes one exact decryption contract as the immutable key source.
After the corresponding asynchronous `inceptTransfer` is submitted to that decryption contract, a direct `onInceptionCompleted` callback can invoke the same internal transition as `completeLock`.

`completeLock` MAY be permissionless because its caller supplies no participants or key material and conveys no authority.
The locking contract MUST load the stored decryption contract, require explicit inception existence and key availability, match the unique leg identifier, transaction, buyer/seller context, and exact callback address to itself, and consume the inception at most once.
The resulting transition confirms and locks the asset; it does not complete the terminal transfer, which remains the responsibility of `transferWithKey`.
In a same-chain deployment, the decryption contract MAY call `transferWithKey` on the exact callback-bound locking contract after releasing a terminal key. Such a relay MUST be best-effort: the decryption contract records and exposes its terminal outcome before the external call, and a locking-contract failure MUST NOT roll that outcome back. A permitted relayer can retry with the released key.
If the asset inception is cancelled while key generation is pending, the locking contract MUST retain a tombstone and acknowledge a later matching authenticated callback without locking, so the decryption contract can complete and expose the failure-key path.
An implementation accepting arbitrary interface-typed decryption contracts is unsafe: it MUST use a trusted implementation, an allowlist, or an independently authenticated seller authorization.

#### Smart Contract on the other chain that performs the conditional decryption (e.g. the payment chain)

The following methods specify the functionality of the smart contract implementing
the conditional decryption. For further information, please also look at the interface
documentation [`IDecryptionContract.sol`](../assets/eip-7573/contracts/IDecryptionContract.sol).

##### Initiation of Transfer: `inceptTransfer`

```solidity
function inceptTransfer(
    uint256 id,
    int amount,
    address from,
    address to,
    bytes memory transaction,
    bytes memory keyEncryptedSuccess,
    bytes memory keyEncryptedFailure
) external;
```

Initiates payment transfer and emits a `TransferIncepted` event.
The parameter `id` is the lifetime-unique identifier of this transfer leg. The parameters `from` and `to` are the sender and receiver of the payment, respectively.
The parameter `transaction` contains immutable application-specific transfer data.
The parameter `keyEncryptedSuccess` is an encryption of a key and will be decrypted if the transfer is successful in a call to `transferAndDecrypt`.
The parameter `keyEncryptedFailure` is an encryption of a key and will be decrypted if the transfer fails in a call to `transferAndDecrypt` or if `cancelAndDecrypt` is successful.

##### Application Initialization and Transfer Identity

ERC-7573 does not standardize an `initTransfer` method. Before the first ERC-7573 call,
the application workflow MUST allocate an identifier for each transfer leg and bind the arbitrary
application data carried in `transaction`. Each implementation MUST treat that identifier as
lifetime-unique within the contract. Corresponding locking and decryption contracts MAY use the
same numeric `id` to correlate the two sides of one DvP operation. How the parties establish that
application-level agreement is outside this interface.

The first valid inception stores `id`, `amount`, `from`, `to`, `transaction`, and its key
references as immutable state. The implementation MUST reject any later inception that reuses
the `id`, including after completion or cancellation. A multi-party or group identifier MUST be
encoded inside `transaction`; distinct transfer legs handled by the same implementation MUST NOT
share an ERC-7573 `id`.
Oracle operations continue to use their separate oracle-assigned `requestId` for callback
correlation.

The `from` and `to` participants are supplied explicitly. `msg.sender` identifies only the caller
and MUST NOT, by itself, determine either participant. Implementations MAY require the caller to be
a participant or an authorized operator. Because `id` identifies one
immutable leg and confirmation and cancellation repeat the complete transfer context—including
the asynchronous callback binding or zero—and both key references, separate inception and
confirmation hashes add no commitment and are omitted.
Implementations MUST compare every repeated argument, including dynamic byte strings, with the
stored inception before applying a state transition.

The success/failure key order is normative. Implementations receiving an unordered key batch
MUST select the keys by their semantic identifiers and MUST NOT use callback array position.
The two outcome references MUST identify distinct keys. An implementation MUST reject equal
encrypted references or equal locking representations where those values are directly comparable;
otherwise success and failure cannot produce unambiguous terminal outcomes.

##### Asynchronous Initiation of Transfer: `inceptTransfer`

The optional `IDecryptionContractWithKeyGeneration` interface extends `IDecryptionContract` with one asynchronous function that does not require keys to exist at inception:

```solidity
function inceptTransfer(
    uint256 id,
    int amount,
    address from,
    address to,
    bytes memory transaction,
    IDecryptionContractInceptionCallback callback
) external;
```

The parameter `transaction` is the transaction specification used for asynchronous generation of the encrypted success and failure keys.
The call stores `id`, `amount`, `from`, `to`, `transaction`, and `callback` as an immutable pending inception.
The `id` MUST satisfy the same lifetime-uniqueness requirement as synchronous inception.
The `callback` parameter MAY be `address(0)` when event-based off-chain continuation is sufficient.
Solidity callers express this as `IDecryptionContractInceptionCallback(address(0))`; ABI callers supply the full twenty-byte zero address.
Any nonzero callback MUST implement `IDecryptionContractInceptionCallback` and provide an on-chain completion trigger.
The generated encrypted and hashed success/failure key material MUST be validated and stored atomically with the pending inception and MUST NOT be replaceable afterwards.
In the same state transition, the contract MUST select the success and failure keys by semantic role and complete the inception.
The contract MUST emit `TransferIncepted` only after both keys have been stored.

If `callback` is not `address(0)`, after storing the complete state and emitting `TransferIncepted`, the decryption contract MUST call:

```solidity
callback.onInceptionCompleted(id)
```

Before this call, the decryption contract MUST make the complete inception context and immutable key material readable through `getInceptionContext(id)` and `getInceptionKeyMaterial(id)`.
The getters return explicit `exists` and `available` values; callers MUST NOT infer either state from zero or empty values.
The callback MUST authenticate the calling decryption contract, match `id` and every semantically corresponding context field to a pending local operation, require `available`, and verify the immutable key material before applying effects. Corresponding asset and payment legs MAY use different amounts, so an asset receiver matches the reversed participants, `transaction`, and callback binding rather than requiring its asset amount to equal the payment amount.
The success/failure ordering of the getter is normative.
The callback MUST return `IDecryptionContractInceptionCallback.onInceptionCompleted.selector`.
If the callback reverts, runs out of its bounded gas allowance, or returns any other value, the completion attempt MUST revert and leave the inception pending for the asynchronous fulfillment mechanism to retry.
If `callback` is `address(0)`, the decryption contract MUST skip callback delivery.
No additional readiness event is required because `TransferIncepted` already signals that the keys are final.

Calls to `confirmTransfer`, `transferAndDecrypt`, and `cancelAndDecrypt` for a pending inception MUST revert until then.
The key-generation mechanism is outside the scope of this interface.

##### Confirmation of Transfer: `confirmTransfer`

```solidity
function confirmTransfer(
    uint256 id,
    int amount,
    address from,
    address to,
    bytes memory transaction,
    IDecryptionContractInceptionCallback callback,
    bytes memory keyEncryptedSuccess,
    bytes memory keyEncryptedFailure
) external;
```

Confirms a completed payment inception and emits a `TransferConfirmed` event.
Every argument MUST exactly match the immutable completed inception. `callback` MUST equal the
stored asynchronous callback, or `address(0)` for a synchronous inception. The `from` and `to`
participants MUST be supplied explicitly. `msg.sender` identifies only the caller and MUST NOT,
by itself, determine either participant. Implementations MAY require the caller to be a participant
or an authorized operator and MUST validate the lifecycle state.

For a two-party DvP, a successful confirmation MAY directly invoke the internal finalization logic.
For a multi-party DvP, the implementation MUST freeze the expected leg set and finalizer policy before accepting the first confirmation; confirmation then marks this payment leg as confirmed without finalizing the group.
The implementation MUST authorize the finalizer independently of the explicit transfer participants.

##### Transfer: `transferAndDecrypt`

```solidity
function transferAndDecrypt(uint256 id) external;
```

Called by an authorized finalizer or operator to initiate completion of the confirmed payment transfer or multi-party DvP. Emits a `TransferKeyRequested` with the encrypted key selected by the completion result.
The method loads the confirmed leg or group state and its immutable transfer values from storage.
It MUST reject the call unless the caller satisfies the configured finalizer policy, the selected
leg is confirmed, every required leg in any frozen group is confirmed, and neither execution nor
cancellation has already been requested.
No additional context is needed at finalization because the lifetime-unique `id` selects immutable confirmed state.

##### Cancellation of Transfer: `cancelAndDecrypt`

```solidity
function cancelAndDecrypt(
    uint256 id,
    int amount,
    address from,
    address to,
    bytes memory transaction,
    IDecryptionContractInceptionCallback callback,
    bytes memory keyEncryptedSuccess,
    bytes memory keyEncryptedFailure
) external;
```

Cancels the specific payment transfer and requests its failure key. Every argument MUST exactly
match the immutable completed inception. Implementations MAY require the caller to be a participant
or an authorized operator. If these preconditions are met and a valid call to `transferAndDecrypt` has not been issued before,
i.e. if the stored success key has not been issued in a `TransferKeyRequested` event,
then this method emits a `TransferKeyRequested` with the stored failure key.

##### Release of ILockingContract Access Key: `releaseKey`

```solidity
function releaseKey(uint256 id, bytes memory key) external;
```

Called from the (possibly external) decryption oracle.

Emits the event `TransferKeyReleased` with the value of `key` if the call was eligible.

##### Summary

The interface `IDecryptionContract`:

```solidity
interface IDecryptionContract {
    event TransferIncepted(
        uint256 id,
        int amount,
        address from,
        address to,
        bytes transaction,
        bytes keyEncryptedSuccess,
        bytes keyEncryptedFailure
    );
    event TransferConfirmed(
        uint256 id,
        int amount,
        address from,
        address to,
        bytes transaction,
        IDecryptionContractInceptionCallback callback,
        bytes keyEncryptedSuccess,
        bytes keyEncryptedFailure
    );
    event TransferKeyRequested(address sender, uint256 id, bytes encryptedKey);
    event TransferKeyReleased(address sender, uint256 id, bool success, bytes key);

    function inceptTransfer(
        uint256 id,
        int amount,
        address from,
        address to,
        bytes memory transaction,
        bytes memory keyEncryptedSuccess,
        bytes memory keyEncryptedFailure
    ) external;
    function confirmTransfer(
        uint256 id,
        int amount,
        address from,
        address to,
        bytes memory transaction,
        IDecryptionContractInceptionCallback callback,
        bytes memory keyEncryptedSuccess,
        bytes memory keyEncryptedFailure
    ) external;
    function transferAndDecrypt(uint256 id) external;
    function cancelAndDecrypt(
        uint256 id,
        int amount,
        address from,
        address to,
        bytes memory transaction,
        IDecryptionContractInceptionCallback callback,
        bytes memory keyEncryptedSuccess,
        bytes memory keyEncryptedFailure
    ) external;
    function releaseKey(uint256 id, bytes memory key) external;
}
```

The optional key-generation extension:

```solidity
interface IDecryptionContractInceptionCallback {
    function onInceptionCompleted(uint256 id) external returns (bytes4 acknowledgement);
}

interface IDecryptionContractWithKeyGeneration is IDecryptionContract {
    function inceptTransfer(
        uint256 id,
        int amount,
        address from,
        address to,
        bytes memory transaction,
        IDecryptionContractInceptionCallback callback
    ) external;

    function getInceptionContext(uint256 id) external view returns (
        bool exists,
        int amount,
        address from,
        address to,
        bytes memory transaction,
        IDecryptionContractInceptionCallback callback
    );

    function getInceptionKeyMaterial(uint256 id) external view returns (
        bool available,
        bytes memory keyEncryptedSuccess,
        bytes memory keyHashedSuccess,
        bytes memory keyEncryptedFailure,
        bytes memory keyHashedFailure
    );
}
```

### Interfaces to External Decryption Oracles (Oracle Proxy + Callback)

This proposal additionally standardizes the on-chain interaction with external (off-chain) decryption oracles via:

- `IKeyDecryptionOracle` (oracle proxy / router)
- `IKeyDecryptionOracleCallback` (consumer callback)

The general flow is:

1. The consumer calls `request*` on the oracle proxy contract (payable), receives the oracle-assigned `requestId`, and stores its operation context under `(oracleProxy, requestId)`.
2. The oracle proxy emits a request event containing the same `requestId` and the consumer-supplied `id`.
3. The off-chain oracle observes the request event, performs decryption or verification off-chain, and calls `fulfill*` on the proxy.
4. The oracle proxy calls the consumer callback `on*` with `requestId` and the fulfillment payload.

The `requestId` MUST be unique within its oracle proxy. Request methods MUST return before attempting the corresponding callback so the consumer can store the returned identifier first.
The oracle method's consumer-supplied context `id` remains event context with consumer-defined semantics and MAY be the ERC-7573 transfer-leg identifier. It is independent of, and MUST NOT be confused with, the oracle proxy's separately assigned `requestId`.

This changes the meaning, but not the ABI type, of the callback's first `uint256` argument.
Existing callback implementations that interpret it as the consumer-supplied `id` MUST be migrated before use with a requestId-based oracle proxy; the unchanged function selector does not provide a version boundary.

#### Batch key generation

Key generation is batch-oriented. The consumer calls
`requestGenerateEncryptedHashedKeys` with a non-empty array of distinct `keyIds`.
Each `keyId` identifies the semantic role of one generated key; a request for a single key
uses a one-element array, so no separate singular method is required.

```solidity
struct EncryptedHashedKey {
    bytes32 keyId;
    bytes encryptedKey;
    bytes hashedKey;
}

function requestGenerateEncryptedHashedKeys(
    uint256 id,
    IKeyDecryptionOracleCallback callback,
    address receiverContract,
    bytes calldata transaction,
    bytes32[] calldata keyIds
) external payable returns (uint256 requestId);

function fulfillEncryptedHashedKeysGeneration(
    uint256 requestId,
    IKeyDecryptionOracleCallback.EncryptedHashedKey[] calldata keys,
    address receiverContract,
    bytes calldata transaction
) external;

function onEncryptedHashedKeysGenerated(
    uint256 requestId,
    EncryptedHashedKey[] calldata keys,
    address receiverContract,
    bytes calldata transaction
) external;
```

The fulfillment MUST contain exactly one result for every requested `keyId`: duplicate,
missing, or unrequested identifiers MUST be rejected. Array ordering has no semantic
meaning. The oracle proxy MUST also verify that `receiverContract` and `transaction`
match the request before invoking `onEncryptedHashedKeysGenerated` once with the complete
batch. It MUST NOT deliver a partial batch.

Implementations MUST enforce a documented finite maximum generation batch size. Every
generated reference MUST authenticate its `keyId` and the same unique, one-use settlement
context, bound to the requesting contract, lifetime-unique transfer `id`, and full generation
context. The consuming contract MUST enforce lifetime non-reuse
of that context. Generation that binds only recurring trade economics is replayable even if
the complete key pair is later verified.

#### Batch key verification

Verification is likewise batch-oriented and atomic. The request contains the encrypted
keys together with their semantic roles; a one-element batch covers the singular case.

```solidity
struct EncryptedKey {
    bytes32 keyId;
    bytes encryptedKey;
}

function requestVerifyEncryptedKeys(
    uint256 id,
    EncryptedKey[] calldata keys,
    IKeyDecryptionOracleCallback callback
) external payable returns (uint256 requestId);

function fulfillEncryptedKeysVerification(
    uint256 requestId,
    bool verified,
    IKeyDecryptionOracleCallback.EncryptedHashedKey[] calldata keys,
    address receiverContract,
    bytes calldata transaction
) external;

function onEncryptedKeysVerificationCompleted(
    uint256 requestId,
    bool verified,
    EncryptedHashedKey[] calldata keys,
    address receiverContract,
    bytes calldata transaction
) external;
```

The proxy MUST reject an empty request, duplicate `keyId` values, empty encrypted keys,
and oversized batches. It MUST retain or commit to the exact requested
`(keyId, encryptedKey)` set. A fulfillment MUST echo exactly that complete set,
independent of array order, and MUST be delivered in one callback. Missing, additional,
substituted, or partially verified entries MUST be rejected.

`verified` is the authoritative batch result; consumers MUST NOT infer it from empty
values. If it is true, every returned hash MUST be non-empty and every encrypted key MUST
have verified against the same `receiverContract` and `transaction`. If it is false, the
receiver MUST be `address(0)`, the transaction MUST be empty, and the entire batch is
rejected. Per-key success is deliberately not represented.

Every encrypted reference MUST authenticate its `keyId` and a common, unique, one-use
settlement context, such as `(chainId, decryptionContract, id)`, in
addition to the common external transaction/batch identifier. Merely returning an array
does not prevent replay of an old complete key pair. Verification-only access to a
reference MUST NOT itself authorize decryption or release of that key.

`encryptedKey` is the protocol's portable key reference; confidentiality of that reference
is not required. An integration MAY use a publicly readable, versioned and signed byte
sequence that identifies the external settlement and authenticates the fields above. This
lets another participant use its own oracle adapter to verify every outcome reference. The
actual key MUST remain undisclosed, and decryption/release authority MUST be enforced
independently of possession or readability of the reference.

Decryption remains a singular operation. A DvP outcome releases exactly one of the
verified outcome keys; an array decryption API would weaken that exclusivity invariant.

#### Callback execution semantics: strict vs best-effort

Implementations MAY choose one of the following fulfillment semantics. Both are compatible with this proposal.

##### Strict fulfillment (reverting)

In **strict fulfillment**, the proxy MUST revert `fulfill*` if the callback call fails (including OOG).

Properties:

- The off-chain oracle operator can treat `receipt.status == 1` as "callback succeeded".
- If `receipt.status == 0`, the request is not fulfilled and can be retried (e.g., with higher tx gas limit).
- The proxy MUST ensure that request state is not lost on revert (e.g., by relying on revert rollback of state changes).

This mode is operationally simple for closed deployments where the off-chain oracle and the consumer are coordinated and where failure handling/retries are primarily managed by the oracle operator.

##### Best-effort fulfillment (non-reverting)

In **best-effort fulfillment**, the proxy MUST NOT revert `fulfill*` solely because the callback call fails (including OOG). Instead, it SHOULD signal callback outcome via events (e.g. `CallbackSucceeded` / `CallbackFailed`). `CallbackFailed` carries `(requestId, callback, selector, consumerId, reason)`; `reason` contains callback revert or return data when available and MAY be empty, including after OOG.

Properties:

- The off-chain oracle operator MUST NOT interpret `receipt.status == 1` as "callback succeeded"; it must also evaluate the emitted outcome signal.
- Failure handling can be shifted to the callback implementer/operator: a consumer can run an off-chain watcher that subscribes to `CallbackFailed` and reacts accordingly (e.g. pull/consume flow, re-request, alerting).
- The proxy may either keep the request pending for retries or consume it and shift retry responsibility to the consumer. The chosen policy SHOULD be documented by the implementation.

This mode is useful when the proxy wants to provide an on-chain observable audit trail for callback failures and to decouple "oracle fulfillment" from "consumer processing".

#### Gas budgeting and forwarding

- The off-chain oracle controls the total cost of `fulfill*` by setting the transaction gas limit.
- The proxy MAY cap or budget the gas forwarded to the callback (e.g., by forwarding "all but a reserve").
- Keeping a small gas reserve in the proxy can help ensure the proxy can finalize `fulfill*` and emit outcome events even if the callback consumes most forwarded gas.

#### Calldata fallback (retrieving fulfillment payload without logging)

In either strict or best-effort mode, the `fulfill*` payload is present in the transaction input calldata of the `fulfill*` call.

Implementations SHOULD document the following operational fallback:

- Off-chain systems that observe an event (e.g., `CallbackFailed`) can use the event's `transactionHash` to fetch the corresponding transaction and decode the input calldata using the `IKeyDecryptionOracle` ABI to recover the fulfillment arguments.
- Practical caveat: Some RPC providers prune old transaction bodies; indexers SHOULD persist decoded fulfillment payload off-chain if long-term retention is required.

This fallback can be used to support consumer-side "pull/consume" flows, or as a recovery mechanism when callback execution fails.

### Encryption and Decryption

The linkage of the two smart contracts relies on use of a `key`, `encryptedKey` and `hashedKey`.
For compatibility the field is named `encryptedKey`, but it may contain either ciphertext or
an authenticated external-system key reference. The implementation is free to support several
encodings as long as the decryption oracle supports them and key-release authorization does not
depend merely on keeping the reference confidential.

The encryption is performed with the public key of  the decryption oracle.
Either the encryption oracle offers a method performing encryption, in which
case the encryption method isn't even required to be known, or both parties
know the public key of the decryption oracle and can perform the generation
of the key and its encryption.

It is implicitly assumed that the two parties may check that
the strings `keyEncryptedBuyer` and `keyEncryptedSeller` are
in a valid format.

To avoid on-chain encryption in the `ILockingContract`, it is possible to use a
simpler hashing algorithm  on the `ILockingContract`. In that case, the decryption oracle has
to provide a method that allows to obtain the hash *H(K)* (`keyHashed`) for an
encrypted key *E(K)* (`keyEncrypted`) without exposing the key *K* (``key`), cf. [^2].


### Sequence diagram of delivery versus payment

The interplay of the two smart contracts is summarized
in the following sequence diagram:

![sequence diagram dvp](../assets/eip-7573/doc/DvP-Seq-Diag.png)

The method declarations above are normative; the diagram illustrates the protocol flow and predates the current explicit-argument and requestId-based callback APIs.

## Rationale

Each implementation treats `id` as a lifetime-unique identifier for one transfer leg and MUST
reject reuse even after that leg completes or is cancelled. Corresponding locking and decryption
contracts may use the same numeric `id` for the two sides of that operation. Applications
correlate several distinct legs by encoding a shared group identifier and any other group
definition in each leg's immutable `transaction` data. Oracle requests use a separate,
oracle-assigned `requestId`.

The `key` and the `encryptedKey` arguments are strings to
allow the flexible use of different encryption schemes.
The decryption/encryption scheme should be inferable from the contents
of the `encryptedKey`.

### Ensuring Secure Key Decryption - Key Format

It has to be ensured that the decryption oracle decrypts a key only for the eligible contract.

It seems as if this would require us to introduce a concept of eligibility to the decryption oracle, which would imply a kind of state.

A fully stateless decryption can be realized by introducing a document format for the key and a corresponding eligibility verification protocol. We propose the following elements:

- The (unencrypted) key documents contain the address of the payment contract implementing `IDecryptionContract`.
- The decryption oracle offers a stateless batch function `verify` that receives role-tagged encrypted keys and returns their hashes and common transaction context without returning any decrypted key. Every key must bind the same callback/receiver and unique settlement context.
- When an encrypted key is presented to the decryption oracle, the oracle decrypts the document and passes the decrypted key to `releaseKey` of the callback contract address found within the document decrypted key.

We propose the following XML schema for the document of the decrypted key:
```xml
<?xml version="1.0" encoding="utf-8"?>
<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" targetNamespace="http://finnmath.net/erc/ILockingContract" xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="releaseKey">
        <xs:complexType>
            <xs:simpleContent>
                <xs:extension base="xs:string">
                    <xs:attribute name="contract" type="xs:string" use="required" />
                    <xs:attribute name="transaction" type="xs:unsignedShort" use="required" />
                </xs:extension>
            </xs:simpleContent>
        </xs:complexType>
    </xs:element>
</xs:schema>
```

A corresponding XML sample is shown below.
```xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<releaseKey contract="eip155:1:0x1234567890abcdef1234567890abcdef12345678" transaction="3141" xmlns="http://finnmath.net/erc/ILockingContract">
    <!-- random data -->
    zZsnePj9ZLPkelpSKUUcg93VGNOPC2oBwX1oCcVwa+U=
</releaseKey>
```

### Multi-Party Delivery versus Payment

#### Locking is a Feature

In Delivery-versus-Payment (DvP) protocols like [ERC-7573](./eip-7573.md), at least one token must be locked to ensure atomicity, even if only for a short period during the transaction.

While locking may appear as an inconvenient necessity, it is in fact a feature that becomes valuable in the construction of conditional trades or multi-party DvPs.

If *n* parties wish to perform bilateral transactions atomically, there are *at least* *m := 2 • (n - 1)* transactions, of which *m-1* require locking. The last one can operate directly, and its success or failure decides whether the other locks are released or reverted.

A multi-party delivery versus payment is a valuable trade feature. Consider, for example, the case where counterparty A wishes to buy a token *Y* (e.g., a bond) from counterparty C, but in order to fund this transaction, counterparty A wishes to sell a token *X* (e.g., another bond) to counterparty B. However, A does not want to sell bond *X* if the purchase of *Y* fails. A multi-party DvP allows these two transactions to be bound into a single atomic unit.

While for a two-party DvP with two tokens only one token requires locking—and hence a DvP can be constructed without locking on the cash chain—a three-party DvP with three tokens in general requires the ability to lock all three tokens.

This highlights that locking is not just a constraint, but a required feature to enable advanced and economically meaningful protocols.

#### N-DvP with [ERC-7573](./eip-7573.md)

A multi-party DvP can be created elegantly by combining multiple (*n-1*) two-party DvPs, for example based on the [ERC-7573](./eip-7573.md) protocol.

Every payment leg in the group uses its own lifetime-unique ERC-7573 `id`. A common group identifier and any frozen group definition are encoded in the immutable `transaction` data of each leg.
Instead of finalizing the respective two-party DvP immediately, each completed payment leg is first confirmed by repeating its complete context, callback binding (or zero), and both key references, leaving group finalization open.

At any time before finalization, an authorized submitter can call `cancelAndDecrypt` with that leg's complete matching context and key references to release the failure key and revert all lockings.

Before accepting the first confirmation, the implementation MUST freeze the expected set of payment legs and the finalizer policy for the group.
It MUST NOT add a leg after that point or treat merely all currently registered legs as a complete group.
Every leg MUST be bound to the same group outcome: an implementation MAY use one shared success/failure key pair, or it MUST store and request the complete frozen set of per-leg outcome keys.

The asynchronous inception callback remains singular and is part of the immutable inception context.
Under `ILockingContractWithKeyGeneration`, each asset lock MUST consume an inception that binds that exact asset as callback; one callback-bound inception cannot be replayed across several asset contracts.
A future coordinator fan-out extension would need to commit the complete frozen asset set and define one-shot group completion explicitly.
An unbounded callback array is not required by this interface.

Once every leg in the frozen set is confirmed, a call to `transferAndDecrypt(id)` on the designated coordinating leg performs locking of the token implementing the `IDecryptionContract` and requests all success keys on success or all failure keys on failure.

##### Initiation and Finalization

The frozen group definition MUST determine the `from` and `to` participants and establish which
submitters are authorized to finalize the group via `transferAndDecrypt`. `msg.sender` identifies
only the finalization caller and MUST NOT, by itself, determine either participant. The finalizer
policy MAY authorize a participant or another operator.

##### Sequence Diagram

Below we depict the corresponding sequence diagram of a multi-party DvP via [ERC-7573](./eip-7573.md).
Note that the individual DvP may come in two different flavors depending on which counterparty is the receiver of the token on the `IDecryptionContract`.

The diagram depicts a multi-party dvp with n+1 counterparties trading n+1 tokens out of which
the DvPs are bound by the contract on token 0.

![sequence diagram multi party dvp](../assets/eip-7573/doc/multi-party-dvp.svg)

The method declarations above are normative. This historical diagram is illustrative and predates the current explicit-argument ABI.

*Note: The more general case of N counterparties trading
M tokens is just a special case where we enumerate all combination as new counterparties and new tokens.*

## Security Considerations

The decryption oracle does not need to be a single trusted entity. Instead, a threshold decryption scheme can be employed, where multiple oracles perform partial decryption, requiring a quorum of them to reconstruct the secret key. This enhances security by mitigating the risk associated with a single point of failure or trust.

In such cases, each participating decryption oracle will observe the decryption request from an emitted `TransferKeyRequested` event, and subsequently call the `releaseKey` method with a partial decryption result. The following sequence diagram illustrates this.

![sequence diagram distributed oracle](../assets/eip-7573/doc/Distributed-Oracle.png)

See [^2] for details.

Additional considerations for the oracle proxy + callback pattern:

- Callback implementations SHOULD restrict callers (e.g. `require(msg.sender == oracleProxy)`), otherwise any address could invoke `on*` directly.
- Callback implementations MUST validate a pending `(oracleProxy, requestId)` of the expected operation kind and consume or mark it before applying callback effects.
- Oracle proxies MUST make a request unavailable to concurrent or reentrant fulfillment before invoking its callback. A best-effort proxy MAY restore the request to pending after callback failure to permit retry.
- Batch verification MUST match the exact requested key set by `keyId`, authenticate every encrypted reference to the same one-use settlement and external batch context, and resolve atomically. Verification of only the success key does not authenticate the failure path.
- A verifier's ability to authenticate a failure-key reference MUST remain separate from authority to decrypt or release that failure key.
- Callback implementations SHOULD be cheap and should avoid unbounded loops or expensive state changes. If heavy work is required, prefer a pull/consume pattern initiated by the consumer.
- Oracle proxy implementations SHOULD document whether they use strict or best-effort fulfillment semantics, and how retries are intended to be handled (oracle-operated retry vs consumer-operated recovery).

For a nonzero asynchronous inception callback, implementations MUST store the completed inception before the external call, use a bounded gas allowance, validate the selector-valued acknowledgement, and rely on the asynchronous fulfillment retry path if delivery fails.
The receiver MUST authenticate the expected decryption contract, match the lifetime-unique `id` and every semantically corresponding getter field to a pending local operation, require explicit key availability, and verify the immutable key material before applying effects. For paired asset and payment legs, the respective amounts are independent; the receiver instead matches the reversed participants, `transaction`, and callback binding.
An `ILockingContractWithKeyGeneration` receiver MUST also require the getter's callback to equal its own address; matching only the identifier and participants would permit one inception to be replayed across asset contracts.

[^1]:
    ```csl-json
        {
          "type": "article",
          "id": 1,
          "author": [
            {
              "family": "La Rocca",
              "given": "Rosario"
            },
            {
              "family": "Mancini",
              "given": "Riccardo"
            },
            {
              "family": "Benedetti",
              "given": "Marco"
            },
            {
              "family": "Caruso",
              "given": "Matteo"
            },
            {
              "family": "Cossu",
              "given": "Stefano"
            },
            {
              "family": "Galano",
              "given": "Giuseppe"
            },
            {
              "family": "Mancini",
              "given": "Simone"
            },
            {
              "family": "Marcelli",
              "given": "Gabriele"
            },
            {
              "family": "Martella",
              "given": "Piero"
            },
            {
              "family": "Nardelli",
              "given": "Matteo"
            },
            {
              "family": "Oliviero",
              "given": "Ciro"
            }
          ],
          "DOI": "10.2139/ssrn.4386904",
          "title": "Integrating DLTs with Market Infrastructures: Analysis and Proof-of-Concept for Secure DvP between TIPS and DLT Platforms",
          "original-date": {
            "date-parts": [
              [2022, 7, 19]
            ]
          },
          "URL": "http://dx.doi.org/10.2139/ssrn.4386904"
        }
    ```

[^2]:
    ```csl-json
        {
          "type": "article",
          "id": 2,
          "author": [
            {
              "family": "Fries",
              "given": "Christian"
            },
            {
              "family": "Kohl-Landgraf",
              "given": "Peter"
            }
          ],
          "DOI": "10.2139/ssrn.4628811",
          "title": "A Proposal for a Lean and Functional Delivery versus Payment across two Blockchains",
          "original-date": {
            "date-parts": [
              [2023, 11, 9]
            ]
          },
          "URL": "http://dx.doi.org/10.2139/ssrn.4628811"
        }
    ```

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
