Skip to content

crazyministr/nft-offer

Repository files navigation

NFT Offer Smart Contract

A universal NFT offer contract written in Tolk for the TON blockchain. Supports offers in both TON and Jettons (e.g. USDT).

Project structure

  • contracts/ — Tolk smart contract source code
  • wrappers/ — TypeScript wrapper classes for contract interaction
  • tests/ — Jest tests using @ton/sandbox
  • scripts/ — deployment scripts

Build & Test

npx blueprint build
npx blueprint test

{
  "hash": "f74dafa31cd3b8615c3cf67cdc464ac5b1708f87d6aad2dd32a3c6259a26bf5e",
  "hashBase64": "902voxzTuGFcPPZ83EZKxbFwj4fWqtLdMqPGJZomv14=",
  "hex": "b5ee9c72410217010005f9000114ff00f4a413f4bcf2c80b01020120021602014803150202ce0413020120051204913e247c903b5134348034c7f4c7f4c7fe923e923e923e80353d0142b5cb08cc98120b2497c2f835cb09f6bb7e8f38c0b5cb080a271b2338c0b5cb08e6c5a13938c0b5cb08000000072006070d0f00c2f8925006c705f89224c705b1f2e3e8296ef2d44d09d0fa48fa50d70bff016ef2e44e058308d718d74c20f900541027f910f2e44cd0fa48fa483002c8fa52fa5415cbffc908c8ca0017cb1f15cb1f13cb1ffa52fa52fa5201fa0212ccf400c9ed5402fe36296eb382100ee6b280821005f5e100e30406d33ffa4830f89226c7052bb33c500bb0f8975008be17b027f823bcb08ec453588e315bf8926d6dc8cf917f30f45215cb3f5230fa5213fa5212f400cf842012f400c9c8cf858812fa5271cf0b6eccc98040fb00ede3ba727fed118aed41edf101f2ffe010575f0732f8926d6d080c02fe20d0fa48d310fa50d310d15250820186a0a9845253820186a0a9845352a121a120c200f2e3ec2e6eb38e342ed0fa4831fa50d3ff31d1547be0045611f00121c20098545a2254166ff001923430e221c200216eb3b09528512cf001925f03e2e30e6d716dc8cf917f30f45219cb3f5250fa521bfa52f4005009fa0215f400c9090b01c48b650726f6669748c8cf9000000002cec9c8cf850852f0fa5258fa0271cf0b6accc971fb0020c2008e298ba4d61726b6574206665658c8cf9000000002cec9c8cf850815fa5201fa0271cf0b6a13ccc971fb00923032e220c200226eb3b0915be30d0a004a8b7526f79616c74798c8cf9000000002cec9c8cf850813fa5201fa0271cf0b6accc971fb00008ac8cf85885230fa5271cf0b6eccc98306fb00f823c8cf8327cf0b1f375257cb1f355255cb1f355215fa52315230fa52335213fa523121fa023121cf14315210f40031c9ed540058c8cf917f30f45215cb3f5230fa5213fa5212f400cf842012f400c9c8cf858812fa5271cf0b6eccc98040fb0001fed33ffa00fa48302c6e2cb18e356c9333f8926d6dc8cf903e29fa9615cb3f5003fa025240fa5214fa52f400cf8420f400c9c8cf858812fa5271cf0b6eccc98042fb00e02cd0fa4831fa50d3ff31d1206ef89258c705b3b15316c705b3b1e302303112a008c8ca0017cb1f15cb1f13cb1ffa52fa52fa5258fa02ccf400c9ed540e006a6c9333f8926d6dc8cf903e29fa9615cb3f5003fa025240fa5214fa52f400cf8420f400c9c8cf858812fa5271cf0b6eccc98042fb0002f88e5e3028f2d3e9f89223c705f89226c705b1f2e3e8f89225c7058e3df8978d06d3d999995c8818d85b98d95b08189e481b585c9ad95d1c1b1858d960c8cf9000000002cec9c8cf85085270fa5258fa0271cf0b6accc971fb00de5508f0025f0ae0d72c2000000004e302375f043334d72c200000115ce302840ff2f0101100a08b663616e63656c8c7058e1228f2d3e9f89223c705f2e3e85508f0025f0ae028f2d3e9f89223c705f2e3e8296ef2e44df89712a008c8ca0017cb1f15cb1f13cb1ffa52fa52fa5258fa02ccf400c9ed540070f89258c70512b0f2e3e8d74cd0d30722c3008e14810258f8235341a1bc04f82302a0b913b0f2d3eb9132e2208020b0f2d3ea01d74c01fb00007b0870406497c17820829896801b5b7233e40f8a7ea585f2cfd4013e80853e94be94bd0033e10804bd00327233e16204fe94807e809c73c2dab3325c7ec02001f5439286eb38e4728d0fa4831fa5030206e91308e386d6dc88bc0f8a7ea500000000000000008cf1625fa025260fa525260fa5212f400cf8420f400c9c8cf858812fa5271cf0b6eccc98306fb00e28e146dc8cf85085240fa5270cf0b6ef400c98306fb00e27fc8cf8329cf0b1f28cf0b1f27cf0b1f5260fa5252508140028fa525240fa5223fa0222cf1452a0f400c9ed540900aba04eefda89a1a401a63fa63fa63ff491f491f491f401a9e80a03a1f491a621f4a1ae1620a6a104030d415308a6c704030d415308a6e342d824434262418401e5c7d8dada4edd226f34b60ba1f491f4a060a0cdc4aa0b0058f230ed44d0d200d31fd31fd31ffa48fa48fa48fa00d4f40529f2d3e9f82328bc96f800f0025f0ae0840ff2f0bf6063e4"
}

