Программа Delivery vs Payment (DvP)

Программа DvP — стандартная программа для поставки против платежа (delivery versus payment) в Solana, которую поддерживает Solana Foundation и аудит которой провела Cantina. Две стороны договариваются о сделке, каждая отправляет свою ногу в эскроу-аккаунт обычным переводом токенов, а расчётный авторитет проводит расчёт по обеим ногам в одной транзакции: либо перемещаются обе, либо ни одна. Расчётный авторитет — это третий адрес, указанный в сделке, например площадка или агент, организовавшие её. Ни одной из сторон не нужно писать или развёртывать программу, а кошелёк или кастодиан, способный отправить стандартный перевод токенов (TransferChecked), может профинансировать ногу без какой-либо интеграции с DvP. До расчёта любая из сторон может забрать свою ногу обратно или отменить сделку.

Прочитайте запись, прежде чем на неё полагаться

Создание записи о сделке не требует разрешений, поэтому сама запись не является доказательством договорённости. Каждая сторона читает сохранённые условия перед финансированием, а расчётный авторитет — перед расчётом (см. проверка).

Как это работает

Для каждой сделки создаются принадлежащая программе запись и два эскроу-аккаунта — по одному на каждую ногу. Сторона ноги актива (user_a) и сторона денежной ноги (user_b) финансируют свои ноги, переводя токены на эскроу-аккаунт соответствующей ноги. Расчётный авторитет — единственный подписант, который может провести расчёт. При расчёте согласованная сумма каждой ноги переводится противоположной стороне, излишек возвращается стороне, которой принадлежит нога, а запись и оба эскроу закрываются. Атомарность обеспечивают транзакции Solana: оба перевода выполняются в одной транзакции, поэтому либо вступают в силу оба, либо ни один.

Любой возврат, востребование и возврат излишка направляются на associated token account самой стороны: аккаунт user_a для mint_a, аккаунт user_b для mint_b. Кастодиан или казначейская система может профинансировать ногу со своего аккаунта, но всё возвращаемое поступает на аккаунт стороны, а не обратно кастодиану. Кроме того, каждая сделка оставляет постоянный аккаунт-маркер (см. Состояние).

Руководства

Страницы с шагами проводят одну сделку от создания до расчёта; для каждой инструкции приведены примеры на TypeScript и Rust.

Демо в devnet проводит сделку от начала до конца: создаёт сделку, финансирует оба эскроу стандартными переводами токенов и проводит расчёт по обеим ногам в одной транзакции.

Выбор между программой и делегированием

Delivery vs Payment (DvP) с использованием делегирования реализует тот же обмен без программы: каждая сторона делегирует свою позицию расчётному агенту, который перемещает обе ноги в одной транзакции.

  • Программа не требует делегирования, поэтому ограничение в один делегат на token account не действует, а расчётный авторитет не может перенаправить поступления. Каждая сделка зависит от обновляемой программы и её полномочий на обновление (upgrade authority).
  • Паттерн делегирования не зависит от программы и работает с минтами, использующими Scaled UI Amount. Программа отклоняет такие минты, что исключает любой минт, созданный из шаблона tokenized-security от Mosaic (см. ограничения конфигурации минта).

Роли

Программа симметрична. В её интерфейсе две стороны — это user_a, поставляющий mint_a, и user_b, поставляющий mint_b. В README и сгенерированных клиентах mint_a называется ногой актива, а mint_b — денежной ногой; эти страницы следуют той же конвенции, но ничто в программе не требует, чтобы денежная нога была денежным инструментом. Два актива или два стейблкоина рассчитываются точно так же.

РольКтоПодписываетПримечания
Сторона ноги активаuser_a, поставляет mint_aСобственный перевод для финансирования; Reclaim, Reject, RecoverКошелёк на keypair или смарт-кошелёк на основе PDA, например хранилище multisig. Multisig-аккаунт SPL Token или program account не может быть стороной: Create завершается ошибкой PartyNotSignerCapable
Сторона денежной ногиuser_b, поставляет mint_bСобственный перевод для финансирования; Reclaim, Reject, RecoverТо же требование
Расчётный авторитетТретий адрес, указанный при созданииSettle, CancelЕдинственный подписант, который может провести расчёт. Не может перенаправить поступления, которые фиксируются при создании. Получает rent закрытых аккаунтов при расчёте или отмене
Плательщик rentТот, кто подписывает CreateDvpCreateВносит rent (депозит в SOL, поддерживающий аккаунт открытым) за запись, nonce tombstone и оба эскроу. Не записывается как получатель rent: rent закрытых аккаунтов достаётся тому, кто закрывает сделку

ID программы

