# BuildBear Tutorial:  Cross-Chain Bridge

> This is post was written by [@0xJustUzair](https://x.com/0xJustUzair)

BuildBear is a specialized platform for DApp development and testing. Developers can create a personalized private testnet sandbox for various EVM and EVM-compatible blockchain networks.

**Key Features**

* Access a private faucet for unlimited minting of native and ERC-20 tokens.
    
* Experience lightning-fast transactions, completing in under three seconds.
    
* Debug transactions effortlessly within your sandbox using the built-in explorer and transaction tracer.
    
* Leverage real-time testing and debugging tools, along with many other advanced features.
    

## Sign in to the BuildBear Dashboard

Sign in using your GitHub, Google, or a Web3 wallet.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759747813501/0b2e5b22-015b-4631-99c6-7fd57600f87c.png align="left")

Once you're logged in, you'll see a page similar to the image below.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759747823747/7e920dbe-e9d7-4932-b268-7d4ad30522ec.png align="left")

## Create a Sandbox

Discover how to set up a sandbox on BuildBear for seamless DApp development and testing. Customize your environment, fork from supported blockchains, and access essential tools for debugging and verification.

### Create Your BuildBear Sandbox

Click **Create a Sandbox** to start configuring your test blockchain.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759747853748/c22abd0e-edc3-4595-8fc8-75c615814044.png align="left")

### Forking Options

This feature enables you to create a sandbox, forked from the state of any supported blockchain, at any block number, with a randomly generated chain ID.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759747871108/f12d7511-d6af-4a27-a1bd-cd5b07ffe3d7.png align="left")

You can also create a test blockchain without forking from an existing chain.

#### Advanced Options

When creating a Sandbox, you can configure additional options to customize your environment:

* **Deploy Large Contracts**: Enable contracts larger than the default 24KB limit.
    
* **Unlocked Accounts**: Access unlocked accounts for easier script execution.
    
* **Mnemonic**: Generate a unique mnemonic phrase for your testnet.
    
* **Override Gas Estimation**: Detect and override gas estimation failures.
    
* **Impersonation Transactions**: Allow transactions to be sent from any address for testing purposes.
    
* **Snapshots**: Take snapshots of the blockchain state for easy rollbacks.
    
* **Block Mining & Timestamp Manipulation**: Manually mine blocks and adjust timestamps.
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759747888176/1b5171e6-f137-47f2-806f-218fecc280aa.png align="left")

Configure these options based on your development needs to optimize testing and debugging.

### Your RPC Is Ready for Use

After clicking **Create**, select **Go to Dashboard** to access your newly created sandbox.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759747904876/dd30b476-2d18-49b0-8829-b4f3b8a42acb.png align="left")

On the **Dashboard**, you’ll find essential details and tools to manage your test environment efficiently.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759747965110/ac3a726e-db88-4097-8ccd-493becc4ab31.png align="left")

Each sandbox ships with a funded mnemonic. To extract the first account’s key:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759747982327/959cc7e3-726f-4f8a-8fbf-533c9092a502.png align="left")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748009170/fb24df82-333c-4815-9248-b8df7e562f22.png align="left")

`cast wallet address PRIVATE_KEY`

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748020929/ba9e0200-3b6a-407b-b3fc-879f738aa1cf.png align="left")

Use the wallet address to obtain funds from your sandbox faucet.

## Faucet

The BuildBear Faucet provides an easy way to acquire native and ERC-20 tokens for your testnet sandbox. Whether you’re testing transactions or deploying smart contracts, you'll need tokens for gas fees. Use the faucet to mint both native tokens and ERC-20 tokens to fund your development efforts.

Start by selecting the type of token you need (native or ERC-20), and you’ll receive the required tokens instantly. The faucet ensures a seamless experience for developers testing on BuildBear’s platform.

## Get ERC20 Tokens

The BuildBear Faucet allows you to request testnet tokens for your blockchain projects, enabling smooth testing and development.

### Add ERC-20 Tokens

Click **Faucet** in the left navigation pane.

Use the faucet to mint ERC-20 tokens for your testing needs.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748111035/f4ffa87a-ee21-4568-9d21-08d090382e3d.png align="left")

The faucet provides a list of supported ERC-20 tokens. You can select the token you wish to mint from the list.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748114622/bb739789-7a8f-472b-9b5b-5432104a8c7b.png align="left")

## Get Native Tokens

The BuildBear Faucet allows you to request testnet tokens for your blockchain projects, enabling smooth testing and development.

### Add Native Tokens or ERC-20 Tokens

Click **Faucet** in the left Navigation Pane.

To perform transactions on any sandbox, you’ll need native tokens for gas fees. Use the faucet to acquire them.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748136794/b09c4ce3-62cd-4df0-a804-113418c5e81a.png align="left")

## Plugins Installation

Install the required plugins from the **BuildBear Plugin Marketplace.**

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748150778/bc31e98b-74c5-4570-b618-b23db37e9d0a.png align="left")

