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.
Tạo thương vụ
Cài đặt các client và ghi lại điều khoản của thương vụ bằng CreateDvp
Nạp các vế
Xác minh các điều khoản đã lưu, rồi nạp từng vế bằng một lệnh chuyển thông thường
Quyết toán thương vụ
Quyết toán cả hai vế trong một giao dịch với tư cách bên quyết toán
Hoàn tác thương vụ
Rút lại một vế, hủy hoặc từ chối thương vụ, và thu hồi các khoản nạp muộn
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-securitycủ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ượng | Ký | Ghi chú |
|---|---|---|---|
| Bên sở hữu vế tài sản | user_a, giao mint_a | Lệnh chuyển nạp vế của chính mình; Reclaim, Reject, Recover | Mộ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ặt | user_b, giao mint_b | Lệnh chuyển nạp vế của chính mình; Reclaim, Reject, Recover | Yêu cầu tương tự |
| Bên quyết toán | Một địa chỉ thứ ba được nêu khi tạo | Settle, Cancel | Ngườ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ả rent | Bất kỳ ai ký CreateDvp | Create | Nạ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
| Cluster | Chương trình | Quyền nâng cấp | slot triển khai gần nhất |
|---|---|---|---|
| mainnet-beta | dvp34bdbcEm4f4FCUjGV4mDAkDshaQR4LkK8fdcsyZq | DXtFpbPjcn2hxPnw79x1Pfoj35vXh5AsWBkS37YnXMVv | 452,058,374 |
| devnet | dvp34bdbcEm4f4FCUjGV4mDAkDshaQR4LkK8fdcsyZq | 5EX1zFeJWj4rGYZv432WnYw8CbaSUeTdxPx4r573UgYU | 479,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ường | Kiểu | Ý nghĩa |
|---|---|---|
bump | u8 | PDA bump |
user_a, user_b | Pubkey | Hai bên tham gia |
mint_a, mint_b | Pubkey | Các mint của vế tài sản và vế tiền mặt |
settlement_authority | Pubkey | Người ký duy nhất có thể quyết toán hoặc hủy |
token_program_a, token_program_b | Pubkey | Token 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_b | u64 | Số lượng đã thỏa thuận của mỗi vế, theo đơn vị nhỏ nhất của mint (base unit) |
expiry_timestamp | i64 | Sau 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 |
nonce | u64 | Phâ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_destination | Pubkey | Nhận vế tiền mặt khi Settle; mặc định là user_a |
user_b_settlement_destination | Pubkey | Nhận vế tài sản khi Settle; mặc định là user_b |
mint_a_authority, mint_b_authority | Pubkey | Các mint authority được ghi lại khi tạo; Settle sẽ lỗi nếu một trong hai đã thay đổi |
earliest_settlement_timestamp | Option<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
| Instruction | Người ký | Tác dụng |
|---|---|---|
CreateDvp | Bất kỳ bên trả phí nào | Tạo bản ghi SwapDvp, nonce tombstone của nó và cả hai tài khoản escrow. Không di chuyển token |
ReclaimDvp | user_a hoặc user_b | Trả 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 |
SettleDvp | Bên quyết toán | Chuyể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 |
CancelDvp | Bên quyết toán | Hoà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 |
RejectDvp | user_a hoặc user_b | Tác dụng giống Cancel, nhưng do một bên tham gia ký |
RecoverDvp | user_a hoặc user_b | Sau 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ỗi | Khi nào |
|---|---|---|
| 0 | SignerNotParty | Reclaim, Reject hoặc Recover được ký bởi một địa chỉ không phải user_a hay user_b |
| 1 | DvpExpired | Settle sau expiry_timestamp |
| 2 | SettlementAuthorityMismatch | Settle hoặc Cancel được ký bởi một địa chỉ khác với bên quyết toán |
| 3 | SettlementTooEarly | Settle trước earliest_settlement_timestamp |
| 4 | LegNotFunded | Settle khi một escrow giữ ít hơn số lượng đã thỏa thuận |
| 5 | ExpiryNotInFuture | Create 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 |
| 6 | EarliestAfterExpiry | Create với thời điểm quyết toán sớm nhất sau thời điểm hết hạn |
| 7 | SelfDvp | Create với user_a bằng user_b |
| 8 | SameMint | Create với mint_a bằng mint_b |
| 9 | ZeroAmount | Create với số lượng bằng 0 ở một trong hai vế |
| 10 | BlockedMintExtension | Create hoặc Settle với một mint có TransferFee, InterestBearing, ScaledUiAmount hoặc NonTransferable |
| 11 | SettlementAuthorityIsParty | Create với bên quyết toán trùng với một trong hai bên tham gia |
| 12 | SettlementAuthorityExecutable | Create với bên quyết toán là một tài khoản có thể thực thi |
| 13 | NonceAlreadyUsed | Create dùng lại một (seeds, nonce) đã có tombstone |
| 14 | ExpiryTooFarInFuture | Create với thời điểm hết hạn xa hơn một năm |
| 15 | RefStringTooLong | Create với tham chiếu dài hơn 64 byte |
| 16 | DvpStillOpen | Recover khi thương vụ vẫn đang mở; hãy dùng Reclaim |
| 17 | DvpNeverCreated | Recover với các seed không có tombstone |
| 18 | EscrowPreloadedWithLamports | Create 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 |
| 19 | SettlementDestinationIsSwapDvp | Create với đích thanh toán trùng với bản ghi giao dịch |
| 20 | SwapDvpPreloadedWithLamports | Create khi địa chỉ bản ghi đang giữ lamports vượt quá khoản rent dự trữ |
| 21 | RecipientAtaMismatch | Mộ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 |
| 22 | PartyNotSignerCapable | Create 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 |
| 23 | MintAuthorityChanged | Settle 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 leg | Hành vi của chương trình |
|---|---|
| TransferFee, InterestBearing, ScaledUiAmount, NonTransferable | Bị 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:
- 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.
user_a,user_b,mint_a,mint_bvàsettlement_authoritykhớp với giao dịch đã thỏa thuận.amount_a,amount_b,expiry_timestampvàearliest_settlement_timestampkhớp với các điều khoản đã thỏa thuận.- 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
- Delivery vs Payment (DvP) dùng ủy quyền: mô hình ủy quyền không cần chương trình
- Token ACL: cách một mint có kiểm soát truy cập tương tác với các tài khoản escrow
Is this page helpful?