Комиссии (Fee)
Что такое Fee
Fee (комиссия) — это процент от суммы трансфера, который удерживается бриджом при переводе токенов между сетями.
Назначение
- Покрытие операционных расходов бриджа
- Экономический стимул для поддержания инфраструктуры
Кто получает Fee
Комиссии получает владелец (owner) прокси-контракта. Owner может в любой момент вывести накопленные комиссии через функцию withdrawTokenFee().
Когда взимается Fee
Fee взимается при TVM→TVM трансферах и бывает двух типов:
| Тип Fee | Когда взимается | Описание |
|---|---|---|
| Incoming | При получении токенов | Комиссия удерживается при mint/transfer получателю |
| Outgoing | При отправке токенов | Комиссия удерживается при burn/lock токенов |
Схема взимания комиссии
Пользователь отправляет 1000 токенов
↓
[Сеть отправления]
Outgoing fee = 1000 × 1% = 10 токенов
В событие записывается: 990 токенов
↓
[Сеть назначения]
Incoming fee = 990 × 1% = 9.9 токенов
Получатель получает: 980.1 токеновВажно
При TVM→TVM трансферах fee может взиматься на обеих сторонах:
- Outgoing fee — в сети отправления
- Incoming fee — в сети назначения
Расчёт комиссии
Формула
fee = amount × numerator / 100000Где:
amount— сумма трансфера в минимальных единицах токенаnumerator— числитель комиссии (от 0 до 10000)FEE_DENOMINATOR = 100_000— константа-знаменатель
Ограничения
| Параметр | Значение |
|---|---|
| Максимальная комиссия | 10% (numerator ≤ 10000) |
| Минимальная комиссия | 0% (numerator = 0) |
| Точность | 0.001% (шаг = 1/100000) |
| Тип комиссии | Только процентная |
Иерархия Fee
Система поддерживает гибкую настройку комиссий с приоритетами.
Порядок применения

