**Week 14 at Blockfuse Labs**
This week has been more about building and this is what we worked on
**Building a Token Bridge: Lock & Mint Cross-Chain Transfers for ERC-20 Tokens**
As blockchain ecosystems continue to grow, the need for interoperability across networks has become more important than ever. That’s where Token Bridges come in — allowing users to move assets seamlessly between chains.
In this post, I’ll walk you through a practical implementation of how we built a token bridge using the Lock and Mint approach. We'll cover how it works under the hood, the smart contract logic, and how we use a backend relayer to synchronize activity between two chains.
**Use Case:** Bridging an ERC-20 token (BBT) from Base to Sepolia, and back.
**Overview: What is a Lock & Mint Bridge?**
The Lock & Mint pattern works by:
- Locking the original tokens on the source chain
- Minting equivalent wrapped tokens on the destination chain
This ensures that the circulating supply of tokens remains controlled — you’re not creating tokens out of thin air, you're just moving value across chains.
Flowchart of tokens going from Base → Sepolia (Lock → Relayer → Mint), and then back (Burn → Relayer → Release)
**Step 1: Locking Tokens on the Source Chain**
Let’s say you want to move 30 BBT tokens from Base to Sepolia. You begin by calling the lockTokens function on the Base chain:
```
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);
}
```
This function:
- Transfers tokens from the user to the bridge contract.
- Emits a BridgeInitiated event for the relayer.
- Stores bridge metadata in bridgeOperations.
**Step 2: Bridging the Gap — Enter the Relayer**
Smart contracts on different chains can’t talk to each other directly. This is where the relayer service comes into play.
The relayer listens for the BridgeInitiated event on the source chain. Once detected, it calls the mintTokens function on the destination chain to mint the equivalent wrapped tokens.
**Step 3: Minting on the Destination Chain**
On the Sepolia chain, the relayer triggers this:
```
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);
}
```
Now the user receives wBBT (wrapped BBT) on Sepolia — a token that is fully backed by the 30 BBT locked on Base.
**Bridging Back: Burn and Release**
To reverse the bridge, the process mirrors itself.
**Step 4: Burn Tokens on Sepolia**
When the user wants to move their wrapped tokens back to Base, they call burnTokens:
```
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))
);
}
```
This burns the user’s wrapped tokens and emits another BridgeInitiated event.
**Step 5: Release Tokens on Base**
Once the relayer detects the burn event, it calls releaseTokens on the Base contract:
```
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);
}
```
The original 30 BBT are now unlocked and returned to the user
The full working implementation of this bridge is available open-source:
https://github.com/chain-builders/Token-Bridge
Token bridges like this unlock a huge amount of flexibility for users, especially in ecosystems where different chains serve different purposes (e.g., fast L2s for gas efficiency, testnets for development, etc.).
By combining event-driven smart contracts with a trusted relayer service, this Lock & Mint bridge offers a simple yet secure way to move tokens across chains.