Skip to content
ChainConnect

Комиссии (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

Система поддерживает гибкую настройку комиссий с приоритетами.

Порядок применения

Порядок применения 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 детерминистически вычисляется из адреса токена с помощью функции прокси:

typescript
const bridgeTokenFeeAddress = await proxy.methods
  .getExpectedTokenFeeAddress({ token: tokenAddress })
  .call();

Проверка существования контракта

После получения адреса нужно проверить, существует ли контракт в блокчейне:

typescript
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 токена

Если контракт не существует, его нужно задеплоить через прокси:

typescript
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');
}

Полный пример: проверка и деплой

typescript
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 перед установкой комиссий для токена:

  1. Получите адрес через getExpectedTokenFeeAddress()
  2. Проверьте state контракта в блокчейне
  3. Если не задеплоен — вызовите deployTokenFee()
  4. Дождитесь подтверждения деплоя
  5. Только после этого устанавливайте fee через setTvmTokenFee()

Управление Fee

Права доступа

Все функции управления fee доступны только владельцу контракта (owner).

Установка дефолтной Fee

solidity
setTvmDefaultFeeNumerator(incoming, outgoing)
ПараметрОписание
incomingNumerator для входящих трансферов (0-10000)
outgoingNumerator для исходящих трансферов (0-10000)

Пример: incoming=1000, outgoing=500 означает 1% на входе и 0.5% на выходе.

Установка Fee для токена

solidity
setTvmTokenFee(token, incoming, outgoing)
ПараметрОписание
tokenАдрес токена
incomingNumerator для входящих (0-10000)
outgoingNumerator для исходящих (0-10000)

Важно — адрес токена

  • Для Native Proxy: адрес Jetton Wallet прокси (адрес кошелька токена, принадлежащего прокси)
  • Для Alien Proxy: адрес Jetton Minter (рут-контракт токена, не смерженного)

Вывод накопленных Fee

solidity
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 - 100900 токенов
Incoming fee (сеть B)900 × 10%90 токенов
Получено900 - 90810 токенов

Отключение комиссии для токена

Если нужно отключить комиссию для конкретного токена при наличии дефолтной fee:

  • Установить setTvmTokenFee(token, 0, 0)
  • Токен будет использовать нулевую комиссию, игнорируя дефолтную

Ошибки и Edge Cases

Таблица ошибок

КодНазваниеОписание
Numerator > 10000 → fee не устанавливается (silent ignore)
1000NOT_OWNERВызов onlyOwner функции не от owner
2713LOW_MSG_VALUEНедостаточно газа для deployTokenFee

Edge Cases

Fee больше суммы трансфера

Теоретически невозможно благодаря ограничению в 10%. Даже при максимальных настройках (10% + 10%) получатель получит минимум 81% от суммы.

BridgeTokenFee не задеплоен

Если контракт BridgeTokenFee не существует:

  • Комиссия всё равно вычитается из суммы трансфера
  • Вызов accumulateFee() идёт с bounce: false
  • Транзакция не откатится, но fee будет потеряна

Рекомендация

Всегда деплоить BridgeTokenFee перед включением комиссий для токена.

ChainConnect Bridge Documentation