Chương trình Delivery vs Payment (DvP)

Chương trình DvP là chương trình tiêu chuẩn cho giao tài sản đối ứng thanh toán (delivery versus payment) trên Solana, do Solana Foundation duy trì và được Cantina kiểm toán. Hai bên thống nhất một thương vụ, mỗi bên gửi vế của mình vào một tài khoản escrow bằng một lệnh chuyển token thông thường, rồi một bên quyết toán sẽ quyết toán cả hai vế trong một giao dịch, nên hoặc cả hai cùng được chuyển hoặc không vế nào được chuyển. Bên quyết toán là một địa chỉ thứ ba được nêu trong thương vụ, chẳng hạn sàn giao dịch hoặc đại lý đã sắp xếp thương vụ đó. Không bên nào phải viết hay triển khai chương trình, và ví hoặc đơn vị lưu ký có thể gửi lệnh chuyển token tiêu chuẩn (TransferChecked) đều có thể nạp một vế mà không cần tích hợp riêng cho DvP. Cho đến khi quyết toán, mỗi bên đều có thể rút lại vế của mình hoặc hoàn tác thương vụ.

Hãy đọc bản ghi trước khi hành động

Việc tạo bản ghi thương vụ không cần cấp quyền, vì vậy một bản ghi không phải là bằng chứng của sự đồng thuận. Mỗi bên cần đọc các điều khoản đã lưu trước khi nạp vế của mình, và bên quyết toán cũng cần đọc trước khi quyết toán (xem xác minh).

Cách hoạt động

Mỗi thương vụ có một bản ghi do chương trình sở hữu và hai tài khoản escrow, mỗi vế một tài khoản. Bên sở hữu vế tài sản (user_a) và bên sở hữu vế tiền mặt (user_b) mỗi bên nạp một vế bằng cách chuyển token vào tài khoản escrow của vế đó. Bên quyết toán là người ký duy nhất có thể quyết toán. Quyết toán chuyển số lượng đã thỏa thuận của mỗi vế sang phía bên kia, trả lại phần dư cho bên sở hữu vế đó, và đóng bản ghi cùng cả hai escrow. Tính nguyên tử đến từ các giao dịch Solana: cả hai lệnh chuyển nằm trong một giao dịch, nên hoặc cả hai có hiệu lực hoặc không lệnh nào có hiệu lực.

Mọi khoản hoàn tiền, rút lại và trả phần dư đều được chuyển vào associated token account của chính mỗi bên: tài khoản của user_a cho mint_a, của user_b cho mint_b. Một đơn vị lưu ký hoặc hệ thống ngân quỹ có thể nạp một vế từ tài khoản của mình, nhưng mọi khoản được trả lại sẽ vào tài khoản của bên đó, không quay về đơn vị lưu ký. Mỗi thương vụ cũng để lại một tài khoản đánh dấu vĩnh viễn (xem State).

Hướng dẫn

Các trang từng bước đưa một thương vụ từ khâu tạo đến khâu quyết toán, kèm TypeScript và Rust cho từng instruction.

Một bản demo trên devnet chạy một thương vụ từ đầu đến cuối: tạo thương vụ, nạp cả hai escrow bằng các lệnh chuyển token tiêu chuẩn, và quyết toán cả hai vế trong một giao dịch.

Lựa chọn giữa chương trình và ủy quyền

Delivery vs Payment (DvP) bằng ủy quyền quyết toán cùng một cuộc trao đổi mà không cần chương trình: mỗi bên ủy quyền vị thế của mình cho một đại lý quyết toán, và đại lý này chuyển cả hai vế trong một giao dịch.

  • Chương trình không cần ủy quyền, nên giới hạn một delegate cho mỗi token account không áp dụng, và bên quyết toán không thể chuyển hướng số tiền nhận được. Mỗi thương vụ phụ thuộc vào một chương trình có thể nâng cấp và quyền nâng cấp của nó.
  • Mô hình ủy quyền không phụ thuộc vào chương trình nào và hoạt động với các mint có Scaled UI Amount. Chương trình từ chối các mint đó, nghĩa là loại trừ mọi mint được tạo từ template tokenized-security của Mosaic (xem các ràng buộc về cấu hình mint).

Vai trò

Chương trình có tính đối xứng. Trong giao diện của nó, hai bên là user_a, người giao mint_a, và user_b, người giao mint_b. README và các client được tạo tự động gọi mint_a là vế tài sản và mint_b là vế tiền mặt, và các trang này theo quy ước đó, nhưng không có gì trong chương trình yêu cầu vế tiền mặt phải là một công cụ tiền mặt. Hai tài sản, hoặc hai stablecoin, đều được quyết toán theo cùng cách.

