- Description
- Installation
- Commands
- Deployed Addresses
- Workflow Summary
- Sequence Diagram
- Events
- Contributing
- Roadmap
- License
This repo is a permissionless implementation of the Delivery Versus Payment (DVP) protocol supporting ERC-20, ERC-721, and Ether transfers. Developed and actively used by PV01, this project is open-sourced under the MIT license and provided as a public good.
- 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.
- Party: An address involved as either a
fromortoin 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.
git clone --recurse-submodules https://github.com/PV01-org/delivery-versus-payment.git
cd delivery-versus-payment
forge build
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. |
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 Block Explorer Link | Address |
|---|---|---|---|
| Arbitrum | Testnet (Sepolia) | DeliveryVersusPaymentV1 | 0xb4bfeA02E82172E8A12F8AA41B5F729B4778F33F |
| Arbitrum | Testnet (Sepolia) | DeliveryVersusPaymentV1HelperV1 | 0x700A55267Dad74763b4B5f0C0B727B6a86DD58D3 |
| Avalanche | Mainnet | DeliveryVersusPaymentV1 | 0xF38a32671795Ffb2dFC7791DC10423b526679929 |
| Avalanche | Mainnet | DeliveryVersusPaymentV1HelperV1 | 0x6297259fB38F4871DF74C510919CF2F7F7831A49 |
| Avalanche | Testnet (Fuji) | DeliveryVersusPaymentV1 | 0xfDD3D731AFb3a1c4eFe18AedF750abBf2189BF9d |
| Avalanche | Testnet (Fuji) | DeliveryVersusPaymentV1HelperV1 | 0x82aF9b22399693b7cFb4b5d827D29d7B0b52F120 |
| Ethereum | Mainnet | DeliveryVersusPaymentV1 | 0x94D01960C2F09502a64ae83547da0828fd3fF4e3 |
| Ethereum | Mainnet | DeliveryVersusPaymentV1HelperV1 | 0xE1e2e50918Bf18207eE87B7618888128d19D8a44 |
| Ethereum | Testnet (Sepolia) | DeliveryVersusPaymentV1 | 0x2A208C4a5b94A6a8E1546e892832F76a9313741b |
| Ethereum | Testnet (Sepolia) | DeliveryVersusPaymentV1HelperV1 | 0x53953083AC1Ad08202398951C77447F9d95B5c98 |
| Polygon | Mainnet | DeliveryVersusPaymentV1 | 0x8275709ca99574b676aDd48C32a9969017E5c69C |
| Polygon | Mainnet | DeliveryVersusPaymentV1HelperV1 | 0x5aA9B6D1F6a37F20D90DCA99f0a78c6ab8e9bD4E |
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> \
--broadcastTo deploy the DVP contract and the DVP helper contract with code verification, on many chains, follow these steps:
- In the
envfile (copy from.env.templateif this does not exist) maintain values for all the environment variables listed. - See the help for this script to see supported chains:
$ scripts/deploy-multi-chain.sh -h- Run:
./scripts/deploy-multi-chain.shwhich deploys to all chains supported by the script, or run:
./scripts/deploy-multi-chain.sh -n sepolia arbsepoliato deploy just to some of the supported chains.
The list of chains to deploy to in the previous section can be extended by following these steps:
- Define the network names to deploy to in
foundry.toml. You can see the existing network names infoundry.tomlwith their RPC URL under[rpc_endpoints]and their verification API key under[etherscan]. - Edit
.envand maintain the environment variables you asked for infoundry.tomlin step 1. - Edit the deploy script
deploy-multi-chain.shin the.scriptsfolder. Change the variable calledNETWORKSto contain the additional network names you want to deploy to. - Run:
./scripts/deploy-multi-chain.sh -n <new network name>to deploy all contracts with code verification to the new chain.
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.
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.
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.
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().
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.
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.
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 for a happy path process though a settlement with auto-settle enabled.

Topic0 values for events are:
| Event | Topic0 |
|---|---|
| ETHReceived(address,uint256) | 0xbfe611b001dfcd411432f7bf0d79b82b4b2ee81511edac123a3403c357fb972a |
| ETHWithdrawn(address,uint256) | 0x94b2de810873337ed265c5f8cf98c9cffefa06b8607f9a2f1fbaebdfbcfbef1c |
| SettlementApprovalRevoked(uint256,address) | 0x96c5a579760c144ad93a5c19d41440d5185ba0451704c0ac7cb22488d8735ac2 |
| SettlementApproved(uint256,address) | 0x7f89b61c53062fb158619c7b66552eabdfb0e1d37c439a62c2d2b5a657bcea93 |
| SettlementAutoExecutionFailedOther(uint256,address,bytes) | 0x63c222ac809d589e48426985c6af11739f936b405e0a78a920fbae6565c07497 |
| SettlementAutoExecutionFailedPanic(uint256,address,uint256) | 0x3c4e728bba5a6c57290cee894ede5970e12dc7d459808344b14cec9a956f1dc2 |
| SettlementAutoExecutionFailedReason(uint256,address,string) | 0xe1c01819733d746479549271d3a51445514b8f678614d50ad34d305c67b83d9c |
| SettlementCreated(uint256,address) | 0x3c521c92800f95c83d088ee8c520c5b47b3676958e48a985fe1d45d7cf6dbd78 |
| SettlementExecuted(uint256,address) | 0xf059ff22963b773739a912cc5c0f2f358be1a072c66ba18e2c31e503fd012195 |
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
- pipx:
- Ensure Foundry is installed and available in your PATH: https://book.getfoundry.sh/getting-started/installation
- 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.tomlunder[fmt](e.g.,line_length = 120,tab_width = 2).
See CONTRIBUTING.md for more details.
See ROADMAP.md for more details.
This project is licensed under the terms of the LICENSE.