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

Программа DvP — стандартная программа для поставки против платежа в 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 не действует, а расчётный орган не может перенаправить средства. Каждая сделка зависит от обновляемой программы и её органа обновления.
  • Шаблон с делегированием не зависит от программы и работает с минтами, у которых есть 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, например мультисиг-хранилище. Мультисиг-аккаунт SPL Token или program account не может быть стороной: Create завершается ошибкой PartyNotSignerCapable
Сторона денежной ногиuser_b, поставляет mint_bСобственный перевод для пополнения; Reclaim, Reject, RecoverТо же требование
Расчётный органТретий адрес, указанный при созданииSettle, CancelЕдинственный подписант, который может провести расчёт. Он не может перенаправить средства, которые фиксируются при создании. При расчёте или отмене получает rent закрытых аккаунтов
Плательщик rentТот, кто подписывает CreateDvpCreateВносит rent (депозит в SOL, который поддерживает аккаунт открытым) за запись, tombstone nonce и оба эскроу. В качестве получателя rent не записывается: rent закрытых аккаунтов получает тот, кто закрывает сделку

ID программы

dvp34bdbcEm4f4FCUjGV4mDAkDshaQR4LkK8fdcsyZq
КластерПрограммаОрган обновленияПоследний 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]). Суммы и время хранятся внутри аккаунта и не влияют на адрес, поэтому их нужно считывать, а не выводить. Небольшой маркерный аккаунт — tombstone nonce по адресу ["nonce", swap_dvp] — создаётся вместе со сделкой и никогда не закрывается, поэтому каждую комбинацию сторон, минтов, органа и nonce можно использовать только для одной сделки.

ПолеТипЗначение
bumpu8Bump PDA
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, её tombstone nonce и оба эскроу-аккаунта. Токены не перемещает
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 с исполняемым расчётным органом
13NonceAlreadyUsedCreate с повторным использованием (seeds, nonce), для которого уже есть tombstone
14ExpiryTooFarInFutureCreate со сроком действия более чем на год вперёд
15RefStringTooLongCreate со ссылкой длиннее 64 байт
16DvpStillOpenRecover, пока сделка ещё открыта; используйте Reclaim
17DvpNeverCreatedRecover с seed-значениями, для которых нет tombstone
18EscrowPreloadedWithLamportsCreate, когда эскроу не в нативном токене удерживает lamports сверх минимума для освобождения от rent
19SettlementDestinationIsSwapDvpCreate с адресом назначения расчёта, совпадающим с записью сделки
20SwapDvpPreloadedWithLamportsCreate, когда адрес записи удерживает lamports сверх резерва rent
21RecipientAtaMismatchToken account получателя или возврата больше не принадлежит ожидаемому кошельку и mint
22PartyNotSignerCapableCreate со стороной, которая не является неисполняемым аккаунтом, принадлежащим System Program
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 не меняют исходные (raw) суммы, но их ставка или множитель могут измениться между созданием и расчётом сделки, из-за чего изменится отображение согласованных сумм. 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 включает permissionless thaw, либо поручая freeze authority разморозить его напрямую. Замороженное эскроу также блокирует Settle, Reclaim, Cancel, Reject и Recover на этой ноге, поэтому freeze authority, а для mint с хуком — hook authority, является доверенной стороной на всё время жизни сделки. Путь с замороженным эскроу описан здесь и не используется в devnet-сниппетах.

Ограничения

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

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

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

CreateDvp не требует разрешений: подписывает только плательщик, а стороны и settlement 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 г.

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

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

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

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

Связанные материалы

Is this page helpful?