Skip to content

Commit

Permalink
Add oracle (#7)
Browse files Browse the repository at this point in the history
* add oracle

* Fix: update observation_state as pda

* update test

* Fix: update observation len

* Fix: update error msg

* update test

---------

Co-authored-by: 0x777A <eddy@raydium.io>
  • Loading branch information
RainRaydium and 0x777A authored May 14, 2024
1 parent 0f86d46 commit 2562fa8
Show file tree
Hide file tree
Showing 12 changed files with 307 additions and 55 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ node_modules
test-ledger
.vscode
.yarn

Makefile

4 changes: 2 additions & 2 deletions programs/cp-swap/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ pub enum ErrorCode {
/// Address of the provided pool token mint is incorrect
#[msg("Address of the provided lp token mint is incorrect")]
IncorrectLpMint,
/// Swap instruction exceeds desired slippage limit
#[msg("Swap instruction exceeds desired slippage limit")]
/// Exceeds desired slippage limit
#[msg("Exceeds desired slippage limit")]
ExceededSlippage,
/// Given pool token amount results in zero trading tokens
#[msg("Given pool token amount results in zero trading tokens")]
Expand Down
1 change: 0 additions & 1 deletion programs/cp-swap/src/instructions/deposit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ pub fn deposit(
maximum_token_0_amount: u64,
maximum_token_1_amount: u64,
) -> Result<()> {
require_gt!(ctx.accounts.lp_mint.supply, 0);
let pool_id = ctx.accounts.pool_state.key();
let pool_state = &mut ctx.accounts.pool_state.load_mut()?;
if !pool_state.get_status_by_bit(PoolStatusBitIndex::Deposit) {
Expand Down
17 changes: 17 additions & 0 deletions programs/cp-swap/src/instructions/initialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,19 @@ pub struct Initialize<'info> {
)]
pub create_pool_fee: Box<InterfaceAccount<'info, TokenAccount>>,

/// an account to store oracle observations
#[account(
init,
seeds = [
OBSERVATION_SEED.as_bytes(),
pool_state.key().as_ref(),
],
bump,
payer = creator,
space = ObservationState::LEN
)]
pub observation_state: AccountLoader<'info, ObservationState>,

/// Program to create mint account and mint tokens
pub token_program: Program<'info, Token>,
/// Spl token program or token program 2022
Expand Down Expand Up @@ -194,6 +207,9 @@ pub fn initialize(
][..]],
)?;

let mut observation_state = ctx.accounts.observation_state.load_init()?;
observation_state.pool_id = ctx.accounts.pool_state.key();

let pool_state = &mut ctx.accounts.pool_state.load_init()?;

transfer_from_user_to_pool_vault(
Expand Down Expand Up @@ -298,6 +314,7 @@ pub fn initialize(
&ctx.accounts.token_0_mint,
&ctx.accounts.token_1_mint,
&ctx.accounts.lp_mint,
ctx.accounts.observation_state.key(),
);

Ok(())
Expand Down
30 changes: 30 additions & 0 deletions programs/cp-swap/src/instructions/swap_base_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ pub struct Swap<'info> {
address = output_vault.mint
)]
pub output_token_mint: Box<InterfaceAccount<'info, Mint>>,
/// The program account for the most recent oracle observation
#[account(mut, address = pool_state.load()?.observation_key)]
pub observation_state: AccountLoader<'info, ObservationState>,
}

pub fn swap_base_input(ctx: Context<Swap>, amount_in: u64, minimum_amount_out: u64) -> Result<()> {
Expand Down Expand Up @@ -220,5 +223,32 @@ pub fn swap_base_input(ctx: Context<Swap>, amount_in: u64, minimum_amount_out: u
&[&[crate::AUTH_SEED.as_bytes(), &[pool_state.auth_bump]]],
)?;

ctx.accounts.input_vault.reload()?;
ctx.accounts.output_vault.reload()?;
let (token_0_price_x64, token_1_price_x64) = if ctx.accounts.input_vault.key()
== pool_state.token_0_vault
&& ctx.accounts.output_vault.key() == pool_state.token_1_vault
{
pool_state.token_price_x32(
ctx.accounts.input_vault.amount,
ctx.accounts.output_vault.amount,
)
} else if ctx.accounts.input_vault.key() == pool_state.token_1_vault
&& ctx.accounts.output_vault.key() == pool_state.token_0_vault
{
pool_state.token_price_x32(
ctx.accounts.output_vault.amount,
ctx.accounts.input_vault.amount,
)
} else {
return err!(ErrorCode::InvalidVault);
};

ctx.accounts.observation_state.load_mut()?.update(
oracle::block_timestamp(),
token_0_price_x64,
token_1_price_x64,
);

Ok(())
}
27 changes: 27 additions & 0 deletions programs/cp-swap/src/instructions/swap_base_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,5 +162,32 @@ pub fn swap_base_output(
&[&[crate::AUTH_SEED.as_bytes(), &[pool_state.auth_bump]]],
)?;

ctx.accounts.input_vault.reload()?;
ctx.accounts.output_vault.reload()?;
let (token_0_price_x64, token_1_price_x64) = if ctx.accounts.input_vault.key()
== pool_state.token_0_vault
&& ctx.accounts.output_vault.key() == pool_state.token_1_vault
{
pool_state.token_price_x32(
ctx.accounts.input_vault.amount,
ctx.accounts.output_vault.amount,
)
} else if ctx.accounts.input_vault.key() == pool_state.token_1_vault
&& ctx.accounts.output_vault.key() == pool_state.token_0_vault
{
pool_state.token_price_x32(
ctx.accounts.output_vault.amount,
ctx.accounts.input_vault.amount,
)
} else {
return err!(ErrorCode::InvalidVault);
};

ctx.accounts.observation_state.load_mut()?.update(
oracle::block_timestamp(),
token_0_price_x64,
token_1_price_x64,
);

Ok(())
}
13 changes: 6 additions & 7 deletions programs/cp-swap/src/instructions/withdraw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,13 +151,6 @@ pub fn withdraw(
receive_token_1_amount,
token_1_transfer_fee
);

if receive_token_0_amount < minimum_token_0_amount
|| receive_token_1_amount < minimum_token_1_amount
{
return Err(ErrorCode::ExceededSlippage.into());
}