dvp34bdbcEm4f4FCUjGV4mDAkDshaQR4LkK8fdcsyZq
КластерПрограммаUpgrade authorityПоследний slot развёртывания
mainnet-betadvp34bdbcEm4f4FCUjGV4mDAkDshaQR4LkK8fdcsyZqDXtFpbPjcn2hxPnw79x1Pfoj35vXh5AsWBkS37YnXMVv452,058,374
devnetdvp34bdbcEm4f4FCUjGV4mDAkDshaQR4LkK8fdcsyZq5EX1zFeJWj4rGYZv432WnYw8CbaSUeTdxPx4r573UgYU479,376,863

Программа является обновляемой в обоих кластерах. Приведённые выше значения — это то, что вывела команда solana program show 2 октября 2026 года; выполните ту же команду, чтобы узнать актуальные значения. Сборка в devnet старше коммита, по которому написаны страницы с шагами (см. Установка), при этом набор инструкций и ошибок тот же.

Жизненный цикл

Сделка открыта с момента CreateDvp, пока её не закроет одна из инструкций Settle, Cancel или Reject. У финансирования нет собственной инструкции: нога считается профинансированной, когда её эскроу-аккаунт содержит не менее согласованной суммы. Reclaim действует для отдельной ноги и оставляет сделку открытой, поэтому проблема с одной ногой не вынуждает другую сторону отменять всю сделку.

Состояние

У каждой сделки есть один аккаунт SwapDvp размером 458 байт, принадлежащий программе. Его адрес выводится из расчётного авторитета, двух сторон, двух минтов и nonce (seeds ["dvp", settlement_authority, user_a, user_b, mint_a, mint_b, nonce]). Суммы и время хранятся внутри аккаунта и не влияют на адрес, поэтому их нужно считывать, а не угадывать. Небольшой аккаунт-маркер, nonce tombstone по адресу ["nonce", swap_dvp], создаётся вместе со сделкой и никогда не закрывается, поэтому каждую комбинацию сторон, минтов, авторитета и nonce можно использовать только для одной сделки.

ПолеТипЗначение
bumpu8PDA bump
user_a, user_bPubkeyДве стороны
mint_a, mint_bPubkeyМинты ноги актива и денежной ноги
settlement_authorityPubkeyЕдинственный подписант, который может провести расчёт или отменить сделку
token_program_a, token_program_bPubkeyToken Program, которой принадлежит каждый минт; в одной сделке можно сочетать ноги SPL Token и Token-2022
amount_a, amount_bu64Согласованная сумма каждой ноги в наименьшей единице минта (базовых единицах)
expiry_timestampi64После этого сетевого времени (ончейн-часов) Settle отклоняется. Reclaim, Cancel и Reject по-прежнему работают
nonceu64Различает сделки, у которых остальные seeds совпадают. Одноразовый
ref_string[u8; 64]Непрозрачная клиентская ссылка, дополненная нулями; программа её никогда не читает
user_a_settlement_destinationPubkeyПолучает денежную ногу при Settle; по умолчанию user_a
user_b_settlement_destinationPubkeyПолучает ногу актива при Settle; по умолчанию user_b
mint_a_authority, mint_b_authorityPubkeyMint authority, зафиксированные при создании; Settle завершается ошибкой, если любая из них изменилась
earliest_settlement_timestampOption<i64>Если задано, Settle отклоняется до наступления этого сетевого времени

Список полей взят из IDL программы в коммите, указанном в разделе Создание сделки.

Инструкции

ИнструкцияПодписантДействие
CreateDvpЛюбой плательщикСоздаёт запись SwapDvp, её nonce tombstone и оба эскроу-аккаунта. Токены не перемещает
ReclaimDvpuser_a или user_bВозвращает из эскроу собственную ногу подписавшего. Сделка остаётся открытой, и ногу можно профинансировать снова
SettleDvpРасчётный авторитетПереводит amount_a на адрес назначения user_b, а amount_b — на адрес назначения user_a, возвращает излишек стороне ноги, закрывает запись и оба эскроу
CancelDvpРасчётный авторитетВозвращает всё, что хранится в эскроу A, стороне user_a, а в эскроу B — стороне user_b, закрывает запись и оба эскроу
RejectDvpuser_a или user_bТот же эффект, что и у Cancel, но подписывает сторона
RecoverDvpuser_a или user_bПосле закрытия сделки: переносит всё из заново созданного эскроу на собственный token account подписавшего и закрывает его. Покрывает депозит, поступивший после Settle, Cancel или Reject

Каждый возврат направляется на associated token account самой стороны для минта соответствующей ноги, независимо от того, кто профинансировал эскроу.

Аккаунты и аргументы каждой инструкции приведены на странице соответствующего шага.

Ошибки