Contract overview

Storage layout

Field Type Description
isComplete bool Whether the offer is finalized (accepted, cancelled, or expired)
createdAt uint32 Offer creation timestamp
validUntil uint32 Expiration timestamp
acceptedAt uint32 Timestamp when the offer was accepted (0 if not accepted)
marketplaceAddress address Marketplace contract address
nftAddress address Target NFT address
offerOwnerAddress address Offer creator address
fullPrice coins Total offer price
feesData Cell<FeesData> Fee configuration (ref cell)
jettonData Cell<JettonData>? Jetton configuration (optional ref cell, null for TON offers)

FeesData:

Field Type Description
marketplaceFeeAddress address Address that receives marketplace fee
marketplacePercent uint17 Marketplace fee, where 100000 = 100% (e.g. 5000 = 5%)
royaltyAddress address? Royalty recipient (optional)
royaltyPercent uint17 Royalty fee, same format

JettonData:

Field Type Description
jettonMasterAddress address Jetton master contract address
jettonWalletAddress address? Contract's jetton wallet (set after deploy via DeployJettonOp)
publicKey uint256 Marketplace public key for signature verification

Fee calculation

Fees use a denominator of 100000:

  • marketplacePercent = 5000 means 5%
  • royaltyPercent = 2500 means 2.5%
  • profitPrice = fullPrice - marketplaceFee - royaltyAmount

Deployment

TON offer

Two options:

  1. With fullPrice in stateInit: set fullPrice in initial data and send a message with opcode 0x664c0905 (DeployTonOp).
  2. With top-up (recommended): set fullPrice = 0 in initial data, deploy with op = 0 (comment message) and attach TON. The attached value is added to fullPrice.

Jetton offer

  1. Set fullPrice in stateInit (can be 0 or the target amount).
  2. Set jettonData with jettonMasterAddress, jettonWalletAddress = null, and publicKey (marketplace Ed25519 public key).
  3. After deployment, send DeployJettonOp to set jettonWalletAddress and marketplaceAddress.
  4. If fullPrice = 0, fund the contract via JettonTransferNotification from the offer owner.

Important: Do NOT fund the contract with jettons via notification in the same deployment flow. There is no guarantee that the JettonTransferNotification will arrive after the contract is deployed and DeployJettonOp is processed. Use notifications only for top-ups after the contract is fully initialized. To deploy with a pre-set price, set fullPrice directly in stateInit and transfer jettons to the contract's jetton wallet without notification.


Internal messages (opcodes)

DeployTonOp0x664c0905

Deploys the contract with TON. No payload processing — the contract simply accepts the message.

DeployJettonOp0xfb5dbf47

Sets jettonWalletAddress and marketplaceAddress with signature verification.

Field Type Description
signature bits512 Ed25519 signature of the payload cell hash
payload cell Ref cell containing jettonWalletAddress + marketplaceAddress

Access: marketplace or offer owner.

The payload cell is signed by the marketplace backend's private key. This prevents address tampering even if the transaction sender is trusted.

OwnershipAssigned0x05138d91 (TEP-62)

