diff --git a/contracts/external/cw-abc/Cargo.toml b/contracts/external/cw-abc/Cargo.toml index bb8fc9d06..0223dc1b9 100644 --- a/contracts/external/cw-abc/Cargo.toml +++ b/contracts/external/cw-abc/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "cw-abc" version = "0.0.1" -authors = ["Ethan Frey ", "Jake Hartnell"] +authors = ["Ethan Frey ", "Jake Hartnell", "Adair "] edition = { workspace = true } description = "Implements an Augmented Bonding Curve" license = "Apache-2.0" @@ -16,6 +16,7 @@ crate-type = ["cdylib", "rlib"] backtraces = ["cosmwasm-std/backtraces"] # use library feature to disable all instantiate/execute/query exports library = [] +boot = ["dep:boot-core"] [dependencies] cw-utils = { workspace = true } @@ -32,7 +33,23 @@ integer-cbrt = "0.1.2" # TODO publish this token-bindings = { git = "https://github.com/CosmosContracts/token-bindings", rev = "1412b94" } cw-ownable = { workspace = true } +cw-paginate-storage = { workspace = true } +boot-core = { version = "0.10.0", optional = true, git = "https://github.com/AbstractSDK/BOOT", branch = "fix/custom_binding_contract_wrapper" } [dev-dependencies] # TODO move to workspace speculoos = "0.11.0" +#cw-multi-test = { version = "0.16.0" } +anyhow = { workspace = true } +cw-abc = { path = ".", features = ["boot"] } + +[profile.release] +rpath = false +lto = true +overflow-checks = true +opt-level = 3 +debug = false +debug-assertions = false +codegen-units = 1 +panic = 'abort' +incremental = false diff --git a/contracts/external/cw-abc/src/abc.rs b/contracts/external/cw-abc/src/abc.rs index 72f73b383..b8c49d8d2 100644 --- a/contracts/external/cw-abc/src/abc.rs +++ b/contracts/external/cw-abc/src/abc.rs @@ -1,10 +1,9 @@ - use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, Api, Decimal as StdDecimal, ensure, StdResult, Uint128}; -use cw_address_like::AddressLike; -use token_bindings::Metadata; -use crate::curves::{Constant, Curve, decimal, DecimalPlaces, Linear, SquareRoot}; +use cosmwasm_std::{ensure, Decimal as StdDecimal, Uint128}; + +use crate::curves::{decimal, Constant, Curve, DecimalPlaces, Linear, SquareRoot}; use crate::ContractError; +use token_bindings::Metadata; #[cw_serde] pub struct SupplyToken { @@ -34,9 +33,7 @@ pub struct MinMax { } #[cw_serde] -pub struct HatchConfig { - // Initial contributors (Hatchers) allow list - pub allowlist: Option>, +pub struct HatchConfig { // /// TODO: The minimum and maximum contribution amounts (min, max) in the reserve token // pub contribution_limits: MinMax, // The initial raise range (min, max) in the reserve token @@ -46,90 +43,70 @@ pub struct HatchConfig { pub initial_price: Uint128, // The initial allocation (θ), percentage of the initial raise allocated to the Funding Pool pub initial_allocation_ratio: StdDecimal, + // Exit tax for the hatch phase + pub exit_tax: StdDecimal, } -impl From> for HatchConfig { - fn from(value: HatchConfig) -> Self { - HatchConfig { - allowlist: value.allowlist.map(|addresses| { - addresses.into_iter().map(|addr| addr.to_string()).collect() - }), - initial_raise: value.initial_raise, - initial_price: value.initial_price, - initial_allocation_ratio: value.initial_allocation_ratio, - } - } -} - - -impl HatchConfig { +impl HatchConfig { /// Validate the hatch config - pub fn validate(&self, api: &dyn Api) -> Result, ContractError> { + pub fn validate(&self) -> Result<(), ContractError> { ensure!( self.initial_raise.min < self.initial_raise.max, - ContractError::HatchPhaseConfigError("Initial raise minimum value must be less than maximum value.".to_string()) + ContractError::HatchPhaseConfigError( + "Initial raise minimum value must be less than maximum value.".to_string() + ) ); ensure!( !self.initial_price.is_zero(), - ContractError::HatchPhaseConfigError("Initial price must be greater than zero.".to_string()) + ContractError::HatchPhaseConfigError( + "Initial price must be greater than zero.".to_string() + ) ); + // TODO: define better values ensure!( self.initial_allocation_ratio <= StdDecimal::percent(100u64), - ContractError::HatchPhaseConfigError("Initial allocation percentage must be between 0 and 100.".to_string()) + ContractError::HatchPhaseConfigError( + "Initial allocation percentage must be between 0 and 100.".to_string() + ) ); - let allowlist = self - .allowlist - .as_ref() - .map(|addresses| { - addresses - .iter() - .map(|addr| api.addr_validate(addr)) - .collect::>>() - }) - .transpose()?; - - Ok(HatchConfig { - allowlist, - initial_raise: self.initial_raise.clone(), - initial_price: self.initial_price, - initial_allocation_ratio: self.initial_allocation_ratio, - }) - } -} - -impl HatchConfig { - /// Check if the sender is allowlisted for the hatch phase - pub fn assert_allowlisted(&self, hatcher: &Addr) -> Result<(), ContractError> { - if let Some(allowlist) = &self.allowlist { - ensure!( - allowlist.contains(hatcher), - ContractError::SenderNotAllowlisted { - sender: hatcher.to_string(), - } - ); - } + // TODO: define better values + ensure!( + self.exit_tax <= StdDecimal::percent(100u64), + ContractError::HatchPhaseConfigError( + "Exit taxation percentage must be between 0 and 100.".to_string() + ) + ); Ok(()) } } - #[cw_serde] pub struct OpenConfig { // Percentage of capital put into the Reserve Pool during the Open phase pub allocation_percentage: StdDecimal, + // Exit taxation ratio + pub exit_tax: StdDecimal, } impl OpenConfig { /// Validate the open config pub fn validate(&self) -> Result<(), ContractError> { - ensure!( self.allocation_percentage <= StdDecimal::percent(100u64), - ContractError::OpenPhaseConfigError("Reserve percentage must be between 0 and 100.".to_string()) + ContractError::OpenPhaseConfigError( + "Reserve percentage must be between 0 and 100.".to_string() + ) + ); + + ensure!( + self.exit_tax <= StdDecimal::percent(100u64), + ContractError::OpenPhaseConfigError( + "Exit taxation percentage must be between 0 and 100.".to_string() + ) ); Ok(()) @@ -139,11 +116,17 @@ impl OpenConfig { #[cw_serde] pub struct ClosedConfig {} +impl ClosedConfig { + /// Validate the closed config + pub fn validate(&self) -> Result<(), ContractError> { + Ok(()) + } +} #[cw_serde] -pub struct CommonsPhaseConfig { +pub struct CommonsPhaseConfig { // The Hatch phase where initial contributors (Hatchers) participate in a hatch sale. - pub hatch: HatchConfig, + pub hatch: HatchConfig, // The Vesting phase where tokens minted during the Hatch phase are locked (burning is disabled) to combat early speculation/arbitrage. // pub vesting: VestingConfig, // The Open phase where anyone can mint tokens by contributing the reserve token into the curve and becoming members of the Commons. @@ -174,24 +157,55 @@ pub enum CommonsPhase { Hatch, Open, // TODO: should we allow for a closed phase? - Closed + Closed, +} + +impl CommonsPhase { + pub fn expect_hatch(&self) -> Result<(), ContractError> { + ensure!( + matches!(self, CommonsPhase::Hatch), + ContractError::InvalidPhase { + expected: "Hatch".to_string(), + actual: format!("{:?}", self) + } + ); + Ok(()) + } + + pub fn expect_open(&self) -> Result<(), ContractError> { + ensure!( + matches!(self, CommonsPhase::Open), + ContractError::InvalidPhase { + expected: "Open".to_string(), + actual: format!("{:?}", self) + } + ); + Ok(()) + } + + pub fn expect_closed(&self) -> Result<(), ContractError> { + ensure!( + matches!(self, CommonsPhase::Closed), + ContractError::InvalidPhase { + expected: "Closed".to_string(), + actual: format!("{:?}", self) + } + ); + Ok(()) + } } -impl CommonsPhaseConfig { +impl CommonsPhaseConfig { /// Validate that the commons configuration is valid - pub fn validate(&self, api: &dyn Api) -> Result, ContractError> { - let hatch = self.hatch.validate(api)?; + pub fn validate(&self) -> Result<(), ContractError> { + self.hatch.validate()?; self.open.validate()?; + self.closed.validate()?; - Ok(CommonsPhaseConfig { - hatch, - open: self.open.clone(), - closed: self.closed.clone(), - }) + Ok(()) } } - pub type CurveFn = Box Box>; #[cw_serde] @@ -228,4 +242,3 @@ impl CurveType { } } } - diff --git a/contracts/external/cw-abc/src/boot.rs b/contracts/external/cw-abc/src/boot.rs new file mode 100644 index 000000000..d8376b0e5 --- /dev/null +++ b/contracts/external/cw-abc/src/boot.rs @@ -0,0 +1,43 @@ +use crate::msg::*; +use boot_core::{contract, Contract, CwEnv}; +#[cfg(feature = "daemon")] +use boot_core::{ArtifactsDir, Daemon, WasmPath}; +use boot_core::{ContractWrapper, Mock, MockState, TxHandler, Uploadable}; +use cosmwasm_std::Empty; +use token_bindings::{TokenFactoryMsg, TokenFactoryQuery}; + +#[contract(InstantiateMsg, ExecuteMsg, QueryMsg, Empty)] +pub struct CwAbc; + +impl CwAbc { + pub fn new(name: &str, chain: Chain) -> Self { + let contract = Contract::new(name, chain); + Self(contract) + } +} + +/// Basic app for the token factory contract +/// TODO: should be in the bindings, along with custom handler for multi-test +pub(crate) type TokenFactoryBasicApp = boot_core::BasicApp; + +type TokenFactoryMock = Mock; + +impl Uploadable for CwAbc { + fn source(&self) -> ::ContractSource { + Box::new(ContractWrapper::new( + crate::contract::execute, + crate::contract::instantiate, + crate::contract::query, + )) + } +} + +#[cfg(feature = "daemon")] +impl Uploadable for CwAbc { + fn source(&self) -> ::ContractSource { + ArtifactsDir::env() + .expect("Expected ARTIFACTS_DIR in env") + .find_wasm_path("cw_abc") + .unwrap() + } +} diff --git a/contracts/external/cw-abc/src/commands.rs b/contracts/external/cw-abc/src/commands.rs index 3a6ef858f..e4a9535cc 100644 --- a/contracts/external/cw-abc/src/commands.rs +++ b/contracts/external/cw-abc/src/commands.rs @@ -1,11 +1,18 @@ -use cosmwasm_std::{BankMsg, coins, DepsMut, Env, MessageInfo, Response, StdError, StdResult, Uint128}; -use token_bindings::{TokenFactoryQuery, TokenMsg}; -use cw_utils::must_pay; -use crate::abc::{CommonsPhase, CurveFn}; -use crate::ContractError; +use crate::abc::{CommonsPhase, CurveFn, MinMax}; use crate::contract::CwAbcResult; +use crate::ContractError; +use cosmwasm_std::{ + coins, ensure, Addr, BankMsg, Decimal as StdDecimal, DepsMut, Env, MessageInfo, QuerierWrapper, + Response, StdError, StdResult, Storage, Uint128, +}; +use cw_utils::must_pay; +use std::collections::HashSet; +use std::ops::Deref; +use token_bindings::{TokenFactoryMsg, TokenFactoryQuery, TokenMsg}; -use crate::state::{CURVE_STATE, HATCHERS, PHASE, PHASE_CONFIG, SUPPLY_DENOM}; +use crate::state::{ + CURVE_STATE, DONATIONS, HATCHERS, HATCHER_ALLOWLIST, PHASE, PHASE_CONFIG, SUPPLY_DENOM, +}; pub fn execute_buy( deps: DepsMut, @@ -21,16 +28,12 @@ pub fn execute_buy( let phase_config = PHASE_CONFIG.load(deps.storage)?; let mut phase = PHASE.load(deps.storage)?; - let (reserved, funded) = match phase { + let (reserved, funded) = match &phase { CommonsPhase::Hatch => { - let hatch_config = &phase_config.hatch; - + let hatch_config = phase_config.hatch; // Check that the potential hatcher is allowlisted - hatch_config.assert_allowlisted(&info.sender)?; - HATCHERS.update(deps.storage, |mut hatchers| -> StdResult<_>{ - hatchers.insert(info.sender.clone()); - Ok(hatchers) - })?; + assert_allowlisted(deps.storage, &info.sender)?; + update_hatcher_contributions(deps.storage, &info.sender, payment)?; // Check if the initial_raise max has been met if curve_state.reserve + payment >= hatch_config.initial_raise.max { @@ -39,26 +42,12 @@ pub fn execute_buy( PHASE.save(deps.storage, &phase)?; } - // Calculate the number of tokens sent to the funding pool using the initial allocation percentage - // TODO: is it safe to multiply a Decimal with a Uint128? - let funded = payment * hatch_config.initial_allocation_ratio; - // Calculate the number of tokens sent to the reserve - let reserved = payment - funded; - - (reserved, funded) + calculate_reserved_and_funded(payment, hatch_config.initial_allocation_ratio) } CommonsPhase::Open => { - let hatch_config = &phase_config.open; - - // Calculate the number of tokens sent to the funding pool using the allocation percentage - let funded = payment * hatch_config.allocation_percentage; - // Calculate the number of tokens sent to the reserve - let reserved = payment - funded; - - (reserved, funded) + calculate_reserved_and_funded(payment, phase_config.open.allocation_percentage) } CommonsPhase::Closed => { - // TODO: what to do here? return Err(ContractError::CommonsClosed {}); } }; @@ -67,6 +56,7 @@ pub fn execute_buy( let curve = curve_fn(curve_state.clone().decimals); curve_state.reserve += reserved; curve_state.funding += funded; + // Calculate the supply based on the reserve let new_supply = curve.supply(curve_state.reserve); let minted = new_supply .checked_sub(curve_state.supply) @@ -74,13 +64,7 @@ pub fn execute_buy( curve_state.supply = new_supply; CURVE_STATE.save(deps.storage, &curve_state)?; - let denom = SUPPLY_DENOM.load(deps.storage)?; - // mint supply token - let mint_msg = TokenMsg::MintTokens { - denom, - amount: minted, - mint_to_address: info.sender.to_string(), - }; + let mint_msg = mint_supply_msg(deps.storage, minted, &info.sender)?; Ok(Response::new() .add_message(mint_msg) @@ -91,51 +75,329 @@ pub fn execute_buy( .add_attribute("supply", minted)) } +/// Build a message to mint the supply token to the sender +fn mint_supply_msg(storage: &dyn Storage, minted: Uint128, minter: &Addr) -> CwAbcResult { + let denom = SUPPLY_DENOM.load(storage)?; + // mint supply token + Ok(TokenMsg::mint_contract_tokens( + denom, + minted, + minter.to_string(), + )) +} + +/// Return the reserved and funded amounts based on the payment and the allocation ratio +fn calculate_reserved_and_funded( + payment: Uint128, + allocation_ratio: StdDecimal, +) -> (Uint128, Uint128) { + let funded = payment * allocation_ratio; + let reserved = payment.checked_sub(funded).unwrap(); // Since allocation_ratio is < 1, this subtraction is safe + (reserved, funded) +} + +/// Add the hatcher's contribution to the total contributions +fn update_hatcher_contributions( + storage: &mut dyn Storage, + hatcher: &Addr, + contribution: Uint128, +) -> StdResult<()> { + HATCHERS.update(storage, hatcher, |amount| -> StdResult<_> { + match amount { + Some(mut amount) => { + amount += contribution; + Ok(amount) + } + None => Ok(contribution), + } + })?; + Ok(()) +} + pub fn execute_sell( deps: DepsMut, _env: Env, info: MessageInfo, curve_fn: CurveFn, - amount: Uint128, ) -> CwAbcResult { - let receiver = info.sender.clone(); + let burner = info.sender.clone(); - let denom = SUPPLY_DENOM.load(deps.storage)?; - let payment = must_pay(&info, &denom)?; + let supply_denom = SUPPLY_DENOM.load(deps.storage)?; + let burn_amount = must_pay(&info, &supply_denom)?; + // Burn the sent supply tokens + let burn_msg = TokenMsg::burn_contract_tokens(supply_denom, burn_amount, burner.to_string()); - // calculate how many tokens can be purchased with this and mint them - let mut state = CURVE_STATE.load(deps.storage)?; - let curve = curve_fn(state.clone().decimals); - state.supply = state + let taxed_amount = calculate_exit_tax(deps.storage, burn_amount)?; + + let mut curve_state = CURVE_STATE.load(deps.storage)?; + let curve = curve_fn(curve_state.clone().decimals); + + // Reduce the supply by the amount burned + curve_state.supply = curve_state .supply - .checked_sub(amount) + .checked_sub(burn_amount) .map_err(StdError::overflow)?; - let new_reserve = curve.reserve(state.supply); - let released = state + + // Calculate the new reserve based on the new supply + let new_reserve = curve.reserve(curve_state.supply); + curve_state.reserve = new_reserve; + curve_state.funding += taxed_amount; + CURVE_STATE.save(deps.storage, &curve_state)?; + + // Calculate how many reserve tokens to release based on the sell amount + let released_reserve = curve_state .reserve .checked_sub(new_reserve) .map_err(StdError::overflow)?; - state.reserve = new_reserve; - CURVE_STATE.save(deps.storage, &state)?; - // Burn the tokens - let burn_msg = TokenMsg::BurnTokens { - denom, - amount: payment, - burn_from_address: info.sender.to_string(), - }; - - // now send the tokens to the sender (TODO: for sell_from we do something else, right???) + // Now send the tokens to the sender let msg = BankMsg::Send { - to_address: receiver.to_string(), - amount: coins(released.u128(), state.reserve_denom), + to_address: burner.to_string(), + amount: coins(released_reserve.u128(), curve_state.reserve_denom), }; Ok(Response::new() .add_message(msg) .add_message(burn_msg) .add_attribute("action", "burn") - .add_attribute("from", info.sender) - .add_attribute("supply", amount) - .add_attribute("reserve", released)) -} \ No newline at end of file + .add_attribute("from", burner) + .add_attribute("amount", burn_amount) + .add_attribute("burned", released_reserve) + .add_attribute("funded", taxed_amount)) +} + +/// Calculate the exit taxation for the sell amount based on the phase +fn calculate_exit_tax(storage: &dyn Storage, sell_amount: Uint128) -> CwAbcResult { + // Load the phase config and phase + let phase = PHASE.load(storage)?; + let phase_config = PHASE_CONFIG.load(storage)?; + + // Calculate the exit tax based on the phase + let exit_tax = match &phase { + CommonsPhase::Hatch => phase_config.hatch.exit_tax, + CommonsPhase::Open => phase_config.open.exit_tax, + CommonsPhase::Closed => return Err(ContractError::CommonsClosed {}), + }; + + debug_assert!( + exit_tax <= StdDecimal::percent(100), + "Exit tax must be <= 100%" + ); + + // This won't ever overflow because it's checked + let taxed_amount = sell_amount * exit_tax; + Ok(taxed_amount) +} + +/// Send a donation to the funding pool +pub fn execute_donate( + deps: DepsMut, + _env: Env, + info: MessageInfo, +) -> CwAbcResult { + let mut curve_state = CURVE_STATE.load(deps.storage)?; + + let payment = must_pay(&info, &curve_state.reserve_denom)?; + curve_state.funding += payment; + CURVE_STATE.save(deps.storage, &curve_state)?; + + // No minting of tokens is necessary, the supply stays the same + DONATIONS.save(deps.storage, &info.sender, &payment)?; + + Ok(Response::new() + .add_attribute("action", "donate") + .add_attribute("donor", info.sender) + .add_attribute("amount", payment)) +} + +/// Check if the sender is allowlisted for the hatch phase +fn assert_allowlisted(storage: &dyn Storage, hatcher: &Addr) -> Result<(), ContractError> { + let allowlist = HATCHER_ALLOWLIST.may_load(storage)?; + if let Some(allowlist) = allowlist { + ensure!( + allowlist.contains(hatcher), + ContractError::SenderNotAllowlisted { + sender: hatcher.to_string(), + } + ); + } + + Ok(()) +} + +/// Add and remove addresses from the hatcher allowlist +pub fn update_hatch_allowlist( + deps: DepsMut, + info: MessageInfo, + to_add: Vec, + to_remove: Vec, +) -> CwAbcResult { + cw_ownable::assert_owner(deps.storage, &info.sender)?; + let mut allowlist = HATCHER_ALLOWLIST.may_load(deps.storage)?; + + if allowlist.is_none() { + allowlist = Some(HashSet::new()); + } + + let allowlist = allowlist.as_mut().unwrap(); + + // Add addresses to the allowlist + for allow in to_add { + let addr = deps.api.addr_validate(allow.as_str())?; + allowlist.insert(addr); + } + + // Remove addresses from the allowlist + for deny in to_remove { + let addr = deps.api.addr_validate(deny.as_str())?; + allowlist.remove(&addr); + } + + HATCHER_ALLOWLIST.save(deps.storage, allowlist)?; + + Ok(Response::new().add_attributes(vec![("action", "update_hatch_allowlist")])) +} + +/// Update the hatch config +pub fn update_hatch_config( + deps: DepsMut, + _env: Env, + info: MessageInfo, + initial_raise: Option, + initial_allocation_ratio: Option, +) -> CwAbcResult { + // Assert that the sender is the contract owner + cw_ownable::assert_owner(deps.storage, &info.sender)?; + + // Ensure we're in the Hatch phase + PHASE.load(deps.storage)?.expect_hatch()?; + + // Load the current phase config + let mut phase_config = PHASE_CONFIG.load(deps.storage)?; + + // Update the hatch config if new values are provided + if let Some(initial_raise) = initial_raise { + phase_config.hatch.initial_raise = initial_raise; + } + if let Some(initial_allocation_ratio) = initial_allocation_ratio { + phase_config.hatch.initial_allocation_ratio = initial_allocation_ratio; + } + + phase_config.hatch.validate()?; + PHASE_CONFIG.save(deps.storage, &phase_config)?; + + Ok(Response::new().add_attribute("action", "update_hatch_config")) +} + +/// Update the ownership of the contract +pub fn update_ownership( + deps: DepsMut, + env: &Env, + info: &MessageInfo, + action: cw_ownable::Action, +) -> Result, ContractError> { + let ownership = cw_ownable::update_ownership( + DepsMut { + storage: deps.storage, + api: deps.api, + querier: QuerierWrapper::new(deps.querier.deref()), + }, + &env.block, + &info.sender, + action, + )?; + + Ok(Response::default().add_attributes(ownership.into_attributes())) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::testing::prelude::*; + use cosmwasm_std::testing::*; + + mod donate { + use super::*; + use crate::abc::CurveType; + use crate::testing::mock_init; + use cosmwasm_std::coin; + use cw_utils::PaymentError; + + const TEST_DONOR: &str = "donor"; + + fn exec_donate(deps: DepsMut, donation_amount: u128) -> CwAbcResult { + execute_donate( + deps, + mock_env(), + mock_info(TEST_DONOR, &[coin(donation_amount, TEST_RESERVE_DENOM)]), + ) + } + + #[test] + fn should_fail_with_no_funds() -> CwAbcResult<()> { + let mut deps = mock_tf_dependencies(); + let curve_type = CurveType::Linear { + slope: Uint128::new(1), + scale: 1, + }; + let init_msg = default_instantiate_msg(2, 8, curve_type); + mock_init(deps.as_mut(), init_msg)?; + + let res = exec_donate(deps.as_mut(), 0); + assert_that!(res) + .is_err() + .is_equal_to(ContractError::Payment(PaymentError::NoFunds {})); + + Ok(()) + } + + #[test] + fn should_fail_with_incorrect_denom() -> CwAbcResult<()> { + let mut deps = mock_tf_dependencies(); + let curve_type = CurveType::Linear { + slope: Uint128::new(1), + scale: 1, + }; + let init_msg = default_instantiate_msg(2, 8, curve_type); + mock_init(deps.as_mut(), init_msg)?; + + let res = execute_donate( + deps.as_mut(), + mock_env(), + mock_info(TEST_DONOR, &[coin(1, "fake")]), + ); + assert_that!(res) + .is_err() + .is_equal_to(ContractError::Payment(PaymentError::MissingDenom( + TEST_RESERVE_DENOM.to_string(), + ))); + + Ok(()) + } + + #[test] + fn should_add_to_funding_pool() -> CwAbcResult<()> { + let mut deps = mock_tf_dependencies(); + // this matches `linear_curve` test case from curves.rs + let curve_type = CurveType::SquareRoot { + slope: Uint128::new(1), + scale: 1, + }; + let init_msg = default_instantiate_msg(2, 8, curve_type); + mock_init(deps.as_mut(), init_msg)?; + + let donation_amount = 5; + let _res = exec_donate(deps.as_mut(), donation_amount)?; + + // check that the curve's funding has been increased while supply and reserve have not + let curve_state = CURVE_STATE.load(&deps.storage)?; + assert_that!(curve_state.funding).is_equal_to(Uint128::new(donation_amount)); + + // check that the donor is in the donations map + let donation = DONATIONS.load(&deps.storage, &Addr::unchecked(TEST_DONOR))?; + assert_that!(donation).is_equal_to(Uint128::new(donation_amount)); + + Ok(()) + } + } +} diff --git a/contracts/external/cw-abc/src/contract.rs b/contracts/external/cw-abc/src/contract.rs index 2e21f0d70..c36470c33 100644 --- a/contracts/external/cw-abc/src/contract.rs +++ b/contracts/external/cw-abc/src/contract.rs @@ -1,21 +1,23 @@ -use std::collections::HashSet; -use std::ops::Deref; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; -use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, QuerierWrapper, Response, StdResult, to_binary}; +use cosmwasm_std::{to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; use cw2::set_contract_version; +use std::collections::HashSet; + use token_bindings::{TokenFactoryMsg, TokenFactoryQuery, TokenMsg}; +use crate::abc::CurveFn; use crate::curves::DecimalPlaces; use crate::error::ContractError; -use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; -use crate::state::{CURVE_STATE, CURVE_TYPE, CurveState, HATCHERS, PHASE_CONFIG, SUPPLY_DENOM}; -use cw_utils::nonpayable; -use crate::abc::CurveFn; +use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg, UpdatePhaseConfigMsg}; +use crate::state::{ + CurveState, CURVE_STATE, CURVE_TYPE, HATCHER_ALLOWLIST, PHASE_CONFIG, SUPPLY_DENOM, +}; use crate::{commands, queries}; +use cw_utils::nonpayable; // version info for migration info -const CONTRACT_NAME: &str = "crates.io:cw20-abc"; +pub(crate) const CONTRACT_NAME: &str = "crates.io:cw20-abc"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); // By default, the prefix for token factory tokens is "factory" @@ -33,20 +35,21 @@ pub fn instantiate( nonpayable(&info)?; set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - let InstantiateMsg { supply, reserve, curve_type, phase_config, + hatcher_allowlist, } = msg; - if supply.subdenom.is_empty() { - return Err(ContractError::SupplyTokenError("Token subdenom must not be empty.".to_string())); + return Err(ContractError::SupplyTokenError( + "Token subdenom must not be empty.".to_string(), + )); } - let phase_config = phase_config.validate(deps.api)?; + phase_config.validate()?; // Create supply denom with metadata let create_supply_denom_msg = TokenMsg::CreateDenom { @@ -72,7 +75,14 @@ pub fn instantiate( let curve_state = CurveState::new(reserve.denom, normalization_places); CURVE_STATE.save(deps.storage, &curve_state)?; CURVE_TYPE.save(deps.storage, &curve_type)?; - HATCHERS.save(deps.storage, &HashSet::new())?; + + if let Some(allowlist) = hatcher_allowlist { + let allowlist = allowlist + .into_iter() + .map(|addr| deps.api.addr_validate(addr.as_str())) + .collect::>>()?; + HATCHER_ALLOWLIST.save(deps.storage, &allowlist)?; + } PHASE_CONFIG.save(deps.storage, &phase_config)?; @@ -81,7 +91,6 @@ pub fn instantiate( Ok(Response::default().add_message(create_supply_denom_msg)) } - #[cfg_attr(not(feature = "library"), entry_point)] pub fn execute( deps: DepsMut, @@ -108,29 +117,30 @@ pub fn do_execute( ) -> CwAbcResult { match msg { ExecuteMsg::Buy {} => commands::execute_buy(deps, env, info, curve_fn), - ExecuteMsg::Burn { amount } => commands::execute_sell(deps, env, info, curve_fn, amount), - ExecuteMsg::UpdateHatchAllowlist { to_add: _, to_remove: _ } => { - cw_ownable::assert_owner(deps.storage, &info.sender)?; - // commands::execute_update_hatch_allowlist(deps, env, info, to_add, to_remove) - todo!() + ExecuteMsg::Burn {} => commands::execute_sell(deps, env, info, curve_fn), + ExecuteMsg::Donate {} => commands::execute_donate(deps, env, info), + ExecuteMsg::UpdateHatchAllowlist { to_add, to_remove } => { + commands::update_hatch_allowlist(deps, info, to_add, to_remove) } - ExecuteMsg::UpdateHatchConfig { .. } => { - cw_ownable::assert_owner(deps.storage, &info.sender)?; - todo!() + ExecuteMsg::UpdatePhaseConfig(update) => match update { + UpdatePhaseConfigMsg::Hatch { + initial_raise, + initial_allocation_ratio, + } => commands::update_hatch_config( + deps, + env, + info, + initial_raise, + initial_allocation_ratio, + ), + _ => todo!(), }, ExecuteMsg::UpdateOwnership(action) => { - let ownership = cw_ownable::update_ownership(DepsMut { - storage: deps.storage, - api: deps.api, - querier: QuerierWrapper::new(deps.querier.deref()), - }, &env.block, &info.sender, action)?; - - Ok(Response::default().add_attributes(ownership.into_attributes())) + commands::update_ownership(deps, &env, &info, action) } } } - #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { // default implementation stores curve info as enum, you can do something else in a derived @@ -153,6 +163,12 @@ pub fn do_query( // custom queries QueryMsg::CurveInfo {} => to_binary(&queries::query_curve_info(deps, curve_fn)?), QueryMsg::PhaseConfig {} => to_binary(&queries::query_phase_config(deps)?), + QueryMsg::Donations { start_after, limit } => { + to_binary(&queries::query_donations(deps, start_after, limit)?) + } + QueryMsg::Hatchers { start_after, limit } => { + to_binary(&queries::query_hatchers(deps, start_after, limit)?) + } QueryMsg::Ownership {} => to_binary(&cw_ownable::get_ownership(deps.storage)?), // QueryMsg::GetDenom { // creator_address, @@ -204,93 +220,34 @@ pub fn do_query( // this is poor man's "skip" flag #[cfg(test)] -mod tests { - use std::marker::PhantomData; - use cosmwasm_std::{CosmosMsg, Decimal, OwnedDeps, testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}, Uint128}; - use token_bindings::Metadata; - use crate::abc::{ClosedConfig, CommonsPhaseConfig, CurveType, HatchConfig, MinMax, OpenConfig, ReserveToken, SupplyToken}; +pub(crate) mod tests { use super::*; - use speculoos::prelude::*; + use crate::abc::CurveType; use crate::queries::query_curve_info; + use cosmwasm_std::{ + testing::{mock_env, mock_info}, + CosmosMsg, Decimal, Uint128, + }; + use speculoos::prelude::*; - const DENOM: &str = "satoshi"; - const CREATOR: &str = "creator"; - const INVESTOR: &str = "investor"; - const BUYER: &str = "buyer"; - - const SUPPLY_DENOM: &str = "subdenom"; - - - - fn default_supply_metadata() -> Metadata { - Metadata { - name: Some("Bonded".to_string()), - symbol: Some("EPOXY".to_string()), - description: None, - denom_units: vec![], - base: None, - display: None, - } - } - - fn default_instantiate( - decimals: u8, - reserve_decimals: u8, - curve_type: CurveType, - ) -> InstantiateMsg { - InstantiateMsg { - supply: SupplyToken { - subdenom: SUPPLY_DENOM.to_string(), - metadata: default_supply_metadata(), - decimals, - }, - reserve: ReserveToken { - denom: DENOM.to_string(), - decimals: reserve_decimals, - }, - phase_config: CommonsPhaseConfig { - hatch: HatchConfig { - initial_raise: MinMax { - min: Uint128::one(), - max: Uint128::from(1000000u128), - }, - initial_price: Uint128::one(), - initial_allocation_ratio: Decimal::percent(10u64), - allowlist: None, - }, - open: OpenConfig { - allocation_percentage: Decimal::percent(10u64), - }, - closed: ClosedConfig {}, - }, - curve_type, - } - } + use crate::testing::*; -// fn get_balance>(deps: Deps, addr: U) -> Uint128 { -// query_balance(deps, addr.into()).unwrap().balance -// } + // fn get_balance>(deps: Deps, addr: U) -> Uint128 { + // query_balance(deps, addr.into()).unwrap().balance + // } -// fn setup_test(deps: DepsMut, decimals: u8, reserve_decimals: u8, curve_type: CurveType) { -// // this matches `linear_curve` test case from curves.rs -// let creator = String::from(CREATOR); -// let msg = default_instantiate(decimals, reserve_decimals, curve_type); -// let info = mock_info(&creator, &[]); + // fn setup_test(deps: DepsMut, decimals: u8, reserve_decimals: u8, curve_type: CurveType) { + // // this matches `linear_curve` test case from curves.rs + // let creator = String::from(CREATOR); + // let msg = default_instantiate(decimals, reserve_decimals, curve_type); + // let info = mock_info(&creator, &[]); -// // make sure we can instantiate with this -// let res = instantiate(deps, mock_env(), info, msg).unwrap(); -// assert_eq!(0, res.messages.len()); -// } + // // make sure we can instantiate with this + // let res = instantiate(deps, mock_env(), info, msg).unwrap(); + // assert_eq!(0, res.messages.len()); + // } /// Mock token factory querier dependencies - fn mock_tf_dependencies() -> OwnedDeps, TokenFactoryQuery> { - OwnedDeps { - storage: MockStorage::default(), - api: MockApi::default(), - querier: MockQuerier::::new(&[]), - custom_query_type: PhantomData::, - } - } #[test] fn proper_instantiation() -> CwAbcResult<()> { @@ -302,17 +259,19 @@ mod tests { slope: Uint128::new(1), scale: 1, }; - let msg = default_instantiate(2, 8, curve_type.clone()); + let msg = default_instantiate_msg(2, 8, curve_type.clone()); let info = mock_info(&creator, &[]); // make sure we can instantiate with this let res = instantiate(deps.as_mut(), mock_env(), info, msg)?; assert_that!(res.messages.len()).is_equal_to(1); let submsg = res.messages.get(0).unwrap(); - assert_that!(submsg.msg).is_equal_to(CosmosMsg::Custom(TokenFactoryMsg::Token(TokenMsg::CreateDenom { - subdenom: SUPPLY_DENOM.to_string(), - metadata: Some(default_supply_metadata()), - }))); + assert_that!(submsg.msg).is_equal_to(CosmosMsg::Custom(TokenFactoryMsg::Token( + TokenMsg::CreateDenom { + subdenom: TEST_SUPPLY_DENOM.to_string(), + metadata: Some(default_supply_metadata()), + }, + ))); // TODO! // // token info is proper @@ -326,7 +285,7 @@ mod tests { let state = query_curve_info(deps.as_ref(), curve_type.to_curve_fn())?; assert_that!(state.reserve).is_equal_to(Uint128::zero()); assert_that!(state.supply).is_equal_to(Uint128::zero()); - assert_that!(state.reserve_denom.as_str()).is_equal_to(DENOM); + assert_that!(state.reserve_denom.as_str()).is_equal_to(TEST_RESERVE_DENOM); // spot price 0 as supply is 0 assert_that!(state.spot_price).is_equal_to(Decimal::zero()); @@ -340,136 +299,136 @@ mod tests { Ok(()) } -// #[test] -// fn buy_issues_tokens() { -// let mut deps = mock_dependencies(); -// let curve_type = CurveType::Linear { -// slope: Uint128::new(1), -// scale: 1, -// }; -// setup_test(deps.as_mut(), 2, 8, curve_type.clone()); - -// // succeeds with proper token (5 BTC = 5*10^8 satoshi) -// let info = mock_info(INVESTOR, &coins(500_000_000, DENOM)); -// let buy = ExecuteMsg::Buy {}; -// execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap(); - -// // bob got 1000 EPOXY (10.00) -// assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000)); -// assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::zero()); - -// // send them all to buyer -// let info = mock_info(INVESTOR, &[]); -// let send = ExecuteMsg::Transfer { -// recipient: BUYER.into(), -// amount: Uint128::new(1000), -// }; -// execute(deps.as_mut(), mock_env(), info, send).unwrap(); - -// // ensure balances updated -// assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::zero()); -// assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::new(1000)); - -// // second stake needs more to get next 1000 EPOXY -// let info = mock_info(INVESTOR, &coins(1_500_000_000, DENOM)); -// execute(deps.as_mut(), mock_env(), info, buy).unwrap(); - -// // ensure balances updated -// assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000)); -// assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::new(1000)); - -// // check curve info updated -// let curve = query_curve_info(deps.as_ref(), curve_type.to_curve_fn()).unwrap(); -// assert_eq!(curve.reserve, Uint128::new(2_000_000_000)); -// assert_eq!(curve.supply, Uint128::new(2000)); -// assert_eq!(curve.spot_price, Decimal::percent(200)); - -// // check token info updated -// let token = query_token_info(deps.as_ref()).unwrap(); -// assert_eq!(token.decimals, 2); -// assert_eq!(token.total_supply, Uint128::new(2000)); -// } - -// #[test] -// fn bonding_fails_with_wrong_denom() { -// let mut deps = mock_dependencies(); -// let curve_type = CurveType::Linear { -// slope: Uint128::new(1), -// scale: 1, -// }; -// setup_test(deps.as_mut(), 2, 8, curve_type); - -// // fails when no tokens sent -// let info = mock_info(INVESTOR, &[]); -// let buy = ExecuteMsg::Buy {}; -// let err = execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap_err(); -// assert_eq!(err, PaymentError::NoFunds {}.into()); - -// // fails when wrong tokens sent -// let info = mock_info(INVESTOR, &coins(1234567, "wei")); -// let err = execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap_err(); -// assert_eq!(err, PaymentError::MissingDenom(DENOM.into()).into()); - -// // fails when too many tokens sent -// let info = mock_info(INVESTOR, &[coin(3400022, DENOM), coin(1234567, "wei")]); -// let err = execute(deps.as_mut(), mock_env(), info, buy).unwrap_err(); -// assert_eq!(err, PaymentError::MultipleDenoms {}.into()); -// } - -// #[test] -// fn burning_sends_reserve() { -// let mut deps = mock_dependencies(); -// let curve_type = CurveType::Linear { -// slope: Uint128::new(1), -// scale: 1, -// }; -// setup_test(deps.as_mut(), 2, 8, curve_type.clone()); - -// // succeeds with proper token (20 BTC = 20*10^8 satoshi) -// let info = mock_info(INVESTOR, &coins(2_000_000_000, DENOM)); -// let buy = ExecuteMsg::Buy {}; -// execute(deps.as_mut(), mock_env(), info, buy).unwrap(); - -// // bob got 2000 EPOXY (20.00) -// assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(2000)); - -// // cannot burn too much -// let info = mock_info(INVESTOR, &[]); -// let burn = ExecuteMsg::Burn { -// amount: Uint128::new(3000), -// }; -// let err = execute(deps.as_mut(), mock_env(), info, burn).unwrap_err(); -// // TODO check error - -// // burn 1000 EPOXY to get back 15BTC (*10^8) -// let info = mock_info(INVESTOR, &[]); -// let burn = ExecuteMsg::Burn { -// amount: Uint128::new(1000), -// }; -// let res = execute(deps.as_mut(), mock_env(), info, burn).unwrap(); - -// // balance is lower -// assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000)); - -// // ensure we got our money back -// assert_eq!(1, res.messages.len()); -// assert_eq!( -// &res.messages[0], -// &SubMsg::new(BankMsg::Send { -// to_address: INVESTOR.into(), -// amount: coins(1_500_000_000, DENOM), -// }) -// ); - -// // check curve info updated -// let curve = query_curve_info(deps.as_ref(), curve_type.to_curve_fn()).unwrap(); -// assert_eq!(curve.reserve, Uint128::new(500_000_000)); -// assert_eq!(curve.supply, Uint128::new(1000)); -// assert_eq!(curve.spot_price, Decimal::percent(100)); - -// // check token info updated -// let token = query_token_info(deps.as_ref()).unwrap(); -// assert_eq!(token.decimals, 2); -// assert_eq!(token.total_supply, Uint128::new(1000)); -// } + // #[test] + // fn buy_issues_tokens() { + // let mut deps = mock_dependencies(); + // let curve_type = CurveType::Linear { + // slope: Uint128::new(1), + // scale: 1, + // }; + // setup_test(deps.as_mut(), 2, 8, curve_type.clone()); + + // // succeeds with proper token (5 BTC = 5*10^8 satoshi) + // let info = mock_info(INVESTOR, &coins(500_000_000, DENOM)); + // let buy = ExecuteMsg::Buy {}; + // execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap(); + + // // bob got 1000 EPOXY (10.00) + // assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000)); + // assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::zero()); + + // // send them all to buyer + // let info = mock_info(INVESTOR, &[]); + // let send = ExecuteMsg::Transfer { + // recipient: BUYER.into(), + // amount: Uint128::new(1000), + // }; + // execute(deps.as_mut(), mock_env(), info, send).unwrap(); + + // // ensure balances updated + // assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::zero()); + // assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::new(1000)); + + // // second stake needs more to get next 1000 EPOXY + // let info = mock_info(INVESTOR, &coins(1_500_000_000, DENOM)); + // execute(deps.as_mut(), mock_env(), info, buy).unwrap(); + + // // ensure balances updated + // assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000)); + // assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::new(1000)); + + // // check curve info updated + // let curve = query_curve_info(deps.as_ref(), curve_type.to_curve_fn()).unwrap(); + // assert_eq!(curve.reserve, Uint128::new(2_000_000_000)); + // assert_eq!(curve.supply, Uint128::new(2000)); + // assert_eq!(curve.spot_price, Decimal::percent(200)); + + // // check token info updated + // let token = query_token_info(deps.as_ref()).unwrap(); + // assert_eq!(token.decimals, 2); + // assert_eq!(token.total_supply, Uint128::new(2000)); + // } + + // #[test] + // fn bonding_fails_with_wrong_denom() { + // let mut deps = mock_dependencies(); + // let curve_type = CurveType::Linear { + // slope: Uint128::new(1), + // scale: 1, + // }; + // setup_test(deps.as_mut(), 2, 8, curve_type); + + // // fails when no tokens sent + // let info = mock_info(INVESTOR, &[]); + // let buy = ExecuteMsg::Buy {}; + // let err = execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap_err(); + // assert_eq!(err, PaymentError::NoFunds {}.into()); + + // // fails when wrong tokens sent + // let info = mock_info(INVESTOR, &coins(1234567, "wei")); + // let err = execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap_err(); + // assert_eq!(err, PaymentError::MissingDenom(DENOM.into()).into()); + + // // fails when too many tokens sent + // let info = mock_info(INVESTOR, &[coin(3400022, DENOM), coin(1234567, "wei")]); + // let err = execute(deps.as_mut(), mock_env(), info, buy).unwrap_err(); + // assert_eq!(err, PaymentError::MultipleDenoms {}.into()); + // } + + // #[test] + // fn burning_sends_reserve() { + // let mut deps = mock_dependencies(); + // let curve_type = CurveType::Linear { + // slope: Uint128::new(1), + // scale: 1, + // }; + // setup_test(deps.as_mut(), 2, 8, curve_type.clone()); + + // // succeeds with proper token (20 BTC = 20*10^8 satoshi) + // let info = mock_info(INVESTOR, &coins(2_000_000_000, DENOM)); + // let buy = ExecuteMsg::Buy {}; + // execute(deps.as_mut(), mock_env(), info, buy).unwrap(); + + // // bob got 2000 EPOXY (20.00) + // assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(2000)); + + // // cannot burn too much + // let info = mock_info(INVESTOR, &[]); + // let burn = ExecuteMsg::Burn { + // amount: Uint128::new(3000), + // }; + // let err = execute(deps.as_mut(), mock_env(), info, burn).unwrap_err(); + // // TODO check error + + // // burn 1000 EPOXY to get back 15BTC (*10^8) + // let info = mock_info(INVESTOR, &[]); + // let burn = ExecuteMsg::Burn { + // amount: Uint128::new(1000), + // }; + // let res = execute(deps.as_mut(), mock_env(), info, burn).unwrap(); + + // // balance is lower + // assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000)); + + // // ensure we got our money back + // assert_eq!(1, res.messages.len()); + // assert_eq!( + // &res.messages[0], + // &SubMsg::new(BankMsg::Send { + // to_address: INVESTOR.into(), + // amount: coins(1_500_000_000, DENOM), + // }) + // ); + + // // check curve info updated + // let curve = query_curve_info(deps.as_ref(), curve_type.to_curve_fn()).unwrap(); + // assert_eq!(curve.reserve, Uint128::new(500_000_000)); + // assert_eq!(curve.supply, Uint128::new(1000)); + // assert_eq!(curve.spot_price, Decimal::percent(100)); + + // // check token info updated + // let token = query_token_info(deps.as_ref()).unwrap(); + // assert_eq!(token.decimals, 2); + // assert_eq!(token.total_supply, Uint128::new(1000)); + // } } diff --git a/contracts/external/cw-abc/src/error.rs b/contracts/external/cw-abc/src/error.rs index 69ece1764..2e3989515 100644 --- a/contracts/external/cw-abc/src/error.rs +++ b/contracts/external/cw-abc/src/error.rs @@ -33,4 +33,13 @@ pub enum ContractError { #[error("The commons is closed to new contributions")] CommonsClosed {}, + + #[error("Selling is disabled during the hatch phase")] + HatchSellingDisabled {}, + + #[error("Invalid sell amount")] + MismatchedSellAmount {}, + + #[error("Invalid phase, expected {expected:?}, actual {actual:?}")] + InvalidPhase { expected: String, actual: String }, } diff --git a/contracts/external/cw-abc/src/integration.rs b/contracts/external/cw-abc/src/integration.rs new file mode 100644 index 000000000..d6b23b8bc --- /dev/null +++ b/contracts/external/cw-abc/src/integration.rs @@ -0,0 +1,38 @@ +use crate::{abc::CurveType, boot::CwAbc}; +use boot_core::{BootUpload, Mock}; +use cosmwasm_std::{Addr, Uint128}; + +use crate::testing::prelude::*; + +type AResult = anyhow::Result<()>; // alias for Result<(), anyhow::Error> + +// TODO: we need to make a PR to token factory bindings for the CustomHandler so that messages will actually execute +#[test] +fn instantiate() -> AResult { + let sender = Addr::unchecked(TEST_CREATOR); + let chain = Mock::new(&sender)?; + + let abc = CwAbc::new("cw:abc", chain); + abc.upload()?; + + let curve_type = CurveType::SquareRoot { + slope: Uint128::new(1), + scale: 1, + }; + + let _init_msg = default_instantiate_msg(5u8, 5u8, curve_type); + // abc.instantiate(&init_msg, None, None)?; + // + // let expected_config = msg::CurveInfoResponse { + // reserve: Default::default(), + // supply: Default::default(), + // funding: Default::default(), + // spot_price: Default::default(), + // reserve_denom: "".to_string(), + // }; + // + // let actual_config = abc.curve_info()?; + // + // assert_that!(&actual_config).is_equal_to(&expected_config); + Ok(()) +} diff --git a/contracts/external/cw-abc/src/lib.rs b/contracts/external/cw-abc/src/lib.rs index d96c62646..5070715b8 100644 --- a/contracts/external/cw-abc/src/lib.rs +++ b/contracts/external/cw-abc/src/lib.rs @@ -1,10 +1,112 @@ +pub mod abc; +#[cfg(feature = "boot")] +pub mod boot; +pub(crate) mod commands; pub mod contract; pub mod curves; mod error; +#[cfg(test)] +mod integration; pub mod msg; -pub mod state; -pub mod abc; -pub(crate) mod commands; mod queries; +pub mod state; pub use crate::error::ContractError; + +#[cfg(test)] +pub(crate) mod testing { + use crate::abc::{ + ClosedConfig, CommonsPhaseConfig, CurveType, HatchConfig, MinMax, OpenConfig, ReserveToken, + SupplyToken, + }; + use crate::msg::InstantiateMsg; + use cosmwasm_std::{ + testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}, + Decimal, OwnedDeps, Uint128, + }; + + use crate::contract; + use crate::contract::CwAbcResult; + use cosmwasm_std::DepsMut; + use std::marker::PhantomData; + use token_bindings::{Metadata, TokenFactoryQuery}; + + pub(crate) mod prelude { + pub use super::{ + default_instantiate_msg, default_supply_metadata, mock_tf_dependencies, TEST_BUYER, + TEST_CREATOR, TEST_INVESTOR, TEST_RESERVE_DENOM, TEST_SUPPLY_DENOM, + }; + pub use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + pub use speculoos::prelude::*; + } + + pub const TEST_RESERVE_DENOM: &str = "satoshi"; + pub const TEST_CREATOR: &str = "creator"; + pub const TEST_INVESTOR: &str = "investor"; + pub const TEST_BUYER: &str = "buyer"; + + pub const TEST_SUPPLY_DENOM: &str = "subdenom"; + + pub fn default_supply_metadata() -> Metadata { + Metadata { + name: Some("Bonded".to_string()), + symbol: Some("EPOXY".to_string()), + description: None, + denom_units: vec![], + base: None, + display: None, + } + } + + pub fn default_instantiate_msg( + decimals: u8, + reserve_decimals: u8, + curve_type: CurveType, + ) -> InstantiateMsg { + InstantiateMsg { + supply: SupplyToken { + subdenom: TEST_SUPPLY_DENOM.to_string(), + metadata: default_supply_metadata(), + decimals, + }, + reserve: ReserveToken { + denom: TEST_RESERVE_DENOM.to_string(), + decimals: reserve_decimals, + }, + phase_config: CommonsPhaseConfig { + hatch: HatchConfig { + initial_raise: MinMax { + min: Uint128::one(), + max: Uint128::from(1000000u128), + }, + initial_price: Uint128::one(), + initial_allocation_ratio: Decimal::percent(10u64), + exit_tax: Decimal::zero(), + }, + open: OpenConfig { + allocation_percentage: Decimal::percent(10u64), + exit_tax: Decimal::percent(10u64), + }, + closed: ClosedConfig {}, + }, + hatcher_allowlist: None, + curve_type, + } + } + + pub fn mock_init(deps: DepsMut, init_msg: InstantiateMsg) -> CwAbcResult { + let info = mock_info(TEST_CREATOR, &[]); + let env = mock_env(); + contract::instantiate(deps, env, info, init_msg) + } + + pub fn mock_tf_dependencies( + ) -> OwnedDeps, TokenFactoryQuery> { + OwnedDeps { + storage: MockStorage::default(), + api: MockApi::default(), + querier: MockQuerier::::new(&[]), + custom_query_type: PhantomData::, + } + } +} diff --git a/contracts/external/cw-abc/src/msg.rs b/contracts/external/cw-abc/src/msg.rs index 7aac7d1b4..c1c30a1ca 100644 --- a/contracts/external/cw-abc/src/msg.rs +++ b/contracts/external/cw-abc/src/msg.rs @@ -1,8 +1,7 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Decimal, Uint128, Decimal as StdDecimal}; - -use crate::abc::{CommonsPhaseConfig, CurveType, MinMax, ReserveToken, SupplyToken}; +use cosmwasm_std::{Addr, Decimal, Decimal as StdDecimal, Uint128}; +use crate::abc::{CommonsPhase, CommonsPhaseConfig, CurveType, MinMax, ReserveToken, SupplyToken}; #[cw_serde] pub struct InstantiateMsg { @@ -16,18 +15,44 @@ pub struct InstantiateMsg { pub curve_type: CurveType, // Hatch configuration information - pub phase_config: CommonsPhaseConfig, + pub phase_config: CommonsPhaseConfig, + + // Hatcher allowlist + pub hatcher_allowlist: Option>, } +/// Update the phase configurations. +/// These can only be called by the admin and only before or during each phase +#[cw_serde] +pub enum UpdatePhaseConfigMsg { + /// Update the hatch phase configuration + Hatch { + initial_raise: Option, + initial_allocation_ratio: Option, + }, + /// Update the open phase configuration + Open { + exit_tax: Option, + reserve_ratio: Option, + }, + /// Update the closed phase configuration + Closed {}, +} #[cw_ownable::cw_ownable_execute] #[cw_serde] +#[cfg_attr(feature = "boot", derive(boot_core::ExecuteFns))] pub enum ExecuteMsg { /// Buy will attempt to purchase as many supply tokens as possible. /// You must send only reserve tokens in that message + #[payable] Buy {}, - /// Implements CW20. Burn is a base message to destroy tokens forever - Burn { amount: Uint128 }, + /// Burn is a base message to destroy tokens forever + #[payable] + Burn {}, + /// Donate will add reserve tokens to the funding pool + #[payable] + Donate {}, /// Update the hatch phase allowlist UpdateHatchAllowlist { to_add: Vec, @@ -35,15 +60,13 @@ pub enum ExecuteMsg { }, /// Update the hatch phase configuration /// This can only be called by the admin and only during the hatch phase - UpdateHatchConfig { - initial_raise: Option, - initial_allocation_ratio: Option, - }, + UpdatePhaseConfig(UpdatePhaseConfigMsg), } #[cw_ownable::cw_ownable_query] #[cw_serde] #[derive(QueryResponses)] +#[cfg_attr(feature = "boot", derive(boot_core::QueryFns))] pub enum QueryMsg { /// Returns the reserve and supply quantities, as well as the spot price to buy 1 token /// Returns [`CurveInfoResponse`] @@ -52,7 +75,21 @@ pub enum QueryMsg { /// Returns the current phase configuration /// Returns [`CommonsPhaseConfigResponse`] #[returns(CommonsPhaseConfigResponse)] - PhaseConfig {} + PhaseConfig {}, + /// Returns a list of the donors and their donations + /// Returns [`DonationsResponse`] + #[returns(DonationsResponse)] + Donations { + start_after: Option, + limit: Option, + }, + /// List the hatchers and their contributions + /// Returns [`HatchersResponse`] + #[returns(HatchersResponse)] + Hatchers { + start_after: Option, + limit: Option, + }, } #[cw_serde] @@ -69,8 +106,29 @@ pub struct CurveInfoResponse { pub reserve_denom: String, } +#[cw_serde] +pub struct HatcherAllowlistResponse { + // hatcher allowlist + pub allowlist: Option>, +} + #[cw_serde] pub struct CommonsPhaseConfigResponse { // the phase configuration - pub phase_config: CommonsPhaseConfig, + pub phase_config: CommonsPhaseConfig, + + // current phase + pub phase: CommonsPhase, +} + +#[cw_serde] +pub struct DonationsResponse { + // the donators mapped to their donation in the reserve token + pub donations: Vec<(Addr, Uint128)>, +} + +#[cw_serde] +pub struct HatchersResponse { + // the hatchers mapped to their contribution in the reserve token + pub hatchers: Vec<(Addr, Uint128)>, } diff --git a/contracts/external/cw-abc/src/queries.rs b/contracts/external/cw-abc/src/queries.rs index d6c655f62..c6d12117b 100644 --- a/contracts/external/cw-abc/src/queries.rs +++ b/contracts/external/cw-abc/src/queries.rs @@ -1,8 +1,11 @@ -use cosmwasm_std::{Deps, StdResult}; -use token_bindings::TokenFactoryQuery; use crate::abc::CurveFn; -use crate::msg::{CommonsPhaseConfigResponse, CurveInfoResponse}; -use crate::state::{CURVE_STATE, CurveState}; +use crate::msg::{ + CommonsPhaseConfigResponse, CurveInfoResponse, DonationsResponse, HatchersResponse, +}; +use crate::state::{CurveState, CURVE_STATE, DONATIONS, HATCHERS, PHASE, PHASE_CONFIG}; +use cosmwasm_std::{Deps, Order, QuerierWrapper, StdResult}; +use std::ops::Deref; +use token_bindings::TokenFactoryQuery; /// Get the current state of the curve pub fn query_curve_info( @@ -31,15 +34,15 @@ pub fn query_curve_info( } /// Load and return the phase config -/// TODO: the allowlist will need to paged... should it be separate? pub fn query_phase_config(deps: Deps) -> StdResult { - let phase_config = crate::state::PHASE_CONFIG.load(deps.storage)?; + let phase = PHASE.load(deps.storage)?; + let phase_config = PHASE_CONFIG.load(deps.storage)?; Ok(CommonsPhaseConfigResponse { - phase_config + phase_config, + phase, }) } - // // TODO, maybe we don't need this // pub fn get_denom( // deps: Deps, @@ -53,3 +56,49 @@ pub fn query_phase_config(deps: Deps) -> StdResult, + start_aftor: Option, + limit: Option, +) -> StdResult { + let donations = cw_paginate::paginate_map( + Deps { + storage: deps.storage, + api: deps.api, + querier: QuerierWrapper::new(deps.querier.deref()), + }, + &DONATIONS, + start_aftor + .map(|addr| deps.api.addr_validate(&addr)) + .transpose()? + .as_ref(), + limit, + Order::Descending, + )?; + + Ok(DonationsResponse { donations }) +} + +pub fn query_hatchers( + deps: Deps, + start_aftor: Option, + limit: Option, +) -> StdResult { + let hatchers = cw_paginate::paginate_map( + Deps { + storage: deps.storage, + api: deps.api, + querier: QuerierWrapper::new(deps.querier.deref()), + }, + &HATCHERS, + start_aftor + .map(|addr| deps.api.addr_validate(&addr)) + .transpose()? + .as_ref(), + limit, + Order::Descending, + )?; + + Ok(HatchersResponse { hatchers }) +} diff --git a/contracts/external/cw-abc/src/state.rs b/contracts/external/cw-abc/src/state.rs index 3006501fb..cb885cc53 100644 --- a/contracts/external/cw-abc/src/state.rs +++ b/contracts/external/cw-abc/src/state.rs @@ -1,9 +1,9 @@ -use std::collections::HashSet; use cosmwasm_schema::cw_serde; +use std::collections::HashSet; +use crate::abc::{CommonsPhase, CommonsPhaseConfig, CurveType}; use cosmwasm_std::{Addr, Uint128}; -use cw_storage_plus::Item; -use crate::abc::{ CommonsPhaseConfig, CurveType, CommonsPhase}; +use cw_storage_plus::{Item, Map}; use crate::curves::DecimalPlaces; @@ -43,13 +43,20 @@ pub const CURVE_TYPE: Item = Item::new("curve_type"); /// The denom used for the supply token pub const SUPPLY_DENOM: Item = Item::new("denom"); +/// Hatcher phase allowlist +/// TODO: we could use the keys for the [`HATCHERS`] map instead setting them to 0 at the beginning, though existing hatchers would not be able to be removed +pub static HATCHER_ALLOWLIST: Item> = Item::new("hatch_allowlist"); + /// Keep track of who has contributed to the hatch phase -/// TODO: cw-set? -pub static HATCHERS: Item> = Item::new("hatchers"); +/// TODO: cw-set? This should be a map because in the open-phase we need to be able +/// to ascertain the amount contributed by a user +pub static HATCHERS: Map<&Addr, Uint128> = Map::new("hatchers"); + +/// Keep track of the donated amounts per user +pub static DONATIONS: Map<&Addr, Uint128> = Map::new("donations"); /// The phase configuration of the Augmented Bonding Curve -pub static PHASE_CONFIG: Item> = Item::new("phase_config"); +pub static PHASE_CONFIG: Item = Item::new("phase_config"); /// The phase state of the Augmented Bonding Curve pub static PHASE: Item = Item::new("phase"); -