КодОшибкаКогда
0SignerNotPartyReclaim, Reject или Recover подписаны адресом, который не является user_a или user_b
1DvpExpiredSettle после expiry_timestamp
2SettlementAuthorityMismatchSettle или Cancel подписаны адресом, отличным от расчётного авторитета
3SettlementTooEarlySettle до earliest_settlement_timestamp
4LegNotFundedSettle, пока в одном из эскроу меньше согласованной суммы
5ExpiryNotInFutureCreate со сроком действия, равным текущему сетевому времени или более ранним
6EarliestAfterExpiryCreate с самым ранним временем расчёта, наступающим после окончания срока действия
7SelfDvpCreate, где user_a равен user_b
8SameMintCreate, где mint_a равен mint_b
9ZeroAmountCreate с нулевой суммой на любой из ног
10BlockedMintExtensionCreate или Settle с минтом, имеющим расширение TransferFee, InterestBearing, ScaledUiAmount или NonTransferable
11SettlementAuthorityIsPartyCreate, где расчётный авторитет совпадает с одной из сторон
12SettlementAuthorityExecutableCreate с исполняемым (executable) расчётным авторитетом
13NonceAlreadyUsedCreate, повторно использующий (seeds, nonce), для которого уже есть tombstone
14ExpiryTooFarInFutureCreate со сроком действия более чем на год вперёд
15RefStringTooLongCreate со ссылкой длиннее 64 байт
16DvpStillOpenRecover, пока сделка ещё открыта; используйте Reclaim
17DvpNeverCreatedRecover с сидами, для которых нет tombstone
18EscrowPreloadedWithLamportsCreate, когда нативно не связанное с SOL эскроу удерживает lamport сверх минимума, освобождающего от rent
19SettlementDestinationIsSwapDvpCreate с адресом назначения расчёта, совпадающим с записью сделки
20SwapDvpPreloadedWithLamportsCreate, когда адрес записи удерживает lamport сверх своего резерва на rent
21RecipientAtaMismatchtoken account получателя или возврата больше не принадлежит ожидаемому кошельку и mint
22PartyNotSignerCapableCreate со стороной, которая не является аккаунтом, принадлежащим системной программе и не являющимся исполняемым
23MintAuthorityChangedSettle, когда mint authority плеча отличается от значения, зафиксированного при создании

События и индексация

Программа не выпускает ончейн-событий. Индексатор или система сверки считывает состояние аккаунта SwapDvp и балансы токенов двух эскроу и использует ref_string, чтобы сопоставить сделку с её офчейн-ордером. Поскольку запись закрывается при расчёте, системе, которой условия нужны постфактум, следует сохранять их при чтении или восстанавливать по транзакции, создавшей сделку.

Поддерживаемые токены

В сделке можно сочетать плечо с SPL Token и плечо с Token-2022; у каждого плеча своя token program. Плечи с Wrapped SOL поддерживаются.

Расширение Token-2022 на mint плечаПоведение программы
TransferFee, InterestBearing, ScaledUiAmount, NonTransferableОтклоняется при Create и Settle с ошибкой BlockedMintExtension
PermanentDelegate, Pausable, DefaultAccountState, freeze authority, MintCloseAuthorityПринимается; каждый authority может действовать с токенами в эскроу
ConfidentialTransferПринимается; суммы на этом плече публичны
TransferHookПоддерживается, до 32 дополнительных аккаунтов на плечо
MemoTransfer на аккаунте назначения или возвратаПринимается; программа отправляет требуемый memo перед переводом

Комиссия за перевод привела бы к тому, что получающая сторона недополучила бы согласованную сумму. Interest Bearing и Scaled UI Amount не меняют исходные суммы, но их ставка или множитель могут измениться между созданием и расчётом сделки, что изменит отображение согласованных сумм. NonTransferable заблокировал бы оба плеча.

Допустимые authority могут перемещать, замораживать или останавливать токены в эскроу на протяжении всей сделки (см. ниже). Эскроу нельзя настроить на приём конфиденциальных переводов, поэтому суммы в эскроу и при расчёте на плече с конфиденциальными переводами публичны.

Для mint с хуком клиент определяет дополнительные аккаунты хука и добавляет их в Settle, Cancel, Reject, Reclaim и Recover. Ограничение в 32 на плечо включает программу хука и её аккаунт валидации. Если hook authority увеличит список аккаунтов хука сверх этого ограничения, каждый перевод на этом плече будет завершаться ошибкой, включая Reclaim, Cancel, Reject и Recover, пока authority не отменит изменение. Лимит аккаунтов в транзакции может сработать раньше ограничения на плечо, когда хуки есть на обоих плечах.

Если аккаунт назначения или возврата требует memo, программа вызывает программу Memo перед переводом, поэтому каждая инструкция, выводящая токены из эскроу, принимает аккаунт memo_program.

