Delivery vs Payment (DvP)

Delivery vs. Payment (DvP) is a settlement mechanism in which the transfer of an asset happens at the same moment as the payment, so that both parties fulfil their obligations atomically and neither carries counterparty risk.

In Rayls, DvP between institutions is cross-chain: the NFT seller sits in one Rayls Sovereign ledger and the Enygma buyer sits in another, with the two-leg settlement coordinated by the Rayls relayer.

Note: DvP within a single Rayls Sovereign ledger is not supported. Each leg of the swap happens in a different ledger.


Contracts

DvP on the institution's side is handled by two dedicated abstract handler contracts, which 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 it in their own ledger and the Enygma buyer responds in theirs.

Seller Sovereign                       Buyer Sovereign
──────────────────────────────────     ──────────────────────────────
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 Sovereign ledger  
    └─ 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,
    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 Sovereign ledger
_destChainIdChain ID of the buyer's ledger
_sharedIdUnique identifier shared by both legs of the swap
_validityTimeSeconds until the swap expires (pass 0 for the default)

The relayer picks up this event and locks the corresponding Enygma amount in the buyer's ledger. Where the buyer's Sovereign ledger holds sufficient Enygma, the relayer proceeds to Step 3a, and where it does not, the relayer 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 in the buyer's Rayls Sovereign ledger
  3. The Enygma payment is released to the seller

Step 3b — Cancel the swap

Call cancelSwap where 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 Sovereign ledger, and the seller's token stays 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, which returns the tokens to the seller's available balance.


Swap validity time

The handler has a configurable validity window, expressed in seconds, with the default and the minimum and maximum values 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.


Did this page help you?