Skip to content

Repository files navigation

Lock-and-mint L2 Testnet Bridge

Minimal unidirectional lock-and-mint ERC20 bridge from Base Sepolia to Arbitrum One Sepolia.

demo img

A user approves and locks an ERC20 token (using TKNX in this demo) into the BridgeLocker contract on chain A, which emits a BridgeLocker__TokensLocked event.

The relayer service polls chain A for these events and, once a lock is sufficiently confirmed (using 5 confirmations for demo purposes), signs an EIP-712 attestation over a Payload that encodes a nonce (to prevent replay attacks), token, amount, recipient, and both chain IDs. The signed attestation is then retrievable by the frontend via the relayer's REST API.

On the frontend, after waiting for the required number of confirmations, the client fetches the attestation and submits it to the BridgeMinter contract on chain B, which verifies the EIP-712 signature, deploys a wrapped/bridged token contract (using bTKNX in this demo) via BridgedTokenFactory if one does not exist yet, and mints the equivalent amount to the recipient.

A rate-limited Faucet is included in the demo so that anyone can test the app.

Features

  • Retrying from failed steps during bridging
  • Better error messages from common transaction errors
  • Automatically resume recent pending transfers
  • Wrapped token deployment is done via CREATE2, which allows predicting the bridged token address of any token
  • Relayer has no persistence but if it gets polled with an unknown attestation nonce, it will query it on-chain and compute the attestation (fallback mode)
  • Pausing capabilities on the contracts allowing for emergency bridge pause
  • Original token metadata (name, symbol, decimals) included in the signed payload so BridgeMinter can deploy the wrapped token on first use without any prior setup on chain B

Extra features already supported by the protocol (contracts and relayer service) but cut from the UI to keep it minimal and in scope:

  • Supports custom recipient
    • mint is permissionless so any user can pay for it
  • Supports any ERC20 to be bridged
    • Rejects tokens with non-standard transfers, e.g. fee-on-transfer tokens, as the locker must receive the exact transfer amount to ensure 1:1 backing on the destination chain
    • Creation of the bridged token contract happens automatically during the first transfer

Tradeoffs

  • Single trusted relayer: simplest to build and operate, the tradeoff is it's a fully trusted intermediary. A compromised relayer key can both censor transfers and forge attestations, minting unbacked wrapped tokens on chain B.
  • One-directional only (lock + mint, no burn + release): halves the contract surface and eliminates a reverse attestation flow. Tradeoff: tokens locked on chain A cannot be returned.
  • In-memory relayer state: no DB to set up, fast to build. Tradeoff: starts tracking from the block at startup, the fallback lookupAndSign handles older nonces by scanning from block 0, but that's an unbounded RPC call that can hit rate limits under load.

Future improvements

Primarily, integrate a database in the relayer service, listening to lock and mint events, generating and storing the attestation/signature as locks come in. Progress would be tracked via a lastCheckedBlock to ensure the relayer wouldn't miss any events due to downtime. That would improve the DApp in several aspects:

  • Currently the relayer relies on querying the blockchain if it doesn't have an attestation in memory, which could rate limit the RPC when polled frequently, causing a DoS. The new method would solve it since all attestation requests would result in a read from a database and not a single RPC call.
  • It would make it trivial to add a list of past transactions for the user, allowing them to resume any past transfer (as opposed to the current limitation of having to check only pending transfers from the past few minutes).

The bridge contracts having pausing functionality with a separate pauser would allow for a watcher service that would monitor the bridge state and if it detected any anomalies, such as minting amounts without having them locked (e.g. via compromised relayer), emergency pause the contracts, which would then be investigated and resumed manually by the owner.

Bridged token deployment could be made cheaper by using Clones.

Getting started

This project has a monorepo format, consisting of a Bun workspace and a Foundry project. It is recommended to run all commands from the root directory.

It has the following structure:

.
├── broadcast - Transaction logs from the contract deployments
├── contracts - Contracts source code
├── client    - The client [Bun package]
├── server    - The relayer service [Bun package]
└── shared    - Shared code between client and server [Bun package]

And the following tech stack:

  • Contracts: Foundry
  • Client: Vite React app, using wagmi and viem for contracts/wallet integration and TailwindCSS and Shadcn for the UI
  • Server: Bun HTTP server, using viem for on-chain interactions

To get started, first install Bun (recommended version 1.3.14) and Foundry (recommended version 1.7.1).

Important

In order to run the DApp with the existing deployments, you will need the relayer private key which is not included in this repository (see server/.env.example).

For that reason, it is recommended to create new deployments, updating all of the addresses accordingly in the deploy scripts, in the shared package and in the server .env file.

Install dependencies:

bun install

Create the .env files from the examples (Foundry env lives in the root directory) and tweak values as needed:

cp .env.example .env
cp client/.env.example client/.env
cp server/.env.example server/.env

Start the relayer:

bun run server:start

And finally start the client:

bun run client:dev

Contracts are fully unit tested. To build:

forge build

To run tests:

forge test

To see the test coverage:

forge coverage

To deploy, refer to Deployment procedure.

Addresses

Deployment procedure

Run the deploy scripts in order (updating the required variables inside them):

forge script contracts/script/01_DeployFaucet.s.sol:DeployFaucet \
  --rpc-url base_sepolia \
  --account beef-dev \
  --sender 0xbeef8A79FF64C0B70e42a2D2daa7Cf8D414db444 \
  --broadcast
# == Logs ==
#   Faucet deployed at: 0xc570f5577B509A287ea78f3cF0e8E18e87A1e00D
forge script contracts/script/02_DeployMockERC20.s.sol:DeployMockERC20 \
  --rpc-url base_sepolia \
  --account beef-dev \
  --sender 0xbeef8A79FF64C0B70e42a2D2daa7Cf8D414db444 \
  --broadcast
# == Logs ==
#   TokenX deployed at: 0xCFA134AC648A11904A35c4B22C95952c2C6574D6
#   TokenX wired to faucet.
forge script contracts/script/03_DeployBridgeLocker.s.sol:DeployBridgeLocker \
  --rpc-url base_sepolia \
  --account beef-dev \
  --sender 0xbeef8A79FF64C0B70e42a2D2daa7Cf8D414db444 \
  --broadcast
# == Logs ==
#   BridgeLocker deployed at: 0x89E39Cb6449A3853Ed78c1715081635B2559E4Ba
forge script contracts/script/04_DeployBridgedTokenFactory.s.sol:DeployBridgedTokenFactory \
  --rpc-url arbitrum_one_sepolia \
  --account beef-dev \
  --sender 0xbeef8A79FF64C0B70e42a2D2daa7Cf8D414db444 \
  --broadcast
# == Logs ==
#   BridgedTokenFactory deployed at: 0xc570f5577B509A287ea78f3cF0e8E18e87A1e00D
forge script contracts/script/05_DeployBridgeMinter.s.sol:DeployBridgeMinter \
  --rpc-url arbitrum_one_sepolia \
  --account beef-dev \
  --sender 0xbeef8A79FF64C0B70e42a2D2daa7Cf8D414db444 \
  --broadcast
# == Logs ==
#   BridgeMinter deployed at: 0xCFA134AC648A11904A35c4B22C95952c2C6574D6
#   BridgedTokenFactory wired to minter.

About

Trusted ERC20 bridge from Base Sepolia to Arbitrum Sepolia

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages