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:
| 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 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| 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,
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 Sovereign ledger |
_destChainId | Chain ID of the buyer's ledger |
_sharedId | Unique identifier shared by both legs of the swap |
_validityTime | Seconds 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 nonReentrantWhen dvpSwapCompleted executes:
- The locked NFT is burned from the seller's balance
- A cross-chain message mints the NFT in the buyer's Rayls Sovereign ledger
- 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 nonReentrantAfter 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 nonReentrantThis 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 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 about 21 hours ago
