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:

ContractToken standardImport path
RaylsErc1155DvpHandlerERC1155@rayls/contracts/tokens/RaylsErc1155DvpHandler.sol
RaylsErc721DvpHandlerERC721@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
ParameterDescription
_tokenIdID of the ERC1155 token to lock
_valueAmount of tokens to lock
_dataOptional 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
ParameterDescription
_tokenIdID of the locked token to offer
_tokenValueAmount of tokens to swap
tokenDataParamOptional extra token data
_enygmaAmountAmount of Enygma tokens requested from buyer
_enygmaResourceIdResource ID of the Enygma contract on the buyer's PN
_destChainIdChain ID of the buyer's Privacy Node
_sharedIdUnique identifier shared by both legs of the swap
_msgOptional programmability data (custom payload routed after settlement)
_validityTimeSeconds 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 nonReentrant

When dvpSwapCompleted executes:

  1. The locked NFT is burned from the seller's balance
  2. A cross-chain message mints the NFT on the buyer's Privacy Node
  3. 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 nonReentrant

After 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 nonReentrant

This 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 restricted

You can also pass a custom _validityTime per swap directly in swapWithDvpForEnygma (pass 0 to use the contract default).


Access control summary

RoleFunctions
Ownermint, burn, submitTokenUpdate, setSwapValidityTime
MESSAGE_EXECUTORunlock, receiveResourceId, MintFromSwapDvp
RELAYERdvpSwapCompleted, unlockFromDvp, notifySenderWithPNCommunicator, notifySenderAndReceiverWithPNCommunicator
Any addressdepositIntoDvp, swapWithDvpForEnygma, cancelSwap, withdrawFromDvp

Roles are managed by the RBAC contract (_userGovernance) configured at deployment. See RaylsApp for details on the access control model.