## Contracts Verification API

Verify your smart contracts and fetch verified contract artifacts via BuildBear’s unified verification endpoints.

### Supported Verification Types

**Foundry**

* Etherscan
    
* Sourcify
    

**Hardhat**

* Etherscan
    
* Sourcify
    

Refer to the following doc to learn to Verify Contracts on BuildBear Sandboxes.

[Docs - Contracts Verification API - BuildBear Labs](https://www.buildbear.io/docs/api-reference/contracts-verification-api?utm_source=devdao&utm_medium=blog&utm_campaign=blog-cross-chain-bridge)

## BuildBear Explorer

BuildBear Explorer provides an **intuitive and powerful interface** for monitoring blockchain transactions. Whether you’re debugging smart contracts or tracking transaction history, the explorer offers real-time insights and seamless navigation.

### Key Features

* **Transaction Monitoring** – View pending and confirmed transactions across multiple explorers (**BuildBear, BlockScout, and Ethernal**).
    
* **Detailed Transaction Insights** – Access transaction details, including hash, block number, sender/receiver addresses, and gas usage.
    
* **Advanced Debugging Tools** – Utilize **Sentio Tracer, Sentio Debugger, and Simbolik Debugger** for in-depth contract execution analysis.
    
* **External Explorer Support** – Open transactions in third-party explorers directly from BuildBear.
    

## Monitoring Transactions on BuildBear Explorer

Click **Explorer** in the left Navigation Pane.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748175518/53488154-949f-4362-ad13-619b237e7170.png align="left")

A list of transactions will be displayed. You can filter the transactions by **All**, **Pending**, and **Confirmed**.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748184199/4d0fe3f2-21c5-40be-96d5-586b018bd0e6.png align="left")

You can also expand the **Explorer** menu to select a default explorer from **BuildBear**, **BlockScout**, or **Ethernal**.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748195519/4ca37cfe-4733-4941-91ec-f2e00fbe522a.png align="left")

Click on a transaction to view its details. The transaction details page includes information such as the transaction hash, block number, sender and receiver addresses, etc.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748210100/9084edd0-18bf-4b35-9c25-8995677a1a39.png align="left")

### Transaction Debugging

The **Transaction Details** page provides essential debugging tools, including **Sentio Tracer**, **Sentio Debugger**, and **Simbolik Debugger,** to analyze transaction execution.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748219960/b8a7b32d-6ef5-45b8-8841-0447c7ec4627.png align="left")

At the top, next to **View all Transactions**, you’ll find a drop-down menu to open the current transaction’s details in an external explorer of your choice.

---

## Building a Cross-Chain Bridge with Node Relayer on BuildBear

We will build a minimal, event-driven bridge that works end-to-end across two BuildBear Mainnet Sandboxes (Ethereum and Polygon).  
Instead of mocks, we will attach real Chainlink ETH/USD feeds via the BuildBear Data Feeds plugin, verify contracts with Sourcify, and debug transactions with Sentio, all in BuildBear's Sandboxes.

### What We Will Learn

* Setting up two BuildBear Mainnet Sandboxes (ETH and Polygon)
    
* Installing the Data Feeds plugin and attaching the WETH/USD feed on both Sandboxes
    
* Preparing environment variables and Foundry configs
    
* Deploying the Bridge with Foundry and verifying via Sourcify
    
* Running a bi-directional relayer to detect and release assets on the destination chain
    
* Interacting with the bridge (WETH ⇄ USDT) and inspecting results
    
* End-to-End transaction debugging with Sentio and BuildBear explorer
    

## Project Repository

You can either use the repository provided by us or create your own `Foundry Project`. Here, we will use the repository that we have provided.

[GitHub - BuildBearLabs/tutorial-buildbear-datafeed-bridge: Build a minimal event-driven bridge (WETH ⇄ USDT) using Foundry and Node relayer, fully built in BuildBear Mainnet Sandboxes with the Chainlink DataFeed Plugin.](https://github.com/BuildBearLabs/tutorial-buildbear-datafeed-bridge)

### How it Works

The following snapshots provide a quick, visual understanding of the system before diving into the code. Add your rendered-mermaid images where indicated.

1. High-level sequence
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748249085/0abc574b-5692-4775-b1d7-29844df3e4d6.png align="left")
    
    1. The user approves and calls `lockAndQuote` on the source bridge.
        
    2. The bridge pulls the source token, reads WETH/USD from the data feed, computes the destination amount, and emits `BridgeRequested`.
        
    3. The relayer observes that event on the source chain and calls `release` on the destination bridge with the mapped token, amount, and nonce.
        
    4. The destination bridge checks the nonce, transfers tokens to the recipient, and emits `TransferReleased`.
        
    5. The destination side is pre-funded, and the relayer must be running for events to be processed.
        
