DvP 程序是 Solana 上用于交付对付款的标准程序,由 Solana 基金会维护,并经 Cantina 审计。双方先就一笔交易达成一致,各自通过普通的代币转账将自己的一条腿发送到托管账户,再由结算授权方在一笔交易中结算两条腿,因此要么两条腿同时转移,要么都不转移。结算授权方是交易中指定的第三方地址,例如撮合该交易的交易场所或代理。任何一方都无需编写或部署程序,只要钱包或托管机构能够发送标准代币转账(TransferChecked),就可以为一条腿注资,无需任何 DvP 专属集成。在结算之前,任何一方都可以取回自己的那条腿或撤销交易。
操作前请先读取记录
创建交易记录无需许可,因此记录本身并不能证明双方已达成一致。各方在注资前应读取已存储的条款,结算授权方在结算前也应如此(参见验证)。
工作原理
每笔交易都有一条由程序所有的记录和两个托管账户,每条腿对应一个。资产腿一方(user_a)和现金腿一方(user_b)分别将代币转入对应腿的托管账户来为该腿注资。结算授权方是唯一能够执行结算的签名者。结算时,会将每条腿约定的数额转给对方,将多余部分退还给该腿的所有方,并关闭记录和两个托管账户。原子性源自 Solana 交易:两笔转账位于同一笔交易中,因此要么都生效,要么都不生效。
每一笔退款、取回和多余部分的退还都会进入该方自己的 associated token account:mint_a 对应 user_a 的账户,mint_b 对应 user_b 的账户。托管机构或资金系统可以用自己的账户为某条腿注资,但任何退还的资金都会进入该方的账户,而不是回到托管机构。每笔交易还会留下一个永久的标记账户(参见状态)。
指南
分步页面带你完成一笔交易从创建到结算的全过程,每条指令都提供 TypeScript 和 Rust 示例。
创建交易
安装客户端,并通过 CreateDvp 记录交易条款
为两条腿注资
验证已存储的条款,然后通过普通转账为每条腿注资
结算交易
以结算授权方的身份在一笔交易中结算两条腿
撤销交易
取回一条腿、取消或拒绝交易,并找回延迟到账的存款
devnet 演示会完整运行一笔交易:创建交易,通过标准代币转账为两个托管账户注资,并在一笔交易中结算两条腿。
在程序与委托之间选择
使用委托的交付对付款(DvP)无需程序即可完成同样的交换:双方各自将其头寸委托给结算代理,由代理在一笔交易中转移两条腿。
- 程序方案无需委托,因此不受每个 token account 只能有一个委托人的限制,结算授权方也无法改变收益的去向。每笔交易都依赖于一个可升级的程序及其升级授权。
- 委托模式不依赖任何程序,并且适用于带有 Scaled UI Amount 的 mint。程序会拒绝这类 mint,这就排除了所有基于 Mosaic
tokenized-security模板创建的 mint(参见mint 配置限制)。
角色
该程序是对称的。在其接口中,双方分别是交付 mint_a 的 user_a 和交付 mint_b 的 user_b。README 和生成的客户端将 mint_a 称为资产腿,将 mint_b 称为现金腿,本文档也遵循这一约定,但程序并不要求现金腿必须是现金工具。两种资产或两种稳定币,结算方式相同。
| 角色 | 对象 | 签名内容 | 说明 |
|---|---|---|---|
| 资产腿一方 | user_a,交付 mint_a | 其自己的注资转账;Reclaim、Reject、Recover | keypair 钱包,或基于 PDA 的智能钱包,例如多签金库。SPL Token 多签账户或 program account 不能作为参与方:Create 会因 PartyNotSignerCapable 而失败 |
| 现金腿一方 | user_b,交付 mint_b | 其自己的注资转账;Reclaim、Reject、Recover | 要求相同 |
| 结算授权方 | 创建时指定的第三方地址 | Settle、Cancel | 唯一能够执行结算的签名者。它无法改变收益的去向,收益在创建时即已固定。结算或取消时,它会收到被关闭账户的 rent |
| rent 支付方 | 任何签署 CreateDvp 的人 | Create | 为记录、nonce 墓碑和两个托管账户支付 rent(用于保持账户开启的 SOL 押金)。不会被记录为 rent 接收方:被关闭账户的 rent 归关闭该交易的人所有 |
程序 ID
dvp34bdbcEm4f4FCUjGV4mDAkDshaQR4LkK8fdcsyZq
| 集群 | 程序 | 升级授权 | 最近部署的 slot |
|---|---|---|---|
| mainnet-beta | dvp34bdbcEm4f4FCUjGV4mDAkDshaQR4LkK8fdcsyZq | DXtFpbPjcn2hxPnw79x1Pfoj35vXh5AsWBkS37YnXMVv | 452,058,374 |
| devnet | dvp34bdbcEm4f4FCUjGV4mDAkDshaQR4LkK8fdcsyZq | 5EX1zFeJWj4rGYZv432WnYw8CbaSUeTdxPx4r573UgYU | 479,376,863 |
该程序在两个集群上均可升级。上述数据是 solana program show 于 2026 年 10 月 2 日返回的结果;请运行同一命令以获取当前值。devnet 版本比分步页面所依据的提交更旧(参见安装),但指令集和错误集相同。
生命周期
交易从 CreateDvp 起保持开启,直到 Settle、Cancel 或 Reject 之一将其关闭。注资没有专门的指令:当某条腿的托管账户持有至少约定数额时,该腿即视为已注资。Reclaim 针对单条腿,且交易保持开启,因此一条腿出现问题时,无需迫使另一方取消整笔交易。
状态
每笔交易有一个 458 字节的 SwapDvp 账户,由程序所有。其地址由结算授权方、双方、两个 mint 和一个 nonce 派生(seeds 为 ["dvp", settlement_authority, user_a, user_b, mint_a, mint_b, nonce])。数额和时间存储在账户内部,不影响地址,因此必须读取,而不能推断。随交易一同创建的还有一个小型标记账户,即位于 ["nonce", swap_dvp] 的 nonce 墓碑,它永远不会被关闭,因此各方、mint、授权方与 nonce 的每种组合只能用于一笔交易。
| 字段 | 类型 | 含义 |
|---|---|---|
bump | u8 | PDA bump |
user_a, user_b | Pubkey | 交易双方 |
mint_a, mint_b | Pubkey | 资产腿和现金腿的 mint |
settlement_authority | Pubkey | 唯一能够执行结算或取消的签名者 |
token_program_a, token_program_b | Pubkey | 拥有各 mint 的 token program;一笔交易可以混用 SPL Token 和 Token-2022 的腿 |
amount_a, amount_b | u64 | 每条腿的约定数额,以 mint 的最小单位(基础单位)计 |
expiry_timestamp | i64 | 超过此网络时间(链上时钟)后,Settle 会被拒绝。Reclaim、Cancel 和 Reject 仍可使用 |
nonce | u64 | 用于区分其他 seeds 相同的交易。仅可使用一次 |
ref_string | [u8; 64] | 不透明的客户端引用,以零填充;程序从不读取它 |
user_a_settlement_destination | Pubkey | 在 Settle 时接收现金腿;默认为 user_a |
user_b_settlement_destination | Pubkey | 在 Settle 时接收资产腿;默认为 user_b |
mint_a_authority, mint_b_authority | Pubkey | 创建时记录的 mint 授权方;任何一方发生变化,Settle 都会失败 |
earliest_settlement_timestamp | Option<i64> | 如果设置,则在此网络时间之前 Settle 会被拒绝 |
字段列表取自程序 IDL 中创建交易下所指明的提交版本。
指令
| 指令 | 签名者 | 效果 |
|---|---|---|
CreateDvp | 任何支付方 | 创建 SwapDvp 记录、其 nonce 墓碑和两个托管账户。不转移任何代币 |
ReclaimDvp | user_a 或 user_b | 从托管中退回签名者自己的那条腿。交易保持开启,该腿可再次注资 |
SettleDvp | 结算授权方 | 将 amount_a 转给 user_b 的目的地址,将 amount_b 转给 user_a 的目的地址,把多余部分退还给该腿所属的一方,并关闭记录和两个托管账户 |
CancelDvp | 结算授权方 | 将托管账户 A 中的全部余额退还给 user_a,将托管账户 B 中的全部余额退还给 user_b,并关闭记录和两个托管账户 |
RejectDvp | user_a 或 user_b | 效果与 Cancel 相同,由参与方签名 |
RecoverDvp | user_a 或 user_b | 交易关闭后:将重新创建的托管账户中的资金转入签名者自己的 token account 并将其关闭。适用于在 Settle、Cancel 或 Reject 之后才到账的存款 |
无论由谁为托管账户注资,每笔退款都会进入该方针对该条腿 mint 的自己的 associated token account。
每条指令的账户和参数见对应步骤的页面。
错误
| 代码 | 错误 | 触发时机 |
|---|---|---|
| 0 | SignerNotParty | Reclaim、Reject 或 Recover 由并非 user_a 或 user_b 的地址签名 |
| 1 | DvpExpired | 在 expiry_timestamp 之后 Settle |
| 2 | SettlementAuthorityMismatch | Settle 或 Cancel 由结算授权方以外的地址签名 |
| 3 | SettlementTooEarly | 在 earliest_settlement_timestamp 之前 Settle |
| 4 | LegNotFunded | Settle 时某个托管账户的余额低于其约定数额 |
| 5 | ExpiryNotInFuture | Create 时到期时间不晚于当前网络时间 |
| 6 | EarliestAfterExpiry | Create 时最早结算时间晚于到期时间 |
| 7 | SelfDvp | Create 时 user_a 等于 user_b |
| 8 | SameMint | Create 时 mint_a 等于 mint_b |
| 9 | ZeroAmount | Create 时任意一条腿的数额为零 |
| 10 | BlockedMintExtension | Create 或 Settle 时 mint 带有 TransferFee、InterestBearing、ScaledUiAmount 或 NonTransferable 扩展 |
| 11 | SettlementAuthorityIsParty | Create 时结算授权方等于某一参与方 |
| 12 | SettlementAuthorityExecutable | Create 时结算授权方为可执行账户 |
| 13 | NonceAlreadyUsed | Create 重复使用了已有墓碑的 (seeds, nonce) |
| 14 | ExpiryTooFarInFuture | Create 时到期时间超过当前时间一年以上 |
| 15 | RefStringTooLong | Create 时引用长度超过 64 字节 |
| 16 | DvpStillOpen | 交易仍处于开启状态时执行 Recover;请改用 Reclaim |
| 17 | DvpNeverCreated | 使用不存在墓碑标记的 seeds 进行 Recover |
| 18 | EscrowPreloadedWithLamports | Create 时,非原生托管账户持有的 lamports 超过其 rent 豁免最低额度 |
| 19 | SettlementDestinationIsSwapDvp | Create 时,结算目标账户与交易记录相同 |
| 20 | SwapDvpPreloadedWithLamports | Create 时,记录地址持有的 lamports 超过其 rent 储备 |
| 21 | RecipientAtaMismatch | 收款方或退款 token account 不再属于预期的钱包和 mint |
| 22 | PartyNotSignerCapable | Create 时,某参与方不是系统程序拥有的、不可执行的账户 |
| 23 | MintAuthorityChanged | Settle 时,某条交易腿的 mint authority 与创建时记录的值不同 |
事件与索引
该程序不会发出链上事件。索引器或对账系统会读取 SwapDvp 账户状态以及两个托管账户的 token 余额,并使用 ref_string 将交易与其链下订单关联。由于记录在结算时会被关闭,事后需要这些条款的系统应在读取时将其存储下来,或从创建该交易的 transaction 中重建。
支持的代币
一笔交易可以将 SPL Token 交易腿与 Token-2022 交易腿配对;每条交易腿都带有各自的 token program。支持 Wrapped SOL 交易腿。
| 交易腿 mint 上的 Token-2022 扩展 | 程序行为 |
|---|---|
| TransferFee, InterestBearing, ScaledUiAmount, NonTransferable | 在 Create 和 Settle 时被拒绝,并返回 BlockedMintExtension |
| PermanentDelegate, Pausable, DefaultAccountState, 冻结 authority, MintCloseAuthority | 接受;每个 authority 都可对托管的代币执行操作 |
| ConfidentialTransfer | 接受;该交易腿上的金额是公开的 |
| TransferHook | 支持,每条交易腿最多 32 个额外账户 |
| 目标账户或退款账户上的 MemoTransfer | 接受;程序会在转账前发出所需的 memo |
转账费用会使接收方收到的金额少于约定金额。Interest Bearing 和 Scaled UI Amount 不会改变原始金额,但其利率或乘数可能在创建与结算交易之间发生变化,从而改变约定金额的显示方式。NonTransferable 会使两条交易腿都被锁死。
被接受的这些 authority 可以在交易存续期间转移、冻结或暂停托管的代币(见下文)。托管账户无法配置为接收机密转账,因此机密交易腿上托管和结算的金额是公开的。
对于带 hook 的 mint,客户端会解析 hook 所需的额外账户,并将其附加到 Settle、Cancel、Reject、Reclaim 和 Recover 中。每条交易腿 32 个的上限包含 hook 程序及其验证账户。如果 hook authority 将 hook 的账户列表扩充到超过该上限,则该交易腿上的所有转账都会失败,包括 Reclaim、Cancel、Reject 和 Recover,直到 authority 撤销该更改。当两条交易腿都带有 hook 时,交易的账户数量限制可能会先于每条交易腿的上限生效。
当目标账户或退款账户要求 memo 时,程序会在转账前调用 Memo 程序,因此每条将代币转出托管的指令都需要一个 memo_program 账户。
Mint 配置限制
Scaled UI Amount。 Mosaic(Solana Foundation 的发行工具包)中的 tokenized-security 模板始终会添加 Scaled UI Amount,因此该程序不接受由该模板创建的 mint 作为交易腿。可以创建不带该扩展的 mint 供本程序使用(参见 Token Extensions);使用委托的 DvP 通过委托授权结算,不适用此检查。
默认冻结的 mint。 每笔交易的托管账户都是由 Program Derived Address 拥有的 associated token account,该地址对每笔交易都是全新的。在 Default Account State 设为冻结的 mint 上,托管账户创建时即处于冻结状态,向其转入资金的转账会失败,直到它被解冻。在 Token ACL 下,这意味着发行方或其过户代理需要在注资前准入该交易的托管账户:要么将交易记录的地址(即托管账户的所有者)加入允许列表,这样在 mint 的 Token ACL 配置启用无许可解冻的情况下任何人都可以解冻该托管账户;要么由 freeze authority 直接解冻。被冻结的托管账户还会阻止该交易腿上的 Settle、Reclaim、Cancel、Reject 和 Recover,因此在交易存续期间,freeze authority(对于带 hook 的 mint 还包括 hook authority)是受信任的一方。此处描述了冻结托管账户的路径,devnet 代码片段中未演示该路径。
限制
- 不提供撮合、价格发现或订单簿。各方在账本之外约定条款,并由其中一方记录。
- 不支持轧差,且仅限双边:一条记录即为两方之间的一笔交易。
- 不支持部分成交。Settle 会精确转移每条交易腿的约定金额,并将任何多余部分退还给该交易腿的参与方。
- 不支持账本外的交易腿。两条交易腿都是 Solana 上的 token account;基于现有支付通道的现金腿单独结算并对账。
- 不进行资格检查、允许列表或 KYC。持有人资格由 mint 自身的控制机制来强制执行。
- 不支持自动结算。结算 authority 必须签名。
- 没有事件,除交易费用和 rent 外没有协议费用。
该程序消除了双方之间的本金风险:除非两条交易腿都转移,否则任何一条都不会转移。但它不能消除任一代币的发行方信用风险或赎回风险,不能消除 mint 的冻结、暂停或永久委托 authority 对托管代币采取操作的可能性(参见支持的代币),也不能消除对结算 authority 能够签名的依赖。
注资和结算前的验证
CreateDvp 是无需许可的:只有付款方签名,参与方和结算 authority 不需要签名。因此任何人都可以创建指定任意参与方和任意条款的记录。在注资之前以及结算之前,请读取 SwapDvp 账户并确认:
- 该账户由 DvP 程序拥有,且大小恰好为 458 字节。不属于该程序的账户可能被填入能解码成真实记录的字节。
user_a、user_b、mint_a、mint_b和settlement_authority与约定的交易一致。amount_a、amount_b、expiry_timestamp和earliest_settlement_timestamp与约定的条款一致。- 两个结算目标都是约定的地址。伪造的记录可能会重定向收益。
为交易腿注资列出了生成的客户端中带检查的读取器,并通过代码展示了该检查。
当 Settle 交易达到 finalized 时,才可视为交易已结算。这是网络的承诺级别(commitment level);它是否同时构成法律意义上的结算终局性,取决于各方的协议以及适用的法律制度。
版本管理
SwapDvp 账户不带版本字段。其布局固定为 458 字节(SwapDvp::LEN),带检查的读取器会拒绝任何其他长度的账户。IDL 有自己的版本,截至 2026 年 10 月 2 日为 0.1.0。
该程序在两个集群上都是可升级的(参见 Program ID)。交易记录仅在结算、取消或拒绝之前存在,因此请查阅代码仓库了解升级如何处理未完成的交易,并根据已部署版本的 IDL 重新生成客户端。
代码仓库与审计
源代码、IDL、生成的客户端和测试位于 solana-foundation/dvp,采用 MIT 许可证。该程序是 no_std 的 Pinocchio 程序;客户端由 Codama 根据 IDL 生成。漏洞报告请按照其 SECURITY.md 的说明,通过代码仓库的安全公告链接提交。
该程序已由 Cantina 审计。
相关内容
- 使用委托的券款对付(DvP):无需程序的委托模式
- Token ACL:受限 mint 如何与托管账户交互
Is this page helpful?