description |
---|
Solidity tutorial for getting started with Switchboard |
Switchboard contracts use the Diamond Pattern. This allowed has allowed Switchboard contracts to be built modularly while retaining a single contract address. However, this also means that traditional explorers cannot find the verified contract code, similar to ordinary proxies.
Using Louper.dev, a custom diamond explorer, we're able to analyze the Switchboard diamond contract and call functions as you would on an ordinary verified contract within a scanner.
You can install the Switchboard On-Demand Solidity SDK by running:
# Add the Switchboard Solidity interfaces
npm add @switchboard-xyz/on-demand-solidity
If you're using Forge, add following to your remappings.txt file:
remappings.txt
@switchboard-xyz/on-demand-solidity/=node_modules/@switchboard-xyz/on-demand-solidity
If you just want to call the Switchboard contract without dealing with any alternative interfaces, you can add only the necessary function signatures and structs. For example, in the following example we'll just be using the following:
struct Update {
bytes32 oracleId; // The publisher of the update
int128 result; // The value of the recorded update
uint256 timestamp; // The timestamp of the update
}
interface ISwitchboard {
function latestUpdate(
bytes32 aggregatorId
) external view returns (Update memory);
function updateFeeds(bytes[] calldata updates) external payable;
function getFee(bytes[] calldata updates) external view returns (uint256);
}
The code below shows the flow for leveraging Switchboard feeds in Solidity.
import {ISwitchboard} from "@switchboard-xyz/on-demand-solidity/ISwitchboard.sol";
import {Structs} from "@switchboard-xyz/on-demand-solidity/Structs.sol";
-
Adding the interface for the Switchboard contract is the first step. If you're using the contract and interface from above it should just be a matter of pasting those in.
ISwitchboard
: The interface for the entire Switchboard ContractStructs
: A contract with all the structs used within Switchboard
contract Example {
ISwitchboard switchboard;
// Every Switchboard Feed has a unique feed ID derived from the OracleJob definition and Switchboard Queue ID.
bytes32 aggregatorId;
/**
* @param _switchboard The address of the Switchboard contract
* @param _aggregatorId The aggregator ID for the feed you want to query
*/
constructor(address _switchboard, bytes32 _aggregatorId) {
// Initialize the target _switchboard
// Get the existing Switchboard contract address on your preferred network from the Switchboard Docs
switchboard = ISwitchboard(_switchboard);
aggregatorId = _aggregatorId;
}
}
- Here we're creating a contract and keeping a reference to both the Switchboard diamond address,
switchboard
, andaggregatorId
.
switchboard
is the reference to the diamond contract, it can be found in Links and Technical DocsaggregatorId
that we're interested in reading. If you don't have an aggregatorId yet, create a feed by following: Designing a Feed (EVM) and Creating Feeds.
/**
* getFeedData is a function that uses an encoded Switchboard update
* If the update is successful, it will read the latest price from the feed
* See below for fetching encoded updates (e.g., using the Switchboard Typescript SDK)
* @param updates Encoded feed updates to update the contract with the latest result
*/
function getFeedData(bytes[] calldata updates) public payable {
//...
}
- Here we're adding the function to get feed data. The idea is that we'll pass in an encoded Switchboard update (or set of updates) that will be used to update the
aggregatorId
of our choice. We can then read our recently-written update safely.
contract Example {
// ...
// If the transaction fee is not paid, the update will fail.
error InsufficientFee(uint256 expected, uint256 received);
function getFeedData(bytes[] calldata updates) public payable {
// Get the fee for updating the feeds.
uint256 fee = switchboard.getFee(updates);
// If the transaction fee is not paid, the update will fail.
if (msg.value < fee) {
revert InsufficientFee(fee, msg.value);
}
// ...
- Here we're doing a few things relating to update fees.
We're adding a new error,InsufficientFee(uint256 expected, uint256 received)
, that will be used if the submitted transaction value isn't enough to cover the update.
We're callinggetFee(bytes[] calldata updates)
to get the cost of submitting a Switchboard update programmatically from the Switchboard program.
We're enforcing that users pay for fees before submitting any updates.
// Submit the updates to the Switchboard contract
switchboard.updateFeeds{ value: fee }(updates);
- This line updates feed values in the Switchboard contract, and sends the required fee. Internally, each update is parsed and encoded signatures are verified and checked against the list of valid oracles on a given chain.
Thisbytes[] calldata
parameter keeps things simple by making common Switchboard updates into one data-type. Everything is handled behind the scenes.
The{ value: fee }
in the call sendsfee
wei over to the Switchboard contract as payment for updates. The intent here is to pay for the updates.
// Read the current value from a Switchboard feed.
// This will fail if the feed doesn't have fresh updates ready (e.g. if the feed update failed)
Structs.Update memory latestUpdate = switchboard.latestUpdate(aggregatorId);
- This line pulls the latest update for the specified aggregatorId. This will fill in the fields
uint64 maxStaleness
,uint32 minSamples
.
// If the feed result is invalid, this error will be emitted.
error InvalidResult(int128 result);
// If the Switchboard update succeeds, this event will be emitted with the latest price.
event FeedData(int128 price);
// ...
function getFeedData(bytes[] calldata updates) public payable {
// ...
// Get the latest feed result
// This is encoded as decimal * 10^18 to avoid floating point issues
// Some feeds require negative numbers, so results are int128's, but this example uses positive numbers
int128 result = latestUpdate.result;
// In this example, we revert if the result is negative
if (result < 0) {
revert InvalidResult(result);
}
// Emit the latest result from the feed
emit FeedData(latestUpdate.result);
}
- Here we're pulling the result out of the latest update. Switchboard updates are encoded as int128's. Another important fact is that values are decimals scaled up by 10^18.
For example, the value1477525556338078708
would represent1.4775..8708
Next, we check that the value is positive and revert with anInvalidResult
if it isn't. Finally, if the update was successful, we emit a FeedData event.
pragma solidity ^0.8.0;
import {ISwitchboard} from "@switchboard-xyz/on-demand-solidity/ISwitchboard.sol";
import {Structs} from "@switchboard-xyz/on-demand-solidity/Structs.sol";
contract Example {
ISwitchboard switchboard;
// Every Switchboard feed has a unique aggregator id
bytes32 aggregatorId;
// Store the latest value
int128 public result;
// If the transaction fee is not paid, the update will fail.
error InsufficientFee(uint256 expected, uint256 received);
// If the feed result is invalid, this error will be emitted.
error InvalidResult(int128 result);
// If the Switchboard update succeeds, this event will be emitted with the latest price.
event FeedData(int128 price);
/**
* @param _switchboard The address of the Switchboard contract
* @param _aggregatorId The feed ID for the feed you want to query
*/
constructor(address _switchboard, bytes32 _aggregatorId) {
// Initialize the target _switchboard
// Get the existing Switchboard contract address on your preferred network from the Switchboard Docs
switchboard = ISwitchboard(_switchboard);
aggregatorId = _aggregatorId;
}
/**
* getFeedData is a function that uses an encoded Switchboard update
* If the update is successful, it will read the latest price from the feed
* See below for fetching encoded updates (e.g., using the Switchboard Typescript SDK)
* @param updates Encoded feed updates to update the contract with the latest result
*/
function getFeedData(bytes[] calldata updates) public payable {
// Get the fee for updating the feeds. If the transaction fee is not paid, the update will fail.
uint256 fees = switchboard.getFee(updates);
if (msg.value < fee) {
revert InsufficientFee(fee, msg.value);
}
// Submit the updates to the Switchboard contract
switchboard.updateFeeds{ value: fees }(updates);
// Read the current value from a Switchboard feed.
// This will fail if the feed doesn't have fresh updates ready (e.g. if the feed update failed)
Structs.Update memory latestUpdate = switchboard.latestUpdate(aggregatorId);
// Get the latest feed result
// This is encoded as decimal * 10^18 to avoid floating point issues
// Some feeds require negative numbers, so results are int128's, but this example uses positive numbers
result = latestUpdate.result;
// In this example, we revert if the result is negative
if (result < 0) {
revert InvalidResult(result);
}
// Emit the latest result from the feed
emit FeedData(latestUpdate.result);
}
}
This contract:
- Sets the Switchboard contract address and feed ID in the constructor
- Defines a function
getFeedData
- Checks if the transaction fee is paid, using
switchboard.getFee(bytes[] calldata updates)
. - Submits the updates to the Switchboard contract using
switchboard.updateFeeds(bytes[] calldata updates)
. - Reads the latest value from the feed using
switchboard.getLatestValue(bytes32 aggregatorId)
. - Emits the latest result from the feed.
After the feed has been initialized, we can now request price signatures from oracles!
So now that we have the contract ready to read and use Switchboard update data, we need a way to fetch these encoded values. Using Crossbar, we can get an encoded feed update with just a fetch. For simplicity, we'll demonstrate a fetch using both.
We'll be working from the Typescript portion of Designing a Feed (EVM):
bun add ethers
import {
CrossbarClient,
} from "@switchboard-xyz/on-demand";
import * as ethers from "ethers";
- We'll be using ethers to write updates to the example contract. Add it to the project and import the Switchboard EVM call.
// for initial testing and development, you can use the rate-limited
// https://crossbar.switchboard.xyz instance of crossbar
const crossbar = new CrossbarClient("https://crossbar.switchboard.xyz");
// Get the latest update data for the feed
const { encoded } = await crossbar.fetchEVMResults({
aggregatorIds: ["0x0eae481a0c635fdfa18ccdccc0f62dfc34b6ef2951f239d4de4acfab0bcdca71"],
chainId: 1115, // 1115 here is the chainId for Core Testnet
});
- Here we're getting the results for the
aggregatorId
from Switchboard using the default crossbar deployment.
// Target contract address
const exampleAddress = process.env.CONTRACT_ADDRESS as string;
// (this is the readable ABI format)
const abi = ["function getFeedData(bytes[] calldata updates) public payable"];
// ... Setup ethers provider ...
// The Contract object
const exampleContract = new ethers.Contract(exampleAddress, abi, provider);
- Pass the encoded updates
bytes[] calldata
into the getFeedData call. This will send the transaction over the wire. - In order to submit transactions on the target chain, you need to plug in the right RPC and private key. The
signerWithProvider
will be what we pass into the contract.
// Pull the private key from the environment 0x..
const pk = process.env.PRIVATE_KEY;
if (!pk) {
throw new Error("Missing PRIVATE_KEY environment variable.");
}
// Provider
const provider = new ethers.JsonRpcProvider(
"https://ethereum.rpc.example"
);
const signerWithProvider = new ethers.Wallet(pk, provider);
- Add the example contract binding with the
getFeedData
call in the ABI.
// Update feeds
await exampleContract.getFeedData(encoded);
Here we're connecting all of these components. We're compiling all of calls into a system where we can pull the encoded updates, and calling the contract.
import {
CrossbarClient,
} from "@switchboard-xyz/on-demand";
import * as ethers from "ethers";
// ... simulation logic ...
// Create a Switchboard On-Demand job
const chainId = 1115; // Core Devnet (as an example)
// for initial testing and development, you can use the rate-limited
// https://crossbar.switchboard.xyz instance of crossbar
const crossbar = new CrossbarClient("https://crossbar.switchboard.xyz");
// Get the latest update data for the feed
const { encoded } = await crossbar.fetchEVMResults({
aggregatorIds: ["0x0eae481a0c635fdfa18ccdccc0f62dfc34b6ef2951f239d4de4acfab0bcdca71"],
chainId, // 1115 here is the chainId for Core Testnet
});
// Target contract address
const exampleAddress = "0xc65f0acf9df6b4312d3f3ce42a778767b3e66b8a";
// The Human Readable contract ABI
const abi = ["function getFeedData(bytes[] calldata updates) public payable"];
// ... Setup ethers provider ...
// The Contract object
const exampleContract = new ethers.Contract(exampleAddress, abi, provider);
// Update feeds
await exampleContract.getFeedData(encoded);