Overview
This example demonstrates how to send ERC-20 tokens cross-chain using deBridge Protocol, including permit support for gasless approvals.
Script Location : examples/src/sendScripts/sendERC20.ts
Quick Start
yarn ts-node examples/src/sendScripts/sendERC20.ts
Configuration
DEBRIDGEGATE_ADDRESS = 0x... # DeBridgeGate contract
SENDER_PRIVATE_KEY = 0x... # Your private key
TOKEN_ADDRESS = 0x... # ERC-20 token to send
CHAIN_ID_FROM = 1 # Source chain (Ethereum)
CHAIN_ID_TO = 56 # Destination chain (BSC)
RECEIVER_ADDRESS = 0x... # Recipient
AMOUNT = 100 # Amount in token units
Standard Approval Method
import { ethers } from 'ethers' ;
import { DeBridgeGate__factory , IERC20__factory } from '../typechain-types' ;
async function sendERC20 () {
const provider = new ethers . providers . JsonRpcProvider ( process . env . RPC_URL );
const wallet = new ethers . Wallet ( process . env . SENDER_PRIVATE_KEY ! , provider );
// Connect to contracts
const token = IERC20__factory . connect ( process . env . TOKEN_ADDRESS ! , wallet );
const debridgeGate = DeBridgeGate__factory . connect (
process . env . DEBRIDGEGATE_ADDRESS ! ,
wallet
);
const amount = ethers . utils . parseUnits (
process . env . AMOUNT ! ,
await token . decimals ()
);
// Step 1: Approve DeBridgeGate to spend tokens
console . log ( 'Approving tokens...' );
const approveTx = await token . approve ( debridgeGate . address , amount );
await approveTx . wait ();
console . log ( 'Approved' );
// Step 2: Send tokens
console . log ( 'Sending tokens...' );
const protocolFee = await debridgeGate . globalFixedNativeFee ();
const tx = await debridgeGate . send (
token . address , // ERC-20 token
amount ,
process . env . CHAIN_ID_TO ! ,
ethers . utils . defaultAbiCoder . encode ([ 'address' ], [ process . env . RECEIVER_ADDRESS ]),
'0x' , // no permit
false , // pay fee in native token
0 , // no referral
'0x' , // no autoParams
{ value: protocolFee } // protocol fee in ETH/BNB
);
console . log ( 'Transaction hash:' , tx . hash );
const receipt = await tx . wait ();
const sentEvent = receipt . events ?. find ( e => e . event === 'Sent' );
console . log ( 'Submission ID:' , sentEvent ?. args ?. submissionId );
}
sendERC20 (). catch ( console . error );
Using Permit (Gasless Approval)
For tokens supporting EIP-2612 permit:
import { ethers } from 'ethers' ;
async function sendWithPermit () {
// ... setup ...
// Get permit parameters
const deadline = Math . floor ( Date . now () / 1000 ) + 3600 ; // 1 hour
const nonce = await token . nonces ( wallet . address );
// Sign permit
const domain = {
name: await token . name (),
version: '1' ,
chainId: await wallet . getChainId (),
verifyingContract: token . address
};
const types = {
Permit: [
{ name: 'owner' , type: 'address' },
{ name: 'spender' , type: 'address' },
{ name: 'value' , type: 'uint256' },
{ name: 'nonce' , type: 'uint256' },
{ name: 'deadline' , type: 'uint256' }
]
};
const value = {
owner: wallet . address ,
spender: debridgeGate . address ,
value: amount ,
nonce: nonce ,
deadline: deadline
};
const signature = await wallet . _signTypedData ( domain , types , value );
const { v , r , s } = ethers . utils . splitSignature ( signature );
// Encode permit data
const permitData = ethers . utils . defaultAbiCoder . encode (
[ 'uint256' , 'uint8' , 'bytes32' , 'bytes32' ],
[ deadline , v , r , s ]
);
// Send with permit (no separate approval transaction needed!)
const tx = await debridgeGate . send (
token . address ,
amount ,
chainIdTo ,
receiverBytes ,
permitData , // Permit signature
false ,
0 ,
'0x' ,
{ value: protocolFee }
);
console . log ( 'Sent with permit:' , tx . hash );
}
Using permit saves one transaction (no separate approval needed) and can be signed off-chain.
Paying Fees in Asset
Instead of paying protocol fees in native tokens, you can deduct fees from the transferred amount:
Fee Deduction from Transfer
const tx = await debridgeGate . send (
token . address ,
amount ,
chainIdTo ,
receiverBytes ,
'0x' ,
true , // useAssetFee = true
0 ,
'0x'
// No value needed - fee deducted from amount
);
With useAssetFee = true:
Protocol fee is converted to token amount and deducted
Receiver gets amount - fees on destination
No native token needed in your wallet
Checking Token Support
Before sending, verify the token is supported:
const debridgeId = await debridgeGate . getDebridgeId (
chainIdFrom ,
ethers . utils . hexZeroPad ( tokenAddress , 32 )
);
const debridgeInfo = await debridgeGate . getDebridge ( debridgeId );
if ( ! debridgeInfo . exist ) {
console . error ( 'Token not supported' );
return ;
}
if ( amount . gt ( debridgeInfo . maxAmount )) {
console . error ( 'Amount exceeds maximum' );
return ;
}
console . log ( 'Token supported, max amount:' , debridgeInfo . maxAmount );
With Automatic Execution
Auto-Execute on Destination
const executionFee = ethers . utils . parseEther ( '0.01' );
const flags = 0 ; // Or use Flags library
const autoParams = ethers . utils . defaultAbiCoder . encode (
[ 'tuple(uint256,uint256,bytes,bytes)' ],
[[
executionFee ,
flags ,
receiverBytes , // Fallback address
'0x' // No additional calldata
]]
);
const tx = await debridgeGate . send (
token . address ,
amount ,
chainIdTo ,
receiverBytes ,
'0x' ,
false ,
0 ,
autoParams ,
{ value: protocolFee . add ( executionFee ) } // Include execution fee
);
What Happens on Destination
Depending on the token and destination chain:
Native Chain → Other Chain : Wrapped deToken is minted (e.g., deUSDC)
Other Chain → Native Chain : Original token is unlocked
Between Non-Native Chains : deToken is transferred or bridged
All deTokens are 1:1 backed by collateral locked on the token’s native chain.
Common Issues
The approval wasn’t sufficient or expired. Solution : Check allowance and re-approve:const allowance = await token . allowance ( wallet . address , debridgeGate . address );
if ( allowance . lt ( amount )) {
await token . approve ( debridgeGate . address , ethers . constants . MaxUint256 );
}
The token hasn’t been added to deBridge for the destination chain. Solution :
Check supported tokens in the deBridge app
Contact deBridge team to add the token
Use a different supported token
The transfer amount exceeds the configured maxAmount for the asset. Solution :
Split into multiple smaller transfers
Check maxAmount with getDebridge()
Sending Native Tokens Transfer ETH, BNB, MATIC
Cross-Chain Swaps Swap tokens across chains