diff --git a/Cargo.lock b/Cargo.lock index 0df67f14..b2f0dab3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -771,6 +771,7 @@ dependencies = [ "cw2", "derivative", "mesh-apis", + "mesh-bindings", "mesh-external-staking", "mesh-native-staking", "mesh-native-staking-proxy", diff --git a/contracts/provider/external-staking/src/multitest.rs b/contracts/provider/external-staking/src/multitest.rs index 6893cb49..ed8298a2 100644 --- a/contracts/provider/external-staking/src/multitest.rs +++ b/contracts/provider/external-staking/src/multitest.rs @@ -3,22 +3,21 @@ mod utils; use anyhow::Result as AnyResult; 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_vault::contract::sv::mt::CodeId as VaultCodeId; -use mesh_vault::contract::VaultContract; +use mesh_vault::mock::sv::mt::{CodeId as VaultCodeId, VaultMockProxy}; +use mesh_vault::mock::VaultMock; use mesh_vault::msg::{LocalStakingInfo, StakingInitInfo}; use mesh_sync::ValueRange; -use cw_multi_test::App as MtApp; use sylvia::multitest::{App, Proxy}; use crate::contract::sv::mt::ExternalStakingContractProxy; use crate::test_methods::sv::mt::TestMethodsProxy; use mesh_apis::cross_staking_api::sv::mt::CrossStakingApiProxy; -use mesh_vault::contract::sv::mt::VaultContractProxy; use crate::contract::sv::mt::CodeId; use crate::contract::ExternalStakingContract; @@ -47,7 +46,7 @@ fn setup<'app>( owner: &'app str, unbond_period: u64, ) -> AnyResult<( - Proxy<'app, MtApp, VaultContract<'app>>, + Proxy<'app, MtApp, VaultMock<'app>>, Proxy<'app, MtApp, ExternalStakingContract<'app>>, )> { let native_staking_proxy_code = NativeStakingProxyCodeId::store_code(app); diff --git a/contracts/provider/external-staking/src/multitest/utils.rs b/contracts/provider/external-staking/src/multitest/utils.rs index 038c1455..7d9a8e3b 100644 --- a/contracts/provider/external-staking/src/multitest/utils.rs +++ b/contracts/provider/external-staking/src/multitest/utils.rs @@ -2,7 +2,7 @@ use cosmwasm_std::{to_json_binary, Addr, Coin}; use cw_multi_test::{App as MtApp, AppResponse}; use mesh_apis::{converter_api::RewardInfo, ibc::AddValidator}; use mesh_sync::Tx; -use mesh_vault::contract::{sv::mt::VaultContractProxy, VaultContract}; +use mesh_vault::mock::{sv::mt::VaultMockProxy, VaultMock}; use sylvia::multitest::{App, Proxy}; use crate::{ @@ -59,7 +59,7 @@ impl AppExt for App { } } -type Vault<'app> = Proxy<'app, MtApp, VaultContract<'app>>; +type Vault<'app> = Proxy<'app, MtApp, VaultMock<'app>>; type Contract<'app> = Proxy<'app, MtApp, ExternalStakingContract<'app>>; pub(crate) trait ContractExt { diff --git a/contracts/provider/native-staking-proxy/src/multitest.rs b/contracts/provider/native-staking-proxy/src/multitest.rs index 9dc18b44..90baa916 100644 --- a/contracts/provider/native-staking-proxy/src/multitest.rs +++ b/contracts/provider/native-staking-proxy/src/multitest.rs @@ -4,11 +4,10 @@ use cosmwasm_std::testing::mock_env; use cosmwasm_std::{coin, coins, to_json_binary, Addr, Decimal, Validator}; use cw_multi_test::{App as MtApp, StakingInfo}; - use sylvia::multitest::{App, Proxy}; -use mesh_vault::contract::sv::mt::VaultContractProxy; -use mesh_vault::contract::VaultContract; +use mesh_vault::mock::sv::mt::VaultMockProxy; +use mesh_vault::mock::VaultMock; use mesh_vault::msg::LocalStakingInfo; use crate::contract; @@ -60,8 +59,8 @@ fn setup<'app>( owner: &'app str, user: &str, validators: &[&str], -) -> AnyResult>> { - let vault_code = mesh_vault::contract::sv::mt::CodeId::store_code(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); diff --git a/contracts/provider/native-staking/src/multitest.rs b/contracts/provider/native-staking/src/multitest.rs index 375b1201..fde0c43a 100644 --- a/contracts/provider/native-staking/src/multitest.rs +++ b/contracts/provider/native-staking/src/multitest.rs @@ -11,7 +11,7 @@ use mesh_native_staking_proxy::contract::sv::mt::{ }; use mesh_native_staking_proxy::contract::NativeStakingProxyContract; use mesh_sync::ValueRange; -use mesh_vault::contract::sv::mt::VaultContractProxy; +use mesh_vault::mock::sv::mt::VaultMockProxy; use mesh_vault::msg::LocalStakingInfo; use crate::contract; @@ -256,7 +256,7 @@ fn releasing_proxy_stake() { let app = app(&[(user, (300, OSMO))], &[validator]); // Contracts setup - let vault_code = mesh_vault::contract::sv::mt::CodeId::store_code(&app); + let vault_code = mesh_vault::mock::sv::mt::CodeId::store_code(&app); let staking_code = contract::sv::mt::CodeId::store_code(&app); let staking_proxy_code = NativeStakingProxyCodeId::store_code(&app); diff --git a/contracts/provider/vault/Cargo.toml b/contracts/provider/vault/Cargo.toml index e5fafea8..9969f529 100644 --- a/contracts/provider/vault/Cargo.toml +++ b/contracts/provider/vault/Cargo.toml @@ -21,6 +21,7 @@ mt = ["library", "sylvia/mt"] [dependencies] mesh-apis = { workspace = true } mesh-sync = { workspace = true } +mesh-bindings = { workspace = true } sylvia = { workspace = true } cosmwasm-schema = { workspace = true } diff --git a/contracts/provider/vault/src/contract.rs b/contracts/provider/vault/src/contract.rs index 04a0d0e6..cf415057 100644 --- a/contracts/provider/vault/src/contract.rs +++ b/contracts/provider/vault/src/contract.rs @@ -1,5 +1,5 @@ use cosmwasm_std::{ - coin, ensure, Addr, BankMsg, Binary, Coin, Decimal, DepsMut, Fraction, Order, Reply, Response, + coin, ensure, Addr, Binary, Coin, Decimal, DepsMut, Fraction, Order, Reply, Response, StdResult, Storage, SubMsg, SubMsgResponse, Uint128, WasmMsg, }; use cw2::set_contract_version; @@ -12,8 +12,10 @@ use mesh_apis::local_staking_api::{ sv::LocalStakingApiQueryMsg, LocalStakingApiHelper, SlashRatioResponse, }; use mesh_apis::vault_api::{self, SlashInfo, VaultApi}; +use mesh_bindings::{ProviderCustomMsg, ProviderMsg}; use mesh_sync::Tx::InFlightStaking; use mesh_sync::{max_range, ValueRange}; + use sylvia::types::{ExecCtx, InstantiateCtx, QueryCtx, ReplyCtx}; use sylvia::{contract, schemars}; @@ -66,6 +68,8 @@ pub struct VaultContract<'a> { #[contract] #[sv::error(ContractError)] #[sv::messages(vault_api as VaultApi)] +/// Workaround for lack of support in communication `Empty` <-> `Custom` Contracts. +#[sv::custom(msg=ProviderCustomMsg)] impl VaultContract<'_> { pub fn new() -> Self { Self { @@ -91,7 +95,7 @@ impl VaultContract<'_> { ctx: InstantiateCtx, denom: String, local_staking: Option, - ) -> Result { + ) -> Result, ContractError> { nonpayable(&ctx.info)?; let config = Config { denom }; @@ -140,31 +144,45 @@ impl VaultContract<'_> { } #[sv::msg(exec)] - fn bond(&self, ctx: ExecCtx) -> Result { + fn bond( + &self, + ctx: ExecCtx, + amount: Coin, + ) -> Result, ContractError> { + nonpayable(&ctx.info)?; + let denom = self.config.load(ctx.deps.storage)?.denom; - let amount = must_pay(&ctx.info, &denom)?; + ensure!(denom == amount.denom, ContractError::UnexpectedDenom(denom)); let mut user = self .users .may_load(ctx.deps.storage, &ctx.info.sender)? .unwrap_or_default(); - user.collateral += amount; + 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 resp = Response::new() - .add_attribute("action", "bond") + .add_message(msg) + .add_attribute("action", "unbond") .add_attribute("sender", ctx.info.sender) - .add_attribute("amount", amount.to_string()); + .add_attribute("amount", amt.to_string()); Ok(resp) } #[sv::msg(exec)] - fn unbond(&self, ctx: ExecCtx, amount: Coin) -> Result { + fn unbond( + &self, + ctx: ExecCtx, + amount: Coin, + ) -> Result, ContractError> { nonpayable(&ctx.info)?; let denom = self.config.load(ctx.deps.storage)?.denom; - ensure!(denom == amount.denom, ContractError::UnexpectedDenom(denom)); let mut user = self @@ -180,17 +198,16 @@ impl VaultContract<'_> { user.collateral -= amount.amount; self.users.save(ctx.deps.storage, &ctx.info.sender, &user)?; - - let msg = BankMsg::Send { - to_address: ctx.info.sender.to_string(), - amount: vec![amount.clone()], + let amt = amount.amount; + let msg = ProviderMsg::Unbond { + delegator: ctx.info.sender.clone().into_string(), + amount, }; - let resp = Response::new() .add_message(msg) .add_attribute("action", "unbond") .add_attribute("sender", ctx.info.sender) - .add_attribute("amount", amount.to_string()); + .add_attribute("amount", amt.to_string()); Ok(resp) } @@ -206,7 +223,7 @@ impl VaultContract<'_> { amount: Coin, // action to take with that stake msg: Binary, - ) -> Result { + ) -> Result, ContractError> { nonpayable(&ctx.info)?; let config = self.config.load(ctx.deps.storage)?; @@ -253,7 +270,7 @@ impl VaultContract<'_> { amount: Coin, // action to take with that stake msg: Binary, - ) -> Result { + ) -> Result, ContractError> { nonpayable(&ctx.info)?; let config = self.config.load(ctx.deps.storage)?; @@ -490,7 +507,11 @@ impl VaultContract<'_> { } #[sv::msg(reply)] - fn reply(&self, ctx: ReplyCtx, reply: Reply) -> Result { + 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)), @@ -501,7 +522,7 @@ impl VaultContract<'_> { &self, deps: DepsMut, reply: SubMsgResponse, - ) -> Result { + ) -> Result, ContractError> { let init_data = parse_instantiate_response_data(&reply.data.unwrap())?; let local_staking = Addr::unchecked(init_data.contract_address); @@ -981,6 +1002,7 @@ impl Default for VaultContract<'_> { impl VaultApi for VaultContract<'_> { type Error = ContractError; + type ExecC = ProviderCustomMsg; /// This must be called by the remote staking contract to release this claim fn release_cross_stake( @@ -990,7 +1012,7 @@ impl VaultApi for VaultContract<'_> { owner: String, // amount to unstake on that contract amount: Coin, - ) -> Result { + ) -> Result, ContractError> { nonpayable(&ctx.info)?; self.unstake(&mut ctx, owner.clone(), amount.clone())?; @@ -1011,7 +1033,7 @@ impl VaultApi for VaultContract<'_> { mut ctx: ExecCtx, // address of the user who originally called stake_remote owner: String, - ) -> Result { + ) -> Result, ContractError> { let denom = self.config.load(ctx.deps.storage)?.denom; let amount = must_pay(&ctx.info, &denom)?; @@ -1032,7 +1054,7 @@ impl VaultApi for VaultContract<'_> { mut ctx: ExecCtx, slashes: Vec, validator: String, - ) -> Result { + ) -> Result, Self::Error> { nonpayable(&ctx.info)?; let msgs = self.slash(&mut ctx, &slashes, &validator)?; @@ -1060,7 +1082,7 @@ impl VaultApi for VaultContract<'_> { mut ctx: ExecCtx, slashes: Vec, validator: String, - ) -> Result { + ) -> Result, Self::Error> { nonpayable(&ctx.info)?; let msgs = self.slash(&mut ctx, &slashes, &validator)?; @@ -1082,7 +1104,11 @@ impl VaultApi for VaultContract<'_> { Ok(resp) } - fn commit_tx(&self, mut ctx: ExecCtx, tx_id: u64) -> Result { + fn commit_tx( + &self, + mut ctx: ExecCtx, + tx_id: u64, + ) -> Result, ContractError> { self.commit_stake(&mut ctx, tx_id)?; let resp = Response::new() @@ -1093,7 +1119,11 @@ impl VaultApi for VaultContract<'_> { Ok(resp) } - fn rollback_tx(&self, mut ctx: ExecCtx, tx_id: u64) -> Result { + 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 77d7a460..e9948f04 100644 --- a/contracts/provider/vault/src/lib.rs +++ b/contracts/provider/vault/src/lib.rs @@ -1,7 +1,8 @@ pub mod contract; pub mod error; +pub mod mock; pub mod msg; #[cfg(test)] -mod multitest; +pub mod multitest; mod state; pub mod txs; diff --git a/contracts/provider/vault/src/mock.rs b/contracts/provider/vault/src/mock.rs new file mode 100644 index 00000000..e1ea1642 --- /dev/null +++ b/contracts/provider/vault/src/mock.rs @@ -0,0 +1,1085 @@ +use cosmwasm_std::{ + 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}; +use cw_utils::{must_pay, nonpayable, parse_instantiate_response_data}; +use std::cmp::min; + +use mesh_apis::cross_staking_api::CrossStakingApiHelper; +use mesh_apis::local_staking_api::{ + sv::LocalStakingApiQueryMsg, LocalStakingApiHelper, SlashRatioResponse, +}; +use mesh_apis::vault_api::{self, SlashInfo, VaultApi}; +use mesh_sync::Tx::InFlightStaking; +use mesh_sync::{max_range, ValueRange}; +use sylvia::types::{ExecCtx, InstantiateCtx, QueryCtx, ReplyCtx}; +use sylvia::{contract, schemars}; + +use crate::contract::{ + CONTRACT_NAME, CONTRACT_VERSION, DEFAULT_PAGE_LIMIT, MAX_PAGE_LIMIT, REPLY_ID_INSTANTIATE, +}; +use crate::error::ContractError; +use crate::msg::{ + AccountClaimsResponse, AccountDetailsResponse, AccountResponse, AllAccountsResponse, + AllAccountsResponseItem, AllActiveExternalStakingResponse, AllTxsResponse, AllTxsResponseItem, + ConfigResponse, LienResponse, LocalStakingInfo, TxResponse, +}; +use crate::state::{Config, Lien, LocalStaking, UserInfo}; +use crate::txs::Txs; + +fn clamp_page_limit(limit: Option) -> usize { + limit.unwrap_or(DEFAULT_PAGE_LIMIT).max(MAX_PAGE_LIMIT) as usize +} + +fn def_false() -> bool { + false +} + +/// This is a stub implementation of the virtual staking contract, for test purposes only. +pub struct VaultMock<'a> { + pub config: Item<'a, Config>, + pub local_staking: Item<'a, Option>, + pub liens: Map<'a, (&'a Addr, &'a Addr), Lien>, + pub users: Map<'a, &'a Addr, UserInfo>, + pub active_external: Map<'a, &'a Addr, ()>, + pub tx_count: Item<'a, u64>, + pub pending: Txs<'a>, +} + +#[contract] +#[sv::error(ContractError)] +#[sv::messages(vault_api as VaultApi)] +impl VaultMock<'_> { + pub fn new() -> Self { + Self { + config: Item::new("config"), + local_staking: Item::new("local_staking"), + liens: Map::new("liens"), + users: Map::new("users"), + pending: Txs::new("pending_txs", "users"), + tx_count: Item::new("tx_count"), + active_external: Map::new("active_external"), + } + } + + pub fn next_tx_id(&self, store: &mut dyn Storage) -> StdResult { + let id: u64 = self.tx_count.may_load(store)?.unwrap_or_default() + 1; + self.tx_count.save(store, &id)?; + Ok(id) + } + + #[sv::msg(instantiate)] + pub fn instantiate( + &self, + ctx: InstantiateCtx, + denom: String, + local_staking: Option, + ) -> Result { + nonpayable(&ctx.info)?; + + let config = Config { denom }; + self.config.save(ctx.deps.storage, &config)?; + set_contract_version(ctx.deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + if let Some(local_staking) = local_staking { + match local_staking { + LocalStakingInfo::Existing(exist) => { + let addr = exist.existing; + + // Query for max slashing percentage + let query = LocalStakingApiQueryMsg::MaxSlash {}; + let SlashRatioResponse { + slash_ratio_dsign, .. + } = ctx.deps.querier.query_wasm_smart(&addr, &query)?; + + let local_staking = LocalStaking { + contract: LocalStakingApiHelper(ctx.deps.api.addr_validate(&addr)?), + max_slash: slash_ratio_dsign, + }; + + self.local_staking + .save(ctx.deps.storage, &Some(local_staking))?; + Ok(Response::new()) + } + LocalStakingInfo::New(local_staking) => { + let msg = WasmMsg::Instantiate { + admin: local_staking.admin, + code_id: local_staking.code_id, + msg: local_staking.msg, + funds: vec![], + label: local_staking + .label + .unwrap_or_else(|| "Mesh Security Local Staking".to_string()), + }; + let sub_msg = SubMsg::reply_on_success(msg, REPLY_ID_INSTANTIATE); + Ok(Response::new().add_submessage(sub_msg)) + } + } + } else { + self.local_staking.save(ctx.deps.storage, &None)?; + Ok(Response::new()) + } + } + + #[sv::msg(exec)] + fn bond(&self, ctx: ExecCtx) -> Result { + let denom = self.config.load(ctx.deps.storage)?.denom; + let amount = must_pay(&ctx.info, &denom)?; + + let mut user = self + .users + .may_load(ctx.deps.storage, &ctx.info.sender)? + .unwrap_or_default(); + user.collateral += amount; + self.users.save(ctx.deps.storage, &ctx.info.sender, &user)?; + + let resp = Response::new() + .add_attribute("action", "bond") + .add_attribute("sender", ctx.info.sender) + .add_attribute("amount", amount.to_string()); + + Ok(resp) + } + + #[sv::msg(exec)] + fn unbond(&self, ctx: ExecCtx, amount: Coin) -> Result { + nonpayable(&ctx.info)?; + + let denom = self.config.load(ctx.deps.storage)?.denom; + + ensure!(denom == amount.denom, ContractError::UnexpectedDenom(denom)); + + let mut user = self + .users + .may_load(ctx.deps.storage, &ctx.info.sender)? + .unwrap_or_default(); + + let free_collateral = user.free_collateral(); + ensure!( + free_collateral.low() >= amount.amount, + ContractError::ClaimsLocked(free_collateral) + ); + + user.collateral -= amount.amount; + self.users.save(ctx.deps.storage, &ctx.info.sender, &user)?; + + let msg = BankMsg::Send { + to_address: ctx.info.sender.to_string(), + amount: vec![amount.clone()], + }; + + let resp = Response::new() + .add_message(msg) + .add_attribute("action", "unbond") + .add_attribute("sender", ctx.info.sender) + .add_attribute("amount", amount.to_string()); + + Ok(resp) + } + + /// This assigns a claim of amount tokens to the remote contract, which can take some action with it + #[sv::msg(exec)] + fn stake_remote( + &self, + mut ctx: ExecCtx, + // address of the contract to virtually stake on + contract: String, + // amount to stake on that contract + amount: Coin, + // action to take with that stake + msg: Binary, + ) -> Result { + nonpayable(&ctx.info)?; + + let config = self.config.load(ctx.deps.storage)?; + let contract = ctx.deps.api.addr_validate(&contract)?; + let contract = CrossStakingApiHelper(contract); + let slashable = contract.max_slash(ctx.deps.as_ref())?; + + let tx_id = self.stake( + &mut ctx, + &config, + &contract.0, + slashable.slash_ratio_dsign, + amount.clone(), + true, + )?; + + let stake_msg = contract.receive_virtual_stake( + ctx.info.sender.to_string(), + amount.clone(), + tx_id, + msg, + vec![], + )?; + + self.active_external + .save(ctx.deps.storage, &contract.0, &())?; + + let resp = Response::new() + .add_message(stake_msg) + .add_attribute("action", "stake_remote") + .add_attribute("sender", ctx.info.sender) + .add_attribute("amount", amount.amount.to_string()) + .add_attribute("tx_id", tx_id.to_string()); + + Ok(resp) + } + + /// This sends actual tokens to the local staking contract + #[sv::msg(exec)] + fn stake_local( + &self, + mut ctx: ExecCtx, + // amount to stake on that contract + amount: Coin, + // action to take with that stake + msg: Binary, + ) -> Result { + nonpayable(&ctx.info)?; + + let config = self.config.load(ctx.deps.storage)?; + if let Some(local_staking) = self.local_staking.load(ctx.deps.storage)? { + self.stake( + &mut ctx, + &config, + &local_staking.contract.0, + local_staking.max_slash, + amount.clone(), + false, + )?; + + let stake_msg = local_staking.contract.receive_stake( + ctx.info.sender.to_string(), + msg, + vec![amount.clone()], + )?; + + let resp = Response::new() + .add_message(stake_msg) + .add_attribute("action", "stake_local") + .add_attribute("sender", ctx.info.sender) + .add_attribute("amount", amount.amount.to_string()); + + Ok(resp) + } else { + Err(ContractError::NoLocalStaking) + } + } + + #[sv::msg(query)] + fn account(&self, ctx: QueryCtx, account: String) -> Result { + let denom = self.config.load(ctx.deps.storage)?.denom; + let account = ctx.deps.api.addr_validate(&account)?; + + let user = self + .users + .may_load(ctx.deps.storage, &account)? + .unwrap_or_default(); + Ok(AccountResponse { + denom, + bonded: user.collateral, + free: user.free_collateral(), + }) + } + + #[sv::msg(query)] + fn account_details( + &self, + ctx: QueryCtx, + account: String, + ) -> Result { + let denom = self.config.load(ctx.deps.storage)?.denom; + let account = ctx.deps.api.addr_validate(&account)?; + + let user = self + .users + .may_load(ctx.deps.storage, &account)? + .unwrap_or_default(); + Ok(AccountDetailsResponse { + denom, + bonded: user.collateral, + free: user.free_collateral(), + max_lien: user.max_lien, + total_slashable: user.total_slashable, + }) + } + + #[sv::msg(query)] + fn config(&self, ctx: QueryCtx) -> Result { + let config = self.config.load(ctx.deps.storage)?; + let local_staking = self.local_staking.load(ctx.deps.storage)?; + + let resp = ConfigResponse { + denom: config.denom, + local_staking: local_staking.map(|ls| ls.contract.0.into()), + }; + + Ok(resp) + } + + #[sv::msg(query)] + fn active_external_staking( + &self, + ctx: QueryCtx, + ) -> Result { + let active = self + .active_external + .keys(ctx.deps.storage, None, None, Order::Ascending) + .collect::>>()?; + + let resp = AllActiveExternalStakingResponse { + contracts: active.into_iter().map(|addr| addr.to_string()).collect(), + }; + + Ok(resp) + } + + /// Returns a single claim between the user and lienholder + #[sv::msg(query)] + fn claim( + &self, + ctx: QueryCtx, + account: String, + lienholder: String, + ) -> Result { + let account = ctx.deps.api.addr_validate(&account)?; + let lienholder = ctx.deps.api.addr_validate(&lienholder)?; + + self.liens + .may_load(ctx.deps.storage, (&account, &lienholder))? + .ok_or(ContractError::NoClaim) + } + + /// Returns paginated claims list for an user + /// + /// `start_after` is a last lienholder of the previous page, and it will not be included + #[sv::msg(query)] + fn account_claims( + &self, + ctx: QueryCtx, + account: String, + start_after: Option, + limit: Option, + ) -> Result { + let limit = clamp_page_limit(limit); + let start_after = start_after.map(Addr::unchecked); + let bound = start_after.as_ref().and_then(Bounder::exclusive_bound); + + let account = Addr::unchecked(account); + let claims = self + .liens + .prefix(&account) + .range(ctx.deps.storage, bound, None, Order::Ascending) + .map(|item| { + let (lienholder, lien) = item?; + Ok::<_, ContractError>(LienResponse { + lienholder: lienholder.to_string(), + amount: lien.amount, + }) + }) + .take(limit) + .collect::>()?; + + let resp = AccountClaimsResponse { claims }; + + Ok(resp) + } + + /// Queries for all users ever performing action in the system, paginating over + /// them. + /// + /// `start_after` is the last account included in previous page + /// + /// `with_collateral` flag filters out users with no collateral, defaulted to false + #[sv::msg(query)] + fn all_accounts( + &self, + ctx: QueryCtx, + #[serde(default = "def_false")] with_collateral: bool, + start_after: Option, + limit: Option, + ) -> Result { + let limit = clamp_page_limit(limit); + let start_after = start_after.map(Addr::unchecked); + let bound = start_after.as_ref().and_then(Bounder::exclusive_bound); + + let denom = self.config.load(ctx.deps.storage)?.denom; + + let accounts: Vec<_> = self + .users + .range(ctx.deps.storage, bound, None, Order::Ascending) + .filter(|account| { + account + .as_ref() + .map(|(_, account)| { + !with_collateral || !account.collateral.is_zero() // Skip zero collateral + }) + .unwrap_or(false) // Skip other errors + }) + .map(|account| { + account.map(|(addr, account)| AllAccountsResponseItem { + user: addr.to_string(), + account: AccountResponse { + denom: denom.clone(), + bonded: account.collateral, + free: account.free_collateral(), + }, + }) + }) + .take(limit) + .collect::>()?; + + let resp = AllAccountsResponse { accounts }; + + Ok(resp) + } + + /// Queries a pending tx. + #[sv::msg(query)] + fn pending_tx(&self, ctx: QueryCtx, tx_id: u64) -> Result { + let resp = self.pending.txs.load(ctx.deps.storage, tx_id)?; + Ok(resp) + } + + #[sv::msg(query)] + fn all_pending_txs_desc( + &self, + ctx: QueryCtx, + start_after: Option, + limit: Option, + ) -> Result { + let limit = clamp_page_limit(limit); + let bound = start_after.and_then(Bounder::exclusive_bound); + + let txs = self + .pending + .txs + .range(ctx.deps.storage, None, bound, Order::Descending) + .map(|item| { + let (_id, tx) = item?; + Ok::(tx) + }) + .take(limit) + .collect::>()?; + + let resp = AllTxsResponse { txs }; + + Ok(resp) + } + + #[sv::msg(reply)] + fn reply(&self, ctx: ReplyCtx, reply: Reply) -> Result { + match reply.id { + REPLY_ID_INSTANTIATE => self.reply_init_callback(ctx.deps, reply.result.unwrap()), + _ => Err(ContractError::InvalidReplyId(reply.id)), + } + } + + fn reply_init_callback( + &self, + deps: DepsMut, + reply: SubMsgResponse, + ) -> Result { + let init_data = parse_instantiate_response_data(&reply.data.unwrap())?; + let local_staking = Addr::unchecked(init_data.contract_address); + + // As we control the local staking contract it might be better to just raw-query it + // on demand instead of duplicating the data. + let query = LocalStakingApiQueryMsg::MaxSlash {}; + let SlashRatioResponse { + slash_ratio_dsign, .. + } = deps.querier.query_wasm_smart(&local_staking, &query)?; + + let local_staking = LocalStaking { + contract: LocalStakingApiHelper(local_staking), + max_slash: slash_ratio_dsign, + }; + + self.local_staking + .save(deps.storage, &Some(local_staking))?; + + Ok(Response::new()) + } + + pub fn stake( + &self, + ctx: &mut ExecCtx, + config: &Config, + lienholder: &Addr, + slashable: Decimal, + amount: Coin, + remote: bool, + ) -> Result { + ensure!( + amount.denom == config.denom, + ContractError::UnexpectedDenom(config.denom.clone()) + ); + + let amount = amount.amount; + let mut lien = self + .liens + .may_load(ctx.deps.storage, (&ctx.info.sender, lienholder))? + .unwrap_or_else(|| Lien { + amount: ValueRange::new_val(Uint128::zero()), + slashable, + }); + let mut user = self + .users + .may_load(ctx.deps.storage, &ctx.info.sender)? + .unwrap_or_default(); + if remote { + lien.amount + .prepare_add(amount, user.collateral) + .map_err(|_| ContractError::InsufficentBalance)?; + // Tentative value + user.max_lien = max_range(user.max_lien, lien.amount); + user.total_slashable + .prepare_add(amount * lien.slashable, user.collateral) + .map_err(|_| ContractError::InsufficentBalance)?; + } else { + // Update lien immediately + lien.amount + .add(amount, user.collateral) + .map_err(|_| ContractError::InsufficentBalance)?; + // Update max lien and total slashable immediately + user.max_lien = max_range(user.max_lien, lien.amount); + user.total_slashable + .add(amount * lien.slashable, user.collateral) + .map_err(|_| ContractError::InsufficentBalance)?; + } + + ensure!(user.verify_collateral(), ContractError::InsufficentBalance); + + self.liens + .save(ctx.deps.storage, (&ctx.info.sender, lienholder), &lien)?; + self.users.save(ctx.deps.storage, &ctx.info.sender, &user)?; + let tx_id = if remote { + // Create new tx + let tx_id = self.next_tx_id(ctx.deps.storage)?; + + let new_tx = InFlightStaking { + id: tx_id, + amount, + slashable, + user: ctx.info.sender.clone(), + lienholder: lienholder.clone(), + }; + self.pending.txs.save(ctx.deps.storage, tx_id, &new_tx)?; + tx_id + } else { + 0 + }; + Ok(tx_id) + } + + /// Commits a pending stake + fn commit_stake(&self, ctx: &mut ExecCtx, tx_id: u64) -> Result<(), ContractError> { + // Load tx + let tx = self.pending.txs.load(ctx.deps.storage, tx_id)?; + + // Verify tx comes from the right contract, and is of the right type + ensure!( + match tx.clone() { + InFlightStaking { lienholder, .. } => { + ensure!( + lienholder == ctx.info.sender, + ContractError::WrongContractTx(tx_id, ctx.info.sender.clone()) + ); + true + } + _ => false, + }, + ContractError::WrongTypeTx(tx_id, tx) + ); + + let (tx_amount, tx_user, tx_lienholder) = match tx { + InFlightStaking { + amount, + user, + lienholder, + .. + } => (amount, user, lienholder), + _ => unreachable!(), + }; + + // Load lien + let mut lien = self + .liens + .load(ctx.deps.storage, (&tx_user, &tx_lienholder))?; + // Commit it + lien.amount.commit_add(tx_amount); + // Save it + self.liens + .save(ctx.deps.storage, (&tx_user, &tx_lienholder), &lien)?; + // Load user + let mut user = self.users.load(ctx.deps.storage, &tx_user)?; + // Update max lien definitive value (it depends on the lien's value range) + user.max_lien = max_range(user.max_lien, lien.amount); + // Commit total slashable + user.total_slashable.commit_add(tx_amount * lien.slashable); + // Save it + self.users.save(ctx.deps.storage, &tx_user, &user)?; + + // Remove tx + self.pending.txs.remove(ctx.deps.storage, tx_id)?; + + Ok(()) + } + + /// Rollbacks a pending tx + fn rollback_stake(&self, ctx: &mut ExecCtx, tx_id: u64) -> Result<(), ContractError> { + // Load tx + let tx = self.pending.txs.load(ctx.deps.storage, tx_id)?; + + // Verify tx comes from the right contract, and is of the right type + ensure!( + match tx.clone() { + InFlightStaking { lienholder, .. } => { + ensure!( + lienholder == ctx.info.sender, + ContractError::WrongContractTx(tx_id, ctx.info.sender.clone()) + ); + true + } + _ => false, + }, + ContractError::WrongTypeTx(tx_id, tx) + ); + + let (tx_amount, tx_slashable, tx_user, tx_lienholder) = match tx { + InFlightStaking { + amount, + slashable, + user, + lienholder, + .. + } => (amount, slashable, user, lienholder), + _ => unreachable!(), + }; + + // Load lien + let mut lien = self + .liens + .load(ctx.deps.storage, (&tx_user, &tx_lienholder))?; + // Rollback amount + lien.amount.rollback_add(tx_amount); + if lien.amount.high().u128() == 0 { + // Remove lien if it's empty + self.liens + .remove(ctx.deps.storage, (&tx_user, &tx_lienholder)); + } else { + // Save lien + self.liens + .save(ctx.deps.storage, (&tx_user, &tx_lienholder), &lien)?; + } + + // Load user + let mut user = self.users.load(ctx.deps.storage, &tx_user)?; + // Rollback user's max_lien + + // Max lien has to be recalculated from scratch; the just rolled back lien + // is already written to storage + self.recalculate_max_lien(ctx.deps.storage, &tx_user, &mut user)?; + + user.total_slashable.rollback_add(tx_amount * tx_slashable); + self.users.save(ctx.deps.storage, &tx_user, &user)?; + + // Remove tx + self.pending.txs.remove(ctx.deps.storage, tx_id)?; + Ok(()) + } + + /// Recalculates the max lien for the user + fn recalculate_max_lien( + &self, + storage: &mut dyn Storage, + user: &Addr, + user_info: &mut UserInfo, + ) -> Result<(), ContractError> { + user_info.max_lien = self + .liens + .prefix(user) + .range(storage, None, None, Order::Ascending) + .try_fold(ValueRange::new_val(Uint128::zero()), |max_lien, item| { + let (_, lien) = item?; + Ok::<_, ContractError>(max_range(max_lien, lien.amount)) + })?; + Ok(()) + } + + /// Updates the local stake for unstaking from any contract + /// + /// The unstake (both local and remote) is always called by the staking contract + /// (aka lien_holder), so the `sender` address is used for that. + fn unstake(&self, ctx: &mut ExecCtx, owner: String, amount: Coin) -> Result<(), ContractError> { + let denom = self.config.load(ctx.deps.storage)?.denom; + ensure!(amount.denom == denom, ContractError::UnexpectedDenom(denom)); + let amount = amount.amount; + + let owner = Addr::unchecked(owner); + let mut lien = self + .liens + .may_load(ctx.deps.storage, (&owner, &ctx.info.sender))? + .ok_or(ContractError::UnknownLienholder)?; + + let slashable = lien.slashable; + lien.amount + .sub(amount, Uint128::zero()) + .map_err(|_| ContractError::InsufficientLien)?; + + if lien.amount.high().u128() == 0 { + // Remove lien if it's empty + self.liens + .remove(ctx.deps.storage, (&owner, &ctx.info.sender)); + } else { + // Save lien + self.liens + .save(ctx.deps.storage, (&owner, &ctx.info.sender), &lien)?; + } + + let mut user = self.users.load(ctx.deps.storage, &owner)?; + + // Max lien has to be recalculated from scratch; the just saved lien + // is already written to storage + self.recalculate_max_lien(ctx.deps.storage, &owner, &mut user)?; + + user.total_slashable + .sub(amount * slashable, Uint128::zero())?; + self.users.save(ctx.deps.storage, &owner, &user)?; + + Ok(()) + } + + /// Processes a (remote or local) slashing event. + /// + /// This slashes the users that have funds delegated to the validator involved in the + /// misbehaviour. + /// It also checks that the mesh security invariants are not violated after slashing, + /// i.e. performs slashing propagation across lien holders, for all of the slashed users. + fn slash( + &self, + ctx: &mut ExecCtx, + slashes: &[SlashInfo], + validator: &str, + ) -> Result, ContractError> { + // Process users that belong to lien_holder + let lien_holder = ctx.info.sender.clone(); + let mut msgs = vec![]; + for slash in slashes { + let slash_user = Addr::unchecked(slash.user.clone()); + // User must have a lien with this lien holder + let mut lien = self + .liens + .load(ctx.deps.storage, (&slash_user, &lien_holder))?; + let slash_amount = slash.slash; + let mut user_info = self.users.load(ctx.deps.storage, &slash_user)?; + let new_collateral = user_info.collateral - slash_amount; + + // Slash user + lien.amount.sub(slash_amount, Uint128::zero())?; + // Save lien + self.liens + .save(ctx.deps.storage, (&slash_user, &lien_holder), &lien)?; + // Adjust total slashable and max lien + user_info + .total_slashable + .sub(slash_amount * lien.slashable, Uint128::zero())?; + self.recalculate_max_lien(ctx.deps.storage, &slash_user, &mut user_info)?; + // Get free collateral before adjusting collateral, but after slashing + let free_collateral = user_info.free_collateral().low(); // For simplicity + if free_collateral < slash_amount { + // Check / adjust mesh security invariants according to the new collateral + let burn_msgs = self.propagate_slash( + ctx.deps.storage, + &slash_user, + &mut user_info, + new_collateral, + slash_amount - free_collateral, + &lien_holder, + validator, + )?; + msgs.extend_from_slice(&burn_msgs); + } + // Adjust collateral + user_info.collateral = new_collateral; + // Recompute max lien + self.recalculate_max_lien(ctx.deps.storage, &slash_user, &mut user_info)?; + // Save user info + self.users.save(ctx.deps.storage, &slash_user, &user_info)?; + } + Ok(msgs) + } + + #[allow(clippy::too_many_arguments)] + fn propagate_slash( + &self, + storage: &mut dyn Storage, + user: &Addr, + user_info: &mut UserInfo, + new_collateral: Uint128, + claimed_collateral: Uint128, + slashed_lien_holder: &Addr, + slashed_validator: &str, + ) -> Result, ContractError> { + let denom = self.config.load(storage)?.denom; + let native_staking = self.local_staking.load(storage)?; + let mut msgs = vec![]; + if user_info.max_lien.high() >= user_info.total_slashable.high() { + // Liens adjustment + let broken_liens = self + .liens + .prefix(user) + .range(storage, None, None, Order::Ascending) + .filter(|item| { + item.as_ref() + .map(|(_, lien)| lien.amount.high() > new_collateral) // Skip in range liens + .unwrap_or(false) // Skip other errors + }) + .collect::>>()?; + for (lien_holder, mut lien) in broken_liens { + let new_low_amount = min(lien.amount.low(), new_collateral); + let new_high_amount = min(lien.amount.high(), new_collateral); + // Adjust the user's total slashable amount + let adjust_amount_low = lien.amount.low() - new_low_amount; + let adjust_amount_high = lien.amount.high() - new_high_amount; + user_info.total_slashable = ValueRange::new( + user_info.total_slashable.low() - adjust_amount_low * lien.slashable, + user_info.total_slashable.high() - adjust_amount_high * lien.slashable, + ); + // Keep the invariant over the lien + lien.amount = ValueRange::new(new_low_amount, new_high_amount); + self.liens.save(storage, (user, &lien_holder), &lien)?; + // Remove the required amount from the user's stake + let validator = if lien_holder == slashed_lien_holder { + Some(slashed_validator.to_string()) + } else { + None + }; + let burn_msg = self.burn_stake( + user, + &denom, + &native_staking, + &lien_holder, + adjust_amount_high, // High amount for simplicity + validator, + )?; + msgs.push(burn_msg); + } + } else { + // Total slashable adjustment + let slash_ratio_sum = self + .liens + .prefix(user) + .range(storage, None, None, Order::Ascending) + .try_fold(Decimal::zero(), |sum, item| { + let (_, lien) = item?; + Ok::<_, ContractError>(sum + lien.slashable) + })?; + let round_up = if (claimed_collateral * slash_ratio_sum.inv().unwrap()) + * slash_ratio_sum + != claimed_collateral + { + Uint128::one() + } else { + Uint128::zero() + }; + let sub_amount = claimed_collateral * slash_ratio_sum.inv().unwrap() + round_up; + let all_liens = self + .liens + .prefix(user) + .range(storage, None, None, Order::Ascending) + .collect::>>()?; + for (lien_holder, mut lien) in all_liens { + // Adjust the user's total slashable amount + user_info + .total_slashable + .sub(sub_amount * lien.slashable, Uint128::zero())?; + // Keep the invariant over the lien + lien.amount.sub(sub_amount, Uint128::zero())?; + self.liens.save(storage, (user, &lien_holder), &lien)?; + // Remove the required amount from the user's stake + let validator = if lien_holder == slashed_lien_holder { + Some(slashed_validator.to_string()) + } else { + None + }; + let burn_msg = self.burn_stake( + user, + &denom, + &native_staking, + &lien_holder, + sub_amount, + validator, + )?; + msgs.push(burn_msg); + } + } + Ok(msgs) + } + + fn burn_stake( + &self, + user: &Addr, + denom: &String, + native_staking: &Option, + lien_holder: &Addr, + amount: Uint128, + validator: Option, + ) -> Result { + // Native vs cross staking + let msg = match &native_staking { + Some(local_staking) if local_staking.contract.0 == lien_holder => { + let contract = local_staking.contract.clone(); + contract.burn_stake(user, coin(amount.u128(), denom), validator)? + } + _ => { + let contract = CrossStakingApiHelper(lien_holder.clone()); + contract.burn_virtual_stake(user, coin(amount.u128(), denom), validator)? + } + }; + Ok(msg) + } +} + +impl Default for VaultMock<'_> { + fn default() -> Self { + Self::new() + } +} + +impl VaultApi for VaultMock<'_> { + type Error = ContractError; + type ExecC = Empty; + + /// This must be called by the remote staking contract to release this claim + fn release_cross_stake( + &self, + mut ctx: ExecCtx, + // address of the user who originally called stake_remote + owner: String, + // amount to unstake on that contract + amount: Coin, + ) -> Result, ContractError> { + nonpayable(&ctx.info)?; + + self.unstake(&mut ctx, owner.clone(), amount.clone())?; + + let resp = Response::new() + .add_attribute("action", "release_cross_stake") + .add_attribute("sender", ctx.info.sender) + .add_attribute("owner", owner) + .add_attribute("amount", amount.amount.to_string()); + + Ok(resp) + } + + /// This must be called by the local staking contract to release this claim + /// Amount of tokens unstaked are those included in ctx.info.funds + fn release_local_stake( + &self, + mut ctx: ExecCtx, + // address of the user who originally called stake_remote + owner: String, + ) -> Result, ContractError> { + let denom = self.config.load(ctx.deps.storage)?.denom; + let amount = must_pay(&ctx.info, &denom)?; + + self.unstake(&mut ctx, owner.clone(), coin(amount.u128(), denom))?; + + let resp = Response::new() + .add_attribute("action", "release_cross_stake") + .add_attribute("sender", ctx.info.sender) + .add_attribute("owner", owner) + .add_attribute("amount", amount.to_string()); + + Ok(resp) + } + + /// This must be called by the native staking contract to process a misbehaviour + fn local_slash( + &self, + mut ctx: ExecCtx, + slashes: Vec, + validator: String, + ) -> Result, Self::Error> { + nonpayable(&ctx.info)?; + + let msgs = self.slash(&mut ctx, &slashes, &validator)?; + + let resp = Response::new() + .add_messages(msgs) + .add_attribute("action", "local_slash") + .add_attribute("lien_holder", ctx.info.sender) + .add_attribute("validator", validator.to_string()) + .add_attribute( + "users", + slashes + .iter() + .map(|s| s.user.clone()) + .collect::>() + .join(", "), + ); + + Ok(resp) + } + + /// This must be called by the external staking contract to process a misbehaviour + fn cross_slash( + &self, + mut ctx: ExecCtx, + slashes: Vec, + validator: String, + ) -> Result, Self::Error> { + nonpayable(&ctx.info)?; + + let msgs = self.slash(&mut ctx, &slashes, &validator)?; + + let resp = Response::new() + .add_messages(msgs) + .add_attribute("action", "cross_slash") + .add_attribute("lien_holder", ctx.info.sender) + .add_attribute("validator", validator.to_string()) + .add_attribute( + "users", + slashes + .iter() + .map(|s| s.user.clone()) + .collect::>() + .join(", "), + ); + + Ok(resp) + } + + fn commit_tx( + &self, + mut ctx: ExecCtx, + tx_id: u64, + ) -> Result, ContractError> { + self.commit_stake(&mut ctx, tx_id)?; + + let resp = Response::new() + .add_attribute("action", "commit_tx") + .add_attribute("sender", ctx.info.sender) + .add_attribute("tx_id", tx_id.to_string()); + + Ok(resp) + } + + fn rollback_tx( + &self, + mut ctx: ExecCtx, + tx_id: u64, + ) -> Result, ContractError> { + self.rollback_stake(&mut ctx, tx_id)?; + + let resp = Response::new() + .add_attribute("action", "rollback_tx") + .add_attribute("sender", ctx.info.sender) + .add_attribute("tx_id", tx_id.to_string()); + Ok(resp) + } +} diff --git a/contracts/provider/vault/src/multitest.rs b/contracts/provider/vault/src/multitest.rs index 73e3c33e..9e8b897a 100644 --- a/contracts/provider/vault/src/multitest.rs +++ b/contracts/provider/vault/src/multitest.rs @@ -17,10 +17,10 @@ use sylvia::multitest::{App, Proxy}; use mesh_apis::vault_api::sv::mt::VaultApiProxy; use mesh_external_staking::test_methods::sv::mt::TestMethodsProxy; -use crate::contract; -use crate::contract::sv::mt::VaultContractProxy; -use crate::contract::VaultContract; use crate::error::ContractError; +use crate::mock::sv::mt::CodeId as VaultCodeId; +use crate::mock::sv::mt::VaultMockProxy; +use crate::mock::VaultMock; use crate::msg::{ AccountResponse, AllAccountsResponseItem, AllActiveExternalStakingResponse, LienResponse, LocalStakingInfo, StakingInitInfo, @@ -32,8 +32,6 @@ const STAR: &str = "star"; /// 10% slashing on the remote chain const SLASHING_PERCENTAGE: u64 = 10; -/// Test utils - /// App initialization fn init_app(users: &[&str], amounts: &[u128]) -> App { let app = App::custom(|router, _api, storage| { @@ -89,7 +87,7 @@ fn setup<'app>( slash_percent: u64, unbond_period: u64, ) -> ( - Proxy<'app, MtApp, VaultContract<'app>>, + Proxy<'app, MtApp, VaultMock<'app>>, Proxy<'app, MtApp, NativeStakingContract<'app>>, Proxy<'app, MtApp, ExternalStakingContract<'app>>, ) { @@ -104,7 +102,7 @@ fn setup_without_local_staking<'app>( slash_percent: u64, unbond_period: u64, ) -> ( - Proxy<'app, MtApp, VaultContract<'app>>, + Proxy<'app, MtApp, VaultMock<'app>>, Proxy<'app, MtApp, ExternalStakingContract<'app>>, ) { let (vault, _, external) = setup_inner(app, owner, slash_percent, unbond_period, false); @@ -119,11 +117,11 @@ fn setup_inner<'app>( unbond_period: u64, local_staking: bool, ) -> ( - Proxy<'app, MtApp, VaultContract<'app>>, + Proxy<'app, MtApp, VaultMock<'app>>, Option>>, Proxy<'app, MtApp, ExternalStakingContract<'app>>, ) { - let vault_code = contract::sv::mt::CodeId::store_code(app); + let vault_code = VaultCodeId::store_code(app); let staking_init_info = if local_staking { let native_staking_code = mesh_native_staking::contract::sv::mt::CodeId::store_code(app); @@ -152,7 +150,6 @@ fn setup_inner<'app>( .with_label("Vault") .call(owner) .unwrap(); - let native_staking_addr = vault.config().unwrap().local_staking.map(Addr::unchecked); let native_staking = native_staking_addr.map(|addr| Proxy::new(addr, app)); @@ -163,7 +160,7 @@ fn setup_inner<'app>( fn setup_cross_stake<'app>( app: &'app App, owner: &'app str, - vault: &Proxy<'app, MtApp, VaultContract<'app>>, + vault: &Proxy<'app, MtApp, VaultMock<'app>>, slash_percent: u64, unbond_period: u64, ) -> Proxy<'app, MtApp, ExternalStakingContract<'app>> { @@ -207,7 +204,7 @@ fn set_active_validators( } /// Bond some tokens -fn bond(vault: &Proxy<'_, MtApp, VaultContract<'_>>, user: &str, amount: u128) { +fn bond(vault: &Proxy<'_, MtApp, VaultMock<'_>>, user: &str, amount: u128) { vault .bond() .with_funds(&coins(amount, OSMO)) @@ -216,7 +213,7 @@ fn bond(vault: &Proxy<'_, MtApp, VaultContract<'_>>, user: &str, amount: u128) { } fn stake_locally( - vault: &Proxy<'_, MtApp, VaultContract<'_>>, + vault: &Proxy<'_, MtApp, VaultMock<'_>>, user: &str, stake: u128, validator: &str, @@ -231,7 +228,7 @@ fn stake_locally( } fn stake_remotely( - vault: &Proxy<'_, MtApp, VaultContract<'_>>, + vault: &Proxy<'_, MtApp, VaultMock<'_>>, cross_staking: &Proxy<'_, MtApp, ExternalStakingContract<'_>>, user: &str, validators: &[&str], @@ -288,7 +285,7 @@ fn process_staking_unbondings(app: &App) { } #[track_caller] -fn get_last_vault_pending_tx_id(contract: &Proxy<'_, MtApp, VaultContract<'_>>) -> Option { +fn get_last_vault_pending_tx_id(contract: &Proxy<'_, MtApp, VaultMock<'_>>) -> Option { let txs = contract.all_pending_txs_desc(None, None).unwrap().txs; txs.first().map(Tx::id) } diff --git a/packages/apis/src/vault_api.rs b/packages/apis/src/vault_api.rs index 9d051627..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, WasmMsg}; +use cosmwasm_std::{to_json_binary, Addr, Coin, CustomMsg, Response, StdError, Uint128, WasmMsg}; use sylvia::types::ExecCtx; use sylvia::{interface, schemars}; @@ -8,6 +8,7 @@ use sylvia::{interface, schemars}; #[interface] pub trait VaultApi { type Error: From; + type ExecC: CustomMsg; /// This must be called by the remote staking contract to release this claim #[sv::msg(exec)] @@ -18,7 +19,7 @@ pub trait VaultApi { owner: String, // amount to unstake on that contract amount: Coin, - ) -> Result; + ) -> Result, Self::Error>; /// This must be called by the local staking contract to release this claim /// Amount of tokens unstaked are those included in ctx.info.funds @@ -28,17 +29,17 @@ pub trait VaultApi { ctx: ExecCtx, // address of the user who originally called stake_remote owner: String, - ) -> Result; + ) -> Result, Self::Error>; /// This must be called by the remote staking contract to commit the remote staking call on success. /// Transaction ID is used to identify the original (vault contract originated) transaction. #[sv::msg(exec)] - fn commit_tx(&self, ctx: ExecCtx, tx_id: u64) -> Result; + fn commit_tx(&self, ctx: ExecCtx, tx_id: u64) -> Result, Self::Error>; /// This must be called by the remote staking contract to rollback the remote staking call on failure. /// Transaction ID is used to identify the original (vault contract originated) transaction. #[sv::msg(exec)] - fn rollback_tx(&self, ctx: ExecCtx, tx_id: u64) -> Result; + fn rollback_tx(&self, ctx: ExecCtx, tx_id: u64) -> Result, Self::Error>; /// This must be called by the native staking contract to process a slashing event /// because of a misbehaviour on the Provider chain. @@ -50,7 +51,7 @@ pub trait VaultApi { ctx: ExecCtx, slashes: Vec, validator: String, - ) -> Result; + ) -> Result, Self::Error>; /// This must be called by the external staking contract to process a slashing event /// because of a misbehaviour on the Consumer chain. @@ -62,7 +63,7 @@ pub trait VaultApi { ctx: ExecCtx, slashes: Vec, validator: String, - ) -> Result; + ) -> Result, Self::Error>; } #[cw_serde] diff --git a/packages/bindings/src/lib.rs b/packages/bindings/src/lib.rs index 15d8f55e..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}; +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 4ecce123..70e94ffe 100644 --- a/packages/bindings/src/msg.rs +++ b/packages/bindings/src/msg.rs @@ -63,3 +63,60 @@ impl From for CosmosMsg { } impl CustomMsg for VirtualStakeCustomMsg {} + +/// A top-level Custom message for the meshsecurityprovider module. +/// It is embedded like this to easily allow adding other variants that are custom +/// to your chain, or other "standardized" extensions along side it. +#[cw_serde] +pub enum ProviderCustomMsg { + Provider(ProviderMsg), +} + +/// Special messages to be supported by any chain that supports meshsecurityprovider +#[cw_serde] +pub enum ProviderMsg { + /// Bond will enforce the calling contract is the vault contract. + /// It ensures amount.denom is the native staking denom. + /// + /// If these conditions are met, it will bond amount.amount tokens + /// to the vault. + Bond { delegator: String, amount: Coin }, + /// Unbond ensures that amount.denom is the native staking denom and + /// the calling contract is the vault contract. + /// + /// If these conditions are met, it will instantly unbond + /// amount.amount tokens from the vault contract. + Unbond { delegator: String, amount: Coin }, +} + +impl ProviderMsg { + pub fn bond(denom: &str, delegator: &str, amount: impl Into) -> ProviderMsg { + let coin = Coin { + amount: amount.into(), + denom: denom.into(), + }; + ProviderMsg::Bond { + delegator: delegator.to_string(), + amount: coin, + } + } + + pub fn unbond(denom: &str, delegator: &str, amount: impl Into) -> ProviderMsg { + let coin = Coin { + amount: amount.into(), + denom: denom.into(), + }; + ProviderMsg::Unbond { + delegator: delegator.to_string(), + amount: coin, + } + } +} + +impl From for CosmosMsg { + fn from(msg: ProviderMsg) -> CosmosMsg { + CosmosMsg::Custom(ProviderCustomMsg::Provider(msg)) + } +} + +impl CustomMsg for ProviderCustomMsg {}