DeFi protocols need real-world data like asset prices, exchange rates, or random numbers. But blockchains are isolated – they can't access external data directly. Oracles bridge this gap by bringing verified off-chain information onto the blockchain. Rootstock supports multiple decentralized oracle solutions, including Chainlink and Tellor. This guide focuses on Chainlink Price Feeds and VRF.
Oracle Integration on Rootstock
Oracles are the bridge between blockchain and external data. On Rootstock, you can use decentralized oracle networks to get tamper-proof price feeds, randomness, and other off-chain information. This guide will walk you through integrating oracles into your smart contracts, with clear, step-by-step examples.
What You'll Learn
- What oracles are and why they matter in DeFi.
- How to fetch real-time asset prices using Chainlink Price Feeds.
- How to generate verifiable randomness with Chainlink VRF.
- Best practices to keep your oracle data secure and reliable.
Supported Oracles on Rootstock
Rootstock is EVM-compatible, so most Ethereum oracle solutions work out of the box. Here are the most popular ones:
- Chainlink – The industry standard. Provides decentralized price feeds, randomness (VRF), and more. Chainlink is officially integrated with Rootstock – you can find the latest price feed addresses in the Chainlink documentation.
- Tellor – A decentralized oracle where reporters stake tokens to submit data. Useful for custom data feeds.
- API3 – First-party oracles that provide data directly from APIs using dAPIs.
In this guide, we'll focus on Chainlink because it's the most widely adopted and easiest to get started with.
Part 1: Getting Price Feeds with Chainlink
Price feeds are the most common oracle use case. Your DeFi protocol might need to know the current price of BTC/USD, ETH/USD, or any other asset to calculate collateral ratios, liquidate positions, or set swap rates.
Step 1: Find the Price Feed Address
Chainlink has deployed price feed contracts on Rootstock mainnet and testnet. You need the correct address for the pair you want.
| Network | Chain ID | Example: BTC/USD Feed Address |
|---|---|---|
| Rootstock Mainnet | 30 | 0x5fb1616f177d9572484717550c27f46F4B5F5B7f (BTC/USD) |
| Rootstock Testnet | 31 | 0x76474B42B0c268a268fC6F0D9B0B6f6c3b3C8f (BTC/USD) |
Where to find the latest addresses:
Visit the Chainlink Rootstock Addresses page. It lists all available feeds for both mainnet and testnet. The addresses above are current examples, but always verify the latest addresses from official Chainlink documentation before deploying.
Step 2: Understand the Aggregator Interface
Chainlink price feeds follow the AggregatorV3Interface. Let's look at its key functions:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface AggregatorV3Interface {
// Returns the number of decimals the answer is represented in.
function decimals() external view returns (uint8);
// Returns a description of the feed (e.g., "BTC / USD").
function description() external view returns (string memory);
// Returns the version of the aggregator.
function version() external view returns (uint256);
// Returns the latest round data. This is the main function we'll use.
function latestRoundData()
external
view
returns (
uint80 roundId, // Round identifier
int256 answer, // The price (with decimals)
uint256 startedAt, // Timestamp when the round started
uint256 updatedAt, // Timestamp when the round was last updated
uint80 answeredInRound // Round in which the answer was computed
);
}
Important: The answer is an int256 (can be negative, but for price feeds it's positive). It includes decimals – for most feeds, it's 8 decimals (e.g., 3000000000 means $30,000.00000000). Always use decimals() to format it correctly.
Step 3: Write a Simple Price Consumer Contract
Now let's build a contract that fetches the latest price. We'll add safety checks to ensure the price is fresh and valid.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./AggregatorV3Interface.sol";
contract PriceConsumer {
AggregatorV3Interface internal priceFeed;
/**
* @param _priceFeed Address of the Chainlink price feed (e.g., BTC/USD on testnet)
*/
constructor(address _priceFeed) {
priceFeed = AggregatorV3Interface(_priceFeed);
}
/**
* Returns the latest price with safety checks.
* @return price The latest price as an integer with 8 decimals.
*/
function getLatestPrice() public view returns (int256) {
(
uint80 roundId,
int256 price,
,
uint256 updatedAt,
uint80 answeredInRound
) = priceFeed.latestRoundData();
// 1. Check staleness: price should have been updated in the last hour.
require(block.timestamp - updatedAt <= 1 hours, "Price is stale");
// 2. Ensure the round is complete (answeredInRound >= roundId).
require(answeredInRound >= roundId, "Round incomplete");
// 3. Price should be positive.
require(price > 0, "Invalid price");
return price;
}
/**
* Returns the number of decimals the price feed uses.
*/
function getDecimals() public view returns (uint8) {
return priceFeed.decimals();
}
/**
* Returns a human-readable description of the feed.
*/
function getDescription() public view returns (string memory) {
return priceFeed.description();
}
}
Explanation of safety checks:
Staleness: If the price hasn't been updated for too long (here, 1 hour), it might be outdated. In a real protocol, you might want a shorter threshold (e.g., 30 minutes) depending on the asset volatility.
Round completeness: answeredInRound should be at least roundId – this ensures the price comes from a completed round, not a pending one.
Positive price: Obvious but good practice.
Step 4: Test Your Contract with Hardhat
We'll write a test that deploys the contract on a local Hardhat network and fetches the price. Since we don't have a live Chainlink feed on the local network, we'll either use a mock or fork the Rootstock testnet.
Option A: Use a Mock (Recommended for Beginners)
Create a mock aggregator in your test folder.
// test/mocks/MockAggregator.sol
pragma solidity ^0.8.0;
import "../../contracts/AggregatorV3Interface.sol";
contract MockAggregator is AggregatorV3Interface {
uint8 public decimals = 8;
string public description = "BTC/USD mock";
uint256 public version = 1;
int256 private mockPrice = 30000 * 1e8; // $30,000 with 8 decimals
function latestRoundData() external view override returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
) {
return (1, mockPrice, block.timestamp, block.timestamp, 1);
}
// Allow tests to update the mock price
function setMockPrice(int256 _price) external {
mockPrice = _price;
}
}
Now the test file:
// test/PriceConsumer.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("PriceConsumer", function () {
let priceConsumer;
let mockAggregator;
beforeEach(async function () {
// Deploy mock aggregator
const MockAggregator = await ethers.getContractFactory("MockAggregator");
mockAggregator = await MockAggregator.deploy();
await mockAggregator.deployed();
// Deploy PriceConsumer with mock address
const PriceConsumer = await ethers.getContractFactory("PriceConsumer");
priceConsumer = await PriceConsumer.deploy(mockAggregator.address);
await priceConsumer.deployed();
});
it("Should return the correct price", async function () {
const price = await priceConsumer.getLatestPrice();
expect(price).to.equal(30000 * 1e8);
});
it("Should revert if price is stale", async function () {
// Simulate time passing (increase block timestamp)
await ethers.provider.send("evm_increaseTime", [2 * 3600]); // 2 hours
await ethers.provider.send("evm_mine", []); // mine a block
await expect(priceConsumer.getLatestPrice()).to.be.revertedWith("Price is stale");
});
it("Should revert if price is negative", async function () {
await mockAggregator.setMockPrice(-100);
await expect(priceConsumer.getLatestPrice()).to.be.revertedWith("Invalid price");
});
});
Option B: Fork Rootstock Testnet (More Realistic)
In hardhat.config.js, add a forking configuration:
module.exports = {
networks: {
hardhat: {
forking: {
url: "https://public-node.testnet.rsk.co",
}
}
}
};
Then in your test, you can use the actual Chainlink feed address:
it("Should fetch real price from testnet fork", async function () {
// Use actual testnet feed address (BTC/USD)
const feedAddress = "0x76474B42B0c268a268fC6F0D9B0B6f6c3b3C8f"; // BTC/USD on Rootstock Testnet
const PriceConsumer = await ethers.getContractFactory("PriceConsumer");
const priceConsumer = await PriceConsumer.deploy(feedAddress);
await priceConsumer.deployed();
const price = await priceConsumer.getLatestPrice();
expect(price).to.be.gt(0);
});
This approach is closer to reality but requires a stable testnet RPC.
Step 4: Deploy on Rootstock Testnet
Once your contract is tested, you can deploy it to the testnet. Use the Hardhat script:
// scripts/deploy-price-consumer.js
async function main() {
const [deployer] = await ethers.getSigners();
console.log("Deploying with account:", deployer.address);
const feedAddress = "0x76474B42B0c268a268fC6F0D9B0B6f6c3b3C8f"; // BTC/USD testnet feed address
const PriceConsumer = await ethers.getContractFactory("PriceConsumer");
const priceConsumer = await PriceConsumer.deploy(feedAddress);
await priceConsumer.deployed();
console.log("PriceConsumer deployed to:", priceConsumer.address);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Run it with:
npx hardhat run scripts/deploy-price-consumer.js --network rsktestnet
Part 2: Using Randomness (Chainlink VRF)
Many DeFi applications need randomness – for example, to select a winner in a lottery, assign a random NFT trait, or shuffle a deck. On-chain randomness is tricky because all blockchain data is deterministic. Chainlink VRF (Verifiable Random Function) provides provably fair randomness.
How Chainlink VRF Works
Your contract requests randomness from Chainlink.
Chainlink's oracle generates a random number off-chain and submits it along with a cryptographic proof.
Your contract verifies the proof and receives the random number.
The entire process is trustless – anyone can verify the proof.