Vai tròĐối tượngKýGhi chú
Bên sở hữu vế tài sảnuser_a, giao mint_aLệnh chuyển nạp vế của chính mình; Reclaim, Reject, RecoverMột ví keypair, hoặc một ví thông minh dựa trên PDA như multisig vault. Tài khoản multisig của SPL Token hoặc một program account không thể là một bên: Create sẽ lỗi với PartyNotSignerCapable
Bên sở hữu vế tiền mặtuser_b, giao mint_bLệnh chuyển nạp vế của chính mình; Reclaim, Reject, RecoverYêu cầu tương tự
Bên quyết toánMột địa chỉ thứ ba được nêu khi tạoSettle, CancelNgười ký duy nhất có thể quyết toán. Không thể chuyển hướng số tiền nhận được, vì chúng đã được cố định khi tạo. Bên này nhận rent của các tài khoản bị đóng khi quyết toán hoặc hủy
Bên trả rentBất kỳ ai ký CreateDvpCreateNạp rent (khoản SOL đặt cọc để giữ một tài khoản tồn tại) cho bản ghi, nonce tombstone và cả hai escrow. Không được ghi nhận là bên nhận rent: rent của tài khoản bị đóng thuộc về bất kỳ ai đóng thương vụ

Program ID

dvp34bdbcEm4f4FCUjGV4mDAkDshaQR4LkK8fdcsyZq
ClusterChương trìnhQuyền nâng cấpslot triển khai gần nhất
mainnet-betadvp34bdbcEm4f4FCUjGV4mDAkDshaQR4LkK8fdcsyZqDXtFpbPjcn2hxPnw79x1Pfoj35vXh5AsWBkS37YnXMVv452,058,374
devnetdvp34bdbcEm4f4FCUjGV4mDAkDshaQR4LkK8fdcsyZq5EX1zFeJWj4rGYZv432WnYw8CbaSUeTdxPx4r573UgYU479,376,863

Chương trình có thể nâng cấp trên cả hai cluster. Các số liệu ở trên là kết quả solana program show báo cáo vào ngày 2 tháng 10 năm 2026; hãy chạy cùng lệnh để xem giá trị hiện tại. Bản build trên devnet cũ hơn commit mà các trang từng bước được viết dựa theo (xem Cài đặt), nhưng có cùng tập instruction và lỗi.

Vòng đời

Một thương vụ ở trạng thái mở từ CreateDvp cho đến khi một trong các lệnh Settle, Cancel hoặc Reject đóng nó. Việc nạp vế không có instruction riêng: một vế được coi là đã nạp khi tài khoản escrow của nó giữ ít nhất số lượng đã thỏa thuận. Reclaim áp dụng riêng cho từng vế và giữ thương vụ ở trạng thái mở, nên sự cố ở một vế không buộc bên kia phải hủy toàn bộ thương vụ.

State

Mỗi thương vụ có một tài khoản SwapDvp dung lượng 458 byte, do chương trình sở hữu. Địa chỉ của nó được suy ra từ bên quyết toán, hai bên, hai mint và một nonce (các seed ["dvp", settlement_authority, user_a, user_b, mint_a, mint_b, nonce]). Các số lượng và thời điểm được lưu bên trong tài khoản và không ảnh hưởng đến địa chỉ, nên chúng phải được đọc chứ không thể suy ra. Một tài khoản đánh dấu nhỏ, nonce tombstone tại ["nonce", swap_dvp], được tạo cùng thương vụ và không bao giờ bị đóng, vì vậy mỗi tổ hợp gồm các bên, mint, bên quyết toán và nonce chỉ có thể dùng cho một thương vụ duy nhất.

TrườngKiểuÝ nghĩa
bumpu8PDA bump
user_a, user_bPubkeyHai bên tham gia
mint_a, mint_bPubkeyCác mint của vế tài sản và vế tiền mặt
settlement_authorityPubkeyNgười ký duy nhất có thể quyết toán hoặc hủy
token_program_a, token_program_bPubkeyToken Program sở hữu mỗi mint; một thương vụ có thể kết hợp các vế SPL Token và Token-2022
amount_a, amount_bu64Số lượng đã thỏa thuận của mỗi vế, theo đơn vị nhỏ nhất của mint (base unit)
expiry_timestampi64Sau thời điểm mạng này (đồng hồ onchain), Settle bị từ chối. Reclaim, Cancel và Reject vẫn hoạt động
nonceu64Phân biệt các thương vụ có cùng các seed còn lại. Chỉ dùng một lần
ref_string[u8; 64]Một tham chiếu client không có cấu trúc cố định, được đệm số 0; chương trình không bao giờ đọc nó
user_a_settlement_destinationPubkeyNhận vế tiền mặt khi Settle; mặc định là user_a
user_b_settlement_destinationPubkeyNhận vế tài sản khi Settle; mặc định là user_b
mint_a_authority, mint_b_authorityPubkeyCác mint authority được ghi lại khi tạo; Settle sẽ lỗi nếu một trong hai đã thay đổi
earliest_settlement_timestampOption<i64>Nếu được đặt, Settle bị từ chối trước thời điểm mạng này

