This tutorial walks you through integrating Switchboard oracle price feeds into your EVM smart contracts. You'll learn how to fetch oracle data, submit updates to your contract, and read verified prices.
What You'll Build
A Solidity smart contract that:
Receives and stores verified oracle price updates
Validates price freshness and deviation
Provides helper functions for DeFi use cases (collateral ratios, liquidations)
Plus a TypeScript client that fetches oracle data and submits it to your contract.
Prerequisites
Foundry for Solidity development (forge, cast)
Bun or Node.js 18+
Native tokens for gas (MON, ETH, etc.)
Basic understanding of Solidity and ethers.js
Key Concepts
How Switchboard On-Demand Works on EVM
Switchboard uses an on-demand model where:
Your client fetches signed price data from Crossbar (Switchboard's gateway)
Your contract submits the signed data to the Switchboard contract for verification
Switchboard verifies the oracle signatures and stores the data
Your contract reads the verified data via latestUpdate()
This pattern ensures prices are cryptographically verified on-chain while keeping gas costs low.
The CrossbarClient
The CrossbarClient from @switchboard-xyz/common is your interface to fetch oracle data:
Fee Handling
Some networks require a fee for oracle updates. Always check before submitting:
The Smart Contract
Here's a complete example contract that integrates Switchboard price feeds:
Contract Walkthrough
State Variables
switchboard - Reference to the deployed Switchboard contract
prices - Maps feed IDs to their latest price data
maxPriceAge - Maximum acceptable age for price data (5 minutes default)
maxDeviationBps - Maximum price change allowed (10% default, prevents manipulation)
The updatePrices Function
This is the main entry point for updating prices:
Check fee - Ensure caller sent enough to cover the oracle update fee
Submit to Switchboard - Call updateFeeds() which verifies oracle signatures
Read verified data - Call latestUpdate() to get the verified price
Store locally - Save the price in your contract's storage
Refund excess - Return any overpayment to the caller
Reading Prices
Always check freshness before using a price:
The TypeScript Client
Here's a complete client that fetches oracle data and submits it to your contract:
Client Walkthrough
Step 1: Fetch Oracle Data
The fetchEVMResults call returns encoded oracle data signed by Switchboard oracles. This data includes:
The price value
Timestamp
Oracle signatures
Step 2: Submit to Contract
Your contract receives the encoded data, submits it to Switchboard for verification, then stores the result.
Step 3-5: Confirm and Read
After confirmation, you can:
Parse PriceUpdated events from the receipt
Read the stored price directly from your contract
Deployment
Using Foundry
Create a deploy script script/Deploy.s.sol:
Deploy Commands
Running the Example
1. Clone the Examples Repository
2. Install Dependencies
3. Configure Environment
Security: Never use export PRIVATE_KEY=...—it appears in shell history. Use a .env file instead.
Create a .env file (add to .gitignore):
4. Run the Example
Expected Output
Adding to Your Project
1. Install the Switchboard Interfaces
Copy the interface files from the examples repo:
Or install via npm:
2. Import and Use
Example: DeFi Business Logic
The example contract includes helper functions for common DeFi patterns:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;
import { ISwitchboard } from "./switchboard/interfaces/ISwitchboard.sol";
import { SwitchboardTypes } from "./switchboard/libraries/SwitchboardTypes.sol";
/**
* @title SwitchboardPriceConsumer
* @notice Example contract demonstrating Switchboard On-Demand oracle integration
*
* Key Features:
* - Secure price updates with signature verification
* - Staleness checks to prevent old data usage
* - Price deviation validation
* - Multi-feed support
*/
contract SwitchboardPriceConsumer {
// ========== State Variables ==========
/// @notice The Switchboard contract interface
ISwitchboard public immutable switchboard;
/// @notice Stored price data for each feed
mapping(bytes32 => PriceData) public prices;
/// @notice Maximum age for price data (default: 5 minutes)
uint256 public maxPriceAge = 300;
/// @notice Maximum price deviation in basis points (default: 10% = 1000 bps)
uint256 public maxDeviationBps = 1000;
/// @notice Contract owner
address public owner;
// ========== Structs ==========
/**
* @notice Stored price information for a feed
* @param value The price value (18 decimals)
* @param timestamp When the price was last updated
* @param slotNumber Solana slot number of the update
*/
struct PriceData {
int128 value;
uint256 timestamp;
uint64 slotNumber;
}
// ========== Events ==========
event PriceUpdated(
bytes32 indexed feedId,
int128 oldPrice,
int128 newPrice,
uint256 timestamp,
uint64 slotNumber
);
event PriceValidationFailed(bytes32 indexed feedId, string reason);
// ========== Errors ==========
error InsufficientFee(uint256 expected, uint256 received);
error PriceTooOld(uint256 age, uint256 maxAge);
error PriceDeviationTooHigh(uint256 deviation, uint256 maxDeviation);
error InvalidFeedId();
error Unauthorized();
// ========== Constructor ==========
constructor(address _switchboard) {
switchboard = ISwitchboard(_switchboard);
owner = msg.sender;
}
// ========== External Functions ==========
/**
* @notice Update price feeds with oracle data
* @param updates Encoded Switchboard updates with signatures
* @param feedIds Array of feed IDs to process from the update
*/
function updatePrices(
bytes[] calldata updates,
bytes32[] calldata feedIds
) external payable {
// Get the required fee
uint256 fee = switchboard.getFee(updates);
if (msg.value < fee) {
revert InsufficientFee(fee, msg.value);
}
// Submit updates to Switchboard (verifies signatures)
switchboard.updateFeeds{ value: fee }(updates);
// Process each feed ID
for (uint256 i = 0; i < feedIds.length; i++) {
bytes32 feedId = feedIds[i];
// Get the latest verified update from Switchboard
SwitchboardTypes.LegacyUpdate memory update = switchboard.latestUpdate(feedId);
// Store the price with validation
_processFeedUpdate(
feedId,
update.result,
uint64(update.timestamp),
update.slotNumber
);
}
// Refund excess payment
if (msg.value > fee) {
(bool success, ) = msg.sender.call{ value: msg.value - fee }("");
require(success, "Refund failed");
}
}
/**
* @notice Get the current price for a feed
* @param feedId The feed identifier
* @return value The price value
* @return timestamp The update timestamp
* @return slotNumber The Solana slot number
*/
function getPrice(
bytes32 feedId
) external view returns (int128 value, uint256 timestamp, uint64 slotNumber) {
PriceData memory priceData = prices[feedId];
if (priceData.timestamp == 0) revert InvalidFeedId();
return (priceData.value, priceData.timestamp, priceData.slotNumber);
}
/**
* @notice Check if a price is fresh (within maxPriceAge)
*/
function isPriceFresh(bytes32 feedId) public view returns (bool) {
PriceData memory priceData = prices[feedId];
if (priceData.timestamp == 0) return false;
return block.timestamp - priceData.timestamp <= maxPriceAge;
}
// ========== Internal Functions ==========
function _processFeedUpdate(
bytes32 feedId,
int128 newValue,
uint64 timestamp,
uint64 slotNumber
) internal {
PriceData memory oldPrice = prices[feedId];
// Validate price deviation if we have a previous price
if (oldPrice.timestamp != 0) {
uint256 deviation = _calculateDeviation(oldPrice.value, newValue);
if (deviation > maxDeviationBps) {
emit PriceValidationFailed(feedId, "Deviation too high");
revert PriceDeviationTooHigh(deviation, maxDeviationBps);
}
}
// Store the new price
prices[feedId] = PriceData({
value: newValue,
timestamp: timestamp,
slotNumber: slotNumber
});
emit PriceUpdated(feedId, oldPrice.value, newValue, timestamp, slotNumber);
}
function _calculateDeviation(
int128 oldValue,
int128 newValue
) internal pure returns (uint256) {
if (oldValue == 0) return 0;
uint128 absOld = oldValue < 0 ? uint128(-oldValue) : uint128(oldValue);
uint128 absNew = newValue < 0 ? uint128(-newValue) : uint128(newValue);
uint128 diff = absNew > absOld ? absNew - absOld : absOld - absNew;
return (uint256(diff) * 10000) / uint256(absOld);
}
}
ISwitchboard public immutable switchboard;
mapping(bytes32 => PriceData) public prices;
uint256 public maxPriceAge = 300;
uint256 public maxDeviationBps = 1000;