2. Contract internals
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748260166/24d88d6c-33e3-47c6-82ee-22af7f7911af.png align="left")
    
    1. `Lock-and-Quote` Functionality
        
        1. Take custody of the source token via `safeTransferFrom`.
            
        2. Read WETH/USD from the data feed and normalize for math.
            
        3. Handle decimals and calculate the destination amount.
            
        4. Emit `BridgeRequested`.
            
        5. Increment `nonce`.
            
    2. `Release` Functionality
        
        1. Enforce admin-only access.
            
        2. Ensure the external nonce is unused and mark it processed.
            
        3. Transfer tokens out to the recipient.
            
        4. Emit `TransferReleased`.
            

## Directory and Project Structure

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748277426/401062b1-8631-484b-981f-7539bf002a16.png align="left")

## Create Sandboxes and Install Plugins

1. Create two Mainnet Sandboxes:
    

* Ethereum Mainnet
    
* Polygon Mainnet
    

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>Use the code </strong><code>DD-BB-BLOG</code><strong> to receive 1 month of free BuildBear Premium. Activate your premium access from </strong><a target="_self" rel="noopener noreferrer nofollow" href="https://app.buildbear.io/settings/billing/?utm_source=devdao&amp;utm_medium=blog&amp;utm_campaign=blog-cross-chain-bridge" style="pointer-events: none"><strong>here</strong></a><strong> and start enjoying the benefits immediately.</strong></div>
</div>

2. Install the following plugins on both Ethereum and Polygon Mainnet Sandboxes:
    

* Installing and configuring the Chainlink Data Feed Plugin.
    
    * Install the "Chainlink Data Feed" Plugin from the Plugin Marketplace.
        
        ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748294780/7a4e7d37-c7b2-495e-8b4c-526541ccb2be.png align="left")
        
    * Search and Subscribe to the "ETH/USD" feed.
        
        ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748306766/28018c45-ab09-49d8-afed-0c71e0c44ed4.png align="left")
        
    * Once you have installed the Data-Feeds on both the Sandboxes, verify that your feed addresses match the addresses below:
        
        * Ethereum Mainnet (ETH/USD Feed): `0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419`
            
        * Polygon Mainnet (ETH/USD Feed): `0xF9680D99D6C9589e2a93a78A04A279e509205945`
            

> You can also visit [Chainlink Data Feed Dashboard](https://app.buildbear.io/data-feeds?utm_source=devdao&utm_medium=blog&utm_campaign=blog-cross-chain-bridge) to see your subscribed feeds, and add/remove feeds.

Also, install `Sentio`, `Simbolik` & `Sourcify` Plugins from BuildBear Plugin Marketplace.

> Simbolik relies on **Sourcify-verified** source code to debug contracts. It currently works best for **unoptimized contracts** as there can be limited debugging information and inconsistencies, with optimized builds. Support for optimized contracts is improving with Solidity’s debugging tooling.

Before running any transactions:

* Go to your Sandbox's **Plugins** tab
    
* Look for **Sourcify,** **Simbolik, and Sentio** plugins\*\*.\*\*
    
* Click **Install Plugin**
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748321574/c0c2d05b-e82f-4270-b9aa-74b6744e291c.png align="left")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748331356/23ee69e2-b670-4226-b6d9-2aaae18fe6fd.png align="left")

## [Environment Variables & Wallet Setup](https://www.buildbear.io/docs/tutorials/chainlink-datafeed-bridge?utm_source=devdao&utm_medium=blog&utm_campaign=blog-cross-chain-bridge#step-2-environment-variables--wallet-setup)

Create a basic Foundry Project, and copy `.env.example` to `.env` and fill in values or use the env example below. We have some values pre-filled to save you some time sleuthing for addresses. These mirror Mainnet token addresses and price feed addresses that BuildBear exposes inside the Sandboxes after the plugins are installed and configured.

```bash
# RPC endpoints (your BuildBear Sandbox RPC URLs) 
ETH_MAINNET_SANDBOX=<YOUR_ETH_MAINNET_SANDBOX_URL> 
POL_MAINNET_SANDBOX=<YOUR_POLYGON_MAINNET_SANDBOX_URL> 

# 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=
```

### `foundry.toml`

```ini
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
# required to dynamically read and interact with latest bridge addresses
fs_permissions = [{ access = "read", path = "./broadcast/DeployBridge.s.sol/" }]
remappings = ['@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts']
[rpc_endpoints]
eth_mainnet_sandbox = "${ETH_MAINNET_SANDBOX}"
pol_mainnet_sandbox = "${POL_MAINNET_SANDBOX}"
```

We also need some `npm packages` for the relayer to work, refer to the `package.json` file or put the contents below in it:

```json
{
  "scripts": {
    "start": "npm run start-relayer",
    "start-relayer": "tsx ./relayer/index.ts"
  },
  "dependencies": {
    "dotenv": "^16.3.1",
    "ethers": "^6.13.5",
    "permissionless": "^0.2.47",
    "viem": "^2.30.0"
  },
  "devDependencies": {
    "@types/node": "^20.11.10",
    "tsx": "^3.13.0"
  }
}
```

