Foundational knowledge for understanding token standards and their implementations across Ethereum Layer 2 networks.
Layer 2 Token Standards and Wrappers
Core Concepts
Canonical Bridging
The canonical bridge is the official, protocol-endorsed channel for moving assets between L1 and L2. It mints a native representation of the asset on the destination chain, maintaining a 1:1 peg backed by assets locked in the source chain's bridge contract. This is the most secure and decentralized bridging method, though it can be slower. Examples include the Optimism Gateway and Arbitrum Bridge.
Bridged Token Standards
These are token contracts deployed on an L2 that represent an asset from another chain. The ERC-20 standard is universal, but implementations vary. Some use custom interfaces (e.g., Optimism's OVM_ETH) while others adhere strictly to the mainnet spec. Understanding the specific standard is crucial for dApp and wallet compatibility, as misalignment can cause integration failures.
Third-Party Wrappers
Third-party wrappers are alternative, often faster bridges provided by protocols like Across, Hop, or Synapse. They issue their own liquid wrapper tokens (e.g., hETH, synthETH) on the destination chain. These wrappers rely on their own liquidity pools and validator sets, introducing different trust and security assumptions compared to canonical bridges, but offering improved speed and capital efficiency.
Native Gas Tokens
Each L2 has a native gas token used to pay for transaction execution. On Optimism and Arbitrum, this is bridged ETH. On zkSync Era and Starknet, it can be ETH or a custom token like STRK. The minting mechanism and underlying guarantee (canonical vs. third-party) of this gas token directly impacts network security and user experience for fee payments.
Token Mint/Burn Mechanics
This describes the on-chain logic for creating and destroying bridged tokens. In a lock-and-mint model, assets are locked on L1 and an equivalent amount is minted on L2. The reverse burn-and-mint process is used for withdrawals. These mechanics are defined in the bridge contract and are critical for understanding finality times, security guarantees, and the redemption process for users.
Standardization Efforts (ERC-7281)
ERC-7281 (xERC-20) is a proposed standard for cross-chain token representation. It aims to solve fragmentation by allowing a token issuer on L1 to delegate minting rights to specific bridges for their token on L2s. This creates a canonical, issuer-controlled representation per bridge, improving security and liquidity uniformity. It represents a shift from chain-centric to token-centric bridging models.
Layer 2 Bridging Architectures
How Bridges Connect Layers
Bridging architectures define the technical and trust models for moving assets between Ethereum's Layer 1 (L1) and its Layer 2 (L2) networks. The primary goal is to enable secure, fast, and cost-effective transfers while maintaining the underlying asset's value and properties. These systems are critical for liquidity flow and user onboarding.
Core Architecture Types
- Native Mint & Burn: The canonical method used by Optimistic and ZK Rollups. Tokens are minted on L2 when deposited from L1 and burned on L2 to withdraw back to L1. This is managed by the L2's smart contracts and is considered the most secure.
- Lock & Mint (Third-Party): Used by many general-purpose bridges like Across or Hop. Assets are locked in a smart contract on the origin chain, and a wrapped representation is minted on the destination chain by a bridge operator.
- Liquidity Networks: Protocols like Connext or Hop utilize liquidity pools on both chains. A user's asset is swapped for a local asset on the destination chain via a liquidity provider, enabling near-instant transfers.
Example: Depositing to Arbitrum
When you bridge ETH from Ethereum to Arbitrum using its native bridge, you initiate a deposit transaction to the Arbitrum Inbox contract on L1. After the challenge period, your ETH is minted as "AETH" on the Arbitrum L2, where it can be used for transactions and gas fees.
Wrapper Contract Interaction Flow
Process overview for bridging and managing tokens via a canonical wrapper contract.
Approve Token Spending
Grant the wrapper contract permission to transfer your Layer 1 tokens.
Detailed Instructions
Before bridging, you must grant the wrapper contract an allowance to spend the specific amount of tokens you intend to deposit. This is a standard ERC-20 approval process. First, identify the official contract address for the canonical bridge on the L1 network (e.g., 0x1234... for Optimism). Then, call the approve function on your L1 token contract, specifying the wrapper contract as the spender and the deposit amount as the value. Always verify the contract address from the official bridge documentation to avoid scams.
- Sub-step 1: Call
token.approve(wrapperAddress, amount)on L1 - Sub-step 2: Wait for the transaction to be confirmed on the L1 network
- Sub-step 3: Verify the approval by checking the allowance:
token.allowance(yourAddress, wrapperAddress)
solidity// Example using ethers.js const tx = await L1TokenContract.approve(wrapperAddress, depositAmount); await tx.wait();
Tip: For a better UX, consider approving a very large amount (like
type(uint256).max) once, to avoid repeated transactions for future deposits.
Deposit Tokens to the Wrapper
Initiate the bridge by locking tokens in the L1 wrapper contract.
Detailed Instructions
Execute the deposit function on the L1 wrapper contract. The standard function is often named depositTo or bridgeTo. You must specify the recipient address on the destination L2 and the deposit amount. The contract will lock your L1 tokens in its escrow and emit an event that the L2's message relayer will pick up. Ensure you have sufficient L1 ETH to pay for gas. The deposit will trigger the minting of a corresponding canonical bridged token on L2. Monitor the transaction receipt for the DepositInitiated event.
- Sub-step 1: Call
wrapper.depositTo(l2RecipientAddress, amount)on L1 - Sub-step 2: Pay the L1 gas fee and confirm the transaction
- Sub-step 3: Capture the transaction hash and look for the deposit event log
solidity// Example deposit call await L1BridgeContract.depositTo( '0xRecipientOnL2', ethers.utils.parseEther('1.0') );
Tip: The deposit typically has a finalization period (e.g., 10-20 minutes). Use the official bridge UI to track the status.
Claim Bridged Tokens on L2
Complete the bridge by claiming the minted tokens on the destination layer.
Detailed Instructions
After the L1 deposit transaction is finalized and the state root is relayed, the funds become claimable on L2. This is often an automatic process handled by the network's infrastructure, but you may need to trigger a finalization. Interact with the L2's bridge contract (the counterpart to the L1 wrapper). Call the finalizeDeposit or claim function, providing the proof of the L1 transaction. The L2 contract will verify the proof and mint the canonical L2 token to your specified address. The token will adhere to the L2's native token standard (e.g., Optimism's OVM_ETH or Arbitrum's AETH).
- Sub-step 1: Wait for the deposit's challenge period to pass (if applicable)
- Sub-step 2: Call
L2Bridge.finalizeDeposit(from, to, amount, data)with the transaction proof - Sub-step 3: Verify the token balance of the recipient address on L2
solidity// Example using an SDK for proof generation const proof = await generateProof(l1TxHash); const tx = await L2BridgeContract.finalizeDeposit(l1Sender, l2Recipient, amount, proof);
Tip: Most user-facing UIs abstract this step. For developers, use the bridge's SDK to generate the necessary proof parameters.
Withdraw Tokens Back to L1
Initiate the process to bridge tokens from L2 back to their native L1 chain.
Detailed Instructions
The withdrawal process is the inverse of deposit. Start by calling the withdraw or initiateWithdrawal function on the L2 token bridge contract. This function will burn the L2 bridged tokens and emit a message for the L1. You must pay gas on L2. After the transaction is included in an L2 block, there is a mandatory challenge period (e.g., 7 days for Optimism) for fraud proofs. Only after this period can the withdrawal be finalized on L1. Save the L2 transaction hash, as you will need it to prove the withdrawal later.
- Sub-step 1: Call
L2TokenBridge.withdraw(yourAddress, amount)on L2 - Sub-step 2: Pay the L2 gas fee and wait for transaction confirmation
- Sub-step 3: Note the L2 block number and transaction hash for the proof
solidity// Example withdrawal initiation await L2StandardBridge.withdraw( L2_TOKEN_ADDRESS, amount, 200000, // gasLimit "0x" // empty data );
Tip: The long challenge period is a security feature. Plan your liquidity needs accordingly.
Finalize Withdrawal on L1
Prove and complete the withdrawal on Layer 1 after the challenge period.
Detailed Instructions
Once the L2 challenge period has elapsed, you must prove the withdrawal on L1. This involves providing a Merkle proof that your withdrawal transaction was included in an L2 state root that has been posted to L1. Use the L2 transaction hash and block number to generate the proof via the bridge's SDK or RPC calls. Then, call the finalizeWithdrawal function on the L1 wrapper contract with this proof. Upon successful verification, the contract will release the originally locked tokens from escrow to your designated L1 address. This step requires an L1 gas payment.
- Sub-step 1: Use the bridge portal to generate the withdrawal proof after the challenge period
- Sub-step 2: Call
L1Bridge.finalizeWithdrawal(l2BlockNumber, proof)with the generated data - Sub-step 3: Confirm the transaction and verify your L1 token balance increase
solidity// Example finalization with proof const proof = await getWithdrawalProof(l2TxHash); await L1BridgeContract.finalizeWithdrawal( proof.l2BlockNumber, proof.l2MessageIndex, proof.l2TxNumberInBlock, proof.merkleProof, proof.withdrawalMessage );
Tip: Many bridges offer a "prove and finalize" transaction bundler to simplify this two-step process.
Token Standard Comparison Across L2s
Comparison of native token standards and canonical bridging implementations across major Layer 2 networks.
| Feature / Metric | Arbitrum (ERC-20) | Optimism (Standard Bridge) | zkSync Era (Native ETH & ERC-20) | Starknet (ERC-20 & ERC-721) |
|---|---|---|---|---|
Bridging Model | Canonical Messaging (L1 <-> L2) | Canonical Messaging (OptimismPortal) | Native Account Abstraction & L1<->L2 Messaging | L1 <-> L2 Messaging via StarkGate |
Withdrawal Time (L2->L1) | ~1 week (Dispute Period) | ~1 week (Fault Proof Period) | ~24 hours (ZK Validity Proof) | Several hours (ZK Validity Proof) |
Gas Fee Structure | L1 Data + L2 Execution | L1 Data + L2 Execution | L1 Pubdata + L2 Execution | L1 Data + L2 Execution |
Token Minting | On L1, locked in bridge | On L1, locked in bridge | Can be minted natively on L2 | Must be bridged from L1 or minted via contract |
Contract Upgradeability | Proxy patterns via L1 governance | Upgradeable via L1 ProxyAdmin | Immutable core, upgradeable via system contracts | Upgradeable via L1 governance (Starknet OS) |
Native Fee Token | ETH (wrapped for gas) | ETH | ETH (paymaster abstraction possible) | ETH (STRK for fee abstraction) |
Cross-Rollup Messaging | Via Arbitrum's cross-chain protocol | Via Optimism's Bedrock & cross-domain messaging | Via native L1/L2 communication & 3rd party | Via Starknet Messaging (L1 <-> L2) |
Security and Trust Considerations
Understanding the security models and trust assumptions of L2 token standards is critical for safe asset management and protocol integration.
Escrow Contract Security
Escrow contracts hold the canonical assets on the L1 while minting wrapped versions on the L2. Their security is paramount.
- Audits must cover upgrade mechanisms and pausability.
- Minter/validator key management must be decentralized or time-locked.
- A compromise here can lead to the permanent loss of all bridged assets.
- Example: A flawed Arbitrum bridge contract could lock all ETH deposited to Arbitrum One.
Prover & Fraud Proof Trust
Optimistic rollups rely on a fraud proof system where a single honest validator can challenge invalid state transitions.
- Users must trust that at least one honest node is monitoring the chain.
- The challenge period (e.g., 7 days) creates a withdrawal delay for security.
- Zero-knowledge rollups replace this with cryptographic validity proofs, offering instant finality.
- This defines the base layer security assumption for your wrapped assets.
Standard Implementation Risks
The specific implementation of a token standard (e.g., L2 ERC20 wrapper) introduces unique risks.
- Reentrancy vulnerabilities in deposit/withdraw functions can be exploited.
- Incorrect fee calculation logic may lock funds or enable theft.
- Integration flaws with the L2's gas metering can cause failed transactions.
- Always verify the contract address and audit status before interacting.
Upgradeability and Admin Keys
Many bridge and token contracts are upgradeable, controlled by a multi-sig or DAO.
- This introduces trust in the admin entity not to act maliciously.
- Transparent governance and timelocks on upgrades are essential.
- A malicious upgrade could change token economics or mint unlimited supply.
- Assess the governance model of standards like Optimism's OVM 2.0 or Arbitrum Nitro.
L1 Reorg & Finality Risks
Wrapped assets depend on the finality of the L1 blockchain. A deep reorg can invalidate L2 state.
- Optimistic rollups require L1 block confirmations for full finality.
- Zero-knowledge rollups post validity proofs to L1, but still rely on its consensus.
- A 51% attack on Ethereum could theoretically compromise bridged asset records.
- This is a systemic risk shared across all L2 solutions.
Oracle and Price Feed Reliance
Cross-chain messaging and some wrapper minting mechanisms depend on oracles for price or state data.
- A manipulated price feed could enable undercollateralized minting on the L2.
- Decentralized oracle networks (e.g., Chainlink) mitigate single points of failure.
- Assess the oracle design for standards involving synthetic assets or collateralized wrappers.
- This is crucial for liquidity pools using L2 tokens as collateral.
Implementing a Custom Wrapper
Process overview
Define the Wrapper Interface and Storage
Establish the core data structures and function signatures for your wrapper contract.
Detailed Instructions
Begin by defining the ERC-20 interface your wrapper will implement and the internal state variables to track wrapped assets. A typical wrapper needs to store the address of the canonical token on L1 or L2 and maintain a mapping of balances.
- Sub-step 1: Import the IERC20 interface from OpenZeppelin or define the standard
transfer,balanceOf, andapprovefunctions. - Sub-step 2: Declare an immutable public variable for the canonical token address, set in the constructor.
- Sub-step 3: Create a
mapping(address => uint256)for user balances and auint256for total supply.
solidity// SPDX-License-Identifier: MIT import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract CustomWrapper { IERC20 public immutable canonicalToken; mapping(address => uint256) private _balances; uint256 private _totalSupply; constructor(address _canonicalToken) { canonicalToken = IERC20(_canonicalToken); } }
Tip: Using
immutablefor the canonical token address saves gas and ensures the reference cannot be changed after deployment.
Implement the Deposit and Withdrawal Logic
Create the core functions for wrapping and unwrapping tokens, handling asset custody.
Detailed Instructions
The deposit function must transfer tokens from the user to the wrapper contract and mint wrapped tokens. The withdraw function burns wrapped tokens and returns the underlying assets. You must handle approval from the user for the deposit and ensure sufficient wrapper balance for withdrawal.
- Sub-step 1: In
deposit(uint256 amount), callcanonicalToken.transferFrom(msg.sender, address(this), amount). On success, mint wrapper tokens to the user by updating_balancesand_totalSupply. - Sub-step 2: In
withdraw(uint256 amount), check that the user's wrapper balance is sufficient, then decrement_balancesand_totalSupply. Transfer the canonical tokens back to the user. - Sub-step 3: Emit corresponding
DepositandWithdrawevents for off-chain tracking.
solidityevent Deposit(address indexed user, uint256 amount); event Withdraw(address indexed user, uint256 amount); function deposit(uint256 amount) external { require(canonicalToken.transferFrom(msg.sender, address(this), amount), "Transfer failed"); _balances[msg.sender] += amount; _totalSupply += amount; emit Deposit(msg.sender, amount); } function withdraw(uint256 amount) external { require(_balances[msg.sender] >= amount, "Insufficient balance"); _balances[msg.sender] -= amount; _totalSupply -= amount; require(canonicalToken.transfer(msg.sender, amount), "Transfer failed"); emit Withdraw(msg.sender, amount); }
Tip: Use the
requirestatement with the return value oftransferFromto safely handle non-compliant ERC-20 tokens that returnfalseon failure.
Integrate with the Native Bridging System
Connect your wrapper to the L1/L2 bridge's messaging layer for cross-chain functionality.
Detailed Instructions
For a wrapper to facilitate cross-chain movement, it must interact with the canonical bridge's messaging contract. On Optimism, this is the L1StandardBridge; on Arbitrum, it's the L1GatewayRouter. Your wrapper's bridge function should lock/burn tokens and send a message to mint on the destination chain.
- Sub-step 1: Import the interface for the bridge messenger (e.g.,
IL1StandardBridgefor Optimism). - Sub-step 2: Create a
bridgeToL1function that burns the user's wrapped tokens and callssendMessageon the messenger contract with the recipient and amount. - Sub-step 3: Implement a function, callable only by the bridge, to mint tokens upon receiving a valid cross-chain message. This uses a modifier like
onlyFromCrossDomainSender.
solidity// Example for an Optimism-style L2 wrapper import "@eth-optimism/contracts/L2/messaging/IL2StandardBridge.sol"; contract CustomWrapperL2 is CustomWrapper { IL2StandardBridge public immutable l2Bridge; constructor(address _l2Bridge, address _l1Token) CustomWrapper(_l1Token) { l2Bridge = IL2StandardBridge(_l2Bridge); } function bridgeToL1(uint256 amount, address recipient) external { _burn(msg.sender, amount); // Internal burn function l2Bridge.withdraw(address(canonicalToken), amount, 0, abi.encode(recipient)); } }
Tip: The bridge call often requires a
gasLimitparameter (set to0for defaults) and encoded calldata for the recipient on the other side.
Add Access Control and Pause Mechanisms
Implement administrative controls for security, upgrades, and emergency response.
Detailed Instructions
Use OpenZeppelin's AccessControl to restrict sensitive functions like changing bridge addresses or pausing deposits. A pause mechanism is critical to halt operations if a vulnerability is discovered in the bridge or wrapper logic.
- Sub-step 1: Import
@openzeppelin/contracts/access/AccessControl.soland@openzeppelin/contracts/security/Pausable.sol. - Sub-step 2: Inherit from
AccessControlandPausable. In the constructor, grant theDEFAULT_ADMIN_ROLEto the deployer and set up aPAUSER_ROLE. - Sub-step 3: Add the
whenNotPausedmodifier todepositandbridgefunctions. Createpause()andunpause()functions restricted to the pauser role.
solidityimport "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/security/Pausable.sol"; contract SecureCustomWrapper is CustomWrapper, AccessControl, Pausable { bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); constructor(address _canonicalToken) CustomWrapper(_canonicalToken) { _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); _grantRole(PAUSER_ROLE, msg.sender); } function deposit(uint256 amount) external whenNotPaused override { super.deposit(amount); } function pause() external onlyRole(PAUSER_ROLE) { _pause(); } function unpause() external onlyRole(PAUSER_ROLE) { _unpause(); } }
Tip: Consider implementing a timelock for the admin role for critical functions to increase decentralization and security.
Deploy and Verify on Testnet
Test the wrapper's integration end-to-end and verify the contract source code.
Detailed Instructions
Deploy your contract to a Layer 2 testnet like Sepolia/Optimism-Goerli or Sepolia/Arbitrum-Sepolia. You must test the full flow: deposit, bridge message initiation, and finalization on the counterparty chain.
- Sub-step 1: Write and run Foundry or Hardhat tests that simulate a user depositing, bridging, and withdrawing. Mock the bridge messenger in unit tests.
- Sub-step 2: Deploy using a script, passing the correct canonical token and bridge addresses for the testnet. For example, the Optimism Goerli L2 Standard Bridge address is
0x4200000000000000000000000000000000000010. - Sub-step 3: Verify the contract source code on the block explorer (e.g., Blockscout for Arbitrum, Optimism Explorer) using the
--verifyflag in your deployment script or the explorer's UI.
bash# Example Foundry deploy command with verification forge create --rpc-url $OPTIMISM_GOERLI_RPC \ --constructor-args 0xDeadBeef... 0x4200000000000000000000000000000000000010 \ --private-key $DEPLOYER_KEY \ src/CustomWrapperL2.sol:CustomWrapperL2 \ --etherscan-api-key $OPTIMISM_ETHERSCAN_KEY \ --verify
Tip: After verification, interact with your live contract via the block explorer to perform a test deposit with a small amount of test tokens to confirm functionality.
Frequently Asked Questions
Further Resources
Ready to Start Building?
Let's bring your Web3 vision to life.
From concept to deployment, ChainScore helps you architect, build, and scale secure blockchain solutions.