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.

Actively used by PV01, this project is open-sourced under the MIT license and provided as a public good. Full repo is here.

Features

  1. Non-upgradeable, singleton Delivery Versus Payment contract, minimizes trust required.
  2. 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).
  3. Permissionless, anyone can create and execute these swaps, so long as the from address parties have approved.
  4. Supports assets including native ETH, ERC-20 and ERC-721.
  5. Optional auto-settlement, the final approving party can trigger auto-processing of the settlement.
  6. Helper contract provides search functionality for off-chain use.

Notable Solidity Features

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.

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. That is, we still want the overall EVM transaction to succeed, just we acknowledge that the execution can fail. Like other languages, Solidity has a try-catch construct, but unlike other languages is also warrants a 40-page write up about the nuances of using it. We simplified this as far as possible to catch 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 < settlementIds.length; i++) {
      uint256 settlementId = settlementIds[i];
      Settlement storage settlement = settlements[settlementId];
      if (settlement.isAutoSettled && isSettlementApproved(settlementId)) {
        // Failed auto-execution will not revert the entire transaction, just that settlement's execution (and
        // the earlier approval will remain)
        try this.executeSettlement(settlementId) {
          // Success
        } catch Error(string memory reason) {
          // Revert with reason string
          emit SettlementAutoExecutionFailedReason(settlementId, msg.sender, reason);
        } catch Panic(uint errorCode) {
          // Revert due to serious error (eg division by zero)
          emit SettlementAutoExecutionFailedPanic(settlementId, msg.sender, errorCode);
        } catch (bytes memory lowLevelData) {
          // Revert in every other case (eg custom error)
          emit SettlementAutoExecutionFailedOther(settlementId, msg.sender, 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
      uint 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