Danh sách trường được lấy từ IDL của chương trình tại commit được nêu trong Tạo thương vụ.

Instruction

InstructionNgười kýTác dụng
CreateDvpBất kỳ bên trả phí nàoTạo bản ghi SwapDvp, nonce tombstone của nó và cả hai tài khoản escrow. Không di chuyển token
ReclaimDvpuser_a hoặc user_bTrả lại vế của chính người ký từ escrow. Thương vụ vẫn mở và vế đó có thể được nạp lại
SettleDvpBên quyết toánChuyển amount_a đến đích của user_b và amount_b đến đích của user_a, trả lại phần dư cho bên sở hữu vế, đóng bản ghi và cả hai escrow
CancelDvpBên quyết toánHoàn lại toàn bộ số mà escrow A đang giữ cho user_a và escrow B cho user_b, đóng bản ghi và cả hai escrow
RejectDvpuser_a hoặc user_bTác dụng giống Cancel, nhưng do một bên tham gia ký
RecoverDvpuser_a hoặc user_bSau khi thương vụ đã đóng: rút sạch một escrow được tạo lại về token account của chính người ký và đóng nó. Xử lý khoản nạp đến sau Settle, Cancel hoặc Reject

Mọi khoản hoàn tiền đều được chuyển vào associated token account của chính mỗi bên cho mint của vế đó, bất kể ai đã nạp escrow.

Các tài khoản và đối số của từng instruction nằm ở trang của bước tương ứng.

Lỗi

MãLỗiKhi nào
0SignerNotPartyReclaim, Reject hoặc Recover được ký bởi một địa chỉ không phải user_a hay user_b
1DvpExpiredSettle sau expiry_timestamp
2SettlementAuthorityMismatchSettle hoặc Cancel được ký bởi một địa chỉ khác với bên quyết toán
3SettlementTooEarlySettle trước earliest_settlement_timestamp
4LegNotFundedSettle khi một escrow giữ ít hơn số lượng đã thỏa thuận
5ExpiryNotInFutureCreate với thời điểm hết hạn bằng hoặc trước thời gian mạng hiện tại
6EarliestAfterExpiryCreate với thời điểm quyết toán sớm nhất sau thời điểm hết hạn
7SelfDvpCreate với user_a bằng user_b
8SameMintCreate với mint_a bằng mint_b
9ZeroAmountCreate với số lượng bằng 0 ở một trong hai vế
10BlockedMintExtensionCreate hoặc Settle với một mint có TransferFee, InterestBearing, ScaledUiAmount hoặc NonTransferable
11SettlementAuthorityIsPartyCreate với bên quyết toán trùng với một trong hai bên tham gia
12SettlementAuthorityExecutableCreate với bên quyết toán là một tài khoản có thể thực thi
13NonceAlreadyUsedCreate dùng lại một (seeds, nonce) đã có tombstone
14ExpiryTooFarInFutureCreate với thời điểm hết hạn xa hơn một năm
15RefStringTooLongCreate với tham chiếu dài hơn 64 byte
16DvpStillOpenRecover khi thương vụ vẫn đang mở; hãy dùng Reclaim
17DvpNeverCreatedRecover với các seed không có tombstone
18EscrowPreloadedWithLamportsCreate khi một escrow không phải native đang giữ lamports vượt quá mức tối thiểu để được miễn rent
19SettlementDestinationIsSwapDvpCreate với đích thanh toán trùng với bản ghi giao dịch
20SwapDvpPreloadedWithLamportsCreate khi địa chỉ bản ghi đang giữ lamports vượt quá khoản rent dự trữ
21RecipientAtaMismatchMột token account nhận hoặc token account hoàn tiền không còn thuộc về ví và mint như dự kiến
22PartyNotSignerCapableCreate với một bên không phải là tài khoản do system sở hữu và không thể thực thi
23MintAuthorityChangedSettle khi mint authority của một leg khác với giá trị được ghi nhận lúc tạo

Sự kiện và lập chỉ mục