Уровни настройки
1. Fee не задана
Если ни токен-специфичная, ни дефолтная fee не установлены — комиссия равна 0.
2. Дефолтная Fee
Применяется ко всем токенам, для которых не задана индивидуальная fee.
- Устанавливается через
setTvmDefaultFeeNumerator() - Удобна для массовой настройки
3. Токен-специфичная Fee
Переопределяет дефолтную fee для конкретного токена.
- Устанавливается через
setTvmTokenFee() - Удаляется через
deleteTvmTokenFee()— после этого токен использует дефолтную fee
Контракт BridgeTokenFee
Для накопления комиссий необходим отдельный контракт BridgeTokenFee для каждого токена.
| Тип прокси | Деплой BridgeTokenFee | Адрес токена для derive |
|---|---|---|
| Alien Proxy | Автоматически при создании токена | Адрес Jetton Minter |
| Native Proxy | Вручную перед включением fee | Адрес Jetton Wallet прокси |
Для Native токенов
BridgeTokenFee для native токенов не деплоится автоматически. Владелец прокси должен задеплоить контракт вручную перед включением комиссий.
Если BridgeTokenFee не задеплоен: комиссия рассчитывается и вычитается из суммы, но не накапливается (теряется).
Проверка и деплой BridgeTokenFee
Derive адреса контракта
Адрес BridgeTokenFee детерминистически вычисляется из адреса токена с помощью функции прокси:
const bridgeTokenFeeAddress = await proxy.methods
.getExpectedTokenFeeAddress({ token: tokenAddress })
.call();Проверка существования контракта
После получения адреса нужно проверить, существует ли контракт в блокчейне:
import { TonClient, Address } from '@ton/ton';
async function isBridgeTokenFeeDeployed(
client: TonClient,
bridgeTokenFeeAddress: string
): Promise<boolean> {
const address = Address.parse(bridgeTokenFeeAddress);
const state = await client.getContractState(address);
// Контракт существует, если state не пустой и есть код
return state.state === 'active';
}
// Использование
const isDeployed = await isBridgeTokenFeeDeployed(client, bridgeTokenFeeAddress);
console.log(`BridgeTokenFee deployed: ${isDeployed}`);Деплой BridgeTokenFee для Native токена
Если контракт не существует, его нужно задеплоить через прокси:
import { Address, toNano, beginCell } from '@ton/ton';
async function deployBridgeTokenFee(
client: TonClient,
proxyAddress: string,
jettonWalletAddress: string, // Адрес Jetton Wallet прокси
senderWallet: any
): Promise<void> {
const proxy = Address.parse(proxyAddress);
const tokenAddress = Address.parse(jettonWalletAddress);
// Вызов deployTokenFee на прокси-контракте
// Требует минимум 0.5 TON для деплоя
const payload = beginCell()
.storeUint(0x12345678, 32) // op: deployTokenFee
.storeAddress(tokenAddress)
.endCell();
await senderWallet.sendTransfer({
to: proxy,
value: toNano('0.5'),
body: payload,
});
console.log('deployTokenFee transaction sent');
}Полный пример: проверка и деплой
import { TonClient, Address, toNano } from '@ton/ton';
interface BridgeTokenFeeCheckResult {
tokenAddress: string;
bridgeTokenFeeAddress: string;
isDeployed: boolean;
}
async function checkAndDeployBridgeTokenFee(
client: TonClient,
proxyContract: any,
jettonWalletAddress: string,
senderWallet?: any
): Promise<BridgeTokenFeeCheckResult> {
// 1. Derive адрес BridgeTokenFee
const { value0: bridgeTokenFeeAddress } = await proxyContract.methods
.getExpectedTokenFeeAddress({ token: jettonWalletAddress })
.call();
console.log(`Token address: ${jettonWalletAddress}`);
console.log(`BridgeTokenFee address: ${bridgeTokenFeeAddress}`);
// 2. Проверить существование контракта
const address = Address.parse(bridgeTokenFeeAddress);
const state = await client.getContractState(address);
const isDeployed = state.state === 'active';
console.log(`Contract deployed: ${isDeployed}`);
// 3. Деплой если нужно и передан senderWallet
if (!isDeployed && senderWallet) {
console.log('Deploying BridgeTokenFee...');
await proxyContract.methods
.deployTokenFee({ token: jettonWalletAddress })
.send({
from: senderWallet.address,
amount: toNano('0.5'),
});
console.log('Deploy transaction sent. Wait for confirmation...');
}
return {
tokenAddress: jettonWalletAddress,
bridgeTokenFeeAddress,
isDeployed,
};
}
// Пример использования
async function main() {
const client = new TonClient({
endpoint: 'https://toncenter.com/api/v2/jsonRPC',
});
// Адрес Jetton Wallet, принадлежащего Native Proxy
const proxyJettonWallet = '0:abc123...';
const result = await checkAndDeployBridgeTokenFee(
client,
proxyContract,
proxyJettonWallet
);
if (!result.isDeployed) {
console.warn(
'BridgeTokenFee not deployed! Fee will be lost until deployed.'
);
}
}Рекомендация
Всегда проверяйте наличие BridgeTokenFee перед установкой комиссий для токена:
- Получите адрес через
getExpectedTokenFeeAddress() - Проверьте state контракта в блокчейне
- Если не задеплоен — вызовите
deployTokenFee() - Дождитесь подтверждения деплоя
- Только после этого устанавливайте fee через
setTvmTokenFee()
Управление Fee
Права доступа
Все функции управления fee доступны только владельцу контракта (owner).
Установка дефолтной Fee
setTvmDefaultFeeNumerator(incoming, outgoing)| Параметр | Описание |
|---|---|
incoming | Numerator для входящих трансферов (0-10000) |
outgoing | Numerator для исходящих трансферов (0-10000) |
Пример: incoming=1000, outgoing=500 означает 1% на входе и 0.5% на выходе.
Установка Fee для токена
setTvmTokenFee(token, incoming, outgoing)| Параметр | Описание |
|---|---|
token | Адрес токена |
incoming | Numerator для входящих (0-10000) |
outgoing | Numerator для исходящих (0-10000) |
Важно — адрес токена
- Для Native Proxy: адрес Jetton Wallet прокси (адрес кошелька токена, принадлежащего прокси)
- Для Alien Proxy: адрес Jetton Minter (рут-контракт токена, не смерженного)
Вывод накопленных Fee
withdrawTokenFee(token, recipient)Contract Reference
Функции чтения (public view)
| Функция | Описание |
|---|---|
getTvmDefaultFee() | Получить дефолтную fee |
getTvmFees() | Получить все токен-специфичные fee |
getTvmTokenFee(token) | Получить fee для конкретного токена |
getExpectedTokenFeeAddress(token) | Получить адрес контракта BridgeTokenFee |
Функции управления (onlyOwner)
| Функция | Описание |
|---|---|
setTvmDefaultFeeNumerator(incoming, outgoing) | Установить дефолтную fee |
setTvmTokenFee(token, incoming, outgoing) | Установить fee для токена |
deleteTvmTokenFee(token) | Удалить fee для токена |
withdrawTokenFee(token, recipient) | Вывести накопленные fee |
События
| Событие | Когда эмитится |
|---|---|
IncomingFeeTaken(fee, token, msgHash) | При удержании incoming fee |
OutgoingFeeTaken(fee, token) | При удержании outgoing fee |
Примеры
Трансфер с комиссией 10%
Настройки:
- Incoming fee: 10% (numerator = 10000)
- Outgoing fee: 10% (numerator = 10000)
Расчёт для Native → Alien трансфера 1000 токенов:
| Этап | Расчёт | Результат |
|---|---|---|
| Отправлено | — | 1000 токенов |
| Outgoing fee (сеть A) | 1000 × 10% | 100 токенов |
| В событии | 1000 - 100 | 900 токенов |
| Incoming fee (сеть B) | 900 × 10% | 90 токенов |
| Получено | 900 - 90 | 810 токенов |
Отключение комиссии для токена
Если нужно отключить комиссию для конкретного токена при наличии дефолтной fee:
- Установить
setTvmTokenFee(token, 0, 0) - Токен будет использовать нулевую комиссию, игнорируя дефолтную
Ошибки и Edge Cases
Таблица ошибок
| Код | Название | Описание |
|---|---|---|
| — | — | Numerator > 10000 → fee не устанавливается (silent ignore) |
| 1000 | NOT_OWNER | Вызов onlyOwner функции не от owner |
| 2713 | LOW_MSG_VALUE | Недостаточно газа для deployTokenFee |
Edge Cases
Fee больше суммы трансфера
Теоретически невозможно благодаря ограничению в 10%. Даже при максимальных настройках (10% + 10%) получатель получит минимум 81% от суммы.
BridgeTokenFee не задеплоен
Если контракт BridgeTokenFee не существует:
- Комиссия всё равно вычитается из суммы трансфера
- Вызов
accumulateFee()идёт сbounce: false - Транзакция не откатится, но fee будет потеряна
Рекомендация
Всегда деплоить BridgeTokenFee перед включением комиссий для токена.