Received when an NFT is transferred to this contract. If sender is the target NFT and the offer is active, triggers acceptOffer:

  • TON offer: sends profit, royalty, and marketplace fee as separate TON transfers, then transfers NFT to the offer owner.
  • Jetton offer: sends profit, royalty, and marketplace fee as jetton transfers via the contract's jetton wallet, then transfers NFT to the offer owner.

If conditions are not met (wrong NFT, already completed, insufficient msg_value, or offer expired — validUntil <= now), the NFT is returned to the previous owner.

Minimum msg_value: 0.1 TON for TON offers, 0.25 TON for jetton offers.

JettonTransferNotification0x7362d09c (TEP-74)

Top-up the offer with jettons. Validates:

  • Contract is a jetton offer and not completed
  • Sender is the contract's jetton wallet
  • Jetton sender is the offer owner

If validation fails, jettons are returned. Otherwise, fullPrice is increased by the received amount.

CancelOfferOp0x00000003

Cancel the offer. Body: just the opcode (32 bits).

Access: offer owner or marketplace.

  • If sent by marketplace: the attached msg_value is returned to the marketplace as a cancel fee.
  • Jetton offers: jettons are returned to the owner via a single jetton transfer with mode 128 (all balance). Excess TON returns to the owner via responseDestination.
  • TON offers: entire contract balance is sent to the owner.

CommentMessage0x00000000

  • Text "cancel": cancels the offer. Access: offer owner only. Marketplace must use CancelOfferOp.
  • Any other text: top-up the offer with attached TON. Access: offer owner only. Only for TON offers (throws NotJettonOffer for jetton offers).

EmergencyMessage0x0000022B

Allows the marketplace to send an arbitrary message from the contract. Only available after the offer is completed (isComplete = true).

Field Type Description
payload cell Ref cell: uint8 mode + cell message

Restrictions:

  • Sender must be the marketplace.
  • Mode 32 (destroy) is forbidden.
  • If acceptedAt != 0, the message is rejected within ±10 minutes of acceptedAt.

External messages

Anyone can send an external message after validUntil to expire the offer:

  • Jetton offers: jettons are returned to the owner.
  • TON offers: balance is sent to the owner.
  • isComplete is set to true.

Rejected with 0xffff if the offer has not expired yet.

Getter: getJettonOfferData

Returns all contract data as a tuple. Works for both TON and Jetton offers.

Return value Type
isComplete bool
createdAt uint32
validUntil uint32
acceptedAt uint32
marketplaceAddress address
nftAddress address
offerOwnerAddress address
fullPrice coins
marketplaceFeeAddress address
marketplacePercent uint17
royaltyAddress address?
royaltyPercent uint17
profitPrice int
jettonMasterAddress address?
jettonWalletAddress address?

Throws ProfitTooLow if the calculated profit is not positive.

Error codes

Code Name Description
1000 Forbidden Sender is not authorized
1001 OfferCompleted Offer is already finalized
1002 DestroyModeNotAllowed Emergency mode 32 is not allowed
1003 TooCloseToSwap Emergency within ±10 min of acceptedAt
1004 ProfitTooLow Calculated profit is not positive
1100 InvalidSignature DeployJettonOp signature verification failed
1101 NotJettonOffer Operation requires a jetton offer
1102 JettonWalletAlreadySet Jetton wallet address is already initialized

Off-chain validation

The following checks must be performed off-chain before deploying or interacting with the contract:

  1. Address workchain: all addresses (nftAddress, offerOwnerAddress, marketplaceAddress, marketplaceFeeAddress, royaltyAddress) must belong to the correct workchain (typically 0).
  2. Jetton wallet address: for jetton offers, compute the correct jetton wallet address for the offer contract by calling get_wallet_address on the jetton master. Do not trust user-provided values.
  3. Jetton master verification: verify that jettonMasterAddress is the actual master of the expected jetton (e.g. USDT). The contract does not verify the jetton type on-chain.
  4. NFT ownership: verify the NFT exists and belongs to the expected owner before creating the offer.
  5. Fee sanity: ensure marketplacePercent + royaltyPercent < 100000 and that profitPrice > 0.
  6. validUntil: must be a reasonable future timestamp.
  7. fullPrice: Ensure the corresponding jetton or TON amount has actually been transferred to the offer's smart contract.
  8. Signature payload: the DeployJettonOp payload must be signed by the private key corresponding to publicKey in JettonData. Sign the hash of the cell containing jettonWalletAddress + marketplaceAddress.

NFT Offer — Смарт-контракт

Универсальный контракт оффера на NFT, написанный на Tolk для блокчейна TON. Поддерживает офферы как в TON, так и в жетонах (например, USDT).

