diff --git a/content/courses/connecting-to-offchain-data/oracles.md b/content/courses/connecting-to-offchain-data/oracles.md index 0688f9603..674ebd9dd 100644 --- a/content/courses/connecting-to-offchain-data/oracles.md +++ b/content/courses/connecting-to-offchain-data/oracles.md @@ -12,22 +12,27 @@ description: Access real-world data inside a Solana program. ## Summary -- Oracles are services that provide external data to a blockchain network -- There are many - [Oracle providers on Solana](https://solana.com/ecosystem/explore?categories=oracle). -- You can build your own Oracle to create a custom data feed -- You have to be careful when choosing your data feed providers +- Oracles are services that provide external data to a blockchain network. +- Solana has a rich ecosystem of oracle providers. Some notable oracle providers + include [Pyth Network](https://pyth.network), + [Switchboard](https://switchboard.xyz), [Chainlink](https://chain.link), and + [DIA](https://www.diadata.org/solana-price-oracles/). +- You can build your own oracle to create a custom data feed. +- When choosing oracle providers, consider reliability, accuracy, + decentralization, update frequency, and cost. Be aware of security risks: + oracles can be potential points of failure or attack. For critical data, use + reputable providers and consider multiple independent oracles to mitigate + risks. ## Lesson -[Oracles](https://solana.com/ecosystem/explore?categories=oracle) are services -that provide external data to a blockchain network. Blockchains by nature are -siloed environments that do not know the outside world. This constraint -inherently puts a limit on the use cases for decentralized applications (dApps). -Oracles provide a solution to this limitation by creating a decentralized way to -get real-world data onchain. +Oracles are services that provide external data to a blockchain network. +Blockchains are siloed environments that do not inherently know the outside +world. This constraint limits the use cases for decentralized applications +(dApps). Oracles solve this limitation by offering a decentralized way to get +real-world data onchain. -Oracles can provide just about any type of data onchain. Examples include: +Oracles can provide various types of data onchain, such as: - Results of sporting events - Weather data @@ -35,232 +40,229 @@ Oracles can provide just about any type of data onchain. Examples include: - Market data - Randomness -While the exact implementation may differ from blockchain to blockchain, -generally Oracles work as follows: +While the implementation may differ across blockchains, oracles generally work +as follows: 1. Data is sourced offchain. -2. That data is published onchain via a transaction, and stored in an account. -3. Programs can read the data stored in the account and use that data in the - program's logic. +2. The data is published onchain via a transaction and stored in an account. +3. Programs can read the data stored in the account and use it in the program's + logic. -This lesson will go over the basics of how oracles work, the state of oracles on +This lesson will cover the basics of how oracles work, the state of oracles on Solana, and how to effectively use oracles in your Solana development. ### Trust and Oracle Networks -The primary hurdle oracles need to overcome is one of trust. Since blockchains -execute irreversible financial transactions, developers and users alike need to -know they can trust the validity and accuracy of oracle data. The first step in -trusting an oracle is understanding how it's implemented. - -Broadly speaking, there are three implementation types: - -1. Single, centralized oracle publishes data onchain. - 1. Pro: It’s simple; there's one source of truth. - 2. Con: nothing is stopping the oracle provider from providing inaccurate - data. -2. Network of oracles publish data and a consensus mechanism is used to - determine the final result. - 1. Pro: Consensus makes it less likely that bad data is pushed onchain. - 2. Con: There is no way to disincentivize bad actors from publishing bad data - and trying to sway the consensus. -3. Oracle network with some kind of proof of stake mechanism. I.e. require - oracles to stake tokens to participate in the consensus mechanism. On every - response, if an oracle deviates by some threshold from the accepted range of - results, their stake is taken by the protocol and they can no longer report. - 1. Pro: Ensures no single oracle can influence the final result too - drastically, while also incentivizing honest and accurate actions. - 2. Con: Building decentralized networks is challenging, incentives need to be - set up properly and be sufficient to get participation, etc. - -Depending on the use case of an oracle, any of the above solutions could be the -right approach. For example, you might be perfectly willing to participate in a -blockchain-based game that utilizes centralized oracles to publish gameplay -information onchain. - -On the other hand, you may be less willing to trust a centralized oracle -providing price information for trading applications. - -You may end up creating many standalone oracles for your own applications simply -as a way to get access to offchain information that you need. However, those -oracles are unlikely to be used by the broader community where decentralization -is a core tenet. You should also be hesitant to use centralized, third-party -oracles yourself. - -In a perfect world, all important and/or valuable data would be provided onchain -through a highly efficient oracle network through a trustworthy proof of stake -consensus mechanism. By introducing a staking mechanism, it’s in the oracle -providers' best interest to ensure their data is accurate to keep their staked -funds. - -Even when an oracle network claims to have such a consensus mechanism, be sure -to know the risks involved with using the network. If the total value involved -of the downstream applications is greater than the oracle's allocated stake, -oracles still may have sufficient incentive to collude. - -It is your job to know how the oracle network is configured and judge if it can -be trusted. Generally, Oracles should only be used for non-mission-critical -functions and worst-case scenarios should be accounted for. +The primary challenge for oracles is trust. Since blockchains execute +irreversible financial transactions, developers and users need to trust the +validity and accuracy of oracle data. The first step in trusting an oracle is +understanding its implementation. + +Broadly speaking, there are three types of implementations: + +1. **Single, centralized oracle publishes data onchain.** + - **Pro:** It's simple; there's one source of truth. + - **Con:** Nothing prevents the oracle provider from supplying inaccurate + data. +2. **Network of oracles publishes data, with consensus determining the final + result.** + + - **Pro:** Consensus reduces the likelihood of bad data being pushed onchain. + - **Con:** There's no direct disincentive for bad actors to publish incorrect + data to sway consensus. + +3. **Oracle network with proof-of-stake mechanism:** Oracles are required to + stake tokens to participate. If an oracle's response deviates too far from + the consensus, its stake is taken by the protocol and it can no longer + report. + - **Pro:** This approach prevents any single oracle from overly influencing + the final result while incentivizing honest and accurate reporting. + - **Con:** Building decentralized networks is challenging; proper incentives + and sufficient participation are necessary for success. + +Each implementation has its place depending on the oracle's use case. For +example, using centralized oracles for a blockchain-based game may be +acceptable. However, you may be less comfortable with a centralized oracle +providing price data for trading applications. + +You may create standalone oracles for your own applications to access offchain +data. However, these are unlikely to be used by the broader community, where +decentralization is a core principle. Be cautious about using centralized +third-party oracles as well. + +In an ideal scenario, all important or valuable data would be provided onchain +via a highly efficient oracle network with a trustworthy proof-of-stake +consensus mechanism. A staking system incentivizes oracle providers to ensure +the accuracy of their data to protect their staked funds. + +Even when an oracle network claims to have a consensus mechanism, be aware of +the risks. If the total value at stake in downstream applications exceeds the +staked amount of the oracle network, there may still be sufficient incentive for +collusion among oracles. + +As a developer, it is your responsibility to understand how an oracle network is +configured and assess whether it can be trusted. Generally, oracles should only +be used for non-mission-critical functions, and worst-case scenarios should +always be accounted for. ### Oracles on Solana -There are many -[Oracle providers on Solana](https://solana.com/ecosystem/explore?categories=oracle). -Two of the most well known are [Pyth](https://pyth.network) and -[Switchboard](https://switchboard.xyz). They’re each unique and follow slightly -different design choices. - -**Pyth** is primarily focused on financial data published from top-tier -financial institutions. Pyth’s data providers publish the market data updates. -These updates are then aggregated and published onchain by the Pyth program. The -data sourced from Pyth is not completely decentralized as only approved data -providers can publish data. The selling point of Pyth is that its data is vetted -directly by the platform and sourced from financial institutions, ensuring -higher quality. - -**Switchboard** is a completely decentralized oracle network and has data of all -kinds available. Check out all of the feeds -[on their website](https://app.switchboard.xyz/solana/devnet/explore) -Additionally, anyone can run a Switchboard oracle and anyone can consume their -data. This means you'll have to be diligent about researching feeds. We'll talk -more about what to look for later in the lesson. - -Switchboard follows a variation of the stake weighted oracle network described -in the third option of the previous section. It does so by introducing what are -called TEEs (Trusted Execution Environments). TEEs are secure environments -isolated from the rest of the system where sensitive code can be executed. In -simple terms, given a program and an input, TEEs can execute and generate an -output along with a proof. If you’d like to learn more about TEEs, please read -[Switchboard’s documentation](https://docs.switchboard.xyz/functions). - -By introducing TEEs on top of stake weighted oracles, Switchboard is able to -verify each oracle’s software to allow participation in the network. If an -oracle operator acts maliciously and attempts to change the operation of the -approved code, a data quote verification will fail. This allows Switchboard -oracles to operate beyond quantitative value reporting, such as functions -- -running offchain custom and confidential computations. +Solana has a diverse ecosystem of oracle providers, each with unique offerings. +Some notable ones include: + +- [**Pyth**](https://www.pyth.network/price-feeds) + Focuses primarily on financial data published by top-tier financial + institutions. Pyth's data providers are approved entities that publish market + data updates, which are then aggregated and made available onchain via the + Pyth program. This data is not fully decentralized since only approved + providers can publish it. However, the key advantage is that Pyth offers + high-quality, vetted data directly sourced from these institutions. +- [**Switchboard**](https://switchboard.xyz) + Completely decentralized oracle network with a variety of data feeds. You can + explore these feeds on + [Switchboard website](https://app.switchboard.xyz/solana/mainnet). Anyone can + run a Switchboard oracle or consume its data, but that means users need to be + diligent in researching the quality of the feeds they use. +- [**Chainlink**](https://chain.link) + Decentralized oracle network providing secure offchain computations and + real-world data across multiple blockchains. +- [**DIA**](https://www.diadata.org/solana-price-oracles/) + Open-source oracle platform delivering transparent and verified data for + digital assets and traditional financial instruments. + +In this lesson, we'll be using **Switchboard**. However, the concepts are +applicable to most oracles, so you should select the oracle provider that best +fits your needs. + +Switchboard follows a stake-weighted oracle network model, as discussed in the +previous section, but with an additional layer of security via +[**Trusted Execution Environments (TEEs)**](https://en.wikipedia.org/wiki/Trusted_execution_environment). +TEEs are secure environments isolated from the rest of the system where +sensitive code can be executed. In simple terms, TEEs can take a program and an +input, execute the program, and produce an output along with a proof. To learn +more about TEEs, check out +[Switchboard's Architecture Design documentation](https://docs.switchboard.xyz/docs/switchboard/readme/architecture-design#trusted-execution-environments-for-layered-security). + +By incorporating TEEs, Switchboard is able to verify each oracle's software, +ensuring its integrity within the network. If an oracle operator acts +maliciously or alters the approved code, the data quote verification process +will fail. This allows Switchboard to support more than just data reporting; it +can also run offchain custom and confidential computations. ### Switchboard Oracles -Switchboard oracles store data on Solana using data feeds. These data feeds, -also called aggregators, are each a collection of jobs that get aggregated to -produce a single result. These aggregators are represented onchain as a regular -Solana account managed by the Switchboard program. When an oracle updates, it -writes the data directly to these accounts. Let's go over a few terms to -understand how Switchboard works: - -- **[Aggregator (Data Feed)](https://github.com/switchboard-xyz/sbv2-solana/blob/0b5e0911a1851f9ca37042e6ff88db4cd840067b/rust/switchboard-solana/src/oracle_program/accounts/aggregator.rs#L60)** - - Contains the data feed configuration, dictating how data feed updates get - requested, updated, and resolved onchain from its assigned source. The - Aggregator is the account owned by the Switchboard Solana program and is where - the data is published onchain. -- **[Job](https://github.com/switchboard-xyz/sbv2-solana/blob/0b5e0911a1851f9ca37042e6ff88db4cd840067b/rust/switchboard-solana/src/oracle_program/accounts/job.rs)** - - Each data source should correspond to a job account. The job account is a - collection of Switchboard tasks used to instruct the oracles on how to fetch - and transform data. In other words, it stores the blueprints for how data is - fetched offchain for a particular data source. -- **Oracle** - A separate program that sits between the internet and the - blockchain and facilitates the flow of information. An oracle reads a feed’s - job definitions, calculates the result, and submits its response onchain. -- **Oracle Queue** - A group of oracles that get assigned to update requests in - a round-robin fashion. The oracles in the queue must be actively heartbeating - onchain to provide updates. Data and configurations for this queue are stored - onchain in an - [account owned by the Switchboard program](https://github.com/switchboard-xyz/solana-sdk/blob/9dc3df8a5abe261e23d46d14f9e80a7032bb346c/javascript/solana.js/src/generated/oracle-program/accounts/OracleQueueAccountData.ts#L8). -- **Oracle Consensus** - Determines how oracles come to agreement on the - accepted onchain result. Switchboard oracles use the median oracle response as - the accepted result. A feed authority can control how many oracles are - requested and how many must respond to influence its security. - -Switchboard oracles are incentivized to update data feeds because they are -rewarded for doing so accurately. Each data feed has a `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: - -1. Oracle queue setup - When an update is requested from a queue, the next `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](https://github.com/switchboard-xyz/solana-sdk/blob/9dc3df8a5abe261e23d46d14f9e80a7032bb346c/javascript/solana.js/src/generated/oracle-program/instructions/oracleQueueInit.ts#L13) - on the Switchboard Solana program. - 1. Some relevant Oracle Queue configurations: - 1. `oracle_timeout` - Interval when stale oracles will be removed if they - fail to heartbeat. - 2. `reward` - Rewards to provide oracles and round openers on this queue. - 3. `min_stake` - The minimum amount of stake that oracles must provide to - remain on the queue. - 4. `size` - The current number of oracles on a queue. - 5. `max_size` - The maximum number of oracles a queue can support. -2. Aggregator/data feed setup - The aggregator/feed account gets created. A feed - belongs to a single oracle queue. The feed’s configuration dictates how - update requests are invoked and routed through the network. -3. Job account setup - In addition to the feed, a job account for each data - source must be set up. This defines how oracles can fulfill the feed’s update - requests. This includes defining where the oracles should fetch the data the - feed is requesting. -4. Request assignment - Once an update has been requested with the feed account, - the oracle queue assigns the request to different oracles/nodes in the queue - to fulfill. The oracles will fetch the data from the data source defined in - each of the feed’s job accounts. Each job account has a weight associated - with it. The oracle will calculate the weighted median of the results from - across all the jobs. -5. After `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`). -6. The updated result is stored in the data feed account so it can be - read/consumed onchain. - -#### How to use Switchboard Oracles - -To use Switchboard oracles and incorporate offchain data into a Solana program, -you first have to find a feed that provides the data you need. Switchboard feeds -are public and there are many -[already available that you can choose from](https://app.switchboard.xyz/solana/devnet/explore). -When looking for a feed, you have to decide how accurate/reliable you want the -feed, where you want to source the data from, as well as the feed’s update -cadence. When consuming a publicly available feed, you have no control over -these things, so choose carefully! - -For example, there is a Switchboard-sponsored -[BTC_USD feed](https://app.switchboard.xyz/solana/devnet/feed/8SXvChNYFhRq4EZuZvnhjrB3jJRQCv4k3P4W6hesH3Ee). -This feed is available on Solana devnet/mainnet with pubkey -`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: +Switchboard oracles store data on Solana using data feeds, also called +**aggregators**. These data feeds consist of multiple jobs that are aggregated +to produce a single result. Aggregators are represented onchain as regular +Solana accounts managed by the Switchboard program, with updates written +directly to these accounts. Let's review some key terms to understand how +Switchboard operates: + +- **[Aggregator (Data Feed)](https://github.com/switchboard-xyz/solana-sdk/blob/main/rust/switchboard-solana/src/oracle_program/accounts/aggregator.rs)** - + Contains the data feed configuration, including how updates are requested, + processed, and resolved onchain. The aggregator account, owned by the + Switchboard program stores the final data onchain. +- **[Job](https://github.com/switchboard-xyz/solana-sdk/blob/main/rust/switchboard-solana/src/oracle_program/accounts/job.rs)** - + Each data source corresponds to a job account, which defines the tasks for + fetching and transforming offchain data. It acts as the blueprint for how data + is retrieved for a particular source. +- **[Oracle](https://github.com/switchboard-xyz/solana-sdk/blob/main/rust/switchboard-solana/src/oracle_program/accounts/oracle.rs)** - + An oracle acts as the intermediary between the internet and the blockchain. It + reads job definitions from the feed, calculates results, and submits them + onchain. +- **Oracle Queue** - A pool of oracles that are assigned update requests in a + round-robin fashion. Oracles in the queue must continuously heartbeat onchain + to provide updates. The queue's data and configuration are stored in an + [onchain account](https://github.com/switchboard-xyz/solana-sdk/blob/main/javascript/solana.js/src/generated/oracle-program/accounts/OracleQueueAccountData.ts) + managed by the Switchboard program. +- **Oracle Consensus** - Oracles come to a consensus by using the median of the + responses as the accepted onchain result. The feed authority controls how many + oracles are required to respond for added security. + +Switchboard incentivizes oracles to update data feeds through a reward system. +Each data feed has a `LeaseContract` account, which is a pre-funded escrow that +rewards oracles for fulfilling update requests. The `leaseAuthority` can +withdraw funds, but anyone can contribute to the contract. When a user requests +a feed update, the escrow rewards both the user and the crank turners (those who +run software to systematically send update requests). Once oracles submit +results onchain, they are paid from this escrow. + +Oracles must also stake tokens to participate in updates. If an oracle submits a +result outside the queue's configured parameters, they can have their stake +slashed, provided the queue has `slashingEnabled`. This mechanism ensures that +oracles act in good faith by providing accurate data. + +#### How Data is Published Onchain + +1. **Oracle Queue Setup** - When an update request is made, the next `N` oracles + are assigned from the queue and moved to the back after completion. Each + queue has its own configuration that dictates security and behavior, tailored + to specific use cases. Queues are stored onchain as accounts and can be + created via the + [`oracleQueueInit` instruction](https://github.com/switchboard-xyz/solana-sdk/blob/main/javascript/solana.js/src/generated/oracle-program/instructions/oracleQueueInit.ts). + - Key + [Oracle Queue configurations](https://docs.rs/switchboard-solana/latest/switchboard_solana/oracle_program/accounts/queue/struct.OracleQueueAccountData.html): + - `oracle_timeout`: Removes stale oracles after a heartbeat timeout. + - `reward`: Defines rewards for oracles and round openers. + - `min_stake`: The minimum stake required for an oracle to participate. + - `size`: The current number of oracles in the queue. + - `max_size`: The maximum number of oracles a queue can support. +2. **[Aggregator/data feed setup](https://docs.rs/switchboard-solana/latest/switchboard_solana/oracle_program/accounts/aggregator/struct.AggregatorAccountData.html)** - + Each feed is linked to a single oracle queue and contains configuration + details on how updates are requested and processed. +3. **[Job Account Setup](https://docs.rs/switchboard-solana/latest/switchboard_solana/oracle_program/accounts/job/struct.JobAccountData.html)** - + Each data source requires a job account that defines how oracles retrieve and + fulfill the feed's update requests. These job accounts also specify where + data is sourced. +4. **Request Assignment** - When an update is requested, the oracle queue + assigns the task to different oracles in the queue. Each oracle processes + data from the sources defined in the feed's job accounts, calculating a + weighted median result based on the data. + +5. **Consensus and Result Calculation** - After the required number of oracle + responses + ([`minOracleResults`](https://docs.rs/switchboard-solana/latest/switchboard_solana/oracle_program/accounts/aggregator/struct.AggregatorAccountData.html#structfield.min_oracle_results)) + is received, the result is calculated as the median of the responses. Oracles + that submit responses within the set parameters are rewarded, while those + outside the threshold are penalized (if `slashingEnabled` is active). +6. **Data Storage** - The final result is stored in the aggregator account, + where it can be accessed onchain for consumption by other programs. + +#### How to Use Switchboard Oracles + +To incorporate offchain data into a Solana program using Switchboard oracles, +the first step is to find a data feed that suits your needs. Switchboard offers +many [publicly available feeds](https://app.switchboard.xyz/solana/mainnet) for +various data types. When selecting a feed, you should consider the following +factors: + +- **Accuracy/Reliability**: Evaluate how precise the data needs to be for your + application. +- **Data Source**: Choose a feed based on where the data is sourced from. +- **Update Cadence**: Understand how frequently the feed is updated to ensure it + meets your use case. + +When consuming public feeds, you won't have control over these aspects, so it's +important to choose carefully based on your requirements. + +For example, Switchboard offers a +[BTC/USD feed](https://app.switchboard.xyz/solana/mainnet/feed/8SXvChNYFhRq4EZuZvnhjrB3jJRQCv4k3P4W6hesH3Ee), +which provides the current Bitcoin price in USD. This feed is available on both +Solana devnet and mainnet with the following public key: +`8SXvChNYFhRq4EZuZvnhjrB3jJRQCv4k3P4W6hesH3Ee`. + +Here's a snapshot of what the onchain data for a Switchboard feed account looks +like: ```rust -// from the switchboard solana program -// https://github.com/switchboard-xyz/sbv2-solana/blob/0b5e0911a1851f9ca37042e6ff88db4cd840067b/rust/switchboard-solana/src/oracle_program/accounts/aggregator.rs#L60 +// From the switchboard solana program +// https://github.com/switchboard-xyz/solana-sdk/blob/main/rust/switchboard-solana/src/oracle_program/accounts/aggregator.rs#L60 pub struct AggregatorAccountData { /// Name of the aggregator to store onchain. pub name: [u8; 32], ... - ... + ... /// Pubkey of the queue the aggregator belongs to. pub queue_pubkey: Pubkey, ... @@ -274,19 +276,19 @@ pub struct AggregatorAccountData { /// Change percentage required between a previous round and the current round. If variance percentage is not met, reject new oracle responses. pub variance_threshold: SwitchboardDecimal, ... - /// Latest confirmed update request result that has been accepted as valid. This is where you will find the data you are requesting in latest_confirmed_round.result - pub latest_confirmed_round: AggregatorRound, - ... + /// Latest confirmed update request result that has been accepted as valid. This is where you will find the data you are requesting in latest_confirmed_round.result + pub latest_confirmed_round: AggregatorRound, + ... /// The previous confirmed round result. pub previous_confirmed_round_result: SwitchboardDecimal, /// The slot when the previous confirmed round was opened. pub previous_confirmed_round_slot: u64, - ... + ... } ``` You can view the full code for this data structure in the -[Switchboard program here](https://github.com/switchboard-xyz/sbv2-solana/blob/0b5e0911a1851f9ca37042e6ff88db4cd840067b/rust/switchboard-solana/src/oracle_program/accounts/aggregator.rs#L60). +[Switchboard program here](https://github.com/switchboard-xyz/solana-sdk/blob/main/rust/switchboard-solana/src/oracle_program/accounts/aggregator.rs#L60). Some relevant fields and configurations on the `AggregatorAccountData` type are: @@ -303,85 +305,82 @@ Some relevant fields and configurations on the `AggregatorAccountData` type are: - `min_update_delay_seconds` - Minimum number of seconds required between aggregator rounds. -The first three configs listed above are directly related to the accuracy and -reliability of a data feed. - -The `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 +The first three configurations listed above directly impact the accuracy and +reliability of a data feed: + +- The `min_job_results` field represents the minimum number of successful + responses an oracle must receive from data sources before it can submit its + response onchain. For example, if `min_job_results` is set to three, each + oracle must pull data from at least three job sources. The higher this number, + the more reliable and accurate the data will be, reducing the influence of any + single data source. + +- The `min_oracle_results` field is the minimum number of oracle responses + required for a round to be successful. Each oracle in a queue pulls data from + each source defined as a job, takes the weighted median of those responses, + and submits that median onchain. The program then waits for + `min_oracle_results` of these weighted medians and calculates the median of + those, which is the final result stored in the data feed account. + +- The `min_update_delay_seconds` field is related to the feed's update cadence. + This value must have passed between rounds of updates before the Switchboard + program will accept results. + +It can help to view the jobs tab for a feed in Switchboard's explorer. For +example, check out the [BTC_USD feed in the explorer](https://app.switchboard.xyz/solana/devnet/feed/8SXvChNYFhRq4EZuZvnhjrB3jJRQCv4k3P4W6hesH3Ee). -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. +Each job defines the data sources the oracles fetch from and the weight assigned +to each source. You can view the actual API endpoints that provide the data for +this feed. When selecting a feed for your program, these considerations are key. -Below is a two of the jobs related to the BTC_USD feed. It shows two sources of -data: [MEXC](https://www.mexc.com/) and [Coinbase](https://www.coinbase.com/). +Below are two of the jobs related to the BTC_USD feed, showing data from +[MEXC](https://www.mexc.com/) and [Coinbase](https://www.coinbase.com/). ![Oracle Jobs](/public/assets/courses/unboxed/oracle-jobs.png) -Once you’ve chosen a feed to use, you can start reading the data in that feed. -You do this by simply deserializing and reading the state stored in the account. -The easiest way to do that is by making use of the `AggregatorAccountData` -struct we defined above from the `switchboard_v2` crate in your program. +Once you've chosen a feed, you can start reading the data from that feed by +deserializing and reading the state stored in the account. The easiest way to do +this is by using the `AggregatorAccountData` struct from the +`switchboard_solana` crate in your program. ```rust -// import anchor and switchboard crates -use { - anchor_lang::prelude::*, - switchboard_v2::AggregatorAccountData, -}; +// Import anchor and switchboard crates +use {anchor_lang::prelude::*, switchboard_solana::AggregatorAccountData}; ... #[derive(Accounts)] pub struct ConsumeDataAccounts<'info> { - // pass in data feed account and deserialize to AggregatorAccountData - pub feed_aggregator: AccountLoader<'info, AggregatorAccountData>, - ... + // Pass in data feed account and deserialize to AggregatorAccountData + pub feed_aggregator: AccountLoader<'info, AggregatorAccountData>, + ... } ``` -Notice that we use the `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: +Using zero-copy deserialization with `AccountLoader` allows the program to +access specific data within large accounts like `AggregatorAccountData` without +loading the entire account into memory. This improves memory efficiency and +performance by only accessing the necessary parts of the account. It avoids +deserializing the whole account, saving both time and resources. This is +especially useful for large account structures. + +When using `AccountLoader`, you can access the data in 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 mutable -- `load_mut` when the account is mutable +- `load_init`: Used after initializing an account (this ignores the missing + account discriminator that gets added only after the user's instruction code) +- `load`: Used when the account is immutable +- `load_mut`: Used when the account is mutable -If you’d like to learn more, check out the -[Advance Program Architecture lesson](/developers/courses/program-optimization/program-architecture) -where we touch on `Zero-Copy` and `AccountLoader`. +To dive deeper, check out the +[Advanced Program Architecture lesson](/content/courses/program-optimization/program-architecture.md), +where we discuss `Zero-Copy` and `AccountLoader` in more detail. -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: +With the aggregator account passed into your program, you can use it to retrieve +the latest oracle result. Specifically, you can use the `get_result()` method on +the aggregator type: ```rust -// inside an Anchor program +// Inside an Anchor program ... let feed = &ctx.accounts.feed_aggregator.load()?; @@ -394,8 +393,8 @@ than fetching the data with `latest_confirmed_round.result` because Switchboard has implemented some nifty safety checks. ```rust -// from switchboard program -// https://github.com/switchboard-xyz/sbv2-solana/blob/0b5e0911a1851f9ca37042e6ff88db4cd840067b/rust/switchboard-solana/src/oracle_program/accounts/aggregator.rs#L195 +// From switchboard program +// https://github.com/switchboard-xyz/solana-sdk/blob/main/rust/switchboard-solana/src/oracle_program/accounts/aggregator.rs#L206 pub fn get_result(&self) -> anchor_lang::Result { if self.resolution_mode == AggregatorResolutionMode::ModeSlidingResolution { @@ -414,27 +413,35 @@ You can also view the current value stored in an `AggregatorAccountData` account client-side in Typescript. ```typescript -import { AggregatorAccount, SwitchboardProgram} from '@switchboard-xyz/solana.js' - +import { AggregatorAccount, SwitchboardProgram } from "@switchboard-xyz/solana.js"; +import { PublicKey, SystemProgram, Connection } from "@solana/web3.js"; +import { Big } from "@switchboard-xyz/common"; ... ... -// create keypair for test user -let user = new anchor.web3.Keypair() -// fetch switchboard devnet program object +const DEVNET_RPC_URL = "https://api.devnet.solana.com"; +const SOL_USD_SWITCHBOARD_FEED = new PublicKey( + "GvDMxPzN1sCj7L26YDK2HnMRXEQmQ2aemov8YBtPS7vR", +); +// Create keypair for test user +let user = new anchor.web3.Keypair(); + +// Fetch switchboard devnet program object switchboardProgram = await SwitchboardProgram.load( - "devnet", - new anchor.web3.Connection("https://api.devnet.solana.com"), - user -) + new Connection(DEVNET_RPC_URL), + payer, +); -// pass switchboard program object and feed pubkey into AggregatorAccount constructor -aggregatorAccount = new AggregatorAccount(switchboardProgram, solUsedSwitchboardFeed) +// Pass switchboard program object and feed pubkey into AggregatorAccount constructor +aggregatorAccount = new AggregatorAccount( + switchboardProgram, + SOL_USD_SWITCHBOARD_FEED, +); -// fetch latest SOL price -const solPrice: Big | null = await aggregatorAccount.fetchLatestValue() +// Fetch latest SOL price +const solPrice: Big | null = await aggregatorAccount.fetchLatestValue(); if (solPrice === null) { - throw new Error('Aggregator holds no value') + throw new Error("Aggregator holds no value"); } ``` @@ -458,12 +465,12 @@ you can see its relevant configurations. ![Oracle Configs](/public/assets/courses/unboxed/oracle-configs.png) -The BTC_USD feed has Min Update Delay = 6 seconds. This means that the price of -BTC is only updated at a minimum of every 6 seconds on this feed. This is +The BTC_USD feed has a Min Update Delay = 6 seconds. This means that the price +of BTC is only updated at a minimum of every 6 seconds on this feed. This is probably fine for most use cases, but if you wanted to use this feed for -something latency sensitive, it’s probably not a good choice. +something latency sensitive, it's probably not a good choice. -It’s also worthwhile to audit a feed's sources in the Jobs section of the oracle +It's also worthwhile to audit a feed's sources in the Jobs section of the oracle explorer. Since the value that is persisted onchain is the weighted median result the oracles pull from each source, the sources directly influence what is stored in the feed. Check for shady links and potentially run the APIs yourself @@ -473,21 +480,21 @@ Once you have found a feed that fits your needs, you still need to make sure you're using the feed appropriately. For example, you should still implement necessary security checks on the account passed into your instruction. Any account can be passed into your program's instructions, so you should verify -it’s the account you expect it to be. +it's the account you expect it to be. In Anchor, if you deserialize the account to the `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. +from the `switchboard_solana` 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. ```rust use { - anchor_lang::prelude::*, - solana_program::{pubkey, pubkey::Pubkey}, - switchboard_v2::{AggregatorAccountData}, + anchor_lang::prelude::*, + solana_program::{pubkey, pubkey::Pubkey}, + switchboard_solana::AggregatorAccountData, }; pub static BTC_USDC_FEED: Pubkey = pubkey!("8SXvChNYFhRq4EZuZvnhjrB3jJRQCv4k3P4W6hesH3Ee"); @@ -497,11 +504,11 @@ pub static BTC_USDC_FEED: Pubkey = pubkey!("8SXvChNYFhRq4EZuZvnhjrB3jJRQCv4k3P4W #[derive(Accounts)] pub struct TestInstruction<'info> { - // Switchboard SOL feed aggregator - #[account( - address = BTC_USDC_FEED - )] - pub feed_aggregator: AccountLoader<'info, AggregatorAccountData>, + // Switchboard SOL feed aggregator + #[account( + address = BTC_USDC_FEED + )] + pub feed_aggregator: AccountLoader<'info, AggregatorAccountData>, } ``` @@ -511,9 +518,9 @@ common things to check for are data staleness and the confidence interval. Each data feed updates the current value stored in it when triggered by the oracles. This means the updates are dependent on the oracles in the queue that -it’s assigned to. Depending on what you intend to use the data feed for, it may +it's assigned to. Depending on what you intend to use the data feed for, it may be beneficial to verify that the value stored in the account was updated -recently. For example, a lending protocol that needs to determine if a loan’s +recently. For example, a lending protocol that needs to determine if a loan's collateral has fallen below a certain level may need the data to be no older than a few seconds. You can have your code check the timestamp of the most recent update stored in the aggregator account. The following code snippet @@ -524,23 +531,23 @@ than 30 seconds ago. use { anchor_lang::prelude::*, anchor_lang::solana_program::clock, - switchboard_v2::{AggregatorAccountData, SwitchboardDecimal}, + switchboard_solana::{AggregatorAccountData, SwitchboardDecimal}, }; ... ... let feed = &ctx.accounts.feed_aggregator.load()?; -if (clock::Clock::get().unwrap().unix_timestamp - feed.latest_confirmed_round.round_open_timestamp) <= 30{ +if (clock::Clock::get().unwrap().unix_timestamp - feed.latest_confirmed_round.round_open_timestamp) <= 30 { valid_transfer = true; - } +} ``` The `latest_confirmed_round` field on the `AggregatorAccountData` struct is of type `AggregatorRound` defined as: ```rust -// https://github.com/switchboard-xyz/sbv2-solana/blob/0b5e0911a1851f9ca37042e6ff88db4cd840067b/rust/switchboard-solana/src/oracle_program/accounts/aggregator.rs#L17 +// https://github.com/switchboard-xyz/solana-sdk/blob/main/rust/switchboard-solana/src/oracle_program/accounts/aggregator.rs#L17 pub struct AggregatorRound { /// Maintains the number of successful responses received from nodes. @@ -593,7 +600,7 @@ the results received from the oracle is greater than the given `max_confidence_interval`, it returns an error. ```rust -// https://github.com/switchboard-xyz/sbv2-solana/blob/0b5e0911a1851f9ca37042e6ff88db4cd840067b/rust/switchboard-solana/src/oracle_program/accounts/aggregator.rs#L228 +// https://github.com/switchboard-xyz/solana-sdk/blob/main/rust/switchboard-solana/src/oracle_program/accounts/aggregator.rs#L228 pub fn check_confidence_interval( &self, @@ -613,7 +620,7 @@ use { crate::{errors::*}, anchor_lang::prelude::*, std::convert::TryInto, - switchboard_v2::{AggregatorAccountData, SwitchboardDecimal}, + use switchboard_solana::{AggregatorAccountData, SwitchboardDecimal}, }; ... @@ -621,131 +628,136 @@ use { let feed = &ctx.accounts.feed_aggregator.load()?; -// check feed does not exceed max_confidence_interval +// Check feed does not exceed max_confidence_interval feed.check_confidence_interval(SwitchboardDecimal::from_f64(max_confidence_interval)) .map_err(|_| error!(ErrorCode::ConfidenceIntervalExceeded))?; ``` -Lastly, it’s important to plan for worst-case scenarios in your programs. Plan +Lastly, it's important to plan for worst-case scenarios in your programs. Plan for feeds going stale and plan for feed accounts closing. ### Conclusion If you want functional programs that can perform actions based on real-world -data, you’re going to have to use oracles. Fortunately, there are some -trustworthy oracle networks, like Switchboard, that make using oracles easier -than they would otherwise be. However, make sure to do your due diligence on the -oracles you use. You are ultimately responsible for your program's behavior! +data, you'll need to use oracles. Fortunately, there are reliable oracle +networks, such as Switchboard, that simplify the process. However, it's crucial +to perform thorough due diligence on any oracle network you choose, as you are +ultimately responsible for your program's behavior. ## Lab -Let's practice using oracles! We'll be building a "Michael Burry Escrow" program -that locks SOL in an escrow account until SOL is above a certain USD value. This -is named after the investor -[Michael Burry](https://en.wikipedia.org/wiki/Michael_Burry) who's famous for -predicting the 2008 housing market crash. +Let's practice working with oracles! We'll be building a "Michael Burry Escrow" +program, which locks SOL in an escrow account until its value surpasses a +specified USD threshold. The program is named after +[Michael Burry](https://en.wikipedia.org/wiki/Michael_Burry), the investor known +for predicting the 2008 housing market crash. -We will be using the devnet -[SOL_USD](https://app.switchboard.xyz/solana/devnet/feed/GvDMxPzN1sCj7L26YDK2HnMRXEQmQ2aemov8YBtPS7vR) -oracle from switchboard. The program will have two main instructions: +For this, we'll use the +[SOL_USD oracle on devnet](https://app.switchboard.xyz/solana/devnet/feed/GvDMxPzN1sCj7L26YDK2HnMRXEQmQ2aemov8YBtPS7vR) +from Switchboard. The program will have two key instructions: -- Deposit - Lock up the SOL and set a USD price to unlock it at. -- Withdraw - Check the USD price and withdraw the SOL if the price is met. +- **Deposit**: Lock up the SOL and set a USD price target for unlocking. +- **Withdraw**: Check the USD price, and if the target is met, withdraw the SOL. -#### 1. Program Setup +### 1. Program Setup -To get started, let’s create the program with +To get started, let's create the program with ```zsh -anchor init burry-escrow +anchor init burry-escrow --template=multiple ``` -Next, replace the program ID in `lib.rs` and `Anchor.toml` with the program ID -shown when you run `anchor keys list`. +Next, replace the program ID in `lib.rs` and `Anchor.toml` by running command +`anchor keys sync`. -Next, add the following to the bottom of your Anchor.toml file. This will tell +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. At the bottom of `Anchor.toml`: -```toml -[test.validator] -url="https://api.devnet.solana.com" - +```toml filename="Anchor.toml" [test] -startup_wait = 10000 +startup_wait = 5000 +shutdown_wait = 2000 +upgradeable = false -[[test.validator.clone]] # sbv2 devnet programID +[test.validator] +bind_address = "0.0.0.0" +url = "https://api.devnet.solana.com" +ledger = ".anchor/test-ledger" +rpc_port = 8899 + +[[test.validator.clone]] # switchboard-solana devnet programID address = "SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f" -[[test.validator.clone]] # sbv2 devnet IDL +[[test.validator.clone]] # switchboard-solana devnet IDL address = "Fi8vncGpNKbq62gPo56G4toCehWNy77GgqGkTaAF5Lkk" -[[test.validator.clone]] # sbv2 SOL/USD Feed -address="GvDMxPzN1sCj7L26YDK2HnMRXEQmQ2aemov8YBtPS7vR" +[[test.validator.clone]] # switchboard-solana SOL/USD Feed +address = "GvDMxPzN1sCj7L26YDK2HnMRXEQmQ2aemov8YBtPS7vR" ``` -Additionally, we want to import the `switchboard-v2` crate in our `Cargo.toml` -file. Make sure your dependencies look as follows: +Additionally, we want to import the `switchboard-solana` crate in our +`Cargo.toml` file. Make sure your dependencies look as follows: -```toml +```toml filename="Cargo.toml" [dependencies] -anchor-lang = "0.28.0" -switchboard-v2 = "0.4.0" +anchor-lang = "0.30.1" +switchboard-solana = "0.30.4" ``` -Before we get started with the logic, let’s go over the structure of our -program. With small programs, it’s very easy to add all of the smart contract -code to a single `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` +Before diving into the program logic, let's review the structure of our smart +contract. For smaller programs, it's tempting to put all the code in a single +`lib.rs` file. However, organizing the code across different files helps +maintain clarity and scalability. Our program will be structured as follows +within the `programs/burry-escrow` directory: + +```sh +└── burry-escrow + ├── Cargo.toml + ├── Xargo.toml + └── src + ├── constants.rs + ├── errors.rs + ├── instructions + │ ├── deposit.rs + │ ├── mod.rs + │ └── withdraw.rs + ├── lib.rs + └── state.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. +In this structure, `lib.rs` serves as the entry point to the program, while the +logic for each instruction handler is stored in separate files under the +`instructions` directory. Go ahead and set up the architecture as shown above, +and we'll proceed from there. -#### 2. `lib.rs` +### 2. Setup lib.rs -Before we write any logic, we are going to set up all of our boilerplate -information. Starting with `lib.rs`. Our actual logic will live in the +Before writing the logic, we'll set up the necessary boilerplate in `lib.rs`. +This file acts as the entry point for the program, defining the API endpoints +that all transactions will pass through. The actual logic will be housed 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. - -```rust +```rust filename="lib.rs" use anchor_lang::prelude::*; -use instructions::deposit::*; -use instructions::withdraw::*; -use state::*; +use instructions::{deposit::*, withdraw::*}; +pub mod errors; pub mod instructions; pub mod state; -pub mod errors; +pub mod constants; declare_id!("YOUR_PROGRAM_KEY_HERE"); #[program] -mod burry_oracle_program { - +pub mod burry_escrow { use super::*; - pub fn deposit(ctx: Context, escrow_amt: u64, unlock_price: u64) -> Result<()> { - deposit_handler(ctx, escrow_amt, unlock_price) + pub fn deposit(ctx: Context, escrow_amount: u64, unlock_price: f64) -> Result<()> { + deposit_handler(ctx, escrow_amount, unlock_price) } pub fn withdraw(ctx: Context) -> Result<()> { @@ -754,39 +766,44 @@ mod burry_oracle_program { } ``` -#### 3. `state.rs` - -Next, let's define our data account for this program: `EscrowState`. Our data -account will store two pieces of info: +### 3. Define state.rs -- `unlock_price` - The price of SOL 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 +Next, let's define our program's data account: `Escrow`. This account will store +two key pieces of information: -We will also be defining our PDA seed of `"MICHAEL BURRY"` and our hardcoded -SOL_USD oracle pubkey `SOL_USDC_FEED`. +- `unlock_price`: The price of SOL in USD at which withdrawals are allowed + (e.g., hard-coded to $21.53). +- `escrow_amount`: Tracks the amount of lamports held in the escrow account. -```rust -// in state.rs +```rust filename="state.rs" use anchor_lang::prelude::*; -pub const ESCROW_SEED: &[u8] = b"MICHAEL BURRY"; -pub const SOL_USDC_FEED: &str = "GvDMxPzN1sCj7L26YDK2HnMRXEQmQ2aemov8YBtPS7vR"; - #[account] -pub struct EscrowState { +#[derive(InitSpace)] +pub struct Escrow { pub unlock_price: f64, pub escrow_amount: u64, } ``` -#### 4. Errors +### 4. Constants -Let’s define the custom errors we’ll use throughout the program. Inside the -`errors.rs` file, paste the following: +Next, we'll define `DISCRIMINATOR_SIZE` as 8, the PDA seed as `"MICHAEL BURRY"`, +and hard-code the SOL/USD oracle pubkey as `SOL_USDC_FEED` in the `constants.rs` +file. -```rust +```rust filename="constants.rs" +pub const DISCRIMINATOR_SIZE: usize = 8; +pub const ESCROW_SEED: &[u8] = b"MICHAEL BURRY"; +pub const SOL_USDC_FEED: &str = "GvDMxPzN1sCj7L26YDK2HnMRXEQmQ2aemov8YBtPS7vR"; +``` + +### 5. Errors + +Next, let's define the custom errors we'll use throughout the program. Inside +the `errors.rs` file, paste the following: + +```rust filename="errors.rs" use anchor_lang::prelude::*; #[error_code] @@ -799,203 +816,201 @@ pub enum EscrowErrorCode { #[msg("Switchboard feed exceeded provided confidence interval")] ConfidenceIntervalExceeded, #[msg("Current SOL price is not above Escrow unlock price.")] - SolPriceAboveUnlockPrice, + SolPriceBelowUnlockPrice, } ``` -#### 5. `mod.rs` +### 6. Setup mod.rs Let's set up our `instructions/mod.rs` file. -```rust -// inside mod.rs +```rust filename="mod.rs" pub mod deposit; pub mod withdraw; ``` -#### 6. **Deposit** +### 7. Deposit + +Now that we have all of the boilerplate out of the way, let's move on to our +`Deposit` instruction. This will live in the `/src/instructions/deposit.rs` +file. -Now that we have all of the boilerplate out of the way, lets move onto our -Deposit instruction. This will live in the `/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 +When a user deposits, a PDA should be created with the "MICHAEL BURRY" string +and the user's pubkey as seeds. This ensures that 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 SOL that the user wants to lock up to it. The user -will need to be a signer. +PDA and transfer the SOL 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 +Let's first build the `Deposit` context struct. To do this, we need to think +about what accounts will be necessary for this instruction. We start with the following: -```rust -//inside deposit.rs +```rust filename="deposit.rs" +use crate::constants::*; use crate::state::*; use anchor_lang::prelude::*; -use anchor_lang::solana_program::{ - system_instruction::transfer, - program::invoke -}; +use anchor_lang::solana_program::{program::invoke, system_instruction::transfer}; #[derive(Accounts)] pub struct Deposit<'info> { - // user account #[account(mut)] pub user: Signer<'info>, + #[account( - init, - seeds = [ESCROW_SEED, user.key().as_ref()], - bump, - payer = user, - space = std::mem::size_of::() + 8 + init, + seeds = [ESCROW_SEED, user.key().as_ref()], + bump, + payer = user, + space = DISCRIMINATOR_SIZE + Escrow::INIT_SPACE )] - pub escrow_account: Account<'info, EscrowState>, - // system program + pub escrow_account: Account<'info, Escrow>, + pub system_program: Program<'info, System>, } ``` Notice the constraints we added to the accounts: -- Because we'll be transferring SOL from the User account to the `escrow_state` +- Because we'll be transferring SOL from the User account to the `escrow` account, they both need to be mutable. - We know the `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 + BURRY” string and the user's pubkey. We can use Anchor account constraints to guarantee that the address passed in actually meets that requirement. - We also know that we have to initialize an account at this PDA to store some state for the program. We use the `init` constraint here. -Let’s move onto the actual logic. All we need to do is to initialize the state -of the `escrow_state` account and transfer the SOL. We expect the user to pass -in the amount of SOL they want to lock up in escrow and the price to unlock it -at. We will store these values in the `escrow_state` account. +Let's move onto the actual logic. All we need to do is to initialize the state +of the `escrow` account and transfer the SOL. We expect the user to pass in the +amount of SOL they want to lock up in escrow and the price to unlock it at. We +will store these values in the `escrow` account. After that, the method should execute the transfer. This program will be locking -up native SOL. Because of this, we don’t need to use token accounts or the -Solana token program. We’ll have to use the `system_program` to transfer the +up native SOL. Because of this, we don't need to use token accounts or the +Solana 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. -```rust -pub fn deposit_handler(ctx: Context, escrow_amt: u64, unlock_price: u64) -> Result<()> { - msg!("Depositing funds in escrow..."); +```rust filename="deposit.rs" +pub fn deposit_handler(ctx: Context, escrow_amount: u64, unlock_price: f64) -> Result<()> { + msg!("Depositing funds in escrow..."); - let escrow_state = &mut ctx.accounts.escrow_account; - escrow_state.unlock_price = unlock_price; - escrow_state.escrow_amount = escrow_amount; + let escrow = &mut ctx.accounts.escrow_account; + escrow.unlock_price = unlock_price; + escrow.escrow_amount = escrow_amount; - let transfer_ix = transfer( - &ctx.accounts.user.key(), - &escrow_state.key(), - escrow_amount - ); + let transfer_instruction = + transfer(&ctx.accounts.user.key(), &escrow.key(), escrow_amount); invoke( - &transfer_ix, + &transfer_instruction, &[ ctx.accounts.user.to_account_info(), ctx.accounts.escrow_account.to_account_info(), - ctx.accounts.system_program.to_account_info() - ] + ctx.accounts.system_program.to_account_info(), + ], )?; - msg!("Transfer complete. Escrow will unlock SOL at {}", &ctx.accounts.escrow_account.unlock_price); + msg!( + "Transfer complete. Escrow will unlock SOL at {}", + &ctx.accounts.escrow_account.unlock_price + ); + + Ok(()) } ``` -That’s is the gist of the deposit instruction! The final result of the +That's is the gist of the deposit instruction handler! The final result of the `deposit.rs` file should look as follows: -```rust +```rust filename="deposit.rs" +use crate::constants::*; use crate::state::*; use anchor_lang::prelude::*; -use anchor_lang::solana_program::{ - system_instruction::transfer, - program::invoke -}; +use anchor_lang::solana_program::{program::invoke, system_instruction::transfer}; pub fn deposit_handler(ctx: Context, escrow_amount: u64, unlock_price: f64) -> Result<()> { msg!("Depositing funds in escrow..."); - let escrow_state = &mut ctx.accounts.escrow_account; - escrow_state.unlock_price = unlock_price; - escrow_state.escrow_amount = escrow_amount; + let escrow = &mut ctx.accounts.escrow_account; + escrow.unlock_price = unlock_price; + escrow.escrow_amount = escrow_amount; - let transfer_ix = transfer( - &ctx.accounts.user.key(), - &escrow_state.key(), - escrow_amount - ); + let transfer_instruction = + transfer(&ctx.accounts.user.key(), &escrow.key(), escrow_amount); invoke( - &transfer_ix, + &transfer_instruction, &[ ctx.accounts.user.to_account_info(), ctx.accounts.escrow_account.to_account_info(), - ctx.accounts.system_program.to_account_info() - ] + ctx.accounts.system_program.to_account_info(), + ], )?; - msg!("Transfer complete. Escrow will unlock SOL at {}", &ctx.accounts.escrow_account.unlock_price); + msg!( + "Transfer complete. Escrow will unlock SOL at {}", + &ctx.accounts.escrow_account.unlock_price + ); Ok(()) } #[derive(Accounts)] pub struct Deposit<'info> { - // user account #[account(mut)] pub user: Signer<'info>, - // account to store SOL in escrow + #[account( init, seeds = [ESCROW_SEED, user.key().as_ref()], bump, payer = user, - space = std::mem::size_of::() + 8 + space = DISCRIMINATOR_SIZE + Escrow::INIT_SPACE )] - pub escrow_account: Account<'info, EscrowState>, + pub escrow_account: Account<'info, Escrow>, pub system_program: Program<'info, System>, } ``` -**Withdraw** +### 8. Withdraw -The withdraw instruction will require the same three accounts as the deposit -instruction plus the SOL_USDC Switchboard feed account. This code will go in the -`withdraw.rs` file. +The `Withdraw` instruction will require the same three accounts as the `Deposit` +instruction, plus the `SOL_USDC` Switchboard feed account. This code will be +placed in the `withdraw.rs` file. -```rust -use crate::state::*; +```rust filename="withdraw.rs" +use crate::constants::*; use crate::errors::*; -use std::str::FromStr; +use crate::state::*; use anchor_lang::prelude::*; -use switchboard_v2::AggregatorAccountData; use anchor_lang::solana_program::clock::Clock; +use std::str::FromStr; +use switchboard_solana::AggregatorAccountData; #[derive(Accounts)] pub struct Withdraw<'info> { - // user account #[account(mut)] pub user: Signer<'info>, - // escrow account + #[account( mut, seeds = [ESCROW_SEED, user.key().as_ref()], bump, close = user )] - pub escrow_account: Account<'info, EscrowState>, - // Switchboard SOL feed aggregator + pub escrow_account: Account<'info, Escrow>, + #[account( address = Pubkey::from_str(SOL_USDC_FEED).unwrap() )] pub feed_aggregator: AccountLoader<'info, AggregatorAccountData>, + pub system_program: Program<'info, System>, } ``` -Notice we’re using the close constraint because once the transaction completes, +Notice we're using the close constraint because once the transaction completes, we want to close the `escrow_account`. The SOL used as rent in the account will be transferred to the user account. @@ -1004,36 +1019,35 @@ 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 +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 SOL stored in the +Now let's implement the withdraw instruction handler's logic. First, we check if +the feed is stale. Then we fetch the current price of SOL 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 SOL 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. +escrow account back to the user and close the account. If it isn't, then the +instruction handler should finish and return an error. -```rust -pub fn withdraw_handler(ctx: Context, params: WithdrawParams) -> Result<()> { +```rust filename="withdraw.rs" +pub fn withdraw_handler(ctx: Context) -> Result<()> { let feed = &ctx.accounts.feed_aggregator.load()?; - let escrow_state = &ctx.accounts.escrow_account; + let escrow = &ctx.accounts.escrow_account; - // get result - let val: f64 = feed.get_result()?.try_into()?; + let current_sol_price: f64 = feed.get_result()?.try_into()?; - // check whether the feed has been updated in the last 300 seconds + // Check if the feed has been updated in the last 5 minutes (300 seconds) feed.check_staleness(Clock::get().unwrap().unix_timestamp, 300) - .map_err(|_| error!(EscrowErrorCode::StaleFeed))?; + .map_err(|_| error!(EscrowErrorCode::StaleFeed))?; - msg!("Current feed result is {}!", val); - msg!("Unlock price is {}", escrow_state.unlock_price); + msg!("Current SOL price is {}", current_sol_price); + msg!("Unlock price is {}", escrow.unlock_price); - if val < escrow_state.unlock_price as f64 { - return Err(EscrowErrorCode::SolPriceAboveUnlockPrice.into()) + if current_sol_price < escrow.unlock_price { + return Err(EscrowErrorCode::SolPriceBelowUnlockPrice.into()); } - .... + .... } ``` @@ -1047,63 +1061,77 @@ following error. 'Transfer: `from` must not carry data' ``` -To account for this, we’ll use `try_borrow_mut_lamports()` on each account and +To account for this, we'll use `try_borrow_mut_lamports()` on each account and add/subtract the amount of lamports stored in each account. -```rust -// 'Transfer: `from` must not carry data' - **escrow_state.to_account_info().try_borrow_mut_lamports()? = escrow_state - .to_account_info() - .lamports() - .checked_sub(escrow_state.escrow_amount) - .ok_or(ProgramError::InvalidArgument)?; - - **ctx.accounts.user.to_account_info().try_borrow_mut_lamports()? = ctx.accounts.user - .to_account_info() - .lamports() - .checked_add(escrow_state.escrow_amount) - .ok_or(ProgramError::InvalidArgument)?; +```rust filename="withdraw.rs" +// Transfer lamports from escrow to user +**escrow.to_account_info().try_borrow_mut_lamports()? = escrow +.to_account_info() +.lamports() +.checked_sub(escrow_lamports) +.ok_or(ProgramError::InsufficientFunds)?; + +**ctx +.accounts +.user +.to_account_info() +.try_borrow_mut_lamports()? = ctx +.accounts +.user +.to_account_info() +.lamports() +.checked_add(escrow_lamports) +.ok_or(ProgramError::InvalidArgument)?; ``` The final withdraw method in the `withdraw.rs` file should look like this: -```rust -use crate::state::*; +```rust filename="withdraw.rs" +use crate::constants::*; use crate::errors::*; -use std::str::FromStr; +use crate::state::*; use anchor_lang::prelude::*; -use switchboard_v2::AggregatorAccountData; use anchor_lang::solana_program::clock::Clock; +use std::str::FromStr; +use switchboard_solana::AggregatorAccountData; pub fn withdraw_handler(ctx: Context) -> Result<()> { let feed = &ctx.accounts.feed_aggregator.load()?; - let escrow_state = &ctx.accounts.escrow_account; + let escrow = &ctx.accounts.escrow_account; - // get result - let val: f64 = feed.get_result()?.try_into()?; + let current_sol_price: f64 = feed.get_result()?.try_into()?; - // check whether the feed has been updated in the last 300 seconds + // Check if the feed has been updated in the last 5 minutes (300 seconds) feed.check_staleness(Clock::get().unwrap().unix_timestamp, 300) - .map_err(|_| error!(EscrowErrorCode::StaleFeed))?; + .map_err(|_| error!(EscrowErrorCode::StaleFeed))?; - msg!("Current feed result is {}!", val); - msg!("Unlock price is {}", escrow_state.unlock_price); + msg!("Current SOL price is {}", current_sol_price); + msg!("Unlock price is {}", escrow.unlock_price); - if val < escrow_state.unlock_price as f64 { - return Err(EscrowErrorCode::SolPriceAboveUnlockPrice.into()) + if current_sol_price < escrow.unlock_price { + return Err(EscrowErrorCode::SolPriceBelowUnlockPrice.into()); } - // 'Transfer: `from` must not carry data' - **escrow_state.to_account_info().try_borrow_mut_lamports()? = escrow_state + let escrow_lamports = escrow.escrow_amount; + + // Transfer lamports from escrow to user + **escrow.to_account_info().try_borrow_mut_lamports()? = escrow .to_account_info() .lamports() - .checked_sub(escrow_state.escrow_amount) - .ok_or(ProgramError::InvalidArgument)?; + .checked_sub(escrow_lamports) + .ok_or(ProgramError::InsufficientFunds)?; - **ctx.accounts.user.to_account_info().try_borrow_mut_lamports()? = ctx.accounts.user + **ctx + .accounts + .user + .to_account_info() + .try_borrow_mut_lamports()? = ctx + .accounts + .user .to_account_info() .lamports() - .checked_add(escrow_state.escrow_amount) + .checked_add(escrow_lamports) .ok_or(ProgramError::InvalidArgument)?; Ok(()) @@ -1111,41 +1139,30 @@ pub fn withdraw_handler(ctx: Context) -> Result<()> { #[derive(Accounts)] pub struct Withdraw<'info> { - // user account #[account(mut)] pub user: Signer<'info>, - // escrow account + #[account( mut, seeds = [ESCROW_SEED, user.key().as_ref()], bump, close = user )] - pub escrow_account: Account<'info, EscrowState>, - // Switchboard SOL feed aggregator + pub escrow_account: Account<'info, Escrow>, + #[account( address = Pubkey::from_str(SOL_USDC_FEED).unwrap() )] pub feed_aggregator: AccountLoader<'info, AggregatorAccountData>, + pub system_program: Program<'info, System>, } ``` -And that’s it for the program! At this point, you should be able to run +And that's it for the program! At this point, you should be able to run `anchor build` without any errors. - - -If you see an error like the one presented below, you can safely ignore it. - -```bash -Compiling switchboard-v2 v0.4.0 -Error: Function _ZN86_$LT$switchboard_v2..aggregator..AggregatorAccountData$u20$as$u20$core..fmt..Debug$GT$3fmt17hea9f7644392c2647E Stack offset of 4128 exceeded max offset of 4096 by 32 bytes, please minimize large stack variables -``` - - - -#### 7. Testing +### 9. Testing Let's write some tests. We should have four of them: @@ -1161,11 +1178,10 @@ Note that there can only be one escrow per user, so the above order matters. We'll provide all the testing code in one snippet. Take a look through to make sure you understand it before running `anchor test`. -```typescript -// tests/burry-escrow.ts - +```typescript filename="burry-escrow.ts" +// Inside tests/burry-escrow.ts import * as anchor from "@coral-xyz/anchor"; -import { Program } from "@coral-xyz/anchor"; +import { Program, AnchorError } from "@coral-xyz/anchor"; import { BurryEscrow } from "../target/types/burry_escrow"; import { Big } from "@switchboard-xyz/common"; import { @@ -1173,229 +1189,227 @@ import { AnchorWallet, SwitchboardProgram, } from "@switchboard-xyz/solana.js"; +import { PublicKey, SystemProgram, Connection } from "@solana/web3.js"; import { assert } from "chai"; +import { confirmTransaction } from "@solana-developers/helpers"; -export const solUsedSwitchboardFeed = new anchor.web3.PublicKey( +const SOL_USD_SWITCHBOARD_FEED = new PublicKey( "GvDMxPzN1sCj7L26YDK2HnMRXEQmQ2aemov8YBtPS7vR", ); +const ESCROW_SEED = "MICHAEL BURRY"; +const DEVNET_RPC_URL = "https://api.devnet.solana.com"; +const CONFIRMATION_COMMITMENT = "confirmed"; +const PRICE_OFFSET = 10; +const ESCROW_AMOUNT = new anchor.BN(100); +const EXPECTED_ERROR_MESSAGE = + "Current SOL price is not above Escrow unlock price."; + +const provider = anchor.AnchorProvider.env(); +anchor.setProvider(provider); + +const program = anchor.workspace.BurryEscrow as Program; +const payer = (provider.wallet as AnchorWallet).payer; + describe("burry-escrow", () => { - // Configure the client to use the local cluster. - anchor.setProvider(anchor.AnchorProvider.env()); - const provider = anchor.AnchorProvider.env(); - const program = anchor.workspace.BurryEscrow as Program; - const payer = (provider.wallet as AnchorWallet).payer; - - it("Create Burry Escrow Below Price", async () => { - // fetch switchboard devnet program object - const switchboardProgram = await SwitchboardProgram.load( - "devnet", - new anchor.web3.Connection("https://api.devnet.solana.com"), + let switchboardProgram: SwitchboardProgram; + let aggregatorAccount: AggregatorAccount; + + before(async () => { + switchboardProgram = await SwitchboardProgram.load( + new Connection(DEVNET_RPC_URL), payer, ); - const aggregatorAccount = new AggregatorAccount( + aggregatorAccount = new AggregatorAccount( switchboardProgram, - solUsedSwitchboardFeed, + SOL_USD_SWITCHBOARD_FEED, ); + }); - // derive escrow state account - const [escrowState] = await anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("MICHAEL BURRY"), payer.publicKey.toBuffer()], + const createAndVerifyEscrow = async (unlockPrice: number) => { + const [escrow] = PublicKey.findProgramAddressSync( + [Buffer.from(ESCROW_SEED), payer.publicKey.toBuffer()], program.programId, ); - // fetch latest SOL price - const solPrice: Big | null = await aggregatorAccount.fetchLatestValue(); - if (solPrice === null) { - throw new Error("Aggregator holds no value"); - } - const failUnlockPrice = solPrice.minus(10).toNumber(); - const amountToLockUp = new anchor.BN(100); - - // Send transaction try { - const tx = await program.methods - .deposit(amountToLockUp, failUnlockPrice) - .accounts({ + const transaction = await program.methods + .deposit(ESCROW_AMOUNT, unlockPrice) + .accountsPartial({ user: payer.publicKey, - escrowAccount: escrowState, - systemProgram: anchor.web3.SystemProgram.programId, + escrowAccount: escrow, + systemProgram: SystemProgram.programId, }) .signers([payer]) .rpc(); - await provider.connection.confirmTransaction(tx, "confirmed"); - - // Fetch the created account - const newAccount = await program.account.escrowState.fetch(escrowState); + await confirmTransaction( + provider.connection, + transaction, + CONFIRMATION_COMMITMENT, + ); + const escrowAccount = await program.account.escrow.fetch(escrow); const escrowBalance = await provider.connection.getBalance( - escrowState, - "confirmed", + escrow, + CONFIRMATION_COMMITMENT, ); - console.log("Onchain unlock price:", newAccount.unlockPrice); + + console.log("Onchain unlock price:", escrowAccount.unlockPrice); console.log("Amount in escrow:", escrowBalance); - // Check whether the data onchain is equal to local 'data' - assert(failUnlockPrice == newAccount.unlockPrice); + assert(unlockPrice === escrowAccount.unlockPrice); assert(escrowBalance > 0); - } catch (e) { - console.log(e); - assert.fail(e); + } catch (error) { + console.error("Error details:", error); + throw new Error(`Failed to create escrow: ${error.message}`); } - }); - - it("Withdraw from escrow", async () => { - // derive escrow address - const [escrowState] = await anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("MICHAEL BURRY"), payer.publicKey.toBuffer()], - program.programId, - ); + }; - // send tx - const tx = await program.methods - .withdraw() - .accounts({ - user: payer.publicKey, - escrowAccount: escrowState, - feedAggregator: solUsedSwitchboardFeed, - systemProgram: anchor.web3.SystemProgram.programId, - }) - .signers([payer]) - .rpc(); - - await provider.connection.confirmTransaction(tx, "confirmed"); - - // assert that the escrow account has been closed - let accountFetchDidFail = false; - try { - await program.account.escrowState.fetch(escrowState); - } catch (e) { - accountFetchDidFail = true; + it("creates Burry Escrow Below Current Price", async () => { + const solPrice: Big | null = await aggregatorAccount.fetchLatestValue(); + if (solPrice === null) { + throw new Error("Aggregator holds no value"); } + // Although `SOL_USD_SWITCHBOARD_FEED` is not changing we are changing the unlockPrice in test as given below to simulate the escrow behaviour + const unlockPrice = solPrice.minus(PRICE_OFFSET).toNumber(); - assert(accountFetchDidFail); + await createAndVerifyEscrow(unlockPrice); }); - it("Create Burry Escrow Above Price", async () => { - // fetch switchboard devnet program object - const switchboardProgram = await SwitchboardProgram.load( - "devnet", - new anchor.web3.Connection("https://api.devnet.solana.com"), - payer, - ); - const aggregatorAccount = new AggregatorAccount( - switchboardProgram, - solUsedSwitchboardFeed, - ); - - // derive escrow state account - const [escrowState] = await anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("MICHAEL BURRY"), payer.publicKey.toBuffer()], + it("withdraws from escrow", async () => { + const [escrow] = PublicKey.findProgramAddressSync( + [Buffer.from(ESCROW_SEED), payer.publicKey.toBuffer()], program.programId, ); - console.log("Escrow Account: ", escrowState.toBase58()); - // fetch latest SOL price - const solPrice: Big | null = await aggregatorAccount.fetchLatestValue(); - if (solPrice === null) { - throw new Error("Aggregator holds no value"); - } - const failUnlockPrice = solPrice.plus(10).toNumber(); - const amountToLockUp = new anchor.BN(100); + const userBalanceBefore = await provider.connection.getBalance( + payer.publicKey, + ); - // Send transaction try { - const tx = await program.methods - .deposit(amountToLockUp, failUnlockPrice) - .accounts({ + const transaction = await program.methods + .withdraw() + .accountsPartial({ user: payer.publicKey, - escrowAccount: escrowState, - systemProgram: anchor.web3.SystemProgram.programId, + escrowAccount: escrow, + feedAggregator: SOL_USD_SWITCHBOARD_FEED, + systemProgram: SystemProgram.programId, }) .signers([payer]) .rpc(); - await provider.connection.confirmTransaction(tx, "confirmed"); - console.log("Your transaction signature", tx); - - // Fetch the created account - const newAccount = await program.account.escrowState.fetch(escrowState); - - const escrowBalance = await provider.connection.getBalance( - escrowState, - "confirmed", + await confirmTransaction( + provider.connection, + transaction, + CONFIRMATION_COMMITMENT, ); - console.log("Onchain unlock price:", newAccount.unlockPrice); - console.log("Amount in escrow:", escrowBalance); - // Check whether the data onchain is equal to local 'data' - assert(failUnlockPrice == newAccount.unlockPrice); - assert(escrowBalance > 0); - } catch (e) { - console.log(e); - assert.fail(e); + // Verify escrow account is closed + try { + await program.account.escrow.fetch(escrow); + assert.fail("Escrow account should have been closed"); + } catch (error) { + console.log(error.message); + assert( + error.message.includes("Account does not exist"), + "Unexpected error: " + error.message, + ); + } + + // Verify user balance increased + const userBalanceAfter = await provider.connection.getBalance( + payer.publicKey, + ); + assert( + userBalanceAfter > userBalanceBefore, + "User balance should have increased", + ); + } catch (error) { + throw new Error(`Failed to withdraw from escrow: ${error.message}`); } }); - it("Attempt to withdraw while price is below UnlockPrice", async () => { - let didFail = false; + it("creates Burry Escrow Above Current Price", async () => { + const solPrice: Big | null = await aggregatorAccount.fetchLatestValue(); + if (solPrice === null) { + throw new Error("Aggregator holds no value"); + } + // Although `SOL_USD_SWITCHBOARD_FEED` is not changing we are changing the unlockPrice in test as given below to simulate the escrow behaviour + const unlockPrice = solPrice.plus(PRICE_OFFSET).toNumber(); + await createAndVerifyEscrow(unlockPrice); + }); - // derive escrow address - const [escrowState] = await anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("MICHAEL BURRY"), payer.publicKey.toBuffer()], + it("fails to withdraw while price is below UnlockPrice", async () => { + const [escrow] = PublicKey.findProgramAddressSync( + [Buffer.from(ESCROW_SEED), payer.publicKey.toBuffer()], program.programId, ); - // send tx try { - const tx = await program.methods + await program.methods .withdraw() - .accounts({ + .accountsPartial({ user: payer.publicKey, - escrowAccount: escrowState, - feedAggregator: solUsedSwitchboardFeed, - systemProgram: anchor.web3.SystemProgram.programId, + escrowAccount: escrow, + feedAggregator: SOL_USD_SWITCHBOARD_FEED, + systemProgram: SystemProgram.programId, }) .signers([payer]) .rpc(); - await provider.connection.confirmTransaction(tx, "confirmed"); - console.log("Your transaction signature", tx); - } catch (e) { - // verify tx returns expected error - didFail = true; - console.log(e.error.errorMessage); - assert( - e.error.errorMessage == - "Current SOL price is not above Escrow unlock price.", - ); + assert.fail("Withdrawal should have failed"); + } catch (error) { + console.log(error.message); + if (error instanceof AnchorError) { + assert.include(error.message, EXPECTED_ERROR_MESSAGE); + } else if (error instanceof Error) { + assert.include(error.message, EXPECTED_ERROR_MESSAGE); + } else { + throw new Error(`Unexpected error type: ${error}`); + } } - - assert(didFail); }); }); ``` -If you feel confident in the testing logic, go ahead and run `anchor test` in -your shell of choice. You should get four passing tests. +Once you're confident with the testing logic, run `anchor test` in your +terminal. You should see four tests pass. -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](https://github.com/Unboxed-Software/michael-burry-escrow). +```bash + burry-escrow +Onchain unlock price: 137.42243 +Amount in escrow: 1058020 + ✔ creates Burry Escrow Below Current Price (765ms) +Account does not exist or has no data LxDZ9DXNwSFsu2e6u37o6C2T3k59B6ySEHHVaNDrgBq + ✔ withdraws from escrow (353ms) +Onchain unlock price: 157.42243 +Amount in escrow: 1058020 + ✔ creates Burry Escrow Above Current Price (406ms) +AnchorError occurred. Error Code: SolPriceBelowUnlockPrice. Error Number: 6003. Error Message: Current SOL price is not above Escrow unlock price.. + ✔ fails to withdraw while price is below UnlockPrice + + + 4 passing (2s) +``` + +If something goes wrong, review the lab and ensure everything is correct. Focus +on understanding the intent behind the code instead of just copying/pasting. You +can also review the working code on the +[`main` branch of burry-escrow GitHub repository](https://github.com/solana-developers/burry-escrow/tree/main). ### Challenge As an independent challenge, create a fallback plan if the data feed ever goes down. If the Oracle queue has not updated the aggregator account in X time or if -the data feed account does not exist anymore, withdraw the user’s escrowed +the data feed account does not exist anymore, withdraw the user's escrowed funds. A potential solution to this challenge can be found [in the Github repository on the `challenge-solution` branch](https://github.com/Unboxed-Software/michael-burry-escrow/tree/challenge-solution). + Push your code to GitHub and [tell us what you thought of this lesson](https://form.typeform.com/to/IPH0UGz7#answers-lesson=1a5d266c-f4c1-4c45-b986-2afd4be59991)!