Build a Cross-Chain Lending dApp using rBTC & USDT0 on Rootstock
This is the comprehensive tutorial for the rBTC-USDT0 cross‑chain lending starter kit.
Introduction
This guide walks you through building, deploying, and using a minimal over‑collateralized lending protocol on Rootstock. Whether you're an Ethereum developer exploring Bitcoin‑backed DeFi or new to Rootstock, this hands‑on tutorial covers everything: from cloning the code to interacting with the deployed contracts via a UI.
No prior knowledge of LayerZero or cross‑chain protocols is required. We'll explain every architectural decision and code pattern, with a particular focus on the teleport‑style messaging model that powers collateral transfer, the oracle routing pattern that separates price feeds from lending logic, and the Loan‑to‑Value (LTV) solvency checks that protect the protocol.
By the end of this tutorial, you will have:
- Deployed a complete lending protocol on Rootstock testnet
- Understood how cross–chain messaging works via LayerZero
- Learned the oracle router pattern for decoupled price feeds
- Tested the system end–to–end with the React/Vite frontend
- Built a mental model for extending the starter kit toward production
A screenshot of the finished UI (the target end‑state for this guide) is shown below:
Figure 1: Demo dApp UI after a successful borrow (rBTC 0.00025 collateral, 1 USDT0 debt, $65k rBTC price).
Below is an architecture illustration showing the high-level system flow:
Figure 2: Cross-chain lending architecture from source chain deposit to destination chain borrowing on Rootstock.
Why build on Rootstock?
Rootstock is Bitcoin's financial infrastructure: an EVM-compatible Bitcoin sidechain secured by over 85% of Bitcoin's hash power through merge mining. Key benefits for developers:
- Bitcoin compatibility: smart contracts can rely on rBTC and the Bitcoin security model.
- EVM compatibility: use the same Solidity, Hardhat, ethers.js, Metamask, etc.
- Low fees & high throughput: test cheaply and scale without congestion.
- Open source tooling: the entire stack is public and free to use.
This starter kit demonstrates a cross‑chain over‑collateralized lending flow that leverages Rootstock's features while remaining easy to understand.
Core architecture
The protocol is intentionally minimal. It consists of three logical layers:
- Cross‑chain messaging:
LZSenderandLZReceiverusing LayerZero to teleport collateral signals. - Lending logic:
LendingPoolmanages rBTC collateral and USDT0 debt with LTV checks. - Oracle routing:
OracleRouterdelegates price requests to adapters (e.g.,UmbrellaOracleAdapterorFixedPriceOracle).
Key contract responsibilities
LZSender/LZBorrowSender/LZRepaySender: source‑chain entry points. They accept rBTC, encode the user address + amount, and callendpoint.send(...)with one of three message types (deposit, borrow, repay). Look atcontracts/crosschain/LZSender.solfor the encoding logic; messages are simply(uint8 msgType, address user, uint256 amount).LZReceiver: destination chain validator. It enforces replay protection, verifiestrustedRemoteaddresses, and dispatches the payload to the pool. The three message types are handled inlzReceive, which conditionally callsdepositRBTC,borrowUSDT0FororrepayUSDT0For. See the implementation below:
// excerpt from LZReceiver.lzReceive
(uint8 msgType, address user, uint256 amount) =
abi.decode(_payload, (uint8, address, uint256));
if (msgType == MSG_DEPOSIT) {
lendingPool.depositRBTC{value: amount}(user);
} else if (msgType == MSG_BORROW) {
lendingPool.borrowUSDT0For(user, amount);
} else if (msgType == MSG_REPAY) {
lendingPool.repayUSDT0For(user, amount);
} else {
revert("INVALID_MSG");
}
LendingPool: the core accounting engine. It stores two mappings (collateralRBTCanddebtUSDT0) keyed by user address and exposes public methods for deposit, withdraw, borrow and repay. TheonlyDepositormodifier restricts cross‑chain deposit/borrow/repay calls to thecrossChainDepositoraddress (set to theLZReceiver). Solvency is calculated in_isSolvent, which fetches the rBTC price from the oracle router and applies the configuredltvBps.
function _isSolvent(uint256 collateralWei, uint256 debtAmount) internal view returns (bool) {
uint256 rbtcPrice;
try oracle.getPrice(address(0)) returns (uint256 price) {
rbtcPrice = price;
} catch {
rbtcPrice = 65_000e18; // testnet fallback
}
uint256 collateralUsd = (collateralWei * rbtcPrice) / 1e18;
uint256 debtUsd = (debtAmount * 1e18) / USDT0_SCALE;
uint256 maxDebtUsd = (collateralUsd * ltvBps) / 10_000;
return debtUsd <= maxDebtUsd;
}
OracleRouter: simple ownership‑controlled mapping of asset→oracle. The router delegations allow you to swap out price feeds without touching the pool.
function getPrice(address asset) external view returns (uint256) {
IPriceOracle oracle = oracles[asset];
require(address(oracle) != address(0), "NO_ORACLE");
return oracle.getPrice(asset);
}
- Adapters:
UmbrellaOracleAdapterimplements theIPriceOracleinterface and wraps the Umbrella on‑chain reader. It normalizes decimals and enforcesMAX_DELAY. AFixedPriceOraclesimply returns a hard‑coded value and is used for testnet demonstrations.
These contracts, together with a handful of mocks (MockLZEndpoint, MockUSDT0, etc.), make up the entire logic of the starter kit. Supporting contracts include mocks for testing and a simple React frontend that consumes the deployed contracts. A high‑level diagram is shown as follows:
Figure 3: High-level illustration of contract interaction
Prerequisites
Install the following tools before you begin. Versions shown are examples; newer versions are usually fine.
- Git:
git --versionshould print ≥ 2.20 - Node.js: v18 or later (
node -v) - npm or yarn: package manager
- Hardhat: installed locally (no global install needed)
- MetaMask: or similar Web3 wallet, configured with Rootstock testnet
Cloning and initial setup
Clone the repository and cd into it:
git clone https://github.com/rsksmart/rbtc-usdt0-crosschain-starter-kit.git
cd rbtc-usdt0-crosschain-starter-kit
The project root contains a Hardhat config, scripts, contracts, tests, and a frontend/ subdirectory for the React UI.
Environment configuration
Create a .env file in the project root. This file is ignored by git and will store sensitive data such as keys and RPC URLs. You can copy the content of the sample .env.example file from the project root into your .env file and fill in the actual values of the environment variables.
# .env
PRIVATE_KEY=0xYOUR_TESTNET_PRIVATE_KEY # account that will deploy contracts
ROOTSTOCK_RPC_URL=https://rpc.testnet.rootstock.io/<RPC_API_KEY>
LZ_ENDPOINT=0xB6318... # LayerZero testnet endpoint for Rootstock
USE_FIXED_ORACLE=true # force deterministic pricing on testnet
USE_MOCK_USDT0=true # deploy a mock USDT0 token
LTV_BPS=7000 # 70% Loan-to-Value ratio (in basis points)
You can get your Rootstock RPC API URL by following the official guide on Getting Started with the Rootstock RPC API.
Never commit this file. In production you would use a secrets manager or hardware wallet.
The Hardhat config (hardhat.config.cjs) reads the above variables to define the rootstock_testnet network, deployer account and other behaviour. You can inspect it if you want to customise gas settings or add more networks.
Installing dependencies
Install JS packages in both root and frontend directories:
npm install # root for contracts & deploy scripts
cd frontend && npm install # frontend UI dependencies
cd .. # back to project root
This populates node_modules/ and ensures Hardhat and ethers are available.
Compiling contracts and generating ABIs
Contracts are written in Solidity (contracts/ subfolders). To compile them:
npx hardhat compile
Compilation output (bytecode, ABI, metadata) appears in artifacts/ and cache/. The frontend imports ABIs directly from artifacts/; therefore, you must compile before starting the UI or it will fail to locate ABIs.
You can re‑compile anytime, and the React app will hot‑reload the updated ABI if running.
Running the test suite
A comprehensive test suite lives in test/ and uses Mocha/Chai. Run all tests with:
npx hardhat test
Key test files:
lending/LendingPool.test.js: sanity checks for collateral, debt, borrow, repay, withdrawal and solvency math.integration/LendingPoolWithRouter.test.js: ensures the OracleRouter wiring returns the correct price.crosschain/LZReceiver.test.js: verifies message validation, replay protection, and receiver behaviour.crosschain/CrossChainBorrow.test.js: end‑to‑end cross‑chain borrow flow using mocked LayerZero endpoints.
The mocks directory contains MockLZEndpoint.sol, MockOracle.sol, MockUSDT0.sol etc., which simulate external systems so tests can run quickly offline. Example invocation of a single file:
npx hardhat test test/crosschain/CrossChainBorrow.test.js
All tests should pass. If they fail, delete artifacts/ and cache/ and try again. Sometimes stale compiled artifacts cause mismatch errors.
Deployment walkthrough
Deploy script
scripts/deploy.js deploys the protocol contracts in order:
OracleRouterand eitherFixedPriceOracleorUmbrellaOracleAdapterdepending onUSE_FIXED_ORACLEMockUSDT0(ifUSE_MOCK_USDT0is true) and mints an initial supplyLendingPooland deploysLZReceiverwith an unlinked pool address- Links the receiver to the pool via
receiver.setLendingPool(lendingPool.address) - Optionally seeds the pool with USDT0 for testing
All addresses printed by the script are needed for the frontend.
npx hardhat run scripts/deploy.js --network rootstock_testnet
A sample output looks like:
Deploying with: 0x...
OracleRouter: 0x...
Fixed Oracle: 0x...
RBTC oracle registered
Mock USDT0: 0x...
Minted 1,000,000 USDT0 to deployer
LZReceiver: 0x...
LendingPool: 0x...
Receiver linked to LendingPool
Seeded pool with 500,000 USDT0
Deployment complete ✅
Addresses will vary; keep them for the frontend or explorer verification.