Skip to content

KevinSmall/delivery-versus-payment

Repository files navigation

Delivery Versus Payment

Build Status GitHub issues GitHub pull requests

Description

This repo contains Solidity smart contracts that implement a permissionless version of the Delivery Versus Payment (DVP) protocol.

This DVP implementation allows multi-party atomic swaps of digital assets including ERC-20, ERC-721 and Ether. Being "DVP" means that either all parts of the swap happen in a single transaction, or nothing happens at all.

The contracts are permissionless, non-upgradeable and have no admins or privileged accounts.

UIs are available at:

Originally developed at PV01 and audited, this fork is also open-sourced under the MIT license.

Features

  • Non-upgradeable, singleton Delivery Versus Payment contract.
  • Allows atomic swaps of an arbitrary number of assets between an arbitrary number of parties.
  • Permissionless, anyone can create and execute these swaps, so long as involved parties have approved.
  • Supports assets including native ETH, ERC-20 and ERC-721.
  • Helper contract provides search functionality for off-chain use.

Terminology

  • Party: An address involved as either a from or to in an asset movement.
  • Flow: A movement of a single asset between two parties.
  • Asset: Ether, ERC-20 or ERC-721 token.
  • Settlement: A collection of an arbitrary number of flows, uniquely identified by a Settlement id. All settlements live in the singleton contract.

Installation

git clone --recurse-submodules https://github.com/KevinSmall/delivery-versus-payment.git
cd delivery-versus-payment
forge build

Commands

The following CLI commands are available:

# Action Usage Description
1 Compile forge build Compile Solidity smart contracts.
2 Test forge test --summary Run smart contract tests.
3 Coverage forge coverage --ir-minimum Run tests and generate coverage reports.
4 Gas Estimate forge test --gas-report Run tests with gas reporting.
5 Sizer forge build --sizes Report contract size.

Deployed Addresses

The DVP contracts are available at the following addresses. Since the solution is permissionless, they can be freely used as they are, without needing further contract deployments. To deploy new contracts see Further Deployments.

Chain Instance Contract Address
Ethereum Mainnet DeliveryVersusPaymentV1 0xb0d73b0559F260bc239FF2ffBc8676595601134c
Ethereum Mainnet DeliveryVersusPaymentV1HelperV1 0x5de79c31355ABD1683e6f41aA75Bc535c56a6156
Ethereum Testnet (Sepolia) DeliveryVersusPaymentV1 0x0270Eca05754e19Ca0321a0A29A8C0e75409A8BD
Ethereum Testnet (Sepolia) DeliveryVersusPaymentV1HelperV1 0x25B3c1ba1C2e38c7C0978F3A43D2D7ab59Beca2d
Base Mainnet DeliveryVersusPaymentV1 0x70F1770C1FCafcd5B178f5EE586a54312718C9aF
Base Mainnet DeliveryVersusPaymentV1HelperV1 0xF78470AfBaA0b3D0079794787FF927919E42D50E
Base Testnet (Sepolia) DeliveryVersusPaymentV1 0x12D5cF7A0de74F2B8810a5Fd2ec0D6B1AC2A9D0E
Base Testnet (Sepolia) DeliveryVersusPaymentV1HelperV1 0xA7785aD291fE4e277cBfa0205A2A3CEF70546490

Further Deployments

Deploying Individual Contracts

To deploy further copies of individual contracts, with code verification, use the deploy scripts in the ./script folder. For example, without requiring any .env file:

forge script script/DeployDvp.s.sol \
  --rpc-url <RPC_URL> \
  --private-key <PRIVATE_KEY> \
  --verify --etherscan-api-key <ETHERSCAN_API_KEY> \
  --broadcast

Deploying Contracts to Many Chains

To deploy the DVP contract and the DVP helper contract with code verification, on many chains, follow these steps:

  1. In the env file (copy from .env.template if this does not exist) maintain values for all the environment variables listed.
  2. See the help for this script to see supported chains:
$ scripts/deploy-multi-chain.sh -h
  1. Run:
./scripts/deploy-multi-chain.sh

which deploys to all chains supported by the script, or run:

./scripts/deploy-multi-chain.sh -n eth_sepolia -n base_sepolia

to deploy just to some of the supported chains.

Deploying Contracts to Many New Chains

The list of chains to deploy to in the previous section can be extended by following these steps:

  1. Define the network names to deploy to in foundry.toml. You can see the existing network names in foundry.toml with their RPC URL under [rpc_endpoints] and their verification API key under [etherscan].
  2. Edit .env and maintain the environment variables you asked for in foundry.toml in step 1.
  3. Edit the deploy script deploy-multi-chain.sh in the .scripts folder. Change the variable called NETWORKS to contain the additional network names you want to deploy to.
  4. Run:
./scripts/deploy-multi-chain.sh -n <new network name>

to deploy all contracts with code verification to the new chain.

Workflow Summary

Create a Settlement

A settlement is collection of intended value transfers (Flows) between parties, along with a free text reference, a deadline (cutoff date) and an auto-settlement flag indicating if settlement should be immediately processed after final approval received. ERC-20, ERC-721 and Ether transfers are supported. For example a settlement could include the following 3 flows, be set to expire in 1 week, and be auto-settled when all from parties (sender addresses) have approved:

From To AmountOrId Token isNFT
Alice -> Bob 1 ETH false
Bob -> Charlie 400 TokenA false
Charlie -> Alice 500(id) TokenB true
  • If a token claims to be an NFT and is not, the creation will revert.
  • If a token claims to be an ERC20, but doesn't implement decimals(), the creation will revert.
  • Anyone can create a settlement involving any parties and any asset.

Approve a Settlement

Each party who is a from address in one or more flows needs to approve the settlement before it can proceed. They do this by calling approveSettlements() and including their necessary total ETH deposit if their flows involve sending ETH. ERC-20 and ERC-721 tokens are not deposited upfront, they only need transfer approval before execution. If a settlement is marked as isAutoSettled:

  • the settlement will be executed automatically after all approvals are in place, the gas cost being borne by the last approver.
  • if settlement approval succeeds, but auto-execution fails, the entire transaction is not reverted. The approval remains on-chain, only the settlement execution is reverted.

Execute a Settlement

Anyone can call executeSettlement() before the cutoff date, if all approvals are in place. At execution time the contract makes the transfers in an atomic, all or nothing, manner. If any Flow transfer fails the entire settlement is reverted.

Changes

If a party changes their mind before the settlement is fully executed — and before the cutoff date — they can revoke their approval by calling revokeApprovals(). This returns any deposited ETH back to them and removes their approval. Once expired a settlement can no longer be executed, any ETH deposited can be withdrawn by each party using withdrawETH().

Gas Usage

There are many unbounded loops in this contract, by design. There is no limit on the number of flows in a settlement, nor on how many settlements can be batch processed (for functions that receive an array of settlementIds). The current chain's block gas limit acts as a cap. In every case it is the caller's responsibility to ensure that the gas requirement can be met.

Griefing

It is acknowledged that bad actors could be annoying by creating flows with fake tokens, or flows with tokens that would intentionally revert when the settlement is executed, and so making a settlement impossible to process. These bad actors could potentially trick other parties into locking ETH into a settlement that could never be processed. There is no financial loss (gas fees excepted) because when other parties discover the ruse, they can withdraw their approval and withdraw their ETH.

Reentrancy Protection

Settlement approval can be done in batches. Settlement execution can potentially be triggered inside that (if auto-settle is switched on and a party is giving the final approval). Settlement execution can make many external calls to process transfers of assets. These patterns lend themselves well to reentrancy, which is protected against as follows:

Function Reentrancy Protection
approveSettlements() OZ nonReentrant modifer
createSettlement() No external calls made
executeSettlement() OZ nonReentrant modifer
revokeApprovals() OZ nonReentrant modifer
withdrawETH() OZ nonReentrant modifer

There are some subtleties inside the protection for approveSettlements() and executeSettlement(), explained here.

Sequence Diagram

Sequence diagram for a happy path process though a settlement with auto-settle enabled. flow

Events

Topic0 values for events are:

Event Topic0
ETHReceived(address,uint256) 0xbfe611b001dfcd411432f7bf0d79b82b4b2ee81511edac123a3403c357fb972a
ETHWithdrawn(address,uint256) 0x94b2de810873337ed265c5f8cf98c9cffefa06b8607f9a2f1fbaebdfbcfbef1c
SettlementApprovalRevoked(uint256,address) 0x96c5a579760c144ad93a5c19d41440d5185ba0451704c0ac7cb22488d8735ac2
SettlementApproved(uint256,address) 0x7f89b61c53062fb158619c7b66552eabdfb0e1d37c439a62c2d2b5a657bcea93
SettlementCreated(uint256,address) 0x3c521c92800f95c83d088ee8c520c5b47b3676958e48a985fe1d45d7cf6dbd78
SettlementExecuted(uint256,address) 0xf059ff22963b773739a912cc5c0f2f358be1a072c66ba18e2c31e503fd012195
SettlementExecutionFailedOther(uint256,address,bool,bytes) 0x2fb0f0e288825d79bc923ab286ce365c1552f8776aa33413af9a35b5ae6028c5
SettlementExecutionFailedPanic(uint256,address,bool,uint256) 0xe8371a49f37ebac2050c0e5b70c4ee88e0776c5ba7e3be09e1b9660fefa3528a
SettlementExecutionFailedReason(uint256,address,bool,string) 0x55e0c9c38879d7b337ccd4db63235e0c504f645e3f925a790a1496ac7d090174

Linting and pre-commit

This repository uses pre-commit to run lightweight checks and enforce a standard Solidity code format via Foundry.

Setup (one-time):

  • Install pre-commit (choose one):
    • pipx: pipx install pre-commit
    • pip: pip install --user pre-commit
    • Homebrew (macOS): brew install pre-commit
  • Ensure Foundry is installed and available in your PATH.
  • Enable hooks in this repo: pre-commit install

Usage:

  • Run on all files: pre-commit run --all-files
  • On commit, hooks will run automatically.
  • The Solidity formatter runs in check mode (forge fmt --check). If formatting fails, fix with: forge fmt

Formatting standard:

  • Uses Foundry's standard formatter configured in foundry.toml under [fmt] (e.g., line_length = 120, tab_width = 2).

Contributing

See CONTRIBUTING.md for more details.

License

This project is licensed under the terms of the LICENSE.

About

Delivery versus payment

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors