DvP 程序是 Solana 上券款对付的标准程序,由 Solana Foundation 维护,并经 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 tombstone 以及两个托管账户支付 rent(使账户保持开启的 SOL 押金)。不会被记录为 rent 接收方:已关闭账户的 rent 归关闭交易的一方 |
程序 ID
dvp34bdbcEm4f4FCUjGV4mDAkDshaQR4LkK8fdcsyZq
| 集群 | 程序 | 升级权限 | 最近部署的 slot |
|---|---|---|---|
| mainnet-beta | dvp34bdbcEm4f4FCUjGV4mDAkDshaQR4LkK8fdcsyZq | DXtFpbPjcn2hxPnw79x1Pfoj35vXh5AsWBkS37YnXMVv | 452,058,374 |
| devnet | dvp34bdbcEm4f4FCUjGV4mDAkDshaQR4LkK8fdcsyZq | 5EX1zFeJWj4rGYZv432WnYw8CbaSUeTdxPx4r573UgYU | 479,376,863 |
该程序在两个集群上均可升级。上表数据为 2026 年 10 月 2 日 solana program show 的输出;如需当前值,请自行运行同一命令。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 tombstone,会随交易一同创建且永不关闭,因此参与方、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 tombstone 以及两个托管账户。不转移任何代币 |
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 | 创建时设置的到期时间不晚于当前网络时间 |
| 6 | EarliestAfterExpiry | 创建时设置的最早结算时间晚于到期时间 |
| 7 | SelfDvp | 创建时 user_a 与 user_b 相同 |
| 8 | SameMint | 创建时 mint_a 与 mint_b 相同 |
| 9 | ZeroAmount | 创建时任一条腿的数量为零 |
| 10 | BlockedMintExtension | 使用带有 TransferFee、InterestBearing、ScaledUiAmount 或 NonTransferable 的 mint 执行 Create 或 Settle |
| 11 | SettlementAuthorityIsParty | 创建时结算授权方与某一参与方相同 |
| 12 | SettlementAuthorityExecutable | 创建时结算授权方是可执行账户 |
| 13 | NonceAlreadyUsed | 创建时重复使用已有 tombstone 的 (seeds, nonce) |
| 14 | ExpiryTooFarInFuture | 创建时设置的到期时间超过一年之后 |
| 15 | RefStringTooLong | 创建时引用长度超过 64 字节 |
| 16 | DvpStillOpen | 在交易仍处于开放状态时执行 Recover;请改用 Reclaim |
| 17 | DvpNeverCreated | 使用没有墓碑(tombstone)的 seeds 执行 Recover |
| 18 | EscrowPreloadedWithLamports | 当非原生托管账户持有的 lamports 超过其 rent 豁免最低额时,执行 Create |
| 19 | SettlementDestinationIsSwapDvp | 在结算目标地址与交易记录相同的情况下执行 Create |
| 20 | SwapDvpPreloadedWithLamports | 当记录地址持有的 lamports 超过其 rent 储备时,执行 Create |
| 21 | RecipientAtaMismatch | 收款方或退款方的 token account 不再属于预期的钱包和 mint |
| 22 | PartyNotSignerCapable | 所指定的参与方不是由系统程序拥有的非可执行账户时,执行 Create |
| 23 | MintAuthorityChanged | 当某一交易腿的 mint authority 与创建时记录的值不一致时,执行 Settle |
事件与索引
该程序不发出任何链上事件。索引器或对账系统会读取 SwapDvp 账户状态以及两个托管账户的 token 余额,并使用 ref_string 将交易与其链下订单关联起来。由于记录在结算时会被关闭,事后需要查看交易条款的系统,应在读取时将其存储下来,或者从创建该交易的 transaction 中重建。
支持的 token
一笔交易可以将 SPL Token 交易腿与 Token-2022 交易腿配对;每条交易腿各自携带其 token program。支持封装的 SOL 交易腿。
| 交易腿 mint 上的 Token-2022 扩展 | 程序行为 |
|---|---|
| TransferFee、InterestBearing、ScaledUiAmount、NonTransferable | 在 Create 和 Settle 时被拒绝,并返回 BlockedMintExtension |
| PermanentDelegate、Pausable、DefaultAccountState、freeze authority、MintCloseAuthority | 允许;各权限方均可对托管的 token 进行操作 |
| ConfidentialTransfer | 允许;该交易腿上的金额是公开的 |
| TransferHook | 支持,每条交易腿最多 32 个额外账户 |
| 目标账户或退款账户上的 MemoTransfer | 允许;程序会在转账前发出所需的 memo |
转账手续费会使接收方收到的金额低于约定金额。Interest Bearing 和 Scaled UI Amount 不会改变原始金额,但其利率或乘数可能在创建与结算交易之间发生变化,从而改变约定金额的显示方式。NonTransferable 会使两条交易腿都被锁死。
在交易存续期间(见下文),被允许的权限方可以转移、冻结或暂停托管的 token。托管账户无法配置为接收保密转账,因此保密交易腿上的托管金额和结算金额都是公开的。
对于带 hook 的 mint,客户端会解析该 hook 的额外账户,并将其附加到 Settle、Cancel、Reject、Reclaim 和 Recover 中。每条交易腿 32 个的上限包含 hook 程序及其验证账户。如果 hook 权限方将 hook 的账户列表扩充到超过该上限,则该交易腿上的所有转账(包括 Reclaim、Cancel、Reject 和 Recover)都会失败,直到该权限方撤销此更改。当两条交易腿都带有 hook 时,transaction 的账户数量限制可能会先于每条交易腿的上限生效。
当目标账户或退款账户要求 memo 时,程序会在转账前调用 Memo 程序,这就是为什么每个将 token 转出托管账户的指令都需要一个 memo_program 账户。
Mint 配置约束
Scaled UI Amount。 Mosaic(Solana Foundation 的发行工具包)中的 tokenized-security 模板始终会添加 Scaled UI Amount,因此程序不接受由该模板创建的 mint 作为交易腿。供本程序使用的 mint 可以在不带该扩展的情况下创建(参见 Token Extensions);使用委托的 DvP 通过委托授权进行结算,不适用此项检查。
默认冻结的 mint。 每笔交易的托管账户都是一个 associated token account,其所有者是为该交易新生成的 program-derived address。在 Default Account State 设为冻结的 mint 上,托管账户创建后即处于冻结状态,在其解冻之前,向其注资的转账会失败。在 Token ACL 下,这意味着发行方或其过户代理必须在注资前准入该交易的托管账户:可以将交易记录的地址(即托管账户的所有者)加入允许列表,这样在 mint 的 Token ACL 配置启用了无许可解冻的情况下,任何人都可以解冻该托管账户;也可以由 freeze authority 直接将其解冻。冻结的托管账户还会阻止该交易腿上的 Settle、Reclaim、Cancel、Reject 和 Recover,因此在交易存续期间,freeze authority(对于带 hook 的 mint,还包括 hook 权限方)是一个受信任的一方。冻结托管账户的路径在此处仅作说明,devnet 代码片段中并未演示。
限制
- 没有撮合、价格发现或订单簿。参与方在账本之外商定条款,并由其中一方将其记录下来。
- 没有净额结算,且仅支持双边:一条记录就是两方之间的一笔交易。
- 不支持部分成交。Settle 会精确转移每条交易腿的约定金额,并将任何多余部分退还给该交易腿的参与方。
- 没有账本之外的交易腿。两条交易腿都是 Solana 上的 token account;基于现有渠道的现金交易腿单独结算并进行对账。
- 没有资格检查、允许列表或 KYC。持有人资格由 mint 自身的控制机制来强制执行。
- 没有自动结算。结算授权方必须签名。
- 没有事件,除交易手续费和 rent 外也没有协议费用。
该程序消除了双方之间的本金风险:除非两条交易腿同时转移,否则任何一条都不会转移。但它并不能消除任一 token 的发行方信用风险或赎回风险、mint 的冻结、暂停或永久委托权限对托管 token 进行操作的可能性(参见 支持的 token),也不能消除对结算授权方能够随时签名的依赖。
注资和结算前的验证
CreateDvp 是无许可的:只有付款方需要签名,参与方和结算授权方无需签名。因此任何人都可以创建指定任意参与方和任意条款的记录。在注资之前以及结算之前,请读取 SwapDvp 账户并确认:
- 该账户由 DvP 程序拥有,且大小恰好为 458 字节。不属于该程序的账户可能被填入能够解码成真实记录的字节。
user_a、user_b、mint_a、mint_b和settlement_authority与约定的交易一致。amount_a、amount_b、expiry_timestamp和earliest_settlement_timestamp与约定的条款一致。- 两个结算目标地址均为约定的地址。伪造的记录可能会重定向收益。
为各交易腿注资列出了生成的客户端中带检查的读取器,并通过代码展示了该检查。
当 Settle transaction 达到 finalized 时,即可将交易视为已结算。这是网络层面的确认级别;它是否同时构成法律意义上的结算终局性,取决于各方的协议以及适用的法律制度。
版本控制
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?