emit!(LpChangeEvent {
pool_id,
lp_amount_before: pool_state.lp_supply,
Expand All @@ -170,6 +163,12 @@ pub fn withdraw(
change_type: 1
});

if receive_token_0_amount < minimum_token_0_amount
|| receive_token_1_amount < minimum_token_1_amount
{
return Err(ErrorCode::ExceededSlippage.into());
}

pool_state.lp_supply = pool_state.lp_supply.checked_sub(lp_token_amount).unwrap();
token_burn(
ctx.accounts.owner.to_account_info(),
Expand Down
7 changes: 5 additions & 2 deletions programs/cp-swap/src/states/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
pub mod config;
pub mod pool;

pub use config::*;

pub mod pool;
pub use pool::*;

pub mod events;
pub use events::*;

pub mod oracle;
pub use oracle::*;
126 changes: 126 additions & 0 deletions programs/cp-swap/src/states/oracle.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/// Oracle provides price data useful for a wide variety of system designs
///
use anchor_lang::prelude::*;
#[cfg(test)]
use std::time::{SystemTime, UNIX_EPOCH};
/// Seed to derive account address and signature
pub const OBSERVATION_SEED: &str = "observation";
// Number of ObservationState element
pub const OBSERVATION_NUM: usize = 100;
pub const OBSERVATION_UPDATE_DURATION_DEFAULT: u64 = 15;

/// The element of observations in ObservationState
#[zero_copy(unsafe)]
#[repr(packed)]
#[derive(Default, Debug)]
pub struct Observation {
/// The block timestamp of the observation
pub block_timestamp: u64,
/// the cumulative of token0 price during the duration time, Q32.32, the remaining 64 bit for overflow
pub cumulative_token_0_price_x32: u128,
/// the cumulative of token1 price during the duration time, Q32.32, the remaining 64 bit for overflow
pub cumulative_token_1_price_x32: u128,
}
impl Observation {
pub const LEN: usize = 8 + 16 + 16;
}

#[account(zero_copy(unsafe))]
#[repr(packed)]
#[cfg_attr(feature = "client", derive(Debug))]
pub struct ObservationState {
/// Whether the ObservationState is initialized
pub initialized: bool,
/// the most-recently updated index of the observations array
pub observation_index: u16,
pub pool_id: Pubkey,
/// observation array
pub observations: [Observation; OBSERVATION_NUM],
/// padding for feature update
pub padding: [u64; 4],
}

impl Default for ObservationState {
#[inline]
fn default() -> ObservationState {
ObservationState {
initialized: false,
observation_index: 0,
pool_id: Pubkey::default(),
observations: [Observation::default(); OBSERVATION_NUM],
padding: [0u64; 4],
}
}
}

impl ObservationState {
pub const LEN: usize = 8 + 1 + 2 + 32 + (Observation::LEN * OBSERVATION_NUM) + 8 * 4;

// Writes an oracle observation to the account, returning the next observation_index.
/// Writable at most once per second. Index represents the most recently written element.
/// If the index is at the end of the allowable array length (100 - 1), the next index will turn to 0.
///
/// # Arguments
///
/// * `self` - The ObservationState account to write in
/// * `block_timestamp` - The current timestamp of to update
/// * `token_0_price_x32` - The token_0_price_x32 at the time of the new observation
/// * `token_1_price_x32` - The token_1_price_x32 at the time of the new observation
/// * `observation_index` - The last update index of element in the oracle array
///
/// # Return
/// * `next_observation_index` - The new index of element to update in the oracle array
///
pub fn update(
&mut self,
block_timestamp: u64,
token_0_price_x32: u128,
token_1_price_x32: u128,
) {
let observation_index = self.observation_index;
if !self.initialized {
self.initialized = true;
self.observations[observation_index as usize].block_timestamp = block_timestamp;
self.observations[observation_index as usize].cumulative_token_0_price_x32 = 0;
self.observations[observation_index as usize].cumulative_token_1_price_x32 = 0;
} else {
let last_observation = self.observations[observation_index as usize];
let delta_time = block_timestamp.saturating_sub(last_observation.block_timestamp);
if delta_time < OBSERVATION_UPDATE_DURATION_DEFAULT {
return;
}
let delta_token_0_price_x32 = token_0_price_x32.checked_mul(delta_time.into()).unwrap();
let delta_token_1_price_x32 = token_1_price_x32.checked_mul(delta_time.into()).unwrap();
let next_observation_index = if observation_index as usize == OBSERVATION_NUM - 1 {
0
} else {
observation_index + 1
};
self.observations[next_observation_index as usize].block_timestamp = block_timestamp;
// cumulative_token_price_x32 only occupies the first 64 bits, and the remaining 64 bits are used to store overflow data
self.observations[next_observation_index as usize].cumulative_token_0_price_x32 =
last_observation
.cumulative_token_0_price_x32
.wrapping_add(delta_token_0_price_x32);
self.observations[next_observation_index as usize].cumulative_token_1_price_x32 =
last_observation
.cumulative_token_1_price_x32
.wrapping_add(delta_token_1_price_x32);
self.observation_index = next_observation_index;
}
}
}

/// Returns the block timestamp truncated to 32 bits, i.e. mod 2**32
///
pub fn block_timestamp() -> u64 {
Clock::get().unwrap().unix_timestamp as u64 // truncation is desired
}

#[cfg(test)]
pub fn block_timestamp_mock() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
}
Loading

0 comments on commit 2562fa8

Please sign in to comment.