Chương trình không phát ra sự kiện onchain nào. Một indexer hoặc hệ thống đối soát sẽ đọc trạng thái tài khoản SwapDvp và số dư token của hai escrow, đồng thời dùng ref_string để đối chiếu một giao dịch với lệnh off-chain tương ứng. Vì bản ghi bị đóng khi thanh toán, hệ thống cần các điều khoản sau đó phải lưu chúng ngay khi đọc, hoặc dựng lại từ giao dịch đã tạo ra giao dịch này.

Các token được hỗ trợ

Một giao dịch có thể ghép một leg SPL Token với một leg Token-2022; mỗi leg mang token program riêng. Các leg Wrapped SOL được hỗ trợ.

Tiện ích mở rộng Token-2022 trên mint của một legHành vi của chương trình
TransferFee, InterestBearing, ScaledUiAmount, NonTransferableBị từ chối tại Create và Settle với BlockedMintExtension
PermanentDelegate, Pausable, DefaultAccountState, freeze authority, MintCloseAuthorityĐược chấp nhận; mỗi authority có thể tác động lên các token đang ký quỹ
ConfidentialTransferĐược chấp nhận; số lượng trên leg đó là công khai
TransferHookĐược hỗ trợ, tối đa 32 tài khoản bổ sung cho mỗi leg
MemoTransfer trên tài khoản đích hoặc tài khoản hoàn tiềnĐược chấp nhận; chương trình phát memo bắt buộc trước khi chuyển

Phí chuyển khoản sẽ khiến bên nhận nhận thiếu so với số lượng đã thỏa thuận. Interest Bearing và Scaled UI Amount không thay đổi số lượng thô, nhưng lãi suất hoặc hệ số nhân của chúng có thể thay đổi giữa lúc tạo và lúc thanh toán giao dịch, làm thay đổi cách hiển thị các số lượng đã thỏa thuận. NonTransferable sẽ khiến cả hai leg bị kẹt.

Các authority được chấp nhận có thể di chuyển, đóng băng hoặc tạm dừng các token đang ký quỹ trong suốt vòng đời của giao dịch (xem bên dưới). Escrow không thể được cấu hình để nhận các giao dịch chuyển bí mật, nên số lượng ký quỹ và số lượng thanh toán trên leg bí mật đều là công khai.

Với mint có hook, client xác định các tài khoản bổ sung của hook và thêm chúng vào Settle, Cancel, Reject, Reclaim và Recover. Giới hạn 32 cho mỗi leg bao gồm cả hook program và tài khoản xác thực của nó. Nếu hook authority mở rộng danh sách tài khoản của hook vượt quá giới hạn đó, mọi giao dịch chuyển trên leg đó sẽ thất bại, bao gồm cả Reclaim, Cancel, Reject và Recover, cho đến khi authority hoàn tác thay đổi. Giới hạn tài khoản của giao dịch có thể bị chạm trước giới hạn theo từng leg khi cả hai leg đều có hook.

Khi một tài khoản đích hoặc tài khoản hoàn tiền yêu cầu memo, chương trình gọi Memo program trước khi chuyển, đó là lý do mọi instruction chuyển token ra khỏi escrow đều cần một tài khoản memo_program.

Ràng buộc cấu hình mint

Scaled UI Amount. Template tokenized-security trong Mosaic, bộ công cụ phát hành của Solana Foundation, luôn thêm Scaled UI Amount, nên chương trình không chấp nhận mint được tạo từ template đó làm một leg. Một mint dành cho chương trình này có thể được tạo mà không có tiện ích mở rộng đó (xem Token Extensions); DvP dùng ủy quyền thanh toán thông qua authority được ủy quyền và không áp dụng kiểm tra này.

Mint bị đóng băng theo mặc định. Escrow của mỗi giao dịch là một associated token account thuộc sở hữu của một program-derived address mới cho giao dịch đó. Trên mint có Default Account State đặt là frozen, escrow được tạo ở trạng thái đóng băng, và giao dịch chuyển vốn vào escrow sẽ thất bại cho đến khi nó được mở băng. Với Token ACL, điều đó có nghĩa là tổ chức phát hành hoặc transfer agent của họ phải cho phép escrow của giao dịch trước khi nạp vốn: bằng cách thêm địa chỉ của bản ghi giao dịch (chủ sở hữu của escrow) vào allowlist, để bất kỳ ai cũng có thể mở băng escrow nếu cấu hình Token ACL của mint bật mở băng không cần cấp phép, hoặc bằng cách để freeze authority mở băng trực tiếp. Một escrow bị đóng băng cũng chặn Settle, Reclaim, Cancel, Reject và Recover trên leg đó, vì vậy freeze authority, và với mint có hook thì là hook authority, là bên cần được tin cậy trong suốt vòng đời của giao dịch. Luồng escrow bị đóng băng được mô tả tại đây và không được thực hiện trong các đoạn mã devnet.