**Need a new wallet keypair? Create one with** `cast`

```bash
cast wallet new
```

---

### Funding your Deployer & Receiver Wallet

Once you have configured your wallets, you will need to fund your wallets with enough funds to:

* Cover deployment and interaction gas costs.
    
* Fund the bridges while deployment from `deployer` to the Bridge
    
    * Requires `deployer` to hold more than `1000` WETH and `25000` USDT.
        
* Fund the receiver's wallet with some `WETH` to allow interaction with Bridge.sol and bridge the assets.
    

> **Why do we need to pre-fund wallet-addresses?**
> 
> The relayer's call to release funds depends on the destination bridge already holding the mapped token. Without initial liquidity, `release` would fail due to insufficient balance.

---

## Creating Contracts & Scripts

### `Bridge.sol` Contract

**Roles and Storage**

* `admin`: deployer; the only permitted caller to `release`.
    
* `nonce`: increments per request; included in `BridgeRequested`.
    
* `processedNonces`: marks opposite-chain nonces as used to prevent re-release.
    
* `wethToken`, `usdtToken`: ERC-20s handled on this chain.
    
* `wethUsdFeed`: Chainlink WETH/USD aggregator.
    

**Price & Quotes**

* `_getWethPrice()` reads Chainlink and normalizes the result to `1e8` decimals for stable math.
    
* Pulls `srcAmount` in via `safeTransferFrom`.
    
* Gets WETH/USD price once & computes `dstAmount`
    
* Emits `BridgeRequested(...)` and increments `nonce`.
    

**Assets Release Post-Bridging**

* Triggered by the relayer with `onlyAdmin` restriction and check for unused `processedNonces[externalChainNonce]`, and marks it used.
    
* Transfers `amount` of `token` to `to`.
    
* Emits `TransferReleased(...)`.
    

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

interface AggregatorV3Interface {
    function decimals() external view returns (uint8);
    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80);
}

contract Bridge {
    using SafeERC20 for IERC20;

    address public admin;
    uint256 public nonce;
    mapping(uint256 => bool) public processedNonces;

    address public wethToken;
    address public usdtToken;
    address public wethUsdFeed;

    event BridgeRequested( // still present, but will be filled by relayer mapping
        address indexed from,
        address indexed to,
        address srcToken,
        address dstToken,
        uint256 srcAmount,
        uint256 dstAmount,
        uint256 nonce,
        uint256 date
    );

    event TransferReleased(address indexed to, address token, uint256 amount, uint256 externalChainNonce, uint256 date);

    modifier onlyAdmin() {
        require(msg.sender == admin, "Not admin");
        _;
    }

    constructor(address _weth, address _usdt, address _wethUsdFeed) {
        admin = msg.sender;
        wethToken = _weth;
        usdtToken = _usdt;
        wethUsdFeed = _wethUsdFeed;
    }

    function _getWethPrice() internal view returns (uint256) {
        AggregatorV3Interface feed = AggregatorV3Interface(wethUsdFeed);
        (, int256 answer,,,) = feed.latestRoundData();
        require(answer > 0, "invalid answer");

        uint8 dec = feed.decimals();
        uint256 price = uint256(answer);

        if (dec > 8) price = price / (10 ** (dec - 8));
        else if (dec < 8) price = price * (10 ** (8 - dec));

        return price; // 1e8
    }

    function lockAndQuote(address srcFromToken, address srcToToken, uint256 srcAmount, address to) external {
        IERC20(srcFromToken).safeTransferFrom(msg.sender, address(this), srcAmount);

        uint256 dstAmount;

        uint256 wethPrice = _getWethPrice(); // 1e8

        if (srcFromToken == wethToken && srcToToken == usdtToken) {
            // WETH → USDT
            dstAmount = (srcAmount * wethPrice) / 1e8 / 1e12; // adjust 18→6 decimals
        } else if (srcFromToken == usdtToken && srcToToken == wethToken) {
            // USDT → WETH
            dstAmount = (srcAmount * 1e12 * 1e8) / wethPrice; // adjust 6→18 decimals
        } else {
            revert("unsupported token pair");
        }

        emit BridgeRequested(msg.sender, to, srcFromToken, srcToToken, srcAmount, dstAmount, nonce, block.timestamp);
        nonce++;
    }

    function release(address token, address to, uint256 amount, uint256 externalChainNonce) external onlyAdmin {
        require(!processedNonces[externalChainNonce], "already processed");
        processedNonces[externalChainNonce] = true;

        IERC20(token).safeTransfer(to, amount);

        emit TransferReleased(to, token, amount, externalChainNonce, block.timestamp);
    }
}