Ограничения конфигурации mint

Scaled UI Amount. Шаблон tokenized-security в Mosaic, наборе инструментов Solana Foundation для эмиссии, всегда добавляет Scaled UI Amount, поэтому программа не принимает mint, созданный из этого шаблона, в качестве плеча. Mint для этой программы можно создать без этого расширения (см. Token Extensions); DvP с использованием делегирования рассчитывается через делегированные полномочия и эту проверку не применяет.

Mint, замороженные по умолчанию. Эскроу каждой сделки — это associated token account, принадлежащий Program Derived Address, новому для этой сделки. Если для mint Default Account State установлено в frozen, эскроу создаётся замороженным, и финансирующий перевод в него завершается ошибкой, пока он не разморожен. В Token ACL это означает, что эмитент или его трансфер-агент допускает эскроу сделки до финансирования: либо добавляя адрес записи сделки (владельца эскроу) в allowlist, чтобы любой мог разморозить эскроу, если конфигурация Token ACL для mint включает размораживание без разрешений, либо поручая freeze authority разморозить его напрямую. Замороженное эскроу также блокирует Settle, Reclaim, Cancel, Reject и Recover на этом плече, поэтому freeze authority, а на mint с хуком — и hook authority, является доверенной стороной на всё время сделки. Путь с замороженным эскроу описан здесь и не используется в примерах для devnet.

Ограничения

  • Нет сопоставления ордеров, определения цены или книги ордеров. Стороны договариваются об условиях вне блокчейна, и одна из них записывает их.
  • Нет неттинга, только двусторонние сделки: одна запись — одна сделка между двумя сторонами.
  • Нет частичного исполнения. Settle перемещает ровно согласованную сумму каждого плеча и возвращает любой излишек стороне плеча.
  • Нет плеча вне блокчейна. Оба плеча — это token account в Solana; денежное плечо по существующим каналам рассчитывается отдельно и сверяется.
  • Нет проверки допуска, allowlist или KYC. Допуск держателей обеспечивают собственные средства контроля mint.
  • Нет автоматического расчёта. Расчётный authority должен подписать транзакцию.
  • Нет событий и нет комиссии протокола сверх комиссий за транзакции и rent.

Программа устраняет принципальный риск между двумя сторонами: ни одно плечо не перемещается, если не перемещаются оба. Она не устраняет кредитный риск эмитента или риск погашения по любому из токенов, возможность freeze, pause или permanent delegate authority mint действовать с токенами в эскроу (см. поддерживаемые токены), а также зависимость от того, что расчётный authority доступен для подписи.

Проверка перед финансированием и расчётом

CreateDvp не требует разрешений: подписывает только плательщик, а стороны и расчётный authority — нет. Поэтому любой может создать запись с произвольными сторонами и условиями. Перед финансированием и снова перед расчётом прочитайте аккаунт SwapDvp и убедитесь, что:

  1. Аккаунт принадлежит программе DvP и имеет ровно 458 байт. Аккаунт, не принадлежащий программе, можно заполнить байтами, которые декодируются как настоящая запись.
  2. user_a, user_b, mint_a, mint_b и settlement_authority соответствуют согласованной сделке.
  3. amount_a, amount_b, expiry_timestamp и earliest_settlement_timestamp соответствуют согласованным условиям.
  4. Оба адреса назначения расчёта — согласованные. Поддельная запись может перенаправить средства.

Финансирование плеч перечисляет проверяющие ридеры сгенерированных клиентов и показывает проверку в коде.

Считайте сделку рассчитанной, когда транзакция Settle достигает finalized. Это уровень подтверждения сети; считается ли он также моментом окончательности расчёта в юридическом смысле, зависит от соглашений сторон и применимого регулирования.

Версионирование

У аккаунта SwapDvp нет поля версии. Его структура фиксирована и занимает 458 байт (SwapDvp::LEN), а проверяющие ридеры отклоняют аккаунт любой другой длины. У IDL своя версия: 0.1.0 по состоянию на 2 октября 2026 г.

Программа поддерживает обновление в обоих кластерах (см. Program ID). Запись сделки существует только до тех пор, пока сделка не рассчитана, не отменена и не отклонена, поэтому в репозитории стоит проверить, как обновление влияет на открытые сделки, и перегенерировать клиентов из IDL развёрнутой сборки.

Репозиторий и аудит

Исходный код, IDL, сгенерированные клиенты и тесты находятся в solana-foundation/dvp под лицензией MIT. Программа написана на Pinocchio в режиме no_std; клиенты генерируются из IDL с помощью Codama. Сообщения об уязвимостях отправляются через ссылку на security advisory репозитория согласно его SECURITY.md.

Программа прошла аудит в Cantina.

См. также

Is this page helpful?