From d83cab757a2f051674c41360df0210dad8bf398f Mon Sep 17 00:00:00 2001 From: Dzung Do Date: Thu, 11 Jul 2024 22:09:52 +0700 Subject: [PATCH 1/6] add unstake custom message --- Cargo.lock | 1 + .../provider/native-staking-proxy/Cargo.toml | 1 + .../native-staking-proxy/src/contract.rs | 3 ++- packages/bindings/src/msg.rs | 17 +++++++++++++++++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index b2f0dab3..9fe473b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -676,6 +676,7 @@ dependencies = [ "cw2", "derivative", "mesh-apis", + "mesh-bindings", "mesh-burn", "mesh-native-staking", "mesh-vault", diff --git a/contracts/provider/native-staking-proxy/Cargo.toml b/contracts/provider/native-staking-proxy/Cargo.toml index a8210310..6328e09e 100644 --- a/contracts/provider/native-staking-proxy/Cargo.toml +++ b/contracts/provider/native-staking-proxy/Cargo.toml @@ -21,6 +21,7 @@ mt = ["library", "sylvia/mt"] [dependencies] mesh-apis = { workspace = true } mesh-burn = { workspace = true } +mesh-bindings = { workspace = true } sylvia = { workspace = true } cosmwasm-schema = { workspace = true } diff --git a/contracts/provider/native-staking-proxy/src/contract.rs b/contracts/provider/native-staking-proxy/src/contract.rs index 89aa6240..decff2c8 100644 --- a/contracts/provider/native-staking-proxy/src/contract.rs +++ b/contracts/provider/native-staking-proxy/src/contract.rs @@ -7,6 +7,7 @@ use cw2::set_contract_version; use cw_storage_plus::Item; use cw_utils::{must_pay, nonpayable}; +use mesh_bindings::ProviderMsg; use sylvia::types::{ExecCtx, InstantiateCtx, QueryCtx}; use sylvia::{contract, schemars}; @@ -288,7 +289,7 @@ impl NativeStakingProxyContract<'_> { ContractError::InvalidDenom(amount.denom) ); - let msg = StakingMsg::Undelegate { validator, amount }; + let msg = ProviderMsg::Unstake { validator, amount }; Ok(Response::new().add_message(msg)) } diff --git a/packages/bindings/src/msg.rs b/packages/bindings/src/msg.rs index 70e94ffe..fde80955 100644 --- a/packages/bindings/src/msg.rs +++ b/packages/bindings/src/msg.rs @@ -87,6 +87,12 @@ pub enum ProviderMsg { /// If these conditions are met, it will instantly unbond /// amount.amount tokens from the vault contract. Unbond { delegator: String, amount: Coin }, + /// Unstake ensures that amount.denom is the native staking denom and + /// the calling contract is the native staking proxy contract. + /// + /// If these conditions are met, it will instantly unstake + /// amount.amount tokens from the native staking proxy contract. + Unstake { validator: String, amount: Coin }, } impl ProviderMsg { @@ -111,6 +117,17 @@ impl ProviderMsg { amount: coin, } } + + pub fn unstake(denom: &str, validator: &str, amount: impl Into) -> ProviderMsg { + let coin = Coin { + amount: amount.into(), + denom: denom.into(), + }; + ProviderMsg::Unstake { + validator: validator.to_string(), + amount: coin, + } + } } impl From for CosmosMsg { From e7b020c1ad45d52e923c55ca527d0b8546d220fa Mon Sep 17 00:00:00 2001 From: Dzung Do Date: Fri, 12 Jul 2024 16:50:09 +0700 Subject: [PATCH 2/6] add native staking proxy mock --- .../external-staking/src/multitest.rs | 2 +- .../native-staking-proxy/src/contract.rs | 21 +- .../provider/native-staking-proxy/src/lib.rs | 1 + .../provider/native-staking-proxy/src/mock.rs | 503 ++++++++++++++++++ .../native-staking-proxy/src/multitest.rs | 23 +- .../provider/native-staking/src/multitest.rs | 8 +- contracts/provider/vault/src/multitest.rs | 8 +- 7 files changed, 535 insertions(+), 31 deletions(-) create mode 100644 contracts/provider/native-staking-proxy/src/mock.rs diff --git a/contracts/provider/external-staking/src/multitest.rs b/contracts/provider/external-staking/src/multitest.rs index ed8298a2..84092f45 100644 --- a/contracts/provider/external-staking/src/multitest.rs +++ b/contracts/provider/external-staking/src/multitest.rs @@ -6,7 +6,7 @@ use cosmwasm_std::{coin, coins, to_json_binary, Decimal, Uint128}; use cw_multi_test::App as MtApp; use mesh_native_staking::contract::sv::mt::CodeId as NativeStakingCodeId; use mesh_native_staking::contract::sv::InstantiateMsg as NativeStakingInstantiateMsg; -use mesh_native_staking_proxy::contract::sv::mt::CodeId as NativeStakingProxyCodeId; +use mesh_native_staking_proxy::mock::sv::mt::CodeId as NativeStakingProxyCodeId; use mesh_vault::mock::sv::mt::{CodeId as VaultCodeId, VaultMockProxy}; use mesh_vault::mock::VaultMock; use mesh_vault::msg::{LocalStakingInfo, StakingInitInfo}; diff --git a/contracts/provider/native-staking-proxy/src/contract.rs b/contracts/provider/native-staking-proxy/src/contract.rs index decff2c8..9b01af2f 100644 --- a/contracts/provider/native-staking-proxy/src/contract.rs +++ b/contracts/provider/native-staking-proxy/src/contract.rs @@ -7,7 +7,7 @@ use cw2::set_contract_version; use cw_storage_plus::Item; use cw_utils::{must_pay, nonpayable}; -use mesh_bindings::ProviderMsg; +use mesh_bindings::{ProviderCustomMsg, ProviderMsg}; use sylvia::types::{ExecCtx, InstantiateCtx, QueryCtx}; use sylvia::{contract, schemars}; @@ -27,6 +27,7 @@ pub struct NativeStakingProxyContract<'a> { #[cfg_attr(not(feature = "library"), sylvia::entry_points)] #[contract] #[sv::error(ContractError)] +#[sv::custom(msg=ProviderCustomMsg)] impl NativeStakingProxyContract<'_> { pub const fn new() -> Self { Self { @@ -44,7 +45,7 @@ impl NativeStakingProxyContract<'_> { denom: String, owner: String, validator: String, - ) -> Result { + ) -> Result, ContractError> { let config = Config { denom, parent: ctx.info.sender.clone(), @@ -77,7 +78,7 @@ impl NativeStakingProxyContract<'_> { /// Stakes the tokens from `info.funds` to the given validator. /// Can only be called by the parent contract #[sv::msg(exec)] - fn stake(&self, ctx: ExecCtx, validator: String) -> Result { + fn stake(&self, ctx: ExecCtx, validator: String) -> Result, ContractError> { let cfg = self.config.load(ctx.deps.storage)?; ensure_eq!(cfg.parent, ctx.info.sender, ContractError::Unauthorized {}); @@ -98,7 +99,7 @@ impl NativeStakingProxyContract<'_> { ctx: ExecCtx, validator: Option, amount: Coin, - ) -> Result { + ) -> Result, ContractError> { let cfg = self.config.load(ctx.deps.storage)?; ensure_eq!(cfg.parent, ctx.info.sender, ContractError::Unauthorized {}); @@ -187,7 +188,7 @@ impl NativeStakingProxyContract<'_> { src_validator: String, dst_validator: String, amount: Coin, - ) -> Result { + ) -> Result, ContractError> { let cfg = self.config.load(ctx.deps.storage)?; ensure_eq!(cfg.owner, ctx.info.sender, ContractError::Unauthorized {}); @@ -214,7 +215,7 @@ impl NativeStakingProxyContract<'_> { ctx: ExecCtx, proposal_id: u64, vote: VoteOption, - ) -> Result { + ) -> Result, ContractError> { let cfg = self.config.load(ctx.deps.storage)?; ensure_eq!(cfg.owner, ctx.info.sender, ContractError::Unauthorized {}); @@ -231,7 +232,7 @@ impl NativeStakingProxyContract<'_> { ctx: ExecCtx, proposal_id: u64, vote: Vec, - ) -> Result { + ) -> Result, ContractError> { let cfg = self.config.load(ctx.deps.storage)?; ensure_eq!(cfg.owner, ctx.info.sender, ContractError::Unauthorized {}); @@ -248,7 +249,7 @@ impl NativeStakingProxyContract<'_> { /// send the tokens to the caller. /// NOTE: must make sure not to release unbonded tokens #[sv::msg(exec)] - fn withdraw_rewards(&self, ctx: ExecCtx) -> Result { + fn withdraw_rewards(&self, ctx: ExecCtx) -> Result, ContractError> { let cfg = self.config.load(ctx.deps.storage)?; ensure_eq!(cfg.owner, ctx.info.sender, ContractError::Unauthorized {}); @@ -277,7 +278,7 @@ impl NativeStakingProxyContract<'_> { ctx: ExecCtx, validator: String, amount: Coin, - ) -> Result { + ) -> Result, ContractError> { let cfg = self.config.load(ctx.deps.storage)?; ensure_eq!(cfg.owner, ctx.info.sender, ContractError::Unauthorized {}); @@ -297,7 +298,7 @@ impl NativeStakingProxyContract<'_> { /// This will go back to the parent via `release_proxy_stake`. /// Errors if the proxy doesn't have any liquid tokens #[sv::msg(exec)] - fn release_unbonded(&self, ctx: ExecCtx) -> Result { + fn release_unbonded(&self, ctx: ExecCtx) -> Result, ContractError> { let cfg = self.config.load(ctx.deps.storage)?; ensure_eq!(cfg.owner, ctx.info.sender, ContractError::Unauthorized {}); diff --git a/contracts/provider/native-staking-proxy/src/lib.rs b/contracts/provider/native-staking-proxy/src/lib.rs index 684e3b37..3431e367 100644 --- a/contracts/provider/native-staking-proxy/src/lib.rs +++ b/contracts/provider/native-staking-proxy/src/lib.rs @@ -3,5 +3,6 @@ pub mod error; pub mod msg; #[cfg(test)] mod multitest; +pub mod mock; pub mod native_staking_callback; mod state; diff --git a/contracts/provider/native-staking-proxy/src/mock.rs b/contracts/provider/native-staking-proxy/src/mock.rs new file mode 100644 index 00000000..19cd50c1 --- /dev/null +++ b/contracts/provider/native-staking-proxy/src/mock.rs @@ -0,0 +1,503 @@ +use cosmwasm_std::WasmMsg::Execute; +use cosmwasm_std::{ + coin, ensure_eq, to_json_binary, Coin, DistributionMsg, GovMsg, Response, StakingMsg, + VoteOption, WeightedVoteOption, +}; +use cw2::set_contract_version; +use cw_storage_plus::Item; + +use cw_utils::{must_pay, nonpayable}; +use sylvia::types::{ExecCtx, InstantiateCtx, QueryCtx}; +use sylvia::{contract, schemars}; + +use crate::error::ContractError; +use crate::msg::{ConfigResponse, OwnerMsg}; +use crate::native_staking_callback; +use crate::state::Config; + +pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub struct NativeStakingProxyMock<'a> { + config: Item<'a, Config>, + burned: Item<'a, u128>, +} + +#[cfg_attr(not(feature = "library"), sylvia::entry_points)] +#[contract] +#[sv::error(ContractError)] +impl NativeStakingProxyMock<'_> { + pub const fn new() -> Self { + Self { + config: Item::new("config"), + burned: Item::new("burned"), + } + } + + /// The caller of the instantiation will be the native-staking contract. + /// We stake `funds.info` on the given validator + #[sv::msg(instantiate)] + pub fn instantiate( + &self, + ctx: InstantiateCtx, + denom: String, + owner: String, + validator: String, + ) -> Result { + let config = Config { + denom, + parent: ctx.info.sender.clone(), + owner: ctx.deps.api.addr_validate(&owner)?, + }; + self.config.save(ctx.deps.storage, &config)?; + set_contract_version(ctx.deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + // Set burned stake to zero + self.burned.save(ctx.deps.storage, &0)?; + + // Stake info.funds on validator + let exec_ctx = ExecCtx { + deps: ctx.deps, + env: ctx.env, + info: ctx.info, + }; + let res = self.stake(exec_ctx, validator)?; + + // Set owner as recipient of future withdrawals + let set_withdrawal = DistributionMsg::SetWithdrawAddress { + address: config.owner.into_string(), + }; + + // Pass owner to caller's reply handler + let owner_msg = to_json_binary(&OwnerMsg { owner })?; + Ok(res.add_message(set_withdrawal).set_data(owner_msg)) + } + + /// Stakes the tokens from `info.funds` to the given validator. + /// Can only be called by the parent contract + #[sv::msg(exec)] + fn stake(&self, ctx: ExecCtx, validator: String) -> Result { + let cfg = self.config.load(ctx.deps.storage)?; + ensure_eq!(cfg.parent, ctx.info.sender, ContractError::Unauthorized {}); + + let amount = must_pay(&ctx.info, &cfg.denom)?; + + let amount = coin(amount.u128(), cfg.denom); + let msg = StakingMsg::Delegate { validator, amount }; + + Ok(Response::new().add_message(msg)) + } + + /// Burn `amount` tokens from the given validator, if set. + /// If `validator` is not set, undelegate evenly from all validators the user has stake. + /// Can only be called by the parent contract + #[sv::msg(exec)] + fn burn( + &self, + ctx: ExecCtx, + validator: Option, + amount: Coin, + ) -> Result { + let cfg = self.config.load(ctx.deps.storage)?; + ensure_eq!(cfg.parent, ctx.info.sender, ContractError::Unauthorized {}); + + nonpayable(&ctx.info)?; + + // Check denom + ensure_eq!( + amount.denom, + cfg.denom, + ContractError::InvalidDenom(amount.denom) + ); + + let delegations = match validator { + Some(validator) => { + match ctx + .deps + .querier + .query_delegation(ctx.env.contract.address.clone(), validator)? + .map(|full_delegation| { + ( + full_delegation.validator, + full_delegation.amount.amount.u128(), + ) + }) { + Some(delegation) => vec![delegation], + None => vec![], + } + } + None => ctx + .deps + .querier + .query_all_delegations(ctx.env.contract.address.clone())? + .iter() + .map(|delegation| { + ( + delegation.validator.clone(), + delegation.amount.amount.u128(), + ) + }) + .collect::>(), + }; + + // Error if no validators + if delegations.is_empty() { + return Err(ContractError::InsufficientDelegations( + ctx.env.contract.address.to_string(), + amount.amount, + )); + } + + let (burned, burns) = mesh_burn::distribute_burn(&delegations, amount.amount.u128()); + + // Bail if we don't have enough delegations + if burned < amount.amount.u128() { + return Err(ContractError::InsufficientDelegations( + ctx.env.contract.address.to_string(), + amount.amount, + )); + } + + // Build undelegate messages + // FIXME: Use an "immediate unbonding" message for undelegation + let mut undelegate_msgs = vec![]; + for (validator, burn_amount) in burns { + let undelegate_msg = StakingMsg::Undelegate { + validator: validator.to_string(), + amount: coin(burn_amount, &cfg.denom), + }; + undelegate_msgs.push(undelegate_msg); + } + + // Accounting trick to avoid burning stake + self.burned.update(ctx.deps.storage, |old| { + Ok::<_, ContractError>(old + amount.amount.u128()) + })?; + + Ok(Response::new().add_messages(undelegate_msgs)) + } + + /// Re-stakes the given amount from the one validator to another on behalf of the calling user. + /// Returns an error if the user doesn't have such stake + #[sv::msg(exec)] + fn restake( + &self, + ctx: ExecCtx, + src_validator: String, + dst_validator: String, + amount: Coin, + ) -> Result { + let cfg = self.config.load(ctx.deps.storage)?; + ensure_eq!(cfg.owner, ctx.info.sender, ContractError::Unauthorized {}); + + nonpayable(&ctx.info)?; + + ensure_eq!( + amount.denom, + cfg.denom, + ContractError::InvalidDenom(amount.denom) + ); + + let msg = StakingMsg::Redelegate { + src_validator, + dst_validator, + amount, + }; + Ok(Response::new().add_message(msg)) + } + + /// Vote with the user's stake (over all delegations) + #[sv::msg(exec)] + fn vote( + &self, + ctx: ExecCtx, + proposal_id: u64, + vote: VoteOption, + ) -> Result { + let cfg = self.config.load(ctx.deps.storage)?; + ensure_eq!(cfg.owner, ctx.info.sender, ContractError::Unauthorized {}); + + nonpayable(&ctx.info)?; + + let msg = GovMsg::Vote { proposal_id, vote }; + Ok(Response::new().add_message(msg)) + } + + /// Vote with the user's stake (over all delegations) + #[sv::msg(exec)] + fn vote_weighted( + &self, + ctx: ExecCtx, + proposal_id: u64, + vote: Vec, + ) -> Result { + let cfg = self.config.load(ctx.deps.storage)?; + ensure_eq!(cfg.owner, ctx.info.sender, ContractError::Unauthorized {}); + + nonpayable(&ctx.info)?; + + let msg = GovMsg::VoteWeighted { + proposal_id, + options: vote, + }; + Ok(Response::new().add_message(msg)) + } + + /// If the caller has any delegations, withdraw all rewards from those delegations and + /// send the tokens to the caller. + /// NOTE: must make sure not to release unbonded tokens + #[sv::msg(exec)] + fn withdraw_rewards(&self, ctx: ExecCtx) -> Result { + let cfg = self.config.load(ctx.deps.storage)?; + ensure_eq!(cfg.owner, ctx.info.sender, ContractError::Unauthorized {}); + + nonpayable(&ctx.info)?; + + // Withdraw all delegations to the owner (already set as withdrawal address in instantiate) + let msgs: Vec<_> = ctx + .deps + .querier + .query_all_delegations(ctx.env.contract.address)? + .into_iter() + .map(|delegation| DistributionMsg::WithdrawDelegatorReward { + validator: delegation.validator, + }) + .collect(); + let res = Response::new().add_messages(msgs); + Ok(res) + } + + /// Unstakes the given amount from the given validator on behalf of the calling user. + /// Returns an error if the user doesn't have such stake. + /// After the unbonding period, it will allow the user to claim the tokens (returning to vault) + #[sv::msg(exec)] + fn unstake( + &self, + ctx: ExecCtx, + validator: String, + amount: Coin, + ) -> Result { + let cfg = self.config.load(ctx.deps.storage)?; + ensure_eq!(cfg.owner, ctx.info.sender, ContractError::Unauthorized {}); + + nonpayable(&ctx.info)?; + + ensure_eq!( + amount.denom, + cfg.denom, + ContractError::InvalidDenom(amount.denom) + ); + + let msg = StakingMsg::Undelegate { validator, amount }; + Ok(Response::new().add_message(msg)) + } + + /// Releases any tokens that have fully unbonded from a previous unstake. + /// This will go back to the parent via `release_proxy_stake`. + /// Errors if the proxy doesn't have any liquid tokens + #[sv::msg(exec)] + fn release_unbonded(&self, ctx: ExecCtx) -> Result { + let cfg = self.config.load(ctx.deps.storage)?; + ensure_eq!(cfg.owner, ctx.info.sender, ContractError::Unauthorized {}); + + nonpayable(&ctx.info)?; + + // Simply assumes all of our liquid assets are from unbondings + let balance = ctx + .deps + .querier + .query_balance(ctx.env.contract.address, cfg.denom)?; + // But discount burned stake + // FIXME: this is not accurate, as it doesn't take into account the unbonding period + let burned = self.burned.load(ctx.deps.storage)?; + let balance = coin(balance.amount.u128().saturating_sub(burned), &balance.denom); + + // Short circuit if there are no funds to send + if balance.amount.is_zero() { + return Ok(Response::new()); + } + + // Send them to the parent contract via `release_proxy_stake` + let msg = to_json_binary(&native_staking_callback::sv::ExecMsg::ReleaseProxyStake {})?; + + let wasm_msg = Execute { + contract_addr: cfg.parent.to_string(), + msg, + funds: vec![balance], + }; + Ok(Response::new().add_message(wasm_msg)) + } + + #[sv::msg(query)] + fn config(&self, ctx: QueryCtx) -> Result { + Ok(self.config.load(ctx.deps.storage)?) + } +} + +// Some unit tests, due to mt limitations / unsupported msgs +#[cfg(test)] +mod tests { + use super::*; + use cosmwasm_std::DistributionMsg::SetWithdrawAddress; + use cosmwasm_std::GovMsg::{Vote, VoteWeighted}; + use cosmwasm_std::{CosmosMsg, Decimal, DepsMut}; + + use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; + use cosmwasm_std::VoteOption::Yes; + use cw_utils::PaymentError; + + static OSMO: &str = "uosmo"; + static CREATOR: &str = "staking"; // The creator of the proxy contract(s) is the staking contract + static OWNER: &str = "user"; + static VALIDATOR: &str = "validator"; + + fn do_instantiate(deps: DepsMut) -> (ExecCtx, NativeStakingProxyMock) { + let contract = NativeStakingProxyMock::new(); + let mut ctx = InstantiateCtx { + deps, + env: mock_env(), + info: mock_info(CREATOR, &[coin(100, OSMO)]), + }; + contract + .instantiate( + ctx.branch(), + OSMO.to_owned(), + OWNER.to_owned(), + VALIDATOR.to_owned(), + ) + .unwrap(); + let exec_ctx = ExecCtx { + deps: ctx.deps, + info: mock_info(OWNER, &[]), + env: ctx.env, + }; + (exec_ctx, contract) + } + + // Extra checks of instantiate returned messages and data + #[test] + fn instantiating() { + let mut deps = mock_dependencies(); + let contract = NativeStakingProxyMock::new(); + let mut ctx = InstantiateCtx { + deps: deps.as_mut(), + env: mock_env(), + info: mock_info(CREATOR, &[coin(100, OSMO)]), + }; + let res = contract + .instantiate( + ctx.branch(), + OSMO.to_owned(), + OWNER.to_owned(), + VALIDATOR.to_owned(), + ) + .unwrap(); + + // Assert returned messages + assert_eq!( + res.messages[0].msg, + CosmosMsg::Staking(StakingMsg::Delegate { + validator: VALIDATOR.to_owned(), + amount: coin(100, OSMO) + }) + ); + assert_eq!( + res.messages[1].msg, + CosmosMsg::Distribution(SetWithdrawAddress { + address: OWNER.to_owned(), + }) + ); + + // Assert data payload + assert_eq!( + res.data.unwrap(), + to_json_binary(&OwnerMsg { + owner: OWNER.to_owned(), + }) + .unwrap() + ); + } + + #[test] + fn voting() { + let mut deps = mock_dependencies(); + let (mut ctx, contract) = do_instantiate(deps.as_mut()); + + // The owner can vote + let proposal_id = 1; + let vote = Yes; + let res = contract + .vote(ctx.branch(), proposal_id, vote.clone()) + .unwrap(); + assert_eq!(1, res.messages.len()); + // assert it's a governance vote + assert_eq!( + res.messages[0].msg, + cosmwasm_std::CosmosMsg::Gov(Vote { + proposal_id, + vote: vote.clone() + }) + ); + + // But not send funds + ctx.info = mock_info(OWNER, &[coin(1, OSMO)]); + let res = contract.vote(ctx.branch(), proposal_id, vote.clone()); + assert!(matches!( + res.unwrap_err(), + ContractError::Payment(PaymentError::NonPayable {}) + )); + + // Nobody else can vote + ctx.info = mock_info("somebody", &[]); + let res = contract.vote(ctx.branch(), proposal_id, vote.clone()); + assert!(matches!(res.unwrap_err(), ContractError::Unauthorized {})); + + // Not even the creator + ctx.info = mock_info(CREATOR, &[]); + let res = contract.vote(ctx, proposal_id, vote); + assert!(matches!(res.unwrap_err(), ContractError::Unauthorized {})); + } + + #[test] + fn weighted_voting() { + let mut deps = mock_dependencies(); + let (mut ctx, contract) = do_instantiate(deps.as_mut()); + + // The owner can weighted vote + let proposal_id = 2; + let vote = vec![WeightedVoteOption { + option: Yes, + weight: Decimal::percent(50), + }]; + let res = contract + .vote_weighted(ctx.branch(), proposal_id, vote.clone()) + .unwrap(); + assert_eq!(1, res.messages.len()); + // Assert it's a weighted governance vote + assert_eq!( + res.messages[0].msg, + cosmwasm_std::CosmosMsg::Gov(VoteWeighted { + proposal_id, + options: vote.clone() + }) + ); + + // But not send funds + ctx.info = mock_info(OWNER, &[coin(1, OSMO)]); + let res = contract.vote_weighted(ctx.branch(), proposal_id, vote.clone()); + assert!(matches!( + res.unwrap_err(), + ContractError::Payment(PaymentError::NonPayable {}) + )); + + // Nobody else can vote + ctx.info = mock_info("somebody", &[]); + let res = contract.vote_weighted(ctx.branch(), proposal_id, vote.clone()); + assert!(matches!(res.unwrap_err(), ContractError::Unauthorized {})); + + // Not even the creator + ctx.info = mock_info(CREATOR, &[]); + let res = contract.vote_weighted(ctx, proposal_id, vote); + assert!(matches!(res.unwrap_err(), ContractError::Unauthorized {})); + } +} \ No newline at end of file diff --git a/contracts/provider/native-staking-proxy/src/multitest.rs b/contracts/provider/native-staking-proxy/src/multitest.rs index 90baa916..b0e684f4 100644 --- a/contracts/provider/native-staking-proxy/src/multitest.rs +++ b/contracts/provider/native-staking-proxy/src/multitest.rs @@ -10,9 +10,8 @@ use mesh_vault::mock::sv::mt::VaultMockProxy; use mesh_vault::mock::VaultMock; use mesh_vault::msg::LocalStakingInfo; -use crate::contract; -use crate::contract::sv::mt::NativeStakingProxyContractProxy; -use crate::contract::NativeStakingProxyContract; +use crate::mock::sv::mt::NativeStakingProxyMockProxy; +use crate::mock::NativeStakingProxyMock; use crate::msg::ConfigResponse; const OSMO: &str = "uosmo"; @@ -62,7 +61,7 @@ fn setup<'app>( ) -> AnyResult>> { let vault_code = mesh_vault::mock::sv::mt::CodeId::store_code(app); let staking_code = mesh_native_staking::contract::sv::mt::CodeId::store_code(app); - let staking_proxy_code = contract::sv::mt::CodeId::store_code(app); + let staking_proxy_code = crate::mock::sv::mt::CodeId::store_code(app); // Instantiate vault msg let staking_init_info = mesh_vault::msg::StakingInitInfo { @@ -126,7 +125,7 @@ fn instantiation() { setup(&app, owner, user, &[validator]).unwrap(); // Access staking proxy instance - let staking_proxy: Proxy<'_, MtApp, NativeStakingProxyContract<'_>> = + let staking_proxy: Proxy<'_, MtApp, NativeStakingProxyMock<'_>> = Proxy::new(Addr::unchecked(proxy_addr), &app); // Check config @@ -170,7 +169,7 @@ fn staking() { let vault = setup(&app, owner, user, &[validator]).unwrap(); // Access staking proxy instance - let staking_proxy: Proxy<'_, MtApp, NativeStakingProxyContract<'_>> = + let staking_proxy: Proxy<'_, MtApp, NativeStakingProxyMock<'_>> = Proxy::new(Addr::unchecked(proxy_addr), &app); // Stake some more @@ -216,7 +215,7 @@ fn restaking() { setup(&app, owner, user, &[validator]).unwrap(); // Access staking proxy instance - let staking_proxy: Proxy<'_, MtApp, NativeStakingProxyContract<'_>> = + let staking_proxy: Proxy<'_, MtApp, NativeStakingProxyMock<'_>> = Proxy::new(Addr::unchecked(proxy_addr), &app); // Restake 30% to a different validator @@ -255,7 +254,7 @@ fn unstaking() { setup(&app, owner, user, &[validator]).unwrap(); // Access staking proxy instance - let staking_proxy: Proxy<'_, MtApp, NativeStakingProxyContract<'_>> = + let staking_proxy: Proxy<'_, MtApp, NativeStakingProxyMock<'_>> = Proxy::new(Addr::unchecked(proxy_addr), &app); // Unstake 50% @@ -310,7 +309,7 @@ fn burning() { setup(&app, owner, user, &[validator]).unwrap(); // Access staking proxy instance - let staking_proxy: Proxy<'_, MtApp, NativeStakingProxyContract<'_>> = + let staking_proxy: Proxy<'_, MtApp, NativeStakingProxyMock<'_>> = Proxy::new(Addr::unchecked(proxy_addr), &app); // Burn 10%, from validator @@ -377,7 +376,7 @@ fn burning_multiple_delegations() { setup(&app, owner, user, &validators).unwrap(); // Access staking proxy instance - let staking_proxy: Proxy<'_, MtApp, NativeStakingProxyContract<'_>> = + let staking_proxy: Proxy<'_, MtApp, NativeStakingProxyMock<'_>> = Proxy::new(Addr::unchecked(proxy_addr), &app); // Burn 15%, no validator specified @@ -458,7 +457,7 @@ fn releasing_unbonded() { let vault = setup(&app, owner, user, &[validator]).unwrap(); // Access staking proxy instance - let staking_proxy: Proxy<'_, MtApp, NativeStakingProxyContract<'_>> = + let staking_proxy: Proxy<'_, MtApp, NativeStakingProxyMock<'_>> = Proxy::new(Addr::unchecked(proxy_addr), &app); // Unstake 100% @@ -514,7 +513,7 @@ fn withdrawing_rewards() { let original_user_funds = app.app().wrap().query_balance(user, OSMO).unwrap(); // Access staking proxy instance - let staking_proxy: Proxy<'_, MtApp, NativeStakingProxyContract<'_>> = + let staking_proxy: Proxy<'_, MtApp, NativeStakingProxyMock<'_>> = Proxy::new(Addr::unchecked(proxy_addr), &app); // Advance time enough for rewards to accrue diff --git a/contracts/provider/native-staking/src/multitest.rs b/contracts/provider/native-staking/src/multitest.rs index fde0c43a..1463a9e8 100644 --- a/contracts/provider/native-staking/src/multitest.rs +++ b/contracts/provider/native-staking/src/multitest.rs @@ -6,10 +6,10 @@ use cw_multi_test::{App as MtApp, StakingInfo}; use sylvia::multitest::{App, Proxy}; use mesh_apis::local_staking_api::sv::mt::LocalStakingApiProxy; -use mesh_native_staking_proxy::contract::sv::mt::{ - CodeId as NativeStakingProxyCodeId, NativeStakingProxyContractProxy, +use mesh_native_staking_proxy::mock::sv::mt::{ + CodeId as NativeStakingProxyCodeId, NativeStakingProxyMockProxy, }; -use mesh_native_staking_proxy::contract::NativeStakingProxyContract; +use mesh_native_staking_proxy::mock::NativeStakingProxyMock; use mesh_sync::ValueRange; use mesh_vault::mock::sv::mt::VaultMockProxy; use mesh_vault::msg::LocalStakingInfo; @@ -291,7 +291,7 @@ fn releasing_proxy_stake() { ); // Access staking instance - let staking_proxy: Proxy<'_, MtApp, NativeStakingProxyContract<'_>> = + let staking_proxy: Proxy<'_, MtApp, NativeStakingProxyMock<'_>> = Proxy::new(Addr::unchecked(proxy_addr), &app); // User bonds some funds to the vault diff --git a/contracts/provider/vault/src/multitest.rs b/contracts/provider/vault/src/multitest.rs index 9e8b897a..fc2f9485 100644 --- a/contracts/provider/vault/src/multitest.rs +++ b/contracts/provider/vault/src/multitest.rs @@ -8,8 +8,8 @@ use mesh_external_staking::state::SlashRatio; use mesh_external_staking::state::Stake; use mesh_native_staking::contract::sv::mt::NativeStakingContractProxy; use mesh_native_staking::contract::NativeStakingContract; -use mesh_native_staking_proxy::contract::sv::mt::NativeStakingProxyContractProxy; -use mesh_native_staking_proxy::contract::NativeStakingProxyContract; +use mesh_native_staking_proxy::mock::sv::mt::NativeStakingProxyMockProxy; +use mesh_native_staking_proxy::mock::NativeStakingProxyMock; use mesh_sync::Tx::InFlightStaking; use mesh_sync::{Tx, ValueRange}; use sylvia::multitest::{App, Proxy}; @@ -126,7 +126,7 @@ fn setup_inner<'app>( let staking_init_info = if local_staking { let native_staking_code = mesh_native_staking::contract::sv::mt::CodeId::store_code(app); let native_staking_proxy_code = - mesh_native_staking_proxy::contract::sv::mt::CodeId::store_code(app); + mesh_native_staking_proxy::mock::sv::mt::CodeId::store_code(app); let native_staking_inst_msg = mesh_native_staking::contract::sv::InstantiateMsg { denom: OSMO.to_string(), @@ -262,7 +262,7 @@ fn proxy_for_user<'a>( local_staking: &Proxy<'_, MtApp, NativeStakingContract<'_>>, user: &str, app: &'a App, -) -> Proxy<'a, MtApp, NativeStakingProxyContract<'a>> { +) -> Proxy<'a, MtApp, NativeStakingProxyMock<'a>> { let proxy_addr = local_staking .proxy_by_owner(user.to_string()) .unwrap() From a773d32e87bb45f846ff3a38d870d989cf498b7c Mon Sep 17 00:00:00 2001 From: Dzung Do Date: Mon, 15 Jul 2024 12:13:07 +0700 Subject: [PATCH 3/6] fix build error --- contracts/provider/native-staking-proxy/src/mock.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/provider/native-staking-proxy/src/mock.rs b/contracts/provider/native-staking-proxy/src/mock.rs index 19cd50c1..20fa153e 100644 --- a/contracts/provider/native-staking-proxy/src/mock.rs +++ b/contracts/provider/native-staking-proxy/src/mock.rs @@ -23,7 +23,6 @@ pub struct NativeStakingProxyMock<'a> { burned: Item<'a, u128>, } -#[cfg_attr(not(feature = "library"), sylvia::entry_points)] #[contract] #[sv::error(ContractError)] impl NativeStakingProxyMock<'_> { From 10142f0bceb3d7bebde49a11ec0ea97163fb7df8 Mon Sep 17 00:00:00 2001 From: Dzung Do Date: Mon, 29 Jul 2024 03:42:50 +0700 Subject: [PATCH 4/6] lint the code --- .../external-staking/src/multitest/utils.rs | 3 +- .../native-staking-proxy/src/contract.rs | 6 ++- .../provider/native-staking-proxy/src/lib.rs | 2 +- .../provider/native-staking-proxy/src/mock.rs | 2 +- .../src/native_staking_callback.rs | 2 +- contracts/provider/vault/src/contract.rs | 43 +++++++++++++++---- contracts/provider/vault/src/lib.rs | 2 +- contracts/provider/vault/src/mock.rs | 3 +- packages/apis/src/vault_api.rs | 2 +- packages/bindings/src/lib.rs | 2 +- packages/bindings/src/msg.rs | 2 +- 11 files changed, 50 insertions(+), 19 deletions(-) diff --git a/contracts/provider/external-staking/src/multitest/utils.rs b/contracts/provider/external-staking/src/multitest/utils.rs index 1b7d52a4..7d9a8e3b 100644 --- a/contracts/provider/external-staking/src/multitest/utils.rs +++ b/contracts/provider/external-staking/src/multitest/utils.rs @@ -47,8 +47,7 @@ pub(crate) trait AppExt { impl AppExt for App { #[track_caller] fn new_with_balances(balances: &[(&str, &[Coin])]) -> Self { - - let app =MtApp::new(|router, _api, storage| { + let app = MtApp::new(|router, _api, storage| { for (addr, coins) in balances { router .bank diff --git a/contracts/provider/native-staking-proxy/src/contract.rs b/contracts/provider/native-staking-proxy/src/contract.rs index 9b01af2f..8cbc9f9b 100644 --- a/contracts/provider/native-staking-proxy/src/contract.rs +++ b/contracts/provider/native-staking-proxy/src/contract.rs @@ -78,7 +78,11 @@ impl NativeStakingProxyContract<'_> { /// Stakes the tokens from `info.funds` to the given validator. /// Can only be called by the parent contract #[sv::msg(exec)] - fn stake(&self, ctx: ExecCtx, validator: String) -> Result, ContractError> { + fn stake( + &self, + ctx: ExecCtx, + validator: String, + ) -> Result, ContractError> { let cfg = self.config.load(ctx.deps.storage)?; ensure_eq!(cfg.parent, ctx.info.sender, ContractError::Unauthorized {}); diff --git a/contracts/provider/native-staking-proxy/src/lib.rs b/contracts/provider/native-staking-proxy/src/lib.rs index 3431e367..54c54224 100644 --- a/contracts/provider/native-staking-proxy/src/lib.rs +++ b/contracts/provider/native-staking-proxy/src/lib.rs @@ -1,8 +1,8 @@ pub mod contract; pub mod error; +pub mod mock; pub mod msg; #[cfg(test)] mod multitest; -pub mod mock; pub mod native_staking_callback; mod state; diff --git a/contracts/provider/native-staking-proxy/src/mock.rs b/contracts/provider/native-staking-proxy/src/mock.rs index 20fa153e..9fc5b3d6 100644 --- a/contracts/provider/native-staking-proxy/src/mock.rs +++ b/contracts/provider/native-staking-proxy/src/mock.rs @@ -499,4 +499,4 @@ mod tests { let res = contract.vote_weighted(ctx, proposal_id, vote); assert!(matches!(res.unwrap_err(), ContractError::Unauthorized {})); } -} \ No newline at end of file +} diff --git a/contracts/provider/native-staking/src/native_staking_callback.rs b/contracts/provider/native-staking/src/native_staking_callback.rs index fbfe4a35..4996296f 100644 --- a/contracts/provider/native-staking/src/native_staking_callback.rs +++ b/contracts/provider/native-staking/src/native_staking_callback.rs @@ -33,4 +33,4 @@ impl NativeStakingCallback for NativeStakingContract<'_> { Ok(Response::new().add_message(msg)) } -} \ No newline at end of file +} diff --git a/contracts/provider/vault/src/contract.rs b/contracts/provider/vault/src/contract.rs index 3138e37a..cf415057 100644 --- a/contracts/provider/vault/src/contract.rs +++ b/contracts/provider/vault/src/contract.rs @@ -1,5 +1,6 @@ use cosmwasm_std::{ - coin, ensure, Addr, Binary, Coin, Decimal, DepsMut, Fraction, Order, Reply, Response, StdResult, Storage, SubMsg, SubMsgResponse, Uint128, WasmMsg + coin, ensure, Addr, Binary, Coin, Decimal, DepsMut, Fraction, Order, Reply, Response, + StdResult, Storage, SubMsg, SubMsgResponse, Uint128, WasmMsg, }; use cw2::set_contract_version; use cw_storage_plus::{Bounder, Item, Map}; @@ -143,7 +144,11 @@ impl VaultContract<'_> { } #[sv::msg(exec)] - fn bond(&self, ctx: ExecCtx, amount: Coin) -> Result, ContractError> { + fn bond( + &self, + ctx: ExecCtx, + amount: Coin, + ) -> Result, ContractError> { nonpayable(&ctx.info)?; let denom = self.config.load(ctx.deps.storage)?.denom; @@ -156,7 +161,10 @@ impl VaultContract<'_> { user.collateral += amount.amount; self.users.save(ctx.deps.storage, &ctx.info.sender, &user)?; let amt = amount.amount; - let msg = ProviderMsg::Bond { delegator: ctx.info.sender.clone().into_string(), amount}; + let msg = ProviderMsg::Bond { + delegator: ctx.info.sender.clone().into_string(), + amount, + }; let resp = Response::new() .add_message(msg) .add_attribute("action", "unbond") @@ -167,7 +175,11 @@ impl VaultContract<'_> { } #[sv::msg(exec)] - fn unbond(&self, ctx: ExecCtx, amount: Coin) -> Result, ContractError> { + fn unbond( + &self, + ctx: ExecCtx, + amount: Coin, + ) -> Result, ContractError> { nonpayable(&ctx.info)?; let denom = self.config.load(ctx.deps.storage)?.denom; @@ -187,7 +199,10 @@ impl VaultContract<'_> { user.collateral -= amount.amount; self.users.save(ctx.deps.storage, &ctx.info.sender, &user)?; let amt = amount.amount; - let msg = ProviderMsg::Unbond { delegator: ctx.info.sender.clone().into_string(), amount}; + let msg = ProviderMsg::Unbond { + delegator: ctx.info.sender.clone().into_string(), + amount, + }; let resp = Response::new() .add_message(msg) .add_attribute("action", "unbond") @@ -492,7 +507,11 @@ impl VaultContract<'_> { } #[sv::msg(reply)] - fn reply(&self, ctx: ReplyCtx, reply: Reply) -> Result, ContractError> { + fn reply( + &self, + ctx: ReplyCtx, + reply: Reply, + ) -> Result, ContractError> { match reply.id { REPLY_ID_INSTANTIATE => self.reply_init_callback(ctx.deps, reply.result.unwrap()), _ => Err(ContractError::InvalidReplyId(reply.id)), @@ -1085,7 +1104,11 @@ impl VaultApi for VaultContract<'_> { Ok(resp) } - fn commit_tx(&self, mut ctx: ExecCtx, tx_id: u64) -> Result, ContractError> { + fn commit_tx( + &self, + mut ctx: ExecCtx, + tx_id: u64, + ) -> Result, ContractError> { self.commit_stake(&mut ctx, tx_id)?; let resp = Response::new() @@ -1096,7 +1119,11 @@ impl VaultApi for VaultContract<'_> { Ok(resp) } - fn rollback_tx(&self, mut ctx: ExecCtx, tx_id: u64) -> Result, ContractError> { + fn rollback_tx( + &self, + mut ctx: ExecCtx, + tx_id: u64, + ) -> Result, ContractError> { self.rollback_stake(&mut ctx, tx_id)?; let resp = Response::new() diff --git a/contracts/provider/vault/src/lib.rs b/contracts/provider/vault/src/lib.rs index 1876926d..e9948f04 100644 --- a/contracts/provider/vault/src/lib.rs +++ b/contracts/provider/vault/src/lib.rs @@ -1,8 +1,8 @@ pub mod contract; pub mod error; +pub mod mock; pub mod msg; #[cfg(test)] pub mod multitest; -pub mod mock; mod state; pub mod txs; diff --git a/contracts/provider/vault/src/mock.rs b/contracts/provider/vault/src/mock.rs index 82235b7b..e1ea1642 100644 --- a/contracts/provider/vault/src/mock.rs +++ b/contracts/provider/vault/src/mock.rs @@ -1,5 +1,6 @@ use cosmwasm_std::{ - coin, ensure, Addr, BankMsg, Binary, Coin, Decimal, DepsMut, Empty, Fraction, Order, Reply, Response, StdResult, Storage, SubMsg, SubMsgResponse, Uint128, WasmMsg + coin, ensure, Addr, BankMsg, Binary, Coin, Decimal, DepsMut, Empty, Fraction, Order, Reply, + Response, StdResult, Storage, SubMsg, SubMsgResponse, Uint128, WasmMsg, }; use cw2::set_contract_version; use cw_storage_plus::{Bounder, Item, Map}; diff --git a/packages/apis/src/vault_api.rs b/packages/apis/src/vault_api.rs index 8e2a4d08..b2c60207 100644 --- a/packages/apis/src/vault_api.rs +++ b/packages/apis/src/vault_api.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{to_json_binary, Addr, Coin, Response, StdError, Uint128, CustomMsg, WasmMsg}; +use cosmwasm_std::{to_json_binary, Addr, Coin, CustomMsg, Response, StdError, Uint128, WasmMsg}; use sylvia::types::ExecCtx; use sylvia::{interface, schemars}; diff --git a/packages/bindings/src/lib.rs b/packages/bindings/src/lib.rs index 880e1b16..45c3ba81 100644 --- a/packages/bindings/src/lib.rs +++ b/packages/bindings/src/lib.rs @@ -1,7 +1,7 @@ mod msg; mod query; -pub use msg::{VirtualStakeCustomMsg, VirtualStakeMsg, ProviderCustomMsg, ProviderMsg}; +pub use msg::{ProviderCustomMsg, ProviderMsg, VirtualStakeCustomMsg, VirtualStakeMsg}; pub use query::{ BondStatusResponse, SlashRatioResponse, TokenQuerier, VirtualStakeCustomQuery, VirtualStakeQuery, diff --git a/packages/bindings/src/msg.rs b/packages/bindings/src/msg.rs index fde80955..2b33bb3d 100644 --- a/packages/bindings/src/msg.rs +++ b/packages/bindings/src/msg.rs @@ -117,7 +117,7 @@ impl ProviderMsg { amount: coin, } } - + pub fn unstake(denom: &str, validator: &str, amount: impl Into) -> ProviderMsg { let coin = Coin { amount: amount.into(), From 5d9d7f9811d83dad26080dc10d42de3315826b7e Mon Sep 17 00:00:00 2001 From: Trinity Date: Tue, 27 Aug 2024 12:49:14 +0700 Subject: [PATCH 5/6] Fix logic for immediate unbonding --- contracts/provider/native-staking-proxy/src/contract.rs | 2 +- packages/bindings/src/msg.rs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/contracts/provider/native-staking-proxy/src/contract.rs b/contracts/provider/native-staking-proxy/src/contract.rs index 8cbc9f9b..e362ee8c 100644 --- a/contracts/provider/native-staking-proxy/src/contract.rs +++ b/contracts/provider/native-staking-proxy/src/contract.rs @@ -294,7 +294,7 @@ impl NativeStakingProxyContract<'_> { ContractError::InvalidDenom(amount.denom) ); - let msg = ProviderMsg::Unstake { validator, amount }; + let msg = ProviderMsg::Unstake { delegator: ctx.info.sender.to_string(), validator, amount }; Ok(Response::new().add_message(msg)) } diff --git a/packages/bindings/src/msg.rs b/packages/bindings/src/msg.rs index 2b33bb3d..01381610 100644 --- a/packages/bindings/src/msg.rs +++ b/packages/bindings/src/msg.rs @@ -92,7 +92,7 @@ pub enum ProviderMsg { /// /// If these conditions are met, it will instantly unstake /// amount.amount tokens from the native staking proxy contract. - Unstake { validator: String, amount: Coin }, + Unstake { delegator: String, validator: String, amount: Coin }, } impl ProviderMsg { @@ -118,12 +118,13 @@ impl ProviderMsg { } } - pub fn unstake(denom: &str, validator: &str, amount: impl Into) -> ProviderMsg { + pub fn unstake(denom: &str, delegator:&str, validator: &str, amount: impl Into) -> ProviderMsg { let coin = Coin { amount: amount.into(), denom: denom.into(), }; ProviderMsg::Unstake { + delegator: delegator.to_string(), validator: validator.to_string(), amount: coin, } From c77d510eb7a71572e49c0ff39f60fc63927fb0a5 Mon Sep 17 00:00:00 2001 From: Trinity Date: Fri, 30 Aug 2024 13:40:46 +0700 Subject: [PATCH 6/6] lint --- .../provider/native-staking-proxy/src/contract.rs | 6 +++++- packages/bindings/src/msg.rs | 13 +++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/contracts/provider/native-staking-proxy/src/contract.rs b/contracts/provider/native-staking-proxy/src/contract.rs index e362ee8c..fcbc52c8 100644 --- a/contracts/provider/native-staking-proxy/src/contract.rs +++ b/contracts/provider/native-staking-proxy/src/contract.rs @@ -294,7 +294,11 @@ impl NativeStakingProxyContract<'_> { ContractError::InvalidDenom(amount.denom) ); - let msg = ProviderMsg::Unstake { delegator: ctx.info.sender.to_string(), validator, amount }; + let msg = ProviderMsg::Unstake { + delegator: ctx.info.sender.to_string(), + validator, + amount, + }; Ok(Response::new().add_message(msg)) } diff --git a/packages/bindings/src/msg.rs b/packages/bindings/src/msg.rs index 01381610..cedb0767 100644 --- a/packages/bindings/src/msg.rs +++ b/packages/bindings/src/msg.rs @@ -92,7 +92,11 @@ pub enum ProviderMsg { /// /// If these conditions are met, it will instantly unstake /// amount.amount tokens from the native staking proxy contract. - Unstake { delegator: String, validator: String, amount: Coin }, + Unstake { + delegator: String, + validator: String, + amount: Coin, + }, } impl ProviderMsg { @@ -118,7 +122,12 @@ impl ProviderMsg { } } - pub fn unstake(denom: &str, delegator:&str, validator: &str, amount: impl Into) -> ProviderMsg { + pub fn unstake( + denom: &str, + delegator: &str, + validator: &str, + amount: impl Into, + ) -> ProviderMsg { let coin = Coin { amount: amount.into(), denom: denom.into(),