Структура проекта

  • contracts/ — исходный код смарт-контракта на Tolk
  • wrappers/ — TypeScript-обёртки для взаимодействия с контрактом
  • tests/ — Jest-тесты с использованием @ton/sandbox
  • scripts/ — скрипты деплоя

Сборка и тесты

npx blueprint build
npx blueprint test

Обзор контракта

Структура хранилища (Storage)

Поле Тип Описание
isComplete bool Завершён ли оффер (принят, отменён или истёк)
createdAt uint32 Временная метка создания
validUntil uint32 Временная метка истечения
acceptedAt uint32 Временная метка принятия (0, если не принят)
marketplaceAddress address Адрес контракта маркетплейса
nftAddress address Адрес NFT
offerOwnerAddress address Адрес создателя оффера
fullPrice coins Полная цена оффера
feesData Cell<FeesData> Конфигурация комиссий (ref cell)
jettonData Cell<JettonData>? Конфигурация жетона (опционально, null для TON-офферов)

FeesData:

Поле Тип Описание
marketplaceFeeAddress address Адрес получателя комиссии маркетплейса
marketplacePercent uint17 Комиссия маркетплейса, 100000 = 100% (например, 5000 = 5%)
royaltyAddress address? Получатель роялти (опционально)
royaltyPercent uint17 Роялти, тот же формат

JettonData:

Поле Тип Описание
jettonMasterAddress address Адрес мастер-контракта жетона
jettonWalletAddress address? Жетон-кошелёк контракта (устанавливается после деплоя через DeployJettonOp)
publicKey uint256 Публичный ключ маркетплейса для проверки подписи

Расчёт комиссий

Знаменатель: 100000:

  • marketplacePercent = 5000 означает 5%
  • royaltyPercent = 2500 означает 2.5%
  • profitPrice = fullPrice - marketplaceFee - royaltyAmount

Деплой

TON-оффер

Два варианта:

  1. С fullPrice в stateInit: указать fullPrice в начальных данных и отправить сообщение с опкодом 0x664c0905 (DeployTonOp).
  2. С пополнением (рекомендуется): указать fullPrice = 0, задеплоить с op = 0 (комментарий) и приложить TON. Приложенная сумма добавляется к fullPrice.

Жетон-оффер

  1. Указать fullPrice в stateInit (может быть 0 или целевая сумма).
  2. Указать jettonData с jettonMasterAddress, jettonWalletAddress = null и publicKey (Ed25519 публичный ключ маркетплейса).
  3. После деплоя отправить DeployJettonOp для установки jettonWalletAddress и marketplaceAddress.
  4. Если fullPrice = 0, пополнить контракт через JettonTransferNotification от владельца оффера.

Важно: НЕ отправляйте жетоны на контракт через нотификацию в рамках деплоя. Нет гарантии, что JettonTransferNotification придёт после того, как контракт будет задеплоен и обработан DeployJettonOp. Используйте нотификации только для пополнения уже полностью инициализированного контракта. Чтобы задеплоить с заданной ценой, укажите fullPrice прямо в stateInit и переведите жетоны на жетон-кошелёк контракта без нотификации.


Внутренние сообщения (опкоды)

DeployTonOp0x664c0905

Деплоит контракт с TON. Payload не обрабатывается — контракт просто принимает сообщение.

DeployJettonOp0xfb5dbf47

Устанавливает jettonWalletAddress и marketplaceAddress с проверкой подписи.

Поле Тип Описание
signature bits512 Ed25519 подпись хеша payload cell
payload cell Ref cell: jettonWalletAddress + marketplaceAddress

Доступ: маркетплейс или владелец оффера.

Payload подписывается приватным ключом бэкенда маркетплейса. Это предотвращает подмену адресов даже если отправитель транзакции доверенный.

OwnershipAssigned0x05138d91 (TEP-62)

Приходит, когда NFT переводится на этот контракт. Если отправитель — целевая NFT и оффер активен, запускается acceptOffer:

  • TON-оффер: отправляет профит, роялти и комиссию маркетплейса отдельными TON-переводами, затем переводит NFT владельцу оффера.
  • Жетон-оффер: отправляет профит, роялти и комиссию маркетплейса жетон-переводами через жетон-кошелёк контракта, затем переводит NFT.

Если условия не выполнены (не та NFT, уже завершён, недостаточный msg_value, или оффер истёк — validUntil <= now), NFT возвращается предыдущему владельцу.

