Delivery vs Payment (DvP)
Delivery vs. Payment (DvP) is a settlement mechanism where the transfer of an asset occurs simultaneously with the payment — ensuring both parties fulfil their obligations atomically, with no counterparty risk.
In Rayls, DvP between Privacy Nodes is cross-chain: the NFT seller lives on one Privacy Node, the Enygma buyer lives on a different Privacy Node. The two-leg settlement is coordinated by the Rayls relayer.
Note: DvP within a single Privacy Node is not supported. Each leg of the swap happens on a different Privacy Node.
Contracts
DvP on the Privacy Node side is handled by two dedicated abstract handler contracts that you extend to create your DvP token:
| Contract | Token standard | Import path |
|---|---|---|
RaylsErc1155DvpHandler | ERC1155 | @rayls/contracts/tokens/RaylsErc1155DvpHandler.sol |
RaylsErc721DvpHandler | ERC721 | @rayls/contracts/tokens/RaylsErc721DvpHandler.sol |
Both follow the same DvP flow. The examples below use RaylsErc1155DvpHandler.
Minimal implementation
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import "@rayls/contracts/tokens/RaylsErc1155DvpHandler.sol";
contract MyDvpToken is RaylsErc1155DvpHandler {
constructor(
string memory _uri,
string memory _name,
address _endpoint,
address _raylsNodeEndpoint,
address _userGovernance,
address _owner
)
RaylsErc1155DvpHandler(
_uri,
_name,
_endpoint,
_raylsNodeEndpoint,
_userGovernance,
_owner,
false
)
{}
}Cross-chain DvP flow
The flow has four steps. The NFT seller initiates on their Privacy Node; the Enygma buyer responds on theirs.
Seller PN Buyer PN
────────────────────────────────── ──────────────────────────────
1. depositIntoDvp()
└─ locks NFT in handler
2. swapWithDvpForEnygma()
└─ sends cross-chain swap offer ──► (relayer routes to buyer's Enygma handler)
└─ buyer's Enygma handler holds payment
3a. dvpSwapCompleted() ◄──────────── (relayer confirms both legs OK)
└─ burns NFT from seller
└─ mints NFT on buyer's PN
└─ Enygma payment released to seller
3b. cancelSwap()
└─ reverts both legs if expired / counterparty doesn't respond
└─ seller calls withdrawFromDvp() to recover locked tokens
Step 1 — Deposit the token
The seller locks their token into the DvP handler before initiating the swap.
function depositIntoDvp(
uint256 _tokenId,
uint256 _value,
bytes memory _data
) public virtual nonReentrant| Parameter | Description |
|---|---|
_tokenId | ID of the ERC1155 token to lock |
_value | Amount of tokens to lock |
_data | Optional extra data |
After this call, lockedForDvp[msg.sender][_tokenId] increases by _value. Locked tokens cannot be transferred until they are released by completing or cancelling the swap.
Step 2 — Initiate the cross-chain swap
function swapWithDvpForEnygma(
uint256 _tokenId,
uint256 _tokenValue,
bytes memory tokenDataParam,
uint256 _enygmaAmount,
bytes32 _enygmaResourceId,
uint256 _destChainId,
bytes32 _sharedId,
SharedObjects.DvpProgramability calldata _msg,
uint64 _validityTime
) public virtual nonReentrant| Parameter | Description |
|---|---|
_tokenId | ID of the locked token to offer |
_tokenValue | Amount of tokens to swap |
tokenDataParam | Optional extra token data |
_enygmaAmount | Amount of Enygma tokens requested from buyer |
_enygmaResourceId | Resource ID of the Enygma contract on the buyer's PN |
_destChainId | Chain ID of the buyer's Privacy Node |
_sharedId | Unique identifier shared by both legs of the swap |
_msg | Optional programmability data (custom payload routed after settlement) |
_validityTime | Seconds until the swap expires (pass 0 for the default) |
The relayer picks up this event and locks the corresponding Enygma amount on the buyer's Privacy Node. If the buyer's PN has sufficient Enygma, the relayer proceeds to Step 3a. If not, it triggers Step 3b.
Step 3a — Swap completed (happy path)
Called automatically by the relayer when both legs succeed. You do not call this directly.
// Called by the relayer — restricted
function dvpSwapCompleted(
SharedObjects.DvpSwapCompletedParams memory params,
address from,
uint256 _value,
bytes memory data
) public virtual restricted nonReentrantWhen dvpSwapCompleted executes:
- The locked NFT is burned from the seller's balance
- A cross-chain message mints the NFT on the buyer's Privacy Node
- The Enygma payment is released to the seller
Step 3b — Cancel the swap
Call cancelSwap if the swap has expired or the counterparty did not respond.
function cancelSwap(
bytes32 _sharedId,
uint256 _toChainId,
uint256 _tokenId,
uint256 _tokenValue,
bytes32 _enygmaResourceId,
uint256 _enygmaAmount
) public virtual nonReentrantAfter cancellation, the relayer releases the locked Enygma on the buyer's PN. The seller's token remains locked locally until they call withdrawFromDvp.
function withdrawFromDvp(
uint256 _tokenId,
uint256 _value,
bytes memory data
) public virtual nonReentrantThis initiates the withdrawal process. The relayer finalises the unlock and calls unlockFromDvp on the handler, returning the tokens to the seller's available balance.
Swap validity time
The handler has a configurable validity window (in seconds). The default and min/max values are defined in Utils.sol. The Owner role can update it:
// Restricted to Owner role
function setSwapValidityTime(uint64 _validityTime) public virtual restrictedYou can also pass a custom _validityTime per swap directly in swapWithDvpForEnygma (pass 0 to use the contract default).
Access control summary
| Role | Functions |
|---|---|
| Owner | mint, burn, submitTokenUpdate, setSwapValidityTime |
| MESSAGE_EXECUTOR | unlock, receiveResourceId, MintFromSwapDvp |
| RELAYER | dvpSwapCompleted, unlockFromDvp, notifySenderWithPNCommunicator, notifySenderAndReceiverWithPNCommunicator |
| Any address | depositIntoDvp, swapWithDvpForEnygma, cancelSwap, withdrawFromDvp |
Roles are managed by the RBAC contract (_userGovernance) configured at deployment. See RaylsApp for details on the access control model.
Updated 19 days ago
