Solidity tutorial for getting started with Switchboard
Switchboard Contract
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.
Installation
You can install the Switchboard On-Demand Solidity SDK by running:
# Add the Switchboard Solidity interfacesnpmadd@switchboard-xyz/on-demand-solidity
Forge (Optional)
If you're using Forge, add following to your remappings.txt file:
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:
structUpdate {bytes32 oracleId; // The publisher of the updateint128 result; // The value of the recorded updateuint256 timestamp; // The timestamp of the update}interface ISwitchboard {functionlatestUpdate(bytes32 aggregatorId ) externalviewreturns (Updatememory);functionupdateFeeds(bytes[] calldata updates) externalpayable;functiongetFee(bytes[] calldata updates) externalviewreturns (uint256);}
Solidity Integration
The code below shows the flow for leveraging Switchboard feeds in Solidity.
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 Contract
Structs: A contract with all the structs used within Switchboard
Adding the Contract
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, and aggregatorId.
switchboard is the reference to the diamond contract, it can be found in Links and Technical Docs
aggregatorId 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.
Adding the function boilerplate
/** * 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 */functiongetFeedData(bytes[] calldata updates) publicpayable {//... }
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.
Adding a fee
contract Example { // ...// If the transaction fee is not paid, the update will fail.errorInsufficientFee(uint256 expected,uint256 received);functiongetFeedData(bytes[] calldata updates) publicpayable {// 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) {revertInsufficientFee(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 calling getFee(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.
Submitting 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.
This bytes[] 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 sends feewei 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.
Checking the Data
// If the feed result is invalid, this error will be emitted.errorInvalidResult(int128 result);// If the Switchboard update succeeds, this event will be emitted with the latest price.eventFeedData(int128 price);// ...functiongetFeedData(bytes[] calldata updates) publicpayable {// ...// 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 numbersint128 result = latestUpdate.result;// In this example, we revert if the result is negativeif (result <0) {revertInvalidResult(result); }// Emit the latest result from the feedemitFeedData(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 value 1477525556338078708 would represent 1.4775..8708
Next, we check that the value is positive and revert with an InvalidResult if it isn't. Finally, if the update was successful, we emit a FeedData event.
Putting It All Together
Example.sol
pragmasolidity ^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 valueint128public result;// If the transaction fee is not paid, the update will fail.errorInsufficientFee(uint256 expected,uint256 received);// If the feed result is invalid, this error will be emitted.errorInvalidResult(int128 result);// If the Switchboard update succeeds, this event will be emitted with the latest price.eventFeedData(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 */functiongetFeedData(bytes[] calldata updates) publicpayable {// 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) {revertInsufficientFee(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 negativeif (result <0) {revertInvalidResult(result); }// Emit the latest result from the feedemitFeedData(latestUpdate.result); }}
Review
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.
Using On-Demand Feeds with Typescript
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 using ethers to write updates to the example contract. Add it to the project and import the Switchboard EVM call.
Setting up the call
// for initial testing and development, you can use the rate-limited // https://crossbar.switchboard.xyz instance of crossbarconstcrossbar=newCrossbarClient("https://crossbar.switchboard.xyz");// Get the latest update data for the feedconst { encoded } =awaitcrossbar.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.
Creating contract bindings
// Target contract addressconstexampleAddress=process.env.CONTRACT_ADDRESSasstring;// (this is the readable ABI format)constabi= ["function getFeedData(bytes[] calldata updates) public payable"];// ... Setup ethers provider ...// The Contract objectconstexampleContract=newethers.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.
Getting the provider
// Pull the private key from the environment 0x..constpk=process.env.PRIVATE_KEY;if (!pk) {thrownewError("Missing PRIVATE_KEY environment variable.");}// Provider constprovider=newethers.JsonRpcProvider("https://ethereum.rpc.example");constsignerWithProvider=newethers.Wallet(pk, provider);
Add the example contract binding with the getFeedData call in the ABI.
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 jobconstchainId=1115; // Core Devnet (as an example)// for initial testing and development, you can use the rate-limited // https://crossbar.switchboard.xyz instance of crossbarconstcrossbar=newCrossbarClient("https://crossbar.switchboard.xyz");// Get the latest update data for the feedconst { encoded } =awaitcrossbar.fetchEVMResults({ aggregatorIds: ["0x0eae481a0c635fdfa18ccdccc0f62dfc34b6ef2951f239d4de4acfab0bcdca71"], chainId,// 1115 here is the chainId for Core Testnet});// Target contract addressconstexampleAddress="0xc65f0acf9df6b4312d3f3ce42a778767b3e66b8a";// The Human Readable contract ABIconstabi= ["function getFeedData(bytes[] calldata updates) public payable"];// ... Setup ethers provider ...// The Contract objectconstexampleContract=newethers.Contract(exampleAddress, abi, provider);// Update feedsawaitexampleContract.getFeedData(encoded);