diff --git a/Cargo.lock b/Cargo.lock index 1bae864c..187e2d9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -774,6 +774,7 @@ dependencies = [ "cosmwasm-storage", "cw-multi-test 0.20.0", "cw-storage-plus", + "neutron-sdk", ] [[package]] diff --git a/contracts/token/src/contract.rs b/contracts/token/src/contract.rs new file mode 100644 index 00000000..126bc685 --- /dev/null +++ b/contracts/token/src/contract.rs @@ -0,0 +1,142 @@ +use cosmwasm_std::{ + attr, ensure_eq, ensure_ne, entry_point, to_json_binary, Binary, Deps, DepsMut, Env, + MessageInfo, Reply, Response, SubMsg, Uint128, +}; + +use lido_staking_base::{ + helpers::answer::{attr_coin, response}, + msg::token::{ConfigResponse, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}, + state::token::{CORE_ADDRESS, DENOM}, +}; +use neutron_sdk::{ + bindings::{msg::NeutronMsg, query::NeutronQuery}, + query::token_factory::query_full_denom, +}; + +use crate::error::{ContractError, ContractResult}; + +pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); +pub const CREATE_DENOM_REPLY_ID: u64 = 1; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> ContractResult> { + cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + let core = deps.api.addr_validate(&msg.core_address)?; + CORE_ADDRESS.save(deps.storage, &core)?; + + DENOM.save(deps.storage, &msg.subdenom)?; + let create_denom_msg = SubMsg::reply_on_success( + NeutronMsg::submit_create_denom(&msg.subdenom), + CREATE_DENOM_REPLY_ID, + ); + + Ok(response( + "instantiate", + CONTRACT_NAME, + [attr("core_address", core), attr("subdenom", msg.subdenom)], + ) + .add_submessage(create_denom_msg)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> ContractResult> { + let core = CORE_ADDRESS.load(deps.storage)?; + ensure_eq!(info.sender, core, ContractError::Unauthorized); + + match msg { + ExecuteMsg::Mint { amount, receiver } => mint(deps, amount, receiver), + ExecuteMsg::Burn {} => burn(deps, info), + } +} + +fn mint( + deps: DepsMut, + amount: Uint128, + receiver: String, +) -> ContractResult> { + ensure_ne!(amount, Uint128::zero(), ContractError::NothingToMint); + + let denom = DENOM.load(deps.storage)?; + let mint_msg = NeutronMsg::submit_mint_tokens(&denom, amount, &receiver); + + Ok(response( + "execute-mint", + CONTRACT_NAME, + [ + attr_coin("amount", amount, denom), + attr("receiver", receiver), + ], + ) + .add_message(mint_msg)) +} + +fn burn(deps: DepsMut, info: MessageInfo) -> ContractResult> { + let denom = DENOM.load(deps.storage)?; + let amount = cw_utils::must_pay(&info, &denom)?; + + let burn_msg = NeutronMsg::submit_burn_tokens(&denom, amount); + + Ok(response( + "execute-burn", + CONTRACT_NAME, + [attr_coin("amount", amount, denom)], + ) + .add_message(burn_msg)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> ContractResult { + match msg { + QueryMsg::Config {} => { + let core_address = CORE_ADDRESS.load(deps.storage)?.into_string(); + let denom = DENOM.load(deps.storage)?; + Ok(to_json_binary(&ConfigResponse { + core_address, + denom, + })?) + } + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate( + _deps: DepsMut, + _env: Env, + _msg: MigrateMsg, +) -> ContractResult> { + Ok(Response::new()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply( + deps: DepsMut, + env: Env, + msg: Reply, +) -> ContractResult> { + match msg.id { + CREATE_DENOM_REPLY_ID => { + let subdenom = DENOM.load(deps.storage)?; + let full_denom = query_full_denom(deps.as_ref(), env.contract.address, subdenom)?; + DENOM.save(deps.storage, &full_denom.denom)?; + + Ok(response( + "reply-create-denom", + CONTRACT_NAME, + [attr("denom", full_denom.denom)], + )) + } + id => Err(ContractError::UnknownReplyId { id }), + } +} diff --git a/contracts/token/src/error.rs b/contracts/token/src/error.rs new file mode 100644 index 00000000..b3dcb594 --- /dev/null +++ b/contracts/token/src/error.rs @@ -0,0 +1,24 @@ +use cosmwasm_std::StdError; + +#[derive(thiserror::Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("{0}")] + NeutronError(#[from] neutron_sdk::NeutronError), + + #[error("{0}")] + PaymentError(#[from] cw_utils::PaymentError), + + #[error("unauthorized")] + Unauthorized, + + #[error("nothing to mint")] + NothingToMint, + + #[error("unknown reply id: {id}")] + UnknownReplyId { id: u64 }, +} + +pub type ContractResult = Result; diff --git a/contracts/token/src/lib.rs b/contracts/token/src/lib.rs index b8397773..8a8512dd 100644 --- a/contracts/token/src/lib.rs +++ b/contracts/token/src/lib.rs @@ -1,173 +1,5 @@ -use cosmwasm_std::{ - attr, ensure_eq, ensure_ne, to_json_binary, Attribute, Binary, Deps, DepsMut, Env, Event, - MessageInfo, Reply, Response, StdError, SubMsg, Uint128, -}; - -use lido_staking_base::{ - msg::token::{ConfigResponse, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}, - state::token::{CORE_ADDRESS, DENOM}, -}; -use neutron_sdk::{ - bindings::{msg::NeutronMsg, query::NeutronQuery}, - query::token_factory::query_full_denom, -}; - #[cfg(test)] mod tests; -#[derive(thiserror::Error, Debug, PartialEq)] -pub enum ContractError { - #[error("{0}")] - Std(#[from] StdError), - - #[error("{0}")] - NeutronError(#[from] neutron_sdk::NeutronError), - - #[error("{0}")] - PaymentError(#[from] cw_utils::PaymentError), - - #[error("unauthorized")] - Unauthorized, - - #[error("nothing to mint")] - NothingToMint, - - #[error("unknown reply id: {id}")] - UnknownReplyId { id: u64 }, -} - -pub type ContractResult = Result; - -const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); -const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); - -const CREATE_DENOM_REPLY_ID: u64 = 1; - -fn response>( - ty: &str, - attrs: impl IntoIterator, -) -> Response { - Response::new().add_event(Event::new(format!("{}-{}", CONTRACT_NAME, ty)).add_attributes(attrs)) -} - -fn attr_coin( - key: impl Into, - amount: impl std::fmt::Display, - denom: impl std::fmt::Display, -) -> Attribute { - attr(key, format!("{}{}", amount, denom)) -} - -#[cfg_attr(not(feature = "library"), cosmwasm_std::entry_point)] -pub fn instantiate( - deps: DepsMut, - _env: Env, - _info: MessageInfo, - msg: InstantiateMsg, -) -> ContractResult> { - cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - - let core = deps.api.addr_validate(&msg.core_address)?; - CORE_ADDRESS.save(deps.storage, &core)?; - - DENOM.save(deps.storage, &msg.subdenom)?; - let create_denom_msg = SubMsg::reply_on_success( - NeutronMsg::submit_create_denom(&msg.subdenom), - CREATE_DENOM_REPLY_ID, - ); - - Ok(response( - "instantiate", - [attr("core_address", core), attr("subdenom", msg.subdenom)], - ) - .add_submessage(create_denom_msg)) -} - -#[cfg_attr(not(feature = "library"), cosmwasm_std::entry_point)] -pub fn execute( - deps: DepsMut, - _env: Env, - info: MessageInfo, - msg: ExecuteMsg, -) -> ContractResult> { - let core = CORE_ADDRESS.load(deps.storage)?; - ensure_eq!(info.sender, core, ContractError::Unauthorized); - - match msg { - ExecuteMsg::Mint { amount, receiver } => mint(deps, amount, receiver), - ExecuteMsg::Burn {} => burn(deps, info), - } -} - -fn mint( - deps: DepsMut, - amount: Uint128, - receiver: String, -) -> ContractResult> { - ensure_ne!(amount, Uint128::zero(), ContractError::NothingToMint); - - let denom = DENOM.load(deps.storage)?; - let mint_msg = NeutronMsg::submit_mint_tokens(&denom, amount, &receiver); - - Ok(response( - "execute-mint", - [ - attr_coin("amount", amount, denom), - attr("receiver", receiver), - ], - ) - .add_message(mint_msg)) -} - -fn burn(deps: DepsMut, info: MessageInfo) -> ContractResult> { - let denom = DENOM.load(deps.storage)?; - let amount = cw_utils::must_pay(&info, &denom)?; - - let burn_msg = NeutronMsg::submit_burn_tokens(&denom, amount); - - Ok(response("execute-burn", [attr_coin("amount", amount, denom)]).add_message(burn_msg)) -} - -#[cfg_attr(not(feature = "library"), cosmwasm_std::entry_point)] -pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> ContractResult { - match msg { - QueryMsg::Config {} => { - let core_address = CORE_ADDRESS.load(deps.storage)?.into_string(); - let denom = DENOM.load(deps.storage)?; - Ok(to_json_binary(&ConfigResponse { - core_address, - denom, - })?) - } - } -} - -#[cfg_attr(not(feature = "library"), cosmwasm_std::entry_point)] -pub fn migrate( - _deps: DepsMut, - _env: Env, - _msg: MigrateMsg, -) -> ContractResult> { - Ok(Response::new()) -} - -#[cfg_attr(not(feature = "library"), cosmwasm_std::entry_point)] -pub fn reply( - deps: DepsMut, - env: Env, - msg: Reply, -) -> ContractResult> { - match msg.id { - CREATE_DENOM_REPLY_ID => { - let subdenom = DENOM.load(deps.storage)?; - let full_denom = query_full_denom(deps.as_ref(), env.contract.address, subdenom)?; - DENOM.save(deps.storage, &full_denom.denom)?; - - Ok(response( - "reply-create-denom", - [attr("denom", full_denom.denom)], - )) - } - id => Err(ContractError::UnknownReplyId { id }), - } -} +pub mod contract; +pub mod error; diff --git a/contracts/token/src/tests.rs b/contracts/token/src/tests.rs index 3b94f1eb..1ada48d4 100644 --- a/contracts/token/src/tests.rs +++ b/contracts/token/src/tests.rs @@ -4,13 +4,21 @@ use cosmwasm_std::{ to_json_binary, Addr, ContractResult, CosmosMsg, Event, OwnedDeps, Querier, QuerierResult, QueryRequest, Reply, ReplyOn, SubMsgResult, SystemError, Uint128, }; -use lido_staking_base::msg::token::InstantiateMsg; +use lido_staking_base::{ + msg::token::{ConfigResponse, ExecuteMsg, InstantiateMsg, QueryMsg}, + state::token::{CORE_ADDRESS, DENOM}, +}; use neutron_sdk::{ bindings::{msg::NeutronMsg, query::NeutronQuery}, query::token_factory::FullDenomResponse, }; use std::marker::PhantomData; +use crate::{ + contract::{self, CREATE_DENOM_REPLY_ID}, + error::ContractError, +}; + fn mock_dependencies() -> OwnedDeps { OwnedDeps { storage: MockStorage::default(), @@ -23,7 +31,7 @@ fn mock_dependencies() -> OwnedDeps(); - let response = crate::instantiate( + let response = contract::instantiate( deps.as_mut(), mock_env(), mock_info("admin", &[]), @@ -34,15 +42,15 @@ fn instantiate() { ) .unwrap(); - let core = crate::CORE_ADDRESS.load(deps.as_ref().storage).unwrap(); + let core = CORE_ADDRESS.load(deps.as_ref().storage).unwrap(); assert_eq!(core, Addr::unchecked("core")); - let denom = crate::DENOM.load(deps.as_ref().storage).unwrap(); + let denom = DENOM.load(deps.as_ref().storage).unwrap(); assert_eq!(denom, "subdenom"); assert_eq!(response.messages.len(), 1); assert_eq!(response.messages[0].reply_on, ReplyOn::Success); - assert_eq!(response.messages[0].id, crate::CREATE_DENOM_REPLY_ID); + assert_eq!(response.messages[0].id, CREATE_DENOM_REPLY_ID); assert_eq!( response.messages[0].msg, CosmosMsg::Custom(NeutronMsg::CreateDenom { @@ -60,7 +68,7 @@ fn instantiate() { #[test] fn reply_unknown_id() { let mut deps = mock_dependencies::(); - let error = crate::reply( + let error = crate::contract::reply( deps.as_mut(), mock_env(), Reply { @@ -69,7 +77,7 @@ fn reply_unknown_id() { }, ) .unwrap_err(); - assert_eq!(error, crate::ContractError::UnknownReplyId { id: 215 }); + assert_eq!(error, ContractError::UnknownReplyId { id: 215 }); } #[test] @@ -111,20 +119,20 @@ fn reply() { } let mut deps = mock_dependencies::(); - crate::DENOM + DENOM .save(deps.as_mut().storage, &String::from("subdenom")) .unwrap(); - let response = crate::reply( + let response = crate::contract::reply( deps.as_mut(), mock_env(), Reply { - id: crate::CREATE_DENOM_REPLY_ID, + id: CREATE_DENOM_REPLY_ID, result: SubMsgResult::Err("".to_string()), }, ) .unwrap(); - let denom = crate::DENOM.load(deps.as_ref().storage).unwrap(); + let denom = DENOM.load(deps.as_ref().storage).unwrap(); assert_eq!(denom, "factory/subdenom"); assert!(response.messages.is_empty()); @@ -139,41 +147,41 @@ fn reply() { #[test] fn mint_zero() { let mut deps = mock_dependencies::(); - crate::CORE_ADDRESS + CORE_ADDRESS .save(deps.as_mut().storage, &Addr::unchecked("core")) .unwrap(); - crate::DENOM + DENOM .save(deps.as_mut().storage, &String::from("denom")) .unwrap(); - let error = crate::execute( + let error = crate::contract::execute( deps.as_mut(), mock_env(), mock_info("core", &[]), - crate::ExecuteMsg::Mint { + ExecuteMsg::Mint { amount: Uint128::zero(), receiver: "receiver".to_string(), }, ) .unwrap_err(); - assert_eq!(error, crate::ContractError::NothingToMint); + assert_eq!(error, ContractError::NothingToMint); } #[test] fn mint() { let mut deps = mock_dependencies::(); - crate::CORE_ADDRESS + CORE_ADDRESS .save(deps.as_mut().storage, &Addr::unchecked("core")) .unwrap(); - crate::DENOM + DENOM .save(deps.as_mut().storage, &String::from("denom")) .unwrap(); - let response = crate::execute( + let response = crate::contract::execute( deps.as_mut(), mock_env(), mock_info("core", &[]), - crate::ExecuteMsg::Mint { + ExecuteMsg::Mint { amount: Uint128::new(220), receiver: "receiver".to_string(), }, @@ -200,113 +208,111 @@ fn mint() { #[test] fn mint_stranger() { let mut deps = mock_dependencies::(); - crate::CORE_ADDRESS + CORE_ADDRESS .save(deps.as_mut().storage, &Addr::unchecked("core")) .unwrap(); - crate::DENOM + DENOM .save(deps.as_mut().storage, &String::from("denom")) .unwrap(); - let error = crate::execute( + let error = crate::contract::execute( deps.as_mut(), mock_env(), mock_info("stranger", &[]), - crate::ExecuteMsg::Mint { + ExecuteMsg::Mint { amount: Uint128::new(220), receiver: "receiver".to_string(), }, ) .unwrap_err(); - assert_eq!(error, crate::ContractError::Unauthorized); + assert_eq!(error, ContractError::Unauthorized); } #[test] fn burn_zero() { let mut deps = mock_dependencies::(); - crate::CORE_ADDRESS + CORE_ADDRESS .save(deps.as_mut().storage, &Addr::unchecked("core")) .unwrap(); - crate::DENOM + DENOM .save(deps.as_mut().storage, &String::from("denom")) .unwrap(); - let error = crate::execute( + let error = crate::contract::execute( deps.as_mut(), mock_env(), mock_info("core", &[]), - crate::ExecuteMsg::Burn {}, + ExecuteMsg::Burn {}, ) .unwrap_err(); assert_eq!( error, - crate::ContractError::PaymentError(cw_utils::PaymentError::NoFunds {}) + ContractError::PaymentError(cw_utils::PaymentError::NoFunds {}) ); } #[test] fn burn_multiple_coins() { let mut deps = mock_dependencies::(); - crate::CORE_ADDRESS + CORE_ADDRESS .save(deps.as_mut().storage, &Addr::unchecked("core")) .unwrap(); - crate::DENOM + DENOM .save(deps.as_mut().storage, &String::from("denom")) .unwrap(); - let error = crate::execute( + let error = crate::contract::execute( deps.as_mut(), mock_env(), mock_info("core", &[coin(20, "coin1"), coin(10, "denom")]), - crate::ExecuteMsg::Burn {}, + ExecuteMsg::Burn {}, ) .unwrap_err(); assert_eq!( error, - crate::ContractError::PaymentError(cw_utils::PaymentError::MultipleDenoms {}) + ContractError::PaymentError(cw_utils::PaymentError::MultipleDenoms {}) ); } #[test] fn burn_invalid_coin() { let mut deps = mock_dependencies::(); - crate::CORE_ADDRESS + CORE_ADDRESS .save(deps.as_mut().storage, &Addr::unchecked("core")) .unwrap(); - crate::DENOM + DENOM .save(deps.as_mut().storage, &String::from("denom")) .unwrap(); - let error = crate::execute( + let error = crate::contract::execute( deps.as_mut(), mock_env(), mock_info("core", &[coin(20, "coin1")]), - crate::ExecuteMsg::Burn {}, + ExecuteMsg::Burn {}, ) .unwrap_err(); assert_eq!( error, - crate::ContractError::PaymentError(cw_utils::PaymentError::MissingDenom( - "denom".to_string() - )) + ContractError::PaymentError(cw_utils::PaymentError::MissingDenom("denom".to_string())) ); } #[test] fn burn() { let mut deps = mock_dependencies::(); - crate::CORE_ADDRESS + CORE_ADDRESS .save(deps.as_mut().storage, &Addr::unchecked("core")) .unwrap(); - crate::DENOM + DENOM .save(deps.as_mut().storage, &String::from("denom")) .unwrap(); - let response = crate::execute( + let response = crate::contract::execute( deps.as_mut(), mock_env(), mock_info("core", &[coin(140, "denom")]), - crate::ExecuteMsg::Burn {}, + ExecuteMsg::Burn {}, ) .unwrap(); @@ -329,38 +335,38 @@ fn burn() { #[test] fn burn_stranger() { let mut deps = mock_dependencies::(); - crate::CORE_ADDRESS + CORE_ADDRESS .save(deps.as_mut().storage, &Addr::unchecked("core")) .unwrap(); - crate::DENOM + DENOM .save(deps.as_mut().storage, &String::from("denom")) .unwrap(); - let error = crate::execute( + let error = crate::contract::execute( deps.as_mut(), mock_env(), mock_info("stranger", &[coin(160, "denom")]), - crate::ExecuteMsg::Burn {}, + ExecuteMsg::Burn {}, ) .unwrap_err(); - assert_eq!(error, crate::ContractError::Unauthorized); + assert_eq!(error, ContractError::Unauthorized); } #[test] fn query_config() { let mut deps = mock_dependencies::(); - crate::CORE_ADDRESS + CORE_ADDRESS .save(deps.as_mut().storage, &Addr::unchecked("core")) .unwrap(); - crate::DENOM + DENOM .save(deps.as_mut().storage, &String::from("denom")) .unwrap(); - let response = crate::query(deps.as_ref(), mock_env(), crate::QueryMsg::Config {}).unwrap(); + let response = crate::contract::query(deps.as_ref(), mock_env(), QueryMsg::Config {}).unwrap(); assert_eq!( response, - to_json_binary(&crate::ConfigResponse { + to_json_binary(&ConfigResponse { core_address: "core".to_string(), denom: "denom".to_string() }) diff --git a/packages/base/Cargo.toml b/packages/base/Cargo.toml index e922d147..206dfff3 100644 --- a/packages/base/Cargo.toml +++ b/packages/base/Cargo.toml @@ -26,7 +26,7 @@ library = [] cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } cw-storage-plus = { workspace = true } - +neutron-sdk = { workspace = true } [dev-dependencies] cosmwasm-storage = { workspace = true } diff --git a/packages/base/src/helpers/answer.rs b/packages/base/src/helpers/answer.rs new file mode 100644 index 00000000..5a62582f --- /dev/null +++ b/packages/base/src/helpers/answer.rs @@ -0,0 +1,18 @@ +use cosmwasm_std::{attr, Attribute, Event, Response}; +use neutron_sdk::bindings::msg::NeutronMsg; + +pub fn response>( + ty: &str, + contract_name: &str, + attrs: impl IntoIterator, +) -> Response { + Response::new().add_event(Event::new(format!("{}-{}", contract_name, ty)).add_attributes(attrs)) +} + +pub fn attr_coin( + key: impl Into, + amount: impl std::fmt::Display, + denom: impl std::fmt::Display, +) -> Attribute { + attr(key, format!("{}{}", amount, denom)) +} diff --git a/packages/base/src/helpers/mod.rs b/packages/base/src/helpers/mod.rs new file mode 100644 index 00000000..7813b98c --- /dev/null +++ b/packages/base/src/helpers/mod.rs @@ -0,0 +1 @@ +pub mod answer; diff --git a/packages/base/src/lib.rs b/packages/base/src/lib.rs index 685329d1..a801d48d 100644 --- a/packages/base/src/lib.rs +++ b/packages/base/src/lib.rs @@ -1,2 +1,3 @@ +pub mod helpers; pub mod msg; pub mod state;