This project is a naive but complete educational bridge that provides mainnet-like behavior end-to-end using BuildBear’s Mainnet Sandboxes:
- You’ll spin up two BuildBear sandboxes, one forked from Ethereum Mainnet, another forked from Polygon Mainnet.
- Each sandbox installs the BuildBear Data Feeds plugin, then you attach the WETH/USD Chainlink feed (same addresses as mainnet).
- Contracts are verified via Sourcify (BuildBear Sourcify plugin), and transactions can be inspected with Sentio (BuildBear Sentio plugin) to see exactly what happens under the hood.
- A relayer watches events on one chain and triggers releases on the other.
Why this matters: Instead of mocking oracles and infrastructure, BuildBear lets you test with real-world style addresses and interfaces (feeds, tokens, explorers, debuggers) while keeping everything safe and fast in sandboxes. Perfect for demos, workshops, and educational content.
sequenceDiagram
autonumber
participant U as User (EOA)
participant B1 as Bridge (ETH Sandbox)
participant EV as Event Log
participant R as Relayer (Off-chain)
participant B2 as Bridge (Polygon Sandbox)
participant Rcpt as Receiver (EOA)
U->>B1: lockAndQuote(srcToken, dstToken, srcAmount, to)
Note right of B1: Pulls srcToken (SafeERC20)\nReads WETH/USD from Data Feeds\nComputes dstAmount
B1-->>EV: BridgeRequested(from,to,srcToken,dstToken,srcAmount,dstAmount,nonce,date)
R-->>EV: Poll for BridgeRequested
R->>R: Map dstToken (TOKEN_MAP)
R->>B2: release(mappedDstToken, to, dstAmount, nonce) [admin]
B2->>Rcpt: ERC20 transfer to recipient
B2-->>R: TransferReleased(to, token, amount, nonce, date)
Explanation:
- User calls
lockAndQuoteon ETH sandbox bridge. Contract pulls tokens in, reads Chainlink WETH/USD feed via the Data Feeds plugin, computes the destination amount, and emits aBridgeRequestedevent. - Relayer sees the event and calls
releaseon the Polygon sandbox bridge, transferring the mapped token to the receiver. Nonces prevent double releases.
flowchart TD
A[lockAndQuote] --> B[Transfer tokens in - SafeERC20]
B --> C[Get WETH price from AggregatorV3 - Data Feeds]
C --> D{Token pair?}
D -->|WETH → USDT| E[dst = src * price / 1e8 / 1e12]
D -->|USDT → WETH| F[dst = src * 1e12 * 1e8 / price]
E --> G[Emit BridgeRequested event]
F --> G
G --> H[Increment nonce]
I[release] --> J[Check nonce not processed]
J --> K[Mark nonce as processed]
K --> L[Transfer tokens out]
L --> M[Emit TransferReleased event]
Notes:
-
The Chainlink price is normalized to 1e8 inside
_getWethPrice(). -
Decimal handling:
- WETH(18) → USDT(6):
dst = src * price / 1e8 / 1e12 - USDT(6) → WETH(18):
dst = src * 1e12 * 1e8 / price
- WETH(18) → USDT(6):
-
Bridge.sol: custody + quoting + events + secure
releasewith nonce tracking. -
Foundry scripts: network config, deployment (with pre-funding), interaction.
-
Relayer (Node + ethers v6): event-driven ETH → POL release.
-
BuildBear First-Class:
- Mainnet Sandboxes for ETH & Polygon.
- Data Feeds plugin (attach WETH/USD).
- Sourcify plugin (one-click/CLI verify).
- Sentio plugin for transaction debugging & tracing.
.
├── src/
│ └── Bridge.sol
├── script/
│ ├── HelperConfig.s.sol
│ ├── DeployBridge.s.sol
│ └── InteractBridge.s.sol
├── relayer/
│ └── index.ts
├── lib/ # foundry dependencies
├── broadcast/ # forge script --broadcast outputs
├── package.json # relayer deps & scripts
├── .env.example # copy to .env and fill
├── Makefile
└── foundry.toml
-
Foundry (forge, cast, anvil): https://book.getfoundry.sh/
-
Node.js 18+ and npm
-
BuildBear account
-
Create two Mainnet Sandboxes: one for Ethereum, one for Polygon.
-
Install plugins in both sandboxes:
- Data Feeds → add WETH/USD feed.
- Sourcify (for contract verification).
- Sentio (for transaction debugging).
-
BuildBear sandboxes mirror mainnet contract addresses (e.g., WETH/USDT, Chainlink feeds), giving you realistic addresses and interfaces without the risks of real mainnet.
Copy .env.example → .env.
We’ve prefilled the mainnet-equivalent addresses for WETH/USDT and WETH/USD feeds to save time.
# RPC endpoints (your BuildBear sandbox RPC URLs)
ETH_MAINNET_SANDBOX=
POL_MAINNET_SANDBOX=
# Deployer/admin key & address
PRIVATE_KEY=
WALLET_ADDRESS=
# Chainlink WETH/USD feeds (mainnet addresses)
MAINNET_FEED_WETH_USD=0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419
POLYGON_FEED_WETH_USD=0xF9680D99D6C9589e2a93a78A04A279e509205945
# Token addresses (mainnet addresses; mirrored in BuildBear sandboxes)
ETH_WETH_TOKEN=0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2
ETH_USDT_TOKEN=0xdAC17F958D2ee523a2206206994597C13D831ec7
POL_WETH_TOKEN=0x7ceb23fd6bc0add59e62ac25578270cff1b9f619
POL_USDT_TOKEN=0xc2132D05D31c914a87C6611C10748AEb04B58e8F
# EOA used to perform lockAndQuote
RECEIVER_WALLET=
RECEIVER_PRIVATE_KEY=Need a new wallet? Generate one with Foundry:
cast wallet newYou’ll get a fresh private key and address for
.env.
Make sure your Makefile includes:
.PHONY: make-deploy
install:
forge install && npm i && forge buildRun:
make installThis installs Foundry deps, Node deps, and builds the contracts.
Your Makefile targets (keep as provided):
deploy-mainnet-sourcify:
forge script script/DeployBridge.s.sol \
--rpc-url eth_mainnet_sandbox \
--verifier sourcify \
--verify \
--verifier-url https://rpc.buildbear.io/verify/sourcify/server/eth-to-pol \
--broadcast \
deploy-pol-sourcify:
forge script script/DeployBridge.s.sol \
--rpc-url pol_mainnet_sandbox \
--verifier sourcify \
--verify \
--verifier-url https://rpc.buildbear.io/verify/sourcify/server/pol-to-eth \
--broadcast \Pre-funding check (in DeployBridge.s.sol):
WALLET_ADDRESSmust hold > 1000 WETH (18) and > 25,000 USDT (6) in each sandbox for liquidity transfer to the bridge.- Adjust/remove requires if you want to demo with smaller amounts.
Make targets:
interact-mainnet-bridge:
forge script script/InteractBridge.s.sol \
--rpc-url eth_mainnet_sandbox \
--broadcast \
interact-pol-bridge:
forge script script/InteractBridge.s.sol \
--rpc-url pol_mainnet_sandbox \
--broadcast \If your file still points
interact-pol-bridgeat the ETH RPC, switch it topol_mainnet_sandboxas above.
What happens:
- Script loads the last deployed bridge address from
broadcast/DeployBridge.s.sol/<chainId>/run-latest.json. - Uses
RECEIVER_PRIVATE_KEYto approve and calllockAndQuote(default1e18WETH). - Emits
BridgeRequestedwith computeddstAmount.
Once everything is set up:
# 1. Deploy both bridges (ETH + POL sandboxes)
make deploy-mainnet-sourcify deploy-pol-sourcify
# 2. Start the relayer (listens for BridgeRequested events)
npm start
# 3. Interact with the ETH sandbox bridge (locks tokens, emits event)
make interact-mainnet-bridgeThis flow will:
- Deploy bridges to both ETH and Polygon sandboxes with Sourcify verification.
- Run the relayer, which listens for
BridgeRequestedevents on the ETH sandbox. - Call
lockAndQuotefrom your receiver wallet, which triggers the relayer to release mapped tokens on the Polygon sandbox.
⚠️ Important Note The relayer is a simple script that only processes events while it is running.
- If you call
lockAndQuotewhile the relayer is offline, that event will not be picked up later.- For this demo bridge, you must have the relayer running to process new transactions.
To run the relayer, run:
npm startRelayer behavior:
- Reads latest ETH/POL bridge addresses from
broadcast/*/run-latest.json. - Subscribes/polls for
BridgeRequestedon ETH sandbox. - Maps tokens (using a token mapping) and calls
release()on POL sandbox with the admin key.
Expected console:
ETH Bridge: 0x...
POL Bridge: 0x...
Relayer started. Watching new events...
Detected BridgeRequested: { ... }
Release tx sent: 0x<hash>
Release confirmed.
-
Sourcify plugin: Contracts are verified automatically using the configured command in
Makefile. You can browse verified source in the sandbox explorer. -
Sentio plugin: Open your sandbox’s Sentio view to:
- Trace transactions and fund-flows
- Trace call flows across functions and across contracts
- Inspect token transfers and event logs
- Confirm nonce usage and balances
This is where BuildBear shines: click into traces and logs and build in an environment that resembles mainnet closely, no mocks, no guess-work.
-
No events detected by relayer
- Confirm
BridgeRequestedtopic signature matches exactly. - Ensure you are calling
lockAndQuoteon the same sandbox the relayer is watching. - Check
lastProcessedblock logic and RPC indexing.
- Confirm
-
“Not admin” on
release()- The relayer wallet must be the
admin(bridge deployer) on the destination sandbox.
- The relayer wallet must be the
-
Amounts look off
- Verify addresses for WETH/USDT and feeds. WETH is 18 decimals, USDT 6.
- Feed returns are normalized to 1e8 inside
_getWethPrice().
-
Liquidity errors
- Pre-funding requires balances on
WALLET_ADDRESSin both sandboxes (WETH, USDT). Adjust the thresholds if needed.
- Pre-funding requires balances on
-
Approvals
- Ensure the interacting EOA (receiver) approves the bridge for the correct token and amount before calling
lockAndQuote.
- Ensure the interacting EOA (receiver) approves the bridge for the correct token and amount before calling
This is a trusted bridge built for educational purposes:
- Single trusted relayer (no quorum/multi-sig).
- No cryptographic proofs / light clients / finality checks.
- No slashing, fraud proofs, or MEV mitigation.
- Simple nonce replay protection.
Ideas to extend:
- Router like structure to deploy token-pairs for bridges and allow same chain/multi-chain swap from the bridge
- Threshold/multi-sig relayers or optimistic verification.
- Per-pair fees, slippage guards, and liquidity accounting.
- Support more tokens and feeds; price oracles per token.
- Timeout and refund flows; reorg-aware relayer.