Giới hạn

  • Không có khớp lệnh, khám phá giá hay sổ lệnh. Các bên thỏa thuận điều khoản bên ngoài sổ cái và một trong các bên ghi lại chúng.
  • Không có bù trừ, và chỉ song phương: một bản ghi là một giao dịch giữa hai bên.
  • Không có khớp một phần. Settle chuyển chính xác số lượng đã thỏa thuận của mỗi leg và trả lại phần dư cho bên của leg đó.
  • Không có leg ngoài sổ cái. Cả hai leg đều là token account trên Solana; leg tiền mặt trên hạ tầng hiện có được thanh toán riêng và đối soát.
  • Không có kiểm tra điều kiện đủ tư cách, allowlist hay KYC. Cơ chế kiểm soát riêng của mint sẽ thực thi điều kiện đủ tư cách của người nắm giữ.
  • Không có thanh toán tự động. Settlement authority phải ký.
  • Không có sự kiện, và không có phí giao thức ngoài phí giao dịch và rent.

Chương trình loại bỏ rủi ro gốc giữa hai bên: không leg nào được chuyển trừ khi cả hai cùng chuyển. Chương trình không loại bỏ rủi ro tín dụng của tổ chức phát hành hay rủi ro mua lại trên bất kỳ token nào, khả năng freeze, pause hoặc permanent delegate authority của mint tác động lên các token đang ký quỹ (xem các token được hỗ trợ), hay sự phụ thuộc vào việc settlement authority sẵn sàng để ký.

Xác minh trước khi nạp vốn và thanh toán

CreateDvp không cần cấp phép: chỉ payer ký, còn các bên và settlement authority thì không. Do đó, bất kỳ ai cũng có thể tạo một bản ghi nêu tên bất kỳ bên nào và bất kỳ điều khoản nào. Trước khi nạp vốn, và một lần nữa trước khi thanh toán, hãy đọc tài khoản SwapDvp và xác nhận:

  1. Tài khoản thuộc sở hữu của chương trình DvP và có kích thước đúng 458 byte. Một tài khoản không thuộc sở hữu của chương trình có thể bị điền các byte giải mã giống như một bản ghi thật.
  2. user_a, user_b, mint_a, mint_b và settlement_authority khớp với giao dịch đã thỏa thuận.
  3. amount_a, amount_b, expiry_timestamp và earliest_settlement_timestamp khớp với các điều khoản đã thỏa thuận.
  4. Cả hai đích thanh toán đều là các địa chỉ đã thỏa thuận. Một bản ghi giả mạo có thể chuyển hướng số tiền thu được.

Nạp vốn cho các leg liệt kê các trình đọc có kiểm tra của client được tạo sinh và trình bày cách kiểm tra bằng mã.

Hãy coi một giao dịch là đã thanh toán khi giao dịch Settle đạt mức finalized. Đó là một mức cam kết của mạng; việc nó có đồng thời đánh dấu tính chung cuộc của thanh toán về mặt pháp lý hay không phụ thuộc vào thỏa thuận của các bên và các chế độ pháp lý áp dụng.

Phiên bản

Tài khoản SwapDvp không có trường phiên bản. Bố cục của nó cố định ở 458 byte (SwapDvp::LEN), và các trình đọc có kiểm tra sẽ từ chối tài khoản có độ dài khác. IDL có phiên bản riêng, 0.1.0 tính đến ngày 2 tháng 10 năm 2026.

Chương trình có thể nâng cấp trên cả hai cluster (xem Program ID). Một bản ghi giao dịch chỉ tồn tại cho đến khi nó được settle, cancel hoặc reject, vì vậy hãy kiểm tra kho mã nguồn để biết một lần nâng cấp xử lý các giao dịch đang mở như thế nào, và tạo lại các client từ IDL của bản build đã triển khai.

Kho mã nguồn và kiểm toán

Mã nguồn, IDL, các client được tạo sinh và các bài kiểm thử nằm trong solana-foundation/dvp theo giấy phép MIT. Chương trình là một chương trình Pinocchio no_std; các client được tạo sinh từ IDL bằng Codama. Báo cáo lỗ hổng bảo mật được gửi qua liên kết security advisory của kho mã nguồn, theo SECURITY.md của kho.

Chương trình đã được Cantina kiểm toán.

Liên quan

Is this page helpful?