Sending Assets
This guide covers everything you need to know about sending assets across chains using the deBridge Protocol.Overview
Thesend() function is your main entry point for cross-chain asset transfers:
contracts/interfaces/IDeBridgeGate.sol:138-147
function send(
address _tokenAddress, // Token to bridge (address(0) for native)
uint256 _amount, // Amount to bridge
uint256 _chainIdTo, // Destination chain ID
bytes memory _receiver, // Receiver address on destination
bytes memory _permitEnvelope, // Optional: EIP-2612 permit
bool _useAssetFee, // Pay fees in asset vs native token
uint32 _referralCode, // Optional: referral tracking
bytes calldata _autoParams // Optional: auto-execution params
) external payable returns (bytes32 submissionId);
Basic ERC20 Transfer
The simplest case: send ERC20 tokens to another chain.Implementation
Example: Basic ERC20 Bridge
pragma solidity ^0.8.7;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
interface IDeBridgeGate {
function send(
address _tokenAddress,
uint256 _amount,
uint256 _chainIdTo,
bytes memory _receiver,
bytes memory _permitEnvelope,
bool _useAssetFee,
uint32 _referralCode,
bytes calldata _autoParams
) external payable returns (bytes32 submissionId);
function globalFixedNativeFee() external view returns (uint256);
}
contract SimpleBridge {
IDeBridgeGate public immutable deBridgeGate;
event AssetBridged(
address indexed sender,
address indexed token,
uint256 amount,
uint256 destChainId,
bytes32 submissionId
);
constructor(address _deBridgeGate) {
deBridgeGate = IDeBridgeGate(_deBridgeGate);
}
function bridgeToken(
address token,
uint256 amount,
uint256 destinationChainId,
address receiver
) external payable returns (bytes32 submissionId) {
// 1. Transfer tokens from user
IERC20(token).transferFrom(msg.sender, address(this), amount);
// 2. Approve DeBridgeGate
IERC20(token).approve(address(deBridgeGate), amount);
// 3. Bridge tokens
submissionId = deBridgeGate.send{value: msg.value}(
token, // Token address
amount, // Amount
destinationChainId, // Destination chain
abi.encodePacked(receiver), // Receiver address as bytes
"", // No permit
false, // Pay fee in native token
0, // No referral
"" // No auto-execution
);
emit AssetBridged(msg.sender, token, amount, destinationChainId, submissionId);
}
}
Usage
Example: Using SimpleBridge
const token = await ethers.getContractAt("IERC20", tokenAddress);
const bridge = await ethers.getContractAt("SimpleBridge", bridgeAddress);
// Approve bridge to spend tokens
await token.approve(bridge.address, amount);
// Get required fee
const deBridgeGate = await ethers.getContractAt("IDeBridgeGate", deBridgeGateAddress);
const fee = await deBridgeGate.globalFixedNativeFee();
// Bridge tokens
const tx = await bridge.bridgeToken(
tokenAddress,
amount,
destinationChainId,
receiverAddress,
{ value: fee }
);
const receipt = await tx.wait();
const event = receipt.events?.find(e => e.event === 'AssetBridged');
const submissionId = event?.args?.submissionId;
console.log("Bridged! Submission ID:", submissionId);
Native Token Transfer
Sending native tokens (ETH, BNB, MATIC, etc.) is even simpler.Implementation
Example: Native Token Bridge
function bridgeNative(
uint256 destinationChainId,
address receiver
) external payable returns (bytes32 submissionId) {
require(msg.value > 0, "Must send native tokens");
// For native tokens, pass address(0) as token address
// The entire msg.value is used (including fees)
submissionId = deBridgeGate.send{value: msg.value}(
address(0), // address(0) = native token
msg.value, // Amount (will be wrapped to WETH)
destinationChainId,
abi.encodePacked(receiver),
"",
true, // useAssetFee must be true for native
0,
""
);
emit NativeBridged(msg.sender, msg.value, destinationChainId, submissionId);
}
For native tokens:
- Use
address(0)as_tokenAddress - Set
_useAssetFee = true - Fees are deducted from
msg.value - Tokens are wrapped to WETH internally
Using EIP-2612 Permit
Avoid separate approval transaction using permit signatures.Implementation
Example: Bridge with Permit
function bridgeWithPermit(
address token,
uint256 amount,
uint256 destinationChainId,
address receiver,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external payable returns (bytes32 submissionId) {
// Encode permit envelope
bytes memory permitEnvelope = abi.encodePacked(
amount, // 32 bytes
deadline, // 32 bytes
r, // 32 bytes
s, // 32 bytes
v // 1 byte
);
// No need for separate approve - permit is called internally
submissionId = deBridgeGate.send{value: msg.value}(
token,
amount,
destinationChainId,
abi.encodePacked(receiver),
permitEnvelope, // Include permit
false,
0,
""
);
}
Generating Permit Signature
Example: Generate Permit Signature
const { signERC2612Permit } = require('eth-permit');
async function generatePermitSignature(
token,
owner,
spender,
value,
deadline
) {
const result = await signERC2612Permit(
ethers.provider,
token.address,
owner,
spender,
value.toString(),
deadline
);
return {
deadline: result.deadline,
v: result.v,
r: result.r,
s: result.s
};
}
// Usage
const permit = await generatePermitSignature(
token,
userAddress,
deBridgeGateAddress,
amount,
Math.floor(Date.now() / 1000) + 3600 // 1 hour from now
);
await bridge.bridgeWithPermit(
tokenAddress,
amount,
destinationChainId,
receiverAddress,
permit.deadline,
permit.v,
permit.r,
permit.s,
{ value: fee }
);
Fee Handling
Querying Fees
Example: Fee Calculator
function calculateTotalFees(
address token,
uint256 amount,
uint256 destChainId,
bool useAssetFee
) public view returns (
uint256 fixedFee,
uint256 transferFee,
uint256 totalFee
) {
// Get chain config
ChainSupportInfo memory chainConfig = deBridgeGate.getChainToConfig(destChainId);
// Fixed fee
fixedFee = chainConfig.fixedNativeFee == 0
? deBridgeGate.globalFixedNativeFee()
: chainConfig.fixedNativeFee;
// Transfer fee
uint16 transferFeeBps = chainConfig.transferFeeBps == 0
? deBridgeGate.globalTransferFeeBps()
: chainConfig.transferFeeBps;
if (useAssetFee) {
// Deducted from amount
transferFee = (amount - fixedFee) * transferFeeBps / 10000;
totalFee = fixedFee + transferFee;
} else {
// Paid in native token
transferFee = amount * transferFeeBps / 10000;
totalFee = fixedFee + transferFee;
}
}
Using Asset Fees
Example: Use Asset Fee
function bridgeWithAssetFee(
address token,
uint256 amount,
uint256 destinationChainId,
address receiver
) external returns (bytes32 submissionId) {
require(token != address(0), "Use bridgeNative for native tokens");
IERC20(token).transferFrom(msg.sender, address(this), amount);
IERC20(token).approve(address(deBridgeGate), amount);
// Fees will be deducted from the token amount
submissionId = deBridgeGate.send(
token,
amount,
destinationChainId,
abi.encodePacked(receiver),
"",
true, // useAssetFee = true
0,
""
);
}
When using
useAssetFee = true, the receiver gets amount - fees. Always account for this in your UI and calculations.Advanced: Referral Tracking
Track referrals using the referral code parameter:Example: Referral System
contract ReferralBridge {
mapping(uint32 => address) public referralCodes;
mapping(address => uint256) public referralEarnings;
uint32 private nextReferralCode = 1;
function registerReferral(address referrer) external returns (uint32 code) {
code = nextReferralCode++;
referralCodes[code] = referrer;
}
function bridgeWithReferral(
address token,
uint256 amount,
uint256 destinationChainId,
address receiver,
uint32 referralCode
) external payable returns (bytes32 submissionId) {
IERC20(token).transferFrom(msg.sender, address(this), amount);
IERC20(token).approve(address(deBridgeGate), amount);
submissionId = deBridgeGate.send{value: msg.value}(
token,
amount,
destinationChainId,
abi.encodePacked(receiver),
"",
false,
referralCode, // Track referral
""
);
// Track referral earnings (simplified)
address referrer = referralCodes[referralCode];
if (referrer != address(0)) {
uint256 commission = msg.value / 100; // 1% commission
referralEarnings[referrer] += commission;
}
}
}
Handling Transfer Events
Monitor theSent event to track transfers:
event Sent(
bytes32 submissionId,
bytes32 indexed debridgeId,
uint256 amount,
bytes receiver,
uint256 nonce,
uint256 indexed chainIdTo,
uint32 referralCode,
FeeParams feeParams,
bytes autoParams,
address nativeSender
);
Example: Event Listener
Example: Monitor Transfers
const deBridgeGate = await ethers.getContractAt("IDeBridgeGate", deBridgeGateAddress);
// Listen for Sent events
deBridgeGate.on("Sent", (
submissionId,
debridgeId,
amount,
receiver,
nonce,
chainIdTo,
referralCode,
feeParams,
autoParams,
nativeSender,
event
) => {
console.log("Transfer initiated:", {
submissionId,
amount: amount.toString(),
from: nativeSender,
to: ethers.utils.hexlify(receiver),
destChain: chainIdTo.toString(),
txHash: event.transactionHash
});
// Store in database, update UI, etc.
});
// Listen for Claimed events on destination chain
destinationDeBridgeGate.on("Claimed", (
submissionId,
debridgeId,
amount,
receiver,
nonce,
chainIdFrom,
autoParams,
isNativeToken,
event
) => {
console.log("Transfer completed:", {
submissionId,
amount: amount.toString(),
receiver,
txHash: event.transactionHash
});
});
Error Handling
Common Errors
TransferAmountNotCoverFees
TransferAmountNotCoverFees
Sent amount doesn’t cover protocol fees.Solution: Query fees first and ensure sufficient amount:
uint256 totalFees = calculateTotalFees(token, amount, destChain, useAssetFee);
require(amount > totalFees || msg.value >= totalFees, "Insufficient for fees");
WrongChainTo
WrongChainTo
Destination chain is not supported.Solution: Check chain support before transfer:
ChainSupportInfo memory info = deBridgeGate.getChainToConfig(destChain);
require(info.isSupported, "Chain not supported");
TransferAmountTooHigh
TransferAmountTooHigh
Amount exceeds maximum allowed for this asset.Solution: Check asset limits:
bytes32 debridgeId = deBridgeGate.getDebridgeId(chainId, tokenAddress);
DebridgeInfo memory info = deBridgeGate.getDebridge(debridgeId);
require(amount <= info.maxAmount, "Amount too high");
NotSupportedFixedFee
NotSupportedFixedFee
Asset fee not configured when
useAssetFee = true.Solution: Use native fee or check asset fee availability:if (useAssetFee) {
uint256 assetFee = deBridgeGate.getDebridgeChainAssetFixedFee(debridgeId, destChain);
require(assetFee > 0, "Asset fee not supported");
}
Robust Implementation
Example: Error-Resistant Bridge
contract SafeBridge {
error InsufficientBalance(uint256 required, uint256 available);
error UnsupportedChain(uint256 chainId);
error AmountTooHigh(uint256 amount, uint256 max);
error InsufficientFees(uint256 required, uint256 provided);
function safeBridge(
address token,
uint256 amount,
uint256 destChainId,
address receiver
) external payable returns (bytes32 submissionId) {
// 1. Validate chain support
ChainSupportInfo memory chainInfo = deBridgeGate.getChainToConfig(destChainId);
if (!chainInfo.isSupported) revert UnsupportedChain(destChainId);
// 2. Validate user balance
uint256 balance = token == address(0)
? msg.sender.balance
: IERC20(token).balanceOf(msg.sender);
if (balance < amount) revert InsufficientBalance(amount, balance);
// 3. Validate amount against limits
bytes32 debridgeId = deBridgeGate.getDebridgeId(
block.chainid,
token == address(0) ? address(weth) : token
);
DebridgeInfo memory debridgeInfo = deBridgeGate.getDebridge(debridgeId);
if (amount > debridgeInfo.maxAmount) {
revert AmountTooHigh(amount, debridgeInfo.maxAmount);
}
// 4. Validate fees
(uint256 fixedFee, uint256 transferFee, uint256 totalFee) =
calculateTotalFees(token, amount, destChainId, token == address(0));
if (token == address(0)) {
if (msg.value < amount) {
revert InsufficientFees(amount, msg.value);
}
} else {
if (msg.value < fixedFee) {
revert InsufficientFees(fixedFee, msg.value);
}
}
// 5. Execute transfer
if (token != address(0)) {
IERC20(token).transferFrom(msg.sender, address(this), amount);
IERC20(token).approve(address(deBridgeGate), amount);
}
submissionId = deBridgeGate.send{value: msg.value}(
token,
token == address(0) ? msg.value : amount,
destChainId,
abi.encodePacked(receiver),
"",
token == address(0),
0,
""
);
emit SafeBridgeCompleted(msg.sender, token, amount, destChainId, submissionId);
}
}
Testing
Unit Test Example
Example: Test Bridge Function
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("SimpleBridge", function () {
let bridge, deBridgeGate, token, owner, user;
beforeEach(async function () {
[owner, user] = await ethers.getSigners();
// Deploy mocks
const MockDeBridgeGate = await ethers.getContractFactory("MockDeBridgeGate");
deBridgeGate = await MockDeBridgeGate.deploy();
const Token = await ethers.getContractFactory("MockERC20");
token = await Token.deploy("Test", "TST");
await token.mint(user.address, ethers.utils.parseEther("1000"));
const Bridge = await ethers.getContractFactory("SimpleBridge");
bridge = await Bridge.deploy(deBridgeGate.address);
});
it("Should bridge tokens successfully", async function () {
const amount = ethers.utils.parseEther("100");
const destChain = 56;
const fee = ethers.utils.parseEther("0.01");
// Approve bridge
await token.connect(user).approve(bridge.address, amount);
// Bridge
const tx = await bridge.connect(user).bridgeToken(
token.address,
amount,
destChain,
user.address,
{ value: fee }
);
// Verify event
await expect(tx)
.to.emit(bridge, "AssetBridged")
.withArgs(user.address, token.address, amount, destChain, anything());
// Verify token transfer
expect(await token.balanceOf(bridge.address)).to.equal(0);
expect(await token.balanceOf(deBridgeGate.address)).to.equal(amount);
});
it("Should revert if insufficient fee", async function () {
const amount = ethers.utils.parseEther("100");
await token.connect(user).approve(bridge.address, amount);
await expect(
bridge.connect(user).bridgeToken(
token.address,
amount,
56,
user.address,
{ value: 0 } // No fee
)
).to.be.reverted;
});
});
Best Practices
Query Fees First
Always query current fees before initiating transfer to ensure user has sufficient balance
Validate Inputs
Validate all inputs (chain support, token balance, amounts) before calling send()
Emit Events
Emit custom events for easier tracking and debugging
Handle Errors
Implement proper error handling with clear, actionable error messages
For production applications, consider implementing a relayer service to automatically claim transfers on the destination chain, providing better UX for your users.
Next Steps
Cross-Chain Calls
Learn to execute contract calls with your transfers
Fee Structure
Deep dive into fee calculation and optimization
Transfers Concept
Understand the underlying transfer mechanism
BridgeAppBase
Advanced pattern for complex applications