Минимальный msg_value: 0.1 TON для TON-офферов, 0.25 TON для жетон-офферов.

JettonTransferNotification0x7362d09c (TEP-74)

Пополнение оффера жетонами. Проверяет:

  • Контракт — жетон-оффер и не завершён
  • Отправитель — жетон-кошелёк контракта
  • Отправитель жетонов — владелец оффера

При ошибке валидации жетоны возвращаются. Иначе fullPrice увеличивается на полученную сумму.

CancelOfferOp0x00000003

Отмена оффера. Body: только опкод (32 бита).

Доступ: владелец оффера или маркетплейс.

  • Если от маркетплейса: приложенный msg_value возвращается маркетплейсу как комиссия за отмену.
  • Жетон-офферы: жетоны возвращаются владельцу одной транзакцией с mode 128 (весь баланс). Излишки TON возвращаются через responseDestination.
  • TON-офферы: весь баланс контракта отправляется владельцу.

CommentMessage0x00000000

  • Текст "cancel": отменяет оффер. Доступ: только владелец оффера. Маркетплейс должен использовать CancelOfferOp.
  • Любой другой текст: пополнение оффера приложенными TON. Доступ: только владелец оффера. Только для TON-офферов (бросает NotJettonOffer для жетон-офферов).

EmergencyMessage0x0000022B

Позволяет маркетплейсу отправить произвольное сообщение от имени контракта. Доступно только после завершения оффера (isComplete = true).

Поле Тип Описание
payload cell Ref cell: uint8 mode + cell message

Ограничения:

  • Отправитель — только маркетплейс.
  • Mode 32 (уничтожение) запрещён.
  • Если acceptedAt != 0, сообщение отклоняется в пределах ±10 минут от acceptedAt.

Внешние сообщения

Любой может отправить внешнее сообщение после validUntil для истечения оффера:

  • Жетон-офферы: жетоны возвращаются владельцу.
  • TON-офферы: баланс отправляется владельцу.
  • isComplete устанавливается в true.

Отклоняется с 0xffff, если оффер ещё не истёк.

Геттер: getJettonOfferData

Возвращает все данные контракта как tuple. Работает и для TON, и для жетон-офферов.

Возвращаемое значение Тип
isComplete bool
createdAt uint32
validUntil uint32
acceptedAt uint32
marketplaceAddress address
nftAddress address
offerOwnerAddress address
fullPrice coins
marketplaceFeeAddress address
marketplacePercent uint17
royaltyAddress address?
royaltyPercent uint17
profitPrice int
jettonMasterAddress address?
jettonWalletAddress address?

Бросает ProfitTooLow, если вычисленный профит не положителен.

Коды ошибок

Код Имя Описание
1000 Forbidden Отправитель не авторизован
1001 OfferCompleted Оффер уже завершён
1002 DestroyModeNotAllowed Emergency mode 32 запрещён
1003 TooCloseToSwap Emergency в пределах ±10 мин от acceptedAt
1004 ProfitTooLow Вычисленный профит не положителен
1100 InvalidSignature Проверка подписи DeployJettonOp не пройдена
1101 NotJettonOffer Операция требует жетон-оффер
1102 JettonWalletAlreadySet Адрес жетон-кошелька уже установлен

Валидация off-chain

Следующие проверки обязательны перед деплоем или взаимодействием с контрактом:

  1. Воркчейн адресов: все адреса (nftAddress, offerOwnerAddress, marketplaceAddress, marketplaceFeeAddress, royaltyAddress) должны принадлежать правильному воркчейну (обычно 0).
  2. Адрес жетон-кошелька: для жетон-офферов вычислить правильный адрес жетон-кошелька контракта, вызвав get_wallet_address на жетон-мастере. Не доверять значениям от пользователя.
  3. Верификация жетон-мастера: убедиться, что jettonMasterAddress — это мастер нужного жетона (например, USDT). Контракт не проверяет тип жетона on-chain.
  4. Владение NFT: убедиться, что NFT существует и принадлежит ожидаемому владельцу.
  5. Корректность комиссий: marketplacePercent + royaltyPercent < 100000 и profitPrice > 0.
  6. validUntil: должен быть разумной будущей временной меткой.
  7. fullPrice: Убедиться, что соответствующее количество жетонов или TON действительно переведено на контракта оффера.
  8. Подпись payload: payload DeployJettonOp должен быть подписан приватным ключом, соответствующим publicKey в JettonData. Подписывается хеш cell, содержащей jettonWalletAddress + marketplaceAddress.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors