From 08fea6b24c34abdab27f907bedd1e20b8f1c73d7 Mon Sep 17 00:00:00 2001 From: Pham Tu Date: Fri, 7 Jun 2024 09:31:58 +0700 Subject: [PATCH] refractor code --- contracts/oraiswap-v3/src/contract.rs | 505 ++---------------- .../oraiswap-v3/src/entrypoints/common.rs | 171 ++++++ .../oraiswap-v3/src/entrypoints/execute.rs | 322 +++++++++++ contracts/oraiswap-v3/src/entrypoints/mod.rs | 7 + .../oraiswap-v3/src/entrypoints/query.rs | 59 ++ contracts/oraiswap-v3/src/lib.rs | 4 +- rustfmt.toml | 14 + 7 files changed, 626 insertions(+), 456 deletions(-) create mode 100644 contracts/oraiswap-v3/src/entrypoints/common.rs create mode 100644 contracts/oraiswap-v3/src/entrypoints/execute.rs create mode 100644 contracts/oraiswap-v3/src/entrypoints/mod.rs create mode 100644 contracts/oraiswap-v3/src/entrypoints/query.rs create mode 100644 rustfmt.toml diff --git a/contracts/oraiswap-v3/src/contract.rs b/contracts/oraiswap-v3/src/contract.rs index df32f22..728ce0a 100644 --- a/contracts/oraiswap-v3/src/contract.rs +++ b/contracts/oraiswap-v3/src/contract.rs @@ -1,27 +1,13 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; -use decimal::{CheckedOps, Decimal}; use crate::error::ContractError; -use crate::interface::{CalculateSwapResult, SwapHop}; -use crate::liquidity::Liquidity; use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; -use crate::percentage::Percentage; -use crate::sqrt_price::{get_max_tick, get_min_tick, SqrtPrice}; -use crate::state::{ - add_position, add_tick, flip_bitmap, get_closer_limit, get_tick, update_tick, CONFIG, POOLS, -}; -use crate::token_amount::TokenAmount; -use crate::{ - check_tick, compute_swap_step, Config, PoolKey, Position, Tick, - UpdatePoolTick, MAX_SQRT_PRICE, MIN_SQRT_PRICE, -}; +use crate::state::CONFIG; +use crate::{entrypoints::*, Config}; -use cosmwasm_std::{ - to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, Storage, WasmMsg, -}; +use cosmwasm_std::{to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; use cw2::set_contract_version; -use cw20::Cw20ExecuteMsg; // version info for migration info const CONTRACT_NAME: &str = "crates.io:oraiswap_v3"; @@ -102,445 +88,54 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { match msg { QueryMsg::ProtocolFee {} => to_binary(&get_protocol_fee(deps)?), QueryMsg::QuoteRoute { amount_in, swaps } => { - to_binary("e_route(deps.storage, env, amount_in, swaps)?) + to_binary("e_route(deps, env, amount_in, swaps)?) } } } -fn withdraw_protocol_fee( - deps: DepsMut, - info: MessageInfo, - pool_key: PoolKey, -) -> Result { - let pool_key_db = pool_key.key(); - let mut pool = POOLS.load(deps.storage, &pool_key_db)?; - - if pool.fee_receiver != info.sender { - return Err(ContractError::Unauthorized {}); - } - - let (fee_protocol_token_x, fee_protocol_token_y) = pool.withdraw_protocol_fee(); - POOLS.save(deps.storage, &pool_key_db, &pool)?; - - let mut msgs = vec![]; - - msgs.push(WasmMsg::Execute { - contract_addr: pool_key.token_x.to_string(), - msg: to_binary(&Cw20ExecuteMsg::Transfer { - recipient: pool.fee_receiver.to_string(), - amount: fee_protocol_token_x.into(), - })?, - funds: vec![], - }); - msgs.push(WasmMsg::Execute { - contract_addr: pool_key.token_y.to_string(), - msg: to_binary(&Cw20ExecuteMsg::Transfer { - recipient: pool.fee_receiver.to_string(), - amount: fee_protocol_token_y.into(), - })?, - funds: vec![], - }); - - Ok(Response::new() - .add_messages(msgs) - .add_attribute("action", "withdraw_protocol_fee")) -} - -fn change_protocol_fee( - deps: DepsMut, - info: MessageInfo, - protocol_fee: Percentage, -) -> Result { - let mut config = CONFIG.load(deps.storage)?; - - if info.sender != config.admin { - return Err(ContractError::Unauthorized {}); - } - - config.protocol_fee = protocol_fee; - CONFIG.save(deps.storage, &config)?; - - Ok(Response::new().add_attribute("action", "change_protocol_fee")) -} - -fn get_protocol_fee(deps: Deps) -> StdResult { - let config = CONFIG.load(deps.storage)?; - Ok(config.protocol_fee) -} - -fn change_fee_receiver( - deps: DepsMut, - info: MessageInfo, - pool_key: PoolKey, - fee_receiver: Addr, -) -> Result { - let config = CONFIG.load(deps.storage)?; - - if info.sender != config.admin { - return Err(ContractError::Unauthorized {}); - } - - let pool_key_db = pool_key.key(); - let mut pool = POOLS.load(deps.storage, &pool_key_db)?; - pool.fee_receiver = fee_receiver; - POOLS.save(deps.storage, &pool_key_db, &pool)?; - - Ok(Response::new().add_attribute("action", "change_fee_receiver")) -} - -fn create_tick( - store: &mut dyn Storage, - current_timestamp: u64, - pool_key: &PoolKey, - index: i32, -) -> Result { - check_tick(index, pool_key.fee_tier.tick_spacing)?; - let pool_key_db = pool_key.key(); - let pool = POOLS.load(store, &pool_key_db)?; - - let tick = Tick::create(index, &pool, current_timestamp); - add_tick(store, pool_key, index, &tick)?; - flip_bitmap(store, true, index, pool_key.fee_tier.tick_spacing, pool_key)?; - - Ok(tick) -} - -fn create_position( - deps: DepsMut, - env: Env, - info: MessageInfo, - pool_key: PoolKey, - lower_tick: i32, - upper_tick: i32, - liquidity_delta: Liquidity, - slippage_limit_lower: SqrtPrice, - slippage_limit_upper: SqrtPrice, -) -> Result { - let current_timestamp = env.block.time.nanos(); - let current_block_number = env.block.height; - - // liquidity delta = 0 => return - if liquidity_delta.is_zero() { - return Err(ContractError::InsufficientLiquidity {}); - } - - if lower_tick == upper_tick { - return Err(ContractError::InvalidTickIndex {}); - } - let pool_key_db = pool_key.key(); - let mut pool = POOLS - .load(deps.storage, &pool_key_db) - .map_err(|_| ContractError::PoolNotFound {})?; - - let mut lower_tick = match get_tick(deps.storage, &pool_key, lower_tick) { - Ok(tick) => tick, - _ => create_tick(deps.storage, current_timestamp, &pool_key, lower_tick)?, - }; - - let mut upper_tick = match get_tick(deps.storage, &pool_key, upper_tick) { - Ok(tick) => tick, - _ => create_tick(deps.storage, current_timestamp, &pool_key, upper_tick)?, - }; - - let (position, x, y) = Position::create( - &mut pool, - pool_key.clone(), - &mut lower_tick, - &mut upper_tick, - current_timestamp, - liquidity_delta, - slippage_limit_lower, - slippage_limit_upper, - current_block_number, - pool_key.fee_tier.tick_spacing, - )?; - - POOLS.save(deps.storage, &pool_key_db, &pool)?; - - add_position(deps.storage, &info.sender, &position)?; - - update_tick(deps.storage, &pool_key, lower_tick.index, &lower_tick)?; - update_tick(deps.storage, &pool_key, upper_tick.index, &upper_tick)?; - - let mut msgs = vec![]; - - msgs.push(WasmMsg::Execute { - contract_addr: pool_key.token_x.to_string(), - msg: to_binary(&Cw20ExecuteMsg::TransferFrom { - owner: info.sender.to_string(), - recipient: env.contract.address.to_string(), - amount: x.into(), - })?, - funds: vec![], - }); - msgs.push(WasmMsg::Execute { - contract_addr: pool_key.token_y.to_string(), - msg: to_binary(&Cw20ExecuteMsg::TransferFrom { - owner: info.sender.to_string(), - recipient: env.contract.address.to_string(), - amount: y.into(), - })?, - funds: vec![], - }); - - Ok(Response::new().add_messages(msgs).add_attributes(vec![ - ("action", "create_position"), - ("sender", info.sender.as_str()), - ("lower_tick", &lower_tick.index.to_string()), - ("upper_tick", &upper_tick.index.to_string()), - ("sqrt_price", &pool.sqrt_price.to_string()), - ])) -} - -fn calculate_swap( - store: &dyn Storage, - current_timestamp: u64, - pool_key: PoolKey, - x_to_y: bool, - amount: TokenAmount, - by_amount_in: bool, - sqrt_price_limit: SqrtPrice, -) -> Result { - if amount.is_zero() { - return Err(ContractError::AmountIsZero {}); - } - - let mut ticks: Vec = vec![]; - - let mut pool = POOLS.load(store, &pool_key.key())?; - - if x_to_y { - if pool.sqrt_price <= sqrt_price_limit || sqrt_price_limit > SqrtPrice::new(MAX_SQRT_PRICE) - { - return Err(ContractError::WrongLimit {}); - } - } else if pool.sqrt_price >= sqrt_price_limit - || sqrt_price_limit < SqrtPrice::new(MIN_SQRT_PRICE) - { - return Err(ContractError::WrongLimit {}); - } - - let tick_limit = if x_to_y { - get_min_tick(pool_key.fee_tier.tick_spacing) - } else { - get_max_tick(pool_key.fee_tier.tick_spacing) - }; - - let mut remaining_amount = amount; - - let mut total_amount_in = TokenAmount::new(0); - let mut total_amount_out = TokenAmount::new(0); - - let event_start_sqrt_price = pool.sqrt_price; - let mut event_fee_amount = TokenAmount::new(0); - - while !remaining_amount.is_zero() { - let (swap_limit, limiting_tick) = get_closer_limit( - store, - sqrt_price_limit, - x_to_y, - pool.current_tick_index, - pool_key.fee_tier.tick_spacing, - &pool_key, - )?; - - let result = compute_swap_step( - pool.sqrt_price, - swap_limit, - pool.liquidity, - remaining_amount, - by_amount_in, - pool_key.fee_tier.fee, - )?; - - // make remaining amount smaller - if by_amount_in { - remaining_amount = remaining_amount - .checked_sub(result.amount_in + result.fee_amount) - .map_err(|_| ContractError::SubtractionError)?; - } else { - remaining_amount = remaining_amount - .checked_sub(result.amount_out) - .map_err(|_| ContractError::SubtractionError)?; - } - - pool.add_fee(result.fee_amount, x_to_y, CONFIG.load(store)?.protocol_fee)?; - event_fee_amount += result.fee_amount; - - pool.sqrt_price = result.next_sqrt_price; - - total_amount_in += result.amount_in + result.fee_amount; - total_amount_out += result.amount_out; - - // Fail if price would go over swap limit - if pool.sqrt_price == sqrt_price_limit && !remaining_amount.is_zero() { - return Err(ContractError::PriceLimitReached {}); - } - - let mut tick_update = { - if let Some((tick_index, is_initialized)) = limiting_tick { - if is_initialized { - let tick = get_tick(store, &pool_key, tick_index)?; - UpdatePoolTick::TickInitialized(tick) - } else { - UpdatePoolTick::TickUninitialized(tick_index) - } - } else { - UpdatePoolTick::NoTick - } - }; - - let (amount_to_add, amount_after_tick_update, has_crossed) = pool.update_tick( - result, - swap_limit, - &mut tick_update, - remaining_amount, - by_amount_in, - x_to_y, - current_timestamp, - CONFIG.load(store)?.protocol_fee, - pool_key.fee_tier, - )?; - - remaining_amount = amount_after_tick_update; - total_amount_in += amount_to_add; - - if let UpdatePoolTick::TickInitialized(tick) = tick_update { - if has_crossed { - ticks.push(tick) - } - } - - let reached_tick_limit = match x_to_y { - true => pool.current_tick_index <= tick_limit, - false => pool.current_tick_index >= tick_limit, - }; - - if reached_tick_limit { - return Err(ContractError::TickLimitReached {}); - } - } - if total_amount_out.is_zero() { - return Err(ContractError::NoGainSwap {}); - } - - Ok(CalculateSwapResult { - amount_in: total_amount_in, - amount_out: total_amount_out, - start_sqrt_price: event_start_sqrt_price, - target_sqrt_price: pool.sqrt_price, - fee: event_fee_amount, - pool, - ticks, - }) -} - -fn swap( - deps: DepsMut, - env: Env, - info: MessageInfo, - pool_key: PoolKey, - x_to_y: bool, - amount: TokenAmount, - by_amount_in: bool, - sqrt_price_limit: SqrtPrice, -) -> Result { - let current_timestamp = env.block.time.nanos(); - - let calculate_swap_result = calculate_swap( - deps.storage, - current_timestamp, - pool_key.clone(), - x_to_y, - amount, - by_amount_in, - sqrt_price_limit, - )?; - - let mut crossed_tick_indexes: Vec = vec![]; - - for tick in calculate_swap_result.ticks.iter() { - update_tick(deps.storage, &pool_key, tick.index, tick)?; - crossed_tick_indexes.push(tick.index); - } - - POOLS.save(deps.storage, &pool_key.key(), &calculate_swap_result.pool)?; - - let mut msgs = vec![]; - - if x_to_y { - msgs.push(WasmMsg::Execute { - contract_addr: pool_key.token_x.to_string(), - msg: to_binary(&Cw20ExecuteMsg::TransferFrom { - owner: info.sender.to_string(), - recipient: env.contract.address.to_string(), - amount: calculate_swap_result.amount_in.into(), - })?, - funds: vec![], - }); - msgs.push(WasmMsg::Execute { - contract_addr: pool_key.token_y.to_string(), - msg: to_binary(&Cw20ExecuteMsg::Transfer { - recipient: info.sender.to_string(), - amount: calculate_swap_result.amount_out.into(), - })?, - funds: vec![], - }); - } else { - msgs.push(WasmMsg::Execute { - contract_addr: pool_key.token_y.to_string(), - msg: to_binary(&Cw20ExecuteMsg::TransferFrom { - owner: info.sender.to_string(), - recipient: env.contract.address.to_string(), - amount: calculate_swap_result.amount_in.into(), - })?, - funds: vec![], - }); - msgs.push(WasmMsg::Execute { - contract_addr: pool_key.token_x.to_string(), - msg: to_binary(&Cw20ExecuteMsg::Transfer { - recipient: info.sender.to_string(), - amount: calculate_swap_result.amount_out.into(), - })?, - funds: vec![], - }); - } - - Ok(Response::new() - .add_messages(msgs) - .add_attribute("action", "swap") - .add_attribute("amount_out", calculate_swap_result.amount_out.to_string())) -} - -fn quote_route( - store: &dyn Storage, - env: Env, - amount_in: TokenAmount, - swaps: Vec, -) -> StdResult { - let mut next_swap_amount = amount_in; - - for swap_hop in swaps.iter() { - let SwapHop { pool_key, x_to_y } = swap_hop; - - let sqrt_price_limit = if *x_to_y { - SqrtPrice::new(MIN_SQRT_PRICE) - } else { - SqrtPrice::new(MAX_SQRT_PRICE) - }; - - let res = calculate_swap( - store, - env.block.time.nanos(), - pool_key.clone(), - *x_to_y, - next_swap_amount, - true, - sqrt_price_limit, - ).unwrap(); - - next_swap_amount = res.amount_out; - } - - Ok(next_swap_amount) -} +// fn route( +// &mut self, +// is_swap: bool, +// amount_in: TokenAmount, +// swaps: Vec, +// ) -> Result { +// let mut next_swap_amount = amount_in; + +// for swap in swaps.iter() { +// let SwapHop { pool_key, x_to_y } = *swap; + +// let sqrt_price_limit = if x_to_y { +// SqrtPrice::new(MIN_SQRT_PRICE) +// } else { +// SqrtPrice::new(MAX_SQRT_PRICE) +// }; + +// let result = if is_swap { +// self.swap(pool_key, x_to_y, next_swap_amount, true, sqrt_price_limit) +// } else { +// self.calculate_swap(pool_key, x_to_y, next_swap_amount, true, sqrt_price_limit) +// }?; + +// next_swap_amount = result.amount_out; +// } + +// Ok(next_swap_amount) +// } + +// fn swap_route( +// deps: Deps, +// amount_in: TokenAmount, +// expected_amount_out: TokenAmount, +// slippage: Percentage, +// swaps: Vec, +// ) -> Result<(), InvariantError> { +// let amount_out = self.route(true, amount_in, swaps)?; + +// let min_amount_out = calculate_min_amount_out(expected_amount_out, slippage); + +// if amount_out < min_amount_out { +// return Err(InvariantError::AmountUnderMinimumAmountOut); +// } + +// Ok(()) +// } diff --git a/contracts/oraiswap-v3/src/entrypoints/common.rs b/contracts/oraiswap-v3/src/entrypoints/common.rs new file mode 100644 index 0000000..953d207 --- /dev/null +++ b/contracts/oraiswap-v3/src/entrypoints/common.rs @@ -0,0 +1,171 @@ +use cosmwasm_std::Storage; +use decimal::{CheckedOps, Decimal}; + +use crate::{ + check_tick, compute_swap_step, + interface::CalculateSwapResult, + sqrt_price::{get_max_tick, get_min_tick, SqrtPrice}, + state::{add_tick, flip_bitmap, get_closer_limit, get_tick, CONFIG, POOLS}, + token_amount::TokenAmount, + ContractError, PoolKey, Tick, UpdatePoolTick, MAX_SQRT_PRICE, MIN_SQRT_PRICE, +}; + +pub fn create_tick( + store: &mut dyn Storage, + current_timestamp: u64, + pool_key: &PoolKey, + index: i32, +) -> Result { + check_tick(index, pool_key.fee_tier.tick_spacing)?; + let pool_key_db = pool_key.key(); + let pool = POOLS.load(store, &pool_key_db)?; + + let tick = Tick::create(index, &pool, current_timestamp); + add_tick(store, pool_key, index, &tick)?; + flip_bitmap(store, true, index, pool_key.fee_tier.tick_spacing, pool_key)?; + + Ok(tick) +} + +pub fn calculate_swap( + store: &dyn Storage, + current_timestamp: u64, + pool_key: PoolKey, + x_to_y: bool, + amount: TokenAmount, + by_amount_in: bool, + sqrt_price_limit: SqrtPrice, +) -> Result { + if amount.is_zero() { + return Err(ContractError::AmountIsZero {}); + } + + let mut ticks: Vec = vec![]; + + let mut pool = POOLS.load(store, &pool_key.key())?; + + if x_to_y { + if pool.sqrt_price <= sqrt_price_limit || sqrt_price_limit > SqrtPrice::new(MAX_SQRT_PRICE) + { + return Err(ContractError::WrongLimit {}); + } + } else if pool.sqrt_price >= sqrt_price_limit + || sqrt_price_limit < SqrtPrice::new(MIN_SQRT_PRICE) + { + return Err(ContractError::WrongLimit {}); + } + + let tick_limit = if x_to_y { + get_min_tick(pool_key.fee_tier.tick_spacing) + } else { + get_max_tick(pool_key.fee_tier.tick_spacing) + }; + + let mut remaining_amount = amount; + + let mut total_amount_in = TokenAmount::new(0); + let mut total_amount_out = TokenAmount::new(0); + + let event_start_sqrt_price = pool.sqrt_price; + let mut event_fee_amount = TokenAmount::new(0); + + while !remaining_amount.is_zero() { + let (swap_limit, limiting_tick) = get_closer_limit( + store, + sqrt_price_limit, + x_to_y, + pool.current_tick_index, + pool_key.fee_tier.tick_spacing, + &pool_key, + )?; + + let result = compute_swap_step( + pool.sqrt_price, + swap_limit, + pool.liquidity, + remaining_amount, + by_amount_in, + pool_key.fee_tier.fee, + )?; + + // make remaining amount smaller + if by_amount_in { + remaining_amount = remaining_amount + .checked_sub(result.amount_in + result.fee_amount) + .map_err(|_| ContractError::SubtractionError)?; + } else { + remaining_amount = remaining_amount + .checked_sub(result.amount_out) + .map_err(|_| ContractError::SubtractionError)?; + } + + pool.add_fee(result.fee_amount, x_to_y, CONFIG.load(store)?.protocol_fee)?; + event_fee_amount += result.fee_amount; + + pool.sqrt_price = result.next_sqrt_price; + + total_amount_in += result.amount_in + result.fee_amount; + total_amount_out += result.amount_out; + + // Fail if price would go over swap limit + if pool.sqrt_price == sqrt_price_limit && !remaining_amount.is_zero() { + return Err(ContractError::PriceLimitReached {}); + } + + let mut tick_update = { + if let Some((tick_index, is_initialized)) = limiting_tick { + if is_initialized { + let tick = get_tick(store, &pool_key, tick_index)?; + UpdatePoolTick::TickInitialized(tick) + } else { + UpdatePoolTick::TickUninitialized(tick_index) + } + } else { + UpdatePoolTick::NoTick + } + }; + + let (amount_to_add, amount_after_tick_update, has_crossed) = pool.update_tick( + result, + swap_limit, + &mut tick_update, + remaining_amount, + by_amount_in, + x_to_y, + current_timestamp, + CONFIG.load(store)?.protocol_fee, + pool_key.fee_tier, + )?; + + remaining_amount = amount_after_tick_update; + total_amount_in += amount_to_add; + + if let UpdatePoolTick::TickInitialized(tick) = tick_update { + if has_crossed { + ticks.push(tick) + } + } + + let reached_tick_limit = match x_to_y { + true => pool.current_tick_index <= tick_limit, + false => pool.current_tick_index >= tick_limit, + }; + + if reached_tick_limit { + return Err(ContractError::TickLimitReached {}); + } + } + if total_amount_out.is_zero() { + return Err(ContractError::NoGainSwap {}); + } + + Ok(CalculateSwapResult { + amount_in: total_amount_in, + amount_out: total_amount_out, + start_sqrt_price: event_start_sqrt_price, + target_sqrt_price: pool.sqrt_price, + fee: event_fee_amount, + pool, + ticks, + }) +} diff --git a/contracts/oraiswap-v3/src/entrypoints/execute.rs b/contracts/oraiswap-v3/src/entrypoints/execute.rs new file mode 100644 index 0000000..42422a4 --- /dev/null +++ b/contracts/oraiswap-v3/src/entrypoints/execute.rs @@ -0,0 +1,322 @@ +use crate::error::ContractError; +use crate::liquidity::Liquidity; +use crate::percentage::Percentage; +use crate::sqrt_price::SqrtPrice; +use crate::state::{add_position, get_tick, update_tick, CONFIG, POOLS}; +use crate::token_amount::TokenAmount; +use crate::{PoolKey, Position}; + +use cosmwasm_std::{to_binary, Addr, DepsMut, Env, MessageInfo, Response, WasmMsg}; +use cw20::Cw20ExecuteMsg; + +use super::{calculate_swap, create_tick}; + +/// Allows an fee receiver to withdraw collected fees. +/// +/// # Parameters +/// - `pool_key`: A unique key that identifies the specified pool. +/// +/// # Errors +/// - Reverts the call when the caller is an unauthorized receiver. +/// +/// # External contracts +/// - PSP22 +pub fn withdraw_protocol_fee( + deps: DepsMut, + info: MessageInfo, + pool_key: PoolKey, +) -> Result { + let pool_key_db = pool_key.key(); + let mut pool = POOLS.load(deps.storage, &pool_key_db)?; + + if pool.fee_receiver != info.sender { + return Err(ContractError::Unauthorized {}); + } + + let (fee_protocol_token_x, fee_protocol_token_y) = pool.withdraw_protocol_fee(); + POOLS.save(deps.storage, &pool_key_db, &pool)?; + + let mut msgs = vec![]; + + msgs.push(WasmMsg::Execute { + contract_addr: pool_key.token_x.to_string(), + msg: to_binary(&Cw20ExecuteMsg::Transfer { + recipient: pool.fee_receiver.to_string(), + amount: fee_protocol_token_x.into(), + })?, + funds: vec![], + }); + msgs.push(WasmMsg::Execute { + contract_addr: pool_key.token_y.to_string(), + msg: to_binary(&Cw20ExecuteMsg::Transfer { + recipient: pool.fee_receiver.to_string(), + amount: fee_protocol_token_y.into(), + })?, + funds: vec![], + }); + + Ok(Response::new() + .add_messages(msgs) + .add_attribute("action", "withdraw_protocol_fee")) +} + +/// Allows an admin to adjust the protocol fee. +/// +/// # Parameters +/// - `protocol_fee`: The expected fee represented as a percentage. +/// +/// # Errors +/// - Reverts the call when the caller is an unauthorized user. +pub fn change_protocol_fee( + deps: DepsMut, + info: MessageInfo, + protocol_fee: Percentage, +) -> Result { + let mut config = CONFIG.load(deps.storage)?; + + if info.sender != config.admin { + return Err(ContractError::Unauthorized {}); + } + + config.protocol_fee = protocol_fee; + CONFIG.save(deps.storage, &config)?; + + Ok(Response::new().add_attribute("action", "change_protocol_fee")) +} + +/// Allows admin to change current fee receiver. +/// +/// # Parameters +/// - `pool_key`: A unique key that identifies the specified pool. +/// - `fee_receiver`: An `AccountId` identifying the user authorized to claim fees. +/// +/// # Errors +/// - Reverts the call when the caller is an unauthorized user. +pub fn change_fee_receiver( + deps: DepsMut, + info: MessageInfo, + pool_key: PoolKey, + fee_receiver: Addr, +) -> Result { + let config = CONFIG.load(deps.storage)?; + + if info.sender != config.admin { + return Err(ContractError::Unauthorized {}); + } + + let pool_key_db = pool_key.key(); + let mut pool = POOLS.load(deps.storage, &pool_key_db)?; + pool.fee_receiver = fee_receiver; + POOLS.save(deps.storage, &pool_key_db, &pool)?; + + Ok(Response::new().add_attribute("action", "change_fee_receiver")) +} + +/// Opens a position. +/// +/// # Parameters +/// - `pool_key`: A unique key that identifies the specified pool. +/// - `lower_tick`: The index of the lower tick for opening the position. +/// - `upper_tick`: The index of the upper tick for opening the position. +/// - `liquidity_delta`: The desired liquidity provided by the user in the specified range. +/// - `slippage_limit_lower`: The price limit for downward movement to execute the position creation. +/// - `slippage_limit_upper`: The price limit for upward movement to execute the position creation. +/// +/// # Events +/// - On successful transfer, emits a `Create Position` event for the newly opened position. +/// +/// # Errors +/// - Fails if the user attempts to open a position with zero liquidity. +/// - Fails if the user attempts to create a position with invalid tick indexes or tick spacing. +/// - Fails if the price has reached the slippage limit. +/// - Fails if the allowance is insufficient or the user balance transfer fails. +/// - Fails if pool does not exist +/// +/// # External contracts +/// - PSP22 +pub fn create_position( + deps: DepsMut, + env: Env, + info: MessageInfo, + pool_key: PoolKey, + lower_tick: i32, + upper_tick: i32, + liquidity_delta: Liquidity, + slippage_limit_lower: SqrtPrice, + slippage_limit_upper: SqrtPrice, +) -> Result { + let current_timestamp = env.block.time.nanos(); + let current_block_number = env.block.height; + + // liquidity delta = 0 => return + if liquidity_delta.is_zero() { + return Err(ContractError::InsufficientLiquidity {}); + } + + if lower_tick == upper_tick { + return Err(ContractError::InvalidTickIndex {}); + } + let pool_key_db = pool_key.key(); + let mut pool = POOLS + .load(deps.storage, &pool_key_db) + .map_err(|_| ContractError::PoolNotFound {})?; + + let mut lower_tick = match get_tick(deps.storage, &pool_key, lower_tick) { + Ok(tick) => tick, + _ => create_tick(deps.storage, current_timestamp, &pool_key, lower_tick)?, + }; + + let mut upper_tick = match get_tick(deps.storage, &pool_key, upper_tick) { + Ok(tick) => tick, + _ => create_tick(deps.storage, current_timestamp, &pool_key, upper_tick)?, + }; + + let (position, x, y) = Position::create( + &mut pool, + pool_key.clone(), + &mut lower_tick, + &mut upper_tick, + current_timestamp, + liquidity_delta, + slippage_limit_lower, + slippage_limit_upper, + current_block_number, + pool_key.fee_tier.tick_spacing, + )?; + + POOLS.save(deps.storage, &pool_key_db, &pool)?; + + add_position(deps.storage, &info.sender, &position)?; + + update_tick(deps.storage, &pool_key, lower_tick.index, &lower_tick)?; + update_tick(deps.storage, &pool_key, upper_tick.index, &upper_tick)?; + + let mut msgs = vec![]; + + msgs.push(WasmMsg::Execute { + contract_addr: pool_key.token_x.to_string(), + msg: to_binary(&Cw20ExecuteMsg::TransferFrom { + owner: info.sender.to_string(), + recipient: env.contract.address.to_string(), + amount: x.into(), + })?, + funds: vec![], + }); + msgs.push(WasmMsg::Execute { + contract_addr: pool_key.token_y.to_string(), + msg: to_binary(&Cw20ExecuteMsg::TransferFrom { + owner: info.sender.to_string(), + recipient: env.contract.address.to_string(), + amount: y.into(), + })?, + funds: vec![], + }); + + Ok(Response::new().add_messages(msgs).add_attributes(vec![ + ("action", "create_position"), + ("sender", info.sender.as_str()), + ("lower_tick", &lower_tick.index.to_string()), + ("upper_tick", &upper_tick.index.to_string()), + ("sqrt_price", &pool.sqrt_price.to_string()), + ])) +} + +/// Performs a single swap based on the provided parameters. +/// +/// # Parameters +/// - `pool_key`: A unique key that identifies the specified pool. +/// - `x_to_y`: A boolean specifying the swap direction. +/// - `amount`: TokenAmount that the user wants to swap. +/// - `by_amount_in`: A boolean specifying whether the user provides the amount to swap or expects the amount out. +/// - `sqrt_price_limit`: A square root of price limit allowing the price to move for the swap to occur. +/// +/// # Events +/// - On a successful swap, emits a `Swap` event for the freshly made swap. +/// - On a successful swap, emits a `Cross Tick` event for every single tick crossed. +/// +/// # Errors +/// - Fails if the user attempts to perform a swap with zero amounts. +/// - Fails if the price has reached the specified price limit (or price associated with specified square root of price). +/// - Fails if the user would receive zero tokens. +/// - Fails if the allowance is insufficient or the user balance transfer fails. +/// - Fails if there is insufficient liquidity in pool +/// - Fails if pool does not exist +/// +/// # External contracts +/// - PSP22 +pub fn swap( + deps: DepsMut, + env: Env, + info: MessageInfo, + pool_key: PoolKey, + x_to_y: bool, + amount: TokenAmount, + by_amount_in: bool, + sqrt_price_limit: SqrtPrice, +) -> Result { + let current_timestamp = env.block.time.nanos(); + + let calculate_swap_result = calculate_swap( + deps.storage, + current_timestamp, + pool_key.clone(), + x_to_y, + amount, + by_amount_in, + sqrt_price_limit, + )?; + + let mut crossed_tick_indexes: Vec = vec![]; + + for tick in calculate_swap_result.ticks.iter() { + update_tick(deps.storage, &pool_key, tick.index, tick)?; + crossed_tick_indexes.push(tick.index); + } + + POOLS.save(deps.storage, &pool_key.key(), &calculate_swap_result.pool)?; + + let mut msgs = vec![]; + + if x_to_y { + msgs.push(WasmMsg::Execute { + contract_addr: pool_key.token_x.to_string(), + msg: to_binary(&Cw20ExecuteMsg::TransferFrom { + owner: info.sender.to_string(), + recipient: env.contract.address.to_string(), + amount: calculate_swap_result.amount_in.into(), + })?, + funds: vec![], + }); + msgs.push(WasmMsg::Execute { + contract_addr: pool_key.token_y.to_string(), + msg: to_binary(&Cw20ExecuteMsg::Transfer { + recipient: info.sender.to_string(), + amount: calculate_swap_result.amount_out.into(), + })?, + funds: vec![], + }); + } else { + msgs.push(WasmMsg::Execute { + contract_addr: pool_key.token_y.to_string(), + msg: to_binary(&Cw20ExecuteMsg::TransferFrom { + owner: info.sender.to_string(), + recipient: env.contract.address.to_string(), + amount: calculate_swap_result.amount_in.into(), + })?, + funds: vec![], + }); + msgs.push(WasmMsg::Execute { + contract_addr: pool_key.token_x.to_string(), + msg: to_binary(&Cw20ExecuteMsg::Transfer { + recipient: info.sender.to_string(), + amount: calculate_swap_result.amount_out.into(), + })?, + funds: vec![], + }); + } + + Ok(Response::new() + .add_messages(msgs) + .add_attribute("action", "swap") + .add_attribute("amount_out", calculate_swap_result.amount_out.to_string())) +} diff --git a/contracts/oraiswap-v3/src/entrypoints/mod.rs b/contracts/oraiswap-v3/src/entrypoints/mod.rs new file mode 100644 index 0000000..2d61618 --- /dev/null +++ b/contracts/oraiswap-v3/src/entrypoints/mod.rs @@ -0,0 +1,7 @@ +mod common; +mod execute; +mod query; + +pub use common::*; +pub use execute::*; +pub use query::*; diff --git a/contracts/oraiswap-v3/src/entrypoints/query.rs b/contracts/oraiswap-v3/src/entrypoints/query.rs new file mode 100644 index 0000000..4bf271a --- /dev/null +++ b/contracts/oraiswap-v3/src/entrypoints/query.rs @@ -0,0 +1,59 @@ +use cosmwasm_std::{Deps, Env, StdResult}; +use decimal::Decimal; + +use crate::{ + interface::SwapHop, percentage::Percentage, sqrt_price::SqrtPrice, state::CONFIG, + token_amount::TokenAmount, MAX_SQRT_PRICE, MIN_SQRT_PRICE, +}; + +use super::calculate_swap; + +/// Retrieves the protocol fee represented as a percentage. +pub fn get_protocol_fee(deps: Deps) -> StdResult { + let config = CONFIG.load(deps.storage)?; + Ok(config.protocol_fee) +} + +/// Simulates multiple swaps without its execution. +/// +/// # Parameters +/// - `amount_in`: The amount of tokens that the user wants to swap. +/// - `swaps`: A vector containing all parameters needed to identify separate swap steps. +/// +/// # Errors +/// - Fails if the user attempts to perform a swap with zero amounts. +/// - Fails if the user would receive zero tokens. +/// - Fails if pool does not exist +pub fn quote_route( + deps: Deps, + env: Env, + amount_in: TokenAmount, + swaps: Vec, +) -> StdResult { + let mut next_swap_amount = amount_in; + + for swap_hop in swaps.iter() { + let SwapHop { pool_key, x_to_y } = swap_hop; + + let sqrt_price_limit = if *x_to_y { + SqrtPrice::new(MIN_SQRT_PRICE) + } else { + SqrtPrice::new(MAX_SQRT_PRICE) + }; + + let res = calculate_swap( + deps.storage, + env.block.time.nanos(), + pool_key.clone(), + *x_to_y, + next_swap_amount, + true, + sqrt_price_limit, + ) + .unwrap(); + + next_swap_amount = res.amount_out; + } + + Ok(next_swap_amount) +} diff --git a/contracts/oraiswap-v3/src/lib.rs b/contracts/oraiswap-v3/src/lib.rs index d2a97dc..f2bda32 100644 --- a/contracts/oraiswap-v3/src/lib.rs +++ b/contracts/oraiswap-v3/src/lib.rs @@ -1,5 +1,7 @@ -pub mod contract; mod error; + +pub mod contract; +pub mod entrypoints; pub mod interface; pub mod msg; pub mod state; diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..0132d87 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,14 @@ +# stable +newline_style = "Unix" +hard_tabs = false +tab_spaces = 4 + +# unstable... should we require `rustup run nightly cargo fmt` ? +# or just update the style guide when they are stable? +#fn_single_line = true +#format_code_in_doc_comments = true +#overflow_delimited_expr = true +#reorder_impl_items = true +#struct_field_align_threshold = 20 +#struct_lit_single_line = true +#report_todo = "Always"