interface IERC20Metadata is IERC20 {
    function decimals() external returns (uint8 decimals);
}
```

### Foundry Scripts & Their Usage

#### `HelperConfig.s.sol`

* Defines a `NetworkConfig` struct that carries:
    
    * `deployerKey` (private key pulled from env),
        
    * `wethToken`, `usdtToken`,
        
    * `wethUsdFeed`,
        
    * `deployer` (EOA that will pre-fund the bridge).
        
* Chooses `activeNetworkConfig` based on `block.chainid`:
    
    * `1` for the Ethereum Sandbox,
        
    * `137` for the Polygon Sandbox.
        
* All addresses and keys are read, `vm.env*` so you configure once in `.env`.
    

When to reference it:

* Deployment and interaction scripts are used `HelperConfig` to get chain-specific parameters without branching logic scattered across scripts.
    

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {Script} from "forge-std/Script.sol";

contract HelperConfig is Script {
    struct NetworkConfig {
        uint256 deployerKey;
        address wethToken;
        address usdtToken;
        address wethUsdFeed;
        address deployer;
    }

    NetworkConfig public activeNetworkConfig;

    constructor() {
        if (block.chainid == 1) {
            activeNetworkConfig = getEthMainnetConfig();
        } else if (block.chainid == 137) {
            activeNetworkConfig = getPolygonMainnetConfig();
        } else {
            revert("Unsupported network");
        }
    }

    function getEthMainnetConfig() public view returns (NetworkConfig memory) {
        return NetworkConfig({
            deployerKey: vm.envUint("PRIVATE_KEY"),
            wethToken: vm.envAddress("ETH_WETH_TOKEN"), // BuildBear faucet weth token
            usdtToken: vm.envAddress("ETH_USDT_TOKEN"), // BuildBear faucet USDT token
            wethUsdFeed: vm.envAddress("MAINNET_FEED_WETH_USD"),
            deployer: vm.envAddress("WALLET_ADDRESS")
        });
    }

    function getPolygonMainnetConfig() public view returns (NetworkConfig memory) {
        return NetworkConfig({
            deployerKey: vm.envUint("PRIVATE_KEY"),
            wethToken: vm.envAddress("POL_WETH_TOKEN"), // BuildBear faucet weth token
            usdtToken: vm.envAddress("POL_USDT_TOKEN"), // BuildBear faucet USDT token
            wethUsdFeed: vm.envAddress("POLYGON_FEED_WETH_USD"),
            deployer: vm.envAddress("WALLET_ADDRESS")
        });
    }
}
```

#### `DeployBridge.s.sol`

* Starts a broadcast with `deployerKey`.
    
* Deploys `Bridge(wethToken, usdtToken, wethUsdFeed)`. The deployer becomes `admin`.
    
* Pre-funding from `deployer` to the deployed bridge:
    
    * Requires `deployer` to hold more than `1000e18` WETH and `25_000e6` USDT.
        
    * Transfers a fixed seed amount to the bridge (`1000e18` WETH and `25_000e6` USDT).
        
        * Lower or remove the `require` thresholds to demo with smaller balances.
            
        * Add more tokens or feeds by extending constructor arguments and storage.
            

> **Why the pre-funding check exists?**
> 
> The relayer's call to release funds depends on the destination bridge already holding the mapped token. Without initial liquidity, `release` would fail due to insufficient balance.

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {Script} from "forge-std/Script.sol";
import {console2} from "forge-std/console2.sol";

import {HelperConfig} from "script/HelperConfig.s.sol";
import {Bridge, IERC20, SafeERC20} from "src/Bridge.sol";

contract DeployBridge is Script {
    using SafeERC20 for IERC20;

    function run() external {
        (uint256 deployerKey, address wethToken, address usdtToken, address wethUsdFeed, address deployer) =
            new HelperConfig().activeNetworkConfig();

        vm.startBroadcast(deployerKey);

        // Deploy bridge
        Bridge bridge = new Bridge(wethToken, usdtToken, wethUsdFeed);

        // Optionally pre-fund bridge with WETH + USDT liquidity from deployer
        uint256 wethBal = IERC20(wethToken).balanceOf(deployer);
        uint256 usdtBal = IERC20(usdtToken).balanceOf(deployer);
        require(wethBal > 1000e18, "Fund your account with atleast 1000 WETH tokens");
        require(usdtBal > 25000e6, "Fund your account with atleast 25000 USDT tokens");

        if (wethBal > 0) {
            // IERC20(wethToken).approve(address(bridge), 1000e18);
            IERC20(wethToken).safeTransfer(address(bridge), 1000e18);
        }
        if (usdtBal > 0) {
            // IERC20(usdtToken).approve(address(bridge), 25000e6);
            IERC20(usdtToken).safeTransfer(address(bridge), 25000e6);
        }

        vm.stopBroadcast();

        console2.log("Bridge deployed at:", address(bridge));
        console2.log("WETH/USD feed:", wethUsdFeed);
    }
}
```

#### `InteractBridge.s.sol`

* Reads the deployed bridge address for the current `block.chainid` from `broadcast/DeployBridge.s.sol/<chainId>/run-latest.json`.
    
* Loads `wethToken` and `usdtToken` from `HelperConfig`.
    
* Uses `RECEIVER_WALLET` and `RECEIVER_PRIVATE_KEY` to send the tx.
    
* Approves the source token, then calls `lockAndQuote(wethToken, usdtToken, 1e18, receiver)` to perform WETH → USDT on the current chain.
    
* Emits `BridgeRequested`, which the relayer observes to call `release` on the opposite chain.
    
* For USDT → WETH, swap the token params from before and use, 6-decimals (e.g., `1000e6`).
    
* Works on chain 1 and 137 since addresses come from `HelperConfig` and broadcast files at runtime.
    

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {Script} from "forge-std/Script.sol";
import {console2} from "forge-std/console2.sol";
import {Bridge, IERC20, SafeERC20} from "src/Bridge.sol";
import {HelperConfig} from "script/HelperConfig.s.sol";

contract InteractBridge is Script {
    using SafeERC20 for IERC20;

    function run() external {
        // Load JSON from broadcast folder (network-specific)
        string memory path = string.concat(
            vm.projectRoot(), "/broadcast/DeployBridge.s.sol/", vm.toString(block.chainid), "/run-latest.json"
        );
        string memory json = vm.readFile(path);

        // Parse contract address from broadcast
        address bridgeAddr = abi.decode(vm.parseJson(json, ".transactions[0].contractAddress"), (address));
        console2.log("Bridge found at:", bridgeAddr);

        Bridge bridge = Bridge(bridgeAddr);

        // Load network config
        (, address wethToken, address usdtToken,,) = new HelperConfig().activeNetworkConfig();

        // Wallet for interaction
        address receiver = vm.envAddress("RECEIVER_WALLET");
        uint256 receiverKey = vm.envUint("RECEIVER_PRIVATE_KEY");
        console2.log("EOA Interacting with Bridge:", receiver);

        // Interact: approve WETH and call lockAndQuote
        vm.startBroadcast(receiverKey);

        uint256 amount = 1e18; // 1 WETH
        IERC20(wethToken).approve(bridgeAddr, amount);

        bridge.lockAndQuote(wethToken, usdtToken, amount, receiver);

        vm.stopBroadcast();

        console2.log("lockAndQuote called with", amount, "WETH -> expecting USDT");
    }
}
```

#### How do these scripts work together?

1. Deploy the bridge on both Sandboxes. Each bridge has its own admin and token/feed wiring.
    
2. Pre-fund both bridges with the destination tokens you expect to release.
    
3. Start the relayer. It subscribes to `BridgeRequested` on both chains, maps tokens for the opposite chain, and calls `release` as the admin there.
    
4. Run the interaction script on the source chain. It locks tokens and emits the event. The relayer fulfills on the destination chain.
    

> *This separation keeps on-chain logic minimal and pushes cross-chain coordination into the relayer, which is appropriate for an educational, event-driven demo.*

### Relayer

* `Purpose`: watch `BridgeRequested` on the source chain and call `release` on the destination chain as the bridge admin.
    
* `Setup`: loads bridge addresses from Foundry broadcasts, creates JSON-RPC providers, and builds a signer on the destination chain using `PRIVATE_KEY`.
    
* `Flow`: poll logs for `BridgeRequested`, parse `{to, dstToken, dstAmount, nonce}`, map `dstToken` via `TOKEN_MAP`, then send `release(mappedDstToken, to, dstAmount, nonce)` on the destination bridge.
    
* `Safety`: destination bridge enforces one-time processing with `processedNonces[nonce]` to prevent replay.
    
* `Requirements`: destination bridge must hold sufficient token liquidity; the signer must be the bridge admin.
    
* `Scope`: the shown script is one-directional (ETH to POL). To support both directions, add a second poller for Polygon and call `release` on Ethereum with an ETH-side admin signer.
    
* `Limitation`: processes only events observed while running; no backfill of past events.
    

