๐งญ BNB Smart Chain Testnet โ Polygon Amoy
Introduction
Cross-chain bridges are essential infrastructure in the evolving blockchain ecosystem. They enable tokens and data to move across independent networks โ allowing true interoperability in the Web3 world.
In this blog, Iโll break down how to build a cross-chain token bridge between the BNB Smart Chain Testnet and Polygon Amoy Testnet. The project uses Solidity smart contracts, a WebSocket-based event listener, a transaction queue with nonce tracking, and a frontend for users to interact with the bridge.
โ๏ธ Smart Contracts: Token Locking & Releasing
The bridge uses two smart contracts โ one on each chain โ to manage token custody and emit events during transfers.
Key Features
- Token Locking: Locks tokens on the source chain when a transfer is initiated.
- Token Releasing: Releases tokens on the destination chain after verification.
- Nonce Verification: Prevents replay attacks by tracking unique nonces per transaction.
- Token Whitelisting: Only pre-approved tokens can be bridged.
- Amount Limits: Defines minimum and maximum bridge amounts.
- Access Control: Only the bridge owner can execute the release function.
๐ Code Snippet
function bridge(IERC20 _tokenAddress, uint256 _amount) public {
// Validate token and amount
if (!whitelistedTokens[_tokenAddress]) revert BridgeContract__Token_Not_Whitelisted();
if (_amount > maxAmounts[_tokenAddress] && maxAmounts[_tokenAddress] > 0)
revert BridgeContract__Amount_Too_Large();
if (_amount < minAmounts[_tokenAddress] && minAmounts[_tokenAddress] > 0)
revert BridgeContract__Amount_Too_Small();
// Check allowance
if (_tokenAddress.allowance(_msgSender(), address(this)) < _amount)
revert BridgeContract__Insufficient_Allowance();
// Transfer tokens from sender to bridge
bool success = _tokenAddress.transferFrom(_msgSender(), address(this), _amount);
if (!success) revert BridgeContract__Transaction_Failed();
// Emit bridge event
emit Bridge(_tokenAddress, _amount, _msgSender());
}
(https://github.com/sumana10/Bridge-Contract/blob/main/contract/src/BridgeContract.sol)
๐ก WebSocket Listener: Real-Time Blockchain Monitoring
Instead of polling blocks, the system uses WebSocket connections to listen for TokenLocked
events in real time from both chains.
Key Features
- Dual Network Monitoring: Monitors both BNB and Polygon chains concurrently.
- Event-Based Architecture: Processes transactions as soon as they're emitted.
- Block Tracking: Records the last processed block to prevent missing events after reconnection.
- Historical Event Replay: On restart, reprocesses missed events from the last saved block.
๐ Code Snippet
const bridgeListener = (tokenAddress, amount, sender, event) => {
console.log(`Bridge event detected on ${network}: Token ${tokenAddress} Amount ${amount}`);
const txhash = event.log.transactionHash.toLowerCase();
console.log(`Adding job to queue for new event: ${txhash}`);
bridgeQueue.add({
txhash,
tokenAddress: tokenAddress.toString(),
amount: amount.toString(),
sender: sender.toString(),
network,
}).catch(error => {
console.error(`Error adding job to queue:`, error);
});
prisma.networkStatus.update({
where: { network },
data: { lastProcessedBlock: event.log.blockNumber },
}).catch(error => {
console.error(`Error updating last processed block:`, error);
});
};
(https://github.com/sumana10/Bridge-Contract/blob/main/indexer/src/index.ts)
๐งต Bridge Queue: Ordered Transaction Processing
The queue service ensures that events are processed sequentially, securely, and only once.
Key Features
- Nonce Management: Prevents duplicate or replayed transactions.
- Sequential Execution: Guarantees events are processed in order.
- Transaction Validation: Verifies if a transaction has already been handled.
- Gas Optimization: Dynamically adjusts gas based on current network conditions.
๐ Code Snippet
bridgeQueue.process(async (job) => {
const { txhash, tokenAddress, amount, sender, network } = job.data;
console.log(`Processing job for txhash ${txhash} on ${network}`);
try {
console.log(`Checking if transaction ${txhash} exists in database`);
let transaction = await prisma.transactionData.findUnique({
where: { txHash: txhash },
});
if (!transaction) {
console.log(`Transaction ${txhash} not found, creating new record`);
console.log(`Getting nonce for ${network}`);
const nonceRecord = await prisma.nonce.upsert({
where: { network },
update: { nonce: { increment: 1 } },
create: { network, nonce: 1 },
});
console.log(`Using nonce ${nonceRecord.nonce} for ${network}`);
transaction = await prisma.transactionData.create({
data: {
txHash: txhash,
tokenAddress,
amount,
sender,
network,
isDone: false,
nonce: nonceRecord.nonce,
},
});
console.log(`Created transaction record for ${txhash}`);
} else {
console.log(`Transaction ${txhash} already exists in database`);
}
if (transaction.isDone) {
console.log(`Transaction ${txhash} already processed, skipping`);
return { success: true, message: "Transaction already processed" };
}
console.log(`Executing transfer for transaction ${txhash}`);
await transferToken(network === "BNB", amount, sender, transaction.nonce);
console.log(`Transfer completed, updating transaction status for ${txhash}`);
await prisma.transactionData.update({
where: { txHash: txhash },
data: { isDone: true },
});
console.log(`Transaction ${txhash} marked as done`);
return { success: true };
} catch (error) {
console.error(`Error processing job for txhash ${txhash}:`, error);
throw error;
}
});
๐๏ธ PostgreSQL Database: State Persistence
The bridge uses PostgreSQL to store event history, transaction data, and the current state of each chain.
Key Tables
-
TransactionData
: Stores hash, amount, token, sender, chain, and nonce. -
NetworkStatus
: Tracks the last processed block per chain. -
Nonce
: Ensures nonce uniqueness per network.
๐ผ๏ธ Frontend: Bridge UI for Users
The frontend allows users to connect their wallets, select a token, approve it, and initiate cross-chain transfers.
Key Features
- Network Detection: Auto-detects the connected chain.
- Token Approval Flow: Prompts users to approve tokens if allowance is insufficient.
- Transaction Submission: Initiates the lock transaction on the source chain.
- Real-Time Feedback: Shows transaction progress and final status.
๐ Security Features
Security is baked into every part of the system:
- Replay Protection via nonce verification
- Whitelisted Tokens Only to prevent malicious tokens
- Minimum/Maximum Amount Limits
- Owner-Only Release Logic
- Reliable Event Tracking across node restarts
- Gas Adjustment based on network conditions
๐ฝ๏ธ Demo and Repository
- ๐ง Explore the Code: [https://github.com/sumana10/Bridge-Contract/]
- ๐ฌ Watch the Demo: [https://www.youtube.com/watch?v=5SE1Bx1tve8]
Youโll find setup instructions, deployment scripts, and detailed documentation in the repo.
๐ง Conclusion
This cross-chain bridge project demonstrates a clean, modular approach to transferring tokens between EVM-compatible blockchains. By combining smart contracts, event-driven services, a transaction queue, and a PostgreSQL database, the system achieves reliable, end-to-end cross-chain communication.
You can extend this project to support additional chains, add NFT bridging, or even decentralize the bridge using a validator network or oracle integration.
Top comments (0)