What is Delivery Versus Payment?

In traditional finance, Delivery Versus Payment (DVP) means the settlement of securities so that delivery only occurs if payment occurs. If we extend that to a blockchain, we can think of a smart contract that arranges atomic swaps of multiple assets between multiple parties. For example, a combination of ETH, ERC-20 tokens and ERC-721 tokens all moving between multiple parties in a single transaction. Either everyone gets what they are owed, or no transaction occurs at all.

We can imagine gold being exchanged for some land in the Wild West, where the exchange is simultaneous. Both parties seem pretty happy with the deal:

Delivery Versus Payment: Simultaneous Exchange of Goods
Delivery Versus Payment: Simultaneous Exchange of Goods

The full smart contract repo is here and is open-sourced under the MIT license. The smart contracts allow a single “exchange event” (a “settlement”) to be carried out between an arbitrary number of parties with an arbitrary number of different assets, including Ether, ERC-20 tokens and NFTs.

Let me try it!

Live UIs are available at:

The UIs share the same permissionless backend contracts, but have a different look and feel. Anyone can create a new UI on top of these contracts.

Delivery versus Payment Smart Contract Features

  1. No fees other than cost of gas.
  2. Non-upgradeable, singleton Delivery Versus Payment contract, minimizes trust required.
  3. Allows atomic swaps of an arbitrary number of assets between an arbitrary number of parties. No party risks sending assets without receiving what is promised (or getting their assets back).
  4. Permissionless, anyone can create and execute these swaps, so long as the from address parties have approved.
  5. Supports assets including native ETH, ERC-20 and ERC-721.
  6. Optional auto-settlement, the final approving party can trigger auto-processing of the settlement.
  7. Helper contract provides search functionality for off-chain use.
  8. Anyone can create and host a UI for the same set of deployed contracts, or can deploy more contracts.

Interesting Solidity Features

There is no Owner

The permissionless nature of the contract was a design choice we made to ensure the contract would be useful to other projects. This means no settings that onlyOwner can change, which contributed to why we chose to not limit the sizes of settlements. There are many loops through unbounded arrays, by design, the idea being that each chain’s block gas limit will provide a natural maximum size. Settlements of around 100 to 200 asset movements between parties should be fine.

Handling Reverts during Auto-Settlement

An interesting detail behind the auto-settlement feature was how to handle cases where settlement execution failed. Auto-settlement is triggered by a party being the last approver, and if settlement execution failed we don’t want the approval to also fail — we still want the overall EVM transaction to succeed while acknowledging that the execution itself can fail. Like other languages, Solidity has a try-catch construct, but unlike other languages it also warrants a 40-page write up about the nuances of using it.

One of those nuances drove a structural decision: Solidity’s try{} only supports external or public function calls. This meant the actual settlement execution logic had to be extracted into a dedicated external function, executeSettlementInner, which guards itself by reverting if the caller is not the DVP contract itself. This inner function is then called via try in two places: inside approveSettlements() for auto-execution, and inside the public executeSettlement() for manual execution. In the auto-execution case, a failure is swallowed (the approval remains, the settlement can be retried); in the manual case the error is re-thrown so the caller gets a proper revert. The same three failure events are emitted in both paths, with an autoExecuted boolean parameter to distinguish which path triggered the failure. We simplified the catch clauses to cover three broad cases of reverts:

    // For any settlement: if we're last approver, and auto settlement is enabled, then execute that settlement
    for (uint256 i = 0; i < lengthSettlements; i++) {
      uint256 settlementId = settlementIds[i];
      Settlement storage settlement = settlements[settlementId];
      if (settlement.isAutoSettled && isSettlementApproved(settlementId)) {
        // Failed auto-execution will not revert the entire transaction, only that settlement's execution will fail.
        // Other settlements will still be processed, and the earlier approval will remain. Note that try{} only
        // supports external/public calls.
        try this.executeSettlementInner(msg.sender, settlementId) {
        // Success
        }
        catch Error(string memory reason) {
          // Revert with reason string
          emit SettlementExecutionFailedReason(settlementId, msg.sender, true, reason);
        } catch Panic(uint256 errorCode) {
          // Revert due to serious error (eg division by zero)
          emit SettlementExecutionFailedPanic(settlementId, msg.sender, true, errorCode);
        } catch (bytes memory lowLevelData) {
          // Revert in every other case (eg custom error)
          emit SettlementExecutionFailedOther(settlementId, msg.sender, true, lowLevelData);
        }
      }
    }

There is a corresponding mock Solidity contract for testing, that triggers each of these revert cases:

  /// @dev The `amount` parameter controls the behaviour of the function as follows:
  /// - If `amount == 1`: The function will revert with a revert string: "AssetTokenThatReverts: transferFrom is disabled".
  /// - If `amount == 2`: The function will revert with a custom error: `ThisIsACustomError()`.
  /// - If `amount == 3`: The function will trigger a panic due to a divide-by-zero error, causing the transaction to fail unexpectedly.
  /// - If `amount >= 4`: The function will revert with no message, using inline assembly to revert the transaction.
  function transferFrom(address, address, uint256 amount) public override returns (bool) {
    dummy = 1;
    if (amount == 1) {
      // Revert with a revert string
      revert("AssetTokenThatReverts: transferFrom is disabled");
    } else if (amount == 2) {
      // Revert with a custom error
      revert ThisIsACustomError();
    } else if (amount == 3) {
      // Revert with panic divide by zero
      uint256 i = 10;
      dummy = i / (i - 10);
      return false;
    } else {
      // Revert with no message
      assembly {
        revert(0, 0)
      }
    }
  }

Happy Path Sequence Diagram

The below diagram shows how the DVP workflow looks for an example three party settlement:

Delivery Versus Payment: Happy Path Sequence Diagram
Delivery Versus Payment: Happy Path Sequence Diagram