```solidity
import fs from "fs";
import path from "path";
import { ethers } from "ethers";
import BridgeAbi from "../out/Bridge.sol/Bridge.json";
import "dotenv/config";

function getLatestDeployment(chainId: number) {
  const filePath = path.join(
    __dirname,
    `../broadcast/DeployBridge.s.sol/${chainId}/run-latest.json`
  );
  const raw = fs.readFileSync(filePath, "utf-8");
  const json = JSON.parse(raw);
  const tx = json.transactions.find((t: any) => t.transactionType === "CREATE");
  if (!tx) throw new Error("No CREATE transaction in broadcast file");
  return tx.contractAddress;
}

const ETH_RPC = process.env.ETH_MAINNET_SANDBOX!;
const POL_RPC = process.env.POL_MAINNET_SANDBOX!;
const PK = process.env.PRIVATE_KEY!;

// cross-chain token mapping
const TOKEN_MAP: Record<string, string> = {
  [process.env.ETH_USDT_TOKEN!]: process.env.POL_USDT_TOKEN!,
  [process.env.ETH_WETH_TOKEN!]: process.env.POL_WETH_TOKEN!,
  [process.env.POL_USDT_TOKEN!]: process.env.ETH_USDT_TOKEN!,
  [process.env.POL_WETH_TOKEN!]: process.env.ETH_WETH_TOKEN!,
};

async function main() {
  const ethProvider = new ethers.JsonRpcProvider(ETH_RPC);
  const polProvider = new ethers.JsonRpcProvider(POL_RPC);
  const wallet = new ethers.Wallet(PK, polProvider);

  const ethBridgeAddr = getLatestDeployment(1);
  const polBridgeAddr = getLatestDeployment(137);

  console.log("ETH Bridge:", ethBridgeAddr);
  console.log("POL Bridge:", polBridgeAddr);

  const ethBridge = new ethers.Contract(
    ethBridgeAddr,
    BridgeAbi.abi,
    ethProvider
  );
  const polBridge = new ethers.Contract(polBridgeAddr, BridgeAbi.abi, wallet);

  const bridgeRequestedTopic = ethers.id(
    "BridgeRequested(address,address,address,address,uint256,uint256,uint256,uint256)"
  );

  let lastProcessed = await ethProvider.getBlockNumber();
  console.log("Relayer started. Watching new events...");

  setInterval(async () => {
    try {
      const latestBlock = await ethProvider.getBlockNumber();
      if (latestBlock <= lastProcessed) return;

      const logs = await ethProvider.getLogs({
        fromBlock: lastProcessed + 1,
        toBlock: latestBlock,
        address: ethBridgeAddr,
        topics: [bridgeRequestedTopic],
      });

      for (const log of logs) {
        const parsed = ethBridge.interface.parseLog(log);
        let { from, to, srcToken, dstToken, srcAmount, dstAmount, nonce } =
          parsed.args;

        console.log("Detected BridgeRequested:");
        console.log({
          from,
          to,
          srcToken,
          dstToken,
          srcAmount,
          dstAmount,
          nonce,
        });

        // map token for destination chain
        const mappedDstToken = TOKEN_MAP[dstToken] || dstToken;

        try {
          const tx = await polBridge.release(
            mappedDstToken,
            to,
            dstAmount,
            nonce
          );
          console.log("Release tx sent:", tx.hash);
          await tx.wait();
          console.log("Release confirmed.");
        } catch (err) {
          console.error("Release failed:", err);
        }
      }

      lastProcessed = latestBlock;
    } catch (err) {
      console.error("Polling error:", err);
    }
  }, 10_000);
}

main().catch(err => {
  console.error(err);
  process.exit(1);
});
```

---

## Configuring Makefile

Now with the project set up, we can configure commands in a `Makefile` to deploy, verify and interact with bridges on different sandboxes.

Put this Makefile at the project root. It installs dependencies, deploys with Sourcify verification on both Sandboxes, and provides interaction targets. It basically takes away the overhead of writing commands again and again, working with complex commands and replacing them with simpler ones.

> Remember to replace the placeholders in verifier-urls below with your actual BuildBear Sandbox ID

```makefile
.PHONY: make-deploy install deploy-mainnet-sourcify deploy-pol-sourcify interact-mainnet-bridge interact-pol-bridge

install:
	forge install && npm i && forge build


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-mainnet-Sandbox-id> \
	 --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/<polygon-mainnet-Sandbox-id>  \
	 --broadcast \


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 eth_mainnet_sandbox \
	 --broadcast \
```

How it is set up:

* `install` installs Foundry dependencies, Node packages, and builds contracts.
    
* `deploy-mainnet-sourcify` deploys to the ETH Sandbox and verifies via the Sourcify plugin endpoint.
    
* `deploy-pol-sourcify` deploys to the Polygon Sandbox and verifies via the corresponding Sourcify plugin endpoint.
    
* `interact-mainnet-bridge` runs your interaction script against the ETH Sandbox to bridge assets.
    
* `interact-pol-bridge` runs your interaction script against the Polygon Sandbox to bridge assets.
    

---

## Install and Build

```bash
make install
```

This installs Foundry dependencies into `lib`, installs `Node dependencies` for the relayer, and compiles the contracts.

---

## Deploy and Verify

Deploy both bridges:

```bash
make deploy-mainnet-sourcify deploy-pol-sourcify
```

Your Foundry broadcast files will include the deployed addresses. Sourcify verification will be available in the Sandbox explorer.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748367729/c0b384a8-a1a2-4792-8c8c-c32871b72120.png align="left")

**Pre-funding note from your deploy script**:

Ensure the deployer wallet has sufficient WETH and USDT in both Sandboxes for the seed transfers. Adjust or remove the `require` checks inside the script if you want smaller demo amounts.

---

## Start the Relayer

Remember to install the dependencies as mentioned before, and run the relayer:

