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.
// 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;
git clone https://github.com/switchboard-xyz/sb-on-demand-examples
cd sb-on-demand-examples/evm/price-feeds
bun install
forge build
PRIVATE_KEY=0x...
RPC_URL=https://testnet-rpc.monad.xyz
NETWORK=monad-testnet
# Optional: if omitted, the script deploys a new consumer contract for you
CONTRACT_ADDRESS=0x...