A sophisticated cross-chain token bridge supporting multiple source chains bridging to SOLO testnet as a unified hub, using direct MetaLayer integration with ERC6909 collection unification and user-friendly original token IDs.
Multiple Source Chains SOLO Hub (8884571)
βββββββββββββββββββββββββββ
B3 (1993) βββββββββββββββΊβ β
βββββββββββββββββββ MetaLayer β BridgedTokenReceiver β
β TokenBridge β Mailbox β β
βββββββββββββββββββ β - Multi-bridge support β
β - Creates ERC6909 β
Polygon (137) βββββββββββββββΊβ - Original token IDs β
βββββββββββββββββββ β - Collection unity β
β TokenBridge β β - Bridge-back routing β
βββββββββββββββββββ β β
βββββββββββββββββββββββββββ
Arbitrum (42161) βββββββββββββββΊ β
βββββββββββββββββββ β
β TokenBridge β MultiTokenFactory
βββββββββββββββββββ
More chains... βββββββββββββββΊ
- Multi-Chain Hub Architecture: Multiple source chains β SOLO as unified hub
- User-Friendly Token IDs: Preserves original token IDs (#1, #2, #3) for better UX
- Collision-Resistant Bridge-Back: Internal hash system ensures safe routing
- Dynamic Bridge Management: Add/remove source chains without redeployment
- ERC6909 Collection Unification: One ERC6909 contract per origin collection
- Source Domain Tracking: Automatic routing back to correct origin chain
- Perfect Bridge-Back Safety: Impossible to get wrong original token
- Direct MetaLayer Integration: No intermediate wrapper contracts
- Backward Compatible: Existing setRemoteBridge() function still works
- No Bridge Fees: Only pay MetaLayer gas fees (with configurable uptick)
- One ERC6909 contract per origin collection (not per token)
- Original token IDs preserved for user experience (#1, #2, #3)
- Internal bridge hashes for collision-resistant bridge-back routing
- Source domain tracking for multi-chain routing
Origin: Collection ABC Token #1 β SOLO: ABC ERC6909 Contract, Token ID #1 (user sees #1)
Origin: Collection ABC Token #2 β SOLO: ABC ERC6909 Contract, Token ID #2 (user sees #2)
Origin: Collection DEF Token #1 β SOLO: DEF ERC6909 Contract, Token ID #1 (user sees #1)
Token from B3 ABC #1 β bridgeHash: hash(ABC,1) + sourceDomain: 1993
Token from Polygon ABC #1 β bridgeHash: hash(ABC,1) + sourceDomain: 137
Token from Arbitrum ABC #1 β bridgeHash: hash(ABC,1) + sourceDomain: 42161
-
Bridge Collection ABC Token #1 from B3:
- Original:
CoolNFT(0xABC...)Token ID#1 - Creates:
Bridged CoolNFT ERC6909(0xXYZ...)with:- User sees: Token ID
#1(original ID preserved) - Bridge stores:
bridgeHash: hash(0xABC..., 1)+sourceDomain: 1993
- User sees: Token ID
- Original:
-
Bridge Collection ABC Token #2 (same collection):
- Original:
CoolNFT(0xABC...)Token ID#2 - Uses existing:
Bridged CoolNFT ERC6909(0xXYZ...)with:- User sees: Token ID
#2(original ID preserved) - Bridge stores:
bridgeHash: hash(0xABC..., 2)+sourceDomain: 1993
- User sees: Token ID
- Original:
-
Bridge Collection DEF Token #1 (different collection):
- Original:
OtherNFT(0xDEF...)Token ID#1 - Creates new:
Bridged OtherNFT ERC6909(0xQRS...)with:- User sees: Token ID
#1(original ID preserved) - Bridge stores:
bridgeHash: hash(0xDEF..., 1)+sourceDomain: 1993
- User sees: Token ID
- Original:
β
User-Friendly: Users see familiar token IDs (#1, #2, #3) instead of large hash numbers
β
Collection Integrity: All tokens from same origin collection stay in one ERC6909 contract
β
Multi-Chain Support: Automatic routing back to correct source chain
β
Collision-Resistant: Internal hash system prevents conflicts during bridge-back
β
Perfect Bridge-Back: Always knows exact original contract, token ID, and source chain
β
Trading Efficiency: Collection floor prices and liquidity maintained
β
Marketplace Compatibility: Collections appear unified with intuitive token numbering
The SOLO hub can accept bridges from any number of source chains. Currently configured for:
- B3 Sepolia (1993) - Primary testnet bridge
- Polygon (137) - Production ready
- Arbitrum (42161) - Production ready
- Ethereum (1) - Production ready
- Any EVM Chain - Easy to add new chains
// Add or update source chain bridge
setRemoteBridge(uint32 domain, address bridgeAddress)
// Remove source chain (set address to 0x0)
setRemoteBridge(uint32 domain, address(0))
// Enable/disable chain
setDomainStatus(uint32 domain, bool active)
// Query functions
getSupportedDomains() β uint32[]
getBridgeForDomain(uint32 domain) β address
isChainSupported(uint32 domain) β bool# Example: Add Polygon bridge
cast send $SOLO_BRIDGED_TOKEN_RECEIVER "setRemoteBridge(uint32,address)" 137 $POLYGON_BRIDGE_ADDRESS
# Example: Add Arbitrum bridge
cast send $SOLO_BRIDGED_TOKEN_RECEIVER "setRemoteBridge(uint32,address)" 42161 $ARBITRUM_BRIDGE_ADDRESS- β ERC20: Fungible tokens β ERC6909 fungible tokens
- β ERC721: Non-fungible tokens β ERC6909 non-fungible tokens
- β ERC1155: Multi-tokens β ERC6909 multi-tokens
cp .env.example .env
# Edit .env with your values
source .env# Deploy contracts to both chains
make deploy-all
# Or step by step:
make deploy-b3 # Deploy TokenBridge on B3
make deploy-solo # Deploy BridgedTokenReceiver on SOLO
make configure # Link contracts together# 1. Approve bridge to transfer your token
cast send <TOKEN_CONTRACT> "approve(address,uint256)" $B3_TOKEN_BRIDGE <TOKEN_ID> --rpc-url $B3_SEPOLIA_RPC_URL --private-key $PRIVATE_KEY
# 2. Bridge the token (creates ERC6909 with original token ID preserved)
cast send $B3_TOKEN_BRIDGE "bridgeERC721(address,uint256,uint256)" <TOKEN_CONTRACT> <TOKEN_ID> <GAS_LIMIT> --value $(cast call $B3_TOKEN_BRIDGE "getRequiredFee()" --rpc-url $B3_SEPOLIA_RPC_URL) --rpc-url $B3_SEPOLIA_RPC_URL --private-key $PRIVATE_KEY# 1. Find your bridged collection and token info (now requires source domain)
cast call $SOLO_BRIDGED_TOKEN_RECEIVER "getBridgedCollection(uint32,address)" <SOURCE_DOMAIN> <ORIGINAL_TOKEN_CONTRACT> --rpc-url $SOLO_TESTNET_RPC_URL
cast call $SOLO_BRIDGED_TOKEN_RECEIVER "getBridgedToken(address,uint256)" <ORIGINAL_TOKEN_CONTRACT> <ORIGINAL_TOKEN_ID> --rpc-url $SOLO_TESTNET_RPC_URL
# 2. Get the user-friendly token ID (same as original)
cast call $SOLO_BRIDGED_TOKEN_RECEIVER "getBridgedTokenIdFromOrigin(address,uint256)" <ORIGINAL_TOKEN_CONTRACT> <ORIGINAL_TOKEN_ID> --rpc-url $SOLO_TESTNET_RPC_URL
# 3. Bridge back using ERC6909 contract and original token ID
# (Bridge automatically routes to correct source chain using stored domain)
cast send $SOLO_BRIDGED_TOKEN_RECEIVER "requestBridgeBack(address,uint256,uint256,uint256)" <ERC6909_CONTRACT> <ORIGINAL_TOKEN_ID> <AMOUNT> <GAS_LIMIT> --value $(cast call $SOLO_BRIDGED_TOKEN_RECEIVER "getRequiredFee()" --rpc-url $SOLO_TESTNET_RPC_URL) --rpc-url $SOLO_TESTNET_RPC_URL --private-key $PRIVATE_KEYAfter deployment, update your .env:
# B3 Sepolia Contracts
B3_TOKEN_BRIDGE=<deployed_address>
B3_MULTI_TOKEN_FACTORY=<deployed_address>
B3_REGISTRY=<deployed_address>
# SOLO Testnet Contracts
SOLO_BRIDGED_TOKEN_RECEIVER=<deployed_address>
SOLO_MULTI_TOKEN_FACTORY=<deployed_address># Network RPCs
B3_SEPOLIA_RPC_URL=https://sepolia.b3.fun/
SOLO_TESTNET_RPC_URL=https://solo-testnet.rpc.caldera.xyz/http
# Chain Configuration
B3_SEPOLIA_CHAIN_ID=1993
SOLO_TESTNET_CHAIN_ID=8884571
# MetaLayer Mailbox (same on both chains)
METALAYER_CONTRACT=0x6F23B0211056035A22430a10fD27DED8547dc377
# No Bridge Fees - Only MetaLayer gas fees apply with configurable uptick
# Deployment
PRIVATE_KEY=your_private_key_here
ETHERSCAN_API_KEY=your_etherscan_key_here# Run contract tests
make test
# Verify configuration
make verify-config
# Check bridge functionality
make test-bridge- Purpose: Locks original tokens, sends cross-chain messages
- Functions:
bridgeERC20(token, amount, gasLimit),bridgeERC721(token, tokenId, gasLimit),bridgeERC1155(token, tokenId, amount, gasLimit) - Key Features:
- Open by default (gatekeeping disabled)
- Configurable gas uptick percentage (10% default)
- Caller-specified gas limits
- Receives: Unlock messages from SOLO to release tokens
- Purpose: Multi-chain hub that creates ERC6909 collection contracts and handles bridge-back routing
- Functions:
requestBridgeBack(erc6909Contract, tokenId, amount, gasLimit)- Routes back to correct source chainaddBridge(domain, bridgeAddress)- Add new source chain supportremoveBridge(domain)- Remove source chain support
- Creates: One ERC6909 contract per origin collection with original token IDs
- Key Features:
generateBridgedTokenId()- Returns original token ID for user-friendly displaygenerateBridgeHash()- Creates collision-resistant hash for internal routing- Multi-chain source domain tracking and automatic routing
getBridgedCollection()- View collection informationgetBridgedToken()- View specific token informationgetSupportedDomains()- List all configured source chainsgetRequiredFeeForDomain(domain)- Get fees for specific source chain
- Purpose: Deploys ERC6909 token contracts for collections
- Standards: Creates ERC6909 contracts optimized for collection unification
function generateBridgedTokenId(address originToken, uint256 originTokenId)
external pure returns (uint256)function getBridgedCollection(uint32 sourceDomain, address originToken)
external view returns (BridgedCollection memory)
// NEW: Collection discovery functions
function getSourceDomainsForToken(address originToken)
external view returns (uint32[] memory)
function hasCollection(uint32 sourceDomain, address originToken)
external view returns (bool)function getBridgedToken(address originToken, uint256 originTokenId)
external view returns (BridgedToken memory)The TokenBridge includes an emergency unlock system for recovering assets when cross-chain messages fail or are delayed:
- Global Toggle: Admin can enable/disable emergency unlocks system-wide
- User Requests: Any user can request unlock of any asset held by the bridge
- Admin Approval: Only owner can approve unlock requests manually
- Automatic Execution: Approved unlocks execute immediately upon approval
- Double-Unlock Prevention: Prevents duplicate unlocks when delayed messages arrive later
- Admin enables emergency unlocks:
setEmergencyUnlockEnabled(true) - User requests unlock:
requestUnsafeUnlock(token, tokenId, amount, tokenType) - Admin reviews and approves:
approveEmergencyUnlock(requestId) - Tokens transferred automatically to requester
- β Balance Verification: Ensures bridge actually owns requested tokens
- β Duplicate Prevention: Tracks executed unlocks to prevent double-spending
- β Event Logging: Full audit trail of all emergency unlock activities
- β Access Control: Only owner can approve unlock requests
- β Request Tracking: Unique request IDs for precise unlock management
The emergency unlock system tracks duplicates using (user, token, tokenId, amount) hash. This provides:
- β Simple and efficient implementation
- β Prevents most double-unlock scenarios
β οΈ Limitation: Cannot distinguish between different bridge transactions of same amount
A more precise tracking system could be implemented that links emergency unlocks to specific depositIds:
- β Perfect precision - tracks exact bridge transactions
- β Eliminates all edge cases with identical amounts
- β Better audit trail linking emergency unlocks to original deposits
β οΈ More complex implementation requiring depositId tracking
Note: Option 2 can be implemented as a future enhancement if more precise tracking is needed. The current Option 1 implementation handles the vast majority of use cases safely and efficiently.
forge buildforge test -vv
# Test specific functionality
forge test --match-test testCollectionUnification -vv
forge test --match-test testCollisionResistance -vvEdit the domain constants in deployment scripts for different chains.
This bridge uses MetaLayer's message passing infrastructure:
- Mailbox Contract:
0x6F23B0211056035A22430a10fD27DED8547dc377 - B3 Domain:
1993 - SOLO Domain:
8884571
Messages are automatically relayed by MetaLayer's decentralized relayer network.
# Get all info about a bridged collection (now requires source domain)
cast call $SOLO_BRIDGED_TOKEN_RECEIVER "getBridgedCollection(uint32,address)" <SOURCE_DOMAIN> <ORIGIN_CONTRACT>
# Discover which domains have bridged this token (NEW!)
cast call $SOLO_BRIDGED_TOKEN_RECEIVER "getSourceDomainsForToken(address)" <ORIGIN_CONTRACT>
# Check if a specific domain has bridged this token (NEW!)
cast call $SOLO_BRIDGED_TOKEN_RECEIVER "hasCollection(uint32,address)" <SOURCE_DOMAIN> <ORIGIN_CONTRACT>
# Get info about a specific bridged token
cast call $SOLO_BRIDGED_TOKEN_RECEIVER "getBridgedToken(address,uint256)" <ORIGIN_CONTRACT> <ORIGIN_TOKEN_ID>
# Calculate what the bridged token ID will be (returns original ID)
cast call $SOLO_BRIDGED_TOKEN_RECEIVER "getBridgedTokenIdFromOrigin(address,uint256)" <ORIGIN_CONTRACT> <ORIGIN_TOKEN_ID>
# Query multi-bridge support
cast call $SOLO_BRIDGED_TOKEN_RECEIVER "getSupportedDomains()"
cast call $SOLO_BRIDGED_TOKEN_RECEIVER "isChainSupported(uint32)" <DOMAIN_ID>All tokens from the same origin collection will be unified in the same ERC6909 contract with user-friendly IDs:
- Bridge
CoolApes #1β CreatesBridged CoolApes ERC6909with token ID#1(user sees #1) - Bridge
CoolApes #5β Uses existingBridged CoolApes ERC6909with token ID#5(user sees #5) - Bridge
CoolApes #10β Uses existingBridged CoolApes ERC6909with token ID#10(user sees #10)
Result: One unified collection on SOLO with all CoolApes maintaining original token numbering and collection integrity.
Now fully implemented with domain-aware collection management.
With multiple chains configured, the same collection from different chains creates separate ERC6909 contracts:
- Bridge
CoolApes #1from B3 β SOLO ERC6909 Contract A, Token #1 (routes back to B3) - Bridge
CoolApes #1from Polygon β SOLO ERC6909 Contract B, Token #1 (routes back to Polygon) - Bridge
CoolApes #1from Arbitrum β SOLO ERC6909 Contract C, Token #1 (routes back to Arbitrum)
Each source chain gets its own dedicated ERC6909 collection contract, preventing collection metadata conflicts and ensuring clean separation between chains.
- Deployment Guide - Detailed deployment instructions
- MetaLayer Documentation - MetaLayer protocol docs
- ERC6909 Specification - Multi-token standard
- Forge Documentation - Foundry development framework
This multi-chain bridge hub enables cross-chain token transfers from any number of source chains to SOLO, while preserving original token IDs, maintaining collection integrity, and ensuring bridge-back safety through internal collision-resistant routing and ERC6909 collection unification.