Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Astroport DCA Module Part 2 Submission #2

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open
Changes from 1 commit
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
refactor: use unique id for each order
y-pakorn committed Jul 29, 2022
commit 188ee40afd1f88eef68131150382749da7eea8e6
41 changes: 17 additions & 24 deletions contracts/dca/src/contract.rs
Original file line number Diff line number Diff line change
@@ -3,9 +3,9 @@ use std::str::FromStr;
use crate::error::ContractError;
use crate::handlers::{
add_bot_tip, cancel_dca_order, create_dca_order, modify_dca_order, perform_dca_purchase,
update_config, update_user_config, withdraw, ModifyDcaOrderParameters,
update_config, update_user_config, withdraw,
};
use crate::queries::{get_config, get_user_config, get_user_dca_orders};
use crate::queries::{get_all_dca_orders, get_config, get_user_config, get_user_dca_orders};
use crate::state::{Config, CONFIG};

use astroport::asset::addr_validate_to_lower;
@@ -156,6 +156,7 @@ pub fn execute(
target_asset,
interval,
dca_amount,
start_at,
} => create_dca_order(
deps,
env,
@@ -164,33 +165,20 @@ pub fn execute(
target_asset,
interval,
dca_amount,
start_at,
),
ExecuteMsg::AddBotTip {} => add_bot_tip(deps, info),
ExecuteMsg::Withdraw { tip: amount } => withdraw(deps, info, amount),
ExecuteMsg::PerformDcaPurchase { user, hops } => {
perform_dca_purchase(deps, env, info, user, hops)
ExecuteMsg::PerformDcaPurchase { id, hops } => {
perform_dca_purchase(deps, env, info, id, hops)
}
ExecuteMsg::CancelDcaOrder { initial_asset } => cancel_dca_order(deps, info, initial_asset),
ExecuteMsg::CancelDcaOrder { id } => cancel_dca_order(deps, info, id),
ExecuteMsg::ModifyDcaOrder {
old_initial_asset,
new_initial_asset,
new_target_asset,
new_interval,
new_dca_amount,
should_reset_purchase_time,
} => modify_dca_order(
deps,
env,
info,
ModifyDcaOrderParameters {
old_initial_asset,
new_initial_asset,
new_target_asset,
new_interval,
new_dca_amount,
should_reset_purchase_time,
},
),
id,
interval,
dca_amount,
initial_amount,
} => modify_dca_order(deps, env, info, id, initial_amount, interval, dca_amount),
}
}