```bash
npm start
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748378634/e4b02aab-9584-4dff-913e-1d7122056b20.png align="left")

> This relayer processes only those events that occur while it is running. Backfill isn't supported, by the script for the purpose of simplicity and ease of implementation

---

## Interact and Bridge the Assets

ETH to Polygon Bridging:

> Remember to start the relayer before you call the Interact-Bridge Script on either of the Sandboxes.

While the relayer is running, in another terminal, execute an interaction with the bridge

```makefile
# This will emit a bridge request from ETH ---> Polygon for WETH to USDT
make interact-mainnet-bridge
```

**Script Broadcast**

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748400850/77d1038c-7a1c-4ec3-b93d-91ab03ab017c.png align="left")

**Tx on BuildBear Explorer**

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748404632/781dcaff-6213-4c60-89a6-9ac1e44967aa.png align="left")

Your interaction script reads the last deployment address from Foundry broadcasts, approves the source token, and calls `lockAndQuote`. The relayer observes the `BridgeRequested` event on the source chain and calls `release` on the destination bridge.

* In the output below, the relayer captured the `BridgeRequested(...)` event and released the asset on the Polygon Mainnet Sandbox
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748425495/fefd66d6-56cf-4815-9c70-d2bf7be80fb2.png align="left")
    
* Asset release tx on Polygon Mainnet Sandbox
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748430314/f06f69fd-5479-4fc7-acc6-65d8bb7fc994.png align="left")
    

---

## Transaction Debugging with the Sentio Plugin

This section is optional and shows how to inspect an end-to-end transfer using the explorer and Sentio. For the Sentio Debugger to work, you will need to install the `Sentio Plugin` from BuildBear Plugin Marketplace

### Source: Bridging Tx

Open the source Sandbox explorer.

Find the tx hash for the interaction with `lockAndQuote`, on the bridge contract.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748443542/09533b84-b044-47d6-8da4-063fb06134ec.png align="left")

Click on **View Trace on Sentio**.

#### Fund Flow

A visual map of token movement for the source transaction. Use it to confirm `transferFrom` into the bridge and any intermediate ERC20 flows.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748454803/aec96bfe-390b-4052-bb53-6ce60a15aa15.png align="left")

#### Call Trace

A step-by-step execution trace. Helpful to see the call into `lockAndQuote`, the oracle read, and the event emission order.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748468216/cdb1a365-6bc7-47d9-a304-5c27c66a04b2.png align="left")

#### Call Graph

Graph view of calls across contracts during the source transaction. Useful for understanding the sequence and flow of internal calls.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748491458/2a76827d-1c91-4123-9db0-502ec63af92b.png align="center")

#### Events Tab

Structured list of emitted events. Verify `BridgeRequested` fields such as `srcToken`, `dstToken`, `dstAmount`, and `nonce`.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748505106/a95bc41b-2187-4c99-aa4e-08eb584b8c6c.png align="center")

#### State Tab

Storage and variable snapshots at key points. Useful for checking `nonce` increments and any flags written during the call.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748523013/81f68a85-86c5-425b-b42e-38d2fde3aef8.png align="center")

---

## Destination: Release Tx

* Open the destination Sandbox Explorer.
    
* Find the relayer `release` transaction on the bridge contract that transfers assets from the bridge to the `receiver`.
    
* ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748546765/cea67143-2c1c-4b06-bf8f-4a5ddc2f276d.png align="center")
    
* Click on **View Trace on Sentio.**
    

#### Fund Flow

Shows token movement out of the bridge to the receiver. Use it to confirm the final ERC20 transfer with the expected amount.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748570897/93415e47-140b-4c1f-a9d5-e7d224d67c45.png align="center")

#### Call Trace

Execution path for the `release` call by the relayer. Check the nonce processing and the order of effects before the transfer.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748599799/96480967-06fc-46d3-b327-fcb6f742fa60.png align="center")

#### Call Graph

Graph view of the destination call sequence during `release`. Helps verify checks, effects, interactions, ordering, and the final token transfer.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748620949/968faf41-86ff-42d0-b3fc-8b1ef9dac4be.png align="center")

#### Events Tab

Verify `TransferReleased` with the same external nonce used on the source chain, along with token and amount fields.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748634135/fba1f475-e218-4d1d-8bdc-585b04a1daad.png align="center")

#### State Tab

Inspect `processedNonces` and any relevant balances after the transfer to ensure idempotence and correct accounting.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759748644668/4f03811c-094b-45a4-a3b7-166c83e0a512.png align="center")

That’s it. You now have a minimal, event-driven cross-chain bridge running end-to-end in BuildBear Mainnet Sandboxes. You can extend this demo by adding multi-sig relayers, accounting, fees, slippage controls, and stronger verification models.

---

You have now set up a minimal, event-driven bridge in BuildBear that uses BuildBear’s Data Feeds Plugin for price quoting, verifies via Sourcify Plugin, and can be traced and debugged in Sentio Plugin, all in BuildBear's Sandbox environment.
