Fund the Legs

Funding is a plain TransferChecked into the leg's escrow, so any wallet or custody system that can send a token transfer can fund a leg. Only the read of the trade record before funding needs the DvP client. The program checks the escrow balances only at settlement; nothing checks the transfer when it lands, so each party reads the trade record before transferring.

This page continues from create a trade; the client and signers carry over.

Verify the terms

Creating a record is permissionless, and the escrow addresses derive from the record and the mint alone, never from the terms. A transfer into an escrow can therefore land against terms you never agreed to, including settlement destinations that redirect your counterparty's proceeds to a third address. Before funding, read the SwapDvp account and check it against the agreed trade.

verifySwapDvp in TypeScript and verify::decode_swap_dvp_account in Rust confirm that the account is owned by the DvP program, is exactly 458 bytes, and sits at the address its own fields derive (fetchSwapDvpChecked performs the first two checks only). The plain fetchSwapDvp and decodeSwapDvp readers check none of these. The checks on parties, amounts, times, and destinations are done by the caller against the agreed trade.

import { unwrapOption } from "@solana/kit";
import { verifySwapDvp } from "./dvp";
const trade = await verifySwapDvp(client.rpc, swapDvp);
const t = trade.data;
const expectedAmountA = 1_000_000_000n;
const expectedAmountB = 998_500_000n;
const expectedEarliestSettlement: bigint | null = null; // as agreed
const expectedExpiry = 1_798_761_600n; // as agreed, unix seconds
if (
t.userA !== userA ||
t.userB !== userB ||
t.mintA !== mintA ||
t.mintB !== mintB ||
t.settlementAuthority !== settlementAuthority ||
t.tokenProgramA !== tokenProgramA ||
t.tokenProgramB !== tokenProgramB ||
t.amountA !== expectedAmountA ||
t.amountB !== expectedAmountB ||
unwrapOption(t.earliestSettlementTimestamp) !== expectedEarliestSettlement ||
t.expiryTimestamp !== expectedExpiry ||
t.userASettlementDestination !== userA ||
t.userBSettlementDestination !== userB
) {
throw new Error("Stored terms do not match the agreed trade; do not fund");
}
if (t.expiryTimestamp <= BigInt(Math.floor(Date.now() / 1000)) + 60n) {
throw new Error("Trade expires too soon to fund safely");
}

If the record was created by your counterparty or the settlement authority, compare every field against the terms you agreed off the ledger. If you created it yourself, this read confirms it landed as sent.

Transfer into the escrow

Fund the leg at exactly the agreed amount. The program clamps settlement to amount_x and returns any surplus to the leg's party, so a surplus is not lost, but it adds a second transfer at settlement and can fail against some transfer hooks (see the notes below).

Use TransferChecked, not an unchecked Transfer. Token-2022 mints with extensions that depend on the mint account, such as Pausable, reject a transfer that does not supply it, and a mint with a transfer hook needs the hook's accounts resolved and appended the same way as for any other transfer of that token.

The asset-leg party, signing as userASigner, funds escrow A:

import {
findAssociatedTokenPda,
getTransferCheckedInstruction,
} from "@solana-program/token-2022";
const [userAAtaA] = await findAssociatedTokenPda({
mint: mintA,
owner: userA,
tokenProgram: tokenProgramA,
});
const [userBAtaB] = await findAssociatedTokenPda({
mint: mintB,
owner: userB,
tokenProgram: tokenProgramB,
});
const fundAssetLeg = getTransferCheckedInstruction({
source: userAAtaA,
mint: mintA,
destination: escrowA,
authority: userASigner,
amount: t.amountA,
decimals: 6,
}, { programAddress: tokenProgramA });
await client.sendTransaction([fundAssetLeg]);

The cash-leg party does the same for escrow B with userBAtaB, mintB, userBSigner, t.amountB, and { programAddress: tokenProgramB }.

A funding transfer seen at confirmed can still be superseded. The program reads the escrow balance onchain at settlement, so a Settle sent against a funding transfer that did not land fails with LegNotFunded and nothing moves; the cost is a failed Settle and a retry. Treat a leg as funded when its transfer is finalized.

Notes

  • Wrapped SOL legs. The program reads a native escrow's balance after syncing it, so lamports sent to a wrapped-SOL escrow by a system transfer are counted. Never send SOL to an escrow for any other mint: the program cannot return it, and it goes to whoever closes the account.
  • Gated mints. On a mint with Default Account State frozen, the escrow is created frozen and the transfer fails until the issuer thaws or admits it. The overview describes the interaction with Token ACL.
  • Transfer hooks. Fund hook-bearing escrows at exactly the agreed amount. If a surplus forces a refund at settlement, the refund reuses the leg's hook accounts with a different destination and amount, which some hooks reject. The recovery is Reclaim, re-fund at the exact amount, and settle again.
  • Funding past expiry. Nothing stops a transfer after expiry_timestamp. It lands, settlement is no longer possible, and the leg's party recovers it with Reclaim while the trade is open or Recover after it closes. See unwind a trade.
  • Who funds first. The program does not sequence the two legs. The order is a matter for the parties; either side can reclaim its own leg at any time before settlement, unless a mint authority (freeze, pause, or transfer hook) blocks the escrow.

Next steps

  • Settle the trade: the settlement authority exchanges both legs in one transaction
  • Unwind a trade: recovering a funded leg when the trade does not settle

Is this page helpful?

Mục lục

Chỉnh sửa trang
Fund a DvP Trade on Solana | Solana