diff --git a/contracts/zapper/src/contract.rs b/contracts/zapper/src/contract.rs index cd65fef..3ab6967 100644 --- a/contracts/zapper/src/contract.rs +++ b/contracts/zapper/src/contract.rs @@ -69,14 +69,18 @@ pub fn execute( routes, minimum_liquidity, ), + ExecuteMsg::ZapInAfterSwapOperation {} => internal::zap_in_liquidity(deps, env, info), + ExecuteMsg::RefundAfterZapInLiquidity {} => internal::refund_after_zap_in(deps, env, info), ExecuteMsg::ZapOutLiquidity { position_index, routes, } => zap_out_liquidity(deps, env, info, position_index, routes), + ExecuteMsg::ZapOutAfterSwapOperation {} => internal::zap_out_liquidity(deps, env, info), ExecuteMsg::RegisterProtocolFee { percent, fee_receiver, } => execute_register_protocol_fee(deps, info, percent, fee_receiver), + ExecuteMsg::Withdraw { assets, recipient } => withdraw(deps, info, assets, recipient), } } @@ -94,13 +98,3 @@ pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result Result { - match reply.id { - ZAP_IN_LIQUIDITY_REPLY_ID => reply::zap_in_liquidity(deps, env), - ZAP_OUT_LIQUIDITY_REPLY_ID => reply::zap_out_liquidity(deps, env), - ADD_LIQUIDITY_REPLY_ID => reply::add_liquidity(deps, env), - _ => Err(ContractError::UnrecognizedReplyId { id: reply.id }), - } -} diff --git a/contracts/zapper/src/entrypoints/execute.rs b/contracts/zapper/src/entrypoints/execute.rs index cb6ae9d..0869209 100644 --- a/contracts/zapper/src/entrypoints/execute.rs +++ b/contracts/zapper/src/entrypoints/execute.rs @@ -14,12 +14,12 @@ use oraiswap_v3_common::{ use crate::{ contract::{ZAP_IN_LIQUIDITY_REPLY_ID, ZAP_OUT_LIQUIDITY_REPLY_ID}, entrypoints::common::get_pool_v3_asset_info, - msg::Route, + msg::{ExecuteMsg, Route}, state::{CONFIG, PENDING_POSITION, PROTOCOL_FEE, RECEIVER, SNAP_BALANCES, ZAP_OUT_ROUTES}, Config, PairBalance, PendingPosition, }; -use super::{build_swap_msg, validate_fund}; +use super::{build_swap_msg, internal, validate_fund}; pub fn update_config( deps: DepsMut, @@ -89,7 +89,6 @@ pub fn zap_in_liquidity( ) -> Result { // init messages and submessages let mut msgs: Vec = vec![]; - let mut sub_msgs: Vec = vec![]; // transfer the amount or check the fund is sent with request validate_fund( @@ -134,6 +133,7 @@ pub fn zap_in_liquidity( owner: env.contract.address.clone(), }, )?; + let position = PendingPosition::new( position_length, pool_key.clone(), @@ -176,17 +176,16 @@ pub fn zap_in_liquidity( None, None, )?; - if i == routes.len() - 1 { - sub_msgs.push(SubMsg::reply_on_success( - swap_msg, - ZAP_IN_LIQUIDITY_REPLY_ID, - )); - } else { - sub_msgs.push(SubMsg::new(swap_msg)); - } + msgs.push(CosmosMsg::Wasm(swap_msg)); } - Ok(Response::new().add_messages(msgs).add_submessages(sub_msgs)) + msgs.push(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: env.contract.address.to_string(), + msg: to_json_binary(&ExecuteMsg::ZapInAfterSwapOperation {})?, + funds: vec![], + })); + + Ok(Response::new().add_messages(msgs)) } pub fn zap_out_liquidity( @@ -197,7 +196,6 @@ pub fn zap_out_liquidity( routes: Vec, ) -> Result { let mut msgs: Vec = vec![]; - let mut sub_msgs: Vec = vec![]; let config = CONFIG.load(deps.storage)?; let position: Position = deps.querier.query_wasm_smart( config.dex_v3.to_string(), @@ -243,16 +241,42 @@ pub fn zap_out_liquidity( ZAP_OUT_ROUTES.save(deps.storage, &routes)?; // 2. Create SubMsg to process remove liquidity in dex_v3 contract - sub_msgs.push(SubMsg::reply_on_success( - WasmMsg::Execute { - contract_addr: config.dex_v3.to_string(), - msg: to_json_binary(&V3ExecuteMsg::Burn { - token_id: position.token_id, - })?, - funds: vec![], - }, - ZAP_OUT_LIQUIDITY_REPLY_ID, - )); + msgs.push(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: config.dex_v3.to_string(), + msg: to_json_binary(&V3ExecuteMsg::Burn { + token_id: position.token_id, + })?, + funds: vec![], + })); + + msgs.push(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: env.contract.address.to_string(), + msg: to_json_binary(&ExecuteMsg::ZapOutAfterSwapOperation {})?, + funds: vec![], + })); + + Ok(Response::new().add_messages(msgs)) +} + +pub fn withdraw( + deps: DepsMut, + info: MessageInfo, + assets: Vec, + recipient: Option, +) -> Result { + let config = CONFIG.load(deps.storage)?; + let receiver = recipient.unwrap_or_else(|| info.sender.clone()); + + if info.sender != config.admin { + return Err(ContractError::Unauthorized {}); + } + + let mut msgs: Vec = vec![]; + for asset in assets { + asset + .info + .transfer(&mut msgs, receiver.to_string(), asset.amount)?; + } - Ok(Response::new().add_messages(msgs).add_submessages(sub_msgs)) + Ok(Response::new().add_messages(msgs)) } diff --git a/contracts/zapper/src/entrypoints/reply.rs b/contracts/zapper/src/entrypoints/internal.rs similarity index 80% rename from contracts/zapper/src/entrypoints/reply.rs rename to contracts/zapper/src/entrypoints/internal.rs index 0034734..2b222b7 100644 --- a/contracts/zapper/src/entrypoints/reply.rs +++ b/contracts/zapper/src/entrypoints/internal.rs @@ -1,8 +1,7 @@ use std::vec; use cosmwasm_std::{ - to_json_binary, Coin, CosmosMsg, Decimal, DepsMut, Env, Order, Response, StdResult, SubMsg, - WasmMsg, + to_json_binary, Coin, CosmosMsg, Decimal, DepsMut, Env, MessageInfo, Order, Response, StdResult, SubMsg, WasmMsg }; use oraiswap_v3_common::{ @@ -19,6 +18,7 @@ use oraiswap_v3_common::{ use crate::{ contract::ADD_LIQUIDITY_REPLY_ID, + msg::ExecuteMsg, state::{ CONFIG, PENDING_POSITION, PROTOCOL_FEE, RECEIVER, SNAP_BALANCE, SNAP_BALANCES, ZAP_OUT_ROUTES, @@ -28,9 +28,12 @@ use crate::{ use super::build_swap_msg; -pub fn zap_in_liquidity(deps: DepsMut, env: Env) -> Result { +pub fn zap_in_liquidity(deps: DepsMut, env: Env, info: MessageInfo) -> Result { + if info.sender != env.contract.address { + return Err(ContractError::Unauthorized {}); + } + let mut msgs: Vec = vec![]; - let mut sub_msgs: Vec = vec![]; // 5. Recheck the balance of tokenX and tokenY in this contract let snap_balance = SNAP_BALANCE.load(deps.storage)?; @@ -49,7 +52,7 @@ pub fn zap_in_liquidity(deps: DepsMut, env: Env) -> Result Result Result { +pub fn refund_after_zap_in(deps: DepsMut, env: Env, info: MessageInfo) -> Result { + if info.sender != env.contract.address { + return Err(ContractError::Unauthorized {}); + } + let mut msgs: Vec = vec![]; // 8. Refund unused tokenX and tokenY to user @@ -191,7 +201,11 @@ pub fn add_liquidity(deps: DepsMut, env: Env) -> Result Ok(Response::new().add_messages(msgs)) } -pub fn zap_out_liquidity(deps: DepsMut, env: Env) -> Result { +pub fn zap_out_liquidity(deps: DepsMut, env: Env, info: MessageInfo) -> Result { + if info.sender != env.contract.address { + return Err(ContractError::Unauthorized {}); + } + let mut msgs: Vec = vec![]; let receiver = RECEIVER.load(deps.storage)?; let zap_out_routes = ZAP_OUT_ROUTES.load(deps.storage)?; diff --git a/contracts/zapper/src/entrypoints/mod.rs b/contracts/zapper/src/entrypoints/mod.rs index dabf91a..aad9f36 100644 --- a/contracts/zapper/src/entrypoints/mod.rs +++ b/contracts/zapper/src/entrypoints/mod.rs @@ -1,6 +1,6 @@ pub mod execute; pub mod query; -pub mod reply; +pub mod internal; pub mod common; pub use execute::*; diff --git a/contracts/zapper/src/msg.rs b/contracts/zapper/src/msg.rs index 0488dec..9d46383 100644 --- a/contracts/zapper/src/msg.rs +++ b/contracts/zapper/src/msg.rs @@ -27,14 +27,21 @@ pub enum ExecuteMsg { routes: Vec, minimum_liquidity: Option, }, + ZapInAfterSwapOperation {}, + RefundAfterZapInLiquidity {}, ZapOutLiquidity { position_index: u32, routes: Vec, }, + ZapOutAfterSwapOperation {}, RegisterProtocolFee { percent: Decimal, fee_receiver: Addr, }, + Withdraw { + assets: Vec, + recipient: Option, + } } #[cw_serde] diff --git a/contracts/zapper/src/tests/helper.rs b/contracts/zapper/src/tests/helper.rs index 476682d..30ecc15 100644 --- a/contracts/zapper/src/tests/helper.rs +++ b/contracts/zapper/src/tests/helper.rs @@ -50,8 +50,7 @@ impl MockApp { crate::contract::execute, crate::contract::instantiate, crate::contract::query, - ) - .with_reply_empty(crate::contract::reply), + ), )); } #[cfg(feature = "test-tube")] @@ -226,6 +225,24 @@ impl MockApp { ) } + pub fn withdraw( + &mut self, + sender: &str, + zapper: &str, + assets: Vec, + recipient: Option<&str>, + ) -> MockResult { + self.execute( + Addr::unchecked(sender), + Addr::unchecked(zapper), + &msg::ExecuteMsg::Withdraw { + assets, + recipient: recipient.map(Addr::unchecked), + }, + &[], + ) + } + pub fn get_zapper_config(&mut self, zapper: &str) -> StdResult { self.query(Addr::unchecked(zapper), &msg::QueryMsg::Config {}) } diff --git a/contracts/zapper/src/tests/mod.rs b/contracts/zapper/src/tests/mod.rs index e2c189b..fb8de01 100644 --- a/contracts/zapper/src/tests/mod.rs +++ b/contracts/zapper/src/tests/mod.rs @@ -3,3 +3,4 @@ mod config; mod helper; mod zapin; mod zapout; +mod withdraw; diff --git a/contracts/zapper/src/tests/testdata/zapper.wasm b/contracts/zapper/src/tests/testdata/zapper.wasm index f8f3895..bbc8f75 100644 Binary files a/contracts/zapper/src/tests/testdata/zapper.wasm and b/contracts/zapper/src/tests/testdata/zapper.wasm differ diff --git a/contracts/zapper/src/tests/withdraw.rs b/contracts/zapper/src/tests/withdraw.rs new file mode 100644 index 0000000..69d64e5 --- /dev/null +++ b/contracts/zapper/src/tests/withdraw.rs @@ -0,0 +1,44 @@ +use cosmwasm_std::{coins, Uint128}; +use decimal::*; +use oraiswap::mixed_router::SwapOperation; +use oraiswap_v3_common::asset::{Asset, AssetInfo}; +use oraiswap_v3_common::math::percentage::Percentage; + +use oraiswap_v3_common::storage::{FeeTier, PoolKey}; + +use crate::msg::Route; +use crate::tests::common::init_basic_v3_pool; +use crate::tests::helper::MockApp; +use crate::tests::helper::{macros::*, FEE_DENOM}; + +#[test] +fn test_withdraw() { + let (mut app, accounts) = MockApp::new(&[ + ("alice", &coins(100_000_000_000, FEE_DENOM)), + ("bob", &coins(100_000_000_000, FEE_DENOM)), + ]); + let alice = &accounts[0]; + let bob = &accounts[1]; + let initial_amount = 10u128.pow(20); + let (token_x, _, _) = + create_3_tokens!(app, initial_amount, initial_amount, initial_amount, alice); + + let zapper = create_zapper!(app, alice); + + app.mint_token(alice, zapper.as_str(), token_x.as_str(), 1) + .unwrap(); + let assets: Vec = vec![Asset { + info: AssetInfo::Token { + contract_addr: token_x.clone(), + }, + amount: Uint128::new(1), + }]; + let err = app.withdraw(bob, zapper.as_str(), assets.clone(), Some(bob)) + .unwrap_err(); + assert!(err.root_cause().to_string().contains("Unauthorized")); + + app.withdraw(alice, zapper.as_str(), assets, Some(bob)) + .unwrap(); + let balance = balance_of!(app, token_x, bob); + assert_eq!(balance, 1); +} diff --git a/contracts/zapper/src/tests/zapin.rs b/contracts/zapper/src/tests/zapin.rs index 7a54c89..867ec1f 100644 --- a/contracts/zapper/src/tests/zapin.rs +++ b/contracts/zapper/src/tests/zapin.rs @@ -626,3 +626,69 @@ fn test_zap_in_with_fee() { let fee_receiver_balance = balance_of!(app, token_z, charlie); assert_eq!(fee_receiver_balance, 100u128); } + +#[test] +fn test_zap_in_no_routes() { + let (mut app, accounts) = MockApp::new(&[ + ("alice", &coins(100_000_000_000, FEE_DENOM)), + ("bob", &coins(100_000_000_000, FEE_DENOM)), + ("charlie", &coins(100_000_000_000, FEE_DENOM)), + ]); + let alice = &accounts[0]; + let bob = &accounts[1]; + let charlie = &accounts[2]; + let initial_amount = 10u128.pow(20); + let (token_x, token_y, token_z) = + create_3_tokens!(app, initial_amount, initial_amount, initial_amount, alice); + + let zapper = create_zapper!(app, alice); + let config = app.get_zapper_config(zapper.as_str()).unwrap(); + + init_basic_v3_pool( + &mut app, &zapper, &token_x, &token_y, &token_z, &alice, &bob, + ); + + let protocol_fee = Percentage::from_scale(6, 3); + let fee_tier = FeeTier::new(protocol_fee, 1).unwrap(); + let pool_key_x_y = PoolKey::new(token_x.to_string(), token_y.to_string(), fee_tier).unwrap(); + + let tick_lower_index = 25; + let tick_upper_index = 30; + + // register protocol fee: 0.1% + app.register_protocol_fee( + &alice, + zapper.as_str(), + StdDecimal::from_ratio(1u128, 10u128), + &charlie, + ) + .unwrap(); + + // asset_in = token_x + let asset_in = Asset { + info: AssetInfo::Token { + contract_addr: token_x.clone(), + }, + amount: Uint128::new(1000), + }; + + // add successful + app.zap_in_liquidity( + &bob, + zapper.as_str(), + pool_key_x_y.clone(), + tick_lower_index, + tick_upper_index, + &asset_in, + vec![], + None, + ) + .unwrap(); + // get all positions + let all_positions = get_all_positions!(app, config.dex_v3, bob); + assert_eq!(all_positions.len(), 1); + + // check balance of fee_receiver + let fee_receiver_balance = balance_of!(app, token_x, charlie); + assert_eq!(fee_receiver_balance, 100u128); +} \ No newline at end of file