A step-by-step walkthrough of building, deploying, and running a working 2-of-3 multisig bridge between Sepolia and Redbelly Testnet with real transaction proof, full code, and every error I actually hit along the way.
Redbelly Network is a fast, deterministic-finality Layer 1 built for compliant real-world-asset tokenization but like most young chains, it has a liquidity problem. If you're a DeFi user sitting on ETH in a MetaMask wallet, there's no obvious, well-documented path to get that value onto Redbelly and start using it.
I set out to fix that gap for myself, and ended up building Redbridge: a working, testnet-deployed lock-and-mint bridge that moves ETH from Ethereum Sepolia to Redbelly Network Testnet as a wrapped ERC-20 (WETH.rb), secured by a 2-of-3 multisig relayer network.
This article is the guide I wish existed when I started. By the end, you'll understand exactly how a lock-and-mint bridge works, you'll have deployed one yourself on testnet, and you'll know the seven specific errors I hit building this with the exact fix for each, so you don't lose the hours I did.
- Live app: redbridge.test-hub.xyz
- Live bridge history API: api.redbridge.test-hub.xyz/api/bridge-history
- Full source code: https://github.com/0xDarkSeidBull/daotask8
Let's get into it.
Table of contents
- Bridge architecture, explained from scratch
- Prerequisites
- Step-by-step: deploying your own bridge
- Code walkthrough: lock, verify, mint
- Gas cost analysis
- Stuck-fund recovery: support tickets
- Troubleshooting: seven real errors and their exact fixes
- Proof: verified testnet transactions
- Does anything already bridge into Redbelly?
- What I'd build next
1. Bridge architecture, explained from scratch
If you've never looked under the hood of a cross-chain bridge before, the mental model is simpler than it sounds. There is no magic teleportation of tokens between chains that's not how any of this works, on any bridge, ever. What actually happens is:
- You lock an asset on the source chain (it never leaves that chain).
- Some mechanism verifies that the lock genuinely happened.
- An equivalent, wrapped representation of that asset gets minted on the destination chain.
This is called a lock-and-mint bridge, and it's the same fundamental pattern used by Wormhole's Token Bridge, LayerZero's OFT standard, and most bridges you've used without realizing it. The differences between bridges mostly come down to step 2 how do you trustlessly verify that a lock actually happened, without just trusting one central party?
Redbridge's architecture:
Ethereum Sepolia Redbelly Testnet
┌───────────────────┐ ┌──────────────────────┐
│ SepoliaLockVault │ │ WETHBridged (ERC-20) │
│ .sol │ │ .sol │
│ │ watches Locked event │ │
│ lock(recipient) ────┼────► relayer (off-chain) ───►│ confirmMint(...) │
│ payable, locks ETH │ 3 independent │ 2-of-3 multisig gate │
└───────────────────┘ signer processes └──────────────────────┘
│
▼
mints WETH.rb 1:1
to the recipient
Why a relayer, and not a full light client?
The gold-standard answer to "how do you verify a lock happened on another chain" is to run a light client of the source chain inside a smart contract on the destination chain this is genuinely how LayerZero and Wormhole work under the hood, with their own decentralized validator/oracle networks doing that verification at scale.
Building a custom light client for Redbelly's consensus mechanism from scratch is a serious undertaking, well beyond the scope of a reference implementation. So Redbridge uses the next-best, widely-used simplification: a permissioned relayer set. A small number of pre-approved off-chain processes independently watch the source chain, and the destination contract only acts once enough of them agree. This is a real, production-adjacent pattern it's just centralized around a known signer set rather than a fully decentralized validator network.
Why 2-of-3, and not a single relayer?
If a single relayer's private key gets compromised, or the relayer operator simply decides to act maliciously, a 1-of-1 design lets them mint unlimited WETH.rb out of thin air nothing locked on Sepolia would back it.
Requiring 2 of 3 independent signers to agree before a mint executes means a single compromised, offline, or malicious signer can neither mint fraudulently nor halt the bridge alone. It's the same threshold-signature philosophy behind the "guardian networks" you'll see described in Wormhole's and other production bridges' documentation, just scaled down to a size appropriate for a testnet reference build.
Clearing up a common point of confusion: who approves what
When I first built this, my own reflex was to think the user should somehow "approve" their own mint after locking surely there's a second click for them, right? There isn't, and understanding why is the key insight of this whole architecture.
| Role | Who | What they actually do |
|---|---|---|
| Bridge user | Anyone | Calls lock() on Sepolia. That's it full stop. No separate approval, no claim transaction. |
| Relayer signers | 3 pre-configured wallets, set at deploy time via updateRelayerSigner()
|
Independently detect the Locked event, wait for confirmations, and call confirmMint(). Once 2 of 3 have done so, the contract mints automatically. |
If the user could approve their own mint, the entire multisig concept would be pointless anyone could self-approve a fraudulent mint with no independent verification that a lock ever occurred. The separation between "the person who locked funds" and "the independent parties who verify the lock" is the whole point.
2. Prerequisites
Before you start, you'll need:
- Node.js 18 or newer.
- Sepolia ETH in a wallet you control. Get some free from sepoliafaucet.com or any Sepolia faucet.
- Redbelly Testnet RBNT for gas but only in the relayer wallets, not the end-user wallet. Bridging as a user costs zero RBNT; running the relayer processes that approve mints does.
-
An RPC provider for Sepolia whose free tier actually supports
eth_getLogsat usable ranges. This sounds like a minor detail. It is not it is the single biggest time-sink in this whole build, and Section 7.1 below is dedicated to walking through exactly why, with the fix. - Basic familiarity with Hardhat and ethers.js v6.
3. Step-by-step: deploying your own bridge
3.1 Clone and install
git clone https://github.com/0xDarkSeidBull/dao-redbelly.git
cd dao-redbelly
npm install
3.2 Configure your environment
cp .env.example .env
nano .env
The fields that matter most to fill in correctly:
-
DEPLOYER_PRIVATE_KEYdeploys both contracts and becomes the initial owner of each. -
SEPOLIA_RPCread Section 7.1 before picking one. A wrong choice here will cost you an hour of confusing crashes. -
REDBELLY_TESTNET_RPChttps://governors.testnet.redbelly.networkworks out of the box. -
RELAYER_PRIVATE_KEYandRELAYER2_PRIVATE_KEYtwo independent wallets, each funded with a small amount of Redbelly Testnet RBNT for gas.
3.3 Deploy the two contracts
npx hardhat run scripts/deploy-sepolia.js --network sepolia
npx hardhat run scripts/deploy-redbelly.js --network redbellyTestnet
Each script prints a deployed contract address. Copy them into LOCK_VAULT_ADDRESS and WETH_BRIDGED_ADDRESS in your .env.
3.4 Register your relayer signers on-chain
The WETHBridged contract needs to know which three addresses are allowed to call confirmMint(). From the contract owner's account:
npx hardhat console --network redbellyTestnet
const weth = await ethers.getContractAt("WETHBridged", process.env.WETH_BRIDGED_ADDRESS);
await weth.updateRelayerSigner(0, "0xYourSigner1Address");
await weth.updateRelayerSigner(1, "0xYourSigner2Address");
await weth.updateRelayerSigner(2, "0xYourSigner3Address"); // fine to leave unused if you're only running 2 signers
Two active signers are enough to satisfy a 2-of-3 threshold the third slot doesn't need a live relayer process behind it for the bridge to function.
3.5 Run the relayer processes
Each signer runs its own independent process, with its own private key, independently watching the same two contracts. Redbridge runs two of these under pm2:
cp .env .env.signer1 # set RELAYER_PRIVATE_KEY to signer 1's key, SIGNER_LABEL=signer1
cp .env .env.signer2 # set RELAYER_PRIVATE_KEY to signer 2's key, SIGNER_LABEL=signer2
pm2 start ecosystem.config.js
pm2 save
Confirm it's alive:
pm2 logs relayer-signer1 --lines 20 --nostream
You should see it scan a window of recent blocks for any locks it missed, then settle into "listening for new Locked events."
3.6 Send your first bridge transaction
Call lock() on SepoliaLockVault, attaching ETH and specifying your Redbelly recipient address (the exact code is in Section 4.2 below). Within roughly 30 seconds to a couple of minutes depending on your confirmation-depth settings both relayer signers will independently detect the lock, verify it, and call confirmMint(). The moment the second approval lands, WETH.rb appears in the recipient's Redbelly Testnet wallet. No further action required from the user.
4. Code walkthrough: lock, verify, mint
4.1 Setup
const { ethers } = require("ethers");
const sepoliaProvider = new ethers.JsonRpcProvider(process.env.SEPOLIA_RPC);
const wallet = new ethers.Wallet(process.env.USER_PRIVATE_KEY, sepoliaProvider);
const LOCK_VAULT_ABI = [
"function lock(address redbellyRecipient) external payable",
"event Locked(uint256 indexed nonce, address indexed sender, address indexed redbellyRecipient, uint256 amount, uint256 timestamp)",
];
const lockVault = new ethers.Contract(process.env.LOCK_VAULT_ADDRESS, LOCK_VAULT_ABI, wallet);
4.2 Bridge: lock ETH on Sepolia
async function bridgeETH(recipientOnRedbelly, amountEth) {
const tx = await lockVault.lock(recipientOnRedbelly, {
value: ethers.parseEther(amountEth),
});
console.log("Lock tx submitted:", tx.hash);
const receipt = await tx.wait();
const lockedEvent = receipt.logs
.map((log) => {
try { return lockVault.interface.parseLog(log); } catch { return null; }
})
.find((e) => e && e.name === "Locked");
console.log("Locked with nonce:", lockedEvent.args.nonce.toString());
return lockedEvent.args.nonce;
}
bridgeETH("0xRecipientAddressOnRedbelly", "0.01");
That's the entire user-facing interaction. There is no second transaction to send.
4.3 Checking mint status (since minting is automatic, not claimed)
Since there's no "claim" button, checking on your bridge's progress means polling the destination contract's own state:
const redbellyProvider = new ethers.JsonRpcProvider(process.env.REDBELLY_TESTNET_RPC);
const WETH_BRIDGED_ABI = [
"function computeMintKey(uint256 sourceChainId, uint256 sourceNonce) public pure returns (bytes32)",
"function mintRequests(bytes32) public view returns (address recipient, uint256 amount, uint256 approvalCount, bool executed)",
];
const wethBridged = new ethers.Contract(process.env.WETH_BRIDGED_ADDRESS, WETH_BRIDGED_ABI, redbellyProvider);
const SEPOLIA_CHAIN_ID = 11155111;
async function checkMintStatus(lockNonce) {
const mintKey = await wethBridged.computeMintKey(SEPOLIA_CHAIN_ID, lockNonce);
const state = await wethBridged.mintRequests(mintKey);
return {
approvals: Number(state.approvalCount),
executed: state.executed,
};
}
// Poll every 15 seconds until executed === true
There's a subtle but important design decision buried in that snippet: notice I'm reading mintRequests() directly from the contract, rather than parsing the approvalCount out of a decoded event log from a transaction receipt. That wasn't the original design I switched to it after hitting a genuinely confusing bug, which is Section 7.4 below. If you only read one troubleshooting entry in this whole article, make it that one; it generalizes to a lot more than just bridges.
4.4 If you're extending this to ERC-20 tokens instead of native ETH
Redbridge moves native ETH, so there's no ERC-20 approve() step in the base flow. If you fork this to lock a token instead (say, a testnet USDC), the user-facing flow gains exactly one extra step before locking:
const TOKEN_ABI = ["function approve(address spender, uint256 amount) external returns (bool)"];
const token = new ethers.Contract(TOKEN_ADDRESS, TOKEN_ABI, wallet);
const approveTx = await token.approve(LOCK_VAULT_ADDRESS, amountToBridge);
await approveTx.wait();
// then call the vault's lock(token, amount, recipient) equivalent
5. Gas cost analysis
Measured directly on Sepolia and Redbelly Testnet in August 2026. These are gas-unit and native-token figures actual USD cost depends on the ETH/RBNT price and network congestion at the time you transact.
| Operation | Chain | Gas used (approx.) | Why |
|---|---|---|---|
lock() 0.001 ETH |
Sepolia | ~68,000 | A single SSTORE for the incrementing nonce plus the Locked event emission dominate the cost |
lock() 0.012 ETH |
Sepolia | ~68,000 | Identical to the row above gas cost here is amount-independent |
confirmMint() 1st approval |
Redbelly Testnet | ~85,000 | Writes a new mintRequests entry, increments the approval counter |
confirmMint() 2nd approval (triggers the mint) |
Redbelly Testnet | ~140,000 | The extra cost comes from the ERC-20 _mint() call plus the MintExecuted event |
The interesting takeaway here isn't the specific numbers it's that the size of the transfer has zero effect on gas cost. Locking 0.001 ETH and locking 0.012 ETH cost exactly the same, because gas is a function of state changes, not value moved. This has a real design consequence: if this bridge were handling meaningful volume, there's no discount for batching, and a production version would want a batched-mint pattern one confirmMint() call that approves many pending locks at once to amortize that fixed per-transaction overhead across many users. This reference implementation doesn't include that, and it's the first thing I'd build if this moved toward mainnet.
6. Stuck-fund recovery: support tickets
Somewhere around transaction #9 in testing, I hit the scenario every bridge builder dreads: a user's lock sat there, fully confirmed on Sepolia, and just... never minted. No error in the UI, nothing actionable just a transaction that looked stuck, with zero visibility into why, and, at that point, no way for a user to do anything about it except message me directly.
That's a bad place to leave a live product, even a testnet one. So before calling this bridge "done," I built a self-serve recovery path: a support-ticket system, backed by the same bridge-history database, wired directly into the app's Contact Support / Reclaim flow.
Here's what it actually does:
- Opening a ticket takes just a Sepolia lock tx hash and a wallet address. The backend automatically looks up the lock's current on-chain status no manual investigation needed on either side.
-
If the transaction already minted successfully, the ticket resolves itself, instantly, in the same request tagged
resolved_already_bridged, with the destination Redbelly transaction hash attached as proof. This turned out to matter more than I expected: a good chunk of "is my bridge stuck?" reports are, on inspection, transactions that bridged completely normally and the user just didn't see it land. Those now get an immediate, correct answer with zero human involvement. -
Genuinely stuck locks stay
active, with the on-chain status attached, until someone manually resolves them so no context gets lost between "user reports it" and "someone investigates it." - Re-submitting the same transaction while a ticket is already open returns the existing ticket rather than spawning a duplicate.
- A wallet can pull up every ticket it has opened, split into Active and Solved, without needing to keep track of a ticket ID.
The API surface is small and deliberately boring:
POST /api/support-ticket open a ticket (auto-resolves if already minted)
GET /api/support-ticket/:ticketId fetch a single ticket
GET /api/support-ticket/wallet/:address all tickets for a wallet, split Active / Solved
POST /api/support-ticket/:ticketId/resolve admin-only, marks a stuck lock as recovered
The transaction that prompted this nonce #9 is in the proof table in Section 8 below, marked as recovered. It was genuinely stuck for about 22 hours, for the reason covered in Section 7.6 next, and it's now minted, with the recovery flow this section describes having actually been exercised on a real, previously-broken transaction rather than just designed in the abstract.
7. Troubleshooting: seven real errors and their exact fixes
Everything below actually happened during this build. I'm including the raw error text because that's what you'll be pasting into a search bar at 1am, and I want this article to be the thing that search turns up.
7.1 eth_getLogs fails with "archive request" / "requires a personal token"
What you'll see:
{"code":-32602,"message":"Archive requests require a personal token. Get one at: https://www.allnodes.com/publicnode"}
Root cause: Free public RPC endpoints restrict how large a block range a single eth_getLogs call can cover. publicnode.com treats essentially any historical range as an "archive request" and blocks it outright, regardless of how small the range is. Alchemy's free tier is more usable but still caps eth_getLogs at a hard 10-block range per call.
The fix is to stop asking for one big range and instead query in small chunks:
async function getLogsChunked(contract, filter, fromBlock, toBlock, chunkSize = 10) {
const allEvents = [];
for (let start = fromBlock; start <= toBlock; start += chunkSize) {
const end = Math.min(start + chunkSize - 1, toBlock);
const events = await contract.queryFilter(filter, start, end);
allEvents.push(...events);
await new Promise((r) => setTimeout(r, 400)); // stay under free-tier rate limits
}
return allEvents;
}
Also keep your HISTORICAL_LOOKBACK_BLOCKS setting modest (300–500) unless you're paying for a higher RPC tier scanning 10,000 blocks in 10-block chunks means 1,000 sequential requests, and that alone will trip the next error.
7.2 Alchemy 429 "exceeded compute units per second capacity"
What you'll see:
{"code":429,"message":"Your app has exceeded its compute units per second capacity..."}
Root cause: Running multiple relayer processes against the same free-tier API key, each independently chunk-scanning history at the same moment, multiplies your request rate. Two processes doing 400ms-apart chunked requests simultaneously will burst well past a free tier's per-second ceiling.
The fix: the same chunk-delay from 7.1 helps, plus keeping the lookback window small so there's simply less to scan. If you're moving beyond a testnet demo into anything approaching real volume, budget for a paid RPC tier this isn't a workaround you want to lean on in production.
7.3 SqliteError: database is locked (SQLITE_BUSY) on startup
What you'll see:
SqliteError: database is locked
at Database.pragma (.../better-sqlite3/lib/methods/pragma.js:11:44)
code: 'SQLITE_BUSY'
Root cause: Two relayer processes (one per signer) both open the same SQLite file at nearly the same startup moment. If one process is midway through setting journal_mode = WAL exactly when the other tries to open the file, SQLite briefly, correctly reports it as busy, and by default that surfaces as a hard crash instead of a retry.
The fix is a one-line addition: give the connection an explicit busy-timeout so it waits and retries instead of throwing immediately:
const db = new Database(DB_PATH, { timeout: 10000 });
db.pragma("busy_timeout = 10000");
db.pragma("journal_mode = WAL");
7.4 approvalCount silently stored as 0 the one that took longest to find
This is the bug I mentioned back in Section 4.3, and it's worth walking through in a bit more detail because the lesson is more broadly useful than the specific fix.
What I saw: the database was recording every relayer approval with approval_count: 0 even though I could independently verify, by reading the contract's own state directly, that the real, on-chain approval count was correctly progressing (1, then 2).
What was actually happening: my original code parsed the MintApproved event's approvalCount argument straight out of the transaction receipt's decoded logs. In Solidity, uint256 values decode to JavaScript BigInt under ethers v6. Under conditions I still can't fully explain, it worked fine in isolated debugging scripts but not in the live relayer process; that decoded value occasionally came back undefined, which turned Number(undefined) into NaN. And here's the part that made this genuinely hard to catch: better-sqlite3 doesn't throw on a NaN integer-bind parameter. It silently coerces it to 0. No error, no warning, no crash just a quietly wrong number sitting in the database until I happened to cross-check it against the chain directly.
The fix and the generalizable lesson: stop trusting a value derived from log-parsing when the same value is available as a direct, authoritative contract read.
const freshState = await wethBridged.mintRequests(mintKey);
const approvalCount = Number(freshState.approvalCount); // authoritative not derived from logs
const executed = freshState.executed;
It costs one extra RPC call. It eliminates an entire category of "my decoded event argument is subtly wrong, and I have no idea why" bugs. If you take away nothing else from this whole troubleshooting section: whenever you have a choice between parsing something out of an event log and reading it directly from contract state, read it directly.
7.5 pm2 restart silently ignores your .env changes
What you'll see: you update SEPOLIA_RPC in .env, run pm2 restart <name>, and the logs keep showing requests going to the old RPC URL as if nothing changed.
Root cause: if your ecosystem.config.js reads process.env.* at file-load time (via a top-level require("dotenv").config()) and copies those values into each app's env object, Node caches that require() call the very first time it runs. pm2 restart restarts the process it does not re-execute ecosystem.config.js. So it keeps reusing whatever environment values were baked in at the last pm2 start ecosystem.config.js, no matter how many times you restart after that.
The fix has two valid versions. The reliable one, which I ended up using: skip ecosystem.config.js's environment-copying entirely, and have each process load its own .env file fresh, directly, at the moment it actually starts, via a tiny shell wrapper:
#!/bin/bash
# start-signer1.sh
set -a
source /path/to/.env.signer1
set +a
exec node /path/to/relayer/relayer.js
// ecosystem.config.js
module.exports = {
apps: [
{ name: "relayer-signer1", script: "./start-signer1.sh" },
{ name: "relayer-signer2", script: "./start-signer2.sh" },
],
};
This guarantees the environment gets read fresh from disk every single time the process actually starts, completely independent of any caching behavior in pm2 itself. (The cruder but valid alternative: always pm2 delete <name> followed by a fresh pm2 start ecosystem.config.js after any .env edit, never just restart.)
7.6 A stuck lock gets permanently skipped as "possible reorg" but it wasn't a reorg
This is the bug behind nonce #9, the transaction discussed in Section 6 above, and it's the one that worried me most not because it was hard to fix, but because of how it failed.
What I saw: nonce #9's lock sat fully confirmed on Sepolia, but the relayer logs showed something oddly final: Lock #9 not verifiable post-confirmation (possible reorg) -- skipping. Once a relayer logs that, it means "give up permanently," not "try again later." Except there was no reorg. The lock was sitting right there on Sepolia, completely untouched, hours later.
Root cause: the verification function, getLogsChunked(), was designed to retry on RPC failures and eventually give up after three attempts reasonable so far. But when it gave up, it didn't throw. It logged the failure and quietly returned whatever partial (sometimes completely empty) results it had collected, as if that were a complete, successful query. The caller, verifyLockStillValid(), had no way to distinguish "I successfully checked and found nothing" from "I failed to check at all." Both cases produced an identical empty result, and an empty result was interpreted as "the lock got reorged out skip it forever."
That's a dangerous class of bug, because it fails both silently and permanently. A transient RPC hiccup the exact kind Sections 7.1 and 7.2 are all about was enough to permanently orphan a real, valid, fully-locked user transaction.
The fix: make the failure loud instead of silent. Have getLogsChunked() throw once its retries are genuinely exhausted, and have the caller treat a thrown error completely differently from a clean, successful, empty result.
if (attempt >= 3) {
console.error(`getLogs chunk [${start}-${end}] failed after 3 attempts:`, err.message);
throw new Error(`getLogsChunked exhausted retries for [${start}-${end}]: ${err.message}`);
}
A thrown error now means "ask again later" the lock gets re-queued with a 30-second backoff instead of abandoned. Only a clean, successful query that genuinely returns zero matching events gets to call itself a reorg. This is the fix that eventually let nonce #9 recover on its own, and it's also what powers the "genuinely stuck" side of the support-ticket system in Section 6.
7.7 The verification window checks blocks that haven't been mined yet
What I saw: even after the fix above, every single fresh lock not just ones hitting RPC trouble went through one unnecessary retry cycle before verifying successfully. Consistently. On every transaction, not occasionally.
Root cause: the verification window was calculated as originalBlockNumber + CHUNK_SIZE (10 blocks past the lock), but CONFIRMATION_BLOCKS was set to just 2. The relayer would attempt verification only 2 blocks after the lock, while asking for a window extending 10 blocks past it a range that, at that point, simply didn't exist on chain yet. The relayer was asking Sepolia for blocks from the future. This wasn't an occasional RPC problem; it was a structural mismatch between two config values that were each individually reasonable, but incompatible together.
The fix: cap the query window at the chain's actual current head, never past it.
const currentHead = await sepoliaProvider.getBlockNumber();
const windowEnd = Math.min(originalBlockNumber + CHUNK_SIZE, currentHead);
With that in place, verification succeeds on the first attempt instead of self-healing through the retry queue 90–100 seconds later. It's a small fix, but it's the difference between "the recovery system quietly compensates for a structural bug on every single transaction" and "the structural bug doesn't exist."
8. Proof: verified testnet transactions
Talk is cheap on a crypto blog; here are real, independently checkable transactions from this exact bridge all bridged in this testing cycle (19–20 August 2026). Nonce #9 is deliberately included: it's the transaction discussed in Sections 6 and 7.6 above, genuinely stuck for roughly 22 hours before the fix, and now minted proof the recovery path works end to end on a real transaction, not just in a design doc.
| Lock nonce | Sepolia lock tx | Redbelly mint tx | Amount | Status |
|---|---|---|---|---|
| #17 | 0xbef7522a...b91893a |
0x815e7c58...f2345c |
0.001 ETH | ✅ Minted |
| #16 | 0x84b13ca3...d05737 |
0x91f30864...d7774a |
0.001 ETH | ✅ Minted |
| #15 | 0xc3d52399...19b657 |
0x1cca9aa1...b44a03 |
0.0011 ETH | ✅ Minted |
| #14 | 0x48a5267f...5381b7 |
0xebac3c36...082ffc |
0.0012 ETH | ✅ Minted |
| #13 | 0x626c2af8...3ba878 |
0xe9a6e522...a48855 |
0.001 ETH | ✅ Minted |
| #9 | 0x2b370c4f...a7889e |
0xe9e8f29f...4891ce |
0.1 ETH | ✅ Minted recovered from a stuck state (Section 7.6) |
Deployed contracts:
-
SepoliaLockVault(Ethereum Sepolia):0x130d07624d00DF30A5C30C3D237fD5d99A3DdE11 -
WETHBridged(Redbelly Testnet):0x11Bef97d2d2063b41887A76403B852b52D151501
If you want to see the full, live, continuously-updating history not just the six rows above, but every transaction any user has bridged the backend exposes it publicly:
GET https://api.redbridge.test-hub.xyz/api/bridge-history
(Screenshot placeholder: insert a screenshot of the Redbridge app's Transfer Status panel showing a completed bridge, and a second screenshot of the Bridge History table, here.)
9. Does anything already bridge into Redbelly?
Part of building this meant actually checking, rather than assuming, whether major cross-chain protocols already support Redbelly natively because if one does, you may not need to build a custom relayer at all. Here's what I found by checking each protocol's own current, live chain-list documentation directly (not blog posts or year-old summaries, which turned out to be stale in more than one case):
| Protocol | Native Redbelly support? |
|---|---|
| LayerZero | ✅ Yes on Mainnet. LayerZero's own docs list a dedicated "Redbelly Mainnet" deployment page and an "OFT Quickstart" guide for it, right alongside every other chain they support. Of everything checked, this is the clearest path to a production-grade Redbelly bridge without maintaining your own relayer infrastructure. |
| Axelar | ❌ No confirmed support in their chain-connectivity documentation or ecosystem listings. |
| Wormhole | ❌ No. Their official Supported Networks reference table (dated July 2026, spanning all five of their products) lists roughly 40 chains; Redbelly isn't among them. This is the most exhaustive and current source of the four, so it's a high-confidence "no." |
| Connext | ⚠️ Unconfirmed. Connext adds EVM-compatible chains on request via Discord rather than publishing a static support list, so this can't be ruled in or out from documentation alone. |
| Multichain | ⛔ Not applicable. Multichain (formerly Anyswap) shut down in 2023. |
A quick correction worth flagging, since it's an easy mistake to make: Redbelly Mainnet is chain ID 151, and Redbelly Testnet is chain ID 153. Mixing these up when configuring an RPC or a bridge contract will cost you a confusing debugging session.
Four other integrations turned out to be more immediately relevant than the protocols above:
- Celer's cBridge added Redbelly Network support in August 2025, connecting it with Ethereum and BNB Chain.
- Redbelly's own official bridge, reddex.io/bridge, is what Redbelly's own X account points users toward for bridging USDC/USDT into the network, describing it as their official and exclusive DEX.
- Lucid Labs Bridge is what Redbelly's own developer docs recommend for the return path bringing RBNT from a non-Redbelly chain back to Redbelly. It also ships a "Resend Transaction" feature, for retrying a transfer that gets stuck mid-relay without contacting support first, which is the same instinct behind Section 6's ticket system above.
- Router Protocol's Nitro and Polymer aren't bridges you'd pick directly, but they're the infrastructure behind the existing wrapped-RBNT deployments: Nitro powers the Solana wrapped-RBNT bridge, Polymer powers the Ethereum one.
Given that LayerZero is confirmed live on Redbelly Mainnet, the most natural "version two" of this project isn't more custom relayer code; it's replacing the relayer network entirely with LayerZero's own DVN/Executor infrastructure, while keeping a similar lock-and-mint (or OFT-based) contract pattern. That's genuinely the next thing I'd build.
10. What I'd build next
This is a testnet reference implementation, not a production system, and I want to be direct about where the real gaps are rather than overselling it:
- No batched minting. As covered in Section 5, gas cost is currently linear in the number of transactions with no economy of scale. At meaningful volume, that's the first thing to fix.
- Native ETH only. Extending to arbitrary ERC-20 tokens is a well-understood next step (Section 4.4 sketches what changes), but it isn't built yet.
- Permissioned relayer set, not a decentralized validator network. This is an honest architectural trade-off for a reference build, not a production security model; see Section 9 for why LayerZero's existing Redbelly deployment is the more credible path to something trustless at scale.
If you build on top of any of this, or find an eighth error I haven't hit yet, I'd genuinely like to hear about it.
Full source code, deployment scripts, tests, and the exact troubleshooting entries above (kept in sync with this article) live in the GitHub repository: https://github.com/0xDarkSeidBull/daotask8. Live app at redbridge.test-hub.xyz.
Written as a submission for TASK-08 (Existing Bridge Integration Guide) on the Redbelly DAO Task Board.

Top comments (0)