## Week 14 Update: Building a Custom Token Bridge This week at BlocfuseLab, we built a custom token bridge that enables cross-chain communication, this allows tokens to move between blockchain networks without relying on pre-built infrastructure like Chainlink CCIP. ### The Idea Behind the Bridge A token bridge enables users to send tokens from one blockchain to another. Instead of a literal transfer (which isn’t possible across isolated chains), the bridge locks tokens on one chain and mints an equivalent amount on the other. When the user wants to return their tokens, they burn them on the destination chain and unlock them on the origin chain. ### Our Bridge Flow * A user locks tokens on the Sepolia network. * The contract emits a BridgeInitiated event. * An off-chain relayer detects the event and triggers the mint function on the Base chain. * The Base contract mints tokens to the user’s address. * When bridging back, the user burns their tokens on Base and the relayer releases the original tokens on Sepolia. ### Key Smart Contracts We created two main contracts — BridgeSepolia for locking and unlocking on Sepolia, and BridgeBase for minting and burning on Base. 🔗 **Base Chain Contract: BridgeBase.sol** ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import "./BridgeBaseAbstract.sol"; import "./library/Errors.sol"; import "./library/Events.sol"; import "./tokens/Bbl.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; contract BridgeBase is BridgeBaseAbstract, ReentrancyGuard { Bbl public immutable token; uint256 public conversionRate; constructor(address _token, uint256 _conversionRate) { if (_token == address(0)) revert Error.InvalidTokenAddress(); token = Bbl(_token); conversionRate = _conversionRate; } function setConversionRate(uint256 _conversionRate) external { conversionRate = _conversionRate; } function mintTokens(address to, uint256 amount, bytes32 sourceTx) external nonReentrant { if (amount == 0) revert Error.InsufficientAmount(); if (to == address(0)) revert Error.InvalidRecipient(); uint256 finalAmount = (amount * conversionRate) / 1e18; token.mint(to, finalAmount); emit Event.BridgeFinalized(to, finalAmount, sourceTx); } function burnTokens(uint256 amount, string calldata targetChain) external nonReentrant { if (amount == 0) revert Error.InsufficientAmount(); if (token.balanceOf(msg.sender) < amount) revert Error.InsufficientBalance(); token.burnFromBridge(msg.sender, amount); emit Event.BridgeInitiated( msg.sender, amount, targetChain, keccak256(abi.encodePacked(msg.sender, amount, block.timestamp)) ); } } ``` 🔗 **Sepolia Chain Contract: BridgeSepolia.sol** ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "./BridgeBaseAbstract.sol"; import "./library/Errors.sol"; import "./library/Events.sol"; import "./tokens/Bbl.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; contract BridgeSepolia is BridgeBaseAbstract, ReentrancyGuard { struct BridgeOperation { address user; uint256 amount; uint40 timestamp; bool finalized; } mapping(bytes32 => BridgeOperation) public bridgeOperations; uint256 public constant refundTime = 1 days; Bbl public immutable token; uint256 public conversionRate; constructor(address _token, uint256 _conversionRate) { if (_token == address(0)) revert Error.InvalidTokenAddress(); token = Bbl(_token); conversionRate = _conversionRate; } function setConversionRate(uint256 _conversionRate) external { conversionRate = _conversionRate; } function lockTokens(uint256 amount, string calldata targetChain) external nonReentrant { if (amount == 0) revert Error.InsufficientAmount(); if (token.balanceOf(msg.sender) < amount) revert Error.InsufficientBalance(); if (token.allowance(msg.sender, address(this)) < amount) revert Error.InsufficientAllowance(); token.transferFrom(msg.sender, address(this), amount); uint256 destAmount = (amount * conversionRate) / 1e18; bytes32 opId = keccak256(abi.encodePacked(msg.sender, destAmount, block.timestamp)); bridgeOperations[opId] = BridgeOperation({ user: msg.sender, amount: amount, timestamp: uint40(block.timestamp), finalized: false }); emit Event.BridgeInitiated(msg.sender, destAmount, targetChain, opId); } function releaseTokens(address to, uint256 amount, bytes32 sourceTx) external onlyOnce(sourceTx) { if (to == address(0)) revert Error.InvalidRecipient(); if (amount == 0) revert Error.InsufficientAmount(); BridgeOperation storage op = bridgeOperations[sourceTx]; if (op.finalized) revert Error.AlreadyFinalized(); op.finalized = true; token.transfer(to, amount); emit Event.BridgeFinalized(to, amount, sourceTx); } function refund(bytes32 opId) external nonReentrant { BridgeOperation storage op = bridgeOperations[opId]; if (block.timestamp < op.timestamp + refundTime) revert Error.RefundTooEarly(); if (op.finalized) revert Error.AlreadyFinalized(); op.finalized = true; token.transfer(op.user, op.amount); emit Event.Refund(op.user, op.amount, opId); } } ``` ### What We Learned * How to design and implement custom bridges * The importance of event-driven off-chain relayers * Managing conversion rates and refunds * Handling non-reentrancy and securing multi-chain operations ### Deployment Links & Live Demo * Github link [https://github.com/chain-builders/Token-Bridge]