---
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/erc-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").
The smart contract implementing `ILockingContract` locks a token (e.g., the asset) on its chain until a key is presented to encrypt to one of two given 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, bytes keyHashedSeller, bytes memory keyEncryptedSeller) external;
```

Called from the buyer of the token to initiate token transfer. Emits a `TransferIncepted` event.
The parameter `id` is an identifier of the trade. The parameter `from` is the address of the seller (the address of the buyer is `msg.sender`).
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 buyer to 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".

##### Initiation of Transfer: `confirmTransfer`

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

Called from the seller of the token to confirm token transfer. Emits a `TransferConfirmed` event.
The parameter `id` is an identifier of the trade. The parameter `to` is the address of the buyer (the address of the seller is `msg.sender`).
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 possibly 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, the quadruple (`id`, `amount`, `from`, `to`), 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.

##### Transfer: `transferWithKey`

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

Called from either the buyer or the seller of the token
of the trade with id `id`.

If the method is called from the buyer (`to`) *and* the hashing of `key` matches `keyHashedBuyer`,
then the locked tokens are transferred to the buyer (`to`). This emits `TokenClaimed`.

If the method is called from the seller (`from`) *and* the hashing of `key` matches `keyHashedSeller`,
then the locked tokens are transferred (back) to the seller (`to`). This emits `TokenReclaimed`.

##### Summary

The interface `ILockingContract`:

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

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

#### 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, bytes memory keyEncryptedSuccess, bytes memory keyEncryptedFailure) external;
```

Called from the receiver of the amount to initiate payment transfer. Emits a `TransferIncepted`.
The parameter `id` is an identifier of the trade. The parameter `from` is the address of the sender of the payment (the address of the receiver is `msg.sender`).
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.

##### Transfer: `transferAndDecrypt`

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

Called from the sender of the amount to initiate completion of the payment transfer. Emits a `TransferKeyRequested` with keys depending on completion success.
The parameter `id` is an identifier of the trade. The parameter `to` is the address of the receiver of the payment (the sender of the payment (from) is implicitly the `msg.sender`).
The parameter `keyEncryptedSuccess` is an encryption of the key and will be decrypted if the transfer is successful.
The parameter `keyEncryptedFailure` is an encryption of the key and will be decrypted if the transfer fails.

The method will not decrypt any key and not perform a transfer of a payment if the values (`id`, `amount`, `from` `to`, `keyEncryptedSuccess`, `keyEncryptedFailure`)
do not match a previous call to `inceptTransfer`.

##### Cancelation of Transfer: `cancelAndDecrypt`

```solidity
function cancelAndDecrypt(uint256 id, address from, bytes memory keyEncryptedSuccess, bytes memory keyEncryptedFailure) external;
```

Called from the receiver of the amount to cancel payment transfer (cancels the incept transfer).

The method must be called from the caller of a previous call to `inceptTransfer`
with the exact same arguments and cancels this specific transfer.
If these preconditions are met and a valid call to `transferAndDecrypt` has not been issued before,
i.e. if `keyEncryptedSuccess` has not been issued in a `TransferKeyRequested` event,
then this method emits a `TransferKeyRequested` with the key `keyEncryptedFailure`.

##### 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 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, bytes memory keyEncryptedSuccess, bytes memory keyEncryptedFailure) external;
    function transferAndDecrypt(uint256 id, int amount, address to, bytes memory keyEncryptedSuccess, bytes memory keyEncryptedFailure) external;
    function cancelAndDecrypt(uint256 id, address from, bytes memory keyEncryptedSuccess, bytes memory keyEncryptedFailure) external;
    function releaseKey(uint256 id, bytes memory key) external;
}
```

### 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).
2. The oracle proxy emits a request event containing a `requestId` (correlation 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 the fulfillment payload.

#### 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`).

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`.
The implementation is free to support several encryption methods for
as long as the decryption oracle supports it.

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)

## Rationale

The protocol tries to be parsimonious. The transfer
is associated with a (preferably unique) `id` possibly
generated by some additional interaction of the trading
parties.

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 description 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 function `verify` that receives an encrypted key and returns the callback address (that will be used for the `releaseKey` call) that is stored inside the decrypted key without returning the decrypted key.
- 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.

The procedure is simple: instead of finalizing the respective two-party DvP by a call to `transferAndDecrypt`, the two-party DvP is first confirmed with a call to `confirm`, leaving the finalization open.

At any time, any party can call `cancelAndDecrypt` to release the failure key and revert all lockings.

Once all parties are linked with their respective two-party DvPs, a single call to `transferAndDecrypt` performs locking of the token implementing the `IDecryptionContract` and releases either the success key on success or the failure key on failure.

##### Initiation and Finalization

The counterparty that initiates the multi-party DvP by making the first call
to the `IDecryptionContract` is the one that is allowed to finalize it via `transferAndDecrypt`, the other may cancel via `cancelAndDecrypt`.

##### 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)

*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 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).

## Copyright

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



[^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"
        }
    ```
