diff --git a/CHANGELOG.md b/CHANGELOG.md index da97387ee..33ee2490f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added Expiry Enum [(#419)](https://github.com/andromedaprotocol/andromeda-core/pull/419) - Added Conditional Splitter [(#441)](https://github.com/andromedaprotocol/andromeda-core/pull/441) - Validator Staking: Added the option to set an amount while unstaking [(#458)](https://github.com/andromedaprotocol/andromeda-core/pull/458) +- Added Curve ADO [(#515)](https://github.com/andromedaprotocol/andromeda-core/pull/515) - Set Amount Splitter [(#507)](https://github.com/andromedaprotocol/andromeda-core/pull/507) - Added String Storage ADO [(#512)](https://github.com/andromedaprotocol/andromeda-core/pull/512) - Boolean Storage ADO [(#513)](https://github.com/andromedaprotocol/andromeda-core/pull/513) diff --git a/Cargo.lock b/Cargo.lock index a64b920ef..fae502308 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -226,6 +226,22 @@ dependencies = [ "cw721 0.18.0", ] +[[package]] +name = "andromeda-curve" +version = "1.0.0" +dependencies = [ + "andromeda-app", + "andromeda-modules", + "andromeda-std", + "andromeda-testing", + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "test-case", +] + [[package]] name = "andromeda-cw20" version = "2.0.5" @@ -4304,6 +4320,39 @@ dependencies = [ "walkdir", ] +[[package]] +name = "test-case" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8" +dependencies = [ + "test-case-macros", +] + +[[package]] +name = "test-case-core" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "test-case-macros" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", + "test-case-core", +] + [[package]] name = "tests-integration" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index bb10198d0..98347e176 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,3 +56,4 @@ semver = "1.0.0" enum-repr = "0.2.6" cw-multi-test = { version = "1.0.0", features = ["cosmwasm_1_2"] } serde = { version = "1.0.127" } +test-case = { version = "3.3.1" } diff --git a/contracts/modules/andromeda-curve/.cargo/config b/contracts/modules/andromeda-curve/.cargo/config new file mode 100644 index 000000000..336b618a1 --- /dev/null +++ b/contracts/modules/andromeda-curve/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --example schema" diff --git a/contracts/modules/andromeda-curve/Cargo.toml b/contracts/modules/andromeda-curve/Cargo.toml new file mode 100644 index 000000000..5a540b487 --- /dev/null +++ b/contracts/modules/andromeda-curve/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "andromeda-curve" +version = "1.0.0" +edition = "2021" +rust-version = "1.75.0" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] +testing = ["cw-multi-test", "andromeda-testing"] + + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-storage-plus = { workspace = true } +cw-utils = { workspace = true } +test-case = { workspace = true } + +andromeda-std = { workspace = true, features = [] } +andromeda-modules = { workspace = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +cw-multi-test = { workspace = true, optional = true } +andromeda-testing = { workspace = true, optional = true } + +[dev-dependencies] +andromeda-app = { workspace = true } diff --git a/contracts/modules/andromeda-curve/examples/schema.rs b/contracts/modules/andromeda-curve/examples/schema.rs new file mode 100644 index 000000000..a54e619ad --- /dev/null +++ b/contracts/modules/andromeda-curve/examples/schema.rs @@ -0,0 +1,10 @@ +use andromeda_modules::curve::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use cosmwasm_schema::write_api; +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + + } +} diff --git a/contracts/modules/andromeda-curve/src/contract.rs b/contracts/modules/andromeda-curve/src/contract.rs new file mode 100644 index 000000000..8a41998be --- /dev/null +++ b/contracts/modules/andromeda-curve/src/contract.rs @@ -0,0 +1,213 @@ +#[cfg(not(feature = "library"))] +use crate::state::{ + CURVE_CONFIG, DEFAULT_CONSTANT_VALUE, DEFAULT_MULTIPLE_VARIABLE_VALUE, RESTRICTION, +}; +use andromeda_modules::curve::{ + CurveConfig, CurveId, CurveRestriction, ExecuteMsg, GetCurveConfigResponse, + GetPlotYFromXResponse, GetRestrictionResponse, InstantiateMsg, QueryMsg, +}; +use andromeda_std::{ + ado_base::{InstantiateMsg as BaseInstantiateMsg, MigrateMsg}, + ado_contract::ADOContract, + common::{actions::call_action, context::ExecuteContext, encode_binary}, + error::ContractError, +}; + +use cosmwasm_std::{ + ensure, entry_point, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Response, Storage, +}; + +use cw_utils::nonpayable; + +// version info for migration info +const CONTRACT_NAME: &str = "crates.io:andromeda-curve"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + let resp = ADOContract::default().instantiate( + deps.storage, + env, + deps.api, + &deps.querier, + info, + BaseInstantiateMsg { + ado_type: CONTRACT_NAME.to_string(), + ado_version: CONTRACT_VERSION.to_string(), + kernel_address: msg.kernel_address, + owner: msg.owner, + }, + )?; + + msg.curve_config.validate()?; + + RESTRICTION.save(deps.storage, &msg.restriction)?; + CURVE_CONFIG.save(deps.storage, &msg.curve_config)?; + + Ok(resp) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + let ctx = ExecuteContext::new(deps, info, env); + match msg { + ExecuteMsg::AMPReceive(pkt) => { + ADOContract::default().execute_amp_receive(ctx, pkt, handle_execute) + } + _ => handle_execute(ctx, msg), + } +} + +fn handle_execute(mut ctx: ExecuteContext, msg: ExecuteMsg) -> Result { + let action_response = call_action( + &mut ctx.deps, + &ctx.info, + &ctx.env, + &ctx.amp_ctx, + msg.as_ref(), + )?; + + let res = match msg.clone() { + ExecuteMsg::UpdateCurveConfig { curve_config } => { + execute_update_curve_config(ctx, curve_config) + } + ExecuteMsg::UpdateRestriction { restriction } => { + execute_update_restriction(ctx, restriction) + } + ExecuteMsg::Reset {} => execute_reset(ctx), + _ => ADOContract::default().execute(ctx, msg), + }?; + + Ok(res + .add_submessages(action_response.messages) + .add_attributes(action_response.attributes) + .add_events(action_response.events)) +} + +pub fn execute_update_curve_config( + ctx: ExecuteContext, + curve_config: CurveConfig, +) -> Result { + nonpayable(&ctx.info)?; + let sender = ctx.info.sender.clone(); + ensure!( + has_permission(ctx.deps.storage, &sender)?, + ContractError::Unauthorized {} + ); + + curve_config.validate()?; + CURVE_CONFIG.update(ctx.deps.storage, |_| { + Ok::(curve_config) + })?; + + Ok(Response::new() + .add_attribute("method", "update_curve_config") + .add_attribute("sender", sender)) +} + +pub fn execute_update_restriction( + ctx: ExecuteContext, + restriction: CurveRestriction, +) -> Result { + nonpayable(&ctx.info)?; + let sender = ctx.info.sender; + ensure!( + ADOContract::default().is_owner_or_operator(ctx.deps.storage, sender.as_ref())?, + ContractError::Unauthorized {} + ); + RESTRICTION.save(ctx.deps.storage, &restriction)?; + + Ok(Response::new() + .add_attribute("method", "update_restriction") + .add_attribute("sender", sender)) +} + +pub fn execute_reset(ctx: ExecuteContext) -> Result { + nonpayable(&ctx.info)?; + let sender = ctx.info.sender.clone(); + ensure!( + has_permission(ctx.deps.storage, &sender)?, + ContractError::Unauthorized {} + ); + + CURVE_CONFIG.remove(ctx.deps.storage); + + Ok(Response::new().add_attribute("method", "reset")) +} + +pub fn has_permission(storage: &dyn Storage, addr: &Addr) -> Result { + let is_operator = ADOContract::default().is_owner_or_operator(storage, addr.as_str())?; + let allowed = match RESTRICTION.load(storage)? { + CurveRestriction::Private => is_operator, + CurveRestriction::Public => true, + }; + Ok(is_operator || allowed) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { + match msg { + QueryMsg::GetCurveConfig {} => encode_binary(&query_curve_config(deps.storage)?), + QueryMsg::GetRestriction {} => encode_binary(&query_restriction(deps.storage)?), + QueryMsg::GetPlotYFromX { x_value } => { + encode_binary(&query_plot_y_from_x(deps.storage, x_value)?) + } + _ => ADOContract::default().query(deps, env, msg), + } +} + +pub fn query_curve_config(storage: &dyn Storage) -> Result { + let curve_config = CURVE_CONFIG.load(storage)?; + Ok(GetCurveConfigResponse { curve_config }) +} + +pub fn query_restriction(storage: &dyn Storage) -> Result { + let restriction = RESTRICTION.load(storage)?; + Ok(GetRestrictionResponse { restriction }) +} + +pub fn query_plot_y_from_x( + storage: &dyn Storage, + x_value: f64, +) -> Result { + let curve_config = CURVE_CONFIG.load(storage)?; + + let y_value = match curve_config { + CurveConfig::ExpConfig { + curve_id, + base_value, + multiple_variable_value, + constant_value, + } => { + let curve_id_f64 = match curve_id { + CurveId::Growth => 1_f64, + CurveId::Decay => -1_f64, + }; + let base_value_f64 = base_value as f64; + let constant_value_f64 = constant_value.unwrap_or(DEFAULT_CONSTANT_VALUE) as f64; + let multiple_variable_value_f64 = + multiple_variable_value.unwrap_or(DEFAULT_MULTIPLE_VARIABLE_VALUE) as f64; + + (constant_value_f64 + * base_value_f64.powf(curve_id_f64 * multiple_variable_value_f64 * x_value)) + .to_string() + } + }; + + Ok(GetPlotYFromXResponse { y_value }) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + ADOContract::default().migrate(deps, CONTRACT_NAME, CONTRACT_VERSION) +} diff --git a/contracts/modules/andromeda-curve/src/lib.rs b/contracts/modules/andromeda-curve/src/lib.rs new file mode 100644 index 000000000..bcbc58bb5 --- /dev/null +++ b/contracts/modules/andromeda-curve/src/lib.rs @@ -0,0 +1,7 @@ +pub mod contract; +pub mod state; +#[cfg(test)] +pub mod testing; + +#[cfg(all(not(target_arch = "wasm32"), feature = "testing"))] +pub mod mock; diff --git a/contracts/modules/andromeda-curve/src/mock.rs b/contracts/modules/andromeda-curve/src/mock.rs new file mode 100644 index 000000000..0b84cee93 --- /dev/null +++ b/contracts/modules/andromeda-curve/src/mock.rs @@ -0,0 +1,174 @@ +#![cfg(all(not(target_arch = "wasm32"), feature = "testing"))] +use crate::contract::{execute, instantiate, query}; +use andromeda_modules::curve::{ + CurveId, CurveRestriction, CurveType, ExecuteMsg, GetConfigurationExpResponse, + GetCurveTypeResponse, GetPlotYFromXResponse, GetRestrictionResponse, InstantiateMsg, QueryMsg, +}; +use andromeda_std::ado_base::rates::{Rate, RatesMessage}; +use andromeda_testing::mock::MockApp; +use andromeda_testing::{ + mock_ado, + mock_contract::{ExecuteResult, MockADO, MockContract}, +}; +use cosmwasm_std::{Addr, Coin, Empty}; +use cw_multi_test::{Contract, ContractWrapper, Executor}; + +pub struct MockCurve(Addr); +mock_ado!(MockCurve, ExecuteMsg, QueryMsg); + +impl MockCurve { + pub fn instantiate( + code_id: u64, + sender: Addr, + app: &mut MockApp, + kernel_address: String, + owner: Option, + curve_type: CurveType, + restriction: CurveRestriction, + ) -> MockCurve { + let msg = mock_curve_instantiate_msg(kernel_address, owner, curve_type, restriction); + let addr = app + .instantiate_contract( + code_id, + sender.clone(), + &msg, + &[], + "Curve Contract", + Some(sender.to_string()), + ) + .unwrap(); + MockCurve(Addr::unchecked(addr)) + } + + pub fn execute_update_curve_type( + &self, + app: &mut MockApp, + sender: Addr, + curve_type: CurveType, + funds: Option, + ) -> ExecuteResult { + let msg = mock_execute_update_curve_type_msg(curve_type); + if let Some(funds) = funds { + app.execute_contract(sender, self.addr().clone(), &msg, &[funds]) + } else { + app.execute_contract(sender, self.addr().clone(), &msg, &[]) + } + } + + pub fn execute_update_restriction( + &self, + app: &mut MockApp, + sender: Addr, + restriction: CurveRestriction, + funds: Option, + ) -> ExecuteResult { + let msg = mock_execute_update_restriction_msg(restriction); + if let Some(funds) = funds { + app.execute_contract(sender, self.addr().clone(), &msg, &[funds]) + } else { + app.execute_contract(sender, self.addr().clone(), &msg, &[]) + } + } + + pub fn execute_configure_exponential( + &self, + app: &mut MockApp, + sender: Addr, + curve_id: CurveId, + base_value: u64, + multiple_variable_value: Option, + constant_value: Option, + funds: Option, + ) -> ExecuteResult { + let msg = mock_execute_configure_exponential_msg( + curve_id, + base_value, + multiple_variable_value, + constant_value, + ); + if let Some(funds) = funds { + app.execute_contract(sender, self.addr().clone(), &msg, &[funds]) + } else { + app.execute_contract(sender, self.addr().clone(), &msg, &[]) + } + } + + pub fn execute_reset( + &self, + app: &mut MockApp, + sender: Addr, + funds: Option, + ) -> ExecuteResult { + let msg = ExecuteMsg::Reset {}; + if let Some(funds) = funds { + app.execute_contract(sender, self.addr().clone(), &msg, &[funds]) + } else { + app.execute_contract(sender, self.addr().clone(), &msg, &[]) + } + } + + pub fn query_restriction(&self, app: &mut MockApp) -> GetRestrictionResponse { + let msg = QueryMsg::GetRestriction {}; + let res: GetRestrictionResponse = self.query(app, msg); + res + } + + pub fn query_curve_type(&self, app: &mut MockApp) -> GetCurveTypeResponse { + let msg = QueryMsg::GetCurveType {}; + let res: GetCurveTypeResponse = self.query(app, msg); + res + } + + pub fn query_configuration_exp(&self, app: &mut MockApp) -> GetConfigurationExpResponse { + let msg = QueryMsg::GetConfigurationExp {}; + let res: GetConfigurationExpResponse = self.query(app, msg); + res + } + + pub fn query_plot_y_from_x(&self, app: &mut MockApp, x_value: f64) -> GetPlotYFromXResponse { + let msg = QueryMsg::GetPlotYFromX { x_value }; + let res: GetPlotYFromXResponse = self.query(app, msg); + res + } +} + +pub fn mock_andromeda_curve() -> Box> { + let contract = ContractWrapper::new_with_empty(execute, instantiate, query); + Box::new(contract) +} + +pub fn mock_curve_instantiate_msg( + kernel_address: String, + owner: Option, + curve_type: CurveType, + restriction: CurveRestriction, +) -> InstantiateMsg { + InstantiateMsg { + kernel_address, + owner, + curve_type, + restriction, + } +} + +pub fn mock_execute_update_curve_type_msg(curve_type: CurveType) -> ExecuteMsg { + ExecuteMsg::UpdateCurveType { curve_type } +} + +pub fn mock_execute_update_restriction_msg(restriction: CurveRestriction) -> ExecuteMsg { + ExecuteMsg::UpdateRestriction { restriction } +} + +pub fn mock_execute_configure_exponential_msg( + curve_id: CurveId, + base_value: u64, + multiple_variable_value: Option, + constant_value: Option, +) -> ExecuteMsg { + ExecuteMsg::ConfigureExponential { + curve_id, + base_value, + multiple_variable_value, + constant_value, + } +} diff --git a/contracts/modules/andromeda-curve/src/state.rs b/contracts/modules/andromeda-curve/src/state.rs new file mode 100644 index 000000000..34967e01c --- /dev/null +++ b/contracts/modules/andromeda-curve/src/state.rs @@ -0,0 +1,8 @@ +use andromeda_modules::curve::{CurveConfig, CurveRestriction}; +use cw_storage_plus::Item; + +pub const CURVE_CONFIG: Item = Item::new("curve_config"); +pub const RESTRICTION: Item = Item::new("curve_restriction"); + +pub const DEFAULT_MULTIPLE_VARIABLE_VALUE: u64 = 1; +pub const DEFAULT_CONSTANT_VALUE: u64 = 1; diff --git a/contracts/modules/andromeda-curve/src/testing/mock.rs b/contracts/modules/andromeda-curve/src/testing/mock.rs new file mode 100644 index 000000000..1d7ed9b1c --- /dev/null +++ b/contracts/modules/andromeda-curve/src/testing/mock.rs @@ -0,0 +1,105 @@ +use andromeda_modules::curve::{ + CurveConfig, CurveRestriction, ExecuteMsg, GetCurveConfigResponse, GetPlotYFromXResponse, + GetRestrictionResponse, InstantiateMsg, QueryMsg, +}; +use andromeda_std::{ + error::ContractError, + testing::mock_querier::{mock_dependencies_custom, WasmMockQuerier, MOCK_KERNEL_CONTRACT}, +}; +use cosmwasm_std::{ + from_json, + testing::{mock_env, mock_info, MockApi, MockStorage}, + Deps, DepsMut, MessageInfo, OwnedDeps, Response, +}; + +use crate::contract::{execute, instantiate, query}; + +pub type MockDeps = OwnedDeps; + +pub fn proper_initialization( + curve_config: CurveConfig, + restriction: CurveRestriction, +) -> (MockDeps, MessageInfo) { + let mut deps = mock_dependencies_custom(&[]); + let info = mock_info("creator", &[]); + let msg = InstantiateMsg { + kernel_address: MOCK_KERNEL_CONTRACT.to_string(), + owner: None, + curve_config, + restriction, + }; + let env = mock_env(); + let res = instantiate(deps.as_mut(), env, info.clone(), msg).unwrap(); + assert_eq!(0, res.messages.len()); + (deps, info) +} + +pub fn error_initialization( + curve_config: CurveConfig, + restriction: CurveRestriction, +) -> ContractError { + let mut deps = mock_dependencies_custom(&[]); + let info = mock_info("creator", &[]); + let msg = InstantiateMsg { + kernel_address: MOCK_KERNEL_CONTRACT.to_string(), + owner: None, + curve_config, + restriction, + }; + let env = mock_env(); + let res = instantiate(deps.as_mut(), env, info.clone(), msg).unwrap_err(); + res +} + +pub fn update_curve_config( + deps: DepsMut<'_>, + curve_config: CurveConfig, + sender: &str, +) -> Result { + let msg = ExecuteMsg::UpdateCurveConfig { curve_config }; + let info = mock_info(sender, &[]); + execute(deps, mock_env(), info, msg) +} + +pub fn reset(deps: DepsMut<'_>, sender: &str) -> Result { + let msg = ExecuteMsg::Reset {}; + let info = mock_info(sender, &[]); + execute(deps, mock_env(), info, msg) +} + +pub fn update_restriction( + deps: DepsMut<'_>, + restriction: CurveRestriction, + sender: &str, +) -> Result { + let msg = ExecuteMsg::UpdateRestriction { restriction }; + let info = mock_info(sender, &[]); + execute(deps, mock_env(), info, msg) +} + +pub fn query_restriction(deps: Deps) -> Result { + let res = query(deps, mock_env(), QueryMsg::GetRestriction {}); + match res { + Ok(res) => Ok(from_json(res).unwrap()), + Err(err) => Err(err), + } +} + +pub fn query_curve_config(deps: Deps) -> Result { + let res = query(deps, mock_env(), QueryMsg::GetCurveConfig {}); + match res { + Ok(res) => Ok(from_json(res).unwrap()), + Err(err) => Err(err), + } +} + +pub fn query_plot_y_from_x( + deps: Deps, + x_value: f64, +) -> Result { + let res = query(deps, mock_env(), QueryMsg::GetPlotYFromX { x_value }); + match res { + Ok(res) => Ok(from_json(res).unwrap()), + Err(err) => Err(err), + } +} diff --git a/contracts/modules/andromeda-curve/src/testing/mod.rs b/contracts/modules/andromeda-curve/src/testing/mod.rs new file mode 100644 index 000000000..3bfda2893 --- /dev/null +++ b/contracts/modules/andromeda-curve/src/testing/mod.rs @@ -0,0 +1,2 @@ +mod mock; +mod tests; diff --git a/contracts/modules/andromeda-curve/src/testing/tests.rs b/contracts/modules/andromeda-curve/src/testing/tests.rs new file mode 100644 index 000000000..2bb3cd31a --- /dev/null +++ b/contracts/modules/andromeda-curve/src/testing/tests.rs @@ -0,0 +1,195 @@ +use super::mock::{ + error_initialization, proper_initialization, query_curve_config, query_plot_y_from_x, + query_restriction, reset, update_curve_config, update_restriction, +}; +use andromeda_modules::curve::{CurveConfig, CurveId, CurveRestriction}; +use andromeda_std::error::ContractError; +use cosmwasm_std::StdError; +use test_case::test_case; + +#[test] +fn test_instantiation() { + proper_initialization( + CurveConfig::ExpConfig { + curve_id: CurveId::Growth, + base_value: 2, + multiple_variable_value: None, + constant_value: None, + }, + CurveRestriction::Private, + ); +} + +#[test] +fn test_update_restriction() { + let (mut deps, info) = proper_initialization( + CurveConfig::ExpConfig { + curve_id: CurveId::Growth, + base_value: 2, + multiple_variable_value: None, + constant_value: None, + }, + CurveRestriction::Private, + ); + let external_user = "external".to_string(); + let res = + update_restriction(deps.as_mut(), CurveRestriction::Private, &external_user).unwrap_err(); + assert_eq!(res, ContractError::Unauthorized {}); + + update_restriction( + deps.as_mut(), + CurveRestriction::Public, + info.sender.as_ref(), + ) + .unwrap(); + let restriction = query_restriction(deps.as_ref()).unwrap().restriction; + assert_eq!(restriction, CurveRestriction::Public); +} + +#[test] +fn test_reset() { + let (mut deps, info) = proper_initialization( + CurveConfig::ExpConfig { + curve_id: CurveId::Growth, + base_value: 2, + multiple_variable_value: None, + constant_value: None, + }, + CurveRestriction::Private, + ); + + reset(deps.as_mut(), info.sender.as_ref()).unwrap(); + let err_res = query_curve_config(deps.as_ref()).unwrap_err(); + assert_eq!(err_res, ContractError::Std(StdError::NotFound { kind: "type: andromeda_modules::curve::CurveConfig; key: [63, 75, 72, 76, 65, 5F, 63, 6F, 6E, 66, 69, 67]".to_string() })); +} + +#[test] +fn test_update_curve_config() { + let (mut deps, info) = proper_initialization( + CurveConfig::ExpConfig { + curve_id: CurveId::Growth, + base_value: 2, + multiple_variable_value: None, + constant_value: None, + }, + CurveRestriction::Private, + ); + update_curve_config( + deps.as_mut(), + CurveConfig::ExpConfig { + curve_id: CurveId::Growth, + base_value: 4, + multiple_variable_value: None, + constant_value: Some(2), + }, + info.sender.as_ref(), + ) + .unwrap(); + + let res = query_curve_config(deps.as_ref()).unwrap().curve_config; + assert_eq!( + res, + CurveConfig::ExpConfig { + curve_id: CurveId::Growth, + base_value: 4, + multiple_variable_value: None, + constant_value: Some(2), + } + ); +} + +#[test] +fn test_query_curve_config() { + let (deps, _info) = proper_initialization( + CurveConfig::ExpConfig { + curve_id: CurveId::Growth, + base_value: 2, + multiple_variable_value: None, + constant_value: None, + }, + CurveRestriction::Private, + ); + let res = query_curve_config(deps.as_ref()).unwrap().curve_config; + assert_eq!( + res, + CurveConfig::ExpConfig { + curve_id: CurveId::Growth, + base_value: 2, + multiple_variable_value: None, + constant_value: None, + } + ); +} + +#[test] +fn test_query_curve_config_base_is_0() { + let err_res = error_initialization( + CurveConfig::ExpConfig { + curve_id: CurveId::Growth, + base_value: 0, + multiple_variable_value: None, + constant_value: None, + }, + CurveRestriction::Private, + ); + assert_eq!( + err_res, + ContractError::CustomError { + msg: "Base Value must be bigger than Zero".to_string() + } + ); +} + +#[test_case(2_f64, "4".to_string() ; "exp(2, 2)")] +#[test_case(3_f64, "8".to_string() ; "exp(2, 3)")] +#[test_case(4_f64, "16".to_string() ; "exp(2, 4)")] +fn test_query_plot_y_from_x_base_2_growth(input_x: f64, expected_y: String) { + let (deps, _info) = proper_initialization( + CurveConfig::ExpConfig { + curve_id: CurveId::Growth, + base_value: 2, + multiple_variable_value: None, + constant_value: None, + }, + CurveRestriction::Private, + ); + + let res = query_plot_y_from_x(deps.as_ref(), input_x).unwrap().y_value; + assert_eq!(res, expected_y); +} + +#[test_case(2_f64, "9".to_string() ; "exp(3, 2)")] +#[test_case(3_f64, "27".to_string() ; "exp(3, 3)")] +#[test_case(4_f64, "81".to_string() ; "exp(3, 4)")] +fn test_query_plot_y_from_x_base_3_growth(input_x: f64, expected_y: String) { + let (deps, _info) = proper_initialization( + CurveConfig::ExpConfig { + curve_id: CurveId::Growth, + base_value: 3, + multiple_variable_value: None, + constant_value: None, + }, + CurveRestriction::Private, + ); + + let res = query_plot_y_from_x(deps.as_ref(), input_x).unwrap().y_value; + assert_eq!(res, expected_y); +} + +#[test_case(2_f64, "0.25".to_string() ; "exp(1/2, 2)")] +#[test_case(3_f64, "0.125".to_string() ; "exp(1/2, 3)")] +#[test_case(4_f64, "0.0625".to_string() ; "exp(1/2, 4)")] +fn test_query_plot_y_from_x_base_2_decay(input_x: f64, expected_y: String) { + let (deps, _info) = proper_initialization( + CurveConfig::ExpConfig { + curve_id: CurveId::Decay, + base_value: 2, + multiple_variable_value: None, + constant_value: None, + }, + CurveRestriction::Private, + ); + + let res = query_plot_y_from_x(deps.as_ref(), input_x).unwrap().y_value; + assert_eq!(res, expected_y); +} diff --git a/packages/andromeda-modules/src/curve.rs b/packages/andromeda-modules/src/curve.rs new file mode 100644 index 000000000..cfab24592 --- /dev/null +++ b/packages/andromeda-modules/src/curve.rs @@ -0,0 +1,115 @@ +use andromeda_std::{andr_exec, andr_instantiate, andr_query, error::ContractError}; +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::ensure; + +#[andr_instantiate] +#[cw_serde] +pub struct InstantiateMsg { + pub curve_config: CurveConfig, + pub restriction: CurveRestriction, +} + +#[cw_serde] +pub enum CurveRestriction { + Private, + Public, +} + +#[cw_serde] +pub enum CurveId { + Growth, + Decay, +} + +#[cw_serde] +pub enum CurveConfig { + ExpConfig { + curve_id: CurveId, + base_value: u64, + multiple_variable_value: Option, + constant_value: Option, + }, +} + +impl CurveConfig { + pub fn validate(&self) -> Result<(), ContractError> { + match self { + CurveConfig::ExpConfig { + curve_id: _, + base_value, + multiple_variable_value: _, + constant_value: _, + } => { + ensure!( + *base_value != 0, + ContractError::CustomError { + msg: "Base Value must be bigger than Zero".to_string() + } + ); + } + } + Ok(()) + } +} + +#[andr_exec] +#[cw_serde] +pub enum ExecuteMsg { + UpdateCurveConfig { curve_config: CurveConfig }, + UpdateRestriction { restriction: CurveRestriction }, + Reset {}, +} + +#[andr_query] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(GetCurveConfigResponse)] + GetCurveConfig {}, + #[returns(GetRestrictionResponse)] + GetRestriction {}, + #[returns(GetPlotYFromXResponse)] + GetPlotYFromX { x_value: f64 }, +} + +#[cw_serde] +pub struct GetCurveConfigResponse { + pub curve_config: CurveConfig, +} + +#[cw_serde] +pub struct GetPlotYFromXResponse { + pub y_value: String, +} + +#[cw_serde] +pub struct GetRestrictionResponse { + pub restriction: CurveRestriction, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_validate_valid() { + let curve_config = CurveConfig::ExpConfig { + curve_id: CurveId::Growth, + base_value: 4, + multiple_variable_value: None, + constant_value: None, + }; + assert!(curve_config.validate().is_ok()); + } + + #[test] + fn test_validate_invalid() { + let curve_config = CurveConfig::ExpConfig { + curve_id: CurveId::Growth, + base_value: 0, + multiple_variable_value: None, + constant_value: None, + }; + assert!(curve_config.validate().is_err()); + } +} diff --git a/packages/andromeda-modules/src/lib.rs b/packages/andromeda-modules/src/lib.rs index becf45359..6ac3bfda7 100644 --- a/packages/andromeda-modules/src/lib.rs +++ b/packages/andromeda-modules/src/lib.rs @@ -1,4 +1,5 @@ pub mod address_list; +pub mod curve; pub mod date_time; pub mod rates; pub mod shunting;