Access real-world data inside a Nexis Native program.
LeaseContract
account.
The lease contract is a pre-funded escrow account to reward oracles for
fulfilling update requests. Only the predefined leaseAuthority
can withdraw
funds from the contract, but anyone can contribute to it. When a new round of
updates is requested for a data feed, the user who requested the update is
rewarded from the escrow. This is to incentivize users and crank turners (anyone
who runs software to systematically send update requests to Oracles) to keep
feeds updating based on a feed’s configurations. Once an update request has been
successfully fulfilled and submitted onchain by the oracles in the queue, the
oracles are transferred rewards from the escrow as well. These payments ensure
active participation.
Additionally, oracles have to stake tokens before they can service update
requests and submit responses onchain. If an oracle submits a result onchain
that falls outside the queue’s configured parameters, their stake will be
slashed (if the queue has slashingEnabled
). This helps ensure that oracles are
responding in good faith with accurate information.
Now that you understand the terminology and economics, let’s take a look at how
data is published onchain:
N
oracles are assigned to the update request and cycled to the back of the
queue. Each oracle queue in the Switchboard network is independent and
maintains its own configuration. The configuration influences its level of
security. This design choice enables users to tailor the oracle queue’s
behavior to match their specific use case. An Oracle queue is stored onchain
as an account and contains metadata about the queue. A queue is created by
invoking the
oracleQueueInit instruction
on the Switchboard Nexis Native program.
oracle_timeout
- Interval when stale oracles will be removed if they
fail to heartbeat.reward
- Rewards to provide oracles and round openers on this queue.min_stake
- The minimum amount of stake that oracles must provide to
remain on the queue.size
- The current number of oracles on a queue.max_size
- The maximum number of oracles a queue can support.minOracleResults
responses are received, the onchain program
calculates the result using the median of the oracle responses. Oracles who
respond within the queue’s configured parameters are rewarded, while the
oracles who respond outside this threshold are slashed (if the queue has
slashingEnabled
).8SXvChNYFhRq4EZuZvnhjrB3jJRQCv4k3P4W6hesH3Ee
. It provides the current price of
Bitcoin in USD onchain.
The actual onchain data for a Switchboard feed account looks a little like this:
AggregatorAccountData
type are:
min_oracle_results
- Minimum number of oracle responses required before a
round is validated.min_job_results
- Minimum number of job results before an oracle accepts a
result.variance_threshold
- Change percentage required between a previous round and
the current round. If variance percentage is not met, reject new oracle
responses.latest_confirmed_round
- Latest confirmed update request result that has
been accepted as valid. This is where you will find the data of the feed in
latest_confirmed_round.result
min_update_delay_seconds
- Minimum number of seconds required between
aggregator rounds.min_job_results
field represents the minimum amount of successful
responses from data sources an oracle must receive before it can submit its
response onchain. Meaning if min_job_results
is three, each oracle has to pull
from three job sources. The higher this number, the more reliable and accurate
the data on the feed will be. This also limits the impact that a single data
source can have on the result.
The min_oracle_results
field is the minimum amount of oracle responses
required for a round to be successful. Remember, each oracle in a queue pulls
data from each source defined as a job. The oracle then takes the weighted
median of the responses from the sources and submits that median onchain. The
program then waits for min_oracle_results
of weighted medians and takes the
median of that, which is the final result stored in the data feed account.
The min_update_delay_seconds
field is directly related to a feed’s update
cadence. min_update_delay_seconds
must have passed between one round of
updates and the next one before the Switchboard program will accept results.
It can help to look at the jobs tab of a feed in Switchboard’s explorer. For
example, you can look at the
BTC_USD feed in the explorer.
Each job listed defines the source the oracles will fetch data from and the
weighting of each source. You can view the actual API endpoints that provide the
data for this specific feed. When determining what data feed to use in your
program, things like this are very important to consider.
Below is a two of the jobs related to the BTC_USD feed. It shows two sources of
data: MEXC and Coinbase.
AggregatorAccountData
struct we defined above from the switchboard_v2
crate in your program.
AccountLoader
type here instead of the normal Account
type to deserialize the aggregator account. Due to the size of
AggregatorAccountData
, the account uses what’s called zero copy. This in
combination with AccountLoader
prevents the account from being loaded into
memory and gives our program direct access to the data instead. When using
AccountLoader
we can access the data stored in the account in one of three
ways:
load_init
after initializing an account (this will ignore the missing
account discriminator that gets added only after the user’s instruction code)load
when the account is not mutableload_mut
when the account is mutableZero-Copy
and AccountLoader
.
With the aggregator account passed into your program, you can use it to get the
latest oracle result. Specifically, you can use the type’s get_result()
method:
get_result()
method defined on the AggregatorAccountData
struct is safer
than fetching the data with latest_confirmed_round.result
because Switchboard
has implemented some nifty safety checks.
AggregatorAccountData
account
client-side in Typescript.
AggregatorAccountData
type
from the switchboard_v2
crate, Anchor checks that the account is owned by the
Switchboard program. If your program expects that only a specific data feed will
be passed in the instruction, then you can also verify that the public key of
the account passed in matches what it should be. One way to do this is to hard
code the address in the program somewhere and use account constraints to verify
the address passed in matches what is expected.
latest_confirmed_round
field on the AggregatorAccountData
struct is of
type AggregatorRound
defined as:
num_success
, medians_data
, std_deviation
, etc.
num_success
is the number of successful responses received from oracles in
this round of updates. medians_data
is an array of all of the successful
responses received from oracles this round. This is the dataset that is used to
derive the median and the final result. std_deviation
is the standard
deviation of the accepted results in this round. You might want to check for a
low standard deviation, meaning that all of the oracle responses were similar.
The switchboard program is in charge of updating the relevant fields on this
struct every time it receives an update from an oracle.
The AggregatorAccountData
also has a check_confidence_interval()
method that
you can use as another verification on the data stored in the feed. The method
allows you to pass in a max_confidence_interval
. If the standard deviation of
the results received from the oracle is greater than the given
max_confidence_interval
, it returns an error.
lib.rs
and Anchor.toml
with the program ID
shown when you run anchor keys list
.
Next, add the following to the bottom of your Anchor.toml file. This will tell
Anchor how to configure our local testing environment. This will allow us to
test our program locally without having to deploy and send transactions to
devnet.
switchboard-v2
crate in our Cargo.toml
file. Make sure your dependencies look as follows:
lib.rs
file and call it a day. To keep it more organized
though, it’s helpful to break it up across different files. Our program will
have the following files within the programs/src
directory:
/instructions/deposit.rs
/instructions/withdraw.rs
/instructions/mod.rs
errors.rs
state.rs
lib.rs
The lib.rs
file will still serve as the entry point to our program, but the
logic for each instruction will be contained in their own separate file. Go
ahead and create the program architecture described above and we’ll get started.
lib.rs
lib.rs
. Our actual logic will live in the
/instructions
directory.
The lib.rs
file will serve as the entrypoint to our program. It will define
the API endpoints that all transactions must go through.
state.rs
EscrowState
. Our data
account will store two pieces of info:
unlock_price
- The price of NZT in USD at which point you can withdraw; you
can hard-code it to whatever you want (e.g. $21.53)escrow_amount
- Keeps track of how many lamports are stored in the escrow
account"MICHAEL BURRY"
and our hardcoded
SOL_USD oracle pubkey SOL_USDC_FEED
.
errors.rs
file, paste the following:
mod.rs
instructions/mod.rs
file.
/src/instructions/deposit.rs
file.
When a user deposits, a PDA should be created with the “MICHAEL BURRY” string
and the user’s pubkey as seeds. This inherently means a user can only open one
escrow account at a time. The instruction should initialize an account at this
PDA and send the amount of NZT that the user wants to lock up to it. The user
will need to be a signer.
Let’s build the Deposit Context struct first. To do that, we need to think about
what accounts will be necessary for this instruction. We start with the
following:
escrow_state
account, they both need to be mutable.escrow_account
is supposed to be a PDA derived with the “MICHAEL
BURRY” string and the user’s pubkey. We can use Anchor account constraints to
guarantee that the address passed in actually meets that requirement.init
constraint here.escrow_state
account and transfer the NZT. We expect the user to pass
in the amount of NZT they want to lock up in escrow and the price to unlock it
at. We will store these values in the escrow_state
account.
After that, the method should execute the transfer. This program will be locking
up native NZT. Because of this, we don’t need to use token accounts or the
Nexis Native token program. We’ll have to use the system_program
to transfer the
lamports the user wants to lock up in escrow and invoke the transfer
instruction.
deposit.rs
file should look as follows:
withdraw.rs
file.
escrow_account
. The NZT used as rent in the account will
be transferred to the user account.
We also use the address constraints to verify that the feed account passed in is
actually the usdc_sol
feed and not some other feed (we have the SOL_USDC_FEED
address hard coded). In addition, the AggregatorAccountData struct that we
deserialize comes from the Switchboard rust crate. It verifies that the given
account is owned by the switchboard program and allows us to easily look at its
values. You’ll notice it’s wrapped in a AccountLoader
. This is because the
feed is actually a fairly large account and it needs to be zero copied.
Now let’s implement the withdraw instruction’s logic. First, we check if the
feed is stale. Then we fetch the current price of NZT stored in the
feed_aggregator
account. Lastly, we want to check that the current price is
above the escrow unlock_price
. If it is, then we transfer the NZT from the
escrow account back to the user and close the account. If it isn’t, then the
instruction should finish and return an error.
system_program::transfer
method
like before. If we try to, the instruction will fail to execute with the
following error.
try_borrow_mut_lamports()
on each account and
add/subtract the amount of lamports stored in each account.
withdraw.rs
file should look like this:
anchor build
without any errors.
anchor test
.
anchor test
in
your shell of choice. You should get four passing tests.
If something went wrong, go back through the lab and make sure you got
everything right. Pay close attention to the intent behind the code rather than
just copy/pasting. Also feel free to review the working code
on the main
branch of its Github repository.
challenge-solution
branch.