Skip to content

This project implements a canonical token bridge that enables seamless asset transfer between Ethereum mainnet and Starknet's ZK-rollup layer. Users can deposit tokens from Ethereum to access Starknet's faster, cheaper transactions, then withdraw back to Ethereum when needed.

Notifications You must be signed in to change notification settings

Aditya-alchemist/Bridging-tokens-starknet-Ethereum-L1-L2

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

6 Commits
Β 
Β 
Β 
Β 

Repository files navigation

image

πŸŒ‰ Ethereum ↔ Starknet Token Bridging

A full-stack implementation of token bridging between Ethereum (L1) and Starknet (L2) using Solidity and Cairo 1.0.


πŸ”Ž What is Bridging?

Bridging is a mechanism that allows you to move digital assets (like tokens or cryptocurrency) between different blockchains. Think of it as a secure tunnel connecting two separate blockchain networks.

The Core Concept

Imagine you have tokens on Ethereum, but you want to use them on another blockchain like Polygon or Starknet. Since blockchains are isolated systems that can't directly communicate, you need a "bridge" to transfer your assets.

Bridging moves token representation between blockchains β€” not actual tokens.

In this project:

  • L1: Ethereum Mainnet β€” handles original tokens
  • L2: Starknet β€” scalable ZK-rollup that mirrors tokens

Why?

  • πŸš€ Scalability: L2 is faster & cheaper
  • πŸ”„ Interoperability: Seamlessly move assets across chains
  • πŸ”— Access: DeFi apps live on both L1 and L2

🧠 How Bridging Works

Bridging is a Lock & Mint / Burn & Unlock process

L1 β†’ L2 (Deposit)

  1. Burn tokens on Ethereum (L1)
  2. Send message to Starknet (L2)
  3. Mint tokens on L2

L2 β†’ L1 (Withdraw)

  1. Burn tokens on Starknet (L2)
  2. Send message to Ethereum (L1)
  3. Mint tokens on L1

πŸ“Š Bridging Flowchart

flowchart LR
    %% Ethereum L1 Side
    subgraph EthereumL1["Ethereum L1"]
        A1["User Wallet (L1)"]
        A2["MintableToken.sol"]
        A3["TokenBridge.sol"]
    end
    
    %% StarkNet L2 Side  
    subgraph StarkNetL2["StarkNet L2"]
        B1["User Wallet (L2)"]
        B2["MintableToken.cairo"]
        B3["TokenBridge.cairo"]
    end
    
    %% L1 β†’ L2 Deposit Flow
    A1 -.->|"πŸ”½ Call bridgeToL2"| A3
    A3 -.->|"πŸ”₯ Burn tokens"| A2
    A3 ==>|"πŸ“¨ Send msg to L2"| B3
    B3 -.->|"⚑ Handle deposit"| B2
    B2 -.->|"✨ Mint tokens"| B1
    
    %% L2 β†’ L1 Withdraw Flow
    B1 -.->|"πŸ”Ό Call bridge_to_l1"| B3
    B3 -.->|"πŸ”₯ Burn tokens"| B2
    B3 ==>|"πŸ“¨ Send msg to L1"| A3
    A3 -.->|"πŸ“₯ Consume msg"| A2
    A2 -.->|"✨ Mint tokens"| A1
    
    %% Styling
    classDef l1Color fill:#4fc3f7,stroke:#0277bd,stroke-width:2px,color:#000
    classDef l2Color fill:#ba68c8,stroke:#7b1fa2,stroke-width:2px,color:#fff
    classDef bridgeColor fill:#ff7043,stroke:#d84315,stroke-width:3px,color:#fff
    classDef walletColor fill:#66bb6a,stroke:#2e7d32,stroke-width:2px,color:#fff
    
    class A2,B2 bridgeColor
    class A3,B3 bridgeColor
    class A1,B1 walletColor
Loading

πŸ—οΈ Architecture Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    Messaging     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Ethereum L1   β”‚ ◄──────────────►│    Starknet L2     β”‚
β”‚                 β”‚                 β”‚                    β”‚
β”‚ TokenBridge.sol β”‚                 β”‚ TokenBridge.cairo  β”‚
β”‚ MintableToken   β”‚                 β”‚ MintableToken.cairoβ”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ”© Components

πŸ“¦ Tokens

  • ERC20 token on L1
  • Mintable token on L2

πŸ”— Bridge Contracts

  • Solidity-based bridge on L1
  • Cairo-based bridge on L2

πŸ“¬ Messaging

  • sendMessageToL2 on L1
  • send_message_to_l1_syscall on L2

πŸ§ͺ Bridging Outputs

Preview Caption
Figure 1: Bridging Tokens to L2 from L1
Figure 2: Tokens Received on Starknet L2
Figure 3: Tokens Reflected in Wallet
Figure 4: Bridging Back to Ethereum
Figure 5: Tokens Recieved on L1 from L2

βš™οΈ Core Functions

πŸ“€ L2 β†’ L1 Withdrawal

fn bridge_to_l1(ref self: ContractState, l1_recipient: EthAddress, amount: u256) {
    // Burn on L2
    IMintableTokenDispatcher { contract_address: self.l2_token.read() }
        .burn(caller_address, amount);

    // Message L1
    let mut payload: Array<felt252> = array![
        l1_recipient.into(), amount.low.into(), amount.high.into(),
    ];
    syscalls::send_message_to_l1_syscall(self.l1_bridge.read(), payload.span()).unwrap_syscall();
}

πŸ“₯ L1 β†’ L2 Deposit Handler

#[l1_handler]
pub fn handle_deposit(ref self: ContractState, from_address: felt252, account: ContractAddress, amount: u256) {
    assert(from_address == self.l1_bridge.read(), Errors::EXPECTED_FROM_BRIDGE_ONLY);
    IMintableTokenDispatcher { contract_address: self.l2_token.read() }.mint(account, amount);
}

🧨 L1 Deposit to L2

function bridgeToL2(uint256 recipientAddress, uint256 amount) external payable {
    token.burn(msg.sender, amount);
    (uint128 low, uint128 high) = splitUint256(amount);

    uint256[] memory payload = new uint256[](3);
    payload[0] = recipientAddress;
    payload[1] = low;
    payload[2] = high;

    snMessaging.sendMessageToL2{value: msg.value}(
        l2Bridge,
        l2HandlerSelector,
        payload
    );
}

βœ… L1 Consumes Withdrawal

function consumeWithdrawal(uint256 fromAddress, address recipient, uint128 low, uint128 high) external {
    uint256[] memory payload = new uint256[](3);
    payload[0] = uint256(uint160(recipient));
    payload[1] = uint256(low);
    payload[2] = uint256(high);

    snMessaging.consumeMessageFromL2(fromAddress, payload);
    uint256 amount = (uint256(high) << 128) | uint256(low);
    token.mint(recipient, amount);
}

πŸ“ Serialization: uint256 in Cairo

function splitUint256(uint256 value) private pure returns (uint128 low, uint128 high) {
    low = uint128(value & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF);
    high = uint128(value >> 128);
}
// Recombined using u256 struct in Cairo
let amount = u256_from_words(low, high);

πŸ› οΈ Dev Tips & Best Practices

  • βœ… Always validate source: assert(from_address == self.l1_bridge.read())
  • πŸ” Mint/burn permissions must be tightly controlled
  • πŸ’‘ Emit events for all cross-chain state changes
  • ⏳ L2 β†’ L1 may require hours (proof generation)

πŸ§ͺ Testing Strategy

  • Unit test: mint/burn, serialization, access control
  • Integration test: complete bridge cycle
  • Failure test: invalid selectors, mismatched amounts

⚠️ Gotchas

  • ❌ L2 β†’ L1 messages aren't auto-processed: user must call consumeWithdrawal
  • βš™οΈ Use uint128 chunks for all messaging payloads
  • 🚨 L1 handler selector mismatch will silently fail

πŸ“š Conclusion

This repo shows how to bridge ERC20-like tokens securely between Ethereum and Starknet using minimal, composable smart contracts. You can extend it with:

  • zk-proof verification for withdrawals
  • on-chain receipts and history
  • batch bridging

PRs & stars welcome ⭐

About

This project implements a canonical token bridge that enables seamless asset transfer between Ethereum mainnet and Starknet's ZK-rollup layer. Users can deposit tokens from Ethereum to access Starknet's faster, cheaper transactions, then withdraw back to Ethereum when needed.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published