@@ -218,5 +206,10 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
QueryMsg::Config {} => to_binary(&get_config(deps)?),
QueryMsg::UserConfig { user } => to_binary(&get_user_config(deps, user)?),
QueryMsg::UserDcaOrders { user } => to_binary(&get_user_dca_orders(deps, env, user)?),
QueryMsg::AllDcaOrders {
start_after,
limit,
is_ascending,
} => to_binary(&get_all_dca_orders(deps, start_after, limit, is_ascending)?),
}
}
12 changes: 12 additions & 0 deletions contracts/dca/src/error.rs
Original file line number Diff line number Diff line change
@@ -26,6 +26,9 @@ pub enum ContractError {
#[error("Token has already been used to DCA")]
AlreadyDeposited {},

#[error("DCA amount is not equal to fund sent")]
InvalidNativeTokenDeposit {},

#[error("DCA amount is not equal to allowance set by token")]
InvalidTokenDeposit {},

@@ -50,6 +53,9 @@ pub enum ContractError {
#[error("Hop route does not end up at target_asset")]
TargetAssetAssertion {},

#[error("Hop route does not start at initial_asset")]
InitialAssetAssertion {},

#[error("Asset balance is less than DCA purchase amount")]
InsufficientBalance {},

@@ -61,4 +67,10 @@ pub enum ContractError {

#[error("Initial asset deposited is not divisible by the DCA amount")]
IndivisibleDeposit {},

#[error("Native swap is not allowed")]
InvalidNativeSwap {},

#[error("New initial amount must be greater than old initial amount")]
InvalidNewInitialAmount {},
}
65 changes: 30 additions & 35 deletions contracts/dca/src/handlers/cancel_dca_order.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use astroport::asset::AssetInfo;
use astroport_dca::DcaInfo;
use cosmwasm_std::{attr, BankMsg, Coin, DepsMut, MessageInfo, Response};
use cosmwasm_std::{attr, BankMsg, Coin, DepsMut, MessageInfo, Response, Uint128};

use crate::{error::ContractError, state::USER_DCA};
use crate::{
error::ContractError,
state::{DCA, DCA_OWNER},
};

/// ## Description
/// Cancels a users DCA purchase so that it will no longer be fulfilled.
@@ -20,40 +22,33 @@ use crate::{error::ContractError, state::USER_DCA};
pub fn cancel_dca_order(
deps: DepsMut,
info: MessageInfo,
initial_asset: AssetInfo,
id: u64,
) -> Result<Response, ContractError> {
let mut funds = Vec::new();
let order = DCA.load(deps.storage, id)?;

// remove order from user dca's, and add any native token funds for `initial_asset` into the `funds`.
USER_DCA.update(
deps.storage,
&info.sender,
|orders| -> Result<Vec<DcaInfo>, ContractError> {
let mut orders = orders.ok_or(ContractError::NonexistentDca {})?;

let order_position = orders
.iter()
.position(|order| order.initial_asset.info == initial_asset)
.ok_or(ContractError::NonexistentDca {})?;

let removed_order = &orders[order_position];
if let AssetInfo::NativeToken { denom } = &removed_order.initial_asset.info {
funds.push(BankMsg::Send {
to_address: info.sender.to_string(),
amount: vec![Coin {
amount: removed_order.initial_asset.amount,
denom: denom.clone(),
}],
})
}
(order.owner == info.sender)
.then(|| ())
.ok_or(ContractError::Unauthorized {})?;

orders.remove(order_position);

Ok(orders)
},
)?;

Ok(Response::new()
.add_messages(funds)
.add_attributes(vec![attr("action", "cancel_dca_order")]))
// remove order from user dca's, and add any native token funds for `initial_asset` into the `funds`.
if let AssetInfo::NativeToken { denom } = order.initial_asset.info {
if order.initial_asset.amount > Uint128::zero() {
funds.push(BankMsg::Send {
to_address: order.owner.to_string(),
amount: vec![Coin {
denom,
amount: order.initial_asset.amount,
}],
})
}
}

DCA.remove(deps.storage, id);
DCA_OWNER.remove(deps.storage, (&order.owner, id));

Ok(Response::new().add_messages(funds).add_attributes(vec![
attr("action", "cancel_dca_order"),
attr("id", id.to_string()),
]))
}
52 changes: 30 additions & 22 deletions contracts/dca/src/handlers/create_dca_order.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
use astroport::asset::{Asset, AssetInfo};
use astroport_dca::DcaInfo;
use cosmwasm_std::{attr, DepsMut, Env, MessageInfo, Response, StdError, Uint128};
use cosmwasm_std::{attr, DepsMut, Empty, Env, MessageInfo, Response, StdError, Uint128};

use crate::{error::ContractError, get_token_allowance::get_token_allowance, state::USER_DCA};
use crate::{
error::ContractError,
get_token_allowance::get_token_allowance,
state::{DCA, DCA_ID, DCA_OWNER},
};

/// ## Description
/// Creates a new DCA order for a user where the `target_asset` will be purchased with `dca_amount`
@@ -28,6 +32,7 @@ use crate::{error::ContractError, get_token_allowance::get_token_allowance, stat
///
/// * `dca_amount` - A [`Uint128`] representing the amount of `initial_asset` to spend each DCA
/// purchase.
#[allow(clippy::too_many_arguments)]
pub fn create_dca_order(
deps: DepsMut,
env: Env,
@@ -36,18 +41,12 @@ pub fn create_dca_order(
target_asset: AssetInfo,
interval: u64,
dca_amount: Uint128,
start_at: Option<u64>,
) -> Result<Response, ContractError> {
// check that user has not previously created dca strategy with this initial_asset
let mut orders = USER_DCA
.may_load(deps.storage, &info.sender)?
.unwrap_or_default();
let id = DCA_ID.load(deps.storage)?;

if orders
.iter()
.any(|order| order.initial_asset.info == initial_asset.info)
{
return Err(ContractError::AlreadyDeposited {});
}
initial_asset.info.check(deps.api)?;
target_asset.check(deps.api)?;

// check that assets are not duplicate
if initial_asset.info == target_asset {
@@ -76,28 +75,37 @@ pub fn create_dca_order(
AssetInfo::NativeToken { .. } => initial_asset.assert_sent_native_token_balance(&info)?,
AssetInfo::Token { contract_addr } => {
let allowance = get_token_allowance(&deps.as_ref(), &env, &info.sender, contract_addr)?;
if allowance != initial_asset.amount {
if allowance < initial_asset.amount {
return Err(ContractError::InvalidTokenDeposit {});
}
}
}

// store dca order
orders.push(DcaInfo {
initial_asset: initial_asset.clone(),
target_asset: target_asset.clone(),
let now = env.block.time.seconds();
let dca_info = DcaInfo {
id,
owner: info.sender,
initial_asset,
target_asset,
interval,
last_purchase: 0,
last_purchase: match start_at {
Some(start_at) if start_at > now => start_at,
_ => now,
} - interval,
dca_amount,
});
};

USER_DCA.save(deps.storage, &info.sender, &orders)?;
DCA_ID.save(deps.storage, &(id + 1))?;
DCA.save(deps.storage, id, &dca_info)?;
DCA_OWNER.save(deps.storage, (&dca_info.owner, id), &Empty {})?;

Ok(Response::new().add_attributes(vec![
attr("action", "create_dca_order"),
attr("initial_asset", initial_asset.to_string()),
attr("target_asset", target_asset.to_string()),
attr("id", id.to_string()),
attr("initial_asset", dca_info.initial_asset.to_string()),
attr("target_asset", dca_info.target_asset.to_string()),
attr("interval", interval.to_string()),
attr("dca_amount", dca_amount),
attr("start_at", dca_info.last_purchase.to_string()),
]))
}
2 changes: 1 addition & 1 deletion contracts/dca/src/handlers/mod.rs
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ mod withdraw;
pub use add_bot_tip::add_bot_tip;
pub use cancel_dca_order::cancel_dca_order;
pub use create_dca_order::create_dca_order;
pub use modify_dca_order::{modify_dca_order, ModifyDcaOrderParameters};
pub use modify_dca_order::modify_dca_order;
pub use perform_dca_purchase::perform_dca_purchase;
pub use update_config::update_config;
pub use update_user_config::update_user_config;
163 changes: 53 additions & 110 deletions contracts/dca/src/handlers/modify_dca_order.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,7 @@
use astroport::asset::{Asset, AssetInfo};
use cosmwasm_std::{attr, coins, BankMsg, DepsMut, Env, MessageInfo, Response, Uint128};
use astroport::asset::AssetInfo;
use cosmwasm_std::{attr, DepsMut, Env, MessageInfo, Response, StdError, Uint128};

use crate::{error::ContractError, get_token_allowance::get_token_allowance, state::USER_DCA};

/// Stores a modified dca order new parameters
pub struct ModifyDcaOrderParameters {
/// The old [`AssetInfo`] that was used to purchase DCA orders.
pub old_initial_asset: AssetInfo,
/// The new [`Asset`] that is being spent to create DCA orders.
pub new_initial_asset: Asset,
/// The [`AssetInfo`] that is being purchased with `new_initial_asset`.
pub new_target_asset: AssetInfo,
/// The time in seconds between DCA purchases.
pub new_interval: u64,
/// a [`Uint128`] amount of `new_initial_asset` to spend each DCA purchase.
pub new_dca_amount: Uint128,
/// A bool flag that determines if the order's last purchase time should be reset.
pub should_reset_purchase_time: bool,
}
use crate::{error::ContractError, get_token_allowance::get_token_allowance, state::DCA};

/// ## Description
/// Modifies an existing DCA order for a user such that the new parameters will apply to the
@@ -44,113 +28,72 @@ pub fn modify_dca_order(
deps: DepsMut,
env: Env,
info: MessageInfo,
order_details: ModifyDcaOrderParameters,
id: u64,
initial_amount: Option<Uint128>,
interval: Option<u64>,
dca_amount: Option<Uint128>,
) -> Result<Response, ContractError> {
let ModifyDcaOrderParameters {
old_initial_asset,
new_initial_asset,
new_target_asset,
new_interval,
new_dca_amount,
should_reset_purchase_time,
} = order_details;

let mut orders = USER_DCA
.may_load(deps.storage, &info.sender)?
.unwrap_or_default();

// check that old_initial_asset.info exists
let order = orders
.iter_mut()
.find(|order| order.initial_asset.info == old_initial_asset)
.ok_or(ContractError::NonexistentDca {})?;

let should_refund = order.initial_asset.amount > new_initial_asset.amount;
let asset_difference = Asset {
info: new_initial_asset.info.clone(),
amount: match should_refund {
true => order
.initial_asset
.amount
.checked_sub(new_initial_asset.amount)?,
false => new_initial_asset
.amount
.checked_sub(order.initial_asset.amount)?,
},
};
let mut attrs = vec![attr("action", "modify_dca_order")];
let mut order = DCA.load(deps.storage, id)?;

let mut messages = Vec::new();
(order.owner == info.sender)
.then(|| ())
.ok_or(ContractError::Unauthorized {})?;

if old_initial_asset == new_initial_asset.info {
if !should_refund {
// if the user needs to have deposited more, check that we have the correct funds/allowance sent
// this is the case only when the old_initial_asset and new_initial_asset are the same

// if native token, they should have included it in the message
// otherwise, if cw20 token, they should have provided the correct allowance
match &old_initial_asset {
AssetInfo::NativeToken { .. } => {
asset_difference.assert_sent_native_token_balance(&info)?
}
AssetInfo::Token { contract_addr } => {
let allowance =
get_token_allowance(&deps.as_ref(), &env, &info.sender, contract_addr)?;
if allowance != new_initial_asset.amount {
return Err(ContractError::InvalidTokenDeposit {});
}
}
}
} else {
// we need to refund the user with the difference if it is a native token
if let AssetInfo::NativeToken { denom } = &new_initial_asset.info {
messages.push(BankMsg::Send {
to_address: info.sender.to_string(),
amount: coins(asset_difference.amount.u128(), denom),
})
}
}
} else {
// they are different assets, so we will return the old_initial_asset if it is a native token
if let AssetInfo::NativeToken { denom } = &new_initial_asset.info {
messages.push(BankMsg::Send {
to_address: info.sender.to_string(),
amount: coins(order.initial_asset.amount.u128(), denom),
})
}
if let Some(initial_amount) = initial_amount {
// check if new amount is greater than old amount
(initial_amount > order.initial_asset.amount)
.then(|| ())
.ok_or(ContractError::InvalidNewInitialAmount {})?;

// validate that user sent either native tokens or has set allowance for the new token
match &new_initial_asset.info {
AssetInfo::NativeToken { .. } => {
new_initial_asset.assert_sent_native_token_balance(&info)?
match &order.initial_asset.info {
AssetInfo::NativeToken { denom } => {
match info.funds.iter().find(|e| &e.denom == denom) {
Some(amt) => (amt.amount >= (initial_amount - order.initial_asset.amount))
.then(|| ())
.ok_or(ContractError::InvalidNativeTokenDeposit {}),
None => Err(ContractError::InvalidNativeTokenDeposit {}),
}?;
}
AssetInfo::Token { contract_addr } => {
let allowance =
get_token_allowance(&deps.as_ref(), &env, &info.sender, contract_addr)?;
if allowance != new_initial_asset.amount {
if allowance < initial_amount {
return Err(ContractError::InvalidTokenDeposit {});
}
}
}

order.initial_asset.amount = initial_amount;
attrs.push(attr("new_initial_asset_amount", initial_amount));
}

// update order
order.initial_asset = new_initial_asset.clone();
order.target_asset = new_target_asset.clone();
order.interval = new_interval;
order.dca_amount = new_dca_amount;
if let Some(interval) = interval {
order.interval = interval;
attrs.push(attr("new_interval", interval.to_string()));
}

if let Some(dca_amount) = dca_amount {
if dca_amount > order.initial_asset.amount {
return Err(ContractError::DepositTooSmall {});
}

// check that initial_asset.amount is divisible by dca_amount
if !order
.initial_asset
.amount
.checked_rem(dca_amount)
.map_err(StdError::divide_by_zero)?
.is_zero()
{
return Err(ContractError::IndivisibleDeposit {});
}

if should_reset_purchase_time {
order.last_purchase = 0;
order.dca_amount = dca_amount;
attrs.push(attr("new_dca_amount", dca_amount));
}

USER_DCA.save(deps.storage, &info.sender, &orders)?;
DCA.save(deps.storage, id, &order)?;

Ok(Response::new().add_attributes(vec![
attr("action", "modify_dca_order"),
attr("old_initial_asset", old_initial_asset.to_string()),
attr("new_initial_asset", new_initial_asset.to_string()),
attr("new_target_asset", new_target_asset.to_string()),
attr("new_interval", new_interval.to_string()),
attr("new_dca_amount", new_dca_amount),
]))
Ok(Response::new().add_attributes(attrs))
}
223 changes: 88 additions & 135 deletions contracts/dca/src/handlers/perform_dca_purchase.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
use astroport::{
asset::{addr_validate_to_lower, AssetInfo, UUSD_DENOM},
asset::{AssetInfo, UUSD_DENOM},
router::{ExecuteMsg as RouterExecuteMsg, SwapOperation},
};
use astroport_dca::{DcaInfo, UserConfig};
use cosmwasm_std::{
attr, to_binary, BankMsg, Coin, CosmosMsg, DepsMut, Env, MessageInfo, Response, Uint128,
WasmMsg,
@@ -11,7 +10,7 @@ use cw20::Cw20ExecuteMsg;

use crate::{
error::ContractError,
state::{CONFIG, USER_CONFIG, USER_DCA},
state::{CONFIG, DCA, USER_CONFIG},
};

/// ## Description
@@ -35,15 +34,15 @@ pub fn perform_dca_purchase(
deps: DepsMut,
env: Env,
info: MessageInfo,
user: String,
id: u64,
hops: Vec<SwapOperation>,
) -> Result<Response, ContractError> {
// validate user address
let user_address = addr_validate_to_lower(deps.api, &user)?;
let mut order = DCA.load(deps.storage, id)?;

// retrieve configs
let user_config = USER_CONFIG
.may_load(deps.storage, &user_address)?
let mut user_config = USER_CONFIG
.may_load(deps.storage, &order.owner)?
.unwrap_or_default();
let contract_config = CONFIG.load(deps.storage)?;

@@ -58,153 +57,106 @@ pub fn perform_dca_purchase(
return Err(ContractError::MaxHopsAssertion { hops: hops_len });
}

// validate that all middle hops (last hop excluded) are whitelisted tokens for the ask_denom or ask_asset
let middle_hops = &hops[..hops.len() - 1];
for swap in middle_hops {
match swap {
SwapOperation::NativeSwap { ask_denom, .. } => {
if !contract_config
.whitelisted_tokens
.iter()
.any(|token| match token {
AssetInfo::NativeToken { denom } => ask_denom == denom,
AssetInfo::Token { .. } => false,
})
{
// not a whitelisted native token
return Err(ContractError::InvalidHopRoute {
token: ask_denom.to_string(),
});
}
}
SwapOperation::AstroSwap { ask_asset_info, .. } => {
if !contract_config.is_whitelisted_asset(ask_asset_info) {
return Err(ContractError::InvalidHopRoute {
token: ask_asset_info.to_string(),
});
}
}
}
}

// validate purchaser has enough funds to pay the sender
let tip_cost = contract_config
.per_hop_fee
.checked_mul(Uint128::from(hops_len))?;
if tip_cost > user_config.tip_balance {
return Err(ContractError::InsufficientTipBalance {});
}

// retrieve max_spread from user config, or default to contract set max_spread
let max_spread = user_config.max_spread.unwrap_or(contract_config.max_spread);

// store messages to send in response
let mut messages: Vec<CosmosMsg> = Vec::new();

// load user dca orders and update the relevant one
USER_DCA.update(
deps.storage,
&user_address,
|orders| -> Result<Vec<DcaInfo>, ContractError> {
let mut orders = orders.ok_or(ContractError::NonexistentDca {})?;

let order = orders
.iter_mut()
.find(|order| match &hops[0] {
SwapOperation::NativeSwap { ask_denom, .. } => {
match &order.initial_asset.info {
AssetInfo::NativeToken { denom } => ask_denom == denom,
_ => false,
}
}
SwapOperation::AstroSwap {
offer_asset_info, ..
} => offer_asset_info == &order.initial_asset.info,
})
.ok_or(ContractError::NonexistentDca {})?;

// check that it has been long enough between dca purchases
if order.last_purchase + order.interval > env.block.time.seconds() {
return Err(ContractError::PurchaseTooEarly {});
}

// check that last hop is target asset
let last_hop = &hops
.last()
.ok_or(ContractError::EmptyHopRoute {})?
.get_target_asset_info();
if last_hop != &order.target_asset {
return Err(ContractError::TargetAssetAssertion {});
}

// subtract dca_amount from order and update last_purchase time
order.initial_asset.amount = order
.initial_asset
.amount
.checked_sub(order.dca_amount)
.map_err(|_| ContractError::InsufficientBalance {})?;
order.last_purchase = env.block.time.seconds();

// add funds and router message to response
if let AssetInfo::Token { contract_addr } = &order.initial_asset.info {
// send a TransferFrom request to the token to the router
messages.push(
WasmMsg::Execute {
contract_addr: contract_addr.to_string(),
funds: vec![],
msg: to_binary(&Cw20ExecuteMsg::TransferFrom {
owner: user_address.to_string(),
recipient: contract_config.router_addr.to_string(),
amount: order.dca_amount,
})?,
}
.into(),
);
// validate all swap operation
for (idx, hop) in hops.iter().enumerate() {
match hop {
SwapOperation::NativeSwap { .. } => Err(ContractError::InvalidNativeSwap {})?,
SwapOperation::AstroSwap {
offer_asset_info,
ask_asset_info,
} => {
// validate the first offer asset info
(idx == 0 && offer_asset_info == &order.initial_asset.info)
.then(|| ())
.ok_or(ContractError::InitialAssetAssertion {})?;

// validate the last ask asset info
(idx == (hops.len() - 1) && ask_asset_info == &order.target_asset)
.then(|| ())
.ok_or(ContractError::TargetAssetAssertion {})?;

// validate that all middle hops (last hop excluded) are whitelisted tokens for the ask_denom or ask_asset
(idx != 0
&& idx != (hops.len() - 1)
&& contract_config.is_whitelisted_asset(ask_asset_info))
.then(|| ())
.ok_or(ContractError::InvalidHopRoute {
token: ask_asset_info.to_string(),
})?;
}
};
}

// if it is a native token, we need to send the funds
let funds = match &order.initial_asset.info {
AssetInfo::NativeToken { denom } => vec![Coin {
amount: order.dca_amount,
denom: denom.clone(),
}],
AssetInfo::Token { .. } => vec![],
};
// check that it has been long enough between dca purchases
if order.last_purchase + order.interval >= env.block.time.seconds() {
return Err(ContractError::PurchaseTooEarly {});
}

// tell the router to perform swap operations
// subtract dca_amount from order and update last_purchase time
order.initial_asset.amount = order
.initial_asset
.amount
.checked_sub(order.dca_amount)
.map_err(|_| ContractError::InsufficientBalance {})?;
order.last_purchase = env.block.time.seconds();

let funds = match &order.initial_asset.info {
// if its a native token, we need to send the funds
AssetInfo::NativeToken { denom } => vec![Coin {
amount: order.dca_amount,
denom: denom.clone(),
}],
//if its a token, send a TransferFrom request to the token to the router
AssetInfo::Token { contract_addr } => {
messages.push(
WasmMsg::Execute {
contract_addr: contract_config.router_addr.to_string(),
funds,
msg: to_binary(&RouterExecuteMsg::ExecuteSwapOperations {
operations: hops,
minimum_receive: None,
to: Some(user_address.to_string()),
max_spread: Some(max_spread),
contract_addr: contract_addr.to_string(),
funds: vec![],
msg: to_binary(&Cw20ExecuteMsg::TransferFrom {
owner: order.owner.to_string(),
recipient: contract_config.router_addr.to_string(),
amount: order.dca_amount,
})?,
}
.into(),
);

Ok(orders)
},
)?;
vec![]
}
};

// remove tip from purchaser
USER_CONFIG.update(
deps.storage,
&user_address,
|user_config| -> Result<UserConfig, ContractError> {
let mut user_config = user_config.unwrap_or_default();
// tell the router to perform swap operations
messages.push(
WasmMsg::Execute {
contract_addr: contract_config.router_addr.to_string(),
funds,
msg: to_binary(&RouterExecuteMsg::ExecuteSwapOperations {
operations: hops,
minimum_receive: None,
to: Some(order.owner.to_string()),
max_spread: Some(max_spread),
})?,
}
.into(),
);

user_config.tip_balance = user_config
.tip_balance
.checked_sub(tip_cost)
.map_err(|_| ContractError::InsufficientTipBalance {})?;
// validate purchaser has enough funds to pay the sender
let tip_cost = contract_config
.per_hop_fee
.checked_mul(Uint128::from(hops_len))?;
if tip_cost >= user_config.tip_balance {
return Err(ContractError::InsufficientTipBalance {});
}

Ok(user_config)
},
)?;
// update user tip balance
user_config.tip_balance -= tip_cost;
USER_CONFIG.save(deps.storage, &order.owner, &user_config)?;

// add tip payment to messages
messages.push(
@@ -220,6 +172,7 @@ pub fn perform_dca_purchase(

Ok(Response::new().add_messages(messages).add_attributes(vec![
attr("action", "perform_dca_purchase"),
attr("id", id.to_string()),
attr("tip_cost", tip_cost),
]))
}
28 changes: 28 additions & 0 deletions contracts/dca/src/queries/get_all_dca_orders.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use astroport_dca::DcaInfo;
use cosmwasm_std::{Deps, Order, StdResult};
use cw_storage_plus::Bound;

use crate::state::DCA;

const ORDER_LIMIT: u64 = 50;

/// ## Description
/// Returns all DCA orders currently set.
///
/// The result is returned in a [`Vec<DcaInfo>`] object.
pub fn get_all_dca_orders(
deps: Deps,
start_after: Option<u64>,
limit: Option<u64>,
is_ascending: Option<bool>,
) -> StdResult<Vec<DcaInfo>> {
let bound = match is_ascending.unwrap_or(true) {
true => (start_after.map(Bound::exclusive), None, Order::Ascending),
false => (None, start_after.map(Bound::exclusive), Order::Ascending),
};

DCA.range(deps.storage, bound.0, bound.1, bound.2)
.map(|e| -> StdResult<_> { Ok(e?.1) })
.take(limit.unwrap_or(ORDER_LIMIT) as usize)
.collect::<StdResult<Vec<_>>>()
}
24 changes: 14 additions & 10 deletions contracts/dca/src/queries/get_user_dca_orders.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
use astroport::asset::{addr_validate_to_lower, AssetInfo};
use astroport_dca::DcaQueryInfo;
use cosmwasm_std::{Deps, Env, StdResult};
use astroport_dca::UserDcaInfo;
use cosmwasm_std::{Deps, Env, Order, StdResult};

use crate::{get_token_allowance::get_token_allowance, state::USER_DCA};
use crate::{
get_token_allowance::get_token_allowance,
state::{DCA, DCA_OWNER},
};

/// ## Description
/// Returns a users DCA orders currently set.
///
/// The result is returned in a [`Vec<DcaQueryInfo`] object of the users current DCA orders with the
/// The result is returned in a [`Vec<UserDcaInfo>`] object of the users current DCA orders with the
/// `amount` of each order set to the native token amount that can be spent, or the token allowance.
///
/// ## Arguments
@@ -16,14 +19,15 @@ use crate::{get_token_allowance::get_token_allowance, state::USER_DCA};
/// * `env` - The [`Env`] of the blockchain.
///
/// * `user` - The users lowercase address as a [`String`].
pub fn get_user_dca_orders(deps: Deps, env: Env, user: String) -> StdResult<Vec<DcaQueryInfo>> {
pub fn get_user_dca_orders(deps: Deps, env: Env, user: String) -> StdResult<Vec<UserDcaInfo>> {
let user_address = addr_validate_to_lower(deps.api, &user)?;

USER_DCA
.load(deps.storage, &user_address)?
.into_iter()
.map(|order| {
Ok(DcaQueryInfo {
DCA_OWNER
.prefix(&user_address)
.keys(deps.storage, None, None, Order::Descending)
.map(|e| -> StdResult<_> {
let order = DCA.load(deps.storage, e?)?;
Ok(UserDcaInfo {
token_allowance: match &order.initial_asset.info {
AssetInfo::NativeToken { .. } => order.initial_asset.amount,
AssetInfo::Token { contract_addr } => {
2 changes: 2 additions & 0 deletions contracts/dca/src/queries/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
mod get_all_dca_orders;
mod get_config;
mod get_user_config;
mod get_user_dca_orders;

pub use get_all_dca_orders::get_all_dca_orders;
pub use get_config::get_config;
pub use get_user_config::get_user_config;
pub use get_user_dca_orders::get_user_dca_orders;
6 changes: 5 additions & 1 deletion contracts/dca/src/state.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use astroport::asset::AssetInfo;
use cosmwasm_std::{Addr, Decimal, Uint128};
use cosmwasm_std::{Addr, Decimal, Empty, Uint128};
use cw_storage_plus::{Item, Map};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -35,3 +35,7 @@ pub const CONFIG: Item<Config> = Item::new("config");
pub const USER_CONFIG: Map<&Addr, UserConfig> = Map::new("user_config");
/// The DCA orders for a user
pub const USER_DCA: Map<&Addr, Vec<DcaInfo>> = Map::new("user_dca");

pub const DCA_ID: Item<u64> = Item::new("dca_id");
pub const DCA: Map<u64, DcaInfo> = Map::new("dca");
pub const DCA_OWNER: Map<(&Addr, u64), Empty> = Map::new("dca_o");
6 changes: 5 additions & 1 deletion packages/astroport-dca/src/dca.rs
Original file line number Diff line number Diff line change
@@ -2,11 +2,15 @@ use astroport::asset::{Asset, AssetInfo};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use cosmwasm_std::Uint128;
use cosmwasm_std::{Addr, Uint128};

/// Describes information about a DCA order
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct DcaInfo {
/// Unique id of this DCA purchases
pub id: u64,
/// Owner of this DCA purchases
pub owner: Addr,
/// The starting asset deposited by the user, with the amount representing the users deposited
/// amount of the token
pub initial_asset: Asset,
28 changes: 15 additions & 13 deletions packages/astroport-dca/src/msg.rs
Original file line number Diff line number Diff line change
@@ -33,7 +33,7 @@ pub enum ExecuteMsg {
/// Add uusd top-up for bots to perform DCA requests
AddBotTip {},
/// Cancels a DCA order, returning any native asset back to the user
CancelDcaOrder { initial_asset: AssetInfo },
CancelDcaOrder { id: u64 },
/// Creates a new DCA order where `dca_amount` of token `initial_asset` will purchase
/// `target_asset` every `interval`
///
@@ -44,21 +44,17 @@ pub enum ExecuteMsg {
target_asset: AssetInfo,
interval: u64,
dca_amount: Uint128,
start_at: Option<u64>,
},
/// Modifies an existing DCA order, allowing the user to change certain parameters
ModifyDcaOrder {
old_initial_asset: AssetInfo,
new_initial_asset: Asset,
new_target_asset: AssetInfo,
new_interval: u64,
new_dca_amount: Uint128,
should_reset_purchase_time: bool,
id: u64,
initial_amount: Option<Uint128>,
interval: Option<u64>,
dca_amount: Option<Uint128>,
},
/// Performs a DCA purchase for a specified user given a hop route
PerformDcaPurchase {
user: String,
hops: Vec<SwapOperation>,
},
PerformDcaPurchase { id: u64, hops: Vec<SwapOperation> },
/// Updates the configuration of the contract
UpdateConfig {
/// The new maximum amount of hops to perform from `initial_asset` to `target_asset` when
@@ -86,7 +82,13 @@ pub enum ExecuteMsg {
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
/// Returns information about the users current active DCA orders in a [`Vec<DcaInfo>`] object.
/// Returns information about all current active DCA orders in a [`Vec<DcaInfo>`] object.
AllDcaOrders {
start_after: Option<u64>,
limit: Option<u64>,
is_ascending: Option<bool>,
},
/// Returns information about the users current active DCA orders in a [`Vec<UserDcaInfo>`] object.
UserDcaOrders { user: String },
/// Returns information about the contract configuration in a [`Config`] object.
Config {},
@@ -107,7 +109,7 @@ pub struct MigrateMsg {}
/// This is useful for bots and front-end to distinguish between a users token allowance (which may
/// have changed) for the DCA contract, and the created DCA order size.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct DcaQueryInfo {
pub struct UserDcaInfo {
pub token_allowance: Uint128,
pub info: DcaInfo,
}