Skip to content

Fees

What is Fee

Fee is a percentage of the transfer amount that is charged by the bridge when transferring tokens between networks.

Purpose

  • Covering bridge operational costs
  • Economic incentive for maintaining infrastructure

Who Receives Fee

Fees are received by the proxy contract owner. The owner can withdraw accumulated fees at any time via withdrawTokenFee() function.

When Fee is Charged

Fee is charged during TVM→TVM transfers and comes in two types:

Fee TypeWhen ChargedDescription
IncomingWhen receiving tokensFee charged on mint/transfer to recipient
OutgoingWhen sending tokensFee charged on burn/lock of tokens

Fee Charging Scheme

User sends 1000 tokens

[Source Network]
Outgoing fee = 1000 × 1% = 10 tokens
Recorded in event: 990 tokens

[Destination Network]
Incoming fee = 990 × 1% = 9.9 tokens
Recipient receives: 980.1 tokens

Important

During TVM→TVM transfers, fee may be charged on both sides:

  • Outgoing fee — in the source network
  • Incoming fee — in the destination network

Fee Calculation

Formula

fee = amount × numerator / 100000

Where:

  • amount — transfer amount in minimum token units
  • numerator — fee numerator (from 0 to 10000)
  • FEE_DENOMINATOR = 100_000 — constant denominator

Constraints

ParameterValue
Maximum fee10% (numerator ≤ 10000)
Minimum fee0% (numerator = 0)
Precision0.001% (step = 1/100000)
Fee typePercentage only

Fee Hierarchy

The system supports flexible fee configuration with priorities.

Application Order

Fee Application Order

Configuration Levels

1. Fee Not Set

If neither token-specific nor default fee is set — fee equals 0.

2. Default Fee

Applied to all tokens that don't have individual fee set.

  • Set via setTvmDefaultFeeNumerator()
  • Convenient for bulk configuration

3. Token-specific Fee

Overrides default fee for a specific token.

  • Set via setTvmTokenFee()
  • Removed via deleteTvmTokenFee() — after this, token uses default fee

BridgeTokenFee Contract

For accumulating fees, a separate BridgeTokenFee contract is needed for each token.

Proxy TypeBridgeTokenFee DeployToken Address for Derive
Alien ProxyAutomatic on token creationJetton Minter address
Native ProxyManual before enabling feeProxy's Jetton Wallet address

For Native Tokens

BridgeTokenFee for native tokens is not deployed automatically. The proxy owner must deploy the contract manually before enabling fees.

If BridgeTokenFee is not deployed: fee is calculated and deducted from amount, but not accumulated (lost).


Checking and Deploying BridgeTokenFee

Deriving Contract Address

BridgeTokenFee address is deterministically computed from token address using proxy function:

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

Checking Contract Existence

After getting the address, verify that the contract exists in blockchain:

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);

  // Contract exists if state is not empty and has code
  return state.state === 'active';
}

// Usage
const isDeployed = await isBridgeTokenFeeDeployed(client, bridgeTokenFeeAddress);
console.log(`BridgeTokenFee deployed: ${isDeployed}`);

Deploying BridgeTokenFee for Native Token

If contract doesn't exist, deploy it via proxy:

typescript
import { Address, toNano, beginCell } from '@ton/ton';

async function deployBridgeTokenFee(
  client: TonClient,
  proxyAddress: string,
  jettonWalletAddress: string, // Proxy's Jetton Wallet address
  senderWallet: any
): Promise<void> {
  const proxy = Address.parse(proxyAddress);
  const tokenAddress = Address.parse(jettonWalletAddress);

  // Call deployTokenFee on proxy contract
  // Requires minimum 0.5 TON for deployment
  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');
}

