#[cfg(not(feature = "library"))]
use cosmwasm_std::entry_point;
use cosmwasm_std::{
attr, coin, ensure, to_json_binary, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Env,
MessageInfo, Reply, Response, StdError, StdResult, SubMsg, Uint128, WasmMsg,
};
use cw2::set_contract_version;
use cw_utils::{must_pay, parse_reply_instantiate_data, MsgInstantiateContractResponse};
use osmosis_std::types::cosmos::bank::v1beta1::{DenomUnit, Metadata};
use osmosis_std::types::osmosis::tokenfactory::v1beta1::{
MsgBurn, MsgCreateDenom, MsgCreateDenomResponse, MsgMint, MsgSetBeforeSendHook,
MsgSetDenomMetadata,
};
use astroport::staking::{
Config, ExecuteMsg, InstantiateMsg, QueryMsg, StakingResponse, TrackerData,
};
use crate::error::ContractError;
use crate::state::{CONFIG, TRACKER_DATA};
pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME");
pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
const TOKEN_NAME: &str = "Staked Astroport Token";
const TOKEN_SYMBOL: &str = "xASTRO";
enum ReplyIds {
InstantiateDenom = 1,
InstantiateTrackingContract = 2,
}
impl TryFrom<u64> for ReplyIds {
type Error = ContractError;
fn try_from(value: u64) -> Result<Self, Self::Error> {
match value {
1 => Ok(ReplyIds::InstantiateDenom),
2 => Ok(ReplyIds::InstantiateTrackingContract),
_ => Err(ContractError::FailedToParseReply {}),
}
}
}
pub(crate) const MINIMUM_STAKE_AMOUNT: Uint128 = Uint128::new(1_000);
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
deps: DepsMut,
env: Env,
_info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response> {
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
let supply = deps.querier.query_supply(&msg.deposit_token_denom)?.amount;
ensure!(
!supply.is_zero(),
StdError::generic_err(
"deposit_token_denom has 0 supply which is likely sign of misconfiguration"
)
);
deps.api.addr_validate(&msg.token_factory_addr)?;
deps.api.addr_validate(&msg.tracking_admin)?;
CONFIG.save(
deps.storage,
&Config {
astro_denom: msg.deposit_token_denom,
xastro_denom: "".to_string(),
},
)?;
TRACKER_DATA.save(
deps.storage,
&TrackerData {
code_id: msg.tracking_code_id,
admin: msg.tracking_admin,
token_factory_addr: msg.token_factory_addr,
tracker_addr: "".to_string(),
},
)?;
let create_denom_msg = SubMsg::reply_on_success(
MsgCreateDenom {
sender: env.contract.address.to_string(),
subdenom: TOKEN_SYMBOL.to_owned(),
},
ReplyIds::InstantiateDenom as u64,
);
Ok(Response::new().add_submessage(create_denom_msg))
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::Enter { receiver } => {
let recipient = receiver.unwrap_or_else(|| info.sender.to_string());
execute_enter(deps, env, info).map(|(resp, minted_coins)| {
resp.add_message(BankMsg::Send {
to_address: recipient.clone(),
amount: vec![minted_coins],
})
.add_attributes([("action", "enter"), ("recipient", recipient.as_str())])
})
}
ExecuteMsg::EnterWithHook {
contract_address,
msg,
} => execute_enter(deps, env, info).map(|(resp, minted_coins)| {
resp.add_message(WasmMsg::Execute {
contract_addr: contract_address.clone(),
msg,
funds: vec![minted_coins],
})
.add_attributes([
("action", "enter_with_hook"),
("next_contract", &contract_address),
])
}),
ExecuteMsg::Leave { receiver } => {
let recipient = receiver.unwrap_or_else(|| info.sender.to_string());
execute_leave(deps, env, info, recipient)
}
}
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result<Response, ContractError> {
match ReplyIds::try_from(msg.id)? {
ReplyIds::InstantiateDenom => {
let MsgCreateDenomResponse { new_token_denom } = msg.result.try_into()?;
let denom_metadata_msg = MsgSetDenomMetadata {
sender: env.contract.address.to_string(),
metadata: Some(Metadata {
symbol: TOKEN_SYMBOL.to_string(),
name: TOKEN_NAME.to_string(),
base: new_token_denom.clone(),
display: TOKEN_SYMBOL.to_string(),
denom_units: vec![
DenomUnit {
denom: new_token_denom.clone(),
exponent: 0,
aliases: vec![],
},
DenomUnit {
denom: TOKEN_SYMBOL.to_string(),
exponent: 6,
aliases: vec![],
},
],
description: "Astroport is a neutral marketplace where anyone, from anywhere in the galaxy, can dock to trade their wares.".to_string(),
uri: "https://app.astroport.fi/tokens/xAstro.svg".to_string(),
uri_hash: "d39cfe20605a9857b2b123c6d6dbbdf4d3b65cb9d411cee1011877b918b4c646".to_string(),
}),
};
CONFIG.update::<_, StdError>(deps.storage, |mut config| {
config.xastro_denom = new_token_denom.clone();
Ok(config)
})?;
let tracker_data = TRACKER_DATA.load(deps.storage)?;
let init_tracking_contract = SubMsg::reply_on_success(
WasmMsg::Instantiate {
admin: Some(tracker_data.admin),
code_id: tracker_data.code_id,
msg: to_json_binary(&astroport_v4::tokenfactory_tracker::InstantiateMsg {
tokenfactory_module_address: tracker_data.token_factory_addr,
tracked_denom: new_token_denom.clone(),
})?,
funds: vec![],
label: format!("{TOKEN_SYMBOL} balances tracker"),
},
ReplyIds::InstantiateTrackingContract as u64,
);
Ok(Response::new()
.add_submessages([SubMsg::new(denom_metadata_msg), init_tracking_contract])
.add_attribute("xastro_denom", new_token_denom))
}
ReplyIds::InstantiateTrackingContract => {
let MsgInstantiateContractResponse {
contract_address, ..
} = parse_reply_instantiate_data(msg)?;
TRACKER_DATA.update::<_, StdError>(deps.storage, |mut tracker_data| {
tracker_data.tracker_addr = contract_address.clone();
Ok(tracker_data)
})?;
let config = CONFIG.load(deps.storage)?;
let set_hook_msg = MsgSetBeforeSendHook {
sender: env.contract.address.to_string(),
denom: config.xastro_denom,
cosmwasm_address: contract_address.clone(),
};
Ok(Response::new()
.add_message(set_hook_msg)
.add_attribute("tracker_contract", contract_address))
}
}
}
fn execute_enter(
deps: DepsMut,
env: Env,
info: MessageInfo,
) -> Result<(Response, Coin), ContractError> {
let config = CONFIG.load(deps.storage)?;
let amount = must_pay(&info, &config.astro_denom)?;
let total_deposit = deps
.querier
.query_balance(&env.contract.address, &config.astro_denom)?
.amount
- amount;
let total_shares = deps.querier.query_supply(&config.xastro_denom)?.amount;
let mut messages: Vec<CosmosMsg> = vec![];
let mint_amount = if total_shares.is_zero() || total_deposit.is_zero() {
if amount.saturating_sub(MINIMUM_STAKE_AMOUNT).is_zero() {
return Err(ContractError::MinimumStakeAmountError {});
}
messages.push(
MsgMint {
sender: env.contract.address.to_string(),
amount: Some(coin(MINIMUM_STAKE_AMOUNT.u128(), &config.xastro_denom).into()),
mint_to_address: env.contract.address.to_string(),
}
.into(),
);
amount - MINIMUM_STAKE_AMOUNT
} else {
amount.multiply_ratio(total_shares, total_deposit)
};
if mint_amount.is_zero() {
return Err(ContractError::StakeAmountTooSmall {});
}
let minted_coins = coin(mint_amount.u128(), config.xastro_denom);
messages.push(
MsgMint {
sender: env.contract.address.to_string(),
amount: Some(minted_coins.clone().into()),
mint_to_address: env.contract.address.to_string(),
}
.into(),
);
let staking_response = to_json_binary(&StakingResponse {
astro_amount: amount,
xastro_amount: mint_amount,
})?;
Ok((
Response::new()
.add_messages(messages)
.set_data(staking_response)
.add_attributes([
attr("astro_amount", amount),
attr("xastro_amount", mint_amount),
]),
minted_coins,
))
}
fn execute_leave(
deps: DepsMut,
env: Env,
info: MessageInfo,
recipient: String,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
let amount = must_pay(&info, &config.xastro_denom)?;
let total_deposit = deps
.querier
.query_balance(&env.contract.address, &config.astro_denom)?
.amount;
let total_shares = deps.querier.query_supply(&config.xastro_denom)?.amount;
let return_amount = amount.multiply_ratio(total_deposit, total_shares);
let messages: Vec<CosmosMsg> = vec![
MsgBurn {
sender: env.contract.address.to_string(),
amount: Some(coin(amount.u128(), &config.xastro_denom).into()),
burn_from_address: "".to_string(), }
.into(),
BankMsg::Send {
to_address: recipient.clone(),
amount: vec![coin(return_amount.u128(), config.astro_denom)],
}
.into(),
BankMsg::Send {
to_address: env.contract.address.to_string(),
amount: vec![coin(1, &config.xastro_denom)],
}
.into(),
];
let staking_response = to_json_binary(&StakingResponse {
astro_amount: return_amount,
xastro_amount: amount,
})?;
Ok(Response::new()
.add_messages(messages)
.set_data(staking_response)
.add_attributes([
attr("action", "leave"),
attr("recipient", recipient),
attr("xastro_amount", amount),
attr("astro_amount", return_amount),
]))
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::Config {} => to_json_binary(&CONFIG.load(deps.storage)?),
QueryMsg::TotalShares {} => {
let config = CONFIG.load(deps.storage)?;
let total_supply = deps.querier.query_supply(config.xastro_denom)?.amount;
to_json_binary(&total_supply)
}
QueryMsg::TotalDeposit {} => {
let config = CONFIG.load(deps.storage)?;
let total_deposit = deps
.querier
.query_balance(env.contract.address, config.astro_denom)?
.amount;
to_json_binary(&total_deposit)
}
QueryMsg::TrackerConfig {} => to_json_binary(&TRACKER_DATA.load(deps.storage)?),
QueryMsg::BalanceAt { address, timestamp } => {
let amount = if timestamp.is_none() {
let config = CONFIG.load(deps.storage)?;
deps.querier
.query_balance(&address, config.xastro_denom)?
.amount
} else {
let tracker_config = TRACKER_DATA.load(deps.storage)?;
deps.querier.query_wasm_smart(
tracker_config.tracker_addr,
&astroport_v4::tokenfactory_tracker::QueryMsg::BalanceAt { address, timestamp },
)?
};
to_json_binary(&amount)
}
QueryMsg::TotalSupplyAt { timestamp } => {
let amount = if timestamp.is_none() {
let config = CONFIG.load(deps.storage)?;
deps.querier.query_supply(config.xastro_denom)?.amount
} else {
let tracker_config = TRACKER_DATA.load(deps.storage)?;
deps.querier.query_wasm_smart(
tracker_config.tracker_addr,
&astroport_v4::tokenfactory_tracker::QueryMsg::TotalSupplyAt { timestamp },
)?
};
to_json_binary(&amount)
}
}
}