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 ​

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
Sent—1000 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