Full Example: Check and Deploy

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 address
  const { value0: bridgeTokenFeeAddress } = await proxyContract.methods
    .getExpectedTokenFeeAddress({ token: jettonWalletAddress })
    .call();

  console.log(`Token address: ${jettonWalletAddress}`);
  console.log(`BridgeTokenFee address: ${bridgeTokenFeeAddress}`);

  // 2. Check contract existence
  const address = Address.parse(bridgeTokenFeeAddress);
  const state = await client.getContractState(address);
  const isDeployed = state.state === 'active';

  console.log(`Contract deployed: ${isDeployed}`);

  // 3. Deploy if needed and senderWallet provided
  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,
  };
}

// Usage example
async function main() {
  const client = new TonClient({
    endpoint: 'https://toncenter.com/api/v2/jsonRPC',
  });

  // Jetton Wallet address belonging to 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.'
    );
  }
}

Recommendation

Always check for BridgeTokenFee existence before setting token fees:

  1. Get address via getExpectedTokenFeeAddress()
  2. Check contract state in blockchain
  3. If not deployed — call deployTokenFee()
  4. Wait for deployment confirmation
  5. Only then set fee via setTvmTokenFee()

Fee Management

Access Rights

All fee management functions are available only to contract owner (owner).

Setting Default Fee

solidity
setTvmDefaultFeeNumerator(incoming, outgoing)
ParameterDescription
incomingNumerator for incoming transfers (0-10000)
outgoingNumerator for outgoing transfers (0-10000)

Example: incoming=1000, outgoing=500 means 1% on incoming and 0.5% on outgoing.

Setting Token Fee

solidity
setTvmTokenFee(token, incoming, outgoing)
ParameterDescription
tokenToken address
incomingIncoming numerator (0-10000)
outgoingOutgoing numerator (0-10000)

Important — Token Address

  • For Native Proxy: proxy's Jetton Wallet address (token wallet address owned by proxy)
  • For Alien Proxy: Jetton Minter address (token root contract, not merged)

Withdrawing Accumulated Fee

solidity
withdrawTokenFee(token, recipient)

Contract Reference

Read Functions (public view)

FunctionDescription
getTvmDefaultFee()Get default fee
getTvmFees()Get all token-specific fees
getTvmTokenFee(token)Get fee for specific token
getExpectedTokenFeeAddress(token)Get BridgeTokenFee contract address

Management Functions (onlyOwner)

FunctionDescription
setTvmDefaultFeeNumerator(incoming, outgoing)Set default fee
setTvmTokenFee(token, incoming, outgoing)Set token fee
deleteTvmTokenFee(token)Delete token fee
withdrawTokenFee(token, recipient)Withdraw accumulated fees

Events

EventWhen Emitted
IncomingFeeTaken(fee, token, msgHash)When incoming fee is charged
OutgoingFeeTaken(fee, token)When outgoing fee is charged

Examples

Transfer with 10% Fee

Settings:

  • Incoming fee: 10% (numerator = 10000)
  • Outgoing fee: 10% (numerator = 10000)

Calculation for Native → Alien transfer of 1000 tokens:

StageCalculationResult
Sent1000 tokens
Outgoing fee (network A)1000 × 10%100 tokens
In event1000 - 100900 tokens
Incoming fee (network B)900 × 10%90 tokens
Received900 - 90810 tokens

Disabling Fee for Token

If you need to disable fee for a specific token when default fee exists:

  • Set setTvmTokenFee(token, 0, 0)
  • Token will use zero fee, ignoring default

Errors and Edge Cases

Error Table

CodeNameDescription
Numerator > 10000 → fee not set (silent ignore)
1000NOT_OWNERCalling onlyOwner function not from owner
2713LOW_MSG_VALUEInsufficient gas for deployTokenFee

Edge Cases

Fee Greater Than Transfer Amount

Theoretically impossible due to 10% limit. Even at maximum settings (10% + 10%) recipient gets minimum 81% of amount.

BridgeTokenFee Not Deployed

If BridgeTokenFee contract doesn't exist:

  • Fee is still deducted from transfer amount
  • accumulateFee() call goes with bounce: false
  • Transaction won't revert, but fee will be lost

Recommendation

Always deploy BridgeTokenFee before enabling token fees.

ChainConnect Bridge Documentation